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}`;
+}