Files
turnosxpress/txclient/src/app/components/FinancialWidget/FinancialWidget.tsx
T

336 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useEffect, useState } from "react";
import { LineChart } from "@mui/x-charts/LineChart";
import {
DashboardFinancialsResult,
getDashboardFinancials,
} from "../../landing/dashboard/Dashboard.Service";
import { loadCollaboratorsByCompanyId } from "../../admin/(organization-profile)/org/profile/[id]/collaborators/Collaborators.Service";
import { CompanyEmployeesView } from "@models/Collaborators.model";
import UnpaidIncomeCard from "../UnpaidIncomeCard/UnpaidIncomeCard";
import styles from "./FinancialWidget.module.css";
import dayjs from "dayjs";
import OpenInNewOutlinedIcon from "@mui/icons-material/OpenInNewOutlined";
import { useNavigation } from "@core/app/hooks/goto";
export interface FinancialWidgetProps {
sessionUser: string;
selectedOrgId: string;
organizationName?: string;
}
export default function FinancialWidget({
sessionUser,
selectedOrgId,
organizationName,
}: FinancialWidgetProps) {
const { goTo } = useNavigation();
const [selectedTimeframe, setSelectedTimeframe] = useState<"week" | "month" | "year">("month");
const [selectedEmployeeId, setSelectedEmployeeId] = useState<string>("");
const [collaborators, setCollaborators] = useState<CompanyEmployeesView[]>([]);
const [financials, setFinancials] = useState<DashboardFinancialsResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(true);
useEffect(() => {
if (selectedOrgId) {
loadCollaboratorsByCompanyId({ companyId: selectedOrgId })
.then((res) => {
setCollaborators(res);
setSelectedEmployeeId(""); // reset employee filter when org changes
})
.catch(console.error);
}
}, [selectedOrgId, sessionUser]);
useEffect(() => {
if (selectedOrgId) {
setLoading(true);
setError(null);
setFinancials(null);
getDashboardFinancials({
sessionUser,
companyId: selectedOrgId,
timeframe: selectedTimeframe,
employeeId: selectedEmployeeId || undefined,
})
.then((res) => {
setFinancials(res);
})
.catch((err) => {
const errMsg = err.format ? err.format() : err.message;
console.warn("Acceso denegado a métricas financieras: ", errMsg);
setError("No tienes permisos de administrador para esta organización.");
})
.finally(() => setLoading(false));
}
}, [selectedOrgId, selectedTimeframe, selectedEmployeeId, sessionUser]);
const formatCurrency = (val: number) => {
return new Intl.NumberFormat("es-AR", { style: "currency", currency: "ARS" }).format(val);
};
const renderComparison = () => {
if (!financials) return null;
const current = financials.currentPeriodTotal;
const previous = financials.previousPeriodTotal;
if (previous === 0) {
if (current > 0)
return <span className={styles.positive}>+100% vs periodo anterior</span>;
return <span className={styles.neutral}>Sin datos previos</span>;
}
const diff = current - previous;
const percent = (diff / previous) * 100;
if (percent > 0)
return (
<span className={styles.positive}>+{percent.toFixed(1)}% vs periodo anterior</span>
);
if (percent < 0)
return (
<span className={styles.negative}>{percent.toFixed(1)}% vs periodo anterior</span>
);
return <span className={styles.neutral}>0% vs periodo anterior</span>;
};
if (!selectedOrgId) return null;
if (error) {
return (
<div className={styles.container}>
<button
className={styles.headerButton}
type="button"
onClick={() => setIsOpen((value) => !value)}
>
<h3 className={styles.title}>
Rendimiento Financiero {organizationName ? `- ${organizationName}` : ""}
</h3>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<button
type="button"
onClick={() => goTo(`/landing/org/${selectedOrgId}`)}
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 12px",
borderRadius: "999px",
color: "var(--green-darkestX1)",
border: "1px solid var(--green-darkestX1)",
background: "var(--white)",
textDecoration: "none",
fontSize: "14px",
fontWeight: 600,
cursor: "pointer",
transition: "all 0.2s ease",
}}
title="Ver perfil público de la organización"
>
<OpenInNewOutlinedIcon fontSize="small" />
Ver perfil
</button>
<span className={styles.toggleIcon}>{isOpen ? "" : "+"}</span>
</div>
</button>
{isOpen && (
<div style={{ padding: "40px 20px", textAlign: "center", color: "#6b7280" }}>
<p style={{ fontWeight: "600", marginBottom: "8px", color: "#374151" }}>
Acceso Restringido
</p>
<p>{error}</p>
</div>
)}
</div>
);
}
const chartData = financials?.currentPeriodData || [];
const xAxisData = chartData.map((d) => {
// format label nicer depending on timeframe
if (selectedTimeframe === "year") return dayjs(d.date).format("MMM YYYY");
return dayjs(d.date).format("DD/MM");
});
const seriesData = chartData.map((d) => d.amount);
const hasChartData = chartData.some((d) => d.amount > 0);
return (
<div className={styles.container}>
<button
className={styles.headerButton}
type="button"
onClick={() => setIsOpen((value) => !value)}
>
<h3 className={styles.title}>
Rendimiento Financiero {organizationName ? `- ${organizationName}` : ""}
</h3>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<button
type="button"
onClick={() => goTo(`/landing/org/${selectedOrgId}`)}
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 12px",
borderRadius: "999px",
color: "var(--green-darkestX1)",
border: "1px solid var(--green-darkestX1)",
background: "var(--white)",
textDecoration: "none",
fontSize: "14px",
fontWeight: 600,
cursor: "pointer",
transition: "all 0.2s ease",
}}
title="Ver perfil público de la organización"
>
<OpenInNewOutlinedIcon fontSize="small" />
Ver perfil
</button>
<span className={styles.toggleIcon}>{isOpen ? "" : "+"}</span>
</div>
</button>
{isOpen && (
<>
<div className={styles.filters}>
<select
className={styles.select}
value={selectedTimeframe}
onChange={(e) =>
setSelectedTimeframe(e.target.value as "week" | "month" | "year")
}
>
<option value="week">Última Semana</option>
<option value="month">Último Mes</option>
<option value="year">Último Año</option>
</select>
<select
className={styles.select}
value={selectedEmployeeId}
onChange={(e) => setSelectedEmployeeId(e.target.value)}
>
<option value="">Todos los Profesionales</option>
{collaborators.map((c) => (
<option key={c.id} value={c.id}>
{c.fullName}
</option>
))}
</select>
</div>
<div className={styles.metricsRow}>
<div className={styles.metricBox}>
<span className={styles.metricLabel}>Ingresos del Periodo</span>
<span className={styles.metricValue}>
{loading
? "..."
: formatCurrency(financials?.currentPeriodTotal || 0)}
</span>
<div className={styles.metricComparison}>
{!loading && renderComparison()}
</div>
</div>
{financials && <UnpaidIncomeCard value={financials.unpaidToday} />}
</div>
{loading && (
<div className={styles.chartContainer}>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
color: "#9ca3af",
}}
>
Cargando datos...
</div>
</div>
)}
{!loading && hasChartData && (
<div className={styles.chartContainer}>
<LineChart
height={350}
xAxis={[
{
scaleType: "point",
data: xAxisData,
},
]}
series={[
{
data: seriesData,
color: "rgb(36, 169, 53)",
area: true,
showMark: true,
curve: "monotoneX",
valueFormatter: (val: number | null) =>
val ? formatCurrency(val) : "0",
},
]}
grid={{ horizontal: true }}
margin={{ top: 20, bottom: 40, left: 70, right: 20 }}
sx={{
".MuiLineElement-root": {
strokeWidth: 3,
},
".MuiAreaElement-root": {
fill: "url(#gradient)",
},
".MuiChartsGrid-line": {
strokeDasharray: "5 5",
stroke: "#e5e7eb",
},
".MuiChartsAxis-line": {
stroke: "transparent",
},
".MuiChartsAxis-tick": {
stroke: "transparent",
},
".MuiChartsAxis-tickLabel": {
fill: "#9ca3af",
fontFamily: "inherit",
fontSize: "12px",
fontWeight: 500,
},
".MuiMarkElement-root": {
stroke: "rgb(36, 169, 53)",
strokeWidth: 2,
fill: "#ffffff",
scale: "1.2",
},
}}
>
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="rgb(36, 169, 53)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="rgb(36, 169, 53)"
stopOpacity={0.0}
/>
</linearGradient>
</defs>
</LineChart>
</div>
)}
</>
)}
</div>
);
}