Files
accounted/lib/hooks/use-error-toast.ts
T
Jakob Wennberg ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

115 lines
3.4 KiB
TypeScript

'use client'
/**
* Client-side helper that turns a fetch failure into a Swedish toast with
* remediation hint and the X-Request-Id for support reference.
*
* Accepts:
* - the `{ error: {...} }` envelope produced by the route wrapper
* - a Response object (the helper reads it for you)
* - a raw Error / string (falls back to getErrorMessage)
*
* Usage:
* const showError = useErrorToast()
* const res = await fetch('/api/invoices/123/send', { method: 'POST' })
* if (!res.ok) {
* await showError(res, { context: 'invoice' })
* return
* }
*/
import { useToast } from '@/components/ui/use-toast'
import { getErrorMessage } from '@/lib/errors/get-error-message'
import type { ErrorEnvelope } from '@/lib/errors/get-structured-error'
type ErrorContext =
| 'invoice'
| 'supplier_invoice'
| 'customer'
| 'supplier'
| 'transaction'
| 'journal_entry'
| 'settings'
| 'auth'
| 'salary'
interface ShowErrorOptions {
context?: ErrorContext
/** Override the toast title (Swedish summary). Defaults to envelope.message. */
title?: string
}
interface NormalizedError {
message: string
remediation?: string
requestId?: string
code?: string
}
async function normalize(input: unknown): Promise<NormalizedError> {
// Response: try to read JSON body and X-Request-Id header
if (input instanceof Response) {
const requestId = input.headers.get('X-Request-Id') ?? undefined
let body: unknown = null
try {
body = await input.json()
} catch {
// ignore: body might be empty
}
const fromBody = readEnvelope(body)
return {
message: fromBody.message ?? getErrorMessage(body, { statusCode: input.status }),
remediation: fromBody.remediation,
requestId: fromBody.requestId ?? requestId,
code: fromBody.code,
}
}
const fromBody = readEnvelope(input)
if (fromBody.message) {
return fromBody
}
return { message: getErrorMessage(input) }
}
function readEnvelope(input: unknown): NormalizedError {
if (!input || typeof input !== 'object') return { message: '' }
const obj = input as Record<string, unknown>
const errObj = obj.error
if (errObj && typeof errObj === 'object') {
const e = errObj as Partial<ErrorEnvelope['error']>
return {
message: typeof e.message === 'string' ? e.message : '',
remediation:
e.remediation && typeof e.remediation === 'object'
? (e.remediation as { description?: string }).description
: undefined,
requestId: typeof e.requestId === 'string' ? e.requestId : undefined,
code: typeof e.code === 'string' ? e.code : undefined,
}
}
return { message: '' }
}
export function useErrorToast() {
const { toast } = useToast()
return async function showError(input: unknown, options: ShowErrorOptions = {}) {
const norm = await normalize(input)
const title = options.title ?? norm.message ?? getErrorMessage(input, { context: options.context })
const descriptionParts: string[] = []
if (norm.remediation) descriptionParts.push(norm.remediation)
if (norm.requestId) descriptionParts.push(`Felreferens: ${norm.requestId}`)
if (process.env.NODE_ENV !== 'production' && norm.code) {
descriptionParts.push(`Kod: ${norm.code}`)
}
toast({
variant: 'destructive',
title,
description: descriptionParts.length > 0 ? descriptionParts.join(' · ') : undefined,
})
}
}