awards
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Plus, Trash2, RotateCcw, Save } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { GetAwardDefs, SaveAwardDefs, ResetAwardDefs, AwardFields } from '../../wailsjs/go/main/App';
|
||||
|
||||
export type AwardDef = {
|
||||
code: string; name: string; field: string; pattern: string;
|
||||
dxcc_filter: number[] | null; confirm: string[] | null; total: number; builtin?: boolean;
|
||||
};
|
||||
|
||||
const CONFIRM_SRC = [{ id: 'lotw', label: 'LoTW' }, { id: 'qsl', label: 'QSL' }, { id: 'eqsl', label: 'eQSL' }];
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
}
|
||||
|
||||
export function AwardEditor({ open, onClose, onSaved }: Props) {
|
||||
const [defs, setDefs] = useState<AwardDef[]>([]);
|
||||
const [fields, setFields] = useState<string[]>([]);
|
||||
const [err, setErr] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setErr('');
|
||||
Promise.all([GetAwardDefs(), AwardFields()])
|
||||
.then(([d, f]) => { setDefs((d ?? []) as any); setFields((f ?? []) as any); })
|
||||
.catch((e) => setErr(String(e?.message ?? e)));
|
||||
}, [open]);
|
||||
|
||||
const patch = (i: number, p: Partial<AwardDef>) => setDefs((ds) => ds.map((d, j) => (j === i ? { ...d, ...p } : d)));
|
||||
const addAward = () => setDefs((ds) => [...ds, { code: 'NEW', name: 'New award', field: 'dxcc', pattern: '', dxcc_filter: null, confirm: ['lotw', 'qsl'], total: 0 }]);
|
||||
const removeAward = (i: number) => setDefs((ds) => ds.filter((_, j) => j !== i));
|
||||
|
||||
const toggleConfirm = (i: number, id: string) => {
|
||||
const cur = defs[i].confirm ?? [];
|
||||
patch(i, { confirm: cur.includes(id) ? cur.filter((c) => c !== id) : [...cur, id] });
|
||||
};
|
||||
|
||||
async function save() {
|
||||
setErr('');
|
||||
try {
|
||||
// Normalise codes (uppercase, no blanks).
|
||||
const clean = defs
|
||||
.filter((d) => d.code.trim())
|
||||
.map((d) => ({ ...d, code: d.code.trim().toUpperCase(), confirm: d.confirm ?? [] }));
|
||||
await SaveAwardDefs(clean as any);
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
try { setDefs((await ResetAwardDefs()) as any); } catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader className="px-6 py-4">
|
||||
<DialogTitle>Edit awards</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 pb-2">
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
Each award scans one QSO <strong>field</strong>. Leave <strong>pattern</strong> empty to use the whole field value,
|
||||
or enter a regular expression where <span className="font-mono">group 1</span> is the reference — e.g. scan
|
||||
the <span className="font-mono">note</span> field with <span className="font-mono">{'D(\\d{1,2}[AB]?)'}</span> so
|
||||
"D74" counts department 74.
|
||||
</p>
|
||||
{err && <div className="text-xs text-destructive mb-2">{err}</div>}
|
||||
|
||||
<div className="space-y-2 max-h-[55vh] overflow-auto pr-1">
|
||||
{defs.map((d, i) => (
|
||||
<div key={i} className="rounded-lg border border-border p-3 space-y-2 bg-card">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input className="h-8 w-24 font-mono font-semibold text-xs" value={d.code}
|
||||
onChange={(e) => patch(i, { code: e.target.value })} placeholder="CODE" />
|
||||
<Input className="h-8 flex-1 text-sm" value={d.name}
|
||||
onChange={(e) => patch(i, { name: e.target.value })} placeholder="Award name" />
|
||||
{d.builtin && <span className="text-[10px] text-muted-foreground border border-border rounded px-1.5 py-0.5">built-in</span>}
|
||||
<button className="text-muted-foreground hover:text-destructive" title="Remove" onClick={() => removeAward(i)}>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-[auto_1fr_auto_1fr] gap-x-3 gap-y-2 items-center text-xs">
|
||||
<label className="text-muted-foreground">Field</label>
|
||||
<Select value={d.field} onValueChange={(v) => patch(i, { field: v })}>
|
||||
<SelectTrigger className="h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent className="max-h-72">
|
||||
{fields.map((f) => <SelectItem key={f} value={f}>{f}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<label className="text-muted-foreground">Pattern</label>
|
||||
<Input className="h-8 font-mono text-xs" value={d.pattern}
|
||||
onChange={(e) => patch(i, { pattern: e.target.value })} placeholder="(optional regex, group 1 = ref)" />
|
||||
|
||||
<label className="text-muted-foreground">DXCC filter</label>
|
||||
<Input className="h-8 font-mono text-xs"
|
||||
value={(d.dxcc_filter ?? []).join(', ')}
|
||||
onChange={(e) => patch(i, { dxcc_filter: e.target.value.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isFinite(n)) })}
|
||||
placeholder="e.g. 227 (empty = any)" />
|
||||
<label className="text-muted-foreground">Total</label>
|
||||
<Input type="number" className="h-8 font-mono text-xs w-28" value={d.total}
|
||||
onChange={(e) => patch(i, { total: parseInt(e.target.value, 10) || 0 })} placeholder="0 = unknown" />
|
||||
|
||||
<label className="text-muted-foreground">Confirmed by</label>
|
||||
<div className="col-span-3 flex items-center gap-4">
|
||||
{CONFIRM_SRC.map((c) => (
|
||||
<label key={c.id} className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Checkbox checked={(d.confirm ?? []).includes(c.id)} onCheckedChange={() => toggleConfirm(i, c.id)} />
|
||||
{c.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" className="h-8 mt-2" onClick={addAward}>
|
||||
<Plus className="size-3.5 mr-1" /> Add award
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="px-6 py-4 bg-transparent border-t-0">
|
||||
<Button variant="ghost" onClick={reset}><RotateCcw className="size-3.5 mr-1" /> Reset to defaults</Button>
|
||||
<div className="flex-1" />
|
||||
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={save}><Save className="size-3.5 mr-1" /> Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user