feat: Update assets and improve code comments across multiple components
- Updated favicon and various image assets. - Enhanced comments. - Adjusted styles and functionality in several components for improved user experience. - Updated package-lock.json to reflect dependency updates.
This commit is contained in:
@@ -14,7 +14,6 @@ export default function ProtectedLayout() {
|
||||
return <Redirect href="/login" />;
|
||||
}
|
||||
|
||||
// TODO: Aggiungere padding per i dispositivi con notch/bottom bar
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
@@ -66,7 +65,7 @@ export default function ProtectedLayout() {
|
||||
tabBarIcon: ({ color, size }) => <Building color={color} size={24} />,
|
||||
}}
|
||||
/>
|
||||
{/* // TODO: Rimuovere all'utente e mostrare solo a admin */}
|
||||
{/* // TODO: Probably needs to be restricted to admin */}
|
||||
<Tabs.Screen
|
||||
name="automation"
|
||||
options={{
|
||||
@@ -74,7 +73,7 @@ export default function ProtectedLayout() {
|
||||
tabBarIcon: ({ color, size }) => <Zap color={color} size={24} />,
|
||||
}}
|
||||
/>
|
||||
{/* TODO: Dovrebbe essere rimosso, va rivisto layout */}
|
||||
{/* TODO: Should be removed - move tabs inside (tabs) and refactor _layout */}
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
|
||||
@@ -9,6 +9,7 @@ import api from '@/utils/api';
|
||||
import { formatDate, formatTime, parseSecondsToTime } from '@/utils/dateTime';
|
||||
import { AttendanceRecord } from '@/types/types';
|
||||
import NfcManager from 'react-native-nfc-manager';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function AttendanceScreen() {
|
||||
const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr');
|
||||
@@ -20,6 +21,7 @@ export default function AttendanceScreen() {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const checkNfcAvailability = async () => {
|
||||
// TODO: add env variable to disable NFC checks in development or if not needed
|
||||
// if (!ENABLE_NFC) return;
|
||||
try {
|
||||
const isSupported = await NfcManager.isSupported();
|
||||
@@ -58,13 +60,13 @@ export default function AttendanceScreen() {
|
||||
};
|
||||
|
||||
const handleStartScan = async () => {
|
||||
// Modalità QR Code
|
||||
// Qr Code mode
|
||||
if (scannerType === 'qr') {
|
||||
setShowScanner(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Modalità NFC
|
||||
// NFC mode
|
||||
if (scannerType === 'nfc') {
|
||||
try {
|
||||
const supported = await NfcManager.isSupported();
|
||||
@@ -117,9 +119,11 @@ export default function AttendanceScreen() {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
{/* Header */}
|
||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100">
|
||||
<Text className="text-3xl font-bold text-gray-800 mb-1">Gestione Presenze</Text>
|
||||
<Text className="text-base text-gray-500">Registra i tuoi movimenti</Text>
|
||||
<View className="bg-white px-6 pb-6 shadow-sm border-b border-gray-100">
|
||||
<SafeAreaView edges={['top']} className='pt-4'>
|
||||
<Text className="text-3xl font-bold text-gray-800 mb-1">Gestione Presenze</Text>
|
||||
<Text className="text-base text-gray-500">Registra i tuoi movimenti</Text>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
@@ -215,7 +219,7 @@ export default function AttendanceScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* TODO: item.time può essere null -> calcolare tempo da in e out? */}
|
||||
{/* TODO: item.time can be null - calculate time from in and 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">
|
||||
|
||||
@@ -197,13 +197,6 @@ export default function AutomationScreen() {
|
||||
)}
|
||||
<View className="h-20" />
|
||||
</ScrollView>
|
||||
|
||||
{/* <TouchableOpacity
|
||||
onPress={() => alert('Aggiungi nuovo collegamento')}
|
||||
className="absolute bottom-8 right-6 w-16 h-16 bg-[#099499] rounded-full shadow-lg items-center justify-center active:scale-90"
|
||||
>
|
||||
<Plus size={32} color="white" />
|
||||
</TouchableOpacity> */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { AuthContext } from '@/utils/authContext';
|
||||
import { ActivityItem } from '@/types/types';
|
||||
import api from '@/utils/api';
|
||||
import LoadingScreen from '@/components/LoadingScreen';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function HomeScreen() {
|
||||
const router = useRouter();
|
||||
@@ -63,27 +64,29 @@ export default function HomeScreen() {
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-[#099499]">
|
||||
{/* Banner Custom */}
|
||||
<View className="pt-16 pb-6 px-6 shadow-sm z-10">
|
||||
<View className="flex-row justify-between items-start">
|
||||
<View className="flex-row items-center gap-4 flex-1 mr-4">
|
||||
<View className="flex-1">
|
||||
<Text className="text-teal-100 text-lg font-medium uppercase tracking-wider mb-1">Benvenuto</Text>
|
||||
<Text className="text-white text-3xl font-bold leading-tight">
|
||||
{user?.name} {user?.surname}
|
||||
</Text>
|
||||
<Text className="text-xl text-teal-200">{user?.role}</Text>
|
||||
<SafeAreaView edges={['top']} className='pt-4'>
|
||||
{/* Custom Banner */}
|
||||
<View className="pb-6 px-6 shadow-sm z-10">
|
||||
<View className="flex-row justify-between items-start">
|
||||
<View className="flex-row items-center gap-4 flex-1 mr-4">
|
||||
<View className="flex-1">
|
||||
<Text className="text-teal-100 text-lg font-medium uppercase tracking-wider mb-1">Benvenuto</Text>
|
||||
<Text className="text-white text-3xl font-bold leading-tight">
|
||||
{user?.name} {user?.surname}
|
||||
</Text>
|
||||
<Text className="text-xl text-teal-200">{user?.role}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row gap-4 flex-shrink-0">
|
||||
<TouchableOpacity className="p-3 bg-white/10 rounded-full active:bg-white/20" onPress={() => router.push('/profile')}>
|
||||
<User size={28} color="white" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row gap-4 flex-shrink-0">
|
||||
<TouchableOpacity className="p-3 bg-white/10 rounded-full active:bg-white/20" onPress={() => router.push('/profile')}>
|
||||
<User size={28} color="white" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
|
||||
{/* Contenuto Scrollabile */}
|
||||
{/* Scrollable Content */}
|
||||
<ScrollView
|
||||
className="flex-1 bg-gray-50 rounded-t-[2.5rem] px-5 pt-6"
|
||||
contentContainerStyle={{ paddingBottom: 50, gap: 24 }}
|
||||
@@ -93,10 +96,13 @@ export default function HomeScreen() {
|
||||
}
|
||||
>
|
||||
|
||||
{/* Warning Card */}
|
||||
{/* Warning Card - Incomplete Attendance */}
|
||||
{incompleteAttendance && (
|
||||
<View className="bg-white p-6 rounded-3xl shadow-md border-l-8 border-orange-500 flex-row items-center justify-between">
|
||||
<View className="flex-row items-center gap-5 flex-1">
|
||||
<TouchableOpacity
|
||||
className="bg-white p-6 rounded-3xl shadow-md border-l-8 border-orange-500"
|
||||
onPress={() => router.push('/attendance')}
|
||||
>
|
||||
<View className="flex-row items-center gap-5">
|
||||
<View className="bg-orange-100 p-4 rounded-full">
|
||||
<AlertTriangle size={32} color="#f97316" />
|
||||
</View>
|
||||
@@ -105,10 +111,7 @@ export default function HomeScreen() {
|
||||
<Text className="text-base text-gray-500 mt-1">{incompleteAttendance}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => router.push('/attendance')} className="bg-orange-50 px-5 py-3 rounded-xl ml-2 active:bg-orange-100">
|
||||
<Text className="text-orange-600 text-sm font-bold uppercase tracking-wide">Risolvi</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
@@ -141,6 +144,7 @@ export default function HomeScreen() {
|
||||
<View>
|
||||
<View className="flex-row justify-between items-center px-1 mb-4">
|
||||
<Text className="text-gray-800 text-xl font-bold">Ultime Attività</Text>
|
||||
{/* TODO: Could be expanded */}
|
||||
{/* <TouchableOpacity>
|
||||
<Text className="text-base text-[#099499] font-bold p-1">Vedi tutto</Text>
|
||||
</TouchableOpacity> */}
|
||||
@@ -153,12 +157,12 @@ export default function HomeScreen() {
|
||||
<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 */}
|
||||
{/* Icon */}
|
||||
<View className={`${config.bg} p-4 rounded-2xl flex-shrink-0`}>
|
||||
<IconComponent size={24} color={config.color} />
|
||||
</View>
|
||||
|
||||
{/* Titolo e Sottotitolo */}
|
||||
{/* Title and Subtitle */}
|
||||
<View className="flex-1 mr-2">
|
||||
<Text
|
||||
className="text-lg font-bold text-gray-800 mb-0.5 leading-tight"
|
||||
@@ -176,7 +180,7 @@ export default function HomeScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Data */}
|
||||
{/* Date */}
|
||||
<View className="flex-shrink-0">
|
||||
<Text className="text-sm font-bold text-gray-300">{item.date_display}</Text>
|
||||
</View>
|
||||
|
||||
@@ -8,13 +8,14 @@ import CalendarWidget from '@/components/CalendarWidget';
|
||||
import LoadingScreen from '@/components/LoadingScreen';
|
||||
import api from '@/utils/api';
|
||||
import { formatDate, formatTime } from '@/utils/dateTime';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
// Icon Mapping
|
||||
const typeIcons: Record<string, (color: string) => JSX.Element> = {
|
||||
Ferie: (color) => <CalendarIcon size={24} color={color} />,
|
||||
Permesso: (color) => <Clock size={24} color={color} />,
|
||||
Malattia: (color) => <Thermometer size={24} color={color} />,
|
||||
Assenza: (color) => <CalendarX size={24} color={color} />,
|
||||
Ferie: (color) => <CalendarIcon size={24} color={color} />,
|
||||
Permesso: (color) => <Clock size={24} color={color} />,
|
||||
Malattia: (color) => <Thermometer size={24} color={color} />,
|
||||
Assenza: (color) => <CalendarX size={24} color={color} />,
|
||||
};
|
||||
|
||||
export default function PermitsScreen() {
|
||||
@@ -33,7 +34,7 @@ export default function PermitsScreen() {
|
||||
// Fetch Permits
|
||||
const response = await api.get('/time-off-request/list');
|
||||
setPermits(response.data);
|
||||
|
||||
|
||||
// Fetch Types
|
||||
const typesResponse = await api.get('/time-off-request/get-types');
|
||||
setTypes(typesResponse.data);
|
||||
@@ -49,22 +50,21 @@ export default function PermitsScreen() {
|
||||
const filteredPermits = useMemo(() => {
|
||||
if (!permits.length) return [];
|
||||
|
||||
// Calcoliamo inizio e fine del mese visualizzato
|
||||
// Calculate start and end of the current month
|
||||
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);
|
||||
// Day 0 of the next month = last day of the current month
|
||||
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)
|
||||
// If there's no end_date, assume it's a single day (so 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.
|
||||
// The permit is visible if it starts before the end of the month
|
||||
// And ends after the start of the month.
|
||||
return itemStart <= endOfMonth && itemEnd >= startOfMonth;
|
||||
});
|
||||
}, [permits, currentMonthDate]);
|
||||
@@ -92,11 +92,13 @@ export default function PermitsScreen() {
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100 flex-row justify-between items-center">
|
||||
<View>
|
||||
<Text className="text-3xl font-bold text-gray-800 mb-1">Ferie e Permessi</Text>
|
||||
<Text className="text-base text-gray-500">Gestisci le tue assenze</Text>
|
||||
</View>
|
||||
<View className="bg-white px-6 pb-6 shadow-sm border-b border-gray-100 flex-row justify-between items-center">
|
||||
<SafeAreaView edges={['top']} className='pt-4'>
|
||||
<View>
|
||||
<Text className="text-3xl font-bold text-gray-800 mb-1">Ferie e Permessi</Text>
|
||||
<Text className="text-base text-gray-500">Gestisci le tue assenze</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
@@ -110,7 +112,7 @@ export default function PermitsScreen() {
|
||||
{/* Calendar Widget */}
|
||||
<CalendarWidget events={permits} types={types} onMonthChange={(date) => setCurrentMonthDate(date)} />
|
||||
|
||||
{/* Lista Richieste Recenti */}
|
||||
{/* Recent Requests List */}
|
||||
<View>
|
||||
{filteredPermits.length === 0 ? (
|
||||
<Text className="text-center text-gray-500 mt-8">Nessuna richiesta di permesso questo mese</Text>
|
||||
@@ -135,7 +137,7 @@ export default function PermitsScreen() {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{/* TODO: Aggiungere funzionalità per modificare/eliminare la richiesta? */}
|
||||
{/* TODO: Add functionality to edit/remove the request */}
|
||||
<View className={`px-3 py-1.5 rounded-lg ${item.status ? 'bg-green-100' : 'bg-yellow-100'}`}>
|
||||
<Text className={`text-xs font-bold uppercase tracking-wide ${item.status ? 'text-green-700' : 'text-yellow-700'}`}>
|
||||
{item.status ? 'Approvato' : 'In Attesa'}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAlert } from '@/components/AlertComponent';
|
||||
import { ArrowLeft, Download, FileText, MapPin, Plus, Search, CalendarIcon } from 'lucide-react-native';
|
||||
import { Download, FileText, MapPin, Plus, Search, CalendarIcon, ChevronLeft } from 'lucide-react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { RangePickerModal } from '@/components/RangePickerModal';
|
||||
@@ -11,6 +11,7 @@ import LoadingScreen from '@/components/LoadingScreen';
|
||||
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
|
||||
import AddDocumentModal from '@/components/AddDocumentModal';
|
||||
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function DocumentsScreen() {
|
||||
const router = useRouter();
|
||||
@@ -32,7 +33,7 @@ export default function DocumentsScreen() {
|
||||
try {
|
||||
if (!refreshing) setIsLoading(true);
|
||||
|
||||
// Fetch Documenti Utente
|
||||
// Fetch User Documents
|
||||
const response = await api.get(`/attachment/get-user-attachments`);
|
||||
setDocuments(response.data);
|
||||
} catch (error) {
|
||||
@@ -53,24 +54,24 @@ export default function DocumentsScreen() {
|
||||
fetchUserDocuments();
|
||||
};
|
||||
|
||||
// Filtra Documenti in base a searchTerm e range
|
||||
// Filter Documents based on searchTerm and range
|
||||
const filteredDocs = documents.filter(doc => {
|
||||
// Filtro Testuale
|
||||
// Text Filter
|
||||
const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
if (!matchesSearch) return false;
|
||||
|
||||
// Filtro Date Range
|
||||
// Date Range Filter
|
||||
if (range.startDate || range.endDate) {
|
||||
const docDate = parseTimestamp(doc.updated_at); // doc.date è "DD/MM/YYYY"
|
||||
|
||||
// Controllo Data Inizio
|
||||
// Start Date Check
|
||||
if (range.startDate) {
|
||||
// dayjs(range.startDate).toDate() converte in oggetto Date JS standard
|
||||
// dayjs(range.startDate).toDate() converts to standard JS Date object
|
||||
const start = dayjs(range.startDate).startOf('day').toDate();
|
||||
if (docDate < start) return false;
|
||||
}
|
||||
|
||||
// Controllo Data Fine
|
||||
// End Date Check
|
||||
if (range.endDate) {
|
||||
const end = dayjs(range.endDate).endOf('day').toDate();
|
||||
if (docDate > end) return false;
|
||||
@@ -80,7 +81,7 @@ export default function DocumentsScreen() {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Gestione Caricamento Documento
|
||||
// Document Upload Handling
|
||||
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
||||
setIsUploading(true);
|
||||
try {
|
||||
@@ -105,14 +106,18 @@ export default function DocumentsScreen() {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
{/* Header */}
|
||||
<View className="flex-row items-center gap-4 bg-white p-6 pt-16 shadow-sm border-b border-gray-100">
|
||||
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100">
|
||||
<ArrowLeft size={24} color="#374151" />
|
||||
</TouchableOpacity>
|
||||
<View className="flex-1">
|
||||
<Text className="text-3xl font-bold text-gray-800">Documenti</Text>
|
||||
<Text className="text-base text-gray-500">Gestisci i tuoi documenti</Text>
|
||||
</View>
|
||||
<View className="bg-white px-4 pb-6 shadow-sm border-b border-gray-100">
|
||||
<SafeAreaView edges={['top']} className='pt-4'>
|
||||
<View className='flex-row items-center gap-4'>
|
||||
<TouchableOpacity onPress={() => router.back()} className="p-2 rounded-full active:bg-gray-100">
|
||||
<ChevronLeft size={28} color="#4b5563" />
|
||||
</TouchableOpacity>
|
||||
<View className="flex-1">
|
||||
<Text className="text-3xl font-bold text-gray-800">Documenti</Text>
|
||||
<Text className="text-base text-gray-500">Gestisci i tuoi documenti</Text>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
|
||||
<View className="p-5 gap-6 flex-1">
|
||||
@@ -141,7 +146,7 @@ export default function DocumentsScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Modale Unico per il Range */}
|
||||
{/* Range Picker Modal */}
|
||||
<RangePickerModal
|
||||
visible={showRangePicker}
|
||||
onClose={() => setShowRangePicker(false)}
|
||||
@@ -149,7 +154,7 @@ export default function DocumentsScreen() {
|
||||
onApply={setRange}
|
||||
/>
|
||||
|
||||
{/* List */}
|
||||
{/* Documents List */}
|
||||
<ScrollView
|
||||
contentContainerStyle={{ gap: 16, paddingBottom: 100 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
@@ -172,7 +177,7 @@ export default function DocumentsScreen() {
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => downloadAndShareDocument(doc.mimetype, doc.title, doc.url)}
|
||||
onPress={() => downloadAndShareDocument(doc.mimetype, doc.title, doc.url)}
|
||||
className="p-4 bg-gray-50 rounded-2xl active:bg-gray-100">
|
||||
<Download size={24} color="#6b7280" />
|
||||
</TouchableOpacity>
|
||||
@@ -193,7 +198,7 @@ export default function DocumentsScreen() {
|
||||
<Plus size={32} color="white" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Modale Caricamento Documento */}
|
||||
{/* Document Upload Modal */}
|
||||
<AddDocumentModal
|
||||
visible={showUploadModal}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
|
||||
@@ -3,55 +3,56 @@ import { ChevronLeft, FileText, LogOut, Mail, Settings, User } from 'lucide-reac
|
||||
import React, { useContext } from 'react';
|
||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { AuthContext } from '@/utils/authContext';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const authContext = useContext(AuthContext);
|
||||
const { user } = authContext;
|
||||
const router = useRouter();
|
||||
|
||||
// Genera le iniziali dell'utente
|
||||
// Generate user initials
|
||||
const initials = `${user?.name?.[0] ?? ''}${user?.surname?.[0] ?? ''}`.toUpperCase();
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-[#099499]">
|
||||
{/* --- SEZIONE HEADER (INVARIATA) --- */}
|
||||
<View className="pt-16 pb-6 px-6">
|
||||
<View className="flex-row justify-start items-center gap-4">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<ChevronLeft size={28} color="white" />
|
||||
</TouchableOpacity>
|
||||
<View className="flex-row items-center gap-4">
|
||||
<View className="w-16 h-16 rounded-full bg-white/20 items-center justify-center">
|
||||
<Text className="text-white font-bold text-2xl">{initials}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-teal-100 text-lg font-medium uppercase tracking-wider mb-1">Profilo</Text>
|
||||
<Text className="text-white text-2xl font-bold">{user?.name} {user?.surname}</Text>
|
||||
<SafeAreaView edges={['top']} className='pt-4'>
|
||||
{/* Header Section */}
|
||||
<View className="pb-6 px-4">
|
||||
<View className="flex-row justify-start items-center gap-4">
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<ChevronLeft size={28} color="white" />
|
||||
</TouchableOpacity>
|
||||
<View className="flex-row items-center gap-4">
|
||||
<View className="w-16 h-16 rounded-full bg-white/20 items-center justify-center">
|
||||
<Text className="text-white font-bold text-2xl">{initials}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-teal-100 text-lg font-medium uppercase tracking-wider mb-1">Profilo</Text>
|
||||
<Text className="text-white text-2xl font-bold">{user?.name} {user?.surname}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
|
||||
<ScrollView
|
||||
className="flex-1 bg-gray-50 rounded-t-[2.5rem] px-5 pt-8"
|
||||
contentContainerStyle={{ paddingBottom: 60, gap: 24 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Card info - Testi ingranditi */}
|
||||
{/* Info Card - Enlarged Texts */}
|
||||
<View className="bg-white p-7 rounded-3xl shadow-sm border border-gray-100">
|
||||
{/* Titolo sezione ingrandito */}
|
||||
{/* Section title */}
|
||||
<Text className="text-2xl font-bold text-gray-800">Informazioni</Text>
|
||||
|
||||
<View className="mt-6 gap-5">
|
||||
<View className="flex-row items-center gap-5">
|
||||
{/* Icona leggermente più grande e container adattato */}
|
||||
<View className="w-14 h-14 bg-gray-100 rounded-2xl items-center justify-center">
|
||||
<Mail size={24} color="#374151" />
|
||||
</View>
|
||||
<View>
|
||||
{/* Label e valore ingranditi */}
|
||||
<Text className="text-lg text-gray-700 font-bold">Email</Text>
|
||||
<Text className="text-gray-500 text-base">{user?.email}</Text>
|
||||
</View>
|
||||
@@ -69,7 +70,7 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Actions - Testi e Pulsanti ingranditi */}
|
||||
{/* Actions */}
|
||||
<View>
|
||||
<Text className="text-gray-800 text-2xl font-bold mb-5 px-1">Azioni</Text>
|
||||
|
||||
@@ -86,7 +87,8 @@ export default function ProfileScreen() {
|
||||
<Text className="text-[#099499] text-base font-bold">Apri</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => console.log('Apri impostazioni')} className="bg-white p-4 rounded-3xl shadow-sm flex-row items-center justify-between border border-gray-100 mb-4">
|
||||
{/* TODO: Settings not implemented at the moment */}
|
||||
{/* <TouchableOpacity onPress={() => console.log('Apri impostazioni')} className="bg-white p-4 rounded-3xl shadow-sm flex-row items-center justify-between border border-gray-100 mb-4">
|
||||
<View className="flex-row items-center gap-5">
|
||||
<View className="bg-gray-100 p-3.5 rounded-2xl">
|
||||
<Settings size={26} color="#374151" />
|
||||
@@ -97,7 +99,7 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-gray-400 text-base font-bold">Apri</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity> */}
|
||||
|
||||
<TouchableOpacity onPress={authContext.logOut} className="bg-white p-4 rounded-3xl shadow-sm flex-row items-center justify-between border border-gray-100">
|
||||
<View className="flex-row items-center gap-5">
|
||||
|
||||
@@ -11,6 +11,7 @@ import dayjs from 'dayjs';
|
||||
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
|
||||
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
|
||||
import AddDocumentModal from '@/components/AddDocumentModal';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function SiteDocumentsScreen() {
|
||||
const router = useRouter();
|
||||
@@ -24,12 +25,12 @@ export default function SiteDocumentsScreen() {
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
// Fetch dei documenti del cantiere
|
||||
// Fetch site documents
|
||||
const fetchSiteDocuments = useCallback(async (siteId: number, isRefreshing = false) => {
|
||||
try {
|
||||
if (!isRefreshing) setIsLoading(true);
|
||||
|
||||
// Fetch Documenti
|
||||
// Fetch Documents
|
||||
const response = await api.get(`/attachment/get-site-attachments`, {
|
||||
params: { siteId }
|
||||
});
|
||||
@@ -74,13 +75,13 @@ export default function SiteDocumentsScreen() {
|
||||
});
|
||||
const [showRangePicker, setShowRangePicker] = useState(false);
|
||||
|
||||
// Filtraggio Documenti
|
||||
// Filter documents based on search term and date range
|
||||
const filteredDocs = documents.filter(doc => {
|
||||
// Filtro Testuale (su nome documento)
|
||||
// Textual Filter (on document name)
|
||||
const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
if (!matchesSearch) return false;
|
||||
|
||||
// Filtro Date Range
|
||||
// Date Range Filter
|
||||
if (range.startDate || range.endDate) {
|
||||
const docDate = parseTimestamp(doc.updated_at);
|
||||
if (range.startDate) {
|
||||
@@ -95,7 +96,7 @@ export default function SiteDocumentsScreen() {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Gestione Download e Condivisione Documento
|
||||
// Handle Document Download and Share
|
||||
const handleDownloadAndShare = async (mimetype: string, fileName: string, fileUrl: string) => {
|
||||
try {
|
||||
await downloadAndShareDocument(mimetype, fileName, fileUrl);
|
||||
@@ -105,7 +106,7 @@ export default function SiteDocumentsScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
// Gestione Caricamento Documento
|
||||
// Handle Document Upload
|
||||
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
||||
setIsUploading(true);
|
||||
try {
|
||||
@@ -130,36 +131,40 @@ export default function SiteDocumentsScreen() {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
{/* Header */}
|
||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100 flex-row items-center gap-4">
|
||||
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100">
|
||||
<ChevronLeft size={28} color="#4b5563" />
|
||||
</TouchableOpacity>
|
||||
<View className="bg-white px-4 pb-6 shadow-sm border-b border-gray-100">
|
||||
<SafeAreaView edges={['top']} className='pt-4'>
|
||||
<View className='flex-row items-center gap-4'>
|
||||
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100">
|
||||
<ChevronLeft size={28} color="#4b5563" />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View className="flex-1">
|
||||
{/* Badge Codice Cantiere */}
|
||||
{site.code && (
|
||||
<View className="self-start bg-[#E6F4F4] px-2 py-0.5 rounded mb-1 border border-[#099499]/20">
|
||||
<Text className="text-base font-bold text-[#099499]">
|
||||
{site.code}
|
||||
<View className="flex-1">
|
||||
{/* Site Code Badge */}
|
||||
{site.code && (
|
||||
<View className="self-start bg-[#E6F4F4] px-2 py-0.5 rounded mb-1 border border-[#099499]/20">
|
||||
<Text className="text-base font-bold text-[#099499]">
|
||||
{site.code}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Site Name with Truncate */}
|
||||
<Text
|
||||
className="text-xl font-bold text-gray-800 leading-tight"
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{site.name}
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm text-gray-500 mt-0.5">Archivio documenti</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Nome Cantiere con Truncate funzionante */}
|
||||
<Text
|
||||
className="text-xl font-bold text-gray-800 leading-tight"
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{site.name}
|
||||
</Text>
|
||||
|
||||
<Text className="text-sm text-gray-500 mt-0.5">Archivio documenti</Text>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
|
||||
<View className="p-5 gap-6 flex-1">
|
||||
{/* Search + Date Row (Spostato qui) */}
|
||||
{/* Search + Date Row */}
|
||||
<View className="flex-row gap-2">
|
||||
<View className="flex-1 relative justify-center">
|
||||
<View className="absolute left-4 z-10">
|
||||
@@ -189,7 +194,7 @@ export default function SiteDocumentsScreen() {
|
||||
onApply={setRange}
|
||||
/>
|
||||
|
||||
{/* Lista Documenti */}
|
||||
{/* Documents List */}
|
||||
<ScrollView
|
||||
contentContainerStyle={{ gap: 16, paddingBottom: 100 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
@@ -222,7 +227,7 @@ export default function SiteDocumentsScreen() {
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* FAB (Spostato qui) */}
|
||||
{/* FAB */}
|
||||
<TouchableOpacity
|
||||
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"
|
||||
@@ -230,13 +235,13 @@ export default function SiteDocumentsScreen() {
|
||||
<Plus size={32} color="white" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Modale Caricamento Documento */}
|
||||
{/* Upload Document Modal */}
|
||||
<AddDocumentModal
|
||||
visible={showUploadModal}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
onUpload={handleUploadDocument}
|
||||
isUploading={isUploading}
|
||||
/>
|
||||
</View>
|
||||
</View >
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import api from '@/utils/api';
|
||||
import LoadingScreen from '@/components/LoadingScreen';
|
||||
import { ConstructionSite } from '@/types/types';
|
||||
import { useAlert } from '@/components/AlertComponent';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function SitesScreen() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -15,12 +16,12 @@ export default function SitesScreen() {
|
||||
const router = useRouter();
|
||||
const alert = useAlert();
|
||||
|
||||
// Fetch cantieri e documenti
|
||||
// Fetch sites and documents
|
||||
const fetchConstructionSites = async () => {
|
||||
try {
|
||||
if (!refreshing) setIsLoading(true);
|
||||
|
||||
// Fetch Cantieri
|
||||
// Fetch Sites and their attachments count
|
||||
const response = await api.get('/construction-site/sites-attachments');
|
||||
setConstructionSites(response.data);
|
||||
} catch (error) {
|
||||
@@ -41,7 +42,7 @@ export default function SitesScreen() {
|
||||
fetchConstructionSites();
|
||||
};
|
||||
|
||||
// Filtriamo i cantieri in base alla ricerca
|
||||
// Filter sites based on search term
|
||||
const filteredSites = constructionSites.filter(site =>
|
||||
site.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
site.code.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
@@ -54,9 +55,11 @@ export default function SitesScreen() {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
{/* Header */}
|
||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100">
|
||||
<Text className="text-3xl font-bold text-gray-800 mb-1">Cantieri</Text>
|
||||
<Text className="text-base text-gray-500">Seleziona un cantiere per vedere i documenti</Text>
|
||||
<View className="bg-white px-6 pb-6 shadow-sm border-b border-gray-100">
|
||||
<SafeAreaView edges={['top']} className='pt-4'>
|
||||
<Text className="text-3xl font-bold text-gray-800 mb-1">Cantieri</Text>
|
||||
<Text className="text-base text-gray-500">Seleziona un cantiere per vedere i documenti</Text>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
|
||||
<View className="px-5 mt-5 gap-6 flex-1">
|
||||
@@ -74,11 +77,10 @@ export default function SitesScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Lista Cantieri */}
|
||||
{/* TODO: Rimuovere lo ScrollIndicator? */}
|
||||
{/* Sites List */}
|
||||
<ScrollView
|
||||
contentContainerStyle={{ gap: 16, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
||||
}
|
||||
@@ -88,8 +90,8 @@ export default function SitesScreen() {
|
||||
key={index}
|
||||
onPress={() => router.push({
|
||||
pathname: '/(protected)/sites/[id]',
|
||||
params: {
|
||||
id: site.id ,
|
||||
params: {
|
||||
id: site.id,
|
||||
siteData: JSON.stringify(site),
|
||||
}
|
||||
})}
|
||||
@@ -111,7 +113,6 @@ export default function SitesScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Freccia al posto del download */}
|
||||
<ChevronRight size={24} color="#9ca3af" />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
@@ -3,18 +3,21 @@ import { AuthProvider } from '@/utils/authContext';
|
||||
import { Stack } from 'expo-router';
|
||||
import { AlertProvider } from '@/components/AlertComponent';
|
||||
import { NetworkProvider } from '@/utils/networkProvider';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
|
||||
export default function AppLayout() {
|
||||
return (
|
||||
<NetworkProvider>
|
||||
<AuthProvider>
|
||||
<AlertProvider>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="(protected)" />
|
||||
<Stack.Screen name="login" />
|
||||
</Stack>
|
||||
</AlertProvider>
|
||||
</AuthProvider>
|
||||
</NetworkProvider>
|
||||
<SafeAreaProvider>
|
||||
<NetworkProvider>
|
||||
<AuthProvider>
|
||||
<AlertProvider>
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="(protected)" />
|
||||
<Stack.Screen name="login" />
|
||||
</Stack>
|
||||
</AlertProvider>
|
||||
</AuthProvider>
|
||||
</NetworkProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
@@ -13,21 +13,19 @@ export default function LoginScreen() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// TODO: Riscrivere funzione per migliorare leggibilità e gestione errori
|
||||
// Login Handler function
|
||||
const handleLogin = async () => {
|
||||
// TODO: Implementa toast o messaggio di errore più user-friendly
|
||||
if (!username || !password) {
|
||||
alert.showAlert('error', 'Attenzione', 'Inserisci username e password');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
username.trim();
|
||||
password.trim();
|
||||
|
||||
// Esegui richiesta di login
|
||||
// Execute login request
|
||||
const response = await api.post("/user/login", {
|
||||
username: username,
|
||||
password: password
|
||||
@@ -35,25 +33,24 @@ export default function LoginScreen() {
|
||||
|
||||
const { token, user } = response.data;
|
||||
|
||||
console.log("Login successo, token ricevuto.");
|
||||
console.log("Login riuscito. Token:", token);
|
||||
console.log("Dati utente:", user);
|
||||
|
||||
// Passiamo token e dati utente al context che gestirà salvataggio e redirect
|
||||
// Pass token and user data to the context which will handle saving and 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) {
|
||||
// Errore dal server (es. 401 Credenziali errate)
|
||||
// Server error (e.g., 401 Invalid credentials)
|
||||
if (error.response.status === 401) {
|
||||
// TODO: Alert o Toast specifico per credenziali errate
|
||||
message = "Credenziali non valide."
|
||||
} else {
|
||||
message = `Errore Server: ${error.response.data.message || error.response.status}`;
|
||||
}
|
||||
} else if (error.request) {
|
||||
// Server non raggiungibile
|
||||
// Server not reachable
|
||||
message = "Impossibile contattare il server. Controlla la connessione.";
|
||||
}
|
||||
|
||||
@@ -65,7 +62,7 @@ export default function LoginScreen() {
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-[#099499] h-screen overflow-hidden">
|
||||
{/* Header con Logo/Titolo */}
|
||||
{/* Header with Logo/Title */}
|
||||
<View className="h-[35%] flex-column justify-center items-center">
|
||||
<Image
|
||||
source={require('@/assets/images/mariani-logo.png')}
|
||||
@@ -120,12 +117,13 @@ export default function LoginScreen() {
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{/* TODO: Implement password recovery functionality */}
|
||||
<TouchableOpacity className="mt-3 self-end" style={{ alignSelf: 'flex-end' }}>
|
||||
<Text className="text-[#099499] font-bold text-base">Password dimenticata?</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Tasto Login */}
|
||||
{/* Login Button */}
|
||||
<TouchableOpacity
|
||||
onPress={handleLogin}
|
||||
activeOpacity={0.8}
|
||||
|
||||
Reference in New Issue
Block a user