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