// Servidor de la barberia. Node puro, sin dependencias. // // Igual que db.js, tiene errores a proposito. El mas importante vive // en la ruta POST /cliente/:id/eliminar. import { createServer } from 'node:http'; import { db, log, exportarACrm } from './db.js'; const PUERTO = process.env.PORT || 3000; const esc = (v) => String(v ?? '') .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') .replaceAll('"', '"'); const CSS = ` * { box-sizing: border-box; } body { margin: 0; background: #f5f4f1; color: #1b1a17; font: 17px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; } .wrap { max-width: 860px; margin: 0 auto; padding: 2.5rem 1.5rem 5rem; } header { display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap; border-bottom: 2px solid #1b1a17; padding-bottom: 1rem; margin-bottom: 2rem; } header h1 { margin: 0; font-size: 1.6rem; letter-spacing: -.01em; } header span { color: #6f6a60; font-size: .9rem; } h2 { font-size: 1.2rem; margin: 2rem 0 .8rem; } a { color: #8a3a1e; } .card { background: #fff; border: 1px solid #ddd9d2; border-radius: 6px; padding: 1.2rem 1.4rem; margin-bottom: 1rem; } .card h3 { margin: 0 0 .3rem; font-size: 1.05rem; } .card .sub { color: #6f6a60; font-size: .9rem; } table { width: 100%; border-collapse: collapse; font-size: .95rem; } td { padding: .45rem .2rem; border-bottom: 1px solid #eeebe6; vertical-align: top; } td.k { color: #6f6a60; width: 42%; white-space: nowrap; } .pill { display: inline-block; font-size: .75rem; padding: .15em .55em; border-radius: 99px; background: #eeebe6; color: #6f6a60; } .pill.off { background: #f6dcdc; color: #8a2020; } form.danger { margin-top: 1.5rem; } button { font: inherit; font-size: .95rem; padding: .6rem 1.1rem; border-radius: 5px; border: 1px solid #8a2020; background: #8a2020; color: #fff; cursor: pointer; } button:hover { background: #6f1919; } button:focus-visible { outline: 3px solid #1b1a17; outline-offset: 2px; } .aviso { background: #e6f2e8; border: 1px solid #b7d8bf; color: #1f5c2e; padding: 1rem 1.2rem; border-radius: 6px; margin-bottom: 1.5rem; } .aviso b { display: block; margin-bottom: .2rem; } label { display: block; margin-bottom: .9rem; font-size: .92rem; } label span { display: block; color: #6f6a60; margin-bottom: .25rem; } input { font: inherit; width: 100%; padding: .5rem .65rem; border: 1px solid #ccc7bf; border-radius: 4px; background: #fff; } .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1.2rem; } @media (max-width: 620px) { .cols { grid-template-columns: 1fr; } } footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #ddd9d2; color: #6f6a60; font-size: .85rem; } `; const pagina = (titulo, cuerpo) => `
No hay clientes. Corre npm run seed.
Sin citas.
'}Completa tus datos para reservar.
`); } // [SEMBRADO 3, continuacion] Aqui esta el corazon de la demo. // El boton promete borrar todo. Lo unico que ocurre es un UPDATE de una bandera. // La fila sigue completa, y sus copias en auditoria, webhooks, el log y el CRM // ni siquiera se tocan. function eliminarCuenta(id) { db.prepare('UPDATE clientes SET activo = 0 WHERE id = ?').run(id); log(`baja logica cliente id=${id}`); return pagina('Cuenta eliminada', `Gracias por haber sido parte de Barberia El Corte.
`); } function crearCliente(datos) { const { lastInsertRowid: id } = db.prepare(` INSERT INTO clientes (nombre, apellido, rut, email, telefono, fecha_nacimiento, direccion, comuna, ocupacion, ingreso_mensual, contacto_emergencia_nombre, contacto_emergencia_tel, acepto_todo) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) `).run( datos.nombre || '', datos.apellido || '', datos.rut || '', datos.email || '', datos.telefono || '', datos.fecha_nacimiento || '', datos.direccion || '', datos.comuna || '', datos.ocupacion || '', Number(datos.ingreso_mensual) || 0, datos.contacto_emergencia_nombre || '', datos.contacto_emergencia_tel || '', datos.acepto_todo ? 1 : 0, ); const fila = db.prepare('SELECT * FROM clientes WHERE id = ?').get(id); db.prepare('INSERT INTO auditoria (tabla, accion, registro_json) VALUES (?,?,?)') .run('clientes', 'alta', JSON.stringify(fila)); db.prepare('INSERT INTO webhooks (evento, destino, payload_json) VALUES (?,?,?)') .run('cliente.creado', 'https://hooks.crm-externo.example/barberia', JSON.stringify(fila)); log(`alta cliente id=${id} rut=${datos.rut} tel=${datos.telefono} email=${datos.email}`); exportarACrm(); return id; } const leerCuerpo = (req) => new Promise((resolve) => { let bruto = ''; req.on('data', (t) => { bruto += t; }); req.on('end', () => resolve(Object.fromEntries(new URLSearchParams(bruto)))); }); const responder = (res, html, codigo = 200) => { res.writeHead(codigo, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html); }; createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); const ruta = url.pathname; if (req.method === 'GET' && ruta === '/') return responder(res, vistaIndex()); if (req.method === 'GET' && ruta === '/registro') return responder(res, vistaRegistro()); if (req.method === 'POST' && ruta === '/registro') { const id = crearCliente(await leerCuerpo(req)); res.writeHead(302, { Location: `/cliente/${id}` }); return res.end(); } const verCliente = ruta.match(/^\/cliente\/(\d+)$/); if (req.method === 'GET' && verCliente) { const html = vistaCliente(Number(verCliente[1])); return html ? responder(res, html) : responder(res, pagina('No encontrado', 'Ese cliente no existe.
'), 404); } const borrar = ruta.match(/^\/cliente\/(\d+)\/eliminar$/); if (req.method === 'POST' && borrar) { return responder(res, eliminarCuenta(Number(borrar[1]))); } responder(res, pagina('No encontrado', 'Esa pagina no existe.
'), 404); }).listen(PUERTO, () => { console.log(`Barberia El Corte corriendo en http://localhost:${PUERTO}`); });