fix: Upload to HRDLog
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { UploadCloud, DownloadCloud, Search, Loader2, ScrollText, ListChecks, Trees, ExternalLink } from 'lucide-react';
|
||||
import { AllCommunityModule, ModuleRegistry, themeQuartz, type ColDef } from 'ag-grid-community';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
@@ -10,11 +12,31 @@ import { FindQSOsForUpload, UploadQSOsManual, DownloadConfirmations, SyncPOTAHun
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
// Warm theme matching the other grids (Recent QSOs / Cluster).
|
||||
const qslTheme = themeQuartz.withParams({
|
||||
fontFamily: 'inherit', fontSize: 12.5, backgroundColor: '#faf6ea', foregroundColor: '#2a2419',
|
||||
headerBackgroundColor: '#e8dfc9', headerTextColor: '#5a4f3a', headerFontWeight: 600,
|
||||
oddRowBackgroundColor: '#f5efe0', rowHoverColor: '#ecdcb4', selectedRowBackgroundColor: '#f0d9a8',
|
||||
borderColor: '#c8b994', rowBorder: { color: '#d8c9a8', width: 1 }, columnBorder: { color: '#d8c9a8', width: 1 },
|
||||
cellHorizontalPadding: 10, rowHeight: 30, headerHeight: 32, spacing: 4, accentColor: '#b8410c', iconSize: 12,
|
||||
});
|
||||
|
||||
type UploadRow = {
|
||||
id: number; qso_date: string; callsign: string;
|
||||
band: string; mode: string; country: string; status: string;
|
||||
};
|
||||
|
||||
const UPLOAD_COLS: ColDef<UploadRow>[] = [
|
||||
{ field: 'qso_date', headerName: 'Date UTC', valueFormatter: (p) => fmtDate(p.value), minWidth: 150 },
|
||||
{ field: 'callsign', headerName: 'Callsign', cellClass: 'font-mono font-bold', width: 130 },
|
||||
{ field: 'band', headerName: 'Band', width: 90 },
|
||||
{ field: 'mode', headerName: 'Mode', width: 100 },
|
||||
{ field: 'country', headerName: 'Country', flex: 1, minWidth: 140 },
|
||||
{ field: 'status', headerName: 'Sent', width: 90, valueFormatter: (p) => p.value || '—' },
|
||||
];
|
||||
|
||||
type Confirmation = {
|
||||
callsign: string; qso_date: string; band: string; mode: string; country: string;
|
||||
new_dxcc: boolean; new_band: boolean; new_slot: boolean;
|
||||
@@ -151,7 +173,10 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
||||
}
|
||||
const [sent, setSent] = useState('R');
|
||||
const [rows, setRows] = useState<UploadRow[]>([]);
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
// Selection lives in the (virtualized) ag-grid — it handles 25k rows smoothly.
|
||||
const gridRef = useRef<AgGridReact<UploadRow>>(null);
|
||||
const [selectedCount, setSelectedCount] = useState(0);
|
||||
const selectAllNext = useRef(false); // selectAll once after the next data load
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [addNotFound, setAddNotFound] = useState(false);
|
||||
@@ -182,10 +207,20 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
||||
return () => { offLog(); offDone(); offConf(); };
|
||||
}, []);
|
||||
|
||||
const selectedCount = selected.size;
|
||||
const allSelected = rows.length > 0 && selected.size === rows.length;
|
||||
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.
|
||||
function onUploadSelChanged() {
|
||||
setSelectedCount(gridRef.current?.api?.getSelectedNodes()?.length ?? 0);
|
||||
}
|
||||
// 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) => {
|
||||
switch (confFilter) {
|
||||
case 'new': return c.new_dxcc || c.new_band || c.new_slot;
|
||||
@@ -202,32 +237,22 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
||||
try {
|
||||
const r: any = await FindQSOsForUpload(service, sent === '_' ? '' : sent);
|
||||
const list = (r ?? []) as UploadRow[];
|
||||
selectAllNext.current = true; // pre-select everything once the grid renders
|
||||
setRows(list);
|
||||
setSelected(new Set(list.map((x) => x.id)));
|
||||
setSelectedCount(list.length);
|
||||
setViewMode('upload');
|
||||
setShowLog(false);
|
||||
} catch (e: any) {
|
||||
setError(String(e?.message ?? e));
|
||||
setRows([]);
|
||||
setSelected(new Set());
|
||||
setSelectedCount(0);
|
||||
} finally {
|
||||
setSearching(false);
|
||||
}
|
||||
}, [service, sent]);
|
||||
|
||||
function toggle(id: number) {
|
||||
setSelected((s) => {
|
||||
const n = new Set(s);
|
||||
if (n.has(id)) n.delete(id); else n.add(id);
|
||||
return n;
|
||||
});
|
||||
}
|
||||
function toggleAll() {
|
||||
setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id)));
|
||||
}
|
||||
|
||||
async function upload() {
|
||||
const ids = rows.filter((r) => selected.has(r.id)).map((r) => r.id);
|
||||
const ids = ((gridRef.current?.api?.getSelectedRows() as UploadRow[] | undefined) ?? []).map((r) => r.id);
|
||||
if (ids.length === 0) return;
|
||||
setLogLines([]); setBusy(true); setLogAction('upload'); setShowLog(true);
|
||||
try { await UploadQSOsManual(service, ids); }
|
||||
@@ -484,33 +509,21 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi
|
||||
) : rows.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground py-10 text-center">Pick a service + sent status, then “Select required”.</div>
|
||||
) : (
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead className="sticky top-0 bg-card">
|
||||
<tr className="text-left text-muted-foreground border-b border-border">
|
||||
<th className="py-1.5 px-2 w-8"><Checkbox checked={allSelected} onCheckedChange={toggleAll} /></th>
|
||||
<th className="py-1.5 px-2">Date UTC</th><th className="py-1.5 px-2">Callsign</th>
|
||||
<th className="py-1.5 px-2">Band</th><th className="py-1.5 px-2">Mode</th>
|
||||
<th className="py-1.5 px-2">Country</th><th className="py-1.5 px-2">Sent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.id}
|
||||
className={cn('border-b border-border/40 cursor-pointer hover:bg-accent/30', selected.has(r.id) && 'bg-primary/5')}
|
||||
onClick={() => toggle(r.id)}>
|
||||
<td className="py-1 px-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox checked={selected.has(r.id)} onCheckedChange={() => toggle(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 text-muted-foreground">{r.country}</td>
|
||||
<td className="py-1 px-2 font-mono">{r.status || '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="h-full w-full">
|
||||
<AgGridReact<UploadRow>
|
||||
ref={gridRef}
|
||||
theme={qslTheme}
|
||||
rowData={rows}
|
||||
columnDefs={UPLOAD_COLS}
|
||||
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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user