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,348 @@
import React, { useEffect, useState, useCallback } from 'react';
import DateRangeFilter from '../../components/Filters/DateRangeFilter';
import './HotelPL.css';
import { useContext } from 'react';
import { langContext } from '../../context/LenguageContext';
export default function RestaurantAnalysis() {
const { lang } = useContext(langContext);
const [dateRange, setDateRange] = useState({ from: '', to: '' });
const [countticket, setCountticket] = useState(0);
const [efectivo, setEfectivo] = useState(0);
const [otros, setOtros] = useState(0);
const [propinas, setPropinas] = useState(0);
const [tarjeta, setTarjeta] = useState(0);
const [vales, setVales] = useState(0);
const [sumatotal, setSumatotal] = useState(0);
const [ticketpromedio, setTicketpromedio] = useState(0);
const [loading, setLoading] = useState(false);
// Set default dates to current month
useEffect(() => {
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const formatDateForInput = (date) => {
return date.toISOString().split('T')[0];
};
setDateRange({
from: formatDateForInput(startOfMonth),
to: formatDateForInput(endOfMonth)
});
}, []);
const formatDate = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toISOString().split('T')[0];
};
const formatCurrency = (value) => {
const num = parseFloat(value || 0);
return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
const loadCountticket = useCallback(async () => {
if (!dateRange.from || !dateRange.to) return;
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/incomes/countticket`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initial_date: formatDate(dateRange.from),
final_date: formatDate(dateRange.to)
})
});
const json = await res.json();
setCountticket(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading countticket:', err);
setCountticket(0);
}
}, [dateRange]);
const loadEfectivo = useCallback(async () => {
if (!dateRange.from || !dateRange.to) return;
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/incomes/efectivo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initial_date: formatDate(dateRange.from),
final_date: formatDate(dateRange.to)
})
});
const json = await res.json();
setEfectivo(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading efectivo:', err);
setEfectivo(0);
}
}, [dateRange]);
const loadOtros = useCallback(async () => {
if (!dateRange.from || !dateRange.to) return;
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/incomes/otros`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initial_date: formatDate(dateRange.from),
final_date: formatDate(dateRange.to)
})
});
const json = await res.json();
setOtros(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading otros:', err);
setOtros(0);
}
}, [dateRange]);
const loadPropinas = useCallback(async () => {
if (!dateRange.from || !dateRange.to) return;
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/incomes/propinas`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initial_date: formatDate(dateRange.from),
final_date: formatDate(dateRange.to)
})
});
const json = await res.json();
setPropinas(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading propinas:', err);
setPropinas(0);
}
}, [dateRange]);
const loadTarjeta = useCallback(async () => {
if (!dateRange.from || !dateRange.to) return;
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/incomes/tarjeta`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initial_date: formatDate(dateRange.from),
final_date: formatDate(dateRange.to)
})
});
const json = await res.json();
setTarjeta(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading tarjeta:', err);
setTarjeta(0);
}
}, [dateRange]);
const loadVales = useCallback(async () => {
if (!dateRange.from || !dateRange.to) return;
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/incomes/vales`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initial_date: formatDate(dateRange.from),
final_date: formatDate(dateRange.to)
})
});
const json = await res.json();
setVales(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading vales:', err);
setVales(0);
}
}, [dateRange]);
const loadSumatotal = useCallback(async () => {
if (!dateRange.from || !dateRange.to) return;
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/incomes/sumatotal`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initial_date: formatDate(dateRange.from),
final_date: formatDate(dateRange.to)
})
});
const json = await res.json();
setSumatotal(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading sumatotal:', err);
setSumatotal(0);
}
}, [dateRange]);
const loadTicketpromedio = useCallback(async () => {
if (!dateRange.from || !dateRange.to) return;
try {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/incomes/ticketpromedio`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initial_date: formatDate(dateRange.from),
final_date: formatDate(dateRange.to)
})
});
const json = await res.json();
setTicketpromedio(parseFloat(json.data || 0));
} catch (err) {
console.error('Error loading ticketpromedio:', err);
setTicketpromedio(0);
}
}, [dateRange]);
useEffect(() => {
const loadAllData = async () => {
if (!dateRange.from || !dateRange.to) return;
setLoading(true);
try {
await Promise.all([
loadCountticket(),
loadEfectivo(),
loadOtros(),
loadPropinas(),
loadTarjeta(),
loadVales(),
loadSumatotal(),
loadTicketpromedio()
]);
} catch (err) {
console.error('Error loading data:', err);
} finally {
setLoading(false);
}
};
loadAllData();
}, [dateRange, loadCountticket, loadEfectivo, loadOtros, loadPropinas, loadTarjeta, loadVales, loadSumatotal, loadTicketpromedio]);
return (
<div className="dashboard-container">
<div className="page-header">
<h2 className="page-title">
{lang === "en" ? "Restaurant Analysis" : "Análisis de Restaurante"}
</h2>
</div>
<div className="filters-section">
<DateRangeFilter
dateRange={dateRange}
onDateChange={setDateRange}
lang={lang}
/>
</div>
<div className="metrics-grid">
{/* Count Tickets Card */}
<div className="metric-card">
<h3>{lang === "en" ? "Count Tickets" : "Cantidad de Tickets"}</h3>
<div className="metric-value">
{loading ? (
<div className="loading-skeleton"></div>
) : (
<span className="count-value">{countticket.toLocaleString()}</span>
)}
</div>
</div>
{/* Efectivo Card */}
<div className="metric-card">
<h3>{lang === "en" ? "Cash" : "Efectivo"}</h3>
<div className="metric-value">
{loading ? (
<div className="loading-skeleton"></div>
) : (
<span className="currency-value">${formatCurrency(efectivo)}</span>
)}
</div>
</div>
{/* Otros Card */}
<div className="metric-card">
<h3>{lang === "en" ? "Others" : "Otros"}</h3>
<div className="metric-value">
{loading ? (
<div className="loading-skeleton"></div>
) : (
<span className="currency-value">${formatCurrency(otros)}</span>
)}
</div>
</div>
{/* Propinas Card */}
<div className="metric-card">
<h3>{lang === "en" ? "Tips" : "Propinas"}</h3>
<div className="metric-value">
{loading ? (
<div className="loading-skeleton"></div>
) : (
<span className="currency-value">${formatCurrency(propinas)}</span>
)}
</div>
</div>
{/* Tarjeta Card */}
<div className="metric-card">
<h3>{lang === "en" ? "Card" : "Tarjeta"}</h3>
<div className="metric-value">
{loading ? (
<div className="loading-skeleton"></div>
) : (
<span className="currency-value">${formatCurrency(tarjeta)}</span>
)}
</div>
</div>
{/* Vales Card */}
<div className="metric-card">
<h3>{lang === "en" ? "Vouchers" : "Vales"}</h3>
<div className="metric-value">
{loading ? (
<div className="loading-skeleton"></div>
) : (
<span className="currency-value">${formatCurrency(vales)}</span>
)}
</div>
</div>
{/* Suma Total Card */}
<div className="metric-card">
<h3>{lang === "en" ? "Total Sum" : "Suma Total"}</h3>
<div className="metric-value">
{loading ? (
<div className="loading-skeleton"></div>
) : (
<span className="currency-value total-value">${formatCurrency(sumatotal)}</span>
)}
</div>
</div>
{/* Ticket Promedio Card */}
<div className="metric-card">
<h3>{lang === "en" ? "Average Ticket" : "Ticket Promedio"}</h3>
<div className="metric-value">
{loading ? (
<div className="loading-skeleton"></div>
) : (
<span className="currency-value">${formatCurrency(ticketpromedio)}</span>
)}
</div>
</div>
</div>
</div>
);
}