This commit is contained in:
2026-05-03 17:01:46 +03:30
parent b5ad5420b2
commit 9c30295b4b
76 changed files with 7891 additions and 461 deletions

View File

@@ -52,9 +52,9 @@ export async function POST(_: NextRequest, { params }: { params: Promise<{ id: s
});
const stat = await db.playerMatchStat.upsert({
where: { playerId_matchId: { playerId, matchId: params.id } },
where: { playerId_matchId: { playerId, matchId: id } },
update: { goals, assists, yellowCards, redCards, minutesPlayed, cleanSheet, penaltySaved, penaltyMissed, ownGoals, isMotm, extraTimeBonus, points },
create: { playerId, matchId: params.id, goals, assists, yellowCards, redCards, minutesPlayed, cleanSheet, penaltySaved, penaltyMissed, ownGoals, isMotm, extraTimeBonus, points },
create: { playerId, matchId: id, goals, assists, yellowCards, redCards, minutesPlayed, cleanSheet, penaltySaved, penaltyMissed, ownGoals, isMotm, extraTimeBonus, points },
});
// آپدیت totalPoints بازیکن

View File

@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
const validTiers = new Set(["GOLD", "SILVER", "BRONZE"]);
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getServerSession(authOptions);
if (!session || (session.user as any).role !== "ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id } = await params;
const { cardTier } = await req.json();
if (!validTiers.has(cardTier)) {
return NextResponse.json({ error: "Invalid card tier" }, { status: 400 });
}
const updated = await db.player.update({
where: { id },
data: {
cardTier,
isGoldenCardEligible: cardTier === "GOLD",
},
});
return NextResponse.json(updated);
}

View File

@@ -0,0 +1,24 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
// PATCH /api/admin/players/[id]/golden-toggle
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getServerSession(authOptions);
if (!session || (session.user as any).role !== "ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id } = await params;
const player = await db.player.findUnique({ where: { id } });
if (!player) return NextResponse.json({ error: "Player not found" }, { status: 404 });
const updated = await db.player.update({
where: { id },
data: { isGoldenCardEligible: !player.isGoldenCardEligible },
});
return NextResponse.json({ isGoldenCardEligible: updated.isGoldenCardEligible });
}

View File

@@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { CARD_TIER_LABELS, resolveQuizRewardTier } from "@/lib/cardTier";
function shuffleArray<T>(items: T[]) {
return [...items].sort(() => Math.random() - 0.5);
}
// POST /api/admin/quiz/[id]/lottery - run reward distribution for a quiz
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getServerSession(authOptions);
if (!session || (session.user as any).role !== "ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id } = await params;
const quiz = await db.dailyQuiz.findUnique({
where: { id },
include: { questions: true },
});
if (!quiz) return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
if (quiz.isProcessed) {
return NextResponse.json({ error: "قرعه کشی قبلا انجام شده" }, { status: 400 });
}
const submissions = await db.quizSubmission.findMany({
where: { quizId: id },
include: { user: true },
});
const tierLimits = {
GOLD: quiz.goldWinnersCount,
SILVER: quiz.silverWinnersCount,
BRONZE: quiz.bronzeWinnersCount,
} as const;
const candidatesByTier = {
GOLD: submissions.filter((submission) => resolveQuizRewardTier(quiz, submission.correctAnswers) === "GOLD"),
SILVER: submissions.filter((submission) => resolveQuizRewardTier(quiz, submission.correctAnswers) === "SILVER"),
BRONZE: submissions.filter((submission) => resolveQuizRewardTier(quiz, submission.correctAnswers) === "BRONZE"),
};
const rewardQueue = (["GOLD", "SILVER", "BRONZE"] as const).flatMap((cardTier) =>
shuffleArray(candidatesByTier[cardTier])
.slice(0, Math.max(tierLimits[cardTier], 0))
.map((submission) => ({ submission, cardTier }))
);
if (rewardQueue.length === 0) {
await db.dailyQuiz.update({ where: { id }, data: { isProcessed: true } });
return NextResponse.json({ winners: [], message: "هیچ شرکت کننده ای واجد دریافت کارت نبود" });
}
const players = await db.player.findMany({
where: {
isActive: true,
cardTier: { in: ["GOLD", "SILVER", "BRONZE"] },
},
include: { country: true },
});
const playersByTier = {
GOLD: players.filter((player) => player.cardTier === "GOLD"),
SILVER: players.filter((player) => player.cardTier === "SILVER"),
BRONZE: players.filter((player) => player.cardTier === "BRONZE"),
};
for (const tier of ["GOLD", "SILVER", "BRONZE"] as const) {
if (rewardQueue.some((item) => item.cardTier === tier) && playersByTier[tier].length === 0) {
return NextResponse.json(
{ error: `برای کارت ${CARD_TIER_LABELS[tier]} هیچ بازیکن فعالی تعریف نشده است` },
{ status: 400 }
);
}
}
const createdCards = await db.$transaction(
rewardQueue.map(({ submission, cardTier }) => {
const tierPlayers = playersByTier[cardTier];
const randomPlayer = tierPlayers[Math.floor(Math.random() * tierPlayers.length)];
return db.goldenCard.create({
data: {
userId: submission.userId,
quizId: id,
playerId: randomPlayer.id,
cardTier,
status: "SEALED",
},
include: {
user: { select: { id: true, name: true, email: true } },
player: { include: { country: true } },
},
});
})
);
await db.dailyQuiz.update({ where: { id }, data: { isProcessed: true } });
return NextResponse.json({ winners: createdCards });
}

View File

@@ -0,0 +1,191 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { Prisma } from "@prisma/client";
async function requireAdmin() {
const session = await getServerSession(authOptions);
if (!session || (session.user as any).role !== "ADMIN") {
return null;
}
return session;
}
function calculateResult(answers: number[], questions: Array<{ correctAnswer: number }>) {
let correct = 0;
questions.forEach((question, index) => {
if (answers[index] === question.correctAnswer) {
correct += 1;
}
});
return {
correct,
score: questions.length > 0 ? Math.round((correct / questions.length) * 100) : 0,
};
}
function validateTierConfig(input: {
goldWinnersCount: number;
silverWinnersCount: number;
bronzeWinnersCount: number;
goldMinCorrect: number | null;
silverMinCorrect: number | null;
bronzeMinCorrect: number | null;
}) {
if (input.goldWinnersCount < 0 || input.silverWinnersCount < 0 || input.bronzeWinnersCount < 0) {
return "Winner counts cannot be negative";
}
if (input.goldWinnersCount + input.silverWinnersCount + input.bronzeWinnersCount <= 0) {
return "At least one winner must be configured";
}
if (input.goldWinnersCount > 0 && input.goldMinCorrect == null) {
return "Gold minimum correct answers is required";
}
if (input.silverWinnersCount > 0 && input.silverMinCorrect == null) {
return "Silver minimum correct answers is required";
}
if (input.bronzeWinnersCount > 0 && input.bronzeMinCorrect == null) {
return "Bronze minimum correct answers is required";
}
return null;
}
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const session = await requireAdmin();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const {
date,
windowStart,
windowEnd,
goldWinnersCount,
silverWinnersCount,
bronzeWinnersCount,
goldMinCorrect,
silverMinCorrect,
bronzeMinCorrect,
questions,
} = await req.json();
const parsedInput = {
goldWinnersCount: Number(goldWinnersCount),
silverWinnersCount: Number(silverWinnersCount),
bronzeWinnersCount: Number(bronzeWinnersCount),
goldMinCorrect: goldMinCorrect == null ? null : Number(goldMinCorrect),
silverMinCorrect: silverMinCorrect == null ? null : Number(silverMinCorrect),
bronzeMinCorrect: bronzeMinCorrect == null ? null : Number(bronzeMinCorrect),
};
const validationError = validateTierConfig(parsedInput);
if (validationError) {
return NextResponse.json({ error: validationError }, { status: 400 });
}
if (!Array.isArray(questions) || questions.length === 0) {
return NextResponse.json({ error: "At least one question is required" }, { status: 400 });
}
const quiz = await db.dailyQuiz.findUnique({
where: { id },
include: {
submissions: {
select: { id: true, answers: true },
},
},
});
if (!quiz) return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
if (quiz.isProcessed) {
return NextResponse.json({ error: "Quiz can no longer be edited after lottery processing" }, { status: 400 });
}
const normalizedQuestions = questions.map((q: any, index: number) => ({
questionText: q.questionText,
options: q.options,
correctAnswer: Number(q.correctAnswer),
order: index,
}));
const updatedQuiz = await db.$transaction(async (tx) => {
await tx.quizQuestion.deleteMany({ where: { quizId: id } });
const updated = await tx.dailyQuiz.update({
where: { id },
data: {
date: new Date(`${date}T00:00:00.000Z`),
windowStart: new Date(windowStart),
windowEnd: new Date(windowEnd),
goldWinnersCount: parsedInput.goldWinnersCount,
silverWinnersCount: parsedInput.silverWinnersCount,
bronzeWinnersCount: parsedInput.bronzeWinnersCount,
goldMinCorrect: parsedInput.goldMinCorrect,
silverMinCorrect: parsedInput.silverMinCorrect,
bronzeMinCorrect: parsedInput.bronzeMinCorrect,
questions: {
create: normalizedQuestions,
},
},
include: {
questions: { orderBy: { order: "asc" } },
},
});
for (const submission of quiz.submissions) {
const result = calculateResult(submission.answers, normalizedQuestions);
await tx.quizSubmission.update({
where: { id: submission.id },
data: {
score: result.score,
correctAnswers: result.correct,
},
});
}
return updated;
});
return NextResponse.json(updatedQuiz);
} catch (error) {
console.error("Failed to update quiz", error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === "P2002") {
return NextResponse.json({ error: "Quiz date already exists" }, { status: 409 });
}
}
return NextResponse.json({ error: "Failed to update quiz" }, { status: 500 });
}
}
export async function DELETE(_: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await requireAdmin();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
const quiz = await db.dailyQuiz.findUnique({
where: { id },
select: {
id: true,
isProcessed: true,
},
});
if (!quiz) return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
if (quiz.isProcessed) {
return NextResponse.json({ error: "Quiz can no longer be deleted after lottery processing" }, { status: 400 });
}
await db.dailyQuiz.delete({ where: { id } });
return NextResponse.json({ success: true });
}

