import { Thermometer, BatteryLow, TriangleAlert, Snowflake, Flame, Fan, Droplet, Lock, Heater, PlaneTakeoff, PlaneLanding, BedDouble } from 'lucide-react' import type { ZoneStatus } from '../types' import { ROOM_STATE_LABELS, MHI_FAN_SPEEDS } from '../types' const AC_DEVICE_TYPES = ['mhi_modbus', 'mhi_mqtt', 'midea', 'daikin', 'home_assistant'] // Departure = takeoff, arrival = landing, stayover = bed (lucide has no "person in // bed" icon, so BedDouble is the closest stand-in). A departure also shows a second // landing icon when has_next_booking — same-day-turnover hint (someone's due in later). // A genuinely vacant room (no icon, since nothing's arriving today) instead gets a // plain night-count-until-next-arrival, or "7+ nights" when nothing's booked at all // within the scheduler's lookahead window. function BookingIcons({ zone }: { zone: ZoneStatus }) { const textStyle = { display: 'inline-flex', alignItems: 'center', gap: 2, marginLeft: 6, fontSize: 11.5, fontWeight: 600, color: 'var(--text-mid)' } as const if (zone.booking_kind) { const iconProps = { size: 13, strokeWidth: 1.75 } return ( {zone.booking_kind === 'departure' && } {zone.booking_kind === 'arrival' && } {zone.booking_kind === 'stayover' && } {zone.booking_kind === 'departure' && zone.has_next_booking && } {zone.total_nights != null && {zone.current_night}/{zone.total_nights}} ) } if (zone.room_state === 'vacant' && zone.zone_type === 'room') { return ( Vacant {zone.vacant_nights != null ? `${zone.vacant_nights} night${zone.vacant_nights === 1 ? '' : 's'}` : '7+ nights'} ) } return null } const MODE_ICONS: Record = { cool: Snowflake, heat: Flame, fan: Fan, dry: Droplet, auto: Fan, } // bed -> living/lounge -> bathroom, matching the order in the sketch; anything else last. function locationRank(location: string | null) { const l = (location || '').toLowerCase() if (l.includes('bed')) return 0 if (l.includes('living') || l.includes('lounge')) return 1 if (l.includes('bath')) return 2 return 3 } function fmtTemp(t: string | number | null | undefined) { return t != null ? `${Number(t).toFixed(1)}°C` : '—' } export default function ZoneCard({ zone, onClick }: { zone: ZoneStatus; onClick: () => void }) { const unhealthy = zone.devices.filter(d => d.health_state !== 'healthy').length const worstHealth = zone.devices.some(d => d.health_state === 'unresponsive' || d.health_state === 'poor') const lowBattery = zone.devices.some(d => d.battery_pct != null && d.battery_pct < 30) const trvs = zone.devices .filter(d => d.device_type === 'shelly_trv') .sort((a, b) => locationRank(a.location) - locationRank(b.location)) const trvReadings = trvs.map(d => d.current_room_temp).filter((t): t is string => t != null).map(Number) const avgRoomTemp = trvReadings.length ? trvReadings.reduce((a, b) => a + b, 0) / trvReadings.length : null const ac = zone.devices.find(d => AC_DEVICE_TYPES.includes(d.device_type)) const acOn = ac?.power_state === 'on' const ModeIcon = MODE_ICONS[ac?.current_mode || ''] || Fan const fanLevel = ac ? (MHI_FAN_SPEEDS as readonly string[]).indexOf(ac.fan_speed || '') + 1 : 0 return ( {zone.name} {unhealthy > 0 && ( )} {!zone.auto_mode && Manual} {Number(zone.target_temp).toFixed(1)}°C / {fmtTemp(avgRoomTemp)} {trvs.length > 0 && ( {trvs.slice(0, 3).map(d => ( {fmtTemp(d.current_target_temp)}/{fmtTemp(d.current_room_temp)} ))} )} {ac && ( {ac.remote_locked && } {[1, 2, 3, 4].map(n => )} {fmtTemp(ac.current_target_temp)} / {fmtTemp(ac.current_room_temp)} )} {zone.device_count ?? zone.devices.length} device{(zone.device_count ?? zone.devices.length) === 1 ? '' : 's'} {lowBattery && } ) }