- 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)
465 lines
15 KiB
JavaScript
465 lines
15 KiB
JavaScript
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>
|
|
// );
|
|
// }
|
|
|
|
|