132
app/api/admin/quiz/route.ts Normal file
View File

@@ -0,0 +1,132 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { Prisma } from "@prisma/client";
async function adminOnly(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session || (session.user as any).role !== "ADMIN") return null;
return session;
}
function validateTierConfig(input: {
goldWinnersCount: number;
silverWinnersCount: number;
bronzeWinnersCount: number;
goldMinCorrect: number | null;
silverMinCorrect: number | null;
bronzeMinCorrect: number | null;
}) {
if (input.goldWinnersCount < 0 || input.silverWinnersCount < 0 || input.bronzeWinnersCount < 0) {
return "Winner counts cannot be negative";
}
if (input.goldWinnersCount + input.silverWinnersCount + input.bronzeWinnersCount <= 0) {
return "At least one winner must be configured";
}
if (input.goldWinnersCount > 0 && input.goldMinCorrect == null) {
return "Gold minimum correct answers is required";
}
if (input.silverWinnersCount > 0 && input.silverMinCorrect == null) {
return "Silver minimum correct answers is required";
}
if (input.bronzeWinnersCount > 0 && input.bronzeMinCorrect == null) {
return "Bronze minimum correct answers is required";
}
return null;
}
// GET /api/admin/quiz - list all quizzes
export async function GET(req: NextRequest) {
const session = await adminOnly(req);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const quizzes = await db.dailyQuiz.findMany({
orderBy: { date: "desc" },
include: {
questions: { orderBy: { order: "asc" } },
_count: { select: { submissions: true } },
},
});
return NextResponse.json(quizzes);
}
// POST /api/admin/quiz - create quiz
export async function POST(req: NextRequest) {
try {
const session = await adminOnly(req);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const {
date,
windowStart,
windowEnd,
goldWinnersCount,
silverWinnersCount,
bronzeWinnersCount,
goldMinCorrect,
silverMinCorrect,
bronzeMinCorrect,
questions,
} = await req.json();
const parsedInput = {
goldWinnersCount: Number(goldWinnersCount),
silverWinnersCount: Number(silverWinnersCount),
bronzeWinnersCount: Number(bronzeWinnersCount),
goldMinCorrect: goldMinCorrect == null ? null : Number(goldMinCorrect),
silverMinCorrect: silverMinCorrect == null ? null : Number(silverMinCorrect),
bronzeMinCorrect: bronzeMinCorrect == null ? null : Number(bronzeMinCorrect),
};
const validationError = validateTierConfig(parsedInput);
if (validationError) {
return NextResponse.json({ error: validationError }, { status: 400 });
}
if (!Array.isArray(questions) || questions.length === 0) {
return NextResponse.json({ error: "At least one question is required" }, { status: 400 });
}
const quiz = await db.dailyQuiz.create({
data: {
date: new Date(`${date}T00:00:00.000Z`),
windowStart: new Date(windowStart),
windowEnd: new Date(windowEnd),
goldWinnersCount: parsedInput.goldWinnersCount,
silverWinnersCount: parsedInput.silverWinnersCount,
bronzeWinnersCount: parsedInput.bronzeWinnersCount,
goldMinCorrect: parsedInput.goldMinCorrect,
silverMinCorrect: parsedInput.silverMinCorrect,
bronzeMinCorrect: parsedInput.bronzeMinCorrect,
questions: {
create: questions.map((q: any, i: number) => ({
questionText: q.questionText,
options: q.options,
correctAnswer: Number(q.correctAnswer),
order: i,
})),
},
},
include: { questions: true },
});
return NextResponse.json(quiz, { status: 201 });
} catch (error) {
console.error("Failed to create quiz", error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === "P2002") {
return NextResponse.json({ error: "Quiz date already exists" }, { status: 409 });
}
}
return NextResponse.json({ error: "Failed to create quiz" }, { status: 500 });
}
}

