feat(tournaments): add tournament management UI
Add complete tournament management interface including: - Tournament card component with status badges and info display - Tournament form for creating/editing tournaments - Bracket view for single elimination visualization - Inscriptions list with payment tracking - Match score dialog for entering results - Tournaments list page with filtering and search - Tournament detail page with tabs (Overview, Inscriptions, Bracket) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
242
apps/web/components/tournaments/inscriptions-list.tsx
Normal file
242
apps/web/components/tournaments/inscriptions-list.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn, formatCurrency } from "@/lib/utils";
|
||||
|
||||
interface Client {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
level: string | null;
|
||||
}
|
||||
|
||||
interface Inscription {
|
||||
id: string;
|
||||
clientId: string;
|
||||
partnerId: string | null;
|
||||
teamName: string | null;
|
||||
seedNumber: number | null;
|
||||
isPaid: boolean;
|
||||
paidAmount: number;
|
||||
registeredAt: string;
|
||||
client: Client;
|
||||
partner?: Client | null;
|
||||
}
|
||||
|
||||
interface InscriptionsListProps {
|
||||
inscriptions: Inscription[];
|
||||
entryFee: number;
|
||||
onRemove?: (inscriptionId: string) => Promise<void>;
|
||||
onTogglePaid?: (inscriptionId: string, isPaid: boolean) => Promise<void>;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
|
||||
export function InscriptionsList({
|
||||
inscriptions,
|
||||
entryFee,
|
||||
onRemove,
|
||||
onTogglePaid,
|
||||
canEdit = false,
|
||||
}: InscriptionsListProps) {
|
||||
const [loadingId, setLoadingId] = useState<string | null>(null);
|
||||
|
||||
const handleRemove = async (inscriptionId: string) => {
|
||||
if (!onRemove) return;
|
||||
if (!confirm("¿Estas seguro de eliminar esta inscripcion?")) return;
|
||||
|
||||
setLoadingId(inscriptionId);
|
||||
try {
|
||||
await onRemove(inscriptionId);
|
||||
} finally {
|
||||
setLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTogglePaid = async (inscription: Inscription) => {
|
||||
if (!onTogglePaid) return;
|
||||
setLoadingId(inscription.id);
|
||||
try {
|
||||
await onTogglePaid(inscription.id, !inscription.isPaid);
|
||||
} finally {
|
||||
setLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getTeamName = (inscription: Inscription): string => {
|
||||
if (inscription.teamName) return inscription.teamName;
|
||||
const p1 = `${inscription.client.firstName} ${inscription.client.lastName}`;
|
||||
if (inscription.partner) {
|
||||
return `${p1} / ${inscription.partner.firstName} ${inscription.partner.lastName}`;
|
||||
}
|
||||
return p1;
|
||||
};
|
||||
|
||||
const getPlayerNames = (inscription: Inscription): string => {
|
||||
const p1 = `${inscription.client.firstName} ${inscription.client.lastName}`;
|
||||
if (inscription.partner) {
|
||||
return `${p1}, ${inscription.partner.firstName} ${inscription.partner.lastName}`;
|
||||
}
|
||||
return p1;
|
||||
};
|
||||
|
||||
const paidCount = inscriptions.filter((i) => i.isPaid).length;
|
||||
const totalCollected = inscriptions
|
||||
.filter((i) => i.isPaid)
|
||||
.reduce((sum, i) => sum + (i.paidAmount || entryFee), 0);
|
||||
|
||||
if (inscriptions.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 text-primary-500">
|
||||
<svg
|
||||
className="w-12 h-12 mx-auto mb-3 text-primary-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
<p>No hay inscripciones todavia</p>
|
||||
<p className="text-sm mt-1">Abre las inscripciones para recibir equipos</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Summary */}
|
||||
<div className="flex flex-wrap gap-4 p-4 bg-primary-50 rounded-lg">
|
||||
<div className="flex-1 min-w-[150px]">
|
||||
<p className="text-sm text-primary-500">Total Equipos</p>
|
||||
<p className="text-2xl font-bold text-primary-800">
|
||||
{inscriptions.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[150px]">
|
||||
<p className="text-sm text-primary-500">Pagados</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
{paidCount} / {inscriptions.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[150px]">
|
||||
<p className="text-sm text-primary-500">Recaudado</p>
|
||||
<p className="text-2xl font-bold text-accent-600">
|
||||
{formatCurrency(totalCollected)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-primary-200">
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-primary-600">
|
||||
#
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-primary-600">
|
||||
Equipo / Jugadores
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-primary-600">
|
||||
Contacto
|
||||
</th>
|
||||
<th className="text-center py-3 px-4 text-sm font-medium text-primary-600">
|
||||
Estado Pago
|
||||
</th>
|
||||
{canEdit && (
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-primary-600">
|
||||
Acciones
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{inscriptions.map((inscription, index) => (
|
||||
<tr
|
||||
key={inscription.id}
|
||||
className={cn(
|
||||
"border-b border-primary-100 hover:bg-primary-50 transition-colors",
|
||||
loadingId === inscription.id && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<td className="py-3 px-4 text-sm text-primary-500">
|
||||
{inscription.seedNumber || index + 1}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div>
|
||||
<p className="font-medium text-primary-800">
|
||||
{getTeamName(inscription)}
|
||||
</p>
|
||||
<p className="text-xs text-primary-500">
|
||||
{getPlayerNames(inscription)}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="text-sm">
|
||||
{inscription.client.email && (
|
||||
<p className="text-primary-600">{inscription.client.email}</p>
|
||||
)}
|
||||
{inscription.client.phone && (
|
||||
<p className="text-primary-500">{inscription.client.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<button
|
||||
onClick={() => canEdit && handleTogglePaid(inscription)}
|
||||
disabled={!canEdit || loadingId === inscription.id}
|
||||
className={cn(
|
||||
"px-3 py-1 text-xs font-medium rounded-full transition-colors",
|
||||
inscription.isPaid
|
||||
? "bg-green-100 text-green-700 hover:bg-green-200"
|
||||
: "bg-yellow-100 text-yellow-700 hover:bg-yellow-200",
|
||||
!canEdit && "cursor-default hover:bg-inherit"
|
||||
)}
|
||||
>
|
||||
{inscription.isPaid ? "Pagado" : "Pendiente"}
|
||||
</button>
|
||||
</td>
|
||||
{canEdit && (
|
||||
<td className="py-3 px-4 text-right">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleRemove(inscription.id)}
|
||||
disabled={loadingId === inscription.id}
|
||||
>
|
||||
{loadingId === inscription.id ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||
) : (
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user