b6705239a1
Deploy / deploy (push) Failing after 33h14m30s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
215 lines
8.1 KiB
TypeScript
215 lines
8.1 KiB
TypeScript
"use client";
|
|
import { Suspense, useEffect, useState } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import Link from "next/link";
|
|
import Card from "@/components/ui/Card";
|
|
import Button from "@/components/ui/Button";
|
|
import Badge from "@/components/ui/Badge";
|
|
|
|
interface TipoServicio { id: string; nombre: string; duracionHoras: number; }
|
|
interface Slot { fechaInicio: string; fechaFin: string; }
|
|
interface Turno {
|
|
id: string;
|
|
fechaInicio: string;
|
|
fechaFin: string;
|
|
estado: "PENDIENTE" | "CONFIRMADO" | "CANCELADO" | "COMPLETADO";
|
|
tipoServicio: { nombre: string };
|
|
auto: { marca: string; modelo: string; patente: string };
|
|
}
|
|
|
|
type Step = "servicio" | "slot" | "confirmado";
|
|
|
|
function TurnosContent() {
|
|
const router = useRouter();
|
|
const params = useSearchParams();
|
|
const autoId = params.get("autoId");
|
|
|
|
const [step, setStep] = useState<Step>("servicio");
|
|
const [servicios, setServicios] = useState<TipoServicio[]>([]);
|
|
const [servicioSel, setServicioSel] = useState<TipoServicio | null>(null);
|
|
const [slots, setSlots] = useState<Slot[]>([]);
|
|
const [slotSel, setSlotSel] = useState<Slot | null>(null);
|
|
const [loadingSlots, setLoadingSlots] = useState(false);
|
|
const [reservando, setReservando] = useState(false);
|
|
const [turnoCreado, setTurnoCreado] = useState<Turno | null>(null);
|
|
const [misTurnos, setMisTurnos] = useState<Turno[]>([]);
|
|
const [loadingTurnos, setLoadingTurnos] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!autoId) { router.push("/cliente/dashboard"); return; }
|
|
fetch("/api/cliente/servicios")
|
|
.then((r) => r.json())
|
|
.then((data) => setServicios(data));
|
|
fetch("/api/cliente/turnos")
|
|
.then((r) => r.json())
|
|
.then((data) => { setMisTurnos(data.filter((t: Turno & { autoId: string }) => t.autoId === autoId)); setLoadingTurnos(false); });
|
|
}, [autoId, router]);
|
|
|
|
async function seleccionarServicio(s: TipoServicio) {
|
|
setServicioSel(s);
|
|
setLoadingSlots(true);
|
|
setStep("slot");
|
|
const res = await fetch(`/api/cliente/slots?tipoServicioId=${s.id}`);
|
|
const data = await res.json();
|
|
setSlots(data);
|
|
setLoadingSlots(false);
|
|
}
|
|
|
|
async function reservar() {
|
|
if (!slotSel || !servicioSel || !autoId) return;
|
|
setReservando(true);
|
|
const res = await fetch("/api/cliente/turnos", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ autoId, tipoServicioId: servicioSel.id, fechaInicio: slotSel.fechaInicio }),
|
|
});
|
|
const data = await res.json();
|
|
setReservando(false);
|
|
if (res.ok) {
|
|
setTurnoCreado(data);
|
|
setStep("confirmado");
|
|
}
|
|
}
|
|
|
|
function formatSlot(slot: Slot) {
|
|
const inicio = new Date(slot.fechaInicio);
|
|
const fin = new Date(slot.fechaFin);
|
|
const dia = inicio.toLocaleDateString("es-AR", { weekday: "long", day: "numeric", month: "long" });
|
|
const h1 = inicio.toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" });
|
|
const h2 = fin.toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" });
|
|
return { dia: dia.charAt(0).toUpperCase() + dia.slice(1), horas: `${h1} - ${h2}` };
|
|
}
|
|
|
|
if (step === "confirmado" && turnoCreado) {
|
|
const { dia, horas } = formatSlot({ fechaInicio: turnoCreado.fechaInicio, fechaFin: turnoCreado.fechaFin });
|
|
return (
|
|
<div className="flex flex-col items-center justify-center min-h-[50vh] text-center gap-4">
|
|
<div className="text-5xl">✅</div>
|
|
<h1 className="text-2xl font-bold text-gray-800">¡Turno reservado!</h1>
|
|
<Card className="text-left w-full max-w-sm">
|
|
<p className="text-sm text-gray-500 mb-1">Servicio</p>
|
|
<p className="font-semibold mb-3">{turnoCreado.tipoServicio.nombre}</p>
|
|
<p className="text-sm text-gray-500 mb-1">Fecha</p>
|
|
<p className="font-semibold mb-1">{dia}</p>
|
|
<p className="text-gray-600">{horas}</p>
|
|
</Card>
|
|
<Link href="/cliente/dashboard">
|
|
<Button variant="secondary">← Volver a mis autos</Button>
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<div className="flex items-center gap-3">
|
|
{step === "slot" ? (
|
|
<Button variant="ghost" className="px-2" onClick={() => setStep("servicio")}>←</Button>
|
|
) : (
|
|
<Link href="/cliente/dashboard"><Button variant="ghost" className="px-2">←</Button></Link>
|
|
)}
|
|
<h1 className="text-2xl font-bold text-gray-800">
|
|
{step === "servicio" ? "Elegí el servicio" : "Elegí el horario"}
|
|
</h1>
|
|
</div>
|
|
|
|
{step === "servicio" && (
|
|
<>
|
|
<div className="grid gap-3">
|
|
{servicios.map((s) => (
|
|
<button
|
|
key={s.id}
|
|
onClick={() => seleccionarServicio(s)}
|
|
className="text-left bg-white border border-gray-200 rounded-xl p-4 hover:border-blue-500 hover:shadow-sm transition flex items-center justify-between"
|
|
>
|
|
<div>
|
|
<p className="font-medium text-gray-800">{s.nombre}</p>
|
|
<p className="text-sm text-gray-500 mt-0.5">
|
|
Duración: {s.duracionHoras === 1 ? "1 hora" : `${s.duracionHoras} horas`}
|
|
</p>
|
|
</div>
|
|
<span className="text-gray-400 text-lg">→</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{!loadingTurnos && misTurnos.length > 0 && (
|
|
<Card title="Mis turnos para este auto">
|
|
<div className="flex flex-col gap-3">
|
|
{misTurnos.map((t) => {
|
|
const { dia, horas } = formatSlot({ fechaInicio: t.fechaInicio, fechaFin: t.fechaFin });
|
|
return (
|
|
<div key={t.id} className="flex items-center justify-between flex-wrap gap-2">
|
|
<div>
|
|
<p className="text-sm font-medium">{t.tipoServicio.nombre}</p>
|
|
<p className="text-xs text-gray-500">{dia} · {horas}</p>
|
|
</div>
|
|
<Badge estado={t.estado} />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{step === "slot" && (
|
|
<div className="flex flex-col gap-4">
|
|
{loadingSlots ? (
|
|
<div className="text-center py-10 text-gray-400">Buscando horarios disponibles...</div>
|
|
) : slots.length === 0 ? (
|
|
<Card>
|
|
<p className="text-center text-gray-400 py-4">
|
|
No hay horarios disponibles en los próximos 14 días. Consultá al taller.
|
|
</p>
|
|
</Card>
|
|
) : (
|
|
<>
|
|
<p className="text-sm text-gray-500">
|
|
Servicio: <strong>{servicioSel?.nombre}</strong>
|
|
</p>
|
|
<div className="grid gap-2">
|
|
{slots.map((slot, i) => {
|
|
const { dia, horas } = formatSlot(slot);
|
|
const sel = slotSel?.fechaInicio === slot.fechaInicio;
|
|
return (
|
|
<button
|
|
key={i}
|
|
onClick={() => setSlotSel(slot)}
|
|
className={`text-left rounded-xl p-4 border transition ${
|
|
sel
|
|
? "border-blue-500 bg-blue-50"
|
|
: "border-gray-200 bg-white hover:border-blue-300"
|
|
}`}
|
|
>
|
|
<p className="font-medium text-gray-800">{dia}</p>
|
|
<p className="text-sm text-gray-500">{horas}</p>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<Button
|
|
onClick={reservar}
|
|
disabled={!slotSel}
|
|
loading={reservando}
|
|
className="w-full mt-2"
|
|
>
|
|
Confirmar turno
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function TurnosPage() {
|
|
return (
|
|
<Suspense fallback={<div className="text-center py-10 text-gray-400">Cargando...</div>}>
|
|
<TurnosContent />
|
|
</Suspense>
|
|
);
|
|
}
|