Initial commit

This commit is contained in:
2026-07-31 16:53:16 +02:00
commit e49d6f0e5b
67 changed files with 20163 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, TouchableOpacity, Linking } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { ChevronLeft, Download, CheckCircle2, XCircle, Calendar, CreditCard } from 'lucide-react-native';
import api from '@/utils/api';
import { useAlert } from '@/components/AlertComponent';
import LoadingScreen from '@/components/LoadingScreen';
import { InvoiceInfo, InvoiceAccounting } from '@/types/types';
export default function InvoiceDetailScreen() {
const router = useRouter();
const { id } = useLocalSearchParams();
const alert = useAlert();
const [invoiceInfo, setInvoiceInfo] = useState<InvoiceInfo | null>(null);
const [partite, setPartite] = useState<InvoiceAccounting[]>([]);
const [isLoading, setIsLoading] = useState(true);
const fetchInvoiceInfo = async () => {
try {
const response = await api.get(`/invoice-supplier/get-info?id=${id}`);
if (response.data?.success) {
setInvoiceInfo(response.data.result.info);
// Map backend keys to our frontend types
const mappedPartite: InvoiceAccounting[] = response.data.result.partite.map((p: any) => ({
expiry: p.scadenza,
tpa_description: p.tpa_descrizione,
amount: p.importo_dovuto,
isPaid: p.pagata
}));
setPartite(mappedPartite);
}
} catch (error) {
console.error('Error fetching invoice info:', error);
alert.showAlert('error', 'Errore', 'Impossibile recuperare il dettaglio della fattura.');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (id) {
fetchInvoiceInfo();
}
}, [id]);
const handleDownload = () => {
if (invoiceInfo?.link) {
Linking.openURL(invoiceInfo.link);
} else {
alert.showAlert('error', 'Nessun Documento', 'Non è presente un documento allegato per questa fattura.');
}
};
if (isLoading || !invoiceInfo) {
return <LoadingScreen />;
}
return (
<View className="flex-1 bg-gray-50">
<StatusBar style="dark" />
{/* Header */}
<View className="bg-white px-6 pb-6 shadow-sm border-b border-gray-100">
<SafeAreaView edges={['top']} className="pt-5 flex-row items-center justify-between">
<TouchableOpacity onPress={() => router.back()} className="p-2 bg-gray-50 rounded-full active:bg-gray-100">
<ChevronLeft size={24} color="#082963" />
</TouchableOpacity>
<Text className="text-xl font-bold text-gray-800 text-center flex-1">Dettaglio Fattura</Text>
<TouchableOpacity onPress={handleDownload} className="p-2.5 bg-blue-50 rounded-full active:bg-blue-100 shadow-sm">
<Download size={22} color="#1071C2" />
</TouchableOpacity>
</SafeAreaView>
</View>
<ScrollView contentContainerStyle={{ padding: 20, paddingBottom: 100, gap: 16 }} showsVerticalScrollIndicator={false}>
{/* General Info Card */}
<View className="bg-white p-6 rounded-3xl shadow-sm border border-gray-100">
<Text className="text-sm font-bold text-gray-400 uppercase tracking-wider mb-4">Informazioni</Text>
<View className="mb-4">
<Text className="text-xs font-bold text-gray-400 uppercase mb-1">Numero Documento</Text>
<Text className="text-base font-bold text-gray-800">{invoiceInfo.documentNumber}</Text>
</View>
<View className="mb-4">
<Text className="text-xs font-bold text-gray-400 uppercase mb-1">Fornitore</Text>
<Text className="text-xl font-bold text-[#082963]">{invoiceInfo.supplier}</Text>
</View>
<View className="mb-4">
<Text className="text-xs font-bold text-gray-400 uppercase mb-1">Cantiere</Text>
<Text className="text-base font-bold text-gray-800">{invoiceInfo.placeName}</Text>
</View>
<View className="flex-row justify-between bg-gray-50 p-4 rounded-2xl mb-2">
<View>
<Text className="text-xs font-bold text-gray-400 uppercase mb-1">Data Documento</Text>
<Text className="font-bold text-gray-800">{invoiceInfo.date}</Text>
</View>
<View className="items-end">
<Text className="text-xs font-bold text-gray-400 uppercase mb-1">Importo</Text>
<Text className="font-bold text-[#1071C2]">{invoiceInfo.totalAmount}</Text>
</View>
</View>
</View>
{/* Expiries */}
{partite && partite.length > 0 && (
<View className="bg-white p-6 rounded-3xl shadow-sm border border-gray-100">
<Text className="text-sm font-bold text-gray-400 uppercase tracking-wider mb-4">Scadenze</Text>
<View className="gap-4">
{partite.map((item, idx) => (
<View key={idx} className={`border border-gray-100 rounded-2xl p-4 ${item.isPaid ? 'bg-green-50/30' : 'bg-red-50/30'}`}>
<View className="flex-row justify-between items-center mb-3">
<View className="flex-row items-center gap-2">
<Calendar size={16} color="#082963" />
<Text className="font-bold text-[#082963]">{item.expiry}</Text>
</View>
<View className={`px-2 py-1 rounded-md flex-row items-center gap-1 ${item.isPaid ? 'bg-green-100' : 'bg-red-100'}`}>
{item.isPaid ? <CheckCircle2 size={12} color="#109D59" /> : <XCircle size={12} color="#DC4437" />}
<Text className={`text-[10px] font-bold uppercase ${item.isPaid ? 'text-green-700' : 'text-red-700'}`}>
{item.isPaid ? 'Pagata' : 'Da Pagare'}
</Text>
</View>
</View>
<View className="flex-row justify-between items-center pt-3 border-t border-gray-100 gap-2">
<View className="flex-row items-center gap-2 flex-1 mr-2">
<View className="mt-0.5">
<CreditCard size={14} color="#8F9BB3" />
</View>
<Text className="text-xs uppercase text-gray-500 font-medium shrink flex-wrap">{item.tpa_description}</Text>
</View>
<Text className={`font-bold text-base whitespace-nowrap ${item.isPaid ? 'text-[#109D59]' : 'text-[#DC4437]'}`}>
{item.amount}
</Text>
</View>
</View>
))}
</View>
</View>
)}
</ScrollView>
</View>
);
}

