From 4eec272c0b9ec8d763190990931f6a068f71c57a Mon Sep 17 00:00:00 2001 From: rouggy Date: Mon, 27 Jul 2026 21:21:09 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20bulk=20edit=20=E2=80=94=20name=20the=20c?= =?UTF-8?q?onfirmation=20fields=20properly,=20add=20the=20missing=20ones?= =?UTF-8?q?=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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. --- app.go | 31 +++++++++--- changelog.json | 6 ++- frontend/src/components/BulkEditModal.tsx | 62 ++++++++++++++++++----- frontend/src/lib/i18n.tsx | 4 +- internal/qso/qso.go | 22 ++++++-- 5 files changed, 97 insertions(+), 28 deletions(-) diff --git a/app.go b/app.go index 05074ef..1ba15dc 100644 --- a/app.go +++ b/app.go @@ -5562,16 +5562,33 @@ func (a *App) BulkUpdateQSL(ids []int64, u QSLBulkUpdate) (int, error) { // layer so the frontend works with stable ids, not raw column names. var bulkFieldColumns = map[string]string{ // QSL / upload status - "lotw_sent": "lotw_sent", - "lotw_rcvd": "lotw_rcvd", - "eqsl_sent": "eqsl_sent", - "eqsl_rcvd": "eqsl_rcvd", - "qsl_sent": "qsl_sent", - "qsl_rcvd": "qsl_rcvd", - "qsl_via": "qsl_via", + "lotw_sent": "lotw_sent", + "lotw_rcvd": "lotw_rcvd", + "eqsl_sent": "eqsl_sent", + "eqsl_rcvd": "eqsl_rcvd", + "qsl_sent": "qsl_sent", + "qsl_rcvd": "qsl_rcvd", + "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", "clublog_upload": "clublog_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 "station_callsign": "station_callsign", "operator": "operator", diff --git a/changelog.json b/changelog.json index 8cae551..26d5397 100644 --- a/changelog.json +++ b/changelog.json @@ -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.", "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).", - "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": [ "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.", "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).", - "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." ] }, { diff --git a/frontend/src/components/BulkEditModal.tsx b/frontend/src/components/BulkEditModal.tsx index db97240..a00fffe 100644 --- a/frontend/src/components/BulkEditModal.tsx +++ b/frontend/src/components/BulkEditModal.tsx @@ -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 " 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) => {t(v.label)})} + ) : 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. +
+ setTextValue(e.target.value ? e.target.value.replace(/-/g, '') : '')} + /> + +
) : (