fix(awards): apply the per-reference validity window
References are not forever: a park is delisted, a district merged, a castle loses its number. A QSO made while the reference existed is a valid contact and must keep counting; one made afterwards must not. The JSON format did not need to change — ValidFrom/ValidTo were already on awardref.Ref, already columns in award_references, already round-tripping through export and import. They were simply never read: awardRefMetas dropped them on the way into the engine, so nothing downstream could enforce them. This carries them through and checks them in keepRefs. Empty means the award's own window governs, which inScope already enforces for every QSO in the award. That fallback is deliberately NOT duplicated per reference — two places enforcing the same dates is two places for them to disagree. The editor shows the award's dates as the hint under the boxes so the operator can see what empty inherits. Dates are compared as ISO strings rather than parsed times: the stored shape is "2006-01-02", lexical order on it IS chronological, and this cannot fail on a malformed value the way a parse can — a reference with a typo in its window keeps counting instead of silently vanishing from an operator's totals. The Explain trace names the date and the cutoff, because "did not count" on a contact the operator remembers making is exactly when they need to be told it is the reference that has a window, not their log that is wrong.
This commit is contained in:
@@ -778,6 +778,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
|
||||
<TabsContent value="refs" className="mt-0">
|
||||
<ReferencesPanel
|
||||
code={cur.code.trim().toUpperCase()} presets={presets} meta={meta[cur.code.toUpperCase()]}
|
||||
awardValidFrom={cur.valid_from} awardValidTo={cur.valid_to}
|
||||
onUpdateOnline={() => updateList(cur.code.toUpperCase())} updating={updating === cur.code.toUpperCase()}
|
||||
onChanged={loadMeta} setErr={setErr}
|
||||
/>
|
||||
@@ -899,8 +900,8 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
|
||||
// ReferencesPanel — manage the reference list of one award: search/list on the
|
||||
// left, a per-reference editor on the right, plus bulk paste/CSV, presets and
|
||||
// the online updater (POTA/SOTA/WWFF).
|
||||
function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChanged, setErr }: {
|
||||
code: string; presets: Preset[]; meta?: RefMeta;
|
||||
function ReferencesPanel({ code, presets, meta, awardValidFrom, awardValidTo, onUpdateOnline, updating, onChanged, setErr }: {
|
||||
code: string; presets: Preset[]; meta?: RefMeta; awardValidFrom?: string; awardValidTo?: string;
|
||||
onUpdateOnline: () => void; updating: boolean; onChanged: () => void; setErr: (s: string) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
@@ -1056,6 +1057,28 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
|
||||
third-party list may carry the values — but they are not offered
|
||||
for editing until something actually reads them. */}
|
||||
<Field2 label={t('awed.grid')}><Input className="h-8 font-mono" value={sel.gridsquare ?? ''} onChange={(e) => patchSel({ gridsquare: e.target.value })} /></Field2>
|
||||
{/* This reference's own validity window. A reference is not forever:
|
||||
a park is delisted, a district merged. A QSO made while it
|
||||
existed still counts — it was a valid contact on the day — and
|
||||
one made afterwards does not.
|
||||
Left empty the award's own dates govern, which is why they show
|
||||
as the placeholder: the operator can see what "empty" inherits
|
||||
instead of having to remember. */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Field2 label={t('awed.refValidFrom')}>
|
||||
<Input type="date" className="h-8" value={sel.valid_from ?? ''}
|
||||
onChange={(e) => patchSel({ valid_from: e.target.value })} />
|
||||
</Field2>
|
||||
<Field2 label={t('awed.refValidTo')}>
|
||||
<Input type="date" className="h-8" value={sel.valid_to ?? ''}
|
||||
onChange={(e) => patchSel({ valid_to: e.target.value })} />
|
||||
</Field2>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(awardValidFrom || awardValidTo)
|
||||
? t('awed.refValidHintAward', { from: awardValidFrom || '—', to: awardValidTo || '—' })
|
||||
: t('awed.refValidHint')}
|
||||
</p>
|
||||
<div className="flex justify-end pt-1"><Button size="sm" className="h-7" onClick={() => sel && saveRef(sel)}><Save className="size-3.5 mr-1" /> {t('awed.saveReference')}</Button></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user