first commit
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { LineChart } from "@mui/x-charts/LineChart";
|
||||
import { MyOranizationsView } from "@core/Models/Company.model";
|
||||
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";
|
||||
|
||||
export interface FinancialWidgetProps {
|
||||
sessionUser: string;
|
||||
selectedOrgId: string;
|
||||
organizationName?: string;
|
||||
}
|
||||
|
||||
export default function FinancialWidget({ sessionUser, selectedOrgId, organizationName }: FinancialWidgetProps) {
|
||||
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);
|
||||
|
||||
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}>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>
|
||||
Rendimiento Financiero {organizationName ? `- ${organizationName}` : ''}
|
||||
</h3>
|
||||
</div>
|
||||
<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);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>
|
||||
Rendimiento Financiero {organizationName ? `- ${organizationName}` : ''}
|
||||
</h3>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<div className={styles.chartContainer}>
|
||||
{!loading && chartData.length > 0 ? (
|
||||
<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 style={{display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#9ca3af'}}>
|
||||
{loading ? 'Cargando datos...' : 'No hay datos para este periodo'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user