feat: Implement alert system and network connectivity handling, refactor error handling across screens
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@ -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]
|
||||||
@ -1,5 +1,6 @@
|
|||||||
|
import { useAlert } from '@/components/AlertComponent';
|
||||||
import React, { useEffect, useState } from 'react';
|
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 { QrCode, CheckCircle2, Nfc } from 'lucide-react-native';
|
||||||
import QrScanModal from '@/components/QrScanModal';
|
import QrScanModal from '@/components/QrScanModal';
|
||||||
import NfcScanModal from '@/components/NfcScanModal';
|
import NfcScanModal from '@/components/NfcScanModal';
|
||||||
@ -11,6 +12,7 @@ import NfcManager from 'react-native-nfc-manager';
|
|||||||
|
|
||||||
export default function AttendanceScreen() {
|
export default function AttendanceScreen() {
|
||||||
const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr');
|
const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr');
|
||||||
|
const alert = useAlert();
|
||||||
const [showScanner, setShowScanner] = useState(false);
|
const [showScanner, setShowScanner] = useState(false);
|
||||||
const [lastScan, setLastScan] = useState<{ type: string; time: string; site: string } | null>(null);
|
const [lastScan, setLastScan] = useState<{ type: string; time: string; site: string } | null>(null);
|
||||||
const [attendances, setAttendances] = useState<AttendanceRecord[]>([]);
|
const [attendances, setAttendances] = useState<AttendanceRecord[]>([]);
|
||||||
@ -36,7 +38,7 @@ export default function AttendanceScreen() {
|
|||||||
setAttendances(response.data);
|
setAttendances(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel recupero delle presenze:', 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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
@ -67,23 +69,20 @@ export default function AttendanceScreen() {
|
|||||||
try {
|
try {
|
||||||
const supported = await NfcManager.isSupported();
|
const supported = await NfcManager.isSupported();
|
||||||
if (!supported) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabled = await NfcManager.isEnabled();
|
const enabled = await NfcManager.isEnabled();
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
Alert.alert('NFC disattivato', 'Per favore attiva l\'NFC nelle impostazioni del dispositivo per continuare.', [
|
alert.showAlert('warning', 'NFC disattivato', 'Per favore attiva l\'NFC nelle impostazioni del dispositivo per continuare.');
|
||||||
{ text: 'OK' },
|
|
||||||
{ text: 'Vai alle impostazioni', onPress: () => NfcManager.goToNfcSetting() }
|
|
||||||
]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowScanner(true);
|
setShowScanner(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(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) {
|
} catch (error) {
|
||||||
console.error('Errore nell\'invio dei dati di scansione:', 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;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
import { useAlert } from '@/components/AlertComponent';
|
||||||
import React, { JSX, useEffect, useMemo, useState } from 'react';
|
import React, { JSX, useEffect, useMemo, useState } from 'react';
|
||||||
import { Calendar as CalendarIcon, CalendarX, Clock, Plus, Thermometer } from 'lucide-react-native';
|
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 { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
||||||
import RequestPermitModal from '@/components/RequestPermitModal';
|
import RequestPermitModal from '@/components/RequestPermitModal';
|
||||||
import CalendarWidget from '@/components/CalendarWidget';
|
import CalendarWidget from '@/components/CalendarWidget';
|
||||||
@ -18,6 +19,7 @@ const typeIcons: Record<string, (color: string) => JSX.Element> = {
|
|||||||
|
|
||||||
export default function PermitsScreen() {
|
export default function PermitsScreen() {
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const alert = useAlert();
|
||||||
const [permits, setPermits] = useState<TimeOffRequest[]>([]);
|
const [permits, setPermits] = useState<TimeOffRequest[]>([]);
|
||||||
const [types, setTypes] = useState<TimeOffRequestType[]>([]);
|
const [types, setTypes] = useState<TimeOffRequestType[]>([]);
|
||||||
const [currentMonthDate, setCurrentMonthDate] = useState(new Date());
|
const [currentMonthDate, setCurrentMonthDate] = useState(new Date());
|
||||||
@ -37,7 +39,7 @@ export default function PermitsScreen() {
|
|||||||
setTypes(typesResponse.data);
|
setTypes(typesResponse.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel recupero dei permessi:', 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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
|
import { useAlert } from '@/components/AlertComponent';
|
||||||
import { ArrowLeft, Download, FileText, MapPin, Plus, Search, CalendarIcon } from 'lucide-react-native';
|
import { ArrowLeft, Download, FileText, MapPin, Plus, Search, CalendarIcon } from 'lucide-react-native';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { RangePickerModal } from '@/components/RangePickerModal';
|
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 { DocumentItem } from '@/types/types';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@ -13,6 +14,7 @@ import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils'
|
|||||||
|
|
||||||
export default function DocumentsScreen() {
|
export default function DocumentsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const alert = useAlert();
|
||||||
const [documents, setDocuments] = useState<DocumentItem[]>([]);
|
const [documents, setDocuments] = useState<DocumentItem[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
@ -35,7 +37,7 @@ export default function DocumentsScreen() {
|
|||||||
setDocuments(response.data);
|
setDocuments(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel recupero dei documenti utente:', 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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
@ -83,12 +85,12 @@ export default function DocumentsScreen() {
|
|||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
await uploadDocument(file, null, customTitle);
|
await uploadDocument(file, null, customTitle);
|
||||||
Alert.alert('Successo', 'Documento caricato con successo!');
|
alert.showAlert('success', 'Successo', 'Documento caricato con successo!');
|
||||||
setShowUploadModal(false);
|
setShowUploadModal(false);
|
||||||
fetchUserDocuments();
|
fetchUserDocuments();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel caricamento del documento:', 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 {
|
} finally {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
|
import { useAlert } from '@/components/AlertComponent';
|
||||||
import { Download, FileText, Plus, Search, Calendar as CalendarIcon, ChevronLeft } from 'lucide-react-native';
|
import { Download, FileText, Plus, Search, Calendar as CalendarIcon, ChevronLeft } from 'lucide-react-native';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { RangePickerModal } from '@/components/RangePickerModal';
|
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 { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import { ConstructionSite, DocumentItem } from '@/types/types';
|
import { ConstructionSite, DocumentItem } from '@/types/types';
|
||||||
import LoadingScreen from '@/components/LoadingScreen';
|
import LoadingScreen from '@/components/LoadingScreen';
|
||||||
@ -13,6 +14,7 @@ import AddDocumentModal from '@/components/AddDocumentModal';
|
|||||||
|
|
||||||
export default function SiteDocumentsScreen() {
|
export default function SiteDocumentsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const alert = useAlert();
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
const [site, setSite] = useState<ConstructionSite | null>(null);
|
const [site, setSite] = useState<ConstructionSite | null>(null);
|
||||||
const [documents, setDocuments] = useState<DocumentItem[]>([]);
|
const [documents, setDocuments] = useState<DocumentItem[]>([]);
|
||||||
@ -34,7 +36,7 @@ export default function SiteDocumentsScreen() {
|
|||||||
setDocuments(response.data);
|
setDocuments(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel recupero dei documenti del cantiere:', 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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
@ -99,7 +101,7 @@ export default function SiteDocumentsScreen() {
|
|||||||
await downloadAndShareDocument(mimetype, fileName, fileUrl);
|
await downloadAndShareDocument(mimetype, fileName, fileUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel download/condivisione del documento:', 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);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
await uploadDocument(file, Number(params.id), customTitle);
|
await uploadDocument(file, Number(params.id), customTitle);
|
||||||
Alert.alert('Successo', 'Documento caricato con successo!');
|
alert.showAlert('success', 'Successo', 'Documento caricato con successo!');
|
||||||
setShowUploadModal(false);
|
setShowUploadModal(false);
|
||||||
fetchSiteDocuments(Number(params.id), true);
|
fetchSiteDocuments(Number(params.id), true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel caricamento del documento:', 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 {
|
} finally {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { Building2, ChevronRight, MapPin, Search } from 'lucide-react-native';
|
import { Building2, ChevronRight, MapPin, Search } from 'lucide-react-native';
|
||||||
import React, { useEffect, useState } from 'react';
|
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 { useRouter } from 'expo-router';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import LoadingScreen from '@/components/LoadingScreen';
|
import LoadingScreen from '@/components/LoadingScreen';
|
||||||
import { ConstructionSite } from '@/types/types';
|
import { ConstructionSite } from '@/types/types';
|
||||||
|
import { useAlert } from '@/components/AlertComponent';
|
||||||
|
|
||||||
export default function SitesScreen() {
|
export default function SitesScreen() {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@ -12,6 +13,7 @@ export default function SitesScreen() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const alert = useAlert();
|
||||||
|
|
||||||
// Fetch cantieri e documenti
|
// Fetch cantieri e documenti
|
||||||
const fetchConstructionSites = async () => {
|
const fetchConstructionSites = async () => {
|
||||||
@ -23,7 +25,7 @@ export default function SitesScreen() {
|
|||||||
setConstructionSites(response.data);
|
setConstructionSites(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel recupero dei cantieri:', 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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
import '../global.css';
|
import '../global.css';
|
||||||
import { AuthProvider } from '@/utils/authContext';
|
import { AuthProvider } from '@/utils/authContext';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
|
import { AlertProvider } from '@/components/AlertComponent';
|
||||||
|
import { NetworkProvider } from '@/utils/networkProvider';
|
||||||
|
|
||||||
export default function AppLayout() {
|
export default function AppLayout() {
|
||||||
return (
|
return (
|
||||||
|
<NetworkProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<AlertProvider>
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="(protected)" />
|
<Stack.Screen name="(protected)" />
|
||||||
<Stack.Screen name="login" />
|
<Stack.Screen name="login" />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</AlertProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</NetworkProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import React, { useState, useContext } from 'react';
|
import { useAlert } from '@/components/AlertComponent';
|
||||||
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 api from '@/utils/api';
|
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() {
|
export default function LoginScreen() {
|
||||||
|
const alert = useAlert();
|
||||||
const authContext = useContext(AuthContext);
|
const authContext = useContext(AuthContext);
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
@ -15,7 +17,7 @@ export default function LoginScreen() {
|
|||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
// TODO: Implementa toast o messaggio di errore più user-friendly
|
// TODO: Implementa toast o messaggio di errore più user-friendly
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
Alert.alert("Attenzione", "Inserisci username e password");
|
alert.showAlert('error', 'Attenzione', 'Inserisci username e password');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ export default function LoginScreen() {
|
|||||||
message = "Impossibile contattare il server. Controlla la connessione.";
|
message = "Impossibile contattare il server. Controlla la connessione.";
|
||||||
}
|
}
|
||||||
|
|
||||||
Alert.alert("Login Fallito", message);
|
alert.showAlert('error', "Login Fallito", message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
113
components/AlertComponent.tsx
Normal file
113
components/AlertComponent.tsx
Normal file
@ -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<AlertContextData>({} 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<AlertType>('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 (
|
||||||
|
<AlertContext.Provider value={{ showAlert, hideAlert }}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{/* IL COMPONENTE ALERT MODALE */}
|
||||||
|
<Modal
|
||||||
|
transparent
|
||||||
|
visible={visible}
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={hideAlert}
|
||||||
|
>
|
||||||
|
{/* Backdrop scuro */}
|
||||||
|
<TouchableOpacity
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={hideAlert} // Chiude se clicchi fuori (opzionale)
|
||||||
|
className="flex-1 bg-black/60 justify-center items-center px-6"
|
||||||
|
>
|
||||||
|
{/* Contenitore Alert */}
|
||||||
|
<TouchableWithoutFeedback>
|
||||||
|
<View className="bg-white w-full max-w-sm rounded-3xl p-6 items-center shadow-2xl">
|
||||||
|
|
||||||
|
{/* Icona Cerchiata */}
|
||||||
|
<View className={`${bgColor} p-4 rounded-full mb-4`}>
|
||||||
|
<Icon size={32} className={color} strokeWidth={2.5} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Testi */}
|
||||||
|
<Text className="text-xl font-bold text-gray-900 text-center mb-2">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="text-lg text-gray-500 text-center leading-relaxed mb-8">
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Pulsante OK */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={hideAlert}
|
||||||
|
className={`w-full py-3.5 rounded-xl ${btnColor} active:opacity-90 shadow-sm`}
|
||||||
|
>
|
||||||
|
<Text className="text-white text-center font-bold text-lg">
|
||||||
|
Ok, ho capito
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
</AlertContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAlert = () => useContext(AlertContext);
|
||||||
48
components/OfflineScreen.tsx
Normal file
48
components/OfflineScreen.tsx
Normal file
@ -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 (
|
||||||
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
|
<View className="flex-1 items-center justify-center px-8">
|
||||||
|
{/* Icona con cerchio di sfondo */}
|
||||||
|
<View className="bg-gray-100 p-6 rounded-full mb-6">
|
||||||
|
<WifiOff size={64} className="text-gray-400" />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text className="text-2xl font-bold text-gray-800 mb-2 text-center">
|
||||||
|
Sei Offline
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="text-base text-gray-500 text-center mb-10 leading-6">
|
||||||
|
Sembra che non ci sia connessione a internet.{'\n'}Controlla il Wi-Fi o i dati mobili e riprova.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Pulsante Riprova */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onRetry}
|
||||||
|
disabled={isRetrying}
|
||||||
|
className={`flex-row items-center justify-center w-full py-4 rounded-xl gap-4 ${
|
||||||
|
isRetrying ? 'bg-gray-300' : 'bg-[#099499] active:bg-[#077f83]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
size={20}
|
||||||
|
color="white"
|
||||||
|
className={`${isRetrying ? 'animate-spin' : ''}`} // TODO: animate-spin richiede config tailwind o gestiscilo manualmente
|
||||||
|
/>
|
||||||
|
<Text className="text-white font-bold text-lg">
|
||||||
|
{isRetrying ? 'Controllo...' : 'Riprova'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
|
import { useAlert } from '@/components/AlertComponent';
|
||||||
import React, { useState } from 'react';
|
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 DateTimePicker, { DateType, useDefaultStyles } from 'react-native-ui-datepicker';
|
||||||
import { TimeOffRequestType } from '@/types/types';
|
import { TimeOffRequestType } from '@/types/types';
|
||||||
import { X } from 'lucide-react-native';
|
import { X } from 'lucide-react-native';
|
||||||
@ -16,6 +17,7 @@ interface RequestPermitModalProps {
|
|||||||
|
|
||||||
export default function RequestPermitModal({ visible, types, onClose, onSubmit }: RequestPermitModalProps) {
|
export default function RequestPermitModal({ visible, types, onClose, onSubmit }: RequestPermitModalProps) {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
|
const alert = useAlert();
|
||||||
const [type, setType] = useState<TimeOffRequestType>(types[0]); // Default to first type
|
const [type, setType] = useState<TimeOffRequestType>(types[0]); // Default to first type
|
||||||
const [date, setDate] = useState<string | null>();
|
const [date, setDate] = useState<string | null>();
|
||||||
const [range, setRange] = 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);
|
const response = await api.post('/time-off-request/save-request', requestData);
|
||||||
|
|
||||||
if (response.data.status === 'success') {
|
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 {
|
} 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) {
|
} catch (error: any) {
|
||||||
console.error('Errore nell\'invio della richiesta:', error);
|
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
|
// Funzione per inviare la richiesta
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const error = validateRequest(type, date, range, startTime, endTime, message);
|
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 = {
|
const requestData = {
|
||||||
id_type: type.id,
|
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?
|
onSubmit(requestData); // TODO: Gestire risposta e controllare fetch in index?
|
||||||
onClose();
|
onClose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert("Errore", "Impossibile inviare la richiesta.");
|
alert.showAlert("error", "Errore", "Impossibile inviare la richiesta.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@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/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
@ -2861,6 +2862,15 @@
|
|||||||
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
"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": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.81.5",
|
"version": "0.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@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/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
|
|||||||
42
utils/networkProvider.tsx
Normal file
42
utils/networkProvider.tsx
Normal file
@ -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 (
|
||||||
|
<OfflineScreen
|
||||||
|
onRetry={handleManualRetry}
|
||||||
|
isRetrying={isRetrying}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user