Files
football-next/app/(admin)/admin/rounds/[id]/match/[matchId]/MatchEventManager.tsx
a.alinaghipour aa9ed69dd2 first commit
2026-04-05 15:53:20 +03:30

303 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
const EVENT_LABELS: Record<string, { label: string; icon: string; color: string }> = {
GOAL: { label: "گل", icon: "⚽", color: "bg-green-100 text-green-700" },
ASSIST: { label: "پاس گل", icon: "🎯", color: "bg-blue-100 text-blue-700" },
YELLOW_CARD: { label: "کارت زرد", icon: "🟨", color: "bg-yellow-100 text-yellow-700" },
RED_CARD: { label: "کارت قرمز", icon: "🟥", color: "bg-red-100 text-red-700" },
SECOND_YELLOW: { label: "کارت زرد دوم", icon: "🟨🟥", color: "bg-orange-100 text-orange-700" },
SUBSTITUTION_IN: { label: "ورود تعویضی", icon: "🔄↑", color: "bg-teal-100 text-teal-700" },
SUBSTITUTION_OUT: { label: "خروج تعویضی", icon: "🔄↓", color: "bg-gray-100 text-gray-600" },
INJURY_NO_SUB: { label: "مصدومیت بدون تعویض", icon: "🤕", color: "bg-red-50 text-red-500" },
CLEAN_SHEET: { label: "کلین‌شیت", icon: "🧤", color: "bg-green-100 text-green-700" },
PENALTY_SAVED: { label: "پنالتی گرفته", icon: "🛡️", color: "bg-purple-100 text-purple-700" },
PENALTY_MISSED: { label: "پنالتی از دست داده", icon: "❌", color: "bg-red-100 text-red-600" },
OWN_GOAL: { label: "گل به خودی", icon: "😬", color: "bg-orange-100 text-orange-700" },
EXTRA_TIME_BONUS: { label: "وقت اضافه", icon: "⏱️", color: "bg-indigo-100 text-indigo-700" },
MOTM: { label: "بازیکن برتر", icon: "🌟", color: "bg-yellow-100 text-yellow-700" },
};
export default function MatchEventManager({ match, roundId }: { match: any; roundId: string }) {
const router = useRouter();
const [tab, setTab] = useState<"events" | "lineup" | "score">("events");
const [eventForm, setEventForm] = useState({ playerId: "", type: "GOAL", minute: "", extraInfo: "" });
const [score, setScore] = useState({ homeScore: match.homeScore ?? 0, awayScore: match.awayScore ?? 0, status: match.status });
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState("");
const allPlayers = [...match.homeTeam.players, ...match.awayTeam.players];
async function addEvent() {
if (!eventForm.playerId || !eventForm.type) return;
setLoading(true);
const res = await fetch(`/api/admin/matches/${match.id}/events`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...eventForm, minute: eventForm.minute ? parseInt(eventForm.minute) : null }),
});
if (res.ok) { router.refresh(); setMsg("رویداد ثبت شد"); setEventForm({ playerId: "", type: "GOAL", minute: "", extraInfo: "" }); }
else { const d = await res.json(); setMsg(d.error); }
setLoading(false);
}
async function deleteEvent(eventId: string) {
await fetch(`/api/admin/matches/${match.id}/events/${eventId}`, { method: "DELETE" });
router.refresh();
}
async function updateScore() {
setLoading(true);
await fetch(`/api/matches/${match.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(score),
});
router.refresh();
setLoading(false);
setMsg("نتیجه ذخیره شد");
}
async function calcPoints() {
setLoading(true);
const res = await fetch(`/api/admin/matches/${match.id}/calc-points`, { method: "POST" });
const d = await res.json();
setMsg(res.ok ? "امتیازات محاسبه شد" : d.error);
setLoading(false);
router.refresh();
}
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* پنل چپ - رویدادها */}
<div className="lg:col-span-2">
{/* تب‌ها */}
<div className="flex gap-2 mb-4">
{[["events", "رویدادها"], ["lineup", "ترکیب تیم‌ها"], ["score", "نتیجه"]].map(([key, label]) => (
<button key={key} onClick={() => setTab(key as any)}
className={`px-4 py-2 rounded-xl text-sm font-medium transition ${tab === key ? "bg-green-700 text-white" : "bg-white shadow text-gray-700 hover:bg-gray-50"}`}>
{label}
</button>
))}
<button onClick={calcPoints} disabled={loading}
className="mr-auto bg-blue-600 text-white px-4 py-2 rounded-xl text-sm font-medium hover:bg-blue-700 disabled:opacity-50">
محاسبه امتیازات
</button>
</div>
{msg && <div className="bg-green-50 text-green-700 px-4 py-2 rounded-xl text-sm mb-4">{msg}</div>}
{tab === "events" && (
<div>
{/* فرم ثبت رویداد */}
<div className="bg-white rounded-2xl shadow p-5 mb-4">
<h3 className="font-bold mb-4">ثبت رویداد جدید</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-gray-600 mb-1 block">بازیکن</label>
<select value={eventForm.playerId} onChange={(e) => setEventForm({ ...eventForm, playerId: e.target.value })}
className="w-full border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-400">
<option value="">انتخاب بازیکن...</option>
<optgroup label={match.homeTeam.name}>
{match.homeTeam.players.map((p: any) => (
<option key={p.id} value={p.id}>{p.name} ({p.position})</option>
))}
</optgroup>
<optgroup label={match.awayTeam.name}>
{match.awayTeam.players.map((p: any) => (
<option key={p.id} value={p.id}>{p.name} ({p.position})</option>
))}
</optgroup>
</select>
</div>
<div>
<label className="text-xs font-medium text-gray-600 mb-1 block">نوع رویداد</label>
<select value={eventForm.type} onChange={(e) => setEventForm({ ...eventForm, type: e.target.value })}
className="w-full border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-400">
{Object.entries(EVENT_LABELS).map(([key, val]) => (
<option key={key} value={key}>{val.icon} {val.label}</option>
))}
</select>
</div>
<div>
<label className="text-xs font-medium text-gray-600 mb-1 block">دقیقه (اختیاری)</label>
<input type="number" min="1" max="120" value={eventForm.minute}
onChange={(e) => setEventForm({ ...eventForm, minute: e.target.value })}
placeholder="مثلاً 45"
className="w-full border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-400" />
</div>
<div>
<label className="text-xs font-medium text-gray-600 mb-1 block">توضیح (اختیاری)</label>
<input type="text" value={eventForm.extraInfo}
onChange={(e) => setEventForm({ ...eventForm, extraInfo: e.target.value })}
placeholder="مثلاً نام بازیکن تعویضی"
className="w-full border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-400" />
</div>
</div>
<button onClick={addEvent} disabled={loading || !eventForm.playerId}
className="mt-4 bg-green-700 text-white px-6 py-2 rounded-xl font-medium hover:bg-green-800 disabled:opacity-50 transition">
ثبت رویداد
</button>
</div>
{/* لیست رویدادها */}
<div className="bg-white rounded-2xl shadow overflow-hidden">
<div className="px-5 py-3 bg-gray-50 font-medium text-sm text-gray-600">رویدادهای ثبتشده ({match.events.length})</div>
{match.events.length === 0 ? (
<div className="text-center text-gray-400 py-8">رویدادی ثبت نشده</div>
) : (
<div className="divide-y">
{match.events.map((ev: any) => {
const evInfo = EVENT_LABELS[ev.type];
return (
<div key={ev.id} className="flex items-center justify-between px-5 py-3">
<div className="flex items-center gap-3">
<span className={`text-xs px-2 py-1 rounded-full font-medium ${evInfo?.color}`}>
{evInfo?.icon} {evInfo?.label}
</span>
<span className="font-medium text-sm">{ev.player.name}</span>
{ev.minute && <span className="text-gray-400 text-xs">{ev.minute}'</span>}
{ev.extraInfo && <span className="text-gray-400 text-xs">({ev.extraInfo})</span>}
</div>
<button onClick={() => deleteEvent(ev.id)} className="text-red-400 hover:text-red-600 text-xs">حذف</button>
</div>
);
})}
</div>
)}
</div>
</div>
)}
{tab === "lineup" && (
<LineupTab match={match} />
)}
{tab === "score" && (
<div className="bg-white rounded-2xl shadow p-6">
<h3 className="font-bold mb-4">نتیجه بازی</h3>
<div className="grid grid-cols-3 gap-4 items-center mb-4">
<div className="text-center">
<div className="font-bold mb-2">{match.homeTeam.name}</div>
<input type="number" min="0" value={score.homeScore}
onChange={(e) => setScore({ ...score, homeScore: parseInt(e.target.value) || 0 })}
className="w-20 border-2 rounded-xl px-3 py-3 text-center text-2xl font-bold focus:outline-none focus:border-green-500" />
</div>
<div className="text-center text-gray-400 text-xl font-bold">-</div>
<div className="text-center">
<div className="font-bold mb-2">{match.awayTeam.name}</div>
<input type="number" min="0" value={score.awayScore}
onChange={(e) => setScore({ ...score, awayScore: parseInt(e.target.value) || 0 })}
className="w-20 border-2 rounded-xl px-3 py-3 text-center text-2xl font-bold focus:outline-none focus:border-green-500" />
</div>
</div>
<div className="mb-4">
<label className="text-sm font-medium mb-1 block">وضعیت</label>
<select value={score.status} onChange={(e) => setScore({ ...score, status: e.target.value })}
className="border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-green-400">
<option value="SCHEDULED">برنامه‌ریزی شده</option>
<option value="LIVE">زنده</option>
<option value="FINISHED">پایان یافته</option>
</select>
</div>
<button onClick={updateScore} disabled={loading}
className="bg-green-700 text-white px-6 py-2 rounded-xl font-medium hover:bg-green-800 disabled:opacity-50">
ذخیره نتیجه
</button>
</div>
)}
</div>
{/* پنل راست - آمار بازیکنان */}
<div>
<div className="bg-white rounded-2xl shadow overflow-hidden">
<div className="px-4 py-3 bg-gray-50 font-medium text-sm">آمار بازیکنان</div>
<div className="divide-y max-h-[600px] overflow-y-auto">
{match.playerStats.map((s: any) => (
<div key={s.id} className="px-4 py-3">
<div className="flex justify-between items-center">
<span className="font-medium text-sm">{s.player.name}</span>
<span className="font-bold text-blue-700">{s.points} pts</span>
</div>
<div className="flex gap-2 mt-1 flex-wrap">
{s.goals > 0 && <span className="text-xs bg-green-50 text-green-700 px-1.5 py-0.5 rounded">⚽ {s.goals}</span>}
{s.assists > 0 && <span className="text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">🎯 {s.assists}</span>}
{s.yellowCards > 0 && <span className="text-xs bg-yellow-50 text-yellow-700 px-1.5 py-0.5 rounded">🟨 {s.yellowCards}</span>}
{s.redCards > 0 && <span className="text-xs bg-red-50 text-red-700 px-1.5 py-0.5 rounded">🟥</span>}
{s.cleanSheet && <span className="text-xs bg-purple-50 text-purple-700 px-1.5 py-0.5 rounded">🧤</span>}
<span className="text-xs text-gray-400">{s.minutesPlayed}'</span>
</div>
</div>
))}
{match.playerStats.length === 0 && <div className="text-center text-gray-400 py-6 text-sm">امتیازی محاسبه نشده</div>}
</div>
</div>
</div>
</div>
);
}
function LineupTab({ match }: { match: any }) {
const [homeLineup, setHomeLineup] = useState<string[]>([]);
const [awayLineup, setAwayLineup] = useState<string[]>([]);
const [homeFormation, setHomeFormation] = useState(match.homeTeam.defaultFormation ?? "4-3-3");
const [awayFormation, setAwayFormation] = useState(match.awayTeam.defaultFormation ?? "4-3-3");
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState("");
async function saveLineup() {
setLoading(true);
await fetch(`/api/admin/matches/${match.id}/lineup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([
{ countryId: match.homeTeamId, formation: homeFormation, playerIds: homeLineup },
{ countryId: match.awayTeamId, formation: awayFormation, playerIds: awayLineup },
]),
});
setMsg("ترکیب ذخیره شد");
setLoading(false);
}
const togglePlayer = (id: string, team: "home" | "away") => {
if (team === "home") setHomeLineup((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
else setAwayLineup((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
};
return (
<div className="bg-white rounded-2xl shadow p-5">
<h3 className="font-bold mb-4">ثبت ترکیب رسمی</h3>
{msg && <div className="bg-green-50 text-green-700 px-3 py-2 rounded-lg text-sm mb-3">{msg}</div>}
<div className="grid grid-cols-2 gap-6">
{[{ team: match.homeTeam, side: "home" as const, lineup: homeLineup, formation: homeFormation, setFormation: setHomeFormation },
{ team: match.awayTeam, side: "away" as const, lineup: awayLineup, formation: awayFormation, setFormation: setAwayFormation }
].map(({ team, side, lineup, formation, setFormation }) => (
<div key={team.id}>
<div className="font-medium mb-2">{team.flagUrl} {team.name}</div>
<input type="text" value={formation} onChange={(e) => setFormation(e.target.value)}
placeholder="ترکیب مثلاً 4-3-3"
className="w-full border rounded-lg px-3 py-1.5 text-sm mb-2 focus:outline-none focus:ring-2 focus:ring-green-400" />
<div className="max-h-48 overflow-y-auto flex flex-col gap-1">
{team.players.map((p: any) => (
<label key={p.id} className="flex items-center gap-2 text-sm cursor-pointer hover:bg-gray-50 px-2 py-1 rounded">
<input type="checkbox" checked={lineup.includes(p.id)} onChange={() => togglePlayer(p.id, side)}
className="accent-green-600" />
<span>{p.name}</span>
<span className="text-xs text-gray-400">{p.position}</span>
</label>
))}
</div>
<div className="text-xs text-gray-400 mt-1">{lineup.length} بازیکن انتخاب شده</div>
</div>
))}
</div>
<button onClick={saveLineup} disabled={loading}
className="mt-4 bg-green-700 text-white px-6 py-2 rounded-xl font-medium hover:bg-green-800 disabled:opacity-50">
ذخیره ترکیب
</button>
</div>
);
}