import { useEffect, useReducer, useCallback } from 'react'; export interface MatchedSpool { id: number; tag_uid: string; material: string; subtype: string | null; color_name: string | null; rgba: string | null; brand: string | null; label_weight: number; core_weight: number; weight_used: number; } export interface SpoolBuddyState { weight: number | null; weightStable: boolean; rawAdc: number | null; matchedSpool: MatchedSpool | null; unknownTagUid: string | null; unknownTrayUuid: string | null; deviceOnline: boolean; deviceId: string | null; } type Action = | { type: 'WEIGHT_UPDATE'; weight: number; stable: boolean; rawAdc: number; deviceId: string } | { type: 'TAG_MATCHED'; spool: MatchedSpool; deviceId: string } | { type: 'UNKNOWN_TAG'; tagUid: string; trayUuid: string | null; deviceId: string } | { type: 'TAG_REMOVED'; deviceId: string } | { type: 'DEVICE_ONLINE'; deviceId: string } | { type: 'DEVICE_OFFLINE'; deviceId: string }; const initialState: SpoolBuddyState = { weight: null, weightStable: false, rawAdc: null, matchedSpool: null, unknownTagUid: null, unknownTrayUuid: null, deviceOnline: false, deviceId: null, }; function reducer(state: SpoolBuddyState, action: Action): SpoolBuddyState { switch (action.type) { case 'WEIGHT_UPDATE': return { ...state, weight: action.weight, weightStable: action.stable, rawAdc: action.rawAdc, deviceId: action.deviceId, deviceOnline: true, }; case 'TAG_MATCHED': return { ...state, matchedSpool: action.spool, unknownTagUid: null, unknownTrayUuid: null, deviceId: action.deviceId, }; case 'UNKNOWN_TAG': return { ...state, matchedSpool: null, unknownTagUid: action.tagUid, unknownTrayUuid: action.trayUuid ?? null, deviceId: action.deviceId, }; case 'TAG_REMOVED': return { ...state, matchedSpool: null, unknownTagUid: null, unknownTrayUuid: null, }; case 'DEVICE_ONLINE': return { ...state, deviceOnline: true, deviceId: action.deviceId, }; case 'DEVICE_OFFLINE': return { ...state, deviceOnline: false, weight: null, weightStable: false, rawAdc: null, }; default: return state; } } export function useSpoolBuddyState() { const [state, dispatch] = useReducer(reducer, initialState); const handleWeight = useCallback((e: Event) => { const detail = (e as CustomEvent).detail; dispatch({ type: 'WEIGHT_UPDATE', weight: detail.weight_grams ?? detail.data?.weight_grams, stable: detail.stable ?? detail.data?.stable ?? false, rawAdc: detail.raw_adc ?? detail.data?.raw_adc ?? null, deviceId: detail.device_id ?? detail.data?.device_id ?? '', }); }, []); const handleTagMatched = useCallback((e: Event) => { const detail = (e as CustomEvent).detail; const spool = detail.spool ?? detail.data?.spool; if (spool) { dispatch({ type: 'TAG_MATCHED', spool: { id: spool.id, tag_uid: detail.tag_uid ?? detail.data?.tag_uid ?? '', material: spool.material ?? '', subtype: spool.subtype ?? null, color_name: spool.color_name ?? null, rgba: spool.rgba ?? null, brand: spool.brand ?? null, label_weight: spool.label_weight ?? 0, core_weight: spool.core_weight ?? 0, weight_used: spool.weight_used ?? 0, }, deviceId: detail.device_id ?? detail.data?.device_id ?? '', }); } }, []); const handleUnknownTag = useCallback((e: Event) => { const detail = (e as CustomEvent).detail; dispatch({ type: 'UNKNOWN_TAG', tagUid: detail.tag_uid ?? detail.data?.tag_uid ?? '', trayUuid: detail.tray_uuid ?? detail.data?.tray_uuid ?? null, deviceId: detail.device_id ?? detail.data?.device_id ?? '', }); }, []); const handleTagRemoved = useCallback((e: Event) => { const detail = (e as CustomEvent).detail; dispatch({ type: 'TAG_REMOVED', deviceId: detail.device_id ?? detail.data?.device_id ?? '', }); }, []); const handleOnline = useCallback((e: Event) => { const detail = (e as CustomEvent).detail; dispatch({ type: 'DEVICE_ONLINE', deviceId: detail.device_id ?? detail.data?.device_id ?? '', }); }, []); const handleOffline = useCallback((e: Event) => { const detail = (e as CustomEvent).detail; dispatch({ type: 'DEVICE_OFFLINE', deviceId: detail.device_id ?? detail.data?.device_id ?? '', }); }, []); useEffect(() => { window.addEventListener('spoolbuddy-weight', handleWeight); window.addEventListener('spoolbuddy-tag-matched', handleTagMatched); window.addEventListener('spoolbuddy-unknown-tag', handleUnknownTag); window.addEventListener('spoolbuddy-tag-removed', handleTagRemoved); window.addEventListener('spoolbuddy-online', handleOnline); window.addEventListener('spoolbuddy-offline', handleOffline); return () => { window.removeEventListener('spoolbuddy-weight', handleWeight); window.removeEventListener('spoolbuddy-tag-matched', handleTagMatched); window.removeEventListener('spoolbuddy-unknown-tag', handleUnknownTag); window.removeEventListener('spoolbuddy-tag-removed', handleTagRemoved); window.removeEventListener('spoolbuddy-online', handleOnline); window.removeEventListener('spoolbuddy-offline', handleOffline); }; }, [handleWeight, handleTagMatched, handleUnknownTag, handleTagRemoved, handleOnline, handleOffline]); const remainingWeight = state.matchedSpool ? Math.max(0, state.matchedSpool.label_weight - state.matchedSpool.weight_used) : null; const netWeight = state.weight !== null && state.matchedSpool ? Math.max(0, state.weight - state.matchedSpool.core_weight) : null; return { ...state, remainingWeight, netWeight, }; }