338 lines
16 KiB
TypeScript
338 lines
16 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 { useRouter } from 'expo-router';
|
|
import { StatusBar } from 'expo-status-bar';
|
|
import { ChevronDown, ChevronUp, LayoutDashboard } from 'lucide-react-native';
|
|
import api from '@/utils/api';
|
|
import { useAlert } from '@/components/AlertComponent';
|
|
import EchartWrapper from '@/components/EchartWrapper';
|
|
import {
|
|
CHART_ENDPOINTS,
|
|
CHART_DATA_KEY,
|
|
cumulate,
|
|
formatEuro,
|
|
buildPieOption,
|
|
buildSoaBarOption,
|
|
commesseLine,
|
|
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 router = useRouter();
|
|
const alert = useAlert();
|
|
|
|
const [chartData, setChartData] = useState<any>({
|
|
costiRicavi: null,
|
|
fatturatoCliente: null,
|
|
aperteChiuseCliente: null,
|
|
aperteChiuseFornitore: null,
|
|
fatturatoSoa: null,
|
|
partiteCliente: null,
|
|
marginalitaMediaSoa: null,
|
|
aggregatiSoa: null
|
|
});
|
|
|
|
const [loadingCharts, setLoadingCharts] = useState<Record<string, boolean>>({});
|
|
const [expandedCards, setExpandedCards] = useState<Record<string, boolean>>({
|
|
costiRicavi: false,
|
|
costiRicaviCum: false,
|
|
aperteChiuseCliente: false,
|
|
aperteChiuseFornitore: false,
|
|
fatturatoSoa: false,
|
|
fatturatoCliente: false,
|
|
apertoCliente: false,
|
|
chiusoCliente: false,
|
|
marginalitaMediaSoa: false,
|
|
marginalitaCategoriaSoa: false,
|
|
pesoFatturatoSoa: 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 costiRicaviCumOption = useMemo(() => {
|
|
if (!chartData.costiRicavi) return null;
|
|
const d = chartData.costiRicavi;
|
|
return {
|
|
tooltip: {
|
|
...TOOLTIP_BASE,
|
|
trigger: 'axis',
|
|
valueFormatter: formatEuro
|
|
},
|
|
legend: {
|
|
bottom: 0,
|
|
itemGap: 6,
|
|
itemWidth: 14,
|
|
itemHeight: 10,
|
|
textStyle: { fontSize: 12 },
|
|
data: [
|
|
`Costi Materiali(${d.current_year})`,
|
|
`Costi Manodopera(${d.current_year})`,
|
|
`Ricavi(${d.current_year})`,
|
|
`Utile(${d.current_year})`,
|
|
`Costi Materiali(${d.prev_year})`,
|
|
`Costi Manodopera(${d.prev_year})`,
|
|
`Ricavi(${d.prev_year})`,
|
|
`Utile(${d.prev_year})`
|
|
],
|
|
selected: {
|
|
[`Utile(${d.prev_year})`]: false,
|
|
[`Ricavi(${d.prev_year})`]: false,
|
|
[`Costi Materiali(${d.prev_year})`]: false,
|
|
[`Costi Manodopera(${d.prev_year})`]: false
|
|
},
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
top: '8%',
|
|
bottom: 95,
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: d.mesi
|
|
},
|
|
yAxis: {
|
|
type: 'value'
|
|
},
|
|
series: [
|
|
{ name: `Costi Materiali(${d.current_year})`, type: 'bar', data: cumulate(d.costi) },
|
|
{ name: `Costi Manodopera(${d.current_year})`, type: 'bar', data: cumulate(d.costi_mdo) },
|
|
{ name: `Ricavi(${d.current_year})`, type: 'bar', data: cumulate(d.ricavi) },
|
|
{ name: `Utile(${d.current_year})`, type: 'bar', lineStyle: { width: 2.5, type: 'dashed' }, data: cumulate(d.utile) },
|
|
{ name: `Costi Materiali(${d.prev_year})`, type: 'bar', itemStyle: { color: '#b3b3b3' }, data: cumulate(d.costi_prev) },
|
|
{ name: `Costi Manodopera(${d.prev_year})`, type: 'bar', itemStyle: { color: '#b3b3b3' }, data: cumulate(d.costi_mdo_prev) },
|
|
{ name: `Ricavi(${d.prev_year})`, type: 'bar', itemStyle: { color: '#b3b3b3' }, data: cumulate(d.ricavi_prev) },
|
|
{ name: `Utile(${d.prev_year})`, type: 'bar', lineStyle: { width: 1.5, type: 'dashed', color: '#b3b3b3' }, data: cumulate(d.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 fatturatoSoaOption = useMemo(() => chartData.fatturatoSoa ? buildPieOption(chartData.fatturatoSoa) : null, [chartData.fatturatoSoa]);
|
|
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 numItemsSoa = chartData.fatturatoSoa?.length || 0;
|
|
const dynamicSoaHeight = Math.max(260, 200 + (numItemsSoa * 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);
|
|
|
|
const marginalitaMediaSoaOption = useMemo(() => chartData.marginalitaMediaSoa
|
|
? buildSoaBarOption(chartData.marginalitaMediaSoa, { color: '#5470c6', valueLabel: 'Marginalità media', extraLines: commesseLine })
|
|
: null, [chartData.marginalitaMediaSoa]);
|
|
|
|
const marginalitaCategoriaOption = useMemo(() => chartData.aggregatiSoa
|
|
? buildSoaBarOption(chartData.aggregatiSoa.marginalitaCategoria, { color: '#3ba272', valueLabel: 'Marginalità categoria', extraLines: commesseLine })
|
|
: null, [chartData.aggregatiSoa]);
|
|
|
|
const pesoFatturatoSoaOption = useMemo(() => chartData.aggregatiSoa
|
|
? buildSoaBarOption(chartData.aggregatiSoa.pesoFatturato, {
|
|
color: '#fac858', valueLabel: 'Peso', extraLines: (r: any) => [
|
|
`Fatturato categoria: ${formatEuro(r.fatturato_tot)}`,
|
|
`Commesse: ${r.n_commesse}`
|
|
]
|
|
}) : null, [chartData.aggregatiSoa]);
|
|
|
|
const soaBarHeight = (arr: any) => Math.max(280, (arr?.length || 0) * 36 + 80);
|
|
const dynamicMarginalitaHeight = soaBarHeight(chartData.marginalitaMediaSoa);
|
|
const dynamicMarginalitaCatHeight = soaBarHeight(chartData.aggregatiSoa?.marginalitaCategoria);
|
|
const dynamicPesoSoaHeight = soaBarHeight(chartData.aggregatiSoa?.pesoFatturato);
|
|
|
|
// 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('costiRicaviCum', 'Costi / Ricavi (Cumulati)', costiRicaviCumOption, 390)}
|
|
{renderCard('aperteChiuseCliente', 'Aperto / Chiuso Clienti', aperteChiuseClienteOption, 330)}
|
|
{renderCard('aperteChiuseFornitore', 'Aperto / Chiuso Fornitori', aperteChiuseFornitoreOption, 330)}
|
|
{renderCard('fatturatoSoa', 'Fatturato per SOA', fatturatoSoaOption, dynamicSoaHeight)}
|
|
{renderCard('fatturatoCliente', 'Fatturato per Cliente', fatturatoClienteOption, dynamicPieHeight)}
|
|
{renderCard('apertoCliente', 'Aperto per Cliente', apertoClienteOption, dynamicApertoHeight)}
|
|
{renderCard('chiusoCliente', 'Chiuso per Cliente', chiusoClienteOption, dynamicChiusoHeight)}
|
|
{renderCard('marginalitaMediaSoa', 'Marginalità media per SOA', marginalitaMediaSoaOption, dynamicMarginalitaHeight)}
|
|
{renderCard('marginalitaCategoriaSoa', 'Marginalità % di categoria SOA', marginalitaCategoriaOption, dynamicMarginalitaCatHeight)}
|
|
{renderCard('pesoFatturatoSoa', 'Peso fatturato SOA', pesoFatturatoSoaOption, dynamicPesoSoaHeight)}
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|