diff --git a/app.json b/app.json index fc3d6ca..f4a298a 100644 --- a/app.json +++ b/app.json @@ -4,12 +4,13 @@ "slug": "mariani_app", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/images/icon.png", + "icon": "./assets/images/mariani-icon.png", "scheme": "marianiapp", "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.anonymous.mariani-app" }, "android": { "adaptiveIcon": { @@ -19,7 +20,12 @@ "monochromeImage": "./assets/images/android-icon-monochrome.png" }, "edgeToEdgeEnabled": true, - "predictiveBackGestureEnabled": false + "predictiveBackGestureEnabled": false, + "permissions": [ + "android.permission.CAMERA", + "android.permission.RECORD_AUDIO" + ], + "package": "com.anonymous.mariani_app" }, "web": { "output": "static", @@ -31,14 +37,22 @@ [ "expo-splash-screen", { - "image": "./assets/images/splash-icon.png", + "image": "./assets/images/mariani-splash.png", "imageWidth": 200, "resizeMode": "contain", - "backgroundColor": "#ffffff", + "backgroundColor": "#099499", "dark": { - "backgroundColor": "#000000" + "backgroundColor": "#099499" } } + ], + [ + "expo-camera", + { + "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera", + "microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone", + "recordAudioAndroid": true + } ] ], "experiments": { diff --git a/app/(protected)/_layout.tsx b/app/(protected)/_layout.tsx index 1cdd77a..87666aa 100644 --- a/app/(protected)/_layout.tsx +++ b/app/(protected)/_layout.tsx @@ -23,14 +23,15 @@ export default function ProtectedLayout() { backgroundColor: '#ffffff', borderTopWidth: 1, borderTopColor: '#f3f4f6', - height: 80, - paddingBottom: 20, + height: 90, + paddingBottom: 30, paddingTop: 10, + paddingHorizontal: 10, }, tabBarActiveTintColor: '#099499', tabBarInactiveTintColor: '#9ca3af', tabBarLabelStyle: { - fontSize: 10, + fontSize: 12, fontWeight: '600', marginTop: 4 } diff --git a/app/(protected)/attendance/index.tsx b/app/(protected)/attendance/index.tsx index ef0e6bf..9af6198 100644 --- a/app/(protected)/attendance/index.tsx +++ b/app/(protected)/attendance/index.tsx @@ -1,24 +1,39 @@ -import React, { use, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { View, Text, TouchableOpacity, ScrollView, Alert, RefreshControl } from 'react-native'; -import { QrCode, CheckCircle2 } from 'lucide-react-native'; -import { ATTENDANCE_DATA } from '@/data/data'; +import { QrCode, CheckCircle2, Nfc } from 'lucide-react-native'; import QrScanModal from '@/components/QrScanModal'; +import NfcScanModal from '@/components/NfcScanModal'; import LoadingScreen from '@/components/LoadingScreen'; import api from '@/utils/api'; +import { formatDate, formatTime, parseSecondsToTime } from '@/utils/dateTime'; +import { AttendanceRecord } from '@/types/types'; +import NfcManager from 'react-native-nfc-manager'; export default function AttendanceScreen() { + const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr'); const [showScanner, setShowScanner] = useState(false); const [lastScan, setLastScan] = useState<{ type: string; time: string; site: string } | null>(null); - const [attendances, setAttendances] = useState(ATTENDANCE_DATA); + const [attendances, setAttendances] = useState([]); const [isLoading, setIsLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); + const checkNfcAvailability = async () => { + // if (!ENABLE_NFC) return; + try { + const isSupported = await NfcManager.isSupported(); + if (isSupported) setScannerType('nfc'); + } catch (error) { + console.warn('NFC non supportato:', error); + } + }; + const fetchAttendances = async () => { try { if (!refreshing) setIsLoading(true); + // Fetch today's attendance data from API const response = await api.get('/attendance/list'); - // setAttendances(response.data); + setAttendances(response.data); } catch (error) { console.error('Errore nel recupero delle presenze:', error); Alert.alert('Errore', 'Impossibile recuperare le presenze. Riprova più tardi.'); @@ -29,26 +44,71 @@ export default function AttendanceScreen() { }; useEffect(() => { + checkNfcAvailability(); fetchAttendances(); + setLastScan(null); }, []); const onRefresh = () => { setRefreshing(true); fetchAttendances(); + setLastScan(null); }; - const handleQRScan = () => { - setShowScanner(true); - // Simulate scanning process - setTimeout(() => { - setShowScanner(false); - // Add new entry or update existing one - setLastScan({ - type: 'Entrata', - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - site: 'Cantiere Ospedale A.' - }); - }, 3000); + const handleStartScan = async () => { + // Modalità QR Code + if (scannerType === 'qr') { + setShowScanner(true); + return; + } + + // Modalità NFC + if (scannerType === 'nfc') { + try { + const supported = await NfcManager.isSupported(); + if (!supported) { + Alert.alert('NFC non supportato', 'Il tuo dispositivo non supporta la scansione NFC.'); + return; + } + + const enabled = await NfcManager.isEnabled(); + if (!enabled) { + Alert.alert('NFC disattivato', 'Per favore attiva l\'NFC nelle impostazioni del dispositivo per continuare.', [ + { text: 'OK' }, + { text: 'Vai alle impostazioni', onPress: () => NfcManager.goToNfcSetting() } + ]); + return; + } + + setShowScanner(true); + } catch (err) { + console.warn(err); + Alert.alert('Errore', 'Impossibile verificare lo stato dell\'NFC.'); + } + } + }; + + const onScan = async (data: string) => { + console.log('Scanned data:', data); + try { + // Send scanned data to API + const response = await api.post('/attendance/scan', data); + if (response) { + console.log('Scan data sent successfully:', response.data); + // Refresh attendance list + fetchAttendances(); + // Update last scan feedback + setLastScan({ + type: response.data.type, + time: formatTime(response.data.time), + site: response.data.site + }); + } + } catch (error) { + console.error('Errore nell\'invio dei dati di scansione:', error); + Alert.alert('Errore', 'Impossibile registrare la presenza. Riprova più tardi.'); + return; + } }; if (isLoading && !refreshing) { @@ -72,36 +132,51 @@ export default function AttendanceScreen() { > - {/* Feedback Card - OPZIONALE */} + {/* Feedback Card */} {lastScan ? ( - - + + - - {lastScan.type} Registrata! - {lastScan.site} alle {lastScan.time} + + + {lastScan.type} Registrata + + + + {lastScan.site} alle {lastScan.time} + + - ) : ( - null - )} + ) : null} {/* Scanner Section */} - Scansione QR/NFC + Scansione {scannerType === 'qr' ? 'QR Code' : 'NFC'} - + {scannerType === 'qr' ? : } Scansiona Codice - Posiziona il codice QR davanti alla fotocamera o usa il lettore NFC + {scannerType === 'qr' + ? 'Posiziona il codice QR davanti alla fotocamera per registrare l\'ingresso o l\'uscita dal cantiere' + : 'Avvicina il dispositivo NFC per registrare l\'ingresso o l\'uscita dal cantiere' + } @@ -109,34 +184,69 @@ export default function AttendanceScreen() { {/* Mini History */} Ultime Presenze - - {ATTENDANCE_DATA.map((item, index) => ( - - - - - {item.site} - {item.in} - {item.out || 'In corso'} + {attendances.length === 0 ? ( + Nessuna presenza registrata + ) : ( + + {attendances.map((item, index) => ( + + + + + + {item.site} + + + + + {formatDate(item.date)} + + + + {formatTime(item.in)} - {item.out ? formatTime(item.out) : 'IN CORSO'} + + + + + {/* TODO: item.time può essere null -> calcolare tempo da in e out? */} + {item.time && ( + + + {parseSecondsToTime(item.time)} + + + )} - {item.status === 'complete' && ( - - 8h - - )} - - ))} - + ))} + + )} {/* Scanner Modal */} - setShowScanner(false)} - /> + {scannerType === 'qr' ? ( + setShowScanner(false)} + onScan={onScan} + /> + ) : ( + setShowScanner(false)} + onScan={onScan} + /> + )} ); } \ No newline at end of file diff --git a/app/(protected)/index.tsx b/app/(protected)/index.tsx index 859e903..ba493c6 100644 --- a/app/(protected)/index.tsx +++ b/app/(protected)/index.tsx @@ -1,28 +1,81 @@ import { useRouter } from 'expo-router'; -import { AlertTriangle, CheckCircle2, FileText, QrCode, User } from 'lucide-react-native'; -import React, { useContext } from 'react'; -import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; -import { ATTENDANCE_DATA, DOCUMENTS_DATA } from '@/data/data'; +import { AlertTriangle, CalendarDays, CheckCircle2, FileText, QrCode, User, CalendarClock, LayoutDashboard } from 'lucide-react-native'; +import React, { useState, useContext, useEffect } from 'react'; +import { RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { AuthContext } from '@/utils/authContext'; +import { ActivityItem } from '@/types/types'; +import api from '@/utils/api'; +import LoadingScreen from '@/components/LoadingScreen'; export default function HomeScreen() { const router = useRouter(); const { user } = useContext(AuthContext); - const incompleteTasks = ATTENDANCE_DATA.filter(item => item.status === 'incomplete'); + const [isLoading, setIsLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [incompleteAttendance, setIncompleteAttendance] = useState(null); + const [recentActivities, setRecentActivities] = useState([]); + + const fetchDashboardData = async () => { + try { + if (!refreshing) setIsLoading(true); + // Fetch incomplete attendance data from API + const attendance = await api.get('/attendance/incomplete'); + setIncompleteAttendance(attendance.data); + + // Fetch recent activities data from API + const activities = await api.get('/user/recent-activities'); + setRecentActivities(activities.data); + } catch (error) { + console.error('Errore nel recupero dei dati della dashboard:', error); + } finally { + setIsLoading(false); + setRefreshing(false); + } + }; + + const getActivityConfig = (type: string) => { + switch (type) { + case 'document': + return { icon: FileText, bg: 'bg-gray-100', color: '#4b5563' }; + case 'attendance': + return { icon: CheckCircle2, bg: 'bg-[#099499]/10', color: '#099499' }; + case 'permit': + return { icon: CalendarClock, bg: 'bg-[#2563eb]/10', color: '#2563eb' }; + default: + return { icon: LayoutDashboard, bg: 'bg-gray-100', color: '#9ca3af' }; + } + }; + + useEffect(() => { + fetchDashboardData(); + }, []); + + const onRefresh = () => { + setRefreshing(true); + fetchDashboardData(); + }; + + if (isLoading && !refreshing) { + return ( + + ); + } return ( {/* Banner Custom */} - - + + Benvenuto - {user?.name} {user?.surname} + + {user?.name} {user?.surname} + {user?.role} - + router.push('/profile')}> @@ -35,18 +88,21 @@ export default function HomeScreen() { className="flex-1 bg-gray-50 rounded-t-[2.5rem] px-5 pt-6" contentContainerStyle={{ paddingBottom: 50, gap: 24 }} showsVerticalScrollIndicator={false} + refreshControl={ + + } > - {/* Warning Card - OPZIONALE */} - {incompleteTasks.length > 0 && ( - + {/* Warning Card */} + {incompleteAttendance && ( + Presenza incompleta - {incompleteTasks[0].site} + {incompleteAttendance} router.push('/attendance')} className="bg-orange-50 px-5 py-3 rounded-xl ml-2 active:bg-orange-100"> @@ -70,13 +126,13 @@ export default function HomeScreen() { router.push('/documents')} + onPress={() => router.push('/permits')} className="flex-1 bg-white p-6 rounded-3xl shadow-sm items-center justify-center gap-4 border border-gray-100 active:scale-[0.98]" > - + - Carica Documento + Gestisci Permessi @@ -85,37 +141,62 @@ export default function HomeScreen() { Ultime Attività - + {/* Vedi tutto - + */} - {DOCUMENTS_DATA.slice(0, 2).map((doc, i) => ( - - - - + {recentActivities.map((item) => { + const config = getActivityConfig(item.type); + const IconComponent = config.icon; + return ( + + + + {/* Icona */} + + + + + {/* Titolo e Sottotitolo */} + + + {item.title} + + + {item.subtitle} + + - - {doc.name} - Nuovo documento • {doc.date} + + {/* Data */} + + {item.date_display} + ); + })} + + {/* Empty State */} + {!isLoading && recentActivities.length === 0 && ( + + Nessuna attività recente - ))} - {ATTENDANCE_DATA.slice(0, 1).map((att, i) => ( - - - - - - - Presenza Completata - {att.site} • {att.in} - - + )} + + {/* Loading State */} + {isLoading && recentActivities.length === 0 && ( + + Caricamento... - ))} + )} diff --git a/app/(protected)/permits/index.tsx b/app/(protected)/permits/index.tsx index aecd218..fc8bf25 100644 --- a/app/(protected)/permits/index.tsx +++ b/app/(protected)/permits/index.tsx @@ -1,4 +1,4 @@ -import React, { JSX, useEffect, useState } from 'react'; +import React, { JSX, useEffect, useMemo, useState } from 'react'; import { Calendar as CalendarIcon, CalendarX, Clock, Plus, Thermometer } from 'lucide-react-native'; import { Alert, ScrollView, Text, TouchableOpacity, View, ActivityIndicator, RefreshControl } from 'react-native'; import { TimeOffRequest, TimeOffRequestType } from '@/types/types'; @@ -20,6 +20,7 @@ export default function PermitsScreen() { const [showModal, setShowModal] = useState(false); const [permits, setPermits] = useState([]); const [types, setTypes] = useState([]); + const [currentMonthDate, setCurrentMonthDate] = useState(new Date()); const [isLoading, setIsLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -43,6 +44,29 @@ export default function PermitsScreen() { } }; + const filteredPermits = useMemo(() => { + if (!permits.length) return []; + + // Calcoliamo inizio e fine del mese visualizzato + const year = currentMonthDate.getFullYear(); + const month = currentMonthDate.getMonth(); + + const startOfMonth = new Date(year, month, 1); + // Trucco JS: giorno 0 del mese successivo = ultimo giorno del mese corrente + const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59); + + return permits.filter(item => { + const itemStart = new Date(item.start_date?.toString() ?? ''); + // Se non c'è end_date, assumiamo sia un giorno singolo (quindi end = start) + const itemEnd = item.end_date ? new Date(item.end_date?.toString() ?? '') : new Date(item.start_date?.toString() ?? ''); + + // FORMULA OVERLAP: + // Il permesso è visibile se inizia prima della fine del mese + // E finisce dopo l'inizio del mese. + return itemStart <= endOfMonth && itemEnd >= startOfMonth; + }); + }, [permits, currentMonthDate]); + useEffect(() => { fetchPermits(); }, []); @@ -82,17 +106,16 @@ export default function PermitsScreen() { > {/* Calendar Widget */} - + setCurrentMonthDate(date)} /> {/* Lista Richieste Recenti */} - {permits.length === 0 ? ( - Nessuna richiesta di permesso trovata. + {filteredPermits.length === 0 ? ( + Nessuna richiesta di permesso questo mese ) : ( Le tue richieste - {/* TODO: Aggiungere una paginazione con delle freccette affianco? - Limite backend? */} - {permits.map((item) => ( + {filteredPermits.map((item) => ( diff --git a/app/(protected)/profile/documents.tsx b/app/(protected)/profile/documents.tsx index cd7f46e..9a19c30 100644 --- a/app/(protected)/profile/documents.tsx +++ b/app/(protected)/profile/documents.tsx @@ -9,7 +9,7 @@ import dayjs from 'dayjs'; import LoadingScreen from '@/components/LoadingScreen'; import { formatTimestamp, parseTimestamp } from '@/utils/dateTime'; import AddDocumentModal from '@/components/AddDocumentModal'; -import { downloadAndShareDocument, downloadDocumentByUrl, downloadDocumentLegacy, uploadDocument } from '@/utils/documentUtils'; +import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils'; export default function DocumentsScreen() { const router = useRouter(); @@ -82,27 +82,16 @@ export default function DocumentsScreen() { const handleUploadDocument = async (file: any, customTitle?: string) => { setIsUploading(true); try { - const response = await uploadDocument(file, null, customTitle); - // console.log('Risposta caricamento:', response.data); - // Alert.alert('Successo', 'Documento caricato con successo!'); - // setShowUploadModal(false); - // fetchUserDocuments(); // Ricarica la lista dei documenti + 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); } - } - - // 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/condividere il documento. Riprova più tardi.'); - } }; if (isLoading && !refreshing) { @@ -181,7 +170,7 @@ export default function DocumentsScreen() { downloadDocumentLegacy(doc.mimetype, doc.title, doc.url)} // downloadDocumentByUrl(doc.url, doc.title) handleDownloadAndShare(doc.mimetype, doc.title, doc.url) + onPress={() => downloadAndShareDocument(doc.mimetype, doc.title, doc.url)} className="p-4 bg-gray-50 rounded-2xl active:bg-gray-100"> diff --git a/app/(protected)/sites/[id].tsx b/app/(protected)/sites/[id].tsx index 1e951c3..4184553 100644 --- a/app/(protected)/sites/[id].tsx +++ b/app/(protected)/sites/[id].tsx @@ -55,7 +55,7 @@ export default function SiteDocumentsScreen() { useEffect(() => { if (params.id) { - setIsLoading(true); // Caricamento iniziale + setIsLoading(true); fetchSiteDocuments(Number(params.id)); } }, [params.id, fetchSiteDocuments]); @@ -107,11 +107,10 @@ export default function SiteDocumentsScreen() { const handleUploadDocument = async (file: any, customTitle?: string) => { setIsUploading(true); try { - const response = await uploadDocument(file, Number(params.id), customTitle); - // console.log('Risposta caricamento:', response.data); - // Alert.alert('Successo', 'Documento caricato con successo!'); - // setShowUploadModal(false); - // fetchSiteDocuments(Number(params.id)); // Ricarica la lista dei documenti + 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.'); diff --git a/app/login.tsx b/app/login.tsx index 8abb513..5296bf9 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -45,7 +45,8 @@ export default function LoginScreen() { if (error.response) { // Errore dal server (es. 401 Credenziali errate) if (error.response.status === 401) { - message = "Credenziali non valide."; + // TODO: Alert o Toast specifico per credenziali errate + message = "Credenziali non valide." } else { message = `Errore Server: ${error.response.data.message || error.response.status}`; } diff --git a/assets/images/mariani-icon.png b/assets/images/mariani-icon.png new file mode 100644 index 0000000..e37b66f Binary files /dev/null and b/assets/images/mariani-icon.png differ diff --git a/assets/images/mariani-splash.png b/assets/images/mariani-splash.png new file mode 100644 index 0000000..31c90f4 Binary files /dev/null and b/assets/images/mariani-splash.png differ diff --git a/components/AddDocumentModal.tsx b/components/AddDocumentModal.tsx index 00a4a0b..e275167 100644 --- a/components/AddDocumentModal.tsx +++ b/components/AddDocumentModal.tsx @@ -13,12 +13,14 @@ interface AddDocumentModalProps { export default function AddDocumentModal({ visible, onClose, onUpload, isUploading = false }: AddDocumentModalProps) { const [selectedFile, setSelectedFile] = useState(null); const [customTitle, setCustomTitle] = useState(''); + const [fileExtension, setFileExtension] = useState(''); // Reset dello stato quando il modale si apre/chiude useEffect(() => { if (!visible) { setSelectedFile(null); setCustomTitle(''); + setFileExtension(''); } }, [visible]); @@ -34,10 +36,16 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi const asset = result.assets[0]; setSelectedFile(asset); - - // Pre-compila il titolo con il nome del file - setCustomTitle(asset.name); - + + const lastDotIndex = asset.name.lastIndexOf('.'); + if (lastDotIndex !== -1) { + setCustomTitle(asset.name.substring(0, lastDotIndex)); + setFileExtension(asset.name.substring(lastDotIndex)); + } else { + setCustomTitle(asset.name); + setFileExtension(''); + } + } catch (err) { console.error("Errore selezione file:", err); } @@ -45,13 +53,16 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi const handleUpload = () => { if (!selectedFile) return; + const fullTitle = customTitle ? `${customTitle}${fileExtension}` : selectedFile.name; + // Se il titolo custom è vuoto, usiamo il nome originale - onUpload(selectedFile, customTitle || selectedFile.name); + onUpload(selectedFile, fullTitle); }; const removeFile = () => { setSelectedFile(null); setCustomTitle(''); + setFileExtension(''); }; // Formatta dimensione file @@ -73,7 +84,7 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi > - + {/* Header */} Carica Documento @@ -84,10 +95,10 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi {/* Body */} - + {/* Area Selezione File */} {!selectedFile ? ( - @@ -120,20 +131,25 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi {/* Campo Rinomina (Visibile solo se c'è un file) */} {selectedFile && ( - Nome Documento (Opzionale) - + Rinomina File + + + + {fileExtension} + + )} {/* Footer Buttons */} - Annulla - void; } -export default function CalendarWidget({ events, types }: CalendarWidgetProps) { +export default function CalendarWidget({ events, types, onMonthChange }: CalendarWidgetProps) { const [currentDate, setCurrentDate] = useState(new Date()); // Helpers per il calendario @@ -21,8 +22,17 @@ export default function CalendarWidget({ events, types }: CalendarWidgetProps) { const changeMonth = (increment: number) => { const newDate = new Date(currentDate.setMonth(currentDate.getMonth() + increment)); setCurrentDate(new Date(newDate)); + if (onMonthChange) { + onMonthChange(newDate); + } }; + useEffect(() => { + if (onMonthChange) { + onMonthChange(currentDate); + } + }, []); + const getEventForDay = (day: number) => { const year = currentDate.getFullYear(); const month = String(currentDate.getMonth() + 1).padStart(2, '0'); diff --git a/components/NfcScanModal.tsx b/components/NfcScanModal.tsx new file mode 100644 index 0000000..cddced9 --- /dev/null +++ b/components/NfcScanModal.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useRef } from 'react'; +import { Modal, Text, TouchableOpacity, View, Animated, Easing, Vibration, Alert } from 'react-native'; +import { X, Radio, SmartphoneNfc } from 'lucide-react-native'; +import NfcManager, { NfcTech } from 'react-native-nfc-manager'; + +interface NfcScanModalProps { + visible: boolean; + onClose: () => void; + onScan: (data: string) => void; +} + +export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalProps) { + // Animazione per l'effetto "pulsante" (Breathing) + const scaleAnim = useRef(new Animated.Value(1)).current; + const opacityAnim = useRef(new Animated.Value(0.3)).current; + + const readNfcTag = async () => { + const supported = await NfcManager.isSupported(); + const nfcScanning = await NfcManager.isEnabled(); + + if (!supported || !nfcScanning) { + onClose(); + return; + } + + NfcManager.start(); + + try { + await NfcManager.requestTechnology(NfcTech.Ndef); + const tag = await NfcManager.getTag(); + + if (tag) { + console.log('NFC Tag Found:', tag); + Vibration.vibrate(); + // onScan(tag.id || JSON.stringify(tag)); + onClose(); + } else { + console.warn('Tag NFC vuoto o non formattato NDEF'); + } + } catch (error) { + console.warn('Error reading NFC tag', error); + } finally { + NfcManager.cancelTechnologyRequest(); + } + }; + + // Loop infinito di espansione e contrazione + const animationLoop = () => { + Animated.loop( + Animated.parallel([ + Animated.sequence([ + Animated.timing(scaleAnim, { + toValue: 1.2, + duration: 1500, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + Animated.timing(scaleAnim, { + toValue: 1, + duration: 1500, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + ]), + Animated.sequence([ + Animated.timing(opacityAnim, { + toValue: 0.1, + duration: 1500, + useNativeDriver: true, + }), + Animated.timing(opacityAnim, { + toValue: 0.3, + duration: 1500, + useNativeDriver: true, + }), + ]) + ]) + ).start(); + } + + useEffect(() => { + if (visible) { + animationLoop(); + readNfcTag(); + } else { + scaleAnim.setValue(1); + opacityAnim.setValue(0.3); + } + }, [visible]); + + return ( + + + + {/* Card Principale */} + + + {/* Bottone Chiudi (Alto Destra) */} + + + + + {/* Area Animata NFC */} + + {/* Cerchio Pulsante (Sfondo) */} + + + {/* Cerchio Fisso (Primo piano) */} + + + + + + {/* Testi */} + + Pronto alla scansione + + + + Avvicina il retro del tuo smartphone al Tag NFC per registrare la presenza. + + + {/* Indicatore Visivo (Simulazione onde) */} + + + + Ricerca in corso... + + + + {/* Footer Button */} + + Annulla + + + + + + ); +} \ No newline at end of file diff --git a/components/QrScanModal.tsx b/components/QrScanModal.tsx index cd66a8e..9da78ef 100644 --- a/components/QrScanModal.tsx +++ b/components/QrScanModal.tsx @@ -1,40 +1,99 @@ -import React from 'react'; -import { View, Text, Modal, TouchableOpacity } from 'react-native'; -import { QrCode } from 'lucide-react-native'; +import React, { useState, useEffect } from 'react'; +import { View, Text, Modal, TouchableOpacity, Vibration, StyleSheet } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { CameraView, useCameraPermissions } from 'expo-camera'; +import { X, ScanLine } from 'lucide-react-native'; interface QrScanModalProps { visible: boolean; onClose: () => void; + onScan: (data: string) => void; } -export default function QrScanModal({ visible, onClose }: QrScanModalProps) { +export default function QrScanModal({ visible, onClose, onScan }: QrScanModalProps) { + const [permission, requestPermission] = useCameraPermissions(); + const [scanned, setScanned] = useState(false); + + // Gestione Permessi e Reset Stato + useEffect(() => { + if (visible) { + setScanned(false); + if (permission && !permission.granted && permission.canAskAgain) { + requestPermission(); + } + } + }, [visible, permission]); + + const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => { + if (scanned) return; + setScanned(true); + Vibration.vibrate(); + console.log(`Bar code with type ${type} and data ${data} has been scanned!`); + onScan(data); + onClose(); + }; + + if (!permission) { + return ; + } + + if (!permission.granted && visible) { + requestPermission(); + } + return ( - - - - Scansione in corso... - - Inquadra il codice QR nel riquadro sottostante - + + {/* Camera Full Screen */} + - {/* Viewfinder Simulata */} - - - Camera Feed + {/* Overlay Oscuro con "buco" trasparente (Simulato visivamente con bordi o opacity) */} + + + {/* Header Overlay */} + + Scansiona QR Code + Inquadra il codice nel riquadro + + + {/* Area Centrale (Trasparente per la camera) */} + + + + {/* Angoli decorativi */} + + + + + + {/* Linea scansione animata o icona */} + {!scanned && } + + - - Annulla - + {/* Footer Overlay */} + + + + + Chiudi + diff --git a/data/data.ts b/data/data.ts index c8131ac..3769ed1 100644 --- a/data/data.ts +++ b/data/data.ts @@ -1,18 +1,4 @@ -import { UserData, AttendanceRecord, DocumentItem, OfficeItem } from '@/types/types'; - -// --- MOCK DATA (File: data.ts) --- -export const ATTENDANCE_DATA: AttendanceRecord[] = [ - { id: 1, site: "Cantiere Ospedale A.", date: "03/12/2025", in: "08:00", out: "17:00", status: "complete" }, - { id: 2, site: "Uffici Centrali", date: "02/12/2025", in: "08:15", out: "17:15", status: "complete" }, - { id: 3, site: "Residenza Parco", date: "01/12/2025", in: "08:00", out: null, status: "incomplete" }, -]; - -export const DOCUMENTS_DATA: DocumentItem[] = [ - { id: 1, name: "Schema Elettrico Piano 1", type: "PDF", site: "Cantiere Ospedale A.", date: "01/12/2025" }, - { id: 2, name: "Modulo Sicurezza v2", type: "PDF", site: "Generale", date: "28/11/2025" }, - { id: 3, name: "Certificazione Impianto", type: "PDF", site: "Residenza Parco", date: "25/11/2025" }, - { id: 4, name: "Manuale Domotica", type: "PDF", site: "Uffici Centrali", date: "20/11/2025" }, -]; +import { OfficeItem } from '@/types/types'; export const OFFICES_DATA: OfficeItem[] = [ { id: 1, name: "Ufficio Tecnico", status: "online", temp: 22, lights: true, power: 450 }, diff --git a/package-lock.json b/package-lock.json index 15cd737..9f81161 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ "@react-navigation/native": "^7.1.8", "axios": "^1.13.2", "expo": "~54.0.25", + "expo-camera": "~17.0.10", "expo-constants": "~18.0.10", + "expo-dev-client": "~6.0.20", "expo-document-picker": "~14.0.8", "expo-file-system": "~19.0.21", "expo-font": "~14.0.9", @@ -37,7 +39,8 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", - "react-native-reanimated": "~3.17.4", + "react-native-nfc-manager": "^3.17.2", + "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-ui-datepicker": "^3.1.2", @@ -6430,6 +6433,26 @@ "react-native": "*" } }, + "node_modules/expo-camera": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-17.0.10.tgz", + "integrity": "sha512-w1RBw83mAGVk4BPPwNrCZyFop0VLiVSRE3c2V9onWbdFwonpRhzmB4drygG8YOUTl1H3wQvALJHyMPTbgsK1Jg==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, "node_modules/expo-constants": { "version": "18.0.11", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.11.tgz", @@ -6445,6 +6468,79 @@ "react-native": "*" } }, + "node_modules/expo-dev-client": { + "version": "6.0.20", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.20.tgz", + "integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==", + "license": "MIT", + "dependencies": { + "expo-dev-launcher": "6.0.20", + "expo-dev-menu": "7.0.18", + "expo-dev-menu-interface": "2.0.0", + "expo-manifests": "~1.0.10", + "expo-updates-interface": "~2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "6.0.20", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz", + "integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "expo-dev-menu": "7.0.18", + "expo-manifests": "~1.0.10" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/expo-dev-launcher/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/expo-dev-menu": { + "version": "7.0.18", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz", + "integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==", + "license": "MIT", + "dependencies": { + "expo-dev-menu-interface": "2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz", + "integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-document-picker": { "version": "14.0.8", "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz", @@ -6505,6 +6601,12 @@ } } }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" + }, "node_modules/expo-keep-awake": { "version": "15.0.8", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz", @@ -6543,6 +6645,19 @@ "react-native": "*" } }, + "node_modules/expo-manifests": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.10.tgz", + "integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==", + "license": "MIT", + "dependencies": { + "@expo/config": "~12.0.11", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "3.0.23", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.23.tgz", @@ -6910,6 +7025,15 @@ } } }, + "node_modules/expo-updates-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz", + "integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-web-browser": { "version": "15.0.10", "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.10.tgz", @@ -7144,6 +7268,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -11380,40 +11520,47 @@ "react-native": "*" } }, + "node_modules/react-native-nfc-manager": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/react-native-nfc-manager/-/react-native-nfc-manager-3.17.2.tgz", + "integrity": "sha512-0NryP/Iw2hzw4MVH5KCngoRerNUrnRok6VfLrlFcFZRKyTQ7KTgpsdDxCB6cR33qYNyEDrWGBayfAI+ym5gt8Q==", + "license": "MIT", + "peerDependencies": { + "@expo/config-plugins": "*" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + } + } + }, "node_modules/react-native-reanimated": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz", - "integrity": "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz", + "integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==", "license": "MIT", "peer": true, "dependencies": { - "@babel/plugin-transform-arrow-functions": "^7.0.0-0", - "@babel/plugin-transform-class-properties": "^7.0.0-0", - "@babel/plugin-transform-classes": "^7.0.0-0", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", - "@babel/plugin-transform-optional-chaining": "^7.0.0-0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", - "@babel/plugin-transform-template-literals": "^7.0.0-0", - "@babel/plugin-transform-unicode-regex": "^7.0.0-0", - "@babel/preset-typescript": "^7.16.7", - "convert-source-map": "^2.0.0", - "invariant": "^2.2.4", - "react-native-is-edge-to-edge": "1.1.7" + "react-native-is-edge-to-edge": "^1.2.1", + "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-worklets": ">=0.5.0" } }, - "node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz", - "integrity": "sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" + "node_modules/react-native-reanimated/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/react-native-safe-area-context": { @@ -11517,6 +11664,7 @@ "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", diff --git a/package.json b/package.json index 12cab9f..b71fb4c 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "start": "expo start", "reset-project": "node ./scripts/reset-project.js", - "android": "expo start --android", - "ios": "expo start --ios", + "android": "expo run:android", + "ios": "expo run:ios", "web": "expo start --web", "lint": "expo lint" }, @@ -18,7 +18,9 @@ "@react-navigation/native": "^7.1.8", "axios": "^1.13.2", "expo": "~54.0.25", + "expo-camera": "~17.0.10", "expo-constants": "~18.0.10", + "expo-dev-client": "~6.0.20", "expo-document-picker": "~14.0.8", "expo-file-system": "~19.0.21", "expo-font": "~14.0.9", @@ -40,7 +42,8 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", - "react-native-reanimated": "~3.17.4", + "react-native-nfc-manager": "^3.17.2", + "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-ui-datepicker": "^3.1.2", diff --git a/types/types.ts b/types/types.ts index 0553dfb..c687359 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,5 +1,3 @@ -// --- TYPES & export INTERFACES (File: types.ts) --- - import { DateType } from "react-native-ui-datepicker"; export interface UserData { @@ -11,13 +9,22 @@ export interface UserData { id: string; } +export interface ActivityItem { + id: string; + type: 'document' | 'attendance' | 'permit'; + title: string; + subtitle: string; + date_display: string; +} + export interface AttendanceRecord { id: number; site: string; + subactivity: string; date: string; in: string; out: string | null; - status: 'complete' | 'incomplete'; + time: number | null; } export interface DocumentItem { @@ -55,7 +62,7 @@ export interface TimeOffRequest { start_time?: string | null; end_time?: string | null; message?: string | null; - status: number; + status: number; timeOffRequestType: TimeOffRequestType; } diff --git a/utils/dateTime.ts b/utils/dateTime.ts index fae36ea..728ad08 100644 --- a/utils/dateTime.ts +++ b/utils/dateTime.ts @@ -72,4 +72,18 @@ export const parseTimestamp = (dateStr: string | undefined | null): Date => { const date = new Date(dateStr); if (isNaN(date.getTime())) return new Date(); return date; -}; \ No newline at end of file +}; + +export const parseSecondsToTime = (totalSeconds: number | null | undefined): string => { + if (totalSeconds == null || isNaN(totalSeconds)) return ''; + + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const hh = String(hours); + const mm = String(minutes).padStart(2, '0'); + const ss = String(seconds).padStart(2, '0'); + + return `${hh}h`; +} \ No newline at end of file diff --git a/utils/documentUtils.tsx b/utils/documentUtils.tsx index 19dfab3..14b3d75 100644 --- a/utils/documentUtils.tsx +++ b/utils/documentUtils.tsx @@ -7,7 +7,7 @@ import * as Linking from 'expo-linking'; import { Platform } from 'react-native'; /** - * Gestisce l'upload di un documento verso il server usando Expo FileSystem + * Gestisce l'upload di un documento verso il server usando FormData * @param file File da caricare (deve avere almeno la proprietà 'uri') * @param siteId ID del sito a cui associare il documento (null per registro generale) * @param customTitle Titolo personalizzato per il documento (opzionale) @@ -21,13 +21,46 @@ export const uploadDocument = async ( throw new Error("File non valido per l'upload."); } - if (siteId === null) { - console.log("Uploading document:", file, "to registry with title:", customTitle); - } else { - console.log("Uploading document:", file, "to site:", siteId, "with title:", customTitle); - } + try { + const formData = new FormData(); + formData.append('file', { + uri: file.uri, + name: customTitle || file.name, + type: file.mimeType + } as any); - // TODO: Funzione di upload (manca lato backend) + if (siteId !== null) { + formData.append('siteId', siteId.toString()); + } + + if (customTitle) { + formData.append('customTitle', customTitle.trim()); + } + + const response = await api.post('/attachment/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + } + }); + + console.log("Risposta server:", response.data); + + if (response.data?.status === 'error') { + throw new Error(response.data.message || "Errore sconosciuto dal server"); + } + + } catch (error: any) { + console.error("Errore durante l'upload del documento:", error); + + if (error.response) { + const serverMessage = error.response.data?.message || error.message; + throw new Error(`Errore Server (${error.response.status}): ${serverMessage}`); + } else if (error.request) { + throw new Error("Il server non risponde. Controlla la connessione."); + } else { + throw error; + } + } }; /** @@ -74,76 +107,3 @@ export const downloadAndShareDocument = async ( throw error; } }; - -// TODO: Test con versione legacy di FileSystem e SAF -export const downloadDocumentLegacy = async ( - mimetype: string, - fileName: string, - fileUrl: string -): Promise => { - try { - if (!fileUrl || !fileName) { - throw new Error("Parametri mancanti per il download del documento."); - } - - const path = FileSystem.cacheDirectory + 'documents/'; - // Download del file nella directory selezionata - const tmpFile = await FileSystem.downloadAsync(fileUrl, path + fileName); - console.log("File temporaneo scaricato in:", tmpFile.uri); - - if (Platform.OS === 'android') { - const permission = await StorageAccessFramework.requestDirectoryPermissionsAsync(); - if (permission.granted) { - // Gets SAF URI from response - const safUri = permission.directoryUri; - console.log("Selected Directory URI:", safUri); - - const fileContent = await FileSystem.readAsStringAsync(tmpFile.uri, { encoding: FileSystem.EncodingType.Base64 }); - console.log("File letto in Base64, dimensione:", fileContent.length); - - // Copia il file nella directory SAF selezionata - const destUri = await StorageAccessFramework.createFileAsync( - safUri, - fileName, - mimetype - ); - console.log("Destinazione SAF URI:", destUri); - - await FileSystem.writeAsStringAsync(destUri, fileContent, { encoding: FileSystem.EncodingType.Base64 }); - console.log("File riscritto in SAF."); - } - } else if (Platform.OS === 'ios') { - if (await Sharing.isAvailableAsync()) { - await Sharing.shareAsync(tmpFile.uri, { - mimeType: mimetype, - dialogTitle: `Scarica ${fileName}`, - UTI: 'public.item' - }); - } else { - throw new Error("Condivisione non supportata su questo dispositivo."); - } - } - - } catch (error) { - console.error("Download Error:", error); - throw error; - } -}; - -// TODO: Test con Linking standard -export const downloadDocumentByUrl = async ( - fileUrl: string, - fileName: string -): Promise => { - try { - if (!fileUrl || !fileName) { - throw new Error("Parametri mancanti per il download del documento."); - } - - Linking.openURL(fileUrl); - - } catch (error) { - console.error("Download Error:", error); - throw error; - } -}; \ No newline at end of file