feat: add user locale preference to user_preferences table (#555)
* feat: add user locale preference to user_preferences table
- Introduced a new column 'locale' in the user_preferences table to store per-user UI language preferences.
- Added a CHECK constraint to ensure only supported locales ('sv', 'en') are allowed.
- Triggered a schema reload notification for the changes.
chore: declare CSS module support in TypeScript
- Added a declaration for CSS modules in globals.d.ts to enable TypeScript support for importing CSS files.
* feat: add Swish as an invoice payment method in company settings
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
@@ -32,7 +33,7 @@ interface EmptyStateProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* EmptyState - Visar ett vänligt meddelande när det inte finns någon data
|
||||
* EmptyState — friendly placeholder shown when there is no data.
|
||||
*/
|
||||
export function EmptyState({
|
||||
icon: Icon,
|
||||
@@ -47,6 +48,7 @@ export function EmptyState({
|
||||
className,
|
||||
children,
|
||||
}: EmptyStateProps) {
|
||||
const t = useTranslations('empty')
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center py-12 px-4 text-center', className)}>
|
||||
{Icon && (
|
||||
@@ -61,8 +63,8 @@ export function EmptyState({
|
||||
|
||||
{supportHint && (
|
||||
<div className="mb-6">
|
||||
<SupportLink variant="muted" subject="Behöver hjälp att komma igång">
|
||||
Behöver du hjälp? Kontakta support
|
||||
<SupportLink variant="muted" subject={t('support_hint_subject')}>
|
||||
{t('support_hint_label')}
|
||||
</SupportLink>
|
||||
</div>
|
||||
)}
|
||||
@@ -95,27 +97,29 @@ export function EmptyState({
|
||||
)
|
||||
}
|
||||
|
||||
// Förkonfigurerade tomma tillstånd för vanliga sidor
|
||||
// Preset empty states for common pages
|
||||
|
||||
export function EmptyInvoices() {
|
||||
const t = useTranslations('empty')
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Receipt}
|
||||
title="Inga fakturor ännu"
|
||||
description="Skapa din första faktura på under 60 sekunder. Vi fyller i dina uppgifter automatiskt."
|
||||
actionLabel="Skapa faktura"
|
||||
title={t('preset_invoices_title')}
|
||||
description={t('preset_invoices_description')}
|
||||
actionLabel={t('preset_invoices_action')}
|
||||
actionHref="/invoices/new"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function EmptyCustomers({ onAction }: { onAction?: () => void } = {}) {
|
||||
const t = useTranslations('empty')
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="Inga kunder ännu"
|
||||
description="Lägg till dina kunder för att enkelt skapa fakturor och hålla koll på betalningar."
|
||||
actionLabel="Lägg till kund"
|
||||
title={t('preset_customers_title')}
|
||||
description={t('preset_customers_description')}
|
||||
actionLabel={t('preset_customers_action')}
|
||||
actionHref={onAction ? undefined : '/customers/new'}
|
||||
onAction={onAction}
|
||||
/>
|
||||
@@ -123,12 +127,13 @@ export function EmptyCustomers({ onAction }: { onAction?: () => void } = {}) {
|
||||
}
|
||||
|
||||
export function EmptyTransactions() {
|
||||
const t = useTranslations('empty')
|
||||
return (
|
||||
<EmptyState
|
||||
icon={ArrowLeftRight}
|
||||
title="Inga transaktioner"
|
||||
description="Importera kontoutdrag från din bank för att automatiskt bokföra och få koll på ekonomin."
|
||||
actionLabel="Importera transaktioner"
|
||||
title={t('preset_transactions_title')}
|
||||
description={t('preset_transactions_description')}
|
||||
actionLabel={t('preset_transactions_action')}
|
||||
actionHref="/import"
|
||||
supportHint
|
||||
/>
|
||||
@@ -136,34 +141,37 @@ export function EmptyTransactions() {
|
||||
}
|
||||
|
||||
export function EmptyReceipts() {
|
||||
const t = useTranslations('empty')
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Camera}
|
||||
title="Inga kvitton"
|
||||
description="Ta en bild på ett kvitto för automatisk avläsning och bokföring. Vi sköter resten!"
|
||||
actionLabel="Skanna kvitto"
|
||||
title={t('preset_receipts_title')}
|
||||
description={t('preset_receipts_description')}
|
||||
actionLabel={t('preset_receipts_action')}
|
||||
actionHref="/receipts/scan"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function EmptyDeadlines() {
|
||||
const t = useTranslations('empty')
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Calendar}
|
||||
title="Inga kommande deadlines"
|
||||
description="Bra jobbat! Du har inga omedelbara deadlines att ta hand om."
|
||||
title={t('preset_deadlines_title')}
|
||||
description={t('preset_deadlines_description')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoBankConnected() {
|
||||
const t = useTranslations('empty')
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Building2}
|
||||
title="Inga transaktioner importerade"
|
||||
description="Importera kontoutdrag från din bank för att automatiskt bokföra och få bättre koll på ekonomin."
|
||||
actionLabel="Importera transaktioner"
|
||||
title={t('preset_no_bank_title')}
|
||||
description={t('preset_no_bank_description')}
|
||||
actionLabel={t('preset_no_bank_action')}
|
||||
actionHref="/import"
|
||||
supportHint
|
||||
/>
|
||||
@@ -171,14 +179,15 @@ export function NoBankConnected() {
|
||||
}
|
||||
|
||||
export function EmptyReports() {
|
||||
const t = useTranslations('empty')
|
||||
return (
|
||||
<EmptyState
|
||||
icon={FileText}
|
||||
title="Inga rapporter tillgängliga"
|
||||
description="Rapporter genereras automatiskt när du har tillräckligt med data. Börja med att skapa fakturor eller importera transaktioner."
|
||||
actionLabel="Skapa faktura"
|
||||
title={t('preset_reports_title')}
|
||||
description={t('preset_reports_description')}
|
||||
actionLabel={t('preset_reports_action')}
|
||||
actionHref="/invoices/new"
|
||||
secondaryActionLabel="Importera transaktioner"
|
||||
secondaryActionLabel={t('preset_reports_secondary')}
|
||||
secondaryActionHref="/import"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getBranding } from '@/lib/branding/service'
|
||||
@@ -21,41 +22,37 @@ interface RetentionNoticeProps {
|
||||
* hides it from the UI and anonymizes PII where applicable.
|
||||
*/
|
||||
export function RetentionNotice({ variant, className }: RetentionNoticeProps) {
|
||||
const t = useTranslations('retention_notice')
|
||||
const { appName } = getBranding()
|
||||
const copy =
|
||||
variant === 'company'
|
||||
? {
|
||||
title: 'Bokföringen behålls i 7 år',
|
||||
title: t('company_title'),
|
||||
body: (
|
||||
<>
|
||||
Enligt bokföringslagen (BFL 7 kap. 2§) sparas räkenskapsinformation i 7 år.
|
||||
När du raderar företaget döljs det i {appName.toLowerCase()}, men verifikationer, dokument och
|
||||
bokföring behålls säkert tills lagkravet löpt ut. Du kan{' '}
|
||||
{t('company_body_prefix', { appName: appName.toLowerCase() })}
|
||||
<Link
|
||||
href="/settings/backup"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
ladda ner en säkerhetsbackup
|
||||
</Link>{' '}
|
||||
innan du fortsätter.
|
||||
{t('company_body_link')}
|
||||
</Link>
|
||||
{t('company_body_suffix')}
|
||||
</>
|
||||
),
|
||||
}
|
||||
: {
|
||||
title: 'Kontoraderingen är permanent',
|
||||
title: t('account_title'),
|
||||
body: (
|
||||
<>
|
||||
Ditt konto avidentifieras och du loggas ut från alla enheter.
|
||||
Räkenskapsinformation från företag du ägt behålls säkert i 7 år enligt BFL 7
|
||||
kap. 2§. Du kan inte skapa ett nytt konto med samma e-postadress — kontakta
|
||||
support om du vill återaktivera kontot i framtiden. Ladda gärna ner en{' '}
|
||||
{t('account_body_prefix')}
|
||||
<Link
|
||||
href="/settings/backup"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
säkerhetsbackup
|
||||
</Link>{' '}
|
||||
innan du fortsätter.
|
||||
{t('account_body_link')}
|
||||
</Link>
|
||||
{t('account_body_suffix')}
|
||||
</>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Mail, Loader2, Send } from 'lucide-react'
|
||||
import {
|
||||
@@ -31,6 +32,7 @@ export function SupportLink({
|
||||
children,
|
||||
className,
|
||||
}: SupportLinkProps) {
|
||||
const t = useTranslations('support_link')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [isSending, setIsSending] = useState(false)
|
||||
@@ -57,8 +59,8 @@ export function SupportLink({
|
||||
}, 2000)
|
||||
} else {
|
||||
toast({
|
||||
title: 'Kunde inte skicka',
|
||||
description: result.error || 'Försök igen.',
|
||||
title: t('send_failed_title'),
|
||||
description: result.error || t('send_failed_fallback'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -82,7 +84,7 @@ export function SupportLink({
|
||||
)}
|
||||
>
|
||||
<Mail className="h-3 w-3" />
|
||||
{children ?? 'Kontakta support'}
|
||||
{children ?? t('default_label')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
@@ -92,7 +94,7 @@ export function SupportLink({
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children ?? 'Kontakta support'}
|
||||
{children ?? t('default_label')}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -101,9 +103,9 @@ export function SupportLink({
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Kontakta support</DialogTitle>
|
||||
<DialogTitle>{t('dialog_title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Beskriv ditt ärende så återkommer vi så snart vi kan.
|
||||
{t('dialog_description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -112,21 +114,21 @@ export function SupportLink({
|
||||
<div className="p-3 rounded-full bg-success/10">
|
||||
<Send className="h-6 w-6 text-success" />
|
||||
</div>
|
||||
<p className="text-sm font-medium">Tack! Vi har mottagit ditt meddelande.</p>
|
||||
<p className="text-sm font-medium">{t('thanks')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Beskriv vad du behöver hjälp med..."
|
||||
placeholder={t('placeholder')}
|
||||
className="min-h-[120px] resize-none"
|
||||
maxLength={5000}
|
||||
disabled={isSending}
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
{message.length}/5000 tecken
|
||||
{t('char_count', { count: message.length })}
|
||||
</p>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
@@ -135,7 +137,7 @@ export function SupportLink({
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={isSending}
|
||||
>
|
||||
Avbryt
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -144,12 +146,12 @@ export function SupportLink({
|
||||
{isSending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Skickar...
|
||||
{t('sending')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
Skicka
|
||||
{t('send')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user