import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Check, AlertTriangle, RefreshCw, Unlink } from 'lucide-react'; import type { MatchedSpool } from '../../hooks/useSpoolBuddyState'; import { spoolbuddyApi } from '../../api/client'; import { SpoolIcon } from './SpoolIcon'; // Storage key for default core weight const DEFAULT_CORE_WEIGHT_KEY = 'spoolbuddy-default-core-weight'; function getDefaultCoreWeight(): number { try { const stored = localStorage.getItem(DEFAULT_CORE_WEIGHT_KEY); if (stored) { const weight = parseInt(stored, 10); if (weight >= 0 && weight <= 500) return weight; } } catch { // Ignore errors } return 250; // Default 250g (typical Bambu spool core) } interface SpoolInfoCardProps { spool: MatchedSpool; scaleWeight: number | null; onClose?: () => void; onSyncWeight?: () => void; onAssignToAms?: () => void; isAssigned?: boolean; onUnassignFromAms?: () => void; } export function SpoolInfoCard({ spool, scaleWeight, onClose, onSyncWeight, onAssignToAms, isAssigned, onUnassignFromAms }: SpoolInfoCardProps) { const { t } = useTranslation(); const [syncing, setSyncing] = useState(false); const [synced, setSynced] = useState(false); const colorHex = spool.rgba ? `#${spool.rgba.slice(0, 6)}` : '#808080'; // Use spool's core_weight if set, otherwise fall back to default const coreWeight = (spool.core_weight && spool.core_weight > 0) ? spool.core_weight : getDefaultCoreWeight(); // Gross weight from scale (live) or fallback const grossWeight = scaleWeight !== null ? Math.round(Math.max(0, scaleWeight)) : null; // Remaining filament = gross - core const remaining = grossWeight !== null ? Math.round(Math.max(0, grossWeight - coreWeight)) : null; const labelWeight = Math.round(spool.label_weight || 1000); const fillPercent = remaining !== null ? Math.min(100, Math.round((remaining / labelWeight) * 100)) : null; const fillColor = fillPercent !== null ? fillPercent > 50 ? '#22c55e' : fillPercent > 20 ? '#eab308' : '#ef4444' : '#808080'; // Weight comparison (scale vs calculated expected) const netWeight = Math.max(0, (spool.label_weight || 0) - (spool.weight_used || 0) ); const calculatedWeight = netWeight + coreWeight; const difference = grossWeight !== null ? grossWeight - calculatedWeight : null; const isMatch = difference !== null ? Math.abs(difference) <= 50 : null; const handleSyncWeight = async () => { if (scaleWeight === null) return; setSyncing(true); try { await spoolbuddyApi.updateSpoolWeight(spool.id, Math.round(scaleWeight)); setSynced(true); onSyncWeight?.(); setTimeout(() => setSynced(false), 3000); } catch (e) { console.error('Failed to sync weight:', e); } finally { setSyncing(false); } }; return (
{/* Top section: Spool icon + main info */}
{/* Spool visualization */}
{fillPercent !== null && (
{fillPercent}%
)}
{/* Main info */}

{spool.color_name || 'Unknown color'}

{spool.brand} • {spool.material} {spool.subtype && ` ${spool.subtype}`}

{/* Filament remaining - big number */} {remaining !== null && (
{remaining}g / {labelWeight}g

{t('spoolbuddy.spool.remaining', 'Remaining')}

{/* Fill bar */}
)}
{/* Details grid */}
{t('spoolbuddy.dashboard.grossWeight', 'Gross weight')} {grossWeight !== null ? `${grossWeight}g` : '\u2014'}
{t('spoolbuddy.spool.coreWeight', 'Core')} {coreWeight}g
{t('spoolbuddy.dashboard.spoolSize', 'Spool size')} {labelWeight}g
{t('spoolbuddy.spool.scaleWeight', 'Scale')} {grossWeight !== null ? ( {grossWeight}g {isMatch ? ( ) : ( <> )} ) : ( {'\u2014'} )}
{t('spoolbuddy.dashboard.tagId', 'Tag')} {spool.tag_uid ? spool.tag_uid.slice(-8) : '\u2014'}
{/* Action buttons */}
{onAssignToAms && ( )} {onUnassignFromAms && ( )} {onClose && ( )}
); } interface UnknownTagCardProps { tagUid: string; scaleWeight: number | null; coreWeight?: number; onLinkSpool?: () => void; onAddToInventory?: () => void; onClose?: () => void; } export function UnknownTagCard({ tagUid, scaleWeight, coreWeight, onLinkSpool, onAddToInventory, onClose }: UnknownTagCardProps) { const { t } = useTranslation(); const defaultCoreWeight = coreWeight ?? getDefaultCoreWeight(); const grossWeight = scaleWeight !== null ? Math.round(Math.max(0, scaleWeight)) : null; const estimatedRemaining = grossWeight !== null ? Math.round(Math.max(0, grossWeight - defaultCoreWeight)) : null; return (

{t('spoolbuddy.dashboard.newTag', 'New Tag Detected')}

{tagUid}

{grossWeight !== null && (
{grossWeight}g {t('spoolbuddy.dashboard.onScale', 'on scale')} {estimatedRemaining !== null && estimatedRemaining > 0 && ( • ~{estimatedRemaining}g filament )}
)}
{onAddToInventory && ( )} {onLinkSpool && ( )} {onClose && ( )}
); }