From ef88c518d1d71a88beabcf3c93f2821a51b73ad4 Mon Sep 17 00:00:00 2001 From: leonardo Date: Mon, 15 Dec 2025 17:20:57 +0100 Subject: [PATCH] feat: add and enhance PermitsScreen and RequestPermitModal - add LoadingScreen component - update utilities --- .gitignore | 1 + app/(protected)/_layout.tsx | 1 + app/(protected)/attendance/index.tsx | 4 +- app/(protected)/permits/index.tsx | 12 +- components/CalendarWidget.tsx | 1 + components/LoadingScreen.tsx | 10 ++ components/RequestPermitModal.tsx | 236 ++++++++++++++------------- utils/api.ts | 3 +- utils/dateTime.ts | 20 +++ 9 files changed, 166 insertions(+), 122 deletions(-) create mode 100644 components/LoadingScreen.tsx diff --git a/.gitignore b/.gitignore index 9874198..c8b52d1 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ yarn-error.* *.pem # local env files +.env .env*.local # typescript diff --git a/app/(protected)/_layout.tsx b/app/(protected)/_layout.tsx index abe43ef..420af35 100644 --- a/app/(protected)/_layout.tsx +++ b/app/(protected)/_layout.tsx @@ -14,6 +14,7 @@ export default function ProtectedLayout() { return ; } + // TODO: Aggiungere padding per i dispositivi con notch/bottom bar return ( {item.site} - {item.in} - {item.out || 'In corso'} + {item.in} - {item.out || 'In corso'} {item.status === 'complete' && ( - 8h + 8h )} diff --git a/app/(protected)/permits/index.tsx b/app/(protected)/permits/index.tsx index 1d17866..aecd218 100644 --- a/app/(protected)/permits/index.tsx +++ b/app/(protected)/permits/index.tsx @@ -4,6 +4,7 @@ import { Alert, ScrollView, Text, TouchableOpacity, View, ActivityIndicator, Ref import { TimeOffRequest, TimeOffRequestType } from '@/types/types'; import RequestPermitModal from '@/components/RequestPermitModal'; import CalendarWidget from '@/components/CalendarWidget'; +import LoadingScreen from '@/components/LoadingScreen'; import api from '@/utils/api'; import { formatDate, formatTime } from '@/utils/dateTime'; @@ -51,14 +52,8 @@ export default function PermitsScreen() { fetchPermits(); }; - // TODO: Migliorare schermata di caricamento -> spostarla in un componente a parte if (isLoading && !refreshing) { - return ( - - - Caricamento... - - ); + return ; } return ( @@ -96,7 +91,7 @@ export default function PermitsScreen() { ) : ( Le tue richieste - {/* TODO: Aggiungere una paginazione con delle freccette affianco? */} + {/* TODO: Aggiungere una paginazione con delle freccette affianco? - Limite backend? */} {permits.map((item) => ( @@ -115,6 +110,7 @@ export default function PermitsScreen() { )} + {/* TODO: Aggiungere funzionalità per modificare/eliminare la richiesta? */} {item.status ? 'Approvato' : 'In Attesa'} diff --git a/components/CalendarWidget.tsx b/components/CalendarWidget.tsx index 6023fd0..19e665f 100644 --- a/components/CalendarWidget.tsx +++ b/components/CalendarWidget.tsx @@ -32,6 +32,7 @@ export default function CalendarWidget({ events, types }: CalendarWidgetProps) { return events.find(event => { // Logica semplice: controlla se la data cade nel range // Nota: per una logica perfetta sui range lunghi, servirebbe un controllo più approfondito + if (!event.start_date) return false; if (event.timeOffRequestType.name === 'Permesso') return event.start_date === dateStr; const end = event.end_date || event.start_date; return dateStr >= event.start_date && dateStr <= end; diff --git a/components/LoadingScreen.tsx b/components/LoadingScreen.tsx new file mode 100644 index 0000000..abb588e --- /dev/null +++ b/components/LoadingScreen.tsx @@ -0,0 +1,10 @@ +import { View, Text, ActivityIndicator } from 'react-native'; + +export default function LoadingScreen() { + return ( + + + Caricamento... + + ); +} \ No newline at end of file diff --git a/components/RequestPermitModal.tsx b/components/RequestPermitModal.tsx index 1596829..408ea8d 100644 --- a/components/RequestPermitModal.tsx +++ b/components/RequestPermitModal.tsx @@ -1,10 +1,11 @@ import React, { useState } from 'react'; import { View, Text, Modal, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native'; import DateTimePicker, { DateType, useDefaultStyles } from 'react-native-ui-datepicker'; -import { TimeOffRequest, TimeOffRequestType } from '@/types/types'; +import { TimeOffRequestType } from '@/types/types'; import { X } from 'lucide-react-native'; import { TimePickerModal } from './TimePickerModal'; import api from '@/utils/api'; +import { formatPickerDate } from '@/utils/dateTime'; interface RequestPermitModalProps { visible: boolean; @@ -15,86 +16,84 @@ interface RequestPermitModalProps { export default function RequestPermitModal({ visible, types, onClose, onSubmit }: RequestPermitModalProps) { const defaultStyles = useDefaultStyles(); - const [type, setType] = useState(types[0]); // Default to first type - const [date, setDate] = useState(); + const [type, setType] = useState(types[0]); // Default to first type + const [date, setDate] = useState(); const [range, setRange] = useState<{ - startDate: DateType; - endDate: DateType; - }>({ startDate: undefined, endDate: undefined }); + startDate: string | null; + endDate: string | null; + }>({ startDate: null, endDate: null }); const [showStartPicker, setShowStartPicker] = useState(false); const [showEndPicker, setShowEndPicker] = useState(false); const [startTime, setStartTime] = useState(''); const [endTime, setEndTime] = useState(''); + const [message, setMessage] = useState(''); // Funzione per resettare le selezioni di data const clearCalendar = () => { - setDate(undefined); - setRange({ startDate: undefined, endDate: undefined }); + setDate(null); + setRange({ startDate: null, endDate: null }); setStartTime(''); setEndTime(''); setType(types[0]); }; + // Funzione per validare la richiesta + function validateRequest(type: TimeOffRequestType, date: string | null | undefined, range: { startDate: string | null; endDate: string | null }, startTime: string, endTime: string, message: string): string | null { + if (!type) return "Seleziona una tipologia di assenza."; + + if (!message || message.trim() === "") return "Inserisci un messaggio."; + + if (type.time_required === 0) { + if (!range.startDate) return "Seleziona una data di inizio."; + return null; + } + + if (!date) return "Seleziona una data."; + if (!startTime || !endTime) return "Seleziona gli orari."; + if (startTime >= endTime) return "L'orario di fine deve essere successivo a quello di inizio."; + + return null; + } + + // Funzione per inviare la richiesta alla API const saveRequest = async (requestData: any) => { try { - // Chiamata API per salvare la richiesta const response = await api.post('/time-off-request/save-request', requestData); - Alert.alert('Successo', 'La tua richiesta è stata inviata con successo.'); - } catch (error) { + + if (response.data.status === 'success') { + Alert.alert('Successo', response.data.message || 'La tua richiesta è stata inviata con successo.'); + } else { + Alert.alert('Errore', response.data.message || 'Impossibile inviare la richiesta.'); + } + } catch (error: any) { console.error('Errore nell\'invio della richiesta:', error); - Alert.alert('Errore', 'Impossibile inviare la richiesta. Riprova più tardi.'); + throw new Error('Impossibile inviare la richiesta.'); } }; // Funzione per inviare la richiesta - const handleSubmit = ( - type: TimeOffRequestType | undefined, - date: DateType | undefined, - range: { startDate: DateType | undefined; endDate: DateType | undefined }, - startTime: string, - endTime: string - ) => { + const handleSubmit = async () => { + const error = validateRequest(type, date, range, startTime, endTime, message); + if (error) return Alert.alert("Errore", error); - if (!type) { - alert('Seleziona una tipologia di assenza.'); - return; - } - - // Validazioni - if (type.time_required === 0) { - if (!range.startDate && !range.endDate) { - alert('Seleziona almeno una data.'); - return; - } - } else { - if (!date) { - alert('Seleziona una data.'); - return; - } - if (!startTime || !endTime) { - alert('Seleziona l\'orario di inizio e fine.'); - return; - } else if (startTime >= endTime) { - alert('L\'orario di fine deve essere successivo all\'orario di inizio.'); - return; - } - } - - // Costruzione oggetto request const requestData = { id_type: type.id, start_date: type.time_required === 0 ? range.startDate : date, end_date: type.time_required === 0 ? range.endDate : null, start_time: type.time_required === 1 ? startTime : null, end_time: type.time_required === 1 ? endTime : null, + message: message || "" }; - saveRequest(requestData); - onSubmit(requestData); - onClose(); + try { + await saveRequest(requestData); + onSubmit(requestData); // TODO: Gestire risposta e controllare fetch in index? + onClose(); + } catch (e) { + Alert.alert("Errore", "Impossibile inviare la richiesta."); + } }; - return ( - + {/* Tipologia */} - + Tipologia Assenza setRange(params)} - timeZone='Universal' + onChange={(params) => { + setRange({ + startDate: params.startDate ? formatPickerDate(params.startDate) : null, + endDate: params.endDate ? formatPickerDate(params.endDate) : null + }) + }} locale='it' styles={{ ...defaultStyles, @@ -152,63 +155,78 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } }} /> ) : ( - - setDate(date)} - timeZone='Universal' - locale='it' - styles={{ - ...defaultStyles, - selected: { backgroundColor: '#099499' } - }} - /> - - - - Dalle Ore - setShowStartPicker(true)}> - - - - - Alle Ore - setShowEndPicker(true)}> - - - - - + setDate(date ? formatPickerDate(date) : null)} + locale='it' + styles={{ + ...defaultStyles, + selected: { backgroundColor: '#099499' } + }} + /> )} - setStartTime(time)} - onClose={() => setShowStartPicker(false)} - /> - setEndTime(time)} - onClose={() => setShowEndPicker(false)} - /> + + {type?.time_required === 1 && ( + + + + Dalle Ore + setShowStartPicker(true)}> + + + + + Alle Ore + setShowEndPicker(true)}> + + + + + setStartTime(time)} + onClose={() => setShowStartPicker(false)} + /> + setEndTime(time)} + onClose={() => setShowEndPicker(false)} + /> + + )} + {/* TODO: Trasformare message in una select? - Predefinito per alcuni tipi */} + + Motivo + + + + + {/* Azioni */} { @@ -221,9 +239,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } { - handleSubmit(type, date, range, startTime, endTime); - }} + onPress={handleSubmit} className="flex-1 py-4 bg-[#099499] rounded-2xl shadow-lg active:scale-[0.98]" > Invia Richiesta diff --git a/utils/api.ts b/utils/api.ts index c905a98..e19bc99 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -1,8 +1,7 @@ import axios from 'axios'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import * as SecureStore from 'expo-secure-store'; -const API_BASE_URL = `http://10.0.2.2:32768/mobile`; +const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL; export const KEY_TOKEN = 'auth_key'; // Crea un'istanza di axios diff --git a/utils/dateTime.ts b/utils/dateTime.ts index c882f85..f0983f6 100644 --- a/utils/dateTime.ts +++ b/utils/dateTime.ts @@ -1,3 +1,5 @@ +import { DateType } from "react-native-ui-datepicker"; + /** * Trasforma una data da "YYYY-MM-DD" a "DD/MM/YYYY" * @param dateStr stringa data in formato ISO "YYYY-MM-DD" @@ -19,3 +21,21 @@ export const formatTime = (timeStr: string | null | undefined): string => { const [hours, minutes] = timeStr.split(':'); return `${hours}:${minutes}`; }; + +/** + * Formatta una data per l'uso con un date picker, normalizzandola a mezzanotte + * @param d Data in formato DateType + * @returns stringa data in formato "YYYY-MM-DD" o null se l'input è null/undefined + */ +export const formatPickerDate = (d: DateType | null | undefined) => { + if (!d) return null; + + const date = new Date(d as string | number | Date); + const normalized = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + + const yyyy = normalized.getFullYear(); + const mm = String(normalized.getMonth() + 1).padStart(2, "0"); + const dd = String(normalized.getDate()).padStart(2, "0"); + + return `${yyyy}-${mm}-${dd}`; +}