- Introduced recurring booking groups with the ability to create and cancel them. - Added database migrations for recurring bookings, including new tables and relationships. - Implemented handlers for creating and canceling recurring bookings in the admin booking module. - Enhanced existing booking services to support recurring bookings logic. - Updated API contract to include new schemas for recurring bookings. - Refactored existing code for improved readability and maintainability.
107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
import { cn } from '@/lib/utils';
|
|
import type { PlanWithFeatures } from '@repo/api-contract';
|
|
import { Check, X } from 'lucide-react';
|
|
import type { UseFormReturn } from 'react-hook-form';
|
|
|
|
type PlanSelectStepProps = {
|
|
form: UseFormReturn<any>;
|
|
plans: PlanWithFeatures[];
|
|
};
|
|
|
|
const FEATURE_LABELS: Record<string, string> = {
|
|
publicBookingPage: 'Página de booking pública',
|
|
onlinePayments: 'Pagos online',
|
|
advancedReports: 'Reportes avanzados',
|
|
whatsappReminders: 'Recordatorios WhatsApp',
|
|
fixedSlots: 'Turnos fijos',
|
|
};
|
|
|
|
export function PlanSelectStep({ form, plans }: PlanSelectStepProps) {
|
|
const selectedCode = form.watch('planCode') as string | undefined;
|
|
const errors = form.formState.errors;
|
|
|
|
const featuresList = [
|
|
'publicBookingPage',
|
|
'onlinePayments',
|
|
'advancedReports',
|
|
'whatsappReminders',
|
|
'fixedSlots',
|
|
] as const;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Elegí tu plan</h2>
|
|
<p className="text-sm text-muted-foreground">
|
|
Seleccioná el plan que mejor se adapte a tu complejo.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
{plans.map((plan) => {
|
|
const isSelected = selectedCode === plan.code;
|
|
return (
|
|
<button
|
|
key={plan.code}
|
|
type="button"
|
|
onClick={() => form.setValue('planCode', plan.code, { shouldValidate: true })}
|
|
className={cn(
|
|
'relative flex flex-col rounded-xl border-2 p-5 text-left transition-all',
|
|
'hover:border-primary/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
isSelected ? 'border-primary bg-primary/5 shadow-sm' : 'border-border bg-card'
|
|
)}
|
|
>
|
|
{isSelected && (
|
|
<div className="absolute right-3 top-3 flex size-6 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
|
<Check className="size-4" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="mb-3">
|
|
<h3 className="text-xl font-bold">{plan.name}</h3>
|
|
<div className="mt-1">
|
|
<span className="text-3xl font-bold">
|
|
{plan.currency === 'ARS'
|
|
? `AR$${plan.price.toLocaleString('es-AR')}`
|
|
: `$${plan.price.toFixed(2)}`}
|
|
</span>
|
|
<span className="text-sm text-muted-foreground">/mes</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-4 space-y-1.5 border-t pt-3 text-xs text-muted-foreground">
|
|
<p className="font-medium text-foreground">Incluye:</p>
|
|
<p>Hasta {plan.limits.maxCourts} canchas</p>
|
|
<p>Hasta {plan.limits.maxBookingsPerDay} reservas/día</p>
|
|
{plan.limits.maxActiveUsers && <p>Hasta {plan.limits.maxActiveUsers} usuarios</p>}
|
|
</div>
|
|
|
|
<div className="space-y-1.5 border-t pt-3">
|
|
{featuresList.map((feature) => {
|
|
const enabled = plan.features[feature as keyof typeof plan.features];
|
|
return (
|
|
<div key={feature} className="flex items-center gap-2 text-xs">
|
|
{enabled ? (
|
|
<Check className="size-3.5 shrink-0 text-green-600" />
|
|
) : (
|
|
<X className="size-3.5 shrink-0 text-muted-foreground" />
|
|
)}
|
|
<span className={enabled ? 'text-foreground' : 'text-muted-foreground'}>
|
|
{FEATURE_LABELS[feature]}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{errors.planCode && (
|
|
<p className="text-sm text-destructive">{String(errors.planCode.message ?? '')}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|