feat: agrega km y chasis al turno/auto, combobox de marcas y modelos
Deploy / deploy (push) Failing after 13m41s
Deploy / deploy (push) Failing after 13m41s
- Turno.kilometros (requerido al reservar) - Auto.nroChasis (opcional) - Combobox con 60 marcas mundiales y modelos precargados - Modelo se habilita al seleccionar marca Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ export async function POST(req: NextRequest) {
|
||||
const sesion = await verificarSesion("cliente_session");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { patente, marca, modelo, anio } = await req.json();
|
||||
const { patente, marca, modelo, anio, nroChasis } = await req.json();
|
||||
if (!patente || !marca || !modelo || !anio)
|
||||
return NextResponse.json({ error: "Todos los campos son requeridos" }, { status: 400 });
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function POST(req: NextRequest) {
|
||||
marca,
|
||||
modelo,
|
||||
anio: Number(anio),
|
||||
nroChasis: nroChasis?.trim() || null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ export async function POST(req: NextRequest) {
|
||||
const sesion = await verificarSesion("cliente_session");
|
||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { autoId, tipoServicioId, fechaInicio } = await req.json();
|
||||
if (!autoId || !tipoServicioId || !fechaInicio)
|
||||
const { autoId, tipoServicioId, fechaInicio, kilometros } = await req.json();
|
||||
if (!autoId || !tipoServicioId || !fechaInicio || !kilometros)
|
||||
return NextResponse.json({ error: "Datos incompletos" }, { status: 400 });
|
||||
|
||||
// Verificar que el auto pertenece al cliente
|
||||
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: "El horario ya no está disponible" }, { status: 409 });
|
||||
|
||||
const turno = await prisma.turno.create({
|
||||
data: { clienteId: sesion.sub, autoId, tipoServicioId, fechaInicio: inicio, fechaFin: fin },
|
||||
data: { clienteId: sesion.sub, autoId, tipoServicioId, fechaInicio: inicio, fechaFin: fin, kilometros: Number(kilometros) },
|
||||
include: { auto: true, tipoServicio: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import Link from "next/link";
|
||||
import Button from "@/components/ui/Button";
|
||||
import Card from "@/components/ui/Card";
|
||||
import Input from "@/components/ui/Input";
|
||||
import Combobox from "@/components/ui/Combobox";
|
||||
import { MARCAS, getModelos } from "@/lib/marcas-modelos";
|
||||
|
||||
interface Auto {
|
||||
id: string;
|
||||
@@ -12,6 +14,7 @@ interface Auto {
|
||||
marca: string;
|
||||
modelo: string;
|
||||
anio: number;
|
||||
nroChasis?: string | null;
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
@@ -19,7 +22,8 @@ export default function DashboardPage() {
|
||||
const [autos, setAutos] = useState<Auto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [mostrarForm, setMostrarForm] = useState(false);
|
||||
const [form, setForm] = useState({ patente: "", marca: "", modelo: "", anio: "" });
|
||||
const [form, setForm] = useState({ patente: "", marca: "", modelo: "", anio: "", nroChasis: "" });
|
||||
const modelosDisponibles = getModelos(form.marca);
|
||||
const [guardando, setGuardando] = useState(false);
|
||||
const [errorForm, setErrorForm] = useState("");
|
||||
|
||||
@@ -51,7 +55,7 @@ export default function DashboardPage() {
|
||||
setGuardando(false);
|
||||
if (!res.ok) { setErrorForm(data.error || "Error al guardar"); return; }
|
||||
setMostrarForm(false);
|
||||
setForm({ patente: "", marca: "", modelo: "", anio: "" });
|
||||
setForm({ patente: "", marca: "", modelo: "", anio: "", nroChasis: "" });
|
||||
cargarAutos();
|
||||
}
|
||||
|
||||
@@ -108,10 +112,30 @@ export default function DashboardPage() {
|
||||
<Card title="Agregar auto">
|
||||
<form onSubmit={agregarAuto} className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input id="marca" label="Marca" placeholder="Ford" value={form.marca} onChange={set("marca")} required />
|
||||
<Input id="modelo" label="Modelo" placeholder="Focus" value={form.modelo} onChange={set("modelo")} required />
|
||||
<Combobox
|
||||
id="marca"
|
||||
label="Marca"
|
||||
options={MARCAS}
|
||||
value={form.marca}
|
||||
onChange={(v) => setForm((f) => ({ ...f, marca: v, modelo: "" }))}
|
||||
onSelect={(v) => setForm((f) => ({ ...f, marca: v, modelo: "" }))}
|
||||
placeholder="Ford"
|
||||
required
|
||||
/>
|
||||
<Combobox
|
||||
id="modelo"
|
||||
label="Modelo"
|
||||
options={modelosDisponibles}
|
||||
value={form.modelo}
|
||||
onChange={(v) => setForm((f) => ({ ...f, modelo: v }))}
|
||||
onSelect={(v) => setForm((f) => ({ ...f, modelo: v }))}
|
||||
placeholder={form.marca ? "Focus" : "Primero elegí la marca"}
|
||||
disabled={modelosDisponibles.length === 0}
|
||||
required
|
||||
/>
|
||||
<Input id="anio" label="Año" type="number" placeholder="2018" value={form.anio} onChange={set("anio")} required min={1960} max={new Date().getFullYear() + 1} />
|
||||
<Input id="patente" label="Patente" placeholder="AB123CD" value={form.patente} onChange={set("patente")} required className="uppercase" />
|
||||
<Input id="nroChasis" label="Nº Chasis (opcional)" placeholder="8AFZZZ11ZYM569874" value={form.nroChasis} onChange={set("nroChasis")} className="col-span-2" />
|
||||
</div>
|
||||
{errorForm && <p className="text-sm text-red-500">{errorForm}</p>}
|
||||
<div className="flex gap-2 justify-end">
|
||||
|
||||
@@ -30,6 +30,7 @@ function TurnosContent() {
|
||||
const [slots, setSlots] = useState<Slot[]>([]);
|
||||
const [slotSel, setSlotSel] = useState<Slot | null>(null);
|
||||
const [loadingSlots, setLoadingSlots] = useState(false);
|
||||
const [kilometros, setKilometros] = useState("");
|
||||
const [reservando, setReservando] = useState(false);
|
||||
const [turnoCreado, setTurnoCreado] = useState<Turno | null>(null);
|
||||
const [misTurnos, setMisTurnos] = useState<Turno[]>([]);
|
||||
@@ -56,12 +57,12 @@ function TurnosContent() {
|
||||
}
|
||||
|
||||
async function reservar() {
|
||||
if (!slotSel || !servicioSel || !autoId) return;
|
||||
if (!slotSel || !servicioSel || !autoId || !kilometros) 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 }),
|
||||
body: JSON.stringify({ autoId, tipoServicioId: servicioSel.id, fechaInicio: slotSel.fechaInicio, kilometros: Number(kilometros) }),
|
||||
});
|
||||
const data = await res.json();
|
||||
setReservando(false);
|
||||
@@ -189,9 +190,23 @@ function TurnosContent() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<label htmlFor="kilometros" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kilometraje actual <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="kilometros"
|
||||
type="number"
|
||||
min={0}
|
||||
placeholder="Ej: 85000"
|
||||
value={kilometros}
|
||||
onChange={(e) => setKilometros(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={reservar}
|
||||
disabled={!slotSel}
|
||||
disabled={!slotSel || !kilometros}
|
||||
loading={reservando}
|
||||
className="w-full mt-2"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user