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,89 @@
.btn {
border: none;
padding: 10px 20px;
font-size: 1rem;
font-weight: bold;
border-radius: 999px; /* Súper redondo */
background-color: #521414;
font-family: 'Roboto', sans-serif;
cursor: pointer;
color: #ffffff;
transition: all 0.3s ease;
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
text-align: center;
transition: all 0.3s ease;
}
.btn.primary {
background-color: #4a0d0d;
color: white;
}
.btn.primary:hover {
transform: translateY(-2px);
box-shadow: 0px 12px 20px rgba(0, 0, 0, 0.3);
}
.btn.secundary {
border: none;
padding: 20px 40px;
font-size: 1.8rem;
font-weight: bold;
border-radius: 999px;
background-color: #eeeeee;
cursor: pointer;
color: #555555;
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.15);
width: 100%;
max-width: 400px;
text-align: center;
transition: all 0.3s ease;
}
.btn.secundary:hover {
transform: translateY(-2px);
box-shadow: 0px 12px 20px rgba(0, 0, 0, 0.2);
}
.button-group {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 30px;
align-items: center;
}
/*
.button-group {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 30px;
align-items: center;
}
.input-field {
background-color: #eeeeee;
color: #555;
font-size: 1.8rem;
font-weight: bold;
border-radius: 999px;
padding: 20px 40px;
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.15);
border: none;
width: 100%;
max-width: 400px;
text-align: center;
outline: none;
}
.btn.gray {
background-color: #ccc;
color: #333;
}
*/

View File

@@ -0,0 +1,18 @@
// src/components/Button.jsx
import React from "react";
import "./Button.css"; // estilos separados
function Button({ label, onClick, variant = "primary" | "secundary"}) {
return (
<div>
<h1>Login</h1>
<Button variant="primary" onClick={() => alert("Ingresando...")}>
Iniciar sesión
</Button>
<Button variant="secundary" onClick={() => alert("Registro")}>
Registrarse
</Button>
</div>
);
}
export default Button;

View File

@@ -0,0 +1,118 @@
import React, { useContext } from "react";
import PropTypes from "prop-types";
import * as XLSX from "xlsx";
import { FiDownload } from "react-icons/fi";
import { langContext } from "../context/LenguageContext";
export default function ExcelExportButton({
data,
columns,
filenamePrefix,
sheetName,
dataTransform = null,
className = "",
style = {},
}) {
const { lang } = useContext(langContext);
const handleExportToExcel = () => {
if (!data || data.length === 0) {
alert(lang === "es" ? "No hay datos para exportar" : "No data to export");
return;
}
let dataToExport = dataTransform ? dataTransform(data) : data;
const excelData = dataToExport.map((row) => {
const excelRow = {};
columns.forEach((column) => {
if (column.key) {
let value = row[column.key];
// Handle render functions (extract text value)
if (column.render && typeof column.render === "function") {
// Try to get the raw value if render is used
value = row[column.key];
}
if (value instanceof Date) {
value = isNaN(value.getTime())
? ""
: value.toLocaleDateString(lang === "es" ? "es-MX" : "en-US");
}
const header =
typeof column.header === "string"
? column.header
: (lang === "es" ? column.header?.es : column.header?.en) ||
column.key;
excelRow[header] = value || "";
}
});
return excelRow;
});
const worksheet = XLSX.utils.json_to_sheet(excelData);
const workbook = XLSX.utils.book_new();
const finalSheetName =
typeof sheetName === "string"
? sheetName
: (lang === "es" ? sheetName?.es : sheetName?.en) || "Sheet1";
XLSX.utils.book_append_sheet(workbook, worksheet, finalSheetName);
const today = new Date();
const dateStr = today.toISOString().split("T")[0];
const filename = `${filenamePrefix}-${dateStr}.xlsx`;
XLSX.writeFile(workbook, filename);
};
const defaultStyle = {
backgroundColor: "#ffcb05",
color: "#fff",
padding: "10px 20px",
fontWeight: "bold",
border: "none",
borderRadius: "8px",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "8px",
fontSize: "14px",
transition: "background-color 0.3s ease",
...style,
};
return (
<button
onClick={handleExportToExcel}
className={className}
style={defaultStyle}
onMouseEnter={(e) => (e.target.style.backgroundColor = "#f4b400")}
onMouseLeave={(e) => (e.target.style.backgroundColor = "#ffcb05")}
>
<FiDownload />
{lang === "es" ? "Exportar a Excel" : "Export to Excel"}
</button>
);
}
ExcelExportButton.propTypes = {
data: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
header: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
key: PropTypes.string.isRequired,
render: PropTypes.func,
})
).isRequired,
filenamePrefix: PropTypes.string.isRequired,
sheetName: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
.isRequired,
dataTransform: PropTypes.func,
className: PropTypes.string,
style: PropTypes.object,
};

View File

