feat: fix backend SQL columns, add dark/light theme, fix legacy CSS

- Fix 7 backend controllers: rename columns to match actual DB schema
  (name_room→room_number, id_room→id, bed_type→room_type,
   id_employee→id, status_employee→status)
- Fix 10 frontend files with matching property renames
- Add ThemeContext with dark/light toggle (localStorage persisted)
- Redesign theme.css with [data-theme] attribute selectors
- Add Google Fonts (Syne, DM Sans, JetBrains Mono)
- Redesign sidebar, topbar, and login page CSS
- Migrate ~44 legacy CSS files from hardcoded colors to CSS variables
- Remove Dashboards/Tableros section from menu
- Clean up commented-out CSS blocks across codebase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 03:05:16 +00:00
parent 98e04de0be
commit 1828311b3a
76 changed files with 1847 additions and 2506 deletions

View File

@@ -10,11 +10,14 @@
"license": "ISC",
"dependencies": {
"axios": "^1.13.2",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"csv-parser": "^3.2.0",
"dotenv": "^17.2.2",
"express": "^5.1.0",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^7.0.10",
"pg": "^8.16.3",
"stripe": "^20.1.2",
@@ -84,6 +87,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -141,6 +150,12 @@
"node": ">=8"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -275,6 +290,25 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
@@ -382,6 +416,15 @@
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -872,12 +915,97 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1070,7 +1198,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@@ -1331,7 +1458,6 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View File

@@ -5,11 +5,11 @@ const getGuests = async (req, res) => {
const { search, limit = 50, offset = 0 } = req.query;
let query = `
SELECT g.*,
r.room_id, rm.name_room,
r.room_id, rm.room_number,
r.check_in as current_check_in
FROM guests g
LEFT JOIN reservations r ON r.guest_id = g.id AND r.status = 'checked_in'
LEFT JOIN rooms rm ON rm.id_room = r.room_id
LEFT JOIN rooms rm ON rm.id = r.room_id
`;
const params = [];
let paramIndex = 1;
@@ -38,10 +38,10 @@ const getGuestById = async (req, res) => {
try {
const { id } = req.params;
const guest = await pool.query(`
SELECT g.*, r.room_id, rm.name_room, r.check_in as current_check_in, r.check_out as current_check_out
SELECT g.*, r.room_id, rm.room_number, r.check_in as current_check_in, r.check_out as current_check_out
FROM guests g
LEFT JOIN reservations r ON r.guest_id = g.id AND r.status = 'checked_in'
LEFT JOIN rooms rm ON rm.id_room = r.room_id
LEFT JOIN rooms rm ON rm.id = r.room_id
WHERE g.id = $1
`, [id]);
@@ -92,9 +92,9 @@ const getGuestStays = async (req, res) => {
try {
const { id } = req.params;
const result = await pool.query(`
SELECT gs.*, rm.name_room, rm.bed_type
SELECT gs.*, rm.room_number, rm.room_type
FROM guest_stays gs
LEFT JOIN rooms rm ON rm.id_room = gs.room_id
LEFT JOIN rooms rm ON rm.id = gs.room_id
WHERE gs.guest_id = $1
ORDER BY gs.check_in DESC
`, [id]);

View File

@@ -4,12 +4,12 @@ const getTasks = async (req, res) => {
try {
const { status, priority, assigned_to } = req.query;
let query = `
SELECT ht.*, rm.name_room, rm.floor,
SELECT ht.*, rm.room_number, rm.floor,
CASE WHEN ht.assigned_to IS NOT NULL THEN
(SELECT e.first_name || ' ' || e.last_name FROM employees e WHERE e.id_employee = ht.assigned_to)
(SELECT e.first_name || ' ' || e.last_name FROM employees e WHERE e.id = ht.assigned_to)
END as assigned_name
FROM housekeeping_tasks ht
LEFT JOIN rooms rm ON rm.id_room = ht.room_id
LEFT JOIN rooms rm ON rm.id = ht.room_id
WHERE 1=1
`;
const params = [];
@@ -62,7 +62,7 @@ const updateTask = async (req, res) => {
updates.status = 'completed';
updates.completed_at = new Date();
// Set room to available
await pool.query("UPDATE rooms SET status = 'available' WHERE id_room = $1", [task.room_id]);
await pool.query("UPDATE rooms SET status = 'available' WHERE id = $1", [task.room_id]);
await pool.query(
'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)',
[task.room_id, 'cleaning', 'available', req.user?.user_id]
@@ -91,10 +91,10 @@ const getStaff = async (req, res) => {
// Get employees from housekeeping-related areas
// Note: the employees table structure uses stored functions, so we query directly
const result = await pool.query(`
SELECT e.id_employee, e.first_name, e.last_name,
(SELECT COUNT(*) FROM housekeeping_tasks ht WHERE ht.assigned_to = e.id_employee AND ht.status = 'in_progress') as active_tasks
SELECT e.id, e.first_name, e.last_name,
(SELECT COUNT(*) FROM housekeeping_tasks ht WHERE ht.assigned_to = e.id AND ht.status = 'in_progress') as active_tasks
FROM employees e
WHERE e.status_employee = true
WHERE e.status = 'active'
ORDER BY e.first_name
LIMIT 50
`);

View File

@@ -1,6 +1,6 @@
const pool = require('../db/connection');
const axios = require('axios');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const stripe = process.env.STRIPE_SECRET_KEY ? require('stripe')(process.env.STRIPE_SECRET_KEY) : null;
const getFacturas = async (initialDate, finalDate) => {
try {
const response = await axios.get(process.env.FACTURAS_API_URL, {

View File

@@ -53,11 +53,11 @@ const getRevenueReport = async (req, res) => {
}
const result = await pool.query(`
SELECT rm.bed_type as room_type, COALESCE(SUM(r.total_amount), 0) as revenue
SELECT rm.room_type, COALESCE(SUM(r.total_amount), 0) as revenue
FROM reservations r
JOIN rooms rm ON rm.id_room = r.room_id
JOIN rooms rm ON rm.id = r.room_id
WHERE r.status = 'checked_out' AND r.check_out >= CURRENT_DATE - INTERVAL '${interval}'
GROUP BY rm.bed_type
GROUP BY rm.room_type
ORDER BY revenue DESC
`);

View File

@@ -5,10 +5,10 @@ const getReservations = async (req, res) => {
const { status, from_date, to_date, search } = req.query;
let query = `
SELECT r.*, g.first_name, g.last_name, g.phone, g.email,
rm.name_room, rm.bed_type
rm.room_number, rm.room_type
FROM reservations r
JOIN guests g ON g.id = r.guest_id
LEFT JOIN rooms rm ON rm.id_room = r.room_id
LEFT JOIN rooms rm ON rm.id = r.room_id
WHERE 1=1
`;
const params = [];
@@ -128,7 +128,7 @@ const updateReservationStatus = async (req, res) => {
// Cascading side effects
if (status === 'checked_in') {
// Set room to occupied
await pool.query("UPDATE rooms SET status = 'occupied' WHERE id_room = $1", [reservation.room_id]);
await pool.query("UPDATE rooms SET status = 'occupied' WHERE id = $1", [reservation.room_id]);
await pool.query(
'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)',
[reservation.room_id, 'available', 'occupied', userId]
@@ -142,7 +142,7 @@ const updateReservationStatus = async (req, res) => {
if (status === 'checked_out') {
// Set room to cleaning
await pool.query("UPDATE rooms SET status = 'cleaning' WHERE id_room = $1", [reservation.room_id]);
await pool.query("UPDATE rooms SET status = 'cleaning' WHERE id = $1", [reservation.room_id]);
await pool.query(
'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)',
[reservation.room_id, 'occupied', 'cleaning', userId]
@@ -162,7 +162,7 @@ const updateReservationStatus = async (req, res) => {
if (status === 'cancelled') {
// Free room if it was occupied
if (reservation.status === 'checked_in') {
await pool.query("UPDATE rooms SET status = 'available' WHERE id_room = $1", [reservation.room_id]);
await pool.query("UPDATE rooms SET status = 'available' WHERE id = $1", [reservation.room_id]);
}
}

View File

@@ -10,9 +10,9 @@ const getRoomsWithStatus = async (req, res) => {
res.check_in,
res.check_out
FROM rooms r
LEFT JOIN reservations res ON res.room_id = r.id_room AND res.status = 'checked_in'
LEFT JOIN reservations res ON res.room_id = r.id AND res.status = 'checked_in'
LEFT JOIN guests g ON g.id = res.guest_id
ORDER BY r.floor, r.name_room
ORDER BY r.floor, r.room_number
`);
res.json({ rooms: result.rows });
} catch (error) {
@@ -32,13 +32,13 @@ const updateRoomStatus = async (req, res) => {
return res.status(400).json({ message: 'Estado invalido' });
}
const current = await pool.query('SELECT status FROM rooms WHERE id_room = $1', [id]);
const current = await pool.query('SELECT status FROM rooms WHERE id = $1', [id]);
if (current.rows.length === 0) {
return res.status(404).json({ message: 'Habitacion no encontrada' });
}
const previousStatus = current.rows[0].status;
await pool.query('UPDATE rooms SET status = $1 WHERE id_room = $2', [status, id]);
await pool.query('UPDATE rooms SET status = $1 WHERE id = $2', [status, id]);
await pool.query(
'INSERT INTO room_status_log (room_id, previous_status, new_status, changed_by) VALUES ($1, $2, $3, $4)',

View File

@@ -4,10 +4,10 @@ const getOrders = async (req, res) => {
try {
const { status } = req.query;
let query = `
SELECT rso.*, rm.name_room, g.first_name || ' ' || g.last_name as guest_name,
SELECT rso.*, rm.room_number, g.first_name || ' ' || g.last_name as guest_name,
json_agg(json_build_object('id', oi.id, 'name', mi.name, 'quantity', oi.quantity, 'price', oi.price, 'notes', oi.notes)) as items
FROM room_service_orders rso
LEFT JOIN rooms rm ON rm.id_room = rso.room_id
LEFT JOIN rooms rm ON rm.id = rso.room_id
LEFT JOIN guests g ON g.id = rso.guest_id
LEFT JOIN order_items oi ON oi.order_id = rso.id
LEFT JOIN menu_items mi ON mi.id = oi.menu_item_id
@@ -19,7 +19,7 @@ const getOrders = async (req, res) => {
} else {
query += " WHERE rso.status NOT IN ('delivered', 'cancelled')";
}
query += ' GROUP BY rso.id, rm.name_room, g.first_name, g.last_name ORDER BY rso.created_at DESC';
query += ' GROUP BY rso.id, rm.room_number, g.first_name, g.last_name ORDER BY rso.created_at DESC';
const result = await pool.query(query, params);
res.json({ orders: result.rows });

View File

@@ -11,7 +11,7 @@ const getSchedules = async (req, res) => {
let query = `
SELECT es.*, e.first_name, e.last_name
FROM employee_schedules es
JOIN employees e ON e.id_employee = es.employee_id
JOIN employees e ON e.id = es.employee_id
WHERE es.schedule_date BETWEEN $1 AND $2
`;
const params = [week_start, weekEnd.toISOString().split('T')[0]];
@@ -67,7 +67,7 @@ const getEmployeeSchedule = async (req, res) => {
const getEmployeesForScheduling = async (req, res) => {
try {
const result = await pool.query(
'SELECT id_employee, first_name, last_name FROM employees WHERE status_employee = true ORDER BY first_name LIMIT 100'
"SELECT id, first_name, last_name FROM employees WHERE status = 'active' ORDER BY first_name LIMIT 100"
);
res.json({ employees: result.rows });
} catch (error) {