- 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)
394 lines
13 KiB
JavaScript
394 lines
13 KiB
JavaScript
import React, { useEffect, useState, useContext } from "react";
|
|
import { Link, useNavigate } from "react-router-dom";
|
|
|
|
import { langContext } from "../../context/LenguageContext";
|
|
import DateRangeFilter from "../../components/Filters/DateRangeFilter";
|
|
import SummaryCard from "../../components/SummaryCard";
|
|
import ExcelExportButton from "../../components/ExcelExportButton";
|
|
import "../../components/Filters/Filters.css";
|
|
import "./IncomeReport.css";
|
|
import Table from "../../components/Table/HotelTable";
|
|
|
|
export default function IncomeReport() {
|
|
const navigate = useNavigate();
|
|
const { lang } = useContext(langContext);
|
|
const [incomeData, setIncomeData] = useState([]);
|
|
const [filteredData, setFilteredData] = useState([]);
|
|
const [dateRange, setDateRange] = useState({ from: "", to: "" });
|
|
const [accountFilter, setAccountFilter] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [accounts, setAccounts] = useState([]);
|
|
|
|
useEffect(() => {
|
|
async function fetchAccounts() {
|
|
try {
|
|
await fetch(
|
|
`${import.meta.env.VITE_API_BASE_URL}/incomeshrx/stripedata`
|
|
);
|
|
const response = await fetch(
|
|
`${import.meta.env.VITE_API_BASE_URL}/incomeshrx/accountincome`
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const data = Array.isArray(result) ? result : result.data || [];
|
|
setAccounts(data);
|
|
} catch (err) {
|
|
console.error("Error fetching accounts:", err);
|
|
setAccounts([]);
|
|
}
|
|
}
|
|
|
|
fetchAccounts();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
async function fetchIncomeData() {
|
|
try {
|
|
setLoading(true);
|
|
setError("");
|
|
|
|
const response = await fetch(
|
|
`${import.meta.env.VITE_API_BASE_URL}/incomeshrx/incomehorux`
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
const data = Array.isArray(result) ? result : result.data || [];
|
|
|
|
const mappedData = data.map((item) => ({
|
|
id_hrx_income: item.id_hrx_income,
|
|
date: item.date_in,
|
|
created_at: item.createddate || item.date_in,
|
|
account: item.account_name,
|
|
amount: item.account_name === "STRIPE" ? parseFloat(item.amountinvoice || 0).toFixed(2) : parseFloat(item.amount || 0).toFixed(2),
|
|
invoiceAmount: item.account_name === "STRIPE" ? parseFloat(item.amount || 0).toFixed(2) : parseFloat(item.amountinvoice || 0).toFixed(2),
|
|
invoices: item.invoice,
|
|
status: item.status_in ? "Distributed" : "Pending",
|
|
categories: item.categories || [],
|
|
}));
|
|
|
|
setIncomeData(mappedData);
|
|
setFilteredData(mappedData);
|
|
} catch (err) {
|
|
console.error("Error fetching income data:", err);
|
|
setError(
|
|
lang === "es"
|
|
? "Error al cargar los datos de ingresos. Por favor, intente de nuevo."
|
|
: "Failed to load income data. Please try again."
|
|
);
|
|
setIncomeData([]);
|
|
setFilteredData([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
fetchIncomeData();
|
|
}, [lang]);
|
|
|
|
useEffect(() => {
|
|
let filtered = [...incomeData];
|
|
|
|
if (dateRange.from && dateRange.to) {
|
|
filtered = filtered.filter((item) => {
|
|
const itemDate = item.created_at?.slice(0, 10);
|
|
|
|
if (!itemDate) return false;
|
|
|
|
return itemDate >= dateRange.from && itemDate <= dateRange.to;
|
|
});
|
|
}
|
|
|
|
if (accountFilter) {
|
|
filtered = filtered.filter((item) => item.account === accountFilter);
|
|
}
|
|
|
|
if (statusFilter) {
|
|
filtered = filtered.filter((item) => item.status === statusFilter);
|
|
}
|
|
|
|
setFilteredData(filtered);
|
|
}, [dateRange, accountFilter, statusFilter, incomeData]);
|
|
|
|
const clearFilters = () => {
|
|
setDateRange({ from: "", to: "" });
|
|
setAccountFilter("");
|
|
setStatusFilter("");
|
|
};
|
|
|
|
const formatDate = (dateStr) => {
|
|
if (!dateStr) return "";
|
|
|
|
const date = new Date(dateStr);
|
|
if (isNaN(date.getTime())) return "";
|
|
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const year = date.getFullYear();
|
|
|
|
return `${day}/${month}/${year}`;
|
|
};
|
|
|
|
const handleStatusClick = (label, row) => {
|
|
if (label === "Pending") {
|
|
navigate(`/app/edit-income-form/${row.id}`, {
|
|
state: {
|
|
incomeData: {
|
|
id: row.id,
|
|
date: row.created_at,
|
|
account: row.account,
|
|
amount: row.amount,
|
|
invoiceAmount: row.invoiceAmount,
|
|
invoices: row.invoices,
|
|
status: row.status,
|
|
categories: row.categories
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const totalIncome = filteredData.reduce((sum, item) => {
|
|
return sum + parseFloat(item.amount || 0);
|
|
}, 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
|
|
const statusOptions = ["Distributed", "Pending"];
|
|
|
|
const columns = [
|
|
{ header: lang === "es" ? "ID" : "ID", key: "id_hrx_income" },
|
|
{
|
|
header: lang === "es" ? "FECHA" : "DATE",
|
|
key: "created_at",
|
|
render: (value) => formatDate(value),
|
|
},
|
|
{
|
|
header: lang === "es" ? "CUENTA" : "ACCOUNT",
|
|
key: "account",
|
|
render: (value, row) => (
|
|
<span
|
|
onClick={() => navigate(`/app/edit-income-form/${row.id}`, {
|
|
state: {
|
|
incomeData: {
|
|
id: row.id,
|
|
date: row.created_at,
|
|
account: row.account,
|
|
amount: row.amount,
|
|
invoiceAmount: row.invoiceAmount,
|
|
invoices: row.invoices,
|
|
status: row.status,
|
|
categories: row.categories
|
|
}
|
|
}
|
|
})}
|
|
style={{
|
|
color: "#5D1A2A",
|
|
textDecoration: "underline",
|
|
cursor: "pointer",
|
|
fontWeight: "500",
|
|
}}
|
|
>
|
|
{value}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
header: lang === "es" ? "MONTO" : "AMOUNT",
|
|
key: "amount",
|
|
render: (value) => {
|
|
const num = parseFloat(value);
|
|
return `$${num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
},
|
|
},
|
|
{
|
|
header: lang === "es" ? "MONTO FACTURA" : "INVOICE AMOUNT",
|
|
key: "invoiceAmount",
|
|
render: (value) => {
|
|
const num = parseFloat(value);
|
|
return `$${num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
},
|
|
},
|
|
{
|
|
header: lang === "es" ? "FACTURA" : "INVOICE",
|
|
key: "invoices",
|
|
},
|
|
{
|
|
header: lang === "es" ? "CATEGORÍAS" : "CATEGORIES",
|
|
key: "categories",
|
|
render: (value, row) => {
|
|
if (!row.categories || row.categories.length === 0) {
|
|
return "-";
|
|
}
|
|
|
|
return row.categories.map((category, index) => {
|
|
const total = parseFloat(category.total || 0);
|
|
const formattedTotal = `$${total.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
return (
|
|
<div key={index} style={{ marginBottom: index < row.categories.length - 1 ? '4px' : '0' }}>
|
|
{category.category}: {formattedTotal}
|
|
</div>
|
|
);
|
|
});
|
|
},
|
|
},
|
|
{
|
|
header: lang === "es" ? "ESTADO" : "STATUS",
|
|
key: "status",
|
|
headerStyle: { textAlign: "center" },
|
|
render: (value, row) => (
|
|
<div style={{ textAlign: "center" }}>
|
|
<button
|
|
onClick={() => handleStatusClick(value, row)}
|
|
style={{
|
|
padding: "4px 12px",
|
|
border: "none",
|
|
borderRadius: "4px",
|
|
cursor: "pointer",
|
|
fontWeight: "bold",
|
|
backgroundColor: value === "Distributed" ? "#4CAF50" : "#FFC107",
|
|
color: value === "Distributed" ? "white" : "black",
|
|
}}
|
|
>
|
|
{lang === "es"
|
|
? value === "Distributed"
|
|
? "Distribuido"
|
|
: "Pendiente"
|
|
: value}
|
|
</button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
const exportColumns = [
|
|
{ header: lang === "es" ? "ID" : "ID", key: "id_hrx_income" },
|
|
{ header: lang === "es" ? "FECHA" : "DATE", key: "created_at" },
|
|
{ header: lang === "es" ? "CUENTA" : "ACCOUNT", key: "account" },
|
|
{ header: lang === "es" ? "MONTO" : "AMOUNT", key: "amount" },
|
|
{ header: lang === "es" ? "MONTO FACTURA" : "INVOICE AMOUNT", key: "invoiceAmount" },
|
|
{ header: lang === "es" ? "FACTURA" : "INVOICE", key: "invoices" },
|
|
{ header: lang === "es" ? "CATEGORÍAS" : "CATEGORIES", key: "categories" },
|
|
{ header: lang === "es" ? "ESTADO" : "STATUS", key: "status" },
|
|
];
|
|
|
|
const dataTransform = (data) => {
|
|
return data.map((row) => ({
|
|
...row,
|
|
created_at: formatDate(row.created_at),
|
|
amount: row.amount ? `$${parseFloat(row.amount).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '',
|
|
invoiceAmount: row.invoiceAmount ? `$${parseFloat(row.invoiceAmount).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '',
|
|
categories: row.categories && row.categories.length > 0
|
|
? row.categories.map(cat => {
|
|
const total = parseFloat(cat.total || 0);
|
|
const formattedTotal = `$${total.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
return `${cat.category}: ${formattedTotal}`;
|
|
}).join('; ')
|
|
: '',
|
|
status: lang === "es"
|
|
? (row.status === "Distributed" ? "Distribuido" : "Pendiente")
|
|
: row.status,
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<div className="income-report-page">
|
|
<div className="page-header">
|
|
<h2 className="page-title">
|
|
{lang === "es" ? "Informe de Ingresos" : "Income Report"}
|
|
</h2>
|
|
</div>
|
|
|
|
{error && <div className="error-message">{error}</div>}
|
|
|
|
<div className="summary-actions-section">
|
|
<div className="summary-card-wrapper">
|
|
<SummaryCard
|
|
title={lang === "es" ? "Total de Ingresos" : "Total Income"}
|
|
amount={totalIncome}
|
|
isLoading={loading}
|
|
/>
|
|
</div>
|
|
<Link to="/app/new-income-form" className="new-income-btn">
|
|
{lang === "es" ? "+ Nuevo Ingreso" : "+ New Income"}
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="filters-section">
|
|
<DateRangeFilter
|
|
dateRange={dateRange}
|
|
onDateChange={setDateRange}
|
|
lang={lang}
|
|
/>
|
|
|
|
<select
|
|
value={accountFilter}
|
|
onChange={(e) => setAccountFilter(e.target.value)}
|
|
className="filter-select"
|
|
>
|
|
<option value="">
|
|
{lang === "es" ? "Todas las cuentas" : "All Accounts"}
|
|
</option>
|
|
{accounts.map((account) => (
|
|
<option key={account.id_acc_income} value={account.name_acc_income}>
|
|
{account.name_acc_income}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<select
|
|
value={statusFilter}
|
|
onChange={(e) => setStatusFilter(e.target.value)}
|
|
className="filter-select"
|
|
>
|
|
<option value="">
|
|
{lang === "es" ? "Todos los estados" : "All Status"}
|
|
</option>
|
|
{statusOptions.map((status, index) => (
|
|
<option key={index} value={status}>
|
|
{lang === "es"
|
|
? status === "Distributed"
|
|
? "Distribuido"
|
|
: "Pendiente"
|
|
: status}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<button onClick={clearFilters} className="clear-filters-btn">
|
|
{lang === "es" ? "Limpiar filtros" : "Clear filters"}
|
|
</button>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '15px' }}>
|
|
<ExcelExportButton
|
|
data={filteredData}
|
|
columns={exportColumns}
|
|
filenamePrefix={lang === "es" ? "informe-ingresos" : "income-report"}
|
|
sheetName={lang === "es" ? "Informe de Ingresos" : "Income Report"}
|
|
dataTransform={dataTransform}
|
|
/>
|
|
</div>
|
|
|
|
<div className="table-section">
|
|
{loading ? (
|
|
<div className="table-loading">
|
|
<div className="loading-spinner-large"></div>
|
|
<p>{lang === "es" ? "Cargando..." : "Loading..."}</p>
|
|
</div>
|
|
) : (
|
|
<Table columns={columns} data={filteredData} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|