diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..256dec3 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +EXPO_PUBLIC_API_URL=[YOUR_API_URL] +EXPO_PUBLIC_HA_API_URL=[YOUR_HOME_ASSISTANT_API_URL] +EXPO_PUBLIC_HA_TOKEN=[YOUR_HOME_ASSISTANT_TOKEN] \ No newline at end of file diff --git a/app/(protected)/attendance/index.tsx b/app/(protected)/attendance/index.tsx index 9af6198..35d24e6 100644 --- a/app/(protected)/attendance/index.tsx +++ b/app/(protected)/attendance/index.tsx @@ -1,5 +1,6 @@ +import { useAlert } from '@/components/AlertComponent'; import React, { useEffect, useState } from 'react'; -import { View, Text, TouchableOpacity, ScrollView, Alert, RefreshControl } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, RefreshControl } from 'react-native'; import { QrCode, CheckCircle2, Nfc } from 'lucide-react-native'; import QrScanModal from '@/components/QrScanModal'; import NfcScanModal from '@/components/NfcScanModal'; @@ -11,6 +12,7 @@ import NfcManager from 'react-native-nfc-manager'; export default function AttendanceScreen() { const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr'); + const alert = useAlert(); const [showScanner, setShowScanner] = useState(false); const [lastScan, setLastScan] = useState<{ type: string; time: string; site: string } | null>(null); const [attendances, setAttendances] = useState([]); @@ -36,7 +38,7 @@ export default function AttendanceScreen() { setAttendances(response.data); } catch (error) { console.error('Errore nel recupero delle presenze:', error); - Alert.alert('Errore', 'Impossibile recuperare le presenze. Riprova più tardi.'); + alert.showAlert('error', 'Errore', 'Impossibile recuperare le presenze. Riprova più tardi.'); } finally { setIsLoading(false); setRefreshing(false); @@ -67,23 +69,20 @@ export default function AttendanceScreen() { try { const supported = await NfcManager.isSupported(); if (!supported) { - Alert.alert('NFC non supportato', 'Il tuo dispositivo non supporta la scansione NFC.'); + alert.showAlert('warning', '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() } - ]); + alert.showAlert('warning', 'NFC disattivato', 'Per favore attiva l\'NFC nelle impostazioni del dispositivo per continuare.'); return; } setShowScanner(true); } catch (err) { console.warn(err); - Alert.alert('Errore', 'Impossibile verificare lo stato dell\'NFC.'); + alert.showAlert('error', 'Errore', 'Impossibile verificare lo stato dell\'NFC.'); } } }; @@ -106,7 +105,7 @@ export default function AttendanceScreen() { } } catch (error) { console.error('Errore nell\'invio dei dati di scansione:', error); - Alert.alert('Errore', 'Impossibile registrare la presenza. Riprova più tardi.'); + alert.showAlert('error', 'Errore', 'Impossibile registrare la presenza. Riprova più tardi.'); return; } }; diff --git a/app/(protected)/permits/index.tsx b/app/(protected)/permits/index.tsx index fc8bf25..af07baf 100644 --- a/app/(protected)/permits/index.tsx +++ b/app/(protected)/permits/index.tsx @@ -1,6 +1,7 @@ +import { useAlert } from '@/components/AlertComponent'; 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 { ScrollView, Text, TouchableOpacity, View, ActivityIndicator, RefreshControl } from 'react-native'; import { TimeOffRequest, TimeOffRequestType } from '@/types/types'; import RequestPermitModal from '@/components/RequestPermitModal'; import CalendarWidget from '@/components/CalendarWidget'; @@ -18,6 +19,7 @@ const typeIcons: Record JSX.Element> = { export default function PermitsScreen() { const [showModal, setShowModal] = useState(false); + const alert = useAlert(); const [permits, setPermits] = useState([]); const [types, setTypes] = useState([]); const [currentMonthDate, setCurrentMonthDate] = useState(new Date()); @@ -37,7 +39,7 @@ export default function PermitsScreen() { setTypes(typesResponse.data); } catch (error) { console.error('Errore nel recupero dei permessi:', error); - Alert.alert('Errore', 'Impossibile recuperare i permessi. Riprova più tardi.'); + alert.showAlert('error', 'Errore', 'Impossibile recuperare i permessi. Riprova più tardi.'); } finally { setIsLoading(false); setRefreshing(false); diff --git a/app/(protected)/profile/documents.tsx b/app/(protected)/profile/documents.tsx index 9a19c30..f59420a 100644 --- a/app/(protected)/profile/documents.tsx +++ b/app/(protected)/profile/documents.tsx @@ -1,8 +1,9 @@ +import { useAlert } from '@/components/AlertComponent'; import { ArrowLeft, Download, FileText, MapPin, Plus, Search, CalendarIcon } from 'lucide-react-native'; import React, { useEffect, useState } from 'react'; import { useRouter } from 'expo-router'; import { RangePickerModal } from '@/components/RangePickerModal'; -import { Alert, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { DocumentItem } from '@/types/types'; import api from '@/utils/api'; import dayjs from 'dayjs'; @@ -13,6 +14,7 @@ import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils' export default function DocumentsScreen() { const router = useRouter(); + const alert = useAlert(); const [documents, setDocuments] = useState([]); const [isLoading, setIsLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -35,7 +37,7 @@ export default function DocumentsScreen() { setDocuments(response.data); } catch (error) { console.error('Errore nel recupero dei documenti utente:', error); - Alert.alert('Errore', 'Impossibile recuperare i documenti. Riprova più tardi.'); + alert.showAlert('error', 'Errore', 'Impossibile recuperare i documenti. Riprova più tardi.'); } finally { setIsLoading(false); setRefreshing(false); @@ -83,12 +85,12 @@ export default function DocumentsScreen() { setIsUploading(true); try { await uploadDocument(file, null, customTitle); - Alert.alert('Successo', 'Documento caricato con successo!'); + alert.showAlert('success', '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.'); + alert.showAlert('error', 'Errore', 'Impossibile caricare il documento. Riprova più tardi.'); } finally { setIsUploading(false); } diff --git a/app/(protected)/sites/[id].tsx b/app/(protected)/sites/[id].tsx index 649f9c3..c951235 100644 --- a/app/(protected)/sites/[id].tsx +++ b/app/(protected)/sites/[id].tsx @@ -1,7 +1,8 @@ +import { useAlert } from '@/components/AlertComponent'; import { Download, FileText, Plus, Search, Calendar as CalendarIcon, ChevronLeft } from 'lucide-react-native'; import React, { useCallback, useEffect, useState } from 'react'; import { RangePickerModal } from '@/components/RangePickerModal'; -import { Alert, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { ConstructionSite, DocumentItem } from '@/types/types'; import LoadingScreen from '@/components/LoadingScreen'; @@ -13,6 +14,7 @@ import AddDocumentModal from '@/components/AddDocumentModal'; export default function SiteDocumentsScreen() { const router = useRouter(); + const alert = useAlert(); const params = useLocalSearchParams(); const [site, setSite] = useState(null); const [documents, setDocuments] = useState([]); @@ -34,7 +36,7 @@ export default function SiteDocumentsScreen() { setDocuments(response.data); } catch (error) { console.error('Errore nel recupero dei documenti del cantiere:', error); - Alert.alert('Errore', 'Impossibile recuperare i documenti del cantiere. Riprova più tardi.'); + alert.showAlert('error', 'Errore', 'Impossibile recuperare i documenti del cantiere. Riprova più tardi.'); } finally { setIsLoading(false); setRefreshing(false); @@ -99,7 +101,7 @@ export default function SiteDocumentsScreen() { await downloadAndShareDocument(mimetype, fileName, fileUrl); } catch (error) { console.error('Errore nel download/condivisione del documento:', error); - Alert.alert('Errore', 'Impossibile scaricare il documento. Riprova più tardi.'); + alert.showAlert('error', 'Errore', 'Impossibile scaricare il documento. Riprova più tardi.'); } }; @@ -108,12 +110,12 @@ export default function SiteDocumentsScreen() { setIsUploading(true); try { await uploadDocument(file, Number(params.id), customTitle); - Alert.alert('Successo', 'Documento caricato con successo!'); + alert.showAlert('success', '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.'); + alert.showAlert('error', 'Errore', 'Impossibile caricare il documento. Riprova più tardi.'); } finally { setIsUploading(false); } diff --git a/app/(protected)/sites/index.tsx b/app/(protected)/sites/index.tsx index f2c5756..41e4684 100644 --- a/app/(protected)/sites/index.tsx +++ b/app/(protected)/sites/index.tsx @@ -1,10 +1,11 @@ import { Building2, ChevronRight, MapPin, Search } from 'lucide-react-native'; import React, { useEffect, useState } from 'react'; -import { Alert, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { useRouter } from 'expo-router'; import api from '@/utils/api'; import LoadingScreen from '@/components/LoadingScreen'; import { ConstructionSite } from '@/types/types'; +import { useAlert } from '@/components/AlertComponent'; export default function SitesScreen() { const [searchTerm, setSearchTerm] = useState(''); @@ -12,6 +13,7 @@ export default function SitesScreen() { const [isLoading, setIsLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const router = useRouter(); + const alert = useAlert(); // Fetch cantieri e documenti const fetchConstructionSites = async () => { @@ -23,7 +25,7 @@ export default function SitesScreen() { setConstructionSites(response.data); } catch (error) { console.error('Errore nel recupero dei cantieri:', error); - Alert.alert('Errore', 'Impossibile recuperare i cantieri. Riprova più tardi.'); + alert.showAlert('error', 'Errore', 'Impossibile recuperare i cantieri. Riprova più tardi.'); } finally { setIsLoading(false); setRefreshing(false); diff --git a/app/_layout.tsx b/app/_layout.tsx index 6a8fe78..edb19b8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,14 +1,20 @@ import '../global.css'; import { AuthProvider } from '@/utils/authContext'; import { Stack } from 'expo-router'; +import { AlertProvider } from '@/components/AlertComponent'; +import { NetworkProvider } from '@/utils/networkProvider'; export default function AppLayout() { return ( - - - - - - + + + + + + + + + + ); } \ No newline at end of file diff --git a/app/login.tsx b/app/login.tsx index 5296bf9..b093933 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -1,10 +1,12 @@ -import React, { useState, useContext } from 'react'; -import { Eye, EyeOff, Lock, LogIn, Mail } from 'lucide-react-native'; -import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View, Image, Alert } from 'react-native'; -import { AuthContext } from '@/utils/authContext'; +import { useAlert } from '@/components/AlertComponent'; import api from '@/utils/api'; +import { AuthContext } from '@/utils/authContext'; +import { Eye, EyeOff, Lock, LogIn, Mail } from 'lucide-react-native'; +import React, { useContext, useState } from 'react'; +import { Image, KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; export default function LoginScreen() { + const alert = useAlert(); const authContext = useContext(AuthContext); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -14,8 +16,8 @@ export default function LoginScreen() { // TODO: Riscrivere funzione per migliorare leggibilità e gestione errori const handleLogin = async () => { // TODO: Implementa toast o messaggio di errore più user-friendly - if (!username || !password) { - Alert.alert("Attenzione", "Inserisci username e password"); + if (!username || !password) { + alert.showAlert('error', 'Attenzione', 'Inserisci username e password'); return; } @@ -55,7 +57,7 @@ export default function LoginScreen() { message = "Impossibile contattare il server. Controlla la connessione."; } - Alert.alert("Login Fallito", message); + alert.showAlert('error', "Login Fallito", message); } finally { setIsLoading(false); } diff --git a/components/AlertComponent.tsx b/components/AlertComponent.tsx new file mode 100644 index 0000000..e6dea51 --- /dev/null +++ b/components/AlertComponent.tsx @@ -0,0 +1,113 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; +import { Modal, View, Text, TouchableOpacity, TouchableWithoutFeedback } from 'react-native'; +import { CheckCircle, XCircle, Info, AlertTriangle } from 'lucide-react-native'; + +type AlertType = 'success' | 'error' | 'info' | 'warning'; + +interface AlertContextData { + showAlert: (type: AlertType, title: string, message: string) => void; + hideAlert: () => void; +} + +const AlertContext = createContext({} as AlertContextData); + +const ALERT_CONFIG = { + success: { + icon: CheckCircle, + color: 'text-green-600', + bgColor: 'bg-green-100', + btnColor: 'bg-green-600', + }, + error: { + icon: XCircle, + color: 'text-red-600', + bgColor: 'bg-red-100', + btnColor: 'bg-red-600', + }, + info: { + icon: Info, + color: 'text-sky-600', + bgColor: 'bg-sky-100', + btnColor: 'bg-sky-600', + }, + warning: { + icon: AlertTriangle, + color: 'text-orange-600', + bgColor: 'bg-orange-100', + btnColor: 'bg-orange-600', + }, +}; + +export const AlertProvider = ({ children }: { children: ReactNode }) => { + const [visible, setVisible] = useState(false); + const [title, setTitle] = useState(''); + const [message, setMessage] = useState(''); + const [type, setType] = useState('info'); + + const showAlert = (newType: AlertType, newTitle: string, newMessage: string) => { + setType(newType); + setTitle(newTitle); + setMessage(newMessage); + setVisible(true); + }; + + const hideAlert = () => { + setVisible(false); + }; + + const { icon: Icon, color, bgColor, btnColor } = ALERT_CONFIG[type]; + + return ( + + {children} + + {/* IL COMPONENTE ALERT MODALE */} + + {/* Backdrop scuro */} + + {/* Contenitore Alert */} + + + + {/* Icona Cerchiata */} + + + + + {/* Testi */} + + {title} + + + + {message} + + + {/* Pulsante OK */} + + + Ok, ho capito + + + + + + + + + ); +}; + +export const useAlert = () => useContext(AlertContext); \ No newline at end of file diff --git a/components/OfflineScreen.tsx b/components/OfflineScreen.tsx new file mode 100644 index 0000000..4ac288e --- /dev/null +++ b/components/OfflineScreen.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { View, Text, TouchableOpacity } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { WifiOff, RefreshCw } from 'lucide-react-native'; + +interface OfflineScreenProps { + onRetry: () => void; + isRetrying?: boolean; +} + +export default function OfflineScreen({ onRetry, isRetrying = false }: OfflineScreenProps) { + return ( + + + {/* Icona con cerchio di sfondo */} + + + + + + Sei Offline + + + + Sembra che non ci sia connessione a internet.{'\n'}Controlla il Wi-Fi o i dati mobili e riprova. + + + {/* Pulsante Riprova */} + + + + {isRetrying ? 'Controllo...' : 'Riprova'} + + + + + ); +} \ No newline at end of file diff --git a/components/RequestPermitModal.tsx b/components/RequestPermitModal.tsx index 408ea8d..09905fc 100644 --- a/components/RequestPermitModal.tsx +++ b/components/RequestPermitModal.tsx @@ -1,5 +1,6 @@ +import { useAlert } from '@/components/AlertComponent'; import React, { useState } from 'react'; -import { View, Text, Modal, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native'; +import { View, Text, Modal, TouchableOpacity, TextInput, ScrollView } from 'react-native'; import DateTimePicker, { DateType, useDefaultStyles } from 'react-native-ui-datepicker'; import { TimeOffRequestType } from '@/types/types'; import { X } from 'lucide-react-native'; @@ -16,6 +17,7 @@ interface RequestPermitModalProps { export default function RequestPermitModal({ visible, types, onClose, onSubmit }: RequestPermitModalProps) { const defaultStyles = useDefaultStyles(); + const alert = useAlert(); const [type, setType] = useState(types[0]); // Default to first type const [date, setDate] = useState(); const [range, setRange] = useState<{ @@ -61,9 +63,9 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } const response = await api.post('/time-off-request/save-request', requestData); if (response.data.status === 'success') { - Alert.alert('Successo', response.data.message || 'La tua richiesta è stata inviata con successo.'); + alert.showAlert('success', 'Successo', response.data.message || 'La tua richiesta è stata inviata con successo.'); } else { - Alert.alert('Errore', response.data.message || 'Impossibile inviare la richiesta.'); + alert.showAlert('error', 'Errore', response.data.message || 'Impossibile inviare la richiesta.'); } } catch (error: any) { console.error('Errore nell\'invio della richiesta:', error); @@ -74,7 +76,10 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } // Funzione per inviare la richiesta const handleSubmit = async () => { const error = validateRequest(type, date, range, startTime, endTime, message); - if (error) return Alert.alert("Errore", error); + if (error) { + alert.showAlert("error", "Errore", error); + return; + } const requestData = { id_type: type.id, @@ -90,7 +95,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } onSubmit(requestData); // TODO: Gestire risposta e controllare fetch in index? onClose(); } catch (e) { - Alert.alert("Errore", "Impossibile inviare la richiesta."); + alert.showAlert("error", "Errore", "Impossibile inviare la richiesta."); } }; diff --git a/package-lock.json b/package-lock.json index 9f81161..43b8b12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-community/netinfo": "11.4.1", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", @@ -2861,6 +2862,15 @@ "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, + "node_modules/@react-native-community/netinfo": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz", + "integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==", + "license": "MIT", + "peerDependencies": { + "react-native": ">=0.59" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", diff --git a/package.json b/package.json index b71fb4c..50bc9f2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "2.2.0", + "@react-native-community/netinfo": "11.4.1", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", diff --git a/utils/networkProvider.tsx b/utils/networkProvider.tsx new file mode 100644 index 0000000..933698a --- /dev/null +++ b/utils/networkProvider.tsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect, ReactNode } from 'react'; +import NetInfo, { useNetInfo } from '@react-native-community/netinfo'; +import OfflineScreen from '@/components/OfflineScreen'; + +interface NetworkProviderProps { + children: ReactNode; +} + +export const NetworkProvider = ({ children }: NetworkProviderProps) => { + const netInfo = useNetInfo(); + const [isOffline, setIsOffline] = useState(false); + const [isRetrying, setIsRetrying] = useState(false); + + useEffect(() => { + if (netInfo.isConnected === false) { + setIsOffline(true); + } else { + setIsOffline(false); + } + }, [netInfo.isConnected]); + + // Manual Retry Handler + const handleManualRetry = async () => { + setIsRetrying(true); + + const state = await NetInfo.fetch(); + setTimeout(() => { + setIsOffline(state.isConnected === false); + setIsRetrying(false); + }, 1000); + }; + + if (isOffline) { + return ( + + ); + } + return <>{children}; +}; \ No newline at end of file