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
@@ -0,0 +1,162 @@
import DocumentListCard from '@/components/DocumentListCard';
import LoadingScreen from '@/components/LoadingScreen';
import { DocumentItem } from '@/types/types';
import api from '@/utils/api';
import * as DocumentPicker from 'expo-document-picker';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { ChevronLeft, AlertCircle } from 'lucide-react-native';
import { useAlert } from '@/components/AlertComponent';
import { uploadDocument } from '@/utils/documentUtils';
import React, { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function ConstructionSiteDocumentsScreen() {
const { id, name } = useLocalSearchParams();
const router = useRouter();
const alert = useAlert();
const [documents, setDocuments] = useState<DocumentItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const fetchDocuments = useCallback(async () => {
try {
const response = await api.get(`/construction-site/get-attachments?id=${id}`);
if (response.data?.success) {
setDocuments(response.data.result || []);
}
} catch (error) {
console.error('Errore nel recupero dei documenti:', error);
alert.showAlert('error', 'Errore', 'Impossibile recuperare la lista dei documenti.');
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, [id]);
useEffect(() => {
if (id) {
fetchDocuments();
}
}, [id, fetchDocuments]);
const onRefresh = () => {
setIsRefreshing(true);
fetchDocuments();
};
const handleUpload = async () => {
try {
const result = await DocumentPicker.getDocumentAsync({
type: '*/*',
copyToCacheDirectory: true,
multiple: false,
});
if (result.canceled || !result.assets || result.assets.length === 0) {
return;
}
const file = result.assets[0];
setIsUploading(true);
await uploadDocument(file, {
endpoint: '/construction-site/upload-attachment',
fileKey: 'files',
extraData: {
model_classname: 'ConstructionSite',
model_id: id as string,
method: 'put',
name: file.name,
type: file.mimeType || 'application/octet-stream'
}
});
alert.showAlert('success', 'Successo', 'Documento caricato con successo.');
onRefresh(); // reload list
} catch (error: any) {
console.error('Errore durante l\'upload:', error);
alert.showAlert('error', 'Errore', error.message || 'Si è verificato un errore durante l\'upload del documento.');
} finally {
setIsUploading(false);
}
};
if (isLoading && !isRefreshing) {
return <LoadingScreen />;
}
return (
<View className="flex-1 bg-primary-dark">
<StatusBar style="light" />
<SafeAreaView edges={['top']} className="pt-5">
{/* Header */}
<View className="pb-6 px-6 z-10 flex-row items-center justify-between">
<TouchableOpacity
onPress={() => router.back()}
className="p-2 -ml-2 rounded-full active:bg-white/20 w-12 items-center justify-center"
>
<ChevronLeft size={28} color="white" pointerEvents="none" />
</TouchableOpacity>
<View className="flex-1 px-2">
<Text className="text-white text-xl font-bold text-center leading-tight uppercase" numberOfLines={1}>
{name || 'Allegati Cantiere'}
</Text>
</View>
<View className="w-10" />
</View>
</SafeAreaView>
{/* Content */}
<View className="flex-1 bg-slate-50 rounded-t-[2.5rem] overflow-hidden">
{isUploading && (
<View className="absolute z-10 top-0 left-0 right-0 bottom-0 bg-white/50 justify-center items-center">
<ActivityIndicator size="large" color="#1071C2" />
<Text className="text-primary-dark font-bold mt-2">Caricamento in corso...</Text>
</View>
)}
<ScrollView
className="flex-1 px-6 pt-6"
contentContainerStyle={{ paddingBottom: 100 }}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} colors={['#1071C2']} />
}
>
<View className="flex-row justify-between items-center mb-6 px-1">
<Text className="text-slate-800 text-xl font-bold">Documenti</Text>
<TouchableOpacity onPress={handleUpload} disabled={isUploading}>
{isUploading ? (
<ActivityIndicator size="small" color="#1071C2" />
) : (
<Text className="text-[#1071C2] text-lg font-semibold">Aggiungi</Text>
)}
</TouchableOpacity>
</View>
{documents.length > 0 ? (
<View className="gap-4">
{documents.map((item, index) => (
<DocumentListCard key={item.id || index} item={item} />
))}
</View>
) : (
<View className="bg-white p-8 rounded-3xl border border-slate-200 items-center justify-center border-dashed mt-4">
<View className="bg-slate-50 p-4 rounded-full mb-3">
<AlertCircle size={32} color="#94a3b8" />
</View>
<Text className="text-slate-500 font-medium text-center">
Nessun documento caricato per questo cantiere.
</Text>
</View>
)}
</ScrollView>
</View>
</View>
);
}
@@ -0,0 +1,177 @@
import LoadingScreen from '@/components/LoadingScreen';
import api from '@/utils/api';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { Building2, ChevronLeft, Paperclip, AlertCircle, FileText } from 'lucide-react-native';
import React, { useEffect, useState } from 'react';
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
const InfoRow = ({
label,
value,
isLast = false,
isCode = false
}: {
label: string;
value?: string | number | null;
isLast?: boolean;
isCode?: boolean
}) => (
<View className={`flex-row justify-between items-start ${!isLast ? 'border-b border-slate-100 pb-3' : ''}`}>
<Text className="text-slate-500 font-medium">{label}</Text>
<Text
selectable={true}
className={`text-primary-dark font-bold text-right flex-1 ${isCode ? 'tracking-widest' : ''}`}
>
{value || '-'}
</Text>
</View>
);
export default function ConstructionSiteDetailsScreen() {
const { id } = useLocalSearchParams();
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const [siteData, setSiteData] = useState<any>(null);
const fetchDetails = async () => {
try {
const response = await api.get(`/construction-site/get-info?id=${id}`);
if (response.data?.success) {
setSiteData(response.data.result);
}
} catch (error) {
console.error('Errore nel recupero dei dettagli del cantiere:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (id) {
fetchDetails();
}
}, [id]);
if (isLoading) {
return <LoadingScreen />;
}
if (!siteData) {
return (
<View className="flex-1 bg-slate-50 justify-center items-center">
<AlertCircle size={48} color="#94a3b8" />
<Text className="text-slate-500 mt-4 font-medium">Impossibile caricare i dettagli.</Text>
<TouchableOpacity onPress={() => router.back()} className="mt-4 px-6 py-2 bg-primary-dark rounded-full">
<Text className="text-white font-bold">Indietro</Text>
</TouchableOpacity>
</View>
);
}
const { constructionSite, client, subactivities } = siteData;
return (
<View className="flex-1 bg-primary-dark">
<StatusBar style="light" />
<SafeAreaView edges={['top']} className="pt-5">
{/* Header */}
<View className="pb-6 px-6 z-10 flex-row items-center justify-between">
<TouchableOpacity
onPress={() => router.back()}
className="p-2 -ml-2 rounded-full active:bg-white/20 w-12 items-center justify-center"
>
<ChevronLeft size={28} color="white" pointerEvents="none" />
</TouchableOpacity>
<View className="flex-1 px-2">
<Text className="text-white text-xl font-bold text-center leading-tight" numberOfLines={1}>
{constructionSite?.name || 'Dettaglio Cantiere'}
</Text>
</View>
<TouchableOpacity
onPress={() => router.push(`/construction-site/${id}/documents?name=${encodeURIComponent(constructionSite?.name || '')}`)}
className="p-2 -mr-2 rounded-full active:bg-white/20 w-12 items-center justify-center"
>
<Paperclip size={22} color="white" pointerEvents="none" />
</TouchableOpacity>
</View>
</SafeAreaView>
{/* Content */}
<View className="flex-1 bg-slate-50 rounded-t-[2.5rem] overflow-hidden">
<ScrollView
className="flex-1 px-6 pt-8"
contentContainerStyle={{ paddingBottom: 100 }}
showsVerticalScrollIndicator={false}
>
{/* Info Card */}
<View className="bg-white rounded-3xl p-6 shadow-sm border border-slate-100 mb-6">
<View className="flex-row items-center mb-6">
<View className="bg-primary-50 p-3 rounded-xl mr-4">
<Building2 size={24} color="#1071C2" />
</View>
<Text className="text-xl font-bold text-slate-800 flex-1">Informazioni</Text>
</View>
<View className="gap-4">
<InfoRow label="Cliente" value={client} />
<InfoRow label="Indirizzo" value={constructionSite?.address} />
<InfoRow label="CIG" value={constructionSite?.cig} isCode={true} />
<InfoRow label="CUP" value={constructionSite?.cup} isCode={true} />
<InfoRow
label="% Ribasso"
value={constructionSite?.reduction ? `${constructionSite.reduction} %` : undefined}
isLast={true}
/>
</View>
</View>
{/* Subactivities */}
{subactivities && subactivities.length > 0 && (
<View className="mb-6">
<Text className="text-slate-800 text-xl font-bold mb-4 px-2">Sottocommesse</Text>
<View className="gap-3">
{subactivities.map((sub: any, index: number) => {
const hasCodes = sub.cig || sub.cup;
return (
<View key={index} className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100">
<View className={`flex-row items-center ${hasCodes ? 'mb-3 border-b border-slate-50 pb-3' : ''}`}>
<View className="bg-slate-50 p-2 rounded-lg mr-3">
<FileText size={20} color="#64748b" />
</View>
<Text selectable={true} className="text-slate-800 font-bold text-sm uppercase flex-1 leading-tight">
{sub.code} - {sub.description}
</Text>
</View>
{hasCodes && (
<View className="gap-2 px-2">
{sub.cig && (
<View className="flex-row justify-between items-center">
<Text className="text-slate-500 text-xs font-medium">CIG</Text>
<Text selectable={true} className="text-primary-dark font-bold text-sm tracking-widest">{sub.cig}</Text>
</View>
)}
{sub.cup && (
<View className="flex-row justify-between items-center">
<Text className="text-slate-500 text-xs font-medium">CUP</Text>
<Text selectable={true} className="text-primary-dark font-bold text-sm tracking-widest">{sub.cup}</Text>
</View>
)}
</View>
)}
</View>
);
})}
</View>
</View>
)}
</ScrollView>
</View>
</View>
);
}