9686b54b41
Seven radii were in circulation (4/5/6/8/12/16px + pill) with no rule for which went where; one toolbar row on /transactions mixed four shape languages. This locks a 4-tier ladder (design.md convention 16): - pill: interactive toolbar controls (buttons, chips, pickers, segmented controls, toolbar search, count nubs) - rounded-xl (12px): overlay tier: page panel, dialogs, slide-overs - rounded-lg (8px): cards, form fields, popover/menu content, boxes - rounded-sm (4px): nested leaves (menu items, checkboxes, kbd/code nubs) Changes: - New SegmentedControl primitive (pill-in-pill tablist, h-8) replaces the hand-rolled bg-muted/70 tablist copied across 11 files - New ToolbarSearch primitive (pill, h-8) adopted on 9 page toolbars; dialog/picker searches keep the rounded-lg Input - dialog.tsx 8px -> 12px, matching SettingsModal/slide-over/CommandPalette - ContextPicker chips at the shared h-8 toolbar height - ~300 rounded-md / bare rounded call sites remapped by role; auth icon tiles and the mobile nav sheet come down from 16px to 12px - rounded-md, bare rounded, rounded-2xl and rounded-[Npx] are dead vocabulary, enforced by a new off-ladder-radius check in check:guards Verified: lint 0 errors, 14422 unit tests pass, check:guards green, tsc clean on all changed files, sandbox screenshots of transactions/ bookkeeping/granskning toolbars and the Ny verifikation dialog. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
169 lines
5.9 KiB
TypeScript
169 lines
5.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { useTranslations } from 'next-intl'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { AlertTriangle, ArrowLeft, Loader2, MoreVertical, Trash2, Undo2 } from 'lucide-react'
|
|
import { formatDate } from '@/lib/utils'
|
|
import { periodLabelOf, type RunDetail } from './types'
|
|
|
|
const STATUS_VARIANTS: Record<string, 'default' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'> = {
|
|
draft: 'secondary',
|
|
review: 'warning',
|
|
approved: 'default',
|
|
paid: 'success',
|
|
booked: 'success',
|
|
corrected: 'outline',
|
|
}
|
|
|
|
interface RunHeaderProps {
|
|
run: RunDetail
|
|
canWrite: boolean
|
|
actionLoading: string | null
|
|
employeeCount: number
|
|
onDelete: () => void
|
|
onCorrect: () => void
|
|
}
|
|
|
|
export function RunHeader({ run, canWrite, actionLoading, employeeCount, onDelete, onCorrect }: RunHeaderProps) {
|
|
const t = useTranslations('salary_run')
|
|
const tSalary = useTranslations('salary')
|
|
const [correctOpen, setCorrectOpen] = useState(false)
|
|
const periodLabel = periodLabelOf(run)
|
|
|
|
const statusKey = `status_${run.status}`
|
|
const showMenu = canWrite && (run.status === 'draft' || run.status === 'booked')
|
|
|
|
return (
|
|
<div className="flex items-start gap-2">
|
|
<Button variant="ghost" size="icon" asChild className="h-8 w-8 -ml-2 shrink-0">
|
|
<Link href="/salary" aria-label={t('back_to_salary')}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<h1 className="font-display text-2xl tracking-tight truncate">
|
|
{t('title', { period: periodLabel })}
|
|
</h1>
|
|
<Badge variant={STATUS_VARIANTS[run.status] || 'secondary'} className="shrink-0">
|
|
{tSalary(statusKey)}
|
|
</Badge>
|
|
</div>
|
|
{/* Inline property row — Linear-style dot-separated metadata. */}
|
|
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
|
|
<span>
|
|
{t('payment_date_label')} {formatDate(run.payment_date)}
|
|
</span>
|
|
<span aria-hidden>·</span>
|
|
<span>{t('header_employees', { count: employeeCount })}</span>
|
|
{run.is_correction && run.corrects_run_id && (
|
|
<>
|
|
<span aria-hidden>·</span>
|
|
<Badge variant="secondary">{t('correction_badge')}</Badge>
|
|
<Link
|
|
href={`/salary/runs/${run.corrects_run_id}`}
|
|
className="underline underline-offset-2 hover:text-foreground"
|
|
>
|
|
{t('corrects_link', { period: periodLabel })}
|
|
</Link>
|
|
</>
|
|
)}
|
|
{run.status === 'corrected' && run.corrected_by_run_id && (
|
|
<>
|
|
<span aria-hidden>·</span>
|
|
<Link
|
|
href={`/salary/runs/${run.corrected_by_run_id}`}
|
|
className="underline underline-offset-2 hover:text-foreground"
|
|
>
|
|
{t('corrected_by_link')}
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
{showMenu && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" aria-label={t('more_actions')}>
|
|
{actionLoading === 'delete' || actionLoading === 'correct' ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<MoreVertical className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{run.status === 'draft' && (
|
|
<DropdownMenuItem
|
|
onClick={onDelete}
|
|
className="text-destructive focus:text-destructive"
|
|
>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
{t('action_delete_draft')}
|
|
</DropdownMenuItem>
|
|
)}
|
|
{run.status === 'booked' && (
|
|
<DropdownMenuItem onClick={() => setCorrectOpen(true)}>
|
|
<Undo2 className="mr-2 h-4 w-4" />
|
|
{t('action_correct')}
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
|
|
{/* Correction confirm — storno per BFL 5 kap. 5 §, nothing is deleted. */}
|
|
<Dialog open={correctOpen} onOpenChange={setCorrectOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{t('correct_dialog_title')}</DialogTitle>
|
|
<DialogDescription className="space-y-3 pt-2 text-left">
|
|
<span className="block">{t('correct_dialog_body', { period: periodLabel })}</span>
|
|
{run.agi_generated_at && (
|
|
<span className="flex items-start gap-2 rounded-lg border border-border p-3 text-xs">
|
|
<AlertTriangle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
|
<span>{t('correct_dialog_agi_warning')}</span>
|
|
</span>
|
|
)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setCorrectOpen(false)}>
|
|
{t('correct_dialog_cancel')}
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setCorrectOpen(false)
|
|
onCorrect()
|
|
}}
|
|
>
|
|
{t('correct_dialog_confirm')}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|