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,464 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import DateRangeFilter from '../../components/Filters/DateRangeFilter';
|
||||
import SummaryCard from '../../components/SummaryCard';
|
||||
import '../../components/Filters/Filters.css';
|
||||
import './Rejected.css';
|
||||
import Table from '../../components/Table/HotelTable';
|
||||
import axios from 'axios';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useContext } from 'react';
|
||||
import { langContext } from '../../context/LenguageContext';
|
||||
//**////** */ */ APROBADOS Y RECHAZADOS
|
||||
export default function Rejected() {
|
||||
const { lang } = useContext(langContext);
|
||||
const [rejectedExpenses, setRejectedExpenses] = useState([]);
|
||||
const [filteredExpenses, setFilteredExpenses] = useState([]);
|
||||
const [dateRange, setDateRange] = useState({ from: '', to: '' });
|
||||
const [totalRejected, setTotalRejected] = useState(0);
|
||||
const [mainSupplier, setMainSupplier] = useState('N/A');
|
||||
const [descriptionFilter, setDescriptionFilter] = useState('');
|
||||
const [areaFilter, setAreaFilter] = useState('');
|
||||
|
||||
//Obtener lista de gastos rechazados
|
||||
useEffect(() => {
|
||||
axios.get(import.meta.env.VITE_API_BASE_URL + '/expenses/rejectedexpenses')
|
||||
.then((res) => {
|
||||
const formatted = res.data.data.map((item, index) => ({
|
||||
id: index + 1,
|
||||
description: item.expense_description,
|
||||
requestDate: new Date(item.request_date).toISOString().split('T')[0],
|
||||
rejectedDate: new Date(item.reject_date).toISOString().split('T')[0],
|
||||
area: item.area,
|
||||
requestedBy: item.requested_by,
|
||||
amount: parseFloat(item.amount),
|
||||
supplier: 'N/A'
|
||||
}));
|
||||
setRejectedExpenses(formatted);
|
||||
setFilteredExpenses(formatted);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌ Error fetching rejected expenses:', err.message);
|
||||
});
|
||||
}, []);
|
||||
|
||||
//Obtener Total Rejected usando método GET correcto
|
||||
useEffect(() => {
|
||||
axios.post(import.meta.env.VITE_API_BASE_URL + '/expenses/totalapproved', { option: 2 })
|
||||
.then((res) => {
|
||||
const total = parseFloat(res.data.data);
|
||||
setTotalRejected(isNaN(total) ? 0 : total);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response) {
|
||||
console.error(`❌ Error ${err.response.status}: ${err.response.data.message}`);
|
||||
} else {
|
||||
console.error('❌ Error fetching total rejected amount:', err.message);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
//Obtener proveedor principal
|
||||
useEffect(() => {
|
||||
axios.get(import.meta.env.VITE_API_BASE_URL + '/expenses/mainsupplier')
|
||||
.then((res) => {
|
||||
if (res.data.data.length > 0) {
|
||||
setMainSupplier(res.data.data[0].supplier);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response) {
|
||||
console.error(`❌ Error ${err.response.status}: ${err.response.data.message}`);
|
||||
} else {
|
||||
console.error('❌ Error fetching main supplier:', err.message);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let filtered = [...rejectedExpenses];
|
||||
|
||||
if (dateRange.from && dateRange.to) {
|
||||
const from = new Date(dateRange.from);
|
||||
const to = new Date(dateRange.to);
|
||||
filtered = filtered.filter((item) => {
|
||||
const itemDate = new Date(item.rejectedDate);
|
||||
return itemDate >= from && itemDate <= to;
|
||||
});
|
||||
}
|
||||
|
||||
if (descriptionFilter) {
|
||||
filtered = filtered.filter((item) =>
|
||||
item.description?.toLowerCase().includes(descriptionFilter.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (areaFilter) {
|
||||
filtered = filtered.filter((item) => item.area === areaFilter);
|
||||
}
|
||||
|
||||
setFilteredExpenses(filtered);
|
||||
}, [dateRange, rejectedExpenses, descriptionFilter, areaFilter]);
|
||||
|
||||
//Columnas de tabla
|
||||
const columns = [
|
||||
{
|
||||
header: lang === "en" ? "EXPENSE DESCRIPTION" : "DESCRIPCIÓN DEL GASTO",
|
||||
key: 'description',
|
||||
render: (text, row) => (
|
||||
<Link
|
||||
to={`/app/expenses/${row.id}`}
|
||||
style={{ color: 'blue', textDecoration: 'underline' }}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{ header: lang === "en" ? "REQUEST DATE" : "FECHA DE SOLICITUD", key: 'requestDate' },
|
||||
{ header: lang === "en" ? "REJECTED DATE" : "FECHA DE RECHAZO", key: 'rejectedDate' },
|
||||
{ header: lang === "en" ? "AREA" : "ÁREA", key: 'area' },
|
||||
{ header: lang === "en" ? "REQUESTED BY" : "SOLICITADO POR", key: 'requestedBy' },
|
||||
{
|
||||
header: lang === "en" ? "AMOUNT" : "IMPORTE",
|
||||
key: 'amount',
|
||||
render: (amount) => `$${amount.toLocaleString()}`
|
||||
},
|
||||
{
|
||||
header: lang === "en" ? "STATUS" : "ESTADO",
|
||||
key: 'id',
|
||||
headerStyle: { textAlign: 'center' },
|
||||
render: () => (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<button className="status-button reject" style={{ pointerEvents: 'none' }}>
|
||||
{lang === "en" ? "REJECTED" : "RECHAZADO"}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const uniqueAreas = [...new Set(rejectedExpenses.map(item => item.area).filter(Boolean))].sort();
|
||||
|
||||
const clearFilters = () => {
|
||||
setDateRange({ from: '', to: '' });
|
||||
setDescriptionFilter('');
|
||||
setAreaFilter('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rejected-page">
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">
|
||||
{lang === "en" ? "Rejected Expenses" : "Gastos Rechazados"}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="filters-section">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={lang === "en" ? "Search by description..." : "Buscar por descripción..."}
|
||||
value={descriptionFilter}
|
||||
onChange={(e) => setDescriptionFilter(e.target.value)}
|
||||
className="filter-search"
|
||||
/>
|
||||
|
||||
<select
|
||||
value={areaFilter}
|
||||
onChange={(e) => setAreaFilter(e.target.value)}
|
||||
className="filter-select"
|
||||
>
|
||||
<option value="">{lang === "en" ? "All Areas" : "Todas las Áreas"}</option>
|
||||
{uniqueAreas.map((area, index) => (
|
||||
<option key={index} value={area}>
|
||||
{area}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<DateRangeFilter
|
||||
dateRange={dateRange}
|
||||
onDateChange={setDateRange}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="clear-filters-btn"
|
||||
>
|
||||
{lang === 'es' ? 'Limpiar filtros' : 'Clear filters'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="summary-cards-wrapper">
|
||||
<SummaryCard
|
||||
title={lang === "en" ? "Total Rejected" : "Total Rechazado"}
|
||||
amount={totalRejected}
|
||||
/>
|
||||
<div className="info-card">
|
||||
<h3 className="card-title">{lang === "en" ? "Main Supplier" : "Proveedor Principal"}</h3>
|
||||
<div className="card-value">{mainSupplier}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-section">
|
||||
<Table columns={columns} data={filteredExpenses} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
// import '../../components/Filters/Filters.css';
|
||||
// import Table from '../../components/Table/HotelTable';
|
||||
// import axios from 'axios';
|
||||
|
||||
// export default function Rejected() {
|
||||
// const [rejectedExpenses, setRejectedExpenses] = useState([]);
|
||||
// const [filteredExpenses, setFilteredExpenses] = useState([]);
|
||||
// const [dateRange, setDateRange] = useState({ from: '', to: '' });
|
||||
// const [totalRejected, setTotalRejected] = useState(0);
|
||||
// const [mainSupplier, setMainSupplier] = useState('N/A');
|
||||
|
||||
// // ✅ Cargar lista de gastos rechazados
|
||||
// useEffect(() => {
|
||||
// axios.get(import.meta.env.VITE_API_BASE_URL + '/expenses/rejectedexpenses')
|
||||
// .then((res) => {
|
||||
// const formatted = res.data.data.map((item, index) => ({
|
||||
// id: index + 1,
|
||||
// description: item.expense_description,
|
||||
// requestDate: new Date(item.request_date).toISOString().split('T')[0],
|
||||
// rejectedDate: new Date(item.reject_date).toISOString().split('T')[0],
|
||||
// area: item.area,
|
||||
// requestedBy: item.requested_by,
|
||||
// amount: parseFloat(item.amount),
|
||||
// supplier: 'N/A'
|
||||
// }));
|
||||
// setRejectedExpenses(formatted);
|
||||
// setFilteredExpenses(formatted);
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.error('Error fetching rejected expenses:', err);
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// // ✅ CORREGIDO: Método GET con parámetro en URL
|
||||
// useEffect(() => {
|
||||
// axios.get(import.meta.env.VITE_API_BASE_URL + '/expenses/totalapproved/2')
|
||||
// .then((res) => {
|
||||
// setTotalRejected(parseFloat(res.data.data));
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.error('Error fetching total rejected amount:', err);
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
|
||||
// // ✅ Obtener Main Supplier
|
||||
// useEffect(() => {
|
||||
// axios.get(import.meta.env.VITE_API_BASE_URL + '/expenses/mainsupplier')
|
||||
// .then((res) => {
|
||||
// if (res.data.data.length > 0) {
|
||||
// setMainSupplier(res.data.data[0].supplier); // "Costco"
|
||||
// }
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.error('Error fetching main supplier:', err);
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// // ✅ Filtrar por fechas
|
||||
// useEffect(() => {
|
||||
// if (dateRange.from && dateRange.to) {
|
||||
// const from = new Date(dateRange.from);
|
||||
// const to = new Date(dateRange.to);
|
||||
// const filtered = rejectedExpenses.filter((item) => {
|
||||
// const itemDate = new Date(item.rejectedDate);
|
||||
// return itemDate >= from && itemDate <= to;
|
||||
// });
|
||||
// setFilteredExpenses(filtered);
|
||||
// } else {
|
||||
// setFilteredExpenses(rejectedExpenses);
|
||||
// }
|
||||
// }, [dateRange, rejectedExpenses]);
|
||||
|
||||
// const columns = [
|
||||
// {
|
||||
// header: 'EXPENSE DESCRIPTION',
|
||||
// key: 'description',
|
||||
// render: (text) => <span>{text}</span>,
|
||||
// },
|
||||
// { header: 'REQUEST DATE', key: 'requestDate' },
|
||||
// { header: 'REJECTED DATE', key: 'rejectedDate' },
|
||||
// { header: 'AREA', key: 'area' },
|
||||
// { header: 'REQUESTED BY', key: 'requestedBy' },
|
||||
// {
|
||||
// header: 'AMOUNT',
|
||||
// key: 'amount',
|
||||
// render: (amount) => `$${amount.toLocaleString()}`
|
||||
// },
|
||||
// {
|
||||
// header: 'STATUS',
|
||||
// key: 'id',
|
||||
// render: () => (
|
||||
// <button className="status-button reject" style={{ pointerEvents: 'none' }}>
|
||||
// REJECTED
|
||||
// </button>
|
||||
// ),
|
||||
// },
|
||||
// ];
|
||||
|
||||
// return (
|
||||
// <div className="rejected-page">
|
||||
// <h2>REJECTED</h2>
|
||||
|
||||
// {/* Filtros de fecha */}
|
||||
// <div className="page-filters">
|
||||
// <input
|
||||
// type="date"
|
||||
// value={dateRange.from}
|
||||
// onChange={(e) => setDateRange({ ...dateRange, from: e.target.value })}
|
||||
// />
|
||||
// <input
|
||||
// type="date"
|
||||
// value={dateRange.to}
|
||||
// onChange={(e) => setDateRange({ ...dateRange, to: e.target.value })}
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// {/* Summary Cards */}
|
||||
// <div className="summary-container">
|
||||
// <div className="summary-card">
|
||||
// <strong>Total Rejected</strong>
|
||||
// <div>${totalRejected.toLocaleString()}</div>
|
||||
// </div>
|
||||
// <div className="summary-card">
|
||||
// <strong>Main Supplier</strong>
|
||||
// <div>{mainSupplier}</div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// {/* Tabla */}
|
||||
// <Table columns={columns} data={filteredExpenses} />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
// import '../../components/Filters/Filters.css';
|
||||
// import Table from '../../components/Table/HotelTable';
|
||||
// import axios from 'axios';
|
||||
|
||||
// export default function Rejected() {
|
||||
// const [rejectedExpenses, setRejectedExpenses] = useState([]);
|
||||
// const [filteredExpenses, setFilteredExpenses] = useState([]);
|
||||
// const [dateRange, setDateRange] = useState({ from: '', to: '' });
|
||||
|
||||
// //Obtener datos reales desde el backend
|
||||
// useEffect(() => {
|
||||
// axios.get(import.meta.env.VITE_API_BASE_URL + '/expenses/rejectedexpenses')
|
||||
// .then((res) => {
|
||||
// const formatted = res.data.data.map((item, index) => ({
|
||||
// id: index + 1, // temporal, ya que no viene `id_expense`
|
||||
// description: item.expense_description,
|
||||
// requestDate: new Date(item.request_date).toISOString().split('T')[0],
|
||||
// rejectedDate: new Date(item.reject_date).toISOString().split('T')[0],
|
||||
// area: item.area,
|
||||
// requestedBy: item.requested_by,
|
||||
// amount: parseFloat(item.amount),
|
||||
// supplier: 'N/A' // ❗ backend no envía proveedor
|
||||
// }));
|
||||
|
||||
// setRejectedExpenses(formatted);
|
||||
// setFilteredExpenses(formatted);
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.error('Error fetching rejected expenses:', err);
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// //Filtrar por fechas
|
||||
// useEffect(() => {
|
||||
// if (dateRange.from && dateRange.to) {
|
||||
// const from = new Date(dateRange.from);
|
||||
// const to = new Date(dateRange.to);
|
||||
// const filtered = rejectedExpenses.filter((item) => {
|
||||
// const itemDate = new Date(item.rejectedDate);
|
||||
// return itemDate >= from && itemDate <= to;
|
||||
// });
|
||||
// setFilteredExpenses(filtered);
|
||||
// } else {
|
||||
// setFilteredExpenses(rejectedExpenses);
|
||||
// }
|
||||
// }, [dateRange, rejectedExpenses]);
|
||||
|
||||
// //Columnas de la tabla
|
||||
// const columns = [
|
||||
// {
|
||||
// header: 'EXPENSE DESCRIPTION',
|
||||
// key: 'description',
|
||||
// render: (text) => <span>{text}</span>, // ❌ sin enlace por falta de ID
|
||||
// },
|
||||
// { header: 'REQUEST DATE', key: 'requestDate' },
|
||||
// { header: 'REJECTED DATE', key: 'rejectedDate' },
|
||||
// { header: 'AREA', key: 'area' },
|
||||
// { header: 'REQUESTED BY', key: 'requestedBy' },
|
||||
// {
|
||||
// header: 'AMOUNT',
|
||||
// key: 'amount',
|
||||
// render: (amount) => `$${amount.toLocaleString()}`
|
||||
// },
|
||||
// {
|
||||
// header: 'STATUS',
|
||||
// key: 'id',
|
||||
// render: () => (
|
||||
// <button className="status-button reject" style={{ pointerEvents: 'none' }}>
|
||||
// REJECTED
|
||||
// </button>
|
||||
// ),
|
||||
// },
|
||||
// ];
|
||||
|
||||
// //Cálculo de total rechazado
|
||||
// const totalAmount = filteredExpenses.reduce((sum, item) => sum + item.amount, 0);
|
||||
|
||||
// //Proveedor principal (no aplica)
|
||||
// const mainSupplier = 'N/A';
|
||||
|
||||
// return (
|
||||
// <div className="rejected-page">
|
||||
// <h2>REJECTED</h2>
|
||||
|
||||
// {/* Filtros de fecha */}
|
||||
// <div className="page-filters">
|
||||
// <input
|
||||
// type="date"
|
||||
// value={dateRange.from}
|
||||
// onChange={(e) => setDateRange({ ...dateRange, from: e.target.value })}
|
||||
// />
|
||||
// <input
|
||||
// type="date"
|
||||
// value={dateRange.to}
|
||||
// onChange={(e) => setDateRange({ ...dateRange, to: e.target.value })}
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// {/* Summary Cards */}
|
||||
// <div className="summary-container">
|
||||
// <div className="summary-card">
|
||||
// <strong>Total Rejected</strong>
|
||||
// <div>${totalAmount.toLocaleString()}</div>
|
||||
// </div>
|
||||
// <div className="summary-card">
|
||||
// <strong>Main Supplier</strong>
|
||||
// <div>{mainSupplier}</div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// {/* Tabla */}
|
||||
// <Table columns={columns} data={filteredExpenses} />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user