View File

@@ -0,0 +1,10 @@
import {Stack} from 'expo-router';
export default function InvoiceLayout() {
return (
<Stack screenOptions={{headerShown: false}}>
<Stack.Screen name="index" />
<Stack.Screen name="[id]" />
</Stack>
);
}

View File

@@ -0,0 +1,159 @@
import React, { useState, useEffect, useCallback } from 'react';
import { View, Text, ScrollView, TouchableOpacity, RefreshControl } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { Filter, ReceiptText } from 'lucide-react-native';
import api from '@/utils/api';
import { useAlert } from '@/components/AlertComponent';
import LoadingScreen from '@/components/LoadingScreen';
import FilterModal from '@/components/FilterModal';
import InvoiceCard from '@/components/InvoiceCard';
import { InvoiceItem, Place } from '@/types/types';
export default function InvoiceScreen() {
const router = useRouter();
const alert = useAlert();
const [invoices, setInvoices] = useState<InvoiceItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
// Filters state
const [showFilterModal, setShowFilterModal] = useState(false);
const [places, setPlaces] = useState<Place[]>([]);
const [filterRange, setFilterRange] = useState<{ startDate: string | null; endDate: string | null }>({ startDate: null, endDate: null });
const [filterPlace, setFilterPlace] = useState<any>(null);
const [filterSupplier, setFilterSupplier] = useState<any>(null);
const activeFiltersCount = (filterRange.startDate ? 1 : 0) + (filterPlace ? 1 : 0) + (filterSupplier ? 1 : 0);
const fetchPlaces = async () => {
try {
const response = await api.get('/place/get-places');
if (response.data?.success) {
setPlaces(response.data.places || []);
}
} catch (error) {
console.error('Errore nel recupero dei cantieri:', error);
}
};
const fetchInvoices = async (currentRange = filterRange, currentPlace = filterPlace, currentSupplier = filterSupplier) => {
try {
if (!refreshing) setIsLoading(true);
const rangeParam = currentRange.startDate ? currentRange : null;
const placeCode = currentPlace ? places.find(p => p.id === currentPlace)?.code : null;
const params = { date: rangeParam, place: placeCode, supplier: currentSupplier };
const response = await api.post('/invoice-supplier/list', { params });
if (response.data?.success) {
// The backend already returns data matching InvoiceItem mostly
setInvoices(response.data.result || []);
}
} catch (error) {
console.error('Error fetching invoices:', error);
alert.showAlert('error', 'Errore', 'Impossibile recuperare la lista delle fatture.');
} finally {
setIsLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchPlaces();
fetchInvoices();
}, []);
const onRefresh = () => {
setRefreshing(true);
fetchInvoices();
};
// Stable reference: keeps InvoiceCard memoization effective across page re-renders
const handleInvoicePress = useCallback((id: number) => {
router.push(`/invoice/${id}`);
}, [router]);
const handleApplyFilters = (range: any, place: any, supplier: any) => {
setFilterRange(range);
setFilterPlace(place);
setFilterSupplier(supplier);
setShowFilterModal(false);
fetchInvoices(range, place, supplier);
};
const handleResetFilters = () => {
const emptyRange = { startDate: null, endDate: null };
setFilterRange(emptyRange);
setFilterPlace(null);
setFilterSupplier(null);
setShowFilterModal(false);
fetchInvoices(emptyRange, null, null);
};
if (isLoading && !refreshing) {
return <LoadingScreen />;
}
return (
<View className="flex-1 bg-primary-dark">
<StatusBar style="light" />
{/* Header */}
<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">Elenco delle fatture</Text>
<Text className="text-yellow-400 text-3xl font-bold leading-tight">Fatture</Text>
</View>
<View className="bg-white/10 p-4 rounded-full">
<ReceiptText size={32} color="white" pointerEvents="none" />
</View>
</View>
</SafeAreaView>
<View className="flex-1 bg-gray-50 rounded-t-[2.5rem] overflow-hidden">
{/* List */}
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 20, paddingBottom: 100, gap: 16 }}
showsVerticalScrollIndicator={true}
scrollIndicatorInsets={{ right: 1 }}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#1071C2']} />}
>
{invoices.length === 0 ? (
<View className="bg-white p-8 rounded-3xl border border-gray-100 items-center justify-center border-dashed mt-4">
<Text className="text-gray-400 font-medium text-center">Nessuna fattura trovata</Text>
</View>
) : (
invoices.map((invoice) => (
<InvoiceCard key={invoice.id} item={invoice} onPress={handleInvoicePress} />
)))}
</ScrollView>
{/* FAB Filter */}
<TouchableOpacity
onPress={() => setShowFilterModal(true)}
className="absolute bottom-8 right-6 w-16 h-16 bg-[#1071C2] rounded-full shadow-lg items-center justify-center active:scale-90"
>
<Filter size={28} color="white" pointerEvents="none" />
{activeFiltersCount > 0 && (
<View className="absolute top-0 right-0 bg-red-500 w-6 h-6 rounded-full items-center justify-center border-2 border-white">
<Text className="text-white text-xs font-bold">{activeFiltersCount}</Text>
</View>
)}
</TouchableOpacity>
<FilterModal
visible={showFilterModal}
places={places}
currentRange={filterRange}
currentPlace={filterPlace}
currentSupplier={filterSupplier}
showSupplier={true}
onClose={() => setShowFilterModal(false)}
onApply={handleApplyFilters}
onReset={handleResetFilters}
/>
</View>
</View>
);
}