feat(cliente): invertir el flujo web a jefe -> trabajo
Revisión con IA / ai-review (pull_request) Failing after 2m12s
Revisión con IA / ai-review (pull_request) Failing after 2m12s
Los trabajos ahora son por jefe mecanico, asi que el cliente elige primero el mecanico y despues el trabajo, igual que en el bot de WhatsApp. Antes listaba todos los servicios del taller y recien despues pedia el jefe. - src/lib/servicios.ts: regla unica de que trabajos ofrece un jefe = los suyos + los globales del taller (jefeMecanicoId null, los carga el admin). Asi no se rompe lo que ya existe en produccion, donde todos los servicios son globales. - /api/cliente/servicios ahora exige jefeMecanicoId y aplica esa regla. - /api/cliente/slots y crearTurno rechazan un trabajo propio de OTRO jefe (400): antes se podia reservar con el jefe A la duracion del trabajo del B. - whatsapp-bot: usa la misma regla, asi la web y el bot ofrecen lo mismo. - cliente/turnos: pasos invertidos, cabeceras y back acordes, y estado vacio propio cuando el jefe no tiene trabajos. - test-whatsapp-bot: soporte de OR en el Prisma en memoria, trabajo global en el seed y escenario D (globales visibles, trabajo ajeno rechazado). 31 asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,10 @@ function match(row: Row, where: Row): boolean {
|
|||||||
if (!(v as Row[]).every((w) => match(row, w))) return false;
|
if (!(v as Row[]).every((w) => match(row, w))) return false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (k === "OR") {
|
||||||
|
if (!(v as Row[]).some((w) => match(row, w))) return false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const rv = row[k];
|
const rv = row[k];
|
||||||
if (v !== null && typeof v === "object" && !(v instanceof Date)) {
|
if (v !== null && typeof v === "object" && !(v instanceof Date)) {
|
||||||
if ("in" in v && !(v.in as any[]).includes(rv)) return false;
|
if ("in" in v && !(v.in as any[]).includes(rv)) return false;
|
||||||
@@ -143,6 +147,8 @@ jefes.push({ id: "j1", nombre: "Carlos", email: "j1@t.com", activo: true, slotsP
|
|||||||
jefes.push({ id: "j2", nombre: "Diego", email: "j2@t.com", activo: true, slotsParalelos: 1 });
|
jefes.push({ id: "j2", nombre: "Diego", email: "j2@t.com", activo: true, slotsParalelos: 1 });
|
||||||
servicios.push({ id: "s1", nombre: "Cambio de aceite", duracionHoras: 1, activo: true, jefeMecanicoId: "j1" });
|
servicios.push({ id: "s1", nombre: "Cambio de aceite", duracionHoras: 1, activo: true, jefeMecanicoId: "j1" });
|
||||||
servicios.push({ id: "s2", nombre: "Alineacion", duracionHoras: 2, activo: true, jefeMecanicoId: "j1" });
|
servicios.push({ id: "s2", nombre: "Alineacion", duracionHoras: 2, activo: true, jefeMecanicoId: "j1" });
|
||||||
|
// Trabajo global del taller (lo carga el admin): lo ofrece cualquier jefe.
|
||||||
|
servicios.push({ id: "s3", nombre: "Revision general", duracionHoras: 1, activo: true, jefeMecanicoId: null });
|
||||||
clientes.push({
|
clientes.push({
|
||||||
id: "c1",
|
id: "c1",
|
||||||
nombre: "Juan",
|
nombre: "Juan",
|
||||||
@@ -187,6 +193,7 @@ async function main() {
|
|||||||
|
|
||||||
r = await env(A, "j1");
|
r = await env(A, "j1");
|
||||||
ok(/Cambio de aceite/i.test(flat(r)), "muestra trabajos del jefe elegido");
|
ok(/Cambio de aceite/i.test(flat(r)), "muestra trabajos del jefe elegido");
|
||||||
|
ok(/Revision general/i.test(flat(r)), "incluye los trabajos globales del taller");
|
||||||
ok(paso(A) === "ELEGIR_TRABAJO", "paso=ELEGIR_TRABAJO");
|
ok(paso(A) === "ELEGIR_TRABAJO", "paso=ELEGIR_TRABAJO");
|
||||||
|
|
||||||
r = await env(A, "a");
|
r = await env(A, "a");
|
||||||
@@ -225,6 +232,26 @@ async function main() {
|
|||||||
ok(/cancel/i.test(flat(r)), "cancelar resetea");
|
ok(/cancel/i.test(flat(r)), "cancelar resetea");
|
||||||
ok(paso(B) === "INICIO", "paso=INICIO tras cancelar");
|
ok(paso(B) === "INICIO", "paso=INICIO tras cancelar");
|
||||||
|
|
||||||
|
console.log("\n── Escenario D: trabajos por jefe ──");
|
||||||
|
const D = "5493518888888";
|
||||||
|
await env(D, "hola");
|
||||||
|
await env(D, "Ana");
|
||||||
|
await env(D, "Lopez");
|
||||||
|
r = await env(D, "ana@mail.com");
|
||||||
|
ok(hayBotones(r), "alta rápida hasta elegir mecánico");
|
||||||
|
r = await env(D, "j2");
|
||||||
|
ok(/Revision general/i.test(flat(r)), "un jefe sin trabajos propios igual ofrece los globales");
|
||||||
|
ok(!/Cambio de aceite/i.test(flat(r)), "NO ofrece los trabajos propios de otro jefe");
|
||||||
|
|
||||||
|
const { crearTurno } = await import("../src/lib/turnos");
|
||||||
|
const cruzado = await crearTurno({
|
||||||
|
clienteId: "c1",
|
||||||
|
jefeMecanicoId: "j2",
|
||||||
|
tipoServicioId: "s1", // trabajo propio de j1
|
||||||
|
fechaInicio: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||||||
|
});
|
||||||
|
ok(!cruzado.ok && cruzado.status === 400, "crearTurno rechaza el trabajo de otro jefe (400)");
|
||||||
|
|
||||||
console.log(`\n${fallos === 0 ? "✅ TODO OK" : `❌ ${fallos} assert(s) fallaron`}\n`);
|
console.log(`\n${fallos === 0 ? "✅ TODO OK" : `❌ ${fallos} assert(s) fallaron`}\n`);
|
||||||
process.exit(fallos === 0 ? 0 : 1);
|
process.exit(fallos === 0 ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { verificarSesion } from "@/lib/auth-admin";
|
import { verificarSesion } from "@/lib/auth-admin";
|
||||||
|
import { whereTrabajosDeJefe } from "@/lib/servicios";
|
||||||
|
|
||||||
export async function GET() {
|
// Trabajos que ofrece un jefe mecánico: los suyos + los globales del taller.
|
||||||
|
// El flujo del cliente es jefe → trabajo, así que el jefe es obligatorio.
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
const sesion = await verificarSesion("cliente_session");
|
const sesion = await verificarSesion("cliente_session");
|
||||||
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
if (!sesion) return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||||
|
|
||||||
|
const jefeMecanicoId = req.nextUrl.searchParams.get("jefeMecanicoId");
|
||||||
|
if (!jefeMecanicoId)
|
||||||
|
return NextResponse.json({ error: "jefeMecanicoId requerido" }, { status: 400 });
|
||||||
|
|
||||||
const servicios = await prisma.tipoServicio.findMany({
|
const servicios = await prisma.tipoServicio.findMany({
|
||||||
where: { activo: true },
|
where: whereTrabajosDeJefe(jefeMecanicoId),
|
||||||
orderBy: { nombre: "asc" },
|
orderBy: { nombre: "asc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { verificarSesion } from "@/lib/auth-admin";
|
import { verificarSesion } from "@/lib/auth-admin";
|
||||||
import { obtenerSlotsDisponibles } from "@/lib/scheduling";
|
import { obtenerSlotsDisponibles } from "@/lib/scheduling";
|
||||||
|
import { trabajoEsDeJefe } from "@/lib/servicios";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const sesion = await verificarSesion("cliente_session");
|
const sesion = await verificarSesion("cliente_session");
|
||||||
@@ -18,6 +19,8 @@ export async function GET(req: NextRequest) {
|
|||||||
const servicio = await prisma.tipoServicio.findUnique({ where: { id: tipoServicioId } });
|
const servicio = await prisma.tipoServicio.findUnique({ where: { id: tipoServicioId } });
|
||||||
if (!servicio || !servicio.activo)
|
if (!servicio || !servicio.activo)
|
||||||
return NextResponse.json({ error: "Servicio no encontrado" }, { status: 404 });
|
return NextResponse.json({ error: "Servicio no encontrado" }, { status: 404 });
|
||||||
|
if (!trabajoEsDeJefe(servicio, jefeMecanicoId))
|
||||||
|
return NextResponse.json({ error: "Ese trabajo no lo hace este mecánico" }, { status: 400 });
|
||||||
|
|
||||||
const slots = await obtenerSlotsDisponibles(jefeMecanicoId, servicio.duracionHoras, 60, 500);
|
const slots = await obtenerSlotsDisponibles(jefeMecanicoId, servicio.duracionHoras, 60, 500);
|
||||||
return NextResponse.json(slots);
|
return NextResponse.json(slots);
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ interface Turno {
|
|||||||
const MESES = ["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
|
const MESES = ["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
|
||||||
const DIAS_HEADER = ["Lun","Mar","Mié","Jue","Vie","Sáb","Dom"];
|
const DIAS_HEADER = ["Lun","Mar","Mié","Jue","Vie","Sáb","Dom"];
|
||||||
|
|
||||||
type Step = "servicio" | "jefe" | "calendario" | "confirmado";
|
// El cliente elige primero el mecánico y después el trabajo, porque cada jefe
|
||||||
|
// carga sus propios trabajos (además de los globales del taller).
|
||||||
|
type Step = "jefe" | "servicio" | "calendario" | "confirmado";
|
||||||
|
|
||||||
function formatDuracion(h: number) {
|
function formatDuracion(h: number) {
|
||||||
if (h < 1) return `${Math.round(h * 60)} min`;
|
if (h < 1) return `${Math.round(h * 60)} min`;
|
||||||
@@ -66,12 +68,13 @@ function TurnosContent() {
|
|||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
const autoId = params.get("autoId");
|
const autoId = params.get("autoId");
|
||||||
|
|
||||||
const [step, setStep] = useState<Step>("servicio");
|
const [step, setStep] = useState<Step>("jefe");
|
||||||
const [servicios, setServicios] = useState<TipoServicio[]>([]);
|
const [servicios, setServicios] = useState<TipoServicio[]>([]);
|
||||||
const [servicioSel, setServicioSel] = useState<TipoServicio | null>(null);
|
const [servicioSel, setServicioSel] = useState<TipoServicio | null>(null);
|
||||||
|
const [loadingServicios, setLoadingServicios] = useState(false);
|
||||||
const [jefes, setJefes] = useState<Jefe[]>([]);
|
const [jefes, setJefes] = useState<Jefe[]>([]);
|
||||||
const [jefeSel, setJefeSel] = useState<Jefe | null>(null);
|
const [jefeSel, setJefeSel] = useState<Jefe | null>(null);
|
||||||
const [loadingJefes, setLoadingJefes] = useState(false);
|
const [loadingJefes, setLoadingJefes] = useState(true);
|
||||||
const [slots, setSlots] = useState<Slot[]>([]);
|
const [slots, setSlots] = useState<Slot[]>([]);
|
||||||
const [loadingSlots, setLoadingSlots] = useState(false);
|
const [loadingSlots, setLoadingSlots] = useState(false);
|
||||||
const [misTurnos, setMisTurnos] = useState<Turno[]>([]);
|
const [misTurnos, setMisTurnos] = useState<Turno[]>([]);
|
||||||
@@ -90,7 +93,12 @@ function TurnosContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoId) { router.push("/cliente/dashboard"); return; }
|
if (!autoId) { router.push("/cliente/dashboard"); return; }
|
||||||
fetch("/api/cliente/servicios").then(r => r.json()).then(setServicios);
|
fetch("/api/cliente/jefes")
|
||||||
|
.then(r => r.json())
|
||||||
|
.then((data) => {
|
||||||
|
setJefes(Array.isArray(data) ? data : []);
|
||||||
|
setLoadingJefes(false);
|
||||||
|
});
|
||||||
fetch("/api/cliente/turnos")
|
fetch("/api/cliente/turnos")
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -110,29 +118,30 @@ function TurnosContent() {
|
|||||||
return m;
|
return m;
|
||||||
}, [slots]);
|
}, [slots]);
|
||||||
|
|
||||||
async function seleccionarServicio(s: TipoServicio) {
|
async function seleccionarJefe(j: Jefe) {
|
||||||
setServicioSel(s);
|
setJefeSel(j);
|
||||||
setJefeSel(null);
|
setServicioSel(null);
|
||||||
|
setServicios([]);
|
||||||
setSlots([]);
|
setSlots([]);
|
||||||
setDiaSeleccionado(null);
|
setDiaSeleccionado(null);
|
||||||
setSlotSel(null);
|
setSlotSel(null);
|
||||||
setLoadingJefes(true);
|
setLoadingServicios(true);
|
||||||
setStep("jefe");
|
setStep("servicio");
|
||||||
const res = await fetch("/api/cliente/jefes");
|
const res = await fetch(`/api/cliente/servicios?jefeMecanicoId=${j.id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setJefes(Array.isArray(data) ? data : []);
|
setServicios(Array.isArray(data) ? data : []);
|
||||||
setLoadingJefes(false);
|
setLoadingServicios(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seleccionarJefe(j: Jefe) {
|
async function seleccionarServicio(s: TipoServicio) {
|
||||||
if (!servicioSel) return;
|
if (!jefeSel) return;
|
||||||
setJefeSel(j);
|
setServicioSel(s);
|
||||||
setLoadingSlots(true);
|
setLoadingSlots(true);
|
||||||
setSlots([]);
|
setSlots([]);
|
||||||
setDiaSeleccionado(null);
|
setDiaSeleccionado(null);
|
||||||
setSlotSel(null);
|
setSlotSel(null);
|
||||||
setStep("calendario");
|
setStep("calendario");
|
||||||
const res = await fetch(`/api/cliente/slots?tipoServicioId=${servicioSel.id}&jefeMecanicoId=${j.id}`);
|
const res = await fetch(`/api/cliente/slots?tipoServicioId=${s.id}&jefeMecanicoId=${jefeSel.id}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setSlots(Array.isArray(data) ? data : []);
|
setSlots(Array.isArray(data) ? data : []);
|
||||||
setLoadingSlots(false);
|
setLoadingSlots(false);
|
||||||
@@ -213,14 +222,14 @@ function TurnosContent() {
|
|||||||
<div className="text-5xl">✅</div>
|
<div className="text-5xl">✅</div>
|
||||||
<h1 className="text-2xl font-bold text-white">¡Turno reservado!</h1>
|
<h1 className="text-2xl font-bold text-white">¡Turno reservado!</h1>
|
||||||
<Card className="text-left w-full max-w-sm">
|
<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>
|
|
||||||
{turnoCreado.jefeMecanico && (
|
{turnoCreado.jefeMecanico && (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-gray-500 mb-1">Jefe mecánico</p>
|
<p className="text-sm text-gray-500 mb-1">Jefe mecánico</p>
|
||||||
<p className="font-semibold mb-3">{turnoCreado.jefeMecanico.nombre}</p>
|
<p className="font-semibold mb-3">{turnoCreado.jefeMecanico.nombre}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<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="text-sm text-gray-500 mb-1">Fecha</p>
|
||||||
<p className="font-semibold">{new Date(turnoCreado.fechaInicio).toLocaleDateString("es-AR", {
|
<p className="font-semibold">{new Date(turnoCreado.fechaInicio).toLocaleDateString("es-AR", {
|
||||||
timeZone: "America/Argentina/Buenos_Aires", weekday: "long", day: "numeric", month: "long"
|
timeZone: "America/Argentina/Buenos_Aires", weekday: "long", day: "numeric", month: "long"
|
||||||
@@ -236,33 +245,46 @@ function TurnosContent() {
|
|||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{step === "calendario" ? (
|
{step === "calendario" ? (
|
||||||
<Button variant="ghost" className="px-2" onClick={() => setStep("jefe")}>←</Button>
|
|
||||||
) : step === "jefe" ? (
|
|
||||||
<Button variant="ghost" className="px-2" onClick={() => setStep("servicio")}>←</Button>
|
<Button variant="ghost" className="px-2" onClick={() => setStep("servicio")}>←</Button>
|
||||||
|
) : step === "servicio" ? (
|
||||||
|
<Button variant="ghost" className="px-2" onClick={() => setStep("jefe")}>←</Button>
|
||||||
) : (
|
) : (
|
||||||
<Link href="/cliente/dashboard"><Button variant="ghost" className="px-2">←</Button></Link>
|
<Link href="/cliente/dashboard"><Button variant="ghost" className="px-2">←</Button></Link>
|
||||||
)}
|
)}
|
||||||
<h1 className="text-xl sm:text-2xl font-bold text-white">
|
<h1 className="text-xl sm:text-2xl font-bold text-white">
|
||||||
{step === "servicio" && "Elegí el servicio"}
|
|
||||||
{step === "jefe" && "Elegí el jefe mecánico"}
|
{step === "jefe" && "Elegí el jefe mecánico"}
|
||||||
{step === "calendario" && `${servicioSel?.nombre}${jefeSel ? ` · ${jefeSel.nombre}` : ""}`}
|
{step === "servicio" && `Trabajos de ${jefeSel?.nombre}`}
|
||||||
|
{step === "calendario" && `${jefeSel?.nombre}${servicioSel ? ` · ${servicioSel.nombre}` : ""}`}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* STEP: servicio */}
|
{/* STEP: jefe */}
|
||||||
{step === "servicio" && (
|
{step === "jefe" && (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{servicios.map(s => (
|
{loadingJefes ? (
|
||||||
<button key={s.id} onClick={() => seleccionarServicio(s)}
|
<p className="text-center text-sm text-gray-200">Cargando jefes...</p>
|
||||||
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">
|
) : jefes.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<p className="text-sm text-gray-600 text-center">
|
||||||
|
No hay jefes mecánicos disponibles en este momento. Intentá más tarde.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
jefes.map((j) => (
|
||||||
|
<button
|
||||||
|
key={j.id}
|
||||||
|
onClick={() => seleccionarJefe(j)}
|
||||||
|
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>
|
<div>
|
||||||
<p className="font-medium text-gray-800">{s.nombre}</p>
|
<p className="font-medium text-gray-800">{j.nombre}</p>
|
||||||
<p className="text-sm text-gray-500 mt-0.5">Duración: {formatDuracion(s.duracionHoras)}</p>
|
<p className="text-sm text-gray-500 mt-0.5">Ver sus trabajos</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-400 text-lg">→</span>
|
<span className="text-gray-400 text-lg">→</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!loadingTurnos && misTurnos.length > 0 && (
|
{!loadingTurnos && misTurnos.length > 0 && (
|
||||||
@@ -288,27 +310,24 @@ function TurnosContent() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* STEP: jefe */}
|
{/* STEP: servicio */}
|
||||||
{step === "jefe" && (
|
{step === "servicio" && (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="grid gap-3">
|
||||||
{loadingJefes ? (
|
{loadingServicios ? (
|
||||||
<p className="text-center text-sm text-gray-200">Cargando jefes...</p>
|
<p className="text-center text-sm text-gray-200">Cargando trabajos...</p>
|
||||||
) : jefes.length === 0 ? (
|
) : servicios.length === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<p className="text-sm text-gray-600 text-center">
|
<p className="text-sm text-gray-600 text-center">
|
||||||
No hay jefes mecánicos disponibles en este momento. Intentá más tarde.
|
{jefeSel?.nombre} todavía no cargó trabajos. Elegí otro mecánico.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
jefes.map((j) => (
|
servicios.map(s => (
|
||||||
<button
|
<button key={s.id} onClick={() => seleccionarServicio(s)}
|
||||||
key={j.id}
|
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">
|
||||||
onClick={() => seleccionarJefe(j)}
|
|
||||||
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>
|
<div>
|
||||||
<p className="font-medium text-gray-800">{j.nombre}</p>
|
<p className="font-medium text-gray-800">{s.nombre}</p>
|
||||||
<p className="text-sm text-gray-500 mt-0.5">Ver disponibilidad</p>
|
<p className="text-sm text-gray-500 mt-0.5">Duración: {formatDuracion(s.duracionHoras)}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-gray-400 text-lg">→</span>
|
<span className="text-gray-400 text-lg">→</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Prisma, TipoServicio } from "@prisma/client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regla única de qué trabajos ofrece un jefe mecánico.
|
||||||
|
*
|
||||||
|
* Los jefes cargan sus propios trabajos desde `/jefe/servicios` (`jefeMecanicoId` = su id),
|
||||||
|
* y el admin puede cargar trabajos globales del taller (`jefeMecanicoId` = null) que ofrece
|
||||||
|
* cualquiera. El cliente elige primero el mecánico y después el trabajo, así que la lista
|
||||||
|
* siempre es: los del jefe + los globales.
|
||||||
|
*/
|
||||||
|
export function whereTrabajosDeJefe(jefeMecanicoId: string): Prisma.TipoServicioWhereInput {
|
||||||
|
return {
|
||||||
|
activo: true,
|
||||||
|
OR: [{ jefeMecanicoId }, { jefeMecanicoId: null }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Un trabajo propio de otro jefe no se puede reservar con este. Los globales sí. */
|
||||||
|
export function trabajoEsDeJefe(
|
||||||
|
servicio: Pick<TipoServicio, "jefeMecanicoId">,
|
||||||
|
jefeMecanicoId: string
|
||||||
|
): boolean {
|
||||||
|
return servicio.jefeMecanicoId === null || servicio.jefeMecanicoId === jefeMecanicoId;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { prisma } from "./prisma";
|
import { prisma } from "./prisma";
|
||||||
import { slotEstaDisponible } from "./scheduling";
|
import { slotEstaDisponible } from "./scheduling";
|
||||||
|
import { trabajoEsDeJefe } from "./servicios";
|
||||||
|
|
||||||
export interface CrearTurnoInput {
|
export interface CrearTurnoInput {
|
||||||
clienteId: string;
|
clienteId: string;
|
||||||
@@ -30,6 +31,10 @@ export async function crearTurno(input: CrearTurnoInput): Promise<CrearTurnoResu
|
|||||||
if (!jefe || !jefe.activo)
|
if (!jefe || !jefe.activo)
|
||||||
return { ok: false, error: "Jefe mecánico no disponible", status: 404 };
|
return { ok: false, error: "Jefe mecánico no disponible", status: 404 };
|
||||||
|
|
||||||
|
// Un trabajo propio de otro jefe no se reserva con éste (los globales sí).
|
||||||
|
if (!trabajoEsDeJefe(servicio, jefeMecanicoId))
|
||||||
|
return { ok: false, error: "Ese trabajo no lo hace este mecánico", status: 400 };
|
||||||
|
|
||||||
const fin = new Date(fechaInicio.getTime() + servicio.duracionHoras * 60 * 60 * 1000);
|
const fin = new Date(fechaInicio.getTime() + servicio.duracionHoras * 60 * 60 * 1000);
|
||||||
|
|
||||||
const disponible = await slotEstaDisponible(fechaInicio, fin, jefeMecanicoId);
|
const disponible = await slotEstaDisponible(fechaInicio, fin, jefeMecanicoId);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { prisma } from "./prisma";
|
|||||||
import { obtenerSlotsDisponibles } from "./scheduling";
|
import { obtenerSlotsDisponibles } from "./scheduling";
|
||||||
import { crearTurno } from "./turnos";
|
import { crearTurno } from "./turnos";
|
||||||
import { normalizarTelefono } from "./telefono";
|
import { normalizarTelefono } from "./telefono";
|
||||||
|
import { trabajoEsDeJefe, whereTrabajosDeJefe } from "./servicios";
|
||||||
|
|
||||||
// ── Tipos de respuesta que el bot le pide al canal (F0 los traduce a la API de Meta) ──
|
// ── Tipos de respuesta que el bot le pide al canal (F0 los traduce a la API de Meta) ──
|
||||||
export type BotonBot = { id: string; titulo: string };
|
export type BotonBot = { id: string; titulo: string };
|
||||||
@@ -96,7 +97,7 @@ async function pasoElegirMecanico(): Promise<{ resp: RespuestaBot[]; datos: Dato
|
|||||||
|
|
||||||
async function pasoElegirTrabajo(datos: DatosConv): Promise<{ resp: RespuestaBot[]; datos: DatosConv; paso: string }> {
|
async function pasoElegirTrabajo(datos: DatosConv): Promise<{ resp: RespuestaBot[]; datos: DatosConv; paso: string }> {
|
||||||
const servicios = await prisma.tipoServicio.findMany({
|
const servicios = await prisma.tipoServicio.findMany({
|
||||||
where: { jefeMecanicoId: datos.jefeMecanicoId, activo: true },
|
where: whereTrabajosDeJefe(datos.jefeMecanicoId!),
|
||||||
orderBy: { nombre: "asc" },
|
orderBy: { nombre: "asc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -303,7 +304,7 @@ export async function procesarMensaje(telefonoRaw: string, textoRaw: string): Pr
|
|||||||
if (!op)
|
if (!op)
|
||||||
return guardar(conv.id, "ELEGIR_TRABAJO", datos, [{ tipo: "texto", texto: "No entendí. Respondé con la letra del trabajo." }]);
|
return guardar(conv.id, "ELEGIR_TRABAJO", datos, [{ tipo: "texto", texto: "No entendí. Respondé con la letra del trabajo." }]);
|
||||||
const servicio = await prisma.tipoServicio.findUnique({ where: { id: op.id } });
|
const servicio = await prisma.tipoServicio.findUnique({ where: { id: op.id } });
|
||||||
if (!servicio || !servicio.activo) {
|
if (!servicio || !servicio.activo || !trabajoEsDeJefe(servicio, datos.jefeMecanicoId!)) {
|
||||||
const m = await pasoElegirTrabajo(datos);
|
const m = await pasoElegirTrabajo(datos);
|
||||||
return guardar(conv.id, m.paso, m.datos, [{ tipo: "texto", texto: "Ese trabajo ya no está disponible." }, ...m.resp]);
|
return guardar(conv.id, m.paso, m.datos, [{ tipo: "texto", texto: "Ese trabajo ya no está disponible." }, ...m.resp]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user