feat: mejorar la estructura y legibilidad del componente FinancialWidget
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user