View File

@@ -3,14 +3,15 @@ import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const session = await getServerSession(authOptions);
if (!session || (session.user as any).role !== "ADMIN")
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { status } = await req.json();
const team = await db.team.update({
where: { id: params.id },
where: { id },
data: { status },
});
return NextResponse.json(team);

View File

@@ -3,7 +3,8 @@ import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export async function POST(_: NextRequest, { params }: { params: { id: string } }) {
export async function POST(_: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const session = await getServerSession(authOptions);
if (!session || (session.user as any).role !== "ADMIN")
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -11,6 +12,6 @@ export async function POST(_: NextRequest, { params }: { params: { id: string }
// غیرفعال کردن همه
await db.gameweek.updateMany({ data: { isActive: false } });
// فعال کردن این هفته
const gw = await db.gameweek.update({ where: { id: params.id }, data: { isActive: true } });
const gw = await db.gameweek.update({ where: { id }, data: { isActive: true } });
return NextResponse.json(gw);
}

View File

@@ -14,6 +14,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const gw = await db.gameweek.create({ data: body });
const gw = await db.gameweek.create({
data: {
...body,
deadline: new Date(body.deadline),
},
});
return NextResponse.json(gw, { status: 201 });
}

View File

@@ -0,0 +1,173 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import {
getAutoPlacement,
getPositionLabel,
SPECIAL_CARD_TEAM_LIMIT,
} from "@/lib/specialCards";
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const userId = (session.user as any).id;
const { id } = await params;
const { replacePlayerId } = await req.json().catch(() => ({}));
const team = await db.team.findUnique({
where: { userId },
include: {
players: {
include: {
player: true,
goldenCard: true,
},
},
},
});
if (!team) return NextResponse.json({ error: "ابتدا تیم بساز" }, { status: 400 });
const card = await db.goldenCard.findUnique({
where: { id },
include: {
player: { include: { country: true } },
teamPlayer: true,
},
});
if (!card) return NextResponse.json({ error: "کارت ویژه پیدا نشد" }, { status: 404 });
if (card.userId !== userId) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
if (card.status !== "OPENED") return NextResponse.json({ error: "ابتدا کارت را باز کنید" }, { status: 400 });
if (card.state === "SOLD") return NextResponse.json({ error: "این کارت فروخته شده است" }, { status: 400 });
if (card.state === "IN_TEAM") return NextResponse.json({ error: "این کارت همین حالا در تیم است" }, { status: 400 });
const existingSpecialCount = team.players.filter((item) => item.goldenCardId).length;
const sameCountry = team.players.filter((item) => item.player.countryId === card.player.countryId).length;
const existingPlayer = team.players.find((item) => item.playerId === card.playerId);
if (!existingPlayer && sameCountry >= 3) {
return NextResponse.json({ error: "حداکثر 3 بازیکن از یک تیم ملی" }, { status: 400 });
}
if (existingPlayer) {
if (existingPlayer.goldenCardId) {
return NextResponse.json({ error: "نسخه ویژه این بازیکن در تیم شما وجود دارد" }, { status: 400 });
}
if (existingSpecialCount >= SPECIAL_CARD_TEAM_LIMIT) {
return NextResponse.json({ error: "ظرفیت 3 کارت ویژه تیم پر است" }, { status: 400 });
}
const updatedTeamPlayer = await db.$transaction(async (tx) => {
const updatedPlayer = await tx.teamPlayer.update({
where: { teamId_playerId: { teamId: team.id, playerId: existingPlayer.playerId } },
data: { goldenCardId: card.id },
});
await tx.goldenCard.update({
where: { id: card.id },
data: { state: "IN_TEAM" },
});
return updatedPlayer;
});
return NextResponse.json({
success: true,
action: "converted_existing",
placement: existingPlayer.isBench ? "ذخیره" : "فیکس",
teamPlayer: updatedTeamPlayer,
card: { ...card, state: "IN_TEAM" },
message: "بازیکن موجود تیم شما به نسخه ویژه تبدیل شد",
});
}
const autoPlacement = getAutoPlacement(team.formation, team.players as any, card.player.position);
if (!replacePlayerId && !autoPlacement) {
const candidates = team.players
.filter((item) => item.player.position === card.player.position)
.map((item) => ({
playerId: item.playerId,
name: item.player.name,
isBench: item.isBench,
isSpecial: Boolean(item.goldenCardId),
}));
return NextResponse.json(
{
error: `پست ${getPositionLabel(card.player.position)} در ترکیب اصلی و ذخیره پر است`,
needsReplacement: true,
candidates,
},
{ status: 409 }
);
}
const replacingPlayer = replacePlayerId
? team.players.find((item) => item.playerId === replacePlayerId)
: null;
if (replacePlayerId && (!replacingPlayer || replacingPlayer.player.position !== card.player.position)) {
return NextResponse.json({ error: "بازیکن انتخاب‌شده برای تعویض معتبر نیست" }, { status: 400 });
}
const nextSpecialCount = existingSpecialCount + 1 - (replacingPlayer?.goldenCardId ? 1 : 0);
if (nextSpecialCount > SPECIAL_CARD_TEAM_LIMIT) {
return NextResponse.json({ error: "ظرفیت 3 کارت ویژه تیم پر است" }, { status: 400 });
}
if (!replacingPlayer && team.players.length >= 15) {
return NextResponse.json({ error: "تیم پر است" }, { status: 400 });
}
const result = await db.$transaction(async (tx) => {
if (replacingPlayer) {
await tx.teamPlayer.delete({
where: { teamId_playerId: { teamId: team.id, playerId: replacingPlayer.playerId } },
});
if (replacingPlayer.goldenCardId) {
await tx.goldenCard.update({
where: { id: replacingPlayer.goldenCardId },
data: { state: "IN_INVENTORY" },
});
}
}
const teamPlayer = await tx.teamPlayer.create({
data: {
teamId: team.id,
playerId: card.playerId,
goldenCardId: card.id,
isBench: replacingPlayer ? replacingPlayer.isBench : autoPlacement!.isBench,
},
});
await tx.goldenCard.update({
where: { id: card.id },
data: { state: "IN_TEAM" },
});
return teamPlayer;
});
const placement = replacingPlayer
? replacingPlayer.isBench
? "ذخیره"
: "فیکس"
: autoPlacement!.placementLabel;
return NextResponse.json({
success: true,
action: replacingPlayer ? "replaced" : "added",
placement,
replacedPlayerId: replacingPlayer?.playerId ?? null,
replacedGoldenCardId: replacingPlayer?.goldenCardId ?? null,
card: { ...card, state: "IN_TEAM" },
teamPlayer: result,
message: replacingPlayer
? "بازیکن ویژه جایگزین بازیکن انتخاب‌شده شد"
: `بازیکن ویژه به صورت خودکار در ${placement} قرار گرفت`,
});
}

View File

@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
// POST /api/golden-cards/[id]/reveal
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const userId = (session.user as any).id;
const { id } = await params;
const card = await db.goldenCard.findUnique({ where: { id } });
if (!card) return NextResponse.json({ error: "Card not found" }, { status: 404 });
if (card.userId !== userId) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
if (card.status === "OPENED") return NextResponse.json({ error: "کارت قبلاً باز شده" }, { status: 400 });
const updated = await db.goldenCard.update({
where: { id },
data: { status: "OPENED", openedAt: new Date() },
include: { player: { include: { country: true } } },
});
return NextResponse.json(updated);
}

