Files
mariani_mobile/app/(protected)/profile/documents.tsx
leonardo 44d021891f feat: Add document download and upload. Add NFC support and enhance attendance and permits views
- 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.
2026-01-19 18:10:31 +01:00

203 lines
8.7 KiB
TypeScript

import { ArrowLeft, Download, FileText, MapPin, Plus, Search, CalendarIcon } from 'lucide-react-native';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'expo-router';
import { RangePickerModal } from '@/components/RangePickerModal';
import { Alert, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { DocumentItem } from '@/types/types';
import api from '@/utils/api';
import dayjs from 'dayjs';
import LoadingScreen from '@/components/LoadingScreen';
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
import AddDocumentModal from '@/components/AddDocumentModal';
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
export default function DocumentsScreen() {
const router = useRouter();
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);
const [showRangePicker, setShowRangePicker] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [range, setRange] = useState<{ startDate: any; endDate: any }>({
startDate: null,
endDate: null
});
const fetchUserDocuments = async () => {
try {
if (!refreshing) setIsLoading(true);
// Fetch Documenti Utente
const response = await api.get(`/attachment/get-user-attachments`);
setDocuments(response.data);
} catch (error) {
console.error('Errore nel recupero dei documenti utente:', error);
Alert.alert('Errore', 'Impossibile recuperare i documenti. Riprova più tardi.');
} finally {
setIsLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
fetchUserDocuments();
}, []);
const onRefresh = () => {
setRefreshing(true);
fetchUserDocuments();
};
// Filtra Documenti in base a searchTerm e range
const filteredDocs = documents.filter(doc => {
// Filtro Testuale
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); // doc.date è "DD/MM/YYYY"
// Controllo Data Inizio
if (range.startDate) {
// dayjs(range.startDate).toDate() converte in oggetto Date JS standard
const start = dayjs(range.startDate).startOf('day').toDate();
if (docDate < start) return false;
}
// Controllo Data Fine
if (range.endDate) {
const end = dayjs(range.endDate).endOf('day').toDate();
if (docDate > end) return false;
}
}
return true;
});
// Gestione Caricamento Documento
const handleUploadDocument = async (file: any, customTitle?: string) => {
setIsUploading(true);
try {
await uploadDocument(file, null, customTitle);
Alert.alert('Successo', 'Documento caricato con successo!');
setShowUploadModal(false);
fetchUserDocuments();
} catch (error) {
console.error('Errore nel caricamento del documento:', error);
Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.');
} finally {
setIsUploading(false);
}
};
if (isLoading && !refreshing) {
return (
<LoadingScreen />
);
}
return (
<View className="flex-1 bg-gray-50">
{/* Header */}
<View className="flex-row items-center gap-4 bg-white p-6 pt-16 shadow-sm border-b border-gray-100">
<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">
<Text className="text-3xl font-bold text-gray-800">Documenti</Text>
<Text className="text-base text-gray-500">Gestisci i tuoi documenti</Text>
</View>
</View>
<View className="p-5 gap-6 flex-1">
{/* Search + Date Row */}
<View className="flex-row gap-2">
{/* Search Bar */}
<View className={`flex-1 relative justify-center`}>
<View className="absolute left-4 z-10">
<Search size={24} color="#9ca3af" />
</View>
<TextInput
placeholder="Cerca nome del documento..."
placeholderTextColor="#9ca3af"
className={`w-full pl-12 pr-4 py-4 bg-white shadow-sm rounded-2xl text-gray-800 text-lg border shadow-sm ${searchTerm ? 'border-[#099499]' : 'border-gray-200'}`}
value={searchTerm}
onChangeText={setSearchTerm}
/>
</View>
{/* Date Filter Button */}
<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>
{/* Modale Unico per il Range */}
<RangePickerModal
visible={showRangePicker}
onClose={() => setShowRangePicker(false)}
currentRange={range}
onApply={setRange}
/>
{/* List */}
<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>
<View className="flex-row items-center mt-1">
<MapPin size={16} color="#9ca3af" />
<Text className="text-sm text-gray-400 ml-1 font-medium">{formatTimestamp(doc.updated_at)}</Text>
</View>
</View>
</View>
<TouchableOpacity
onPress={() => downloadAndShareDocument(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 */}
<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>
);
}