- Refactor Profile and Login screens to use AuthContext for user data
- Enhance RequestPermitModal with multiple time-off types and validation - Implement CalendarWidget for visualizing time-off requests - Improve API error handling and token management - Add utility functions for consistent date and time formatting - Clean up unused mock data and update types
This commit is contained in:
@ -64,6 +64,7 @@ export default function ProtectedLayout() {
|
||||
tabBarIcon: ({ color, size }) => <FileText color={color} size={24} />,
|
||||
}}
|
||||
/>
|
||||
{/* // TODO: Rimuovere all'utente e mostrare solo a admin */}
|
||||
<Tabs.Screen
|
||||
name="automation"
|
||||
options={{
|
||||
@ -71,7 +72,7 @@ export default function ProtectedLayout() {
|
||||
tabBarIcon: ({ color, size }) => <Zap color={color} size={24} />,
|
||||
}}
|
||||
/>
|
||||
{/* TODO: Da rimuovere */}
|
||||
{/* TODO: Dovrebbe essere rimosso, va rivisto layout */}
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { useRouter } from 'expo-router';
|
||||
import { AlertTriangle, Bell, CheckCircle2, FileText, QrCode, User } from 'lucide-react-native';
|
||||
import React from 'react';
|
||||
import { AlertTriangle, CheckCircle2, FileText, QrCode, User } from 'lucide-react-native';
|
||||
import React, { useContext } from 'react';
|
||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { ATTENDANCE_DATA, DOCUMENTS_DATA, MOCK_USER } from '../../data/data';
|
||||
import { ATTENDANCE_DATA, DOCUMENTS_DATA } from '@/data/data';
|
||||
import { AuthContext } from '@/utils/authContext';
|
||||
|
||||
export default function HomeScreen() {
|
||||
const router = useRouter();
|
||||
const { user } = useContext(AuthContext);
|
||||
const incompleteTasks = ATTENDANCE_DATA.filter(item => item.status === 'incomplete');
|
||||
|
||||
return (
|
||||
@ -16,15 +18,11 @@ export default function HomeScreen() {
|
||||
<View className="flex-row items-center gap-4">
|
||||
<View>
|
||||
<Text className="text-teal-100 text-lg font-medium uppercase tracking-wider mb-1">Benvenuto</Text>
|
||||
<Text className="text-white text-3xl font-bold">{MOCK_USER.name} {MOCK_USER.surname}</Text>
|
||||
<Text className="text-teal-200">{MOCK_USER.role}</Text>
|
||||
<Text className="text-white text-3xl font-bold">{user?.name} {user?.surname}</Text>
|
||||
<Text className="text-xl text-teal-200">{user?.role}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row gap-4">
|
||||
<TouchableOpacity className="relative p-3 bg-white/10 rounded-full active:bg-white/20">
|
||||
<Bell size={28} color="white" />
|
||||
<View className="absolute top-2.5 right-3 w-3 h-3 bg-red-500 rounded-full border-2 border-[#099499]" />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity className="p-3 bg-white/10 rounded-full active:bg-white/20" onPress={() => router.push('/profile')}>
|
||||
<User size={28} color="white" />
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -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<string, (color: string) => JSX.Element> = {
|
||||
Ferie: (color) => <CalendarIcon size={24} color={color} />,
|
||||
Permesso: (color) => <Clock size={24} color={color} />,
|
||||
Malattia: (color) => <Thermometer size={24} color={color} />,
|
||||
Assenza: (color) => <CalendarX size={24} color={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<TimeOffRequest[]>([]);
|
||||
const [types, setTypes] = useState<TimeOffRequestType[]>([]);
|
||||
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 (
|
||||
<View className="flex-1 justify-center items-center bg-gray-50">
|
||||
<ActivityIndicator size="large" color="#099499" />
|
||||
<Text className="text-gray-500 mt-2">Caricamento...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
<RequestPermitModal
|
||||
visible={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
onSubmit={(data) => console.log('Richiesta:', data)}
|
||||
<RequestPermitModal
|
||||
visible={showModal}
|
||||
types={types}
|
||||
onClose={() => setShowModal(false)}
|
||||
onSubmit={(data) => { console.log('Richiesta:', data); fetchPermits(); }}
|
||||
/>
|
||||
|
||||
|
||||
{/* Header */}
|
||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100 flex-row justify-between items-center">
|
||||
<View>
|
||||
@ -40,132 +78,57 @@ export default function PermitsScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={{ padding: 20, paddingBottom: 100, gap: 24 }} showsVerticalScrollIndicator={false}>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 100, gap: 24 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
||||
}
|
||||
>
|
||||
|
||||
{/* Calendar Widget */}
|
||||
<View className="bg-white rounded-[2rem] p-6 shadow-sm border border-gray-100">
|
||||
<View className="flex-row justify-between items-center mb-6">
|
||||
<TouchableOpacity className="p-2 bg-gray-50 rounded-full">
|
||||
<ChevronLeft size={24} color="#374151" />
|
||||
</TouchableOpacity>
|
||||
<Text className="text-xl font-bold text-gray-800 capitalize">
|
||||
{currentDate.toLocaleString('it-IT', { month: 'long', year: 'numeric' })}
|
||||
</Text>
|
||||
<TouchableOpacity className="p-2 bg-gray-50 rounded-full">
|
||||
<ChevronRight size={24} color="#374151" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Week Header */}
|
||||
<View className="flex-row justify-between mb-4">
|
||||
{weekDays.map(day => (
|
||||
<Text key={day} className="w-10 text-center text-xs font-bold text-gray-400 uppercase">{day}</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Days Grid */}
|
||||
<View className="flex-row flex-wrap gap-y-4">
|
||||
{/* Empty slots for alignment */}
|
||||
{Array.from({ length: adjustedFirstDay }).map((_, i) => (
|
||||
<View key={`empty-${i}`} style={{ width: '14.28%' }} />
|
||||
))}
|
||||
{/* 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 (
|
||||
<View key={day} style={{ width: '14.28%' }} className="items-center">
|
||||
<View className={`w-10 h-10 rounded-full items-center justify-center border ${bgClass} ${borderClass}`}>
|
||||
<Text className={`text-sm ${textClass}`}>{day}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Legenda */}
|
||||
<View className="flex-row justify-center gap-4 mt-8 pt-4 border-t border-gray-100">
|
||||
<View className="flex-row items-center">
|
||||
<View className="w-3 h-3 rounded-full bg-purple-500 mr-2" />
|
||||
<Text className="text-xs font-medium text-gray-500">Ferie</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<View className="w-3 h-3 rounded-full bg-orange-500 mr-2" />
|
||||
<Text className="text-xs font-medium text-gray-500">Permessi</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<View className="w-3 h-3 rounded-full bg-red-500 mr-2" />
|
||||
<Text className="text-xs font-medium text-gray-500">Malattia</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<CalendarWidget events={permits} types={types} />
|
||||
|
||||
{/* Lista Richieste Recenti */}
|
||||
<View>
|
||||
<Text className="text-lg font-bold text-gray-800 mb-4 px-1">Le tue richieste</Text>
|
||||
<View className="gap-4">
|
||||
{PERMITS_DATA.map((item) => (
|
||||
<View key={item.id} className="bg-white p-5 rounded-3xl shadow-sm border border-gray-100 flex-row justify-between items-center">
|
||||
<View className="flex-row items-center gap-4">
|
||||
<View className={`p-4 rounded-2xl ${
|
||||
item.type === 'Ferie' ? 'bg-purple-50' :
|
||||
item.type === 'Permesso' ? 'bg-orange-50' : 'bg-red-50'
|
||||
}`}>
|
||||
{item.type === 'Ferie' && <CalendarIcon size={24} color="#9333ea" />}
|
||||
{item.type === 'Permesso' && <Clock size={24} color="#ea580c" />}
|
||||
{item.type === 'Malattia' && <Thermometer size={24} color="#dc2626" />}
|
||||
</View>
|
||||
<View>
|
||||
<Text className="font-bold text-gray-800 text-lg">{item.type}</Text>
|
||||
<Text className="text-sm text-gray-500 mt-0.5">
|
||||
{item.startDate} {item.endDate ? `- ${item.endDate}` : ''}
|
||||
</Text>
|
||||
{item.type === 'Permesso' && (
|
||||
<Text className="text-xs text-orange-600 font-bold mt-1">
|
||||
{item.startTime} - {item.endTime}
|
||||
{permits.length === 0 ? (
|
||||
<Text className="text-center text-gray-500 mt-8">Nessuna richiesta di permesso trovata.</Text>
|
||||
) : (
|
||||
<View className="gap-4">
|
||||
<Text className="text-xl font-bold text-gray-800 px-1">Le tue richieste</Text>
|
||||
{/* TODO: Aggiungere una paginazione con delle freccette affianco? */}
|
||||
{permits.map((item) => (
|
||||
<View key={item.id} className="bg-white p-5 rounded-3xl shadow-sm border border-gray-100 flex-row justify-between items-center">
|
||||
<View className="flex-row items-center gap-4">
|
||||
<View className={`p-4 rounded-2xl`} style={{ backgroundColor: item.timeOffRequestType.color ? `${item.timeOffRequestType.color}25` : '#E5E7EB' }}>
|
||||
{typeIcons[item.timeOffRequestType.name]?.(item.timeOffRequestType.color)}
|
||||
</View>
|
||||
<View>
|
||||
<Text className="font-bold text-gray-800 text-lg">{item.timeOffRequestType.name}</Text>
|
||||
<Text className="text-base text-gray-500 mt-0.5">
|
||||
{formatDate(item.start_date?.toLocaleString())} {item.end_date ? `- ${formatDate(item.end_date.toLocaleString())}` : ''}
|
||||
</Text>
|
||||
)}
|
||||
{item.timeOffRequestType.name === 'Permesso' && (
|
||||
<Text className="text-sm text-orange-600 font-bold mt-1">
|
||||
{formatTime(item.start_time)} - {formatTime(item.end_time)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View className={`px-3 py-1.5 rounded-lg ${item.status ? 'bg-green-100' : 'bg-yellow-100'}`}>
|
||||
<Text className={`text-xs font-bold uppercase tracking-wide ${item.status ? 'text-green-700' : 'text-yellow-700'}`}>
|
||||
{item.status ? 'Approvato' : 'In Attesa'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={`px-3 py-1.5 rounded-lg ${
|
||||
item.status === 'Approvato' ? 'bg-green-100' :
|
||||
item.status === 'In Attesa' ? 'bg-yellow-100' : 'bg-red-100'
|
||||
}`}>
|
||||
<Text className={`text-xs font-bold uppercase tracking-wide ${
|
||||
item.status === 'Approvato' ? 'text-green-700' :
|
||||
item.status === 'In Attesa' ? 'text-yellow-700' : 'text-red-700'
|
||||
}`}>
|
||||
{item.status}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* FAB */}
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
onPress={() => 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"
|
||||
>
|
||||
|
||||
@ -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 (
|
||||
<View className="flex-1 bg-[#099499]">
|
||||
@ -31,7 +29,7 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-teal-100 text-lg font-medium uppercase tracking-wider mb-1">Profilo</Text>
|
||||
<Text className="text-white text-2xl font-bold">{MOCK_USER.name} {MOCK_USER.surname}</Text>
|
||||
<Text className="text-white text-2xl font-bold">{user?.name} {user?.surname}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@ -56,7 +54,7 @@ export default function ProfileScreen() {
|
||||
<View>
|
||||
{/* Label e valore ingranditi */}
|
||||
<Text className="text-lg text-gray-700 font-bold">Email</Text>
|
||||
<Text className="text-gray-500 text-base">{email}</Text>
|
||||
<Text className="text-gray-500 text-base">{user?.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -77,7 +75,7 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-lg text-gray-700 font-bold">Ruolo</Text>
|
||||
<Text className="text-gray-500 text-base capitalize">{MOCK_USER.role}</Text>
|
||||
<Text className="text-gray-500 text-base capitalize">{user?.role}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user