fix: recording not being sent if mail enable was not checked
This commit is contained in:
@@ -3356,6 +3356,8 @@ type QSLBulkUpdate struct {
|
|||||||
SentDate string `json:"sent_date"` // YYYYMMDD — "" = unchanged
|
SentDate string `json:"sent_date"` // YYYYMMDD — "" = unchanged
|
||||||
RcvdDate string `json:"rcvd_date"` // YYYYMMDD — "" = unchanged
|
RcvdDate string `json:"rcvd_date"` // YYYYMMDD — "" = unchanged
|
||||||
Via string `json:"via"` // QSL_VIA — "" = unchanged
|
Via string `json:"via"` // QSL_VIA — "" = unchanged
|
||||||
|
Notes string `json:"notes"` // NOTES — "" = unchanged
|
||||||
|
Comment string `json:"comment"` // COMMENT — "" = unchanged
|
||||||
}
|
}
|
||||||
|
|
||||||
// BulkUpdateQSL applies paper-QSL sent/received status, dates and via to the
|
// BulkUpdateQSL applies paper-QSL sent/received status, dates and via to the
|
||||||
@@ -3388,6 +3390,12 @@ func (a *App) BulkUpdateQSL(ids []int64, u QSLBulkUpdate) (int, error) {
|
|||||||
if v := strings.TrimSpace(u.Via); v != "" {
|
if v := strings.TrimSpace(u.Via); v != "" {
|
||||||
q.QSLVia, changed = v, true
|
q.QSLVia, changed = v, true
|
||||||
}
|
}
|
||||||
|
if v := strings.TrimSpace(u.Notes); v != "" {
|
||||||
|
q.Notes, changed = v, true
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(u.Comment); v != "" {
|
||||||
|
q.Comment, changed = v, true
|
||||||
|
}
|
||||||
if changed {
|
if changed {
|
||||||
if a.qso.Update(a.ctx, q) == nil {
|
if a.qso.Update(a.ctx, q) == nil {
|
||||||
n++
|
n++
|
||||||
@@ -4207,9 +4215,12 @@ func (a *App) saveQSORecording(q *qso.QSO) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
applog.Printf("qso-rec: saved %s", path)
|
applog.Printf("qso-rec: saved %s", path)
|
||||||
// Auto-send the recording once the file exists. Give the operator visible
|
// Auto-send the recording once the file exists. Gated ONLY on its own
|
||||||
// feedback either way — silently skipping (no e-mail known) was confusing.
|
// "auto-send recording" toggle (email.auto_send) — NOT the SMTP panel's
|
||||||
if es, _ := a.GetEmailSettings(); es.Enabled && es.AutoSend {
|
// master "Enabled" flag, mirroring the eQSL-card auto-send. (Requiring
|
||||||
|
// Enabled silently skipped sending with no log when only this box was on.)
|
||||||
|
// Give the operator visible feedback either way.
|
||||||
|
if es, _ := a.GetEmailSettings(); es.AutoSend {
|
||||||
if strings.TrimSpace(qc.Email) == "" {
|
if strings.TrimSpace(qc.Email) == "" {
|
||||||
applog.Printf("qso-rec: %s not e-mailed — no recipient address known", qc.Callsign)
|
applog.Printf("qso-rec: %s not e-mailed — no recipient address known", qc.Callsign)
|
||||||
a.toast(fmt.Sprintf("Recording saved — no e-mail for %s, not sent", qc.Callsign))
|
a.toast(fmt.Sprintf("Recording saved — no e-mail for %s, not sent", qc.Callsign))
|
||||||
@@ -5669,7 +5680,9 @@ func uploadColumnFor(service string) string {
|
|||||||
|
|
||||||
// FindQSOsForUpload returns QSOs whose sent status for the given service
|
// FindQSOsForUpload returns QSOs whose sent status for the given service
|
||||||
// matches sentStatus ("" = blank). Powers the QSL Manager's Select required.
|
// matches sentStatus ("" = blank). Powers the QSL Manager's Select required.
|
||||||
func (a *App) FindQSOsForUpload(service, sentStatus string) ([]qso.UploadRow, error) {
|
// FindQSOsForUpload returns FULL QSO rows eligible for upload to a service, so
|
||||||
|
// the QSL Manager shows the same rich, column-pickable table as Recent QSOs.
|
||||||
|
func (a *App) FindQSOsForUpload(service, sentStatus string) ([]qso.QSO, error) {
|
||||||
if a.qso == nil {
|
if a.qso == nil {
|
||||||
return nil, fmt.Errorf("db not initialized")
|
return nil, fmt.Errorf("db not initialized")
|
||||||
}
|
}
|
||||||
@@ -5677,7 +5690,7 @@ func (a *App) FindQSOsForUpload(service, sentStatus string) ([]qso.UploadRow, er
|
|||||||
if col == "" {
|
if col == "" {
|
||||||
return nil, fmt.Errorf("unknown service %q", service)
|
return nil, fmt.Errorf("unknown service %q", service)
|
||||||
}
|
}
|
||||||
return a.qso.ListForUpload(a.ctx, col, strings.ToUpper(strings.TrimSpace(sentStatus)))
|
return a.qso.ListForUploadFull(a.ctx, col, strings.ToUpper(strings.TrimSpace(sentStatus)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadQSOsManual uploads the given QSO ids to a service on demand
|
// UploadQSOsManual uploads the given QSO ids to a service on demand
|
||||||
|
|||||||
+33
-11
@@ -117,7 +117,7 @@ const emptyDetails: DetailsState = {
|
|||||||
prop_mode: '', my_rig: '', my_antenna: '',
|
prop_mode: '', my_rig: '', my_antenna: '',
|
||||||
tx_pwr: undefined,
|
tx_pwr: undefined,
|
||||||
sat_name: '', sat_mode: '',
|
sat_name: '', sat_mode: '',
|
||||||
contest_id: '', srx: undefined, stx: undefined,
|
contest_id: '', srx_string: '', stx_string: '',
|
||||||
email: '',
|
email: '',
|
||||||
award_refs: '',
|
award_refs: '',
|
||||||
};
|
};
|
||||||
@@ -216,6 +216,14 @@ function estimateCwMs(resolved: string, wpm: number): number {
|
|||||||
const spaces = (resolved.match(/\s/g) || []).length;
|
const spaces = (resolved.match(/\s/g) || []).length;
|
||||||
return ((chars * 10 + spaces * 4) * 1200) / w;
|
return ((chars * 10 + spaces * 4) * 1200) / w;
|
||||||
}
|
}
|
||||||
|
// exchangeFields splits a contest exchange the operator typed as free text into
|
||||||
|
// the ADIF numeric field (SRX/STX, when it's all digits) or the string field
|
||||||
|
// (SRX_STRING/STX_STRING, when it's alphanumeric like a section/zone).
|
||||||
|
function exchangeFields(raw?: string): { num?: number; str: string } {
|
||||||
|
const t = (raw || '').trim();
|
||||||
|
if (t === '') return { str: '' };
|
||||||
|
return /^\d+$/.test(t) ? { num: parseInt(t, 10), str: '' } : { str: t };
|
||||||
|
}
|
||||||
// rstOptions returns the valid report choices for a mode from the user's
|
// rstOptions returns the valid report choices for a mode from the user's
|
||||||
// editable lists (Settings → Modes), with a tiny fallback before they load.
|
// editable lists (Settings → Modes), with a tiny fallback before they load.
|
||||||
function rstOptions(mode: string, lists: RSTLists): string[] {
|
function rstOptions(mode: string, lists: RSTLists): string[] {
|
||||||
@@ -413,10 +421,12 @@ export default function App() {
|
|||||||
|
|
||||||
// User changed band/mode in the entry strip → push to the rig if CAT is up.
|
// User changed band/mode in the entry strip → push to the rig if CAT is up.
|
||||||
// Both calls are fire-and-forget; CAT will reflect back via cat:state.
|
// Both calls are fire-and-forget; CAT will reflect back via cat:state.
|
||||||
|
// When the field is LOCKED (🔒, e.g. logging an old QSO off-frequency), we do
|
||||||
|
// NOT drive the radio — the lock means "this value is decoupled from the rig".
|
||||||
function onBandUserChange(v: string) {
|
function onBandUserChange(v: string) {
|
||||||
setBand(v);
|
setBand(v);
|
||||||
noteManualEdit();
|
noteManualEdit();
|
||||||
if (catState.enabled && catState.connected) {
|
if (catState.enabled && catState.connected && !locks.band && !locks.freq) {
|
||||||
const hz = qsyFreqHz(v, mode);
|
const hz = qsyFreqHz(v, mode);
|
||||||
if (hz > 0) SetCATFrequency(hz).catch(() => {});
|
if (hz > 0) SetCATFrequency(hz).catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -425,7 +435,7 @@ export default function App() {
|
|||||||
setMode(v);
|
setMode(v);
|
||||||
applyModePreset(v);
|
applyModePreset(v);
|
||||||
noteManualEdit();
|
noteManualEdit();
|
||||||
if (catState.enabled && catState.connected) {
|
if (catState.enabled && catState.connected && !locks.mode) {
|
||||||
SetCATMode(v).catch(() => {});
|
SetCATMode(v).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1465,30 +1475,38 @@ export default function App() {
|
|||||||
// typing: only update when the field is empty, already shows this call, or
|
// typing: only update when the field is empty, already shows this call, or
|
||||||
// still shows the previous broadcast (i.e. the field content is ours, not
|
// still shows the previous broadcast (i.e. the field content is ours, not
|
||||||
// a different call the user typed). Returns true if it actually changed.
|
// a different call the user typed). Returns true if it actually changed.
|
||||||
const applyUdpCall = (raw: string): boolean => {
|
// force = true means an EXPLICIT action (a panadapter/spot click, a remote
|
||||||
|
// "set call" command) that should always win, even over a call the operator
|
||||||
|
// already typed. Only WSJT-X's CONTINUOUS DX-call stream keeps the no-clobber
|
||||||
|
// guard (it re-broadcasts constantly and must not fight what you typed).
|
||||||
|
const applyUdpCall = (raw: string, force = false): boolean => {
|
||||||
const call = String(raw ?? '').trim();
|
const call = String(raw ?? '').trim();
|
||||||
if (!call) return false;
|
if (!call) return false;
|
||||||
const upper = call.toUpperCase();
|
const upper = call.toUpperCase();
|
||||||
const current = callsignValRef.current.trim().toUpperCase();
|
const current = callsignValRef.current.trim().toUpperCase();
|
||||||
const prev = lastUdpCallRef.current;
|
const prev = lastUdpCallRef.current;
|
||||||
lastUdpCallRef.current = upper; // remember this broadcast either way
|
lastUdpCallRef.current = upper; // remember this broadcast either way
|
||||||
if (current === upper) return false; // already shown → no-op
|
if (current === upper) return false; // already shown → no-op
|
||||||
if (current !== '' && current !== prev) return false; // user typed a different call → leave it
|
if (!force && current !== '' && current !== prev) return false; // user typed a different call → leave it
|
||||||
onCallsignInput(call, { force: true }); // programmatic → always look up
|
onCallsignInput(call, { force: true }); // programmatic → always look up
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const unsubDX = EventsOn('udp:dx_call', (p: any) => {
|
const unsubDX = EventsOn('udp:dx_call', (p: any) => {
|
||||||
|
// Anything that isn't WSJT-X (N1MM, ADIF, a panadapter/cluster click relayed
|
||||||
|
// over UDP…) is an explicit pick → force it over an existing call.
|
||||||
|
const force = String(p?.service ?? '').toLowerCase() !== 'wsjt';
|
||||||
// External app moved to a new station → fresh recording for the new target.
|
// External app moved to a new station → fresh recording for the new target.
|
||||||
if (applyUdpCall(p?.call)) restartRecordingForNewTarget(String(p?.call ?? ''));
|
if (applyUdpCall(p?.call, force)) restartRecordingForNewTarget(String(p?.call ?? ''));
|
||||||
});
|
});
|
||||||
const unsubRC = EventsOn('udp:remote_call', (raw: string) => {
|
const unsubRC = EventsOn('udp:remote_call', (raw: string) => {
|
||||||
if (applyUdpCall(raw)) restartRecordingForNewTarget(String(raw ?? ''));
|
if (applyUdpCall(raw, true)) restartRecordingForNewTarget(String(raw ?? '')); // explicit remote pick
|
||||||
});
|
});
|
||||||
// Clicked one of OpsLog's spots on the FlexRadio panadapter → fill the call
|
// Clicked one of OpsLog's spots on the FlexRadio panadapter → fill the call
|
||||||
// (the radio already tuned via trigger_action=Tune, and CAT reads the freq).
|
// (the radio already tuned via trigger_action=Tune, and CAT reads the freq).
|
||||||
|
// An explicit click always wins over whatever call is currently in the field.
|
||||||
const unsubFlexSpot = EventsOn('flex:spot_clicked', (p: any) => {
|
const unsubFlexSpot = EventsOn('flex:spot_clicked', (p: any) => {
|
||||||
const call = String(p?.call ?? '');
|
const call = String(p?.call ?? '');
|
||||||
if (applyUdpCall(call)) restartRecordingForNewTarget(call);
|
if (applyUdpCall(call, true)) restartRecordingForNewTarget(call);
|
||||||
});
|
});
|
||||||
const unsubProg = EventsOn('import:progress', (p: any) => {
|
const unsubProg = EventsOn('import:progress', (p: any) => {
|
||||||
setImportProgress({ processed: Number(p?.processed ?? 0), total: Number(p?.total ?? 0) });
|
setImportProgress({ processed: Number(p?.processed ?? 0), total: Number(p?.total ?? 0) });
|
||||||
@@ -1585,7 +1603,7 @@ export default function App() {
|
|||||||
GRID: station.my_grid || '', COUNTRY: (station as any).my_country || '',
|
GRID: station.my_grid || '', COUNTRY: (station as any).my_country || '',
|
||||||
MY_QTH: (station as any).my_city || '', MY_RIG: details.my_rig || '', MY_ANTENNA: details.my_antenna || '',
|
MY_QTH: (station as any).my_city || '', MY_RIG: details.my_rig || '', MY_ANTENNA: details.my_antenna || '',
|
||||||
MY_IOTA: (station as any).my_iota || '', MY_SOTA: (station as any).my_sota_ref || '',
|
MY_IOTA: (station as any).my_iota || '', MY_SOTA: (station as any).my_sota_ref || '',
|
||||||
CONT_RX: details.srx != null ? String(details.srx) : '', CONT_TX: details.stx != null ? String(details.stx) : '',
|
CONT_RX: details.srx_string || '', CONT_TX: details.stx_string || '',
|
||||||
};
|
};
|
||||||
let out = text.replace(/<([A-Z_]+)>/g, (_m, k) => vars[k] ?? '');
|
let out = text.replace(/<([A-Z_]+)>/g, (_m, k) => vars[k] ?? '');
|
||||||
out = out.replace(/\*/g, myCall).replace(/!/g, his);
|
out = out.replace(/\*/g, myCall).replace(/!/g, his);
|
||||||
@@ -1728,6 +1746,10 @@ export default function App() {
|
|||||||
// when you first entered the call (minutes early) and won't match LoTW.
|
// when you first entered the call (minutes early) and won't match LoTW.
|
||||||
const startEqualsEnd = localStorage.getItem('opslog.startEqualsEnd') === '1';
|
const startEqualsEnd = localStorage.getItem('opslog.startEqualsEnd') === '1';
|
||||||
const start = (startEqualsEnd && !locks.start) ? end : baseStart;
|
const start = (startEqualsEnd && !locks.start) ? end : baseStart;
|
||||||
|
// Contest exchanges: a purely-numeric exchange → ADIF SRX/STX (int); an
|
||||||
|
// alphanumeric one (e.g. a section/zone) → SRX_STRING/STX_STRING.
|
||||||
|
const srxE = exchangeFields(details.srx_string);
|
||||||
|
const stxE = exchangeFields(details.stx_string);
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
callsign: callsign.trim().toUpperCase(),
|
callsign: callsign.trim().toUpperCase(),
|
||||||
qso_date: start.toISOString(),
|
qso_date: start.toISOString(),
|
||||||
@@ -1746,7 +1768,7 @@ export default function App() {
|
|||||||
tx_pwr: details.tx_pwr,
|
tx_pwr: details.tx_pwr,
|
||||||
sat_name: details.sat_name, sat_mode: details.sat_mode,
|
sat_name: details.sat_name, sat_mode: details.sat_mode,
|
||||||
contest_id: details.contest_id,
|
contest_id: details.contest_id,
|
||||||
srx: details.srx, stx: details.stx,
|
srx: srxE.num, stx: stxE.num, srx_string: srxE.str, stx_string: stxE.str,
|
||||||
email: details.email,
|
email: details.email,
|
||||||
};
|
};
|
||||||
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
|
applyAwardRefs(payload, details.award_refs ?? '', awardFieldRef.current);
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ export interface DetailsState {
|
|||||||
sat_name: string;
|
sat_name: string;
|
||||||
sat_mode: string;
|
sat_mode: string;
|
||||||
contest_id: string;
|
contest_id: string;
|
||||||
srx?: number;
|
// Contest exchanges as free text — most are serials (001) but some are
|
||||||
stx?: number;
|
// alphanumeric (e.g. a section/zone like "ON4"). Stored to ADIF SRX/STX when
|
||||||
|
// purely numeric, else to SRX_STRING/STX_STRING (handled in App on save).
|
||||||
|
srx_string?: string;
|
||||||
|
stx_string?: string;
|
||||||
email: string;
|
email: string;
|
||||||
// Award references for the contacted station (set via the Awards tab picker).
|
// Award references for the contacted station (set via the Awards tab picker).
|
||||||
// Semicolon-delimited "AWARD@REF" entries, e.g. "POTA@FR-11553;IOTA@EU-064".
|
// Semicolon-delimited "AWARD@REF" entries, e.g. "POTA@FR-11553;IOTA@EU-064".
|
||||||
@@ -371,10 +374,10 @@ export function DetailsPanel({ callsign, prefix, operatorGrid, remoteGrid, detai
|
|||||||
<Input value={details.contest_id} onChange={(e) => onChange({ contest_id: e.target.value })} />
|
<Input value={details.contest_id} onChange={(e) => onChange({ contest_id: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="SRX">
|
<Field label="SRX">
|
||||||
<Input type="number" value={details.srx ?? ''} onChange={(e) => onChange({ srx: numOrUndef(e.target.value) })} />
|
<Input value={details.srx_string ?? ''} placeholder="rcvd exchange" onChange={(e) => onChange({ srx_string: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="STX">
|
<Field label="STX">
|
||||||
<Input type="number" value={details.stx ?? ''} onChange={(e) => onChange({ stx: numOrUndef(e.target.value) })} />
|
<Input value={details.stx_string ?? ''} placeholder="sent exchange" onChange={(e) => onChange({ stx_string: e.target.value })} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Contacted email" span={3}>
|
<Field label="Contacted email" span={3}>
|
||||||
<Input value={details.email} placeholder="[email protected]" onChange={(e) => onChange({ email: e.target.value })} />
|
<Input value={details.email} placeholder="[email protected]" onChange={(e) => onChange({ email: e.target.value })} />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign } from '../../wailsjs/go/main/App';
|
import { FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign } from '../../wailsjs/go/main/App';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
|
||||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||||
|
|
||||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||||
@@ -60,6 +61,14 @@ const QSL_STATUSES = [
|
|||||||
{ v: 'I', label: 'Ignore' },
|
{ v: 'I', label: 'Ignore' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// QSL routing methods for the paper-QSL "Via" dropdown (was free text).
|
||||||
|
const QSL_VIA_OPTIONS = [
|
||||||
|
{ v: '_', label: '— leave —' },
|
||||||
|
{ v: 'Bureau', label: 'Bureau' },
|
||||||
|
{ v: 'Direct', label: 'Direct' },
|
||||||
|
{ v: 'Electronic', label: 'Electronic' },
|
||||||
|
];
|
||||||
|
|
||||||
type LogQSO = {
|
type LogQSO = {
|
||||||
id: number; qso_date: string; callsign: string; band: string; mode: string; country?: string;
|
id: number; qso_date: string; callsign: string; band: string; mode: string; country?: string;
|
||||||
qsl_sent?: string; qsl_rcvd?: string; qsl_via?: string; qsl_sent_date?: string; qsl_rcvd_date?: string;
|
qsl_sent?: string; qsl_rcvd?: string; qsl_via?: string; qsl_sent_date?: string; qsl_rcvd_date?: string;
|
||||||
@@ -127,13 +136,16 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
const [paperCall, setPaperCall] = useState('');
|
const [paperCall, setPaperCall] = useState('');
|
||||||
const [paperRows, setPaperRows] = useState<LogQSO[]>([]);
|
const [paperRows, setPaperRows] = useState<LogQSO[]>([]);
|
||||||
const [paperSel, setPaperSel] = useState<Set<number>>(new Set());
|
const [paperSel, setPaperSel] = useState<Set<number>>(new Set());
|
||||||
|
const [paperSelAllSig, setPaperSelAllSig] = useState(0); // bump → grid selects all
|
||||||
const [paperBusy, setPaperBusy] = useState(false);
|
const [paperBusy, setPaperBusy] = useState(false);
|
||||||
const [paperMsg, setPaperMsg] = useState('');
|
const [paperMsg, setPaperMsg] = useState('');
|
||||||
const [qslRcvd, setQslRcvd] = useState('Y');
|
const [qslRcvd, setQslRcvd] = useState('N');
|
||||||
const [qslSent, setQslSent] = useState('_');
|
const [qslSent, setQslSent] = useState('_');
|
||||||
const [qslRcvdDate, setQslRcvdDate] = useState('');
|
const [qslRcvdDate, setQslRcvdDate] = useState('');
|
||||||
const [qslSentDate, setQslSentDate] = useState('');
|
const [qslSentDate, setQslSentDate] = useState('');
|
||||||
const [qslVia, setQslVia] = useState('');
|
const [qslVia, setQslVia] = useState('_');
|
||||||
|
const [qslNotes, setQslNotes] = useState('');
|
||||||
|
const [qslComment, setQslComment] = useState('');
|
||||||
|
|
||||||
const searchPaper = useCallback(async () => {
|
const searchPaper = useCallback(async () => {
|
||||||
const c = paperCall.trim().toUpperCase();
|
const c = paperCall.trim().toUpperCase();
|
||||||
@@ -144,14 +156,12 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
const list = (r ?? []) as LogQSO[];
|
const list = (r ?? []) as LogQSO[];
|
||||||
setPaperRows(list);
|
setPaperRows(list);
|
||||||
setPaperSel(new Set(list.map((x) => x.id)));
|
setPaperSel(new Set(list.map((x) => x.id)));
|
||||||
|
setPaperSelAllSig((n) => n + 1); // tell the grid to select every row
|
||||||
|
|
||||||
} catch (e: any) { setPaperMsg(String(e?.message ?? e)); setPaperRows([]); }
|
} catch (e: any) { setPaperMsg(String(e?.message ?? e)); setPaperRows([]); }
|
||||||
finally { setPaperBusy(false); }
|
finally { setPaperBusy(false); }
|
||||||
}, [paperCall]);
|
}, [paperCall]);
|
||||||
|
|
||||||
function togglePaper(id: number) {
|
|
||||||
setPaperSel((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
|
||||||
}
|
|
||||||
const paperAllSel = paperRows.length > 0 && paperSel.size === paperRows.length;
|
|
||||||
|
|
||||||
async function applyPaper() {
|
async function applyPaper() {
|
||||||
const ids = paperRows.filter((r) => paperSel.has(r.id)).map((r) => r.id);
|
const ids = paperRows.filter((r) => paperSel.has(r.id)).map((r) => r.id);
|
||||||
@@ -164,7 +174,9 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
rcvd_status: qslRcvd === '_' ? '' : qslRcvd,
|
rcvd_status: qslRcvd === '_' ? '' : qslRcvd,
|
||||||
sent_date: ymd(qslSentDate),
|
sent_date: ymd(qslSentDate),
|
||||||
rcvd_date: ymd(qslRcvdDate),
|
rcvd_date: ymd(qslRcvdDate),
|
||||||
via: qslVia,
|
via: qslVia === '_' ? '' : qslVia,
|
||||||
|
notes: qslNotes,
|
||||||
|
comment: qslComment,
|
||||||
} as any);
|
} as any);
|
||||||
setPaperMsg(`${n} QSO updated.`);
|
setPaperMsg(`${n} QSO updated.`);
|
||||||
await searchPaper();
|
await searchPaper();
|
||||||
@@ -172,11 +184,11 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
finally { setPaperBusy(false); }
|
finally { setPaperBusy(false); }
|
||||||
}
|
}
|
||||||
const [sent, setSent] = useState('R');
|
const [sent, setSent] = useState('R');
|
||||||
const [rows, setRows] = useState<UploadRow[]>([]);
|
// Full QSO rows (so the upload view uses the same rich grid as Recent QSOs).
|
||||||
// Selection lives in the (virtualized) ag-grid — it handles 25k rows smoothly.
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
const gridRef = useRef<AgGridReact<UploadRow>>(null);
|
|
||||||
const [selectedCount, setSelectedCount] = useState(0);
|
const [selectedCount, setSelectedCount] = useState(0);
|
||||||
const selectAllNext = useRef(false); // selectAll once after the next data load
|
const [uploadSelIds, setUploadSelIds] = useState<number[]>([]); // selected QSO ids → upload
|
||||||
|
const [uploadSelAllSig, setUploadSelAllSig] = useState(0); // bump → grid selects all
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [addNotFound, setAddNotFound] = useState(false);
|
const [addNotFound, setAddNotFound] = useState(false);
|
||||||
@@ -210,15 +222,9 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
const serviceLabel = useMemo(() => SERVICES.find((s) => s.v === service)?.label ?? service, [service]);
|
const serviceLabel = useMemo(() => SERVICES.find((s) => s.v === service)?.label ?? service, [service]);
|
||||||
|
|
||||||
// Grid selection → just track the count; ids are read from the grid at upload.
|
// Grid selection → just track the count; ids are read from the grid at upload.
|
||||||
function onUploadSelChanged() {
|
function onUploadRowSelected(ids: number[]) {
|
||||||
setSelectedCount(gridRef.current?.api?.getSelectedNodes()?.length ?? 0);
|
setUploadSelIds(ids);
|
||||||
}
|
setSelectedCount(ids.length);
|
||||||
// After "Select required" loads new rows, select them all (the old default).
|
|
||||||
function onUploadRowsLoaded() {
|
|
||||||
if (selectAllNext.current) {
|
|
||||||
selectAllNext.current = false;
|
|
||||||
gridRef.current?.api?.selectAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const shownConfs = useMemo(() => confirmations.filter((c) => {
|
const shownConfs = useMemo(() => confirmations.filter((c) => {
|
||||||
@@ -236,10 +242,11 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const r: any = await FindQSOsForUpload(service, sent === '_' ? '' : sent);
|
const r: any = await FindQSOsForUpload(service, sent === '_' ? '' : sent);
|
||||||
const list = (r ?? []) as UploadRow[];
|
const list = (r ?? []) as any[];
|
||||||
selectAllNext.current = true; // pre-select everything once the grid renders
|
|
||||||
setRows(list);
|
setRows(list);
|
||||||
|
setUploadSelIds(list.map((x) => x.id));
|
||||||
setSelectedCount(list.length);
|
setSelectedCount(list.length);
|
||||||
|
setUploadSelAllSig((n) => n + 1); // pre-select everything once the grid renders
|
||||||
setViewMode('upload');
|
setViewMode('upload');
|
||||||
setShowLog(false);
|
setShowLog(false);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -252,7 +259,7 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
}, [service, sent]);
|
}, [service, sent]);
|
||||||
|
|
||||||
async function upload() {
|
async function upload() {
|
||||||
const ids = ((gridRef.current?.api?.getSelectedRows() as UploadRow[] | undefined) ?? []).map((r) => r.id);
|
const ids = uploadSelIds;
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
setLogLines([]); setBusy(true); setLogAction('upload'); setShowLog(true);
|
setLogLines([]); setBusy(true); setLogAction('upload'); setShowLog(true);
|
||||||
try { await UploadQSOsManual(service, ids); }
|
try { await UploadQSOsManual(service, ids); }
|
||||||
@@ -387,32 +394,16 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
paperRows.length === 0 ? (
|
paperRows.length === 0 ? (
|
||||||
<div className="text-sm text-muted-foreground py-10 text-center">Search a callsign to list its QSOs, then set QSL status below.</div>
|
<div className="text-sm text-muted-foreground py-10 text-center">Search a callsign to list its QSOs, then set QSL status below.</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-xs border-collapse">
|
// Same grid as Recent QSOs (full columns + column picker). Selection
|
||||||
<thead className="sticky top-0 bg-card">
|
// drives which QSOs the apply-form below updates; a search selects all.
|
||||||
<tr className="text-left text-muted-foreground border-b border-border">
|
<div className="flex flex-col h-full min-h-0 -mx-3 -my-2">
|
||||||
<th className="py-1.5 px-2 w-8"><Checkbox checked={paperAllSel} onCheckedChange={() => setPaperSel(paperAllSel ? new Set() : new Set(paperRows.map((r) => r.id)))} /></th>
|
<RecentQSOsGrid
|
||||||
<th className="py-1.5 px-2">Date UTC</th><th className="py-1.5 px-2">Callsign</th>
|
rows={paperRows as any}
|
||||||
<th className="py-1.5 px-2">Band</th><th className="py-1.5 px-2">Mode</th>
|
total={paperRows.length}
|
||||||
<th className="py-1.5 px-2">QSL Sent</th><th className="py-1.5 px-2">QSL Rcvd</th><th className="py-1.5 px-2">Via</th>
|
selectAllSignal={paperSelAllSig}
|
||||||
</tr>
|
onRowSelected={(ids) => setPaperSel(new Set(ids))}
|
||||||
</thead>
|
/>
|
||||||
<tbody>
|
</div>
|
||||||
{paperRows.map((r) => (
|
|
||||||
<tr key={r.id}
|
|
||||||
className={cn('border-b border-border/40 cursor-pointer hover:bg-accent/30', paperSel.has(r.id) && 'bg-primary/5')}
|
|
||||||
onClick={() => togglePaper(r.id)}>
|
|
||||||
<td className="py-1 px-2" onClick={(e) => e.stopPropagation()}><Checkbox checked={paperSel.has(r.id)} onCheckedChange={() => togglePaper(r.id)} /></td>
|
|
||||||
<td className="py-1 px-2 font-mono">{fmtDate(r.qso_date)}</td>
|
|
||||||
<td className="py-1 px-2 font-mono font-bold">{r.callsign}</td>
|
|
||||||
<td className="py-1 px-2">{r.band}</td>
|
|
||||||
<td className="py-1 px-2">{r.mode}</td>
|
|
||||||
<td className="py-1 px-2 font-mono">{r.qsl_sent || '—'}{r.qsl_sent_date ? ` ${fmtQslDate(r.qsl_sent_date)}` : ''}</td>
|
|
||||||
<td className="py-1 px-2 font-mono">{r.qsl_rcvd || '—'}{r.qsl_rcvd_date ? ` ${fmtQslDate(r.qsl_rcvd_date)}` : ''}</td>
|
|
||||||
<td className="py-1 px-2 text-muted-foreground truncate max-w-[160px]">{r.qsl_via}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
)
|
||||||
) : service === 'pota' ? (
|
) : service === 'pota' ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -509,19 +500,12 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<div className="text-sm text-muted-foreground py-10 text-center">Pick a service + sent status, then “Select required”.</div>
|
<div className="text-sm text-muted-foreground py-10 text-center">Pick a service + sent status, then “Select required”.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full">
|
<div className="flex flex-col h-full min-h-0 -mx-3 -my-2">
|
||||||
<AgGridReact<UploadRow>
|
<RecentQSOsGrid
|
||||||
ref={gridRef}
|
rows={rows as any}
|
||||||
theme={qslTheme}
|
total={rows.length}
|
||||||
rowData={rows}
|
selectAllSignal={uploadSelAllSig}
|
||||||
columnDefs={UPLOAD_COLS}
|
onRowSelected={onUploadRowSelected}
|
||||||
defaultColDef={{ sortable: true, resizable: true, filter: true }}
|
|
||||||
rowSelection={{ mode: 'multiRow', checkboxes: true, headerCheckbox: true }}
|
|
||||||
onSelectionChanged={onUploadSelChanged}
|
|
||||||
onRowDataUpdated={onUploadRowsLoaded}
|
|
||||||
animateRows={false}
|
|
||||||
suppressCellFocus
|
|
||||||
getRowId={(p) => String((p.data as any).id)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -552,7 +536,18 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">Via</label>
|
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">Via</label>
|
||||||
<Input className="h-8 w-40" value={qslVia} onChange={(e) => setQslVia(e.target.value)} placeholder="BUREAU / DIRECT / manager" />
|
<Select value={qslVia} onValueChange={setQslVia}>
|
||||||
|
<SelectTrigger className="h-8 w-32"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>{QSL_VIA_OPTIONS.map((o) => <SelectItem key={o.v} value={o.v}>{o.label}</SelectItem>)}</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">Notes</label>
|
||||||
|
<Input className="h-8 w-40" value={qslNotes} onChange={(e) => setQslNotes(e.target.value)} placeholder="e.g. paid 3€" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[10px] uppercase tracking-wider text-muted-foreground">Comment</label>
|
||||||
|
<Input className="h-8 w-40" value={qslComment} onChange={(e) => setQslComment(e.target.value)} placeholder="comment" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
{paperMsg && <span className="text-[11px] text-muted-foreground self-center">{paperMsg}</span>}
|
{paperMsg && <span className="text-[11px] text-muted-foreground self-center">{paperMsg}</span>}
|
||||||
|
|||||||
@@ -215,6 +215,14 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
|
|||||||
return () => window.clearTimeout(t);
|
return () => window.clearTimeout(t);
|
||||||
}, [draft.dxcc, draft.cqz, draft.ituz, draft.cont, draft.state, draft.callsign, draft.notes, draft.band]);
|
}, [draft.dxcc, draft.cqz, draft.ituz, draft.cont, draft.state, draft.callsign, draft.notes, draft.band]);
|
||||||
|
|
||||||
|
// Contest exchange typed as free text → numeric SRX/STX when all-digits, else
|
||||||
|
// the SRX_STRING/STX_STRING field. Mirrors the entry strip (F5) so the field
|
||||||
|
// accepts letters (sections/zones), not just numbers.
|
||||||
|
function setExchange(which: 'srx' | 'stx', raw: string) {
|
||||||
|
const t = raw.trim();
|
||||||
|
const num = /^\d+$/.test(t) ? parseInt(t, 10) : undefined;
|
||||||
|
setDraft((d) => ({ ...d, [which]: num, [`${which}_string`]: num != null ? '' : t } as any));
|
||||||
|
}
|
||||||
function set<K extends keyof QSO>(key: K, value: QSO[K]) {
|
function set<K extends keyof QSO>(key: K, value: QSO[K]) {
|
||||||
setDraft((d) => ({ ...d, [key]: value }));
|
setDraft((d) => ({ ...d, [key]: value }));
|
||||||
}
|
}
|
||||||
@@ -561,7 +569,7 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{CONFIRMATIONS.map((c) => (
|
{CONFIRMATIONS.map((c) => (
|
||||||
<tr key={c.key} className={cn('text-xs', c.key === confSel && 'bg-accent/40')}>
|
<tr key={c.key} className="text-xs">
|
||||||
<td className="font-medium pr-2 py-0.5">{c.label}</td>
|
<td className="font-medium pr-2 py-0.5">{c.label}</td>
|
||||||
<td className="w-24"><StatusCell value={val(c.sent)} /></td>
|
<td className="w-24"><StatusCell value={val(c.sent)} /></td>
|
||||||
<td className="w-24">{c.rcvd ? <StatusCell value={val(c.rcvd)} /> : <span className="block text-center text-[11px] text-muted-foreground">—</span>}</td>
|
<td className="w-24">{c.rcvd ? <StatusCell value={val(c.rcvd)} /> : <span className="block text-center text-[11px] text-muted-foreground">—</span>}</td>
|
||||||
@@ -578,10 +586,8 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
|
|||||||
<TabsContent value="contest" className="mt-0">
|
<TabsContent value="contest" className="mt-0">
|
||||||
<div className="grid grid-cols-6 gap-3">
|
<div className="grid grid-cols-6 gap-3">
|
||||||
<F label="Contest ID" span={2}><Input value={draft.contest_id ?? ''} onChange={(e) => set('contest_id', e.target.value)} /></F>
|
<F label="Contest ID" span={2}><Input value={draft.contest_id ?? ''} onChange={(e) => set('contest_id', e.target.value)} /></F>
|
||||||
<F label="SRX"><Input type="number" value={draft.srx ?? ''} onChange={(e) => set('srx', intOrUndef(e.target.value) as any)} /></F>
|
<F label="SRX" span={2}><Input value={draft.srx_string || (draft.srx ?? '')} placeholder="rcvd exchange" onChange={(e) => setExchange('srx', e.target.value)} /></F>
|
||||||
<F label="STX"><Input type="number" value={draft.stx ?? ''} onChange={(e) => set('stx', intOrUndef(e.target.value) as any)} /></F>
|
<F label="STX" span={2}><Input value={draft.stx_string || (draft.stx ?? '')} placeholder="sent exchange" onChange={(e) => setExchange('stx', e.target.value)} /></F>
|
||||||
<F label="SRX string" span={3}><Input value={draft.srx_string ?? ''} onChange={(e) => set('srx_string', e.target.value)} /></F>
|
|
||||||
<F label="STX string" span={3}><Input value={draft.stx_string ?? ''} onChange={(e) => set('stx_string', e.target.value)} /></F>
|
|
||||||
<F label="Check"><Input value={draft.check ?? ''} onChange={(e) => set('check', e.target.value)} /></F>
|
<F label="Check"><Input value={draft.check ?? ''} onChange={(e) => set('check', e.target.value)} /></F>
|
||||||
<F label="Precedence"><Input value={draft.precedence ?? ''} onChange={(e) => set('precedence', e.target.value)} /></F>
|
<F label="Precedence"><Input value={draft.precedence ?? ''} onChange={(e) => set('precedence', e.target.value)} /></F>
|
||||||
<F label="ARRL section"><Input value={draft.arrl_sect ?? ''} onChange={(e) => set('arrl_sect', e.target.value)} /></F>
|
<F label="ARRL section"><Input value={draft.arrl_sect ?? ''} onChange={(e) => set('arrl_sect', e.target.value)} /></F>
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ const hamlogTheme = themeQuartz.withParams({
|
|||||||
type Props = {
|
type Props = {
|
||||||
rows: QSOForm[];
|
rows: QSOForm[];
|
||||||
total: number;
|
total: number;
|
||||||
|
// Bump this number to programmatically select every row (e.g. after a search
|
||||||
|
// in the QSL Manager, where the default is "all selected").
|
||||||
|
selectAllSignal?: number;
|
||||||
onRowDoubleClicked?: (q: QSOForm) => void;
|
onRowDoubleClicked?: (q: QSOForm) => void;
|
||||||
onRowSelected?: (ids: number[]) => void;
|
onRowSelected?: (ids: number[]) => void;
|
||||||
onUpdateFromCty?: (ids: number[]) => void;
|
onUpdateFromCty?: (ids: number[]) => void;
|
||||||
@@ -225,7 +228,7 @@ export const GROUP_ORDER = [
|
|||||||
'Contest', 'Propagation', 'My station', 'Misc',
|
'Contest', 'Propagation', 'My station', 'Misc',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, onDelete, awardCols }: Props) {
|
export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, onDelete, awardCols }: Props) {
|
||||||
const gridRef = useRef<any>(null);
|
const gridRef = useRef<any>(null);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
const [menu, setMenu] = useState<QSOMenuState>(null);
|
const [menu, setMenu] = useState<QSOMenuState>(null);
|
||||||
@@ -310,6 +313,11 @@ export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpda
|
|||||||
const sel = (gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined) ?? [];
|
const sel = (gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined) ?? [];
|
||||||
onRowSelected?.(sel.map((r) => r.id as number).filter((id) => id != null));
|
onRowSelected?.(sel.map((r) => r.id as number).filter((id) => id != null));
|
||||||
}
|
}
|
||||||
|
// Select every row when the caller bumps selectAllSignal (QSL Manager search).
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectAllSignal === undefined) return;
|
||||||
|
gridRef.current?.api?.selectAll();
|
||||||
|
}, [selectAllSignal]);
|
||||||
|
|
||||||
// ── Column picker (visibility) ──
|
// ── Column picker (visibility) ──
|
||||||
// Drives AG Grid via setColumnsVisible(). We don't keep a parallel React
|
// Drives AG Grid via setColumnsVisible(). We don't keep a parallel React
|
||||||
|
|||||||
Vendored
+1
-1
@@ -130,7 +130,7 @@ export function ExportAwards():Promise<string>;
|
|||||||
|
|
||||||
export function FilterFields():Promise<Array<string>>;
|
export function FilterFields():Promise<Array<string>>;
|
||||||
|
|
||||||
export function FindQSOsForUpload(arg1:string,arg2:string):Promise<Array<qso.UploadRow>>;
|
export function FindQSOsForUpload(arg1:string,arg2:string):Promise<Array<qso.QSO>>;
|
||||||
|
|
||||||
export function FlexATUBypass():Promise<void>;
|
export function FlexATUBypass():Promise<void>;
|
||||||
|
|
||||||
|
|||||||
@@ -1564,6 +1564,8 @@ export namespace main {
|
|||||||
sent_date: string;
|
sent_date: string;
|
||||||
rcvd_date: string;
|
rcvd_date: string;
|
||||||
via: string;
|
via: string;
|
||||||
|
notes: string;
|
||||||
|
comment: string;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new QSLBulkUpdate(source);
|
return new QSLBulkUpdate(source);
|
||||||
@@ -1576,6 +1578,8 @@ export namespace main {
|
|||||||
this.sent_date = source["sent_date"];
|
this.sent_date = source["sent_date"];
|
||||||
this.rcvd_date = source["rcvd_date"];
|
this.rcvd_date = source["rcvd_date"];
|
||||||
this.via = source["via"];
|
this.via = source["via"];
|
||||||
|
this.notes = source["notes"];
|
||||||
|
this.comment = source["comment"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class QSLDefaults {
|
export class QSLDefaults {
|
||||||
@@ -2897,30 +2901,6 @@ export namespace qso {
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class UploadRow {
|
|
||||||
id: number;
|
|
||||||
qso_date: string;
|
|
||||||
callsign: string;
|
|
||||||
band: string;
|
|
||||||
mode: string;
|
|
||||||
country: string;
|
|
||||||
status: string;
|
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
|
||||||
return new UploadRow(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: any = {}) {
|
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
|
||||||
this.id = source["id"];
|
|
||||||
this.qso_date = source["qso_date"];
|
|
||||||
this.callsign = source["callsign"];
|
|
||||||
this.band = source["band"];
|
|
||||||
this.mode = source["mode"];
|
|
||||||
this.country = source["country"];
|
|
||||||
this.status = source["status"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class WorkedBefore {
|
export class WorkedBefore {
|
||||||
callsign: string;
|
callsign: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
|||||||
@@ -497,6 +497,31 @@ var uploadStatusCols = map[string]bool{
|
|||||||
|
|
||||||
// ListForUpload returns QSOs whose per-service sent-status column equals
|
// ListForUpload returns QSOs whose per-service sent-status column equals
|
||||||
// value ("" matches blank/NULL). Used by the QSL Manager's "Select required".
|
// value ("" matches blank/NULL). Used by the QSL Manager's "Select required".
|
||||||
|
// ListForUploadFull is like ListForUpload but returns FULL QSO rows so the UI
|
||||||
|
// can show the same rich, column-pickable table as Recent QSOs. column is an
|
||||||
|
// upload-status column (validated); value is the status to match ("" = not yet
|
||||||
|
// uploaded).
|
||||||
|
func (r *Repo) ListForUploadFull(ctx context.Context, column, value string) ([]QSO, error) {
|
||||||
|
if !uploadStatusCols[column] {
|
||||||
|
return nil, fmt.Errorf("invalid upload column %q", column)
|
||||||
|
}
|
||||||
|
rows, err := r.db.QueryContext(ctx,
|
||||||
|
`SELECT `+selectCols+` FROM qso WHERE COALESCE(`+column+`,'') = ? ORDER BY qso_date DESC, id DESC`, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list for upload: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
out := make([]QSO, 0, 64)
|
||||||
|
for rows.Next() {
|
||||||
|
q, err := scanQSO(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, q)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repo) ListForUpload(ctx context.Context, column, value string) ([]UploadRow, error) {
|
func (r *Repo) ListForUpload(ctx context.Context, column, value string) ([]UploadRow, error) {
|
||||||
if !uploadStatusCols[column] {
|
if !uploadStatusCols[column] {
|
||||||
return nil, fmt.Errorf("invalid upload column %q", column)
|
return nil, fmt.Errorf("invalid upload column %q", column)
|
||||||
|
|||||||
Reference in New Issue
Block a user