336 lines
15 KiB
TypeScript
336 lines
15 KiB
TypeScript
"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>
|
||
);
|
||
}
|