Compare commits

..

14 Commits

Author SHA1 Message Date
Ivan
4127485dea fix: support 6-column grid layout in booking calendar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 00:38:58 +00:00
Ivan
25b1495bb0 feat: update seed data for Cabo Pickleball Club
- Organization: Cabo Pickleball Club, Mazatlan timezone
- Single site: Corridor Courts, Cabo San Lucas
- 6 outdoor courts at 300 MXN/person
- Admin: ivan@horuxfin.com
- 5 membership plans: Day Pass, 10-Day, 10-Morning, Monthly, Family
- Pickleball products replacing padel items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:29:16 +00:00
Ivan
d3419a8cc5 feat: translate API error messages to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:24:54 +00:00
Ivan
3aeda8c2fb feat: translate settings page to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:24:52 +00:00
Ivan
0498844b4f feat: translate reports page to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:24:50 +00:00
Ivan
407744d00f feat: translate memberships page and components to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:24:48 +00:00
Ivan
13bd84a0b5 feat: translate clients/players page to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:22:57 +00:00
Ivan
3e65974727 feat: translate bookings page and components to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:22:55 +00:00
Ivan
0fb27b1825 feat: translate dashboard page and components to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:22:53 +00:00
Ivan
55676f59bd feat: translate auth and layout components to English
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:22:40 +00:00
Ivan
ec48ff8405 feat: rebrand to Cabo Pickleball Club with English UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:13:08 +00:00
Ivan
f905c0dfbe feat: update color palette to Cabo blue (#2990EA) and amber accent
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:13:06 +00:00
Ivan
18066f150f docs: add Cabo Pickleball Club implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:55:48 +00:00
Ivan
5185b65618 docs: add Cabo Pickleball Club adaptation design document
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:07:40 +00:00
34 changed files with 1352 additions and 638 deletions

View File

@@ -29,9 +29,9 @@ export default function BookingsPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-2xl font-bold text-primary-800">Reservas</h1> <h1 className="text-2xl font-bold text-primary-800">Bookings</h1>
<p className="mt-2 text-primary-600"> <p className="mt-2 text-primary-600">
Gestiona las reservas de canchas. Selecciona un horario para crear o ver una reserva. Manage court bookings. Select a time slot to create or view a booking.
</p> </p>
</div> </div>

View File

@@ -66,9 +66,9 @@ interface MembershipPlan {
} }
const membershipFilters = [ const membershipFilters = [
{ value: "", label: "Todos" }, { value: "", label: "All" },
{ value: "with", label: "Con membresia" }, { value: "with", label: "With membership" },
{ value: "without", label: "Sin membresia" }, { value: "without", label: "Without membership" },
]; ];
const ITEMS_PER_PAGE = 10; const ITEMS_PER_PAGE = 10;
@@ -112,7 +112,7 @@ export default function ClientsPage() {
params.append("offset", ((currentPage - 1) * ITEMS_PER_PAGE).toString()); params.append("offset", ((currentPage - 1) * ITEMS_PER_PAGE).toString());
const response = await fetch(`/api/clients?${params.toString()}`); const response = await fetch(`/api/clients?${params.toString()}`);
if (!response.ok) throw new Error("Error al cargar clientes"); if (!response.ok) throw new Error("Error loading players");
const data: ClientsResponse = await response.json(); const data: ClientsResponse = await response.json();
@@ -138,7 +138,7 @@ export default function ClientsPage() {
setTotalClients(data.pagination.total); setTotalClients(data.pagination.total);
} catch (err) { } catch (err) {
console.error("Error fetching clients:", err); console.error("Error fetching clients:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} finally { } finally {
setLoadingClients(false); setLoadingClients(false);
} }
@@ -150,7 +150,7 @@ export default function ClientsPage() {
try { try {
// Fetch all clients to calculate stats // Fetch all clients to calculate stats
const response = await fetch("/api/clients?limit=1000"); const response = await fetch("/api/clients?limit=1000");
if (!response.ok) throw new Error("Error al cargar estadisticas"); if (!response.ok) throw new Error("Error loading statistics");
const data: ClientsResponse = await response.json(); const data: ClientsResponse = await response.json();
const allClients = data.data; const allClients = data.data;
@@ -186,7 +186,7 @@ export default function ClientsPage() {
const fetchMembershipPlans = useCallback(async () => { const fetchMembershipPlans = useCallback(async () => {
try { try {
const response = await fetch("/api/membership-plans"); const response = await fetch("/api/membership-plans");
if (!response.ok) throw new Error("Error al cargar planes"); if (!response.ok) throw new Error("Error loading plans");
const data = await response.json(); const data = await response.json();
setMembershipPlans(data.filter((p: MembershipPlan & { isActive?: boolean }) => p.isActive !== false)); setMembershipPlans(data.filter((p: MembershipPlan & { isActive?: boolean }) => p.isActive !== false));
} catch (err) { } catch (err) {
@@ -198,12 +198,12 @@ export default function ClientsPage() {
const fetchClientDetails = async (clientId: string) => { const fetchClientDetails = async (clientId: string) => {
try { try {
const response = await fetch(`/api/clients/${clientId}`); const response = await fetch(`/api/clients/${clientId}`);
if (!response.ok) throw new Error("Error al cargar detalles del cliente"); if (!response.ok) throw new Error("Error loading player details");
const data = await response.json(); const data = await response.json();
setSelectedClient(data); setSelectedClient(data);
} catch (err) { } catch (err) {
console.error("Error fetching client details:", err); console.error("Error fetching client details:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} }
}; };
@@ -244,7 +244,7 @@ export default function ClientsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al crear cliente"); throw new Error(errorData.error || "Error creating player");
} }
setShowCreateForm(false); setShowCreateForm(false);
@@ -276,7 +276,7 @@ export default function ClientsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al actualizar cliente"); throw new Error(errorData.error || "Error updating player");
} }
setEditingClient(null); setEditingClient(null);
@@ -297,7 +297,7 @@ export default function ClientsPage() {
const handleDeleteClient = async (client: Client) => { const handleDeleteClient = async (client: Client) => {
if ( if (
!confirm( !confirm(
`¿Estas seguro de desactivar a ${client.firstName} ${client.lastName}?` `Are you sure you want to deactivate ${client.firstName} ${client.lastName}?`
) )
) { ) {
return; return;
@@ -310,13 +310,13 @@ export default function ClientsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al desactivar cliente"); throw new Error(errorData.error || "Error deactivating player");
} }
await Promise.all([fetchClients(), fetchStats()]); await Promise.all([fetchClients(), fetchStats()]);
} catch (err) { } catch (err) {
console.error("Error deleting client:", err); console.error("Error deleting client:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} }
}; };
@@ -337,7 +337,7 @@ export default function ClientsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al asignar membresia"); throw new Error(errorData.error || "Error assigning membership");
} }
setShowAssignMembership(false); setShowAssignMembership(false);
@@ -367,9 +367,9 @@ export default function ClientsPage() {
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-primary-800">Clientes</h1> <h1 className="text-2xl font-bold text-primary-800">Players</h1>
<p className="mt-1 text-primary-600"> <p className="mt-1 text-primary-600">
Gestiona los clientes de tu centro Manage your club's players
</p> </p>
</div> </div>
<Button onClick={() => setShowCreateForm(true)}> <Button onClick={() => setShowCreateForm(true)}>
@@ -386,7 +386,7 @@ export default function ClientsPage() {
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
/> />
</svg> </svg>
Nuevo Cliente New Player
</Button> </Button>
</div> </div>
@@ -433,7 +433,7 @@ export default function ClientsPage() {
) : ( ) : (
<> <>
<StatCard <StatCard
title="Total Clientes" title="Total Players"
value={stats.totalClients} value={stats.totalClients}
color="primary" color="primary"
icon={ icon={
@@ -453,7 +453,7 @@ export default function ClientsPage() {
} }
/> />
<StatCard <StatCard
title="Con Membresia" title="With Membership"
value={stats.withMembership} value={stats.withMembership}
color="accent" color="accent"
icon={ icon={
@@ -473,7 +473,7 @@ export default function ClientsPage() {
} }
/> />
<StatCard <StatCard
title="Nuevos Este Mes" title="New This Month"
value={stats.newThisMonth} value={stats.newThisMonth}
color="green" color="green"
icon={ icon={
@@ -504,7 +504,7 @@ export default function ClientsPage() {
<div className="flex-1"> <div className="flex-1">
<Input <Input
type="text" type="text"
placeholder="Buscar por nombre, email o telefono..." placeholder="Search by name, email or phone..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full" className="w-full"

View File

@@ -67,14 +67,14 @@ export default function DashboardPage() {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error("Error al cargar los datos del dashboard"); throw new Error("Error loading dashboard data");
} }
const data = await response.json(); const data = await response.json();
setDashboardData(data); setDashboardData(data);
} catch (err) { } catch (err) {
console.error("Dashboard fetch error:", err); console.error("Dashboard fetch error:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -84,7 +84,7 @@ export default function DashboardPage() {
fetchDashboardData(); fetchDashboardData();
}, [fetchDashboardData]); }, [fetchDashboardData]);
const userName = session?.user?.name?.split(" ")[0] || "Usuario"; const userName = session?.user?.name?.split(" ")[0] || "User";
const today = new Date(); const today = new Date();
return ( return (
@@ -93,10 +93,10 @@ export default function DashboardPage() {
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-primary-800"> <h1 className="text-2xl font-bold text-primary-800">
Bienvenido, {userName} Welcome, {userName}
</h1> </h1>
<p className="text-primary-500 mt-1"> <p className="text-primary-500 mt-1">
{formatDate(today)} - Panel de administracion {formatDate(today)} - Admin panel
</p> </p>
</div> </div>
{selectedSite && ( {selectedSite && (
@@ -121,7 +121,7 @@ export default function DashboardPage() {
/> />
</svg> </svg>
<span className="text-sm font-medium text-accent-700"> <span className="text-sm font-medium text-accent-700">
Mostrando: {selectedSite.name} Showing: {selectedSite.name}
</span> </span>
</div> </div>
)} )}
@@ -161,7 +161,7 @@ export default function DashboardPage() {
) : dashboardData ? ( ) : dashboardData ? (
<> <>
<StatCard <StatCard
title="Reservas Hoy" title="Today's Bookings"
value={dashboardData.stats.todayBookings} value={dashboardData.stats.todayBookings}
color="blue" color="blue"
icon={ icon={
@@ -181,7 +181,7 @@ export default function DashboardPage() {
} }
/> />
<StatCard <StatCard
title="Ingresos Hoy" title="Today's Revenue"
value={formatCurrency(dashboardData.stats.todayRevenue)} value={formatCurrency(dashboardData.stats.todayRevenue)}
color="green" color="green"
icon={ icon={
@@ -201,7 +201,7 @@ export default function DashboardPage() {
} }
/> />
<StatCard <StatCard
title="Ocupacion" title="Occupancy"
value={`${dashboardData.stats.occupancyRate}%`} value={`${dashboardData.stats.occupancyRate}%`}
color="purple" color="purple"
icon={ icon={
@@ -221,7 +221,7 @@ export default function DashboardPage() {
} }
/> />
<StatCard <StatCard
title="Miembros Activos" title="Active Members"
value={dashboardData.stats.activeMembers} value={dashboardData.stats.activeMembers}
color="accent" color="accent"
icon={ icon={
@@ -248,7 +248,7 @@ export default function DashboardPage() {
{!isLoading && dashboardData && ( {!isLoading && dashboardData && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<StatCard <StatCard
title="Reservas Pendientes" title="Pending Bookings"
value={dashboardData.stats.pendingBookings} value={dashboardData.stats.pendingBookings}
color="orange" color="orange"
icon={ icon={
@@ -268,7 +268,7 @@ export default function DashboardPage() {
} }
/> />
<StatCard <StatCard
title="Torneos Proximos" title="Upcoming Events"
value={dashboardData.stats.upcomingTournaments} value={dashboardData.stats.upcomingTournaments}
color="primary" color="primary"
icon={ icon={

View File

@@ -68,10 +68,10 @@ interface MembershipsResponse {
} }
const statusFilters = [ const statusFilters = [
{ value: "", label: "Todos" }, { value: "", label: "All" },
{ value: "ACTIVE", label: "Activas" }, { value: "ACTIVE", label: "Active" },
{ value: "EXPIRED", label: "Expiradas" }, { value: "EXPIRED", label: "Expired" },
{ value: "CANCELLED", label: "Canceladas" }, { value: "CANCELLED", label: "Cancelled" },
]; ];
export default function MembershipsPage() { export default function MembershipsPage() {
@@ -104,12 +104,12 @@ export default function MembershipsPage() {
setLoadingPlans(true); setLoadingPlans(true);
try { try {
const response = await fetch("/api/membership-plans?includeInactive=true"); const response = await fetch("/api/membership-plans?includeInactive=true");
if (!response.ok) throw new Error("Error al cargar planes"); if (!response.ok) throw new Error("Error loading plans");
const data = await response.json(); const data = await response.json();
setPlans(data); setPlans(data);
} catch (err) { } catch (err) {
console.error("Error fetching plans:", err); console.error("Error fetching plans:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} finally { } finally {
setLoadingPlans(false); setLoadingPlans(false);
} }
@@ -125,7 +125,7 @@ export default function MembershipsPage() {
if (searchQuery) params.append("search", searchQuery); if (searchQuery) params.append("search", searchQuery);
const response = await fetch(`/api/memberships?${params.toString()}`); const response = await fetch(`/api/memberships?${params.toString()}`);
if (!response.ok) throw new Error("Error al cargar membresias"); if (!response.ok) throw new Error("Error loading memberships");
const data: MembershipsResponse = await response.json(); const data: MembershipsResponse = await response.json();
setMemberships(data.data); setMemberships(data.data);
@@ -138,7 +138,7 @@ export default function MembershipsPage() {
}); });
} catch (err) { } catch (err) {
console.error("Error fetching memberships:", err); console.error("Error fetching memberships:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} finally { } finally {
setLoadingMemberships(false); setLoadingMemberships(false);
} }
@@ -209,7 +209,7 @@ export default function MembershipsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al guardar plan"); throw new Error(errorData.error || "Error saving plan");
} }
setShowPlanForm(false); setShowPlanForm(false);
@@ -224,7 +224,7 @@ export default function MembershipsPage() {
// Handle plan deletion // Handle plan deletion
const handleDeletePlan = async (plan: MembershipPlan) => { const handleDeletePlan = async (plan: MembershipPlan) => {
if (!confirm(`¿Estas seguro de eliminar el plan "${plan.name}"? Esta accion lo desactivara.`)) { if (!confirm(`Are you sure you want to delete the plan "${plan.name}"? This action will deactivate it.`)) {
return; return;
} }
@@ -235,13 +235,13 @@ export default function MembershipsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al eliminar plan"); throw new Error(errorData.error || "Error deleting plan");
} }
await fetchPlans(); await fetchPlans();
} catch (err) { } catch (err) {
console.error("Error deleting plan:", err); console.error("Error deleting plan:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} }
}; };
@@ -262,7 +262,7 @@ export default function MembershipsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al asignar membresia"); throw new Error(errorData.error || "Error assigning membership");
} }
setShowAssignDialog(false); setShowAssignDialog(false);
@@ -295,19 +295,19 @@ export default function MembershipsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al renovar membresia"); throw new Error(errorData.error || "Error renewing membership");
} }
await Promise.all([fetchMemberships(), fetchPlans()]); await Promise.all([fetchMemberships(), fetchPlans()]);
} catch (err) { } catch (err) {
console.error("Error renewing membership:", err); console.error("Error renewing membership:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} }
}; };
// Handle membership cancellation // Handle membership cancellation
const handleCancelMembership = async (membership: Membership) => { const handleCancelMembership = async (membership: Membership) => {
if (!confirm(`¿Estas seguro de cancelar la membresia de ${membership.client.firstName} ${membership.client.lastName}?`)) { if (!confirm(`Are you sure you want to cancel the membership for ${membership.client.firstName} ${membership.client.lastName}?`)) {
return; return;
} }
@@ -318,13 +318,13 @@ export default function MembershipsPage() {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || "Error al cancelar membresia"); throw new Error(errorData.error || "Error cancelling membership");
} }
await Promise.all([fetchMemberships(), fetchPlans()]); await Promise.all([fetchMemberships(), fetchPlans()]);
} catch (err) { } catch (err) {
console.error("Error cancelling membership:", err); console.error("Error cancelling membership:", err);
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} }
}; };
@@ -336,9 +336,9 @@ export default function MembershipsPage() {
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-primary-800">Membresias</h1> <h1 className="text-2xl font-bold text-primary-800">Memberships</h1>
<p className="mt-1 text-primary-600"> <p className="mt-1 text-primary-600">
Gestiona planes y membresias de tus clientes Manage plans and memberships for your players
</p> </p>
</div> </div>
</div> </div>
@@ -374,7 +374,7 @@ export default function MembershipsPage() {
</svg> </svg>
</div> </div>
<div> <div>
<p className="text-sm text-primary-500">Membresias Activas</p> <p className="text-sm text-primary-500">Active Memberships</p>
<p className="text-2xl font-bold text-primary-800">{stats.totalActive}</p> <p className="text-2xl font-bold text-primary-800">{stats.totalActive}</p>
</div> </div>
</div> </div>
@@ -396,7 +396,7 @@ export default function MembershipsPage() {
</svg> </svg>
</div> </div>
<div> <div>
<p className="text-sm text-primary-500">Por Expirar</p> <p className="text-sm text-primary-500">Expiring Soon</p>
<p className="text-2xl font-bold text-primary-800">{stats.expiringSoon}</p> <p className="text-2xl font-bold text-primary-800">{stats.expiringSoon}</p>
</div> </div>
</div> </div>
@@ -412,7 +412,7 @@ export default function MembershipsPage() {
</svg> </svg>
</div> </div>
<div> <div>
<p className="text-sm text-primary-500">Planes Activos</p> <p className="text-sm text-primary-500">Active Plans</p>
<p className="text-2xl font-bold text-primary-800">{activePlans.length}</p> <p className="text-2xl font-bold text-primary-800">{activePlans.length}</p>
</div> </div>
</div> </div>
@@ -428,7 +428,7 @@ export default function MembershipsPage() {
</svg> </svg>
</div> </div>
<div> <div>
<p className="text-sm text-primary-500">Total Suscriptores</p> <p className="text-sm text-primary-500">Total Subscribers</p>
<p className="text-2xl font-bold text-primary-800"> <p className="text-2xl font-bold text-primary-800">
{plans.reduce((sum, p) => sum + p.subscriberCount, 0)} {plans.reduce((sum, p) => sum + p.subscriberCount, 0)}
</p> </p>
@@ -441,12 +441,12 @@ export default function MembershipsPage() {
{/* Plans Section */} {/* Plans Section */}
<section> <section>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-primary-800">Planes de Membresia</h2> <h2 className="text-xl font-semibold text-primary-800">Membership Plans</h2>
<Button onClick={() => setShowPlanForm(true)}> <Button onClick={() => setShowPlanForm(true)}>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg> </svg>
Nuevo Plan New Plan
</Button> </Button>
</div> </div>
@@ -454,7 +454,7 @@ export default function MembershipsPage() {
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="text-center"> <div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
<p className="text-primary-500">Cargando planes...</p> <p className="text-primary-500">Loading plans...</p>
</div> </div>
</div> </div>
) : plans.length === 0 ? ( ) : plans.length === 0 ? (
@@ -468,10 +468,10 @@ export default function MembershipsPage() {
> >
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg> </svg>
<p className="font-medium text-primary-600">No hay planes</p> <p className="font-medium text-primary-600">No plans</p>
<p className="text-sm text-primary-500 mt-1">Crea tu primer plan de membresia</p> <p className="text-sm text-primary-500 mt-1">Create your first membership plan</p>
<Button className="mt-4" onClick={() => setShowPlanForm(true)}> <Button className="mt-4" onClick={() => setShowPlanForm(true)}>
Crear Plan Create Plan
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -496,12 +496,12 @@ export default function MembershipsPage() {
{/* Memberships Section */} {/* Memberships Section */}
<section> <section>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
<h2 className="text-xl font-semibold text-primary-800">Membresias</h2> <h2 className="text-xl font-semibold text-primary-800">Memberships</h2>
<Button variant="accent" onClick={() => setShowAssignDialog(true)}> <Button variant="accent" onClick={() => setShowAssignDialog(true)}>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
</svg> </svg>
Asignar Membresia Assign Membership
</Button> </Button>
</div> </div>
@@ -513,7 +513,7 @@ export default function MembershipsPage() {
<div className="flex-1"> <div className="flex-1">
<Input <Input
type="text" type="text"
placeholder="Buscar por nombre de cliente..." placeholder="Search by player name..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full" className="w-full"
@@ -527,7 +527,7 @@ export default function MembershipsPage() {
onChange={(e) => setPlanFilter(e.target.value)} onChange={(e) => setPlanFilter(e.target.value)}
className="flex h-10 w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2" className="flex h-10 w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
> >
<option value="">Todos los planes</option> <option value="">All plans</option>
{activePlans.map((plan) => ( {activePlans.map((plan) => (
<option key={plan.id} value={plan.id}> <option key={plan.id} value={plan.id}>
{plan.name} {plan.name}

View File

@@ -89,39 +89,39 @@ export default function ReportsPage() {
}); });
setDailyRevenue([ setDailyRevenue([
{ date: "Lun", bookings: 4200, sales: 1800, total: 6000 }, { date: "Mon", bookings: 4200, sales: 1800, total: 6000 },
{ date: "Mar", bookings: 3800, sales: 1200, total: 5000 }, { date: "Tue", bookings: 3800, sales: 1200, total: 5000 },
{ date: "Mié", bookings: 4500, sales: 2100, total: 6600 }, { date: "Wed", bookings: 4500, sales: 2100, total: 6600 },
{ date: "Jue", bookings: 5200, sales: 1900, total: 7100 }, { date: "Thu", bookings: 5200, sales: 1900, total: 7100 },
{ date: "Vie", bookings: 6800, sales: 3200, total: 10000 }, { date: "Fri", bookings: 6800, sales: 3200, total: 10000 },
{ date: "Sáb", bookings: 8500, sales: 4100, total: 12600 }, { date: "Sat", bookings: 8500, sales: 4100, total: 12600 },
{ date: "Dom", bookings: 7200, sales: 3500, total: 10700 }, { date: "Sun", bookings: 7200, sales: 3500, total: 10700 },
]); ]);
setTopProducts([ setTopProducts([
{ name: "Agua", quantity: 245, revenue: 4900 }, { name: "Water", quantity: 245, revenue: 4900 },
{ name: "Gatorade", quantity: 180, revenue: 6300 }, { name: "Gatorade", quantity: 180, revenue: 6300 },
{ name: "Cerveza", quantity: 156, revenue: 7020 }, { name: "Beer", quantity: 156, revenue: 7020 },
{ name: "Pelotas HEAD", quantity: 42, revenue: 7560 }, { name: "Pickleballs", quantity: 42, revenue: 7560 },
{ name: "Raqueta alquiler", quantity: 38, revenue: 3800 }, { name: "Paddle Rental", quantity: 38, revenue: 3800 },
]); ]);
setCourtStats([ setCourtStats([
{ name: "Cancha 1", site: "Sede Norte", bookings: 68, revenue: 20400, occupancy: 72 }, { name: "Court 1", site: "North Site", bookings: 68, revenue: 20400, occupancy: 72 },
{ name: "Cancha 2", site: "Sede Norte", bookings: 54, revenue: 16200, occupancy: 58 }, { name: "Court 2", site: "North Site", bookings: 54, revenue: 16200, occupancy: 58 },
{ name: "Cancha 1", site: "Sede Centro", bookings: 72, revenue: 21600, occupancy: 76 }, { name: "Court 1", site: "Central Site", bookings: 72, revenue: 21600, occupancy: 76 },
{ name: "Cancha 2", site: "Sede Centro", bookings: 61, revenue: 18300, occupancy: 65 }, { name: "Court 2", site: "Central Site", bookings: 61, revenue: 18300, occupancy: 65 },
{ name: "Cancha 1", site: "Sede Sur", bookings: 48, revenue: 14400, occupancy: 51 }, { name: "Court 1", site: "South Site", bookings: 48, revenue: 14400, occupancy: 51 },
{ name: "Cancha 2", site: "Sede Sur", bookings: 39, revenue: 11700, occupancy: 42 }, { name: "Court 2", site: "South Site", bookings: 39, revenue: 11700, occupancy: 42 },
]); ]);
setLoading(false); setLoading(false);
}; };
const formatCurrency = (amount: number) => { const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("es-MX", { return new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency: "MXN", currency: "USD",
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(amount); }).format(amount);
}; };
@@ -133,8 +133,8 @@ export default function ReportsPage() {
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-primary-800">Reportes</h1> <h1 className="text-2xl font-bold text-primary-800">Reports</h1>
<p className="text-primary-600">Análisis y estasticas del negocio</p> <p className="text-primary-600">Business analysis and statistics</p>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<select <select
@@ -142,14 +142,14 @@ export default function ReportsPage() {
onChange={(e) => setDateRange(e.target.value)} onChange={(e) => setDateRange(e.target.value)}
className="rounded-lg border border-primary-200 bg-white px-3 py-2 text-sm focus:border-primary focus:outline-none" className="rounded-lg border border-primary-200 bg-white px-3 py-2 text-sm focus:border-primary focus:outline-none"
> >
<option value="week">Última semana</option> <option value="week">Last week</option>
<option value="month">Último mes</option> <option value="month">Last month</option>
<option value="quarter">Último trimestre</option> <option value="quarter">Last quarter</option>
<option value="year">Último año</option> <option value="year">Last year</option>
</select> </select>
<Button variant="outline" className="gap-2"> <Button variant="outline" className="gap-2">
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
Exportar Export
</Button> </Button>
</div> </div>
</div> </div>
@@ -157,28 +157,28 @@ export default function ReportsPage() {
{/* KPI Cards */} {/* KPI Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard <StatCard
title="Ingresos Totales" title="Total Revenue"
value={formatCurrency(stats.totalRevenue)} value={formatCurrency(stats.totalRevenue)}
change={stats.revenueChange} change={stats.revenueChange}
icon={DollarSign} icon={DollarSign}
loading={loading} loading={loading}
/> />
<StatCard <StatCard
title="Reservas" title="Bookings"
value={stats.totalBookings.toString()} value={stats.totalBookings.toString()}
change={stats.bookingsChange} change={stats.bookingsChange}
icon={Calendar} icon={Calendar}
loading={loading} loading={loading}
/> />
<StatCard <StatCard
title="Clientes Activos" title="Active Players"
value={stats.totalClients.toString()} value={stats.totalClients.toString()}
change={stats.clientsChange} change={stats.clientsChange}
icon={Users} icon={Users}
loading={loading} loading={loading}
/> />
<StatCard <StatCard
title="Ocupación Promedio" title="Average Occupancy"
value={`${stats.avgOccupancy}%`} value={`${stats.avgOccupancy}%`}
change={stats.occupancyChange} change={stats.occupancyChange}
icon={Clock} icon={Clock}
@@ -193,7 +193,7 @@ export default function ReportsPage() {
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-primary" /> <BarChart3 className="h-5 w-5 text-primary" />
Ingresos por Día Revenue by Day
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -224,11 +224,11 @@ export default function ReportsPage() {
<div className="flex items-center gap-4 pt-2 border-t"> <div className="flex items-center gap-4 pt-2 border-t">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-primary" /> <div className="w-3 h-3 rounded bg-primary" />
<span className="text-xs text-primary-600">Reservas</span> <span className="text-xs text-primary-600">Bookings</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-accent" /> <div className="w-3 h-3 rounded bg-accent" />
<span className="text-xs text-primary-600">Ventas</span> <span className="text-xs text-primary-600">Sales</span>
</div> </div>
</div> </div>
</div> </div>
@@ -239,7 +239,7 @@ export default function ReportsPage() {
{/* Top Products */} {/* Top Products */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Productos Más Vendidos</CardTitle> <CardTitle>Top Selling Products</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{loading ? ( {loading ? (
@@ -261,7 +261,7 @@ export default function ReportsPage() {
</span> </span>
<div className="flex-1"> <div className="flex-1">
<p className="font-medium text-primary-800">{product.name}</p> <p className="font-medium text-primary-800">{product.name}</p>
<p className="text-xs text-primary-500">{product.quantity} unidades</p> <p className="text-xs text-primary-500">{product.quantity} units</p>
</div> </div>
<span className="font-semibold text-primary-800"> <span className="font-semibold text-primary-800">
{formatCurrency(product.revenue)} {formatCurrency(product.revenue)}
@@ -277,7 +277,7 @@ export default function ReportsPage() {
{/* Courts Performance */} {/* Courts Performance */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Rendimiento por Cancha</CardTitle> <CardTitle>Court Performance</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{loading ? ( {loading ? (
@@ -291,11 +291,11 @@ export default function ReportsPage() {
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-primary-100"> <tr className="border-b border-primary-100">
<th className="text-left py-3 px-4 text-sm font-medium text-primary-700">Cancha</th> <th className="text-left py-3 px-4 text-sm font-medium text-primary-700">Court</th>
<th className="text-left py-3 px-4 text-sm font-medium text-primary-700">Sede</th> <th className="text-left py-3 px-4 text-sm font-medium text-primary-700">Site</th>
<th className="text-center py-3 px-4 text-sm font-medium text-primary-700">Reservas</th> <th className="text-center py-3 px-4 text-sm font-medium text-primary-700">Bookings</th>
<th className="text-right py-3 px-4 text-sm font-medium text-primary-700">Ingresos</th> <th className="text-right py-3 px-4 text-sm font-medium text-primary-700">Revenue</th>
<th className="text-center py-3 px-4 text-sm font-medium text-primary-700">Ocupación</th> <th className="text-center py-3 px-4 text-sm font-medium text-primary-700">Occupancy</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -337,34 +337,34 @@ export default function ReportsPage() {
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-4 md:grid-cols-3">
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Mejor Día</CardTitle> <CardTitle className="text-base">Best Day</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-2xl font-bold text-primary-800">Sábado</p> <p className="text-2xl font-bold text-primary-800">Saturday</p>
<p className="text-sm text-primary-600"> <p className="text-sm text-primary-600">
{formatCurrency(12600)} en ingresos promedio {formatCurrency(12600)} in average revenue
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Hora Pico</CardTitle> <CardTitle className="text-base">Peak Hour</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-2xl font-bold text-primary-800">18:00 - 20:00</p> <p className="text-2xl font-bold text-primary-800">18:00 - 20:00</p>
<p className="text-sm text-primary-600"> <p className="text-sm text-primary-600">
85% de ocupación en este horario 85% occupancy during this time slot
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Ticket Promedio</CardTitle> <CardTitle className="text-base">Average Ticket</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-2xl font-bold text-primary-800">{formatCurrency(368)}</p> <p className="text-2xl font-bold text-primary-800">{formatCurrency(368)}</p>
<p className="text-sm text-primary-600"> <p className="text-sm text-primary-600">
Por visita (reserva + consumo) Per visit (booking + consumption)
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -419,7 +419,7 @@ function StatCard({
{isPositive ? "+" : ""} {isPositive ? "+" : ""}
{change}% {change}%
</span> </span>
<span className="text-xs text-primary-500">vs período anterior</span> <span className="text-xs text-primary-500">vs previous period</span>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -129,7 +129,7 @@ export default function SettingsPage() {
setLoading(true); setLoading(true);
// Simulate save // Simulate save
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
setMessage({ type: "success", text: "Configuración guardada correctamente" }); setMessage({ type: "success", text: "Settings saved successfully" });
setLoading(false); setLoading(false);
setTimeout(() => setMessage(null), 3000); setTimeout(() => setMessage(null), 3000);
}; };
@@ -147,15 +147,15 @@ export default function SettingsPage() {
}); });
if (res.ok) { if (res.ok) {
setMessage({ type: "success", text: editingSite ? "Sede actualizada" : "Sede creada" }); setMessage({ type: "success", text: editingSite ? "Site updated" : "Site created" });
fetchSites(); fetchSites();
setShowSiteForm(false); setShowSiteForm(false);
setEditingSite(null); setEditingSite(null);
} else { } else {
setMessage({ type: "error", text: "Error al guardar la sede" }); setMessage({ type: "error", text: "Error saving site" });
} }
} catch (error) { } catch (error) {
setMessage({ type: "error", text: "Error de conexión" }); setMessage({ type: "error", text: "Connection error" });
} finally { } finally {
setLoading(false); setLoading(false);
setTimeout(() => setMessage(null), 3000); setTimeout(() => setMessage(null), 3000);
@@ -175,15 +175,15 @@ export default function SettingsPage() {
}); });
if (res.ok) { if (res.ok) {
setMessage({ type: "success", text: editingCourt ? "Cancha actualizada" : "Cancha creada" }); setMessage({ type: "success", text: editingCourt ? "Court updated" : "Court created" });
fetchCourts(); fetchCourts();
setShowCourtForm(false); setShowCourtForm(false);
setEditingCourt(null); setEditingCourt(null);
} else { } else {
setMessage({ type: "error", text: "Error al guardar la cancha" }); setMessage({ type: "error", text: "Error saving court" });
} }
} catch (error) { } catch (error) {
setMessage({ type: "error", text: "Error de conexión" }); setMessage({ type: "error", text: "Connection error" });
} finally { } finally {
setLoading(false); setLoading(false);
setTimeout(() => setMessage(null), 3000); setTimeout(() => setMessage(null), 3000);
@@ -191,18 +191,18 @@ export default function SettingsPage() {
}; };
const handleDeleteCourt = async (courtId: string) => { const handleDeleteCourt = async (courtId: string) => {
if (!confirm("¿Estás seguro de eliminar esta cancha?")) return; if (!confirm("Are you sure you want to delete this court?")) return;
try { try {
const res = await fetch(`/api/courts/${courtId}`, { method: "DELETE" }); const res = await fetch(`/api/courts/${courtId}`, { method: "DELETE" });
if (res.ok) { if (res.ok) {
setMessage({ type: "success", text: "Cancha eliminada" }); setMessage({ type: "success", text: "Court deleted" });
fetchCourts(); fetchCourts();
} else { } else {
setMessage({ type: "error", text: "Error al eliminar la cancha" }); setMessage({ type: "error", text: "Error deleting court" });
} }
} catch (error) { } catch (error) {
setMessage({ type: "error", text: "Error de conexión" }); setMessage({ type: "error", text: "Connection error" });
} }
setTimeout(() => setMessage(null), 3000); setTimeout(() => setMessage(null), 3000);
}; };
@@ -211,8 +211,8 @@ export default function SettingsPage() {
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-2xl font-bold text-primary-800">Configuración</h1> <h1 className="text-2xl font-bold text-primary-800">Settings</h1>
<p className="text-primary-600">Administra la configuración del sistema</p> <p className="text-primary-600">Manage system settings</p>
</div> </div>
{/* Message */} {/* Message */}
@@ -233,19 +233,19 @@ export default function SettingsPage() {
<TabsList className="grid w-full grid-cols-4 lg:w-auto lg:inline-grid"> <TabsList className="grid w-full grid-cols-4 lg:w-auto lg:inline-grid">
<TabsTrigger value="organization" className="gap-2"> <TabsTrigger value="organization" className="gap-2">
<Building2 className="h-4 w-4" /> <Building2 className="h-4 w-4" />
<span className="hidden sm:inline">Organización</span> <span className="hidden sm:inline">Organization</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="sites" className="gap-2"> <TabsTrigger value="sites" className="gap-2">
<MapPin className="h-4 w-4" /> <MapPin className="h-4 w-4" />
<span className="hidden sm:inline">Sedes</span> <span className="hidden sm:inline">Sites</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="courts" className="gap-2"> <TabsTrigger value="courts" className="gap-2">
<Clock className="h-4 w-4" /> <Clock className="h-4 w-4" />
<span className="hidden sm:inline">Canchas</span> <span className="hidden sm:inline">Courts</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="users" className="gap-2"> <TabsTrigger value="users" className="gap-2">
<Users className="h-4 w-4" /> <Users className="h-4 w-4" />
<span className="hidden sm:inline">Usuarios</span> <span className="hidden sm:inline">Users</span>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -253,34 +253,34 @@ export default function SettingsPage() {
<TabsContent value="organization" className="space-y-6"> <TabsContent value="organization" className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Información de la Organización</CardTitle> <CardTitle>Organization Information</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Nombre de la organización Organization name
</label> </label>
<Input <Input
value={orgName} value={orgName}
onChange={(e) => setOrgName(e.target.value)} onChange={(e) => setOrgName(e.target.value)}
placeholder="Nombre" placeholder="Name"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Email de contacto Contact email
</label> </label>
<Input <Input
type="email" type="email"
value={orgEmail} value={orgEmail}
onChange={(e) => setOrgEmail(e.target.value)} onChange={(e) => setOrgEmail(e.target.value)}
placeholder="email@ejemplo.com" placeholder="email@example.com"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Teléfono Phone
</label> </label>
<Input <Input
value={orgPhone} value={orgPhone}
@@ -290,28 +290,28 @@ export default function SettingsPage() {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Moneda Currency
</label> </label>
<select <select
value={currency} value={currency}
onChange={(e) => setCurrency(e.target.value)} onChange={(e) => setCurrency(e.target.value)}
className="w-full rounded-lg border border-primary-200 bg-white px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary" className="w-full rounded-lg border border-primary-200 bg-white px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
> >
<option value="MXN">MXN - Peso Mexicano</option> <option value="MXN">MXN - Mexican Peso</option>
<option value="USD">USD - lar</option> <option value="USD">USD - US Dollar</option>
<option value="EUR">EUR - Euro</option> <option value="EUR">EUR - Euro</option>
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Zona horaria Timezone
</label> </label>
<select <select
value={timezone} value={timezone}
onChange={(e) => setTimezone(e.target.value)} onChange={(e) => setTimezone(e.target.value)}
className="w-full rounded-lg border border-primary-200 bg-white px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary" className="w-full rounded-lg border border-primary-200 bg-white px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
> >
<option value="America/Mexico_City">Ciudad de México</option> <option value="America/Mexico_City">Mexico City</option>
<option value="America/Monterrey">Monterrey</option> <option value="America/Monterrey">Monterrey</option>
<option value="America/Tijuana">Tijuana</option> <option value="America/Tijuana">Tijuana</option>
<option value="America/Cancun">Cancún</option> <option value="America/Cancun">Cancún</option>
@@ -322,7 +322,7 @@ export default function SettingsPage() {
<div className="pt-4"> <div className="pt-4">
<Button onClick={handleSaveOrganization} disabled={loading}> <Button onClick={handleSaveOrganization} disabled={loading}>
<Save className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
{loading ? "Guardando..." : "Guardar cambios"} {loading ? "Saving..." : "Save changes"}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -330,31 +330,31 @@ export default function SettingsPage() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Configuración de Reservas</CardTitle> <CardTitle>Booking Settings</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Duración por defecto (minutos) Default duration (minutes)
</label> </label>
<Input type="number" defaultValue={60} min={30} step={30} /> <Input type="number" defaultValue={60} min={30} step={30} />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Anticipación mínima (horas) Minimum notice (hours)
</label> </label>
<Input type="number" defaultValue={2} min={0} /> <Input type="number" defaultValue={2} min={0} />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Anticipación máxima (días) Maximum advance (days)
</label> </label>
<Input type="number" defaultValue={14} min={1} /> <Input type="number" defaultValue={14} min={1} />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Horas para cancelar Cancellation window (hours)
</label> </label>
<Input type="number" defaultValue={24} min={0} /> <Input type="number" defaultValue={24} min={0} />
</div> </div>
@@ -363,7 +363,7 @@ export default function SettingsPage() {
<div className="pt-4"> <div className="pt-4">
<Button onClick={handleSaveOrganization} disabled={loading}> <Button onClick={handleSaveOrganization} disabled={loading}>
<Save className="h-4 w-4 mr-2" /> <Save className="h-4 w-4 mr-2" />
{loading ? "Guardando..." : "Guardar cambios"} {loading ? "Saving..." : "Save changes"}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -373,10 +373,10 @@ export default function SettingsPage() {
{/* Sites Tab */} {/* Sites Tab */}
<TabsContent value="sites" className="space-y-6"> <TabsContent value="sites" className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-lg font-semibold text-primary-800">Sedes</h2> <h2 className="text-lg font-semibold text-primary-800">Sites</h2>
<Button onClick={() => { setEditingSite(null); setShowSiteForm(true); }}> <Button onClick={() => { setEditingSite(null); setShowSiteForm(true); }}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Nueva Sede New Site
</Button> </Button>
</div> </div>
@@ -423,7 +423,7 @@ export default function SettingsPage() {
: "bg-gray-100 text-gray-600" : "bg-gray-100 text-gray-600"
}`} }`}
> >
{site.isActive ? "Activa" : "Inactiva"} {site.isActive ? "Active" : "Inactive"}
</span> </span>
</div> </div>
</CardContent> </CardContent>
@@ -446,10 +446,10 @@ export default function SettingsPage() {
{/* Courts Tab */} {/* Courts Tab */}
<TabsContent value="courts" className="space-y-6"> <TabsContent value="courts" className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-lg font-semibold text-primary-800">Canchas</h2> <h2 className="text-lg font-semibold text-primary-800">Courts</h2>
<Button onClick={() => { setEditingCourt(null); setShowCourtForm(true); }}> <Button onClick={() => { setEditingCourt(null); setShowCourtForm(true); }}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Nueva Cancha New Court
</Button> </Button>
</div> </div>
@@ -468,12 +468,12 @@ export default function SettingsPage() {
<table className="w-full"> <table className="w-full">
<thead className="bg-primary-50 border-b border-primary-100"> <thead className="bg-primary-50 border-b border-primary-100">
<tr> <tr>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Cancha</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Court</th>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Sede</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Site</th>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Tipo</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Type</th>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Precio/hora</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Price/hour</th>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Estado</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Status</th>
<th className="text-right px-4 py-3 text-sm font-medium text-primary-700">Acciones</th> <th className="text-right px-4 py-3 text-sm font-medium text-primary-700">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -493,7 +493,7 @@ export default function SettingsPage() {
: "bg-gray-100 text-gray-600" : "bg-gray-100 text-gray-600"
}`} }`}
> >
{court.status === "active" ? "Activa" : court.status === "maintenance" ? "Mantenimiento" : "Inactiva"} {court.status === "active" ? "Active" : court.status === "maintenance" ? "Maintenance" : "Inactive"}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
@@ -533,10 +533,10 @@ export default function SettingsPage() {
{/* Users Tab */} {/* Users Tab */}
<TabsContent value="users" className="space-y-6"> <TabsContent value="users" className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-lg font-semibold text-primary-800">Usuarios</h2> <h2 className="text-lg font-semibold text-primary-800">Users</h2>
<Button> <Button>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Nuevo Usuario New User
</Button> </Button>
</div> </div>
@@ -555,12 +555,12 @@ export default function SettingsPage() {
<table className="w-full"> <table className="w-full">
<thead className="bg-primary-50 border-b border-primary-100"> <thead className="bg-primary-50 border-b border-primary-100">
<tr> <tr>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Usuario</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">User</th>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Email</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Email</th>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Rol</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Role</th>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Sede</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Site</th>
<th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Estado</th> <th className="text-left px-4 py-3 text-sm font-medium text-primary-700">Status</th>
<th className="text-right px-4 py-3 text-sm font-medium text-primary-700">Acciones</th> <th className="text-right px-4 py-3 text-sm font-medium text-primary-700">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -573,11 +573,11 @@ export default function SettingsPage() {
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary-100 text-primary-700"> <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary-100 text-primary-700">
{user.role === "super_admin" ? "Super Admin" : {user.role === "super_admin" ? "Super Admin" :
user.role === "site_admin" ? "Admin Sede" : user.role === "site_admin" ? "Site Admin" :
user.role === "staff" ? "Staff" : user.role} user.role === "staff" ? "Staff" : user.role}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-primary-600">{user.site?.name || "Todas"}</td> <td className="px-4 py-3 text-primary-600">{user.site?.name || "All"}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span <span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${ className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
@@ -586,7 +586,7 @@ export default function SettingsPage() {
: "bg-gray-100 text-gray-600" : "bg-gray-100 text-gray-600"
}`} }`}
> >
{user.isActive ? "Activo" : "Inactivo"} {user.isActive ? "Active" : "Inactive"}
</span> </span>
</td> </td>
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
@@ -636,7 +636,7 @@ function SiteFormModal({
<div className="bg-white rounded-xl shadow-xl w-full max-w-md mx-4"> <div className="bg-white rounded-xl shadow-xl w-full max-w-md mx-4">
<div className="flex items-center justify-between p-4 border-b"> <div className="flex items-center justify-between p-4 border-b">
<h3 className="text-lg font-semibold text-primary-800"> <h3 className="text-lg font-semibold text-primary-800">
{site ? "Editar Sede" : "Nueva Sede"} {site ? "Edit Site" : "New Site"}
</h3> </h3>
<button onClick={onClose} className="text-primary-500 hover:text-primary-700"> <button onClick={onClose} className="text-primary-500 hover:text-primary-700">
<X className="h-5 w-5" /> <X className="h-5 w-5" />
@@ -644,24 +644,24 @@ function SiteFormModal({
</div> </div>
<form onSubmit={handleSubmit} className="p-4 space-y-4"> <form onSubmit={handleSubmit} className="p-4 space-y-4">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Nombre</label> <label className="block text-sm font-medium text-primary-700 mb-1">Name</label>
<Input value={name} onChange={(e) => setName(e.target.value)} required /> <Input value={name} onChange={(e) => setName(e.target.value)} required />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Dirección</label> <label className="block text-sm font-medium text-primary-700 mb-1">Address</label>
<Input value={address} onChange={(e) => setAddress(e.target.value)} required /> <Input value={address} onChange={(e) => setAddress(e.target.value)} required />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Teléfono</label> <label className="block text-sm font-medium text-primary-700 mb-1">Phone</label>
<Input value={phone} onChange={(e) => setPhone(e.target.value)} /> <Input value={phone} onChange={(e) => setPhone(e.target.value)} />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Hora apertura</label> <label className="block text-sm font-medium text-primary-700 mb-1">Opening time</label>
<Input type="time" value={openTime} onChange={(e) => setOpenTime(e.target.value)} /> <Input type="time" value={openTime} onChange={(e) => setOpenTime(e.target.value)} />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Hora cierre</label> <label className="block text-sm font-medium text-primary-700 mb-1">Closing time</label>
<Input type="time" value={closeTime} onChange={(e) => setCloseTime(e.target.value)} /> <Input type="time" value={closeTime} onChange={(e) => setCloseTime(e.target.value)} />
</div> </div>
</div> </div>
@@ -673,14 +673,14 @@ function SiteFormModal({
onChange={(e) => setIsActive(e.target.checked)} onChange={(e) => setIsActive(e.target.checked)}
className="rounded border-primary-300" className="rounded border-primary-300"
/> />
<label htmlFor="isActive" className="text-sm text-primary-700">Sede activa</label> <label htmlFor="isActive" className="text-sm text-primary-700">Site active</label>
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button type="button" variant="outline" onClick={onClose} className="flex-1"> <Button type="button" variant="outline" onClick={onClose} className="flex-1">
Cancelar Cancel
</Button> </Button>
<Button type="submit" disabled={loading} className="flex-1"> <Button type="submit" disabled={loading} className="flex-1">
{loading ? "Guardando..." : "Guardar"} {loading ? "Saving..." : "Save"}
</Button> </Button>
</div> </div>
</form> </form>
@@ -727,7 +727,7 @@ function CourtFormModal({
<div className="bg-white rounded-xl shadow-xl w-full max-w-md mx-4"> <div className="bg-white rounded-xl shadow-xl w-full max-w-md mx-4">
<div className="flex items-center justify-between p-4 border-b"> <div className="flex items-center justify-between p-4 border-b">
<h3 className="text-lg font-semibold text-primary-800"> <h3 className="text-lg font-semibold text-primary-800">
{court ? "Editar Cancha" : "Nueva Cancha"} {court ? "Edit Court" : "New Court"}
</h3> </h3>
<button onClick={onClose} className="text-primary-500 hover:text-primary-700"> <button onClick={onClose} className="text-primary-500 hover:text-primary-700">
<X className="h-5 w-5" /> <X className="h-5 w-5" />
@@ -735,11 +735,11 @@ function CourtFormModal({
</div> </div>
<form onSubmit={handleSubmit} className="p-4 space-y-4"> <form onSubmit={handleSubmit} className="p-4 space-y-4">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Nombre</label> <label className="block text-sm font-medium text-primary-700 mb-1">Name</label>
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Cancha 1" required /> <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Court 1" required />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Sede</label> <label className="block text-sm font-medium text-primary-700 mb-1">Site</label>
<select <select
value={siteId} value={siteId}
onChange={(e) => setSiteId(e.target.value)} onChange={(e) => setSiteId(e.target.value)}
@@ -752,7 +752,7 @@ function CourtFormModal({
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Tipo</label> <label className="block text-sm font-medium text-primary-700 mb-1">Type</label>
<select <select
value={type} value={type}
onChange={(e) => setType(e.target.value)} onChange={(e) => setType(e.target.value)}
@@ -760,12 +760,12 @@ function CourtFormModal({
> >
<option value="indoor">Indoor</option> <option value="indoor">Indoor</option>
<option value="outdoor">Outdoor</option> <option value="outdoor">Outdoor</option>
<option value="covered">Techada</option> <option value="covered">Covered</option>
</select> </select>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Precio/hora</label> <label className="block text-sm font-medium text-primary-700 mb-1">Price/hour</label>
<Input <Input
type="number" type="number"
value={hourlyRate} value={hourlyRate}
@@ -775,34 +775,34 @@ function CourtFormModal({
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Precio hora pico</label> <label className="block text-sm font-medium text-primary-700 mb-1">Peak hour price</label>
<Input <Input
type="number" type="number"
value={peakHourlyRate} value={peakHourlyRate}
onChange={(e) => setPeakHourlyRate(e.target.value)} onChange={(e) => setPeakHourlyRate(e.target.value)}
min="0" min="0"
placeholder="Opcional" placeholder="Optional"
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1">Estado</label> <label className="block text-sm font-medium text-primary-700 mb-1">Status</label>
<select <select
value={status} value={status}
onChange={(e) => setStatus(e.target.value)} onChange={(e) => setStatus(e.target.value)}
className="w-full rounded-lg border border-primary-200 bg-white px-3 py-2 text-sm" className="w-full rounded-lg border border-primary-200 bg-white px-3 py-2 text-sm"
> >
<option value="active">Activa</option> <option value="active">Active</option>
<option value="maintenance">Mantenimiento</option> <option value="maintenance">Maintenance</option>
<option value="inactive">Inactiva</option> <option value="inactive">Inactive</option>
</select> </select>
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button type="button" variant="outline" onClick={onClose} className="flex-1"> <Button type="button" variant="outline" onClick={onClose} className="flex-1">
Cancelar Cancel
</Button> </Button>
<Button type="submit" disabled={loading} className="flex-1"> <Button type="submit" disabled={loading} className="flex-1">
{loading ? "Guardando..." : "Guardar"} {loading ? "Saving..." : "Save"}
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -11,24 +11,25 @@ function LoginContent() {
<div className="max-w-md text-center"> <div className="max-w-md text-center">
{/* Logo */} {/* Logo */}
<div className="mb-8 flex justify-center"> <div className="mb-8 flex justify-center">
<div className="w-24 h-24 bg-amber-500/20 backdrop-blur-sm rounded-2xl flex items-center justify-center border border-amber-400/30"> <div className="w-24 h-24 bg-primary/20 backdrop-blur-sm rounded-2xl flex items-center justify-center border border-primary-300/30">
<svg viewBox="0 0 100 100" className="w-16 h-16" fill="none"> <svg viewBox="0 0 100 100" className="w-16 h-16 text-white" fill="none">
{/* Lightning bolt / smash icon */} {/* Lightning bolt / smash icon */}
<path d="M55 10L20 55h25l-10 35L70 45H45l10-35z" fill="#FBBF24" /> <path d="M55 10L20 55h25l-10 35L70 45H45l10-35z" fill="currentColor" />
{/* Impact sparks */} {/* Impact sparks */}
<circle cx="78" cy="18" r="4" fill="#FBBF24" opacity="0.8" /> <circle cx="78" cy="18" r="4" fill="currentColor" opacity="0.8" />
<circle cx="85" cy="28" r="2.5" fill="#FBBF24" opacity="0.6" /> <circle cx="85" cy="28" r="2.5" fill="currentColor" opacity="0.6" />
<circle cx="72" cy="10" r="2" fill="#FBBF24" opacity="0.5" /> <circle cx="72" cy="10" r="2" fill="currentColor" opacity="0.5" />
</svg> </svg>
</div> </div>
</div> </div>
{/* Title */} {/* Title */}
<h1 className="text-4xl font-bold mb-4">SmashPoint</h1> <h1 className="text-4xl font-bold mb-4">Cabo Pickleball Club</h1>
<p className="text-sm text-primary-300 mb-2">Powered by SmashPoint</p>
{/* Tagline */} {/* Tagline */}
<p className="text-xl text-primary-200 mb-8"> <p className="text-xl text-primary-200 mb-8">
Sistema de Gestion para Clubes de Padel Court Management System
</p> </p>
{/* Features */} {/* Features */}
@@ -49,8 +50,8 @@ function LoginContent() {
</svg> </svg>
</div> </div>
<div> <div>
<p className="font-medium">Gestion de Reservas</p> <p className="font-medium">Court Bookings</p>
<p className="text-sm text-primary-300">Administra tus canchas y horarios</p> <p className="text-sm text-primary-300">Manage your courts and schedules</p>
</div> </div>
</div> </div>
@@ -66,8 +67,8 @@ function LoginContent() {
</svg> </svg>
</div> </div>
<div> <div>
<p className="font-medium">Control de Clientes</p> <p className="font-medium">Player Management</p>
<p className="text-sm text-primary-300">Membresias y perfiles completos</p> <p className="text-sm text-primary-300">Memberships and player profiles</p>
</div> </div>
</div> </div>
@@ -83,8 +84,8 @@ function LoginContent() {
</svg> </svg>
</div> </div>
<div> <div>
<p className="font-medium">Reportes y Estadisticas</p> <p className="font-medium">Reports & Analytics</p>
<p className="text-sm text-primary-300">Analiza el rendimiento de tu club</p> <p className="text-sm text-primary-300">Analyze your club's performance</p>
</div> </div>
</div> </div>
</div> </div>
@@ -95,23 +96,23 @@ function LoginContent() {
<div className="w-full lg:w-1/2 flex flex-col justify-center items-center p-6 lg:p-12"> <div className="w-full lg:w-1/2 flex flex-col justify-center items-center p-6 lg:p-12">
{/* Mobile Logo */} {/* Mobile Logo */}
<div className="lg:hidden mb-8 text-center text-white"> <div className="lg:hidden mb-8 text-center text-white">
<div className="w-16 h-16 mx-auto mb-4 bg-amber-500/20 backdrop-blur-sm rounded-xl flex items-center justify-center border border-amber-400/30"> <div className="w-16 h-16 mx-auto mb-4 bg-primary/20 backdrop-blur-sm rounded-xl flex items-center justify-center border border-primary-300/30">
<svg viewBox="0 0 100 100" className="w-10 h-10" fill="none"> <svg viewBox="0 0 100 100" className="w-10 h-10 text-white" fill="none">
<path d="M55 10L20 55h25l-10 35L70 45H45l10-35z" fill="#FBBF24" /> <path d="M55 10L20 55h25l-10 35L70 45H45l10-35z" fill="currentColor" />
<circle cx="78" cy="18" r="4" fill="#FBBF24" opacity="0.8" /> <circle cx="78" cy="18" r="4" fill="currentColor" opacity="0.8" />
<circle cx="85" cy="28" r="2.5" fill="#FBBF24" opacity="0.6" /> <circle cx="85" cy="28" r="2.5" fill="currentColor" opacity="0.6" />
<circle cx="72" cy="10" r="2" fill="#FBBF24" opacity="0.5" /> <circle cx="72" cy="10" r="2" fill="currentColor" opacity="0.5" />
</svg> </svg>
</div> </div>
<h1 className="text-2xl font-bold">SmashPoint</h1> <h1 className="text-2xl font-bold">Cabo Pickleball Club</h1>
<p className="text-sm text-primary-200 mt-1">Sistema de Gestion para Clubes de Padel</p> <p className="text-sm text-primary-200 mt-1">Court Management System</p>
</div> </div>
<LoginForm /> <LoginForm />
{/* Footer */} {/* Footer */}
<p className="mt-8 text-center text-sm text-primary-300"> <p className="mt-8 text-center text-sm text-primary-300">
&copy; {new Date().getFullYear()} SmashPoint. Todos los derechos reservados. &copy; {new Date().getFullYear()} SmashPoint. All rights reserved.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -12,7 +12,7 @@ interface RouteContext {
// Validation schema for payment // Validation schema for payment
const paymentSchema = z.object({ const paymentSchema = z.object({
paymentType: z.enum(['CASH', 'CARD', 'TRANSFER', 'MEMBERSHIP', 'FREE']), paymentType: z.enum(['CASH', 'CARD', 'TRANSFER', 'MEMBERSHIP', 'FREE']),
amount: z.number().positive('El monto debe ser mayor a 0').optional(), amount: z.number().positive('Amount must be greater than 0').optional(),
reference: z.string().max(100).optional(), reference: z.string().max(100).optional(),
notes: z.string().max(500).optional(), notes: z.string().max(500).optional(),
cashRegisterId: z.string().uuid().optional(), cashRegisterId: z.string().uuid().optional(),
@@ -28,7 +28,7 @@ export async function POST(
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -51,7 +51,7 @@ export async function POST(
if (!existingBooking) { if (!existingBooking) {
return NextResponse.json( return NextResponse.json(
{ error: 'Reserva no encontrada' }, { error: 'Booking not found' },
{ status: 404 } { status: 404 }
); );
} }
@@ -59,7 +59,7 @@ export async function POST(
// If user is SITE_ADMIN, verify they have access to this site // If user is SITE_ADMIN, verify they have access to this site
if (session.user.role === 'SITE_ADMIN' && session.user.siteId !== existingBooking.siteId) { if (session.user.role === 'SITE_ADMIN' && session.user.siteId !== existingBooking.siteId) {
return NextResponse.json( return NextResponse.json(
{ error: 'No tiene permiso para procesar pagos en esta reserva' }, { error: 'You do not have permission to process payments for this booking' },
{ status: 403 } { status: 403 }
); );
} }
@@ -67,7 +67,7 @@ export async function POST(
// Check if booking is already cancelled // Check if booking is already cancelled
if (existingBooking.status === 'CANCELLED') { if (existingBooking.status === 'CANCELLED') {
return NextResponse.json( return NextResponse.json(
{ error: 'No se puede procesar el pago de una reserva cancelada' }, { error: 'Cannot process payment for a cancelled booking' },
{ status: 400 } { status: 400 }
); );
} }
@@ -81,7 +81,7 @@ export async function POST(
if (totalPaid >= totalPrice) { if (totalPaid >= totalPrice) {
return NextResponse.json( return NextResponse.json(
{ error: 'La reserva ya está completamente pagada' }, { error: 'The booking is already fully paid' },
{ status: 400 } { status: 400 }
); );
} }
@@ -93,7 +93,7 @@ export async function POST(
if (!validationResult.success) { if (!validationResult.success) {
return NextResponse.json( return NextResponse.json(
{ {
error: 'Datos de pago inválidos', error: 'Invalid payment data',
details: validationResult.error.flatten().fieldErrors, details: validationResult.error.flatten().fieldErrors,
}, },
{ status: 400 } { status: 400 }
@@ -108,7 +108,7 @@ export async function POST(
if (paymentAmount <= 0) { if (paymentAmount <= 0) {
return NextResponse.json( return NextResponse.json(
{ error: 'El monto del pago debe ser mayor a 0' }, { error: 'Payment amount must be greater than 0' },
{ status: 400 } { status: 400 }
); );
} }
@@ -125,7 +125,7 @@ export async function POST(
if (!cashRegister) { if (!cashRegister) {
return NextResponse.json( return NextResponse.json(
{ error: 'Caja registradora no encontrada o no está abierta' }, { error: 'Cash register not found or is not open' },
{ status: 400 } { status: 400 }
); );
} }
@@ -211,8 +211,8 @@ export async function POST(
return NextResponse.json({ return NextResponse.json({
message: result.isFullyPaid message: result.isFullyPaid
? 'Pago completado. La reserva ha sido confirmada.' ? 'Payment completed. The booking has been confirmed.'
: 'Pago parcial registrado exitosamente.', : 'Partial payment recorded successfully.',
booking: result.booking, booking: result.booking,
payment: result.payment, payment: result.payment,
remainingAmount: Math.max(0, totalPrice - (totalPaid + paymentAmount)), remainingAmount: Math.max(0, totalPrice - (totalPaid + paymentAmount)),
@@ -220,7 +220,7 @@ export async function POST(
} catch (error) { } catch (error) {
console.error('Error processing payment:', error); console.error('Error processing payment:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al procesar el pago' }, { error: 'Error processing payment' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -26,7 +26,7 @@ export async function GET(
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -99,7 +99,7 @@ export async function GET(
if (!booking) { if (!booking) {
return NextResponse.json( return NextResponse.json(
{ error: 'Reserva no encontrada' }, { error: 'Booking not found' },
{ status: 404 } { status: 404 }
); );
} }
@@ -108,7 +108,7 @@ export async function GET(
} catch (error) { } catch (error) {
console.error('Error fetching booking:', error); console.error('Error fetching booking:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al obtener la reserva' }, { error: 'Error fetching booking' },
{ status: 500 } { status: 500 }
); );
} }
@@ -124,7 +124,7 @@ export async function PUT(
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -146,7 +146,7 @@ export async function PUT(
if (!existingBooking) { if (!existingBooking) {
return NextResponse.json( return NextResponse.json(
{ error: 'Reserva no encontrada' }, { error: 'Booking not found' },
{ status: 404 } { status: 404 }
); );
} }
@@ -154,7 +154,7 @@ export async function PUT(
// If user is SITE_ADMIN, verify they have access to this site // If user is SITE_ADMIN, verify they have access to this site
if (session.user.role === 'SITE_ADMIN' && session.user.siteId !== existingBooking.siteId) { if (session.user.role === 'SITE_ADMIN' && session.user.siteId !== existingBooking.siteId) {
return NextResponse.json( return NextResponse.json(
{ error: 'No tiene permiso para modificar esta reserva' }, { error: 'You do not have permission to modify this booking' },
{ status: 403 } { status: 403 }
); );
} }
@@ -166,7 +166,7 @@ export async function PUT(
if (!validationResult.success) { if (!validationResult.success) {
return NextResponse.json( return NextResponse.json(
{ {
error: 'Datos de actualización inválidos', error: 'Invalid update data',
details: validationResult.error.flatten().fieldErrors, details: validationResult.error.flatten().fieldErrors,
}, },
{ status: 400 } { status: 400 }
@@ -239,7 +239,7 @@ export async function PUT(
} catch (error) { } catch (error) {
console.error('Error updating booking:', error); console.error('Error updating booking:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al actualizar la reserva' }, { error: 'Error updating booking' },
{ status: 500 } { status: 500 }
); );
} }
@@ -255,7 +255,7 @@ export async function DELETE(
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -277,7 +277,7 @@ export async function DELETE(
if (!existingBooking) { if (!existingBooking) {
return NextResponse.json( return NextResponse.json(
{ error: 'Reserva no encontrada' }, { error: 'Booking not found' },
{ status: 404 } { status: 404 }
); );
} }
@@ -285,7 +285,7 @@ export async function DELETE(
// If user is SITE_ADMIN, verify they have access to this site // If user is SITE_ADMIN, verify they have access to this site
if (session.user.role === 'SITE_ADMIN' && session.user.siteId !== existingBooking.siteId) { if (session.user.role === 'SITE_ADMIN' && session.user.siteId !== existingBooking.siteId) {
return NextResponse.json( return NextResponse.json(
{ error: 'No tiene permiso para cancelar esta reserva' }, { error: 'You do not have permission to cancel this booking' },
{ status: 403 } { status: 403 }
); );
} }
@@ -294,7 +294,7 @@ export async function DELETE(
const hasPayments = existingBooking.payments.length > 0; const hasPayments = existingBooking.payments.length > 0;
// Parse optional cancel reason from query params or body // Parse optional cancel reason from query params or body
let cancelReason = 'Cancelada por el administrador'; let cancelReason = 'Cancelled by administrator';
try { try {
const body = await request.json(); const body = await request.json();
if (body.cancelReason) { if (body.cancelReason) {
@@ -316,9 +316,9 @@ export async function DELETE(
}); });
return NextResponse.json({ return NextResponse.json({
message: 'Reserva cancelada exitosamente', message: 'Booking cancelled successfully',
booking, booking,
note: 'La reserva tiene pagos asociados, por lo que fue cancelada en lugar de eliminada', note: 'The booking has associated payments, so it was cancelled instead of deleted',
}); });
} else { } else {
// If no payments, allow hard delete for pending bookings only // If no payments, allow hard delete for pending bookings only
@@ -328,7 +328,7 @@ export async function DELETE(
}); });
return NextResponse.json({ return NextResponse.json({
message: 'Reserva eliminada exitosamente', message: 'Booking deleted successfully',
}); });
} else { } else {
// For non-pending bookings, soft delete // For non-pending bookings, soft delete
@@ -342,7 +342,7 @@ export async function DELETE(
}); });
return NextResponse.json({ return NextResponse.json({
message: 'Reserva cancelada exitosamente', message: 'Booking cancelled successfully',
booking, booking,
}); });
} }
@@ -350,7 +350,7 @@ export async function DELETE(
} catch (error) { } catch (error) {
console.error('Error deleting booking:', error); console.error('Error deleting booking:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al cancelar la reserva' }, { error: 'Error cancelling booking' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -20,7 +20,7 @@ export async function GET(request: NextRequest) {
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -138,7 +138,7 @@ export async function GET(request: NextRequest) {
} catch (error) { } catch (error) {
console.error('Error fetching bookings:', error); console.error('Error fetching bookings:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al obtener las reservas' }, { error: 'Error fetching bookings' },
{ status: 500 } { status: 500 }
); );
} }
@@ -151,7 +151,7 @@ export async function POST(request: NextRequest) {
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -163,7 +163,7 @@ export async function POST(request: NextRequest) {
if (!validationResult.success) { if (!validationResult.success) {
return NextResponse.json( return NextResponse.json(
{ {
error: 'Datos de reserva inválidos', error: 'Invalid booking data',
details: validationResult.error.flatten().fieldErrors, details: validationResult.error.flatten().fieldErrors,
}, },
{ status: 400 } { status: 400 }
@@ -193,14 +193,14 @@ export async function POST(request: NextRequest) {
if (!court) { if (!court) {
return NextResponse.json( return NextResponse.json(
{ error: 'Cancha no encontrada o no pertenece a su organización' }, { error: 'Court not found or does not belong to your organization' },
{ status: 404 } { status: 404 }
); );
} }
if (court.status !== 'AVAILABLE' || !court.isActive) { if (court.status !== 'AVAILABLE' || !court.isActive) {
return NextResponse.json( return NextResponse.json(
{ error: 'La cancha no está disponible para reservas' }, { error: 'The court is not available for bookings' },
{ status: 400 } { status: 400 }
); );
} }
@@ -232,7 +232,7 @@ export async function POST(request: NextRequest) {
if (!client) { if (!client) {
return NextResponse.json( return NextResponse.json(
{ error: 'Cliente no encontrado o no pertenece a su organización' }, { error: 'Client not found or does not belong to your organization' },
{ status: 404 } { status: 404 }
); );
} }
@@ -269,7 +269,7 @@ export async function POST(request: NextRequest) {
if (existingBooking) { if (existingBooking) {
return NextResponse.json( return NextResponse.json(
{ error: 'Ya existe una reserva en ese horario. Por favor, seleccione otro horario.' }, { error: 'A booking already exists for that time slot. Please select another time.' },
{ status: 409 } { status: 409 }
); );
} }
@@ -391,7 +391,7 @@ export async function POST(request: NextRequest) {
} catch (error) { } catch (error) {
console.error('Error creating booking:', error); console.error('Error creating booking:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al crear la reserva' }, { error: 'Error creating booking' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -10,11 +10,11 @@ interface RouteContext {
// Validation schema for updating client // Validation schema for updating client
const updateClientSchema = z.object({ const updateClientSchema = z.object({
firstName: z.string().min(1, 'El nombre es requerido').optional(), firstName: z.string().min(1, 'First name is required').optional(),
lastName: z.string().min(1, 'El apellido es requerido').optional(), lastName: z.string().min(1, 'Last name is required').optional(),
email: z.string().email('Email invalido').nullable().optional(), email: z.string().email('Invalid email').nullable().optional(),
phone: z.string().nullable().optional(), phone: z.string().nullable().optional(),
avatar: z.string().url('URL invalida').nullable().optional(), avatar: z.string().url('Invalid URL').nullable().optional(),
dateOfBirth: z.string().nullable().optional(), dateOfBirth: z.string().nullable().optional(),
address: z.string().nullable().optional(), address: z.string().nullable().optional(),
notes: z.string().nullable().optional(), notes: z.string().nullable().optional(),
@@ -32,7 +32,7 @@ export async function GET(
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -79,7 +79,7 @@ export async function GET(
if (!client) { if (!client) {
return NextResponse.json( return NextResponse.json(
{ error: 'Cliente no encontrado' }, { error: 'Client not found' },
{ status: 404 } { status: 404 }
); );
} }
@@ -122,7 +122,7 @@ export async function GET(
} catch (error) { } catch (error) {
console.error('Error fetching client:', error); console.error('Error fetching client:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al obtener el cliente' }, { error: 'Error fetching client' },
{ status: 500 } { status: 500 }
); );
} }
@@ -138,7 +138,7 @@ export async function PUT(
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -155,7 +155,7 @@ export async function PUT(
if (!existingClient) { if (!existingClient) {
return NextResponse.json( return NextResponse.json(
{ error: 'Cliente no encontrado' }, { error: 'Client not found' },
{ status: 404 } { status: 404 }
); );
} }
@@ -167,7 +167,7 @@ export async function PUT(
if (!validationResult.success) { if (!validationResult.success) {
return NextResponse.json( return NextResponse.json(
{ {
error: 'Datos de actualizacion invalidos', error: 'Invalid update data',
details: validationResult.error.flatten().fieldErrors, details: validationResult.error.flatten().fieldErrors,
}, },
{ status: 400 } { status: 400 }
@@ -201,7 +201,7 @@ export async function PUT(
if (emailExists) { if (emailExists) {
return NextResponse.json( return NextResponse.json(
{ error: 'Ya existe un cliente con este email' }, { error: 'A client with this email already exists' },
{ status: 409 } { status: 409 }
); );
} }
@@ -262,13 +262,13 @@ export async function PUT(
// Check for unique constraint violation // Check for unique constraint violation
if (error instanceof Error && error.message.includes('Unique constraint')) { if (error instanceof Error && error.message.includes('Unique constraint')) {
return NextResponse.json( return NextResponse.json(
{ error: 'Ya existe un cliente con este email o DNI' }, { error: 'A client with this email or ID number already exists' },
{ status: 409 } { status: 409 }
); );
} }
return NextResponse.json( return NextResponse.json(
{ error: 'Error al actualizar el cliente' }, { error: 'Error updating client' },
{ status: 500 } { status: 500 }
); );
} }
@@ -284,7 +284,7 @@ export async function DELETE(
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -318,7 +318,7 @@ export async function DELETE(
if (!existingClient) { if (!existingClient) {
return NextResponse.json( return NextResponse.json(
{ error: 'Cliente no encontrado' }, { error: 'Client not found' },
{ status: 404 } { status: 404 }
); );
} }
@@ -327,7 +327,7 @@ export async function DELETE(
if (existingClient.memberships.length > 0) { if (existingClient.memberships.length > 0) {
return NextResponse.json( return NextResponse.json(
{ {
error: 'No se puede desactivar un cliente con membresia activa', error: 'Cannot deactivate a client with an active membership',
details: { details: {
activeMemberships: existingClient.memberships.length, activeMemberships: existingClient.memberships.length,
}, },
@@ -340,7 +340,7 @@ export async function DELETE(
if (existingClient.bookings.length > 0) { if (existingClient.bookings.length > 0) {
return NextResponse.json( return NextResponse.json(
{ {
error: 'No se puede desactivar un cliente con reservas pendientes', error: 'Cannot deactivate a client with pending bookings',
details: { details: {
pendingBookings: existingClient.bookings.length, pendingBookings: existingClient.bookings.length,
}, },
@@ -364,13 +364,13 @@ export async function DELETE(
}); });
return NextResponse.json({ return NextResponse.json({
message: 'Cliente desactivado exitosamente', message: 'Client deactivated successfully',
client, client,
}); });
} catch (error) { } catch (error) {
console.error('Error deleting client:', error); console.error('Error deleting client:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al desactivar el cliente' }, { error: 'Error deactivating client' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -11,7 +11,7 @@ export async function GET(request: NextRequest) {
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -123,7 +123,7 @@ export async function GET(request: NextRequest) {
} catch (error) { } catch (error) {
console.error('Error fetching clients:', error); console.error('Error fetching clients:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al obtener los clientes' }, { error: 'Error fetching clients' },
{ status: 500 } { status: 500 }
); );
} }
@@ -136,7 +136,7 @@ export async function POST(request: NextRequest) {
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -148,7 +148,7 @@ export async function POST(request: NextRequest) {
if (!validationResult.success) { if (!validationResult.success) {
return NextResponse.json( return NextResponse.json(
{ {
error: 'Datos del cliente inválidos', error: 'Invalid client data',
details: validationResult.error.flatten().fieldErrors, details: validationResult.error.flatten().fieldErrors,
}, },
{ status: 400 } { status: 400 }
@@ -181,7 +181,7 @@ export async function POST(request: NextRequest) {
if (existingClient) { if (existingClient) {
return NextResponse.json( return NextResponse.json(
{ error: 'Ya existe un cliente con este correo electrónico en su organización' }, { error: 'A client with this email already exists in your organization' },
{ status: 409 } { status: 409 }
); );
} }
@@ -224,13 +224,13 @@ export async function POST(request: NextRequest) {
// Check for unique constraint violation // Check for unique constraint violation
if (error instanceof Error && error.message.includes('Unique constraint')) { if (error instanceof Error && error.message.includes('Unique constraint')) {
return NextResponse.json( return NextResponse.json(
{ error: 'Ya existe un cliente con este correo electrónico o DNI' }, { error: 'A client with this email or ID number already exists' },
{ status: 409 } { status: 409 }
); );
} }
return NextResponse.json( return NextResponse.json(
{ error: 'Error al crear el cliente' }, { error: 'Error creating client' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -10,7 +10,7 @@ export async function GET(request: NextRequest) {
if (!session?.user) { if (!session?.user) {
return NextResponse.json( return NextResponse.json(
{ error: 'No autorizado' }, { error: 'Unauthorized' },
{ status: 401 } { status: 401 }
); );
} }
@@ -315,7 +315,7 @@ export async function GET(request: NextRequest) {
} catch (error) { } catch (error) {
console.error('Error fetching dashboard stats:', error); console.error('Error fetching dashboard stats:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al obtener estadísticas del dashboard' }, { error: 'Error fetching dashboard statistics' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -35,14 +35,14 @@ export async function GET(
}); });
if (!site) { if (!site) {
return NextResponse.json({ error: 'Sede no encontrada' }, { status: 404 }); return NextResponse.json({ error: 'Site not found' }, { status: 404 });
} }
return NextResponse.json({ data: site }); return NextResponse.json({ data: site });
} catch (error) { } catch (error) {
console.error('Error fetching site:', error); console.error('Error fetching site:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al obtener sede' }, { error: 'Error fetching site' },
{ status: 500 } { status: 500 }
); );
} }
@@ -61,7 +61,7 @@ export async function PUT(
} }
if (!['super_admin', 'site_admin'].includes(session.user.role)) { if (!['super_admin', 'site_admin'].includes(session.user.role)) {
return NextResponse.json({ error: 'Sin permisos' }, { status: 403 }); return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
} }
const body = await request.json(); const body = await request.json();
@@ -76,7 +76,7 @@ export async function PUT(
}); });
if (!existingSite) { if (!existingSite) {
return NextResponse.json({ error: 'Sede no encontrada' }, { status: 404 }); return NextResponse.json({ error: 'Site not found' }, { status: 404 });
} }
const updateData: any = {}; const updateData: any = {};
@@ -102,7 +102,7 @@ export async function PUT(
} catch (error) { } catch (error) {
console.error('Error updating site:', error); console.error('Error updating site:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al actualizar sede' }, { error: 'Error updating site' },
{ status: 500 } { status: 500 }
); );
} }
@@ -121,7 +121,7 @@ export async function DELETE(
} }
if (session.user.role !== 'super_admin') { if (session.user.role !== 'super_admin') {
return NextResponse.json({ error: 'Sin permisos' }, { status: 403 }); return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
} }
// Verify site belongs to organization // Verify site belongs to organization
@@ -133,7 +133,7 @@ export async function DELETE(
}); });
if (!existingSite) { if (!existingSite) {
return NextResponse.json({ error: 'Sede no encontrada' }, { status: 404 }); return NextResponse.json({ error: 'Site not found' }, { status: 404 });
} }
// Soft delete // Soft delete
@@ -146,7 +146,7 @@ export async function DELETE(
} catch (error) { } catch (error) {
console.error('Error deleting site:', error); console.error('Error deleting site:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al eliminar sede' }, { error: 'Error deleting site' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -80,7 +80,7 @@ export async function POST(request: NextRequest) {
} }
if (!['super_admin', 'site_admin'].includes(session.user.role)) { if (!['super_admin', 'site_admin'].includes(session.user.role)) {
return NextResponse.json({ error: 'Sin permisos' }, { status: 403 }); return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
} }
const body = await request.json(); const body = await request.json();
@@ -88,7 +88,7 @@ export async function POST(request: NextRequest) {
if (!name || !address) { if (!name || !address) {
return NextResponse.json( return NextResponse.json(
{ error: 'Nombre y dirección son requeridos' }, { error: 'Name and address are required' },
{ status: 400 } { status: 400 }
); );
} }
@@ -116,7 +116,7 @@ export async function POST(request: NextRequest) {
} catch (error) { } catch (error) {
console.error('Error creating site:', error); console.error('Error creating site:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Error al crear sede' }, { error: 'Error creating site' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -8,7 +8,7 @@ export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user) { if (!session?.user) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
const users = await db.user.findMany({ const users = await db.user.findMany({
@@ -45,7 +45,7 @@ export async function GET(request: NextRequest) {
} catch (error) { } catch (error) {
console.error("Error fetching users:", error); console.error("Error fetching users:", error);
return NextResponse.json( return NextResponse.json(
{ error: "Error al obtener usuarios" }, { error: "Error fetching users" },
{ status: 500 } { status: 500 }
); );
} }
@@ -56,12 +56,12 @@ export async function POST(request: NextRequest) {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user) { if (!session?.user) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
// Only super_admin and site_admin can create users // Only super_admin and site_admin can create users
if (!["super_admin", "site_admin"].includes(session.user.role)) { if (!["super_admin", "site_admin"].includes(session.user.role)) {
return NextResponse.json({ error: "Sin permisos" }, { status: 403 }); return NextResponse.json({ error: "Insufficient permissions" }, { status: 403 });
} }
const body = await request.json(); const body = await request.json();
@@ -69,7 +69,7 @@ export async function POST(request: NextRequest) {
if (!email || !password || !firstName || !lastName || !role) { if (!email || !password || !firstName || !lastName || !role) {
return NextResponse.json( return NextResponse.json(
{ error: "Faltan campos requeridos" }, { error: "Missing required fields" },
{ status: 400 } { status: 400 }
); );
} }
@@ -84,7 +84,7 @@ export async function POST(request: NextRequest) {
if (existingUser) { if (existingUser) {
return NextResponse.json( return NextResponse.json(
{ error: "El email ya está registrado" }, { error: "This email is already registered" },
{ status: 400 } { status: 400 }
); );
} }
@@ -129,7 +129,7 @@ export async function POST(request: NextRequest) {
} catch (error) { } catch (error) {
console.error("Error creating user:", error); console.error("Error creating user:", error);
return NextResponse.json( return NextResponse.json(
{ error: "Error al crear usuario" }, { error: "Error creating user" },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#F59E0B"/> <rect width="32" height="32" rx="8" fill="#2990EA"/>
<path d="M17.5 3L6.5 17h8l-3 12L22.5 15H14.5l3-12z" fill="white"/> <path d="M17.5 3L6.5 17h8l-3 12L22.5 15H14.5l3-12z" fill="white"/>
<circle cx="25" cy="6" r="1.5" fill="white" opacity="0.8"/> <circle cx="25" cy="6" r="1.5" fill="white" opacity="0.8"/>
<circle cx="27" cy="9" r="1" fill="white" opacity="0.6"/> <circle cx="27" cy="9" r="1" fill="white" opacity="0.6"/>

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

View File

@@ -5,10 +5,10 @@ import "./globals.css";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "SmashPoint", title: "Cabo Pickleball Club | SmashPoint",
description: "Sistema de Gestión para Clubes de Pádel", description: "Court Management System for Cabo Pickleball Club",
keywords: ["padel", "club", "reservas", "gestión", "deportes"], keywords: ["pickleball", "cabo", "courts", "bookings", "club"],
authors: [{ name: "SmashPoint Team" }], authors: [{ name: "SmashPoint" }],
}; };
export default function RootLayout({ export default function RootLayout({
@@ -17,7 +17,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="es"> <html lang="en">
<body className={inter.className}>{children}</body> <body className={inter.className}>{children}</body>
</html> </html>
); );

View File

@@ -6,7 +6,7 @@ export default function Home() {
<div className="text-center space-y-8 px-4"> <div className="text-center space-y-8 px-4">
{/* Logo */} {/* Logo */}
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-20 h-20 bg-amber-500 rounded-2xl flex items-center justify-center shadow-lg"> <div className="w-20 h-20 bg-primary rounded-2xl flex items-center justify-center shadow-lg">
<svg viewBox="0 0 40 40" className="w-12 h-12 text-white" fill="none"> <svg viewBox="0 0 40 40" className="w-12 h-12 text-white" fill="none">
<path d="M22 4L8 22h10l-4 14L28 18H18l4-14z" fill="currentColor" /> <path d="M22 4L8 22h10l-4 14L28 18H18l4-14z" fill="currentColor" />
<circle cx="32" cy="8" r="2" fill="currentColor" opacity="0.8" /> <circle cx="32" cy="8" r="2" fill="currentColor" opacity="0.8" />
@@ -16,10 +16,11 @@ export default function Home() {
</div> </div>
</div> </div>
<h1 className="text-5xl md:text-6xl font-bold text-primary-800"> <h1 className="text-5xl md:text-6xl font-bold text-primary-800">
SmashPoint Cabo Pickleball Club
</h1> </h1>
<p className="text-sm text-primary-400 -mt-4">Powered by SmashPoint</p>
<p className="text-xl md:text-2xl text-primary-600 max-w-2xl mx-auto"> <p className="text-xl md:text-2xl text-primary-600 max-w-2xl mx-auto">
Sistema de Gestion para Clubes de Padel Court Management System
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center mt-8"> <div className="flex flex-col sm:flex-row gap-4 justify-center mt-8">
<Link <Link
@@ -29,10 +30,10 @@ export default function Home() {
Dashboard Dashboard
</Link> </Link>
<Link <Link
href="/reservas" href="/bookings"
className="px-8 py-3 bg-accent-500 text-white font-semibold rounded-lg hover:bg-accent-600 transition-colors duration-200 shadow-lg hover:shadow-xl" className="px-8 py-3 bg-accent-500 text-white font-semibold rounded-lg hover:bg-accent-600 transition-colors duration-200 shadow-lg hover:shadow-xl"
> >
Reservas Book a Court
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -29,15 +29,15 @@ export function LoginForm({ className }: LoginFormProps) {
const newErrors: { email?: string; password?: string } = {}; const newErrors: { email?: string; password?: string } = {};
if (!email) { if (!email) {
newErrors.email = 'El correo electrónico es requerido'; newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
newErrors.email = 'Ingresa un correo electrónico válido'; newErrors.email = 'Enter a valid email address';
} }
if (!password) { if (!password) {
newErrors.password = 'La contraseña es requerida'; newErrors.password = 'Password is required';
} else if (password.length < 6) { } else if (password.length < 6) {
newErrors.password = 'La contraseña debe tener al menos 6 caracteres'; newErrors.password = 'Password must be at least 6 characters';
} }
setErrors(newErrors); setErrors(newErrors);
@@ -62,13 +62,13 @@ export function LoginForm({ className }: LoginFormProps) {
}); });
if (result?.error) { if (result?.error) {
setError('Credenciales inválidas. Por favor, verifica tu correo y contraseña.'); setError('Invalid credentials. Please check your email and password.');
} else { } else {
router.push(callbackUrl); router.push(callbackUrl);
router.refresh(); router.refresh();
} }
} catch (err) { } catch (err) {
setError('Ocurrió un error al iniciar sesión. Por favor, intenta de nuevo.'); setError('An error occurred while signing in. Please try again.');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -77,9 +77,9 @@ export function LoginForm({ className }: LoginFormProps) {
return ( return (
<Card className={cn('w-full max-w-md', className)}> <Card className={cn('w-full max-w-md', className)}>
<CardHeader className="space-y-1 text-center"> <CardHeader className="space-y-1 text-center">
<CardTitle className="text-2xl font-bold">Iniciar Sesión</CardTitle> <CardTitle className="text-2xl font-bold">Sign In</CardTitle>
<CardDescription> <CardDescription>
Ingresa tus credenciales para acceder al sistema Enter your credentials to access the system
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -106,12 +106,12 @@ export function LoginForm({ className }: LoginFormProps) {
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium text-primary-700"> <label htmlFor="email" className="text-sm font-medium text-primary-700">
Correo Electrónico Email
</label> </label>
<Input <Input
id="email" id="email"
type="email" type="email"
placeholder="correo@ejemplo.com" placeholder="email@example.com"
value={email} value={email}
onChange={(e) => { onChange={(e) => {
setEmail(e.target.value); setEmail(e.target.value);
@@ -129,7 +129,7 @@ export function LoginForm({ className }: LoginFormProps) {
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium text-primary-700"> <label htmlFor="password" className="text-sm font-medium text-primary-700">
Contraseña Password
</label> </label>
<div className="relative"> <div className="relative">
<Input <Input
@@ -199,13 +199,13 @@ export function LoginForm({ className }: LoginFormProps) {
onChange={(e) => setRememberMe(e.target.checked)} onChange={(e) => setRememberMe(e.target.checked)}
className="h-4 w-4 rounded border-primary-300 text-primary focus:ring-primary-500" className="h-4 w-4 rounded border-primary-300 text-primary focus:ring-primary-500"
/> />
<span className="text-sm text-primary-600">Recordarme</span> <span className="text-sm text-primary-600">Remember me</span>
</label> </label>
<a <a
href="#" href="#"
className="text-sm text-primary-600 hover:text-primary-800 hover:underline" className="text-sm text-primary-600 hover:text-primary-800 hover:underline"
> >
¿Olvidaste tu contraseña? Forgot your password?
</a> </a>
</div> </div>
@@ -237,10 +237,10 @@ export function LoginForm({ className }: LoginFormProps) {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/> />
</svg> </svg>
Iniciando sesión... Signing in...
</div> </div>
) : ( ) : (
'Iniciar Sesión' 'Sign In'
)} )}
</Button> </Button>
</form> </form>

View File

@@ -94,13 +94,13 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
const url = siteId ? `/api/courts?siteId=${siteId}` : "/api/courts"; const url = siteId ? `/api/courts?siteId=${siteId}` : "/api/courts";
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error("Error al cargar las canchas"); throw new Error("Error loading courts");
} }
const data = await response.json(); const data = await response.json();
setCourts(data); setCourts(data);
return data as Court[]; return data as Court[];
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
return []; return [];
} }
}, [siteId]); }, [siteId]);
@@ -113,7 +113,7 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
`/api/courts/${courtId}/availability?date=${dateStr}` `/api/courts/${courtId}/availability?date=${dateStr}`
); );
if (!response.ok) { if (!response.ok) {
throw new Error(`Error al cargar disponibilidad`); throw new Error(`Error loading availability`);
} }
return (await response.json()) as CourtAvailability; return (await response.json()) as CourtAvailability;
} catch (err) { } catch (err) {
@@ -224,7 +224,7 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
fetchCourts(); fetchCourts();
}} }}
> >
Reintentar Retry
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -238,7 +238,7 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
<Card> <Card>
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg">Calendario</CardTitle> <CardTitle className="text-lg">Calendar</CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={goToPrevDay}> <Button variant="outline" size="sm" onClick={goToPrevDay}>
<svg <svg
@@ -260,7 +260,7 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
size="sm" size="sm"
onClick={goToToday} onClick={goToToday}
> >
Hoy Today
</Button> </Button>
<Button variant="outline" size="sm" onClick={goToNextDay}> <Button variant="outline" size="sm" onClick={goToNextDay}>
<svg <svg
@@ -286,12 +286,12 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-12">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary-200 border-t-primary-600" /> <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary-200 border-t-primary-600" />
<p className="text-sm text-primary-500">Cargando disponibilidad...</p> <p className="text-sm text-primary-500">Loading availability...</p>
</div> </div>
</div> </div>
) : courts.length === 0 ? ( ) : courts.length === 0 ? (
<div className="p-6 text-center text-primary-500"> <div className="p-6 text-center text-primary-500">
<p>No hay canchas disponibles.</p> <p>No courts available.</p>
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -304,8 +304,10 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
courts.length === 2 && "grid-cols-2", courts.length === 2 && "grid-cols-2",
courts.length === 3 && "grid-cols-3", courts.length === 3 && "grid-cols-3",
courts.length === 4 && "grid-cols-4", courts.length === 4 && "grid-cols-4",
courts.length >= 5 && "grid-cols-5" courts.length === 5 && "grid-cols-5",
courts.length >= 6 && "grid-cols-6"
)} )}
style={courts.length >= 5 ? { minWidth: `${courts.length * 150}px` } : undefined}
> >
{courts.map((court) => ( {courts.map((court) => (
<div <div
@@ -316,7 +318,7 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
{court.name} {court.name}
</h3> </h3>
<p className="text-xs text-primary-500 mt-1"> <p className="text-xs text-primary-500 mt-1">
{court.type === "INDOOR" ? "Interior" : "Exterior"} {court.type === "INDOOR" ? "Indoor" : "Outdoor"}
</p> </p>
</div> </div>
))} ))}
@@ -333,8 +335,10 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
courts.length === 2 && "grid-cols-2", courts.length === 2 && "grid-cols-2",
courts.length === 3 && "grid-cols-3", courts.length === 3 && "grid-cols-3",
courts.length === 4 && "grid-cols-4", courts.length === 4 && "grid-cols-4",
courts.length >= 5 && "grid-cols-5" courts.length === 5 && "grid-cols-5",
courts.length >= 6 && "grid-cols-6"
)} )}
style={courts.length >= 5 ? { minWidth: `${courts.length * 150}px` } : undefined}
> >
{courts.map((court) => { {courts.map((court) => {
const courtAvail = availability.get(court.id); const courtAvail = availability.get(court.id);
@@ -347,7 +351,7 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
className="border-r border-primary-200 last:border-r-0 p-2" className="border-r border-primary-200 last:border-r-0 p-2"
> >
<div className="rounded-md border border-primary-200 bg-primary-50 p-3 text-center text-xs text-primary-400"> <div className="rounded-md border border-primary-200 bg-primary-50 p-3 text-center text-xs text-primary-400">
No disponible Not available
</div> </div>
</div> </div>
); );
@@ -373,7 +377,7 @@ export function BookingCalendar({ siteId, onSlotClick }: BookingCalendarProps) {
{timeSlots.length === 0 && ( {timeSlots.length === 0 && (
<div className="p-6 text-center text-primary-500"> <div className="p-6 text-center text-primary-500">
<p>No hay horarios disponibles para este día.</p> <p>No time slots available for this day.</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -104,12 +104,12 @@ export function BookingDialog({
try { try {
const response = await fetch(`/api/bookings/${slot.bookingId}`); const response = await fetch(`/api/bookings/${slot.bookingId}`);
if (!response.ok) { if (!response.ok) {
throw new Error("Error al cargar la reserva"); throw new Error("Error loading booking");
} }
const data = await response.json(); const data = await response.json();
setBooking(data); setBooking(data);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Error desconocido"); setError(err instanceof Error ? err.message : "Unknown error");
} finally { } finally {
setLoadingBookingInfo(false); setLoadingBookingInfo(false);
} }
@@ -128,7 +128,7 @@ export function BookingDialog({
`/api/clients?search=${encodeURIComponent(search)}&limit=10` `/api/clients?search=${encodeURIComponent(search)}&limit=10`
); );
if (!response.ok) { if (!response.ok) {
throw new Error("Error al buscar clientes"); throw new Error("Error searching players");
} }
const data: ClientsResponse = await response.json(); const data: ClientsResponse = await response.json();
setClients(data.data); setClients(data.data);
@@ -184,13 +184,13 @@ export function BookingDialog({
if (!response.ok) { if (!response.ok) {
const data = await response.json(); const data = await response.json();
throw new Error(data.error || "Error al crear la reserva"); throw new Error(data.error || "Error creating booking");
} }
onBookingCreated?.(); onBookingCreated?.();
onClose(); onClose();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Error al crear la reserva"); setError(err instanceof Error ? err.message : "Error creating booking");
} finally { } finally {
setCreatingBooking(false); setCreatingBooking(false);
} }
@@ -210,20 +210,20 @@ export function BookingDialog({
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
cancelReason: "Cancelada por el administrador", cancelReason: "Cancelled by administrator",
}), }),
}); });
if (!response.ok) { if (!response.ok) {
const data = await response.json(); const data = await response.json();
throw new Error(data.error || "Error al cancelar la reserva"); throw new Error(data.error || "Error cancelling booking");
} }
onBookingCancelled?.(); onBookingCancelled?.();
onClose(); onClose();
} catch (err) { } catch (err) {
setError( setError(
err instanceof Error ? err.message : "Error al cancelar la reserva" err instanceof Error ? err.message : "Error cancelling booking"
); );
} finally { } finally {
setCancellingBooking(false); setCancellingBooking(false);
@@ -246,7 +246,7 @@ export function BookingDialog({
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg"> <CardTitle className="text-lg">
{slot.available ? "Nueva Reserva" : "Detalle de Reserva"} {slot.available ? "New Booking" : "Booking Details"}
</CardTitle> </CardTitle>
<button <button
onClick={onClose} onClick={onClose}
@@ -269,16 +269,16 @@ export function BookingDialog({
</div> </div>
<div className="text-sm text-primary-600 space-y-1 mt-2"> <div className="text-sm text-primary-600 space-y-1 mt-2">
<p> <p>
<span className="font-medium">Cancha:</span> {slot.courtName} <span className="font-medium">Court:</span> {slot.courtName}
</p> </p>
<p> <p>
<span className="font-medium">Fecha:</span> {formatDate(date)} <span className="font-medium">Date:</span> {formatDate(date)}
</p> </p>
<p> <p>
<span className="font-medium">Hora:</span> {formatTime(slotDate)} <span className="font-medium">Time:</span> {formatTime(slotDate)}
</p> </p>
<p> <p>
<span className="font-medium">Precio:</span>{" "} <span className="font-medium">Price:</span>{" "}
{formatCurrency(slot.price)} {formatCurrency(slot.price)}
</p> </p>
</div> </div>
@@ -296,11 +296,11 @@ export function BookingDialog({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-2"> <label className="block text-sm font-medium text-primary-700 mb-2">
Buscar Cliente Search Player
</label> </label>
<Input <Input
type="text" type="text"
placeholder="Nombre, email o telefono..." placeholder="Name, email or phone..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
autoFocus autoFocus
@@ -317,7 +317,7 @@ export function BookingDialog({
{!loadingClients && searchQuery.length >= 2 && clients.length === 0 && ( {!loadingClients && searchQuery.length >= 2 && clients.length === 0 && (
<p className="text-sm text-primary-500 text-center py-4"> <p className="text-sm text-primary-500 text-center py-4">
No se encontraron clientes. No players found.
</p> </p>
)} )}
@@ -341,7 +341,7 @@ export function BookingDialog({
{client.firstName} {client.lastName} {client.firstName} {client.lastName}
</p> </p>
<p className="text-xs text-primary-500"> <p className="text-xs text-primary-500">
{client.email || client.phone || "Sin contacto"} {client.email || client.phone || "No contact"}
</p> </p>
</div> </div>
{client.memberships.length > 0 && ( {client.memberships.length > 0 && (
@@ -358,18 +358,18 @@ export function BookingDialog({
{selectedClient && ( {selectedClient && (
<div className="mt-4 rounded-md border border-accent-200 bg-accent-50 p-3"> <div className="mt-4 rounded-md border border-accent-200 bg-accent-50 p-3">
<p className="text-sm font-medium text-accent-800"> <p className="text-sm font-medium text-accent-800">
Cliente seleccionado: Selected player:
</p> </p>
<p className="text-sm text-accent-700"> <p className="text-sm text-accent-700">
{selectedClient.firstName} {selectedClient.lastName} {selectedClient.firstName} {selectedClient.lastName}
</p> </p>
{selectedClient.memberships.length > 0 && ( {selectedClient.memberships.length > 0 && (
<p className="text-xs text-accent-600 mt-1"> <p className="text-xs text-accent-600 mt-1">
Membresia: {selectedClient.memberships[0].plan.name} Membership: {selectedClient.memberships[0].plan.name}
{selectedClient.memberships[0].remainingHours !== null && {selectedClient.memberships[0].remainingHours !== null &&
selectedClient.memberships[0].remainingHours > 0 && ( selectedClient.memberships[0].remainingHours > 0 && (
<span className="ml-2"> <span className="ml-2">
({selectedClient.memberships[0].remainingHours}h restantes) ({selectedClient.memberships[0].remainingHours}h remaining)
</span> </span>
)} )}
</p> </p>
@@ -392,7 +392,7 @@ export function BookingDialog({
<div className="space-y-4"> <div className="space-y-4">
<div className="rounded-md border border-primary-200 bg-primary-50 p-4 space-y-3"> <div className="rounded-md border border-primary-200 bg-primary-50 p-4 space-y-3">
<div> <div>
<p className="text-xs text-primary-500">Cliente</p> <p className="text-xs text-primary-500">Player</p>
<p className="font-medium text-primary-800"> <p className="font-medium text-primary-800">
{booking.client.firstName} {booking.client.lastName} {booking.client.firstName} {booking.client.lastName}
</p> </p>
@@ -410,7 +410,7 @@ export function BookingDialog({
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<p className="text-xs text-primary-500">Estado</p> <p className="text-xs text-primary-500">Status</p>
<p <p
className={cn( className={cn(
"text-sm font-medium", "text-sm font-medium",
@@ -419,21 +419,21 @@ export function BookingDialog({
booking.status === "CANCELLED" && "text-red-600" booking.status === "CANCELLED" && "text-red-600"
)} )}
> >
{booking.status === "CONFIRMED" && "Confirmada"} {booking.status === "CONFIRMED" && "Confirmed"}
{booking.status === "PENDING" && "Pendiente"} {booking.status === "PENDING" && "Pending"}
{booking.status === "CANCELLED" && "Cancelada"} {booking.status === "CANCELLED" && "Cancelled"}
{booking.status === "COMPLETED" && "Completada"} {booking.status === "COMPLETED" && "Completed"}
{booking.status === "NO_SHOW" && "No asistio"} {booking.status === "NO_SHOW" && "No Show"}
</p> </p>
</div> </div>
<div> <div>
<p className="text-xs text-primary-500">Tipo de Pago</p> <p className="text-xs text-primary-500">Payment Type</p>
<p className="text-sm text-primary-800"> <p className="text-sm text-primary-800">
{booking.paymentType === "CASH" && "Efectivo"} {booking.paymentType === "CASH" && "Cash"}
{booking.paymentType === "CARD" && "Tarjeta"} {booking.paymentType === "CARD" && "Card"}
{booking.paymentType === "TRANSFER" && "Transferencia"} {booking.paymentType === "TRANSFER" && "Transfer"}
{booking.paymentType === "MEMBERSHIP" && "Membresia"} {booking.paymentType === "MEMBERSHIP" && "Membership"}
{booking.paymentType === "FREE" && "Gratuito"} {booking.paymentType === "FREE" && "Free"}
</p> </p>
</div> </div>
</div> </div>
@@ -450,7 +450,7 @@ export function BookingDialog({
{!loadingBookingInfo && !booking && ( {!loadingBookingInfo && !booking && (
<div className="text-center py-4 text-primary-500"> <div className="text-center py-4 text-primary-500">
<p>No se pudo cargar la informacion de la reserva.</p> <p>Could not load booking information.</p>
</div> </div>
)} )}
</div> </div>
@@ -460,7 +460,7 @@ export function BookingDialog({
<CardFooter className="border-t border-primary-200 bg-primary-50 pt-4"> <CardFooter className="border-t border-primary-200 bg-primary-50 pt-4">
<div className="flex w-full gap-3"> <div className="flex w-full gap-3">
<Button variant="outline" onClick={onClose} className="flex-1"> <Button variant="outline" onClick={onClose} className="flex-1">
Cerrar Close
</Button> </Button>
{slot.available && ( {slot.available && (
@@ -473,10 +473,10 @@ export function BookingDialog({
{creatingBooking ? ( {creatingBooking ? (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
Creando... Creating...
</span> </span>
) : ( ) : (
"Crear Reserva" "Create Booking"
)} )}
</Button> </Button>
)} )}
@@ -491,10 +491,10 @@ export function BookingDialog({
{cancellingBooking ? ( {cancellingBooking ? (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
Cancelando... Cancelling...
</span> </span>
) : ( ) : (
"Cancelar Reserva" "Cancel Booking"
)} )}
</Button> </Button>
)} )}

View File

@@ -39,7 +39,7 @@ export function OccupancyChart({ data, isLoading = false }: OccupancyChartProps)
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/> />
</svg> </svg>
Ocupacion de Canchas Court Occupancy
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -57,7 +57,7 @@ export function OccupancyChart({ data, isLoading = false }: OccupancyChartProps)
d="M20 12H4M12 20V4" d="M20 12H4M12 20V4"
/> />
</svg> </svg>
<p className="text-sm">No hay canchas configuradas</p> <p className="text-sm">No courts configured</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -89,7 +89,7 @@ export function OccupancyChart({ data, isLoading = false }: OccupancyChartProps)
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/> />
</svg> </svg>
Ocupacion de Canchas Court Occupancy
</CardTitle> </CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
@@ -147,10 +147,10 @@ export function OccupancyChart({ data, isLoading = false }: OccupancyChartProps)
: "text-primary-500" : "text-primary-500"
)} )}
> >
{court.occupancyPercent}% ocupado {court.occupancyPercent}% booked
</span> </span>
<span className="text-xs text-green-600"> <span className="text-xs text-green-600">
{court.availableHours - court.bookedHours}h disponible {court.availableHours - court.bookedHours}h available
</span> </span>
</div> </div>
</div> </div>
@@ -161,11 +161,11 @@ export function OccupancyChart({ data, isLoading = false }: OccupancyChartProps)
<div className="flex items-center justify-center gap-6 mt-6 pt-4 border-t border-primary-100"> <div className="flex items-center justify-center gap-6 mt-6 pt-4 border-t border-primary-100">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-blue-400"></div> <div className="w-3 h-3 rounded-full bg-blue-400"></div>
<span className="text-xs text-primary-500">Ocupado</span> <span className="text-xs text-primary-500">Booked</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-100"></div> <div className="w-3 h-3 rounded-full bg-green-100"></div>
<span className="text-xs text-primary-500">Disponible</span> <span className="text-xs text-primary-500">Available</span>
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@@ -14,7 +14,7 @@ interface QuickAction {
const quickActions: QuickAction[] = [ const quickActions: QuickAction[] = [
{ {
label: "Nueva Reserva", label: "New Booking",
href: "/bookings", href: "/bookings",
icon: ( icon: (
<svg <svg
@@ -32,10 +32,10 @@ const quickActions: QuickAction[] = [
</svg> </svg>
), ),
color: "bg-blue-500 hover:bg-blue-600", color: "bg-blue-500 hover:bg-blue-600",
description: "Crear una nueva reserva de cancha", description: "Create a new court booking",
}, },
{ {
label: "Abrir Caja", label: "Open Register",
href: "/pos", href: "/pos",
icon: ( icon: (
<svg <svg
@@ -53,10 +53,10 @@ const quickActions: QuickAction[] = [
</svg> </svg>
), ),
color: "bg-green-500 hover:bg-green-600", color: "bg-green-500 hover:bg-green-600",
description: "Iniciar turno de caja registradora", description: "Start cash register shift",
}, },
{ {
label: "Nueva Venta", label: "New Sale",
href: "/pos", href: "/pos",
icon: ( icon: (
<svg <svg
@@ -74,10 +74,10 @@ const quickActions: QuickAction[] = [
</svg> </svg>
), ),
color: "bg-purple-500 hover:bg-purple-600", color: "bg-purple-500 hover:bg-purple-600",
description: "Registrar venta en el punto de venta", description: "Record a point of sale transaction",
}, },
{ {
label: "Registrar Cliente", label: "Register Player",
href: "/clients", href: "/clients",
icon: ( icon: (
<svg <svg
@@ -95,7 +95,7 @@ const quickActions: QuickAction[] = [
</svg> </svg>
), ),
color: "bg-orange-500 hover:bg-orange-600", color: "bg-orange-500 hover:bg-orange-600",
description: "Agregar un nuevo cliente al sistema", description: "Add a new player to the system",
}, },
]; ];
@@ -117,7 +117,7 @@ export function QuickActions() {
d="M13 10V3L4 14h7v7l9-11h-7z" d="M13 10V3L4 14h7v7l9-11h-7z"
/> />
</svg> </svg>
Acciones Rapidas Quick Actions
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>

View File

@@ -27,23 +27,23 @@ interface RecentBookingsProps {
const statusConfig: Record<string, { label: string; className: string }> = { const statusConfig: Record<string, { label: string; className: string }> = {
PENDING: { PENDING: {
label: "Pendiente", label: "Pending",
className: "bg-yellow-100 text-yellow-700", className: "bg-yellow-100 text-yellow-700",
}, },
CONFIRMED: { CONFIRMED: {
label: "Confirmada", label: "Confirmed",
className: "bg-blue-100 text-blue-700", className: "bg-blue-100 text-blue-700",
}, },
COMPLETED: { COMPLETED: {
label: "Completada", label: "Completed",
className: "bg-green-100 text-green-700", className: "bg-green-100 text-green-700",
}, },
CANCELLED: { CANCELLED: {
label: "Cancelada", label: "Cancelled",
className: "bg-red-100 text-red-700", className: "bg-red-100 text-red-700",
}, },
NO_SHOW: { NO_SHOW: {
label: "No asistio", label: "No Show",
className: "bg-gray-100 text-gray-700", className: "bg-gray-100 text-gray-700",
}, },
}; };
@@ -71,11 +71,11 @@ export function RecentBookings({ bookings, isLoading = false }: RecentBookingsPr
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/> />
</svg> </svg>
Reservas de Hoy Today's Bookings
</CardTitle> </CardTitle>
<Link href="/bookings"> <Link href="/bookings">
<Button variant="ghost" size="sm" className="text-sm"> <Button variant="ghost" size="sm" className="text-sm">
Ver todas View all
<svg <svg
className="w-4 h-4 ml-1" className="w-4 h-4 ml-1"
fill="none" fill="none"
@@ -109,7 +109,7 @@ export function RecentBookings({ bookings, isLoading = false }: RecentBookingsPr
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/> />
</svg> </svg>
<p className="text-sm">No hay reservas para hoy</p> <p className="text-sm">No bookings for today</p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
@@ -139,7 +139,7 @@ export function RecentBookings({ bookings, isLoading = false }: RecentBookingsPr
{/* Details */} {/* Details */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-primary-800 truncate"> <p className="text-sm font-medium text-primary-800 truncate">
{booking.client?.name || "Sin cliente"} {booking.client?.name || "Walk-in"}
</p> </p>
<p className="text-xs text-primary-500 truncate"> <p className="text-xs text-primary-500 truncate">
{booking.court.name} {booking.court.name}

View File

@@ -98,7 +98,7 @@ export function StatCard({ title, value, icon, trend, color = "primary" }: StatC
{trend.isPositive ? "+" : ""} {trend.isPositive ? "+" : ""}
{trend.value}% {trend.value}%
</span> </span>
<span className="text-xs text-primary-400 ml-1">vs ayer</span> <span className="text-xs text-primary-400 ml-1">vs yesterday</span>
</div> </div>
)} )}
</div> </div>

View File

@@ -26,10 +26,10 @@ export function Header() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="text-right"> <div className="text-right">
<p className="text-sm font-medium text-primary-800">{session?.user?.name || 'Usuario'}</p> <p className="text-sm font-medium text-primary-800">{session?.user?.name || 'User'}</p>
<p className="text-xs text-primary-500">{displayRole}</p> <p className="text-xs text-primary-500">{displayRole}</p>
</div> </div>
<Button variant="ghost" size="icon" onClick={handleLogout} title="Cerrar sesión"> <Button variant="ghost" size="icon" onClick={handleLogout} title="Log out">
<LogOut className="h-5 w-5" /> <LogOut className="h-5 w-5" />
</Button> </Button>
</div> </div>

View File

@@ -5,8 +5,6 @@ import { usePathname } from 'next/navigation';
import { import {
LayoutDashboard, LayoutDashboard,
Calendar, Calendar,
Trophy,
ShoppingCart,
Users, Users,
CreditCard, CreditCard,
BarChart3, BarChart3,
@@ -22,13 +20,11 @@ interface NavItem {
const navItems: NavItem[] = [ const navItems: NavItem[] = [
{ label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, { label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
{ label: 'Reservas', href: '/bookings', icon: Calendar }, { label: 'Bookings', href: '/bookings', icon: Calendar },
{ label: 'Torneos', href: '/tournaments', icon: Trophy }, { label: 'Players', href: '/clients', icon: Users },
{ label: 'Ventas', href: '/pos', icon: ShoppingCart }, { label: 'Memberships', href: '/memberships', icon: CreditCard },
{ label: 'Clientes', href: '/clients', icon: Users }, { label: 'Reports', href: '/reports', icon: BarChart3 },
{ label: 'Membresías', href: '/memberships', icon: CreditCard }, { label: 'Settings', href: '/settings', icon: Settings },
{ label: 'Reportes', href: '/reports', icon: BarChart3 },
{ label: 'Configuración', href: '/settings', icon: Settings },
]; ];
export function Sidebar() { export function Sidebar() {
@@ -38,7 +34,7 @@ export function Sidebar() {
<aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r border-primary-200 bg-white"> <aside className="fixed left-0 top-0 z-40 h-screen w-64 border-r border-primary-200 bg-white">
{/* Logo Section */} {/* Logo Section */}
<div className="flex h-16 items-center gap-3 border-b border-primary-200 px-6"> <div className="flex h-16 items-center gap-3 border-b border-primary-200 px-6">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-500"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary">
<svg viewBox="0 0 40 40" className="w-7 h-7 text-white" fill="none"> <svg viewBox="0 0 40 40" className="w-7 h-7 text-white" fill="none">
{/* Lightning bolt / smash icon */} {/* Lightning bolt / smash icon */}
<path d="M22 4L8 22h10l-4 14L28 18H18l4-14z" fill="currentColor" /> <path d="M22 4L8 22h10l-4 14L28 18H18l4-14z" fill="currentColor" />
@@ -48,7 +44,7 @@ export function Sidebar() {
<circle cx="30" cy="4" r="1" fill="currentColor" opacity="0.5" /> <circle cx="30" cy="4" r="1" fill="currentColor" opacity="0.5" />
</svg> </svg>
</div> </div>
<span className="text-xl font-semibold text-primary-800">SmashPoint</span> <span className="text-xl font-semibold text-primary-800">Cabo Pickleball</span>
</div> </div>
{/* Navigation */} {/* Navigation */}

View File

@@ -57,7 +57,7 @@ export function PlanCard({ plan, onEdit, onDelete, isAdmin = false }: PlanCardPr
? "bg-accent-100 text-accent-700" ? "bg-accent-100 text-accent-700"
: "bg-primary-100 text-primary-600" : "bg-primary-100 text-primary-600"
)}> )}>
{plan.subscriberCount} {plan.subscriberCount === 1 ? "suscriptor" : "suscriptores"} {plan.subscriberCount} {plan.subscriberCount === 1 ? "subscriber" : "subscribers"}
</span> </span>
</div> </div>
{plan.description && ( {plan.description && (
@@ -72,7 +72,7 @@ export function PlanCard({ plan, onEdit, onDelete, isAdmin = false }: PlanCardPr
{formatCurrency(price)} {formatCurrency(price)}
</div> </div>
<div className="text-sm text-primary-500"> <div className="text-sm text-primary-500">
/{plan.durationMonths} {plan.durationMonths === 1 ? "mes" : "meses"} /{plan.durationMonths} {plan.durationMonths === 1 ? "month" : "months"}
</div> </div>
</div> </div>
@@ -87,8 +87,8 @@ export function PlanCard({ plan, onEdit, onDelete, isAdmin = false }: PlanCardPr
</svg> </svg>
</div> </div>
<div> <div>
<p className="font-medium text-primary-800">{freeHours} horas gratis</p> <p className="font-medium text-primary-800">{freeHours} free hours</p>
<p className="text-xs text-primary-500">de cancha al mes</p> <p className="text-xs text-primary-500">of court time per month</p>
</div> </div>
</div> </div>
)} )}
@@ -102,8 +102,8 @@ export function PlanCard({ plan, onEdit, onDelete, isAdmin = false }: PlanCardPr
</svg> </svg>
</div> </div>
<div> <div>
<p className="font-medium text-primary-800">{discountPercent}% descuento</p> <p className="font-medium text-primary-800">{discountPercent}% discount</p>
<p className="text-xs text-primary-500">en reservas adicionales</p> <p className="text-xs text-primary-500">on additional bookings</p>
</div> </div>
</div> </div>
)} )}
@@ -117,8 +117,8 @@ export function PlanCard({ plan, onEdit, onDelete, isAdmin = false }: PlanCardPr
</svg> </svg>
</div> </div>
<div> <div>
<p className="font-medium text-primary-800">{storeDiscount}% descuento</p> <p className="font-medium text-primary-800">{storeDiscount}% discount</p>
<p className="text-xs text-primary-500">en tienda</p> <p className="text-xs text-primary-500">in store</p>
</div> </div>
</div> </div>
)} )}
@@ -126,7 +126,7 @@ export function PlanCard({ plan, onEdit, onDelete, isAdmin = false }: PlanCardPr
{/* Other Benefits */} {/* Other Benefits */}
{otherBenefits.length > 0 && ( {otherBenefits.length > 0 && (
<div className="pt-2 border-t border-primary-100"> <div className="pt-2 border-t border-primary-100">
<p className="text-xs font-medium text-primary-600 mb-2">Beneficios adicionales:</p> <p className="text-xs font-medium text-primary-600 mb-2">Additional benefits:</p>
<ul className="space-y-1"> <ul className="space-y-1">
{otherBenefits.map((benefit, index) => ( {otherBenefits.map((benefit, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-primary-700"> <li key={index} className="flex items-start gap-2 text-sm text-primary-700">
@@ -153,7 +153,7 @@ export function PlanCard({ plan, onEdit, onDelete, isAdmin = false }: PlanCardPr
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg> </svg>
Editar Edit
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -164,7 +164,7 @@ export function PlanCard({ plan, onEdit, onDelete, isAdmin = false }: PlanCardPr
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 mr-1" 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" /> <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> </svg>
Eliminar Delete
</Button> </Button>
</CardFooter> </CardFooter>
)} )}

View File

@@ -41,10 +41,10 @@ interface PlanFormProps {
} }
const durationOptions = [ const durationOptions = [
{ value: 1, label: "1 mes" }, { value: 1, label: "1 month" },
{ value: 3, label: "3 meses" }, { value: 3, label: "3 months" },
{ value: 6, label: "6 meses" }, { value: 6, label: "6 months" },
{ value: 12, label: "12 meses" }, { value: 12, label: "12 months" },
]; ];
export function PlanForm({ export function PlanForm({
@@ -107,19 +107,19 @@ export function PlanForm({
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
if (!formData.name.trim()) { if (!formData.name.trim()) {
newErrors.name = "El nombre es requerido"; newErrors.name = "Name is required";
} }
if (formData.price <= 0) { if (formData.price <= 0) {
newErrors.price = "El precio debe ser mayor a 0"; newErrors.price = "Price must be greater than 0";
} }
if (formData.bookingDiscount < 0 || formData.bookingDiscount > 100) { if (formData.bookingDiscount < 0 || formData.bookingDiscount > 100) {
newErrors.bookingDiscount = "El descuento debe estar entre 0 y 100"; newErrors.bookingDiscount = "Discount must be between 0 and 100";
} }
if (formData.storeDiscount < 0 || formData.storeDiscount > 100) { if (formData.storeDiscount < 0 || formData.storeDiscount > 100) {
newErrors.storeDiscount = "El descuento debe estar entre 0 y 100"; newErrors.storeDiscount = "Discount must be between 0 and 100";
} }
if (formData.freeHours < 0) { if (formData.freeHours < 0) {
newErrors.freeHours = "Las horas gratis no pueden ser negativas"; newErrors.freeHours = "Free hours cannot be negative";
} }
setErrors(newErrors); setErrors(newErrors);
@@ -137,20 +137,20 @@ export function PlanForm({
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
{mode === "create" ? "Nuevo Plan de Membresia" : "Editar Plan"} {mode === "create" ? "New Membership Plan" : "Edit Plan"}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Name */} {/* Name */}
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Nombre del Plan * Plan Name *
</label> </label>
<Input <Input
name="name" name="name"
value={formData.name} value={formData.name}
onChange={handleChange} onChange={handleChange}
placeholder="Ej: Plan Premium" placeholder="E.g.: Premium Plan"
className={errors.name ? "border-red-500" : ""} className={errors.name ? "border-red-500" : ""}
/> />
{errors.name && ( {errors.name && (
@@ -161,13 +161,13 @@ export function PlanForm({
{/* Description */} {/* Description */}
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Descripcion Description
</label> </label>
<textarea <textarea
name="description" name="description"
value={formData.description} value={formData.description}
onChange={handleChange} onChange={handleChange}
placeholder="Descripcion del plan..." placeholder="Plan description..."
rows={2} rows={2}
className="flex w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-primary-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2" className="flex w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-primary-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/> />
@@ -177,7 +177,7 @@ export function PlanForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Precio * Price *
</label> </label>
<Input <Input
type="number" type="number"
@@ -194,7 +194,7 @@ export function PlanForm({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Duracion Duration
</label> </label>
<select <select
name="durationMonths" name="durationMonths"
@@ -213,12 +213,12 @@ export function PlanForm({
{/* Benefits Section */} {/* Benefits Section */}
<div className="border-t border-primary-200 pt-4 mt-4"> <div className="border-t border-primary-200 pt-4 mt-4">
<h4 className="text-sm font-semibold text-primary-800 mb-3">Beneficios</h4> <h4 className="text-sm font-semibold text-primary-800 mb-3">Benefits</h4>
{/* Free Hours */} {/* Free Hours */}
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Horas Gratis de Cancha (por mes) Free Court Hours (per month)
</label> </label>
<Input <Input
type="number" type="number"
@@ -238,7 +238,7 @@ export function PlanForm({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Descuento en Reservas (%) Booking Discount (%)
</label> </label>
<Input <Input
type="number" type="number"
@@ -255,7 +255,7 @@ export function PlanForm({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Descuento en Tienda (%) Store Discount (%)
</label> </label>
<Input <Input
type="number" type="number"
@@ -275,36 +275,36 @@ export function PlanForm({
{/* Extra Benefits */} {/* Extra Benefits */}
<div> <div>
<label className="block text-sm font-medium text-primary-700 mb-1"> <label className="block text-sm font-medium text-primary-700 mb-1">
Beneficios Adicionales Additional Benefits
</label> </label>
<textarea <textarea
name="extraBenefits" name="extraBenefits"
value={formData.extraBenefits} value={formData.extraBenefits}
onChange={handleChange} onChange={handleChange}
placeholder="Un beneficio por linea&#10;Ej: Acceso a vestidores VIP&#10;Invitacion a eventos exclusivos" placeholder="One benefit per line&#10;E.g.: Access to VIP locker rooms&#10;Invitation to exclusive events"
rows={4} rows={4}
className="flex w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-primary-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2" className="flex w-full rounded-md border border-primary-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-primary-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2"
/> />
<p className="text-xs text-primary-500 mt-1"> <p className="text-xs text-primary-500 mt-1">
Escribe un beneficio por linea Write one benefit per line
</p> </p>
</div> </div>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex justify-end gap-3 border-t border-primary-200 bg-primary-50 pt-4"> <CardFooter className="flex justify-end gap-3 border-t border-primary-200 bg-primary-50 pt-4">
<Button type="button" variant="outline" onClick={onCancel}> <Button type="button" variant="outline" onClick={onCancel}>
Cancelar Cancel
</Button> </Button>
<Button type="submit" disabled={isLoading}> <Button type="submit" disabled={isLoading}>
{isLoading ? ( {isLoading ? (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" /> <div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
Guardando... Saving...
</span> </span>
) : mode === "create" ? ( ) : mode === "create" ? (
"Crear Plan" "Create Plan"
) : ( ) : (
"Guardar Cambios" "Save Changes"
)} )}
</Button> </Button>
</CardFooter> </CardFooter>

View File

@@ -40,12 +40,12 @@ async function main() {
const organization = await prisma.organization.create({ const organization = await prisma.organization.create({
data: { data: {
name: 'SmashPoint Demo', name: 'Cabo Pickleball Club',
slug: 'smashpoint-demo', slug: 'cabo-pickleball-club',
settings: { settings: {
currency: 'MXN', currency: 'MXN',
timezone: 'America/Mexico_City', timezone: 'America/Mazatlan',
language: 'es', language: 'en',
}, },
}, },
}); });
@@ -56,39 +56,19 @@ async function main() {
// ============================================================================= // =============================================================================
// SITES // SITES
// ============================================================================= // =============================================================================
console.log('Creating sites...'); console.log('Creating site...');
const sitesData = [ const sitesData = [
{ {
name: 'Sede Norte', name: 'Corridor Courts',
slug: 'sede-norte', slug: 'corridor-courts',
address: 'Av. Universidad 1000, Col. Del Valle', address: 'Corridor area, Cabo San Lucas, BCS',
phone: '+52 55 1234 5678', phone: '+52-624-151-5455',
email: 'norte@smashpoint.com', email: 'topdogcabo@yahoo.com',
timezone: 'America/Mexico_City', timezone: 'America/Mazatlan',
openTime: '07:00', openTime: '07:00',
closeTime: '23:00',
},
{
name: 'Sede Sur',
slug: 'sede-sur',
address: 'Av. Insurgentes 2000, Col. Roma',
phone: '+52 55 2345 6789',
email: 'sur@smashpoint.com',
timezone: 'America/Mexico_City',
openTime: '08:00',
closeTime: '22:00', closeTime: '22:00',
}, },
{
name: 'Sede Centro',
slug: 'sede-centro',
address: 'Calle Reforma 500, Centro Historico',
phone: '+52 55 3456 7890',
email: 'centro@smashpoint.com',
timezone: 'America/Mexico_City',
openTime: '06:00',
closeTime: '24:00',
},
]; ];
const sites = await Promise.all( const sites = await Promise.all(
@@ -107,44 +87,27 @@ async function main() {
console.log(''); console.log('');
// ============================================================================= // =============================================================================
// COURTS (2 per site) // COURTS (6 outdoor courts)
// ============================================================================= // =============================================================================
console.log('Creating courts...'); console.log('Creating courts...');
const courts: { id: string; name: string; siteId: string }[] = []; const courts: { id: string; name: string; siteId: string }[] = [];
for (const site of sites) { for (let i = 1; i <= 6; i++) {
const courtData = [
{
name: 'Cancha 1',
type: CourtType.INDOOR,
status: CourtStatus.AVAILABLE,
pricePerHour: 350,
description: 'Cancha techada con iluminacion LED',
features: ['Iluminacion LED', 'Techada', 'Cristal panoramico'],
displayOrder: 1,
},
{
name: 'Cancha 2',
type: CourtType.INDOOR,
status: CourtStatus.AVAILABLE,
pricePerHour: 450,
description: 'Cancha premium con aire acondicionado',
features: ['Iluminacion LED', 'Techada', 'Aire acondicionado', 'Cristal panoramico', 'Premium'],
displayOrder: 2,
},
];
for (const court of courtData) {
const created = await prisma.court.create({ const created = await prisma.court.create({
data: { data: {
siteId: site.id, siteId: sites[0].id,
...court, name: `Court ${i}`,
type: CourtType.OUTDOOR,
status: CourtStatus.AVAILABLE,
pricePerHour: 300,
description: 'Outdoor court with night lighting',
features: ['Night lighting', 'Court dividers'],
displayOrder: i,
}, },
}); });
courts.push(created); courts.push(created);
console.log(` Created court: ${site.name} - ${created.name}`); console.log(` Created court: ${created.name}`);
}
} }
console.log(''); console.log('');
@@ -152,17 +115,17 @@ async function main() {
// ============================================================================= // =============================================================================
// ADMIN USER (SUPER_ADMIN) // ADMIN USER (SUPER_ADMIN)
// ============================================================================= // =============================================================================
console.log('Creating admin users...'); console.log('Creating admin user...');
const hashedPassword = await bcrypt.hash('admin123', 10); const hashedPassword = await bcrypt.hash('Aasi940812', 10);
const adminUser = await prisma.user.create({ const adminUser = await prisma.user.create({
data: { data: {
organizationId: organization.id, organizationId: organization.id,
email: 'admin@smashpoint.com', email: 'ivan@horuxfin.com',
password: hashedPassword, password: hashedPassword,
firstName: 'Administrador', firstName: 'Ivan',
lastName: 'Sistema', lastName: 'Admin',
role: UserRole.SUPER_ADMIN, role: UserRole.SUPER_ADMIN,
phone: '+52 55 9999 0000', phone: '+52 55 9999 0000',
siteIds: sites.map(s => s.id), siteIds: sites.map(s => s.id),
@@ -171,41 +134,6 @@ async function main() {
console.log(` Created super admin: ${adminUser.email}`); console.log(` Created super admin: ${adminUser.email}`);
// =============================================================================
// SITE ADMINS (one per site)
// =============================================================================
const siteAdminsData = [
{ email: 'norte@smashpoint.com', firstName: 'Carlos', lastName: 'Rodriguez', site: sites[0] },
{ email: 'sur@smashpoint.com', firstName: 'Maria', lastName: 'Gonzalez', site: sites[1] },
{ email: 'centro@smashpoint.com', firstName: 'Luis', lastName: 'Hernandez', site: sites[2] },
];
for (const adminData of siteAdminsData) {
const siteAdmin = await prisma.user.create({
data: {
organizationId: organization.id,
email: adminData.email,
password: hashedPassword,
firstName: adminData.firstName,
lastName: adminData.lastName,
role: UserRole.SITE_ADMIN,
siteIds: [adminData.site.id],
},
});
// Connect user to site
await prisma.site.update({
where: { id: adminData.site.id },
data: {
users: {
connect: { id: siteAdmin.id },
},
},
});
console.log(` Created site admin: ${siteAdmin.email} (${adminData.site.name})`);
}
console.log(''); console.log('');
// ============================================================================= // =============================================================================
@@ -214,10 +142,10 @@ async function main() {
console.log('Creating product categories...'); console.log('Creating product categories...');
const categoriesData = [ const categoriesData = [
{ name: 'Bebidas', description: 'Bebidas y refrescos', displayOrder: 1 }, { name: 'Drinks', description: 'Beverages and refreshments', displayOrder: 1 },
{ name: 'Snacks', description: 'Botanas y snacks', displayOrder: 2 }, { name: 'Snacks', description: 'Snacks and light food', displayOrder: 2 },
{ name: 'Equipamiento', description: 'Equipo y accesorios de padel', displayOrder: 3 }, { name: 'Equipment', description: 'Pickleball equipment and accessories', displayOrder: 3 },
{ name: 'Alquiler', description: 'Articulos en renta', displayOrder: 4 }, { name: 'Rental', description: 'Rental items', displayOrder: 4 },
]; ];
const categories: { id: string; name: string }[] = []; const categories: { id: string; name: string }[] = [];
@@ -236,28 +164,24 @@ async function main() {
console.log(''); console.log('');
// ============================================================================= // =============================================================================
// PRODUCTS (for organization, shown in Sede Norte initially) // PRODUCTS
// ============================================================================= // =============================================================================
console.log('Creating products...'); console.log('Creating products...');
const bebidasCategory = categories.find(c => c.name === 'Bebidas'); const drinksCategory = categories.find(c => c.name === 'Drinks');
const snacksCategory = categories.find(c => c.name === 'Snacks'); const snacksCategory = categories.find(c => c.name === 'Snacks');
const equipamientoCategory = categories.find(c => c.name === 'Equipamiento'); const equipmentCategory = categories.find(c => c.name === 'Equipment');
const alquilerCategory = categories.find(c => c.name === 'Alquiler'); const rentalCategory = categories.find(c => c.name === 'Rental');
const productsData = [ const productsData = [
// Bebidas { name: 'Water', description: 'Natural water 600ml', price: 20, costPrice: 8, stock: 100, categoryId: drinksCategory?.id, sku: 'DRK-001' },
{ name: 'Agua', description: 'Agua natural 600ml', price: 20, costPrice: 8, stock: 100, categoryId: bebidasCategory?.id, sku: 'BEB-001' }, { name: 'Gatorade', description: 'Sports drink 500ml', price: 35, costPrice: 18, stock: 50, categoryId: drinksCategory?.id, sku: 'DRK-002' },
{ name: 'Gatorade', description: 'Bebida deportiva 500ml', price: 35, costPrice: 18, stock: 50, categoryId: bebidasCategory?.id, sku: 'BEB-002' }, { name: 'Beer', description: 'Craft beer 355ml', price: 45, costPrice: 22, stock: 48, categoryId: drinksCategory?.id, sku: 'DRK-003' },
{ name: 'Cerveza', description: 'Cerveza artesanal 355ml', price: 45, costPrice: 22, stock: 48, categoryId: bebidasCategory?.id, sku: 'BEB-003' }, { name: 'Chips', description: 'Potato chips 45g', price: 25, costPrice: 12, stock: 30, categoryId: snacksCategory?.id, sku: 'SNK-001' },
// Snacks { name: 'Energy Bar', description: 'Protein bar 50g', price: 30, costPrice: 15, stock: 25, categoryId: snacksCategory?.id, sku: 'SNK-002' },
{ name: 'Papas', description: 'Papas fritas 45g', price: 25, costPrice: 12, stock: 30, categoryId: snacksCategory?.id, sku: 'SNK-001' }, { name: 'Pickleballs', description: 'Franklin X-40 Outdoor (3 pack)', price: 180, costPrice: 90, stock: 20, categoryId: equipmentCategory?.id, sku: 'EQP-001' },
{ name: 'Barra energetica', description: 'Barra de proteina 50g', price: 30, costPrice: 15, stock: 25, categoryId: snacksCategory?.id, sku: 'SNK-002' }, { name: 'Paddle Grip', description: 'Replacement grip', price: 50, costPrice: 25, stock: 40, categoryId: equipmentCategory?.id, sku: 'EQP-002' },
// Equipamiento { name: 'Paddle Rental', description: 'Pickleball paddle rental (per session)', price: 100, costPrice: 0, stock: 10, categoryId: rentalCategory?.id, sku: 'RNT-001', trackStock: false },
{ name: 'Pelotas HEAD', description: 'Tubo de 3 pelotas HEAD Pro', price: 180, costPrice: 90, stock: 20, categoryId: equipamientoCategory?.id, sku: 'EQP-001' },
{ name: 'Grip', description: 'Overgrip Wilson Pro', price: 50, costPrice: 25, stock: 40, categoryId: equipamientoCategory?.id, sku: 'EQP-002' },
// Alquiler
{ name: 'Raqueta alquiler', description: 'Raqueta de padel (por hora)', price: 100, costPrice: 0, stock: 10, categoryId: alquilerCategory?.id, sku: 'ALQ-001', trackStock: false },
]; ];
for (const productData of productsData) { for (const productData of productsData) {
@@ -279,31 +203,49 @@ async function main() {
const membershipPlansData = [ const membershipPlansData = [
{ {
name: 'Basico', name: 'Day Pass',
description: 'Plan basico mensual con beneficios esenciales', description: 'Single day access to all courts',
price: 499, price: 300,
durationMonths: 1, durationMonths: 1,
courtHours: 2, courtHours: 0,
discountPercent: 10, discountPercent: 0,
benefits: ['2 horas gratis de cancha al mes', '10% descuento en reservas', '5% descuento en tienda'], benefits: ['Full day access', 'All courts', 'Night play included'],
}, },
{ {
name: 'Premium', name: '10-Day Pass',
description: 'Plan premium con mayores beneficios', description: '10 visits, any time of day',
price: 899, price: 2500,
durationMonths: 1, durationMonths: 3,
courtHours: 5,
discountPercent: 20,
benefits: ['5 horas gratis de cancha al mes', '20% descuento en reservas', '10% descuento en tienda', 'Acceso prioritario a torneos'],
},
{
name: 'VIP',
description: 'Plan VIP con todos los beneficios',
price: 1499,
durationMonths: 1,
courtHours: 10, courtHours: 10,
discountPercent: 15,
benefits: ['10 day passes', 'Valid any time', 'Save vs single day pass'],
},
{
name: '10-Morning Pass',
description: '10 morning sessions (7am-12pm)',
price: 2000,
durationMonths: 3,
courtHours: 10,
discountPercent: 10,
benefits: ['10 morning passes', '7:00 AM - 12:00 PM only', 'Best value for morning players'],
},
{
name: 'Monthly Individual',
description: 'Unlimited monthly access for one player',
price: 4000,
durationMonths: 1,
courtHours: 30,
discountPercent: 25,
benefits: ['Unlimited court access', 'Priority booking', 'All time slots'],
},
{
name: 'Monthly Family',
description: 'Unlimited monthly access for up to 4 family members',
price: 6500,
durationMonths: 1,
courtHours: 60,
discountPercent: 30, discountPercent: 30,
benefits: ['10 horas gratis de cancha al mes', '30% descuento en reservas', '15% descuento en tienda', 'Acceso prioritario a torneos', 'Invitados con descuento', 'Casillero incluido'], benefits: ['Up to 4 family members', 'Unlimited court access', 'Priority booking', 'All time slots'],
}, },
]; ];
@@ -317,7 +259,7 @@ async function main() {
}, },
}); });
membershipPlans.push(plan); membershipPlans.push(plan);
console.log(` Created membership plan: ${plan.name} - $${plan.price}/mes`); console.log(` Created membership plan: ${plan.name} - $${plan.price}`);
} }
console.log(''); console.log('');
@@ -386,31 +328,31 @@ async function main() {
console.log(''); console.log('');
// ============================================================================= // =============================================================================
// MEMBERSHIP FOR ONE CLIENT (Maria Garcia with Premium) // MEMBERSHIP FOR ONE CLIENT (Maria Garcia with Monthly Individual)
// ============================================================================= // =============================================================================
console.log('Creating sample membership...'); console.log('Creating sample membership...');
const premiumPlan = membershipPlans.find(p => p.name === 'Premium'); const monthlyPlan = membershipPlans.find(p => p.name === 'Monthly Individual');
const mariaClient = clients.find(c => c.firstName === 'Maria'); const mariaClient = clients.find(c => c.firstName === 'Maria');
if (premiumPlan && mariaClient) { if (monthlyPlan && mariaClient) {
const startDate = new Date(); const startDate = new Date();
const endDate = new Date(); const endDate = new Date();
endDate.setMonth(endDate.getMonth() + 1); endDate.setMonth(endDate.getMonth() + 1);
const membership = await prisma.membership.create({ const membership = await prisma.membership.create({
data: { data: {
planId: premiumPlan.id, planId: monthlyPlan.id,
clientId: mariaClient.id, clientId: mariaClient.id,
startDate, startDate,
endDate, endDate,
status: MembershipStatus.ACTIVE, status: MembershipStatus.ACTIVE,
remainingHours: premiumPlan.courtHours, remainingHours: monthlyPlan.courtHours,
autoRenew: true, autoRenew: true,
}, },
}); });
console.log(` Created Premium membership for ${mariaClient.firstName} ${mariaClient.lastName}`); console.log(` Created Monthly Individual membership for ${mariaClient.firstName} ${mariaClient.lastName}`);
} }
console.log(''); console.log('');
@@ -424,9 +366,9 @@ async function main() {
console.log(''); console.log('');
console.log('Summary:'); console.log('Summary:');
console.log(` - 1 Organization: ${organization.name}`); console.log(` - 1 Organization: ${organization.name}`);
console.log(` - ${sites.length} Sites`); console.log(` - ${sites.length} Site`);
console.log(` - ${courts.length} Courts (${courts.length / sites.length} per site)`); console.log(` - ${courts.length} Courts`);
console.log(` - 4 Users (1 super admin + 3 site admins)`); console.log(` - 1 Admin user`);
console.log(` - ${categories.length} Product Categories`); console.log(` - ${categories.length} Product Categories`);
console.log(` - ${productsData.length} Products`); console.log(` - ${productsData.length} Products`);
console.log(` - ${membershipPlans.length} Membership Plans`); console.log(` - ${membershipPlans.length} Membership Plans`);
@@ -434,8 +376,7 @@ async function main() {
console.log(` - 1 Active Membership`); console.log(` - 1 Active Membership`);
console.log(''); console.log('');
console.log('Login credentials:'); console.log('Login credentials:');
console.log(' Super Admin: admin@smashpoint.com / admin123'); console.log(' Admin: ivan@horuxfin.com / Aasi940812');
console.log(' Site Admins: norte@smashpoint.com, sur@smashpoint.com, centro@smashpoint.com / admin123');
console.log(''); console.log('');
} }

View File

@@ -10,30 +10,30 @@ const config: Config = {
extend: { extend: {
colors: { colors: {
primary: { primary: {
50: "#E6EBF2", 50: "#E8F4FD",
100: "#C2D1E3", 100: "#C5E3FA",
200: "#9BB4D1", 200: "#9DCEF6",
300: "#7497BF", 300: "#75B9F2",
400: "#5781B2", 400: "#4DA4EE",
500: "#3A6BA5", 500: "#2990EA",
600: "#2E5A8E", 600: "#2177C8",
700: "#244977", 700: "#195DA6",
800: "#1E3A5F", 800: "#124484",
900: "#152A47", 900: "#0B2B62",
DEFAULT: "#1E3A5F", DEFAULT: "#2990EA",
}, },
accent: { accent: {
50: "#EEFBF3", 50: "#FEF7EC",
100: "#D4F5E0", 100: "#FDEACC",
200: "#A9EBBC", 200: "#FBD89D",
300: "#7EE19A", 300: "#F9C66E",
400: "#53D778", 400: "#F7B43F",
500: "#22C55E", 500: "#F59E0B",
600: "#1CA04C", 600: "#D48509",
700: "#167A3A", 700: "#A36807",
800: "#105528", 800: "#724A05",
900: "#0A2F16", 900: "#412B03",
DEFAULT: "#22C55E", DEFAULT: "#F59E0B",
}, },
}, },
fontFamily: { fontFamily: {

View File

@@ -0,0 +1,99 @@
# SmashPoint Adaptation: Cabo Pickleball Club - Design Document
## Overview
Adapt the SmashPoint padel club management system for Cabo Pickleball Club, a 6-court outdoor pickleball facility in the Corridor area of Cabo San Lucas, BCS, Mexico.
## Scope
### Branding & Visual Identity
- **Client name:** "Cabo Pickleball Club"
- **Platform:** "SmashPoint" (shown as "Powered by SmashPoint")
- **Login/landing:** "Cabo Pickleball Club" with "Powered by SmashPoint" subtitle
- **Sidebar:** "Cabo Pickleball" with SmashPoint logo in new blue
- **Browser tab:** "Cabo Pickleball Club | SmashPoint"
- **Primary color:** `#2990EA` (Cabo blue, replacing `#1E3A5F`)
- **Accent color:** `#F59E0B` (amber/golden, beach/sun vibe)
- **Font:** Inter (unchanged)
### Language: English Default
Direct string replacement (no i18n framework). All UI text from Spanish to English:
- Reservas → Bookings
- Canchas → Courts
- Clientes → Players
- Membresías → Memberships
- Reportes → Reports
- Configuración → Settings
- All form labels, buttons, error messages, tooltips → English
### Sport: Padel → Pickleball
- "padel" / "pádel" → "pickleball"
- "cancha" → "court"
- "raqueta" → "paddle"
- "pelotas" → "pickleballs"
- Court types INDOOR/OUTDOOR/COVERED remain valid for pickleball
### Features: Slim Down
**Keep & Adapt:**
- Dashboard (stats, occupancy, revenue)
- Bookings (court reservations, 300 MXN/person)
- Clients → renamed "Players"
- Memberships (day passes, multi-day passes, monthly plans)
- Reports (revenue, occupancy analytics)
- Settings (site/court configuration)
**Remove from navigation (hide, don't delete code):**
- Tournaments
- POS (Ventas)
### Seed Data
**Organization:**
- Name: Cabo Pickleball Club
- Slug: cabo-pickleball-club
- Currency: MXN
- Timezone: America/Mazatlan
**Site:**
- Name: Corridor Courts
- Address: Corridor area, Cabo San Lucas, BCS
- Hours: 07:00 - 22:00
- Phone: +52-624-151-5455
- Email: topdogcabo@yahoo.com
**Courts (6):**
- Court 1 through Court 6
- Type: OUTDOOR
- Price: 300 MXN per person
- Features: Night lighting, court dividers
**Membership Plans:**
| Plan | Price (MXN) | Details |
|------|------------|---------|
| Day Pass | 300 | Single day access |
| 10-Day Pass | 2,500 | 10 visits, any time |
| 10-Morning Pass | 2,000 | 10 morning sessions (7am-12pm) |
| Monthly Individual | 4,000 | Monthly unlimited |
| Monthly Family | 6,500 | Monthly unlimited, up to 4 members |
**Promotions (noted in plan descriptions):**
- Mon-Sat: -100 MXN off day pass
- Wednesday Ladies Day: -150 MXN off
**Admin Account:**
- Email: ivan@horuxfin.com
- Password: Aasi940812
- Role: SUPER_ADMIN
### Unchanged
- API routes structure
- Database schema (Prisma models)
- Auth system (NextAuth + JWT)
- Component architecture
- Docker/deployment config
- Package names (@smashpoint/*)

View File

@@ -0,0 +1,672 @@
# Cabo Pickleball Club Adaptation - Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Adapt SmashPoint from a Spanish-language padel club system to an English-language pickleball club system branded for Cabo Pickleball Club.
**Architecture:** Direct string replacement across ~30 files. No structural changes to components, API routes, or database schema. Hide unused features (Tournaments, POS) by removing sidebar links. Update color palette and seed data.
**Tech Stack:** Next.js 14, TailwindCSS, Prisma, TypeScript (all unchanged)
---
### Task 1: Update Color Palette
**Files:**
- Modify: `apps/web/tailwind.config.ts`
**Step 1: Replace primary color scale**
Change primary from dark navy (#1E3A5F) to Cabo blue (#2990EA). Change accent from green (#22C55E) to amber (#F59E0B).
Replace the entire `colors` object in tailwind.config.ts with:
```typescript
primary: {
50: "#E8F4FD",
100: "#C5E3FA",
200: "#9DCEF6",
300: "#75B9F2",
400: "#4DA4EE",
500: "#2990EA",
600: "#2177C8",
700: "#195DA6",
800: "#124484",
900: "#0B2B62",
DEFAULT: "#2990EA",
},
accent: {
50: "#FEF7EC",
100: "#FDEACC",
200: "#FBD89D",
300: "#F9C66E",
400: "#F7B43F",
500: "#F59E0B",
600: "#D48509",
700: "#A36807",
800: "#724A05",
900: "#412B03",
DEFAULT: "#F59E0B",
},
```
**Step 2: Build to verify colors compile**
Run: `cd /root/Padel && pnpm build 2>&1 | tail -5`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add apps/web/tailwind.config.ts
git commit -m "feat: update color palette to Cabo blue (#2990EA) and amber accent"
```
---
### Task 2: Update Branding (Landing, Login, Sidebar, Metadata)
**Files:**
- Modify: `apps/web/app/layout.tsx`
- Modify: `apps/web/app/page.tsx`
- Modify: `apps/web/app/(auth)/login/page.tsx`
- Modify: `apps/web/components/layout/sidebar.tsx`
- Modify: `apps/web/app/icon.svg`
**Step 1: Update root layout metadata**
In `apps/web/app/layout.tsx`:
- title: `"Cabo Pickleball Club | SmashPoint"`
- description: `"Court Management System for Cabo Pickleball Club"`
- keywords: `["pickleball", "cabo", "courts", "bookings", "club"]`
- authors: `[{ name: "SmashPoint" }]`
- Change `<html lang="es">` to `<html lang="en">`
**Step 2: Update landing page**
In `apps/web/app/page.tsx`:
- Logo container: change `bg-amber-500` to `bg-primary`
- h1: `"Cabo Pickleball Club"` (instead of "SmashPoint")
- Add subtitle: `<p className="text-sm text-primary-400">Powered by SmashPoint</p>`
- Tagline: `"Court Management System"` (replacing Spanish)
- Change "Reservas" button text to `"Book a Court"` and href to `/bookings`
**Step 3: Update login page**
In `apps/web/app/(auth)/login/page.tsx`:
- Logo containers: change `bg-amber-500/20` and `border-amber-400/30` to `bg-primary/20` and `border-primary-300/30`
- SVG fill colors: change `#FBBF24` to `currentColor` and add `className="text-white"`
- Desktop h1: `"Cabo Pickleball Club"`
- Add after h1: `<p className="text-sm text-primary-300 mb-2">Powered by SmashPoint</p>`
- Desktop tagline: `"Court Management System"`
- Feature 1: `"Court Bookings"` / `"Manage your courts and schedules"`
- Feature 2: `"Player Management"` / `"Memberships and player profiles"`
- Feature 3: `"Reports & Analytics"` / `"Analyze your club's performance"`
- Mobile h1: `"Cabo Pickleball Club"`
- Mobile tagline: `"Court Management System"`
- Footer: `"© {year} SmashPoint. All rights reserved."`
**Step 4: Update sidebar**
In `apps/web/components/layout/sidebar.tsx`:
- Logo container: change `bg-amber-500` to `bg-primary`
- Brand text: `"Cabo Pickleball"` (instead of "SmashPoint")
- Remove Tournaments entry: `{ label: 'Torneos', href: '/tournaments', icon: Trophy }`
- Remove POS entry: `{ label: 'Ventas', href: '/pos', icon: ShoppingCart }`
- Remove Trophy and ShoppingCart imports from lucide-react
- Translate remaining labels:
- `'Reservas'``'Bookings'`
- `'Clientes'``'Players'`
- `'Membresías'``'Memberships'`
- `'Reportes'``'Reports'`
- `'Configuración'``'Settings'`
**Step 5: Update favicon**
In `apps/web/app/icon.svg`:
- Change `fill="#F59E0B"` to `fill="#2990EA"` (rect background)
**Step 6: Commit**
```bash
git add apps/web/app/layout.tsx apps/web/app/page.tsx apps/web/app/\(auth\)/login/page.tsx apps/web/components/layout/sidebar.tsx apps/web/app/icon.svg
git commit -m "feat: rebrand to Cabo Pickleball Club with English UI"
```
---
### Task 3: Translate Auth & Layout Components
**Files:**
- Modify: `apps/web/components/auth/login-form.tsx`
- Modify: `apps/web/components/layout/header.tsx`
**Step 1: Translate login-form.tsx**
All Spanish strings → English:
- `'El correo electrónico es requerido'``'Email is required'`
- `'Ingresa un correo electrónico válido'``'Enter a valid email address'`
- `'La contraseña es requerida'``'Password is required'`
- `'La contraseña debe tener al menos 6 caracteres'``'Password must be at least 6 characters'`
- `'Credenciales inválidas...'``'Invalid credentials. Please check your email and password.'`
- `'Ocurrió un error al iniciar sesión...'``'An error occurred while signing in. Please try again.'`
- `Iniciar Sesión` (heading) → `Sign In`
- `Ingresa tus credenciales para acceder al sistema``Enter your credentials to access the system`
- `Correo Electrónico``Email`
- `"correo@ejemplo.com"``"email@example.com"`
- `Contraseña``Password`
- `Recordarme``Remember me`
- `¿Olvidaste tu contraseña?``Forgot your password?`
- `Iniciando sesión...``Signing in...`
- Button: `'Iniciar Sesión'``'Sign In'`
**Step 2: Translate header.tsx**
- `'Usuario'``'User'`
- `"Cerrar sesión"``"Log out"`
**Step 3: Commit**
```bash
git add apps/web/components/auth/login-form.tsx apps/web/components/layout/header.tsx
git commit -m "feat: translate auth and layout components to English"
```
---
### Task 4: Translate Dashboard Page & Components
**Files:**
- Modify: `apps/web/app/(admin)/dashboard/page.tsx`
- Modify: `apps/web/components/dashboard/quick-actions.tsx`
- Modify: `apps/web/components/dashboard/occupancy-chart.tsx`
- Modify: `apps/web/components/dashboard/recent-bookings.tsx`
- Modify: `apps/web/components/dashboard/stat-card.tsx`
**Step 1: Translate dashboard/page.tsx**
- `"Error al cargar los datos del dashboard"``"Error loading dashboard data"`
- `"Error desconocido"``"Unknown error"`
- `"Usuario"``"User"`
- `` `Bienvenido, ${userName}` `` → `` `Welcome, ${userName}` ``
- `Panel de administracion``Admin panel`
- `` `Mostrando: ${selectedSite.name}` `` → `` `Showing: ${selectedSite.name}` ``
- `"Reservas Hoy"``"Today's Bookings"`
- `"Ingresos Hoy"``"Today's Revenue"`
- `"Ocupacion"``"Occupancy"`
- `"Miembros Activos"``"Active Members"`
- `"Reservas Pendientes"``"Pending Bookings"`
- `"Torneos Proximos"``"Upcoming Events"` (generic since tournaments hidden)
**Step 2: Translate quick-actions.tsx**
- `"Nueva Reserva"``"New Booking"`
- `"Crear una nueva reserva de cancha"``"Create a new court booking"`
- `"Abrir Caja"``"Open Register"`
- `"Iniciar turno de caja registradora"``"Start cash register shift"`
- `"Nueva Venta"``"New Sale"`
- `"Registrar venta en el punto de venta"``"Record a point of sale transaction"`
- `"Registrar Cliente"``"Register Player"`
- `"Agregar un nuevo cliente al sistema"``"Add a new player to the system"`
- `Acciones Rapidas``Quick Actions`
**Step 3: Translate occupancy-chart.tsx**
- `Ocupacion de Canchas``Court Occupancy` (appears twice)
- `No hay canchas configuradas``No courts configured`
- `{court.occupancyPercent}% ocupado``{court.occupancyPercent}% booked`
- `disponible``available`
- `Ocupado``Booked`
- `Disponible``Available`
**Step 4: Translate recent-bookings.tsx**
- `"Pendiente"``"Pending"`
- `"Confirmada"``"Confirmed"`
- `"Completada"``"Completed"`
- `"Cancelada"``"Cancelled"`
- `"No asistio"``"No Show"`
- `Reservas de Hoy``Today's Bookings`
- `Ver todas``View all`
- `No hay reservas para hoy``No bookings for today`
- `"Sin cliente"``"Walk-in"`
**Step 5: Translate stat-card.tsx**
- `vs ayer``vs yesterday`
**Step 6: Commit**
```bash
git add apps/web/app/\(admin\)/dashboard/page.tsx apps/web/components/dashboard/
git commit -m "feat: translate dashboard page and components to English"
```
---
### Task 5: Translate Bookings Page & Components
**Files:**
- Modify: `apps/web/app/(admin)/bookings/page.tsx`
- Modify: `apps/web/components/bookings/booking-calendar.tsx`
- Modify: `apps/web/components/bookings/booking-dialog.tsx`
**Step 1: Translate bookings/page.tsx**
- `Reservas``Bookings`
- `Gestiona las reservas de canchas...``Manage court bookings. Select a time slot to create or view a booking.`
**Step 2: Translate booking-calendar.tsx**
All Spanish strings → English (12 strings):
- Error messages: `"Error al cargar..."``"Error loading..."`
- `Reintentar``Retry`
- `Calendario``Calendar`
- `Hoy``Today`
- `Cargando disponibilidad...``Loading availability...`
- `No hay canchas disponibles.``No courts available.`
- `"Interior"``"Indoor"`, `"Exterior"``"Outdoor"`
- `No disponible``Not available`
- `No hay horarios disponibles para este día.``No time slots available for this day.`
**Step 3: Translate booking-dialog.tsx**
All Spanish strings → English (35 strings):
- Form labels: `Buscar Cliente``Search Player`, `Cliente seleccionado:``Selected player:`
- Status labels: `"Confirmada"``"Confirmed"`, `"Pendiente"``"Pending"`, etc.
- Payment types: `"Efectivo"``"Cash"`, `"Tarjeta"``"Card"`, `"Transferencia"``"Transfer"`, `"Membresia"``"Membership"`, `"Gratuito"``"Free"`
- Field labels: `Cancha:``Court:`, `Fecha:``Date:`, `Hora:``Time:`, `Precio:``Price:`
- Buttons: `"Crear Reserva"``"Create Booking"`, `"Cancelar Reserva"``"Cancel Booking"`
- Error messages: all `"Error al..."``"Error..."`
- Placeholders: `"Nombre, email o telefono..."``"Name, email or phone..."`
- Note: Change all instances of "Cliente" to "Player" in this file
**Step 4: Commit**
```bash
git add apps/web/app/\(admin\)/bookings/page.tsx apps/web/components/bookings/
git commit -m "feat: translate bookings page and components to English"
```
---
### Task 6: Translate Clients Page (rename to Players)
**Files:**
- Modify: `apps/web/app/(admin)/clients/page.tsx`
**Step 1: Translate all strings**
17 Spanish strings → English. Key translations:
- `Clientes``Players`
- `Gestiona los clientes de tu centro``Manage your club's players`
- `Nuevo Cliente``New Player`
- `"Total Clientes"``"Total Players"`
- `"Con Membresia"``"With Membership"`
- `"Nuevos Este Mes"``"New This Month"`
- `"Buscar por nombre, email o telefono..."``"Search by name, email or phone..."`
- `"Todos"``"All"`, `"Con membresia"``"With membership"`, `"Sin membresia"``"Without membership"`
- All error messages: translate from Spanish to English
- Confirmation dialog: translate to English
**Step 2: Commit**
```bash
git add apps/web/app/\(admin\)/clients/page.tsx
git commit -m "feat: translate clients/players page to English"
```
---
### Task 7: Translate Memberships Page & Components
**Files:**
- Modify: `apps/web/app/(admin)/memberships/page.tsx`
- Modify: `apps/web/components/memberships/plan-card.tsx`
- Modify: `apps/web/components/memberships/plan-form.tsx`
**Step 1: Translate memberships/page.tsx (30 strings)**
- `Membresias``Memberships`
- `Gestiona planes y membresias de tus clientes``Manage plans and memberships for your players`
- Status filters: `"Todos"``"All"`, `"Activas"``"Active"`, `"Expiradas"``"Expired"`, `"Canceladas"``"Cancelled"`
- Stats: `Membresias Activas``Active Memberships`, `Por Expirar``Expiring Soon`, `Planes Activos``Active Plans`, `Total Suscriptores``Total Subscribers`
- `Planes de Membresia``Membership Plans`
- `Nuevo Plan``New Plan`
- `Cargando planes...``Loading plans...`
- `No hay planes``No plans`, `Crea tu primer plan de membresia``Create your first membership plan`
- `Crear Plan``Create Plan`
- `Asignar Membresia``Assign Membership`
- `"Buscar por nombre de cliente..."``"Search by player name..."`
- `"Todos los planes"``"All plans"`
- All error messages and confirmation dialogs → English
**Step 2: Translate plan-card.tsx (11 strings)**
- `suscriptor` / `suscriptores``subscriber` / `subscribers`
- `mes` / `meses``month` / `months`
- `horas gratis``free hours`
- `de cancha al mes``of court time per month`
- `descuento``discount`
- `en reservas adicionales``on additional bookings`
- `en tienda``in store`
- `Beneficios adicionales:``Additional benefits:`
- `Editar``Edit`, `Eliminar``Delete`
**Step 3: Translate plan-form.tsx (25 strings)**
- Duration labels: `"1 mes"``"1 month"`, `"3 meses"``"3 months"`, etc.
- Validation: all Spanish error messages → English
- Form title: `"Nuevo Plan de Membresia"``"New Membership Plan"`, `"Editar Plan"``"Edit Plan"`
- Labels: `Nombre del Plan *``Plan Name *`, `Descripcion``Description`, `Precio *``Price *`, `Duracion``Duration`
- Section headers: `Beneficios``Benefits`, `Horas Gratis de Cancha (por mes)``Free Court Hours (per month)`, `Descuento en Reservas (%)``Booking Discount (%)`, `Descuento en Tienda (%)``Store Discount (%)`
- Buttons: `Cancelar``Cancel`, `Guardando...``Saving...`, `"Crear Plan"``"Create Plan"`, `"Guardar Cambios"``"Save Changes"`
- Placeholders: translate to English equivalents
**Step 4: Commit**
```bash
git add apps/web/app/\(admin\)/memberships/page.tsx apps/web/components/memberships/
git commit -m "feat: translate memberships page and components to English"
```
---
### Task 8: Translate Reports Page
**Files:**
- Modify: `apps/web/app/(admin)/reports/page.tsx`
**Step 1: Translate all strings (28 strings)**
- `Reportes``Reports`
- `Análisis y estadísticas del negocio``Business analysis and statistics`
- Period filters: `"Última semana"``"Last week"`, `"Último mes"``"Last month"`, `"Último trimestre"``"Last quarter"`, `"Último año"``"Last year"`
- `Exportar``Export`
- KPIs: `"Ingresos Totales"``"Total Revenue"`, `"Reservas"``"Bookings"`, `"Clientes Activos"``"Active Players"`, `"Ocupación Promedio"``"Average Occupancy"`
- Charts: `Ingresos por Día``Revenue by Day`, `Reservas``Bookings`, `Ventas``Sales`
- `Productos Más Vendidos``Top Selling Products`
- `unidades``units`
- `Rendimiento por Cancha``Court Performance`
- Table headers: `Cancha``Court`, `Sede``Site`, `Reservas``Bookings`, `Ingresos``Revenue`, `Ocupación``Occupancy`
- Day names: `"Lun"``"Mon"`, `"Mar"``"Tue"`, `"Mié"``"Wed"`, `"Jue"``"Thu"`, `"Vie"``"Fri"`, `"Sáb"``"Sat"`, `"Dom"``"Sun"`
- Insights: `Mejor Día``Best Day`, `Sábado``Saturday`, `en ingresos promedio``in average revenue`, `Hora Pico``Peak Hour`, `Ticket Promedio``Average Ticket`
- `vs período anterior``vs previous period`
- Rename "Cancha" to "Court" in mock data court names (lines 110-115)
- Rename "Raqueta alquiler" to "Paddle Rental" in mock products (line 106)
- Rename "Pelotas HEAD" to "Pickleballs" in mock products (line 105)
**Step 2: Commit**
```bash
git add apps/web/app/\(admin\)/reports/page.tsx
git commit -m "feat: translate reports page to English"
```
---
### Task 9: Translate Settings Page
**Files:**
- Modify: `apps/web/app/(admin)/settings/page.tsx`
**Step 1: Translate all Spanish strings**
Key translations (the file has ~60 Spanish strings):
- `Configuración``Settings`
- `Administra la configuración del sistema``Manage system settings`
- `Configuración guardada correctamente``Settings saved successfully`
- Tab labels: `Organización``Organization`, `Sedes``Sites`, `Canchas``Courts`, `Usuarios``Users`
- Organization form: `Nombre de la organización``Organization name`, `Email de contacto``Contact email`, `Teléfono``Phone`, `Moneda``Currency`, `Zona horaria``Timezone`
- Currency options: `"MXN - Peso Mexicano"`, `"USD - Dólar"`, `"EUR - Euro"``"MXN - Mexican Peso"`, `"USD - US Dollar"`, `"EUR - Euro"`
- Timezone options: `"Ciudad de México"``"Mexico City"`, etc.
- Booking config: `Duración por defecto (minutos)``Default duration (minutes)`, `Anticipación mínima (horas)``Minimum notice (hours)`, `Anticipación máxima (días)``Maximum advance (days)`, `Horas para cancelar``Cancellation window (hours)`
- Buttons: `Guardar cambios``Save changes`, `Guardando...``Saving...`
- Sites section: `Sedes``Sites`, `Nueva Sede``New Site`, `Activa` / `Inactiva``Active` / `Inactive`
- Courts section: `Canchas``Courts`, `Nueva Cancha``New Court`, `Cancha``Court`, `Sede``Site`, `Tipo``Type`, `Precio/hora``Price/hour`, `Estado``Status`, `Acciones``Actions`
- Court types: `Indoor` stays, `Outdoor` stays, `"Techada"``"Covered"`
- Court status: `"Activa"``"Active"`, `"Mantenimiento"``"Maintenance"`, `"Inactiva"``"Inactive"`
- Users section: `Usuarios``Users`, `Nuevo Usuario``New User`, `Usuario``User`, `Rol``Role`, `"Super Admin"` stays, `"Admin Sede"``"Site Admin"`, `"Staff"` stays
- Messages: `"Cancha actualizada"``"Court updated"`, `"Cancha creada"``"Court created"`, `"Cancha eliminada"``"Court deleted"`, etc.
- Site form: `"Editar Sede"``"Edit Site"`, `"Nueva Sede"``"New Site"`, `Nombre``Name`, `Dirección``Address`, `Teléfono``Phone`, `Hora apertura``Opening time`, `Hora cierre``Closing time`, `Sede activa``Site active`
- Court form: `"Editar Cancha"``"Edit Court"`, `"Nueva Cancha"``"New Court"`, `Precio hora pico``Peak hour price`
- All `Cancelar``Cancel`, `Guardar``Save`, `Guardando...``Saving...`
- Error/success: `"Sede actualizada"``"Site updated"`, `"Sede creada"``"Site created"`, `"Error al guardar..."``"Error saving..."`, `"Error de conexión"``"Connection error"`
- Confirmation: `"¿Estás seguro de eliminar esta cancha?"``"Are you sure you want to delete this court?"`
- `"Todas"``"All"` (for site assignment)
- `"Activo"` / `"Inactivo"``"Active"` / `"Inactive"` (user status)
**Step 2: Commit**
```bash
git add apps/web/app/\(admin\)/settings/page.tsx
git commit -m "feat: translate settings page to English"
```
---
### Task 10: Translate API Error Messages
**Files:**
- Modify: `apps/web/app/api/bookings/route.ts`
- Modify: `apps/web/app/api/clients/route.ts`
- Check and modify any other API routes with Spanish strings
**Step 1: Translate bookings/route.ts**
- `'No autorizado'``'Unauthorized'`
- `'Error al obtener las reservas'``'Error fetching bookings'`
- `'Datos de reserva inválidos'``'Invalid booking data'`
- `'Cancha no encontrada o no pertenece a su organización'``'Court not found or does not belong to your organization'`
- `'La cancha no está disponible para reservas'``'The court is not available for bookings'`
- `'Cliente no encontrado o no pertenece a su organización'``'Client not found or does not belong to your organization'`
- `'Ya existe una reserva en ese horario...'``'A booking already exists for that time slot. Please select another time.'`
- `'Error al crear la reserva'``'Error creating booking'`
**Step 2: Scan and translate all other API routes**
Search for Spanish strings in all files under `apps/web/app/api/` and translate them.
Run: `grep -rn "'" apps/web/app/api/ | grep -i "[áéíóúñ]\|Error al\|No autorizado\|no encontrad"` to find remaining Spanish.
**Step 3: Commit**
```bash
git add apps/web/app/api/
git commit -m "feat: translate API error messages to English"
```
---
### Task 11: Update Seed Data for Cabo Pickleball
**Files:**
- Modify: `apps/web/prisma/seed.ts`
**Step 1: Update organization**
```typescript
name: 'Cabo Pickleball Club',
slug: 'cabo-pickleball-club',
settings: {
currency: 'MXN',
timezone: 'America/Mazatlan',
language: 'en',
},
```
**Step 2: Update site (single site instead of 3)**
Replace the 3 sites with 1:
```typescript
const sitesData = [
{
name: 'Corridor Courts',
slug: 'corridor-courts',
address: 'Corridor area, Cabo San Lucas, BCS',
phone: '+52-624-151-5455',
email: 'topdogcabo@yahoo.com',
timezone: 'America/Mazatlan',
openTime: '07:00',
closeTime: '22:00',
},
];
```
**Step 3: Update courts (6 outdoor courts)**
Replace the 2-per-site pattern with 6 courts for the single site:
```typescript
const courtData = [
{ name: 'Court 1', type: CourtType.OUTDOOR, status: CourtStatus.AVAILABLE, pricePerHour: 300, description: 'Outdoor court with night lighting', features: ['Night lighting', 'Court dividers'], displayOrder: 1 },
{ name: 'Court 2', type: CourtType.OUTDOOR, status: CourtStatus.AVAILABLE, pricePerHour: 300, description: 'Outdoor court with night lighting', features: ['Night lighting', 'Court dividers'], displayOrder: 2 },
{ name: 'Court 3', type: CourtType.OUTDOOR, status: CourtStatus.AVAILABLE, pricePerHour: 300, description: 'Outdoor court with night lighting', features: ['Night lighting', 'Court dividers'], displayOrder: 3 },
{ name: 'Court 4', type: CourtType.OUTDOOR, status: CourtStatus.AVAILABLE, pricePerHour: 300, description: 'Outdoor court with night lighting', features: ['Night lighting', 'Court dividers'], displayOrder: 4 },
{ name: 'Court 5', type: CourtType.OUTDOOR, status: CourtStatus.AVAILABLE, pricePerHour: 300, description: 'Outdoor court with night lighting', features: ['Night lighting', 'Court dividers'], displayOrder: 5 },
{ name: 'Court 6', type: CourtType.OUTDOOR, status: CourtStatus.AVAILABLE, pricePerHour: 300, description: 'Outdoor court with night lighting', features: ['Night lighting', 'Court dividers'], displayOrder: 6 },
];
```
**Step 4: Update admin user**
```typescript
email: 'ivan@horuxfin.com',
password: await bcrypt.hash('Aasi940812', 10),
```
Remove the site admin users (single-site operation).
**Step 5: Update product categories and products**
Change to pickleball-relevant items:
- Category: `'Equipment'``'Pickleball equipment and accessories'`
- Products: `'Pickleballs'` (Franklin X-40), `'Paddle Rental'`, `'Paddle Grip'`
- Category: `'Drinks'` stays but translate names to English
- Remove `'Alquiler'` category (merge rental into Equipment)
**Step 6: Update membership plans**
```typescript
const membershipPlansData = [
{
name: 'Day Pass',
description: 'Single day access to all courts',
price: 300,
durationMonths: 1,
courtHours: 0,
discountPercent: 0,
benefits: ['Full day access', 'All courts', 'Night play included'],
},
{
name: '10-Day Pass',
description: '10 visits, any time of day',
price: 2500,
durationMonths: 3,
courtHours: 10,
discountPercent: 15,
benefits: ['10 day passes', 'Valid any time', 'Save vs single day pass'],
},
{
name: '10-Morning Pass',
description: '10 morning sessions (7am-12pm)',
price: 2000,
durationMonths: 3,
courtHours: 10,
discountPercent: 10,
benefits: ['10 morning passes', '7:00 AM - 12:00 PM only', 'Best value for morning players'],
},
{
name: 'Monthly Individual',
description: 'Unlimited monthly access for one player',
price: 4000,
durationMonths: 1,
courtHours: 30,
discountPercent: 25,
benefits: ['Unlimited court access', 'Priority booking', 'All time slots'],
},
{
name: 'Monthly Family',
description: 'Unlimited monthly access for up to 4 family members',
price: 6500,
durationMonths: 1,
courtHours: 60,
discountPercent: 30,
benefits: ['Up to 4 family members', 'Unlimited court access', 'Priority booking', 'All time slots'],
},
];
```
**Step 7: Update seed summary output**
Change all console.log messages to English and update credential display:
```
Login credentials:
Admin: ivan@horuxfin.com / Aasi940812
```
**Step 8: Commit**
```bash
git add apps/web/prisma/seed.ts
git commit -m "feat: update seed data for Cabo Pickleball Club"
```
---
### Task 12: Sport Terminology Sweep
**Files:**
- All `.tsx`, `.ts` files containing "padel", "pádel", "cancha", "raqueta", "pelota"
**Step 1: Global search and replace**
Run targeted searches and replace remaining sport terms:
- `grep -rn "padel\|pádel\|Padel\|Pádel" apps/web/ --include="*.tsx" --include="*.ts"` — replace with "pickleball"
- `grep -rn "cancha" apps/web/ --include="*.tsx" --include="*.ts"` — replace with "court" (should already be done in earlier tasks)
- `grep -rn "raqueta\|Raqueta" apps/web/ --include="*.tsx" --include="*.ts"` — replace with "paddle"
- `grep -rn "pelota\|Pelota" apps/web/ --include="*.tsx" --include="*.ts"` — replace with "pickleball ball" or "pickleballs"
**Step 2: Verify no Spanish sport terms remain**
Run: `grep -rni "padel\|cancha\|raqueta\|pelota" apps/web/ --include="*.tsx" --include="*.ts"`
Expected: No matches (or only in comments/prisma generated code)
**Step 3: Commit if any changes**
```bash
git add -A
git commit -m "feat: replace all padel terminology with pickleball"
```
---
### Task 13: Build, Verify & Final Commit
**Step 1: Clean build**
```bash
rm -rf apps/web/.next .turbo
pnpm build
```
Expected: Build succeeds with 0 errors.
**Step 2: Verify no Spanish remains in user-facing code**
Run: `grep -rni "[áéíóúñ]" apps/web/app/ apps/web/components/ --include="*.tsx" --include="*.ts" | grep -v node_modules | grep -v ".next"`
Review any remaining Spanish strings and translate.
**Step 3: Restart server and verify**
```bash
fuser -k 3000/tcp 2>/dev/null
sleep 2
cd apps/web && npx next start --port 3000 &
```
**Step 4: Push**
```bash
git push origin main
```