Add swipe-to-delete functionality for requests and improve QR Code scanning

This commit is contained in:
2026-02-17 16:12:15 +01:00
parent 68e25fac71
commit ed25c5299d
7 changed files with 249 additions and 73 deletions

View File

@@ -21,8 +21,7 @@ export default function AttendanceScreen() {
const [refreshing, setRefreshing] = useState(false);
const checkNfcAvailability = async () => {
// TODO: add env variable to disable NFC checks in development or if not needed
// if (!ENABLE_NFC) return;
if (process.env.EXPO_PUBLIC_ENABLE_NFC!=='true') return;
try {
const isSupported = await NfcManager.isSupported();
if (isSupported) setScannerType('nfc');
@@ -252,4 +251,4 @@ export default function AttendanceScreen() {
)}
</View>
);
}
}

View File

@@ -5,10 +5,11 @@ 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 } from 'lucide-react-native';
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> = {
@@ -78,6 +79,73 @@ export default function PermitsScreen() {
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 />;
}
@@ -119,32 +187,57 @@ export default function PermitsScreen() {
) : (
<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)}
{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>
{/* TODO: Add functionality to edit/remove the request */}
<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>
))}
);
// 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>
@@ -159,4 +252,4 @@ export default function PermitsScreen() {
</TouchableOpacity>
</View>
);
}
}

View File

@@ -5,22 +5,25 @@ import { AlertProvider } from '@/components/AlertComponent';
import { NetworkProvider } from '@/utils/networkProvider';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { KeyboardProvider } from "react-native-keyboard-controller";
import {GestureHandlerRootView} from "react-native-gesture-handler";
export default function AppLayout() {
return (
<SafeAreaProvider>
<KeyboardProvider>
<NetworkProvider>
<AuthProvider>
<AlertProvider>
<Stack screenOptions={{ headerShown: false, animation: 'flip' }}>
<Stack.Screen name="(protected)" />
<Stack.Screen name="login" />
</Stack>
</AlertProvider>
</AuthProvider>
</NetworkProvider>
</KeyboardProvider>
<GestureHandlerRootView>
<KeyboardProvider>
<NetworkProvider>
<AuthProvider>
<AlertProvider>
<Stack screenOptions={{ headerShown: false, animation: 'flip' }}>
<Stack.Screen name="(protected)" />
<Stack.Screen name="login" />
</Stack>
</AlertProvider>
</AuthProvider>
</NetworkProvider>
</KeyboardProvider>
</GestureHandlerRootView>
</SafeAreaProvider>
);
}
}