diff --git a/DECISIONS.md b/DECISIONS.md index b426d744..55345b76 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -369,3 +369,5 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-24] Declined compliance-bot ask to re-box the bolagsskattMissing warning (#1161): text-attn IS the locked house attention idiom (one ochre sentence, banners forbidden by design.md); PreviewStep keeps the inline action to the dispositions step, so salience + remediation path both remain. [2026-07-24] VAT RC checks proportional (0.5% + 1 kr tolerance) + latched stepper landing: the binary present/absent RC_BASIS_MISSING check cleared after one korrigering and hid a 38-voucher worklist behind "klart"; tolerance absorbs per-voucher basis rounding (moms/sats vs invoiced amount) without hiding a missing voucher; landing step latches once per period so a mid-work refetch cannot navigate the user off Kontrollera. [2026-07-25] Removed invented 6-month minimum for first räkenskapsår: BFL 3 kap 3 § sets no floor (Bolagsverket: "hur kort som helst", max 18 months); the check only existed for isFirstPeriod, exactly the case the law exempts, and blocked a customer shortening an autumn-registered first year to Dec 31. +[2026-07-25] Article EUR-price support bug: root cause was the edit dialog omitting currency from initialData (form defaulted SEK and PATCHed it back) plus kr-hardcoded formatCurrency calls; export gets a Valuta column + suffix-free decimalColumn instead of extending CURRENCY_FORMAT, importer Valuta detection deferred as follow-up to keep the diff scoped. +[2026-07-25] Reinstated article deactivation as an explicit PATCH active-toggle button on the detail page (support: odinaero.se) instead of reverting DELETE to soft-delete: 8a9a930f intentionally made DELETE hard-delete for unused articles, but that left invoice-referenced articles (ARTICLE_IN_USE) with no retire path; the old deactivate i18n keys were still in messages/ and are reused. diff --git a/app/(dashboard)/articles/[id]/page.tsx b/app/(dashboard)/articles/[id]/page.tsx index 9cf1d5fc..b52d91f0 100644 --- a/app/(dashboard)/articles/[id]/page.tsx +++ b/app/(dashboard)/articles/[id]/page.tsx @@ -18,6 +18,8 @@ import { import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message' import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog' import { + Archive, + ArchiveRestore, ArrowLeft, Package, Wrench, @@ -56,6 +58,7 @@ export default function ArticleDetailPage({ const [isEditOpen, setIsEditOpen] = useState(false) const [isUpdating, setIsUpdating] = useState(false) const [isDeleting, setIsDeleting] = useState(false) + const [isTogglingActive, setIsTogglingActive] = useState(false) const { dialogProps: confirmDialogProps, confirm: confirmAction } = useDestructiveConfirm() useEffect(() => { @@ -128,6 +131,46 @@ export default function ArticleDetailPage({ } } + // Soft retire/restore: the only path for articles already used on invoices, + // where hard delete is refused (ARTICLE_IN_USE) to keep invoice history. + async function handleToggleActive() { + if (!article) return + const deactivating = article.active + if (deactivating) { + const ok = await confirmAction({ + title: t('deactivate_confirm_title', { name: article.name }), + description: t('deactivate_confirm_description'), + confirmLabel: t('deactivate_confirm_label'), + variant: 'destructive', + }) + if (!ok) return + } + + setIsTogglingActive(true) + try { + const response = await fetch(`/api/articles/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ active: !article.active }), + }) + await throwOnStructuredError(response) + toast({ + title: deactivating ? t('deactivated_title') : t('activated_title'), + description: article.name, + }) + fetchArticle() + } catch (err) { + const body = (err as { body?: unknown }).body + toast({ + title: deactivating ? t('deactivate_failed_title') : t('activate_failed_title'), + description: getErrorMessage(body ?? err, { context: 'article', locale: errorLocale }), + variant: 'destructive', + }) + } finally { + setIsTogglingActive(false) + } + } + async function handleDelete() { if (!article) return const ok = await confirmAction({ @@ -217,6 +260,24 @@ export default function ArticleDetailPage({ {canWrite ? : } {t('edit')} +