Creado y levantado del workaround

This commit is contained in:
2025-08-25 17:21:27 +00:00
parent f7962f894d
commit 922da441eb
12 changed files with 3121 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# Dockerfile.dev
FROM node:22.18
# Definir variables de entorno con valores predeterminados
# ARG NODE_ENV=production
# ARG PORT=3000
RUN apt-get update
RUN apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Copia archivos de configuración primero para aprovechar el cache
COPY package*.json ./
# Instala dependencias
RUN npm i
# Copia el resto de la app
COPY . .
CMD ["npm", "run", "start"]
+1485
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "workarround",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "NODE_ENV=production node ./src/index.js",
"dev": "NODE_ENV=development npx nodemon ./src/index.js",
"test": "NODE_ENV=stage node ./src/index.js"
},
"author": "Mateo Saldain",
"license": "ISC",
"type": "module",
"devDependencies": {
"cross-env": "^10.0.0",
"nodemon": "^3.1.10"
},
"dependencies": {
"chalk": "^5.6.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-ejs-layouts": "^2.5.1",
"pg": "^8.16.3",
"pg-format": "^1.0.4"
},
"keywords": [],
"description": "Workarround para tener un MVP que llegue al verano para usarse"
}
+360
View File
@@ -0,0 +1,360 @@
// app/src/index.js
import chalk from 'chalk'; // Colores!
import express from 'express';
import expressLayouts from 'express-ejs-layouts';
import cors from 'cors';
import { Pool } from 'pg';
// Rutas
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Variables de Entorno
import dotenv, { config } from 'dotenv';
// Obtención de la ruta de la variable de entorno correspondiente a NODE_ENV
try {
if (process.env.NODE_ENV === 'development') {
dotenv.config({ path: path.resolve(__dirname, '../.env.development' )});
console.log(`Activando entorno de ->${chalk.green(` DEVELOPMENT `)}`);
} else if (process.env.NODE_ENV === 'stage') {
dotenv.config({ path: path.resolve(__dirname, '../.env.test' )});
console.log(`Activando entorno de ->${chalk.yellow(` TESTING `)}`);
} else if (process.env.NODE_ENV === 'production') {
dotenv.config({ path: path.resolve(__dirname, '../.env.production' )});
console.log(`Activando entorno de ->${chalk.red(` PRODUCTION `)}`);
}
} catch (error) {
console.log("A ocurrido un error al seleccionar el entorno. \nError: " + error);
}
// Renderiado
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'pages')));
// Configuración de conexión PostgreSQL
const dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: process.env.DB_LOCAL_PORT
};
const pool = new Pool(dbConfig);
async function verificarConexion() {
try {
const client = await pool.connect();
const res = await client.query('SELECT NOW() AS hora');
console.log(`\nConexión con la base de datos ${chalk.green(process.env.DB_NAME)} fue exitosa.`);
console.log('Fecha y hora actual de la base de datos:', res.rows[0].hora);
client.release(); // liberar el cliente de nuevo al pool
} catch (error) {
console.error('Error al conectar con la base de datos al iniciar:', error.message);
console.error(`Troubleshooting:\n1. Compruebe que las bases de datos se iniciaron correctamente.\n2. Verifique las credenciales y puertos de acceso a la base de datos.\n3. Si está conectandose a una base de datos externa a localhost, verifique las reglas del firewal de entrada y salida de ambos dispositivos.`);
}
}
// === Servir páginas estáticas ===
app.get('/',(req, res) => res.sendFile(path.join(__dirname, 'pages', 'index.html')));
app.get('/planes', async (req, res) => {
try {
const { rows: [row] } = await pool.query(
'SELECT api.get_planes_json($1) AS data;',
[true]
);
res.type('application/json').send(row.data);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error al cargar planes' });
}
});
app.post('/api/registro', async (req, res) => {
const {
nombre_empresa,
rut,
correo,
telefono,
direccion,
logo,
clave_acceso,
plan_id
} = req.body;
try {
const client = await pool.connect();
// 1. Hashear la contraseña
const hash = await bcrypt.hash(clave_acceso, 10);
// 2. Insertar el tenant
const result = await client.query(`
INSERT INTO tenant (
nombre_empresa, rut, correo, telefono, direccion, logo,
clave_acceso, plan_id, nombre_base_datos
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, 'TEMPORAL'
)
RETURNING uuid;
`, [
nombre_empresa, rut, correo, telefono, direccion, logo,
hash, plan_id
]);
const uuid = result.rows[0].uuid;
const nombre_base_datos = `tenantdb_${uuid}`.replace(/-/g, '').substring(0, 24); // ajustamos para longitud segura
// 3. Actualizar el campo nombre_base_datos
await client.query(`
UPDATE tenant SET nombre_base_datos = $1 WHERE uuid = $2
`, [nombre_base_datos, uuid]);
client.release();
return res.status(201).json({
message: 'Tenant registrado correctamente',
uuid,
nombre_base_datos
});
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Error al registrar tenant' });
}
});
app.post('/api/login', async (req, res) => {
const { correo, clave_acceso } = req.body;
try {
const client = await pool.connect();
const result = await client.query(`
SELECT uuid, clave_acceso, nombre_empresa, nombre_base_datos
FROM tenant
WHERE correo = $1 AND estado = true
`, [correo]);
client.release();
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Correo no registrado o inactivo' });
}
const tenant = result.rows[0];
const coincide = await bcrypt.compare(clave_acceso, tenant.clave_acceso);
if (!coincide) {
return res.status(401).json({ error: 'Clave incorrecta' });
}
return res.status(200).json({
message: 'Login correcto',
uuid: tenant.uuid,
nombre_empresa: tenant.nombre_empresa,
base_datos: tenant.nombre_base_datos
});
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Error al validar login' });
}
});
app.get('/roles', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'roles.html')));
app.get('/usuarios', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'usuarios.html')));
app.get('/categorias',(req, res) => res.sendFile(path.join(__dirname, 'pages', 'categorias.html')));
app.get('/productos', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'productos.html')));
// Helper de consulta con acquire/release explícito
async function q(text, params) {
const client = await pool.connect();
try {
return await client.query(text, params);
} finally {
client.release();
}
}
// === API Roles ===
// GET: listar
app.get('/api/roles', async (req, res) => {
try {
const { rows } = await q('SELECT id_rol, nombre FROM roles ORDER BY id_rol ASC');
res.json(rows);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'No se pudo listar roles' });
}
});
// POST: crear
app.post('/api/roles', async (req, res) => {
try {
const { nombre } = req.body;
if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' });
const { rows } = await q(
'INSERT INTO roles (nombre) VALUES ($1) RETURNING id_rol, nombre',
[nombre.trim()]
);
res.status(201).json(rows[0]);
} catch (e) {
console.error(e);
// Manejo de único/duplicado
if (e.code === '23505') return res.status(409).json({ error: 'El rol ya existe' });
res.status(500).json({ error: 'No se pudo crear el rol' });
}
});
// === API Usuarios ===
// GET: listar
app.get('/api/usuarios', async (req, res) => {
try {
const { rows } = await q(`
SELECT id_usuario, documento, img_perfil, nombre, apellido, correo, telefono, fec_nacimiento, activo
FROM usuarios
ORDER BY id_usuario ASC
`);
res.json(rows);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'No se pudo listar usuarios' });
}
});
// POST: crear
app.post('/api/usuarios', async (req, res) => {
try {
const { documento, nombre, apellido, correo, telefono, fec_nacimiento } = req.body;
if (!nombre || !apellido) return res.status(400).json({ error: 'Nombre y apellido requeridos' });
const { rows } = await q(`
INSERT INTO usuarios (documento, nombre, apellido, correo, telefono, fec_nacimiento)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id_usuario, documento, nombre, apellido, correo, telefono, fec_nacimiento, activo
`, [
documento || null,
nombre.trim(),
apellido.trim(),
correo || null,
telefono || null,
fec_nacimiento || null
]);
res.status(201).json(rows[0]);
} catch (e) {
console.error(e);
if (e.code === '23505') return res.status(409).json({ error: 'Documento/Correo/Teléfono ya existe' });
res.status(500).json({ error: 'No se pudo crear el usuario' });
}
});
// === API Categorías ===
// GET: listar
app.get('/api/categorias', async (req, res) => {
try {
const { rows } = await q('SELECT id_categoria, nombre, visible FROM categorias ORDER BY id_categoria ASC');
res.json(rows);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'No se pudo listar categorías' });
}
});
// POST: crear
app.post('/api/categorias', async (req, res) => {
try {
const { nombre, visible } = req.body;
if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' });
const vis = (typeof visible === 'boolean') ? visible : true;
const { rows } = await q(`
INSERT INTO categorias (nombre, visible)
VALUES ($1, $2)
RETURNING id_categoria, nombre, visible
`, [nombre.trim(), vis]);
res.status(201).json(rows[0]);
} catch (e) {
console.error(e);
if (e.code === '23505') return res.status(409).json({ error: 'La categoría ya existe' });
res.status(500).json({ error: 'No se pudo crear la categoría' });
}
});
// === API Productos ===
// GET: listar
app.get('/api/productos', async (req, res) => {
try {
const { rows } = await q(`
SELECT id_producto, nombre, img_producto, precio, activo, id_categoria
FROM productos
ORDER BY id_producto ASC
`);
res.json(rows);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'No se pudo listar productos' });
}
});
// POST: crear
app.post('/api/productos', async (req, res) => {
try {
let { nombre, id_categoria, precio } = req.body;
if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' });
id_categoria = parseInt(id_categoria, 10);
precio = parseFloat(precio);
if (!Number.isInteger(id_categoria)) return res.status(400).json({ error: 'id_categoria inválido' });
if (!(precio >= 0)) return res.status(400).json({ error: 'precio inválido' });
const { rows } = await q(`
INSERT INTO productos (nombre, id_categoria, precio)
VALUES ($1, $2, $3)
RETURNING id_producto, nombre, precio, activo, id_categoria
`, [nombre.trim(), id_categoria, precio]);
res.status(201).json(rows[0]);
} catch (e) {
console.error(e);
// FK categories / checks
if (e.code === '23503') return res.status(400).json({ error: 'La categoría no existe' });
res.status(500).json({ error: 'No se pudo crear el producto' });
}
});
// Colores personalizados
let primaryColor = chalk.hex('#'+`${process.env.COL_PRI}`);
let secondaryColor = chalk.hex('#'+`${process.env.COL_SEC}`);
// let backgroundColor = chalk.hex('#'+`${process.env.COL_BG}`);
app.use(expressLayouts);
// Iniciar servidor
app.listen( process.env.PORT, () => {
console.log(`Servidor de ${chalk.red('aplicación')} de ${secondaryColor('SuiteCoffee')} corriendo en ${chalk.yellow(`http://localhost:${process.env.PORT}\n`)}` );
console.log(chalk.grey(`Comprobando accesibilidad a la db ${chalk.green(process.env.DB_NAME)} del host ${chalk.white(`${process.env.DB_HOST}`)} ...`));
verificarConexion();
});
app.get("/health", async (req, res) => {
// Podés chequear DB aquí. 200 = healthy; 503 = not ready.
res.status(200).json({ status: "ok" });
});
+70
View File
@@ -0,0 +1,70 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Categorías</title>
</head>
<body>
<h1>Categorías</h1>
<h2>Crear categoría</h2>
<form id="form-categoria">
<label>Nombre:
<input type="text" name="nombre" required />
</label>
<label>Visible:
<select name="visible">
<option value="true" selected></option>
<option value="false">No</option>
</select>
</label>
<button type="submit">Guardar</button>
</form>
<h2>Listado</h2>
<button id="btn-recargar">Recargar</button>
<table border="1" cellpadding="6">
<thead><tr><th>ID</th><th>Nombre</th><th>Visible</th></tr></thead>
<tbody id="tbody"></tbody>
</table>
<script>
const API = '/api/categorias';
async function listar() {
const res = await fetch(API);
const data = await res.json();
const tbody = document.getElementById('tbody');
tbody.innerHTML = '';
data.forEach(c => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${c.id_categoria}</td><td>${c.nombre}</td><td>${c.visible ? 'Sí' : 'No'}</td>`;
tbody.appendChild(tr);
});
}
document.getElementById('form-categoria').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const nombre = fd.get('nombre').trim();
const visible = fd.get('visible') === 'true';
if (!nombre) return;
const res = await fetch(API, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ nombre, visible })
});
if (!res.ok) {
const err = await res.json().catch(()=>({error:'Error'}));
alert('Error: ' + (err.error || res.statusText));
return;
}
e.target.reset();
await listar();
});
document.getElementById('btn-recargar').addEventListener('click', listar);
listar();
</script>
</body>
</html>
+154
View File
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>SuiteCoffee - Autenticación</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light d-flex justify-content-center align-items-center vh-100">
<div class="card shadow p-4" style="width: 100%; max-width: 400px;">
<h4 class="text-center mb-3" id="form-title">Iniciar Sesión</h4>
<!-- Mensajes -->
<div id="mensaje" class="alert d-none" role="alert"></div>
<!-- Formulario compartido -->
<form id="formulario">
<div id="registro-extra" style="display: none;">
<div class="mb-2">
<input type="text" class="form-control" id="nombre_empresa" placeholder="Nombre de la empresa" required>
</div>
<div class="mb-2">
<input type="text" class="form-control" id="rut" placeholder="RUT (opcional)" required>
</div>
<div class="mb-2">
<input type="text" class="form-control" id="telefono" placeholder="Teléfono">
</div>
<div class="mb-2">
<input type="text" class="form-control" id="direccion" placeholder="Dirección">
</div>
<div class="mb-2">
<input type="text" class="form-control" id="logo" placeholder="Logo URL (opcional)">
</div>
<div class="mb-2">
<select class="form-select" id="plan_id" required>
<option value="">Cargando planes...</option>
</select>
</div>
</div>
<div class="mb-2">
<input type="email" class="form-control" id="correo" placeholder="Correo" required>
</div>
<div class="mb-3">
<input type="password" class="form-control" id="clave" placeholder="Contraseña" required>
</div>
<button type="submit" class="btn btn-primary w-100" id="btn-submit">Entrar</button>
</form>
<div class="text-center mt-3">
<button class="btn btn-link btn-sm" id="toggle-mode">¿No tienes cuenta? Regístrate</button>
</div>
</div>
<script>
const form = document.getElementById('formulario');
const mensaje = document.getElementById('mensaje');
const toggleModeBtn = document.getElementById('toggle-mode');
const registroExtra = document.getElementById('registro-extra');
const formTitle = document.getElementById('form-title');
const btnSubmit = document.getElementById('btn-submit');
let modoRegistro = false;
toggleModeBtn.addEventListener('click', () => {
modoRegistro = !modoRegistro;
registroExtra.style.display = modoRegistro ? 'block' : 'none';
formTitle.textContent = modoRegistro ? 'Registrar Cuenta' : 'Iniciar Sesión';
btnSubmit.textContent = modoRegistro ? 'Registrarse' : 'Entrar';
toggleModeBtn.textContent = modoRegistro ? '¿Ya tienes cuenta? Inicia sesión' : '¿No tienes cuenta? Regístrate';
if (modoRegistro) {
cargarPlanes(); // ✅ ahora sí se ejecutará correctamente
}
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
mensaje.classList.add('d-none');
const datos = {
correo: document.getElementById('correo').value,
clave_acceso: document.getElementById('clave').value
};
if (modoRegistro) {
Object.assign(datos, {
nombre_empresa: document.getElementById('nombre_empresa').value,
rut: document.getElementById('rut').value,
telefono: document.getElementById('telefono').value,
direccion: document.getElementById('direccion').value,
logo: document.getElementById('logo').value,
plan_id: document.getElementById('plan_id').value
});
}
try {
const url = modoRegistro ? '/api/registro' : '/api/login';
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos)
});
const resultado = await res.json();
if (!res.ok) {
throw new Error(resultado.error || 'Error inesperado');
}
mensaje.className = 'alert alert-success';
mensaje.textContent = resultado.message || (modoRegistro ? 'Registro exitoso' : 'Inicio exitoso');
mensaje.classList.remove('d-none');
if (!modoRegistro) {
// Redirigir a dashboard, por ejemplo
// window.location.href = `/dashboard?tenant=${resultado.uuid}`;
}
} catch (err) {
mensaje.className = 'alert alert-danger';
mensaje.textContent = err.message;
mensaje.classList.remove('d-none');
}
});
// ✅ Ahora la función está declarada correctamente
async function cargarPlanes() {
const select = document.getElementById('plan_id');
select.innerHTML = '<option value="">Cargando planes...</option>';
try {
const res = await fetch('/planes');
const planes = await res.json();
select.innerHTML = '<option value="">Seleccione un plan</option>';
planes.forEach(plan => {
const opt = document.createElement('option');
opt.value = plan.id;
opt.textContent = plan.nombre.charAt(0).toUpperCase() + plan.nombre.slice(1);
select.appendChild(opt);
});
} catch (err) {
select.innerHTML = '<option value="">Error al cargar planes</option>';
console.error('Error cargando planes:', err);
}
}
</script>
</body>
</html>
+106
View File
@@ -0,0 +1,106 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Productos</title>
</head>
<body>
<h1>Productos</h1>
<h2>Crear producto</h2>
<form id="form-producto">
<div>
<label>Nombre:
<input name="nombre" type="text" required />
</label>
</div>
<div>
<label>Precio:
<input name="precio" type="number" step="0.01" min="0" required />
</label>
</div>
<div>
<label>Categoría:
<select name="id_categoria" id="sel-categoria" required></select>
</label>
</div>
<button type="submit">Guardar</button>
</form>
<h2>Listado</h2>
<button id="btn-recargar">Recargar</button>
<table border="1" cellpadding="6">
<thead>
<tr><th>ID</th><th>Nombre</th><th>Precio</th><th>Activo</th><th>ID Categoría</th></tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<script>
const API = '/api/productos';
const API_CAT = '/api/categorias';
async function cargarCategorias() {
const res = await fetch(API_CAT);
const data = await res.json();
const sel = document.getElementById('sel-categoria');
sel.innerHTML = '<option value="" disabled selected>Seleccione...</option>';
data.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id_categoria;
opt.textContent = `${c.id_categoria} - ${c.nombre}`;
sel.appendChild(opt);
});
}
async function listar() {
const res = await fetch(API);
const data = await res.json();
const tbody = document.getElementById('tbody');
tbody.innerHTML = '';
data.forEach(p => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${p.id_producto}</td>
<td>${p.nombre}</td>
<td>${Number(p.precio).toFixed(2)}</td>
<td>${p.activo ? 'Sí' : 'No'}</td>
<td>${p.id_categoria}</td>
`;
tbody.appendChild(tr);
});
}
document.getElementById('form-producto').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const payload = {
nombre: fd.get('nombre').trim(),
precio: parseFloat(fd.get('precio')),
id_categoria: parseInt(fd.get('id_categoria'), 10)
};
if (!payload.nombre || isNaN(payload.precio) || isNaN(payload.id_categoria)) return;
const res = await fetch(API, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
if (!res.ok) {
const err = await res.json().catch(()=>({error:'Error'}));
alert('Error: ' + (err.error || res.statusText));
return;
}
e.target.reset();
await listar();
});
document.getElementById('btn-recargar').addEventListener('click', listar);
(async () => {
await cargarCategorias();
await listar();
})();
</script>
</body>
</html>
+62
View File
@@ -0,0 +1,62 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Roles</title>
</head>
<body>
<h1>Roles</h1>
<h2>Crear rol</h2>
<form id="form-rol">
<label>Nombre del rol:
<input type="text" name="nombre" required />
</label>
<button type="submit">Guardar</button>
</form>
<h2>Listado</h2>
<button id="btn-recargar">Recargar</button>
<table border="1" cellpadding="6">
<thead><tr><th>ID</th><th>Nombre</th></tr></thead>
<tbody id="tbody"></tbody>
</table>
<script>
const API = '/api/roles';
async function listar() {
const res = await fetch(API);
const data = await res.json();
const tbody = document.getElementById('tbody');
tbody.innerHTML = '';
data.forEach(r => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${r.id_rol}</td><td>${r.nombre}</td>`;
tbody.appendChild(tr);
});
}
document.getElementById('form-rol').addEventListener('submit', async (e) => {
e.preventDefault();
const nombre = e.target.nombre.value.trim();
if (!nombre) return;
const res = await fetch(API, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ nombre })
});
if (!res.ok) {
const err = await res.json().catch(()=>({error:'Error'}));
alert('Error: ' + (err.error || res.statusText));
return;
}
e.target.reset();
await listar();
});
document.getElementById('btn-recargar').addEventListener('click', listar);
listar();
</script>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Usuarios</title>
</head>
<body>
<h1>Usuarios</h1>
<h2>Crear usuario</h2>
<form id="form-usuario">
<div>
<label>Documento:
<input name="documento" type="text" />
</label>
</div>
<div>
<label>Nombre:
<input name="nombre" type="text" required />
</label>
</div>
<div>
<label>Apellido:
<input name="apellido" type="text" required />
</label>
</div>
<div>
<label>Correo:
<input name="correo" type="email" />
</label>
</div>
<div>
<label>Teléfono:
<input name="telefono" type="text" />
</label>
</div>
<div>
<label>Fecha de nacimiento:
<input name="fec_nacimiento" type="date" />
</label>
</div>
<button type="submit">Guardar</button>
</form>
<h2>Listado</h2>
<button id="btn-recargar">Recargar</button>
<table border="1" cellpadding="6">
<thead>
<tr>
<th>ID</th><th>Documento</th><th>Nombre</th><th>Apellido</th>
<th>Correo</th><th>Teléfono</th><th>Nacimiento</th><th>Activo</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<script>
const API = '/api/usuarios';
async function listar() {
const res = await fetch(API);
const data = await res.json();
const tbody = document.getElementById('tbody');
tbody.innerHTML = '';
data.forEach(u => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${u.id_usuario}</td>
<td>${u.documento ?? ''}</td>
<td>${u.nombre}</td>
<td>${u.apellido}</td>
<td>${u.correo ?? ''}</td>
<td>${u.telefono ?? ''}</td>
<td>${u.fec_nacimiento ? u.fec_nacimiento.substring(0,10) : ''}</td>
<td>${u.activo ? 'Sí' : 'No'}</td>
`;
tbody.appendChild(tr);
});
}
document.getElementById('form-usuario').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const payload = Object.fromEntries(fd.entries());
if (payload.fec_nacimiento === '') delete payload.fec_nacimiento;
const res = await fetch(API, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
if (!res.ok) {
const err = await res.json().catch(()=>({error:'Error'}));
alert('Error: ' + (err.error || res.statusText));
return;
}
e.target.reset();
await listar();
});
document.getElementById('btn-recargar').addEventListener('click', listar);
listar();
</script>
</body>
</html>