feat: Add document download and upload. Add NFC support and enhance attendance and permits views
- 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.
This commit is contained in:
@ -23,14 +23,15 @@ export default function ProtectedLayout() {
|
||||
backgroundColor: '#ffffff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#f3f4f6',
|
||||
height: 80,
|
||||
paddingBottom: 20,
|
||||
height: 90,
|
||||
paddingBottom: 30,
|
||||
paddingTop: 10,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
tabBarActiveTintColor: '#099499',
|
||||
tabBarInactiveTintColor: '#9ca3af',
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 10,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: 4
|
||||
}
|
||||
|
||||
@ -1,24 +1,39 @@
|
||||
import React, { use, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, Alert, RefreshControl } from 'react-native';
|
||||
import { QrCode, CheckCircle2 } from 'lucide-react-native';
|
||||
import { ATTENDANCE_DATA } from '@/data/data';
|
||||
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(ATTENDANCE_DATA);
|
||||
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);
|
||||
setAttendances(response.data);
|
||||
} catch (error) {
|
||||
console.error('Errore nel recupero delle presenze:', error);
|
||||
Alert.alert('Errore', 'Impossibile recuperare le presenze. Riprova più tardi.');
|
||||
@ -29,26 +44,71 @@ export default function AttendanceScreen() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkNfcAvailability();
|
||||
fetchAttendances();
|
||||
setLastScan(null);
|
||||
}, []);
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchAttendances();
|
||||
setLastScan(null);
|
||||
};
|
||||
|
||||
const handleQRScan = () => {
|
||||
setShowScanner(true);
|
||||
// Simulate scanning process
|
||||
setTimeout(() => {
|
||||
setShowScanner(false);
|
||||
// Add new entry or update existing one
|
||||
setLastScan({
|
||||
type: 'Entrata',
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
site: 'Cantiere Ospedale A.'
|
||||
});
|
||||
}, 3000);
|
||||
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) {
|
||||
@ -72,36 +132,51 @@ export default function AttendanceScreen() {
|
||||
>
|
||||
<View className="flex-1 p-5 items-center">
|
||||
|
||||
{/* Feedback Card - OPZIONALE */}
|
||||
{/* Feedback Card */}
|
||||
{lastScan ? (
|
||||
<View className="w-full bg-green-50 border border-green-200 rounded-3xl p-6 mb-8 flex-row items-center gap-5 shadow-sm">
|
||||
<View className="bg-green-500 rounded-full p-3 shadow-lg shadow-green-500/40">
|
||||
<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>
|
||||
<Text className="font-bold text-green-800 text-xl">{lastScan.type} Registrata!</Text>
|
||||
<Text className="text-base text-green-700 font-medium">{lastScan.site} alle {lastScan.time}</Text>
|
||||
<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
|
||||
)}
|
||||
) : 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 QR/NFC</Text>
|
||||
<Text className="text-2xl font-bold text-gray-800 mb-6 text-center">Scansione {scannerType === 'qr' ? 'QR Code' : 'NFC'}</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleQRScan}
|
||||
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]"
|
||||
>
|
||||
<QrCode color="white" size={32} />
|
||||
{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">
|
||||
Posiziona il codice QR davanti alla fotocamera o usa il lettore NFC
|
||||
{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>
|
||||
@ -109,34 +184,69 @@ export default function AttendanceScreen() {
|
||||
{/* 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>
|
||||
<View className="bg-white rounded-3xl shadow-sm overflow-hidden border border-gray-100">
|
||||
{ATTENDANCE_DATA.map((item, index) => (
|
||||
<View key={item.id} className={`p-6 flex-row justify-between items-center ${index !== 0 ? 'border-t border-gray-100' : ''}`}>
|
||||
<View className="flex-row items-center gap-4">
|
||||
<View className={`w-3 h-3 rounded-full shadow-sm ${item.status === 'complete' ? 'bg-green-500' : 'bg-orange-500'}`} />
|
||||
<View>
|
||||
<Text className="font-bold text-gray-800 text-lg mb-0.5">{item.site}</Text>
|
||||
<Text className="text-base text-gray-400 font-medium">{item.in} - {item.out || 'In corso'}</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>
|
||||
{item.status === 'complete' && (
|
||||
<View className="bg-gray-100 px-3 py-1.5 rounded-lg">
|
||||
<Text className="text-base font-mono text-gray-600 font-bold">8h</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Scanner Modal */}
|
||||
<QrScanModal
|
||||
visible={showScanner}
|
||||
onClose={() => setShowScanner(false)}
|
||||
/>
|
||||
{scannerType === 'qr' ? (
|
||||
<QrScanModal
|
||||
visible={showScanner}
|
||||
onClose={() => setShowScanner(false)}
|
||||
onScan={onScan}
|
||||
/>
|
||||
) : (
|
||||
<NfcScanModal
|
||||
visible={showScanner}
|
||||
onClose={() => setShowScanner(false)}
|
||||
onScan={onScan}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@ -1,28 +1,81 @@
|
||||
import { useRouter } from 'expo-router';
|
||||
import { AlertTriangle, CheckCircle2, FileText, QrCode, User } from 'lucide-react-native';
|
||||
import React, { useContext } from 'react';
|
||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { ATTENDANCE_DATA, DOCUMENTS_DATA } from '@/data/data';
|
||||
import { AlertTriangle, CalendarDays, CheckCircle2, FileText, QrCode, User, CalendarClock, LayoutDashboard } from 'lucide-react-native';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { AuthContext } from '@/utils/authContext';
|
||||
import { ActivityItem } from '@/types/types';
|
||||
import api from '@/utils/api';
|
||||
import LoadingScreen from '@/components/LoadingScreen';
|
||||
|
||||
export default function HomeScreen() {
|
||||
const router = useRouter();
|
||||
const { user } = useContext(AuthContext);
|
||||
const incompleteTasks = ATTENDANCE_DATA.filter(item => item.status === 'incomplete');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [incompleteAttendance, setIncompleteAttendance] = useState<string | null>(null);
|
||||
const [recentActivities, setRecentActivities] = useState<ActivityItem[]>([]);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
if (!refreshing) setIsLoading(true);
|
||||
// Fetch incomplete attendance data from API
|
||||
const attendance = await api.get('/attendance/incomplete');
|
||||
setIncompleteAttendance(attendance.data);
|
||||
|
||||
// Fetch recent activities data from API
|
||||
const activities = await api.get('/user/recent-activities');
|
||||
setRecentActivities(activities.data);
|
||||
} catch (error) {
|
||||
console.error('Errore nel recupero dei dati della dashboard:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getActivityConfig = (type: string) => {
|
||||
switch (type) {
|
||||
case 'document':
|
||||
return { icon: FileText, bg: 'bg-gray-100', color: '#4b5563' };
|
||||
case 'attendance':
|
||||
return { icon: CheckCircle2, bg: 'bg-[#099499]/10', color: '#099499' };
|
||||
case 'permit':
|
||||
return { icon: CalendarClock, bg: 'bg-[#2563eb]/10', color: '#2563eb' };
|
||||
default:
|
||||
return { icon: LayoutDashboard, bg: 'bg-gray-100', color: '#9ca3af' };
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchDashboardData();
|
||||
};
|
||||
|
||||
if (isLoading && !refreshing) {
|
||||
return (
|
||||
<LoadingScreen />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-[#099499]">
|
||||
{/* Banner Custom */}
|
||||
<View className="pt-16 pb-6 px-6 shadow-sm z-10">
|
||||
<View className="flex-row justify-between items-start">
|
||||
<View className="flex-row items-center gap-4">
|
||||
<View>
|
||||
<View className="flex-row items-center gap-4 flex-1 mr-4">
|
||||
<View className="flex-1">
|
||||
<Text className="text-teal-100 text-lg font-medium uppercase tracking-wider mb-1">Benvenuto</Text>
|
||||
<Text className="text-white text-3xl font-bold">{user?.name} {user?.surname}</Text>
|
||||
<Text className="text-white text-3xl font-bold leading-tight">
|
||||
{user?.name} {user?.surname}
|
||||
</Text>
|
||||
<Text className="text-xl text-teal-200">{user?.role}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row gap-4">
|
||||
<View className="flex-row gap-4 flex-shrink-0">
|
||||
<TouchableOpacity className="p-3 bg-white/10 rounded-full active:bg-white/20" onPress={() => router.push('/profile')}>
|
||||
<User size={28} color="white" />
|
||||
</TouchableOpacity>
|
||||
@ -35,18 +88,21 @@ export default function HomeScreen() {
|
||||
className="flex-1 bg-gray-50 rounded-t-[2.5rem] px-5 pt-6"
|
||||
contentContainerStyle={{ paddingBottom: 50, gap: 24 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
||||
}
|
||||
>
|
||||
|
||||
{/* Warning Card - OPZIONALE */}
|
||||
{incompleteTasks.length > 0 && (
|
||||
<View className="bg-white p-6 rounded-3xl shadow-sm border-l-8 border-orange-500 flex-row items-center justify-between">
|
||||
{/* Warning Card */}
|
||||
{incompleteAttendance && (
|
||||
<View className="bg-white p-6 rounded-3xl shadow-md border-l-8 border-orange-500 flex-row items-center justify-between">
|
||||
<View className="flex-row items-center gap-5 flex-1">
|
||||
<View className="bg-orange-100 p-4 rounded-full">
|
||||
<AlertTriangle size={32} color="#f97316" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="font-bold text-gray-800 text-lg">Presenza incompleta</Text>
|
||||
<Text className="text-base text-gray-500 mt-1">{incompleteTasks[0].site}</Text>
|
||||
<Text className="text-base text-gray-500 mt-1">{incompleteAttendance}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => router.push('/attendance')} className="bg-orange-50 px-5 py-3 rounded-xl ml-2 active:bg-orange-100">
|
||||
@ -70,13 +126,13 @@ export default function HomeScreen() {
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/documents')}
|
||||
onPress={() => router.push('/permits')}
|
||||
className="flex-1 bg-white p-6 rounded-3xl shadow-sm items-center justify-center gap-4 border border-gray-100 active:scale-[0.98]"
|
||||
>
|
||||
<View className="w-20 h-20 rounded-full bg-blue-50 items-center justify-center mb-1">
|
||||
<FileText size={40} color="#2563eb" />
|
||||
<CalendarDays size={40} color="#2563eb" />
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-gray-700 text-center">Carica Documento</Text>
|
||||
<Text className="text-lg font-bold text-gray-700 text-center">Gestisci Permessi</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@ -85,37 +141,62 @@ export default function HomeScreen() {
|
||||
<View>
|
||||
<View className="flex-row justify-between items-center px-1 mb-4">
|
||||
<Text className="text-gray-800 text-xl font-bold">Ultime Attività</Text>
|
||||
<TouchableOpacity>
|
||||
{/* <TouchableOpacity>
|
||||
<Text className="text-base text-[#099499] font-bold p-1">Vedi tutto</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity> */}
|
||||
</View>
|
||||
<View className="gap-4">
|
||||
{DOCUMENTS_DATA.slice(0, 2).map((doc, i) => (
|
||||
<View key={i} className="bg-white p-5 rounded-2xl shadow-sm flex-row items-center justify-between border border-gray-100">
|
||||
<View className="flex-row items-center gap-5">
|
||||
<View className="bg-gray-100 p-4 rounded-2xl">
|
||||
<FileText size={28} color="#4b5563" />
|
||||
{recentActivities.map((item) => {
|
||||
const config = getActivityConfig(item.type);
|
||||
const IconComponent = config.icon;
|
||||
return (
|
||||
<View key={item.id} className="bg-white p-5 rounded-3xl shadow-sm border border-gray-100 flex-row items-center justify-between">
|
||||
<View className="flex-row items-center gap-4 flex-1">
|
||||
|
||||
{/* Icona */}
|
||||
<View className={`${config.bg} p-4 rounded-2xl flex-shrink-0`}>
|
||||
<IconComponent size={24} color={config.color} />
|
||||
</View>
|
||||
|
||||
{/* Titolo e Sottotitolo */}
|
||||
<View className="flex-1 mr-2">
|
||||
<Text
|
||||
className="text-lg font-bold text-gray-800 mb-0.5 leading-tight"
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text
|
||||
className="text-base text-gray-400 font-medium"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-lg font-bold text-gray-800 mb-1">{doc.name}</Text>
|
||||
<Text className="text-sm text-gray-400">Nuovo documento • {doc.date}</Text>
|
||||
|
||||
{/* Data */}
|
||||
<View className="flex-shrink-0">
|
||||
<Text className="text-sm font-bold text-gray-300">{item.date_display}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Empty State */}
|
||||
{!isLoading && recentActivities.length === 0 && (
|
||||
<View className="bg-white p-8 rounded-3xl border border-gray-100 items-center justify-center border-dashed">
|
||||
<Text className="text-gray-400 font-medium">Nessuna attività recente</Text>
|
||||
</View>
|
||||
))}
|
||||
{ATTENDANCE_DATA.slice(0, 1).map((att, i) => (
|
||||
<View key={i + 10} className="bg-white p-5 rounded-2xl shadow-sm flex-row items-center justify-between border border-gray-100">
|
||||
<View className="flex-row items-center gap-5">
|
||||
<View className="bg-[#099499]/10 p-4 rounded-2xl">
|
||||
<CheckCircle2 size={28} color="#099499" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-lg font-bold text-gray-800 mb-1">Presenza Completata</Text>
|
||||
<Text className="text-sm text-gray-400">{att.site} • {att.in}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{isLoading && recentActivities.length === 0 && (
|
||||
<View className="bg-white p-5 rounded-3xl border border-gray-100 h-24 justify-center items-center">
|
||||
<Text className="text-gray-400">Caricamento...</Text>
|
||||
</View>
|
||||
))}
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { JSX, useEffect, useState } from 'react';
|
||||
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';
|
||||
@ -20,6 +20,7 @@ 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);
|
||||
|
||||
@ -43,6 +44,29 @@ export default function PermitsScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
}, []);
|
||||
@ -82,17 +106,16 @@ export default function PermitsScreen() {
|
||||
>
|
||||
|
||||
{/* Calendar Widget */}
|
||||
<CalendarWidget events={permits} types={types} />
|
||||
<CalendarWidget events={permits} types={types} onMonthChange={(date) => setCurrentMonthDate(date)} />
|
||||
|
||||
{/* Lista Richieste Recenti */}
|
||||
<View>
|
||||
{permits.length === 0 ? (
|
||||
<Text className="text-center text-gray-500 mt-8">Nessuna richiesta di permesso trovata.</Text>
|
||||
{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>
|
||||
{/* TODO: Aggiungere una paginazione con delle freccette affianco? - Limite backend? */}
|
||||
{permits.map((item) => (
|
||||
{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' }}>
|
||||
|
||||
@ -9,7 +9,7 @@ import dayjs from 'dayjs';
|
||||
import LoadingScreen from '@/components/LoadingScreen';
|
||||
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
|
||||
import AddDocumentModal from '@/components/AddDocumentModal';
|
||||
import { downloadAndShareDocument, downloadDocumentByUrl, downloadDocumentLegacy, uploadDocument } from '@/utils/documentUtils';
|
||||
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
|
||||
|
||||
export default function DocumentsScreen() {
|
||||
const router = useRouter();
|
||||
@ -82,27 +82,16 @@ export default function DocumentsScreen() {
|
||||
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const response = await uploadDocument(file, null, customTitle);
|
||||
// console.log('Risposta caricamento:', response.data);
|
||||
// Alert.alert('Successo', 'Documento caricato con successo!');
|
||||
// setShowUploadModal(false);
|
||||
// fetchUserDocuments(); // Ricarica la lista dei documenti
|
||||
await uploadDocument(file, null, customTitle);
|
||||
Alert.alert('Successo', 'Documento caricato con successo!');
|
||||
setShowUploadModal(false);
|
||||
fetchUserDocuments();
|
||||
} catch (error) {
|
||||
console.error('Errore nel caricamento del documento:', error);
|
||||
Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Gestione Download e Condivisione Documento
|
||||
const handleDownloadAndShare = async (mimetype: string, fileName: string, fileUrl: string) => {
|
||||
try {
|
||||
await downloadAndShareDocument(mimetype, fileName, fileUrl);
|
||||
} catch (error) {
|
||||
console.error('Errore nel download/condivisione del documento:', error);
|
||||
Alert.alert('Errore', 'Impossibile scaricare/condividere il documento. Riprova più tardi.');
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading && !refreshing) {
|
||||
@ -181,7 +170,7 @@ export default function DocumentsScreen() {
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => downloadDocumentLegacy(doc.mimetype, doc.title, doc.url)} // downloadDocumentByUrl(doc.url, doc.title) handleDownloadAndShare(doc.mimetype, doc.title, doc.url)
|
||||
onPress={() => downloadAndShareDocument(doc.mimetype, doc.title, doc.url)}
|
||||
className="p-4 bg-gray-50 rounded-2xl active:bg-gray-100">
|
||||
<Download size={24} color="#6b7280" />
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -55,7 +55,7 @@ export default function SiteDocumentsScreen() {
|
||||
|
||||
useEffect(() => {
|
||||
if (params.id) {
|
||||
setIsLoading(true); // Caricamento iniziale
|
||||
setIsLoading(true);
|
||||
fetchSiteDocuments(Number(params.id));
|
||||
}
|
||||
}, [params.id, fetchSiteDocuments]);
|
||||
@ -107,11 +107,10 @@ export default function SiteDocumentsScreen() {
|
||||
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const response = await uploadDocument(file, Number(params.id), customTitle);
|
||||
// console.log('Risposta caricamento:', response.data);
|
||||
// Alert.alert('Successo', 'Documento caricato con successo!');
|
||||
// setShowUploadModal(false);
|
||||
// fetchSiteDocuments(Number(params.id)); // Ricarica la lista dei documenti
|
||||
await uploadDocument(file, Number(params.id), customTitle);
|
||||
Alert.alert('Successo', 'Documento caricato con successo!');
|
||||
setShowUploadModal(false);
|
||||
fetchSiteDocuments(Number(params.id), true);
|
||||
} catch (error) {
|
||||
console.error('Errore nel caricamento del documento:', error);
|
||||
Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.');
|
||||
|
||||
Reference in New Issue
Block a user