diff --git a/changelog.json b/changelog.json index 43944f3..5807a2c 100644 --- a/changelog.json +++ b/changelog.json @@ -17,7 +17,8 @@ "The four HAMLOG.online fields are filterable too, so 'never sent there' is a filter and not a guess.", "RDA: a read-only comparison of the two district sources — the offline RDA database against the CNTY carried by a log imported from HAMLOG.online — reporting how many agree, how many differ, and listing the disagreements with the contact, both districts, and whether the database answer was a dated record or an assumption.", "The QSO right-click menu can now export the selection to ADIF with the OpsLog fields, so the award references (RDA@KR-04 and the like) travel with the contacts. The existing entry still writes standard ADIF for other loggers.", - "The filtered-view export gets the same treatment: a second right-click entry writes the whole filtered view to ADIF with the OpsLog fields." + "The filtered-view export gets the same treatment: a second right-click entry writes the whole filtered view to ADIF with the OpsLog fields.", + "Edit QSO: HAMLOG.online joins the QSL Info tab — its own channel in the picker, with sent/received and both dates, and a row in the status table. Its four columns also move from the QSL group to Uploads, next to Club Log and QRZ.com." ], "fr": [ "Yaesu : une ligne ANT dans le panneau du poste sélectionne la prise d'antenne, et le choix est mémorisé pour la bande sur laquelle il a été fait — on change de bande, l'antenne suit. Aucune page à configurer : choisir l'antenne une fois sur une bande le dit très bien. Les postes à une seule prise n'affichent jamais la ligne.", @@ -34,7 +35,8 @@ "Les quatre champs HAMLOG.online sont aussi filtrables, pour que « jamais envoyé là-bas » soit un filtre et non une supposition.", "RDA : une comparaison en lecture seule des deux sources de district — la base RDA hors ligne face au CNTY d'un journal importé depuis HAMLOG.online — indiquant combien concordent, combien divergent, et listant les désaccords avec le contact, les deux districts, et si la réponse de la base était un enregistrement daté ou une hypothèse.", "Le menu contextuel des QSO peut maintenant exporter la sélection en ADIF avec les champs OpsLog, pour que les références de diplômes (RDA@KR-04 et compagnie) suivent les contacts. L'entrée existante écrit toujours de l'ADIF standard pour les autres carnets.", - "L'export de la vue filtrée reçoit le même traitement : une seconde entrée du menu contextuel écrit toute la vue filtrée en ADIF avec les champs OpsLog." + "L'export de la vue filtrée reçoit le même traitement : une seconde entrée du menu contextuel écrit toute la vue filtrée en ADIF avec les champs OpsLog.", + "Édition de QSO : HAMLOG.online rejoint l'onglet QSL Info — son propre canal dans la liste, avec envoyé/reçu et les deux dates, et une ligne dans le tableau de statut. Ses quatre colonnes passent aussi du groupe QSL au groupe Uploads, à côté de Club Log et QRZ.com." ] }, { diff --git a/frontend/src/components/QSOEditModal.tsx b/frontend/src/components/QSOEditModal.tsx index 8bf36e6..4178a0e 100644 --- a/frontend/src/components/QSOEditModal.tsx +++ b/frontend/src/components/QSOEditModal.tsx @@ -85,6 +85,19 @@ const CONF_LABEL_KEYS: Record = { // the channel picker and the status table alongside the rest. const OPSLOG_CONF = 'OPSLOG'; +// HAMLOG.online. Out of CONFIRMATIONS for the same reason as the OpsLog card — +// it is stored in the ADIF extras, not in QSO columns — but it is an upload +// service like QRZ.com or Club Log, so it gets the ordinary sent/received/date +// editor rather than a special one. The received key is THEIR field name, the +// one their ADIF export writes; see award.HamlogQSLKey. +const HAMLOG_CONF = 'HAMLOG'; +const HAMLOG_KEYS = { + sent: 'APP_OPSLOG_HAMLOG_SENT', + sentDate: 'APP_OPSLOG_HAMLOG_SENT_DATE', + rcvd: 'APP_HAMLOG_QSO_CFM', + rcvdDate: 'APP_OPSLOG_HAMLOG_QSL_DATE', +}; + // Colour-coded status cell for the confirmation grid. function StatusCell({ value }: { value?: string }) { const { t } = useI18n(); @@ -715,6 +728,16 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b const def = CONFIRMATIONS.find((c) => c.key === confSel) ?? CONFIRMATIONS[0]; const val = (k?: keyof QSOForm) => (k ? ((draft as any)[k] ?? '') : ''); const put = (k: keyof QSOForm | undefined, v: any) => { if (k) (set as any)(k, v); }; + // The extras-backed channel reads and writes the same way, one + // level down. Save merges the extras rather than replacing them, + // so an emptied field keeps its stored value — which is why the + // statuses are written as explicit Y/N and never removed. + const exVal = (k: string) => String(draft.extras?.[k] ?? ''); + const exPut = (k: string, v: any) => { + const next = { ...(draft.extras ?? {}) }; + next[k] = String(v ?? ''); + set('extras', next as any); + }; return (
{/* Left: edit one confirmation channel at a time */} @@ -731,11 +754,24 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b generic sent/received/date grid, which has no field to bind to. */} {t('qedit.confOpsLog')} + HAMLOG.online
- {confSel === OPSLOG_CONF ? ( + {confSel === HAMLOG_CONF ? ( + <> +
+
exPut(HAMLOG_KEYS.sent, v)} />
+
exPut(HAMLOG_KEYS.rcvd, v)} />
+
exPut(HAMLOG_KEYS.sentDate, v)} />
+
exPut(HAMLOG_KEYS.rcvdDate, v)} />
+
+

+ {t('qedit.qslPanelHint')} {t('qedit.saveChanges')}. +

+ + ) : confSel === OPSLOG_CONF ? ( /* OpsLog's own card. "Sent" is stamped when the card actually goes out, so it is shown, not offered: ticking it by hand would record something that never happened. @@ -827,6 +863,12 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b + {/* HAMLOG.online — extras again, same hand-written row. */} + + HAMLOG.online + + + diff --git a/frontend/src/components/RecentQSOsGrid.tsx b/frontend/src/components/RecentQSOsGrid.tsx index c1ceba6..63ae695 100644 --- a/frontend/src/components/RecentQSOsGrid.tsx +++ b/frontend/src/components/RecentQSOsGrid.tsx @@ -213,10 +213,10 @@ export const makeColCatalog = (t: TFn, myGrid?: string): ColEntry[] => [ // hamlog.EU and none for hamlog.ONLINE — so these read the extras, exactly // like the OpsLog card columns above. Hidden by default: a column per // service, shown to everyone, is how a grid becomes unreadable. - { group: 'QSL', label: t('rqg.c.hamlog_sent'), colId: 'hamlog_sent', headerName: t('rqg.h.hamlog_sent'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_OPSLOG_HAMLOG_SENT'] || 'N'; }, defaultVisible: false }, - { group: 'QSL', label: t('rqg.c.hamlog_sent_date'), colId: 'hamlog_sent_date', headerName: t('rqg.h.hamlog_sent_date'), width: 110, valueGetter: (p) => fmtDateOnly((p.data as any)?.extras?.['APP_OPSLOG_HAMLOG_SENT_DATE']), defaultVisible: false }, - { group: 'QSL', label: t('rqg.c.hamlog_rcvd'), colId: 'hamlog_rcvd', headerName: t('rqg.h.hamlog_rcvd'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_HAMLOG_QSO_CFM'] || e['APP_OPSLOG_HAMLOG_QSL'] || 'N'; }, defaultVisible: false }, - { group: 'QSL', label: t('rqg.c.hamlog_rcvd_date'), colId: 'hamlog_rcvd_date', headerName: t('rqg.h.hamlog_rcvd_date'), width: 110, valueGetter: (p) => fmtDateOnly((p.data as any)?.extras?.['APP_OPSLOG_HAMLOG_QSL_DATE']), defaultVisible: false }, + { group: 'Uploads', label: t('rqg.c.hamlog_sent'), colId: 'hamlog_sent', headerName: t('rqg.h.hamlog_sent'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_OPSLOG_HAMLOG_SENT'] || 'N'; }, defaultVisible: false }, + { group: 'Uploads', label: t('rqg.c.hamlog_sent_date'), colId: 'hamlog_sent_date', headerName: t('rqg.h.hamlog_sent_date'), width: 110, valueGetter: (p) => fmtDateOnly((p.data as any)?.extras?.['APP_OPSLOG_HAMLOG_SENT_DATE']), defaultVisible: false }, + { group: 'Uploads', label: t('rqg.c.hamlog_rcvd'), colId: 'hamlog_rcvd', headerName: t('rqg.h.hamlog_rcvd'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_HAMLOG_QSO_CFM'] || e['APP_OPSLOG_HAMLOG_QSL'] || 'N'; }, defaultVisible: false }, + { group: 'Uploads', label: t('rqg.c.hamlog_rcvd_date'), colId: 'hamlog_rcvd_date', headerName: t('rqg.h.hamlog_rcvd_date'), width: 110, valueGetter: (p) => fmtDateOnly((p.data as any)?.extras?.['APP_OPSLOG_HAMLOG_QSL_DATE']), defaultVisible: false }, // App-specific: when the QSO's audio recording was e-mailed to the station. { group: 'QSL', label: t('rqg.c.opslog_recording_sent'), colId: 'opslog_recording_sent', headerName: t('rqg.h.opslog_recording_sent'), width: 100, cellClass: qslStatusCellClass, valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_OPSLOG_RECORDING_SENT'] ? 'Y' : 'N'; }, defaultVisible: false },