feat(qsl): set the QSL route per direction from the QSL Manager

The paper form had one "Via", which since the QSL_VIA split writes
QSL_SENT_VIA. QSL_RCVD_VIA had no way in at all outside the per-QSO editor —
a field with a column, an import and an export, and nothing to fill it.

Each direction now has its own route dropdown, sitting with the status and
date it belongs to: how the card was sent, beside Sent; how it came back,
beside Received. Both apply to the whole selection, which is the shape of the
job — a stack confirmed in one go usually arrived the same way, a bureau
delivery being exactly that, and doing it one QSO at a time in the editor was
the only option before.

Both store the ADIF enumeration (B/D/E), and neither touches QSL_VIA — that
holds the manager's callsign and belongs to the QSO, not to a stack of cards
being confirmed together.

The two columns were already available in the paper list: it renders through
RecentQSOsGrid, which gained them with the field itself.
This commit is contained in:
2026-08-14 16:32:12 +02:00
parent 2cd1274975
commit 7470ebf47e
5 changed files with 48 additions and 24 deletions
+18 -9
View File
@@ -54,7 +54,9 @@ const QSL_STATUSES = [
{ v: 'I', label: 'qslm.ignore' },
];
// QSL routing methods for the paper-QSL "Via" dropdown (was free text).
// QSL routing methods for the paper-QSL route dropdowns, one per direction.
// These are the ADIF QSL Via enumeration (B/D/E) and go to QSL_SENT_VIA and
// QSL_RCVD_VIA — NOT to QSL_VIA, which holds the manager's callsign.
const QSL_VIA_OPTIONS = [
{ v: '_', label: 'qslm.leave' },
{ v: 'B', label: 'qslm.viaBureau' },
@@ -149,7 +151,8 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
const [qslSent, setQslSent] = useState('_');
const [qslRcvdDate, setQslRcvdDate] = useState('');
const [qslSentDate, setQslSentDate] = useState('');
const [qslVia, setQslVia] = useState('_');
const [qslVia, setQslVia] = useState('_'); // how the card was SENT
const [qslRcvdVia, setQslRcvdVia] = useState('_'); // how it CAME BACK
const [qslNotes, setQslNotes] = useState('');
const [qslComment, setQslComment] = useState('');
@@ -181,6 +184,7 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
sent_date: ymd(qslSentDate),
rcvd_date: ymd(qslRcvdDate),
via: qslVia === '_' ? '' : qslVia,
rcvd_via: qslRcvdVia === '_' ? '' : qslRcvdVia,
notes: qslNotes,
comment: qslComment,
} as any);
@@ -553,6 +557,10 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
{/* Paper-QSL apply form */}
{service === 'paper' && (
<div className="flex items-end flex-wrap gap-3 px-3 py-2 border-t border-border bg-muted/20 shrink-0">
{/* Each direction carries its own route, and each sits with the status
and date it belongs to. A stack confirmed in one go usually arrived
the same way — a bureau delivery is exactly that — which is why the
route belongs on this form and not one QSO at a time in the editor. */}
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.qslReceived')}</label>
<div className="flex gap-1.5">
@@ -561,6 +569,10 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
<SelectContent>{QSL_STATUSES.map((s) => <SelectItem key={s.v} value={s.v}>{t(s.label)}</SelectItem>)}</SelectContent>
</Select>
<Input type="date" className="h-8 w-36" value={qslRcvdDate} onChange={(e) => setQslRcvdDate(e.target.value)} title={t('qslm.qslRcvdDateTitle')} />
<Select value={qslRcvdVia} onValueChange={setQslRcvdVia}>
<SelectTrigger className="h-8 w-32" title={t('qslm.rcvdViaTitle')}><SelectValue /></SelectTrigger>
<SelectContent>{QSL_VIA_OPTIONS.map((o) => <SelectItem key={o.v} value={o.v}>{t(o.label)}</SelectItem>)}</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col gap-1">
@@ -571,15 +583,12 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
<SelectContent>{QSL_STATUSES.map((s) => <SelectItem key={s.v} value={s.v}>{t(s.label)}</SelectItem>)}</SelectContent>
</Select>
<Input type="date" className="h-8 w-36" value={qslSentDate} onChange={(e) => setQslSentDate(e.target.value)} title={t('qslm.qslSentDateTitle')} />
<Select value={qslVia} onValueChange={setQslVia}>
<SelectTrigger className="h-8 w-32" title={t('qslm.sentViaTitle')}><SelectValue /></SelectTrigger>
<SelectContent>{QSL_VIA_OPTIONS.map((o) => <SelectItem key={o.v} value={o.v}>{t(o.label)}</SelectItem>)}</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.via')}</label>
<Select value={qslVia} onValueChange={setQslVia}>
<SelectTrigger className="h-8 w-32"><SelectValue /></SelectTrigger>
<SelectContent>{QSL_VIA_OPTIONS.map((o) => <SelectItem key={o.v} value={o.v}>{t(o.label)}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.notes')}</label>
<Input className="h-8 w-40" value={qslNotes} onChange={(e) => setQslNotes(e.target.value)} placeholder={t('qslm.notesPlaceholder')} />
File diff suppressed because one or more lines are too long
+2
View File
@@ -2739,6 +2739,7 @@ export namespace main {
sent_date: string;
rcvd_date: string;
via: string;
rcvd_via: string;
notes: string;
comment: string;
@@ -2753,6 +2754,7 @@ export namespace main {
this.sent_date = source["sent_date"];
this.rcvd_date = source["rcvd_date"];
this.via = source["via"];
this.rcvd_via = source["rcvd_via"];
this.notes = source["notes"];
this.comment = source["comment"];
}