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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user