feat: open-source under AGPL-3.0, redesign UI to grayscale palette, add uncategorize API, fix VAT account names
Add LICENSE (AGPL-3.0-or-later), CONTRIBUTING.md, SECURITY.md, DCO, and NOTICE files. Rewrite README for open-source audience with self-hosting instructions. Redesign color palette to grayscale chrome theme across all components. Add transaction uncategorize API route with tests. Fix VAT account name mismatches in migration 052. Improve import page with SIE file support and loading skeleton. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
29240738fa
commit
f3ec634a46
@@ -338,14 +338,14 @@ export default function JournalEntryForm({
|
||||
</td>
|
||||
<td
|
||||
className={`py-2 px-1 text-right ${
|
||||
isBalanced ? 'text-green-600' : 'text-red-600'
|
||||
isBalanced ? 'text-success' : 'text-destructive'
|
||||
}`}
|
||||
>
|
||||
{totalDebit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td
|
||||
className={`py-2 px-1 text-right ${
|
||||
isBalanced ? 'text-green-600' : 'text-red-600'
|
||||
isBalanced ? 'text-success' : 'text-destructive'
|
||||
}`}
|
||||
>
|
||||
{totalCredit.toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
@@ -378,7 +378,7 @@ export default function JournalEntryForm({
|
||||
)}
|
||||
|
||||
{!isBalanced && totalDebit > 0 && (
|
||||
<p className="text-sm text-red-600">
|
||||
<p className="text-sm text-destructive">
|
||||
Differens: {formatCurrency(Math.abs(totalDebit - totalCredit))}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function JournalEntryReviewContent({
|
||||
{(showBalanceBadge || (attachmentCount != null && attachmentCount > 0)) && (
|
||||
<div className="flex items-center gap-2">
|
||||
{showBalanceBadge && (
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
<Badge className="bg-success/10 text-success">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Debet = Kredit
|
||||
</Badge>
|
||||
@@ -117,8 +117,8 @@ export function JournalEntryReviewContent({
|
||||
<tfoot>
|
||||
<tr className="font-semibold border-t-2">
|
||||
<td colSpan={2} className="py-2">Summa</td>
|
||||
<td className="py-2 text-right text-green-600">{formatAmount(totalDebit)}</td>
|
||||
<td className="py-2 text-right text-green-600">{formatAmount(totalCredit)}</td>
|
||||
<td className="py-2 text-right text-success">{formatAmount(totalDebit)}</td>
|
||||
<td className="py-2 text-right text-success">{formatAmount(totalCredit)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@@ -4,11 +4,11 @@ import type { AgingBucketsArtifact } from '@/types/chat'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
|
||||
const BUCKET_COLORS = [
|
||||
'bg-green-500',
|
||||
'bg-yellow-400',
|
||||
'bg-orange-400',
|
||||
'bg-red-400',
|
||||
'bg-red-600',
|
||||
'bg-success',
|
||||
'bg-warning',
|
||||
'bg-warning/70',
|
||||
'bg-destructive/70',
|
||||
'bg-destructive',
|
||||
]
|
||||
|
||||
interface ChatAgingBucketsProps {
|
||||
|
||||
@@ -26,9 +26,9 @@ export function ChatKpiCards({ artifact }: ChatKpiCardsProps) {
|
||||
<span
|
||||
className={`flex items-center gap-0.5 text-xs ${
|
||||
card.trend === 'up'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
? 'text-success'
|
||||
: card.trend === 'down'
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
? 'text-destructive'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
<>
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:fixed md:inset-y-0 md:flex md:w-[232px] md:flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col border-r border-border/30 bg-card/80">
|
||||
<div className="flex min-h-0 flex-1 flex-col border-r border-border/30 bg-card/90">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto pt-7 pb-4">
|
||||
{/* Company name */}
|
||||
<div className="px-5 mb-8">
|
||||
@@ -139,7 +139,7 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
className={cn(
|
||||
'group flex items-center px-3 py-[7px] text-[13px] transition-colors duration-150 rounded-lg',
|
||||
active
|
||||
? 'bg-primary/8 text-foreground font-medium'
|
||||
? 'bg-primary/12 text-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
|
||||
)}
|
||||
>
|
||||
@@ -173,7 +173,7 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
className={cn(
|
||||
'group flex items-center px-3 py-[7px] text-[13px] transition-colors duration-150 rounded-lg',
|
||||
active
|
||||
? 'bg-primary/8 text-foreground font-medium'
|
||||
? 'bg-primary/12 text-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
|
||||
)}
|
||||
>
|
||||
@@ -217,7 +217,7 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
className={cn(
|
||||
'group flex items-center px-3 py-[7px] text-[13px] transition-colors duration-150 rounded-lg',
|
||||
active
|
||||
? 'bg-primary/8 text-foreground font-medium'
|
||||
? 'bg-primary/12 text-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
|
||||
)}
|
||||
>
|
||||
@@ -236,7 +236,7 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
</div>
|
||||
|
||||
{/* Logout button */}
|
||||
<div className="flex-shrink-0 px-3 py-3 border-t border-border/20">
|
||||
<div className="flex-shrink-0 px-3 py-3 border-t border-border/30">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-muted-foreground hover:text-foreground text-[13px] h-9 px-3"
|
||||
|
||||
@@ -188,8 +188,8 @@ export default function ReceiptInboxDetail({
|
||||
<p className="font-medium flex items-center gap-1">
|
||||
{receipt.matched_transaction_id ? (
|
||||
<>
|
||||
<LinkIcon className="h-3 w-3 text-green-600" />
|
||||
<span className="text-green-600">Matchad</span>
|
||||
<LinkIcon className="h-3 w-3 text-success" />
|
||||
<span className="text-success">Matchad</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Ingen match</span>
|
||||
|
||||
@@ -205,7 +205,7 @@ export default function InboxUploadZone({
|
||||
{fileProgress.map((fp) => (
|
||||
<div key={fp.name} className="flex items-center gap-2 text-sm">
|
||||
{fp.status === 'uploading' && <Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />}
|
||||
{fp.status === 'done' && <CheckCircle2 className="h-3.5 w-3.5 text-green-500" />}
|
||||
{fp.status === 'done' && <CheckCircle2 className="h-3.5 w-3.5 text-success" />}
|
||||
{fp.status === 'error' && <AlertCircle className="h-3.5 w-3.5 text-destructive" />}
|
||||
<span className={cn(
|
||||
'truncate',
|
||||
|
||||
@@ -33,8 +33,8 @@ export default function ConfirmDeleteDialog({
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-red-100 dark:bg-red-950/30">
|
||||
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
|
||||
@@ -213,8 +213,8 @@ export default function CsvImportWizard({
|
||||
|
||||
{step === 3 && (
|
||||
<div className="text-center py-4">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 mb-3">
|
||||
<Check className="h-6 w-6 text-green-600" />
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-success/10 mb-3">
|
||||
<Check className="h-6 w-6 text-success" />
|
||||
</div>
|
||||
<p className="font-medium">{importCount} rader importerades</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function KPICard({ label, value, suffix, trend, className }: KPIC
|
||||
{trend && (
|
||||
<p className={cn(
|
||||
'text-xs mt-1',
|
||||
trend.value > 0 ? 'text-green-600' : trend.value < 0 ? 'text-red-600' : 'text-muted-foreground'
|
||||
trend.value > 0 ? 'text-success' : trend.value < 0 ? 'text-destructive' : 'text-muted-foreground'
|
||||
)}>
|
||||
{trend.value > 0 ? '+' : ''}{trend.value}% {trend.label}
|
||||
</p>
|
||||
|
||||
@@ -377,7 +377,7 @@ export default function MockDataImportDialog<T>({
|
||||
<span>{fileName}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 p-3 rounded-md bg-green-50 dark:bg-green-950/30 text-green-700 dark:text-green-400 text-sm">
|
||||
<div className="flex items-center gap-2 p-3 rounded-md bg-success/10 text-success text-sm">
|
||||
<Check className="h-4 w-4 shrink-0" />
|
||||
<span>Giltig JSON — {jsonSummary}</span>
|
||||
</div>
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function BankFileColumnMappingStep({
|
||||
<TableCell className="text-sm">{row[descCol] || '–'}</TableCell>
|
||||
<TableCell
|
||||
className={`text-right font-mono text-sm ${
|
||||
!isNaN(amount) && amount >= 0 ? 'text-green-600' : 'text-red-600'
|
||||
!isNaN(amount) && amount >= 0 ? 'text-success' : 'text-destructive'
|
||||
}`}
|
||||
>
|
||||
{!isNaN(amount) ? formatCurrency(amount) : amountStr}
|
||||
|
||||
@@ -166,7 +166,7 @@ export default function BankFileUploadStep({
|
||||
</div>
|
||||
) : selectedFile && detectedFormat ? (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle className="mx-auto h-12 w-12 text-green-600" />
|
||||
<CheckCircle className="mx-auto h-12 w-12 text-success" />
|
||||
<div>
|
||||
<p className="font-medium">{selectedFile.name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -12,7 +12,9 @@ import type { EntityType } from '@/types'
|
||||
|
||||
const schema = z.object({
|
||||
company_name: z.string().min(1, 'Företagsnamn krävs'),
|
||||
org_number: z.string().min(1, 'Organisationsnummer krävs'),
|
||||
org_number: z.string()
|
||||
.min(1, 'Organisationsnummer krävs')
|
||||
.regex(/^\d{6,8}[-\s]?\d{4}$/, 'Ogiltigt format. Ange XXXXXX-XXXX'),
|
||||
address_line1: z.string().optional(),
|
||||
postal_code: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
@@ -41,6 +43,7 @@ export default function Step2CompanyDetails({
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
mode: 'onTouched',
|
||||
defaultValues: {
|
||||
company_name: initialData.company_name || '',
|
||||
org_number: initialData.org_number || '',
|
||||
|
||||
@@ -155,6 +155,7 @@ export default function Step3TaxRegistration({
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
mode: 'onTouched',
|
||||
defaultValues: {
|
||||
f_skatt: initialData.f_skatt ?? true,
|
||||
is_first_fiscal_year: initialData.is_first_fiscal_year ?? false,
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function Step4PreliminaryTax({
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
mode: 'onTouched',
|
||||
defaultValues: {
|
||||
preliminary_tax_monthly: initialData.preliminary_tax_monthly ?? undefined,
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ export default function Step6ConnectBank({
|
||||
handleSubmit,
|
||||
} = useForm<ManualBankData>({
|
||||
resolver: zodResolver(manualBankSchema),
|
||||
mode: 'onTouched',
|
||||
defaultValues: {
|
||||
bank_name: initialData?.bank_name || '',
|
||||
clearing_number: initialData?.clearing_number || '',
|
||||
|
||||
@@ -282,7 +282,7 @@ export function BankReconciliationView() {
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Avstämning mot <AccountNumber number="1930" /></CardTitle>
|
||||
{status.is_reconciled ? (
|
||||
<Badge className="bg-green-100 text-green-800">Avstämd</Badge>
|
||||
<Badge className="bg-success/10 text-success">Avstämd</Badge>
|
||||
) : (
|
||||
<Badge variant="destructive">Ej avstämd</Badge>
|
||||
)}
|
||||
|
||||
@@ -203,13 +203,13 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
|
||||
<tfoot>
|
||||
<tr className="border-t font-medium">
|
||||
<td className="py-2">Rörelseresultat</td>
|
||||
<td className={`py-2 text-right ${data.totals.operatingResult >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
<td className={`py-2 text-right ${data.totals.operatingResult >= 0 ? 'text-success' : 'text-destructive'}`}>
|
||||
{formatCurrency(data.totals.operatingResult)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-t-2 font-semibold">
|
||||
<td className="py-2">Resultat efter finansiella poster</td>
|
||||
<td className={`py-2 text-right ${data.totals.resultAfterFinancial >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
<td className={`py-2 text-right ${data.totals.resultAfterFinancial >= 0 ? 'text-success' : 'text-destructive'}`}>
|
||||
{formatCurrency(data.totals.resultAfterFinancial)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -211,7 +211,7 @@ export function NEDeclarationView({ periodId }: { periodId: string }) {
|
||||
</div>
|
||||
<span
|
||||
className={`text-2xl font-bold ${
|
||||
data.rutor.R11 >= 0 ? 'text-green-600' : 'text-red-600'
|
||||
data.rutor.R11 >= 0 ? 'text-success' : 'text-destructive'
|
||||
}`}
|
||||
>
|
||||
{formatCurrency(data.rutor.R11)}
|
||||
|
||||
@@ -510,7 +510,7 @@ export default function DescribeTransactionDialog({
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="font-medium text-sm">{template.name_sv}</p>
|
||||
{aiAgreesWithTop && template.template_id === topTemplate.template_id && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 bg-success/10 text-success">
|
||||
AI bekräftar
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const TYPE_COLORS: Record<AccountType, string> = {
|
||||
asset: 'bg-green-500',
|
||||
liability: 'bg-orange-500',
|
||||
equity: 'bg-blue-500',
|
||||
revenue: 'bg-purple-500',
|
||||
expense: 'bg-red-500',
|
||||
untaxed_reserves: 'bg-amber-500',
|
||||
asset: 'bg-success',
|
||||
liability: 'bg-warning',
|
||||
equity: 'bg-primary',
|
||||
revenue: 'bg-muted-foreground',
|
||||
expense: 'bg-destructive',
|
||||
untaxed_reserves: 'bg-warning',
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<AccountType, string> = {
|
||||
|
||||
@@ -8,7 +8,7 @@ const Card = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border border-border/50 bg-card text-card-foreground shadow-[var(--shadow-sm)]",
|
||||
"rounded-xl border border-border/60 bg-card text-card-foreground shadow-[var(--shadow-sm)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -12,7 +12,7 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded border border-input transition-colors duration-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/20 focus-visible:border-primary/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground hover:border-primary/50",
|
||||
"peer h-4 w-4 shrink-0 rounded border border-input transition-colors duration-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/35 focus-visible:border-primary/60 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground hover:border-primary/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -61,8 +61,8 @@ export function DestructiveConfirmDialog({
|
||||
className={cn(
|
||||
'flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full',
|
||||
variant === 'destructive'
|
||||
? 'bg-red-100 text-red-600'
|
||||
: 'bg-amber-100 text-amber-600'
|
||||
? 'bg-destructive/10 text-destructive'
|
||||
: 'bg-warning/10 text-warning'
|
||||
)}
|
||||
>
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
@@ -87,7 +87,7 @@ export function DestructiveConfirmDialog({
|
||||
disabled={isLoading}
|
||||
className={
|
||||
variant === 'warning'
|
||||
? 'bg-amber-600 hover:bg-amber-700 text-white'
|
||||
? 'bg-warning hover:bg-warning/90 text-warning-foreground'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-lg border border-input bg-card px-4 py-2 text-sm transition-colors duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:border-primary/50 focus-visible:ring-1 focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-lg border border-input bg-card px-4 py-2 text-sm transition-colors duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:border-primary/60 focus-visible:ring-1 focus-visible:ring-primary/35 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-lg border border-input bg-card px-4 py-2 text-sm transition-colors duration-200 placeholder:text-muted-foreground/60 focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-10 w-full items-center justify-between rounded-lg border border-input bg-card px-4 py-2 text-sm transition-colors duration-200 placeholder:text-muted-foreground/60 focus:outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/35 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user