- Improved error message handling in LoginScreen for invalid credentials. - Added new images: mariani-icon.png and mariani-splash.png. - Updated AddDocumentModal to handle file extensions and improve UI. - Enhanced CalendarWidget to support month change callbacks. - Introduced NfcScanModal for NFC tag scanning with animations. - Revamped QrScanModal to utilize camera for QR code scanning. - Removed mock data from data.ts and streamlined Office data. - Updated package dependencies for expo-camera and react-native-nfc-manager. - Added utility function to parse seconds to time format. - Refactored document upload logic to use FormData for server uploads.
158 lines
7.6 KiB
TypeScript
158 lines
7.6 KiB
TypeScript
import React, { JSX, useEffect, useMemo, 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 LoadingScreen from '@/components/LoadingScreen';
|
|
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 [showModal, setShowModal] = useState(false);
|
|
const [permits, setPermits] = useState<TimeOffRequest[]>([]);
|
|
const [types, setTypes] = useState<TimeOffRequestType[]>([]);
|
|
const [currentMonthDate, setCurrentMonthDate] = useState(new Date());
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
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 filteredPermits = useMemo(() => {
|
|
if (!permits.length) return [];
|
|
|
|
// Calcoliamo inizio e fine del mese visualizzato
|
|
const year = currentMonthDate.getFullYear();
|
|
const month = currentMonthDate.getMonth();
|
|
|
|
const startOfMonth = new Date(year, month, 1);
|
|
// Trucco JS: giorno 0 del mese successivo = ultimo giorno del mese corrente
|
|
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);
|
|
|
|
return permits.filter(item => {
|
|
const itemStart = new Date(item.start_date?.toString() ?? '');
|
|
// Se non c'è end_date, assumiamo sia un giorno singolo (quindi end = start)
|
|
const itemEnd = item.end_date ? new Date(item.end_date?.toString() ?? '') : new Date(item.start_date?.toString() ?? '');
|
|
|
|
// FORMULA OVERLAP:
|
|
// Il permesso è visibile se inizia prima della fine del mese
|
|
// E finisce dopo l'inizio del mese.
|
|
return itemStart <= endOfMonth && itemEnd >= startOfMonth;
|
|
});
|
|
}, [permits, currentMonthDate]);
|
|
|
|
useEffect(() => {
|
|
fetchPermits();
|
|
}, []);
|
|
|
|
const onRefresh = () => {
|
|
setRefreshing(true);
|
|
fetchPermits();
|
|
};
|
|
|
|
if (isLoading && !refreshing) {
|
|
return <LoadingScreen />;
|
|
}
|
|
|
|
return (
|
|
<View className="flex-1 bg-gray-50">
|
|
<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>
|
|
<Text className="text-3xl font-bold text-gray-800 mb-1">Ferie e Permessi</Text>
|
|
<Text className="text-base text-gray-500">Gestisci le tue assenze</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<ScrollView
|
|
contentContainerStyle={{ padding: 20, paddingBottom: 100, gap: 24 }}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
|
}
|
|
>
|
|
|
|
{/* Calendar Widget */}
|
|
<CalendarWidget events={permits} types={types} onMonthChange={(date) => setCurrentMonthDate(date)} />
|
|
|
|
{/* Lista Richieste Recenti */}
|
|
<View>
|
|
{filteredPermits.length === 0 ? (
|
|
<Text className="text-center text-gray-500 mt-8">Nessuna richiesta di permesso questo mese</Text>
|
|
) : (
|
|
<View className="gap-4">
|
|
<Text className="text-xl font-bold text-gray-800 px-1">Le tue richieste</Text>
|
|
{filteredPermits.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>
|
|
{/* TODO: Aggiungere funzionalità per modificare/eliminare la richiesta? */}
|
|
<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>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* FAB */}
|
|
<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"
|
|
>
|
|
<Plus size={32} color="white" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
} |