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.
8
app.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "mariani_app",
|
"name": "Mariani",
|
||||||
"slug": "mariani_app",
|
"slug": "mariani-app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/mariani-icon.png",
|
"icon": "./assets/images/mariani-icon.png",
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "com.anonymous.mariani-app"
|
"bundleIdentifier": "com.pcrt.mariani-app"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"android.permission.CAMERA",
|
"android.permission.CAMERA",
|
||||||
"android.permission.RECORD_AUDIO"
|
"android.permission.RECORD_AUDIO"
|
||||||
],
|
],
|
||||||
"package": "com.anonymous.mariani_app"
|
"package": "com.pcrt.mariani-app"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"output": "static",
|
"output": "static",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export default function ProtectedLayout() {
|
|||||||
return <Redirect href="/login" />;
|
return <Redirect href="/login" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Aggiungere padding per i dispositivi con notch/bottom bar
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
@@ -66,7 +65,7 @@ export default function ProtectedLayout() {
|
|||||||
tabBarIcon: ({ color, size }) => <Building color={color} size={24} />,
|
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
|
<Tabs.Screen
|
||||||
name="automation"
|
name="automation"
|
||||||
options={{
|
options={{
|
||||||
@@ -74,7 +73,7 @@ export default function ProtectedLayout() {
|
|||||||
tabBarIcon: ({ color, size }) => <Zap color={color} size={24} />,
|
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
|
<Tabs.Screen
|
||||||
name="profile"
|
name="profile"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import api from '@/utils/api';
|
|||||||
import { formatDate, formatTime, parseSecondsToTime } from '@/utils/dateTime';
|
import { formatDate, formatTime, parseSecondsToTime } from '@/utils/dateTime';
|
||||||
import { AttendanceRecord } from '@/types/types';
|
import { AttendanceRecord } from '@/types/types';
|
||||||
import NfcManager from 'react-native-nfc-manager';
|
import NfcManager from 'react-native-nfc-manager';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function AttendanceScreen() {
|
export default function AttendanceScreen() {
|
||||||
const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr');
|
const [scannerType, setScannerType] = useState<'qr' | 'nfc'>('qr');
|
||||||
@@ -20,6 +21,7 @@ export default function AttendanceScreen() {
|
|||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const checkNfcAvailability = async () => {
|
const checkNfcAvailability = async () => {
|
||||||
|
// TODO: add env variable to disable NFC checks in development or if not needed
|
||||||
// if (!ENABLE_NFC) return;
|
// if (!ENABLE_NFC) return;
|
||||||
try {
|
try {
|
||||||
const isSupported = await NfcManager.isSupported();
|
const isSupported = await NfcManager.isSupported();
|
||||||
@@ -58,13 +60,13 @@ export default function AttendanceScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleStartScan = async () => {
|
const handleStartScan = async () => {
|
||||||
// Modalità QR Code
|
// Qr Code mode
|
||||||
if (scannerType === 'qr') {
|
if (scannerType === 'qr') {
|
||||||
setShowScanner(true);
|
setShowScanner(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modalità NFC
|
// NFC mode
|
||||||
if (scannerType === 'nfc') {
|
if (scannerType === 'nfc') {
|
||||||
try {
|
try {
|
||||||
const supported = await NfcManager.isSupported();
|
const supported = await NfcManager.isSupported();
|
||||||
@@ -117,9 +119,11 @@ export default function AttendanceScreen() {
|
|||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-gray-50">
|
<View className="flex-1 bg-gray-50">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100">
|
<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-3xl font-bold text-gray-800 mb-1">Gestione Presenze</Text>
|
||||||
<Text className="text-base text-gray-500">Registra i tuoi movimenti</Text>
|
<Text className="text-base text-gray-500">Registra i tuoi movimenti</Text>
|
||||||
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -215,7 +219,7 @@ export default function AttendanceScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</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 && (
|
{item.time && (
|
||||||
<View className="bg-gray-50 px-3 py-1.5 rounded-xl border border-gray-100 flex-shrink-0">
|
<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">
|
<Text className="text-base font-mono text-gray-600 font-bold">
|
||||||
|
|||||||
@@ -197,13 +197,6 @@ export default function AutomationScreen() {
|
|||||||
)}
|
)}
|
||||||
<View className="h-20" />
|
<View className="h-20" />
|
||||||
</ScrollView>
|
</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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import { AuthContext } from '@/utils/authContext';
|
|||||||
import { ActivityItem } from '@/types/types';
|
import { ActivityItem } from '@/types/types';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import LoadingScreen from '@/components/LoadingScreen';
|
import LoadingScreen from '@/components/LoadingScreen';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -63,8 +64,9 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-[#099499]">
|
<View className="flex-1 bg-[#099499]">
|
||||||
{/* Banner Custom */}
|
<SafeAreaView edges={['top']} className='pt-4'>
|
||||||
<View className="pt-16 pb-6 px-6 shadow-sm z-10">
|
{/* Custom Banner */}
|
||||||
|
<View className="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 flex-1 mr-4">
|
<View className="flex-row items-center gap-4 flex-1 mr-4">
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
@@ -82,8 +84,9 @@ export default function HomeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
|
||||||
{/* Contenuto Scrollabile */}
|
{/* Scrollable Content */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
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 }}
|
||||||
@@ -93,10 +96,13 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* Warning Card */}
|
{/* Warning Card - Incomplete Attendance */}
|
||||||
{incompleteAttendance && (
|
{incompleteAttendance && (
|
||||||
<View className="bg-white p-6 rounded-3xl shadow-md border-l-8 border-orange-500 flex-row items-center justify-between">
|
<TouchableOpacity
|
||||||
<View className="flex-row items-center gap-5 flex-1">
|
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">
|
<View className="bg-orange-100 p-4 rounded-full">
|
||||||
<AlertTriangle size={32} color="#f97316" />
|
<AlertTriangle size={32} color="#f97316" />
|
||||||
</View>
|
</View>
|
||||||
@@ -105,10 +111,7 @@ export default function HomeScreen() {
|
|||||||
<Text className="text-base text-gray-500 mt-1">{incompleteAttendance}</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">
|
|
||||||
<Text className="text-orange-600 text-sm font-bold uppercase tracking-wide">Risolvi</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
@@ -141,6 +144,7 @@ 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>
|
||||||
|
{/* TODO: Could be expanded */}
|
||||||
{/* <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> */}
|
||||||
@@ -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 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">
|
<View className="flex-row items-center gap-4 flex-1">
|
||||||
|
|
||||||
{/* Icona */}
|
{/* Icon */}
|
||||||
<View className={`${config.bg} p-4 rounded-2xl flex-shrink-0`}>
|
<View className={`${config.bg} p-4 rounded-2xl flex-shrink-0`}>
|
||||||
<IconComponent size={24} color={config.color} />
|
<IconComponent size={24} color={config.color} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Titolo e Sottotitolo */}
|
{/* Title and Subtitle */}
|
||||||
<View className="flex-1 mr-2">
|
<View className="flex-1 mr-2">
|
||||||
<Text
|
<Text
|
||||||
className="text-lg font-bold text-gray-800 mb-0.5 leading-tight"
|
className="text-lg font-bold text-gray-800 mb-0.5 leading-tight"
|
||||||
@@ -176,7 +180,7 @@ export default function HomeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Data */}
|
{/* Date */}
|
||||||
<View className="flex-shrink-0">
|
<View className="flex-shrink-0">
|
||||||
<Text className="text-sm font-bold text-gray-300">{item.date_display}</Text>
|
<Text className="text-sm font-bold text-gray-300">{item.date_display}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import CalendarWidget from '@/components/CalendarWidget';
|
|||||||
import LoadingScreen from '@/components/LoadingScreen';
|
import LoadingScreen from '@/components/LoadingScreen';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import { formatDate, formatTime } from '@/utils/dateTime';
|
import { formatDate, formatTime } from '@/utils/dateTime';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
// Icon Mapping
|
// Icon Mapping
|
||||||
const typeIcons: Record<string, (color: string) => JSX.Element> = {
|
const typeIcons: Record<string, (color: string) => JSX.Element> = {
|
||||||
@@ -49,22 +50,21 @@ export default function PermitsScreen() {
|
|||||||
const filteredPermits = useMemo(() => {
|
const filteredPermits = useMemo(() => {
|
||||||
if (!permits.length) return [];
|
if (!permits.length) return [];
|
||||||
|
|
||||||
// Calcoliamo inizio e fine del mese visualizzato
|
// Calculate start and end of the current month
|
||||||
const year = currentMonthDate.getFullYear();
|
const year = currentMonthDate.getFullYear();
|
||||||
const month = currentMonthDate.getMonth();
|
const month = currentMonthDate.getMonth();
|
||||||
|
|
||||||
const startOfMonth = new Date(year, month, 1);
|
const startOfMonth = new Date(year, month, 1);
|
||||||
// Trucco JS: giorno 0 del mese successivo = ultimo giorno del mese corrente
|
// Day 0 of the next month = last day of the current month
|
||||||
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);
|
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59);
|
||||||
|
|
||||||
return permits.filter(item => {
|
return permits.filter(item => {
|
||||||
const itemStart = new Date(item.start_date?.toString() ?? '');
|
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() ?? '');
|
const itemEnd = item.end_date ? new Date(item.end_date?.toString() ?? '') : new Date(item.start_date?.toString() ?? '');
|
||||||
|
|
||||||
// FORMULA OVERLAP:
|
// The permit is visible if it starts before the end of the month
|
||||||
// Il permesso è visibile se inizia prima della fine del mese
|
// And ends after the start of the month.
|
||||||
// E finisce dopo l'inizio del mese.
|
|
||||||
return itemStart <= endOfMonth && itemEnd >= startOfMonth;
|
return itemStart <= endOfMonth && itemEnd >= startOfMonth;
|
||||||
});
|
});
|
||||||
}, [permits, currentMonthDate]);
|
}, [permits, currentMonthDate]);
|
||||||
@@ -92,11 +92,13 @@ export default function PermitsScreen() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100 flex-row justify-between items-center">
|
<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>
|
<View>
|
||||||
<Text className="text-3xl font-bold text-gray-800 mb-1">Ferie e Permessi</Text>
|
<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>
|
<Text className="text-base text-gray-500">Gestisci le tue assenze</Text>
|
||||||
</View>
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -110,7 +112,7 @@ export default function PermitsScreen() {
|
|||||||
{/* Calendar Widget */}
|
{/* Calendar Widget */}
|
||||||
<CalendarWidget events={permits} types={types} onMonthChange={(date) => setCurrentMonthDate(date)} />
|
<CalendarWidget events={permits} types={types} onMonthChange={(date) => setCurrentMonthDate(date)} />
|
||||||
|
|
||||||
{/* Lista Richieste Recenti */}
|
{/* Recent Requests List */}
|
||||||
<View>
|
<View>
|
||||||
{filteredPermits.length === 0 ? (
|
{filteredPermits.length === 0 ? (
|
||||||
<Text className="text-center text-gray-500 mt-8">Nessuna richiesta di permesso questo mese</Text>
|
<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>
|
||||||
</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'}`}>
|
<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'}`}>
|
<Text className={`text-xs font-bold uppercase tracking-wide ${item.status ? 'text-green-700' : 'text-yellow-700'}`}>
|
||||||
{item.status ? 'Approvato' : 'In Attesa'}
|
{item.status ? 'Approvato' : 'In Attesa'}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useAlert } from '@/components/AlertComponent';
|
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 React, { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { RangePickerModal } from '@/components/RangePickerModal';
|
import { RangePickerModal } from '@/components/RangePickerModal';
|
||||||
@@ -11,6 +11,7 @@ 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, uploadDocument } from '@/utils/documentUtils';
|
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function DocumentsScreen() {
|
export default function DocumentsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -32,7 +33,7 @@ export default function DocumentsScreen() {
|
|||||||
try {
|
try {
|
||||||
if (!refreshing) setIsLoading(true);
|
if (!refreshing) setIsLoading(true);
|
||||||
|
|
||||||
// Fetch Documenti Utente
|
// Fetch User Documents
|
||||||
const response = await api.get(`/attachment/get-user-attachments`);
|
const response = await api.get(`/attachment/get-user-attachments`);
|
||||||
setDocuments(response.data);
|
setDocuments(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -53,24 +54,24 @@ export default function DocumentsScreen() {
|
|||||||
fetchUserDocuments();
|
fetchUserDocuments();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filtra Documenti in base a searchTerm e range
|
// Filter Documents based on searchTerm and range
|
||||||
const filteredDocs = documents.filter(doc => {
|
const filteredDocs = documents.filter(doc => {
|
||||||
// Filtro Testuale
|
// Text Filter
|
||||||
const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase());
|
const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
if (!matchesSearch) return false;
|
if (!matchesSearch) return false;
|
||||||
|
|
||||||
// Filtro Date Range
|
// Date Range Filter
|
||||||
if (range.startDate || range.endDate) {
|
if (range.startDate || range.endDate) {
|
||||||
const docDate = parseTimestamp(doc.updated_at); // doc.date è "DD/MM/YYYY"
|
const docDate = parseTimestamp(doc.updated_at); // doc.date è "DD/MM/YYYY"
|
||||||
|
|
||||||
// Controllo Data Inizio
|
// Start Date Check
|
||||||
if (range.startDate) {
|
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();
|
const start = dayjs(range.startDate).startOf('day').toDate();
|
||||||
if (docDate < start) return false;
|
if (docDate < start) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controllo Data Fine
|
// End Date Check
|
||||||
if (range.endDate) {
|
if (range.endDate) {
|
||||||
const end = dayjs(range.endDate).endOf('day').toDate();
|
const end = dayjs(range.endDate).endOf('day').toDate();
|
||||||
if (docDate > end) return false;
|
if (docDate > end) return false;
|
||||||
@@ -80,7 +81,7 @@ export default function DocumentsScreen() {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestione Caricamento Documento
|
// Document Upload Handling
|
||||||
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
@@ -105,15 +106,19 @@ export default function DocumentsScreen() {
|
|||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-gray-50">
|
<View className="flex-1 bg-gray-50">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View className="flex-row items-center gap-4 bg-white p-6 pt-16 shadow-sm border-b border-gray-100">
|
<View className="bg-white px-4 pb-6 shadow-sm border-b border-gray-100">
|
||||||
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100">
|
<SafeAreaView edges={['top']} className='pt-4'>
|
||||||
<ArrowLeft size={24} color="#374151" />
|
<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>
|
</TouchableOpacity>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Text className="text-3xl font-bold text-gray-800">Documenti</Text>
|
<Text className="text-3xl font-bold text-gray-800">Documenti</Text>
|
||||||
<Text className="text-base text-gray-500">Gestisci i tuoi documenti</Text>
|
<Text className="text-base text-gray-500">Gestisci i tuoi documenti</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View className="p-5 gap-6 flex-1">
|
<View className="p-5 gap-6 flex-1">
|
||||||
{/* Search + Date Row */}
|
{/* Search + Date Row */}
|
||||||
@@ -141,7 +146,7 @@ export default function DocumentsScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Modale Unico per il Range */}
|
{/* Range Picker Modal */}
|
||||||
<RangePickerModal
|
<RangePickerModal
|
||||||
visible={showRangePicker}
|
visible={showRangePicker}
|
||||||
onClose={() => setShowRangePicker(false)}
|
onClose={() => setShowRangePicker(false)}
|
||||||
@@ -149,7 +154,7 @@ export default function DocumentsScreen() {
|
|||||||
onApply={setRange}
|
onApply={setRange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* List */}
|
{/* Documents List */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{ gap: 16, paddingBottom: 100 }}
|
contentContainerStyle={{ gap: 16, paddingBottom: 100 }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
@@ -193,7 +198,7 @@ export default function DocumentsScreen() {
|
|||||||
<Plus size={32} color="white" />
|
<Plus size={32} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Modale Caricamento Documento */}
|
{/* Document Upload Modal */}
|
||||||
<AddDocumentModal
|
<AddDocumentModal
|
||||||
visible={showUploadModal}
|
visible={showUploadModal}
|
||||||
onClose={() => setShowUploadModal(false)}
|
onClose={() => setShowUploadModal(false)}
|
||||||
|
|||||||
@@ -3,19 +3,21 @@ import { ChevronLeft, FileText, LogOut, Mail, Settings, User } from 'lucide-reac
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { AuthContext } from '@/utils/authContext';
|
import { AuthContext } from '@/utils/authContext';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const authContext = useContext(AuthContext);
|
const authContext = useContext(AuthContext);
|
||||||
const { user } = authContext;
|
const { user } = authContext;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Genera le iniziali dell'utente
|
// Generate user initials
|
||||||
const initials = `${user?.name?.[0] ?? ''}${user?.surname?.[0] ?? ''}`.toUpperCase();
|
const initials = `${user?.name?.[0] ?? ''}${user?.surname?.[0] ?? ''}`.toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-[#099499]">
|
<View className="flex-1 bg-[#099499]">
|
||||||
{/* --- SEZIONE HEADER (INVARIATA) --- */}
|
<SafeAreaView edges={['top']} className='pt-4'>
|
||||||
<View className="pt-16 pb-6 px-6">
|
{/* Header Section */}
|
||||||
|
<View className="pb-6 px-4">
|
||||||
<View className="flex-row justify-start items-center gap-4">
|
<View className="flex-row justify-start items-center gap-4">
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.back()}
|
onPress={() => router.back()}
|
||||||
@@ -33,25 +35,24 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
className="flex-1 bg-gray-50 rounded-t-[2.5rem] px-5 pt-8"
|
className="flex-1 bg-gray-50 rounded-t-[2.5rem] px-5 pt-8"
|
||||||
contentContainerStyle={{ paddingBottom: 60, gap: 24 }}
|
contentContainerStyle={{ paddingBottom: 60, gap: 24 }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{/* Card info - Testi ingranditi */}
|
{/* Info Card - Enlarged Texts */}
|
||||||
<View className="bg-white p-7 rounded-3xl shadow-sm border border-gray-100">
|
<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>
|
<Text className="text-2xl font-bold text-gray-800">Informazioni</Text>
|
||||||
|
|
||||||
<View className="mt-6 gap-5">
|
<View className="mt-6 gap-5">
|
||||||
<View className="flex-row items-center 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">
|
<View className="w-14 h-14 bg-gray-100 rounded-2xl items-center justify-center">
|
||||||
<Mail size={24} color="#374151" />
|
<Mail size={24} color="#374151" />
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
{/* Label e valore ingranditi */}
|
|
||||||
<Text className="text-lg text-gray-700 font-bold">Email</Text>
|
<Text className="text-lg text-gray-700 font-bold">Email</Text>
|
||||||
<Text className="text-gray-500 text-base">{user?.email}</Text>
|
<Text className="text-gray-500 text-base">{user?.email}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -69,7 +70,7 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Actions - Testi e Pulsanti ingranditi */}
|
{/* Actions */}
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-gray-800 text-2xl font-bold mb-5 px-1">Azioni</Text>
|
<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>
|
<Text className="text-[#099499] text-base font-bold">Apri</Text>
|
||||||
</TouchableOpacity>
|
</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="flex-row items-center gap-5">
|
||||||
<View className="bg-gray-100 p-3.5 rounded-2xl">
|
<View className="bg-gray-100 p-3.5 rounded-2xl">
|
||||||
<Settings size={26} color="#374151" />
|
<Settings size={26} color="#374151" />
|
||||||
@@ -97,7 +99,7 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-gray-400 text-base font-bold">Apri</Text>
|
<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">
|
<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">
|
<View className="flex-row items-center gap-5">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import dayjs from 'dayjs';
|
|||||||
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
|
import { formatTimestamp, parseTimestamp } from '@/utils/dateTime';
|
||||||
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
|
import { downloadAndShareDocument, uploadDocument } from '@/utils/documentUtils';
|
||||||
import AddDocumentModal from '@/components/AddDocumentModal';
|
import AddDocumentModal from '@/components/AddDocumentModal';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function SiteDocumentsScreen() {
|
export default function SiteDocumentsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -24,12 +25,12 @@ export default function SiteDocumentsScreen() {
|
|||||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
// Fetch dei documenti del cantiere
|
// Fetch site documents
|
||||||
const fetchSiteDocuments = useCallback(async (siteId: number, isRefreshing = false) => {
|
const fetchSiteDocuments = useCallback(async (siteId: number, isRefreshing = false) => {
|
||||||
try {
|
try {
|
||||||
if (!isRefreshing) setIsLoading(true);
|
if (!isRefreshing) setIsLoading(true);
|
||||||
|
|
||||||
// Fetch Documenti
|
// Fetch Documents
|
||||||
const response = await api.get(`/attachment/get-site-attachments`, {
|
const response = await api.get(`/attachment/get-site-attachments`, {
|
||||||
params: { siteId }
|
params: { siteId }
|
||||||
});
|
});
|
||||||
@@ -74,13 +75,13 @@ export default function SiteDocumentsScreen() {
|
|||||||
});
|
});
|
||||||
const [showRangePicker, setShowRangePicker] = useState(false);
|
const [showRangePicker, setShowRangePicker] = useState(false);
|
||||||
|
|
||||||
// Filtraggio Documenti
|
// Filter documents based on search term and date range
|
||||||
const filteredDocs = documents.filter(doc => {
|
const filteredDocs = documents.filter(doc => {
|
||||||
// Filtro Testuale (su nome documento)
|
// Textual Filter (on document name)
|
||||||
const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase());
|
const matchesSearch = doc.title.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
if (!matchesSearch) return false;
|
if (!matchesSearch) return false;
|
||||||
|
|
||||||
// Filtro Date Range
|
// Date Range Filter
|
||||||
if (range.startDate || range.endDate) {
|
if (range.startDate || range.endDate) {
|
||||||
const docDate = parseTimestamp(doc.updated_at);
|
const docDate = parseTimestamp(doc.updated_at);
|
||||||
if (range.startDate) {
|
if (range.startDate) {
|
||||||
@@ -95,7 +96,7 @@ export default function SiteDocumentsScreen() {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestione Download e Condivisione Documento
|
// Handle Document Download and Share
|
||||||
const handleDownloadAndShare = async (mimetype: string, fileName: string, fileUrl: string) => {
|
const handleDownloadAndShare = async (mimetype: string, fileName: string, fileUrl: string) => {
|
||||||
try {
|
try {
|
||||||
await downloadAndShareDocument(mimetype, fileName, fileUrl);
|
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) => {
|
const handleUploadDocument = async (file: any, customTitle?: string) => {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
@@ -130,13 +131,15 @@ export default function SiteDocumentsScreen() {
|
|||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-gray-50">
|
<View className="flex-1 bg-gray-50">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100 flex-row items-center gap-4">
|
<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">
|
<TouchableOpacity onPress={() => router.back()} className="p-2 -ml-2 rounded-full active:bg-gray-100">
|
||||||
<ChevronLeft size={28} color="#4b5563" />
|
<ChevronLeft size={28} color="#4b5563" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
{/* Badge Codice Cantiere */}
|
{/* Site Code Badge */}
|
||||||
{site.code && (
|
{site.code && (
|
||||||
<View className="self-start bg-[#E6F4F4] px-2 py-0.5 rounded mb-1 border border-[#099499]/20">
|
<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]">
|
<Text className="text-base font-bold text-[#099499]">
|
||||||
@@ -145,7 +148,7 @@ export default function SiteDocumentsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Nome Cantiere con Truncate funzionante */}
|
{/* Site Name with Truncate */}
|
||||||
<Text
|
<Text
|
||||||
className="text-xl font-bold text-gray-800 leading-tight"
|
className="text-xl font-bold text-gray-800 leading-tight"
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
@@ -157,9 +160,11 @@ export default function SiteDocumentsScreen() {
|
|||||||
<Text className="text-sm text-gray-500 mt-0.5">Archivio documenti</Text>
|
<Text className="text-sm text-gray-500 mt-0.5">Archivio documenti</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View className="p-5 gap-6 flex-1">
|
<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-row gap-2">
|
||||||
<View className="flex-1 relative justify-center">
|
<View className="flex-1 relative justify-center">
|
||||||
<View className="absolute left-4 z-10">
|
<View className="absolute left-4 z-10">
|
||||||
@@ -189,7 +194,7 @@ export default function SiteDocumentsScreen() {
|
|||||||
onApply={setRange}
|
onApply={setRange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Lista Documenti */}
|
{/* Documents List */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{ gap: 16, paddingBottom: 100 }}
|
contentContainerStyle={{ gap: 16, paddingBottom: 100 }}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
@@ -222,7 +227,7 @@ export default function SiteDocumentsScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* FAB (Spostato qui) */}
|
{/* FAB */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => setShowUploadModal(true)}
|
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"
|
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" />
|
<Plus size={32} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Modale Caricamento Documento */}
|
{/* Upload Document Modal */}
|
||||||
<AddDocumentModal
|
<AddDocumentModal
|
||||||
visible={showUploadModal}
|
visible={showUploadModal}
|
||||||
onClose={() => setShowUploadModal(false)}
|
onClose={() => setShowUploadModal(false)}
|
||||||
onUpload={handleUploadDocument}
|
onUpload={handleUploadDocument}
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import api from '@/utils/api';
|
|||||||
import LoadingScreen from '@/components/LoadingScreen';
|
import LoadingScreen from '@/components/LoadingScreen';
|
||||||
import { ConstructionSite } from '@/types/types';
|
import { ConstructionSite } from '@/types/types';
|
||||||
import { useAlert } from '@/components/AlertComponent';
|
import { useAlert } from '@/components/AlertComponent';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function SitesScreen() {
|
export default function SitesScreen() {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -15,12 +16,12 @@ export default function SitesScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
|
||||||
// Fetch cantieri e documenti
|
// Fetch sites and documents
|
||||||
const fetchConstructionSites = async () => {
|
const fetchConstructionSites = async () => {
|
||||||
try {
|
try {
|
||||||
if (!refreshing) setIsLoading(true);
|
if (!refreshing) setIsLoading(true);
|
||||||
|
|
||||||
// Fetch Cantieri
|
// Fetch Sites and their attachments count
|
||||||
const response = await api.get('/construction-site/sites-attachments');
|
const response = await api.get('/construction-site/sites-attachments');
|
||||||
setConstructionSites(response.data);
|
setConstructionSites(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -41,7 +42,7 @@ export default function SitesScreen() {
|
|||||||
fetchConstructionSites();
|
fetchConstructionSites();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filtriamo i cantieri in base alla ricerca
|
// Filter sites based on search term
|
||||||
const filteredSites = constructionSites.filter(site =>
|
const filteredSites = constructionSites.filter(site =>
|
||||||
site.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
site.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
site.code.toLowerCase().includes(searchTerm.toLowerCase())
|
site.code.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
@@ -54,9 +55,11 @@ export default function SitesScreen() {
|
|||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-gray-50">
|
<View className="flex-1 bg-gray-50">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View className="bg-white p-6 pt-16 shadow-sm border-b border-gray-100">
|
<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-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>
|
<Text className="text-base text-gray-500">Seleziona un cantiere per vedere i documenti</Text>
|
||||||
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="px-5 mt-5 gap-6 flex-1">
|
<View className="px-5 mt-5 gap-6 flex-1">
|
||||||
@@ -74,11 +77,10 @@ export default function SitesScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Lista Cantieri */}
|
{/* Sites List */}
|
||||||
{/* TODO: Rimuovere lo ScrollIndicator? */}
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{ gap: 16, paddingBottom: 40 }}
|
contentContainerStyle={{ gap: 16, paddingBottom: 40 }}
|
||||||
showsVerticalScrollIndicator={true}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} />
|
||||||
}
|
}
|
||||||
@@ -89,7 +91,7 @@ export default function SitesScreen() {
|
|||||||
onPress={() => router.push({
|
onPress={() => router.push({
|
||||||
pathname: '/(protected)/sites/[id]',
|
pathname: '/(protected)/sites/[id]',
|
||||||
params: {
|
params: {
|
||||||
id: site.id ,
|
id: site.id,
|
||||||
siteData: JSON.stringify(site),
|
siteData: JSON.stringify(site),
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
@@ -111,7 +113,6 @@ export default function SitesScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Freccia al posto del download */}
|
|
||||||
<ChevronRight size={24} color="#9ca3af" />
|
<ChevronRight size={24} color="#9ca3af" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { AuthProvider } from '@/utils/authContext';
|
|||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { AlertProvider } from '@/components/AlertComponent';
|
import { AlertProvider } from '@/components/AlertComponent';
|
||||||
import { NetworkProvider } from '@/utils/networkProvider';
|
import { NetworkProvider } from '@/utils/networkProvider';
|
||||||
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function AppLayout() {
|
export default function AppLayout() {
|
||||||
return (
|
return (
|
||||||
|
<SafeAreaProvider>
|
||||||
<NetworkProvider>
|
<NetworkProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
@@ -16,5 +18,6 @@ export default function AppLayout() {
|
|||||||
</AlertProvider>
|
</AlertProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</NetworkProvider>
|
</NetworkProvider>
|
||||||
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -13,21 +13,19 @@ export default function LoginScreen() {
|
|||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
// TODO: Riscrivere funzione per migliorare leggibilità e gestione errori
|
// Login Handler function
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
// TODO: Implementa toast o messaggio di errore più user-friendly
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
alert.showAlert('error', 'Attenzione', 'Inserisci username e password');
|
alert.showAlert('error', 'Attenzione', 'Inserisci username e password');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
username.trim();
|
username.trim();
|
||||||
password.trim();
|
password.trim();
|
||||||
|
|
||||||
// Esegui richiesta di login
|
// Execute login request
|
||||||
const response = await api.post("/user/login", {
|
const response = await api.post("/user/login", {
|
||||||
username: username,
|
username: username,
|
||||||
password: password
|
password: password
|
||||||
@@ -35,25 +33,24 @@ export default function LoginScreen() {
|
|||||||
|
|
||||||
const { token, user } = response.data;
|
const { token, user } = response.data;
|
||||||
|
|
||||||
console.log("Login successo, token ricevuto.");
|
console.log("Login riuscito. Token:", token);
|
||||||
console.log("Dati utente:", user);
|
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);
|
authContext.logIn(token, user);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login Error:", error);
|
console.error("Login Error:", error);
|
||||||
let message = "Si è verificato un errore durante l'accesso.";
|
let message = "Si è verificato un errore durante l'accesso.";
|
||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// Errore dal server (es. 401 Credenziali errate)
|
// Server error (e.g., 401 Invalid credentials)
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
// TODO: Alert o Toast specifico per credenziali errate
|
|
||||||
message = "Credenziali non valide."
|
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}`;
|
||||||
}
|
}
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
// Server non raggiungibile
|
// Server not reachable
|
||||||
message = "Impossibile contattare il server. Controlla la connessione.";
|
message = "Impossibile contattare il server. Controlla la connessione.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +62,7 @@ export default function LoginScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-[#099499] h-screen overflow-hidden">
|
<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">
|
<View className="h-[35%] flex-column justify-center items-center">
|
||||||
<Image
|
<Image
|
||||||
source={require('@/assets/images/mariani-logo.png')}
|
source={require('@/assets/images/mariani-logo.png')}
|
||||||
@@ -120,12 +117,13 @@ export default function LoginScreen() {
|
|||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
{/* TODO: Implement password recovery functionality */}
|
||||||
<TouchableOpacity className="mt-3 self-end" style={{ alignSelf: 'flex-end' }}>
|
<TouchableOpacity className="mt-3 self-end" style={{ alignSelf: 'flex-end' }}>
|
||||||
<Text className="text-[#099499] font-bold text-base">Password dimenticata?</Text>
|
<Text className="text-[#099499] font-bold text-base">Password dimenticata?</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tasto Login */}
|
{/* Login Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleLogin}
|
onPress={handleLogin}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
|
|||||||
BIN
assets/images/adaptive-icon.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 53 KiB |
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Text, TouchableOpacity, View, TextInput, ActivityIndicator } from 'react-native';
|
import { Modal, Text, TouchableOpacity, View, TextInput, ActivityIndicator } from 'react-native';
|
||||||
import { X, Upload, FileText, Trash2 } from 'lucide-react-native';
|
import { X, Upload, FileText, Trash2 } from 'lucide-react-native';
|
||||||
import * as DocumentPicker from 'expo-document-picker'; // TODO: Testare in ambiente iOS
|
import * as DocumentPicker from 'expo-document-picker'; // TODO: Test on iOS environment
|
||||||
|
|
||||||
interface AddDocumentModalProps {
|
interface AddDocumentModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -15,7 +15,7 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
const [customTitle, setCustomTitle] = useState('');
|
const [customTitle, setCustomTitle] = useState('');
|
||||||
const [fileExtension, setFileExtension] = useState('');
|
const [fileExtension, setFileExtension] = useState('');
|
||||||
|
|
||||||
// Reset dello stato quando il modale si apre/chiude
|
// Reset of state when modal is closed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
@@ -24,11 +24,11 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
// TODO: Considerare selezione multipla?
|
// TODO: Need to handle multiple file selection?
|
||||||
const pickDocument = async () => {
|
const pickDocument = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await DocumentPicker.getDocumentAsync({
|
const result = await DocumentPicker.getDocumentAsync({
|
||||||
type: '*/*', // Puoi limitare a 'application/pdf', 'image/*', ecc.
|
type: '*/*', // You can limit to 'application/pdf', 'image/*', etc.
|
||||||
copyToCacheDirectory: true,
|
copyToCacheDirectory: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
if (!selectedFile) return;
|
if (!selectedFile) return;
|
||||||
const fullTitle = customTitle ? `${customTitle}${fileExtension}` : selectedFile.name;
|
const fullTitle = customTitle ? `${customTitle}${fileExtension}` : selectedFile.name;
|
||||||
|
|
||||||
// Se il titolo custom è vuoto, usiamo il nome originale
|
// If customTitle is empty, we use the original file name
|
||||||
onUpload(selectedFile, fullTitle);
|
onUpload(selectedFile, fullTitle);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,8 +65,7 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
setFileExtension('');
|
setFileExtension('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Formatta dimensione file
|
// Format file size in a human-readable format
|
||||||
// TODO: Spostare in utils?
|
|
||||||
const formatSize = (size?: number) => {
|
const formatSize = (size?: number) => {
|
||||||
if (!size) return '0 B';
|
if (!size) return '0 B';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -93,10 +92,8 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Body */}
|
|
||||||
<View className="gap-5">
|
<View className="gap-5">
|
||||||
|
{/* File Selection Area */}
|
||||||
{/* Area Selezione File */}
|
|
||||||
{!selectedFile ? (
|
{!selectedFile ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={pickDocument}
|
onPress={pickDocument}
|
||||||
@@ -109,7 +106,7 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
<Text className="text-gray-400 text-sm mt-1">PDF, Immagini, Word</Text>
|
<Text className="text-gray-400 text-sm mt-1">PDF, Immagini, Word</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
// Visualizzazione File Selezionato
|
// Selected File View
|
||||||
<View className="bg-[#E6F4F4] p-4 rounded-2xl border border-[#099499]/20 flex-row items-center gap-4">
|
<View className="bg-[#E6F4F4] p-4 rounded-2xl border border-[#099499]/20 flex-row items-center gap-4">
|
||||||
<View className="bg-white p-3 rounded-xl">
|
<View className="bg-white p-3 rounded-xl">
|
||||||
<FileText size={24} color="#099499" />
|
<FileText size={24} color="#099499" />
|
||||||
@@ -128,7 +125,7 @@ export default function AddDocumentModal({ visible, onClose, onUpload, isUploadi
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Campo Rinomina (Visibile solo se c'è un file) */}
|
{/* Rename field (Visible only if a file is selected) */}
|
||||||
{selectedFile && (
|
{selectedFile && (
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-gray-700 font-bold mb-2 ml-1 text-sm">Rinomina File</Text>
|
<Text className="text-gray-700 font-bold mb-2 ml-1 text-sm">Rinomina File</Text>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface AlertContextData {
|
|||||||
|
|
||||||
const AlertContext = createContext<AlertContextData>({} as AlertContextData);
|
const AlertContext = createContext<AlertContextData>({} as AlertContextData);
|
||||||
|
|
||||||
|
// TODO: Move this config to a separate file
|
||||||
const ALERT_CONFIG = {
|
const ALERT_CONFIG = {
|
||||||
success: {
|
success: {
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
@@ -57,33 +58,33 @@ export const AlertProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
|
|
||||||
const { icon: Icon, color, bgColor, btnColor } = ALERT_CONFIG[type];
|
const { icon: Icon, color, bgColor, btnColor } = ALERT_CONFIG[type];
|
||||||
|
|
||||||
|
// TODO: Need to refactor component styles
|
||||||
return (
|
return (
|
||||||
<AlertContext.Provider value={{ showAlert, hideAlert }}>
|
<AlertContext.Provider value={{ showAlert, hideAlert }}>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{/* IL COMPONENTE ALERT MODALE */}
|
|
||||||
<Modal
|
<Modal
|
||||||
transparent
|
transparent
|
||||||
visible={visible}
|
visible={visible}
|
||||||
animationType="fade"
|
animationType="fade"
|
||||||
onRequestClose={hideAlert}
|
onRequestClose={hideAlert}
|
||||||
>
|
>
|
||||||
{/* Backdrop scuro */}
|
{/* Dark Backdrop */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
onPress={hideAlert} // Chiude se clicchi fuori (opzionale)
|
onPress={hideAlert} // Closes if you click outside (optional)
|
||||||
className="flex-1 bg-black/60 justify-center items-center px-6"
|
className="flex-1 bg-black/60 justify-center items-center px-6"
|
||||||
>
|
>
|
||||||
{/* Contenitore Alert */}
|
{/* Alert Container */}
|
||||||
<TouchableWithoutFeedback>
|
<TouchableWithoutFeedback>
|
||||||
<View className="bg-white w-full max-w-sm rounded-3xl p-6 items-center shadow-2xl">
|
<View className="bg-white w-full max-w-sm rounded-3xl p-6 items-center shadow-2xl">
|
||||||
|
|
||||||
{/* Icona Cerchiata */}
|
{/* Icon Circle */}
|
||||||
<View className={`${bgColor} p-4 rounded-full mb-4`}>
|
<View className={`${bgColor} p-4 rounded-full mb-4`}>
|
||||||
<Icon size={32} className={color} strokeWidth={2.5} />
|
<Icon size={32} className={color} strokeWidth={2.5} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Testi */}
|
{/* Texts */}
|
||||||
<Text className="text-xl font-bold text-gray-900 text-center mb-2">
|
<Text className="text-xl font-bold text-gray-900 text-center mb-2">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -92,10 +93,10 @@ export const AlertProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Pulsante OK */}
|
{/* OK Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={hideAlert}
|
onPress={hideAlert}
|
||||||
className={`w-full py-3.5 rounded-xl ${btnColor} active:opacity-90 shadow-sm`}
|
className={`w-full py-3.5 rounded-3xl ${btnColor} active:opacity-90 shadow-sm`}
|
||||||
>
|
>
|
||||||
<Text className="text-white text-center font-bold text-lg">
|
<Text className="text-white text-center font-bold text-lg">
|
||||||
Ok, ho capito
|
Ok, ho capito
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface CalendarWidgetProps {
|
|||||||
export default function CalendarWidget({ events, types, onMonthChange }: 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
|
// Calendar helpers
|
||||||
const daysInMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
|
const daysInMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate();
|
||||||
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDay(); // 0 = Sun
|
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDay(); // 0 = Sun
|
||||||
const adjustedFirstDay = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1; // 0 = Mon
|
const adjustedFirstDay = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1; // 0 = Mon
|
||||||
@@ -40,8 +40,8 @@ export default function CalendarWidget({ events, types, onMonthChange }: Calenda
|
|||||||
const dateStr = `${year}-${month}-${dayStr}`;
|
const dateStr = `${year}-${month}-${dayStr}`;
|
||||||
|
|
||||||
return events.find(event => {
|
return events.find(event => {
|
||||||
// Logica semplice: controlla se la data cade nel range
|
// Simple logic: check if the date falls within the range
|
||||||
// Nota: per una logica perfetta sui range lunghi, servirebbe un controllo più approfondito
|
// Note: for perfect logic on long ranges, a more thorough check would be needed
|
||||||
if (!event.start_date) return false;
|
if (!event.start_date) return false;
|
||||||
if (event.timeOffRequestType.name === 'Permesso') return event.start_date === dateStr;
|
if (event.timeOffRequestType.name === 'Permesso') return event.start_date === dateStr;
|
||||||
const end = event.end_date || event.start_date;
|
const end = event.end_date || event.start_date;
|
||||||
@@ -51,7 +51,7 @@ export default function CalendarWidget({ events, types, onMonthChange }: Calenda
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-white rounded-[2rem] p-6 shadow-sm border border-gray-100">
|
<View className="bg-white rounded-[2rem] p-6 shadow-sm border border-gray-100">
|
||||||
{/* Header Mese */}
|
{/* Month Header */}
|
||||||
<View className="flex-row justify-between items-center mb-6">
|
<View className="flex-row justify-between items-center mb-6">
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => changeMonth(-1)}
|
onPress={() => changeMonth(-1)}
|
||||||
@@ -106,7 +106,7 @@ export default function CalendarWidget({ events, types, onMonthChange }: Calenda
|
|||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Legenda */}
|
{/* Legend */}
|
||||||
<View className="flex-row flex-wrap justify-center gap-4 mt-8 pt-4 border-t border-gray-100">
|
<View className="flex-row flex-wrap justify-center gap-4 mt-8 pt-4 border-t border-gray-100">
|
||||||
{types.map((type) => (
|
{types.map((type) => (
|
||||||
<View key={type.id} className="flex-row items-center" >
|
<View key={type.id} className="flex-row items-center" >
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import { Text, TouchableOpacity, View } from 'react-native';
|
import { Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
const DeviceCard = ({ device, onToggle }: { device: HaEntity; onToggle: (entityId: string) => void; }) => {
|
const DeviceCard = ({ device, onToggle }: { device: HaEntity; onToggle: (entityId: string) => void; }) => {
|
||||||
|
// Icon mapping based on entity domain
|
||||||
const getIcon = () => {
|
const getIcon = () => {
|
||||||
const domain = device.entity_id.split('.')[0];
|
const domain = device.entity_id.split('.')[0];
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ const DeviceCard = ({ device, onToggle }: { device: HaEntity; onToggle: (entityI
|
|||||||
const isToggleable = ['light', 'switch', 'fan', 'input_boolean'].includes(device.entity_id.split('.')[0]);
|
const isToggleable = ['light', 'switch', 'fan', 'input_boolean'].includes(device.entity_id.split('.')[0]);
|
||||||
const isOn = device.state === 'on';
|
const isOn = device.state === 'on';
|
||||||
|
|
||||||
|
// State mapping
|
||||||
const getDeviceState = (state: string) => {
|
const getDeviceState = (state: string) => {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'on':
|
case 'on':
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ interface NfcScanModalProps {
|
|||||||
onScan: (data: string) => void;
|
onScan: (data: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Needs to be tested on a real device
|
||||||
export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalProps) {
|
export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalProps) {
|
||||||
// Animazione per l'effetto "pulsante" (Breathing)
|
// Breathing animation effect
|
||||||
const scaleAnim = useRef(new Animated.Value(1)).current;
|
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||||
const opacityAnim = useRef(new Animated.Value(0.3)).current;
|
const opacityAnim = useRef(new Animated.Value(0.3)).current;
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalP
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Loop infinito di espansione e contrazione
|
// Animation loop for the pulsing effect
|
||||||
const animationLoop = () => {
|
const animationLoop = () => {
|
||||||
Animated.loop(
|
Animated.loop(
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
@@ -97,11 +98,9 @@ export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalP
|
|||||||
statusBarTranslucent
|
statusBarTranslucent
|
||||||
>
|
>
|
||||||
<View className="flex-1 bg-black/80 items-center justify-center p-6">
|
<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">
|
<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) */}
|
{/* Close Button (Top Right) */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
className="absolute top-6 right-6 z-10 p-2 bg-gray-50 rounded-full"
|
className="absolute top-6 right-6 z-10 p-2 bg-gray-50 rounded-full"
|
||||||
@@ -109,9 +108,9 @@ export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalP
|
|||||||
<X size={20} color="#9ca3af" />
|
<X size={20} color="#9ca3af" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Area Animata NFC */}
|
{/* NFC Animated Area */}
|
||||||
<View className="mt-6 mb-10 items-center justify-center h-40 w-40">
|
<View className="mt-6 mb-10 items-center justify-center h-40 w-40">
|
||||||
{/* Cerchio Pulsante (Sfondo) */}
|
{/* Pulsing Circle (Background) */}
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -124,13 +123,13 @@ export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalP
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Cerchio Fisso (Primo piano) */}
|
{/* Fixed Circle (Foreground) */}
|
||||||
<View className="bg-[#E6F4F4] p-6 rounded-full border-4 border-white shadow-sm z-10">
|
<View className="bg-[#E6F4F4] p-6 rounded-full border-4 border-white shadow-sm z-10">
|
||||||
<SmartphoneNfc size={64} color="#099499" />
|
<SmartphoneNfc size={64} color="#099499" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Testi */}
|
{/* Texts */}
|
||||||
<Text className="text-2xl font-bold text-gray-800 text-center mb-2">
|
<Text className="text-2xl font-bold text-gray-800 text-center mb-2">
|
||||||
Pronto alla scansione
|
Pronto alla scansione
|
||||||
</Text>
|
</Text>
|
||||||
@@ -139,7 +138,7 @@ export default function NfcScanModal({ visible, onClose, onScan }: NfcScanModalP
|
|||||||
Avvicina il retro del tuo smartphone al <Text className="font-bold text-gray-700">Tag NFC</Text> per registrare la presenza.
|
Avvicina il retro del tuo smartphone al <Text className="font-bold text-gray-700">Tag NFC</Text> per registrare la presenza.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Indicatore Visivo (Simulazione onde) */}
|
{/* Indicator (Wave Simulation) */}
|
||||||
<View className="flex-row gap-2 mt-8 items-center opacity-60">
|
<View className="flex-row gap-2 mt-8 items-center opacity-60">
|
||||||
<Radio size={20} color="#099499" />
|
<Radio size={20} color="#099499" />
|
||||||
<Text className="text-[#099499] font-medium text-sm uppercase tracking-widest">
|
<Text className="text-[#099499] font-medium text-sm uppercase tracking-widest">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity } from 'react-native';
|
import { View, Text, TouchableOpacity } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { WifiOff, RefreshCw } from 'lucide-react-native';
|
import { WifiOff } from 'lucide-react-native';
|
||||||
|
|
||||||
interface OfflineScreenProps {
|
interface OfflineScreenProps {
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
@@ -12,7 +12,7 @@ export default function OfflineScreen({ onRetry, isRetrying = false }: OfflineSc
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 bg-white">
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
<View className="flex-1 items-center justify-center px-8">
|
<View className="flex-1 items-center justify-center px-8">
|
||||||
{/* Icona con cerchio di sfondo */}
|
{/* Icon */}
|
||||||
<View className="bg-gray-100 p-6 rounded-full mb-6">
|
<View className="bg-gray-100 p-6 rounded-full mb-6">
|
||||||
<WifiOff size={64} className="text-gray-400" />
|
<WifiOff size={64} className="text-gray-400" />
|
||||||
</View>
|
</View>
|
||||||
@@ -25,7 +25,7 @@ export default function OfflineScreen({ onRetry, isRetrying = false }: OfflineSc
|
|||||||
Sembra che non ci sia connessione a internet.{'\n'}Controlla il Wi-Fi o i dati mobili e riprova.
|
Sembra che non ci sia connessione a internet.{'\n'}Controlla il Wi-Fi o i dati mobili e riprova.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Pulsante Riprova */}
|
{/* Retry Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onRetry}
|
onPress={onRetry}
|
||||||
disabled={isRetrying}
|
disabled={isRetrying}
|
||||||
@@ -33,11 +33,6 @@ export default function OfflineScreen({ onRetry, isRetrying = false }: OfflineSc
|
|||||||
isRetrying ? 'bg-gray-300' : 'bg-[#099499] active:bg-[#077f83]'
|
isRetrying ? 'bg-gray-300' : 'bg-[#099499] active:bg-[#077f83]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
|
||||||
size={20}
|
|
||||||
color="white"
|
|
||||||
className={`${isRetrying ? 'animate-spin' : ''}`} // TODO: animate-spin richiede config tailwind o gestiscilo manualmente
|
|
||||||
/>
|
|
||||||
<Text className="text-white font-bold text-lg">
|
<Text className="text-white font-bold text-lg">
|
||||||
{isRetrying ? 'Controllo...' : 'Riprova'}
|
{isRetrying ? 'Controllo...' : 'Riprova'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, Modal, TouchableOpacity, Vibration, StyleSheet } from 'react-native';
|
import { View, Text, Modal, TouchableOpacity, Vibration, StyleSheet, Dimensions } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { CameraView, useCameraPermissions } from 'expo-camera';
|
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||||
import { X, ScanLine } from 'lucide-react-native';
|
import { X, ScanLine } from 'lucide-react-native';
|
||||||
@@ -13,8 +13,10 @@ interface QrScanModalProps {
|
|||||||
export default function QrScanModal({ visible, onClose, onScan }: QrScanModalProps) {
|
export default function QrScanModal({ visible, onClose, onScan }: QrScanModalProps) {
|
||||||
const [permission, requestPermission] = useCameraPermissions();
|
const [permission, requestPermission] = useCameraPermissions();
|
||||||
const [scanned, setScanned] = useState(false);
|
const [scanned, setScanned] = useState(false);
|
||||||
|
const { width, height } = Dimensions.get('window');
|
||||||
|
const squareSize = Math.min(width * 0.8, height * 0.8, 400);
|
||||||
|
|
||||||
// Gestione Permessi e Reset Stato
|
// Permission Handling and Reset Scanned State on Modal Open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setScanned(false);
|
setScanned(false);
|
||||||
@@ -59,26 +61,25 @@ export default function QrScanModal({ visible, onClose, onScan }: QrScanModalPro
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Overlay Oscuro con "buco" trasparente (Simulato visivamente con bordi o opacity) */}
|
{/* Dark Overlay with Transparent "Hole" (Visually Simulated with Borders or Opacity) */}
|
||||||
<View className="flex-1">
|
<SafeAreaView className="flex-1">
|
||||||
|
|
||||||
{/* Header Overlay */}
|
{/* Header Overlay */}
|
||||||
<SafeAreaView className="flex-1 bg-black/60 items-center pt-8">
|
<View 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-white text-xl font-bold">Scansiona QR Code</Text>
|
||||||
<Text className="text-gray-300 text-base mt-1">Inquadra il codice nel riquadro</Text>
|
<Text className="text-gray-300 text-base mt-1">Inquadra il codice nel riquadro</Text>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
|
|
||||||
{/* Area Centrale (Trasparente per la camera) */}
|
{/* Central Area (Transparent for the camera) */}
|
||||||
<View className="flex-row h-[400px]">
|
<View className="flex-row" style={{ height: squareSize }}>
|
||||||
<View className="flex-1 bg-black/60" />
|
<View className="flex-1 bg-black/60" />
|
||||||
<View className="w-[400px] h-[400px] border-2 border-[#099499] bg-transparent relative justify-center items-center">
|
<View style={{ width: squareSize, height: squareSize }} className="border-2 border-[#099499] bg-transparent relative justify-center items-center">
|
||||||
{/* Angoli decorativi */}
|
{/* Decorative Corners */}
|
||||||
<View className="absolute top-0 left-0 w-6 h-6 border-l-4 border-t-4 border-[#099499]" />
|
<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 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 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]" />
|
<View className="absolute bottom-0 right-0 w-6 h-6 border-r-4 border-b-4 border-[#099499]" />
|
||||||
|
|
||||||
{/* Linea scansione animata o icona */}
|
{/* Animated Scan Line or Icon */}
|
||||||
{!scanned && <ScanLine color="#099499" size={40} className="opacity-50" />}
|
{!scanned && <ScanLine color="#099499" size={40} className="opacity-50" />}
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1 bg-black/60" />
|
<View className="flex-1 bg-black/60" />
|
||||||
@@ -94,7 +95,7 @@ export default function QrScanModal({ visible, onClose, onScan }: QrScanModalPro
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text className="text-white mt-4 font-medium">Chiudi</Text>
|
<Text className="text-white mt-4 font-medium">Chiudi</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import DateTimePicker, { DateType, useDefaultStyles } from 'react-native-ui-date
|
|||||||
import { Check, X } from 'lucide-react-native';
|
import { Check, X } from 'lucide-react-native';
|
||||||
|
|
||||||
export const RangePickerModal = ({ visible, onClose, onApply }: any) => {
|
export const RangePickerModal = ({ visible, onClose, onApply }: any) => {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles('light');
|
||||||
const [range, setRange] = useState<{
|
const [range, setRange] = useState<{
|
||||||
startDate: DateType;
|
startDate: DateType;
|
||||||
endDate: DateType;
|
endDate: DateType;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface RequestPermitModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RequestPermitModal({ visible, types, onClose, onSubmit }: RequestPermitModalProps) {
|
export default function RequestPermitModal({ visible, types, onClose, onSubmit }: RequestPermitModalProps) {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles('light');
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [type, setType] = useState<TimeOffRequestType>(types[0]); // Default to first type
|
const [type, setType] = useState<TimeOffRequestType>(types[0]); // Default to first type
|
||||||
const [date, setDate] = useState<string | null>();
|
const [date, setDate] = useState<string | null>();
|
||||||
@@ -31,7 +31,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
const [endTime, setEndTime] = useState('');
|
const [endTime, setEndTime] = useState('');
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
// Funzione per resettare le selezioni di data
|
// Clean up function to reset all fields
|
||||||
const clearCalendar = () => {
|
const clearCalendar = () => {
|
||||||
setDate(null);
|
setDate(null);
|
||||||
setRange({ startDate: null, endDate: null });
|
setRange({ startDate: null, endDate: null });
|
||||||
@@ -39,7 +39,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
setType(types[0]);
|
setType(types[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Funzione per validare la richiesta
|
// Function to validate the request
|
||||||
function validateRequest(type: TimeOffRequestType, date: string | null | undefined, range: { startDate: string | null; endDate: string | null }, startTime: string, endTime: string, message: string): string | null {
|
function validateRequest(type: TimeOffRequestType, date: string | null | undefined, range: { startDate: string | null; endDate: string | null }, startTime: string, endTime: string, message: string): string | null {
|
||||||
if (!type) return "Seleziona una tipologia di assenza.";
|
if (!type) return "Seleziona una tipologia di assenza.";
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Funzione per inviare la richiesta alla API
|
// Function to send the request to the API
|
||||||
const saveRequest = async (requestData: any) => {
|
const saveRequest = async (requestData: any) => {
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/time-off-request/save-request', requestData);
|
const response = await api.post('/time-off-request/save-request', requestData);
|
||||||
@@ -73,7 +73,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Funzione per inviare la richiesta
|
// Function to submit the request
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const error = validateRequest(type, date, range, startTime, endTime, message);
|
const error = validateRequest(type, date, range, startTime, endTime, message);
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -92,7 +92,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await saveRequest(requestData);
|
await saveRequest(requestData);
|
||||||
onSubmit(requestData); // TODO: Gestire risposta e controllare fetch in index?
|
onSubmit(requestData);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert.showAlert("error", "Errore", "Impossibile inviare la richiesta.");
|
alert.showAlert("error", "Errore", "Impossibile inviare la richiesta.");
|
||||||
@@ -108,7 +108,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
>
|
>
|
||||||
<View className="flex-1 bg-black/60 justify-end sm:justify-center">
|
<View className="flex-1 bg-black/60 justify-end sm:justify-center">
|
||||||
<View className="bg-white w-full rounded-t-[2.5rem] p-6 shadow-2xl h-[85%] sm:h-auto">
|
<View className="bg-white w-full rounded-t-[2.5rem] p-6 shadow-2xl h-[85%] sm:h-auto">
|
||||||
{/* Header Modale */}
|
{/* Modal Header */}
|
||||||
<View className="flex-row justify-between items-center mb-6">
|
<View className="flex-row justify-between items-center mb-6">
|
||||||
<Text className="text-2xl font-bold text-gray-800">Nuova Richiesta</Text>
|
<Text className="text-2xl font-bold text-gray-800">Nuova Richiesta</Text>
|
||||||
<TouchableOpacity onPress={onClose} className="p-2 bg-gray-100 rounded-full">
|
<TouchableOpacity onPress={onClose} className="p-2 bg-gray-100 rounded-full">
|
||||||
@@ -118,7 +118,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
|
|
||||||
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 40 }}>
|
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 40 }}>
|
||||||
<View className="space-y-6">
|
<View className="space-y-6">
|
||||||
{/* Tipologia */}
|
{/* Permit Type */}
|
||||||
<View className='mb-6'>
|
<View className='mb-6'>
|
||||||
<Text className="text-lg font-bold text-gray-700 mb-3">Tipologia Assenza</Text>
|
<Text className="text-lg font-bold text-gray-700 mb-3">Tipologia Assenza</Text>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -156,7 +156,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
locale='it'
|
locale='it'
|
||||||
styles={{
|
styles={{
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
selected: { backgroundColor: '#099499' }
|
selected: { backgroundColor: '#099499' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -218,7 +218,8 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{/* TODO: Trasformare message in una select? - Predefinito per alcuni tipi */}
|
|
||||||
|
{/* Reason field */}
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
<Text className="text-sm font-bold text-orange-800 mb-2 uppercase">Motivo</Text>
|
<Text className="text-sm font-bold text-orange-800 mb-2 uppercase">Motivo</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -231,7 +232,7 @@ export default function RequestPermitModal({ visible, types, onClose, onSubmit }
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Azioni */}
|
{/* Actions */}
|
||||||
<View className="flex-row gap-4">
|
<View className="flex-row gap-4">
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface TimePickerModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TimePickerModal = ({ visible, initialDate, title, onConfirm, onClose }: TimePickerModalProps) => {
|
export const TimePickerModal = ({ visible, initialDate, title, onConfirm, onClose }: TimePickerModalProps) => {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles('light');
|
||||||
const [selectedDate, setSelectedDate] = useState<DateType>(initialDate || new Date());
|
const [selectedDate, setSelectedDate] = useState<DateType>(initialDate || new Date());
|
||||||
|
|
||||||
const formatTime = (date?: DateType | null) => {
|
const formatTime = (date?: DateType | null) => {
|
||||||
@@ -36,7 +36,7 @@ export const TimePickerModal = ({ visible, initialDate, title, onConfirm, onClos
|
|||||||
<View className="flex-1 justify-center items-center bg-black/50">
|
<View className="flex-1 justify-center items-center bg-black/50">
|
||||||
<View className="bg-white rounded-xl p-4 w-[90%] max-h-[400px]">
|
<View className="bg-white rounded-xl p-4 w-[90%] max-h-[400px]">
|
||||||
|
|
||||||
{/* Header con chiusura */}
|
{/* Header */}
|
||||||
<View className="flex-row justify-between items-center mb-4">
|
<View className="flex-row justify-between items-center mb-4">
|
||||||
<Text className="text-lg font-bold text-gray-800">{title}</Text>
|
<Text className="text-lg font-bold text-gray-800">{title}</Text>
|
||||||
<TouchableOpacity onPress={onClose} className="p-2 bg-gray-100 rounded-full">
|
<TouchableOpacity onPress={onClose} className="p-2 bg-gray-100 rounded-full">
|
||||||
@@ -56,7 +56,7 @@ export const TimePickerModal = ({ visible, initialDate, title, onConfirm, onClos
|
|||||||
onChange={(d) => setSelectedDate(d.date || new Date())}
|
onChange={(d) => setSelectedDate(d.date || new Date())}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Bottone conferma */}
|
{/* Confirm Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleConfirm}
|
onPress={handleConfirm}
|
||||||
className="mt-4 w-full py-3 bg-[#099499] rounded-xl shadow-lg active:scale-[0.98]"
|
className="mt-4 w-full py-3 bg-[#099499] rounded-xl shadow-lg active:scale-[0.98]"
|
||||||
|
|||||||
24
package-lock.json
generated
@@ -2311,9 +2311,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/brace-expansion": {
|
"node_modules/@isaacs/brace-expansion": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
||||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/balanced-match": "^4.0.1"
|
"@isaacs/balanced-match": "^4.0.1"
|
||||||
@@ -9302,9 +9302,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
@@ -13017,9 +13017,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.2",
|
"version": "7.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
||||||
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
|
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
@@ -13452,9 +13452,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.22.0",
|
"version": "6.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
|
|||||||
12
utils/api.ts
@@ -4,17 +4,17 @@ import * as SecureStore from 'expo-secure-store';
|
|||||||
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL;
|
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL;
|
||||||
export const KEY_TOKEN = 'auth_key';
|
export const KEY_TOKEN = 'auth_key';
|
||||||
|
|
||||||
// Crea un'istanza di axios
|
// Create an Axios instance with default configuration
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: 10000, // 10 secondi timeout
|
timeout: 10000, // 10 seconds timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
// Interceptor: Aggiunge il token a OGNI richiesta se esiste
|
// Interceptor: Adds the token to EVERY request if it exists
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
async (config) => {
|
async (config) => {
|
||||||
const token = await SecureStore.getItemAsync(KEY_TOKEN);
|
const token = await SecureStore.getItemAsync(KEY_TOKEN);
|
||||||
@@ -29,7 +29,7 @@ api.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Interceptor: Gestione errori globale (es. token scaduto)
|
// Interceptor: Global error handling (e.g., expired token)
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error) => {
|
async (error) => {
|
||||||
@@ -38,9 +38,9 @@ api.interceptors.response.use(
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
console.error('[API ERROR]', error.response.status, error.response.data);
|
console.error('[API ERROR]', error.response.status, error.response.data);
|
||||||
|
|
||||||
// Se riceviamo 401 (Unauthorized), potremmo voler fare il logout forzato
|
// If we receive 401 (Unauthorized), we might want to force logout
|
||||||
if (error.response.status === 401) {
|
if (error.response.status === 401) {
|
||||||
// TODO: Qui potresti emettere un evento per disconnettere l'utente
|
// TODO: Here you can add logic to redirect to login screen if needed
|
||||||
await SecureStore.deleteItemAsync(KEY_TOKEN);
|
await SecureStore.deleteItemAsync(KEY_TOKEN);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -58,15 +58,15 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initApp = async () => {
|
const initApp = async () => {
|
||||||
try {
|
try {
|
||||||
// 1. Recupero Token salvato
|
// Get saved Token from SecureStore
|
||||||
const savedToken = await SecureStore.getItemAsync(KEY_TOKEN);
|
const savedToken = await SecureStore.getItemAsync(KEY_TOKEN);
|
||||||
|
|
||||||
if (savedToken) {
|
if (savedToken) {
|
||||||
console.log("Token trovato: ", savedToken);
|
console.log("Token trovato: ", savedToken);
|
||||||
|
|
||||||
// 2. Chiamata al backend per verificare il token e scaricare i dati utente
|
// Call backend to verify token and fetch user data
|
||||||
// Nota: api.ts aggiunge già l'header Authorization grazie all'interceptor (se configurato per leggere da SecureStore)
|
// Note: api.ts already adds the Authorization header thanks to the interceptor (if configured to read from SecureStore)
|
||||||
// Se il tuo api.ts legge da AsyncStorage, assicurati che siano allineati, altrimenti passalo a mano qui:
|
// If your api.ts reads from AsyncStorage, make sure they are aligned, otherwise pass it manually here:
|
||||||
const response = await api.get("/user/info", {
|
const response = await api.get("/user/info", {
|
||||||
headers: { Authorization: `Bearer ${savedToken}` }
|
headers: { Authorization: `Bearer ${savedToken}` }
|
||||||
});
|
});
|
||||||
@@ -75,13 +75,13 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
const userData = result.user;
|
const userData = result.user;
|
||||||
console.log("Sessione valida, dati utente caricati:", userData);
|
console.log("Sessione valida, dati utente caricati:", userData);
|
||||||
|
|
||||||
// 3. Mappatura dati (Backend -> Frontend)
|
// Data mapping (Backend -> Frontend)
|
||||||
// Il backend actionMe ritorna: { id, username, role }
|
// The backend actionMe returns: { id, username, role }
|
||||||
const loadedUser: UserData = {
|
const loadedUser: UserData = {
|
||||||
id: userData.id,
|
id: userData.id,
|
||||||
username: userData.username,
|
username: userData.username,
|
||||||
role: userData.role,
|
role: userData.role,
|
||||||
// Gestiamo i campi opzionali se il backend non li manda ancora
|
// Handle optional fields if the backend doesn't send them yet
|
||||||
name: userData.name,
|
name: userData.name,
|
||||||
surname: userData.surname || '',
|
surname: userData.surname || '',
|
||||||
email: userData.email || '',
|
email: userData.email || '',
|
||||||
@@ -96,7 +96,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Errore inizializzazione (Token scaduto o Server down):', error.message);
|
console.error('Errore inizializzazione (Token scaduto o Server down):', error.message);
|
||||||
|
|
||||||
// Se il token non è valido, puliamo tutto
|
// If the token is not valid, clear everything
|
||||||
await SecureStore.deleteItemAsync(KEY_TOKEN);
|
await SecureStore.deleteItemAsync(KEY_TOKEN);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@@ -109,7 +109,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|||||||
initApp();
|
initApp();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Protezione rotte (opzionale, ma consigliata qui o nel Layout)
|
// Route protection (optional, but recommended here or in the Layout)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isReady) return;
|
if (!isReady) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { DateType } from "react-native-ui-datepicker";
|
import { DateType } from "react-native-ui-datepicker";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trasforma una data da "YYYY-MM-DD" a "DD/MM/YYYY"
|
* Transforms "YYYY-MM-DD" to "DD/MM/YYYY"
|
||||||
* @param dateStr stringa data in formato ISO "YYYY-MM-DD"
|
* @param dateStr string in ISO date format "YYYY-MM-DD"
|
||||||
* @returns stringa formattata "DD/MM/YYYY"
|
* @returns formatted string "DD/MM/YYYY"
|
||||||
*/
|
*/
|
||||||
export const formatDate = (dateStr: string | null | undefined): string => {
|
export const formatDate = (dateStr: string | null | undefined): string => {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
@@ -12,9 +12,9 @@ export const formatDate = (dateStr: string | null | undefined): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trasforma un'ora da "HH:MM:SS" a "HH:MM"
|
* Transforms time from "HH:MM:SS" to "HH:MM"
|
||||||
* @param timeStr stringa ora in formato "HH:MM:SS"
|
* @param timeStr string in time format "HH:MM:SS"
|
||||||
* @returns stringa formattata "HH:MM"
|
* @returns formatted string "HH:MM"
|
||||||
*/
|
*/
|
||||||
export const formatTime = (timeStr: string | null | undefined): string => {
|
export const formatTime = (timeStr: string | null | undefined): string => {
|
||||||
if (!timeStr) return '';
|
if (!timeStr) return '';
|
||||||
@@ -23,9 +23,9 @@ export const formatTime = (timeStr: string | null | undefined): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatta una data per l'uso con un date picker, normalizzandola a mezzanotte
|
* Formats a date for use with a date picker, normalizing it to midnight
|
||||||
* @param d Data in formato DateType
|
* @param d Date in DateType format
|
||||||
* @returns stringa data in formato "YYYY-MM-DD" o null se l'input è null/undefined
|
* @returns string in "YYYY-MM-DD" format or null if input is null/undefined
|
||||||
*/
|
*/
|
||||||
export const formatPickerDate = (d: DateType | null | undefined) => {
|
export const formatPickerDate = (d: DateType | null | undefined) => {
|
||||||
if (!d) return null;
|
if (!d) return null;
|
||||||
@@ -41,9 +41,9 @@ export const formatPickerDate = (d: DateType | null | undefined) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trasforma un timestamp in stringa "DD/MM/YYYY HH:mm:ss"
|
* Transforms a timestamp into a string "DD/MM/YYYY HH:mm:ss"
|
||||||
* @param timestamp stringa o oggetto Date
|
* @param timestamp string or Date object
|
||||||
* @returns stringa formattata oppure vuota se input non valido
|
* @returns formatted string or empty string if input is invalid
|
||||||
*/
|
*/
|
||||||
export const formatTimestamp = (timestamp: string | Date | null | undefined): string => {
|
export const formatTimestamp = (timestamp: string | Date | null | undefined): string => {
|
||||||
if (!timestamp) return '';
|
if (!timestamp) return '';
|
||||||
@@ -52,7 +52,7 @@ export const formatTimestamp = (timestamp: string | Date | null | undefined): st
|
|||||||
if (isNaN(date.getTime())) return '';
|
if (isNaN(date.getTime())) return '';
|
||||||
|
|
||||||
const dd = String(date.getDate()).padStart(2, '0');
|
const dd = String(date.getDate()).padStart(2, '0');
|
||||||
const mm = String(date.getMonth() + 1).padStart(2, '0'); // mesi da 0 a 11
|
const mm = String(date.getMonth() + 1).padStart(2, '0'); // months from 0 to 11
|
||||||
const yyyy = date.getFullYear();
|
const yyyy = date.getFullYear();
|
||||||
|
|
||||||
const hh = String(date.getHours()).padStart(2, '0');
|
const hh = String(date.getHours()).padStart(2, '0');
|
||||||
@@ -63,9 +63,9 @@ export const formatTimestamp = (timestamp: string | Date | null | undefined): st
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converte un timestamp ISO in oggetto Date
|
* Converts an ISO timestamp to a Date object
|
||||||
* @param dateStr stringa data in formato ISO
|
* @param dateStr string in ISO date format
|
||||||
* @returns oggetto Date corrispondente
|
* @returns corresponding Date object
|
||||||
*/
|
*/
|
||||||
export const parseTimestamp = (dateStr: string | undefined | null): Date => {
|
export const parseTimestamp = (dateStr: string | undefined | null): Date => {
|
||||||
if (!dateStr) return new Date();
|
if (!dateStr) return new Date();
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import { Directory, File, Paths } from 'expo-file-system';
|
import { Directory, File, Paths } from 'expo-file-system';
|
||||||
import * as FileSystem from 'expo-file-system/legacy';
|
|
||||||
import { StorageAccessFramework } from 'expo-file-system/legacy';
|
|
||||||
import * as Sharing from 'expo-sharing';
|
import * as Sharing from 'expo-sharing';
|
||||||
import * as Linking from 'expo-linking';
|
|
||||||
import { Platform } from 'react-native';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gestisce l'upload di un documento verso il server usando FormData
|
* Handles upload of a document through the server using FormData
|
||||||
* @param file File da caricare (deve avere almeno la proprietà 'uri')
|
* @param file File to upload (must have at least the 'uri' property)
|
||||||
* @param siteId ID del sito a cui associare il documento (null per registro generale)
|
* @param siteId ID of the site to associate the document with (null for general register)
|
||||||
* @param customTitle Titolo personalizzato per il documento (opzionale)
|
* @param customTitle Custom title for the document (optional)
|
||||||
*/
|
*/
|
||||||
export const uploadDocument = async (
|
export const uploadDocument = async (
|
||||||
file: any,
|
file: any,
|
||||||
@@ -64,10 +60,10 @@ export const uploadDocument = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scarica un documento e offre di aprirlo/condividerlo (expo-sharing)
|
* Download and share a document (expo-sharing)
|
||||||
* @param attachmentId ID o URL relativo del documento
|
* @param attachmentId ID or relative URL of the document
|
||||||
* @param fileName Nome con cui salvare il file
|
* @param fileName Name to save the file as
|
||||||
* @param fileUrl URL completo del file da scaricare
|
* @param fileUrl Full URL of the file to download
|
||||||
*/
|
*/
|
||||||
export const downloadAndShareDocument = async (
|
export const downloadAndShareDocument = async (
|
||||||
mimetype: string,
|
mimetype: string,
|
||||||
@@ -75,7 +71,7 @@ export const downloadAndShareDocument = async (
|
|||||||
fileUrl: string
|
fileUrl: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// TODO: Gestire meglio il download (attualmente si basa su expo-sharing)
|
// TODO: Download based on expo-sharing - some mime types may not be supported
|
||||||
if (!fileUrl || !fileName) {
|
if (!fileUrl || !fileName) {
|
||||||
throw new Error("Parametri mancanti per il download del documento.");
|
throw new Error("Parametri mancanti per il download del documento.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import { HaArea, HaEntity } from '@/types/types';
|
import { HaArea, HaEntity } from '@/types/types';
|
||||||
|
|
||||||
// CONFIGURAZIONE
|
// HOME ASSISTANT API CONFIGURATION
|
||||||
const HA_API_URL = process.env.EXPO_PUBLIC_HA_API_URL;
|
const HA_API_URL = process.env.EXPO_PUBLIC_HA_API_URL;
|
||||||
const HA_TOKEN = process.env.EXPO_PUBLIC_HA_TOKEN;
|
const HA_TOKEN = process.env.EXPO_PUBLIC_HA_TOKEN;
|
||||||
|
|
||||||
// Crea un'istanza di axios per Home Assistant
|
// Create an axios instance for Home Assistant
|
||||||
const haApi = axios.create({
|
const haApi = axios.create({
|
||||||
baseURL: HA_API_URL,
|
baseURL: HA_API_URL,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${HA_TOKEN}`,
|
'Authorization': `Bearer ${HA_TOKEN}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: 5000, // 5 secondi di timeout
|
timeout: 5000, // 5 seconds timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
haApi.interceptors.request.use((config) => {
|
haApi.interceptors.request.use((config) => {
|
||||||
@@ -24,7 +24,7 @@ haApi.interceptors.request.use((config) => {
|
|||||||
* Connection test
|
* Connection test
|
||||||
*/
|
*/
|
||||||
export const testHaConnection = async (): Promise<{ success: boolean; message: string }> => {
|
export const testHaConnection = async (): Promise<{ success: boolean; message: string }> => {
|
||||||
// Controlla se le variabili d'ambiente sono caricate
|
// Check if environment variables are loaded
|
||||||
if (!HA_API_URL || !HA_TOKEN) {
|
if (!HA_API_URL || !HA_TOKEN) {
|
||||||
console.error("Variabili d'ambiente per Home Assistant non trovate. Assicurati che EXPO_PUBLIC_HA_API_URL and EXPO_PUBLIC_HA_TOKEN siano definite nel file .env");
|
console.error("Variabili d'ambiente per Home Assistant non trovate. Assicurati che EXPO_PUBLIC_HA_API_URL and EXPO_PUBLIC_HA_TOKEN siano definite nel file .env");
|
||||||
return { success: false, message: "Configurazione API per Home Assistant mancante." };
|
return { success: false, message: "Configurazione API per Home Assistant mancante." };
|
||||||
@@ -32,7 +32,7 @@ export const testHaConnection = async (): Promise<{ success: boolean; message: s
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await haApi.get('/');
|
const response = await haApi.get('/');
|
||||||
// Se la risposta è OK, HA restituisce un JSON con 'message'
|
// If the response is OK, HA returns a JSON with 'message'
|
||||||
if (response.status === 200 && response.data.message) {
|
if (response.status === 200 && response.data.message) {
|
||||||
return { success: true, message: response.data.message };
|
return { success: true, message: response.data.message };
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ export const testHaConnection = async (): Promise<{ success: boolean; message: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (axiosError.response) {
|
if (axiosError.response) {
|
||||||
// Errori con una risposta dal server (es. 401, 404)
|
// Errors with a server response (e.g., 401, 404)
|
||||||
if (axiosError.response.status === 401) {
|
if (axiosError.response.status === 401) {
|
||||||
return { success: false, message: "Errore 401: Token non autorizzato. Controlla il Long-Lived Token." };
|
return { success: false, message: "Errore 401: Token non autorizzato. Controlla il Long-Lived Token." };
|
||||||
}
|
}
|
||||||
@@ -55,10 +55,10 @@ export const testHaConnection = async (): Promise<{ success: boolean; message: s
|
|||||||
}
|
}
|
||||||
return { success: false, message: `Errore server: Status ${axiosError.response.status}` };
|
return { success: false, message: `Errore server: Status ${axiosError.response.status}` };
|
||||||
} else if (axiosError.request) {
|
} else if (axiosError.request) {
|
||||||
// Errori di rete (la richiesta è partita ma non ha ricevuto risposta)
|
// Network errors (cannot receive a response)
|
||||||
return { success: false, message: `Errore di rete: Impossibile raggiungere ${HA_API_URL}` };
|
return { success: false, message: `Errore di rete: Impossibile raggiungere ${HA_API_URL}` };
|
||||||
} else {
|
} else {
|
||||||
// Errore generico
|
// Generic errors
|
||||||
return { success: false, message: `Errore sconosciuto: ${axiosError.message}` };
|
return { success: false, message: `Errore sconosciuto: ${axiosError.message}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ export const getHaAreas = async (): Promise<HaArea[]> => {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Errore recupero aree:", error);
|
console.error("Errore recupero aree:", error);
|
||||||
return []; // Restituisce un array vuoto in caso di errore
|
return []; // Return an empty array in case of error
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ export const getHaEntitiesByArea = async (areaId: string): Promise<HaEntity[]> =
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Errore recupero entità per l'area ${areaId}:`, error);
|
console.error(`Errore recupero entità per l'area ${areaId}:`, error);
|
||||||
return []; // Restituisce un array vuoto in caso di errore
|
return []; // Return an empty array in case of error
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||