256 lines
12 KiB
TypeScript
256 lines
12 KiB
TypeScript
import { useAlert } from '@/components/AlertComponent';
|
|
import CalendarWidget from '@/components/CalendarWidget';
|
|
import LoadingScreen from '@/components/LoadingScreen';
|
|
import RequestPermitModal from '@/components/RequestPermitModal';
|
|
import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
|
import api from '@/utils/api';
|
|
import { formatDate, formatTime } from '@/utils/dateTime';
|
|
import { Calendar as CalendarIcon, CalendarX, Clock, Plus, Thermometer, Trash2 } from 'lucide-react-native';
|
|
import React, { JSX, useEffect, useMemo, useState } from 'react';
|
|
import { RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable';
|
|
|
|
// Icon Mapping
|
|
const typeIcons: Record<string, (color: string) => JSX.Element> = {
|
|
Ferie: (color) => <CalendarIcon size={24} color={color} pointerEvents="none" />,
|
|
Permesso: (color) => <Clock size={24} color={color} pointerEvents="none" />,
|
|
Malattia: (color) => <Thermometer size={24} color={color} pointerEvents="none" />,
|
|
Assenza: (color) => <CalendarX size={24} color={color} pointerEvents="none" />,
|
|
};
|
|
|
|
export default function PermitsScreen() {
|
|
const [showModal, setShowModal] = useState(false);
|
|
const alert = useAlert();
|
|
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.showAlert('error', 'Errore', 'Impossibile recuperare i permessi. Riprova più tardi.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
const filteredPermits = useMemo(() => {
|
|
if (!permits.length) return [];
|
|
|
|
// Calculate start and end of the current month
|
|
const year = currentMonthDate.getFullYear();
|
|
const month = currentMonthDate.getMonth();
|
|
|
|
const startOfMonth = new Date(year, month, 1);
|
|
// Day 0 of the next month = last day of the current month
|
|
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);
|
|
|
|
return permits.filter(item => {
|
|
const itemStart = new Date(item.start_date?.toString() ?? '');
|
|
// If there's no end_date, assume it's a single day (so end = start)
|
|
const itemEnd = item.end_date ? new Date(item.end_date?.toString() ?? '') : new Date(item.start_date?.toString() ?? '');
|
|
|
|
// The permit is visible if it starts before the end of the month
|
|
// And ends after the start of the month.
|
|
return itemStart <= endOfMonth && itemEnd >= startOfMonth;
|
|
});
|
|
}, [permits, currentMonthDate]);
|
|
|
|
useEffect(() => {
|
|
fetchPermits();
|
|
}, []);
|
|
|
|
const onRefresh = () => {
|
|
setRefreshing(true);
|
|
fetchPermits();
|
|
};
|
|
|
|
// Funzione per eliminare una richiesta
|
|
const deletePermitRequest = async (id: number, itemRef?: React.ElementRef<typeof Swipeable> | null) => {
|
|
try {
|
|
itemRef?.close();
|
|
await api.post(`/time-off-request/delete-request`, {id: id});
|
|
// Optimistic update
|
|
setPermits(prevPermits => prevPermits.filter(p => p.id !== id));
|
|
alert.showAlert('success', 'Richiesta eliminata', 'La richiesta è stata eliminata con successo.');
|
|
// Refresh
|
|
fetchPermits();
|
|
} catch (error: any) {
|
|
console.error('Errore eliminazione richiesta:', error);
|
|
|
|
const errorMessage = error?.response?.data?.message || 'Impossibile eliminare la richiesta.';
|
|
alert.showAlert('error', 'Errore', errorMessage);
|
|
|
|
fetchPermits(); // Ripristina stato corretto
|
|
}
|
|
};
|
|
|
|
// Dialogo di conferma
|
|
const confirmDelete = (item: TimeOffRequest, itemRef?: React.ElementRef<typeof Swipeable> | null) => {
|
|
const requestType = item.timeOffRequestType.name;
|
|
const dateRange = item.end_date
|
|
? `${formatDate(item.start_date?.toLocaleString())} - ${formatDate(item.end_date.toLocaleString())}`
|
|
: formatDate(item.start_date?.toLocaleString());
|
|
|
|
alert.showConfirm(
|
|
'Conferma eliminazione',
|
|
`Sei sicuro di voler eliminare questa richiesta?\n\n${requestType}\n${dateRange}`,
|
|
[
|
|
{
|
|
text: 'Annulla',
|
|
style: 'cancel',
|
|
onPress: () => itemRef?.close()
|
|
},
|
|
{
|
|
text: 'Elimina',
|
|
style: 'destructive',
|
|
onPress: () => deletePermitRequest(item.id, itemRef)
|
|
}
|
|
]
|
|
);
|
|
};
|
|
|
|
// Renderizza pulsante DELETE al swipe
|
|
const renderRightActions = (
|
|
progress: any,
|
|
dragX: any,
|
|
item: TimeOffRequest,
|
|
swipeableRef: React.RefObject<React.ElementRef<typeof Swipeable> | null>
|
|
) => {
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={() => confirmDelete(item, swipeableRef.current)}
|
|
className="bg-red-500 justify-center items-center px-6 rounded-3xl ml-3"
|
|
activeOpacity={0.7}
|
|
style={{margin: 2}}
|
|
>
|
|
<View className="items-center gap-1">
|
|
<Trash2 size={24} color="white" strokeWidth={2.5} pointerEvents="none" />
|
|
<Text className="text-white font-bold text-sm">Elimina</Text>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
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 px-6 pb-6 shadow-sm border-b border-gray-100 flex-row justify-between items-center">
|
|
<SafeAreaView edges={['top']} className='pt-5'>
|
|
<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>
|
|
</SafeAreaView>
|
|
</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)} />
|
|
|
|
{/* Recent Requests List */}
|
|
<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) => {
|
|
const swipeableRef = React.createRef<React.ElementRef<typeof Swipeable>>();
|
|
const canDelete = item.status === null; // Solo "In Attesa"
|
|
const cardContent = (
|
|
<View 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===1 ? 'bg-green-100' : item.status===0 ? 'bg-red-100' : 'bg-yellow-100'}`}>
|
|
<Text className={`text-xs font-bold uppercase tracking-wide ${item.status===1 ? 'text-green-700' : item.status===0 ? 'text-red-700' : 'text-yellow-700'}`}>
|
|
{item.status===1 ? 'Approvata' : item.status===0 ? 'Rifiutata' : 'In Attesa'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
|
|
// Wrappa solo richieste "In Attesa" con Swipeable
|
|
if (canDelete) {
|
|
return (
|
|
<Swipeable
|
|
key={item.id}
|
|
ref={swipeableRef}
|
|
renderRightActions={(progress, dragX) =>
|
|
renderRightActions(progress, dragX, item, swipeableRef)
|
|
}
|
|
rightThreshold={40}
|
|
friction={2}
|
|
overshootFriction={8}
|
|
containerStyle={{padding: 2}}
|
|
>
|
|
{cardContent}
|
|
</Swipeable>
|
|
);
|
|
}
|
|
|
|
// Richieste approvate senza swipe
|
|
return <View key={item.id}>{cardContent}</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" pointerEvents="none" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|