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')
}