Files
CRM-Hotel/frontend/Frontend-Hotel/src/pages/Dashboard/HotelPL.jsx
Consultoria AS 0211bea186 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)
2026-01-17 18:52:34 -08:00

439 lines
14 KiB
JavaScript

import React, { useEffect, useState } from 'react';
import DateRangeFilter from '../../components/Filters/DateRangeFilter';
import './HotelPL.css';
import { useContext } from 'react';
import { langContext } from '../../context/LenguageContext';
export default function HotelPL() {
const { lang } = useContext(langContext);
const [dateRange, setDateRange] = useState({ from: '', to: '' });
const [revenue, setRevenue] = useState(0);
const [cogs, setCogs] = useState(0);
const [employeeShare, setEmployeeShare] = useState(0);
const [tips, setTips] = useState(0);
const [grossProfit, setGrossProfit] = useState(0);
const [weightedCategoriesCost, setWeightedCategoriesCost] = useState([]);
const [totalExpenses, setTotalExpenses] = useState(0);
const [ebitda, setEbitda] = useState(0);
const [loading, setLoading] = useState(false);
const formatDate = (dateStr) => {
return dateStr;
};
const formatCurrency = (value) => {
const num = parseFloat(value || 0);
return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
const calculatePercentage = (value, total) => {
if (!total || total === 0) return '0.00';
return ((parseFloat(value) / parseFloat(total)) * 100).toFixed(2);
};
const formatPercentageForDisplay = (percentageValue) => {
const numValue = parseFloat(percentageValue);
return (numValue * 100).toFixed(2);
};
const loadRevenue = async () => {
const start_date = formatDate(dateRange.from);
const end_date = formatDate(dateRange.to);
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/hotelpl/totalrevenue`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_date, end_date })
});
const json = await res.json();
setRevenue(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading revenue:', err);
setRevenue(0);
}
};
const loadCogs = async () => {
const start_date = formatDate(dateRange.from);
const end_date = formatDate(dateRange.to);
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/hotelpl/cogs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_date, end_date })
});
const json = await res.json();
setCogs(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading COGS:', err);
setCogs(0);
}
};
const loadEmployeeShare = async () => {
const start_date = formatDate(dateRange.from);
const end_date = formatDate(dateRange.to);
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/hotelpl/employeeshare`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_date, end_date })
});
const json = await res.json();
setEmployeeShare(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading employee share:', err);
setEmployeeShare(0);
}
};
const loadTips = async () => {
const start_date = formatDate(dateRange.from);
const end_date = formatDate(dateRange.to);
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/hotelpl/tips`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_date, end_date })
});
const json = await res.json();
setTips(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading tips:', err);
setTips(0);
}
};
const loadGrossProfit = async () => {
const start_date = formatDate(dateRange.from);
const end_date = formatDate(dateRange.to);
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/hotelpl/grossprofit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_date, end_date })
});
const json = await res.json();
setGrossProfit(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading gross profit:', err);
setGrossProfit(0);
}
};
const loadWeightedCategoriesCost = async () => {
const start_date = formatDate(dateRange.from);
const end_date = formatDate(dateRange.to);
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/hotelpl/weightedcategoriescost`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_date, end_date })
});
const json = await res.json();
const mapped = json.data.map(item => ({
id: item.id_expense_cat,
category: item.category_name,
es_category: item.spanish_name,
total: item.total,
participation: parseFloat(item.participation).toFixed(2) + '%'
}));
setWeightedCategoriesCost(mapped);
} catch (err) {
console.error('Error loading weigted categories:', err);
}
};
const loadEbidta = async () => {
const start_date = formatDate(dateRange.from);
const end_date = formatDate(dateRange.to);
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/hotelpl/ebitda`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_date, end_date })
});
const json = await res.json();
const data = json.data[0];
setTotalExpenses(parseFloat(data.expenses_total || 0));
setEbitda(parseFloat(data.ebitda || 0));
} catch (err) {
console.error('Error loading ebitda:', err);
setTotalExpenses(0);
setEbitda(0);
}
};
useEffect(() => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const pad = (n) => n.toString().padStart(2, '0');
const from = `${year}-${pad(month + 1)}-${pad(firstDay.getDate())}`;
const to = `${year}-${pad(month + 1)}-${pad(lastDay.getDate())}`;
setDateRange({ from, to });
}, []);
useEffect(() => {
if (!dateRange.from || !dateRange.to) return;
setLoading(true);
setRevenue(0);
setCogs(0);
setEmployeeShare(0);
setGrossProfit(0);
setWeightedCategoriesCost([]);
setTotalExpenses(0);
setEbitda(0);
Promise.all([
loadRevenue(),
loadCogs(),
loadEmployeeShare(),
loadTips(),
loadGrossProfit(),
loadWeightedCategoriesCost(),
loadEbidta()
]).finally(() => {
setLoading(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRange]);
const totalNetRevenue = revenue;
return (
<div className="report-page">
<div className="page-header">
<h2 className="page-title">
{lang === "en" ? "Hotel P&L" : "P&L del Hotel"}
</h2>
</div>
<DateRangeFilter
dateRange={dateRange}
onDateChange={setDateRange}
lang={lang}
/>
{loading ? (
<div className="loading-container">
<div className="loading-spinner" />
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
<span className="loading-text">
{lang === "en" ? "Loading financial data..." : "Cargando datos financieros..."}
</span>
</div>
) : (
<div className="pl-form-container">
<div className="pl-section">
<div className="section-header-row">
<h3 className="section-title">
{lang === "en" ? "Income" : "Ingresos"}
</h3>
<span className="section-header-percentage">
% {lang === "en" ? "Total Net Revenue" : "Ingreso Neto Total"}
</span>
</div>
<div className="form-row-split">
<div className="form-col">
<label className="form-label">Lodging</label>
<input
type="text"
className="form-input"
value={`$${formatCurrency(revenue)}`}
readOnly
/>
</div>
</div>
<div className="form-row-split">
<div className="form-col">
<label className="form-label label-bold">
{lang === "en" ? "Total Net Revenue" : "Ingreso Neto Total"}
</label>
<input
type="text"
className="form-input"
value={`$${formatCurrency(totalNetRevenue)}`}
readOnly
/>
</div>
</div>
<div className="form-row-split">
<div className="form-col">
<label className="form-label">COGS</label>
<input
type="text"
className="form-input"
value={`$${formatCurrency(cogs)}`}
readOnly
/>
</div>
<div className="form-col">
<input
type="text"
className="form-input"
value={`${formatPercentageForDisplay(calculatePercentage(cogs, totalNetRevenue))}%`}
readOnly
/>
</div>
</div>
<div className="form-row-split">
<div className="form-col">
<label className="form-label">
{lang === "en" ? "Employee Share" : "Participación Laboral"}
</label>
<input
type="text"
className="form-input"
value={`$${formatCurrency(employeeShare)}`}
readOnly
/>
</div>
<div className="form-col">
<input
type="text"
className="form-input"
value={`${formatPercentageForDisplay(calculatePercentage(employeeShare, totalNetRevenue))}%`}
readOnly
/>
</div>
</div>
<div className="form-row-split">
<div className="form-col">
<label className="form-label">
{lang === "en" ? "Tips" : "Propinas"}
</label>
<input
type="text"
className="form-input"
value={`$${formatCurrency(tips)}`}
readOnly
/>
</div>
</div>
<div className="form-row-split">
<div className="form-col">
<label className="form-label label-bold">
{lang === "en" ? "Gross Profit" : "Utilidad Bruta"}
</label>
<input
type="text"
className="form-input"
value={`$${formatCurrency(grossProfit)}`}
readOnly
/>
</div>
<div className="form-col">
<input
type="text"
className="form-input"
value={`${formatPercentageForDisplay(calculatePercentage(grossProfit, totalNetRevenue))}%`}
readOnly
/>
</div>
</div>
</div>
<div className="pl-section">
<div className="section-header-row">
<h3 className="section-title">
{lang === "en" ? "Expenses" : "Gastos"}
</h3>
<span className="section-header-percentage">
% {lang === "en" ? "Total Net Revenue" : "Ingreso Neto Total"}
</span>
</div>
{weightedCategoriesCost.length > 0 ? (
<>
{weightedCategoriesCost.map((expense, index) => (
<div key={index} className="form-row-split">
<div className="form-col">
<label className="form-label">
{lang === "en" ? expense.category : expense.es_category}
</label>
<input
type="text"
className="form-input"
value={`$${formatCurrency(expense.total)}`}
readOnly
/>
</div>
<div className="form-col">
<input
type="text"
className="form-input"
value={`${formatPercentageForDisplay(expense.participation.replace('%', ''))}%`}
readOnly
/>
</div>
</div>
))}
</>
) : (
<div className="no-data-message">
{lang === "en" ? "No expense data available" : "No hay datos de gastos disponibles"}
</div>
)}
</div>
<div className="pl-section">
<div className="form-row">
<label className="form-label">
{lang === "en" ? "Total Expenses" : "Gastos Totales"}
</label>
<input
type="text"
className="form-input"
value={`$${formatCurrency(totalExpenses)}`}
readOnly
/>
</div>
<div className="form-row">
<label className="form-label">
{lang === "en" ? "EBITDA" : "Utilidad Operativa"}
</label>
<input
type="text"
className="form-input highlight"
value={`$${formatCurrency(ebitda)}`}
readOnly
/>
</div>
</div>
</div>
)}
</div>
);
}