From 44d021891f2dbe9524f25d73752c07ed9e6807ab Mon Sep 17 00:00:00 2001 From: leonardo Date: Mon, 19 Jan 2026 18:10:31 +0100 Subject: [PATCH] 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. --- app.json | 26 +++- app/(protected)/_layout.tsx | 7 +- app/(protected)/attendance/index.tsx | 210 ++++++++++++++++++++------ app/(protected)/index.tsx | 159 ++++++++++++++----- app/(protected)/permits/index.tsx | 35 ++++- app/(protected)/profile/documents.tsx | 23 +-- app/(protected)/sites/[id].tsx | 11 +- app/login.tsx | 3 +- assets/images/mariani-icon.png | Bin 0 -> 57139 bytes assets/images/mariani-splash.png | Bin 0 -> 4230 bytes components/AddDocumentModal.tsx | 50 +++--- components/CalendarWidget.tsx | 14 +- components/NfcScanModal.tsx | 162 ++++++++++++++++++++ components/QrScanModal.tsx | 107 ++++++++++--- data/data.ts | 16 +- package-lock.json | 198 +++++++++++++++++++++--- package.json | 9 +- types/types.ts | 15 +- utils/dateTime.ts | 16 +- utils/documentUtils.tsx | 120 +++++---------- 20 files changed, 882 insertions(+), 299 deletions(-) create mode 100644 assets/images/mariani-icon.png create mode 100644 assets/images/mariani-splash.png create mode 100644 components/NfcScanModal.tsx 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 0000000000000000000000000000000000000000..e37b66fc846c346c1101f24196bb8ee49422f108 GIT binary patch literal 57139 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 literal 0 HcmV?d00001 diff --git a/assets/images/mariani-splash.png b/assets/images/mariani-splash.png new file mode 100644 index 0000000000000000000000000000000000000000..31c90f4c21aa1954b28939e691cb84bad22740f4 GIT binary patch literal 4230 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 literal 0 HcmV?d00001 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