import { useMemo } from 'react'; import { Star, Radio } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; import type { WorkedBeforeView } from '@/types'; type WorkedBefore = WorkedBeforeView; interface Props { wb: WorkedBefore | null; busy: boolean; currentBand: string; currentMode: string; bands?: string[]; // operator's configured bands; falls back to DEFAULT_BANDS hasCall?: boolean; // a callsign is being entered — only then highlight the "current entry" cell } // Compact column label for a band tag: keep the classic V/U for 2m/70cm, // strip the trailing "m" for meter bands (160m→160), and shorten cm bands // (13cm→13c) so the column stays narrow. function bandColLabel(tag: string): string { if (tag === '2m') return 'V'; if (tag === '70cm') return 'U'; if (tag.endsWith('cm')) return tag.replace('cm', 'c'); return tag.replace(/m$/, ''); } // Default 13-column band layout, used when the operator hasn't configured bands. const DEFAULT_BANDS: { tag: string; label: string }[] = [ { tag: '160m', label: '160' }, { tag: '80m', label: '80' }, { tag: '60m', label: '60' }, { tag: '40m', label: '40' }, { tag: '30m', label: '30' }, { tag: '20m', label: '20' }, { tag: '17m', label: '17' }, { tag: '15m', label: '15' }, { tag: '12m', label: '12' }, { tag: '10m', label: '10' }, { tag: '6m', label: '6' }, { tag: '2m', label: 'V' }, { tag: '70cm', label: 'U' }, ]; const CLASSES = ['PH', 'CW', 'DIG'] as const; const PHONE_MODES = new Set(['SSB','USB','LSB','AM','FM','DIGITALVOICE','PHONE']); function classMatchesMode(cls: string, mode: string): boolean { const u = (mode || '').toUpperCase(); if (cls === 'PH') return PHONE_MODES.has(u); if (cls === 'CW') return u === 'CW'; return u !== '' && u !== 'CW' && !PHONE_MODES.has(u); } const STATUS_CLASSES: Record = { call_c: 'bg-emerald-700 hover:bg-emerald-600', call_w: 'bg-emerald-300 hover:bg-emerald-200', dxcc_c: 'bg-indigo-800 hover:bg-indigo-700', dxcc_w: 'bg-indigo-300 hover:bg-indigo-200', }; // Legend entries, in the same colour order as the cells. swatch = the // background class (or a special ring marker for the current-entry cell). const LEGEND: { swatch: string; ring?: boolean; label: string }[] = [ { swatch: 'bg-emerald-700', label: 'Call confirmed' }, { swatch: 'bg-emerald-300', label: 'Call worked' }, { swatch: 'bg-indigo-800', label: 'Entity confirmed' }, { swatch: 'bg-indigo-300', label: 'Entity worked' }, { swatch: 'bg-stone-200', label: 'Not worked' }, { swatch: 'bg-stone-200', ring: true, label: 'Current entry' }, ]; function cellTitle(band: string, cls: string, status: string, current: boolean): string { const desc = status === 'call_c' ? 'This callsign confirmed' : status === 'call_w' ? 'This callsign worked (not confirmed)' : status === 'dxcc_c' ? 'Entity confirmed (other callsign)' : status === 'dxcc_w' ? 'Entity worked (other callsign)' : 'Never worked'; return `${band} ${cls}: ${desc}${current ? ' — current entry' : ''}`; } export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, hasCall = true }: Props) { // Columns from the operator's configured bands (so the matrix shows only the // bands they actually use), falling back to the built-in default set. const cols = useMemo( () => (bands && bands.length ? bands.map((tag) => ({ tag, label: bandColLabel(tag) })) : DEFAULT_BANDS), [bands], ); const dxcc = wb?.dxcc ?? 0; const dxccName = wb?.dxcc_name ?? ''; const dxccCount = wb?.dxcc_count ?? 0; const callCount = wb?.count ?? 0; // QSOs with this exact callsign const hasDxcc = dxcc > 0; const newOne = hasDxcc && dxccCount === 0; const statusMap = useMemo(() => { const m = new Map(); for (const s of wb?.band_status ?? []) { m.set(`${s.band}|${s.class}`, s.status); } return m; }, [wb]); // "Newness" of the current band+mode entry, for the award/DX-chase badges. // Derived straight from the entity's real band_status (all bands it was // worked on — not just the operator's configured column list). // Newness uses the ACTUAL mode (FT8 / FT4 / RTTY…), not the PH/CW/DIG class: // DIG is a group, so FT4 after FT8 is genuinely a new mode. dxcc_band_modes // lists every real (band, mode) the entity was worked on. const bandModes = (wb?.dxcc_band_modes ?? []) as { band: string; mode: string }[]; const curMode = (currentMode || '').toUpperCase().trim(); const bandWorked = bandModes.some((bm) => bm.band === currentBand); // entity worked on this band (any mode) const modeWorked = !!curMode && bandModes.some((bm) => (bm.mode || '').toUpperCase() === curMode); // …in this exact mode (any band) const slotWorked = !!curMode && bandModes.some((bm) => bm.band === currentBand && (bm.mode || '').toUpperCase() === curMode); // Mutually-exclusive badges, shown only when the entity is worked but this // exact band+mode is NOT yet: // New Band & Mode = both the band AND the mode are new for this entity. // New Band = the band is new (the mode was worked on another band). // New Mode = the mode is new (the band was worked in another mode). // New Slot = both band and mode already worked — just not together. const slotNew = hasDxcc && !newOne && !!curMode && !slotWorked; const newBandMode = slotNew && !bandWorked && !modeWorked; const newBand = slotNew && !bandWorked && modeWorked; const newMode = slotNew && bandWorked && !modeWorked; const newSlot = slotNew && bandWorked && modeWorked; return (
{newOne ? ( <> NEW ONE {dxccName || `DXCC #${dxcc}`} {' '}· never worked this entity ) : hasDxcc ? ( <> {dxccName || `DXCC #${dxcc}`} {dxccCount}{' '} QSO{dxccCount > 1 ? 's' : ''} with this entity {callCount > 0 && ( <> {' · '} {callCount}{' '} with this call )} {(newBand || newMode || newBandMode || newSlot) && (
{newBandMode && New Band & Mode} {newBand && New Band} {newMode && New Mode} {newSlot && New Slot}
)} ) : busy ? ( looking up… ) : ( Type a callsign to see entity stats )}
))} {CLASSES.map((cls) => { const classCurrent = classMatchesMode(cls, currentMode); return ( {cols.map((b) => { const st = statusMap.get(`${b.tag}|${cls}`) ?? ''; const isCurrent = hasCall && b.tag === currentBand && classCurrent; return ( ); })}
{cols.map((b) => ( {b.label}
{cls} ); })}
{/* Colour legend — sits in the spare room under the matrix. */}
{LEGEND.map((l) => ( {l.label} ))}
); }