import { Router } from "express"; import { db } from "../db.ts"; export const bookingRouter = Router(); function publicBusiness(slug: string) { return db .prepare( `SELECT id, name, industry, currency, currency_symbol, phone, address, timezone, booking_enabled, cancel_window_hours, cancel_penalty_pct, require_deposit, deposit_pct FROM businesses WHERE slug = ? AND status = 'active'` ) .get(slug) as any; } // Public business info + services + employees bookingRouter.get("/:slug", (req, res) => { const biz = publicBusiness(req.params.slug); if (!biz) return res.status(404).json({ error: "Negocio no encontrado" }); if (!biz.booking_enabled) return res.status(403).json({ error: "Las reservas online están desactivadas" }); const services = db .prepare(`SELECT id, name, description, category, duration_min, price, color FROM services WHERE business_id = ? AND active = 1 ORDER BY category, name`) .all(biz.id); const employees = db .prepare(`SELECT id, name, role, color FROM employees WHERE business_id = ? AND active = 1 ORDER BY name`) .all(biz.id); res.json({ business: { ...biz, currency_symbol: biz.currency_symbol || "$" }, services, employees }); }); // Available slots for a service + employee on a given date bookingRouter.get("/:slug/slots", (req, res) => { const biz = publicBusiness(req.params.slug); if (!biz || !biz.booking_enabled) return res.status(404).json({ error: "No disponible" }); const serviceId = Number(req.query.service_id); const employeeId = req.query.employee_id ? Number(req.query.employee_id) : null; const date = req.query.date as string; // YYYY-MM-DD if (!serviceId || !date) return res.status(400).json({ error: "service_id y date son obligatorios" }); const service = db.prepare(`SELECT * FROM services WHERE id = ? AND business_id = ? AND active = 1`).get(serviceId, biz.id) as any; if (!service) return res.status(400).json({ error: "Servicio no válido" }); // candidate employees let candidates: number[]; if (employeeId) { const ok = db.prepare(`SELECT 1 FROM employee_services WHERE service_id = ? AND employee_id = ?`).get(serviceId, employeeId); if (!ok) return res.status(400).json({ error: "El especialista no ofrece este servicio" }); candidates = [employeeId]; } else { candidates = db.prepare(`SELECT employee_id id FROM employee_services WHERE service_id = ?`).all(serviceId).map((r: any) => r.id); if (candidates.length === 0) { const any = db.prepare(`SELECT id FROM employees WHERE business_id = ? AND active = 1 LIMIT 1`).get(biz.id) as any; candidates = any ? [any.id] : []; } } if (candidates.length === 0) return res.json({ slots: [] }); // business hours 09:00–20:00, 30-min grid; reject past times const dayStart = new Date(`${date}T09:00:00`); const dayEnd = new Date(`${date}T20:00:00`); const now = new Date(); const slots: { time: string; iso: string; employee_id: number; employee_name: string }[] = []; const dur = service.duration_min; // gather existing appointments that day for these employees const startOfDayIso = `${date}T00:00:00`; const endOfDayIso = `${date}T23:59:59`; const existing = db .prepare( `SELECT employee_id, start_at, end_at FROM appointments WHERE business_id = ? AND employee_id IN (${candidates.map(() => "?").join(",")}) AND status IN ('scheduled','completed') AND start_at >= ? AND start_at <= ?` ) .all(biz.id, ...candidates, startOfDayIso, endOfDayIso) as any[]; for (let t = dayStart.getTime(); t + dur * 60000 <= dayEnd.getTime(); t += 30 * 60000) { if (t < now.getTime() + 30 * 60000) continue; // skip past / too-soon const slotStart = t; const slotEnd = t + dur * 60000; // find an employee free in this window for (const empId of candidates) { const busy = existing.some( (b) => b.employee_id === empId && slotStart < new Date(b.end_at).getTime() && slotEnd > new Date(b.start_at).getTime() ); if (!busy) { const emp = db.prepare(`SELECT name FROM employees WHERE id = ?`).get(empId) as any; slots.push({ time: new Date(t).toLocaleTimeString("es-MX", { hour: "2-digit", minute: "2-digit", hour12: true }), iso: new Date(t).toISOString(), employee_id: empId, employee_name: emp?.name, }); break; // one free employee per slot is enough } } if (slots.length >= 40) break; } res.json({ slots, service: { id: service.id, name: service.name, price: service.price, duration_min: service.duration_min } }); }); // Create a booking from the public site (no auth): creates/finds client + appointment bookingRouter.post("/:slug/book", (req, res) => { const biz = publicBusiness(req.params.slug); if (!biz || !biz.booking_enabled) return res.status(404).json({ error: "No disponible" }); const { service_id, employee_id, start_at, client } = req.body ?? {}; if (!service_id || !start_at || !client?.name) return res.status(400).json({ error: "Faltan datos (servicio, hora o nombre)" }); const service = db.prepare(`SELECT * FROM services WHERE id = ? AND business_id = ? AND active = 1`).get(Number(service_id), biz.id) as any; if (!service) return res.status(400).json({ error: "Servicio no válido" }); const start = new Date(start_at); if (isNaN(start.getTime()) || start.getTime() < Date.now()) return res.status(400).json({ error: "Fecha inválida" }); // resolve employee let empId = employee_id ? Number(employee_id) : null; if (!empId) { const cand = db.prepare(`SELECT employee_id id FROM employee_services WHERE service_id = ? LIMIT 1`).get(Number(service_id)) as any; empId = cand?.id ?? null; } if (!empId) return res.status(400).json({ error: "Sin especialista disponible" }); // resolve client (by phone/email or create) let clientId: number | null = null; const match = client.phone ? (db.prepare(`SELECT id FROM clients WHERE business_id = ? AND phone = ?`).get(biz.id, client.phone) as any) : client.email ? (db.prepare(`SELECT id FROM clients WHERE business_id = ? AND email = ?`).get(biz.id, client.email) as any) : null; if (match) clientId = match.id; else { const created = db .prepare(`INSERT INTO clients (business_id, name, email, phone, tags) VALUES (?, ?, ?, ?, 'Online') RETURNING id`) .get(biz.id, client.name, client.email || null, client.phone || null) as any; clientId = created.id; } const endIso = new Date(start.getTime() + service.duration_min * 60000).toISOString().replace(/\.\d{3}Z$/, "Z"); const startIso = start.toISOString().replace(/\.\d{3}Z$/, "Z"); const appt = db .prepare( `INSERT INTO appointments (business_id, service_id, employee_id, client_id, start_at, end_at, status, price, notes) VALUES (?, ?, ?, ?, ?, ?, 'scheduled', ?, ?) RETURNING *` ) .get(biz.id, Number(service_id), empId, clientId, startIso, endIso, service.price, client.notes || "Reserva online") as any; res.status(201).json({ appointment: { id: appt.id, start_at: appt.start_at, end_at: appt.end_at, price: appt.price, service_name: service.name, employee_name: (db.prepare(`SELECT name FROM employees WHERE id = ?`).get(empId) as any)?.name, }, business: { name: biz.name, currency_symbol: biz.currency_symbol }, client_created: !match, }); });