View File

@@ -0,0 +1,52 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { getSpecialCardSalePrice } from "@/lib/specialCards";
export async function POST(_: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const userId = (session.user as any).id;
const { id } = await params;
const card = await db.goldenCard.findUnique({
where: { id },
include: { player: true, teamPlayer: true },
});
if (!card) return NextResponse.json({ error: "کارت ویژه پیدا نشد" }, { status: 404 });
if (card.userId !== userId) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
if (card.status !== "OPENED") return NextResponse.json({ error: "ابتدا کارت را باز کنید" }, { status: 400 });
if (card.state === "SOLD") return NextResponse.json({ error: "این کارت قبلاً فروخته شده" }, { status: 400 });
const team = await db.team.findUnique({ where: { userId } });
if (!team) return NextResponse.json({ error: "تیم پیدا نشد" }, { status: 404 });
const addedBudget = getSpecialCardSalePrice(card.player.price);
await db.$transaction(async (tx) => {
if (card.teamPlayer) {
await tx.teamPlayer.delete({
where: { teamId_playerId: { teamId: card.teamPlayer.teamId, playerId: card.teamPlayer.playerId } },
});
}
await tx.goldenCard.update({
where: { id },
data: { state: "SOLD" },
});
await tx.team.update({
where: { id: team.id },
data: { budget: { increment: addedBudget } },
});
});
return NextResponse.json({
success: true,
addedBudget,
cardId: id,
});
}

