f79e5ce75c
Deploy / deploy (push) Successful in 4m2s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
154 lines
6.1 KiB
TypeScript
154 lines
6.1 KiB
TypeScript
"use client";
|
||
import { useEffect, useState } from "react";
|
||
import Card from "@/components/ui/Card";
|
||
import Badge from "@/components/ui/Badge";
|
||
import Button from "@/components/ui/Button";
|
||
|
||
interface Turno {
|
||
id: string;
|
||
fechaInicio: string;
|
||
fechaFin: string;
|
||
estado: "PENDIENTE" | "CONFIRMADO" | "CANCELADO" | "COMPLETADO";
|
||
tipoServicio: { nombre: string };
|
||
auto: { marca: string; modelo: string; patente: string };
|
||
cliente: { nombre: string; apellido: string; telefono: string };
|
||
}
|
||
|
||
export default function AdminDashboard() {
|
||
const [fecha, setFecha] = useState(() => new Date().toISOString().slice(0, 10));
|
||
const [turnos, setTurnos] = useState<Turno[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [completando, setCompletando] = useState<Set<string>>(new Set());
|
||
const [comentarios, setComentarios] = useState<Record<string, string>>({});
|
||
|
||
useEffect(() => { cargar(); }, [fecha]);
|
||
|
||
async function cargar() {
|
||
setLoading(true);
|
||
const res = await fetch(`/api/admin/turnos?fecha=${fecha}`);
|
||
const data = await res.json();
|
||
setTurnos(data);
|
||
setLoading(false);
|
||
}
|
||
|
||
async function cambiarEstado(id: string, estado: string, comentario?: string) {
|
||
await fetch(`/api/admin/turnos/${id}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ estado, comentario }),
|
||
});
|
||
setCompletando((prev) => { const s = new Set(prev); s.delete(id); return s; });
|
||
setComentarios((prev) => { const c = { ...prev }; delete c[id]; return c; });
|
||
cargar();
|
||
}
|
||
|
||
function iniciarCompletado(id: string) {
|
||
setCompletando((prev) => new Set(prev).add(id));
|
||
}
|
||
|
||
function cancelarCompletado(id: string) {
|
||
setCompletando((prev) => { const s = new Set(prev); s.delete(id); return s; });
|
||
setComentarios((prev) => { const c = { ...prev }; delete c[id]; return c; });
|
||
}
|
||
|
||
function formatHora(iso: string) {
|
||
return new Date(iso).toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" });
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||
<h1 className="text-xl sm:text-2xl font-bold text-white">Turnos del día</h1>
|
||
<input
|
||
type="date"
|
||
value={fecha}
|
||
onChange={(e) => setFecha(e.target.value)}
|
||
className="border border-gray-300 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-500 w-full sm:w-auto"
|
||
/>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="text-center py-10 text-gray-200">Cargando...</div>
|
||
) : turnos.length === 0 ? (
|
||
<Card>
|
||
<p className="text-center text-gray-400 py-4">No hay turnos para esta fecha.</p>
|
||
</Card>
|
||
) : (
|
||
<div className="flex flex-col gap-4">
|
||
{turnos.map((t) => (
|
||
<Card key={t.id}>
|
||
<div className="flex flex-col gap-3">
|
||
<div className="flex items-start justify-between flex-wrap gap-2">
|
||
<div>
|
||
<p className="font-semibold text-gray-800">
|
||
{formatHora(t.fechaInicio)} – {formatHora(t.fechaFin)}
|
||
</p>
|
||
<p className="text-sm text-gray-600 mt-0.5">{t.tipoServicio.nombre}</p>
|
||
</div>
|
||
<Badge estado={t.estado} />
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm text-gray-600 bg-gray-50 rounded-lg p-3">
|
||
<span className="text-gray-400">Cliente</span>
|
||
<span>{t.cliente.nombre} {t.cliente.apellido}</span>
|
||
<span className="text-gray-400">Teléfono</span>
|
||
<span>{t.cliente.telefono}</span>
|
||
<span className="text-gray-400">Auto</span>
|
||
<span>{t.auto.marca} {t.auto.modelo}</span>
|
||
<span className="text-gray-400">Patente</span>
|
||
<span className="font-mono">{t.auto.patente}</span>
|
||
</div>
|
||
|
||
{t.estado === "PENDIENTE" && (
|
||
<div className="flex gap-2">
|
||
<Button onClick={() => cambiarEstado(t.id, "CONFIRMADO")} className="flex-1 text-sm">
|
||
✓ Confirmar
|
||
</Button>
|
||
<Button variant="danger" onClick={() => cambiarEstado(t.id, "CANCELADO")} className="flex-1 text-sm">
|
||
✕ Cancelar
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{t.estado === "CONFIRMADO" && !completando.has(t.id) && (
|
||
<Button onClick={() => iniciarCompletado(t.id)} variant="secondary" className="text-sm">
|
||
✓ Marcar completado
|
||
</Button>
|
||
)}
|
||
|
||
{t.estado === "CONFIRMADO" && completando.has(t.id) && (
|
||
<div className="flex flex-col gap-2">
|
||
<textarea
|
||
value={comentarios[t.id] ?? ""}
|
||
onChange={(e) => setComentarios((prev) => ({ ...prev, [t.id]: e.target.value }))}
|
||
placeholder="Observaciones (opcional)..."
|
||
rows={3}
|
||
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||
/>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
onClick={() => cambiarEstado(t.id, "COMPLETADO", comentarios[t.id])}
|
||
variant="secondary"
|
||
className="flex-1 text-sm"
|
||
>
|
||
✓ Confirmar completado
|
||
</Button>
|
||
<Button
|
||
variant="danger"
|
||
onClick={() => cancelarCompletado(t.id)}
|
||
className="flex-1 text-sm"
|
||
>
|
||
Cancelar
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|