- 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)
348 lines
11 KiB
JavaScript
348 lines
11 KiB
JavaScript
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>
|
|
);
|
|
} |