Files
fonsi_app/app/(protected)/dashboard/index.tsx
T
2026-09-10 14:08:41 +02:00

236 lines
11 KiB
TypeScript

import React, { useState, useEffect, useMemo, useRef } from 'react';
import { View, Text, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar';
import { ChevronDown, ChevronUp, LayoutDashboard } from 'lucide-react-native';
import api from '@/utils/api';
import EchartWrapper from '@/components/EchartWrapper';
import {
CHART_ENDPOINTS,
CHART_DATA_KEY,
formatEuro,
buildPieOption,
buildAperteChiuseOption
} from '@/utils/dashboardCharts';
const TOOLTIP_BASE = {
renderMode: 'richText',
confine: true,
textStyle: { fontSize: 10 }
};
const screenWidth = Dimensions.get("window").width;
export default function DashboardScreen() {
const [chartData, setChartData] = useState<any>({
costiRicavi: null,
fatturatoCliente: null,
aperteChiuseCliente: null,
aperteChiuseFornitore: null,
partiteCliente: null,
});
const [loadingCharts, setLoadingCharts] = useState<Record<string, boolean>>({});
const [expandedCards, setExpandedCards] = useState<Record<string, boolean>>({
costiRicavi: false,
aperteChiuseCliente: false,
aperteChiuseFornitore: false,
fatturatoCliente: false,
apertoCliente: false,
chiusoCliente: false,
});
const chartRichiesti = useRef(new Set<string>());
const isLoading = (key: string) => !!loadingCharts[CHART_DATA_KEY[key] || key];
const fetchChart = (key: keyof typeof CHART_ENDPOINTS) => {
const dataKey = CHART_DATA_KEY[key] || key;
const endpoint = CHART_ENDPOINTS[key];
if (!endpoint || chartRichiesti.current.has(dataKey)) { return Promise.resolve(); }
chartRichiesti.current.add(dataKey);
setLoadingCharts(prev => ({ ...prev, [dataKey]: true }));
return api.get(endpoint).then(res => {
if (res.data && res.data.success) {
setChartData((prev: any) => ({ ...prev, [dataKey]: res.data.result }));
}
}).catch(err => {
chartRichiesti.current.delete(dataKey);
console.log("Errore caricamento dashboard:", err);
}).finally(() => {
setLoadingCharts(prev => ({ ...prev, [dataKey]: false }));
});
};
// Sequential Prefetching
useEffect(() => {
let cancelled = false;
const prefetch = async () => {
const richieste: string[] = [];
const visti = new Set<string>();
Object.keys(CHART_ENDPOINTS).forEach(key => {
const dataKey = CHART_DATA_KEY[key] || key;
if (visti.has(dataKey)) { return; }
visti.add(dataKey);
richieste.push(key);
});
for (const key of richieste) {
if (cancelled) { return; }
await fetchChart(key as keyof typeof CHART_ENDPOINTS);
}
};
prefetch();
return () => { cancelled = true; };
}, []);
const toggleCard = (key: string) => {
const isCurrentlyExpanded = expandedCards[key];
setExpandedCards(prev => ({ ...prev, [key]: !prev[key] }));
const dataKey = CHART_DATA_KEY[key] || key;
if (!isCurrentlyExpanded && !chartData[dataKey] && !isLoading(key)) {
fetchChart(key as keyof typeof CHART_ENDPOINTS);
}
};
// --- Chart Options ---
const costiRicaviOption = useMemo(() => !chartData.costiRicavi ? null : {
tooltip: {
...TOOLTIP_BASE,
trigger: 'axis',
valueFormatter: formatEuro
},
legend: {
bottom: 0,
itemGap: 6,
itemWidth: 14,
itemHeight: 10,
textStyle: { fontSize: 12 },
data: [
`Costi Materiali(${chartData.costiRicavi.current_year})`,
`Costi Manodopera(${chartData.costiRicavi.current_year})`,
`Ricavi(${chartData.costiRicavi.current_year})`,
`Utile(${chartData.costiRicavi.current_year})`,
`Costi Materiali(${chartData.costiRicavi.prev_year})`,
`Costi Manodopera(${chartData.costiRicavi.prev_year})`,
`Ricavi(${chartData.costiRicavi.prev_year})`,
`Utile(${chartData.costiRicavi.prev_year})`
],
selected: {
[`Utile(${chartData.costiRicavi.prev_year})`]: false,
[`Ricavi(${chartData.costiRicavi.prev_year})`]: false,
[`Costi Materiali(${chartData.costiRicavi.prev_year})`]: false,
[`Costi Manodopera(${chartData.costiRicavi.prev_year})`]: false
},
},
grid: {
left: '3%',
right: '4%',
top: '8%',
bottom: 95,
containLabel: true
},
xAxis: {
type: 'category',
data: chartData.costiRicavi.mesi
},
yAxis: {
type: 'value'
},
series: [
{ name: `Costi Materiali(${chartData.costiRicavi.current_year})`, type: 'bar', data: chartData.costiRicavi.costi },
{ name: `Costi Manodopera(${chartData.costiRicavi.current_year})`, type: 'bar', data: chartData.costiRicavi.costi_mdo },
{ name: `Ricavi(${chartData.costiRicavi.current_year})`, type: 'bar', data: chartData.costiRicavi.ricavi },
{ name: `Utile(${chartData.costiRicavi.current_year})`, type: 'bar', lineStyle: { width: 2.5, type: 'dashed' }, data: chartData.costiRicavi.utile },
{ name: `Costi Materiali(${chartData.costiRicavi.prev_year})`, type: 'bar', itemStyle: { color: '#b3b3b3' }, data: chartData.costiRicavi.costi_prev },
{ name: `Costi Manodopera(${chartData.costiRicavi.prev_year})`, type: 'bar', itemStyle: { color: '#b3b3b3' }, data: chartData.costiRicavi.costi_mdo_prev },
{ name: `Ricavi(${chartData.costiRicavi.prev_year})`, type: 'bar', itemStyle: { color: '#b3b3b3' }, data: chartData.costiRicavi.ricavi_prev },
{ name: `Utile(${chartData.costiRicavi.prev_year})`, type: 'bar', lineStyle: { width: 1.5, type: 'dashed', color: '#b3b3b3' }, data: chartData.costiRicavi.utile_prev }
]
}, [chartData.costiRicavi]);
const aperteChiuseClienteOption = useMemo(() => chartData.aperteChiuseCliente ? buildAperteChiuseOption(chartData.aperteChiuseCliente) : null, [chartData.aperteChiuseCliente]);
const aperteChiuseFornitoreOption = useMemo(() => chartData.aperteChiuseFornitore ? buildAperteChiuseOption(chartData.aperteChiuseFornitore) : null, [chartData.aperteChiuseFornitore]);
const fatturatoClienteOption = useMemo(() => chartData.fatturatoCliente ? buildPieOption(chartData.fatturatoCliente) : null, [chartData.fatturatoCliente]);
const apertoClienteOption = useMemo(() => chartData.partiteCliente ? buildPieOption(chartData.partiteCliente.aperto) : null, [chartData.partiteCliente]);
const chiusoClienteOption = useMemo(() => chartData.partiteCliente ? buildPieOption(chartData.partiteCliente.chiuso) : null, [chartData.partiteCliente]);
const numItems = chartData.fatturatoCliente?.length || 0;
const dynamicPieHeight = Math.max(260, 200 + (Math.ceil(numItems / 2) * 26));
const pieHeightFor = (arr: any) => Math.max(260, 200 + (Math.ceil((arr?.length || 0) / 2) * 26));
const dynamicApertoHeight = pieHeightFor(chartData.partiteCliente?.aperto);
const dynamicChiusoHeight = pieHeightFor(chartData.partiteCliente?.chiuso);
// Render Card Helper
const renderCard = (key: string, title: string, option: any, height: number) => {
const isExpanded = expandedCards[key];
const loading = isLoading(key);
return (
<View className="bg-white rounded-2xl shadow-sm border border-gray-100 w-full mb-4 overflow-hidden" key={key}>
<TouchableOpacity
className="flex-row items-center justify-between p-4 active:bg-gray-50"
onPress={() => toggleCard(key)}
>
<Text className="text-gray-800 font-bold text-lg">{title}</Text>
{isExpanded ? <ChevronUp size={24} color="#082963" /> : <ChevronDown size={24} color="#082963" />}
</TouchableOpacity>
{isExpanded && (
<View className="p-4 pt-0 border-t border-gray-50">
{loading ? (
<ActivityIndicator size="small" color="#1071C2" className="my-6" />
) : option ? (
<View style={{ width: screenWidth - 64, height: height, overflow: 'hidden' }} className="self-center">
<EchartWrapper option={option} width={screenWidth - 64} height={height} />
</View>
) : (
<Text className="text-gray-400 text-center my-4">Nessun dato disponibile</Text>
)}
</View>
)}
</View>
);
};
return (
<View className="flex-1 bg-primary-dark">
<StatusBar style="light" />
<SafeAreaView edges={['top']} className="pt-5">
<View className="pb-6 px-6 z-10 flex-row justify-between items-center">
<View>
<Text className="text-white text-md font-semibold uppercase tracking-wider mb-1">Grafici e Statistiche</Text>
<Text className="text-yellow-400 text-3xl font-bold leading-tight">Dashboard</Text>
</View>
<View className="bg-white/10 p-4 rounded-full">
<LayoutDashboard size={32} color="white" pointerEvents="none" />
</View>
</View>
</SafeAreaView>
<View className="flex-1 bg-gray-50 rounded-t-[2.5rem] overflow-hidden">
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 20 }}
showsVerticalScrollIndicator={false}
>
{renderCard('costiRicavi', 'Costi / Ricavi', costiRicaviOption, 390)}
{renderCard('aperteChiuseCliente', 'Aperto / Chiuso Clienti', aperteChiuseClienteOption, 330)}
{renderCard('aperteChiuseFornitore', 'Aperto / Chiuso Fornitori', aperteChiuseFornitoreOption, 330)}
{renderCard('fatturatoCliente', 'Fatturato per Cliente', fatturatoClienteOption, dynamicPieHeight)}
{renderCard('apertoCliente', 'Aperto per Cliente', apertoClienteOption, dynamicApertoHeight)}
{renderCard('chiusoCliente', 'Chiuso per Cliente', chiusoClienteOption, dynamicChiusoHeight)}
</ScrollView>
</View>
</View>
);
}