303 lines
16 KiB
TypeScript
303 lines
16 KiB
TypeScript
"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>
|
||
);
|
||
}
|