258 lines
12 KiB
TypeScript
258 lines
12 KiB
TypeScript
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, useState } from 'react';
|
|
import { Dimensions, RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
|
import { SafeAreaView } 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 [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>
|
|
);
|
|
}
|