@@ -0,0 +1,64 @@
.date-range-filter {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.date-input-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-label {
font-size: 14px;
font-weight: 600;
color: #333;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
white-space: nowrap;
}
.date-input {
padding: 10px 16px;
border: none;
border-radius: 30px;
background-color: white;
box-shadow: 0 0 0 2px #f4f4f4;
font-size: 14px;
color: #333;
min-width: 150px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
height: 42px;
box-sizing: border-box;
}
.date-input:focus {
outline: none;
box-shadow: 0 0 0 2px #fcd200;
}
.date-input:hover {
box-shadow: 0 0 0 2px #e6e6e6;
}
@media (max-width: 768px) {
.date-range-filter {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.date-input-group {
width: 100%;
}
.date-input {
flex: 1;
}
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import './DateRangeFilter.css';
const DateRangeFilter = ({
dateRange,
onDateChange,
labels = { from: 'From:', to: 'To:' },
lang = 'en'
}) => {
const fromLabel = lang === 'en' ? labels.from : 'Desde:';
const toLabel = lang === 'en' ? labels.to : 'Hasta:';
return (
<div className="date-range-filter">
<div className="date-input-group">
<label htmlFor="date-from" className="date-label">
{fromLabel}
</label>
<input
id="date-from"
type="date"
className="date-input"
value={dateRange.from}
onChange={(e) => onDateChange({ ...dateRange, from: e.target.value })}
/>
</div>
<div className="date-input-group">
<label htmlFor="date-to" className="date-label">
{toLabel}
</label>
<input
id="date-to"
type="date"
className="date-input"
value={dateRange.to}
onChange={(e) => onDateChange({ ...dateRange, to: e.target.value })}
/>
</div>
</div>
);
};
export default DateRangeFilter;

View File

@@ -0,0 +1,265 @@
/* src/styles/Filters.css */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-title {
color: #1a1a1a;
font-size: 28px;
font-weight: 700;
margin: 0;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
letter-spacing: -0.5px;
}
.new-payment-btn {
background-color: #FFD700;
color: #800000;
font-weight: bold;
padding: 10px 20px;
border-radius: 20px;
border: none;
cursor: pointer;
font-size: 14px;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.new-payment-btn:hover {
background-color: #fcd200;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.new-payment-btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.report-page {
padding: 0;
max-width: 100%;
}
.table-section {
margin-top: 20px;
}
select,
input[type="date"] {
padding: 10px 16px;
border: none;
border-radius: 30px;
background-color: white;
box-shadow: 0 0 0 2px #f4f4f4;
font-size: 14px;
color: #333;
max-width: 250px;
min-width: 150px;
width: auto;
flex: 0 1 auto;
/* appearance: none;
-webkit-appearance: none;
-moz-appearance: none; */
/* background-image: url("data:image/svg+xml,%3Csvg fill='gold' viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 2px center;
background-size: 18px 18px; */
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: bold;
cursor: pointer;
box-sizing: border-box;
/* display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 15px; */
}
select:focus,
input[type="date"]:focus {
outline: none;
box-shadow: 0 0 0 2px #fcd200;
}
.filters {
display: flex;
gap: 15px;
margin: 20px 0;
}
.filter-container {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.filter-select, .filter-date {
background-color: white;
border: none;
border-radius: 2rem;
padding: 0.5rem 1rem;
font-size: 1rem;
color: #333;
box-shadow: 0 0 3px rgba(0,0,0,0.1);
appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg fill="gold" ...>...</svg>'); /* usa ícono amarillo aquí */
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 1rem;
}
.filter-select:focus, .filter-date:focus {
outline: none;
box-shadow: 0 0 0 2px #FFD700;
}
.status-button {
border: 2px solid;
padding: 4px 10px;
border-radius: 6px;
font-weight: bold;
text-transform: uppercase;
font-size: 12px;
min-width: 80px;
}
.status-button.approve {
color: #28a745;
border-color: #28a745;
background-color: #e6fff0;
}
.status-button.approved {
color: #28a745;
border-color: #28a745;
background-color: #e6fff0;
}
.status-button.reject {
color: #dc3545;
border-color: #dc3545;
background-color: #ffe6e6;
}
.status-button.rejected {
color: #dc3545;
border-color: #dc3545;
background-color: #ffe6e6;
}
.status-button.pending {
color: #f0ad4e;
border-color: #f0ad4e;
background-color: #fff7e6;
}
.status-button.paid {
color: #28a745;
border-color: #28a745;
background-color: #e6fff0;
}
.income-card {
background-color: white;
border-radius: 15px;
padding: 15px 20px;
min-width: 140px;
text-align: center;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
align-items: center;
}
.summary-container {
display: flex;
gap: 80px;
margin-bottom: 20px;
}
/* .summary-card {
background: #ffffff;
border-radius: 10px;
padding: 10px 20px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
font-size: 1rem;
min-width: 160px;
} */
.summary-card {
background-color: white;
border-radius: 12px;
padding: 15px 25px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
min-width: 140px;
text-align: center;
color: #333;
align-items: center;
display: block;
}
/*Date*/
.date-filter {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 20px;
font-size: 14px;
color: #333;
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: bold;
}
.date-filter-button {
background: none;
border: none;
cursor: pointer;
padding-left: 8px;
display: flex;
align-items: center;
}
/*
.status-button.approve {
background-color: #8bed92;
border: 1.5px solid #33a544;
color: #33a544;
border-radius: 20px;
padding: 5px 15px;
font-weight: bold;
pointer-events: none;
} */
.page-filters {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 15px;
}
.approved-page h2 {
margin-bottom: 10px;
}
/*
.status-button {
border: 2px solid #28a745;
color: #28a745;
padding: 4px 10px;
border-radius: 6px;
font-weight: bold;
background-color: #e6fff0;
}
.status-button.inactive {
border-color: #dc3545;
color: #dc3545;
background-color: #ffe6e6;
}
*/

View File

@@ -0,0 +1,99 @@
// src/components/Filters/Filters.jsx
import React from 'react';
import './Filters.css';
const Filters = ({
areaOptions = [],
statusOptions = [],
selectedArea,
selectedStatus,
onAreaChange,
onStatusChange,
startDate,
endDate,
onStartDateChange,
onEndDateChange,
}) => {
return (
<div className="filters-container">
{areaOptions.length > 0 && (
<div className="filter-select-wrapper">
<select className="filter-select" value={selectedArea} onChange={onAreaChange}>
{areaOptions.map((area, index) => (
<option key={index} value={area}>
{area}
</option>
))}
</select>
</div>
)}
{statusOptions.length > 0 && (
<div className="filter-select-wrapper">
<select className="filter-select" value={selectedStatus} onChange={onStatusChange}>
{statusOptions.map((status, index) => (
<option key={index} value={status}>
{status}
</option>
))}
</select>
</div>
)}
{onStartDateChange && (
<input
type="date"
className="filter-date"
value={startDate}
onChange={onStartDateChange}
/>
)}
{onEndDateChange && (
<input
type="date"
className="filter-date"
value={endDate}
onChange={onEndDateChange}
/>
)}
</div>
);
};
export default Filters;
// src/components/Filters.jsx
// import React from 'react';
// import '../styles/Filters.css';
// export default function Filters({ area, status, startDate, endDate, onChange }) {
// return (
// <div className="filters">
// <select value={area} onChange={(e) => onChange('area', e.target.value)}>
// <option value="">Area: Hotel, Restaurant</option>
// <option value="Hotel">Hotel</option>
// <option value="Restaurant">Restaurant</option>
// </select>
// <select value={status} onChange={(e) => onChange('status', e.target.value)}>
// <option value="">Status: Active, Inactive</option>
// <option value="Active">Active</option>
// <option value="Inactive">Inactive</option>
// </select>
// <input
// type="date"
// value={startDate}
// onChange={(e) => onChange('startDate', e.target.value)}
// />
// <input
// type="date"
// value={endDate}
// onChange={(e) => onChange('endDate', e.target.value)}
// />
// </div>
// );
// }

View File

@@ -0,0 +1,32 @@
import React from 'react';
// export default function FormInput({ label, name, value, onChange, ...rest }) {
// return (
// <div className="form-input">
// {label && <label>{label}</label>}
// <input
// name={name}
// value={value}
// onChange={onChange}
// {...rest}
// />
// </div>
// );
// }
export default function FormInput({ label, name, value, onChange, placeholder, type = "text", ...props }) {
return (
<div>
<label>{label}</label>
<input
type={type}
name={name}
value={value}
onChange={onChange}
placeholder={placeholder}
{...props}
/>
</div>
);
}

View File

@@ -0,0 +1,13 @@
export default function FormSelect({ label, name, value, onChange, options }) {
return (
<div>
<label>{label}</label>
<select name={name} value={value} onChange={onChange}>
<option value="">Select</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
);
}

View File

@@ -0,0 +1,8 @@
.input {
width: 100%;
padding: 12px;
margin: 8px 0;
border-radius: 12px;
border: 1px solid #ccc;
font-size: 1rem;
}

View File

@@ -0,0 +1,17 @@
// src/components/Input.jsx
import React from "react";
import "./Input.css";
function Input({ type = "text", placeholder, value, onChange }) {
return (
<input
className="input"
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
/>
);
}
export default Input;

View File

@@ -0,0 +1,357 @@
// Layout.jsx
// import React, { 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";
// 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" },
// ],
// },
// {
// 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" },
// ],
// },
// {
// label: "Expenses",
// basePath: "/app/report",
// submenu: [
// { label: "Report", route: "/app/report" },
// { label: "New Expense", route: "/app/new-expense" },
// { label: "Payments", route: "/app/payments" },
// { label: "Monthly Payments", route: "/app/monthly-payments" },
// { label: "Id", route: "/app/expense-id" },
// ],
// },
// {
// label: "Inventory",
// basePath: "/app/products",
// submenu: [
// { label: "Products", route: "/app/products" },
// { label: "New Product", route: "/app/new-product" },
// { label: "Report", route: "/app/inventory-report" },
// ],
// },
// {
// label: "Payroll",
// basePath: "/app/payroll",
// submenu: [
// { label: "Report", route: "/app/payroll" },
// ],
// },
// {
// label: "Hotel",
// basePath: "/app/properties",
// submenu: [
// { label: "Properties", route: "/app/properties" },
// // { label: "New Property", route: "/app/properties/:id"},
// ],
// },
// ];
// export default function Layout() {
// const navigate = useNavigate();
// const location = useLocation();
// const [isSidebarOpen, setSidebarOpen] = useState(true);
// const toggleSidebar = () => setSidebarOpen(!isSidebarOpen);
// const isDetailPage = /^\/app\/properties\/\d+$/.test(location.pathname);
// const isSettingsPage = location.pathname === "/app/settings";
// // Detectar qué menú está activo según la ruta actual
// // const activeSection = menuConfig.find(section =>
// // location.pathname.startsWith(section.basePath)
// // );
// // const activeSection = menuConfig.find(section =>
// // section.submenu.some(item => location.pathname.startsWith(item.route))
// // );
// // Encuentra la sección activa o ignórala si es una ruta especial como "/app/properties/:id"
// const activeSection = menuConfig.find(section =>
// section.submenu.some(item => location.pathname.startsWith(item.route))
// );
// // Si no hay sección activa, es una página sin menú (como detalles)
// const activeSubmenu = activeSection?.submenu || [];
// return (
// <div className="dashboard-layout">
// {/* Sidebar */}
// {isSidebarOpen && (
// <aside className="sidebar">
// <nav>
// <NavLink to="/app/income">Dashboards</NavLink>
// <NavLink to="/app/pending-approval">Expenses to be approved</NavLink>
// <NavLink to="/app/report">Expenses</NavLink>
// <NavLink to="/app/products">Inventory</NavLink>
// <NavLink to="/app/payroll">Payroll</NavLink>
// <NavLink to="/app/properties">Hotel</NavLink>
// </nav>
// </aside>
// )}
// {/* Main content */}
// <div className="main-content">
// {/* Topbar */}
// <div className="topbar">
// <div className="topbar-header">
// {/* Oculta título si estamos en /app/settings */}
// {!isSettingsPage && (
// <div className="topbar-title">{activeSection?.label}</div>
// )}
// <div className="topbar-icons">
// <FaBell className="topbar-icon" />
// <FaCog
// className="topbar-icon cursor-pointer"
// onClick={() => navigate("/app/settings")}
// />
// </div>
// </div>
// {/*Oculta submenú si es página de detalles o settings */}
// {!isDetailPage && !isSettingsPage && (
// <div className="topbar-submenu">
// {activeSubmenu.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>
// );
// return (
// <div className="dashboard-layout">
// {/* Sidebar */}
// {isSidebarOpen && (
// <aside className="sidebar">
// <nav>
// <NavLink to="/app/income">Dashboards</NavLink>
// <NavLink to="/app/pending-approval">Expenses to be approved</NavLink>
// <NavLink to="/app/report">Expenses</NavLink>
// <NavLink to="/app/products">Inventory</NavLink>
// <NavLink to="/app/payroll">Payroll</NavLink>
// <NavLink to="/app/properties">Hotel</NavLink>
// </nav>
// </aside>
// )}
// {/* Contenido principal */}
// <div className="main-content">
// {/* ÚNICO Topbar */}
// <div className="topbar">
// {/* Línea superior: título + iconos */}
// <div className="topbar-header">
// <div className="topbar-title">{activeSection?.label}</div>
// <div className="topbar-icons">
// <FaBell className="topbar-icon" />
// <FaCog className="topbar-icon cursor-pointer" onClick={() => navigate("/app/settings")} />
// </div>
// </div>
// {/* Línea inferior: submenú dinámico */}
// {/* Línea inferior: submenú dinámico */}
// {!isDetailPage && (
// <div className="topbar-submenu">
// {activeSubmenu.map((item, index) => (
// <NavLink
// key={index}
// to={item.route}
// className={({ isActive }) =>
// isActive ? "submenu-link active" : "submenu-link"
// }
// >
// {item.label}
// </NavLink>
// ))}
// </div>
// )}
// {/* <div className="topbar-submenu">
// {activeSubmenu.map((item, index) => (
// <NavLink
// key={index}
// to={item.route}
// className={({ isActive }) =>
// isActive ? "submenu-link active" : "submenu-link"
// }
// >
// {item.label}
// </NavLink>
// ))}
// </div> */}
// </div>
// {/* Aquí va el contenido de la página */}
// <div className="content">
// <Outlet />
// </div>
// </div>
// </div>
// );
//}
//{ label: "Property", route: "/app/properties/:id" },
// import React from "react";
// import { Outlet, useLocation, NavLink } from "react-router-dom";
// import { menuConfig } from "../constants/menuConfig";
// import { FaBell, FaCog } from "react-icons/fa";
// import "../styles/Dashboard.css";
// export default function Layout() {
// const location = useLocation();
// const pathname = location.pathname;
// // Encuentra la sección activa
// const activeSectionKey = Object.keys(menuConfig).find((key) =>
// pathname.startsWith(menuConfig[key].baseRoute)
// );
// const activeSection = menuConfig[activeSectionKey];
// const activeSubmenu = activeSection?.submenu || [];
// return (
// <div className="dashboard-layout">
// {/* SIDEBAR */}
// <aside className="sidebar">
// <nav>
// {Object.entries(menuConfig).map(([key, section]) => (
// <NavLink
// key={key}
// to={section.baseRoute}
// className={({ isActive }) =>
// isActive ? "menu-items a active" : "menu-items a"
// }
// >
// {section.label}
// </NavLink>
// ))}
// </nav>
// </aside>
// {/* CONTENIDO PRINCIPAL */}
// <div className="main-content">
// {/* TOPBAR */}
// <div className="topbar">
// <div className="topbar-header">
// <div className="topbar-title">{activeSection?.label}</div>
// <div className="topbar-icons">
// <FaBell className="topbar-icon" />
// <FaCog className="topbar-icon" />
// </div>
// </div>
// <div className="topbar-submenu">
// {activeSubmenu.map((item, index) => (
// <NavLink
// key={index}
// to={item.route}
// className={({ isActive }) =>
// isActive ? "submenu-link active" : "submenu-link"
// }
// >
// {item.label}
// </NavLink>
// ))}
// </div>
// </div>
// {/* CONTENIDO */}
// <div className="content">
// <Outlet />
// </div>
// </div>
// </div>
// );
// }
// // src/components/Layout.jsx
// import React, { useState } from "react";
// import { Outlet, NavLink } from "react-router-dom";
// import "../styles/Dashboard.css";
// export default function Layout() {
// const [isSidebarOpen, setSidebarOpen] = useState(true);
// const toggleSidebar = () => {
// setSidebarOpen(!isSidebarOpen);
// };
// return (
// <div className="dashboard-layout">
// {/* Sidebar */}
// {isSidebarOpen && (
// <aside className="sidebar">
// <nav>
// <h1></h1>
// <NavLink to="/app/dashboard">Dashboards</NavLink>
// <NavLink to="/app/expenses-to-approve">Expenses to be approved</NavLink>
// <NavLink to="/app/expenses">Expenses</NavLink>
// <NavLink to="/app/inventory">Inventory</NavLink>
// <NavLink to="/app/payroll">Payroll</NavLink>
// <NavLink to="/app/hotel">Hotel</NavLink>
// <NavLink to="/app/income">Income</NavLink>
// <NavLink to="/app/employees">Employees</NavLink>
// <NavLink to="/app/contracts">Contracts</NavLink>
// <NavLink to="/app/payments">Payments</NavLink>
// <NavLink to="/app/pending-approval">PendingApproval</NavLink>
// </nav>
// </aside>
// )}
// {/* Contenedor principal */}
// <div className="main-content">
// {/* Topbar */}
// <div className="topbar">
// <button onClick={toggleSidebar} style={{ fontSize: "1.2rem", marginRight: "auto", background: "none", border: "none", color: "white", cursor: "pointer" }}>
// ☰
// </button>
// <span >Dashboard</span> {/* Cambia esto dinámicamente si deseas */}
// </div>
// {/* Contenido de cada página */}
// <div className="content">
// <Outlet />
// </div>
// </div>
// </div>
// );
// }
// <NavLink to="/app/users">Users</NavLink>

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>
// );
// }

View File

@@ -0,0 +1,74 @@
/* components/Modals/ConfirmationModal.css */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(122, 0, 41, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal {
background: white;
padding: 30px;
border-radius: 6px;
width: 450px;
max-width: 95%;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4);
border: 5px solid #7a0029;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
color: white;
background-color: #7a0029;
padding: 15px 20px;
border-radius: 5px 5px 0 0;
}
.modal-body {
padding: 20px;
font-size: 16px;
text-align: center;
}
.modal-buttons {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 20px;
}
.modal-button {
padding: 10px 25px;
font-size: 18px;
border-radius: 30px;
border: 2px solid #7a0029;
cursor: pointer;
min-width: 100px;
}
.modal-button.yes {
color: green;
background-color: white;
}
.modal-button.no {
color: #7a0029;
background-color: white;
}
.close-button {
font-size: 24px;
color: white;
background: none;
border: none;
cursor: pointer;
}

View File

@@ -0,0 +1,28 @@
// components/Modals/ConfirmationModal.jsx
import React from 'react';
import './ConfirmationModal.css'; // Estilos separados
import { useContext } from 'react';
import { langContext } from '../../context/LenguageContext';
export default function ConfirmationModal({ isOpen, statusType, onConfirm, onCancel }) {
const { lang } = useContext(langContext);
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h3>{lang === "en" ? "Confirm the status change" : "Confirmar el cambio de estado"}</h3>
<button className="close-button" onClick={onCancel}>×</button>
</div>
<div className="modal-body">
<p>{lang === "en" ? "Are you sure you received" : "¿Estás seguro de que recibiste"} "{statusType}"?</p>
<div className="modal-buttons">
<button className="modal-button yes" onClick={onConfirm}>{lang === "en" ? "YES" : "SÍ"}</button>
<button className="modal-button no" onClick={onCancel}>{lang === "en" ? "NO" : "NO"}</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,70 @@
// components/Modals/ConfirmationModal.jsx
import React, { useState, useEffect } from 'react';
import './ConfirmationModal.css'; // Estilos separados
import { useContext } from 'react';
import { langContext } from '../../context/LenguageContext';
export default function ConfirmationOutcome({ isOpen, onConfirm, onCancel, description, taxes, initialAmount, initialTaxId, isFixedPayment }) {
/*const [formHousekepeer, setFormHousekepeer] = useState(null)/*/
const { lang } = useContext(langContext);
const [form, setForm] = useState({
tax: '',
amount: ''
});
useEffect(() => {
if (isOpen) {
setForm({
tax: initialTaxId || '',
amount: initialAmount || ''
});
}
}, [isOpen, initialAmount, initialTaxId]);
if (!isOpen) return null;
const handleChange = (e) => {
const { name, value } = e.target;
//console.log(name, value);
setForm((prev) => ({ ...prev, [name]: value }));
};
const handleConfirmClick = () => {
// Envía los valores al padre (Outcomes.jsx)
onConfirm(form.tax, form.amount);
};
return (
<div className="modal-overlay">
<form onSubmit={handleConfirmClick}>
<div className="modal">
<div className="modal-header">
<h3>{lang === "en" ? "Confirm the status change" : "Confirmar el cambio de estado"}</h3>
<button className="close-button" onClick={onCancel}>×</button>
</div>
<div className="modal-body">
<div>
<p>{lang === "en" ? "Payment" : "Pago"}</p>
<input name = "PCO" value={description} onChange={handleChange} disabled={true}></input>
</div>
<div>
<p>{lang === "en" ? "Amount" : "Subtotal"}</p>
<input type='number' required name = "amount" value={form.amount} onChange={handleChange} disabled={isFixedPayment}></input>
</div>
<div>
<p>{lang === "en" ? "Tax" : "Impuesto"}</p>
<select name = "tax" required value={form.tax} onChange={handleChange} disabled={isFixedPayment}>
<option value="">{lang === "en" ? "Select a tax" : "Selecciona un impuesto"}</option>
{taxes?.map(tax => (
<option key={tax.id} value={tax.id}>{tax.name}</option>
))}
</select>
</div>
<div className="modal-buttons">
<button className="modal-button yes" type='submit'>{lang === "en" ? "PAID" : "PAGAR"}</button>
</div>
</div>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,122 @@
// components/Modals/ConfirmationModal.jsx
import React, { useState, useEffect } from 'react';
import './ConfirmationModal.css'; // Estilos separados
import { useContext } from 'react';
import { langContext } from '../../context/LenguageContext';
export default function ConfirmationOutcome({ isOpen, onConfirm, onCancel, formHousekepeer, idproduct, nameProduct, productStock }) {
/*const [formHousekepeer, setFormHousekepeer] = useState(null)/*/
const { lang } = useContext(langContext);
if (!isOpen) return null;
const [PCO, setProduct] = useState(idproduct);
const [UCO, setUnits] = useState(null);
const [HCO, setHousekeeper] = useState(null);
const [form, setForm] = useState({
PCO: idproduct || '',
UCO: '',
HCO: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
if (name === 'UCO') {
const numericValue = Number(value);
const maxStock = Number(productStock) || 0;
if (value === '') {
setForm((prev) => ({ ...prev, [name]: '' }));
} else if (!isNaN(numericValue) && numericValue >= 0) {
const cappedValue = numericValue > maxStock ? maxStock : numericValue;
setForm((prev) => ({ ...prev, [name]: cappedValue.toString() }));
}
} else {
setForm((prev) => ({ ...prev, [name]: value }));
}
};
const handleConfirmClick = () => {
// Envía los valores al padre (Outcomes.jsx)
onConfirm(form.PCO, form.UCO, form.HCO);
};
return (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h3>{lang === "en" ? "Confirm the status change" : "Confirmar el cambio de estado"}</h3>
<button className="close-button" onClick={onCancel}>×</button>
</div>
<div className="modal-body">
<div>
<p>{lang === "en" ? "Product" : "Producto"}</p>
<input
name="PCO"
value={nameProduct}
onChange={handleChange}
disabled={true}
style={{ color: '#000', width: '100%', padding: '8px' }}
/>
</div>
<div>
<p>{lang === "en" ? "Units" : "Unidades"}</p>
<input
type='number'
name="UCO"
value={form.UCO}
min={0}
max={productStock ? Number(productStock) : undefined}
disabled={!productStock || Number(productStock) <= 0}
title={(!productStock || Number(productStock) <= 0) ? (lang === "es" ? "No se puede consumir cuando el stock es 0" : "Cannot consume when stock is 0") : (lang === "es" ? `Máximo disponible: ${productStock}` : `Maximum available: ${productStock}`)}
onKeyDown={(e) => {
if (e.key === '-' || e.key === 'e' || e.key === 'E' || e.key === '+') {
e.preventDefault();
}
}}
onChange={handleChange}
style={{
color: '#000',
width: '100%',
padding: '8px',
backgroundColor: (!productStock || Number(productStock) <= 0) ? '#f0f0f0' : 'white',
cursor: (!productStock || Number(productStock) <= 0) ? 'not-allowed' : 'text',
opacity: (!productStock || Number(productStock) <= 0) ? 0.6 : 1
}}
/>
{(!productStock || Number(productStock) <= 0) && (
<p style={{ color: 'red', fontSize: '12px', marginTop: '5px', marginBottom: 0 }}>
{lang === "es" ? "No hay stock disponible" : "There is no stock"}
</p>
)}
</div>
<div>
<p>{lang === "en" ? "Housekeeper" : "Camarista"}</p>
<select
name="HCO"
value={form.HCO}
onChange={handleChange}
style={{ color: '#000', width: '100%', padding: '8px' }}
>
<option value="">{lang === "en" ? "Select a Housekepeer" : "Selecciona una ama de llaves"}</option>
{formHousekepeer && formHousekepeer.map(HK => (
<option key={HK.rfc_employee} value={HK.rfc_employee}>{HK.name_emp}</option>
))}
</select>
</div>
<div className="modal-buttons">
<button
className="modal-button yes"
onClick={handleConfirmClick}
disabled={!productStock || Number(productStock) <= 0}
style={{
opacity: (!productStock || Number(productStock) <= 0) ? 0.5 : 1,
cursor: (!productStock || Number(productStock) <= 0) ? 'not-allowed' : 'pointer'
}}
>
{lang === "en" ? "SAVE" : "GUARDAR"}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,111 @@
.discard-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease-in-out;
}
.discard-modal-box {
background: white;
padding: 0;
border-radius: 12px;
width: 450px;
max-width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
animation: slideUp 0.3s ease-out;
overflow: hidden;
}
.discard-modal-header {
background-color: #7a0029;
color: white;
padding: 20px;
text-align: center;
}
.discard-modal-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.discard-modal-body {
padding: 24px;
text-align: center;
color: #213547;
font-size: 1rem;
line-height: 1.5;
}
.discard-modal-body p {
margin: 0;
}
.discard-modal-actions {
display: flex;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #e2e8f0;
justify-content: flex-end;
}
.discard-modal-button {
padding: 10px 24px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s ease;
min-width: 100px;
}
.discard-modal-button-cancel {
background-color: #e2e8f0;
color: #4a5568;
}
.discard-modal-button-cancel:hover {
background-color: #cbd5e0;
}
.discard-modal-button-confirm {
background-color: #7a0029;
color: white;
}
.discard-modal-button-confirm:hover {
background-color: #5a001f;
}
.discard-modal-button:active {
transform: translateY(1px);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { useContext } from 'react';
import { langContext } from '../../context/LenguageContext';
import './DiscardConfirmModal.css';
export default function DiscardConfirmModal({ isOpen, message, onConfirm, onCancel }) {
const { lang } = useContext(langContext);
if (!isOpen) return null;
return (
<div className="discard-modal-overlay" onClick={onCancel}>
<div className="discard-modal-box" onClick={(e) => e.stopPropagation()}>
<div className="discard-modal-header">
<h3>{lang === "es" ? "Confirmar descarte" : "Confirm Discard"}</h3>
</div>
<div className="discard-modal-body">
<p>{message}</p>
</div>
<div className="discard-modal-actions">
<button className="discard-modal-button discard-modal-button-cancel" onClick={onCancel}>
{lang === "es" ? "Cancelar" : "Cancel"}
</button>
<button className="discard-modal-button discard-modal-button-confirm" onClick={onConfirm}>
{lang === "es" ? "Confirmar" : "Confirm"}
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-box {
background: white;
padding: 30px;
border-radius: 10px;
width: 350px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
.modal-box h3 {
margin-top: 0;
margin-bottom: 15px;
color: #111;
text-align: center;
}
.modal-input {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: none;
border-radius: 12px;
background: #f1f1f1;
font-size: 14px;
}
.btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 6px;
font-weight: 600;
margin-bottom: 10px;
cursor: pointer;
}
.btn--primary {
background-color: #8b0000;
color: white;
}
.btn--primary:hover {
background-color: #a00000;
}

View File

@@ -0,0 +1,22 @@
import React, { useState } from 'react';
import './Modal.css'; // Asegúrate de tener el estilo del modal
export default function Modal({ isOpen, closeModal }) {
return (
isOpen && (
<div className="modal-overlay">
<div className="modal-box">
<h3>Enter your email address and we'll send a new password to your email.</h3>
<input
type="email"
placeholder="Email"
className="modal-input"
/>
<button onClick={closeModal} className="btn btn--primary">
Send
</button>
</div>
</div>
)
);
}

View File

@@ -0,0 +1,53 @@
z/* src/styles/Navbar.css */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #4a0d0d; /* Marrón oscuro */
padding: 12px 20px;
color: white;
}
.navbar__brand {
font-size: 1.2rem;
font-weight: bold;
}
.navbar__nav {
display: flex;
gap: 15px;
}
.nav__link {
color: white;
text-decoration: none;
font-size: 0.95rem;
}
.nav__link.active {
border-bottom: 2px solid #f8d47b; /* Amarillo suave */
}
.navbar__actions {
display: flex;
gap: 10px;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
}
.btn--primary {
background-color: #f8d47b; /* Amarillo */
color: #4a0d0d;
}
.btn--secondary {
background-color: #ddd;
color: #333;
}

View File

@@ -0,0 +1,33 @@
// src/components/Navbar.jsx
import { NavLink, useNavigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext.jsx";
import "./Navbar.css"; // estilos separados
export default function Navbar() {
const { isAuthenticated, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate("/"); // 👈 al cerrar sesión vuelve al Login
};
return (
<header className="navbar">
<div className="navbar__brand"></div>
<div className="navbar__actions">
{isAuthenticated ? (
<button className="btn btn--secondary" onClick={handleLogout}>
Logout
</button>
) : (
<NavLink to="/" className="btn btn--primary">
Login
</NavLink>
)}
</div>
</header>
);
}

View File

@@ -0,0 +1,53 @@
// Sidebar.jsx
import { useNavigate } from "react-router-dom";
import { menuConfig } from "../constants/menuConfig";
const Sidebar = () => {
const navigate = useNavigate();
return (
<aside className="sidebar">
<nav>
{Object.entries(menuConfig).map(([key, section]) => (
<button
key={key}
className="sidebar-link"
onClick={() => navigate(section.basePath)}
>
{section.label}
</button>
))}
</nav>
</aside>
);
};
export default Sidebar;
// import React, { useState } from "react";
// import { NavLink } from "react-router-dom";
// import "./..styles/Sidebar.css";
// export default function Sidebar() {
// const [collapsed, setCollapsed] = useState(false);
// return (
// <div className={`sidebar ${collapsed ? "collapsed" : ""}`}>
// <div className="sidebar-header">
// <button className="toggle-btn" onClick={() => setCollapsed(!collapsed)}>
// ☰
// </button>
// {!collapsed && <span className="title">Dashboard</span>}
// </div>
// <nav className="sidebar-nav">
// <NavLink to="/app/dashboard">Dashboards</NavLink>
// <NavLink to="/app/expenses-to-approve">Expenses to be approved</NavLink>
// <NavLink to="/app/expenses">Expenses</NavLink>
// <NavLink to="/app/inventory">Inventory</NavLink>
// <NavLink to="/app/payroll">Payroll</NavLink>
// <NavLink to="/app/hotel">Hotel</NavLink>
// </nav>
// </div>
// );
// }

View File

@@ -0,0 +1,100 @@
.summary-card-enhanced {
background: linear-gradient(145deg, #ffffff, #f8f9fa);
border-radius: 16px;
padding: 20px 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
min-width: 180px;
flex: 1;
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.summary-card-enhanced:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.summary-card-enhanced.primary {
background: linear-gradient(145deg, #ffffff, #f8f9fa);
color: #333;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.card-title {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0;
color: #666;
}
.card-amount {
font-size: 28px;
font-weight: 700;
color: #333;
margin: 8px 0;
line-height: 1.2;
}
.card-percentage {
font-size: 16px;
font-weight: 600;
color: #28a745;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.card-loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 100px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-top-color: #fcd200;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 1200px) {
.summary-card-enhanced {
min-width: 150px;
}
.card-amount {
font-size: 24px;
}
}
@media (max-width: 768px) {
.summary-card-enhanced {
min-width: 100%;
}
.card-amount {
font-size: 22px;
}
.card-title {
font-size: 12px;
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import './SummaryCard.css';
const SummaryCard = ({
title,
amount,
isLoading = false,
isPrimary = false
}) => {
if (isLoading) {
return (
<div className={`summary-card-enhanced ${isPrimary ? 'primary' : ''}`}>
<div className="card-loading">
<div className="loading-spinner"></div>
</div>
</div>
);
}
return (
<div className={`summary-card-enhanced ${isPrimary ? 'primary' : ''}`}>
<div className="card-header">
<h3 className="card-title">{title}</h3>
</div>
<div className="card-amount">${amount || 0}</div>
</div>
);
};
export default SummaryCard;

View File

@@ -0,0 +1,28 @@
import React from 'react';
const Switch = ({ checked, onChange, disabled = false }) => {
return (
<label className="inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only"
checked={checked}
onChange={onChange}
disabled={disabled}
/>
<div
className={`relative inline-block w-10 h-6 transition duration-200 ease-in-out rounded-full ${
checked ? 'bg-green-500' : 'bg-gray-300'
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<span
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-200 ease-in-out ${
checked ? 'translate-x-4' : 'translate-x-0'
}`}
/>
</div>
</label>
);
};
export default Switch;

View File

@@ -0,0 +1,152 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { FaExternalLinkAlt } from 'react-icons/fa';
import './Table.css';
export default function Table({ columns, data }) {
return (
<table className="custom-table">
<thead>
<tr>
{columns.map((col) => (
<th key={col.key}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<tr key={rowIndex}>
{columns.map((col, colIndex) => (
<td key={colIndex}>
{col.render
? col.render(row[col.key], row, rowIndex)
: col.key === 'propertyId'
? (
<Link
to={`/app/properties/${row[col.key]}`}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
textDecoration: 'none',
color: '#003366',
fontWeight: 'bold'
}}
>
{row[col.key]}
<FaExternalLinkAlt size={12} />
</Link>
)
: row[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// import React from 'react';
// import { Link } from 'react-router-dom';
// import { FaArrowRight } from 'react-icons/fa';
// import './Table.css';
// export default function Table({ columns, data }) {
// return (
// <table className="custom-table">
// <thead>
// <tr>
// {columns.map((col) => (
// <th key={col.key}>{col.header}</th>
// ))}
// </tr>
// </thead>
// <tbody>
// {data.map((row, rowIndex) => (
// <tr key={rowIndex}>
// {columns.map((col, colIndex) => (
// <td key={colIndex}>
// {col.render
// ? col.render(row[col.key], row)
// : col.key === 'propertyId'
// ? (
// <Link to={`/app/properties/${row[col.key]}`} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
// {row[col.key]}
// <FaArrowRight size={12} />
// </Link>
// )
// : row[col.key]}
// </td>
// ))}
// </tr>
// ))}
// </tbody>
// </table>
// );
// }
// import React from 'react';
// import { Link } from 'react-router-dom';
// import './Table.css';
// export default function Table({ columns, data }) {
// return (
// <table className="custom-table">
// <thead>
// <tr>
// {columns.map((col) => (
// <th key={col.key}>{col.header}</th>
// ))}
// </tr>
// </thead>
// <tbody>
// {data.map((row, rowIndex) => (
// <tr key={rowIndex}>
// {columns.map((col, colIndex) => (
// <td key={colIndex}>
// {col.render
// ? col.render(row[col.key], row) // usar render si está definido
// : col.key === 'propertyId'
// ? <Link to={`/app/properties/${row[col.key]}`}>{row[col.key]}</Link>
// : row[col.key]}
// </td>
// ))}
// </tr>
// ))}
// </tbody>
// </table>
// );
// }
// import React from 'react';
// import './Table.css';
// export default function Table({ columns, data }) {
// return (
// <table className="custom-table">
// <thead>
// <tr>
// {columns.map((col) => (
// <th key={col.key}>{col.header}</th>
// ))}
// </tr>
// </thead>
// <tbody>
// {data.map((row, rowIndex) => (
// <tr key={rowIndex}>
// {columns.map((col, colIndex) => (
// <td key={colIndex}>
// {col.render
// ? col.render(row[col.key], row) // usar render si está definido
// : row[col.key]}
// </td>
// ))}
// </tr>
// ))}
// </tbody>
// </table>
// );
// }

View File

@@ -0,0 +1,152 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { FaExternalLinkAlt } from 'react-icons/fa';
import './Table.css';
export default function Table({ columns, data }) {
return (
<table className="custom-table">
<thead>
<tr>
{columns.map((col) => (
<th key={col.key} style={col.headerStyle || {}}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<tr key={rowIndex}>
{columns.map((col, colIndex) => (
<td key={colIndex}>
{col.render
? col.render(row[col.key], row)
: col.key === 'propertyId'
? (
<Link
to={`/app/properties/${row[col.key]}`}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
textDecoration: 'none',
color: '#003366',
fontWeight: 'bold'
}}
>
{row[col.key]}
<FaExternalLinkAlt size={12} />
</Link>
)
: row[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// import React from 'react';
// import { Link } from 'react-router-dom';
// import { FaArrowRight } from 'react-icons/fa';
// import './Table.css';
// export default function Table({ columns, data }) {
// return (
// <table className="custom-table">
// <thead>
// <tr>
// {columns.map((col) => (
// <th key={col.key}>{col.header}</th>
// ))}
// </tr>
// </thead>
// <tbody>
// {data.map((row, rowIndex) => (
// <tr key={rowIndex}>
// {columns.map((col, colIndex) => (
// <td key={colIndex}>
// {col.render
// ? col.render(row[col.key], row)
// : col.key === 'propertyId'
// ? (
// <Link to={`/app/properties/${row[col.key]}`} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
// {row[col.key]}
// <FaArrowRight size={12} />
// </Link>
// )
// : row[col.key]}
// </td>
// ))}
// </tr>
// ))}
// </tbody>
// </table>
// );
// }
// import React from 'react';
// import { Link } from 'react-router-dom';
// import './Table.css';
// export default function Table({ columns, data }) {
// return (
// <table className="custom-table">
// <thead>
// <tr>
// {columns.map((col) => (
// <th key={col.key}>{col.header}</th>
// ))}
// </tr>
// </thead>
// <tbody>
// {data.map((row, rowIndex) => (
// <tr key={rowIndex}>
// {columns.map((col, colIndex) => (
// <td key={colIndex}>
// {col.render
// ? col.render(row[col.key], row) // usar render si está definido
// : col.key === 'propertyId'
// ? <Link to={`/app/properties/${row[col.key]}`}>{row[col.key]}</Link>
// : row[col.key]}
// </td>
// ))}
// </tr>
// ))}
// </tbody>
// </table>
// );
// }
// import React from 'react';
// import './Table.css';
// export default function Table({ columns, data }) {
// return (
// <table className="custom-table">
// <thead>
// <tr>
// {columns.map((col) => (
// <th key={col.key}>{col.header}</th>
// ))}
// </tr>
// </thead>
// <tbody>
// {data.map((row, rowIndex) => (
// <tr key={rowIndex}>
// {columns.map((col, colIndex) => (
// <td key={colIndex}>
// {col.render
// ? col.render(row[col.key], row) // usar render si está definido
// : row[col.key]}
// </td>
// ))}
// </tr>
// ))}
// </tbody>
// </table>
// );
// }

View File

@@ -0,0 +1,184 @@
/* .custom-table {
width: 100%;
border-collapse: collapse;
font-family:'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
}
.custom-table thead tr {
background-color: #7b001a;
color: white;
}
.custom-table th,
.custom-table td {
padding: 10px 15px;
border: 2px solid #ffcc00;
text-align: left;
}
.custom-table tbody tr {
background-color: #ffffff;
color: black;
}
.status-badge {
padding: 5px 10px;
border-radius: 14px;
font-weight: bold;
display: inline-block;
text-align: center;
}
.status-badge.active {
color: green;
border: 1px solid green;
background-color: rgba(0, 128, 0, 0.192);
}
.status-badge.reject {
color: red;
border: 1px solid rgb(190, 4, 4);
background-color: rgba(255, 0, 0, 0.171);
}
.status-badge.pending {
color: rgb(128, 83, 0);
border: 1px solid rgb(235, 158, 15);
background-color: rgba(248, 176, 42, 0.76);
}
.status-button.approve {
background-color: #009e2db0;
color: white;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-weight: bold;
}
.status-button.reject {
background-color: #d80404b0;
color: white;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-weight: bold;
} */
.custom-table {
width: 100%;
border-collapse: collapse;
/* font-family:'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; */
font-family: 'Franklin Gothic', 'Arial Narrow', Monserrat, sans-serif
}
.custom-table thead tr {
background-color: #7b001a;
color: white;
}
/*Bordes de línea*/
.custom-table th,
.custom-table td {
padding: 10px 15px;
border: 2px solid #ffcc00;
text-align: left;
}
.custom-table tbody tr {
font-family: montserrat;
background-color: #ffffff;
color: #515151;
}
.status-badge {
padding: 5px 10px;
border-radius: 14px;
font-weight: bold;
display: inline-block;
text-align: center;
}
.status-badge.active {
color: green;
border: 1px solid green;
background-color: rgba(0, 128, 0, 0.192);
}
.status-badge.reject {
color: red;
border: 1px solid rgb(190, 4, 4);
background-color: rgba(255, 0, 0, 0.171);
}
.status-badge.pending {
color: rgb(128, 83, 0);
border: 1px solid rgb(235, 158, 15);
background-color: rgba(248, 176, 42, 0.76);
}
.status-badge.paid {
color: green;
border: 1px solid green;
background-color: rgba(0, 128, 0, 0.192);
}
.status-button.approve {
background-color: #009e2db0;
color: white;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-weight: bold;
}
.status-button.reject {
background-color: #d80404b0;
color: white;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-weight: bold;
}
.status-button.pending {
background-color: #e2c000;
color: white;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-weight: bold;
}
.status-button.paid {
background-color: #009e2db0;
color: white;
border: none;
padding: 4px 12px;
border-radius: 2px;
font-weight: bold;
}
.add-button {
background-color: transparent;
color: #7b001a;
font-weight: bold;
border: none;
cursor: pointer;
font-size: 14px;
}
.add-button:hover {
text-decoration: underline;
}
/*Icono de flecha*/
.custom-table a:hover {
color: #0055aa;
}
.custom-table a:hover svg {
transform: translateX(2px);
transition: transform 0.2s ease;
}

View File

@@ -0,0 +1,8 @@
/* .topbar {
background-color: #e0b200;
padding: 1rem;
color: #0f0d75;
font-weight: bold;
font-size: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
} */

View File

@@ -0,0 +1,13 @@
// src/components/Topbar.jsx
import React from "react";
import "./Topbar.css";
const Topbar = () => {
return (
<header className="topbar">
<h1 className="logo">Hacienda San Ángel</h1>
</header>
);
};
export default Topbar;