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:
26
app.json
26
app.json
@ -4,12 +4,13 @@
|
|||||||
"slug": "mariani_app",
|
"slug": "mariani_app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/mariani-icon.png",
|
||||||
"scheme": "marianiapp",
|
"scheme": "marianiapp",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"supportsTablet": true,
|
||||||
|
"bundleIdentifier": "com.anonymous.mariani-app"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
@ -19,7 +20,12 @@
|
|||||||
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"predictiveBackGestureEnabled": false
|
"predictiveBackGestureEnabled": false,
|
||||||
|
"permissions": [
|
||||||
|
"android.permission.CAMERA",
|
||||||
|
"android.permission.RECORD_AUDIO"
|
||||||
|
],
|
||||||
|
"package": "com.anonymous.mariani_app"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"output": "static",
|
"output": "static",
|
||||||
@ -31,14 +37,22 @@
|
|||||||
[
|
[
|
||||||
"expo-splash-screen",
|
"expo-splash-screen",
|
||||||
{
|
{
|
||||||
"image": "./assets/images/splash-icon.png",
|
"image": "./assets/images/mariani-splash.png",
|
||||||
"imageWidth": 200,
|
"imageWidth": 200,
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#099499",
|
||||||
"dark": {
|
"dark": {
|
||||||
"backgroundColor": "#000000"
|
"backgroundColor": "#099499"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"expo-camera",
|
||||||
|
{
|
||||||
|
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera",
|
||||||
|
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone",
|
||||||
|
"recordAudioAndroid": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
|
|||||||
@ -23,14 +23,15 @@ export default function ProtectedLayout() {
|
|||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#f3f4f6',
|
borderTopColor: '#f3f4f6',
|
||||||
height: 80,
|
height: 90,
|
||||||
paddingBottom: 20,
|
paddingBottom: 30,
|
||||||
paddingTop: 10,
|
paddingTop: 10,
|
||||||
|
paddingHorizontal: 10,
|
||||||
},
|
},
|
||||||
tabBarActiveTintColor: '#099499',
|
tabBarActiveTintColor: '#099499',
|
||||||
tabBarInactiveTintColor: '#9ca3af',
|
tabBarInactiveTintColor: '#9ca3af',
|
||||||
tabBarLabelStyle: {
|
tabBarLabelStyle: {
|
||||||
fontSize: 10,
|
fontSize: 12,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginTop: 4
|
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 { View, Text, TouchableOpacity, ScrollView, Alert, RefreshControl } from 'react-native';
|
||||||
import { QrCode, CheckCircle2 } from 'lucide-react-native';
|
import { QrCode, CheckCircle2, Nfc } from 'lucide-react-native';
|
||||||
import { ATTENDANCE_DATA } from '@/data/data';
|
|
||||||
import QrScanModal from '@/components/QrScanModal';
|
import QrScanModal from '@/components/QrScanModal';
|
||||||
|
import NfcScanModal from '@/components/NfcScanModal';
|
||||||
import LoadingScreen from '@/components/LoadingScreen';
|
import LoadingScreen from '@/components/LoadingScreen';
|
||||||
import api from '@/utils/api';
|
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() {
|
export default function AttendanceScreen() {
|
||||||
|
const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr');
|
||||||
const [showScanner, setShowScanner] = useState(false);
|
const [showScanner, setShowScanner] = useState(false);
|
||||||
const [lastScan, setLastScan] = useState<{ type: string; time: string; site: string } | null>(null);
|
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 [isLoading, setIsLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
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 () => {
|
const fetchAttendances = async () => {
|
||||||
try {
|
try {
|
||||||
if (!refreshing) setIsLoading(true);
|
if (!refreshing) setIsLoading(true);
|
||||||
|
|
||||||
// Fetch today's attendance data from API
|
// Fetch today's attendance data from API
|
||||||
const response = await api.get('/attendance/list');
|
const response = await api.get('/attendance/list');
|
||||||
// setAttendances(response.data);
|
setAttendances(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel recupero delle presenze:', error);
|
console.error('Errore nel recupero delle presenze:', error);
|
||||||
Alert.alert('Errore', 'Impossibile recuperare le presenze. Riprova più tardi.');
|
Alert.alert('Errore', 'Impossibile recuperare le presenze. Riprova più tardi.');
|
||||||
@ -29,26 +44,71 @@ export default function AttendanceScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
checkNfcAvailability();
|
||||||
fetchAttendances();
|
fetchAttendances();
|
||||||
|
setLastScan(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
fetchAttendances();
|
fetchAttendances();
|
||||||
|
setLastScan(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQRScan = () => {
|
const handleStartScan = async () => {
|
||||||
|
// Modalità QR Code
|
||||||
|
if (scannerType === 'qr') {
|
||||||
setShowScanner(true);
|
setShowScanner(true);
|
||||||
// Simulate scanning process
|
return;
|
||||||
setTimeout(() => {
|
}
|
||||||
setShowScanner(false);
|
|
||||||
// Add new entry or update existing one
|
// 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({
|
setLastScan({
|
||||||
type: 'Entrata',
|
type: response.data.type,
|
||||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
time: formatTime(response.data.time),
|
||||||
site: 'Cantiere Ospedale A.'
|
site: response.data.site
|
||||||
});
|
});
|
||||||
}, 3000);
|
}
|
||||||
|
} 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) {
|
if (isLoading && !refreshing) {
|
||||||
@ -72,36 +132,51 @@ export default function AttendanceScreen() {
|
|||||||
>
|
>
|
||||||
<View className="flex-1 p-5 items-center">
|
<View className="flex-1 p-5 items-center">
|
||||||
|
|
||||||
{/* Feedback Card - OPZIONALE */}
|
{/* Feedback Card */}
|
||||||
{lastScan ? (
|
{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="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">
|
<View className="bg-green-500 rounded-full p-3 shadow-lg shadow-green-500/40 flex-shrink-0">
|
||||||
<CheckCircle2 size={32} color="white" />
|
<CheckCircle2 size={32} color="white" />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View className="flex-1">
|
||||||
<Text className="font-bold text-green-800 text-xl">{lastScan.type} Registrata!</Text>
|
<Text
|
||||||
<Text className="text-base text-green-700 font-medium">{lastScan.site} alle {lastScan.time}</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>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : null}
|
||||||
null
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Scanner Section */}
|
{/* Scanner Section */}
|
||||||
<View className="w-full mb-6">
|
<View className="w-full mb-6">
|
||||||
<View className="bg-white rounded-3xl p-8 shadow-sm border border-gray-100">
|
<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
|
<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]"
|
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>
|
<Text className="text-white text-xl font-bold ml-3">Scansiona Codice</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Text className="text-gray-500 text-center mt-6 text-base px-2 leading-relaxed">
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -109,34 +184,69 @@ export default function AttendanceScreen() {
|
|||||||
{/* Mini History */}
|
{/* Mini History */}
|
||||||
<View className="w-full mt-4">
|
<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>
|
<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">
|
<View className="bg-white rounded-3xl shadow-sm overflow-hidden border border-gray-100">
|
||||||
{ATTENDANCE_DATA.map((item, index) => (
|
{attendances.map((item, index) => (
|
||||||
<View key={item.id} className={`p-6 flex-row justify-between items-center ${index !== 0 ? 'border-t border-gray-100' : ''}`}>
|
<View
|
||||||
<View className="flex-row items-center gap-4">
|
key={item.id}
|
||||||
<View className={`w-3 h-3 rounded-full shadow-sm ${item.status === 'complete' ? 'bg-green-500' : 'bg-orange-500'}`} />
|
className={`p-5 flex-row justify-between items-center ${index !== 0 ? 'border-t border-gray-100' : ''}`}
|
||||||
<View>
|
>
|
||||||
<Text className="font-bold text-gray-800 text-lg mb-0.5">{item.site}</Text>
|
<View className="flex-row items-center gap-4 flex-1 mr-2">
|
||||||
<Text className="text-base text-gray-400 font-medium">{item.in} - {item.out || 'In corso'}</Text>
|
<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>
|
</View>
|
||||||
{item.status === 'complete' && (
|
</View>
|
||||||
<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>
|
{/* 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>
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* Scanner Modal */}
|
{/* Scanner Modal */}
|
||||||
|
{scannerType === 'qr' ? (
|
||||||
<QrScanModal
|
<QrScanModal
|
||||||
visible={showScanner}
|
visible={showScanner}
|
||||||
onClose={() => setShowScanner(false)}
|
onClose={() => setShowScanner(false)}
|
||||||
|
onScan={onScan}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<NfcScanModal
|
||||||
|
visible={showScanner}
|
||||||
|
onClose={() => setShowScanner(false)}
|
||||||
|
onScan={onScan}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,28 +1,81 @@
|
|||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { AlertTriangle, CheckCircle2, FileText, QrCode, User } from 'lucide-react-native';
|
import { AlertTriangle, CalendarDays, CheckCircle2, FileText, QrCode, User, CalendarClock, LayoutDashboard } from 'lucide-react-native';
|
||||||
import React, { useContext } from 'react';
|
import React, { useState, useContext, useEffect } from 'react';
|
||||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
import { RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { ATTENDANCE_DATA, DOCUMENTS_DATA } from '@/data/data';
|
|
||||||
import { AuthContext } from '@/utils/authContext';
|
import { AuthContext } from '@/utils/authContext';
|
||||||
|
import { ActivityItem } from '@/types/types';
|
||||||
|
import api from '@/utils/api';
|
||||||
|
import LoadingScreen from '@/components/LoadingScreen';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user } = useContext(AuthContext);
|
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 (
|
return (
|
||||||
<View className="flex-1 bg-[#099499]">
|
<View className="flex-1 bg-[#099499]">
|
||||||
{/* Banner Custom */}
|
{/* Banner Custom */}
|
||||||
<View className="pt-16 pb-6 px-6 shadow-sm z-10">
|
<View className="pt-16 pb-6 px-6 shadow-sm z-10">
|
||||||
<View className="flex-row justify-between items-start">
|
<View className="flex-row justify-between items-start">
|
||||||
<View className="flex-row items-center gap-4">
|
<View className="flex-row items-center gap-4 flex-1 mr-4">
|
||||||
<View>
|
<View className="flex-1">
|
||||||
<Text className="text-teal-100 text-lg font-medium uppercase tracking-wider mb-1">Benvenuto</Text>
|
<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>
|
<Text className="text-xl text-teal-200">{user?.role}</Text>
|
||||||
</View>
|
</View>
|
||||||
</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')}>
|
<TouchableOpacity className="p-3 bg-white/10 rounded-full active:bg-white/20" onPress={() => router.push('/profile')}>
|
||||||
<User size={28} color="white" />
|
<User size={28} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -35,18 +88,21 @@ export default function HomeScreen() {
|
|||||||
className="flex-1 bg-gray-50 rounded-t-[2.5rem] px-5 pt-6"
|
className="flex-1 bg-gray-50 rounded-t-[2.5rem] px-5 pt-6"
|
||||||
contentContainerStyle={{ paddingBottom: 50, gap: 24 }}
|
contentContainerStyle={{ paddingBottom: 50, gap: 24 }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* Warning Card - OPZIONALE */}
|
{/* Warning Card */}
|
||||||
{incompleteTasks.length > 0 && (
|
{incompleteAttendance && (
|
||||||
<View className="bg-white p-6 rounded-3xl shadow-sm border-l-8 border-orange-500 flex-row items-center justify-between">
|
<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="flex-row items-center gap-5 flex-1">
|
||||||
<View className="bg-orange-100 p-4 rounded-full">
|
<View className="bg-orange-100 p-4 rounded-full">
|
||||||
<AlertTriangle size={32} color="#f97316" />
|
<AlertTriangle size={32} color="#f97316" />
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Text className="font-bold text-gray-800 text-lg">Presenza incompleta</Text>
|
<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>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => router.push('/attendance')} className="bg-orange-50 px-5 py-3 rounded-xl ml-2 active:bg-orange-100">
|
<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>
|
||||||
|
|
||||||
<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]"
|
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">
|
<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>
|
</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>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -85,37 +141,62 @@ export default function HomeScreen() {
|
|||||||
<View>
|
<View>
|
||||||
<View className="flex-row justify-between items-center px-1 mb-4">
|
<View className="flex-row justify-between items-center px-1 mb-4">
|
||||||
<Text className="text-gray-800 text-xl font-bold">Ultime Attività</Text>
|
<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>
|
<Text className="text-base text-[#099499] font-bold p-1">Vedi tutto</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity> */}
|
||||||
</View>
|
</View>
|
||||||
<View className="gap-4">
|
<View className="gap-4">
|
||||||
{DOCUMENTS_DATA.slice(0, 2).map((doc, i) => (
|
{recentActivities.map((item) => {
|
||||||
<View key={i} className="bg-white p-5 rounded-2xl shadow-sm flex-row items-center justify-between border border-gray-100">
|
const config = getActivityConfig(item.type);
|
||||||
<View className="flex-row items-center gap-5">
|
const IconComponent = config.icon;
|
||||||
<View className="bg-gray-100 p-4 rounded-2xl">
|
return (
|
||||||
<FileText size={28} color="#4b5563" />
|
<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>
|
</View>
|
||||||
<View>
|
|
||||||
<Text className="text-lg font-bold text-gray-800 mb-1">{doc.name}</Text>
|
{/* Titolo e Sottotitolo */}
|
||||||
<Text className="text-sm text-gray-400">Nuovo documento • {doc.date}</Text>
|
<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>
|
</View>
|
||||||
</View>
|
|
||||||
))}
|
{/* Data */}
|
||||||
{ATTENDANCE_DATA.slice(0, 1).map((att, i) => (
|
<View className="flex-shrink-0">
|
||||||
<View key={i + 10} className="bg-white p-5 rounded-2xl shadow-sm flex-row items-center justify-between border border-gray-100">
|
<Text className="text-sm font-bold text-gray-300">{item.date_display}</Text>
|
||||||
<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>
|
||||||
</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>
|
</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>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</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 { Calendar as CalendarIcon, CalendarX, Clock, Plus, Thermometer } from 'lucide-react-native';
|
||||||
import { Alert, ScrollView, Text, TouchableOpacity, View, ActivityIndicator, RefreshControl } from 'react-native';
|
import { Alert, ScrollView, Text, TouchableOpacity, View, ActivityIndicator, RefreshControl } from 'react-native';
|
||||||
import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
||||||
@ -20,6 +20,7 @@ export default function PermitsScreen() {
|
|||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [permits, setPermits] = useState<TimeOffRequest[]>([]);
|
const [permits, setPermits] = useState<TimeOffRequest[]>([]);
|
||||||
const [types, setTypes] = useState<TimeOffRequestType[]>([]);
|
const [types, setTypes] = useState<TimeOffRequestType[]>([]);
|
||||||
|
const [currentMonthDate, setCurrentMonthDate] = useState(new Date());
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchPermits();
|
fetchPermits();
|
||||||
}, []);
|
}, []);
|
||||||
@ -82,17 +106,16 @@ export default function PermitsScreen() {
|
|||||||
>
|
>
|
||||||
|
|
||||||
{/* Calendar Widget */}
|
{/* Calendar Widget */}
|
||||||
<CalendarWidget events={permits} types={types} />
|
<CalendarWidget events={permits} types={types} onMonthChange={(date) => setCurrentMonthDate(date)} />
|
||||||
|
|
||||||
{/* Lista Richieste Recenti */}
|
{/* Lista Richieste Recenti */}
|
||||||
<View>
|
<View>
|
||||||
{permits.length === 0 ? (
|
{filteredPermits.length === 0 ? (
|
||||||
<Text className="text-center text-gray-500 mt-8">Nessuna richiesta di permesso trovata.</Text>
|
<Text className="text-center text-gray-500 mt-8">Nessuna richiesta di permesso questo mese</Text>
|
||||||
) : (
|
) : (
|
||||||
<View className="gap-4">
|
<View className="gap-4">
|
||||||
<Text className="text-xl font-bold text-gray-800 px-1">Le tue richieste</Text>
|
<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? */}
|
{filteredPermits.map((item) => (
|
||||||
{permits.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 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="flex-row items-center gap-4">
|
||||||
<View className={`p-4 rounded-2xl`} style={{ backgroundColor: item.timeOffRequestType.color ? `${item.timeOffRequestType.color}25` : '#E5E7EB' }}>
|
<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 LoadingScreen from '@/components/LoadingScreen';
|
||||||
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
|
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
|
||||||
import AddDocumentModal from '@/components/AddDocumentModal';
|
import AddDocumentModal from '@/components/AddDocumentModal';
|
||||||
import { downloadAndShareDocument, downloadDocumentByUrl, downloadDocumentLegacy, uploadDocument } from '@/utils/documentUtils';
|
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
|
||||||
|
|
||||||
export default function DocumentsScreen() {
|
export default function DocumentsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -82,27 +82,16 @@ export default function DocumentsScreen() {
|
|||||||
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
const response = await uploadDocument(file, null, customTitle);
|
await uploadDocument(file, null, customTitle);
|
||||||
// console.log('Risposta caricamento:', response.data);
|
Alert.alert('Successo', 'Documento caricato con successo!');
|
||||||
// Alert.alert('Successo', 'Documento caricato con successo!');
|
setShowUploadModal(false);
|
||||||
// setShowUploadModal(false);
|
fetchUserDocuments();
|
||||||
// fetchUserDocuments(); // Ricarica la lista dei documenti
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel caricamento del documento:', error);
|
console.error('Errore nel caricamento del documento:', error);
|
||||||
Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.');
|
Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false);
|
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) {
|
if (isLoading && !refreshing) {
|
||||||
@ -181,7 +170,7 @@ export default function DocumentsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<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">
|
className="p-4 bg-gray-50 rounded-2xl active:bg-gray-100">
|
||||||
<Download size={24} color="#6b7280" />
|
<Download size={24} color="#6b7280" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export default function SiteDocumentsScreen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (params.id) {
|
if (params.id) {
|
||||||
setIsLoading(true); // Caricamento iniziale
|
setIsLoading(true);
|
||||||
fetchSiteDocuments(Number(params.id));
|
fetchSiteDocuments(Number(params.id));
|
||||||
}
|
}
|
||||||
}, [params.id, fetchSiteDocuments]);
|
}, [params.id, fetchSiteDocuments]);
|
||||||
@ -107,11 +107,10 @@ export default function SiteDocumentsScreen() {
|
|||||||
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
const response = await uploadDocument(file, Number(params.id), customTitle);
|
await uploadDocument(file, Number(params.id), customTitle);
|
||||||
// console.log('Risposta caricamento:', response.data);
|
Alert.alert('Successo', 'Documento caricato con successo!');
|
||||||
// Alert.alert('Successo', 'Documento caricato con successo!');
|
setShowUploadModal(false);
|
||||||
// setShowUploadModal(false);
|
fetchSiteDocuments(Number(params.id), true);
|
||||||
// fetchSiteDocuments(Number(params.id)); // Ricarica la lista dei documenti
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Errore nel caricamento del documento:', error);
|
console.error('Errore nel caricamento del documento:', error);
|
||||||
Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.');
|
Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.');
|
||||||
|
|||||||
@ -45,7 +45,8 @@ export default function LoginScreen() {
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
// Errore dal server (es. 401 Credenziali errate)
|
// Errore dal server (es. 401 Credenziali errate)
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
message = "Credenziali non valide.";
|
// TODO: Alert o Toast specifico per credenziali errate
|
||||||
|
message = "Credenziali non valide."
|
||||||
} else {
|
} else {
|
||||||
message = `Errore Server: ${error.response.data.message || error.response.status}`;
|
message = `Errore Server: ${error.response.data.message || error.response.status}`;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
assets/images/mariani-icon.png
Normal file
BIN
assets/images/mariani-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
assets/images/mariani-splash.png
Normal file
BIN
assets/images/mariani-splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@ -13,12 +13,14 @@ interface AddDocumentModalProps {
|
|||||||
export default function AddDocumentModal({ visible, onClose, onUpload, isUploading = false }: AddDocumentModalProps) {
|
export default function AddDocumentModal({ visible, onClose, onUpload, isUploading = false }: AddDocumentModalProps) {
|
||||||
const [selectedFile, setSelectedFile] = useState<DocumentPicker.DocumentPickerAsset | null>(null);
|
const [selectedFile, setSelectedFile] = useState<DocumentPicker.DocumentPickerAsset | null>(null);
|
||||||
const [customTitle, setCustomTitle] = useState('');
|
const [customTitle, setCustomTitle] = useState('');
|
||||||
|
const [fileExtension, setFileExtension] = useState('');
|
||||||
|
|
||||||
// Reset dello stato quando il modale si apre/chiude
|
// Reset dello stato quando il modale si apre/chiude
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
setCustomTitle('');
|
setCustomTitle('');
|
||||||
|
setFileExtension('');
|
||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
@ -35,8 +37,14 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
const asset = result.assets[0];
|
const asset = result.assets[0];
|
||||||
setSelectedFile(asset);
|
setSelectedFile(asset);
|
||||||
|
|
||||||
// Pre-compila il titolo con il nome del file
|
const lastDotIndex = asset.name.lastIndexOf('.');
|
||||||
|
if (lastDotIndex !== -1) {
|
||||||
|
setCustomTitle(asset.name.substring(0, lastDotIndex));
|
||||||
|
setFileExtension(asset.name.substring(lastDotIndex));
|
||||||
|
} else {
|
||||||
setCustomTitle(asset.name);
|
setCustomTitle(asset.name);
|
||||||
|
setFileExtension('');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Errore selezione file:", err);
|
console.error("Errore selezione file:", err);
|
||||||
@ -45,13 +53,16 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
|
|
||||||
const handleUpload = () => {
|
const handleUpload = () => {
|
||||||
if (!selectedFile) return;
|
if (!selectedFile) return;
|
||||||
|
const fullTitle = customTitle ? `${customTitle}${fileExtension}` : selectedFile.name;
|
||||||
|
|
||||||
// Se il titolo custom è vuoto, usiamo il nome originale
|
// Se il titolo custom è vuoto, usiamo il nome originale
|
||||||
onUpload(selectedFile, customTitle || selectedFile.name);
|
onUpload(selectedFile, fullTitle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFile = () => {
|
const removeFile = () => {
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
setCustomTitle('');
|
setCustomTitle('');
|
||||||
|
setFileExtension('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Formatta dimensione file
|
// Formatta dimensione file
|
||||||
@ -120,13 +131,18 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
{/* Campo Rinomina (Visibile solo se c'è un file) */}
|
{/* Campo Rinomina (Visibile solo se c'è un file) */}
|
||||||
{selectedFile && (
|
{selectedFile && (
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-gray-700 font-bold mb-2 ml-1 text-sm">Nome Documento (Opzionale)</Text>
|
<Text className="text-gray-700 font-bold mb-2 ml-1 text-sm">Rinomina File</Text>
|
||||||
|
<View className="flex-row items-center w-full bg-gray-50 rounded-xl border border-gray-200 overflow-hidden">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={customTitle}
|
value={customTitle}
|
||||||
onChangeText={setCustomTitle}
|
onChangeText={setCustomTitle}
|
||||||
placeholder="Come vuoi chiamare questo file?"
|
placeholder="Nome del file"
|
||||||
className="w-full p-4 bg-gray-50 rounded-xl border border-gray-200 text-gray-800 text-base"
|
className="flex-1 p-4 text-gray-800 text-base"
|
||||||
/>
|
/>
|
||||||
|
<Text className="px-4 text-gray-400 font-medium text-base flex items-center justify-center">
|
||||||
|
{fileExtension}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, TouchableOpacity } from 'react-native';
|
import { View, Text, TouchableOpacity } from 'react-native';
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react-native';
|
import { ChevronLeft, ChevronRight } from 'lucide-react-native';
|
||||||
import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
||||||
@ -6,9 +6,10 @@ import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
|||||||
interface CalendarWidgetProps {
|
interface CalendarWidgetProps {
|
||||||
events: TimeOffRequest[];
|
events: TimeOffRequest[];
|
||||||
types: TimeOffRequestType[];
|
types: TimeOffRequestType[];
|
||||||
|
onMonthChange?: (date: Date) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CalendarWidget({ events, types }: CalendarWidgetProps) {
|
export default function CalendarWidget({ events, types, onMonthChange }: CalendarWidgetProps) {
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
|
||||||
// Helpers per il calendario
|
// Helpers per il calendario
|
||||||
@ -21,8 +22,17 @@ export default function CalendarWidget({ events, types }: CalendarWidgetProps) {
|
|||||||
const changeMonth = (increment: number) => {
|
const changeMonth = (increment: number) => {
|
||||||
const newDate = new Date(currentDate.setMonth(currentDate.getMonth() + increment));
|
const newDate = new Date(currentDate.setMonth(currentDate.getMonth() + increment));
|
||||||
setCurrentDate(new Date(newDate));
|
setCurrentDate(new Date(newDate));
|
||||||
|
if (onMonthChange) {
|
||||||
|
onMonthChange(newDate);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onMonthChange) {
|
||||||
|
onMonthChange(currentDate);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const getEventForDay = (day: number) => {
|
const getEventForDay = (day: number) => {
|
||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
|
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
|||||||
162
components/NfcScanModal.tsx
Normal file
162
components/NfcScanModal.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Modal, Text, TouchableOpacity, View, Animated, Easing, Vibration, Alert } from 'react-native';
|
||||||
|
import { X, Radio, SmartphoneNfc } from 'lucide-react-native';
|
||||||
|
import NfcManager, { NfcTech } from 'react-native-nfc-manager';
|
||||||
|
|
||||||
|
interface NfcScanModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onScan: (data: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalProps) {
|
||||||
|
// Animazione per l'effetto "pulsante" (Breathing)
|
||||||
|
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||||
|
const opacityAnim = useRef(new Animated.Value(0.3)).current;
|
||||||
|
|
||||||
|
const readNfcTag = async () => {
|
||||||
|
const supported = await NfcManager.isSupported();
|
||||||
|
const nfcScanning = await NfcManager.isEnabled();
|
||||||
|
|
||||||
|
if (!supported || !nfcScanning) {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NfcManager.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await NfcManager.requestTechnology(NfcTech.Ndef);
|
||||||
|
const tag = await NfcManager.getTag();
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
console.log('NFC Tag Found:', tag);
|
||||||
|
Vibration.vibrate();
|
||||||
|
// onScan(tag.id || JSON.stringify(tag));
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
console.warn('Tag NFC vuoto o non formattato NDEF');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error reading NFC tag', error);
|
||||||
|
} finally {
|
||||||
|
NfcManager.cancelTechnologyRequest();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loop infinito di espansione e contrazione
|
||||||
|
const animationLoop = () => {
|
||||||
|
Animated.loop(
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.sequence([
|
||||||
|
Animated.timing(scaleAnim, {
|
||||||
|
toValue: 1.2,
|
||||||
|
duration: 1500,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(scaleAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 1500,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
Animated.sequence([
|
||||||
|
Animated.timing(opacityAnim, {
|
||||||
|
toValue: 0.1,
|
||||||
|
duration: 1500,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(opacityAnim, {
|
||||||
|
toValue: 0.3,
|
||||||
|
duration: 1500,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
animationLoop();
|
||||||
|
readNfcTag();
|
||||||
|
} else {
|
||||||
|
scaleAnim.setValue(1);
|
||||||
|
opacityAnim.setValue(0.3);
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
animationType="fade"
|
||||||
|
transparent={true}
|
||||||
|
visible={visible}
|
||||||
|
onRequestClose={onClose}
|
||||||
|
statusBarTranslucent
|
||||||
|
>
|
||||||
|
<View className="flex-1 bg-black/80 items-center justify-center p-6">
|
||||||
|
|
||||||
|
{/* Card Principale */}
|
||||||
|
<View className="bg-white w-full max-w-sm rounded-[2.5rem] p-8 items-center shadow-2xl relative overflow-hidden">
|
||||||
|
|
||||||
|
{/* Bottone Chiudi (Alto Destra) */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onClose}
|
||||||
|
className="absolute top-6 right-6 z-10 p-2 bg-gray-50 rounded-full"
|
||||||
|
>
|
||||||
|
<X size={20} color="#9ca3af" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* Area Animata NFC */}
|
||||||
|
<View className="mt-6 mb-10 items-center justify-center h-40 w-40">
|
||||||
|
{/* Cerchio Pulsante (Sfondo) */}
|
||||||
|
<Animated.View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
borderRadius: 70,
|
||||||
|
backgroundColor: '#099499',
|
||||||
|
opacity: opacityAnim,
|
||||||
|
transform: [{ scale: scaleAnim }]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Cerchio Fisso (Primo piano) */}
|
||||||
|
<View className="bg-[#E6F4F4] p-6 rounded-full border-4 border-white shadow-sm z-10">
|
||||||
|
<SmartphoneNfc size={64} color="#099499" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Testi */}
|
||||||
|
<Text className="text-2xl font-bold text-gray-800 text-center mb-2">
|
||||||
|
Pronto alla scansione
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text className="text-gray-500 text-center text-base px-2 leading-relaxed">
|
||||||
|
Avvicina il retro del tuo smartphone al <Text className="font-bold text-gray-700">Tag NFC</Text> per registrare la presenza.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Indicatore Visivo (Simulazione onde) */}
|
||||||
|
<View className="flex-row gap-2 mt-8 items-center opacity-60">
|
||||||
|
<Radio size={20} color="#099499" />
|
||||||
|
<Text className="text-[#099499] font-medium text-sm uppercase tracking-widest">
|
||||||
|
Ricerca in corso...
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Footer Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onClose}
|
||||||
|
className="mt-10 bg-gray-100 rounded-2xl px-10 py-4 w-full active:bg-gray-200"
|
||||||
|
>
|
||||||
|
<Text className="text-gray-600 font-bold text-lg text-center">Annulla</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,40 +1,99 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, Modal, TouchableOpacity } from 'react-native';
|
import { View, Text, Modal, TouchableOpacity, Vibration, StyleSheet } from 'react-native';
|
||||||
import { QrCode } from 'lucide-react-native';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||||
|
import { X, ScanLine } from 'lucide-react-native';
|
||||||
|
|
||||||
interface QrScanModalProps {
|
interface QrScanModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onScan: (data: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QrScanModal({ visible, onClose, onScan }: QrScanModalProps) {
|
||||||
|
const [permission, requestPermission] = useCameraPermissions();
|
||||||
|
const [scanned, setScanned] = useState(false);
|
||||||
|
|
||||||
|
// Gestione Permessi e Reset Stato
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setScanned(false);
|
||||||
|
if (permission && !permission.granted && permission.canAskAgain) {
|
||||||
|
requestPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [visible, permission]);
|
||||||
|
|
||||||
|
const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => {
|
||||||
|
if (scanned) return;
|
||||||
|
setScanned(true);
|
||||||
|
Vibration.vibrate();
|
||||||
|
console.log(`Bar code with type ${type} and data ${data} has been scanned!`);
|
||||||
|
onScan(data);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!permission) {
|
||||||
|
return <View />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permission.granted && visible) {
|
||||||
|
requestPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QrScanModal({ visible, onClose }: QrScanModalProps) {
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
transparent={true}
|
animationType="slide"
|
||||||
animationType="fade"
|
presentationStyle="fullScreen"
|
||||||
statusBarTranslucent
|
onRequestClose={onClose}
|
||||||
>
|
>
|
||||||
<View className="flex-1 bg-black/90 items-center justify-center p-4">
|
<View className="flex-1 bg-black">
|
||||||
<View className="bg-white rounded-[2rem] p-8 w-full max-w-sm items-center shadow-2xl">
|
{/* Camera Full Screen */}
|
||||||
<QrCode color="#099499" size={80} />
|
<CameraView
|
||||||
<Text className="text-2xl font-bold mt-6 text-gray-800 text-center">Scansione in corso...</Text>
|
style={StyleSheet.absoluteFillObject}
|
||||||
<Text className="text-gray-500 mt-3 text-center text-base px-4">
|
facing="back"
|
||||||
Inquadra il codice QR nel riquadro sottostante
|
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
|
||||||
</Text>
|
barcodeScannerSettings={{
|
||||||
|
barcodeTypes: ["qr"],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Viewfinder Simulata */}
|
{/* Overlay Oscuro con "buco" trasparente (Simulato visivamente con bordi o opacity) */}
|
||||||
<View className="mt-8 w-64 h-64 border-4 border-[#099499] rounded-3xl bg-gray-50 relative overflow-hidden items-center justify-center">
|
<View className="flex-1">
|
||||||
<View className="absolute top-0 w-full h-1 bg-red-500 shadow-[0_0_15px_rgba(239,68,68,0.8)]" />
|
|
||||||
<Text className="text-gray-400 text-sm">Camera Feed</Text>
|
{/* Header Overlay */}
|
||||||
|
<SafeAreaView className="flex-1 bg-black/60 items-center pt-8">
|
||||||
|
<Text className="text-white text-xl font-bold">Scansiona QR Code</Text>
|
||||||
|
<Text className="text-gray-300 text-base mt-1">Inquadra il codice nel riquadro</Text>
|
||||||
|
</SafeAreaView>
|
||||||
|
|
||||||
|
{/* Area Centrale (Trasparente per la camera) */}
|
||||||
|
<View className="flex-row h-[400px]">
|
||||||
|
<View className="flex-1 bg-black/60" />
|
||||||
|
<View className="w-[400px] h-[400px] border-2 border-[#099499] bg-transparent relative justify-center items-center">
|
||||||
|
{/* Angoli decorativi */}
|
||||||
|
<View className="absolute top-0 left-0 w-6 h-6 border-l-4 border-t-4 border-[#099499]" />
|
||||||
|
<View className="absolute top-0 right-0 w-6 h-6 border-r-4 border-t-4 border-[#099499]" />
|
||||||
|
<View className="absolute bottom-0 left-0 w-6 h-6 border-l-4 border-b-4 border-[#099499]" />
|
||||||
|
<View className="absolute bottom-0 right-0 w-6 h-6 border-r-4 border-b-4 border-[#099499]" />
|
||||||
|
|
||||||
|
{/* Linea scansione animata o icona */}
|
||||||
|
{!scanned && <ScanLine color="#099499" size={40} className="opacity-50" />}
|
||||||
|
</View>
|
||||||
|
<View className="flex-1 bg-black/60" />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Footer Overlay */}
|
||||||
|
<View className="flex-1 bg-black/60 items-center justify-end pb-12 px-6">
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
className="mt-10 bg-gray-100 rounded-2xl px-10 py-4 w-full active:bg-gray-200"
|
className="bg-white/20 p-4 rounded-full"
|
||||||
>
|
>
|
||||||
<Text className="text-gray-800 font-bold text-lg text-center">Annulla</Text>
|
<X color="white" size={32} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<Text className="text-white mt-4 font-medium">Chiudi</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
16
data/data.ts
16
data/data.ts
@ -1,18 +1,4 @@
|
|||||||
import { UserData, AttendanceRecord, DocumentItem, OfficeItem } from '@/types/types';
|
import { OfficeItem } from '@/types/types';
|
||||||
|
|
||||||
// --- MOCK DATA (File: data.ts) ---
|
|
||||||
export const ATTENDANCE_DATA: AttendanceRecord[] = [
|
|
||||||
{ id: 1, site: "Cantiere Ospedale A.", date: "03/12/2025", in: "08:00", out: "17:00", status: "complete" },
|
|
||||||
{ id: 2, site: "Uffici Centrali", date: "02/12/2025", in: "08:15", out: "17:15", status: "complete" },
|
|
||||||
{ id: 3, site: "Residenza Parco", date: "01/12/2025", in: "08:00", out: null, status: "incomplete" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const DOCUMENTS_DATA: DocumentItem[] = [
|
|
||||||
{ id: 1, name: "Schema Elettrico Piano 1", type: "PDF", site: "Cantiere Ospedale A.", date: "01/12/2025" },
|
|
||||||
{ id: 2, name: "Modulo Sicurezza v2", type: "PDF", site: "Generale", date: "28/11/2025" },
|
|
||||||
{ id: 3, name: "Certificazione Impianto", type: "PDF", site: "Residenza Parco", date: "25/11/2025" },
|
|
||||||
{ id: 4, name: "Manuale Domotica", type: "PDF", site: "Uffici Centrali", date: "20/11/2025" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const OFFICES_DATA: OfficeItem[] = [
|
export const OFFICES_DATA: OfficeItem[] = [
|
||||||
{ id: 1, name: "Ufficio Tecnico", status: "online", temp: 22, lights: true, power: 450 },
|
{ id: 1, name: "Ufficio Tecnico", status: "online", temp: 22, lights: true, power: 450 },
|
||||||
|
|||||||
198
package-lock.json
generated
198
package-lock.json
generated
@ -15,7 +15,9 @@
|
|||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"expo": "~54.0.25",
|
"expo": "~54.0.25",
|
||||||
|
"expo-camera": "~17.0.10",
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
|
"expo-dev-client": "~6.0.20",
|
||||||
"expo-document-picker": "~14.0.8",
|
"expo-document-picker": "~14.0.8",
|
||||||
"expo-file-system": "~19.0.21",
|
"expo-file-system": "~19.0.21",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
@ -37,7 +39,8 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-nfc-manager": "^3.17.2",
|
||||||
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-ui-datepicker": "^3.1.2",
|
"react-native-ui-datepicker": "^3.1.2",
|
||||||
@ -6430,6 +6433,26 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-camera": {
|
||||||
|
"version": "17.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-17.0.10.tgz",
|
||||||
|
"integrity": "sha512-w1RBw83mAGVk4BPPwNrCZyFop0VLiVSRE3c2V9onWbdFwonpRhzmB4drygG8YOUTl1H3wQvALJHyMPTbgsK1Jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"invariant": "^2.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-web": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-web": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
"version": "18.0.11",
|
"version": "18.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.11.tgz",
|
||||||
@ -6445,6 +6468,79 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-dev-client": {
|
||||||
|
"version": "6.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.20.tgz",
|
||||||
|
"integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-dev-launcher": "6.0.20",
|
||||||
|
"expo-dev-menu": "7.0.18",
|
||||||
|
"expo-dev-menu-interface": "2.0.0",
|
||||||
|
"expo-manifests": "~1.0.10",
|
||||||
|
"expo-updates-interface": "~2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-dev-launcher": {
|
||||||
|
"version": "6.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz",
|
||||||
|
"integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.11.0",
|
||||||
|
"expo-dev-menu": "7.0.18",
|
||||||
|
"expo-manifests": "~1.0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-dev-launcher/node_modules/ajv": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-dev-launcher/node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/expo-dev-menu": {
|
||||||
|
"version": "7.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz",
|
||||||
|
"integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-dev-menu-interface": "2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-dev-menu-interface": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-document-picker": {
|
"node_modules/expo-document-picker": {
|
||||||
"version": "14.0.8",
|
"version": "14.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz",
|
||||||
@ -6505,6 +6601,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-json-utils": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz",
|
||||||
|
"integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/expo-keep-awake": {
|
"node_modules/expo-keep-awake": {
|
||||||
"version": "15.0.8",
|
"version": "15.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz",
|
||||||
@ -6543,6 +6645,19 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-manifests": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/config": "~12.0.11",
|
||||||
|
"expo-json-utils": "~0.15.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-modules-autolinking": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "3.0.23",
|
"version": "3.0.23",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.23.tgz",
|
||||||
@ -6910,6 +7025,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-updates-interface": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-web-browser": {
|
"node_modules/expo-web-browser": {
|
||||||
"version": "15.0.10",
|
"version": "15.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.10.tgz",
|
||||||
@ -7144,6 +7268,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
@ -11380,40 +11520,47 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-nfc-manager": {
|
||||||
|
"version": "3.17.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-nfc-manager/-/react-native-nfc-manager-3.17.2.tgz",
|
||||||
|
"integrity": "sha512-0NryP/Iw2hzw4MVH5KCngoRerNUrnRok6VfLrlFcFZRKyTQ7KTgpsdDxCB6cR33qYNyEDrWGBayfAI+ym5gt8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@expo/config-plugins": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@expo/config-plugins": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-reanimated": {
|
"node_modules/react-native-reanimated": {
|
||||||
"version": "3.17.5",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
|
||||||
"integrity": "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw==",
|
"integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
"react-native-is-edge-to-edge": "^1.2.1",
|
||||||
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
"semver": "7.7.2"
|
||||||
"@babel/plugin-transform-classes": "^7.0.0-0",
|
|
||||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0",
|
|
||||||
"@babel/plugin-transform-optional-chaining": "^7.0.0-0",
|
|
||||||
"@babel/plugin-transform-shorthand-properties": "^7.0.0-0",
|
|
||||||
"@babel/plugin-transform-template-literals": "^7.0.0-0",
|
|
||||||
"@babel/plugin-transform-unicode-regex": "^7.0.0-0",
|
|
||||||
"@babel/preset-typescript": "^7.16.7",
|
|
||||||
"convert-source-map": "^2.0.0",
|
|
||||||
"invariant": "^2.2.4",
|
|
||||||
"react-native-is-edge-to-edge": "1.1.7"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.0.0-0",
|
"@babel/core": "^7.0.0-0",
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*",
|
||||||
|
"react-native-worklets": ">=0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": {
|
"node_modules/react-native-reanimated/node_modules/semver": {
|
||||||
"version": "1.1.7",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
"integrity": "sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==",
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
"license": "MIT",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"bin": {
|
||||||
"react": "*",
|
"semver": "bin/semver.js"
|
||||||
"react-native": "*"
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-safe-area-context": {
|
"node_modules/react-native-safe-area-context": {
|
||||||
@ -11517,6 +11664,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
||||||
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
|
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
||||||
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"android": "expo start --android",
|
"android": "expo run:android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"lint": "expo lint"
|
"lint": "expo lint"
|
||||||
},
|
},
|
||||||
@ -18,7 +18,9 @@
|
|||||||
"@react-navigation/native": "^7.1.8",
|
"@react-navigation/native": "^7.1.8",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"expo": "~54.0.25",
|
"expo": "~54.0.25",
|
||||||
|
"expo-camera": "~17.0.10",
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
|
"expo-dev-client": "~6.0.20",
|
||||||
"expo-document-picker": "~14.0.8",
|
"expo-document-picker": "~14.0.8",
|
||||||
"expo-file-system": "~19.0.21",
|
"expo-file-system": "~19.0.21",
|
||||||
"expo-font": "~14.0.9",
|
"expo-font": "~14.0.9",
|
||||||
@ -40,7 +42,8 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-nfc-manager": "^3.17.2",
|
||||||
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-ui-datepicker": "^3.1.2",
|
"react-native-ui-datepicker": "^3.1.2",
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
// --- TYPES & export INTERFACES (File: types.ts) ---
|
|
||||||
|
|
||||||
import { DateType } from "react-native-ui-datepicker";
|
import { DateType } from "react-native-ui-datepicker";
|
||||||
|
|
||||||
export interface UserData {
|
export interface UserData {
|
||||||
@ -11,13 +9,22 @@ export interface UserData {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActivityItem {
|
||||||
|
id: string;
|
||||||
|
type: 'document' | 'attendance' | 'permit';
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
date_display: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AttendanceRecord {
|
export interface AttendanceRecord {
|
||||||
id: number;
|
id: number;
|
||||||
site: string;
|
site: string;
|
||||||
|
subactivity: string;
|
||||||
date: string;
|
date: string;
|
||||||
in: string;
|
in: string;
|
||||||
out: string | null;
|
out: string | null;
|
||||||
status: 'complete' | 'incomplete';
|
time: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentItem {
|
export interface DocumentItem {
|
||||||
|
|||||||
@ -73,3 +73,17 @@ export const parseTimestamp = (dateStr: string | undefined | null): Date => {
|
|||||||
if (isNaN(date.getTime())) return new Date();
|
if (isNaN(date.getTime())) return new Date();
|
||||||
return date;
|
return date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseSecondsToTime = (totalSeconds: number | null | undefined): string => {
|
||||||
|
if (totalSeconds == null || isNaN(totalSeconds)) return '';
|
||||||
|
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
const hh = String(hours);
|
||||||
|
const mm = String(minutes).padStart(2, '0');
|
||||||
|
const ss = String(seconds).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${hh}h`;
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ import * as Linking from 'expo-linking';
|
|||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gestisce l'upload di un documento verso il server usando Expo FileSystem
|
* Gestisce l'upload di un documento verso il server usando FormData
|
||||||
* @param file File da caricare (deve avere almeno la proprietà 'uri')
|
* @param file File da caricare (deve avere almeno la proprietà 'uri')
|
||||||
* @param siteId ID del sito a cui associare il documento (null per registro generale)
|
* @param siteId ID del sito a cui associare il documento (null per registro generale)
|
||||||
* @param customTitle Titolo personalizzato per il documento (opzionale)
|
* @param customTitle Titolo personalizzato per il documento (opzionale)
|
||||||
@ -21,13 +21,46 @@ export const uploadDocument = async (
|
|||||||
throw new Error("File non valido per l'upload.");
|
throw new Error("File non valido per l'upload.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (siteId === null) {
|
try {
|
||||||
console.log("Uploading document:", file, "to registry with title:", customTitle);
|
const formData = new FormData();
|
||||||
} else {
|
formData.append('file', {
|
||||||
console.log("Uploading document:", file, "to site:", siteId, "with title:", customTitle);
|
uri: file.uri,
|
||||||
|
name: customTitle || file.name,
|
||||||
|
type: file.mimeType
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
if (siteId !== null) {
|
||||||
|
formData.append('siteId', siteId.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Funzione di upload (manca lato backend)
|
if (customTitle) {
|
||||||
|
formData.append('customTitle', customTitle.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post('/attachment/upload', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Risposta server:", response.data);
|
||||||
|
|
||||||
|
if (response.data?.status === 'error') {
|
||||||
|
throw new Error(response.data.message || "Errore sconosciuto dal server");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Errore durante l'upload del documento:", error);
|
||||||
|
|
||||||
|
if (error.response) {
|
||||||
|
const serverMessage = error.response.data?.message || error.message;
|
||||||
|
throw new Error(`Errore Server (${error.response.status}): ${serverMessage}`);
|
||||||
|
} else if (error.request) {
|
||||||
|
throw new Error("Il server non risponde. Controlla la connessione.");
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,76 +107,3 @@ export const downloadAndShareDocument = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Test con versione legacy di FileSystem e SAF
|
|
||||||
export const downloadDocumentLegacy = async (
|
|
||||||
mimetype: string,
|
|
||||||
fileName: string,
|
|
||||||
fileUrl: string
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
if (!fileUrl || !fileName) {
|
|
||||||
throw new Error("Parametri mancanti per il download del documento.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = FileSystem.cacheDirectory + 'documents/';
|
|
||||||
// Download del file nella directory selezionata
|
|
||||||
const tmpFile = await FileSystem.downloadAsync(fileUrl, path + fileName);
|
|
||||||
console.log("File temporaneo scaricato in:", tmpFile.uri);
|
|
||||||
|
|
||||||
if (Platform.OS === 'android') {
|
|
||||||
const permission = await StorageAccessFramework.requestDirectoryPermissionsAsync();
|
|
||||||
if (permission.granted) {
|
|
||||||
// Gets SAF URI from response
|
|
||||||
const safUri = permission.directoryUri;
|
|
||||||
console.log("Selected Directory URI:", safUri);
|
|
||||||
|
|
||||||
const fileContent = await FileSystem.readAsStringAsync(tmpFile.uri, { encoding: FileSystem.EncodingType.Base64 });
|
|
||||||
console.log("File letto in Base64, dimensione:", fileContent.length);
|
|
||||||
|
|
||||||
// Copia il file nella directory SAF selezionata
|
|
||||||
const destUri = await StorageAccessFramework.createFileAsync(
|
|
||||||
safUri,
|
|
||||||
fileName,
|
|
||||||
mimetype
|
|
||||||
);
|
|
||||||
console.log("Destinazione SAF URI:", destUri);
|
|
||||||
|
|
||||||
await FileSystem.writeAsStringAsync(destUri, fileContent, { encoding: FileSystem.EncodingType.Base64 });
|
|
||||||
console.log("File riscritto in SAF.");
|
|
||||||
}
|
|
||||||
} else if (Platform.OS === 'ios') {
|
|
||||||
if (await Sharing.isAvailableAsync()) {
|
|
||||||
await Sharing.shareAsync(tmpFile.uri, {
|
|
||||||
mimeType: mimetype,
|
|
||||||
dialogTitle: `Scarica ${fileName}`,
|
|
||||||
UTI: 'public.item'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error("Condivisione non supportata su questo dispositivo.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Download Error:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Test con Linking standard
|
|
||||||
export const downloadDocumentByUrl = async (
|
|
||||||
fileUrl: string,
|
|
||||||
fileName: string
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
if (!fileUrl || !fileName) {
|
|
||||||
throw new Error("Parametri mancanti per il download del documento.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Linking.openURL(fileUrl);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Download Error:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user