Initial commit
This commit is contained in:
159
app/(protected)/invoice/index.tsx
Normal file
159
app/(protected)/invoice/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user