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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user