Design critique: normalize daily-operator flows + UX improvements (#741)
* style(design): normalize daily-operator flows to locked design system Sweep the dashboard, transactions, reconciliation, invoicing and supplier flows for design-system violations (.claude/rules/design.md): - font-medium removed from Hedvig display headings/numerals - font-mono -> tabular-nums on monetary values (voucher ids stay mono) - raw Tailwind status colors -> Badge variants / muted-alert pattern - semantic colors removed from chrome backgrounds (deadline widgets, icon halos) - hand-rolled skeletons/empty-states -> Skeleton / EmptyState primitives - opacity-suffixed borders, the invisible warning-foreground count color, and shadow-sm/rounded-xl on non-overlay surfaces normalized The four files that also received UX changes carry their token fixes in the following commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ux): clearer dashboard CTA, match confidence, AI provenance, invoice actions Four high-impact UX fixes from the design critique (these files also carry their design-system token normalization): - Dashboard: render the next-best-action hero for every agent-built company, not only 'slim' nav density, so there is always one obvious next step instead of four equal-weight metric tiles. - Reconciliation: surface the match engine's 0-1 confidence as a graded strength badge (Stark / Trolig / Svag traff) in the shared verifikat picker rows and selected chip; drop the uninformative binary "Foreslagen traff" badge from the match dialog. - Supplier inbox: show AI-filled provenance per extracted field (a success dot that clears once the user verifies/edits the value) so misparsed amounts/dates get proofread before they post to an immutable verifikat. - Invoice detail: keep each status's primary action only in the header row; the sidebar "Status actions" card now holds secondary/reversible actions only (makulera, ta bort, skapa kreditnota, manual-send alternative), removing the duplicated CTAs and closing a viewer-permission gap on the old sidebar buttons. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ux): Tier-B medium design-critique fixes across daily-operator flows - Reconciliation: standardise the match-confirm verb on "Matcha" (was "Koppla" in MatchVoucherDialog) and replace the hand-rolled date <input>s in the bank reconciliation view with the Input primitive. - Supplier inbox: fold the two alternative bookings (Skapa leverantorsfaktura / Bokfor som verifikat) behind a single "Andra satt att bokfora" dropdown so the default path (Matcha mot transaktion) stays the lone primary action. - Duplicate-payment guard: demote the "Skapa ny verifikation anda" escape hatch to a ghost button so the safe "Koppla till befintlig" path dominates. - Onboarding: raise the "start fresh" escape hatch from a muted text link to a visible secondary button; normalise the checklist's off-scale spacing. - Supplier flows: finish the font-mono -> tabular-nums sweep on monetary values in the supplier-invoice detail / create / review surfaces (ids stay mono). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deadlines): keep overdue rows visually distinct (destructive chrome is allowed) The Tier-A normalization stripped all semantic-color row tints from the deadline widgets, but design.md exempts --destructive ("only --destructive survives in chrome"). Restore a subtle bg-destructive/5 on OVERDUE rows so missed tax/AGI deadlines (-> skattetillagg) stay noticeable in a list scan; action-needed (warning) rows stay clean since warning is data-only. Surfaced by the Swedish compliance review on #741. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): hide match-strength badge on already-matched verifikat Per the Swedish compliance review on #741: a green "Stark traff" confidence badge rendered alongside "Redan matchad" could visually nudge an accidental double-match of a posted verifikat (a BFL 5 kap audit-trail concern). Suppress the strength badge when linked_transaction_count > 0 so "Redan matchad" is the lone signal there; N:1 matching stays an explicit opt-in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
274fe0743f
commit
36e3f6ceb0
@@ -6,6 +6,13 @@ import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import AiFilledIndicator from '@/components/ui/ai-filled-indicator'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import {
|
||||
Inbox,
|
||||
@@ -24,6 +31,7 @@ import {
|
||||
Search,
|
||||
Circle,
|
||||
X,
|
||||
ChevronDown,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { cn, formatCurrency } from '@/lib/utils'
|
||||
@@ -664,7 +672,7 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className="xl:h-full flex flex-col rounded-lg border bg-card xl:overflow-hidden shadow-sm">
|
||||
<div className="xl:h-full flex flex-col rounded-lg border bg-card xl:overflow-hidden">
|
||||
{/* Top bar */}
|
||||
<header className="flex items-center justify-between gap-4 border-b px-4 py-2.5 flex-wrap">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@@ -1084,10 +1092,10 @@ function InboxRow({
|
||||
<AlertTriangle className="h-3 w-3 text-destructive shrink-0" aria-label="Fel vid bearbetning" />
|
||||
)}
|
||||
{isLinkedToTransaction && (
|
||||
<Link2 className="h-3 w-3 text-emerald-600 shrink-0" aria-label="Kopplad till transaktion" />
|
||||
<Link2 className="h-3 w-3 text-success shrink-0" aria-label="Kopplad till transaktion" />
|
||||
)}
|
||||
{isBooked && (
|
||||
<Check className="h-3 w-3 text-emerald-600 shrink-0" aria-label="Bokförd" />
|
||||
<Check className="h-3 w-3 text-success shrink-0" aria-label="Bokförd" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
@@ -1144,7 +1152,7 @@ export function DocumentPreview({
|
||||
<div className="h-full w-full p-4 flex items-start justify-center overflow-hidden">
|
||||
{docMime?.startsWith('image/') ? (
|
||||
// Image: frame hugs the image, capped at the parent's visible box.
|
||||
<div className="max-h-full max-w-3xl bg-background rounded-md border shadow-sm overflow-hidden flex">
|
||||
<div className="max-h-full max-w-3xl bg-background rounded-md border overflow-hidden flex">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={docUrl}
|
||||
@@ -1154,7 +1162,7 @@ export function DocumentPreview({
|
||||
</div>
|
||||
) : (
|
||||
// PDF: iframe needs explicit height — frame fills the available pane.
|
||||
<div className="h-full w-full max-w-3xl bg-background rounded-md border shadow-sm overflow-hidden">
|
||||
<div className="h-full w-full max-w-3xl bg-background rounded-md border overflow-hidden">
|
||||
<iframe src={docUrl} className="w-full h-full border-0" title="Underlag" />
|
||||
</div>
|
||||
)}
|
||||
@@ -1211,7 +1219,7 @@ function OnboardingCard({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative rounded-xl border bg-card shadow-sm',
|
||||
'relative rounded-lg border bg-card',
|
||||
compact ? 'mx-3 my-3 p-4 text-xs' : 'max-w-md mx-auto p-6'
|
||||
)}
|
||||
>
|
||||
@@ -1228,7 +1236,7 @@ function OnboardingCard({
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h2
|
||||
className={cn(
|
||||
'font-display font-medium tracking-tight',
|
||||
'font-display tracking-tight',
|
||||
compact ? 'text-sm' : 'text-lg'
|
||||
)}
|
||||
>
|
||||
@@ -1266,7 +1274,7 @@ function OnboardingCard({
|
||||
>
|
||||
<span className="shrink-0 mt-0.5">
|
||||
{isDone ? (
|
||||
<Check className={cn('text-emerald-600', compact ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
|
||||
<Check className={cn('text-success', compact ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
|
||||
) : isCurrent ? (
|
||||
<span
|
||||
className={cn(
|
||||
@@ -1647,19 +1655,36 @@ function FieldsRail({
|
||||
>
|
||||
Matcha mot transaktion
|
||||
</Button>
|
||||
<Link href={`/supplier-invoices/new?inbox_item_id=${item.id}`} className="block">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
Skapa leverantörsfaktura
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={onBookDirect}
|
||||
>
|
||||
Bokför som verifikat
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="w-full justify-between">
|
||||
Andra sätt att bokföra
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-64">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/supplier-invoices/new?inbox_item_id=${item.id}`}
|
||||
className="flex flex-col items-start gap-1"
|
||||
>
|
||||
<span>Skapa leverantörsfaktura</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
För leverantörsskulder du vill följa (periodisering).
|
||||
</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={onBookDirect}
|
||||
className="flex flex-col items-start gap-1"
|
||||
>
|
||||
<span>Bokför som verifikat</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
För underlag som inte är en leverantörsfaktura (bankavgift, utlägg).
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
@@ -1802,6 +1827,10 @@ export function EditableFieldsList({
|
||||
const [drafts, setDrafts] = useState<Record<FieldKey, string>>(() =>
|
||||
Object.fromEntries(FIELD_DEFS.map((f) => [f.key, readField(data, f.key)])) as Record<FieldKey, string>
|
||||
)
|
||||
// Per-field provenance: a populated field starts "AI-filled" (its value came
|
||||
// from the extraction) and flips to user-verified once the user edits it —
|
||||
// mirrors the create form's AiFilledIndicator. Reset when switching items.
|
||||
const [edited, setEdited] = useState<Partial<Record<FieldKey, boolean>>>({})
|
||||
const timersRef = useRef<Partial<Record<FieldKey, ReturnType<typeof setTimeout>>>>({})
|
||||
// Last-known server values per field. Used to detect when the server
|
||||
// normalises a value (currency upper-cased, whitespace trimmed) so we can
|
||||
@@ -1818,6 +1847,7 @@ export function EditableFieldsList({
|
||||
) as Record<FieldKey, string>
|
||||
setDrafts(seeded)
|
||||
lastServerRef.current = seeded
|
||||
setEdited({})
|
||||
return () => {
|
||||
for (const t of Object.values(timersRef.current)) {
|
||||
if (t) clearTimeout(t)
|
||||
@@ -1905,6 +1935,7 @@ export function EditableFieldsList({
|
||||
const onChange = useCallback(
|
||||
(key: FieldKey, raw: string) => {
|
||||
setDrafts((prev) => ({ ...prev, [key]: raw }))
|
||||
setEdited((prev) => (prev[key] ? prev : { ...prev, [key]: true }))
|
||||
const existing = timersRef.current[key]
|
||||
if (existing) clearTimeout(existing)
|
||||
timersRef.current[key] = setTimeout(() => {
|
||||
@@ -1935,12 +1966,18 @@ export function EditableFieldsList({
|
||||
<div className="space-y-2">
|
||||
{FIELD_DEFS.map((f) => (
|
||||
<div key={f.key} className="flex flex-col gap-0.5">
|
||||
<label
|
||||
htmlFor={`field-${f.key}`}
|
||||
className="text-[10px] uppercase tracking-wide text-muted-foreground/80"
|
||||
>
|
||||
{f.label}
|
||||
</label>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label
|
||||
htmlFor={`field-${f.key}`}
|
||||
className="text-[10px] uppercase tracking-wide text-muted-foreground/80"
|
||||
>
|
||||
{f.label}
|
||||
</label>
|
||||
<AiFilledIndicator
|
||||
active={drafts[f.key].trim() !== '' && !edited[f.key]}
|
||||
title="Ifyllt av AI — kontrollera mot dokumentet"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id={`field-${f.key}`}
|
||||
type={f.type}
|
||||
|
||||
Reference in New Issue
Block a user