style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup (#835)
* style(ui): system-wide UX/UI polish pass — design-system conformance + copy cleanup Multi-agent scan of all 404 UI files against the locked design system, then 141 verified surgical fixes across 109 files (net -32 lines): - Remove forbidden elevation/motion: shadow-* and rounded-xl on cards, active:scale bounce, hover:shadow on list items, transition-all -> transition-colors. - Drop font-medium from single-weight Hedvig display headings/numerals. - Replace raw rainbow Tailwind status colors with Badge variants / brand tokens / neutral surfaces (achromatic chrome, semantic colors stay data-only). - Route raw dates through formatDate(), hand-rolled currency through formatCurrency(), add tabular-nums to financial figures; text-gray-* -> text-foreground tokens. - Swap hand-rolled skeletons for the Skeleton primitive; off-scale spacing -> token scale. - Fix copy: mislabeled "Leverantörsfakturor" -> "Utgifter" on bank-import outflow total, collapse no-op identical-branch ternaries, broken Swedish diacritics (mojibake), correct mismatch-password toast, correct supplier currency-field label. - Remove PII-leaking debug console.log on register, stray console.logs. Verified: tsc clean on all changed files, eslint clean, production build passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth): sanitize residual error logs in register flow Follow-up to PR review (compliance swarm V16 / GDPR Art.5(1)(f)): the remaining console.error calls in the register flow passed raw error objects, which Supabase may populate with PII (email) in nested fields. Log only sanitized message strings instead. 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
a8072f6423
commit
b800dcd403
@@ -276,7 +276,7 @@ function LoginPageContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<p className="text-sm text-muted-foreground text-center leading-relaxed">
|
||||
{showResetPassword ? tAuth('email_sent_hint_reset') : tAuth('email_sent_hint_login')}
|
||||
</p>
|
||||
@@ -327,7 +327,7 @@ function LoginPageContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
<form onSubmit={handleResetPassword} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">{tAuth('email_label')}</Label>
|
||||
@@ -382,7 +382,7 @@ function LoginPageContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
{callbackError === 'auth_error' && (
|
||||
<div className="mb-5 rounded-lg border border-destructive/30 bg-destructive/5 p-4">
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
|
||||
@@ -188,7 +188,7 @@ function MfaEnrollContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border bg-muted/50 p-4">
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
@@ -242,7 +242,7 @@ function MfaEnrollContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6 space-y-6" style={{ boxShadow: 'var(--shadow-md)' }}>
|
||||
<div className="rounded-lg border bg-card p-6 space-y-6">
|
||||
{/* QR Code */}
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
|
||||
@@ -171,7 +171,7 @@ function MfaVerifyContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
<form onSubmit={handleVerify} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code">{t('verify_code_label')}</Label>
|
||||
|
||||
@@ -140,7 +140,7 @@ function RegisterPageContent() {
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('[register] BankID verifyOtp failed', error)
|
||||
console.error('[register] BankID verifyOtp failed', error.message)
|
||||
toast({
|
||||
title: t('register_failed_complete'),
|
||||
description: getErrorMessage(error, { context: 'auth', locale: errorLocale }),
|
||||
@@ -152,7 +152,7 @@ function RegisterPageContent() {
|
||||
router.push('/select-company')
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error('[register] BankID signup error', error)
|
||||
console.error('[register] BankID signup error', error instanceof Error ? error.message : String(error))
|
||||
toast({
|
||||
title: t('register_failed_title'),
|
||||
description: getErrorMessage(error, { context: 'auth', locale: errorLocale }),
|
||||
@@ -201,14 +201,6 @@ function RegisterPageContent() {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[register] attempting signUp', {
|
||||
email: emailValue,
|
||||
hasPassword: !!passwordValue,
|
||||
passwordLength: passwordValue.length,
|
||||
redirectTo: `${window.location.origin}/auth/callback`,
|
||||
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
})
|
||||
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email: emailValue,
|
||||
password: passwordValue,
|
||||
@@ -218,15 +210,7 @@ function RegisterPageContent() {
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('[register] signUp error', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.status,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
cause: error.cause,
|
||||
fullError: JSON.stringify(error, Object.getOwnPropertyNames(error)),
|
||||
})
|
||||
console.error('[register] signUp error', error.message)
|
||||
toast({
|
||||
title: t('register_failed_title'),
|
||||
description: getErrorMessage(error, { context: 'auth', locale: errorLocale }),
|
||||
@@ -235,16 +219,6 @@ function RegisterPageContent() {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[register] signUp response', {
|
||||
userId: data.user?.id,
|
||||
email: data.user?.email,
|
||||
isAnonymous: data.user?.is_anonymous,
|
||||
identities: data.user?.identities?.length,
|
||||
hasSession: !!data.session,
|
||||
confirmationSentAt: data.user?.confirmation_sent_at,
|
||||
provider: data.user?.app_metadata?.provider,
|
||||
})
|
||||
|
||||
// If auto-confirmed (local dev), process invite immediately and redirect
|
||||
if (data.session) {
|
||||
const cookieMatch = document.cookie.match(/gnubok-invite-token=([^;]+)/)
|
||||
@@ -260,19 +234,11 @@ function RegisterPageContent() {
|
||||
|
||||
if (res.ok) {
|
||||
document.cookie = 'gnubok-invite-token=; path=/; max-age=0'
|
||||
console.log('[register] invite accepted after auto-confirm — redirecting')
|
||||
window.location.href = '/'
|
||||
return
|
||||
}
|
||||
|
||||
// Log the error response so we can diagnose invite failures
|
||||
const errBody = await res.json().catch(() => ({}))
|
||||
console.error('[register] invite acceptance returned non-ok', {
|
||||
status: res.status,
|
||||
error: errBody.error,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[register] invite acceptance failed:', err)
|
||||
console.error('[register] invite acceptance failed:', err instanceof Error ? err.message : String(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,13 +260,7 @@ function RegisterPageContent() {
|
||||
setEmail(emailValue)
|
||||
setIsRegistered(true)
|
||||
} catch (error) {
|
||||
console.error('[register] unexpected exception', {
|
||||
error,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
type: typeof error,
|
||||
constructor: error?.constructor?.name,
|
||||
})
|
||||
console.error('[register] unexpected exception', error instanceof Error ? error.message : String(error))
|
||||
toast({
|
||||
title: t('register_failed_title'),
|
||||
description: getErrorMessage(error, { context: 'auth', locale: errorLocale }),
|
||||
@@ -329,7 +289,7 @@ function RegisterPageContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<p className="text-sm text-muted-foreground text-center leading-relaxed">
|
||||
{t('duplicate_hint')}
|
||||
</p>
|
||||
@@ -377,7 +337,7 @@ function RegisterPageContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-4">
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<p className="text-sm text-muted-foreground text-center leading-relaxed">
|
||||
{t('confirm_email_hint')}
|
||||
</p>
|
||||
@@ -416,7 +376,7 @@ function RegisterPageContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
{bankIdEnabled && !bankIdUser && (
|
||||
<>
|
||||
<div className="mb-5">
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function ResetPasswordPage() {
|
||||
if (password !== confirmPassword) {
|
||||
toast({
|
||||
title: t('mismatch_title'),
|
||||
description: t('weak_description'),
|
||||
description: t('mismatch_description'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
setIsLoading(false)
|
||||
@@ -102,7 +102,7 @@ export default function ResetPasswordPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card p-6" style={{ boxShadow: 'var(--shadow-md)' }}>
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
<form onSubmit={handleResetPassword} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">{t('new_password_label')}</Label>
|
||||
|
||||
@@ -189,7 +189,7 @@ export default function ArticleDetailPage({
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{article.name}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{article.name}</h1>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="secondary">{t(ARTICLE_TYPE_KEY[article.type])}</Badge>
|
||||
{article.article_number && (
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight font-mono">
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight font-mono">
|
||||
{formatVoucher(entry)}
|
||||
</h1>
|
||||
<JournalEntryStatusBadge entry={entry} />
|
||||
@@ -366,7 +366,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
|
||||
{entry.committed_at && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">{t('field_posted_at')}</span>
|
||||
<span>{new Date(entry.committed_at).toLocaleDateString('sv-SE')}</span>
|
||||
<span>{formatDate(entry.committed_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -808,7 +808,7 @@ function ManualEntryEditor({
|
||||
<div className="rounded-md border border-border p-3 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium">{template.name}</p>
|
||||
<Button variant="ghost" size="sm" onClick={onRemove} className="h-7 px-2">
|
||||
<Button variant="ghost" size="sm" onClick={onRemove} className="h-7 px-2" aria-label="Ta bort">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function CustomerDetailPage({
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{customer.name}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{customer.name}</h1>
|
||||
<Badge variant="secondary">{t(CUSTOMER_TYPE_KEY[customer.customer_type])}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DeadlineList } from '@/components/deadlines/DeadlineList'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
import { AlertTriangle, ArrowRight } from 'lucide-react'
|
||||
import { useCompany } from '@/contexts/CompanyContext'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import type { Deadline } from '@/types'
|
||||
|
||||
const supabase = createClient()
|
||||
@@ -224,7 +225,7 @@ export default function DeadlinesPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
<PageHeader title={t('title')} />
|
||||
|
||||
{/* Overdue invoices alert */}
|
||||
@@ -235,8 +236,8 @@ export default function DeadlinesPage() {
|
||||
<AlertTriangle className="h-4 w-4 text-destructive flex-shrink-0" />
|
||||
<p className="text-sm">
|
||||
<span className="font-medium">{t('overdue_invoices', { count: overdueInvoices.count })}</span>
|
||||
<span className="text-muted-foreground ml-1.5">
|
||||
{overdueInvoices.total.toLocaleString('sv-SE')} kr
|
||||
<span className="text-muted-foreground ml-1.5 tabular-nums">
|
||||
{formatCurrency(overdueInvoices.total)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function DashboardError({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
|
||||
<h2 className="text-xl font-semibold">Nagot gick fel</h2>
|
||||
<h2 className="text-xl font-semibold">Något gick fel</h2>
|
||||
<p className="text-muted-foreground text-sm max-w-md text-center">
|
||||
Ett oväntat fel uppstod. Försök igen eller{' '}
|
||||
<SupportLink variant="inline" subject="Oväntat fel">kontakta support</SupportLink>{' '}
|
||||
|
||||
@@ -210,12 +210,12 @@ const glossaryTerms: GlossaryTerm[] = [
|
||||
]
|
||||
|
||||
const categoryConfig = {
|
||||
skatt: { labelKey: 'category_skatt', icon: Calculator, color: 'bg-orange-500/10 text-orange-600' },
|
||||
moms: { labelKey: 'category_moms', icon: Receipt, color: 'bg-blue-500/10 text-blue-600' },
|
||||
faktura: { labelKey: 'category_faktura', icon: FileText, color: 'bg-success/10 text-success' },
|
||||
bokföring: { labelKey: 'category_bokforing', icon: BookOpen, color: 'bg-purple-500/10 text-purple-600' },
|
||||
bank: { labelKey: 'category_bank', icon: Building2, color: 'bg-pink-500/10 text-pink-600' },
|
||||
företag: { labelKey: 'category_foretag', icon: Building2, color: 'bg-cyan-500/10 text-cyan-600' },
|
||||
skatt: { labelKey: 'category_skatt', icon: Calculator, color: 'bg-secondary text-muted-foreground' },
|
||||
moms: { labelKey: 'category_moms', icon: Receipt, color: 'bg-secondary text-muted-foreground' },
|
||||
faktura: { labelKey: 'category_faktura', icon: FileText, color: 'bg-secondary text-muted-foreground' },
|
||||
bokföring: { labelKey: 'category_bokforing', icon: BookOpen, color: 'bg-secondary text-muted-foreground' },
|
||||
bank: { labelKey: 'category_bank', icon: Building2, color: 'bg-secondary text-muted-foreground' },
|
||||
företag: { labelKey: 'category_foretag', icon: Building2, color: 'bg-secondary text-muted-foreground' },
|
||||
}
|
||||
|
||||
function TermCard({ term, isExpanded, onToggle }: { term: GlossaryTerm; isExpanded: boolean; onToggle: () => void }) {
|
||||
@@ -224,7 +224,7 @@ function TermCard({ term, isExpanded, onToggle }: { term: GlossaryTerm; isExpand
|
||||
const CategoryIcon = config.icon
|
||||
|
||||
return (
|
||||
<Card className={cn('transition-all', isExpanded && 'ring-2 ring-primary/20')}>
|
||||
<Card className={cn('transition-colors', isExpanded && 'ring-2 ring-primary/20')}>
|
||||
<CardContent className="pt-4">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
|
||||
@@ -2164,7 +2164,7 @@ export default function ImportPage() {
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">
|
||||
{view === 'export' ? t('export_title') : t('title')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
@@ -2198,10 +2198,10 @@ export default function ImportPage() {
|
||||
tabIndex={isSandbox ? -1 : 0}
|
||||
aria-disabled={isSandbox}
|
||||
className={cn(
|
||||
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-all',
|
||||
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-colors',
|
||||
isSandbox
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
|
||||
: 'cursor-pointer hover:border-foreground/15'
|
||||
)}
|
||||
onClick={() => { if (!isSandbox) setMode('psd2') }}
|
||||
onKeyDown={(e) => { if (!isSandbox && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); setMode('psd2') } }}
|
||||
@@ -2231,10 +2231,10 @@ export default function ImportPage() {
|
||||
tabIndex={isSandbox ? -1 : 0}
|
||||
aria-disabled={isSandbox}
|
||||
className={cn(
|
||||
'group rounded-lg border bg-card p-5 transition-all',
|
||||
'group rounded-lg border bg-card p-5 transition-colors',
|
||||
isSandbox
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
|
||||
: 'cursor-pointer hover:border-foreground/15'
|
||||
)}
|
||||
onClick={() => { if (!isSandbox) setMode('migration') }}
|
||||
onKeyDown={(e) => { if (!isSandbox && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); setMode('migration') } }}
|
||||
@@ -2277,8 +2277,8 @@ export default function ImportPage() {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-all',
|
||||
'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
|
||||
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-colors',
|
||||
'cursor-pointer hover:border-foreground/15'
|
||||
)}
|
||||
onClick={() => setMode('bank')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('bank') } }}
|
||||
@@ -2307,8 +2307,8 @@ export default function ImportPage() {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-all',
|
||||
'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
|
||||
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-colors',
|
||||
'cursor-pointer hover:border-foreground/15'
|
||||
)}
|
||||
onClick={() => setMode('csv_data')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('csv_data') } }}
|
||||
@@ -2344,8 +2344,8 @@ export default function ImportPage() {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-all',
|
||||
'cursor-pointer hover:border-foreground/15 hover:shadow-[var(--shadow-sm)] active:scale-[0.998]'
|
||||
'group flex items-start gap-4 rounded-lg border bg-card p-5 transition-colors',
|
||||
'cursor-pointer hover:border-foreground/15'
|
||||
)}
|
||||
onClick={() => setMode('sie')}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMode('sie') } }}
|
||||
|
||||
@@ -155,7 +155,7 @@ export default function CreateCreditNotePage({ params }: { params: Promise<{ id:
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{t('title')}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t('subtitle', { number: invoice.invoice_number ?? '' })}
|
||||
</p>
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function KpiPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{t('title')}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
|
||||
<div className="flex gap-2">
|
||||
<KPISettingsDialog
|
||||
preferences={preferences}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function DashboardLoading() {
|
||||
{/* Metrics row */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="rounded-xl border border-border/60 bg-card p-4 space-y-2">
|
||||
<div key={i} className="rounded-lg border border-border bg-card p-4 space-y-2">
|
||||
<Skeleton className="h-3.5 w-20" />
|
||||
<Skeleton className="h-7 w-28" />
|
||||
<Skeleton className="h-3 w-16" />
|
||||
@@ -17,7 +17,7 @@ export default function DashboardLoading() {
|
||||
{/* Income / Expenses */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="rounded-xl border border-border/60 bg-card p-6 space-y-3">
|
||||
<div key={i} className="rounded-lg border border-border bg-card p-6 space-y-3">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
@@ -27,12 +27,12 @@ export default function DashboardLoading() {
|
||||
|
||||
{/* Alerts + deadlines */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-xl border border-border/60 bg-card p-4 space-y-3">
|
||||
<div className="rounded-lg border border-border bg-card p-4 space-y-3">
|
||||
<Skeleton className="h-5 w-24" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/60 bg-card p-4 space-y-3">
|
||||
<div className="rounded-lg border border-border bg-card p-4 space-y-3">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center justify-between">
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function EmployeeDetailPage({ params }: { params: Promise<{ id: s
|
||||
<Link href="/salary/employees" aria-label="Tillbaka till anställda"><ArrowLeft className="h-4 w-4" /></Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">
|
||||
{employee.first_name} {employee.last_name}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function NewEmployeePage() {
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<Link href="/salary/employees" aria-label="Tillbaka till anställda"><ArrowLeft className="h-4 w-4" /></Link>
|
||||
</Button>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Ny anställd</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">Ny anställd</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function EmployeesPage() {
|
||||
<Link href="/salary" aria-label={t('back_to_payroll')}><ArrowLeft className="h-4 w-4" /></Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{t('title')}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('registered_count', { count: employees.length })}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function SalaryRunEmployeeDetailPage({
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<div>
|
||||
<h1 className="font-serif text-2xl font-medium tracking-tight">
|
||||
<h1 className="font-display text-2xl tracking-tight">
|
||||
{employee.first_name} {employee.last_name}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground tabular-nums">
|
||||
@@ -312,7 +312,7 @@ function SummaryCard({ label, value, accent, overridden }: { label: string; valu
|
||||
|
||||
function AbsenceCount({ label, days }: { label: string; days: number }) {
|
||||
return (
|
||||
<div className="rounded-md border bg-muted/30 px-2.5 py-1.5">
|
||||
<div className="rounded-md border bg-muted/30 px-3 py-2">
|
||||
<div className="text-[11px] text-muted-foreground">{label}</div>
|
||||
<div className="text-sm font-medium tabular-nums">{days} dagar</div>
|
||||
</div>
|
||||
|
||||
@@ -387,7 +387,7 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
|
||||
<Link href="/salary" aria-label="Tillbaka till löner"><ArrowLeft className="h-4 w-4" /></Link>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">
|
||||
Lönekörning {periodLabel}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
@@ -426,7 +426,7 @@ export default function SalaryRunDetailPage({ params }: { params: Promise<{ id:
|
||||
<Card key={label}>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground mb-2">{label}</p>
|
||||
<p className={`font-display text-xl font-medium tabular-nums leading-tight ${accent ? 'text-success' : ''}`}>
|
||||
<p className={`font-display text-xl tabular-nums leading-tight ${accent ? 'text-success' : ''}`}>
|
||||
{formatCurrency(value)}
|
||||
</p>
|
||||
</CardContent>
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function NewSalaryRunPage() {
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<Link href="/salary" aria-label="Tillbaka till löner"><ArrowLeft className="h-4 w-4" /></Link>
|
||||
</Button>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Ny lönekörning</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">Ny lönekörning</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
|
||||
@@ -581,7 +581,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
{invoice.delivery_date && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">{t('delivery_date_label')}</span>
|
||||
<span>{invoice.delivery_date}</span>
|
||||
<span className="tabular-nums">{formatDate(invoice.delivery_date)}</span>
|
||||
</div>
|
||||
)}
|
||||
{invoice.payment_reference && (
|
||||
|
||||
@@ -1439,7 +1439,7 @@ export default function NewSupplierInvoicePage() {
|
||||
{/* Valuta & moms — kept inline with the line items because they
|
||||
drive how each row is interpreted. Hidden defaults (SEK +
|
||||
normal moms) collapse to nothing so most users don't see this. */}
|
||||
<div className="mb-5 pb-5 border-b grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="mb-4 pb-4 border-b grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">{t('currency_label')}</Label>
|
||||
<Controller
|
||||
|
||||
@@ -152,7 +152,7 @@ export default function SupplierDetailPage() {
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{supplier.name}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{supplier.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{supplierTypeLabels[supplier.supplier_type]}
|
||||
{supplier.org_number && t('org_number_inline', { number: supplier.org_number })}
|
||||
@@ -189,7 +189,7 @@ export default function SupplierDetailPage() {
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('outstanding')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(supplier.stats?.total_outstanding || 0)} kr</p>
|
||||
<p className="font-display text-2xl tabular-nums">{formatAmount(supplier.stats?.total_outstanding || 0)} kr</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -197,7 +197,7 @@ export default function SupplierDetailPage() {
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('total_paid')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(supplier.stats?.total_paid || 0)} kr</p>
|
||||
<p className="font-display text-2xl tabular-nums">{formatAmount(supplier.stats?.total_paid || 0)} kr</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -205,7 +205,7 @@ export default function SupplierDetailPage() {
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('invoice_count')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{supplier.stats?.invoice_count || 0}</p>
|
||||
<p className="font-display text-2xl tabular-nums">{supplier.stats?.invoice_count || 0}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { ReportExportMenu } from '@/components/reports/ReportExportMenu'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Plus, Search, Building2, Lock } from 'lucide-react'
|
||||
@@ -114,7 +115,7 @@ export default function SuppliersPage() {
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{t('title')}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight">{t('title')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
@@ -168,13 +169,13 @@ export default function SuppliersPage() {
|
||||
{isLoading ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i} className="animate-pulse">
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6 space-y-3">
|
||||
<div className="h-5 bg-muted rounded w-1/2" />
|
||||
<div className="h-3 bg-muted rounded w-2/3" />
|
||||
<Skeleton className="h-5 w-1/2" />
|
||||
<Skeleton className="h-3 w-2/3" />
|
||||
<div className="h-px bg-muted" />
|
||||
<div className="h-3 bg-muted rounded w-1/2" />
|
||||
<div className="h-3 bg-muted rounded w-1/3" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
<Skeleton className="h-3 w-1/3" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
@@ -206,7 +207,7 @@ export default function SuppliersPage() {
|
||||
const location = formatLocation(supplier)
|
||||
return (
|
||||
<Link key={supplier.id} href={`/suppliers/${supplier.id}`} className="group">
|
||||
<Card className="h-full cursor-pointer transition-all duration-150 hover:border-foreground/20 hover:shadow-sm motion-safe:active:scale-[0.99] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
<Card className="h-full cursor-pointer transition-colors duration-150 hover:border-foreground/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
<CardContent className="p-6 flex flex-col h-full">
|
||||
<div className="space-y-1 mb-4">
|
||||
<h3 className="text-[15px] font-semibold tracking-tight leading-tight truncate group-hover:text-primary transition-colors">
|
||||
@@ -217,7 +218,7 @@ export default function SuppliersPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl className="mt-auto space-y-1.5 text-sm border-t pt-3">
|
||||
<dl className="mt-auto space-y-2 text-sm border-t pt-3">
|
||||
{supplier.org_number && (
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<dt className="text-xs text-muted-foreground shrink-0">{t('label_org_number')}</dt>
|
||||
|
||||
@@ -2061,7 +2061,7 @@ export default function TransactionsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
{/* Status bar */}
|
||||
<TransactionStatusBar
|
||||
uncategorizedCount={totalUncategorizedCount ?? uncategorizedTransactions.length}
|
||||
@@ -2091,7 +2091,7 @@ export default function TransactionsPage() {
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-9 gap-1.5 px-3 text-sm">
|
||||
<Button variant="outline" size="sm" className="h-9 gap-2 px-3 text-sm">
|
||||
{mode === 'inbox'
|
||||
? `Att bokföra${(totalUncategorizedCount ?? uncategorizedTransactions.length) > 0 ? ` (${totalUncategorizedCount ?? uncategorizedTransactions.length})` : ''}`
|
||||
: 'Alla transaktioner'}
|
||||
@@ -2138,7 +2138,7 @@ export default function TransactionsPage() {
|
||||
</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-7 gap-1.5 px-2 text-xs">
|
||||
<Button variant="outline" size="sm" className="h-7 gap-2 px-2 text-xs">
|
||||
{sourceFilter === 'all'
|
||||
? t('source_all', { count: uncategorizedTransactions.length + skvUnmatched.length })
|
||||
: sourceFilter === 'bank'
|
||||
|
||||
@@ -46,7 +46,7 @@ export default async function OnboardingLayout({
|
||||
href="/settings/account"
|
||||
aria-label="Kontoinställningar"
|
||||
title="Kontoinställningar"
|
||||
className="fixed bottom-5 right-5 z-50 flex h-10 w-10 items-center justify-center rounded-full border border-border bg-background/80 text-muted-foreground shadow-sm backdrop-blur transition-colors hover:border-foreground/40 hover:text-foreground"
|
||||
className="fixed bottom-6 right-6 z-50 flex h-10 w-10 items-center justify-center rounded-full border border-border bg-background/80 text-muted-foreground shadow-sm backdrop-blur transition-colors hover:border-foreground/40 hover:text-foreground"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Link>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getBranding } from '@/lib/branding/service'
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return {
|
||||
title: `Personuppgiftsbitradesavtal - ${getBranding().appName}`,
|
||||
title: `Personuppgiftsbiträdesavtal - ${getBranding().appName}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function DPAPage() {
|
||||
<div className="max-w-3xl mx-auto space-y-8">
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="font-display text-3xl md:text-4xl tracking-tight text-foreground">
|
||||
Personuppgiftsbitradesavtal (DPA)
|
||||
Personuppgiftsbiträdesavtal (DPA)
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enligt GDPR Art. 28 · Senast uppdaterad: 2026-06-03
|
||||
@@ -112,7 +112,7 @@ export default function DPAPage() {
|
||||
åtkomst</li>
|
||||
<li><strong>Integritetskontroll:</strong> SHA-256 checksummor för alla dokument, med
|
||||
regelbunden verifiering</li>
|
||||
<li><strong>Revisionslogg:</strong> Alla ändringshandelser loggas automatiskt av databasen
|
||||
<li><strong>Revisionslogg:</strong> Alla ändringshändelser loggas automatiskt av databasen
|
||||
(ej redigerbara)</li>
|
||||
<li><strong>Oföränderlig bokföring:</strong> Bokförda verifikationer kan inte ändras eller
|
||||
raderas (databasutlösare)</li>
|
||||
@@ -243,7 +243,7 @@ export default function DPAPage() {
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Detta personuppgiftsbitradesavtal träder i kraft när du skapar ett konto på
|
||||
Detta personuppgiftsbiträdesavtal träder i kraft när du skapar ett konto på
|
||||
{' '}{appName} och gäller så länge du använder tjänsten. För frågor, kontakta oss
|
||||
på {privacyEmail}.
|
||||
</p>
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function InvoiceActionPage({ params }: { params: Promise<{ token:
|
||||
<div className="max-w-lg mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
<h1 className="text-2xl font-bold text-foreground mb-2">
|
||||
Betalningspåminnelse
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
@@ -268,7 +268,7 @@ export default function InvoiceActionPage({ params }: { params: Promise<{ token:
|
||||
onClick={() => handleAction('disputed')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<MessageSquare className="h-5 w-5 mr-3 text-orange-600" />
|
||||
<MessageSquare className="h-5 w-5 mr-3 text-muted-foreground" />
|
||||
<div className="text-left">
|
||||
<p className="font-medium">Kontakta avsändaren</p>
|
||||
<p className="text-sm text-muted-foreground font-normal">
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function PrivacyPolicyPage() {
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white py-12 px-4">
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">
|
||||
Integritetspolicy
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
@@ -69,7 +69,7 @@ export default function PrivacyPolicyPage() {
|
||||
</li>
|
||||
<li>
|
||||
<strong>Berättigat intresse (Art. 6.1f):</strong> Produktförbättringar, säkerhet och
|
||||
bedrägeriforbud.
|
||||
bedrägeribekämpning.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Samtycke (Art. 6.1a):</strong> För AI-baserade funktioner som skickar data
|
||||
|
||||
@@ -181,7 +181,6 @@ function NewCompanyContent() {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(LOG, 'created company', result.companyId)
|
||||
toast({
|
||||
title: t('toast_company_created'),
|
||||
description: t('toast_switched_to_new'),
|
||||
@@ -263,7 +262,7 @@ function NewCompanyContent() {
|
||||
</div>
|
||||
|
||||
<div key={`title-${currentStep}`} className="animate-fade-in">
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight leading-[1.1]">
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight leading-[1.1]">
|
||||
{stepInfo.title}
|
||||
</h1>
|
||||
<p className="text-white/40 mt-1.5 text-sm max-w-sm leading-relaxed">
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function InvitePage() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="animate-fade-in">
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight leading-[1.1]">
|
||||
<h1 className="font-display text-2xl md:text-3xl tracking-tight leading-[1.1]">
|
||||
{error ? t('header_invalid') : t('header_invited')}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user