diff --git a/app/(protected)/_layout.tsx b/app/(protected)/_layout.tsx
index 1f86f1c..abe43ef 100644
--- a/app/(protected)/_layout.tsx
+++ b/app/(protected)/_layout.tsx
@@ -64,6 +64,7 @@ export default function ProtectedLayout() {
tabBarIcon: ({ color, size }) => ,
}}
/>
+ {/* // TODO: Rimuovere all'utente e mostrare solo a admin */}
,
}}
/>
- {/* TODO: Da rimuovere */}
+ {/* TODO: Dovrebbe essere rimosso, va rivisto layout */}
item.status === 'incomplete');
return (
@@ -16,15 +18,11 @@ export default function HomeScreen() {
Benvenuto
- {MOCK_USER.name} {MOCK_USER.surname}
- {MOCK_USER.role}
+ {user?.name} {user?.surname}
+ {user?.role}
-
-
-
-
router.push('/profile')}>
diff --git a/app/(protected)/permits/index.tsx b/app/(protected)/permits/index.tsx
index 7635954..1d17866 100644
--- a/app/(protected)/permits/index.tsx
+++ b/app/(protected)/permits/index.tsx
@@ -1,37 +1,75 @@
-import React, { useState } from 'react';
-import { PERMITS_DATA } from '@/data/data';
-import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, Clock, Plus, Thermometer, X } from 'lucide-react-native';
-import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
+import React, { JSX, useEffect, 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';
import RequestPermitModal from '@/components/RequestPermitModal';
+import CalendarWidget from '@/components/CalendarWidget';
+import api from '@/utils/api';
+import { formatDate, formatTime } from '@/utils/dateTime';
+
+// Icon Mapping
+const typeIcons: Record JSX.Element> = {
+ Ferie: (color) => ,
+ Permesso: (color) => ,
+ Malattia: (color) => ,
+ Assenza: (color) => ,
+};
export default function PermitsScreen() {
- const [currentDate, setCurrentDate] = useState(new Date(2025, 11, 1)); // Dicembre 2025 Mock
const [showModal, setShowModal] = useState(false);
+ const [permits, setPermits] = useState([]);
+ const [types, setTypes] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
- // Helpers per il calendario
- const daysInMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
- const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDay(); // 0 = Sun
- const adjustedFirstDay = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1; // 0 = Mon
-
- const getEventForDay = (day: number) => {
- const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
-
- return PERMITS_DATA.find(leave => {
- if (leave.type === 'Permesso') return leave.startDate === dateStr;
- return dateStr >= leave.startDate && dateStr <= (leave.endDate || leave.startDate);
- });
+ const fetchPermits = async () => {
+ try {
+ if (!refreshing) setIsLoading(true);
+
+ // Fetch Permits
+ const response = await api.get('/time-off-request/list');
+ setPermits(response.data);
+
+ // Fetch Types
+ const typesResponse = await api.get('/time-off-request/get-types');
+ setTypes(typesResponse.data);
+ } catch (error) {
+ console.error('Errore nel recupero dei permessi:', error);
+ Alert.alert('Errore', 'Impossibile recuperare i permessi. Riprova più tardi.');
+ } finally {
+ setIsLoading(false);
+ setRefreshing(false);
+ }
};
- const weekDays = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'];
+ useEffect(() => {
+ fetchPermits();
+ }, []);
+
+ const onRefresh = () => {
+ setRefreshing(true);
+ fetchPermits();
+ };
+
+ // TODO: Migliorare schermata di caricamento -> spostarla in un componente a parte
+ if (isLoading && !refreshing) {
+ return (
+
+
+ Caricamento...
+
+ );
+ }
return (
- setShowModal(false)}
- onSubmit={(data) => console.log('Richiesta:', data)}
+ setShowModal(false)}
+ onSubmit={(data) => { console.log('Richiesta:', data); fetchPermits(); }}
/>
-
+
{/* Header */}
@@ -40,132 +78,57 @@ export default function PermitsScreen() {
-
-
+
+ }
+ >
+
{/* Calendar Widget */}
-
-
-
-
-
-
- {currentDate.toLocaleString('it-IT', { month: 'long', year: 'numeric' })}
-
-
-
-
-
-
- {/* Week Header */}
-
- {weekDays.map(day => (
- {day}
- ))}
-
-
- {/* Days Grid */}
-
- {/* Empty slots for alignment */}
- {Array.from({ length: adjustedFirstDay }).map((_, i) => (
-
- ))}
- {/* Days */}
- {Array.from({ length: daysInMonth }).map((_, i) => {
- const day = i + 1;
- const event = getEventForDay(day);
- let bgClass = 'bg-transparent';
- let textClass = 'text-gray-700';
- let borderClass = 'border-transparent';
-
- if (event) {
- if (event.type === 'Ferie') {
- bgClass = 'bg-purple-100';
- textClass = 'text-purple-700 font-bold';
- borderClass = 'border-purple-200';
- } else if (event.type === 'Permesso') {
- bgClass = 'bg-orange-100';
- textClass = 'text-orange-700 font-bold';
- borderClass = 'border-orange-200';
- } else if (event.type === 'Malattia') {
- bgClass = 'bg-red-100';
- textClass = 'text-red-700 font-bold';
- borderClass = 'border-red-200';
- }
- }
-
- return (
-
-
- {day}
-
-
- );
- })}
-
-
- {/* Legenda */}
-
-
-
- Ferie
-
-
-
- Permessi
-
-
-
- Malattia
-
-
-
+
{/* Lista Richieste Recenti */}
- Le tue richieste
-
- {PERMITS_DATA.map((item) => (
-
-
-
- {item.type === 'Ferie' && }
- {item.type === 'Permesso' && }
- {item.type === 'Malattia' && }
-
-
- {item.type}
-
- {item.startDate} {item.endDate ? `- ${item.endDate}` : ''}
-
- {item.type === 'Permesso' && (
-
- {item.startTime} - {item.endTime}
+ {permits.length === 0 ? (
+ Nessuna richiesta di permesso trovata.
+ ) : (
+
+ Le tue richieste
+ {/* TODO: Aggiungere una paginazione con delle freccette affianco? */}
+ {permits.map((item) => (
+
+
+
+ {typeIcons[item.timeOffRequestType.name]?.(item.timeOffRequestType.color)}
+
+
+ {item.timeOffRequestType.name}
+
+ {formatDate(item.start_date?.toLocaleString())} {item.end_date ? `- ${formatDate(item.end_date.toLocaleString())}` : ''}
- )}
+ {item.timeOffRequestType.name === 'Permesso' && (
+
+ {formatTime(item.start_time)} - {formatTime(item.end_time)}
+
+ )}
+
+
+
+
+ {item.status ? 'Approvato' : 'In Attesa'}
+
-
-
- {item.status}
-
-
-
- ))}
-
+ ))}
+
+ )}
{/* FAB */}
- setShowModal(true)}
className="absolute bottom-8 right-6 w-16 h-16 bg-[#099499] rounded-full shadow-lg items-center justify-center active:scale-90"
>
diff --git a/app/(protected)/profile/index.tsx b/app/(protected)/profile/index.tsx
index ec262d9..fbf8433 100644
--- a/app/(protected)/profile/index.tsx
+++ b/app/(protected)/profile/index.tsx
@@ -7,13 +7,11 @@ import { AuthContext } from '@/utils/authContext';
export default function ProfileScreen() {
const authContext = useContext(AuthContext);
+ const { user } = authContext;
const router = useRouter();
- // Dati fittizi aggiuntivi (possono essere presi dal backend in seguito)
- const email = `${MOCK_USER.name.toLowerCase().replace(/\s+/g, '.')}@example.com`;
- const phone = '+39 345 123 4567';
-
- const initials = MOCK_USER.name.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase();
+ // Genera le iniziali dell'utente
+ const initials = `${user?.name?.[0] ?? ''}${user?.surname?.[0] ?? ''}`.toUpperCase();
return (
@@ -31,7 +29,7 @@ export default function ProfileScreen() {
Profilo
- {MOCK_USER.name} {MOCK_USER.surname}
+ {user?.name} {user?.surname}
@@ -56,7 +54,7 @@ export default function ProfileScreen() {
{/* Label e valore ingranditi */}
Email
- {email}
+ {user?.email}
@@ -77,7 +75,7 @@ export default function ProfileScreen() {
Ruolo
- {MOCK_USER.role}
+ {user?.role}
diff --git a/app/login.tsx b/app/login.tsx
index 6dbdca1..c944ff2 100644
--- a/app/login.tsx
+++ b/app/login.tsx
@@ -8,24 +8,66 @@ import {
TextInput,
TouchableOpacity,
View,
- Image
+ Image,
+ Alert
} from 'react-native';
import { AuthContext } from '@/utils/authContext';
+import api from '@/utils/api';
export default function LoginScreen() {
const authContext = useContext(AuthContext);
- const [email, setEmail] = useState('');
+ const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- const handleLogin = () => {
+ const handleLogin = async () => {
+ // TODO: Implementa toast o messaggio di errore più user-friendly
+ if (!username || !password) {
+ Alert.alert("Attenzione", "Inserisci username e password");
+ return;
+ }
+
setIsLoading(true);
- // Simulazione login
- setTimeout(() => {
+
+ try {
+ console.log("Tentativo login con:", username);
+
+ // Chiamata vera al backend
+ const response = await api.post("/user/login", {
+ username: username,
+ password: password
+ });
+
+ const { token, user } = response.data;
+
+ console.log("Login successo, token ricevuto.");
+ console.log("Dati utente:", user);
+
+ // Passiamo token e dati utente al context che gestirà salvataggio e redirect
+ authContext.logIn(token, user);
+
+ } catch (error: any) {
+ console.error("Login Error:", error);
+
+ let message = "Si è verificato un errore durante l'accesso.";
+
+ if (error.response) {
+ // Errore dal server (es. 401 Credenziali errate)
+ if (error.response.status === 401) {
+ message = "Credenziali non valide.";
+ } else {
+ message = `Errore Server: ${error.response.data.message || error.response.status}`;
+ }
+ } else if (error.request) {
+ // Server non raggiungibile
+ message = "Impossibile contattare il server. Controlla la connessione.";
+ }
+
+ Alert.alert("Login Fallito", message);
+ } finally {
setIsLoading(false);
- authContext.logIn();
- }, 1500);
+ }
};
return (
@@ -47,17 +89,17 @@ export default function LoginScreen() {
>
- {/* Input Email */}
+ {/* Input Username / Email */}
- Email o Username
+ Username o Email
diff --git a/components/CalendarWidget.tsx b/components/CalendarWidget.tsx
new file mode 100644
index 0000000..6023fd0
--- /dev/null
+++ b/components/CalendarWidget.tsx
@@ -0,0 +1,109 @@
+import React, { useState } from 'react';
+import { View, Text, TouchableOpacity } from 'react-native';
+import { ChevronLeft, ChevronRight } from 'lucide-react-native';
+import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
+
+interface CalendarWidgetProps {
+ events: TimeOffRequest[];
+ types: TimeOffRequestType[];
+}
+
+export default function CalendarWidget({ events, types }: CalendarWidgetProps) {
+ const [currentDate, setCurrentDate] = useState(new Date());
+
+ // Helpers per il calendario
+ const daysInMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
+ const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDay(); // 0 = Sun
+ const adjustedFirstDay = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1; // 0 = Mon
+
+ const weekDays = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'];
+
+ const changeMonth = (increment: number) => {
+ const newDate = new Date(currentDate.setMonth(currentDate.getMonth() + increment));
+ setCurrentDate(new Date(newDate));
+ };
+
+ const getEventForDay = (day: number) => {
+ const year = currentDate.getFullYear();
+ const month = String(currentDate.getMonth() + 1).padStart(2, '0');
+ const dayStr = String(day).padStart(2, '0');
+ const dateStr = `${year}-${month}-${dayStr}`;
+
+ 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.timeOffRequestType.name === 'Permesso') return event.start_date === dateStr;
+ const end = event.end_date || event.start_date;
+ return dateStr >= event.start_date && dateStr <= end;
+ });
+ };
+
+ return (
+
+ {/* Header Mese */}
+
+ changeMonth(-1)}
+ className="p-2 bg-gray-50 rounded-full"
+ >
+
+
+
+ {currentDate.toLocaleString('it-IT', { month: 'long', year: 'numeric' })}
+
+ changeMonth(1)}
+ className="p-2 bg-gray-50 rounded-full"
+ >
+
+
+
+
+ {/* Week Header */}
+
+ {weekDays.map(day => (
+ {day}
+ ))}
+
+
+ {/* Days Grid */}
+
+ {/* Empty slots for alignment */}
+ {Array.from({ length: adjustedFirstDay }).map((_, i) => (
+
+ ))}
+ {/* Days */}
+ {Array.from({ length: daysInMonth }).map((_, i) => {
+ const day = i + 1;
+ const event = getEventForDay(day);
+
+ let bgClass = 'bg-transparent';
+ let textClass = 'text-gray-700';
+ let borderClass = 'border-transparent';
+
+ const bgColor = event?.timeOffRequestType?.color ? `${event.timeOffRequestType.color}25` : 'transparent';
+ const borderColor = event?.timeOffRequestType?.color || 'transparent';
+ const textColor = event ? event.timeOffRequestType?.color : '#374151';
+
+ return (
+
+
+ {day}
+
+
+ );
+ })}
+
+
+ {/* Legenda */}
+
+ {types.map((type) => (
+
+
+ {type.name}
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/RequestPermitModal.tsx b/components/RequestPermitModal.tsx
index 959cf84..1596829 100644
--- a/components/RequestPermitModal.tsx
+++ b/components/RequestPermitModal.tsx
@@ -1,19 +1,21 @@
import React, { useState } from 'react';
-import { View, Text, Modal, TouchableOpacity, TextInput, ScrollView } from 'react-native';
-import DateTimePicker, { DateType, useDefaultStyles, useDefaultClassNames } from 'react-native-ui-datepicker';
-import { PermitType } from '@/types/types';
+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 { X } from 'lucide-react-native';
import { TimePickerModal } from './TimePickerModal';
+import api from '@/utils/api';
interface RequestPermitModalProps {
visible: boolean;
+ types: TimeOffRequestType[];
onClose: () => void;
onSubmit: (data: any) => void;
}
-export default function RequestPermitModal({ visible, onClose, onSubmit }: RequestPermitModalProps) {
+export default function RequestPermitModal({ visible, types, onClose, onSubmit }: RequestPermitModalProps) {
const defaultStyles = useDefaultStyles();
- const [type, setType] = useState('Ferie');
+ const [type, setType] = useState(types[0]); // Default to first type
const [date, setDate] = useState();
const [range, setRange] = useState<{
startDate: DateType;
@@ -25,6 +27,74 @@ export default function RequestPermitModal({ visible, onClose, onSubmit }: Reque
const [startTime, setStartTime] = useState('');
const [endTime, setEndTime] = useState('');
+ // Funzione per resettare le selezioni di data
+ const clearCalendar = () => {
+ setDate(undefined);
+ setRange({ startDate: undefined, endDate: undefined });
+ setStartTime(''); setEndTime('');
+ setType(types[0]);
+ };
+
+ 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) {
+ console.error('Errore nell\'invio della richiesta:', error);
+ Alert.alert('Errore', 'Impossibile inviare la richiesta. Riprova più tardi.');
+ }
+ };
+
+ // Funzione per inviare la richiesta
+ const handleSubmit = (
+ type: TimeOffRequestType | undefined,
+ date: DateType | undefined,
+ range: { startDate: DateType | undefined; endDate: DateType | undefined },
+ startTime: string,
+ endTime: string
+ ) => {
+
+ 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,
+ };
+
+ saveRequest(requestData);
+ onSubmit(requestData);
+ onClose();
+ };
+
+
return (
{/* Tipologia */}
- Tipologia Assenza
-
- {(['Ferie', 'Permesso', 'Malattia'] as PermitType[]).map((t) => (
+ Tipologia Assenza
+
+ {types.map((t) => (
setType(t)}
- className={`flex-1 py-4 rounded-xl border-2 items-center justify-center ${type === t
- ? 'border-[#099499] bg-teal-50'
- : 'border-gray-100 bg-white'
+ className={`py-4 px-5 rounded-xl border-2 items-center justify-center ${type?.id === t.id ? 'border-[#099499] bg-teal-50' : 'border-gray-100 bg-white'
}`}
>
-
- {t}
+
+ {t.name}
))}
-
+
- {/* Date Selection */}
- {type !== 'Permesso' ? (
+ {/* Date and Time Selection */}
+ {type?.time_required === 0 ? (
setRange(params)}
- timeZone='Europe/Rome'
+ timeZone='Universal'
locale='it'
styles={{
...defaultStyles,
@@ -86,7 +157,7 @@ export default function RequestPermitModal({ visible, onClose, onSubmit }: Reque
mode="single"
date={date}
onChange={({ date }) => setDate(date)}
- timeZone='Europe/Rome'
+ timeZone='Universal'
locale='it'
styles={{
...defaultStyles,
@@ -126,36 +197,37 @@ export default function RequestPermitModal({ visible, onClose, onSubmit }: Reque
setStartTime(time)}
onClose={() => setShowStartPicker(false)}
/>
setEndTime(time)}
onClose={() => setShowEndPicker(false)}
/>
-
+
{
- onSubmit({ type, date, range, startTime, endTime });
+ clearCalendar();
onClose();
}}
- className="w-full py-4 bg-[#099499] rounded-2xl shadow-lg active:scale-[0.98]"
+ className="flex-1 py-4 bg-gray-200 rounded-2xl shadow-sm active:bg-gray-300"
+ >
+ Annulla Richiesta
+
+
+ {
+ handleSubmit(type, date, range, startTime, endTime);
+ }}
+ className="flex-1 py-4 bg-[#099499] rounded-2xl shadow-lg active:scale-[0.98]"
>
Invia Richiesta
- {
- console.log("Reset pressed");
- onClose();
- // TODO: deselect dates from calendar
- }}
- className="mt-2 bg-gray-200 rounded-xl py-4 flex-row justify-center items-center active:bg-gray-300"
- >
- Annulla Richiesta
-
diff --git a/components/TimePickerModal.tsx b/components/TimePickerModal.tsx
index a885de9..445e7c7 100644
--- a/components/TimePickerModal.tsx
+++ b/components/TimePickerModal.tsx
@@ -7,11 +7,12 @@ import dayjs from 'dayjs';
interface TimePickerModalProps {
visible: boolean;
initialDate?: DateType;
+ title?: string;
onConfirm: (time: string) => void;
onClose: () => void;
}
-export const TimePickerModal = ({ visible, initialDate, onConfirm, onClose }: TimePickerModalProps) => {
+export const TimePickerModal = ({ visible, initialDate, title, onConfirm, onClose }: TimePickerModalProps) => {
const defaultStyles = useDefaultStyles();
const [selectedDate, setSelectedDate] = useState(initialDate || new Date());
@@ -36,7 +37,8 @@ export const TimePickerModal = ({ visible, initialDate, onConfirm, onClose }: Ti
{/* Header con chiusura */}
-
+
+ {title}
diff --git a/data/data.ts b/data/data.ts
index 0eb1b86..c8131ac 100644
--- a/data/data.ts
+++ b/data/data.ts
@@ -1,15 +1,6 @@
-import { UserData, AttendanceRecord, DocumentItem, OfficeItem, PermitRecord } from '@/types/types';
+import { UserData, AttendanceRecord, DocumentItem, OfficeItem } from '@/types/types';
// --- MOCK DATA (File: data.ts) ---
-export const MOCK_USER: UserData = {
- name: "Mario",
- surname: "Rossi",
- username: "mario.rossi",
- email: "mario.rossi@esempio.com",
- role: "Tecnico Specializzato",
- id: "EMP-8842"
-};
-
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" },
@@ -28,17 +19,4 @@ export const OFFICES_DATA: OfficeItem[] = [
{ id: 2, name: "Sala Riunioni", status: "offline", temp: 19, lights: false, power: 0 },
{ id: 3, name: "Amministrazione", status: "online", temp: 24, lights: true, power: 320 },
{ id: 4, name: "Magazzino", status: "online", temp: 18, lights: false, power: 120 },
-];
-
-export const COLORS = {
- primary: '#099499',
- bg: '#f3f4f6',
- white: '#ffffff',
- text: '#1f2937',
-};
-
-export const PERMITS_DATA: PermitRecord[] = [
- { id: 1, type: 'Ferie', startDate: '2025-12-24', endDate: '2025-12-31', status: 'Approvato' },
- { id: 2, type: 'Permesso', startDate: '2025-12-10', startTime: '09:00', endTime: '11:00', status: 'In Attesa' },
- { id: 3, type: 'Malattia', startDate: '2025-11-15', endDate: '2025-11-16', status: 'Approvato' },
];
\ No newline at end of file
diff --git a/types/types.ts b/types/types.ts
index 3134a98..c02e005 100644
--- a/types/types.ts
+++ b/types/types.ts
@@ -1,5 +1,7 @@
// --- TYPES & export INTERFACES (File: types.ts) ---
+import { DateType } from "react-native-ui-datepicker";
+
export interface UserData {
name: string;
surname: string;
@@ -35,16 +37,24 @@ export interface OfficeItem {
power: number;
}
-export type ScreenName = 'home' | 'attendance' | 'documents' | 'smart-office' | 'profile';
-
-export type PermitType = 'Ferie' | 'Permesso' | 'Malattia';
-
-export interface PermitRecord {
+export interface TimeOffRequestType {
id: number;
- type: PermitType;
- startDate: string;
- endDate?: string; // Opzionale per permessi giornalieri
- startTime?: string; // Solo per permessi
- endTime?: string; // Solo per permessi
- status: 'Approvato' | 'In Attesa' | 'Rifiutato';
+ name: string;
+ color: string;
+ abbreviation: string;
+ time_required: number; // backend usa 0/1
+ deleted: number; // backend usa 0/1
+}
+
+export interface TimeOffRequest {
+ id: number;
+ id_type: number;
+ id_user: number;
+ start_date: DateType;
+ end_date?: DateType | null;
+ start_time?: string | null;
+ end_time?: string | null;
+ message?: string | null;
+ status: number;
+ timeOffRequestType: TimeOffRequestType;
}
\ No newline at end of file
diff --git a/utils/api.ts b/utils/api.ts
index 089a7e3..c905a98 100644
--- a/utils/api.ts
+++ b/utils/api.ts
@@ -1,43 +1,55 @@
import axios from 'axios';
+import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
-// CONFIGURAZIONE GATEWAY (Adatta questi valori al tuo DDEV)
-// Se sei su emulatore Android usa 10.0.2.2, se su iOS o fisico usa il tuo IP LAN (es 192.168.1.x)
-const GATEWAY_BASE_URL = "http://10.0.2.2:PORTA";
-export const GATEWAY_ENDPOINT = `${GATEWAY_BASE_URL}/tuo_endpoint_gateway`;
-export const GATEWAY_TOKEN = "il_tuo_token_statico_se_esiste";
+const API_BASE_URL = `http://10.0.2.2:32768/mobile`;
+export const KEY_TOKEN = 'auth_key';
+// Crea un'istanza di axios
const api = axios.create({
-
+ baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
+ 'Accept': 'application/json',
},
+ timeout: 10000, // 10 secondi timeout
});
-// INTERCEPTOR: Configura ogni chiamata al volo
-api.interceptors.request.use(async (config) => {
- try {
- // 1. Cerchiamo se abbiamo già salvato l'URL finale del backend (post-gateway)
- const savedBaseUrl = await SecureStore.getItemAsync('App_URL');
-
- if (savedBaseUrl) {
- config.baseURL = savedBaseUrl;
- } else {
- // Se non c'è, usiamo il gateway come fallback o gestiamo l'errore
- // (La logica di init nell'AuthContext dovrebbe averlo già settato)
- config.baseURL = GATEWAY_BASE_URL;
- }
-
- // 2. Cerchiamo il token utente
- const token = await SecureStore.getItemAsync('auth-token');
+// Interceptor: Aggiunge il token a OGNI richiesta se esiste
+api.interceptors.request.use(
+ async (config) => {
+ const token = await SecureStore.getItemAsync(KEY_TOKEN);
if (token) {
- // Adatta l'header in base al tuo backend (Bearer, x-access-tokens, etc.)
config.headers.Authorization = `Bearer ${token}`;
}
- } catch (error) {
- console.error("Errore interceptor:", error);
+ console.log(`[API REQUEST] ${config.method?.toUpperCase()} ${config.url}`);
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
}
- return config;
-});
+);
+
+// Interceptor: Gestione errori globale (es. token scaduto)
+api.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ const originalRequest = error.config;
+
+ if (error.response) {
+ console.error('[API ERROR]', error.response.status, error.response.data);
+
+ // Se riceviamo 401 (Unauthorized), potremmo voler fare il logout forzato
+ if (error.response.status === 401) {
+ // TODO: Qui potresti emettere un evento per disconnettere l'utente
+ await SecureStore.deleteItemAsync(KEY_TOKEN);
+ }
+ } else {
+ console.error('[API NETWORK ERROR]', error.message);
+ }
+
+ return Promise.reject(error);
+ }
+);
export default api;
\ No newline at end of file
diff --git a/utils/authContext.tsx b/utils/authContext.tsx
index 6585ac4..9313b99 100644
--- a/utils/authContext.tsx
+++ b/utils/authContext.tsx
@@ -1,10 +1,8 @@
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react';
import { SplashScreen, useRouter, useSegments } from 'expo-router';
-import AsyncStorage from '@react-native-async-storage/async-storage';
import { UserData } from '@/types/types';
import * as SecureStore from 'expo-secure-store';
-import api, { GATEWAY_ENDPOINT, GATEWAY_TOKEN } from './api';
-import axios from 'axios';
+import api, { KEY_TOKEN } from './api';
type AuthState = {
isAuthenticated: boolean;
@@ -16,9 +14,6 @@ type AuthState = {
SplashScreen.preventAutoHideAsync();
-const KEY_TOKEN = 'auth-token';
-const KEY_URL = 'App_URL';
-
export const AuthContext = createContext({
isAuthenticated: false,
isReady: false,
@@ -36,21 +31,12 @@ export function AuthProvider({ children }: PropsWithChildren) {
const router = useRouter();
const segments = useSegments();
- const storeAuthState = async (newState: { isAuthenticated: boolean }) => {
- try {
- const jsonValue = JSON.stringify(newState);
- await AsyncStorage.setItem(KEY_TOKEN, jsonValue);
- } catch (error) {
- console.error('Errore nel salvataggio dello stato di autenticazione:', error);
- }
- }
-
const logIn = async (token: string, userData: UserData) => {
try {
await SecureStore.setItemAsync(KEY_TOKEN, token);
setIsAuthenticated(true);
setUser(userData);
- storeAuthState({ isAuthenticated: true }); // TODO: can be removed later
+
router.replace('/');
} catch (error) {
console.error('Errore durante il login:', error);
@@ -62,7 +48,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
await SecureStore.deleteItemAsync(KEY_TOKEN);
setIsAuthenticated(false);
setUser(null);
- storeAuthState({ isAuthenticated: false });
+
router.replace('/login');
} catch (error) {
console.error('Errore durante il logout:', error);
@@ -72,54 +58,45 @@ export function AuthProvider({ children }: PropsWithChildren) {
useEffect(() => {
const initApp = async () => {
try {
- // 1. Gestione URL Gateway (Logica "else" del vecchio snippet)
- let currentApiUrl = await SecureStore.getItemAsync(KEY_URL);
-
- if (!currentApiUrl) {
- console.log("URL non trovato, contatto Gateway...");
- try {
- // Chiamata diretta al gateway (senza interceptor api.ts)
- const gwResponse = await axios.get(GATEWAY_ENDPOINT, {
- headers: { "x-access-tokens": GATEWAY_TOKEN }
- });
-
- // Supponiamo che il backend ritorni { url: "http://..." }
- // Adatta questo parsing alla risposta reale del tuo backend
- const newUrl = gwResponse.data.url + "/api/app_cantieri";
-
- await SecureStore.setItemAsync(KEY_URL, newUrl);
- currentApiUrl = newUrl;
- console.log("URL acquisito:", newUrl);
- } catch (gwError) {
- console.error("Errore connessione Gateway:", gwError);
- // Qui potresti decidere di non bloccare l'app o mostrare un errore
- }
- }
-
- // 2. Controllo Token e Recupero User (Logica "if" del vecchio snippet)
+ // 1. Recupero Token salvato
const savedToken = await SecureStore.getItemAsync(KEY_TOKEN);
- if (savedToken && currentApiUrl) {
- // Verifichiamo il token chiamando /user
- // Qui usiamo l'istanza 'api' importata che ora userà l'URL e il token
- const userRes = await api.get("/user");
+ if (savedToken) {
+ console.log("Token trovato: ", savedToken);
- const result = userRes.data;
+ // 2. Chiamata al backend per verificare il token e scaricare i dati utente
+ // Nota: api.ts aggiunge già l'header Authorization grazie all'interceptor (se configurato per leggere da SecureStore)
+ // Se il tuo api.ts legge da AsyncStorage, assicurati che siano allineati, altrimenti passalo a mano qui:
+ const response = await api.get("/user/info", {
+ headers: { Authorization: `Bearer ${savedToken}` }
+ });
+
+ const result = response.data;
+ const userData = result.user;
+ console.log("Sessione valida, dati utente caricati:", userData);
+
+ // 3. Mappatura dati (Backend -> Frontend)
+ // Il backend actionMe ritorna: { id, username, role }
const loadedUser: UserData = {
- name: result.nome,
- surname: result.cognome,
- username: result.username,
- email: result.email,
- role: result.role,
- id: result.id,
+ id: userData.id,
+ username: userData.username,
+ role: userData.role,
+ // Gestiamo i campi opzionali se il backend non li manda ancora
+ name: userData.name,
+ surname: userData.surname || '',
+ email: userData.email || '',
};
setUser(loadedUser);
setIsAuthenticated(true);
+ } else {
+ console.log("Nessun token salvato.");
}
- } catch (error) {
- console.error('Errore durante l\'inizializzazione dell\'app:', error);
- // Se il token è scaduto o l'API fallisce, consideriamo l'utente non loggato
+
+ } catch (error: any) {
+ console.error('Errore inizializzazione (Token scaduto o Server down):', error.message);
+
+ // Se il token non è valido, puliamo tutto
await SecureStore.deleteItemAsync(KEY_TOKEN);
setIsAuthenticated(false);
setUser(null);
@@ -129,32 +106,9 @@ export function AuthProvider({ children }: PropsWithChildren) {
}
};
-
- // TODO: can be removed later
- // const getAuthFromStorage = async () => {
- // try {
- // const jsonValue = await AsyncStorage.getItem(KEY_TOKEN);
- // if (jsonValue != null) {
- // const auth = JSON.parse(jsonValue);
- // setIsAuthenticated(auth.isAuthenticated);
- // }
- // } catch (error) {
- // console.error('Errore nel recupero dello stato di autenticazione:', error);
- // }
- // setIsReady(true);
- // };
- // getAuthFromStorage();
-
initApp();
}, []);
- // TODO: Can be removed later
- // useEffect(() => {
- // if (isReady) {
- // SplashScreen.hideAsync();
- // }
- // }, [isReady]);
-
// Protezione rotte (opzionale, ma consigliata qui o nel Layout)
useEffect(() => {
if (!isReady) return;
@@ -164,7 +118,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
if (!isAuthenticated && inAuthGroup) {
router.replace('/login');
} else if (isAuthenticated && !inAuthGroup) {
- // router.replace('/(protected)/home'); // Decommenta se vuoi redirect automatico da login a home
+ router.replace('/');
}
}, [isReady, isAuthenticated, segments]);
diff --git a/utils/dateTime.ts b/utils/dateTime.ts
new file mode 100644
index 0000000..c882f85
--- /dev/null
+++ b/utils/dateTime.ts
@@ -0,0 +1,21 @@
+/**
+ * Trasforma una data da "YYYY-MM-DD" a "DD/MM/YYYY"
+ * @param dateStr stringa data in formato ISO "YYYY-MM-DD"
+ * @returns stringa formattata "DD/MM/YYYY"
+ */
+export const formatDate = (dateStr: string | null | undefined): string => {
+ if (!dateStr) return '';
+ const [year, month, day] = dateStr.split('-');
+ return `${day}/${month}/${year}`;
+};
+
+/**
+ * Trasforma un'ora da "HH:MM:SS" a "HH:MM"
+ * @param timeStr stringa ora in formato "HH:MM:SS"
+ * @returns stringa formattata "HH:MM"
+ */
+export const formatTime = (timeStr: string | null | undefined): string => {
+ if (!timeStr) return '';
+ const [hours, minutes] = timeStr.split(':');
+ return `${hours}:${minutes}`;
+};