fix(ui): migrate remaining native confirm() calls to DestructiveConfirmDialog (#1046)

Migrates the three remaining bare confirm() sites from issue #1038 to the
imperative useDestructiveConfirm() pattern already mounted on both pages:
resume-autosend and run-now on the recurring-invoices page, and unapprove
on the salary-run page (its dynamically assembled multi-line copy now
renders as paragraphs via whitespace-pre-line on DialogDescription).

Also adds a togglingId in-flight guard to togglePause, mirroring the
deletingId/runningId guards from PR #1036, so the pause/resume button
cannot fire a duplicate PATCH while one is pending.

Fixes #1038

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-17 11:51:00 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent a5e37d3510
commit f91536c86c
5 changed files with 59 additions and 24 deletions
+39 -21
View File
@@ -35,6 +35,7 @@ export default function RecurringInvoicesPage() {
const [schedules, setSchedules] = useState<ScheduleRow[]>([])
const [isLoading, setIsLoading] = useState(true)
const [runningId, setRunningId] = useState<string | null>(null)
const [togglingId, setTogglingId] = useState<string | null>(null)
const [deletingId, setDeletingId] = useState<string | null>(null)
const { canWrite } = useCanWrite()
const { toast } = useToast()
@@ -81,31 +82,41 @@ export default function RecurringInvoicesPage() {
}, [])
async function togglePause(s: ScheduleRow) {
// In-flight guard: the confirm dialog closes before the PATCH settles,
// so a second click would fire a duplicate request.
if (togglingId) return
const next = s.status === 'active' ? 'paused' : 'active'
// Reactivating an auto-send schedule resumes automatic emails to the
// customer, so make the user consciously confirm they mean to turn it on.
if (
next === 'active' &&
s.auto_send &&
!confirm(t('resume_autosend_confirm', { name: s.name }))
) {
return
if (next === 'active' && s.auto_send) {
const ok = await confirmAction({
title: t('resume_autosend_confirm_title'),
description: t('resume_autosend_confirm', { name: s.name }),
confirmLabel: t('resume'),
variant: 'warning',
})
if (!ok) return
}
const res = await fetch(`/api/invoices/recurring/${s.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: next }),
})
if (res.ok) {
toast({
title: next === 'paused' ? t('schedule_paused_title') : t('schedule_resumed_title'),
})
fetchSchedules()
} else {
toast({
title: t('schedule_update_failed_title'),
variant: 'destructive',
setTogglingId(s.id)
try {
const res = await fetch(`/api/invoices/recurring/${s.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: next }),
})
if (res.ok) {
toast({
title: next === 'paused' ? t('schedule_paused_title') : t('schedule_resumed_title'),
})
fetchSchedules()
} else {
toast({
title: t('schedule_update_failed_title'),
variant: 'destructive',
})
}
} finally {
setTogglingId(null)
}
}
@@ -113,7 +124,13 @@ export default function RecurringInvoicesPage() {
// In-flight guard: a second click while the request runs would create a
// duplicate invoice for the customer.
if (runningId) return
if (!confirm(t('run_now_confirm', { name: s.name }))) return
const ok = await confirmAction({
title: t('run_now_confirm_title'),
description: t('run_now_confirm', { name: s.name }),
confirmLabel: t('run_now'),
variant: 'warning',
})
if (!ok) return
setRunningId(s.id)
try {
const res = await fetch(`/api/invoices/recurring/${s.id}/run`, { method: 'POST' })
@@ -270,6 +287,7 @@ export default function RecurringInvoicesPage() {
<Button
variant="secondary"
size="sm"
disabled={togglingId !== null}
onClick={() => togglePause(s)}
>
{s.status === 'active' ? t('pause') : t('resume')}
+8 -2
View File
@@ -224,13 +224,19 @@ export default function SalaryRunPage({ params }: { params: Promise<{ id: string
// could be sitting at the bank, sent payslips, a generated AGI — so the
// confirm spells out exactly the ones that apply to this run. The API
// refuses outright once the AGI has been filed with Skatteverket.
function handleUnapprove() {
async function handleUnapprove() {
if (!run) return
const lines = [t('confirm_unapprove_intro')]
if (run.payment_file_generated_at) lines.push(t('confirm_unapprove_payment_file'))
if ((run.payslip_deliveries_summary?.sent ?? 0) > 0) lines.push(t('confirm_unapprove_payslips'))
if (run.agi_generated_at) lines.push(t('confirm_unapprove_agi'))
if (!confirm(lines.join('\n\n'))) return
const ok = await confirmAction({
title: t('confirm_unapprove_title'),
description: lines.join('\n\n'),
confirmLabel: t('action_unapprove'),
variant: 'warning',
})
if (!ok) return
handleAction('unapprove')
}
+6 -1
View File
@@ -69,7 +69,12 @@ export function DestructiveConfirmDialog({
</div>
<div className="space-y-1">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
{/* pre-line so callers can pass newline-separated paragraphs
(e.g. the salary unapprove confirm assembles its copy
dynamically); single-line descriptions render unchanged. */}
<DialogDescription className="whitespace-pre-line">
{description}
</DialogDescription>
</div>
</div>
</DialogHeader>
+3
View File
@@ -2960,9 +2960,11 @@
"delete_confirm_title": "Remove the schedule?",
"run_now": "Create invoice now",
"run_now_confirm": "Create an invoice for \"{name}\" now? If the schedule has automatic sending, it is emailed to the customer immediately. The next scheduled run is not affected.",
"run_now_confirm_title": "Create invoice now?",
"run_now_success_title": "Invoice created",
"run_now_failed_title": "Could not create invoice",
"resume_autosend_confirm": "Reactivate \"{name}\"? This resumes automatic monthly emails to the customer.",
"resume_autosend_confirm_title": "Resume automatic sending?",
"send_time": "at {time}"
},
"invoice_recurring_new": {
@@ -4952,6 +4954,7 @@
"approve_override_confirm": "Approve anyway",
"action_revert": "Back to draft",
"action_unapprove": "Recall approval",
"confirm_unapprove_title": "Recall approval?",
"confirm_unapprove_intro": "The approval is recalled and the run goes back to review.",
"confirm_unapprove_payment_file": "A payment file has been generated. If it has already been uploaded to the bank, the payment must be cancelled there — otherwise the old amounts will be paid out.",
"confirm_unapprove_payslips": "Payslips have already been sent and need to be re-sent after recalculation.",
+3
View File
@@ -2960,9 +2960,11 @@
"delete_confirm_title": "Ta bort schemat?",
"run_now": "Skapa faktura nu",
"run_now_confirm": "Skapa en faktura för \"{name}\" nu? Om schemat har automatiskt utskick skickas den direkt till kunden. Nästa schemalagda körning påverkas inte.",
"run_now_confirm_title": "Skapa faktura nu?",
"run_now_success_title": "Faktura skapad",
"run_now_failed_title": "Kunde inte skapa faktura",
"resume_autosend_confirm": "Aktivera \"{name}\" igen? Detta återupptar automatiska månatliga utskick till kunden.",
"resume_autosend_confirm_title": "Återuppta automatiska utskick?",
"send_time": "kl {time}"
},
"invoice_recurring_new": {
@@ -4952,6 +4954,7 @@
"approve_override_confirm": "Godkänn ändå",
"action_revert": "Tillbaka till utkast",
"action_unapprove": "Ångra godkännande",
"confirm_unapprove_title": "Ångra godkännande?",
"confirm_unapprove_intro": "Godkännandet återkallas och körningen går tillbaka till granskning.",
"confirm_unapprove_payment_file": "En betalfil har genererats. Om den redan laddats upp till banken måste betalningen makuleras där — annars betalas de gamla beloppen ut.",
"confirm_unapprove_payslips": "Lönebesked har redan skickats och behöver skickas om efter ny beräkning.",