diff --git a/app/(protected)/_layout.tsx b/app/(protected)/_layout.tsx index 420af35..1cdd77a 100644 --- a/app/(protected)/_layout.tsx +++ b/app/(protected)/_layout.tsx @@ -1,5 +1,5 @@ import { Redirect, Tabs } from 'expo-router'; -import { Home, Clock, FileText, Zap, CalendarIcon } from 'lucide-react-native'; +import { Home, Clock, Zap, CalendarIcon, Building } from 'lucide-react-native'; import { useContext } from 'react'; import { AuthContext } from '@/utils/authContext'; @@ -59,10 +59,10 @@ export default function ProtectedLayout() { }} /> , + title: 'Cantieri', + tabBarIcon: ({ color, size }) => , }} /> {/* // TODO: Rimuovere all'utente e mostrare solo a admin */} diff --git a/app/(protected)/attendance/index.tsx b/app/(protected)/attendance/index.tsx index 5d47269..ef0e6bf 100644 --- a/app/(protected)/attendance/index.tsx +++ b/app/(protected)/attendance/index.tsx @@ -1,12 +1,41 @@ -import React, { useState } from 'react'; -import { View, Text, TouchableOpacity, ScrollView } from 'react-native'; +import React, { use, useEffect, useState } from 'react'; +import { View, Text, TouchableOpacity, ScrollView, Alert, RefreshControl } from 'react-native'; import { QrCode, CheckCircle2 } from 'lucide-react-native'; import { ATTENDANCE_DATA } from '@/data/data'; import QrScanModal from '@/components/QrScanModal'; +import LoadingScreen from '@/components/LoadingScreen'; +import api from '@/utils/api'; export default function AttendanceScreen() { const [showScanner, setShowScanner] = useState(false); const [lastScan, setLastScan] = useState<{ type: string; time: string; site: string } | null>(null); + const [attendances, setAttendances] = useState(ATTENDANCE_DATA); + const [isLoading, setIsLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const fetchAttendances = async () => { + try { + if (!refreshing) setIsLoading(true); + // Fetch today's attendance data from API + const response = await api.get('/attendance/list'); + // setAttendances(response.data); + } catch (error) { + console.error('Errore nel recupero delle presenze:', error); + Alert.alert('Errore', 'Impossibile recuperare le presenze. Riprova più tardi.'); + } finally { + setIsLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + fetchAttendances(); + }, []); + + const onRefresh = () => { + setRefreshing(true); + fetchAttendances(); + }; const handleQRScan = () => { setShowScanner(true); @@ -22,6 +51,10 @@ export default function AttendanceScreen() { }, 3000); }; + if (isLoading && !refreshing) { + return ; + } + return ( {/* Header */} @@ -30,7 +63,13 @@ export default function AttendanceScreen() { Registra i tuoi movimenti - + + } + > {/* Feedback Card - OPZIONALE */} @@ -69,7 +108,7 @@ export default function AttendanceScreen() { {/* Mini History */} - Oggi + Ultime Presenze {ATTENDANCE_DATA.map((item, index) => ( @@ -81,9 +120,9 @@ export default function AttendanceScreen() { {item.status === 'complete' && ( - - 8h - + + 8h + )} ))} @@ -94,9 +133,9 @@ export default function AttendanceScreen() { {/* Scanner Modal */} - setShowScanner(false)} + setShowScanner(false)} /> ); diff --git a/app/(protected)/profile/_layout.tsx b/app/(protected)/profile/_layout.tsx index 3424f0a..c4da687 100644 --- a/app/(protected)/profile/_layout.tsx +++ b/app/(protected)/profile/_layout.tsx @@ -2,6 +2,9 @@ import { Stack } from "expo-router"; export default function ProfileLayout() { return ( - + + + + ); } \ No newline at end of file diff --git a/app/(protected)/documents/index.tsx b/app/(protected)/profile/documents.tsx similarity index 51% rename from app/(protected)/documents/index.tsx rename to app/(protected)/profile/documents.tsx index 77b2b16..2982115 100644 --- a/app/(protected)/documents/index.tsx +++ b/app/(protected)/profile/documents.tsx @@ -1,37 +1,62 @@ -import { DOCUMENTS_DATA } from '@/data/data'; import { Download, FileText, MapPin, Plus, Search, CalendarIcon } from 'lucide-react-native'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { RangePickerModal } from '@/components/RangePickerModal'; -import { ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { Alert, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { DocumentItem } from '@/types/types'; +import api from '@/utils/api'; import dayjs from 'dayjs'; +import LoadingScreen from '@/components/LoadingScreen'; +import { formatTimestamp, parseTimestamp } from '@/utils/dateTime'; +import AddDocumentModal from '@/components/AddDocumentModal'; export default function DocumentsScreen() { + const [documents, setDocuments] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [showUploadModal, setShowUploadModal] = useState(false); + const [isUploading, setIsUploading] = useState(false); + + const [showRangePicker, setShowRangePicker] = useState(false); const [searchTerm, setSearchTerm] = useState(''); - // Gestiamo le date come oggetti Dayjs o null per il datepicker, e stringhe per il filtro const [range, setRange] = useState<{ startDate: any; endDate: any }>({ startDate: null, endDate: null }); - const [showRangePicker, setShowRangePicker] = useState(false); + const fetchUserDocuments = async () => { + try { + if (!refreshing) setIsLoading(true); - // Funzione helper per convertire DD/MM/YYYY in oggetto Date (per il filtro esistente) - const parseDate = (dateStr: string) => { - if (!dateStr) return new Date(); - const [day, month, year] = dateStr.split('/').map(Number); - return new Date(year, month - 1, day); + // Fetch Documenti Utente + const response = await api.get(`/attachment/get-user-attachments`); + setDocuments(response.data); + } catch (error) { + console.error('Errore nel recupero dei documenti utente:', error); + Alert.alert('Errore', 'Impossibile recuperare i documenti. Riprova più tardi.'); + } finally { + setIsLoading(false); + setRefreshing(false); + } }; - const filteredDocs = DOCUMENTS_DATA.filter(doc => { - // Filtro Testuale - const matchesSearch = doc.name.toLowerCase().includes(searchTerm.toLowerCase()) || - doc.site.toLowerCase().includes(searchTerm.toLowerCase()); + useEffect(() => { + fetchUserDocuments(); + }, []); + const onRefresh = () => { + setRefreshing(true); + fetchUserDocuments(); + }; + + // Filtra Documenti in base a searchTerm e range + const filteredDocs = documents.filter(doc => { + // Filtro Testuale + const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase()); if (!matchesSearch) return false; // Filtro Date Range if (range.startDate || range.endDate) { - const docDate = parseDate(doc.date); // doc.date è "DD/MM/YYYY" + const docDate = parseTimestamp(doc.updated_at); // doc.date è "DD/MM/YYYY" // Controllo Data Inizio if (range.startDate) { @@ -50,31 +75,66 @@ export default function DocumentsScreen() { return true; }); - // Funzione per formattare la visualizzazione - const formatDateDisplay = (date: any) => { - return date ? dayjs(date).format('DD/MM/YYYY') : 'gg/mm/aaaa'; - }; + // Gestione Caricamento Documento + const handleUploadDocument = async (file: any, customTitle?: string) => { + console.log('Caricamento documento:', file, 'con titolo personalizzato:', customTitle); + setIsUploading(true); + try { + // const formData = new FormData(); + // formData.append('file', { + // uri: file.uri, + // name: file.name, + // type: file.mimeType || 'application/octet-stream', + // } as any); + // if (customTitle) { + // formData.append('title', customTitle); + // } + + // const response = await api.post('/attachment/upload', formData, { + // headers: { + // 'Content-Type': 'multipart/form-data', + // }, + // }); + + // console.log('Risposta caricamento:', response.data); + // Alert.alert('Successo', 'Documento caricato con successo!'); + // setShowUploadModal(false); + // fetchUserDocuments(); // Ricarica la lista dei documenti + } catch (error) { + console.error('Errore nel caricamento del documento:', error); + Alert.alert('Errore', 'Impossibile caricare il documento. Riprova più tardi.'); + } finally { + setIsUploading(false); + } + } + + if (isLoading && !refreshing) { + return ( + + ); + } return ( {/* Header */} + {/* TODO: Aggiungi torna indietro */} Documenti - Gestione modulistica e schemi + Gestisci i tuoi documenti {/* Search + Date Row */} {/* Search Bar */} - + @@ -101,6 +161,9 @@ export default function DocumentsScreen() { + } > {filteredDocs.map((doc) => ( @@ -109,10 +172,10 @@ export default function DocumentsScreen() { - {doc.name} + {doc.title} - {doc.site} + {formatTimestamp(doc.updated_at)} @@ -121,16 +184,28 @@ export default function DocumentsScreen() { ))} + + {filteredDocs.length === 0 && ( + Nessun documento trovato + )} {/* FAB */} alert('Aggiungi nuovo documento')} + onPress={() => setShowUploadModal(true)} className="absolute bottom-8 right-6 w-16 h-16 bg-[#099499] rounded-full shadow-lg items-center justify-center active:scale-90" > + + {/* Modale Caricamento Documento */} + setShowUploadModal(false)} + onUpload={handleUploadDocument} + isUploading={isUploading} + /> ); } \ No newline at end of file diff --git a/app/(protected)/profile/index.tsx b/app/(protected)/profile/index.tsx index fbf8433..781f25e 100644 --- a/app/(protected)/profile/index.tsx +++ b/app/(protected)/profile/index.tsx @@ -1,8 +1,7 @@ import { useRouter } from 'expo-router'; -import { ChevronLeft, LogOut, Mail, Settings, Smartphone, User } from 'lucide-react-native'; +import { ChevronLeft, FileText, LogOut, Mail, Settings, User } from 'lucide-react-native'; import React, { useContext } from 'react'; import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; -import { MOCK_USER } from '@/data/data'; import { AuthContext } from '@/utils/authContext'; export default function ProfileScreen() { @@ -58,17 +57,6 @@ export default function ProfileScreen() { - {/* // TODO: Rimuovere telefono, si potrebbe sostituire con altro dato? */} - {/* - - - - - Telefono - {phone} - - */} - @@ -85,14 +73,14 @@ export default function ProfileScreen() { Azioni - router.push('/permits')} className="bg-white p-4 rounded-3xl shadow-sm flex-row items-center justify-between border border-gray-100 mb-4"> + router.push('/profile/documents')} className="bg-white p-4 rounded-3xl shadow-sm flex-row items-center justify-between border border-gray-100 mb-4"> - + - I miei permessi - Richiedi o controlla lo stato + I miei documenti + Gestisci i tuoi documenti Apri diff --git a/app/(protected)/sites/[id].tsx b/app/(protected)/sites/[id].tsx new file mode 100644 index 0000000..40e0f9b --- /dev/null +++ b/app/(protected)/sites/[id].tsx @@ -0,0 +1,199 @@ +import { Download, FileText, Plus, Search, Calendar as CalendarIcon, ArrowLeft } from 'lucide-react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { RangePickerModal } from '@/components/RangePickerModal'; +import { Alert, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import { ConstructionSite, DocumentItem } from '@/types/types'; +import LoadingScreen from '@/components/LoadingScreen'; +import api from '@/utils/api'; +import dayjs from 'dayjs'; +import { formatTimestamp, parseTimestamp } from '@/utils/dateTime'; + +export default function SiteDocumentsScreen() { + const router = useRouter(); + const params = useLocalSearchParams(); + const [site, setSite] = useState(null); + const [documents, setDocuments] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + // Fetch dei documenti del cantiere + const fetchSiteDocuments = useCallback(async (siteId: number, isRefreshing = false) => { + try { + if (!isRefreshing) setIsLoading(true); + + // Fetch Documenti + const response = await api.get(`/attachment/get-site-attachments`, { + params: { siteId } + }); + setDocuments(response.data); + } catch (error) { + console.error('Errore nel recupero dei documenti del cantiere:', error); + Alert.alert('Errore', 'Impossibile recuperare i documenti del cantiere. Riprova più tardi.'); + } finally { + setIsLoading(false); + setRefreshing(false); + } + }, []); + + useEffect(() => { + if (params.siteData) { + try { + const jsonString = Array.isArray(params.siteData) ? params.siteData[0] : params.siteData; + const parsedSite = JSON.parse(jsonString); + setSite(parsedSite); + } catch (error) { + console.error('Errore nel parsing dei dati del cantiere:', error); + } + } + }, [params.siteData]); + + useEffect(() => { + if (params.id) { + setIsLoading(true); // Caricamento iniziale + fetchSiteDocuments(Number(params.id)); + } + }, [params.id, fetchSiteDocuments]); + + const onRefresh = () => { + setRefreshing(true); + fetchSiteDocuments(Number(params.id)); + }; + + const [searchTerm, setSearchTerm] = useState(''); + const [range, setRange] = useState<{ startDate: any; endDate: any }>({ + startDate: null, + endDate: null + }); + const [showRangePicker, setShowRangePicker] = useState(false); + + // Filtraggio Documenti + const filteredDocs = documents.filter(doc => { + // Filtro Testuale (su nome documento) + const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase()); + if (!matchesSearch) return false; + + // Filtro Date Range + if (range.startDate || range.endDate) { + const docDate = parseTimestamp(doc.updated_at); + if (range.startDate) { + const start = dayjs(range.startDate).startOf('day').toDate(); + if (docDate <= start) return false; + } + if (range.endDate) { + const end = dayjs(range.endDate).endOf('day').toDate(); + if (docDate >= end) return false; + } + } + return true; + }); + + if (!site || (isLoading && !refreshing)) { + return ( + + ); + } + + return ( + + {/* Header */} + + router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100"> + + + + + {/* Badge Codice Cantiere */} + {site.code && ( + + + {site.code} + + + )} + + {/* Nome Cantiere con Truncate funzionante */} + + {site.name} + + + Archivio documenti + + + + + {/* Search + Date Row (Spostato qui) */} + + + + + + + + + setShowRangePicker(true)} + className={`p-4 bg-white rounded-2xl shadow-sm border ${range.startDate ? 'border-[#099499]' : 'border-gray-200'}`} + > + + + + + setShowRangePicker(false)} + currentRange={range} + onApply={setRange} + /> + + {/* Lista Documenti */} + + } + > + {filteredDocs.map((doc) => ( + + + + + + + {doc.title} + {formatTimestamp(doc.updated_at)} + + + + + + + ))} + + {filteredDocs.length === 0 && ( + Nessun documento trovato + )} + + + + {/* FAB (Spostato qui) */} + alert(`Aggiungi doc a ${site.name}`)} + className="absolute bottom-8 right-6 w-16 h-16 bg-[#099499] rounded-full shadow-lg items-center justify-center active:scale-90" + > + + + + ); +} \ No newline at end of file diff --git a/app/(protected)/sites/_layout.tsx b/app/(protected)/sites/_layout.tsx new file mode 100644 index 0000000..d86dfc8 --- /dev/null +++ b/app/(protected)/sites/_layout.tsx @@ -0,0 +1,10 @@ +import {Stack} from 'expo-router'; + +export default function SitesLayout() { + return ( + + + + + ); +} \ No newline at end of file diff --git a/app/(protected)/sites/index.tsx b/app/(protected)/sites/index.tsx new file mode 100644 index 0000000..f2c5756 --- /dev/null +++ b/app/(protected)/sites/index.tsx @@ -0,0 +1,124 @@ +import { Building2, ChevronRight, MapPin, Search } from 'lucide-react-native'; +import React, { useEffect, useState } from 'react'; +import { Alert, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { useRouter } from 'expo-router'; +import api from '@/utils/api'; +import LoadingScreen from '@/components/LoadingScreen'; +import { ConstructionSite } from '@/types/types'; + +export default function SitesScreen() { + const [searchTerm, setSearchTerm] = useState(''); + const [constructionSites, setConstructionSites] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const router = useRouter(); + + // Fetch cantieri e documenti + const fetchConstructionSites = async () => { + try { + if (!refreshing) setIsLoading(true); + + // Fetch Cantieri + const response = await api.get('/construction-site/sites-attachments'); + setConstructionSites(response.data); + } catch (error) { + console.error('Errore nel recupero dei cantieri:', error); + Alert.alert('Errore', 'Impossibile recuperare i cantieri. Riprova più tardi.'); + } finally { + setIsLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + fetchConstructionSites(); + }, []); + + const onRefresh = () => { + setRefreshing(true); + fetchConstructionSites(); + }; + + // Filtriamo i cantieri in base alla ricerca + const filteredSites = constructionSites.filter(site => + site.name.toLowerCase().includes(searchTerm.toLowerCase()) || + site.code.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + if (isLoading && !refreshing) { + return ; + } + + return ( + + {/* Header */} + + Cantieri + Seleziona un cantiere per vedere i documenti + + + + {/* Search Bar */} + + + + + + + + {/* Lista Cantieri */} + {/* TODO: Rimuovere lo ScrollIndicator? */} + + } + > + {filteredSites.map((site, index) => ( + router.push({ + pathname: '/(protected)/sites/[id]', + params: { + id: site.id , + siteData: JSON.stringify(site), + } + })} + className="bg-white p-5 rounded-3xl shadow-sm flex-row items-center justify-between border border-gray-100 active:bg-gray-50" + > + + + + + + {site.code} + {site.name} + + + + {site.attachments_count} Documenti disponibili + + + + + + {/* Freccia al posto del download */} + + + ))} + + {filteredSites.length === 0 && ( + Nessun cantiere trovato + )} + + + + ); +} \ No newline at end of file diff --git a/app/login.tsx b/app/login.tsx index c944ff2..8abb513 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -1,16 +1,6 @@ import React, { useState, useContext } from 'react'; import { Eye, EyeOff, Lock, LogIn, Mail } from 'lucide-react-native'; -import { - KeyboardAvoidingView, - Platform, - ScrollView, - Text, - TextInput, - TouchableOpacity, - View, - Image, - Alert -} from 'react-native'; +import { KeyboardAvoidingView, Platform, ScrollView, Text, TextInput, TouchableOpacity, View, Image, Alert } from 'react-native'; import { AuthContext } from '@/utils/authContext'; import api from '@/utils/api'; @@ -21,6 +11,7 @@ export default function LoginScreen() { const [showPassword, setShowPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); + // TODO: Riscrivere funzione per migliorare leggibilità e gestione errori const handleLogin = async () => { // TODO: Implementa toast o messaggio di errore più user-friendly if (!username || !password) { @@ -31,9 +22,10 @@ export default function LoginScreen() { setIsLoading(true); try { - console.log("Tentativo login con:", username); + username.trim(); + password.trim(); - // Chiamata vera al backend + // Esegui richiesta di login const response = await api.post("/user/login", { username: username, password: password @@ -46,10 +38,8 @@ export default function LoginScreen() { // Passiamo token e dati utente al context che gestirà salvataggio e redirect authContext.logIn(token, user); - } catch (error: any) { console.error("Login Error:", error); - let message = "Si è verificato un errore durante l'accesso."; if (error.response) { diff --git a/components/AddDocumentModal.tsx b/components/AddDocumentModal.tsx new file mode 100644 index 0000000..9c09a2a --- /dev/null +++ b/components/AddDocumentModal.tsx @@ -0,0 +1,161 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Text, TouchableOpacity, View, TextInput, ActivityIndicator } from 'react-native'; +import { X, Upload, FileText, Trash2 } from 'lucide-react-native'; +import * as DocumentPicker from 'expo-document-picker'; // TODO: Testare in ambiente iOS + +interface AddDocumentModalProps { + visible: boolean; + onClose: () => void; + onUpload: (file: DocumentPicker.DocumentPickerAsset, customTitle?: string) => Promise; + isUploading?: boolean; +} + +export default function AddDocumentModal({ visible, onClose, onUpload, isUploading = false }: AddDocumentModalProps) { + const [selectedFile, setSelectedFile] = useState(null); + const [customTitle, setCustomTitle] = useState(''); + + // Reset dello stato quando il modale si apre/chiude + useEffect(() => { + if (!visible) { + setSelectedFile(null); + setCustomTitle(''); + } + }, [visible]); + + const pickDocument = async () => { + try { + const result = await DocumentPicker.getDocumentAsync({ + type: '*/*', // Puoi limitare a 'application/pdf', 'image/*', ecc. + copyToCacheDirectory: true, + }); + + if (result.canceled) return; + + const asset = result.assets[0]; + setSelectedFile(asset); + + // Pre-compila il titolo con il nome del file (senza estensione se vuoi essere fancy, qui lo lascio intero) + setCustomTitle(asset.name); + + } catch (err) { + console.error("Errore selezione file:", err); + } + }; + + const handleUpload = () => { + if (!selectedFile) return; + // Se il titolo custom è vuoto, usiamo il nome originale + onUpload(selectedFile, customTitle || selectedFile.name); + }; + + const removeFile = () => { + setSelectedFile(null); + setCustomTitle(''); + }; + + // Formatta dimensione file + const formatSize = (size?: number) => { + if (!size) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(size) / Math.log(k)); + return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + return ( + + + + + {/* Header */} + + Carica Documento + + + + + + {/* Body */} + + + {/* Area Selezione File */} + {!selectedFile ? ( + + + + + Tocca per selezionare un file + PDF, Immagini, Word + + ) : ( + // Visualizzazione File Selezionato + + + + + + + {selectedFile.name} + + + {formatSize(selectedFile.size)} + + + + + + + )} + + {/* Campo Rinomina (Visibile solo se c'è un file) */} + {selectedFile && ( + + Nome Documento (Opzionale) + + + )} + + + {/* Footer Buttons */} + + + Annulla + + + + {isUploading ? ( + + ) : ( + <> + + Carica + + )} + + + + + + ); +} \ No newline at end of file diff --git a/components/RangePickerModal.tsx b/components/RangePickerModal.tsx index b8a3eb5..3561c60 100644 --- a/components/RangePickerModal.tsx +++ b/components/RangePickerModal.tsx @@ -1,15 +1,20 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Alert, Modal, Text, TouchableOpacity, View } from 'react-native'; -import DateTimePicker, { DateType, useDefaultStyles, useDefaultClassNames } from 'react-native-ui-datepicker'; +import DateTimePicker, { DateType, useDefaultStyles } from 'react-native-ui-datepicker'; import { Check, X } from 'lucide-react-native'; -export const RangePickerModal = ({ visible, onClose, currentRange, onApply }: any) => { +export const RangePickerModal = ({ visible, onClose, onApply }: any) => { const defaultStyles = useDefaultStyles(); - // const defaultClassNames = useDefaultClassNames(); const [range, setRange] = useState<{ startDate: DateType; endDate: DateType; - }>({ startDate: undefined, endDate: undefined }); + }>({ startDate: null, endDate: null }); + + const clearCalendar = () => { + setRange({ startDate: null, endDate: null }); + onApply({ startDate: null, endDate: null }); + onClose(); + }; return ( Applica Filtro { - console.log("Reset pressed"); - range.startDate = range.endDate = undefined; - // TODO: deselect dates from calendar - }} + onPress={clearCalendar} className="mt-2 bg-gray-200 rounded-xl py-4 flex-row justify-center items-center active:bg-gray-300" > Reset diff --git a/package-lock.json b/package-lock.json index 9e19ef8..0a9aa1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "axios": "^1.13.2", "expo": "~54.0.25", "expo-constants": "~18.0.10", + "expo-document-picker": "~14.0.8", "expo-font": "~14.0.9", "expo-haptics": "~15.0.7", "expo-image": "~3.0.10", @@ -6442,6 +6443,15 @@ "react-native": "*" } }, + "node_modules/expo-document-picker": { + "version": "14.0.8", + "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz", + "integrity": "sha512-3tyQKpPqWWFlI8p9RiMX1+T1Zge5mEKeBuXWp1h8PEItFMUDSiOJbQ112sfdC6Hxt8wSxreV9bCRl/NgBdt+fA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-file-system": { "version": "19.0.20", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.20.tgz", diff --git a/package.json b/package.json index a58e3c8..2d6d979 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "axios": "^1.13.2", "expo": "~54.0.25", "expo-constants": "~18.0.10", + "expo-document-picker": "~14.0.8", "expo-font": "~14.0.9", "expo-haptics": "~15.0.7", "expo-image": "~3.0.10", diff --git a/types/types.ts b/types/types.ts index c02e005..8706744 100644 --- a/types/types.ts +++ b/types/types.ts @@ -22,10 +22,9 @@ export interface AttendanceRecord { export interface DocumentItem { id: number; - name: string; - type: string; - site: string; - date: string; + title: string; + url: string; + updated_at: string; } export interface OfficeItem { @@ -42,8 +41,8 @@ export interface TimeOffRequestType { name: string; color: string; abbreviation: string; - time_required: number; // backend usa 0/1 - deleted: number; // backend usa 0/1 + time_required: number; + deleted: number; } export interface TimeOffRequest { @@ -57,4 +56,12 @@ export interface TimeOffRequest { message?: string | null; status: number; timeOffRequestType: TimeOffRequestType; +} + +export interface ConstructionSite { + id: number; + // id_client: number; + name: string; + code: string; + attachments_count: number; } \ No newline at end of file diff --git a/utils/dateTime.ts b/utils/dateTime.ts index f0983f6..fae36ea 100644 --- a/utils/dateTime.ts +++ b/utils/dateTime.ts @@ -39,3 +39,37 @@ export const formatPickerDate = (d: DateType | null | undefined) => { return `${yyyy}-${mm}-${dd}`; } + +/** + * Trasforma un timestamp in stringa "DD/MM/YYYY HH:mm:ss" + * @param timestamp stringa o oggetto Date + * @returns stringa formattata oppure vuota se input non valido + */ +export const formatTimestamp = (timestamp: string | Date | null | undefined): string => { + if (!timestamp) return ''; + + const date = timestamp instanceof Date ? timestamp : new Date(timestamp); + if (isNaN(date.getTime())) return ''; + + const dd = String(date.getDate()).padStart(2, '0'); + const mm = String(date.getMonth() + 1).padStart(2, '0'); // mesi da 0 a 11 + const yyyy = date.getFullYear(); + + const hh = String(date.getHours()).padStart(2, '0'); + const min = String(date.getMinutes()).padStart(2, '0'); + const ss = String(date.getSeconds()).padStart(2, '0'); + + return `${dd}/${mm}/${yyyy} ${hh}:${min}:${ss}`; +}; + +/** + * Converte un timestamp ISO in oggetto Date + * @param dateStr stringa data in formato ISO + * @returns oggetto Date corrispondente + */ +export const parseTimestamp = (dateStr: string | undefined | null): Date => { + if (!dateStr) return new Date(); + const date = new Date(dateStr); + if (isNaN(date.getTime())) return new Date(); + return date; +}; \ No newline at end of file