feat: mejorar la estructura y legibilidad del componente FinancialWidget

This commit is contained in:
2026-09-15 09:41:07 -03:00
parent 421a8b5582
commit fccaa44e9c
@@ -1,13 +1,17 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { LineChart } from "@mui/x-charts/LineChart"; import { LineChart } from "@mui/x-charts/LineChart";
import { MyOranizationsView } from "@core/Models/Company.model"; import {
import { DashboardFinancialsResult, getDashboardFinancials } from "../../landing/dashboard/Dashboard.Service"; DashboardFinancialsResult,
getDashboardFinancials,
} from "../../landing/dashboard/Dashboard.Service";
import { loadCollaboratorsByCompanyId } from "../../admin/(organization-profile)/org/profile/[id]/collaborators/Collaborators.Service"; import { loadCollaboratorsByCompanyId } from "../../admin/(organization-profile)/org/profile/[id]/collaborators/Collaborators.Service";
import { CompanyEmployeesView } from "@models/Collaborators.model"; import { CompanyEmployeesView } from "@models/Collaborators.model";
import UnpaidIncomeCard from "../UnpaidIncomeCard/UnpaidIncomeCard"; import UnpaidIncomeCard from "../UnpaidIncomeCard/UnpaidIncomeCard";
import styles from "./FinancialWidget.module.css"; import styles from "./FinancialWidget.module.css";
import dayjs from "dayjs"; import dayjs from "dayjs";
import OpenInNewOutlinedIcon from "@mui/icons-material/OpenInNewOutlined";
import { useNavigation } from "@core/app/hooks/goto";
export interface FinancialWidgetProps { export interface FinancialWidgetProps {
sessionUser: string; sessionUser: string;
@@ -15,7 +19,12 @@ export interface FinancialWidgetProps {
organizationName?: string; organizationName?: string;
} }
export default function FinancialWidget({ sessionUser, selectedOrgId, organizationName }: FinancialWidgetProps) { export default function FinancialWidget({
sessionUser,
selectedOrgId,
organizationName,
}: FinancialWidgetProps) {
const { goTo } = useNavigation();
const [selectedTimeframe, setSelectedTimeframe] = useState<"week" | "month" | "year">("month"); const [selectedTimeframe, setSelectedTimeframe] = useState<"week" | "month" | "year">("month");
const [selectedEmployeeId, setSelectedEmployeeId] = useState<string>(""); const [selectedEmployeeId, setSelectedEmployeeId] = useState<string>("");
const [collaborators, setCollaborators] = useState<CompanyEmployeesView[]>([]); const [collaborators, setCollaborators] = useState<CompanyEmployeesView[]>([]);
@@ -27,10 +36,12 @@ export default function FinancialWidget({ sessionUser, selectedOrgId, organizati
useEffect(() => { useEffect(() => {
if (selectedOrgId) { if (selectedOrgId) {
loadCollaboratorsByCompanyId({ companyId: selectedOrgId }).then((res) => { loadCollaboratorsByCompanyId({ companyId: selectedOrgId })
setCollaborators(res); .then((res) => {
setSelectedEmployeeId(""); // reset employee filter when org changes setCollaborators(res);
}).catch(console.error); setSelectedEmployeeId(""); // reset employee filter when org changes
})
.catch(console.error);
} }
}, [selectedOrgId, sessionUser]); }, [selectedOrgId, sessionUser]);
@@ -43,14 +54,17 @@ export default function FinancialWidget({ sessionUser, selectedOrgId, organizati
sessionUser, sessionUser,
companyId: selectedOrgId, companyId: selectedOrgId,
timeframe: selectedTimeframe, timeframe: selectedTimeframe,
employeeId: selectedEmployeeId || undefined employeeId: selectedEmployeeId || undefined,
}).then((res) => { })
setFinancials(res); .then((res) => {
}).catch((err) => { setFinancials(res);
const errMsg = err.format ? err.format() : err.message; })
console.warn("Acceso denegado a métricas financieras: ", errMsg); .catch((err) => {
setError("No tienes permisos de administrador para esta organización."); const errMsg = err.format ? err.format() : err.message;
}).finally(() => setLoading(false)); 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]); }, [selectedOrgId, selectedTimeframe, selectedEmployeeId, sessionUser]);
@@ -64,15 +78,22 @@ export default function FinancialWidget({ sessionUser, selectedOrgId, organizati
const previous = financials.previousPeriodTotal; const previous = financials.previousPeriodTotal;
if (previous === 0) { if (previous === 0) {
if (current > 0) return <span className={styles.positive}>+100% vs periodo anterior</span>; if (current > 0)
return <span className={styles.positive}>+100% vs periodo anterior</span>;
return <span className={styles.neutral}>Sin datos previos</span>; return <span className={styles.neutral}>Sin datos previos</span>;
} }
const diff = current - previous; const diff = current - previous;
const percent = (diff / previous) * 100; const percent = (diff / previous) * 100;
if (percent > 0) return <span className={styles.positive}>+{percent.toFixed(1)}% vs periodo anterior</span>; if (percent > 0)
if (percent < 0) return <span className={styles.negative}>{percent.toFixed(1)}% vs periodo anterior</span>; 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>; return <span className={styles.neutral}>0% vs periodo anterior</span>;
}; };
@@ -81,15 +102,46 @@ export default function FinancialWidget({ sessionUser, selectedOrgId, organizati
if (error) { if (error) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<button className={styles.headerButton} type="button" onClick={() => setIsOpen((value) => !value)}> <button
className={styles.headerButton}
type="button"
onClick={() => setIsOpen((value) => !value)}
>
<h3 className={styles.title}> <h3 className={styles.title}>
Rendimiento Financiero {organizationName ? `- ${organizationName}` : ''} Rendimiento Financiero {organizationName ? `- ${organizationName}` : ""}
</h3> </h3>
<span className={styles.toggleIcon}>{isOpen ? "" : "+"}</span> <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> </button>
{isOpen && ( {isOpen && (
<div style={{ padding: '40px 20px', textAlign: 'center', color: '#6b7280' }}> <div style={{ padding: "40px 20px", textAlign: "center", color: "#6b7280" }}>
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#374151' }}>Acceso Restringido</p> <p style={{ fontWeight: "600", marginBottom: "8px", color: "#374151" }}>
Acceso Restringido
</p>
<p>{error}</p> <p>{error}</p>
</div> </div>
)} )}
@@ -98,67 +150,108 @@ export default function FinancialWidget({ sessionUser, selectedOrgId, organizati
} }
const chartData = financials?.currentPeriodData || []; const chartData = financials?.currentPeriodData || [];
const xAxisData = chartData.map(d => { const xAxisData = chartData.map((d) => {
// format label nicer depending on timeframe // format label nicer depending on timeframe
if (selectedTimeframe === "year") return dayjs(d.date).format("MMM YYYY"); if (selectedTimeframe === "year") return dayjs(d.date).format("MMM YYYY");
return dayjs(d.date).format("DD/MM"); return dayjs(d.date).format("DD/MM");
}); });
const seriesData = chartData.map(d => d.amount); const seriesData = chartData.map((d) => d.amount);
const hasChartData = chartData.some(d => d.amount > 0); const hasChartData = chartData.some((d) => d.amount > 0);
return ( return (
<div className={styles.container}> <div className={styles.container}>
<button className={styles.headerButton} type="button" onClick={() => setIsOpen((value) => !value)}> <button
className={styles.headerButton}
type="button"
onClick={() => setIsOpen((value) => !value)}
>
<h3 className={styles.title}> <h3 className={styles.title}>
Rendimiento Financiero {organizationName ? `- ${organizationName}` : ''} Rendimiento Financiero {organizationName ? `- ${organizationName}` : ""}
</h3> </h3>
<span className={styles.toggleIcon}>{isOpen ? "" : "+"}</span> <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> </button>
{isOpen && ( {isOpen && (
<> <>
<div className={styles.filters}> <div className={styles.filters}>
<select <select
className={styles.select} className={styles.select}
value={selectedTimeframe} value={selectedTimeframe}
onChange={e => setSelectedTimeframe(e.target.value as "week" | "month" | "year")} 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> <option value="week">Última Semana</option>
</select> <option value="month">Último Mes</option>
<option value="year">Último Año</option>
</select>
<select <select
className={styles.select} className={styles.select}
value={selectedEmployeeId} value={selectedEmployeeId}
onChange={e => setSelectedEmployeeId(e.target.value)} onChange={(e) => setSelectedEmployeeId(e.target.value)}
> >
<option value="">Todos los Profesionales</option> <option value="">Todos los Profesionales</option>
{collaborators.map(c => ( {collaborators.map((c) => (
<option key={c.id} value={c.id}>{c.fullName}</option> <option key={c.id} value={c.id}>
))} {c.fullName}
</select> </option>
))}
</select>
</div> </div>
<div className={styles.metricsRow}> <div className={styles.metricsRow}>
<div className={styles.metricBox}> <div className={styles.metricBox}>
<span className={styles.metricLabel}>Ingresos del Periodo</span> <span className={styles.metricLabel}>Ingresos del Periodo</span>
<span className={styles.metricValue}> <span className={styles.metricValue}>
{loading ? "..." : formatCurrency(financials?.currentPeriodTotal || 0)} {loading
</span> ? "..."
<div className={styles.metricComparison}> : formatCurrency(financials?.currentPeriodTotal || 0)}
{!loading && renderComparison()} </span>
</div> <div className={styles.metricComparison}>
</div> {!loading && renderComparison()}
</div>
</div>
{financials && ( {financials && <UnpaidIncomeCard value={financials.unpaidToday} />}
<UnpaidIncomeCard value={financials.unpaidToday} />
)}
</div> </div>
{loading && ( {loading && (
<div className={styles.chartContainer}> <div className={styles.chartContainer}>
<div style={{display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#9ca3af'}}> <div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
color: "#9ca3af",
}}
>
Cargando datos... Cargando datos...
</div> </div>
</div> </div>
@@ -168,57 +261,68 @@ export default function FinancialWidget({ sessionUser, selectedOrgId, organizati
<div className={styles.chartContainer}> <div className={styles.chartContainer}>
<LineChart <LineChart
height={350} height={350}
xAxis={[{ xAxis={[
scaleType: 'point', {
data: xAxisData, scaleType: "point",
}]} data: xAxisData,
},
]}
series={[ series={[
{ {
data: seriesData, data: seriesData,
color: 'rgb(36, 169, 53)', color: "rgb(36, 169, 53)",
area: true, area: true,
showMark: true, showMark: true,
curve: 'monotoneX', curve: "monotoneX",
valueFormatter: (val: number | null) => val ? formatCurrency(val) : "0", valueFormatter: (val: number | null) =>
val ? formatCurrency(val) : "0",
}, },
]} ]}
grid={{ horizontal: true }} grid={{ horizontal: true }}
margin={{ top: 20, bottom: 40, left: 70, right: 20 }} margin={{ top: 20, bottom: 40, left: 70, right: 20 }}
sx={{ sx={{
'.MuiLineElement-root': { ".MuiLineElement-root": {
strokeWidth: 3, strokeWidth: 3,
}, },
'.MuiAreaElement-root': { ".MuiAreaElement-root": {
fill: 'url(#gradient)', fill: "url(#gradient)",
}, },
'.MuiChartsGrid-line': { ".MuiChartsGrid-line": {
strokeDasharray: '5 5', strokeDasharray: "5 5",
stroke: '#e5e7eb', stroke: "#e5e7eb",
}, },
'.MuiChartsAxis-line': { ".MuiChartsAxis-line": {
stroke: 'transparent', stroke: "transparent",
}, },
'.MuiChartsAxis-tick': { ".MuiChartsAxis-tick": {
stroke: 'transparent', stroke: "transparent",
}, },
'.MuiChartsAxis-tickLabel': { ".MuiChartsAxis-tickLabel": {
fill: '#9ca3af', fill: "#9ca3af",
fontFamily: 'inherit', fontFamily: "inherit",
fontSize: '12px', fontSize: "12px",
fontWeight: 500, fontWeight: 500,
}, },
'.MuiMarkElement-root': { ".MuiMarkElement-root": {
stroke: 'rgb(36, 169, 53)', stroke: "rgb(36, 169, 53)",
strokeWidth: 2, strokeWidth: 2,
fill: '#ffffff', fill: "#ffffff",
scale: '1.2', scale: "1.2",
} },
}} }}
> >
<defs> <defs>
<linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="rgb(36, 169, 53)" stopOpacity={0.3}/> <stop
<stop offset="95%" stopColor="rgb(36, 169, 53)" stopOpacity={0.0}/> offset="5%"
stopColor="rgb(36, 169, 53)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="rgb(36, 169, 53)"
stopOpacity={0.0}
/>
</linearGradient> </linearGradient>
</defs> </defs>
</LineChart> </LineChart>