diff --git a/DECISIONS.md b/DECISIONS.md index 3653ddb5..fb5911d6 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -993,6 +993,7 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-14] The onboarding branch question ("Var fanns bokföringen innan?") became its own journey step at the existing KLART station (done -> source, same station grammar as momsyn/moms under MOMSEN) instead of a sixth rail station: a 6-point rail crowds the 680px band's 150px label slots, "BOKFÖRINGEN INNAN" would sit next to the existing "BOKFÖRINGEN" station, and mode='add' (which never sees the branch question) would need an asymmetric rail. The done screen ends in a revealed Fortsätt that dispatches DONE_CONTINUE (mode='first' only, reducer-guarded). [2026-08-14] Migration /preview fetches ALL allowed fiscal years (dropped latestOnly): the connect step's "Hittade X konton och Y verifikationer" renders from /preview's sieStats, not /sie-data's generateImportPreview, so fixing only /sie-data would have left the founder-reported "0 verifikationer" (actual: 4153) on screen. Costs one SIE export per extra year at connect time, the same work /sie-data repeats right after; honest counts won over latency. [2026-08-14] /sie-data validation stays newest-file-only (not per-file, not on the merged parse): preserves exactly which datasets are accepted today, and validateSIEFile assumes single-file invariants (balance yearIndexes relative to ONE current year) that mergeParsedSIEFiles deliberately does not preserve. Older files' problems still surface per-file at import time. +[2026-08-15] Register detail de-box (customers/suppliers/articles): added new def_* label keys next to the old colon-suffixed and inline-format keys instead of renaming or deleting them: message files are only ever edited line-wise (duplicate-key hazard forbids JSON round-trips), so dead keys are cheaper than a risky rewrite. Also converted the customer invoice list's per-row status Badge to muted text with a chip only for Förfallen, per the locked "chips mark exceptions" convention: paid on every row was the same-chip-on-every-row bug the convention names. [2026-08-15] Employee form save-scope fix: moved the self-saving sections (Förmåner, Ingående saldon) OUTSIDE the page
under a "Sparas separat" kicker with a one-line scope hint, kept them as Cards (locked section pattern; the task-suggested hairline no-box region would deviate from every sibling section) and rejected the tab/sub-page shape (page has no tab structure, would be more invasive). This also fixes a real double-fire: shadcn Button has no default type, so the panels' save/add/delete buttons inside the form implicitly submitted the whole employee PATCH on every click; opening-balances now has its own whose save enables only when dirty. [2026-08-14] SKV manual-verifikat deep link payload moved from URL params to single-use sessionStorage (supersedes same-day URL-params decision): compliance swarm flagged financial data in query strings landing in history/access logs/Referer (GDPR Art.5(1)(f), ISO A.8.12); URL now carries only the opaque row id. [2026-08-14] SKV prefill sessionStorage XSS window accepted as residual risk (ISO A.8.12 low, swarm PR #1621): script execution already implies full ledger read via authenticated APIs; a server-issued staging token adds a roundtrip, not protection. Documented in manual-verifikat-prefill.ts header. diff --git a/app/(dashboard)/articles/[id]/page.tsx b/app/(dashboard)/articles/[id]/page.tsx index 15cef32e..c7d45eac 100644 --- a/app/(dashboard)/articles/[id]/page.tsx +++ b/app/(dashboard)/articles/[id]/page.tsx @@ -4,9 +4,9 @@ import { useState, useEffect, useCallback, useRef, use } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { useLocale, useTranslations } from 'next-intl' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' +import { DetailSection, DefRow } from '@/components/ui/detail-section' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { useToast } from '@/components/ui/use-toast' import ArticleForm from '@/components/articles/ArticleForm' @@ -17,17 +17,7 @@ import { } from '@/lib/hooks/use-submit-with-account-activation' import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message' import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog' -import { - Archive, - ArchiveRestore, - ArrowLeft, - Package, - Wrench, - Edit2, - Trash2, - Loader2, - Lock, -} from 'lucide-react' +import { ArrowLeft, Loader2, Lock } from 'lucide-react' import { useCanWrite } from '@/lib/hooks/use-can-write' import { formatCurrency } from '@/lib/utils' import type { Article, ArticleType, CreateArticleInput } from '@/types' @@ -37,11 +27,6 @@ const ARTICLE_TYPE_KEY: Record = { tjanst: 'type_tjanst', } -const articleTypeIcons: Record = { - vara: Package, - tjanst: Wrench, -} - export default function ArticleDetailPage({ params, }: { @@ -221,178 +206,130 @@ export default function ArticleDetailPage({ if (!article) return null - const Icon = articleTypeIcons[article.type] - return ( -
- {/* Header */} -
-
- - - {t('back')} - -
-
- -
-
-

{article.name}

-
- {article.active ? ( - {t('status_active')} - ) : ( - - {t('status_inactive')} - - )} - - {t(ARTICLE_TYPE_KEY[article.type])} - {article.article_number ? ` · #${article.article_number}` : ''} - -
-
+
+ {/* Header: serif name over a quiet type/status kicker, quiet actions right */} +
+ + + {t('back')} + +
+
+

{article.name}

+

+ + {t(ARTICLE_TYPE_KEY[article.type])} + {article.article_number ? ` · #${article.article_number}` : ''} + {article.active ? ` · ${t('status_active')}` : ''} + + {!article.active && ( + + {t('status_inactive')} + + )} +

+
+ +
+ + +
- -
- - - -
- {/* Info cards */} -
- {/* Pricing */} - - - {t('section_pricing')} - - -
- {t('label_price')} - {formatCurrency(article.price_excl_vat, article.currency)} -
-
- {t('label_vat')} - {article.vat_rate} % -
-
- {t('label_unit')} - {article.unit} -
- {article.cost_price != null && ( -
- {t('label_cost_price')} - {formatCurrency(article.cost_price, article.currency)} -
- )} -
-
+ + + + {formatCurrency(article.price_excl_vat, article.currency)} + + + + {article.vat_rate} % + + {article.unit} + {article.cost_price != null && ( + + + {formatCurrency(article.cost_price, article.currency)} + + + )} + - {/* Accounting */} - - - {t('section_accounting')} - - -
- {t('label_revenue_account')} - - {article.revenue_account || t('revenue_account_auto')} - -
- {article.type === 'tjanst' && article.housework_type && ( -
- {t('label_housework')} - {article.housework_type} -
- )} -
-
+ + + {article.revenue_account ? ( + {article.revenue_account} + ) : ( + {t('revenue_account_auto')} + )} + + {article.type === 'tjanst' && article.housework_type && ( + {article.housework_type} + )} + - {/* Details */} - - - {t('section_details')} - - - {article.name_en && ( -
- {t('label_name_en')} - {article.name_en} -
- )} - {article.ean && ( -
- {t('label_ean')} - {article.ean} -
- )} - {!article.name_en && !article.ean && ( -

{t('no_details')}

- )} -
-
-
+ {/* Optional facts (English name, EAN) are omitted row-wise; when none + exist the whole section goes, so the document never pads itself + with placeholders for facts nobody entered. */} + {(article.name_en || article.ean) && ( + + {article.name_en && {article.name_en}} + {article.ean && ( + + {article.ean} + + )} + + )} - {/* Notes */} {article.notes && ( - - - {t('section_notes')} - - -

{article.notes}

-
-
+ +

{article.notes}

+
)} diff --git a/app/(dashboard)/customers/[id]/page.tsx b/app/(dashboard)/customers/[id]/page.tsx index 1d3a7c2d..bd92d6a0 100644 --- a/app/(dashboard)/customers/[id]/page.tsx +++ b/app/(dashboard)/customers/[id]/page.tsx @@ -5,9 +5,9 @@ import { useRouter } from 'next/navigation' import { use } from 'react' import Link from 'next/link' import { useTranslations } from 'next-intl' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' +import { DetailSection, DefRow, DefEmpty } from '@/components/ui/detail-section' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { useToast } from '@/components/ui/use-toast' import { @@ -17,22 +17,7 @@ import { import { AttnLine } from '@/components/ui/attn-line' import CustomerForm from '@/components/customers/CustomerForm' import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog' -import { - ArrowLeft, - Building, - Globe, - User, - Mail, - Phone, - MapPin, - Edit2, - Trash2, - Loader2, - ReceiptText, - Lock, - Eye, - EyeOff, -} from 'lucide-react' +import { ArrowLeft, Loader2, Lock, Eye, EyeOff } from 'lucide-react' import { useLocale } from 'next-intl' import { useCanWrite } from '@/lib/hooks/use-can-write' import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message' @@ -47,13 +32,6 @@ const CUSTOMER_TYPE_KEY: Record = { non_eu_business: 'type_non_eu_business', } -const customerTypeIcons: Record = { - individual: User, - swedish_business: Building, - eu_business: Globe, - non_eu_business: Globe, -} - interface RelatedInvoice { id: string invoice_number: string | null @@ -232,244 +210,200 @@ export default function CustomerDetailPage({ if (!customer) return null - const Icon = customerTypeIcons[customer.customer_type] - return ( -
- {/* Header */} -
-
- - - {t('back')} - -
-
- -
-
-

{customer.name}

-

{t(CUSTOMER_TYPE_KEY[customer.customer_type])}

-
+
+ {/* Header: serif name over a quiet type kicker, quiet actions right */} +
+ + + {t('back')} + +
+
+

{customer.name}

+

+ {t(CUSTOMER_TYPE_KEY[customer.customer_type])} +

+
+ +
+ +
- -
- - -
- {/* Info cards */} -
- {/* Contact */} - - - {t('section_contact')} - - - {customer.email && ( - - )} - {customer.phone && ( -
- - {customer.phone} -
- )} - {(customer.address_line1 || customer.city) && ( -
- -
- {customer.address_line1 &&

{customer.address_line1}

} - {customer.address_line2 &&

{customer.address_line2}

} - {(customer.postal_code || customer.city) && ( -

{[customer.postal_code, customer.city].filter(Boolean).join(' ')}

- )} - {customer.country &&

{customer.country}

} -
-
- )} - {!customer.email && !customer.phone && !customer.address_line1 && !customer.city && ( -

{t('no_contact_info')}

- )} -
-
- - {/* Customer details */} - - - {t('section_business')} - - - {customer.customer_number && ( -
- {t('label_customer_number')} - {customer.customer_number} -
- )} - {customer.customer_type !== 'individual' && customer.org_number && ( -
- {t('label_org_number')} - {customer.org_number} -
- )} - {customer.customer_type === 'individual' && (customer.personal_number || customer.org_number) && ( -
- {t('label_personal_number')} - - {revealedPersonalNumber ?? - maskCustomerPersonalNumber(customer.personal_number || customer.org_number)} - - {/* Viewers keep the mask: the endpoint refuses them anyway. */} - {canWrite && customer.personal_number && !isUnreadablePersonalNumber && ( - - )} - {isUnreadablePersonalNumber && ( - setIsEditOpen(true) }} - > - {t('personal_number_unreadable')} - - )} -
- )} - {customer.vat_number && ( -
- {t('label_vat')} - {customer.vat_number} - {customer.vat_number_validated && ( - {t('verified')} - )} -
- )} -
- {t('label_payment_terms')} - {t('payment_terms_value', { days: customer.default_payment_terms || 30 })} -
- {!customer.customer_number && !customer.org_number && !customer.personal_number && !customer.vat_number && ( -

{t('no_business_info')}

- )} -
-
- - {/* Summary */} - - - {t('section_summary')} - - -
- - {t('invoice_count', { count: customer.invoices?.length || 0 })} -
-
-
-
- - {/* Notes */} - {customer.notes && ( - - - {t('section_notes')} - - -

{customer.notes}

-
-
- )} - - {/* Related invoices */} - - - - - {t('section_invoices')} - {customer.invoices?.length > 0 && ( - ({customer.invoices.length}) - )} - - - - {customer.invoices?.length > 0 ? ( -
- {customer.invoices.map((invoice) => ( - -
-

{invoiceNumberDisplay(invoice.invoice_number)}

-

{formatDate(invoice.invoice_date)}

-
-
- - {formatCurrency(invoice.total, invoice.currency)} - - - {invoice.payment_status === 'paid' - ? t('invoice_status_paid') - : invoice.payment_status === 'overdue' - ? t('invoice_status_overdue') - : t('invoice_status_unpaid')} - -
- - ))} + + + {customer.email ? ( + + {customer.email} + + ) : ( + + )} + + {customer.phone || } + + {customer.address_line1 || customer.city ? ( +
+ {customer.address_line1 &&

{customer.address_line1}

} + {customer.address_line2 &&

{customer.address_line2}

} + {(customer.postal_code || customer.city) && ( +

{[customer.postal_code, customer.city].filter(Boolean).join(' ')}

+ )} + {customer.country &&

{customer.country}

}
) : ( -

- {t('no_invoices')} -

+ )} - - +
+
+ + + + {customer.customer_number || } + + {customer.customer_type !== 'individual' && ( + + {customer.org_number ? ( + {customer.org_number} + ) : ( + + )} + + )} + {customer.customer_type === 'individual' && (customer.personal_number || customer.org_number) && ( + + + {revealedPersonalNumber ?? + maskCustomerPersonalNumber(customer.personal_number || customer.org_number)} + + {/* Viewers keep the mask: the endpoint refuses them anyway. */} + {canWrite && customer.personal_number && !isUnreadablePersonalNumber && ( + + )} + {isUnreadablePersonalNumber && ( + setIsEditOpen(true) }} + > + {t('personal_number_unreadable')} + + )} + + )} + {customer.vat_number && ( + + + {customer.vat_number} + {customer.vat_number_validated && ( + {t('verified')} + )} + + + )} + + {t('payment_terms_value', { days: customer.default_payment_terms || 30 })} + + + + {customer.notes && ( + +

{customer.notes}

+
+ )} + + 0 ? ( + + {t('invoice_count', { count: customer.invoices.length })} + + ) : undefined + } + > + {customer.invoices?.length > 0 ? ( +
+ {customer.invoices.map((invoice) => ( + + + {invoiceNumberDisplay(invoice.invoice_number)} + + + {formatDate(invoice.invoice_date)} + + + {formatCurrency(invoice.total, invoice.currency)} + + {/* Chips mark exceptions: an overdue invoice is the deviation + worth a chip; paid and not-yet-due render as muted text. */} + {invoice.payment_status === 'overdue' ? ( + {t('invoice_status_overdue')} + ) : ( + + {invoice.payment_status === 'paid' + ? t('invoice_status_paid') + : t('invoice_status_unpaid')} + + )} + + ))} +
+ ) : ( +

{t('no_invoices')}

+ )} +
diff --git a/app/(dashboard)/suppliers/[id]/page.tsx b/app/(dashboard)/suppliers/[id]/page.tsx index 9e18183b..ef92a7f7 100644 --- a/app/(dashboard)/suppliers/[id]/page.tsx +++ b/app/(dashboard)/suppliers/[id]/page.tsx @@ -5,13 +5,14 @@ import { useParams, useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Skeleton } from "@/components/ui/skeleton" -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { DetailSection, DefRow, DefEmpty } from '@/components/ui/detail-section' +import { QUIET_LINK_CLASS } from '@/components/ui/dry-table' import { useToast } from '@/components/ui/use-toast' import { getErrorMessage } from '@/lib/errors/get-error-message' -import { ArrowLeft, Edit, Trash2, FileText, Lock } from 'lucide-react' +import { ArrowLeft, Lock } from 'lucide-react' import { useCanWrite } from '@/lib/hooks/use-can-write' import { formatDate } from '@/lib/utils' import SupplierForm from '@/components/suppliers/SupplierForm' @@ -181,124 +182,159 @@ export default function SupplierDetailPage() { credited: t('status_credited'), } + // One fallback row keeps the figures band composed for a supplier that has + // no invoices yet: zeros in the supplier's own default currency. + const currencyRows = supplier.stats?.by_currency?.length + ? supplier.stats.by_currency + : [{ currency: supplier.default_currency || 'SEK', total_outstanding: 0, total_paid: 0 }] + return ( -
-
-
- -
+
+ {/* Header: serif name over a quiet type/org kicker, quiet actions right */} +
+ + + {t('back')} + +
+

{supplier.name}

-

+

{supplierTypeLabels[supplier.supplier_type]} - {supplier.org_number && t('org_number_inline', { number: supplier.org_number })} + {supplier.org_number ? ` · ${t('kicker_org', { number: supplier.org_number })}` : ''}

-
-
- - + +
+ + +
- {/* Stats */} -
- - - {t('outstanding')} - - - {(supplier.stats?.by_currency?.length ? supplier.stats.by_currency : [{ currency: supplier.default_currency || 'SEK', total_outstanding: 0, total_paid: 0 }]).map((row) => ( -

+ {/* Figures band: the three headline numbers, flat on the page */} +

+
+

+ {t('outstanding')} +

+
+ {currencyRows.map((row) => ( +

{amountWithCurrency(row.total_outstanding, row.currency)}

))} - - - - - {t('total_paid')} - - - {(supplier.stats?.by_currency?.length ? supplier.stats.by_currency : [{ currency: supplier.default_currency || 'SEK', total_outstanding: 0, total_paid: 0 }]).map((row) => ( -

+

+
+
+

+ {t('total_paid')} +

+
+ {currencyRows.map((row) => ( +

{amountWithCurrency(row.total_paid, row.currency)}

))} - - - - - {t('invoice_count')} - - -

{supplier.stats?.invoice_count || 0}

-
-
+
+
+
+

+ {t('invoice_count')} +

+

+ {supplier.stats?.invoice_count || 0} +

+
- {/* Contact & Payment Info */} -
- - - {t('contact_section_title')} - - - {supplier.email &&

{t('email_inline', { email: supplier.email })}

} - {supplier.phone &&

{t('phone_inline', { phone: supplier.phone })}

} - {supplier.address_line1 &&

{supplier.address_line1}

} - {supplier.postal_code &&

{supplier.postal_code} {supplier.city}

} - {supplier.vat_number &&

{t('vat_inline', { vat: supplier.vat_number })}

} -
-
- - - {t('payment_section_title')} - - - {supplier.bankgiro &&

{t('bankgiro_inline', { value: supplier.bankgiro })}

} - {supplier.plusgiro &&

{t('plusgiro_inline', { value: supplier.plusgiro })}

} - {supplier.iban &&

{t('iban_inline', { value: supplier.iban })}

} - {supplier.bic &&

{t('bic_inline', { value: supplier.bic })}

} -

{t('payment_terms_inline', { days: supplier.default_payment_terms })}

-

{t('currency_inline', { currency: supplier.default_currency })}

- {supplier.default_expense_account &&

{t('expense_account_inline', { account: supplier.default_expense_account })}

} -
-
-
+ + {supplier.email || } + {supplier.phone || } + + {supplier.address_line1 || supplier.city ? ( +
+ {supplier.address_line1 &&

{supplier.address_line1}

} + {supplier.address_line2 &&

{supplier.address_line2}

} + {(supplier.postal_code || supplier.city) && ( +

{[supplier.postal_code, supplier.city].filter(Boolean).join(' ')}

+ )} +
+ ) : ( + + )} +
+ {supplier.vat_number && {supplier.vat_number}} +
- {/* Invoices */} - - - {t('invoices_section_title')} - - + + {supplier.bankgiro && ( + + {supplier.bankgiro} + + )} + {supplier.plusgiro && ( + + {supplier.plusgiro} + + )} + {supplier.iban && ( + + {supplier.iban} + + )} + {supplier.bic && ( + + {supplier.bic} + + )} + + {t('payment_terms_value', { days: supplier.default_payment_terms })} + + {supplier.default_currency} + + {supplier.default_expense_account ? ( + {supplier.default_expense_account} + ) : ( + + )} + + + + + {t('new_invoice')} - - + } + > {invoices.length === 0 ? ( -

+

{t('no_invoices')}

) : ( @@ -366,8 +402,7 @@ export default function SupplierDetailPage() {
)} - - + diff --git a/components/ui/detail-section.tsx b/components/ui/detail-section.tsx new file mode 100644 index 00000000..e5487326 --- /dev/null +++ b/components/ui/detail-section.tsx @@ -0,0 +1,66 @@ +import { cn } from '@/lib/utils' + +/** + * Register-detail document grammar (customers, suppliers, articles). + * + * A detail page is one flowing document, not a pile of cards: each group of + * facts is introduced by an uppercase hairline kicker and set as aligned + * label/value rows. The kicker's hairline is the only rule; groups are + * separated by whitespace, never borders (Living Paper, design.md). + */ + +export function DetailSection({ + kicker, + aside, + children, + className, +}: { + kicker: string + /** Optional right-aligned element on the kicker line: a count, a quiet action. */ + aside?: React.ReactNode + children: React.ReactNode + className?: string +}) { + return ( +
+
+

+ {kicker} +

+ {aside} +
+
{children}
+
+ ) +} + +export function DefRow({ + label, + children, + className, +}: { + label: string + children: React.ReactNode + className?: string +}) { + return ( +
+
{label}
+
{children}
+
+ ) +} + +/** + * Muted placeholder for a value that matters but is not filled in. The en dash + * is the literal rendered value (same vocabulary as the orders list), which is + * why it is allowed to be a dash at all. + */ +export function DefEmpty() { + return {'–'} +} diff --git a/messages/en.json b/messages/en.json index b0173387..5c6e1014 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1274,9 +1274,17 @@ "label_personal_number": "Personal no.:", "label_vat": "VAT:", "label_payment_terms": "Payment terms:", + "def_email": "Email", + "def_phone": "Phone", + "def_address": "Address", + "def_customer_number": "Customer number", + "def_org_number": "Org. no.", + "def_personal_number": "Personal number", + "def_vat": "VAT number", + "def_payment_terms": "Payment terms", "payment_terms_value": "{days} days", "verified": "Verified", - "invoice_count": "{count} invoices", + "invoice_count": "{count, plural, one {# invoice} other {# invoices}}", "load_failed_title": "Could not load customer", "load_failed_description": "Customer not found.", "updated_title": "Customer updated", @@ -4456,6 +4464,20 @@ "payment_terms_inline": "Payment terms: {days} days", "currency_inline": "Currency: {currency}", "expense_account_inline": "Expense account: {account}", + "delete": "Delete", + "kicker_org": "Org. no. {number}", + "def_email": "Email", + "def_phone": "Phone", + "def_address": "Address", + "def_vat": "VAT number", + "def_bankgiro": "Bankgiro", + "def_plusgiro": "Plusgiro", + "def_iban": "IBAN", + "def_bic": "BIC", + "def_payment_terms": "Payment terms", + "payment_terms_value": "{days} days", + "def_currency": "Currency", + "def_expense_account": "Expense account", "invoices_section_title": "Invoices", "new_invoice": "New invoice", "no_invoices": "No invoices registered for this supplier", diff --git a/messages/sv.json b/messages/sv.json index 929c2822..a588ce25 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -1274,9 +1274,17 @@ "label_personal_number": "Persnr:", "label_vat": "VAT:", "label_payment_terms": "Betalningsvillkor:", + "def_email": "E-post", + "def_phone": "Telefon", + "def_address": "Adress", + "def_customer_number": "Kundnummer", + "def_org_number": "Org.nr", + "def_personal_number": "Personnummer", + "def_vat": "VAT-nummer", + "def_payment_terms": "Betalningsvillkor", "payment_terms_value": "{days} dagar", "verified": "Verifierad", - "invoice_count": "{count} fakturor", + "invoice_count": "{count, plural, one {# faktura} other {# fakturor}}", "load_failed_title": "Kunde inte ladda kund", "load_failed_description": "Kunden hittades inte.", "updated_title": "Kund uppdaterad", @@ -4456,6 +4464,20 @@ "payment_terms_inline": "Betalningsvillkor: {days} dagar", "currency_inline": "Valuta: {currency}", "expense_account_inline": "Kostnadskonto: {account}", + "delete": "Ta bort", + "kicker_org": "Org.nr {number}", + "def_email": "E-post", + "def_phone": "Telefon", + "def_address": "Adress", + "def_vat": "VAT-nummer", + "def_bankgiro": "Bankgiro", + "def_plusgiro": "Plusgiro", + "def_iban": "IBAN", + "def_bic": "BIC", + "def_payment_terms": "Betalningsvillkor", + "payment_terms_value": "{days} dagar", + "def_currency": "Valuta", + "def_expense_account": "Kostnadskonto", "invoices_section_title": "Fakturor", "new_invoice": "Ny faktura", "no_invoices": "Inga fakturor registrerade för denna leverantör",