fix: bulk edit — name the confirmation fields properly, add the missing ones (#3)

"Upload" was wrong for half of them: a paper QSL is not uploaded. Every entry
now says whether it sets a STATUS or a DATE, and in which direction — "QRZ.com
sent status", "LoTW received date" — which is also how the ADIF fields read.

Added: the QRZ.com received status (the column existed, it was simply never
offered) and the ten confirmation DATES — QSL / LoTW / eQSL / QRZ.com in both
directions, Club Log and HRDLog outbound only, those two having no return
channel. Dates use the same picker as the QSL Info tab, with a Today (UTC)
button; clearing the field writes an empty value, which is how a wrong date is
removed from a batch.

Fields are grouped by CHANNEL now, each status followed by its date, in the same
order as the QSL Info tab — a longer list, but nothing to hunt for.

The repository whitelist is extended to match: without it the new columns would
have been refused at write time. The old ids (qrz_upload, clublog_upload,
hrdlog_upload) still resolve, in case anything still holds one.
This commit is contained in:
2026-07-27 21:21:09 +02:00
parent 816a727e88
commit 4eec272c0b
5 changed files with 97 additions and 28 deletions
+50 -12
View File
@@ -12,7 +12,7 @@ import {
} from '@/components/ui/select';
import { useI18n } from '@/lib/i18n';
type FieldKind = 'status' | 'text' | 'freq';
type FieldKind = 'status' | 'text' | 'freq' | 'date';
type FieldDef = { id: string; label: string; group: string; kind: FieldKind; upper?: boolean };
// Fields a bulk edit may target. status → Y/N/R/I dropdown; text → free input.
@@ -21,16 +21,31 @@ type FieldDef = { id: string; label: string; group: string; kind: FieldKind; upp
// label holds an i18n key (resolved with t() at render time).
const FIELDS: FieldDef[] = [
// QSL / upload status
{ id: 'lotw_sent', label: 'bulk.fLotwSent', group: 'QSL / upload', kind: 'status' },
{ id: 'lotw_rcvd', label: 'bulk.fLotwRcvd', group: 'QSL / upload', kind: 'status' },
{ id: 'eqsl_sent', label: 'bulk.fEqslSent', group: 'QSL / upload', kind: 'status' },
{ id: 'eqsl_rcvd', label: 'bulk.fEqslRcvd', group: 'QSL / upload', kind: 'status' },
{ id: 'qsl_sent', label: 'bulk.fQslSent', group: 'QSL / upload', kind: 'status' },
{ id: 'qsl_rcvd', label: 'bulk.fQslRcvd', group: 'QSL / upload', kind: 'status' },
{ id: 'qrz_upload', label: 'bulk.fQrzUpload', group: 'QSL / upload', kind: 'status' },
{ id: 'clublog_upload', label: 'bulk.fClublogUpload', group: 'QSL / upload', kind: 'status' },
{ id: 'hrdlog_upload', label: 'bulk.fHrdlogUpload', group: 'QSL / upload', kind: 'status' },
{ id: 'qsl_via', label: 'bulk.fQslVia', group: 'QSL / upload', kind: 'text' },
// One channel at a time, each with its status and its date, in the same order
// as the QSL Info tab. "Upload" was the wrong word for half of them — a paper
// QSL is not uploaded — so every entry now reads "<channel> sent/received
// status" or "… date", which is also what the ADIF field is called.
{ id: 'qsl_sent', label: 'bulk.fQslSent', group: 'QSL / upload', kind: 'status' },
{ id: 'qsl_sent_date', label: 'bulk.fQslSentDate', group: 'QSL / upload', kind: 'date' },
{ id: 'qsl_rcvd', label: 'bulk.fQslRcvd', group: 'QSL / upload', kind: 'status' },
{ id: 'qsl_rcvd_date', label: 'bulk.fQslRcvdDate', group: 'QSL / upload', kind: 'date' },
{ id: 'qsl_via', label: 'bulk.fQslVia', group: 'QSL / upload', kind: 'text' },
{ id: 'lotw_sent', label: 'bulk.fLotwSent', group: 'QSL / upload', kind: 'status' },
{ id: 'lotw_sent_date', label: 'bulk.fLotwSentDate', group: 'QSL / upload', kind: 'date' },
{ id: 'lotw_rcvd', label: 'bulk.fLotwRcvd', group: 'QSL / upload', kind: 'status' },
{ id: 'lotw_rcvd_date', label: 'bulk.fLotwRcvdDate', group: 'QSL / upload', kind: 'date' },
{ id: 'eqsl_sent', label: 'bulk.fEqslSent', group: 'QSL / upload', kind: 'status' },
{ id: 'eqsl_sent_date', label: 'bulk.fEqslSentDate', group: 'QSL / upload', kind: 'date' },
{ id: 'eqsl_rcvd', label: 'bulk.fEqslRcvd', group: 'QSL / upload', kind: 'status' },
{ id: 'eqsl_rcvd_date', label: 'bulk.fEqslRcvdDate', group: 'QSL / upload', kind: 'date' },
{ id: 'qrz_sent', label: 'bulk.fQrzSent', group: 'QSL / upload', kind: 'status' },
{ id: 'qrz_sent_date', label: 'bulk.fQrzSentDate', group: 'QSL / upload', kind: 'date' },
{ id: 'qrz_rcvd', label: 'bulk.fQrzRcvd', group: 'QSL / upload', kind: 'status' },
{ id: 'qrz_rcvd_date', label: 'bulk.fQrzRcvdDate', group: 'QSL / upload', kind: 'date' },
{ id: 'clublog_sent', label: 'bulk.fClublogSent', group: 'QSL / upload', kind: 'status' },
{ id: 'clublog_sent_date', label: 'bulk.fClublogSentDate', group: 'QSL / upload', kind: 'date' },
{ id: 'hrdlog_sent', label: 'bulk.fHrdlogSent', group: 'QSL / upload', kind: 'status' },
{ id: 'hrdlog_sent_date', label: 'bulk.fHrdlogSentDate', group: 'QSL / upload', kind: 'date' },
// My station / operator
{ id: 'station_callsign', label: 'bulk.fStationCall', group: 'My station', kind: 'text', upper: true },
{ id: 'operator', label: 'bulk.fOperator', group: 'My station', kind: 'text', upper: true },
@@ -117,7 +132,7 @@ type Props = {
// so they become eligible for upload.
export function BulkEditModal({ open, ids, onClose, onApplied }: Props) {
const { t } = useI18n();
const [field, setField] = useState('hrdlog_upload');
const [field, setField] = useState('qsl_sent');
const [statusValue, setStatusValue] = useState('R');
const [textValue, setTextValue] = useState('');
const [busy, setBusy] = useState(false);
@@ -181,6 +196,29 @@ export function BulkEditModal({ open, ids, onClose, onApplied }: Props) {
{STATUS_VALUES.map((v) => <SelectItem key={v.v} value={v.v}>{t(v.label)}</SelectItem>)}
</SelectContent>
</Select>
) : def.kind === 'date' ? (
// ADIF stores YYYYMMDD; the picker speaks YYYY-MM-DD. Clearing it
// writes an empty value — which is how a wrong date is removed
// from a batch, the same meaning as leaving a text field blank.
<div className="flex gap-1">
<Input
type="date"
className="h-8 text-xs font-mono"
value={/^\d{8}$/.test(textValue) ? `${textValue.slice(0, 4)}-${textValue.slice(4, 6)}-${textValue.slice(6, 8)}` : ''}
onChange={(e) => setTextValue(e.target.value ? e.target.value.replace(/-/g, '') : '')}
/>
<Button
type="button" variant="outline" size="sm" className="h-8 shrink-0 px-2"
title={t('bulk.todayUtc')}
onClick={() => {
const d = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
// UTC: the whole log is UTC, and near midnight the local day
// is the wrong one.
setTextValue(`${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}`);
}}
>{t('bulk.today')}</Button>
</div>
) : (
<Input
className="h-8 text-xs"
File diff suppressed because one or more lines are too long