View File

@@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
// GET /api/golden-cards - get current user's golden cards
export async function GET() {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const userId = (session.user as any).id;
const cards = await db.goldenCard.findMany({
where: { userId },
include: {
player: { include: { country: true } },
},
orderBy: { acquiredDate: "desc" },
});
return NextResponse.json(cards);
}

View File

@@ -20,7 +20,13 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const match = await db.match.update({ where: { id }, data: body });
const match = await db.match.update({
where: { id },
data: {
...body,
matchDate: new Date(body.matchDate),
},
});
return NextResponse.json(match);
}

View File

@@ -2,9 +2,10 @@ import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { calculatePoints } from "@/lib/points";
import { calculateMatchPoints } from "@/lib/points";
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const session = await getServerSession(authOptions);
if (!session || (session.user as any).role !== "ADMIN")
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -25,12 +26,25 @@ export async function POST(req: NextRequest, { params }: { params: { id: string
const player = await db.player.findUnique({ where: { id: stat.playerId } });
if (!player) continue;
const points = calculatePoints({ position: player.position, ...stat });
const points = await calculateMatchPoints({
position: player.position,
goals: stat.goals,
assists: stat.assists,
yellowCards: stat.yellowCards,
redCards: stat.redCards,
minutesPlayed: stat.minutesPlayed,
cleanSheet: stat.cleanSheet,
penaltySaved: 0,
penaltyMissed: 0,
ownGoals: 0,
isMotm: false,
extraTimeBonus: 0,
});
const record = await db.playerMatchStat.upsert({
where: { playerId_matchId: { playerId: stat.playerId, matchId: params.id } },
where: { playerId_matchId: { playerId: stat.playerId, matchId: id } },
update: { ...stat, points },
create: { ...stat, matchId: params.id, points },
create: { ...stat, matchId: id, points },
});
// آپدیت امتیاز کل بازیکن

View File

@@ -22,7 +22,10 @@ export async function POST(req: NextRequest) {
const body = await req.json();
const match = await db.match.create({
data: body,
data: {
...body,
matchDate: new Date(body.matchDate),
},
include: { homeTeam: true, awayTeam: true },
});
return NextResponse.json(match, { status: 201 });

11
app/api/openapi/route.ts Normal file
View File

@@ -0,0 +1,11 @@
import { NextResponse } from "next/server";
import { openApiSpec } from "@/lib/openapi";
export async function GET() {
return NextResponse.json(openApiSpec, {
headers: {
"Cache-Control": "no-store",
},
});
}

View File

@@ -13,7 +13,11 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
const body = await req.json();
const player = await db.player.update({
where: { id },
data: body,
data: {
...body,
cardTier: body.cardTier ?? undefined,
isGoldenCardEligible: body.cardTier ? body.cardTier === "GOLD" : undefined,
},
});
return NextResponse.json(player);
}

View File

@@ -27,6 +27,12 @@ export async function POST(req: NextRequest) {
}
const body = await req.json();
const player = await db.player.create({ data: body });
const player = await db.player.create({
data: {
...body,
cardTier: body.cardTier ?? "BRONZE",
isGoldenCardEligible: (body.cardTier ?? "BRONZE") === "GOLD",
},
});
return NextResponse.json(player, { status: 201 });
}

View File

@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
// GET /api/quiz/my-results
export async function GET() {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const userId = (session.user as any).id;
const submissions = await db.quizSubmission.findMany({
where: { userId },
include: {
quiz: {
include: { questions: { orderBy: { order: "asc" } } },
},
},
orderBy: { submittedAt: "desc" },
});
return NextResponse.json(submissions);
}

31
app/api/quiz/route.ts Normal file
View File

@@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
// GET /api/quiz - get today's active quiz
export async function GET() {
const now = new Date();
const todayStart = new Date(now);
todayStart.setHours(0, 0, 0, 0);
const todayEnd = new Date(now);
todayEnd.setHours(23, 59, 59, 999);
const quiz = await db.dailyQuiz.findFirst({
where: { date: { gte: todayStart, lte: todayEnd } },
include: {
questions: {
orderBy: { order: "asc" },
select: {
id: true,
questionText: true,
options: true,
order: true,
},
},
},
});
if (!quiz) return NextResponse.json(null);
const isActive = !quiz.isProcessed && now >= quiz.windowStart && now <= quiz.windowEnd;
return NextResponse.json({ ...quiz, isActive });
}

View File

@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { CARD_TIER_LABELS, resolveQuizRewardTier } from "@/lib/cardTier";
// POST /api/quiz/submit
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const userId = (session.user as any).id;
const { quizId, answers } = await req.json();
if (!quizId || !Array.isArray(answers)) {
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
}
const quiz = await db.dailyQuiz.findUnique({
where: { id: quizId },
include: { questions: { orderBy: { order: "asc" } } },
});
if (!quiz) return NextResponse.json({ error: "Quiz not found" }, { status: 404 });
if (quiz.isProcessed) {
return NextResponse.json({ error: "این کوییز بعد از قرعه‌کشی بسته شده است" }, { status: 400 });
}
const now = new Date();
if (now < quiz.windowStart || now > quiz.windowEnd) {
return NextResponse.json({ error: "خارج از بازه زمانی مجاز" }, { status: 400 });
}
const existing = await db.quizSubmission.findUnique({
where: { userId_quizId: { userId, quizId } },
});
if (existing) return NextResponse.json({ error: "قبلاً شرکت کرده‌اید" }, { status: 400 });
let correct = 0;
quiz.questions.forEach((q, i) => {
if (answers[i] === q.correctAnswer) correct++;
});
const score = quiz.questions.length > 0
? Math.round((correct / quiz.questions.length) * 100)
: 0;
const rewardTier = resolveQuizRewardTier(quiz, correct);
const submission = await db.quizSubmission.create({
data: { userId, quizId, answers, correctAnswers: correct, score },
});
return NextResponse.json({
score,
correct,
total: quiz.questions.length,
rewardTier,
rewardTierLabel: rewardTier ? CARD_TIER_LABELS[rewardTier] : null,
submission,
});
}

View File

@@ -3,7 +3,6 @@ import { db } from "@/lib/db";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
// اضافه کردن بازیکن به تیم
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -13,7 +12,7 @@ export async function POST(req: NextRequest) {
const team = await db.team.findUnique({
where: { userId },
include: { players: { include: { player: true } } },
include: { players: { include: { player: true, goldenCard: true } } },
});
if (!team) return NextResponse.json({ error: "ابتدا تیم بساز" }, { status: 400 });
@@ -21,32 +20,34 @@ export async function POST(req: NextRequest) {
const player = await db.player.findUnique({ where: { id: playerId } });
if (!player) return NextResponse.json({ error: "بازیکن پیدا نشد" }, { status: 404 });
// چک بودجه
const spent = team.players.reduce((s, tp) => s + tp.player.price, 0);
if (spent + player.price > team.budget)
const spent = team.players
.filter((item) => !item.goldenCardId)
.reduce((sum, item) => sum + item.player.price, 0);
if (spent + player.price > team.budget) {
return NextResponse.json({ error: "بودجه کافی نیست" }, { status: 400 });
}
// چک تعداد (۱۵ نفر: ۱۱ اصلی + ۴ ذخیره)
if (team.players.length >= 15)
return NextResponse.json({ error: "تیم پر است (حداکثر ۱۵ بازیکن)" }, { status: 400 });
if (team.players.length >= 15) {
return NextResponse.json({ error: "تیم پر است (حداکثر 15 بازیکن)" }, { status: 400 });
}
// چک تکراری
const exists = team.players.find((tp) => tp.playerId === playerId);
if (exists) return NextResponse.json({ error: "این بازیکن قبلاً انتخاب شده" }, { status: 400 });
const exists = team.players.find((item) => item.playerId === playerId);
if (exists) {
return NextResponse.json({ error: "این بازیکن قبلاً انتخاب شده" }, { status: 400 });
}
// چک حداکثر ۳ بازیکن از یک تیم ملی
const sameCountry = team.players.filter((tp) => tp.player.countryId === player.countryId).length;
if (sameCountry >= 3)
return NextResponse.json({ error: "حداکثر ۳ بازیکن از یک تیم ملی" }, { status: 400 });
const sameCountry = team.players.filter((item) => item.player.countryId === player.countryId).length;
if (sameCountry >= 3) {
return NextResponse.json({ error: "حداکثر 3 بازیکن از یک تیم ملی" }, { status: 400 });
}
const tp = await db.teamPlayer.create({
const teamPlayer = await db.teamPlayer.create({
data: { teamId: team.id, playerId, isBench: isBench ?? false },
});
return NextResponse.json(tp, { status: 201 });
return NextResponse.json(teamPlayer, { status: 201 });
}
// حذف بازیکن از تیم
export async function DELETE(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -54,11 +55,26 @@ export async function DELETE(req: NextRequest) {
const { playerId } = await req.json();
const userId = (session.user as any).id;
const team = await db.team.findUnique({ where: { userId } });
const team = await db.team.findUnique({
where: { userId },
include: { players: true },
});
if (!team) return NextResponse.json({ error: "تیم پیدا نشد" }, { status: 404 });
await db.teamPlayer.delete({
where: { teamId_playerId: { teamId: team.id, playerId } },
const teamPlayer = team.players.find((item) => item.playerId === playerId);
if (!teamPlayer) return NextResponse.json({ error: "بازیکن در تیم نیست" }, { status: 404 });
await db.$transaction(async (tx) => {
await tx.teamPlayer.delete({
where: { teamId_playerId: { teamId: team.id, playerId } },
});
if (teamPlayer.goldenCardId) {
await tx.goldenCard.update({
where: { id: teamPlayer.goldenCardId },
data: { state: "IN_INVENTORY" },
});
}
});
return NextResponse.json({ success: true });