197 lines
8.8 KiB
TypeScript
197 lines
8.8 KiB
TypeScript
import { useAlert } from '@/components/AlertComponent';
|
|
import LoadingScreen from '@/components/LoadingScreen';
|
|
import api from '@/utils/api';
|
|
import { ImageIcon, Plus, Filter, Newspaper } from 'lucide-react-native';
|
|
import React, { useEffect, useState } from 'react';
|
|
import { RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import FilterModal from '@/components/FilterModal';
|
|
import { Place } from '@/types/types';
|
|
import { useRouter, useFocusEffect } from 'expo-router';
|
|
import { StatusBar } from 'expo-status-bar';
|
|
import { useCallback } from 'react';
|
|
|
|
export default function JournalScreen() {
|
|
const router = useRouter();
|
|
const alert = useAlert();
|
|
const [updates, setUpdates] = useState<any[]>([]);
|
|
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 activeFiltersCount = (filterRange.startDate ? 1 : 0) + (filterPlace ? 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 fetchUpdates = async (currentRange = filterRange, currentPlace = filterPlace) => {
|
|
try {
|
|
if (!refreshing) setIsLoading(true);
|
|
|
|
const rangeParam = currentRange.startDate ? currentRange : null;
|
|
const params = { range: rangeParam, place: currentPlace };
|
|
const response = await api.post('/journal/list', { params });
|
|
|
|
if (response.data?.success) {
|
|
setUpdates(response.data.result || []);
|
|
}
|
|
} catch (error) {
|
|
console.error('Errore nel recupero del giornale:', error);
|
|
alert.showAlert('error', 'Errore', 'Impossibile recuperare il giornale di cantiere.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchPlaces();
|
|
}, []);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
fetchUpdates();
|
|
}, [filterRange, filterPlace])
|
|
);
|
|
|
|
const onRefresh = () => {
|
|
setRefreshing(true);
|
|
fetchUpdates();
|
|
};
|
|
|
|
// The fetch is triggered by useFocusEffect, which reacts to filter changes:
|
|
// calling fetchUpdates here too would fire a second, identical request.
|
|
const handleApplyFilters = (range: any, place: any) => {
|
|
setFilterRange(range);
|
|
setFilterPlace(place);
|
|
setShowFilterModal(false);
|
|
};
|
|
|
|
const handleResetFilters = () => {
|
|
setFilterRange({ startDate: null, endDate: null });
|
|
setFilterPlace(null);
|
|
setShowFilterModal(false);
|
|
};
|
|
|
|
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 invii al giornale</Text>
|
|
<Text className="text-yellow-400 text-3xl font-bold leading-tight">Giornale di Cantiere</Text>
|
|
</View>
|
|
<View className="bg-white/10 p-4 rounded-full">
|
|
<Newspaper size={32} color="white" pointerEvents="none" />
|
|
</View>
|
|
</View>
|
|
</SafeAreaView>
|
|
|
|
<View className="flex-1 bg-gray-50 rounded-t-[2.5rem] overflow-hidden">
|
|
{/* List */}
|
|
<ScrollView
|
|
contentContainerStyle={{ padding: 20, paddingBottom: 180 }}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#1071C2']} />}
|
|
>
|
|
{updates.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">Nessun invio registrato al giornale di cantiere</Text>
|
|
</View>
|
|
) : (
|
|
<View className="gap-4">
|
|
{updates.map((item, index) => (
|
|
<View key={index} className="bg-white p-5 rounded-3xl shadow-sm border border-gray-100">
|
|
<View className="mb-3">
|
|
<Text className="font-bold text-[#082963] text-lg uppercase tracking-wide leading-tight">
|
|
{item.place_name}
|
|
</Text>
|
|
<Text className="text-gray-500 text-sm font-medium mt-1">
|
|
{item.place_address}
|
|
</Text>
|
|
<Text className="text-gray-400 text-base leading-relaxed mt-1">
|
|
{item.description}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="flex-row items-center justify-between border-t border-gray-50 pt-4">
|
|
<View className="flex-row items-center gap-4">
|
|
<View className="bg-blue-50 p-3 rounded-2xl">
|
|
<ImageIcon size={24} color="#082963" />
|
|
</View>
|
|
<View>
|
|
<Text className="font-bold text-primary text-lg">
|
|
{item.n_files} File Inviati
|
|
</Text>
|
|
<Text className="text-gray-400 font-bold text-sm mt-0.5">{item.date}</Text>
|
|
</View>
|
|
</View>
|
|
{item.n_files > 0 && (
|
|
<TouchableOpacity
|
|
className="bg-gray-100 px-4 py-2 rounded-xl active:bg-gray-200"
|
|
onPress={() => router.push(`/journal/${item.id}`)}
|
|
>
|
|
<Text className="text-gray-600 font-bold text-sm">Vedi</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{/* FAB Add Journal */}
|
|
<TouchableOpacity
|
|
onPress={() => router.push('/journal/add')}
|
|
className="absolute bottom-[6.5rem] right-6 w-16 h-16 bg-white border border-[#1071C2] rounded-full shadow-lg items-center justify-center active:scale-90"
|
|
>
|
|
<Plus size={32} color="#1071C2" pointerEvents="none" />
|
|
</TouchableOpacity>
|
|
|
|
{/* 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}
|
|
onClose={() => setShowFilterModal(false)}
|
|
onApply={handleApplyFilters}
|
|
onReset={handleResetFilters}
|
|
/>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|