- 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.
252 lines
12 KiB
TypeScript
252 lines
12 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { View, Text, TouchableOpacity, ScrollView, Alert, RefreshControl } from 'react-native';
|
|
import { QrCode, CheckCircle2, Nfc } from 'lucide-react-native';
|
|
import QrScanModal from '@/components/QrScanModal';
|
|
import NfcScanModal from '@/components/NfcScanModal';
|
|
import LoadingScreen from '@/components/LoadingScreen';
|
|
import api from '@/utils/api';
|
|
import { formatDate, formatTime, parseSecondsToTime } from '@/utils/dateTime';
|
|
import { AttendanceRecord } from '@/types/types';
|
|
import NfcManager from 'react-native-nfc-manager';
|
|
|
|
export default function AttendanceScreen() {
|
|
const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr');
|
|
const [showScanner, setShowScanner] = useState(false);
|
|
const [lastScan, setLastScan] = useState<{ type: string; time: string; site: string } | null>(null);
|
|
const [attendances, setAttendances] = useState<AttendanceRecord[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const checkNfcAvailability = async () => {
|
|
// if (!ENABLE_NFC) return;
|
|
try {
|
|
const isSupported = await NfcManager.isSupported();
|
|
if (isSupported) setScannerType('nfc');
|
|
} catch (error) {
|
|
console.warn('NFC non supportato:', error);
|
|
}
|
|
};
|
|
|
|
const fetchAttendances = async () => {
|
|
try {
|
|
if (!refreshing) setIsLoading(true);
|
|
|
|
// Fetch today's attendance data from API
|
|
const response = await api.get('/attendance/list');
|
|
setAttendances(response.data);
|
|
} catch (error) {
|
|
console.error('Errore nel recupero delle presenze:', error);
|
|
Alert.alert('Errore', 'Impossibile recuperare le presenze. Riprova più tardi.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
checkNfcAvailability();
|
|
fetchAttendances();
|
|
setLastScan(null);
|
|
}, []);
|
|
|
|
const onRefresh = () => {
|
|
setRefreshing(true);
|
|
fetchAttendances();
|
|
setLastScan(null);
|
|
};
|
|
|
|
const handleStartScan = async () => {
|
|
// Modalità QR Code
|
|
if (scannerType === 'qr') {
|
|
setShowScanner(true);
|
|
return;
|
|
}
|
|
|
|
// Modalità NFC
|
|
if (scannerType === 'nfc') {
|
|
try {
|
|
const supported = await NfcManager.isSupported();
|
|
if (!supported) {
|
|
Alert.alert('NFC non supportato', 'Il tuo dispositivo non supporta la scansione NFC.');
|
|
return;
|
|
}
|
|
|
|
const enabled = await NfcManager.isEnabled();
|
|
if (!enabled) {
|
|
Alert.alert('NFC disattivato', 'Per favore attiva l\'NFC nelle impostazioni del dispositivo per continuare.', [
|
|
{ text: 'OK' },
|
|
{ text: 'Vai alle impostazioni', onPress: () => NfcManager.goToNfcSetting() }
|
|
]);
|
|
return;
|
|
}
|
|
|
|
setShowScanner(true);
|
|
} catch (err) {
|
|
console.warn(err);
|
|
Alert.alert('Errore', 'Impossibile verificare lo stato dell\'NFC.');
|
|
}
|
|
}
|
|
};
|
|
|
|
const onScan = async (data: string) => {
|
|
console.log('Scanned data:', data);
|
|
try {
|
|
// Send scanned data to API
|
|
const response = await api.post('/attendance/scan', data);
|
|
if (response) {
|
|
console.log('Scan data sent successfully:', response.data);
|
|
// Refresh attendance list
|
|
fetchAttendances();
|
|
// Update last scan feedback
|
|
setLastScan({
|
|
type: response.data.type,
|
|
time: formatTime(response.data.time),
|
|
site: response.data.site
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Errore nell\'invio dei dati di scansione:', error);
|
|
Alert.alert('Errore', 'Impossibile registrare la presenza. Riprova più tardi.');
|
|
return;
|
|
}
|
|
};
|
|
|
|
if (isLoading && !refreshing) {
|
|
return <LoadingScreen />;
|
|
}
|
|
|
|
return (
|
|
<View className="flex-1 bg-gray-50">
|
|
{/* Header */}
|
|
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100">
|
|
<Text className="text-3xl font-bold text-gray-800 mb-1">Gestione Presenze</Text>
|
|
<Text className="text-base text-gray-500">Registra i tuoi movimenti</Text>
|
|
</View>
|
|
|
|
<ScrollView
|
|
contentContainerStyle={{ paddingBottom: 40 }}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
|
}
|
|
>
|
|
<View className="flex-1 p-5 items-center">
|
|
|
|
{/* Feedback Card */}
|
|
{lastScan ? (
|
|
<View className="w-full bg-green-50 border border-green-200 rounded-3xl p-5 mb-8 flex-row items-center gap-4 shadow-sm">
|
|
<View className="bg-green-500 rounded-full p-3 shadow-lg shadow-green-500/40 flex-shrink-0">
|
|
<CheckCircle2 size={32} color="white" />
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text
|
|
className="font-bold text-green-800 text-xl leading-tight"
|
|
numberOfLines={1}
|
|
ellipsizeMode="tail"
|
|
>
|
|
{lastScan.type} Registrata
|
|
</Text>
|
|
|
|
<Text
|
|
className="text-base text-green-700 font-medium mt-0.5 leading-snug"
|
|
numberOfLines={2}
|
|
ellipsizeMode="tail"
|
|
>
|
|
{lastScan.site} alle {lastScan.time}
|
|
</Text>
|
|
</View>
|
|
|
|
</View>
|
|
) : null}
|
|
|
|
{/* Scanner Section */}
|
|
<View className="w-full mb-6">
|
|
<View className="bg-white rounded-3xl p-8 shadow-sm border border-gray-100">
|
|
<Text className="text-2xl font-bold text-gray-800 mb-6 text-center">Scansione {scannerType === 'qr' ? 'QR Code' : 'NFC'}</Text>
|
|
|
|
<TouchableOpacity
|
|
onPress={handleStartScan}
|
|
className="bg-[#099499] rounded-2xl py-6 flex-row items-center justify-center active:bg-[#077d82] shadow-lg shadow-teal-900/20 active:scale-[0.98]"
|
|
>
|
|
{scannerType === 'qr' ? <QrCode color="white" size={32} /> : <Nfc color="white" size={32} />}
|
|
<Text className="text-white text-xl font-bold ml-3">Scansiona Codice</Text>
|
|
</TouchableOpacity>
|
|
|
|
<Text className="text-gray-500 text-center mt-6 text-base px-2 leading-relaxed">
|
|
{scannerType === 'qr'
|
|
? 'Posiziona il codice QR davanti alla fotocamera per registrare l\'ingresso o l\'uscita dal cantiere'
|
|
: 'Avvicina il dispositivo NFC per registrare l\'ingresso o l\'uscita dal cantiere'
|
|
}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Mini History */}
|
|
<View className="w-full mt-4">
|
|
<Text className="text-gray-500 font-bold text-base mb-4 uppercase tracking-wider px-2">Ultime Presenze</Text>
|
|
{attendances.length === 0 ? (
|
|
<Text className="text-center text-gray-500 p-6">Nessuna presenza registrata</Text>
|
|
) : (
|
|
<View className="bg-white rounded-3xl shadow-sm overflow-hidden border border-gray-100">
|
|
{attendances.map((item, index) => (
|
|
<View
|
|
key={item.id}
|
|
className={`p-5 flex-row justify-between items-center ${index !== 0 ? 'border-t border-gray-100' : ''}`}
|
|
>
|
|
<View className="flex-row items-center gap-4 flex-1 mr-2">
|
|
<View className={`w-3 h-3 rounded-full shadow-sm flex-shrink-0 ${item.out != null ? 'bg-green-500' : 'bg-orange-500'}`} />
|
|
<View className="flex-1">
|
|
<Text
|
|
className="font-bold text-gray-800 text-lg leading-tight"
|
|
numberOfLines={1}
|
|
ellipsizeMode="tail"
|
|
>
|
|
{item.site}
|
|
</Text>
|
|
|
|
<View className="flex-row items-center mt-1">
|
|
<Text className="text-base text-gray-400 font-medium whitespace-nowrap">
|
|
{formatDate(item.date)}
|
|
</Text>
|
|
<View className="w-1 h-1 rounded-full bg-gray-300 mx-2" />
|
|
<Text className={`text-base ${item.out != null ? 'text-gray-400' : 'text-orange-400'} font-medium whitespace-nowrap`}>
|
|
{formatTime(item.in)} - {item.out ? formatTime(item.out) : 'IN CORSO'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* TODO: item.time può essere null -> calcolare tempo da in e out? */}
|
|
{item.time && (
|
|
<View className="bg-gray-50 px-3 py-1.5 rounded-xl border border-gray-100 flex-shrink-0">
|
|
<Text className="text-base font-mono text-gray-600 font-bold">
|
|
{parseSecondsToTime(item.time)}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* Scanner Modal */}
|
|
{scannerType === 'qr' ? (
|
|
<QrScanModal
|
|
visible={showScanner}
|
|
onClose={() => setShowScanner(false)}
|
|
onScan={onScan}
|
|
/>
|
|
) : (
|
|
<NfcScanModal
|
|
visible={showScanner}
|
|
onClose={() => setShowScanner(false)}
|
|
onScan={onScan}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
} |