From abe14f4c3f17db3e6e3fdaa8f084dd8e81f0227b Mon Sep 17 00:00:00 2001 From: leonardo Date: Tue, 9 Dec 2025 13:06:35 +0100 Subject: [PATCH] Add DatePicker and refactor some views layout --- .gitignore | 4 + app/_layout.tsx | 2 +- app/attendance/index.tsx | 2 + app/automation/[id].tsx | 75 ++++++++++++++ app/automation/_layout.tsx | 10 ++ app/automation/index.tsx | 87 +++------------- app/index.tsx | 8 +- app/permits/index.tsx | 2 +- components/RangePickerModal.tsx | 52 ++++++---- components/RenamePermitModal.tsx | 127 ----------------------- components/RequestPermitModal.tsx | 166 ++++++++++++++++++++++++++++++ components/TimePickerModal.tsx | 68 ++++++++++++ 12 files changed, 379 insertions(+), 224 deletions(-) create mode 100644 app/automation/[id].tsx create mode 100644 app/automation/_layout.tsx delete mode 100644 components/RenamePermitModal.tsx create mode 100644 components/RequestPermitModal.tsx create mode 100644 components/TimePickerModal.tsx diff --git a/.gitignore b/.gitignore index f8c6c2e..9874198 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,7 @@ app-example # generated native folders /ios /android + +# IDE +.idea +.vscode \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index 7c6bdff..fe65220 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -53,7 +53,7 @@ export default function AppLayout() { }} /> , diff --git a/app/attendance/index.tsx b/app/attendance/index.tsx index b4b8b0e..b7098ba 100644 --- a/app/attendance/index.tsx +++ b/app/attendance/index.tsx @@ -80,9 +80,11 @@ export default function AttendanceScreen() { {item.in} - {item.out || 'In corso'} + {item.status === 'complete' && ( 8h + )} ))} diff --git a/app/automation/[id].tsx b/app/automation/[id].tsx new file mode 100644 index 0000000..fd60752 --- /dev/null +++ b/app/automation/[id].tsx @@ -0,0 +1,75 @@ +import { OFFICES_DATA } from '@/data/data'; +import type { OfficeItem } from '@/types/types'; +import { Activity, ChevronLeft, Lightbulb, Thermometer, Wifi, WifiOff, Zap, Plus } from 'lucide-react-native'; +import React from 'react'; +import { useRouter, useLocalSearchParams } from 'expo-router'; +import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; + +export default function AutomationDetail() { + const router = useRouter(); + const { id } = useLocalSearchParams<{ id: string }>(); + + const selectedOffice: OfficeItem | undefined = OFFICES_DATA.find(o => o.id.toString() === id); + if (!selectedOffice) return Ufficio non trovato; + + return ( + + {/* Header Dettaglio */} + + + router.back()} + className="mr-4 p-3 rounded-full bg-gray-50 active:bg-gray-200" + > + + + {selectedOffice.name} + + {/* Status Dot */} + + + + + + {/* Lights Card Grande */} + + + + {/* Switch UI Grande - FIXED: Rimossa 'transition-colors' che causava il crash */} + + + + + Luci + {selectedOffice.lights ? 'Accese - 80%' : 'Spente'} + + + {/* Temp Card Grande */} + + + Clima + + {selectedOffice.temp} + °C + + + + + {/* Chart Card Grande */} + + + + Consumo Oggi + + + {[40, 65, 30, 80, 55, 90, 45].map((h, i) => ( + + + + ))} + + + + + ); +} \ No newline at end of file diff --git a/app/automation/_layout.tsx b/app/automation/_layout.tsx new file mode 100644 index 0000000..b187840 --- /dev/null +++ b/app/automation/_layout.tsx @@ -0,0 +1,10 @@ +import { Stack } from "expo-router"; + +export default function AutomationLayout() { + return ( + + + + + ); +} diff --git a/app/automation/index.tsx b/app/automation/index.tsx index 4dba59a..91df11e 100644 --- a/app/automation/index.tsx +++ b/app/automation/index.tsx @@ -1,91 +1,24 @@ import { OFFICES_DATA } from '@/data/data'; import type { OfficeItem } from '@/types/types'; -import { Activity, ChevronRight, Lightbulb, Thermometer, Wifi, WifiOff, Zap } from 'lucide-react-native'; +import { useRouter } from 'expo-router'; +import { Activity, ChevronLeft, Lightbulb, Thermometer, Wifi, WifiOff, Zap, Plus } from 'lucide-react-native'; import React, { useState } from 'react'; import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; export default function AutomationScreen() { + const router = useRouter(); const [selectedOffice, setSelectedOffice] = useState(null); - // --- DETAIL VIEW --- - if (selectedOffice) { - return ( - - {/* Header Dettaglio */} - - setSelectedOffice(null)} - className="mr-4 p-3 rounded-full bg-gray-50 active:bg-gray-200" - > - - - {selectedOffice.name} - - - - {/* Status Banner Grande */} - - - - - {selectedOffice.status} - - - - - {/* Lights Card Grande */} - - - - {/* Switch UI Grande - FIXED: Rimossa 'transition-colors' che causava il crash */} - - - - - Luci - {selectedOffice.lights ? 'Accese - 80%' : 'Spente'} - - - {/* Temp Card Grande */} - - - Clima - - {selectedOffice.temp} - °C - - - - - {/* Chart Card Grande */} - - - - Consumo Oggi - - - {[40, 65, 30, 80, 55, 90, 45].map((h, i) => ( - - - - ))} - - - - - ); - } - // --- LIST VIEW (INGRANDITA) --- return ( - + Domotica Controlla gli ambienti - SYSTEM OK + ONLINE @@ -93,7 +26,7 @@ export default function AutomationScreen() { {OFFICES_DATA.map((office) => ( setSelectedOffice(office)} + onPress={() => router.push(`/automation/${office.id}`)} className="bg-white rounded-3xl p-6 shadow-sm flex-row items-center justify-between border border-gray-100 active:border-[#099499]/30 active:scale-[0.98]" > @@ -125,6 +58,14 @@ export default function AutomationScreen() { {/* Spacer finale per la navbar */} + + {/* FAB */} + 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" + > + + ); } \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index 6fca274..ddcd58e 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -9,9 +9,9 @@ export default function HomeScreen() { const incompleteTasks = ATTENDANCE_DATA.filter(item => item.status === 'incomplete'); return ( - + {/* Banner Custom */} - + @@ -34,7 +34,7 @@ export default function HomeScreen() { {/* Contenuto Scrollabile */} @@ -48,7 +48,7 @@ export default function HomeScreen() { Presenza incompleta - Manca uscita: {incompleteTasks[0].site} + {incompleteTasks[0].site} router.push('/attendance')} className="bg-orange-50 px-5 py-3 rounded-xl ml-2 active:bg-orange-100"> diff --git a/app/permits/index.tsx b/app/permits/index.tsx index 2f3bfc3..7635954 100644 --- a/app/permits/index.tsx +++ b/app/permits/index.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { PERMITS_DATA } from '@/data/data'; import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, Clock, Plus, Thermometer, X } from 'lucide-react-native'; import { ScrollView, Text, TouchableOpacity, View } from 'react-native'; -import RequestPermitModal from '@/components/RenamePermitModal'; +import RequestPermitModal from '@/components/RequestPermitModal'; export default function PermitsScreen() { const [currentDate, setCurrentDate] = useState(new Date(2025, 11, 1)); // Dicembre 2025 Mock diff --git a/components/RangePickerModal.tsx b/components/RangePickerModal.tsx index 73578e6..b8a3eb5 100644 --- a/components/RangePickerModal.tsx +++ b/components/RangePickerModal.tsx @@ -1,16 +1,15 @@ import React, { useState, useEffect } from 'react'; -import { Modal, Text, TouchableOpacity, View } from 'react-native'; -import DateTimePicker, { DateType, useDefaultStyles } from 'react-native-ui-datepicker'; +import { Alert, Modal, Text, TouchableOpacity, View } from 'react-native'; +import DateTimePicker, { DateType, useDefaultStyles, useDefaultClassNames } from 'react-native-ui-datepicker'; import { Check, X } from 'lucide-react-native'; export const RangePickerModal = ({ visible, onClose, currentRange, onApply }: any) => { - // Stato locale temporaneo per la selezione nel modale - const [localRange, setLocalRange] = useState(currentRange); - - // Sincronizza lo stato locale quando il modale si apre - useEffect(() => { - if (visible) setLocalRange(currentRange); - }, [visible, currentRange]); + const defaultStyles = useDefaultStyles(); + // const defaultClassNames = useDefaultClassNames(); + const [range, setRange] = useState<{ + startDate: DateType; + endDate: DateType; + }>({ startDate: undefined, endDate: undefined }); return ( setLocalRange(params)} - // selectedItemColor="#099499" - // headerTextStyle={{ color: '#1f2937', fontWeight: 'bold', fontSize: 18 }} - // calendarTextStyle={{ color: '#374151' }} - // weekDaysTextStyle={{ color: '#9ca3af', fontWeight: 'bold' }} + startDate={range.startDate} + endDate={range.endDate} + onChange={(params) => setRange(params)} + timeZone='Europe/Rome' + locale='it' + styles={{ + ...defaultStyles, + selected: { backgroundColor: '#099499' } + }} + // classNames={{ + // ...defaultClassNames, + // selected: 'bg-#099499' + // }} /> { - onApply(localRange); + console.log("Data inizio: ", range.startDate?.toLocaleString()); + console.log("Data fine: ", range.endDate?.toLocaleString()); + onApply(range); onClose(); }} className="mt-6 bg-[#099499] rounded-xl py-4 flex-row justify-center items-center active:bg-[#077d82]" @@ -50,6 +56,16 @@ export const RangePickerModal = ({ visible, onClose, currentRange, onApply }: an Applica Filtro + { + console.log("Reset pressed"); + range.startDate = range.endDate = undefined; + // TODO: deselect dates from calendar + }} + className="mt-2 bg-gray-200 rounded-xl py-4 flex-row justify-center items-center active:bg-gray-300" + > + Reset + diff --git a/components/RenamePermitModal.tsx b/components/RenamePermitModal.tsx deleted file mode 100644 index e7f2769..0000000 --- a/components/RenamePermitModal.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { useState } from 'react'; -import { View, Text, Modal, TouchableOpacity, TextInput, ScrollView } from 'react-native'; -import { PermitType } from '@/types/types'; -import { X } from 'lucide-react-native'; - -interface RequestPermitModalProps { - visible: boolean; - onClose: () => void; - onSubmit: (data: any) => void; -} - -export default function RequestPermitModal({ visible, onClose, onSubmit}: RequestPermitModalProps) { - const [type, setType] = useState('Ferie'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); - const [startTime, setStartTime] = useState(''); - const [endTime, setEndTime] = useState(''); - - return ( - - - - {/* Header Modale */} - - Nuova Richiesta - - - - - - - - {/* Tipologia */} - - Tipologia Assenza - - {(['Ferie', 'Permesso', 'Malattia'] as PermitType[]).map((t) => ( - setType(t)} - className={`flex-1 py-4 rounded-xl border-2 items-center justify-center ${ - type === t - ? 'border-[#099499] bg-teal-50' - : 'border-gray-100 bg-white' - }`} - > - - {t} - - - ))} - - - - {/* Date Selection */} - - - - {type === 'Permesso' ? 'Data' : 'Dal'} - - - - {type !== 'Permesso' && ( - - Al - - - )} - - - {/* Time Selection (Solo Permessi) */} - {type === 'Permesso' && ( - - - Dalle Ore - - - - Alle Ore - - - - )} - - { - onSubmit({ type, startDate, endDate, startTime, endTime }); - onClose(); - }} - className="w-full py-4 bg-[#099499] rounded-2xl shadow-lg mt-4 active:scale-[0.98]" - > - Invia Richiesta - - - - - - - ); -}; \ No newline at end of file diff --git a/components/RequestPermitModal.tsx b/components/RequestPermitModal.tsx new file mode 100644 index 0000000..959cf84 --- /dev/null +++ b/components/RequestPermitModal.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { View, Text, Modal, TouchableOpacity, TextInput, ScrollView } from 'react-native'; +import DateTimePicker, { DateType, useDefaultStyles, useDefaultClassNames } from 'react-native-ui-datepicker'; +import { PermitType } from '@/types/types'; +import { X } from 'lucide-react-native'; +import { TimePickerModal } from './TimePickerModal'; + +interface RequestPermitModalProps { + visible: boolean; + onClose: () => void; + onSubmit: (data: any) => void; +} + +export default function RequestPermitModal({ visible, onClose, onSubmit }: RequestPermitModalProps) { + const defaultStyles = useDefaultStyles(); + const [type, setType] = useState('Ferie'); + const [date, setDate] = useState(); + const [range, setRange] = useState<{ + startDate: DateType; + endDate: DateType; + }>({ startDate: undefined, endDate: undefined }); + + const [showStartPicker, setShowStartPicker] = useState(false); + const [showEndPicker, setShowEndPicker] = useState(false); + const [startTime, setStartTime] = useState(''); + const [endTime, setEndTime] = useState(''); + + return ( + + + + {/* Header Modale */} + + Nuova Richiesta + + + + + + + + {/* Tipologia */} + + Tipologia Assenza + + {(['Ferie', 'Permesso', 'Malattia'] as PermitType[]).map((t) => ( + setType(t)} + className={`flex-1 py-4 rounded-xl border-2 items-center justify-center ${type === t + ? 'border-[#099499] bg-teal-50' + : 'border-gray-100 bg-white' + }`} + > + + {t} + + + ))} + + + + {/* Date Selection */} + {type !== 'Permesso' ? ( + setRange(params)} + timeZone='Europe/Rome' + locale='it' + styles={{ + ...defaultStyles, + selected: { backgroundColor: '#099499' } + }} + /> + ) : ( + + setDate(date)} + timeZone='Europe/Rome' + locale='it' + styles={{ + ...defaultStyles, + selected: { backgroundColor: '#099499' } + }} + /> + + + + Dalle Ore + setShowStartPicker(true)}> + + + + + Alle Ore + setShowEndPicker(true)}> + + + + + + )} + + setStartTime(time)} + onClose={() => setShowStartPicker(false)} + /> + setEndTime(time)} + onClose={() => setShowEndPicker(false)} + /> + + + { + onSubmit({ type, date, range, startTime, endTime }); + onClose(); + }} + className="w-full py-4 bg-[#099499] rounded-2xl shadow-lg active:scale-[0.98]" + > + Invia Richiesta + + { + console.log("Reset pressed"); + onClose(); + // TODO: deselect dates from calendar + }} + className="mt-2 bg-gray-200 rounded-xl py-4 flex-row justify-center items-center active:bg-gray-300" + > + Annulla Richiesta + + + + + + + + ); +}; \ No newline at end of file diff --git a/components/TimePickerModal.tsx b/components/TimePickerModal.tsx new file mode 100644 index 0000000..a885de9 --- /dev/null +++ b/components/TimePickerModal.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { Modal, View, TouchableOpacity, Text } from 'react-native'; +import DateTimePicker, { DateType, useDefaultStyles } from 'react-native-ui-datepicker'; +import { X } from 'lucide-react-native'; +import dayjs from 'dayjs'; + +interface TimePickerModalProps { + visible: boolean; + initialDate?: DateType; + onConfirm: (time: string) => void; + onClose: () => void; +} + +export const TimePickerModal = ({ visible, initialDate, onConfirm, onClose }: TimePickerModalProps) => { + const defaultStyles = useDefaultStyles(); + const [selectedDate, setSelectedDate] = useState(initialDate || new Date()); + + const formatTime = (date?: DateType | null) => { + if (!date) return "00:00"; + date = dayjs(date); + const hour = date?.hour().toString().padStart(2, "0") ?? "00"; + const minute = date?.minute().toString().padStart(2, "0") ?? "00"; + return `${hour}:${minute}`; + }; + + const handleConfirm = () => { + const time = formatTime(selectedDate); + console.log("Selected time:", time); + onConfirm(time); + onClose(); + }; + + return ( + + + + + {/* Header con chiusura */} + + + + + + + {/* TimePicker */} + setSelectedDate(d.date || new Date())} + /> + + {/* Bottone conferma */} + + Applica + + + + + ); +};