diff --git a/server.js b/server.js index 5076fbc..43fac17 100644 --- a/server.js +++ b/server.js @@ -431,6 +431,44 @@ function signToken(user) { const accountId = user.owner_id || user.id; return jw function authMiddleware(req, res, next) { const h = req.headers.authorization || ""; const token = h.startsWith("Bearer ") ? h.slice(7) : ""; if (!token) return res.status(401).json({ ok: false, error: "No token" }); try { req.user = jwt.verify(token, JWT_SECRET); next(); } catch { return res.status(401).json({ ok: false, error: "Token inválido" }); } } function genCode6() { return String(Math.floor(100000 + Math.random() * 900000)); } + +// ========================================== +// 🛡️ ESCUDO DE TITANIO: ANTI-SOLAPAMIENTOS BBDD +// ========================================== +async function comprobarDisponibilidad(ownerId, workerId, date, time, durationMin, excludeId = null) { + if (!workerId || !date || !time) return { choca: false }; + + let [newH, newM] = time.split(':').map(Number); + let newStart = newH * 60 + newM; + let newEnd = newStart + (parseInt(durationMin) || 60); + + let query = ` + SELECT id, service_ref, raw_data->>'scheduled_time' as time, raw_data->>'duration_minutes' as dur, + raw_data->>'requested_time' as req_time, raw_data->>'appointment_status' as app_status + FROM scraped_services + WHERE owner_id = $1 AND assigned_to = $2 AND status != 'archived' + AND ( + raw_data->>'scheduled_date' = $3 OR + (raw_data->>'requested_date' = $3 AND raw_data->>'appointment_status' = 'pending') + ) + `; + let params = [ownerId, workerId, date]; + if (excludeId) { query += ` AND id != $4`; params.push(excludeId); } + + const q = await pool.query(query, params); + + for (const s of q.rows) { + let sTime = s.time || (s.app_status === 'pending' ? s.req_time : null); + if (sTime && sTime.includes(':')) { + let [sH, sM] = sTime.split(':').map(Number); + let sStart = sH * 60 + sM; + let sEnd = sStart + parseInt(s.dur || 60); + if (newStart < sEnd && newEnd > sStart) return { choca: true, ref: s.service_ref, time: sTime }; + } + } + return { choca: false }; +} + // ========================================== // 🔐 SISTEMA DE AUTENTICACIÓN (LOGIN Y SESIÓN) // ========================================== @@ -1287,8 +1325,8 @@ app.get("/public/portal/:token/slots", async (req, res) => { const targetGuildId = raw["guild_id"]; const agendaQ = await pool.query(` - SELECT raw_data->>'scheduled_date' as date, - raw_data->>'scheduled_time' as time, + SELECT COALESCE(NULLIF(raw_data->>'scheduled_date', ''), raw_data->>'requested_date') as date, + COALESCE(NULLIF(raw_data->>'scheduled_time', ''), raw_data->>'requested_time') as time, raw_data->>'duration_minutes' as duration, raw_data->>'Población' as poblacion, raw_data->>'Código Postal' as cp, @@ -1296,8 +1334,12 @@ app.get("/public/portal/:token/slots", async (req, res) => { raw_data->>'blocked_guild_id' as blocked_guild_id FROM scraped_services WHERE assigned_to = $1 - AND raw_data->>'scheduled_date' IS NOT NULL - AND raw_data->>'scheduled_date' >= CURRENT_DATE::text + AND status != 'archived' + AND ( + (raw_data->>'scheduled_date' IS NOT NULL AND raw_data->>'scheduled_date' >= CURRENT_DATE::text) + OR + (raw_data->>'appointment_status' = 'pending' AND raw_data->>'requested_date' >= CURRENT_DATE::text) + ) `, [assignedTo]); const agendaMap = {}; @@ -1432,6 +1474,14 @@ app.post("/public/portal/:token/book", async (req, res) => { const srv = serviceQ.rows[0]; const raw = srv.raw_data || {}; + // 🛡️ ESCUDO: Verificamos que el hueco siga libre (Por si 2 clientes hacen clic en el mismo milisegundo) + if (srv.assigned_to) { + const solapamiento = await comprobarDisponibilidad(ownerId, srv.assigned_to, date, time, 60, serviceId); + if (solapamiento.choca) { + return res.status(400).json({ ok: false, error: "Lo sentimos, alguien acaba de reservar ese mismo hueco hace unos instantes. Por favor, elige otro horario." }); + } + } + // Grabamos la solicitud en el jsonb para que el admin la vea en agenda.html raw.requested_date = date; raw.requested_time = time;