From ed25c5299df2a1c36097bd1170e8d40e805ff233 Mon Sep 17 00:00:00 2001 From: Riccardo Mancini Date: Tue, 17 Feb 2026 16:12:15 +0100 Subject: [PATCH] Add swipe-to-delete functionality for requests and improve QR Code scanning --- .env.example | 3 +- app/(protected)/attendance/index.tsx | 5 +- app/(protected)/permits/index.tsx | 143 ++++++++++++++++++++++----- app/_layout.tsx | 29 +++--- components/AlertComponent.tsx | 101 +++++++++++++++---- components/QrScanModal.tsx | 8 +- components/RequestPermitModal.tsx | 33 ++++--- 7 files changed, 249 insertions(+), 73 deletions(-) diff --git a/.env.example b/.env.example index 256dec3..15ec0f2 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ EXPO_PUBLIC_API_URL=[YOUR_API_URL] EXPO_PUBLIC_HA_API_URL=[YOUR_HOME_ASSISTANT_API_URL] -EXPO_PUBLIC_HA_TOKEN=[YOUR_HOME_ASSISTANT_TOKEN] \ No newline at end of file +EXPO_PUBLIC_HA_TOKEN=[YOUR_HOME_ASSISTANT_TOKEN] +EXPO_PUBLIC_ENABLE_NFC=[true|false] diff --git a/app/(protected)/attendance/index.tsx b/app/(protected)/attendance/index.tsx index bb8b17c..f40fe06 100644 --- a/app/(protected)/attendance/index.tsx +++ b/app/(protected)/attendance/index.tsx @@ -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() { )} ); -} \ No newline at end of file +} diff --git a/app/(protected)/permits/index.tsx b/app/(protected)/permits/index.tsx index 58d6851..29ec6b4 100644 --- a/app/(protected)/permits/index.tsx +++ b/app/(protected)/permits/index.tsx @@ -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 JSX.Element> = { @@ -78,6 +79,73 @@ export default function PermitsScreen() { fetchPermits(); }; + // Funzione per eliminare una richiesta + const deletePermitRequest = async (id: number, itemRef?: React.ElementRef | 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 | 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 | null> + ) => { + return ( + confirmDelete(item, swipeableRef.current)} + className="bg-red-500 justify-center items-center px-6 rounded-3xl ml-3" + activeOpacity={0.7} + style={{margin: 2}} + > + + + Elimina + + + ); + }; + if (isLoading && !refreshing) { return ; } @@ -119,32 +187,57 @@ export default function PermitsScreen() { ) : ( Le tue richieste - {filteredPermits.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)} + {filteredPermits.map((item) => { + const swipeableRef = React.createRef>(); + const canDelete = item.status === null; // Solo "In Attesa" + const cardContent = ( + + + + {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===1 ? 'Approvata' : item.status===0 ? 'Rifiutata' : 'In Attesa'} + - {/* TODO: Add functionality to edit/remove the request */} - - - {item.status ? 'Approvato' : 'In Attesa'} - - - - ))} + ); + + // Wrappa solo richieste "In Attesa" con Swipeable + if (canDelete) { + return ( + + renderRightActions(progress, dragX, item, swipeableRef) + } + rightThreshold={40} + friction={2} + overshootFriction={8} + containerStyle={{padding: 2}} + > + {cardContent} + + ); + } + + // Richieste approvate senza swipe + return {cardContent}; + })} )} @@ -159,4 +252,4 @@ export default function PermitsScreen() { ); -} \ No newline at end of file +} diff --git a/app/_layout.tsx b/app/_layout.tsx index 63140bc..8162644 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -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 ( - - - - - - - - - - - - + + + + + + + + + + + + + + ); -} \ No newline at end of file +} diff --git a/components/AlertComponent.tsx b/components/AlertComponent.tsx index 4cf3a10..2d33a26 100644 --- a/components/AlertComponent.tsx +++ b/components/AlertComponent.tsx @@ -4,8 +4,21 @@ import { CheckCircle, XCircle, Info, AlertTriangle } from 'lucide-react-native'; type AlertType = 'success' | 'error' | 'info' | 'warning'; +type ConfirmButtonStyle = 'default' | 'destructive' | 'cancel'; + +interface ConfirmButton { + text: string; + onPress: () => void; + style?: ConfirmButtonStyle; +} + interface AlertContextData { showAlert: (type: AlertType, title: string, message: string) => void; + showConfirm: ( + title: string, + message: string, + buttons: [ConfirmButton, ConfirmButton] + ) => void; hideAlert: () => void; } @@ -44,11 +57,29 @@ export const AlertProvider = ({ children }: { children: ReactNode }) => { const [title, setTitle] = useState(''); const [message, setMessage] = useState(''); const [type, setType] = useState('info'); + const [isConfirmMode, setIsConfirmMode] = useState(false); + const [confirmButtons, setConfirmButtons] = useState<[ConfirmButton, ConfirmButton]>([ + { text: 'Annulla', onPress: () => {}, style: 'cancel' }, + { text: 'Conferma', onPress: () => {}, style: 'default' } + ]); const showAlert = (newType: AlertType, newTitle: string, newMessage: string) => { setType(newType); setTitle(newTitle); setMessage(newMessage); + setIsConfirmMode(false); + setVisible(true); + }; + + const showConfirm = ( + newTitle: string, + newMessage: string, + buttons: [ConfirmButton, ConfirmButton] + ) => { + setTitle(newTitle); + setMessage(newMessage); + setConfirmButtons(buttons); + setIsConfirmMode(true); setVisible(true); }; @@ -60,7 +91,7 @@ export const AlertProvider = ({ children }: { children: ReactNode }) => { // TODO: Need to refactor component styles return ( - + {children} { {/* Alert Container */} - - {/* Icon Circle */} - - - + + {/* Icon Circle - Solo per alert normali */} + {!isConfirmMode && ( + + + + )} {/* Texts */} {title} - + {message} - {/* OK Button */} - - - Ok, ho capito - - - + {/* Buttons - Condizionale */} + {isConfirmMode ? ( + // Conferma: 2 bottoni orizzontali + + {confirmButtons.map((button, index) => { + const isDestructive = button.style === 'destructive'; + const isCancel = button.style === 'cancel'; + + return ( + { + hideAlert(); + button.onPress(); + }} + className={`flex-1 py-3.5 rounded-3xl ${ + isDestructive + ? 'bg-red-600' + : isCancel + ? 'bg-gray-200' + : 'bg-[#099499]' + } active:opacity-90 shadow-sm`} + > + + {button.text} + + + ); + })} + + ) : ( + // Alert normale: singolo bottone OK + + + Ok, ho capito + + + )} + diff --git a/components/QrScanModal.tsx b/components/QrScanModal.tsx index 7cd31a8..47c9916 100644 --- a/components/QrScanModal.tsx +++ b/components/QrScanModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { View, Text, Modal, TouchableOpacity, Vibration, StyleSheet, Dimensions } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { CameraView, useCameraPermissions } from 'expo-camera'; @@ -13,6 +13,7 @@ interface QrScanModalProps { export default function QrScanModal({ visible, onClose, onScan }: QrScanModalProps) { const [permission, requestPermission] = useCameraPermissions(); const [scanned, setScanned] = useState(false); + const scanInProgress = useRef(false); const { width, height } = Dimensions.get('window'); const squareSize = Math.min(width * 0.8, height * 0.8, 400); @@ -20,6 +21,7 @@ export default function QrScanModal({ visible, onClose, onScan }: QrScanModalPro useEffect(() => { if (visible) { setScanned(false); + scanInProgress.current = false; if (permission && !permission.granted && permission.canAskAgain) { requestPermission(); } @@ -27,7 +29,9 @@ export default function QrScanModal({ visible, onClose, onScan }: QrScanModalPro }, [visible, permission]); const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => { - if (scanned) return; + if (scanInProgress.current) return; + scanInProgress.current = true; + setScanned(true); Vibration.vibrate(); console.log(`Bar code with type ${type} and data ${data} has been scanned!`); diff --git a/components/RequestPermitModal.tsx b/components/RequestPermitModal.tsx index 29a12c7..7109807 100644 --- a/components/RequestPermitModal.tsx +++ b/components/RequestPermitModal.tsx @@ -7,7 +7,7 @@ import { TimePickerModal } from './TimePickerModal'; import api from '@/utils/api'; import { formatPickerDate } from '@/utils/dateTime'; import { AppDatePicker } from '@/components/AppDatePicker'; -import { KeyboardAvoidingView } from 'react-native-keyboard-controller'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; interface RequestPermitModalProps { visible: boolean; @@ -116,13 +116,19 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } - - - + {/* Permit Type */} Tipologia Assenza @@ -181,6 +187,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } value={startTime} onChangeText={setStartTime} editable={false} + pointerEvents="none" /> @@ -194,6 +201,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } value={endTime} onChangeText={setEndTime} editable={false} + pointerEvents="none" /> @@ -222,10 +230,12 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } @@ -249,11 +259,10 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit } Invia Richiesta - - - + + ); -}; \ No newline at end of file +};