- Improved error message handling in LoginScreen for invalid credentials. - Added new images: mariani-icon.png and mariani-splash.png. - Updated AddDocumentModal to handle file extensions and improve UI. - Enhanced CalendarWidget to support month change callbacks. - Introduced NfcScanModal for NFC tag scanning with animations. - Revamped QrScanModal to utilize camera for QR code scanning. - Removed mock data from data.ts and streamlined Office data. - Updated package dependencies for expo-camera and react-native-nfc-manager. - Added utility function to parse seconds to time format. - Refactored document upload logic to use FormData for server uploads.
240 lines
10 KiB
TypeScript
240 lines
10 KiB
TypeScript
import { Download, FileText, Plus, Search, Calendar as CalendarIcon, ArrowLeft } from 'lucide-react-native';
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
import { RangePickerModal } from '@/components/RangePickerModal';
|
|
import { Alert, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { ConstructionSite, DocumentItem } from '@/types/types';
|
|
import LoadingScreen from '@/components/LoadingScreen';
|
|
import api from '@/utils/api';
|
|
import dayjs from 'dayjs';
|
|
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
|
|
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
|
|
import AddDocumentModal from '@/components/AddDocumentModal';
|
|
|
|
export default function SiteDocumentsScreen() {
|
|
const router = useRouter();
|
|
const params = useLocalSearchParams();
|
|
const [site, setSite] = useState<ConstructionSite | null>(null);
|
|
const [documents, setDocuments] = useState<DocumentItem[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
|
|
// Fetch dei documenti del cantiere
|
|
const fetchSiteDocuments = useCallback(async (siteId: number, isRefreshing = false) => {
|
|
try {
|
|
if (!isRefreshing) setIsLoading(true);
|
|
|
|
// Fetch Documenti
|
|
const response = await api.get(`/attachment/get-site-attachments`, {
|
|
params: { siteId }
|
|
});
|
|
setDocuments(response.data);
|
|
} catch (error) {
|
|
console.error('Errore nel recupero dei documenti del cantiere:', error);
|
|
Alert.alert('Errore', 'Impossibile recuperare i documenti del cantiere. Riprova più tardi.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (params.siteData) {
|
|
try {
|
|
const jsonString = Array.isArray(params.siteData) ? params.siteData[0] : params.siteData;
|
|
const parsedSite = JSON.parse(jsonString);
|
|
setSite(parsedSite);
|
|
} catch (error) {
|
|
console.error('Errore nel parsing dei dati del cantiere:', error);
|
|
}
|
|
}
|
|
}, [params.siteData]);
|
|
|
|
useEffect(() => {
|
|
if (params.id) {
|
|
setIsLoading(true);
|
|
fetchSiteDocuments(Number(params.id));
|
|
}
|
|
}, [params.id, fetchSiteDocuments]);
|
|
|
|
const onRefresh = () => {
|
|
setRefreshing(true);
|
|
fetchSiteDocuments(Number(params.id));
|
|
};
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [range, setRange] = useState<{ startDate: any; endDate: any }>({
|
|
startDate: null,
|
|
endDate: null
|
|
});
|
|
const [showRangePicker, setShowRangePicker] = useState(false);
|
|
|
|
// Filtraggio Documenti
|
|
const filteredDocs = documents.filter(doc => {
|
|
// Filtro Testuale (su nome documento)
|
|
const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase());
|
|
if (!matchesSearch) return false;
|
|
|
|
// Filtro Date Range
|
|
if (range.startDate || range.endDate) {
|
|
const docDate = parseTimestamp(doc.updated_at);
|
|
if (range.startDate) {
|
|
const start = dayjs(range.startDate).startOf('day').toDate();
|
|
if (docDate <= start) return false;
|
|
}
|
|
if (range.endDate) {
|
|
const end = dayjs(range.endDate).endOf('day').toDate();
|
|
if (docDate >= end) return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Gestione Download e Condivisione Documento
|
|
const handleDownloadAndShare = async (mimetype: string, fileName: string, fileUrl: string) => {
|
|
try {
|
|
await downloadAndShareDocument(mimetype, fileName, fileUrl);
|
|
} catch (error) {
|
|
console.error('Errore nel download/condivisione del documento:', error);
|
|
Alert.alert('Errore', 'Impossibile scaricare il documento. Riprova più tardi.');
|
|
}
|
|
};
|
|
|
|
// Gestione Caricamento Documento
|
|
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
|
setIsUploading(true);
|
|
try {
|
|
await uploadDocument(file, Number(params.id), customTitle);
|
|
Alert.alert('Successo', 'Documento caricato con successo!');
|
|
setShowUploadModal(false);
|
|
fetchSiteDocuments(Number(params.id), true);
|
|
} catch (error) {
|
|
console.error('Errore nel caricamento del documento:', error);
|
|
Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.');
|
|
} finally {
|
|
setIsUploading(false);
|
|
}
|
|
}
|
|
|
|
if (!site || (isLoading && !refreshing)) {
|
|
return (
|
|
<LoadingScreen />
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View className="flex-1 bg-gray-50">
|
|
{/* Header */}
|
|
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100 flex-row items-center gap-4">
|
|
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100">
|
|
<ArrowLeft size={24} color="#374151" />
|
|
</TouchableOpacity>
|
|
|
|
<View className="flex-1">
|
|
{/* Badge Codice Cantiere */}
|
|
{site.code && (
|
|
<View className="self-start bg-[#E6F4F4] px-2 py-0.5 rounded mb-1 border border-[#099499]/20">
|
|
<Text className="text-base font-bold text-[#099499]">
|
|
{site.code}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Nome Cantiere con Truncate funzionante */}
|
|
<Text
|
|
className="text-xl font-bold text-gray-800 leading-tight"
|
|
numberOfLines={1}
|
|
ellipsizeMode="tail"
|
|
>
|
|
{site.name}
|
|
</Text>
|
|
|
|
<Text className="text-sm text-gray-500 mt-0.5">Archivio documenti</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View className="p-5 gap-6 flex-1">
|
|
{/* Search + Date Row (Spostato qui) */}
|
|
<View className="flex-row gap-2">
|
|
<View className="flex-1 relative justify-center">
|
|
<View className="absolute left-4 z-10">
|
|
<Search size={24} color="#9ca3af" />
|
|
</View>
|
|
<TextInput
|
|
placeholder="Cerca documento..."
|
|
placeholderTextColor="#9ca3af"
|
|
className={`w-full pl-12 pr-4 py-4 bg-white rounded-2xl text-gray-800 text-lg border shadow-sm ${searchTerm ? 'border-[#099499]' : 'border-gray-200'}`}
|
|
value={searchTerm}
|
|
onChangeText={setSearchTerm}
|
|
/>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => setShowRangePicker(true)}
|
|
className={`p-4 bg-white rounded-2xl shadow-sm border ${range.startDate ? 'border-[#099499]' : 'border-gray-200'}`}
|
|
>
|
|
<CalendarIcon size={24} color={range.startDate ? "#099499" : "#9ca3af"} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<RangePickerModal
|
|
visible={showRangePicker}
|
|
onClose={() => setShowRangePicker(false)}
|
|
currentRange={range}
|
|
onApply={setRange}
|
|
/>
|
|
|
|
{/* Lista Documenti */}
|
|
<ScrollView
|
|
contentContainerStyle={{ gap: 16, paddingBottom: 100 }}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
|
}
|
|
>
|
|
{filteredDocs.map((doc) => (
|
|
<View key={doc.id} className="bg-white p-5 rounded-3xl shadow-sm flex-row items-center justify-between border border-gray-100">
|
|
<View className="flex-row items-center gap-5 flex-1">
|
|
<View className="bg-red-50 p-4 rounded-2xl">
|
|
<FileText size={32} color="#ef4444" />
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="font-bold text-gray-800 text-lg mb-1">{doc.title}</Text>
|
|
<Text className="text-sm text-gray-400 font-medium">{formatTimestamp(doc.updated_at)}</Text>
|
|
</View>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={() => handleDownloadAndShare(doc.mimetype, doc.title, doc.url)}
|
|
className="p-4 bg-gray-50 rounded-2xl active:bg-gray-100">
|
|
<Download size={24} color="#6b7280" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
))}
|
|
|
|
{filteredDocs.length === 0 && (
|
|
<Text className="text-center text-gray-400 mt-10">Nessun documento trovato</Text>
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
|
|
{/* FAB (Spostato qui) */}
|
|
<TouchableOpacity
|
|
onPress={() => setShowUploadModal(true)}
|
|
className="absolute bottom-8 right-6 w-16 h-16 bg-[#099499] rounded-full shadow-lg items-center justify-center active:scale-90"
|
|
>
|
|
<Plus size={32} color="white" />
|
|
</TouchableOpacity>
|
|
|
|
{/* Modale Caricamento Documento */}
|
|
<AddDocumentModal
|
|
visible={showUploadModal}
|
|
onClose={() => setShowUploadModal(false)}
|
|
onUpload={handleUploadDocument}
|
|
isUploading={isUploading}
|
|
/>
|
|
</View>
|
|
);
|
|
} |