620 lines
24 KiB
JavaScript
620 lines
24 KiB
JavaScript
import { useEffect, useState, useCallback } from 'react';
|
|
import { Plus, ChevronRight, Trash2 } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
|
|
AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction,
|
|
} from '@/components/ui/alert-dialog';
|
|
import {
|
|
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
|
} from '@/components/ui/select';
|
|
import { api } from '@/api';
|
|
import { cn } from '@/lib/utils';
|
|
import BillsTableInner from '@/components/BillsTableInner';
|
|
import BillModal from '@/components/BillModal';
|
|
|
|
const VISIBILITY_OPTIONS = [
|
|
{ value: 'default', label: 'Default', description: 'Use the app default behavior for inactive bill history.' },
|
|
{ value: 'all', label: 'Show all history', description: 'Show all historical tracker data for this inactive bill.' },
|
|
{ value: 'none', label: 'Show no history', description: 'Hide historical tracker data for this inactive bill.' },
|
|
{ value: 'ranges', label: 'Show only selected date ranges', description: 'Show history only for the ranges listed below.' },
|
|
];
|
|
const MONTH_OPTIONS = [
|
|
['1', 'Jan'], ['2', 'Feb'], ['3', 'Mar'], ['4', 'Apr'], ['5', 'May'], ['6', 'Jun'],
|
|
['7', 'Jul'], ['8', 'Aug'], ['9', 'Sep'], ['10', 'Oct'], ['11', 'Nov'], ['12', 'Dec'],
|
|
];
|
|
|
|
// BillModal is imported from @/components/BillModal
|
|
|
|
function blankRange() {
|
|
const year = String(new Date().getFullYear());
|
|
return {
|
|
_key: `new-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
start_year: year,
|
|
start_month: String(new Date().getMonth() + 1),
|
|
end_year: '',
|
|
end_month: '',
|
|
label: '',
|
|
};
|
|
}
|
|
|
|
function normalizeRange(range) {
|
|
return {
|
|
...range,
|
|
_key: range._key || String(range.id),
|
|
start_year: String(range.start_year ?? ''),
|
|
start_month: String(range.start_month ?? ''),
|
|
end_year: range.end_year == null ? '' : String(range.end_year),
|
|
end_month: range.end_month == null ? '' : String(range.end_month),
|
|
label: range.label || '',
|
|
};
|
|
}
|
|
|
|
function rangePayload(range) {
|
|
return {
|
|
start_year: parseInt(range.start_year, 10),
|
|
start_month: parseInt(range.start_month, 10),
|
|
end_year: range.end_year ? parseInt(range.end_year, 10) : null,
|
|
end_month: range.end_month ? parseInt(range.end_month, 10) : null,
|
|
label: range.label?.trim() || null,
|
|
};
|
|
}
|
|
|
|
function validateRange(range) {
|
|
const payload = rangePayload(range);
|
|
if (!Number.isInteger(payload.start_year) || payload.start_year < 2000 || payload.start_year > 2100) {
|
|
return 'Start year must be between 2000 and 2100.';
|
|
}
|
|
if (!Number.isInteger(payload.start_month) || payload.start_month < 1 || payload.start_month > 12) {
|
|
return 'Start month must be between 1 and 12.';
|
|
}
|
|
if ((payload.end_year == null) !== (payload.end_month == null)) {
|
|
return 'End year and end month must both be provided or both left blank.';
|
|
}
|
|
if (payload.end_year != null) {
|
|
if (payload.end_year < 2000 || payload.end_year > 2100) {
|
|
return 'End year must be between 2000 and 2100.';
|
|
}
|
|
if (payload.end_month < 1 || payload.end_month > 12) {
|
|
return 'End month must be between 1 and 12.';
|
|
}
|
|
if ((payload.end_year * 12 + payload.end_month) < (payload.start_year * 12 + payload.start_month)) {
|
|
return 'Range end must be on or after the start.';
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function HistoryVisibilityDialog({ bill, onClose, onSaved }) {
|
|
const [visibility, setVisibility] = useState(bill?.history_visibility || 'default');
|
|
const [ranges, setRanges] = useState([]);
|
|
const [deletedIds, setDeletedIds] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
setLoading(true);
|
|
api.billHistoryRanges(bill.id)
|
|
.then(data => {
|
|
if (!mounted) return;
|
|
setVisibility(data.history_visibility || bill.history_visibility || 'default');
|
|
setRanges((data.ranges || []).map(normalizeRange));
|
|
})
|
|
.catch(err => {
|
|
toast.error(err.message || 'Failed to load history visibility.');
|
|
onClose();
|
|
})
|
|
.finally(() => mounted && setLoading(false));
|
|
return () => { mounted = false; };
|
|
}, [bill, onClose]);
|
|
|
|
const updateRange = (key, patch) => {
|
|
setRanges(prev => prev.map(r => r._key === key ? { ...r, ...patch } : r));
|
|
};
|
|
|
|
const deleteRange = (range) => {
|
|
if (range.id) setDeletedIds(prev => [...prev, range.id]);
|
|
setRanges(prev => prev.filter(r => r._key !== range._key));
|
|
};
|
|
|
|
const save = async () => {
|
|
if (visibility === 'ranges') {
|
|
if (ranges.length === 0) {
|
|
toast.error('Add at least one date range or choose another visibility option.');
|
|
return;
|
|
}
|
|
const invalid = ranges.map(validateRange).find(Boolean);
|
|
if (invalid) {
|
|
toast.error(invalid);
|
|
return;
|
|
}
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
await api.updateBill(bill.id, { history_visibility: visibility });
|
|
for (const id of deletedIds) {
|
|
await api.deleteBillHistoryRange(bill.id, id);
|
|
}
|
|
if (visibility === 'ranges') {
|
|
for (const range of ranges) {
|
|
const payload = rangePayload(range);
|
|
if (range.id) {
|
|
await api.updateBillHistoryRange(bill.id, range.id, payload);
|
|
} else {
|
|
await api.createBillHistoryRange(bill.id, payload);
|
|
}
|
|
}
|
|
}
|
|
toast.success('History visibility saved.');
|
|
onSaved();
|
|
onClose();
|
|
} catch (err) {
|
|
toast.error(err.message || 'Failed to save history visibility.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open onOpenChange={v => { if (!v && !saving) onClose(); }}>
|
|
<DialogContent className="sm:max-w-4xl border-border/60 bg-card/95 backdrop-blur-xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base font-semibold tracking-tight">
|
|
History Visibility: {bill.name}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
{loading ? (
|
|
<div className="py-12 text-center text-sm text-muted-foreground">Loading history visibility...</div>
|
|
) : (
|
|
<div className="space-y-5">
|
|
<div className="rounded-lg border border-border/60 bg-muted/25 p-4">
|
|
<p className="text-sm font-medium">Inactive bill history</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
Choose whether this inactive bill should appear in historical tracker views. Active bill behavior is unchanged.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
{VISIBILITY_OPTIONS.map(option => (
|
|
<label
|
|
key={option.value}
|
|
className={cn(
|
|
'flex cursor-pointer gap-3 rounded-lg border px-4 py-3 transition-colors',
|
|
visibility === option.value
|
|
? 'border-primary/40 bg-primary/10'
|
|
: 'border-border/60 bg-muted/15 hover:bg-muted/30'
|
|
)}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="history_visibility"
|
|
value={option.value}
|
|
checked={visibility === option.value}
|
|
onChange={() => setVisibility(option.value)}
|
|
className="mt-1 h-4 w-4 accent-primary"
|
|
/>
|
|
<span>
|
|
<span className="block text-sm font-medium">{option.label}</span>
|
|
<span className="mt-0.5 block text-xs text-muted-foreground">{option.description}</span>
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
|
|
{visibility === 'ranges' && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium">Selected date ranges</p>
|
|
<p className="text-xs text-muted-foreground">End month/year are optional for open-ended ranges.</p>
|
|
</div>
|
|
<Button type="button" size="sm" variant="outline" onClick={() => setRanges(prev => [...prev, blankRange()])}>
|
|
<Plus className="h-4 w-4" />
|
|
Add range
|
|
</Button>
|
|
</div>
|
|
|
|
{ranges.length === 0 ? (
|
|
<div className="rounded-lg border border-dashed border-border px-4 py-8 text-center text-sm text-muted-foreground">
|
|
No ranges yet. Add a range to use selected history.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{ranges.map(range => (
|
|
<div key={range._key} className="grid gap-2 rounded-lg border border-border/60 bg-background/40 p-3 md:grid-cols-[1fr_120px_1fr_120px_1.2fr_auto] md:items-end">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Start year</Label>
|
|
<Input
|
|
type="number"
|
|
min="2000"
|
|
max="2100"
|
|
value={range.start_year}
|
|
onChange={e => updateRange(range._key, { start_year: e.target.value })}
|
|
className="h-8"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Start month</Label>
|
|
<Select value={range.start_month} onValueChange={v => updateRange(range._key, { start_month: v })}>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{MONTH_OPTIONS.map(([value, label]) => <SelectItem key={value} value={value}>{label}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px] uppercase tracking-wider text-muted-foreground">End year</Label>
|
|
<Input
|
|
type="number"
|
|
min="2000"
|
|
max="2100"
|
|
placeholder="Open"
|
|
value={range.end_year}
|
|
onChange={e => updateRange(range._key, { end_year: e.target.value })}
|
|
className="h-8"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px] uppercase tracking-wider text-muted-foreground">End month</Label>
|
|
<Select value={range.end_month || 'open'} onValueChange={v => updateRange(range._key, { end_month: v === 'open' ? '' : v })}>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="open">Open</SelectItem>
|
|
{MONTH_OPTIONS.map(([value, label]) => <SelectItem key={value} value={value}>{label}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Label</Label>
|
|
<Input
|
|
value={range.label}
|
|
onChange={e => updateRange(range._key, { label: e.target.value })}
|
|
placeholder="Optional"
|
|
className="h-8"
|
|
/>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
onClick={() => deleteRange(range)}
|
|
aria-label="Delete history range"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter className="mt-2">
|
|
<Button type="button" variant="ghost" disabled={saving} onClick={onClose} className="text-xs">
|
|
Cancel
|
|
</Button>
|
|
<Button type="button" disabled={saving || loading} onClick={save} className="text-xs">
|
|
{saving ? 'Saving...' : 'Save History Visibility'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
// ── Bills Page ─────────────────────────────────────────────────────────────
|
|
export default function BillsPage() {
|
|
const [bills, setBills] = useState([]);
|
|
const [categories, setCategories] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showInactive, setShowInactive] = useState(false);
|
|
|
|
// modal state: null = closed, { bill: null } = add new, { bill: {...} } = edit
|
|
const [modal, setModal] = useState(null);
|
|
// Bill pending deactivation confirmation (AlertDialog replaces window.confirm)
|
|
const [deactivate, setDeactivate] = useState(null);
|
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
const [deleteConfirmed, setDeleteConfirmed] = useState(false);
|
|
const [deleteBusy, setDeleteBusy] = useState(false);
|
|
const [historyTarget, setHistoryTarget] = useState(null);
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
const [billsRes, catRes] = await Promise.all([
|
|
api.allBills(),
|
|
api.categories(),
|
|
]);
|
|
setBills(billsRes || []);
|
|
setCategories(catRes || []);
|
|
} catch (err) {
|
|
toast.error(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
async function handleEdit(billId) {
|
|
try {
|
|
const bill = await api.bill(billId);
|
|
setModal({ bill });
|
|
} catch (err) {
|
|
toast.error(err.message);
|
|
}
|
|
}
|
|
|
|
function handleToggle(bill) {
|
|
if (bill.active) {
|
|
// Prompt confirmation before deactivating
|
|
setDeactivate(bill);
|
|
} else {
|
|
doToggle(bill);
|
|
}
|
|
}
|
|
|
|
async function doToggle(bill) {
|
|
if (!bill) return;
|
|
try {
|
|
await api.updateBill(bill.id, { active: bill.active ? 0 : 1 });
|
|
toast.success(bill.active ? 'Bill deactivated' : 'Bill activated');
|
|
load();
|
|
} catch (err) {
|
|
toast.error(err.message);
|
|
}
|
|
}
|
|
|
|
function handleDeleteRequest(bill) {
|
|
setDeleteConfirmed(false);
|
|
setDeleteTarget(bill);
|
|
}
|
|
|
|
async function handleDeleteConfirmed() {
|
|
if (!deleteTarget || !deleteConfirmed) return;
|
|
setDeleteBusy(true);
|
|
try {
|
|
const bill = deleteTarget;
|
|
await api.deleteBill(bill.id);
|
|
setBills(prev => prev.filter(b => b.id !== bill.id));
|
|
toast.success(`"${bill.name}" deleted permanently`);
|
|
setDeleteTarget(null);
|
|
setDeleteConfirmed(false);
|
|
load();
|
|
} catch (err) {
|
|
toast.error(err.message || 'Failed to delete bill');
|
|
} finally {
|
|
setDeleteBusy(false);
|
|
}
|
|
}
|
|
|
|
async function handleDeleteAlternative() {
|
|
if (!deleteTarget) return;
|
|
const bill = deleteTarget;
|
|
setDeleteTarget(null);
|
|
setDeleteConfirmed(false);
|
|
await doToggle(bill);
|
|
}
|
|
|
|
const active = bills.filter(b => b.active);
|
|
const inactive = bills.filter(b => !b.active);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
|
|
{/* ── Header ── */}
|
|
<div className="flex items-end justify-between">
|
|
<div>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground mb-0.5">
|
|
Manage
|
|
</p>
|
|
<h1 className="text-2xl font-bold tracking-tight">Bills</h1>
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
{active.length} active
|
|
{inactive.length > 0 && (
|
|
<span className="text-muted-foreground/50"> · {inactive.length} inactive</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={() => setModal({ bill: null })}
|
|
className="h-9 px-4 gap-2 text-sm font-medium"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Add Bill
|
|
</Button>
|
|
</div>
|
|
|
|
{/* ── Active Bills ── */}
|
|
<div className="rounded-xl border border-border overflow-hidden bg-card">
|
|
<div className="flex items-center justify-between px-6 py-3 bg-muted/30 border-b border-border">
|
|
<span className="text-[11px] font-bold uppercase tracking-[0.12em] text-muted-foreground">
|
|
Active Bills
|
|
</span>
|
|
<span className="text-xs font-mono text-muted-foreground">{active.length}</span>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="py-16 text-center text-sm text-muted-foreground animate-pulse">
|
|
Loading bills…
|
|
</div>
|
|
) : active.length === 0 ? (
|
|
<div className="py-16 text-center text-sm text-muted-foreground">
|
|
No active bills.{' '}
|
|
<button
|
|
onClick={() => setModal({ bill: null })}
|
|
className="underline underline-offset-4 hover:text-foreground transition-colors"
|
|
>
|
|
Add your first bill
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<BillsTableInner
|
|
bills={active}
|
|
onEdit={handleEdit}
|
|
onToggle={handleToggle}
|
|
onDelete={handleDeleteRequest}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* ── Inactive Bills ── */}
|
|
{!loading && inactive.length > 0 && (
|
|
<div className="space-y-3">
|
|
|
|
<button
|
|
onClick={() => setShowInactive(v => !v)}
|
|
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
<ChevronRight className={cn(
|
|
'h-3 w-3 transition-transform duration-200',
|
|
showInactive && 'rotate-90',
|
|
)} />
|
|
<span className="uppercase tracking-[0.08em]">
|
|
{inactive.length} inactive {inactive.length === 1 ? 'bill' : 'bills'}
|
|
</span>
|
|
</button>
|
|
|
|
{showInactive && (
|
|
<div className="rounded-xl border border-border overflow-hidden bg-card">
|
|
<div className="flex items-center justify-between px-6 py-3 bg-muted/30 border-b border-border">
|
|
<span className="text-[11px] font-bold uppercase tracking-[0.12em] text-muted-foreground">
|
|
Inactive Bills
|
|
</span>
|
|
<span className="text-xs font-mono text-muted-foreground">{inactive.length}</span>
|
|
</div>
|
|
<BillsTableInner
|
|
bills={inactive}
|
|
onEdit={handleEdit}
|
|
onToggle={handleToggle}
|
|
onDelete={handleDeleteRequest}
|
|
onHistory={setHistoryTarget}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Bill Modal ── */}
|
|
{modal && (
|
|
<BillModal
|
|
bill={modal.bill}
|
|
categories={categories}
|
|
onClose={() => setModal(null)}
|
|
onSave={load}
|
|
/>
|
|
)}
|
|
|
|
{historyTarget && (
|
|
<HistoryVisibilityDialog
|
|
bill={historyTarget}
|
|
onClose={() => setHistoryTarget(null)}
|
|
onSaved={load}
|
|
/>
|
|
)}
|
|
|
|
{/* ── Deactivate confirmation (replaces window.confirm) ── */}
|
|
<AlertDialog open={!!deactivate} onOpenChange={open => { if (!open) setDeactivate(null); }}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Deactivate "{deactivate?.name}"?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This bill will be hidden from the tracker. You can reactivate it at any time.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={() => setDeactivate(null)}>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
onClick={() => { const b = deactivate; setDeactivate(null); doToggle(b); }}
|
|
>
|
|
Deactivate
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* ── Permanent delete confirmation ── */}
|
|
<AlertDialog
|
|
open={!!deleteTarget}
|
|
onOpenChange={open => {
|
|
if (!open && !deleteBusy) {
|
|
setDeleteTarget(null);
|
|
setDeleteConfirmed(false);
|
|
}
|
|
}}
|
|
>
|
|
<AlertDialogContent className="max-w-lg">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete "{deleteTarget?.name}" permanently?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This permanently deletes the bill and all data for this bill, including payments,
|
|
monthly history, notes, and history ranges. This cannot be undone.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
|
|
<div className="rounded-lg border border-destructive/25 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
Deactivate is the safer option if you only want to hide this bill from active tracking.
|
|
</div>
|
|
|
|
<label className="flex items-start gap-3 rounded-lg border border-border bg-muted/25 px-4 py-3 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
checked={deleteConfirmed}
|
|
onChange={e => setDeleteConfirmed(e.target.checked)}
|
|
disabled={deleteBusy}
|
|
className="mt-0.5 h-4 w-4 rounded border-input bg-input accent-destructive"
|
|
/>
|
|
<span>I understand this deletes all data for this bill and cannot be undone.</span>
|
|
</label>
|
|
|
|
<AlertDialogFooter className="sm:justify-between">
|
|
<AlertDialogCancel
|
|
disabled={deleteBusy}
|
|
className="border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
|
|
>
|
|
Cancel
|
|
</AlertDialogCancel>
|
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
disabled={deleteBusy}
|
|
onClick={handleDeleteAlternative}
|
|
>
|
|
{deleteTarget?.active ? 'Deactivate instead' : 'Activate instead'}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
disabled={!deleteConfirmed || deleteBusy}
|
|
onClick={handleDeleteConfirmed}
|
|
>
|
|
{deleteBusy ? 'Deleting…' : 'Delete permanently'}
|
|
</Button>
|
|
</div>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
</div>
|
|
);
|
|
}
|