import { useState, useEffect, useCallback } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { PowerOff, RotateCw, Monitor, ChevronDown, Loader2 } from 'lucide-react'; import { api, spoolbuddyApi, type Printer, type SmartPlug, type SmartPlugStatus } from '../../api/client'; interface SpoolBuddyQuickMenuProps { isOpen: boolean; onClose: () => void; deviceId: string | null; deviceOnline: boolean; } type SystemCommand = 'reboot' | 'shutdown' | 'restart_daemon' | 'restart_browser'; type PendingConfirm = | { type: 'system'; command: SystemCommand } | { type: 'plug'; plug: SmartPlug; printer: Printer; currentState: string | null }; interface PlugState { plug: SmartPlug; printer: Printer; status: SmartPlugStatus | null; loading: boolean; } export function SpoolBuddyQuickMenu({ isOpen, onClose, deviceId, deviceOnline }: SpoolBuddyQuickMenuProps) { const { t } = useTranslation(); const [pendingConfirm, setPendingConfirm] = useState(null); const [commandBusy, setCommandBusy] = useState(false); const [plugStates, setPlugStates] = useState>(new Map()); // Fetch printers and smart plugs const { data: printers = [] } = useQuery({ queryKey: ['printers'], queryFn: () => api.getPrinters(), enabled: isOpen, }); const { data: smartPlugs = [] } = useQuery({ queryKey: ['smart-plugs'], queryFn: () => api.getSmartPlugs(), enabled: isOpen, }); // Build printer-plug pairs (only main power plugs linked to printers) const printerPlugs: PlugState[] = printers .map((printer) => { const plug = smartPlugs.find( (p) => p.printer_id === printer.id && p.plug_type !== 'mqtt' && p.enabled ); if (!plug) return null; const state = plugStates.get(plug.id); return { plug, printer, status: state ? { state: state.state, reachable: true, device_name: null, energy: null } : null, loading: state?.loading ?? false, }; }) .filter(Boolean) as PlugState[]; // Fetch plug statuses when menu opens useEffect(() => { if (!isOpen || smartPlugs.length === 0) return; const linkedPlugs = smartPlugs.filter( (p) => p.printer_id !== null && p.plug_type !== 'mqtt' && p.enabled ); linkedPlugs.forEach(async (plug) => { try { const status = await api.getSmartPlugStatus(plug.id); setPlugStates((prev) => { const next = new Map(prev); next.set(plug.id, { loading: false, state: status.state }); return next; }); } catch { setPlugStates((prev) => { const next = new Map(prev); next.set(plug.id, { loading: false, state: null }); return next; }); } }); }, [isOpen, smartPlugs]); // Clear state when menu closes useEffect(() => { if (!isOpen) { setPendingConfirm(null); setCommandBusy(false); } }, [isOpen]); const handleTogglePlug = useCallback(async (plug: SmartPlug) => { setPlugStates((prev) => { const next = new Map(prev); const current = next.get(plug.id); next.set(plug.id, { loading: true, state: current?.state ?? null }); return next; }); try { await api.controlSmartPlug(plug.id, 'toggle'); const status = await api.getSmartPlugStatus(plug.id); setPlugStates((prev) => { const next = new Map(prev); next.set(plug.id, { loading: false, state: status.state }); return next; }); } catch { setPlugStates((prev) => { const next = new Map(prev); const current = next.get(plug.id); next.set(plug.id, { loading: false, state: current?.state ?? null }); return next; }); } }, []); const handleSystemCommand = useCallback(async (command: SystemCommand) => { if (!deviceId) return; setCommandBusy(true); try { await spoolbuddyApi.systemCommand(deviceId, command); // Close menu after successful command setTimeout(() => onClose(), 500); } catch { setCommandBusy(false); } }, [deviceId, onClose]); const executeConfirmed = useCallback(() => { if (!pendingConfirm) return; if (pendingConfirm.type === 'system') { handleSystemCommand(pendingConfirm.command); } else { handleTogglePlug(pendingConfirm.plug); } setPendingConfirm(null); }, [pendingConfirm, handleSystemCommand, handleTogglePlug]); if (!isOpen) return null; const isPlugOn = (state: string | null) => state === 'ON' || state === 'on'; return ( <> {/* Backdrop */}
{/* Slide-down panel */}
{/* Handle bar */}
{/* Printer Power Section */} {printerPlugs.length > 0 && (

{t('spoolbuddy.quickMenu.printerPower', 'Printer Power')}

{printerPlugs.map(({ plug, printer, loading }) => { const state = plugStates.get(plug.id); const on = isPlugOn(state?.state ?? null); return ( ); })}
)} {/* System Controls Section */}

{t('spoolbuddy.quickMenu.systemControls', 'System')}

} label={t('spoolbuddy.quickMenu.restartDaemon', 'Restart Daemon')} onClick={() => setPendingConfirm({ type: 'system', command: 'restart_daemon' })} disabled={!deviceId || !deviceOnline || commandBusy} /> } label={t('spoolbuddy.quickMenu.restartBrowser', 'Restart Browser')} onClick={() => setPendingConfirm({ type: 'system', command: 'restart_browser' })} disabled={!deviceId || !deviceOnline || commandBusy} /> } label={t('spoolbuddy.quickMenu.reboot', 'Reboot')} onClick={() => setPendingConfirm({ type: 'system', command: 'reboot' })} disabled={!deviceId || !deviceOnline || commandBusy} variant="warning" /> } label={t('spoolbuddy.quickMenu.shutdown', 'Shutdown')} onClick={() => setPendingConfirm({ type: 'system', command: 'shutdown' })} disabled={!deviceId || !deviceOnline || commandBusy} variant="danger" />
{/* Swipe hint */}
{t('spoolbuddy.quickMenu.swipeToClose', 'Swipe down to close')}
{/* Confirmation Dialog */} {pendingConfirm && (

{t('spoolbuddy.quickMenu.confirmTitle', 'Confirm')}

{pendingConfirm.type === 'plug' ? (isPlugOn(pendingConfirm.currentState) ? t('spoolbuddy.quickMenu.confirmPlugOff', 'Turn off {{name}}?', { name: pendingConfirm.printer.name }) : t('spoolbuddy.quickMenu.confirmPlugOn', 'Turn on {{name}}?', { name: pendingConfirm.printer.name })) : pendingConfirm.command === 'shutdown' ? t('spoolbuddy.quickMenu.confirmShutdown', 'Are you sure you want to shut down the SpoolBuddy? You will need physical access to turn it back on.') : pendingConfirm.command === 'reboot' ? t('spoolbuddy.quickMenu.confirmReboot', 'Are you sure you want to reboot the SpoolBuddy?') : pendingConfirm.command === 'restart_daemon' ? t('spoolbuddy.quickMenu.confirmRestartDaemon', 'Restart the SpoolBuddy daemon? NFC and scale will be temporarily unavailable.') : t('spoolbuddy.quickMenu.confirmRestartBrowser', 'Restart the kiosk browser? The display will briefly go blank.')}

)} ); } function SystemButton({ icon, label, onClick, disabled, variant = 'default', }: { icon: React.ReactNode; label: string; onClick: () => void; disabled: boolean; variant?: 'default' | 'warning' | 'danger'; }) { const variantClasses = { default: 'bg-zinc-800/60 hover:bg-zinc-700/60 text-zinc-300', warning: 'bg-amber-900/30 hover:bg-amber-900/50 text-amber-400', danger: 'bg-red-900/30 hover:bg-red-900/50 text-red-400', }; return ( ); }