feat: Refactor automation and site document screens, add device management features, and implement Home Assistant API integration
This commit is contained in:
@ -1,74 +1,130 @@
|
||||
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';
|
||||
import DeviceCard from '@/components/DeviceCard';
|
||||
import LoadingScreen from '@/components/LoadingScreen';
|
||||
import { HaArea, HaEntity } from '@/types/types';
|
||||
import { getHaEntitiesByArea, toggleHaEntity } from '@/utils/haApi';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { ChevronLeft, ServerOff, WifiOff } from 'lucide-react-native';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
export default function AutomationDetail() {
|
||||
export default function AutomationDetailScreen() {
|
||||
const router = useRouter();
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const params = useLocalSearchParams();
|
||||
const [area, setArea] = useState<HaArea | null>(null);
|
||||
const [devices, setDevices] = useState<HaEntity[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const selectedOffice: OfficeItem | undefined = OFFICES_DATA.find(o => o.id.toString() === id);
|
||||
if (!selectedOffice) return <Text>Ufficio non trovato</Text>;
|
||||
// Fetch dei devices dell'area
|
||||
const fetchAreaDevices = useCallback(async (areaId: string, isRefreshing = false) => {
|
||||
try {
|
||||
if (!isRefreshing) setIsLoading(true);
|
||||
|
||||
// Fetch Documenti
|
||||
const response = await getHaEntitiesByArea(areaId);
|
||||
const filteredDevices = response.filter(device => device.name);
|
||||
setDevices(filteredDevices);
|
||||
} catch (error) {
|
||||
console.error('Errore nel recupero dei dispositivi dell\'area:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.areaData) {
|
||||
try {
|
||||
const jsonString = Array.isArray(params.areaData) ? params.areaData[0] : params.areaData;
|
||||
const parsedArea = JSON.parse(jsonString);
|
||||
setArea(parsedArea);
|
||||
} catch (error) {
|
||||
console.error('Errore nel parsing dei dati del cantiere:', error);
|
||||
}
|
||||
}
|
||||
}, [params.areaData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (params.id) {
|
||||
setIsLoading(true);
|
||||
const areaId = Array.isArray(params.id) ? params.id[0] : params.id;
|
||||
fetchAreaDevices(areaId);
|
||||
}
|
||||
}, [params.id, fetchAreaDevices]);
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
const areaId = Array.isArray(params.id) ? params.id[0] : params.id;
|
||||
fetchAreaDevices(areaId, true);
|
||||
};
|
||||
|
||||
const onToggle = useCallback((entityId: string) => {
|
||||
// Optimistic UI update
|
||||
setDevices(prevDevices =>
|
||||
prevDevices.map(d =>
|
||||
d.entity_id === entityId
|
||||
? { ...d, state: d.state === 'on' ? 'off' : 'on' }
|
||||
: d
|
||||
)
|
||||
);
|
||||
// Fire and forget the API call
|
||||
toggleHaEntity(entityId).catch(() => {
|
||||
// Revert on error
|
||||
console.log("Toggle failed, reverting UI.");
|
||||
const areaId = Array.isArray(params.id) ? params.id[0] : params.id;
|
||||
if (areaId) fetchAreaDevices(areaId);
|
||||
});
|
||||
}, [fetchAreaDevices, params.id]);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (!area) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<WifiOff size={48} color="#9ca3af" />
|
||||
<Text className="text-xl text-gray-600 font-bold mt-4">Area non trovata</Text>
|
||||
<Text className="text-gray-400 text-center mt-2 mb-4">L'area che stai cercando non esiste o è stata rimossa.</Text>
|
||||
<TouchableOpacity onPress={() => router.back()} className="mt-4 bg-[#099499] px-6 py-3 rounded-xl active:scale-95">
|
||||
<Text className="text-white font-bold text-base">Torna Indietro</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
{/* Header Dettaglio */}
|
||||
<View className="bg-white p-6 pt-16 shadow-sm flex-row justify-between items-center border-b border-gray-100">
|
||||
<View className='flex-row items-center'>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
className="mr-4 p-3 rounded-full bg-gray-50 active:bg-gray-200"
|
||||
>
|
||||
<View className='flex-row items-center gap-4'>
|
||||
<TouchableOpacity onPress={() => router.back()} className='rounded-full active:bg-gray-100'>
|
||||
<ChevronLeft size={28} color="#4b5563" />
|
||||
</TouchableOpacity>
|
||||
<Text className="text-2xl font-bold text-gray-800">{selectedOffice.name}</Text>
|
||||
<Text className="text-2xl font-bold text-gray-800">{area.name}</Text>
|
||||
</View>
|
||||
{/* Status Dot */}
|
||||
<View className={`ms-auto w-4 h-4 rounded-full border-2 border-white shadow-sm ${selectedOffice.status === 'online' ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
<View className={`ms-auto w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm bg-green-500`} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={{ padding: 20, gap: 24 }} showsVerticalScrollIndicator={false}>
|
||||
<View className="flex-row gap-5">
|
||||
{/* Lights Card Grande */}
|
||||
<TouchableOpacity className={`flex-1 p-6 rounded-3xl border-2 active:scale-95 ${selectedOffice.lights ? 'border-[#099499] bg-teal-50' : 'border-transparent bg-white shadow-sm'}`}>
|
||||
<View className="flex-row justify-between items-start mb-6">
|
||||
<Lightbulb size={40} color={selectedOffice.lights ? '#099499' : '#d1d5db'} />
|
||||
{/* Switch UI Grande - FIXED: Rimossa 'transition-colors' che causava il crash */}
|
||||
<View className={`w-14 h-8 rounded-full p-1 ${selectedOffice.lights ? 'bg-[#099499]' : 'bg-gray-200'}`}>
|
||||
<View className={`bg-white w-6 h-6 rounded-full shadow-sm ${selectedOffice.lights ? 'translate-x-6' : 'translate-x-0'}`} />
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-lg font-bold text-gray-800 mb-1">Luci</Text>
|
||||
<Text className="text-gray-500 font-medium">{selectedOffice.lights ? 'Accese - 80%' : 'Spente'}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Temp Card Grande */}
|
||||
<View className="flex-1 bg-white p-6 rounded-3xl border border-transparent shadow-sm">
|
||||
<Thermometer size={40} color="#fb923c" className="mb-6" />
|
||||
<Text className="text-lg font-bold text-gray-800 mb-1">Clima</Text>
|
||||
<View className="flex-row items-end">
|
||||
<Text className="text-4xl font-bold text-gray-800">{selectedOffice.temp}</Text>
|
||||
<Text className="text-gray-500 mb-2 ml-1 text-lg">°C</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Chart Card Grande */}
|
||||
<View className="bg-white p-6 rounded-3xl shadow-sm border border-gray-100">
|
||||
<View className="flex-row items-center mb-6 gap-3">
|
||||
<Activity size={28} color="#099499" />
|
||||
<Text className="text-xl font-bold text-gray-700">Consumo Oggi</Text>
|
||||
</View>
|
||||
<View className="flex-row items-end justify-between h-48 gap-4">
|
||||
{[40, 65, 30, 80, 55, 90, 45].map((h, i) => (
|
||||
<View key={i} className="flex-1 bg-gray-100 rounded-t-xl relative overflow-hidden h-full justify-end">
|
||||
<View style={{ height: `${h}%` }} className="w-full bg-[#099499] rounded-t-xl opacity-80" />
|
||||
</View>
|
||||
<ScrollView
|
||||
contentContainerClassName={`flex-grow p-5 ${devices.length > 0 ? 'justify-start' : 'justify-center'}`}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={['#099499']} tintColor={'#099499'} />}
|
||||
>
|
||||
{devices.length > 0 ? (
|
||||
<View className="flex-row flex-wrap justify-between">
|
||||
{devices.map(device => (
|
||||
<DeviceCard key={device.entity_id} device={device} onToggle={onToggle} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className="items-center">
|
||||
<ServerOff size={48} color="#9ca3af" />
|
||||
<Text className="text-lg text-gray-500 font-medium mt-4">Nessun dispositivo</Text>
|
||||
<Text className="text-center text-gray-400 mt-2">
|
||||
Non ci sono dispositivi in quest'area.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className="h-20" />
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user