Initial commit
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user