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:
@ -13,12 +13,14 @@ interface AddDocumentModalProps {
|
||||
export default function AddDocumentModal({ visible, onClose, onUpload, isUploading = false }: AddDocumentModalProps) {
|
||||
const [selectedFile, setSelectedFile] = useState<DocumentPicker.DocumentPickerAsset | null>(null);
|
||||
const [customTitle, setCustomTitle] = useState('');
|
||||
const [fileExtension, setFileExtension] = useState('');
|
||||
|
||||
// Reset dello stato quando il modale si apre/chiude
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setSelectedFile(null);
|
||||
setCustomTitle('');
|
||||
setFileExtension('');
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
@ -34,10 +36,16 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
||||
|
||||
const asset = result.assets[0];
|
||||
setSelectedFile(asset);
|
||||
|
||||
// Pre-compila il titolo con il nome del file
|
||||
setCustomTitle(asset.name);
|
||||
|
||||
|
||||
const lastDotIndex = asset.name.lastIndexOf('.');
|
||||
if (lastDotIndex !== -1) {
|
||||
setCustomTitle(asset.name.substring(0, lastDotIndex));
|
||||
setFileExtension(asset.name.substring(lastDotIndex));
|
||||
} else {
|
||||
setCustomTitle(asset.name);
|
||||
setFileExtension('');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("Errore selezione file:", err);
|
||||
}
|
||||
@ -45,13 +53,16 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
||||
|
||||
const handleUpload = () => {
|
||||
if (!selectedFile) return;
|
||||
const fullTitle = customTitle ? `${customTitle}${fileExtension}` : selectedFile.name;
|
||||
|
||||
// Se il titolo custom è vuoto, usiamo il nome originale
|
||||
onUpload(selectedFile, customTitle || selectedFile.name);
|
||||
onUpload(selectedFile, fullTitle);
|
||||
};
|
||||
|
||||
const removeFile = () => {
|
||||
setSelectedFile(null);
|
||||
setCustomTitle('');
|
||||
setFileExtension('');
|
||||
};
|
||||
|
||||
// Formatta dimensione file
|
||||
@ -73,7 +84,7 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
||||
>
|
||||
<View className="flex-1 justify-center items-center bg-black/50 px-6">
|
||||
<View className="bg-white w-full rounded-[2rem] p-6 shadow-xl">
|
||||
|
||||
|
||||
{/* Header */}
|
||||
<View className="flex-row justify-between items-center mb-6">
|
||||
<Text className="text-xl font-bold text-gray-800">Carica Documento</Text>
|
||||
@ -84,10 +95,10 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
||||
|
||||
{/* Body */}
|
||||
<View className="gap-5">
|
||||
|
||||
|
||||
{/* Area Selezione File */}
|
||||
{!selectedFile ? (
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
onPress={pickDocument}
|
||||
className="h-64 border-2 border-dashed border-gray-300 rounded-2xl items-center justify-center bg-gray-50 active:bg-gray-100"
|
||||
>
|
||||
@ -120,20 +131,25 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
||||
{/* Campo Rinomina (Visibile solo se c'è un file) */}
|
||||
{selectedFile && (
|
||||
<View>
|
||||
<Text className="text-gray-700 font-bold mb-2 ml-1 text-sm">Nome Documento (Opzionale)</Text>
|
||||
<TextInput
|
||||
value={customTitle}
|
||||
onChangeText={setCustomTitle}
|
||||
placeholder="Come vuoi chiamare questo file?"
|
||||
className="w-full p-4 bg-gray-50 rounded-xl border border-gray-200 text-gray-800 text-base"
|
||||
/>
|
||||
<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
|
||||
value={customTitle}
|
||||
onChangeText={setCustomTitle}
|
||||
placeholder="Nome del file"
|
||||
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>
|
||||
|
||||
{/* Footer Buttons */}
|
||||
<View className="flex-row gap-3 mt-8">
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
className="flex-1 py-4 bg-gray-100 rounded-xl items-center"
|
||||
disabled={isUploading}
|
||||
@ -141,7 +157,7 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
||||
<Text className="text-gray-600 font-bold text-base">Annulla</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
onPress={handleUpload}
|
||||
disabled={!selectedFile || isUploading}
|
||||
className={`flex-1 py-4 rounded-xl items-center flex-row justify-center gap-2 ${!selectedFile ? 'bg-gray-300' : 'bg-[#099499]'}`}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react-native';
|
||||
import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
||||
@ -6,9 +6,10 @@ import { TimeOffRequest, TimeOffRequestType } from '@/types/types';
|
||||
interface CalendarWidgetProps {
|
||||
events: TimeOffRequest[];
|
||||
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());
|
||||
|
||||
// Helpers per il calendario
|
||||
@ -21,8 +22,17 @@ export default function CalendarWidget({ events, types }: CalendarWidgetProps) {
|
||||
const changeMonth = (increment: number) => {
|
||||
const newDate = new Date(currentDate.setMonth(currentDate.getMonth() + increment));
|
||||
setCurrentDate(new Date(newDate));
|
||||
if (onMonthChange) {
|
||||
onMonthChange(newDate);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (onMonthChange) {
|
||||
onMonthChange(currentDate);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getEventForDay = (day: number) => {
|
||||
const year = currentDate.getFullYear();
|
||||
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 { View, Text, Modal, TouchableOpacity } from 'react-native';
|
||||
import { QrCode } from 'lucide-react-native';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, Modal, TouchableOpacity, Vibration, StyleSheet } from '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 {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onScan: (data: string) => void;
|
||||
}
|
||||
|
||||
export default function QrScanModal({ visible, onClose }: QrScanModalProps) {
|
||||
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();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
statusBarTranslucent
|
||||
animationType="slide"
|
||||
presentationStyle="fullScreen"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View className="flex-1 bg-black/90 items-center justify-center p-4">
|
||||
<View className="bg-white rounded-[2rem] p-8 w-full max-w-sm items-center shadow-2xl">
|
||||
<QrCode color="#099499" size={80} />
|
||||
<Text className="text-2xl font-bold mt-6 text-gray-800 text-center">Scansione in corso...</Text>
|
||||
<Text className="text-gray-500 mt-3 text-center text-base px-4">
|
||||
Inquadra il codice QR nel riquadro sottostante
|
||||
</Text>
|
||||
<View className="flex-1 bg-black">
|
||||
{/* Camera Full Screen */}
|
||||
<CameraView
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
facing="back"
|
||||
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ["qr"],
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Viewfinder Simulata */}
|
||||
<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="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>
|
||||
{/* Overlay Oscuro con "buco" trasparente (Simulato visivamente con bordi o opacity) */}
|
||||
<View className="flex-1">
|
||||
|
||||
{/* 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>
|
||||
|
||||
<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-800 font-bold text-lg text-center">Annulla</Text>
|
||||
</TouchableOpacity>
|
||||
{/* Footer Overlay */}
|
||||
<View className="flex-1 bg-black/60 items-center justify-end pb-12 px-6">
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
className="bg-white/20 p-4 rounded-full"
|
||||
>
|
||||
<X color="white" size={32} />
|
||||
</TouchableOpacity>
|
||||
<Text className="text-white mt-4 font-medium">Chiudi</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user