diff --git a/server.js b/server.js index 01edc55..d442ddc 100644 --- a/server.js +++ b/server.js @@ -29,25 +29,29 @@ const pool = new Pool({ }); // ========================================== -// 🧠 AUTO-ACTUALIZACIÓN DB +// 🧠 AUTO-ACTUALIZACIÓN DB (ESTADOS Y LOGS) // ========================================== async function autoUpdateDB() { const client = await pool.connect(); try { console.log("🔄 Revisando estructura de base de datos..."); + // 1. Tablas Base await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, full_name TEXT NOT NULL, phone TEXT NOT NULL, - email TEXT NOT NULL, -- MODIFICADO: YA NO ES UNIQUE GLOBALMENTE + email TEXT NOT NULL, dni TEXT, address TEXT, password_hash TEXT NOT NULL, is_verified BOOLEAN DEFAULT FALSE, + owner_id INT, + role TEXT DEFAULT 'operario', created_at TIMESTAMP DEFAULT NOW() ); + CREATE TABLE IF NOT EXISTS login_codes ( id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id) ON DELETE CASCADE, @@ -58,51 +62,112 @@ async function autoUpdateDB() { expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); + CREATE TABLE IF NOT EXISTS guilds ( id SERIAL PRIMARY KEY, + owner_id INT REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); + CREATE TABLE IF NOT EXISTS user_guilds ( user_id INT REFERENCES users(id) ON DELETE CASCADE, guild_id INT REFERENCES guilds(id) ON DELETE CASCADE, PRIMARY KEY (user_id, guild_id) ); - CREATE TABLE IF NOT EXISTS services ( + + CREATE TABLE IF NOT EXISTS companies ( id SERIAL PRIMARY KEY, - user_id INT REFERENCES users(id) ON DELETE CASCADE, - title TEXT NOT NULL, - client_name TEXT, + owner_id INT REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + cif TEXT, + email TEXT, phone TEXT, address TEXT, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS clients ( + id SERIAL PRIMARY KEY, + owner_id INT REFERENCES users(id) ON DELETE CASCADE, + full_name TEXT NOT NULL, + phone TEXT NOT NULL, + email TEXT, + addresses JSONB DEFAULT '[]', notes TEXT, created_at TIMESTAMP DEFAULT NOW() ); `); - // Parches y correcciones de restricciones (Multitenancy) + // 2. NUEVO: TABLA DE ESTADOS DE SERVICIO + await client.query(` + CREATE TABLE IF NOT EXISTS service_statuses ( + id SERIAL PRIMARY KEY, + owner_id INT REFERENCES users(id) ON DELETE CASCADE, + name TEXT NOT NULL, -- Ej: Pendiente, En Proceso, Completado + color TEXT DEFAULT 'gray', -- gray, blue, green, red, yellow + is_default BOOLEAN DEFAULT FALSE, -- Estado inicial al crear servicio + is_final BOOLEAN DEFAULT FALSE, -- Si es estado final (cierra el servicio) + created_at TIMESTAMP DEFAULT NOW() + ); + `); + + // 3. TABLA SERVICIOS (Con FK a status) + await client.query(` + CREATE TABLE IF NOT EXISTS services ( + id SERIAL PRIMARY KEY, + owner_id INT REFERENCES users(id) ON DELETE CASCADE, + client_id INT REFERENCES clients(id) ON DELETE SET NULL, + status_id INT REFERENCES service_statuses(id) ON DELETE SET NULL, -- NUEVO: ID del estado + title TEXT, + description TEXT, + contact_phone TEXT NOT NULL, + contact_name TEXT NOT NULL, + address TEXT NOT NULL, + email TEXT, + scheduled_date DATE DEFAULT CURRENT_DATE, + scheduled_time TIME DEFAULT CURRENT_TIME, + duration_minutes INT DEFAULT 30, + is_urgent BOOLEAN DEFAULT FALSE, + is_company BOOLEAN DEFAULT FALSE, + company_id INT REFERENCES companies(id) ON DELETE SET NULL, + company_ref TEXT, + internal_notes TEXT, + client_notes TEXT, + closed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() + ); + `); + + // 4. NUEVO: TABLA LOGS DE ESTADOS (TRAZABILIDAD) + await client.query(` + CREATE TABLE IF NOT EXISTS service_logs ( + id SERIAL PRIMARY KEY, + service_id INT REFERENCES services(id) ON DELETE CASCADE, + user_id INT REFERENCES users(id) ON DELETE SET NULL, -- Quién hizo el cambio + old_status_id INT REFERENCES service_statuses(id), + new_status_id INT REFERENCES service_statuses(id), + comment TEXT, -- Comentario del cambio (ej: "Cliente no estaba") + created_at TIMESTAMP DEFAULT NOW() + ); + `); + + // 5. PARCHES Y DATOS POR DEFECTO try { await client.query(`ALTER TABLE users DROP CONSTRAINT IF EXISTS users_phone_key`); } catch (e) {} - try { await client.query(`ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key`); } catch (e) {} // NUEVO: Elimina restricción global de email + try { await client.query(`ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key`); } catch (e) {} await client.query(` DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='role') THEN - ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'operario'; - END IF; - IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='owner_id') THEN - ALTER TABLE users ADD COLUMN owner_id INT; - END IF; - IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='guilds' AND column_name='owner_id') THEN - ALTER TABLE guilds ADD COLUMN owner_id INT REFERENCES users(id) ON DELETE CASCADE; - END IF; - IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='owner_id') THEN - ALTER TABLE services ADD COLUMN owner_id INT REFERENCES users(id) ON DELETE CASCADE; - END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='status_id') THEN + ALTER TABLE services ADD COLUMN status_id INT REFERENCES service_statuses(id) ON DELETE SET NULL; + END IF; END $$; `); - try { await client.query(`ALTER TABLE guilds DROP CONSTRAINT IF EXISTS guilds_name_key`); } catch (e) {} - console.log("✅ DB Sincronizada."); + // Crear estados por defecto para cada cuenta nueva (Esto se haría al registrarse, pero lo simulamos aquí) + // Nota: Como es multitenant, cada usuario crea sus estados. Para simplificar, asumiremos que el frontend pide crear los defaults si no existen. + + console.log("✅ DB Sincronizada (Estados y Logs listos)."); } catch (e) { console.error("❌ Error DB:", e); } finally { client.release(); } } @@ -113,16 +178,10 @@ function normalizePhone(phone) { if (!p.startsWith("+") && /^[6789]\d{8}$/.test(p)) return "+34" + p; return p; } -function genCode6() { return String(Math.floor(100000 + Math.random() * 900000)); } - function signToken(user) { const accountId = user.owner_id || user.id; - return jwt.sign( - { sub: user.id, email: user.email, phone: user.phone, role: user.role || 'operario', accountId: accountId }, - JWT_SECRET, { expiresIn: "30d" } - ); + return jwt.sign({ sub: user.id, email: user.email, phone: user.phone, role: user.role || 'operario', accountId }, JWT_SECRET, { expiresIn: "30d" }); } - function authMiddleware(req, res, next) { const h = req.headers.authorization || ""; const token = h.startsWith("Bearer ") ? h.slice(7) : ""; @@ -131,286 +190,147 @@ function authMiddleware(req, res, next) { catch { return res.status(401).json({ ok: false, error: "Token inválido" }); } } -async function sendWhatsAppCode(phone, code) { - if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) { - console.log("⚠️ Evolution no configurado. Código:", code); - return; - } - const url = `${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${EVOLUTION_INSTANCE}`; - await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY }, - body: JSON.stringify({ number: phone.replace("+", ""), text: `🔐 Código IntegraRepara: *${code}*` }) - }).catch(console.error); -} +// ========================= +// RUTAS PRINCIPALES (AUTH, ETC.) +// ========================= +app.post("/auth/login", async (req, res) => { /* ... (Copia tu login actual) ... */ }); +// (Mantén tus rutas de auth, usuarios y gremios iguales) // ========================= -// RUTAS DE AUTENTICACIÓN +// API ESTADOS (STATUSES) // ========================= - -// REGISTRO -app.post("/auth/register", async (req, res) => { - const client = await pool.connect(); - try { - const { fullName, phone, address, dni, email, password } = req.body; - const p = normalizePhone(phone); - if (!fullName || !p || !email || !password) return res.status(400).json({ ok: false, error: "Faltan datos" }); - - const passwordHash = await bcrypt.hash(password, 10); - await client.query('BEGIN'); - - const checkUser = await client.query("SELECT * FROM users WHERE email = $1 OR phone = $2", [email, p]); - let userId; - - if (checkUser.rowCount > 0) { - const existing = checkUser.rows[0]; - if (existing.is_verified) { - await client.query('ROLLBACK'); - return res.status(409).json({ ok: false, error: "Usuario ya registrado." }); - } - await client.query("UPDATE users SET full_name=$1, address=$2, dni=$3, password_hash=$4 WHERE id=$5", [fullName, address, dni, passwordHash, existing.id]); - userId = existing.id; - } else { - const insert = await client.query( - "INSERT INTO users (full_name, phone, address, dni, email, password_hash, role, owner_id) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL) RETURNING id", - [fullName, p, address, dni, email, passwordHash] - ); - userId = insert.rows[0].id; - } - - const code = genCode6(); - const codeHash = await bcrypt.hash(code, 10); - const expiresAt = new Date(Date.now() + 10 * 60 * 1000); - - await client.query("DELETE FROM login_codes WHERE user_id = $1", [userId]); - await client.query("INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1, $2, $3, $4)", [userId, p, codeHash, expiresAt]); - await sendWhatsAppCode(p, code); - - await client.query('COMMIT'); - res.json({ ok: true, phone: p }); - } catch (e) { - await client.query('ROLLBACK'); - res.status(500).json({ ok: false, error: "Error server" }); - } finally { client.release(); } -}); - -// VERIFICACIÓN & LOGIN -app.post("/auth/verify", async (req, res) => { - try { - const { phone, code } = req.body; - const p = normalizePhone(phone); - const q = await pool.query(`SELECT lc.*, u.id as uid, u.email, u.role, u.owner_id FROM login_codes lc JOIN users u ON lc.user_id = u.id WHERE lc.phone=$1 AND lc.consumed_at IS NULL AND lc.expires_at > NOW() ORDER BY lc.created_at DESC LIMIT 1`, [p]); - if (q.rowCount === 0) return res.status(400).json({ ok: false, error: "Código inválido" }); - const row = q.rows[0]; - if (!(await bcrypt.compare(String(code), row.code_hash))) return res.status(400).json({ ok: false, error: "Incorrecto" }); - await pool.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1", [row.id]); - await pool.query("UPDATE users SET is_verified=TRUE WHERE id=$1", [row.uid]); - res.json({ ok: true, token: signToken({ id: row.uid, email: row.email, phone: p, role: row.role, owner_id: row.owner_id }) }); - } catch (e) { res.status(500).json({ ok: false, error: "Error verify" }); } -}); - -app.post("/auth/login", async (req, res) => { - try { - const { email, password } = req.body; - // Buscamos TODOS los usuarios con ese email (puede haber varios en distintas empresas) - const q = await pool.query("SELECT * FROM users WHERE email=$1", [email]); - - if (q.rowCount === 0) return res.status(401).json({ ok: false, error: "Datos incorrectos" }); - - // Probamos la contraseña con cada usuario encontrado - let user = null; - for (const u of q.rows) { - if (await bcrypt.compare(password, u.password_hash)) { - user = u; - break; // ¡Encontrado el correcto! +app.get("/statuses", authMiddleware, async (req, res) => { + try { + let q = await pool.query("SELECT * FROM service_statuses WHERE owner_id=$1 ORDER BY id ASC", [req.user.accountId]); + + // Si no tiene estados, creamos los por defecto + if (q.rowCount === 0) { + const defaults = [ + { name: 'Pendiente', color: 'gray', def: true, fin: false }, + { name: 'En Proceso', color: 'blue', def: false, fin: false }, + { name: 'Completado', color: 'green', def: false, fin: true }, + { name: 'Cancelado', color: 'red', def: false, fin: true } + ]; + for (const s of defaults) { + await pool.query( + "INSERT INTO service_statuses (owner_id, name, color, is_default, is_final) VALUES ($1, $2, $3, $4, $5)", + [req.user.accountId, s.name, s.color, s.def, s.fin] + ); + } + q = await pool.query("SELECT * FROM service_statuses WHERE owner_id=$1 ORDER BY id ASC", [req.user.accountId]); } - } - - if (!user) return res.status(401).json({ ok: false, error: "Datos incorrectos" }); - res.json({ ok: true, token: signToken(user) }); - } catch(e) { console.error(e); res.status(500).json({ ok: false, error: "Error login" }); } + res.json({ ok: true, statuses: q.rows }); + } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); -// RECUPERAR CONTRASEÑA -app.post("/auth/forgot-password", async (req, res) => { - try { - const { dni, phone } = req.body; - const p = normalizePhone(phone); - const userQuery = await pool.query("SELECT id FROM users WHERE dni = $1 AND phone = $2", [dni, p]); - - if (userQuery.rowCount === 0) { - return res.status(404).json({ ok: false, error: "No coinciden los datos." }); - } - - const userId = userQuery.rows[0].id; - const code = genCode6(); - const codeHash = await bcrypt.hash(code, 10); - const expiresAt = new Date(Date.now() + 10 * 60 * 1000); - - await pool.query("INSERT INTO login_codes (user_id, phone, code_hash, purpose, expires_at) VALUES ($1, $2, $3, 'password_reset', $4)", - [userId, p, codeHash, expiresAt]); - - await sendWhatsAppCode(p, code); - res.json({ ok: true, msg: "Código enviado" }); - - } catch (e) { - console.error(e); - res.status(500).json({ ok: false, error: "Error interno" }); - } -}); - -app.post("/auth/reset-password", async (req, res) => { - const client = await pool.connect(); - try { - const { phone, code, newPassword } = req.body; - const p = normalizePhone(phone); - - const codeQuery = await client.query(` - SELECT lc.*, u.id as uid - FROM login_codes lc - JOIN users u ON lc.user_id = u.id - WHERE lc.phone=$1 - AND lc.purpose='password_reset' - AND lc.consumed_at IS NULL - AND lc.expires_at > NOW() - ORDER BY lc.created_at DESC LIMIT 1 - `, [p]); - - if (codeQuery.rowCount === 0) return res.status(400).json({ ok: false, error: "Código inválido o expirado" }); - - const row = codeQuery.rows[0]; - const valid = await bcrypt.compare(String(code), row.code_hash); - if (!valid) return res.status(400).json({ ok: false, error: "Código incorrecto" }); - - const newHash = await bcrypt.hash(newPassword, 10); - - await client.query('BEGIN'); - await client.query("UPDATE users SET password_hash=$1 WHERE id=$2", [newHash, row.uid]); - await client.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1", [row.id]); - await client.query('COMMIT'); - - res.json({ ok: true, msg: "Contraseña actualizada" }); - - } catch (e) { - await client.query('ROLLBACK'); - console.error(e); - res.status(500).json({ ok: false, error: "Error al cambiar contraseña" }); - } finally { client.release(); } -}); - - // ========================= -// RUTAS PROTEGIDAS +// API SERVICIOS (ACTUALIZADA) // ========================= +// GET SERVICIOS (Con Estado actual) app.get("/services", authMiddleware, async (req, res) => { try { - const q = await pool.query("SELECT * FROM services WHERE owner_id=$1 ORDER BY created_at DESC", [req.user.accountId]); + const q = await pool.query(` + SELECT s.*, c.name as company_name, st.name as status_name, st.color as status_color + FROM services s + LEFT JOIN companies c ON s.company_id = c.id + LEFT JOIN service_statuses st ON s.status_id = st.id + WHERE s.owner_id=$1 + ORDER BY s.created_at DESC`, + [req.user.accountId] + ); res.json({ ok: true, services: q.rows }); } catch (e) { res.status(500).json({ ok: false, error: "Error servicios" }); } }); -app.get("/guilds", authMiddleware, async (req, res) => { - try { - const q = await pool.query("SELECT * FROM guilds WHERE owner_id=$1 ORDER BY name ASC", [req.user.accountId]); - res.json({ ok: true, guilds: q.rows }); - } catch (e) { res.status(500).json({ ok: false, error: e.message }); } +// GET HISTORIAL DE UN SERVICIO +app.get("/services/:id/logs", authMiddleware, async (req, res) => { + try { + const q = await pool.query(` + SELECT l.*, u.full_name as user_name, s1.name as old_status, s2.name as new_status, s2.color as new_color + FROM service_logs l + LEFT JOIN users u ON l.user_id = u.id + LEFT JOIN service_statuses s1 ON l.old_status_id = s1.id + LEFT JOIN service_statuses s2 ON l.new_status_id = s2.id + WHERE l.service_id = $1 + ORDER BY l.created_at DESC + `, [req.params.id]); + res.json({ ok: true, logs: q.rows }); + } catch (e) { res.status(500).json({ ok: false }); } }); -app.post("/guilds", authMiddleware, async (req, res) => { - try { - const { name } = req.body; - if (!name) return res.status(400).json({ ok: false, error: "Falta nombre" }); - const q = await pool.query("INSERT INTO guilds (name, owner_id) VALUES ($1, $2) RETURNING *", [name.toUpperCase(), req.user.accountId]); - res.json({ ok: true, guild: q.rows[0] }); - } catch (e) { res.status(500).json({ ok: false, error: "Gremio duplicado" }); } -}); - -app.delete("/guilds/:id", authMiddleware, async (req, res) => { - try { - await pool.query("DELETE FROM guilds WHERE id = $1 AND owner_id = $2", [req.params.id, req.user.accountId]); - res.json({ ok: true }); - } catch (e) { res.status(500).json({ ok: false, error: e.message }); } -}); - -// ADMIN USERS -app.get("/admin/users", authMiddleware, async (req, res) => { - try { - const q = await pool.query(` - SELECT u.id, u.full_name, u.email, u.phone, u.role, COALESCE(json_agg(g.id) FILTER (WHERE g.id IS NOT NULL), '[]') as guilds - FROM users u LEFT JOIN user_guilds ug ON u.id = ug.user_id LEFT JOIN guilds g ON ug.guild_id = g.id - WHERE u.owner_id = $1 GROUP BY u.id ORDER BY u.id DESC - `, [req.user.accountId]); - res.json({ ok: true, users: q.rows }); - } catch (e) { res.status(500).json({ ok: false, error: "Error listar users" }); } -}); - -app.post("/admin/users", authMiddleware, async (req, res) => { - const client = await pool.connect(); - try { - const { fullName, email, password, role, guilds, phone } = req.body; - if (!email || !password || !fullName || !phone) return res.status(400).json({ ok: false, error: "Faltan datos" }); - const p = normalizePhone(phone); - const passwordHash = await bcrypt.hash(password, 10); - - // NUEVO: Verificamos duplicado SOLO dentro de mi empresa - const checkDup = await client.query( - "SELECT id FROM users WHERE (phone=$1 OR email=$2) AND owner_id=$3", - [p, email, req.user.accountId] - ); - if (checkDup.rowCount > 0) { - return res.status(400).json({ ok: false, error: "❌ Este empleado ya existe en tu empresa (Email o Teléfono)." }); - } - - await client.query('BEGIN'); - const insert = await client.query( - `INSERT INTO users (full_name, email, password_hash, role, phone, is_verified, owner_id) VALUES ($1, $2, $3, $4, $5, TRUE, $6) RETURNING id`, - [fullName, email, passwordHash, role || 'operario', p, req.user.accountId] - ); - const userId = insert.rows[0].id; - if (guilds && Array.isArray(guilds)) { - for (const guildId of guilds) await client.query("INSERT INTO user_guilds (user_id, guild_id) VALUES ($1, $2)", [userId, guildId]); - } - await client.query('COMMIT'); - res.json({ ok: true, msg: "Usuario creado" }); - } catch (e) { - await client.query('ROLLBACK'); - // El DB ya no dará error 23505 por email/phone global porque eliminamos la restricción - console.error(e); - res.status(500).json({ ok: false, error: "Error creando usuario" }); - } finally { client.release(); } -}); - -app.put("/admin/users/:id", authMiddleware, async (req, res) => { +// CAMBIAR ESTADO SERVICIO +app.put("/services/:id/status", authMiddleware, async (req, res) => { const client = await pool.connect(); try { - const userId = req.params.id; - const { fullName, email, phone, role, guilds, password } = req.body; - const p = normalizePhone(phone); + const { status_id, comment } = req.body; + const serviceId = req.params.id; + await client.query('BEGIN'); - const check = await client.query("SELECT id FROM users WHERE id=$1 AND owner_id=$2", [userId, req.user.accountId]); - if(check.rowCount === 0) throw new Error("No encontrado"); - if(password) { - const hash = await bcrypt.hash(password, 10); - await client.query("UPDATE users SET full_name=$1, email=$2, phone=$3, role=$4, password_hash=$5 WHERE id=$6", [fullName, email, p, role, hash, userId]); - } else { - await client.query("UPDATE users SET full_name=$1, email=$2, phone=$3, role=$4 WHERE id=$5", [fullName, email, p, role, userId]); - } - if (guilds && Array.isArray(guilds)) { - await client.query("DELETE FROM user_guilds WHERE user_id=$1", [userId]); - for (const guildId of guilds) await client.query("INSERT INTO user_guilds (user_id, guild_id) VALUES ($1, $2)", [userId, guildId]); - } + + // 1. Obtener estado actual + const current = await client.query("SELECT status_id FROM services WHERE id=$1", [serviceId]); + const oldStatusId = current.rows[0].status_id; + + // 2. Actualizar servicio + await client.query("UPDATE services SET status_id=$1 WHERE id=$2", [status_id, serviceId]); + + // 3. Insertar Log + await client.query(` + INSERT INTO service_logs (service_id, user_id, old_status_id, new_status_id, comment) + VALUES ($1, $2, $3, $4, $5) + `, [serviceId, req.user.sub, oldStatusId, status_id, comment || "Cambio de estado manual"]); + await client.query('COMMIT'); - res.json({ ok: true, msg: "Actualizado" }); - } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false, error: "Error updating" }); } finally { client.release(); } + res.json({ ok: true }); + } catch (e) { + await client.query('ROLLBACK'); + res.status(500).json({ ok: false }); + } finally { client.release(); } }); -app.delete("/admin/users/:id", authMiddleware, async (req, res) => { +// CREAR SERVICIO (Asigna estado por defecto) +app.post("/services", authMiddleware, async (req, res) => { + const client = await pool.connect(); try { - const result = await pool.query("DELETE FROM users WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]); - if (result.rowCount === 0) return res.status(404).json({ ok: false, error: "No encontrado" }); + const { phone, name, address, email, description, scheduled_date, scheduled_time, duration, is_urgent, is_company, company_id, company_ref, internal_notes, client_notes } = req.body; + const p = normalizePhone(phone); + + await client.query('BEGIN'); + + // Buscar estado por defecto + const defStatus = await client.query("SELECT id FROM service_statuses WHERE owner_id=$1 AND is_default=TRUE LIMIT 1", [req.user.accountId]); + const statusId = defStatus.rows[0]?.id; + + // ... (Lógica de Cliente igual que antes) ... + // Simplificado para brevedad, aquí iría la búsqueda/creación de cliente que ya tienes. + // Asumimos clientId ya obtenido. + let clientId; + // [TU CÓDIGO DE CLIENTE AQUÍ] - Si quieres que lo repita dímelo, pero es igual al anterior. + + // Crear Servicio + const insert = await client.query(` + INSERT INTO services ( + owner_id, client_id, status_id, contact_phone, contact_name, address, email, + description, scheduled_date, scheduled_time, duration_minutes, is_urgent, + is_company, company_id, company_ref, internal_notes, client_notes, title + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + RETURNING id + `, [ + req.user.accountId, 1, statusId, p, name, address, email, // Pongo client_id=1 hardcodeado para ejemplo, usa tu lógica real + description, scheduled_date || 'NOW()', scheduled_time || 'NOW()', duration || 30, is_urgent || false, + is_company || false, company_id || null, company_ref, internal_notes, client_notes, name + " - Servicio" + ]); + + // Log inicial + await client.query(` + INSERT INTO service_logs (service_id, user_id, new_status_id, comment) + VALUES ($1, $2, $3, 'Servicio creado') + `, [insert.rows[0].id, req.user.sub, statusId]); + + await client.query('COMMIT'); res.json({ ok: true }); - } catch (e) { res.status(500).json({ ok: false, error: "Error deleting" }); } + } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } }); const port = process.env.PORT || 3000;