Initial commit

This commit is contained in:
2026-08-31 16:50:53 +02:00
commit a68c8864b0
80 changed files with 22222 additions and 0 deletions
+258
View File
@@ -0,0 +1,258 @@
import { useAlert } from '@/components/AlertComponent';
import LoadingScreen from '@/components/LoadingScreen';
import api from '@/utils/api';
import { Image } from 'expo-image';
import ImageView from "react-native-image-viewing";
import { useLocalSearchParams, useRouter, useFocusEffect } from 'expo-router';
import { ChevronLeft, ImageIcon, Users, HardHat, MapPin, Calendar as CalendarIcon, Briefcase, TextAlignStart, CheckCircle2, Wrench, Pencil } from 'lucide-react-native';
import React, { useCallback, useEffect, useState } from 'react';
import { Dimensions, RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar';
export default function ActivityDetailScreen() {
const router = useRouter();
const alert = useAlert();
const params = useLocalSearchParams();
const insets = useSafeAreaInsets();
const [activityData, setActivityData] = useState<any>(null);
const [placeName, setPlaceName] = useState<string>('');
const [photos, setPhotos] = useState<{uri: string}[]>([]);
// Labor states
const [operatorLabor, setOperatorLabor] = useState<any[]>([]);
const [subcontractorLabor, setSubcontractorLabor] = useState<any[]>([]);
const [otherOperatorLabor, setOtherOperatorLabor] = useState<any[]>([]);
const [equipmentLabor, setEquipmentLabor] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
// Image Viewer states
const [isVisible, setIsVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const fetchDetails = useCallback(async (isRefreshing = false) => {
try {
if (!isRefreshing) setIsLoading(true);
const paramsData = JSON.stringify({ id: params.id });
const [activityRes, subactivitiesRes] = await Promise.all([
api.post('/activity/get-activity-data', { params: paramsData }),
api.get('/subactivity/get-subactivities')
]);
if (activityRes.data?.success) {
const data = activityRes.data;
setActivityData(data.activity);
setPhotos(data.attachments || []);
setOperatorLabor(data.operator_labor || []);
setSubcontractorLabor(data.subcontractor_labor || []);
setOtherOperatorLabor(data.other_operator_labor || []);
setEquipmentLabor(data.materials || []);
if (subactivitiesRes.data?.success) {
const subactivities = subactivitiesRes.data.subactivities;
const match = subactivities.find((s: any) => s.id === data.activity.id_subactivity);
if (match) {
setPlaceName(match.label);
} else {
setPlaceName('Cantiere Non Specificato');
}
}
} else {
alert.showAlert('error', 'Errore', 'Impossibile caricare i dettagli dell\'attività.');
}
} catch (error) {
console.error('Errore nel recupero del dettaglio attività:', error);
alert.showAlert('error', 'Errore', 'Si è verificato un errore di rete.');
} finally {
setIsLoading(false);
setRefreshing(false);
}
}, [params.id]);
useFocusEffect(
useCallback(() => {
if (params.id) {
fetchDetails(true);
}
}, [params.id, fetchDetails])
);
const onRefresh = () => {
setRefreshing(true);
fetchDetails(true);
};
if (isLoading && !refreshing) {
return <LoadingScreen />;
}
// Calculate dimensions for the grid layout of photos
const windowWidth = Dimensions.get('window').width;
const padding = 40;
const gap = 8;
const itemSize = (windowWidth - padding - (gap * 2)) / 3;
const imageSource = photos.map(photo => ({ uri: photo.uri }));
const renderLaborSection = (title: string, icon: React.ReactNode, laborData: any[]) => {
if (!laborData || laborData.length === 0) return null;
return (
<View className="bg-white rounded-3xl p-5 shadow-sm border border-gray-100 mb-4">
<View className="flex-row items-center border-b border-gray-50 pb-3 mb-3">
<View className="bg-blue-50 p-2 rounded-xl mr-3">
{icon}
</View>
<Text className="text-[#082963] font-bold text-lg">{title}</Text>
</View>
<View className="gap-3">
{laborData.map((labor, idx) => (
<View key={idx} className="flex-row justify-between items-center bg-gray-50 p-3 rounded-2xl">
<Text className="text-gray-800 font-medium flex-1 mr-2" numberOfLines={3}>
{labor.name}
</Text>
<View className="flex-row items-center gap-2">
<View className="bg-white px-3 py-1.5 rounded-xl border border-gray-200">
<Text className="text-[#1071C2] font-bold">
{labor.hours}h {labor.minutes}m
</Text>
</View>
{labor.sync && (
<CheckCircle2 size={16} color="#0F9D58" />
)}
</View>
</View>
))}
</View>
</View>
);
};
return (
<View className="flex-1 bg-gray-50">
<StatusBar style="dark" />
{/* Header */}
<View className="bg-white px-4 pb-6 shadow-sm border-b border-gray-100">
<SafeAreaView edges={['top']} className='pt-5'>
<View className='flex-row items-center justify-between px-2'>
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100 w-12 items-center justify-center">
<ChevronLeft size={28} color="#4b5563" pointerEvents="none" />
</TouchableOpacity>
<Text
className="text-xl font-bold text-gray-800 leading-tight uppercase flex-1 text-center"
numberOfLines={1}
ellipsizeMode="tail"
>
Dettaglio Attività
</Text>
<TouchableOpacity onPress={() => router.push(`/activity/add?id=${params.id}`)} className="p-2 -mr-2 active:bg-gray-100 rounded-full w-12 items-center justify-center">
<Pencil size={20} color="#082963" />
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
<ScrollView
contentContainerStyle={{ padding: 20 }}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#1071C2']} />}
>
{/* General Info */}
<View className="bg-white rounded-3xl p-5 shadow-sm border border-gray-100 mb-4">
<View className="flex-row items-center mb-4">
<View className="bg-blue-50 p-3 rounded-2xl mr-4">
<MapPin size={24} color="#082963" />
</View>
<View className="flex-1">
<Text className="text-gray-400 text-sm font-bold uppercase mb-1">Cantiere</Text>
<Text className="text-[#082963] font-bold text-md leading-tight">
{placeName}
</Text>
</View>
</View>
<View className="flex-row items-center mb-4 pt-4 border-t border-gray-50">
<View className="bg-blue-50 p-3 rounded-2xl mr-4">
<CalendarIcon size={24} color="#082963" />
</View>
<View className="flex-1">
<Text className="text-gray-400 text-sm font-bold uppercase mb-1">Data</Text>
<Text className="text-[#082963] font-bold text-md leading-tight">
{activityData?.date ? new Date(activityData.date).toLocaleDateString('it-IT') : '-'}
</Text>
</View>
</View>
<View className="pt-4 border-t border-gray-50">
<View className="flex-row items-center gap-2 mb-2">
<TextAlignStart size={20} color="#9ca3af" className="mr-2" />
<Text className="text-gray-400 text-xs font-bold uppercase">Descrizione</Text>
</View>
<Text className="text-gray-700 text-md font-medium leading-relaxed">
{activityData?.description || 'Nessuna descrizione.'}
</Text>
</View>
</View>
{/* Labor and Materials Sections */}
{renderLaborSection('Operai', <HardHat size={24} color="#082963" />, operatorLabor)}
{renderLaborSection('Subappaltatori', <Briefcase size={24} color="#082963" />, subcontractorLabor)}
{renderLaborSection('Operai Distaccati', <Users size={24} color="#082963" />, otherOperatorLabor)}
{renderLaborSection('Attrezzature', <Wrench size={24} color="#082963" />, equipmentLabor)}
{/* Photos */}
<View className="mt-4 mb-2 flex-row items-center justify-between">
<Text className="text-lg font-bold text-[#082963] ml-2">Allegati</Text>
</View>
{photos.length === 0 ? (
<View className="bg-white p-8 rounded-3xl border border-gray-100 items-center justify-center border-dashed mb-4">
<ImageIcon size={48} color="#d1d5db" />
<Text className="text-gray-400 font-medium text-center mt-4">Nessun allegato presente per questa attività.</Text>
</View>
) : (
<View className="flex-row flex-wrap" style={{ gap: gap }}>
{photos.map((item, index) => (
<TouchableOpacity
key={index}
activeOpacity={0.8}
onPress={() => {
setCurrentIndex(index);
setIsVisible(true);
}}
style={{ width: itemSize }}
className="mb-2"
>
<View className="bg-gray-100 rounded-2xl overflow-hidden shadow-sm border border-gray-200 aspect-square items-center justify-center">
<Image
source={{ uri: item.uri }}
style={{ width: '100%', height: '100%' }}
contentFit="cover"
transition={200}
/>
</View>
</TouchableOpacity>
))}
</View>
)}
</ScrollView>
<ImageView
images={imageSource}
imageIndex={currentIndex}
visible={isVisible}
onRequestClose={() => setIsVisible(false)}
swipeToCloseEnabled={true}
doubleTapToZoomEnabled={true}
presentationStyle="overFullScreen"
/>
</View>
);
}
+11
View File
@@ -0,0 +1,11 @@
import {Stack} from 'expo-router';
export default function JournalLayout() {
return (
<Stack screenOptions={{headerShown: false}}>
<Stack.Screen name="index" />
<Stack.Screen name="add" />
<Stack.Screen name="[id]" />
</Stack>
);
}
+430
View File
@@ -0,0 +1,430 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, TextInput, KeyboardAvoidingView, ScrollView, Platform, Dimensions, ActivityIndicator, Modal } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { ChevronLeft, ImageIcon, Calendar as CalendarIcon } from 'lucide-react-native';
import { AppDatePicker } from '@/components/AppDatePicker';
import { DateType } from 'react-native-ui-datepicker';
import { formatDate, formatPickerDate } from '@/utils/dateTime';
import { Image } from 'expo-image';
import * as ImagePicker from 'expo-image-picker';
import api from '@/utils/api';
import { uploadDocument } from '@/utils/documentUtils';
import { useAlert } from '@/components/AlertComponent';
import GenericDropdown from '@/components/GenericDropdown';
import ActivityLaborCard from '@/components/ActivityLaborCard';
import RemovablePhotoTile from '@/components/RemovablePhotoTile';
import CameraAddTile from '@/components/CameraAddTile';
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
export default function ActivityFormScreen() {
const router = useRouter();
const alert = useAlert();
const { id } = useLocalSearchParams();
const isEditing = !!id;
// Data lists
const [subactivities, setSubactivities] = useState<any[]>([]);
// Form states
const [date, setDate] = useState<DateType>(new Date());
const [showDatePicker, setShowDatePicker] = useState(false);
const [selectedSubactivityUuid, setSelectedSubactivityUuid] = useState<string | null>(null);
const [selectedSubactivityId, setSelectedSubactivityId] = useState<number | null>(null);
const [description, setDescription] = useState('');
// Labor states
const [operatorLabor, setOperatorLabor] = useState<any[]>([]);
const [subcontractorLabor, setSubcontractorLabor] = useState<any[]>([]);
const [otherOperatorLabor, setOtherOperatorLabor] = useState<any[]>([]);
const [equipmentLabor, setEquipmentLabor] = useState<any[]>([]);
// Photos
const [photos, setPhotos] = useState<any[]>([]); // New photos
const [existingPhotos, setExistingPhotos] = useState<any[]>([]); // Existing (readonly)
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
// Initial load
useEffect(() => {
loadInitialData();
}, []);
const loadInitialData = async () => {
setIsLoading(true);
try {
// Load subactivities
const subRes = await api.get('/subactivity/get-subactivities');
if (subRes.data?.success) {
setSubactivities(subRes.data.subactivities);
}
// If editing, load activity data
if (isEditing) {
const actRes = await api.post('/activity/get-activity-data', {
params: JSON.stringify({ id: id })
});
if (actRes.data?.success) {
const data = actRes.data;
const activity = data.activity;
setDate(new Date(activity.date));
setDescription(activity.description || '');
if (subRes.data?.success) {
const match = subRes.data.subactivities.find((s: any) => s.id === activity.id_subactivity);
if (match) {
setSelectedSubactivityUuid(match.uuid);
setSelectedSubactivityId(match.id);
}
}
setOperatorLabor(data.operator_labor || []);
setSubcontractorLabor(data.subcontractor_labor || []);
setOtherOperatorLabor(data.other_operator_labor || []);
setEquipmentLabor(data.materials || []);
setExistingPhotos(data.attachments || []);
} else {
alert.showAlert('error', 'Errore', 'Impossibile caricare i dati dell\'attività.');
}
}
} catch (error) {
console.error('Error loading data:', error);
alert.showAlert('error', 'Errore', 'Si è verificato un errore di connessione.');
} finally {
setIsLoading(false);
}
};
const handleDateChange = (params: any) => {
setDate(params.date);
setShowDatePicker(false);
};
const pickFromGallery = async () => {
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permissionResult.granted === false) {
alert.showAlert('error', 'Permessi Negati', 'È necessario consentire l\'accesso alla galleria.');
return;
}
const limit = 50 - photos.length;
if (limit <= 0) return;
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsMultipleSelection: true,
selectionLimit: limit,
quality: 0.8,
});
if (!result.canceled && result.assets) {
setPhotos(prev => [...prev, ...result.assets]);
}
} catch (error) {
console.error('Errore gallery:', error);
}
};
const takePhoto = async () => {
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
if (permissionResult.granted === false) {
alert.showAlert('error', 'Permessi Negati', 'È necessario consentire l\'accesso alla fotocamera.');
return;
}
if (photos.length >= 50) return;
try {
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ['images'],
quality: 0.8,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
setPhotos(prev => [...prev, result.assets[0]]);
}
} catch (error) {
console.error('Errore fotocamera:', error);
}
};
const removePhoto = (index: number) => {
setPhotos(prev => prev.filter((_, i) => i !== index));
};
const handleSave = async () => {
if (!selectedSubactivityUuid) {
alert.showAlert('error', 'Campi obbligatori', 'Selezionare un Cantiere.');
return;
}
setIsSubmitting(true);
try {
// Se in edit mode dobbiamo passare subactivity_id, se in add mode subactivity_uuid.
let subactivity_id = selectedSubactivityId;
if (!subactivity_id) {
const match = subactivities.find(s => s.uuid === selectedSubactivityUuid);
if (match) subactivity_id = match.id;
}
const payload: any = {
description: description,
date: date ? formatPickerDate(date) : new Date().toISOString().split('T')[0], // YYYY-MM-DD
n_files: photos.length,
operator_labor: operatorLabor,
subcontractor_labor: subcontractorLabor,
other_operators_labor: otherOperatorLabor,
equipment_labor: equipmentLabor,
};
if (isEditing) {
payload.id = id;
payload.subactivity_id = subactivity_id;
} else {
payload.subactivity_uuid = selectedSubactivityUuid;
}
const params = {
post: JSON.stringify(payload)
};
const endpoint = isEditing ? '/activity/edit' : '/activity/add';
const res = await api.post(endpoint, params);
if (res.data?.success) {
const savedId = res.data.id;
// Upload new photos sequentially
if (photos.length > 0) {
for (const file of photos) {
const fileName = file.fileName || file.uri.split('/').pop() || 'photo.jpg';
const mimeType = file.mimeType || 'image/jpeg';
await uploadDocument({
uri: file.uri,
name: fileName,
mimeType: mimeType
}, {
endpoint: '/activity/upload',
fileKey: 'files',
extraData: {
model_classname: 'Activity',
model_id: savedId.toString(),
method: 'put',
name: fileName,
type: mimeType
}
});
}
}
alert.showAlert('success', 'Salvato', 'Attività salvata con successo.');
if (isEditing) {
router.back();
} else {
router.push('/(protected)/activity');
}
} else {
alert.showAlert('error', 'Errore', res.data?.message || 'Impossibile salvare l\'attività.');
}
} catch (error) {
console.error('Errore salvataggio:', error);
alert.showAlert('error', 'Errore di connessione', 'Verifica la connessione e riprova.');
} finally {
setIsSubmitting(false);
}
};
if (isLoading) {
return (
<View className="flex-1 bg-gray-50 items-center justify-center">
<ActivityIndicator size="large" color="#1071C2" />
</View>
);
}
const windowWidth = Dimensions.get('window').width;
const itemsPerRow = 4;
const padding = 20;
const gap = 12;
const tileWidth = (windowWidth - (padding * 2) - (gap * (itemsPerRow - 1))) / itemsPerRow;
return (
<View className="flex-1 bg-gray-50">
<StatusBar style="dark" />
{/* Header */}
<View className="bg-white px-4 pb-4 shadow-sm border-b border-gray-100">
<SafeAreaView edges={['top']} className="pt-2">
<View className="flex-row items-center justify-between px-2">
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100 w-12 items-center justify-center">
<ChevronLeft size={28} color="#4b5563" pointerEvents="none" />
</TouchableOpacity>
<Text className="text-xl font-bold text-gray-800 uppercase flex-1 text-center" numberOfLines={1}>
{isEditing ? 'Modifica Attività' : 'Nuova Attività'}
</Text>
<View className="w-12" />
</View>
</SafeAreaView>
</View>
<KeyboardAwareScrollView
contentContainerStyle={{ padding: 20 }}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* General Info Card */}
<View className="bg-white rounded-3xl p-5 shadow-sm border border-gray-100 mb-4">
<View className="mb-4">
<Text className="text-[#082963] font-bold text-sm mb-2 uppercase">Cantiere <Text className="text-red-500">*</Text></Text>
<View>
<GenericDropdown
options={subactivities.map(s => ({ id: s.uuid, label: s.label }))}
selectedId={selectedSubactivityUuid}
onSelect={(id) => setSelectedSubactivityUuid(id as string)}
placeholder="Seleziona il cantiere"
searchPlaceholder="Cerca cantiere"
/>
</View>
</View>
<View className="mb-4">
<Text className="text-[#082963] font-bold text-sm mb-2 uppercase">Data</Text>
<TouchableOpacity
onPress={() => setShowDatePicker(true)}
className="bg-gray-50 border border-gray-200 p-3 rounded-2xl flex-row items-center justify-between"
>
<Text className="text-gray-800 text-base">{date ? formatDate(formatPickerDate(date) || undefined) : ''}</Text>
<CalendarIcon size={20} color="#9ca3af" />
</TouchableOpacity>
{showDatePicker && (
<Modal visible={showDatePicker} transparent animationType="fade">
<View className="flex-1 bg-black/50 justify-center px-4">
<View className="bg-white rounded-3xl p-5 w-full max-w-sm self-center shadow-lg">
<AppDatePicker
date={date}
mode="single"
onChange={handleDateChange}
/>
<TouchableOpacity
onPress={() => setShowDatePicker(false)}
className="mt-4 p-3 rounded-xl items-center border border-gray-200 active:bg-gray-50"
>
<Text className="text-gray-600 font-bold">Chiudi</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
)}
</View>
<View>
<Text className="text-[#082963] font-bold text-sm mb-2 uppercase">Descrizione</Text>
<TextInput
value={description}
onChangeText={setDescription}
placeholder="Inserisci una descrizione (opzionale)"
multiline
numberOfLines={4}
textAlignVertical="top"
className="bg-gray-50 border border-gray-200 p-4 rounded-2xl text-gray-800 text-base min-h-[100px]"
/>
</View>
</View>
{/* Labor Cards */}
<View>
<ActivityLaborCard
title="Operai"
fetchUrl="/activity/get-operators"
laborList={operatorLabor}
onAddLabor={(labor) => setOperatorLabor([...operatorLabor, labor])}
onRemoveLabor={(idx) => setOperatorLabor(operatorLabor.filter((_, i) => i !== idx))}
/>
<ActivityLaborCard
title="Subappaltatori"
fetchUrl="/activity/get-subcontractors"
laborList={subcontractorLabor}
onAddLabor={(labor) => setSubcontractorLabor([...subcontractorLabor, labor])}
onRemoveLabor={(idx) => setSubcontractorLabor(subcontractorLabor.filter((_, i) => i !== idx))}
/>
<ActivityLaborCard
title="Operai Distaccati"
fetchUrl="/activity/get-other-operators"
laborList={otherOperatorLabor}
onAddLabor={(labor) => setOtherOperatorLabor([...otherOperatorLabor, labor])}
onRemoveLabor={(idx) => setOtherOperatorLabor(otherOperatorLabor.filter((_, i) => i !== idx))}
/>
<ActivityLaborCard
title="Attrezzature"
fetchUrl="/activity/get-equipment"
laborList={equipmentLabor}
onAddLabor={(labor) => setEquipmentLabor([...equipmentLabor, labor])}
onRemoveLabor={(idx) => setEquipmentLabor(equipmentLabor.filter((_, i) => i !== idx))}
/>
</View>
{/* Photos */}
<View className="bg-white rounded-3xl p-5 shadow-sm border border-gray-100 mb-6">
<Text className="text-[#082963] font-bold text-lg mb-4">Allegati</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: gap }}>
{/* Existing Photos (Read-only) */}
{existingPhotos.map((photo, index) => (
<View key={`ext-${index}`} style={{ width: tileWidth, height: tileWidth }} className="rounded-2xl overflow-hidden border border-gray-200">
<Image source={{ uri: photo.uri }} style={{ width: '100%', height: '100%' }} contentFit="cover" />
</View>
))}
{/* New Photos */}
{photos.map((photo, index) => (
<RemovablePhotoTile key={`new-${index}`} uri={photo.uri} onRemove={() => removePhoto(index)} size={tileWidth} />
))}
{/* Add Buttons */}
{(photos.length + existingPhotos.length) < 50 && (
<>
<CameraAddTile onPress={takePhoto} size={tileWidth} />
<TouchableOpacity
onPress={pickFromGallery}
style={{ width: tileWidth, height: tileWidth }}
className="bg-blue-50 items-center justify-center rounded-2xl border border-blue-100 border-dashed"
>
<ImageIcon size={24} color="#1071C2" />
</TouchableOpacity>
</>
)}
</View>
</View>
{/* Submit */}
<TouchableOpacity
onPress={handleSave}
disabled={isSubmitting}
className={`bg-[#1071C2] p-4 rounded-full items-center justify-center mt-2 flex-row gap-2 ${isSubmitting ? 'opacity-70' : 'active:bg-[#0d5a9b]'}`}
>
{isSubmitting ? (
<ActivityIndicator color="white" />
) : (
<Text className="text-white font-bold text-lg uppercase tracking-wider">
{isEditing ? 'Salva Modifiche' : 'Salva Attività'}
</Text>
)}
</TouchableOpacity>
</KeyboardAwareScrollView>
</View>
);
}
+191
View File
@@ -0,0 +1,191 @@
import { useAlert } from '@/components/AlertComponent';
import LoadingScreen from '@/components/LoadingScreen';
import api from '@/utils/api';
import { Plus, Filter, Newspaper, ShoppingBag } 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, ActivityItem } 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<ActivityItem[]>([]);
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('/construction-site/get-construction-sites');
if (response.data?.success) {
setPlaces(response.data.constructionSites || []);
}
} 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, constructionSite: currentPlace };
const response = await api.post('/activity/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">Lista delle attività nei cantieri</Text>
<Text className="text-yellow-400 text-3xl font-bold leading-tight">Attività Cantieri</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 alla lista delle attività di cantiere</Text>
</View>
) : (
<View className="gap-4">
{updates.map((item, index) => (
<TouchableOpacity
key={index}
className="bg-white p-5 rounded-2xl shadow-sm border border-gray-100 active:bg-gray-50"
onPress={() => router.push(`/activity/${item.id}`)}
>
<View className="flex-row items-center mb-3">
<View className="bg-blue-50 p-3 rounded-full items-center justify-center mr-4">
<ShoppingBag size={24} color="#082963" />
</View>
<View className="flex-1">
<Text className="font-bold text-[#082963] text-lg uppercase leading-tight mb-1">
{item.constructionSite}
</Text>
<Text className="text-[#082963] text-sm font-medium leading-tight mb-1">
{item.subactivity ?? '-'}
</Text>
<Text className="text-gray-400 text-sm font-bold">
{item.date}
</Text>
</View>
</View>
{item.description ? (
<View className="flex-row items-center pt-3 border-t border-gray-50">
<Text numberOfLines={4} className="text-[#082963] text-[13px] font-medium leading-tight">
{item.description}
</Text>
</View>
) : null}
</TouchableOpacity>
))}
</View>
)}
</ScrollView>
{/* FAB Add Journal */}
<TouchableOpacity
onPress={() => router.push('/activity/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>
);
}