Commit inicial - Sistema de Gestion Hotelera Hacienda San Angel

- Backend Node.js/Express con PostgreSQL
- Frontend React 19 con Vite
- Docker Compose para orquestacion
- Documentacion completa en README.md
- Scripts SQL para base de datos
- Configuracion de ejemplo (.env.example)
This commit is contained in:
Consultoria AS
2026-01-17 18:52:34 -08:00
commit 0211bea186
210 changed files with 47045 additions and 0 deletions

View File

@@ -0,0 +1,426 @@
// Layout.jsx
import React, { useContext, useState } from "react";
import { Outlet, NavLink, useLocation } from "react-router-dom";
import { useNavigate } from 'react-router-dom';
import "../styles/Dashboard.css";
import { AuthContext } from "../context/AuthContext";
import { langContext } from "../context/LenguageContext";
import { FaSignOutAlt } from "react-icons/fa";
import { menuConfig } from "../constants/menuconfig";
export default function Layout() {
const { toggleLang, lang } = useContext(langContext);
const { user, logout } = useContext(AuthContext);
console.log('user', user);
const handleLogout = () => {
logout();
navigate("/");
};
const menuConfigWithPermissions = Object.values(menuConfig).map(section => ({
...section,
hidden:
section.label === "Dashboards" ? (user >= 1 && user <= 2 ? false : true) :
section.label === "Expenses to be approved" ? (user === 1 || user === 2 ? false : true) :
section.label === "Expenses" ? (user >= 1 && user <= 5 ? false : true) :
section.label === "Inventory" ? (user >= 1 && user <= 5 ? false : true) :
section.label === "Payroll" ? (user >= 1 && user <= 4 ? false : true) :
section.label === "Hotel" ? (user === 1 ? false : true) :
section.label === "Income" ? (user >= 1 && user <= 4 ? false : true) :
section.label === "Housekeeper" ? (user === 6 ? false : true) :
false,
submenu: section.submenu?.map(item => ({
...item,
hidden: item.hidden ||
(section.label === "Expenses" && user === 2 && item.label !== "Report" && item.label !== "Monthly Report" ? true : false) ||
(section.label === "Payroll" && user === 2 && !["Report", "Attendance", "Employees", "Contracts"].includes(item.label) ? true : false) ||
(section.label === "Expenses" && user === 5 && !["New Expense", "Purchase Entries", "New Suppliers"].includes(item.label) ? true : false)
}))
}));
const navigate = useNavigate();
const location = useLocation();
const [isSidebarOpen, setSidebarOpen] = useState(true);
const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
// const isDetailPage = /^\/app\/properties\/\d+$/.test(location.pathname);
// Detectar páginas de detalle (para ocultar el submenú)
const isDetailPage =
/^\/app\/properties\/\d+$/.test(location.pathname) || // Propiedades (ya existente)
/^\/app\/payroll\/contracts-detail(\/.*)?$/.test(location.pathname) || // Contract Detail
/^\/app\/expenses\/detail(\/.*)?$/.test(location.pathname); // Otros detalles si los tienes
const activeSection = menuConfigWithPermissions.find(section => {
if (section.hidden) return false;
const matchesSubmenu = section.submenu.some(item => location.pathname.startsWith(item.route));
const matchesBasePath = location.pathname.startsWith(section.basePath);
if (matchesSubmenu || matchesBasePath) {
return true;
}
if (section.label === "Income" && location.pathname.startsWith("/app/edit-income-form")) {
return true;
}
if (section.label === "Expenses" && (location.pathname.startsWith("/app/expenses/edit/") || location.pathname.startsWith("/app/expenses/") || location.pathname === "/app/new-monthly")) {
return true;
}
if (section.label === "Inventory" && location.pathname.startsWith("/app/alter-product/")) {
return true;
}
if (section.label === "Payroll" && (location.pathname.startsWith("/app/payroll/employee/") || location.pathname.startsWith("/app/payroll/contract/") || location.pathname.startsWith("/app/payroll/edit/") || location.pathname.startsWith("/app/payroll/contracts-detail/"))) {
return true;
}
if (section.label === "Hotel" && location.pathname.startsWith("/app/properties/")) {
return true;
}
return false;
});
const activeSubmenu = activeSection?.label === "Housekeeper"
? [{ label: "Outcomes", spanish_label: "Salidas", route: "/app/housekeeper/outcomes" }]
: activeSection?.submenu || [];
const isLandingPage = location.pathname === '/app' || !activeSection;
return (
<div className="dashboard-layout">
{/* Sidebar */}
{isSidebarOpen && (
<aside className="sidebar">
<nav>
{/*sSolo se muestran secciones que no están ocultas */}
{menuConfigWithPermissions
.filter(section => !section.hidden)
.map((section, index) => (
<NavLink key={index} to={section.basePath}>
{lang === "en" ? section.label : section.spanish_label}
</NavLink>
))}
</nav>
</aside>
)}
{/* Main content */}
<div className="main-content">
{/* Topbar */}
<div className="topbar">
<div className="topbar-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
{/* Botón + Título (alineados a la izquierda) */}
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<button onClick={toggleSidebar} className="sidebar-toggle-button">
</button>
<div className="topbar-title">
{lang === "en" ? activeSection?.label : activeSection?.spanish_label}
</div>
</div>
{/* Iconos a la derecha */}
<div className="topbar-icons" style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<select
className="language-select"
onChange={toggleLang}
>
<option value="en">EN</option>
<option value="es">ES</option>
</select>
<button
onClick={handleLogout}
style={{
display: "flex",
alignItems: "center",
gap: "6px",
background: "transparent",
color: "#ffffffff",
border: "none",
fontWeight: "bold",
cursor: "pointer"
}}
>
<FaSignOutAlt className="topbar-icon" />
</button>
</div>
</div>
{/* Submenú (solo si no es página de detalle) */}
{!isDetailPage && (
<div className="topbar-submenu">
{activeSubmenu.filter(section => !section.hidden).map((item, index) => (
<NavLink
key={index}
to={item.route}
className={({ isActive }) =>
isActive ? "submenu-link active" : "submenu-link"
}
>
{lang === "en" ? item.label : item.spanish_label}
</NavLink>
))}
</div>
)}
</div>
{/* Página actual */}
<div className="content">
{isLandingPage ? (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
gap: '2rem'
}}>
<img
src="/logoHotel.png"
alt="Hotel Logo"
style={{
maxWidth: '300px',
width: '100%',
height: 'auto'
}}
/>
<p style={{
fontSize: '1.25rem',
color: '#666',
textAlign: 'center',
maxWidth: '600px',
padding: '0 1rem'
}}>
{lang === "en"
? "To get started, select an option from the menu on the left."
: "Para comenzar, selecciona una opción del menú de la izquierda."}
</p>
</div>
) : (
<Outlet />
)}
</div>
</div>
</div>
);
}
// // Layout.jsx
// import React, { use, useContext, useState } from "react";
// import { Outlet, NavLink, useLocation } from "react-router-dom";
// import { useNavigate } from 'react-router-dom';
// import { FaBell, FaCog } from "react-icons/fa";
// import "../styles/Dashboard.css";
// import { AuthContext } from "../context/AuthContext";
// import { langContext } from "../context/LenguageContext";
// import { FaSignOutAlt } from "react-icons/fa";
// export default function Layout() {
// const { toggleLang } = useContext(langContext);
// const { user, logout } = useContext(AuthContext);
// const handleLogout = () => {
// logout();
// navigate("/");
// };
// // const {lang, setLang} = useContext(AuthContext);
// // console.log(lang);
// const menuConfig = [
// {
// label: "Dashboards",
// basePath: "/app/income",
// submenu: [
// { label: "Income", route: "/app/income" },
// // { label: "Expenses", route: "/app/expenses" },
// // { label: "Cost per room", route: "/app/cost-per-room" },
// // { label: "Budget", route: "/app/budget" },
// ],
// hidden: user === 1 ? false : true //Solo admin puede ver dashboards,
// },
// {
// label: "Expenses to be approved",
// basePath: "/app/pending-approval",
// submenu: [
// { label: "Pending approval", route: "/app/pending-approval" },
// { label: "Approved", route: "/app/approved" },
// { label: "Rejected", route: "/app/rejected" },
// ],
// hidden: user === 1 ? false : true
// },
// {
// label: "Expenses",
// basePath: "/app/report-expense",
// submenu: [
// { label: "Report", route: "/app/report-expense" },
// { label: "New Expense", route: "/app/new-expense" },
// { label: "Payments", route: "/app/payments" },
// { label: "Monthly Payments", route: "/app/monthly-payments" },
// { label: "New Monthly Payments", route: "/app/new-monthly" },
// { label: "Purchase Entries", route: "/app/purchase-entries" },
// ],
// hidden: user >= 1 && user <= 3 ? false : true
// },
// {
// label: "Inventory",
// basePath: "/app/products",
// submenu: [
// { label: "Products", route: "/app/products", hidden: user === 5 ? true : false },
// { label: "New Product", route: "/app/new-product", hidden: user === 5 ? true : false },
// { label: "Report", route: "/app/inventory-report", hidden: user === 5 ? true : false },
// { label: "Discard Product", route: "/app/discard-product", hidden: user === 5 ? true : false },
// { label: "Adjustments", route: "/app/product-adjustments", hidden: user === 5 ? true : false }
// ],
// hidden: user >= 1 && user <= 4 ? false : true
// },
// {
// label: "Payroll",
// basePath: "/app/payroll",
// submenu: [
// { label: "Report", route: "/app/payroll" },
// { label: "New Contract", route: "/app/payroll/NewPayRoll"},
// { label: "Attendance", route: "/app/payroll/attendance" },
// { label: "Employees", route: "/app/payroll/employees" },
// { label: "New Employee", route: "/app/payroll/newemployee" },
// ],
// hidden: user >= 1 && user <= 3 ? false : true
// },
// {
// label: "Hotel",
// basePath: "/app/properties",
// submenu: [
// { label: "Properties", route: "/app/properties" },
// ],
// hidden: user === 1 ? false : true
// },
// //SECCIÓN "OCULTA" PARA SETTINGS
// {
// label: "Settings",
// basePath: "/app/settings",
// submenu: [
// { label: "General", route: "/app/settings" },
// { label: "Users", route: "/app/settings/users" },
// { label: "Units", route: "/app/settings/roles" },
// { label: "Room management", route: "/app/settings/room-management" },
// ],
// hidden: true, //etiqueta para ignorar en sidebar
// },
// ];
// const navigate = useNavigate();
// const location = useLocation();
// const [isSidebarOpen, setSidebarOpen] = useState(true);
// const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
// const isDetailPage = /^\/app\/properties\/\d+$/.test(location.pathname);
// //Identificar la sección activa, incluyendo settings
// const activeSection = menuConfig.find(section =>
// section.submenu.some(item => location.pathname.startsWith(item.route))
// );
// const activeSubmenu = activeSection?.submenu || [];
// return (
// <div className="dashboard-layout">
// {/* Sidebar */}
// {isSidebarOpen && (
// <aside className="sidebar">
// <nav>
// {/*sSolo se muestran secciones que no están ocultas */}
// {menuConfig
// .filter(section => !section.hidden)
// .map((section, index) => (
// <NavLink key={index} to={section.basePath}>
// {section.label}
// </NavLink>
// ))}
// </nav>
// </aside>
// )}
// {/* Main content */}
// <div className="main-content">
// {/* Topbar */}
// <div className="topbar">
// <div className="topbar-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
// {/* Botón + Título (alineados a la izquierda) */}
// <div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
// <button onClick={toggleSidebar} className="sidebar-toggle-button">
// ☰
// </button>
// <div className="topbar-title">{activeSection?.label}</div>
// </div>
// {/* Iconos a la derecha */}
// <div className="topbar-icons" style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
// <FaBell className="topbar-icon" />
// <FaCog
// className="topbar-icon cursor-pointer"
// onClick={() => navigate("/app/settings")}
// />
// <select
// className="language-select"
// onChange={toggleLang}
// >
// <option value="en">EN</option>
// <option value="es">ES</option>
// </select>
// <button
// onClick={handleLogout}
// style={{
// display: "flex",
// alignItems: "center",
// gap: "6px",
// background: "transparent",
// color: "#ffffffff",
// border: "none",
// fontWeight: "bold",
// cursor: "pointer"
// }}
// >
// <FaSignOutAlt className="topbar-icon" />
// </button>
// </div>
// </div>
// {/* Submenú (solo si no es página de detalle) */}
// {!isDetailPage && (
// <div className="topbar-submenu">
// {activeSubmenu.filter(section => !section.hidden).map((item, index) => (
// <NavLink
// key={index}
// to={item.route}
// className={({ isActive }) =>
// isActive ? "submenu-link active" : "submenu-link"
// }
// >
// {item.label}
// </NavLink>
// ))}
// </div>
// )}
// </div>
// {/* Página actual */}
// <div className="content">
// <Outlet />
// </div>
// </div>
// </div>
// );
// }