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:
@@ -5569,9 +5569,26 @@ var bulkFieldColumns = map[string]string{
|
|||||||
"qsl_sent": "qsl_sent",
|
"qsl_sent": "qsl_sent",
|
||||||
"qsl_rcvd": "qsl_rcvd",
|
"qsl_rcvd": "qsl_rcvd",
|
||||||
"qsl_via": "qsl_via",
|
"qsl_via": "qsl_via",
|
||||||
|
"qrz_sent": "qrzcom_qso_upload_status",
|
||||||
|
"qrz_rcvd": "qrzcom_qso_download_status",
|
||||||
|
"clublog_sent": "clublog_qso_upload_status",
|
||||||
|
"hrdlog_sent": "hrdlog_qso_upload_status",
|
||||||
|
// Old ids kept so anything holding one keeps working.
|
||||||
"qrz_upload": "qrzcom_qso_upload_status",
|
"qrz_upload": "qrzcom_qso_upload_status",
|
||||||
"clublog_upload": "clublog_qso_upload_status",
|
"clublog_upload": "clublog_qso_upload_status",
|
||||||
"hrdlog_upload": "hrdlog_qso_upload_status",
|
"hrdlog_upload": "hrdlog_qso_upload_status",
|
||||||
|
// Confirmation DATES. A status without its date is half the record: an
|
||||||
|
// operator correcting a batch after an import needs both.
|
||||||
|
"qsl_sent_date": "qsl_sent_date",
|
||||||
|
"qsl_rcvd_date": "qsl_rcvd_date",
|
||||||
|
"lotw_sent_date": "lotw_sent_date",
|
||||||
|
"lotw_rcvd_date": "lotw_rcvd_date",
|
||||||
|
"eqsl_sent_date": "eqsl_sent_date",
|
||||||
|
"eqsl_rcvd_date": "eqsl_rcvd_date",
|
||||||
|
"qrz_sent_date": "qrzcom_qso_upload_date",
|
||||||
|
"qrz_rcvd_date": "qrzcom_qso_download_date",
|
||||||
|
"clublog_sent_date": "clublog_qso_upload_date",
|
||||||
|
"hrdlog_sent_date": "hrdlog_qso_upload_date",
|
||||||
// My station / operator
|
// My station / operator
|
||||||
"station_callsign": "station_callsign",
|
"station_callsign": "station_callsign",
|
||||||
"operator": "operator",
|
"operator": "operator",
|
||||||
|
|||||||
+4
-2
@@ -12,7 +12,8 @@
|
|||||||
"The Windows title bar is gone: minimise, maximise and close now sit in the OpsLog bar, which you drag to move the window (double-click to maximise). That is 32 pixels of screen back.",
|
"The Windows title bar is gone: minimise, maximise and close now sit in the OpsLog bar, which you drag to move the window (double-click to maximise). That is 32 pixels of screen back.",
|
||||||
"Saving the settings no longer freezes OpsLog while a device is slow to answer. Choosing OmniRig while another program held the rig locked the window for about 45 seconds — the time OmniRig takes to give up. The link is now re-established in the background, and a slow restart is written to the diagnostic log.",
|
"Saving the settings no longer freezes OpsLog while a device is slow to answer. Choosing OmniRig while another program held the rig locked the window for about 45 seconds — the time OmniRig takes to give up. The link is now re-established in the background, and a slow restart is written to the diagnostic log.",
|
||||||
"Edit QSO → QSL Info: the sent and received dates now have a calendar picker, plus a button for today's date (UTC).",
|
"Edit QSO → QSL Info: the sent and received dates now have a calendar picker, plus a button for today's date (UTC).",
|
||||||
"CW keyer: the diagnostic log now names the keyer generation (WK1 / WK2 / WK3), and Settings → CW keyer can log every byte exchanged with it — for reporting a keyer that behaves oddly."
|
"CW keyer: the diagnostic log now names the keyer generation (WK1 / WK2 / WK3), and Settings → CW keyer can log every byte exchanged with it — for reporting a keyer that behaves oddly.",
|
||||||
|
"Bulk edit: the confirmation fields are renamed \"sent/received status\" instead of \"upload\", the missing QRZ.com received status is added, and every channel now offers its sent and received DATE with a calendar."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Statistiques : le graphique d'activité suit désormais la période choisie, et Jour / Semaine / Mois / Année en choisissent le pas (les QSO par semaine sur toute la période, par exemple). Il affichait auparavant des fenêtres fixes — les 7 derniers jours, les 30 derniers — quelle que soit la période.",
|
"Statistiques : le graphique d'activité suit désormais la période choisie, et Jour / Semaine / Mois / Année en choisissent le pas (les QSO par semaine sur toute la période, par exemple). Il affichait auparavant des fenêtres fixes — les 7 derniers jours, les 30 derniers — quelle que soit la période.",
|
||||||
@@ -24,7 +25,8 @@
|
|||||||
"La barre de titre Windows a disparu : réduire, agrandir et fermer sont désormais dans la barre OpsLog, qu'on saisit pour déplacer la fenêtre (double-clic pour agrandir). Autant de pixels d'écran repris.",
|
"La barre de titre Windows a disparu : réduire, agrandir et fermer sont désormais dans la barre OpsLog, qu'on saisit pour déplacer la fenêtre (double-clic pour agrandir). Autant de pixels d'écran repris.",
|
||||||
"Enregistrer les réglages ne fige plus OpsLog quand un appareil tarde à répondre. Choisir OmniRig alors qu'un autre logiciel tenait la radio bloquait la fenêtre une quarantaine de secondes — le temps qu'OmniRig renonce. La liaison est désormais rétablie en arrière-plan, et un redémarrage lent est noté dans le journal de diagnostic.",
|
"Enregistrer les réglages ne fige plus OpsLog quand un appareil tarde à répondre. Choisir OmniRig alors qu'un autre logiciel tenait la radio bloquait la fenêtre une quarantaine de secondes — le temps qu'OmniRig renonce. La liaison est désormais rétablie en arrière-plan, et un redémarrage lent est noté dans le journal de diagnostic.",
|
||||||
"Édition QSO → Infos QSL : les dates d'envoi et de réception disposent d'un calendrier, et d'un bouton pour la date du jour (UTC).",
|
"Édition QSO → Infos QSL : les dates d'envoi et de réception disposent d'un calendrier, et d'un bouton pour la date du jour (UTC).",
|
||||||
"Keyer CW : le journal de diagnostic nomme désormais la génération du keyer (WK1 / WK2 / WK3), et Réglages → Keyer CW permet de journaliser chaque octet échangé avec lui — pour signaler un keyer au comportement anormal."
|
"Keyer CW : le journal de diagnostic nomme désormais la génération du keyer (WK1 / WK2 / WK3), et Réglages → Keyer CW permet de journaliser chaque octet échangé avec lui — pour signaler un keyer au comportement anormal.",
|
||||||
|
"Édition en lot : les champs de confirmation s'appellent désormais « envoi/réception (statut) » au lieu d'« upload », le statut de réception QRZ.com manquant a été ajouté, et chaque canal propose ses DATES d'envoi et de réception avec un calendrier."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useI18n } from '@/lib/i18n';
|
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 };
|
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.
|
// 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).
|
// label holds an i18n key (resolved with t() at render time).
|
||||||
const FIELDS: FieldDef[] = [
|
const FIELDS: FieldDef[] = [
|
||||||
// QSL / upload status
|
// QSL / upload status
|
||||||
{ id: 'lotw_sent', label: 'bulk.fLotwSent', group: 'QSL / upload', kind: 'status' },
|
// One channel at a time, each with its status and its date, in the same order
|
||||||
{ id: 'lotw_rcvd', label: 'bulk.fLotwRcvd', group: 'QSL / upload', kind: 'status' },
|
// as the QSL Info tab. "Upload" was the wrong word for half of them — a paper
|
||||||
{ id: 'eqsl_sent', label: 'bulk.fEqslSent', group: 'QSL / upload', kind: 'status' },
|
// QSL is not uploaded — so every entry now reads "<channel> sent/received
|
||||||
{ id: 'eqsl_rcvd', label: 'bulk.fEqslRcvd', group: 'QSL / upload', kind: 'status' },
|
// 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', 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', label: 'bulk.fQslRcvd', group: 'QSL / upload', kind: 'status' },
|
||||||
{ id: 'qrz_upload', label: 'bulk.fQrzUpload', group: 'QSL / upload', kind: 'status' },
|
{ id: 'qsl_rcvd_date', label: 'bulk.fQslRcvdDate', group: 'QSL / upload', kind: 'date' },
|
||||||
{ 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' },
|
{ 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
|
// My station / operator
|
||||||
{ id: 'station_callsign', label: 'bulk.fStationCall', group: 'My station', kind: 'text', upper: true },
|
{ 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 },
|
{ id: 'operator', label: 'bulk.fOperator', group: 'My station', kind: 'text', upper: true },
|
||||||
@@ -117,7 +132,7 @@ type Props = {
|
|||||||
// so they become eligible for upload.
|
// so they become eligible for upload.
|
||||||
export function BulkEditModal({ open, ids, onClose, onApplied }: Props) {
|
export function BulkEditModal({ open, ids, onClose, onApplied }: Props) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [field, setField] = useState('hrdlog_upload');
|
const [field, setField] = useState('qsl_sent');
|
||||||
const [statusValue, setStatusValue] = useState('R');
|
const [statusValue, setStatusValue] = useState('R');
|
||||||
const [textValue, setTextValue] = useState('');
|
const [textValue, setTextValue] = useState('');
|
||||||
const [busy, setBusy] = useState(false);
|
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>)}
|
{STATUS_VALUES.map((v) => <SelectItem key={v.v} value={v.v}>{t(v.label)}</SelectItem>)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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
|
<Input
|
||||||
className="h-8 text-xs"
|
className="h-8 text-xs"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -539,8 +539,20 @@ var uploadStatusCols = map[string]bool{
|
|||||||
"lotw_sent": true,
|
"lotw_sent": true,
|
||||||
"eqsl_sent": true,
|
"eqsl_sent": true,
|
||||||
"qrzcom_qso_upload_status": true,
|
"qrzcom_qso_upload_status": true,
|
||||||
|
"qrzcom_qso_download_status": true,
|
||||||
"clublog_qso_upload_status": true,
|
"clublog_qso_upload_status": true,
|
||||||
"hrdlog_qso_upload_status": true,
|
"hrdlog_qso_upload_status": true,
|
||||||
|
// Confirmation dates (ADIF YYYYMMDD strings, so plain TEXT like the rest).
|
||||||
|
"qsl_sent_date": true,
|
||||||
|
"qsl_rcvd_date": true,
|
||||||
|
"lotw_sent_date": true,
|
||||||
|
"lotw_rcvd_date": true,
|
||||||
|
"eqsl_sent_date": true,
|
||||||
|
"eqsl_rcvd_date": true,
|
||||||
|
"qrzcom_qso_upload_date": true,
|
||||||
|
"qrzcom_qso_download_date": true,
|
||||||
|
"clublog_qso_upload_date": true,
|
||||||
|
"hrdlog_qso_upload_date": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListForUpload returns QSOs whose per-service sent-status column equals
|
// ListForUpload returns QSOs whose per-service sent-status column equals
|
||||||
|
|||||||
Reference in New Issue
Block a user