refactor(ui): validation as errors on attempt, help text behind the ? (#1561)
The founder flagged the app as bloated with standing instructional text. Ny verifikation: the what's-missing lines (Ange en beskrivning, Minst två rader...) rendered from the first frame because their only gate was form validity, which an empty form fails: instructions dressed as validation. The submit buttons now stay enabled and an attempt on an incomplete form is what surfaces the lines, in destructive red, per the error-on-submit idiom. The Enter-to-advance flow keeps the old completeness predicate so navigation is untouched. Matcha mot befintlig verifikation: the two-sentence explainer moved behind a ? (HelpPopover, convention 7), the N:1 note tightened, the Visa även matchade switch became a quiet link (switches are settings idiom), and Stark träff, the normal auto-selected case, renders as muted text instead of a chip (chips mark exceptions, convention 5). Deleted always-visible paraphrase lines and their orphaned keys: items_card_description, picker_description, references_subtitle, sort_stack_hint, dimensions hints, document_help, and the dead fill_balance_hint key that had no render site at all. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -936,7 +936,6 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
|
||||
<div className="mb-4 space-y-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">{t('references_title')}</h4>
|
||||
<p className="text-xs text-muted-foreground">{t('references_subtitle')}</p>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{references.map((ref) => (
|
||||
|
||||
@@ -172,7 +172,6 @@ export default function InboxDocumentPicker({ open, onClose, journalEntryId, onL
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('picker_title')}</DialogTitle>
|
||||
<DialogDescription>{t('picker_description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="relative">
|
||||
|
||||
@@ -204,6 +204,10 @@ export default function JournalEntryForm({
|
||||
const [isSavingDraft, setIsSavingDraft] = useState(false)
|
||||
const saveAsDraftRef = useRef(false)
|
||||
const [showNoDocWarning, setShowNoDocWarning] = useState(false)
|
||||
// The what's-missing lines render only after a submit attempt (error-on-
|
||||
// submit), never as standing chrome on an empty form. The buttons stay
|
||||
// enabled so the attempt can happen; the handlers gate on validity.
|
||||
const [showValidationHints, setShowValidationHints] = useState(false)
|
||||
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
|
||||
const [accounts, setAccounts] = useState<BASAccount[]>([])
|
||||
// Full BAS catalogue (static reference data, fetched once per session). Lets
|
||||
@@ -809,7 +813,11 @@ export default function JournalEntryForm({
|
||||
}
|
||||
|
||||
const handleReview = () => {
|
||||
if (!selectedPeriod || !description || !isBalanced || periodMismatch) return
|
||||
if (!selectedPeriod || !description || !isBalanced || periodMismatch) {
|
||||
setShowValidationHints(true)
|
||||
return
|
||||
}
|
||||
setShowValidationHints(false)
|
||||
const hasDocuments = uploadedFiles.some((f) => f.status === 'uploaded')
|
||||
if (!embedded && !bare && !hasDocuments) {
|
||||
setShowNoDocWarning(true)
|
||||
@@ -818,17 +826,24 @@ export default function JournalEntryForm({
|
||||
setShowReview(true)
|
||||
}
|
||||
|
||||
// Whether an Enter should open the review: mirrors the review button's
|
||||
// enable gate exactly, so Enter never submits something the button wouldn't.
|
||||
// Nothing in flight and the user may write: the buttons' enable gate.
|
||||
// Validity is deliberately NOT part of it; an attempt on an incomplete
|
||||
// form is what surfaces the validation hints.
|
||||
const processReady = () =>
|
||||
!isUploading &&
|
||||
canWrite &&
|
||||
!isSubmitting &&
|
||||
!isSavingDraft
|
||||
|
||||
// Whether the entry is actually submittable. The Enter-to-advance handlers
|
||||
// below key off this: navigation fires while the entry is incomplete, and
|
||||
// once it balances Enter falls through to the review instead.
|
||||
const canSubmitReview = () =>
|
||||
isBalanced &&
|
||||
!!description &&
|
||||
!!selectedPeriod &&
|
||||
!periodMismatch &&
|
||||
!isUploading &&
|
||||
canWrite &&
|
||||
!isSubmitting &&
|
||||
!isSavingDraft
|
||||
processReady()
|
||||
|
||||
// Enter anywhere in the form = "Granska & skapa": opens the review exactly as
|
||||
// the button does, from any field. Navigation is Tab's job. Two Enter
|
||||
@@ -840,7 +855,7 @@ export default function JournalEntryForm({
|
||||
if (e.defaultPrevented || showReview) return
|
||||
if ((e.target as HTMLElement).tagName === 'TEXTAREA') return
|
||||
e.preventDefault()
|
||||
if (canSubmitReview()) handleReview()
|
||||
if (processReady()) handleReview()
|
||||
}
|
||||
|
||||
// Enter-to-advance inside the konteringsrader: konto → debet → kredit →
|
||||
@@ -1132,7 +1147,11 @@ export default function JournalEntryForm({
|
||||
}
|
||||
|
||||
const handleSaveDraft = async () => {
|
||||
if (!selectedPeriod || !description || !isBalanced || periodMismatch) return
|
||||
if (!selectedPeriod || !description || !isBalanced || periodMismatch) {
|
||||
setShowValidationHints(true)
|
||||
return
|
||||
}
|
||||
setShowValidationHints(false)
|
||||
setIsSavingDraft(true)
|
||||
saveAsDraftRef.current = true
|
||||
try {
|
||||
@@ -1198,7 +1217,11 @@ export default function JournalEntryForm({
|
||||
// editEntryId URL) and keep it a draft. No field reset: the host dialog
|
||||
// closes on success via onUpdated.
|
||||
const handleSaveEdit = async () => {
|
||||
if (!selectedPeriod || !description || !isBalanced || periodMismatch) return
|
||||
if (!selectedPeriod || !description || !isBalanced || periodMismatch) {
|
||||
setShowValidationHints(true)
|
||||
return
|
||||
}
|
||||
setShowValidationHints(false)
|
||||
setIsSavingDraft(true)
|
||||
try {
|
||||
await runSubmit()
|
||||
@@ -1483,7 +1506,6 @@ export default function JournalEntryForm({
|
||||
onChange={setHeaderDimension}
|
||||
inputClassName="h-8"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('dimensions_apply_all_hint')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1880,7 +1902,7 @@ export default function JournalEntryForm({
|
||||
{editEntryId ? (
|
||||
<Button
|
||||
onClick={handleSaveEdit}
|
||||
disabled={!isBalanced || !description || !selectedPeriod || !!periodMismatch || isSubmitting || isSavingDraft || isUploading || !canWrite}
|
||||
disabled={isSubmitting || isSavingDraft || isUploading || !canWrite}
|
||||
title={!canWrite ? t('read_only_tooltip') : undefined}
|
||||
>
|
||||
{!canWrite ? <Lock className="mr-2 h-4 w-4" /> : isSavingDraft && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
@@ -1907,7 +1929,7 @@ export default function JournalEntryForm({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSaveDraft}
|
||||
disabled={!isBalanced || !description || !selectedPeriod || !!periodMismatch || isSubmitting || isSavingDraft || isUploading || !canWrite}
|
||||
disabled={isSubmitting || isSavingDraft || isUploading || !canWrite}
|
||||
title={!canWrite ? t('read_only_tooltip') : t('save_draft_tooltip')}
|
||||
>
|
||||
{!canWrite ? <Lock className="mr-2 h-4 w-4" /> : isSavingDraft && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
@@ -1916,7 +1938,7 @@ export default function JournalEntryForm({
|
||||
)}
|
||||
<Button
|
||||
onClick={handleReview}
|
||||
disabled={!isBalanced || !description || !selectedPeriod || !!periodMismatch || isSubmitting || isSavingDraft || isUploading || !canWrite}
|
||||
disabled={isSubmitting || isSavingDraft || isUploading || !canWrite}
|
||||
title={!canWrite ? t('read_only_tooltip') : undefined}
|
||||
>
|
||||
{!canWrite && <Lock className="mr-2 h-4 w-4" />}
|
||||
@@ -1925,8 +1947,8 @@ export default function JournalEntryForm({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{(!description || !selectedPeriod || isUploading || periodMismatch || incompleteLineCount > 0 || (!isBalanced && submittableLines.length < 2)) && (
|
||||
<div className="text-xs text-muted-foreground space-y-0.5 text-right">
|
||||
{showValidationHints && (!description || !selectedPeriod || isUploading || periodMismatch || incompleteLineCount > 0 || (!isBalanced && submittableLines.length < 2)) && (
|
||||
<div className="text-xs text-destructive space-y-0.5 text-right">
|
||||
{!description && <p>{t('validation_description')}</p>}
|
||||
{!selectedPeriod && <p>{t('validation_period')}</p>}
|
||||
{periodMismatch === 'no_period' && <p>{t('validation_no_matching_period')}</p>}
|
||||
|
||||
@@ -1066,7 +1066,6 @@ export default function JournalEntryList() {
|
||||
<SelectItem value="description_desc">{t('sort_description_desc')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">{t('sort_stack_hint')}</p>
|
||||
</div>
|
||||
|
||||
{/* Verifikationsserie */}
|
||||
|
||||
@@ -1565,7 +1565,6 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('items_card_title')}</CardTitle>
|
||||
<CardDescription>{t('items_card_description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
@@ -2386,9 +2385,6 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
onChange={setDefaultDimension}
|
||||
inputClassName="h-9"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('dimensions_default_hint')}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -10,15 +10,19 @@ import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
|
||||
|
||||
/**
|
||||
* Map the endpoint's 0-1 match confidence (attached only when candidates are
|
||||
* ranked for a specific transaction) to a labelled strength badge, so the user
|
||||
* can tell an exact-amount hit from a fuzzy guess before vouching for an
|
||||
* immutable verifikat. Returns null when no confidence was attached.
|
||||
* ranked for a specific transaction) to a strength label, so the user can tell
|
||||
* an exact-amount hit from a fuzzy guess before vouching for an immutable
|
||||
* verifikat. Returns null when no confidence was attached.
|
||||
*
|
||||
* Chips mark exceptions (convention 5): a strong hit is the normal,
|
||||
* auto-selected case and renders as muted text; only the fuzzy guesses that
|
||||
* deserve a second look get a chip.
|
||||
*/
|
||||
function confidenceBadge(
|
||||
function confidenceMark(
|
||||
confidence: number | undefined,
|
||||
): { label: string; variant: 'success' | 'secondary' | 'outline' } | null {
|
||||
): { label: string; variant: 'secondary' | 'outline' | null } | null {
|
||||
if (confidence == null) return null
|
||||
if (confidence >= 0.85) return { label: 'Stark träff', variant: 'success' }
|
||||
if (confidence >= 0.85) return { label: 'Stark träff', variant: null }
|
||||
if (confidence >= 0.6) return { label: 'Trolig träff', variant: 'secondary' }
|
||||
return { label: 'Svag träff', variant: 'outline' }
|
||||
}
|
||||
@@ -132,7 +136,7 @@ export function MatchVerifikationPicker({
|
||||
// green "Stark träff" can't visually encourage an accidental double-match:
|
||||
// "Redan matchad" is the signal that matters there (N:1 stays opt-in).
|
||||
const strength =
|
||||
(selected.linked_transaction_count ?? 0) > 0 ? null : confidenceBadge(selected.confidence)
|
||||
(selected.linked_transaction_count ?? 0) > 0 ? null : confidenceMark(selected.confidence)
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-border bg-secondary/40 px-3 py-2 text-sm">
|
||||
<span className="font-mono text-xs shrink-0">{formatVoucher(selected)}</span>
|
||||
@@ -140,9 +144,13 @@ export function MatchVerifikationPicker({
|
||||
<span className="tabular-nums shrink-0">{formatCurrency(amount)}</span>
|
||||
<span className="truncate text-muted-foreground flex-1 min-w-0">{selected.entry_description}</span>
|
||||
{strength && (
|
||||
<Badge variant={strength.variant} className="shrink-0 text-[10px]">
|
||||
{strength.label}
|
||||
</Badge>
|
||||
strength.variant ? (
|
||||
<Badge variant={strength.variant} className="shrink-0 text-[10px]">
|
||||
{strength.label}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground">{strength.label}</span>
|
||||
)
|
||||
)}
|
||||
{(selected.linked_transaction_count ?? 0) > 0 && (
|
||||
<Badge variant="secondary" className="shrink-0 text-[10px]">
|
||||
@@ -175,7 +183,7 @@ export function MatchVerifikationPicker({
|
||||
{filtered.map((line) => {
|
||||
const amount = line.debit_amount > 0 ? line.debit_amount : -line.credit_amount
|
||||
const strength =
|
||||
(line.linked_transaction_count ?? 0) > 0 ? null : confidenceBadge(line.confidence)
|
||||
(line.linked_transaction_count ?? 0) > 0 ? null : confidenceMark(line.confidence)
|
||||
return (
|
||||
<button
|
||||
key={line.line_id}
|
||||
@@ -200,9 +208,13 @@ export function MatchVerifikationPicker({
|
||||
{line.line_description || line.entry_description}
|
||||
</span>
|
||||
{strength && (
|
||||
<Badge variant={strength.variant} className="shrink-0 text-[10px]">
|
||||
{strength.label}
|
||||
</Badge>
|
||||
strength.variant ? (
|
||||
<Badge variant={strength.variant} className="shrink-0 text-[10px]">
|
||||
{strength.label}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground">{strength.label}</span>
|
||||
)
|
||||
)}
|
||||
{(line.linked_transaction_count ?? 0) > 0 && (
|
||||
<Badge variant="secondary" className="shrink-0 text-[10px]">
|
||||
|
||||
@@ -1837,7 +1837,6 @@ export default function NewSupplierInvoiceForm({
|
||||
<Paperclip className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div>
|
||||
<Label>{t('document_label')}</Label>
|
||||
<p className="text-xs text-muted-foreground">{t('document_help')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<DocumentUploadZone
|
||||
@@ -1862,7 +1861,6 @@ export default function NewSupplierInvoiceForm({
|
||||
inputClassName="h-9"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('dimensions_default_hint')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { HelpPopover } from '@/components/ui/help-popover'
|
||||
import {
|
||||
MatchVerifikationPicker,
|
||||
type UnlinkedGLLine,
|
||||
@@ -188,11 +187,23 @@ export function MatchVoucherDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Matcha mot befintlig verifikation</DialogTitle>
|
||||
<DialogDescription>
|
||||
Koppla bankhändelsen till en verifikation som redan är bokförd (t.ex. en
|
||||
lön eller en post importerad från Fortnox). Ingen ny bokföring skapas.
|
||||
</DialogDescription>
|
||||
{/* Convention 7: the how-it-works copy lives behind the "?", not in
|
||||
the dialog flow. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<DialogTitle>Matcha mot befintlig verifikation</DialogTitle>
|
||||
<HelpPopover>
|
||||
<p>
|
||||
Kopplar bankhändelsen till en verifikation som redan är bokförd,
|
||||
t.ex. en lön eller en post importerad från Fortnox. Ingen ny
|
||||
bokföring skapas.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Med "Visa även matchade" kan flera bankhändelser kopplas
|
||||
till samma verifikation, t.ex. en lön utbetald i flera
|
||||
överföringar.
|
||||
</p>
|
||||
</HelpPopover>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Transaction summary */}
|
||||
@@ -240,26 +251,26 @@ export function MatchVoucherDialog({
|
||||
<MatchVerifikationPicker glLines={glLines} value={selected} onChange={setSelected} inline />
|
||||
{(selectedLine?.linked_transaction_count ?? 0) > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Verifikationen är redan matchad mot {selectedLine?.linked_transaction_count}{' '}
|
||||
transaktion{(selectedLine?.linked_transaction_count ?? 0) === 1 ? '' : 'er'}.
|
||||
Kopplingen lägger till den här transaktionen också: t.ex. en lön utbetald i
|
||||
flera överföringar.
|
||||
Redan matchad mot {selectedLine?.linked_transaction_count}{' '}
|
||||
transaktion{(selectedLine?.linked_transaction_count ?? 0) === 1 ? '' : 'er'};
|
||||
den här läggs till.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Discovery affordances: widen the date window, and surface vouchers
|
||||
already matched so another transaction can be attached (N:1). */}
|
||||
already matched so another transaction can be attached (N:1).
|
||||
Quiet links, not switches: these are list filters, and the switch
|
||||
idiom belongs to settings (convention 15). */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 pt-1">
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs text-muted-foreground">
|
||||
<Switch
|
||||
checked={includeMatched}
|
||||
onCheckedChange={setIncludeMatched}
|
||||
aria-label="Visa även matchade verifikationer"
|
||||
/>
|
||||
Visa även matchade verifikationer
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground"
|
||||
onClick={() => setIncludeMatched((v) => !v)}
|
||||
>
|
||||
{includeMatched ? 'Dölj matchade' : 'Visa även matchade'}
|
||||
</button>
|
||||
{!wideRange && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3370,7 +3370,6 @@
|
||||
"row_menu_remove_account": "Remove posting account",
|
||||
"row_menu_set_dimensions": "Set cost centre/project",
|
||||
"row_menu_remove_dimensions": "Remove cost centre/project",
|
||||
"dimensions_default_hint": "Cost centre/project applies to all rows without their own tagging.",
|
||||
"row_dimensions_inherit_hint": "Empty fields inherit the invoice default ({dims}).",
|
||||
"revenue_account_label": "Posting account",
|
||||
"revenue_account_hint": "Leave blank to derive the sales account from the VAT rate. You may select an active balance-sheet or revenue account in class 1-3. Ignored for reverse charge and export.",
|
||||
@@ -3409,7 +3408,6 @@
|
||||
"load_customers_failed_title": "Could not load customers",
|
||||
"load_customers_failed_description": "Check your connection and try again.",
|
||||
"items_card_title": "Invoice lines",
|
||||
"items_card_description": "Add products or services",
|
||||
"more_references": "References & more",
|
||||
"description_label": "Description",
|
||||
"description_placeholder": "E.g. Instagram campaign",
|
||||
@@ -4106,7 +4104,6 @@
|
||||
"payment_reference_label": "OCR / Payment reference",
|
||||
"payment_reference_placeholder": "OCR-nummer",
|
||||
"document_label": "Invoice document",
|
||||
"document_help": "Attach the supplier's PDF or an image. The document is stored with the invoice and automatically attached to the verifikation.",
|
||||
"document_upload_in_progress_title": "The document is still uploading",
|
||||
"document_upload_in_progress_description": "Wait for the upload to finish before registering the invoice.",
|
||||
"document_upload_failed_title": "The document could not be uploaded",
|
||||
@@ -4137,7 +4134,6 @@
|
||||
"row_dimensions_aria": "Cost centre/project for row {index}",
|
||||
"row_dimensions_title": "Cost centre/project",
|
||||
"row_dimensions_inherit_hint": "Empty fields inherit the invoice default ({dims}).",
|
||||
"dimensions_default_hint": "Cost centre/project applies to all rows without their own tagging.",
|
||||
"ai_totals_label": "From invoice (AI):",
|
||||
"ai_net": "Net {amount}",
|
||||
"ai_vat": "VAT {amount}",
|
||||
@@ -4452,7 +4448,6 @@
|
||||
"sort_description_asc": "Description, A to Z",
|
||||
"sort_description_desc": "Description, Z to A",
|
||||
"sort_by": "Sort by {column}",
|
||||
"sort_stack_hint": "Tip: Shift-click a column header to add it as a secondary sort.",
|
||||
"date_from_placeholder": "From YYYY-MM-DD",
|
||||
"date_to_placeholder": "To YYYY-MM-DD",
|
||||
"filter": "Filter",
|
||||
@@ -4600,7 +4595,6 @@
|
||||
"replace_failed": "Could not upload new version.",
|
||||
"choose_from_inbox": "Choose from inbox",
|
||||
"picker_title": "Choose a document from the inbox",
|
||||
"picker_description": "Documents received by email or upload that haven't been used yet.",
|
||||
"picker_search_placeholder": "Search supplier or file name…",
|
||||
"picker_results": "{count}",
|
||||
"picker_empty": "No unused documents in the inbox.",
|
||||
@@ -4704,7 +4698,6 @@
|
||||
"no_attachments": "No documents attached",
|
||||
"references_count": "{count, plural, one {# reference} other {# references}}",
|
||||
"references_title": "Linked supporting documents",
|
||||
"references_subtitle": "Reference to the invoice that identifies the transaction, part of the audit trail.",
|
||||
"reference_invoice": "Customer invoice {number}",
|
||||
"reference_supplier_invoice": "Supplier invoice {number}",
|
||||
"currency_title": "Currency conversion",
|
||||
@@ -4817,12 +4810,10 @@
|
||||
"toast_draft_missing_docs_description": "The draft was saved, but {count} document(s) could not be attached: {files}. Open the draft to attach them again.",
|
||||
"toast_open_entry": "Open the entry",
|
||||
"fill_balance_tooltip": "Double-click to fill the balancing amount",
|
||||
"fill_balance_hint": "Tip: double-click debit or credit to fill the remaining difference.",
|
||||
"keyboard_hint": "Enter jumps to the next field: once the entry balances, Enter opens the review.",
|
||||
"review_month_changed": "Note: different month than the previous voucher ({prev} → {current}).",
|
||||
"review_period_locked": "This period is closed or locked: posting may be rejected.",
|
||||
"add_dimensions": "Cost centre/Project",
|
||||
"dimensions_apply_all_hint": "Applies to all rows without their own tag.",
|
||||
"row_dimensions_aria": "Cost centre/project for this row"
|
||||
},
|
||||
"chart_of_accounts": {
|
||||
|
||||
@@ -3370,7 +3370,6 @@
|
||||
"row_menu_remove_account": "Ta bort bokföringskonto",
|
||||
"row_menu_set_dimensions": "Ange kostnadsställe/projekt",
|
||||
"row_menu_remove_dimensions": "Ta bort kostnadsställe/projekt",
|
||||
"dimensions_default_hint": "Kostnadsställe/projekt gäller alla rader utan egen märkning.",
|
||||
"row_dimensions_inherit_hint": "Tomma fält ärver fakturans standard ({dims}).",
|
||||
"revenue_account_label": "Bokföringskonto",
|
||||
"revenue_account_hint": "Lämna tomt för att härleda försäljningskontot från momssatsen. Du kan välja ett aktivt balans- eller intäktskonto i klass 1-3. Ignoreras för omvänd skattskyldighet och export.",
|
||||
@@ -3409,7 +3408,6 @@
|
||||
"load_customers_failed_title": "Kunde inte ladda kunder",
|
||||
"load_customers_failed_description": "Kontrollera din anslutning och försök igen.",
|
||||
"items_card_title": "Fakturarader",
|
||||
"items_card_description": "Lägg till produkter eller tjänster",
|
||||
"more_references": "Referenser & mer",
|
||||
"description_label": "Beskrivning",
|
||||
"description_placeholder": "T.ex. Instagram-kampanj",
|
||||
@@ -4106,7 +4104,6 @@
|
||||
"payment_reference_label": "OCR / Betalningsreferens",
|
||||
"payment_reference_placeholder": "OCR-nummer",
|
||||
"document_label": "Fakturaunderlag",
|
||||
"document_help": "Bifoga leverantörens PDF eller en bild. Underlaget sparas med fakturan och kopplas automatiskt till verifikationen.",
|
||||
"document_upload_in_progress_title": "Underlaget laddas fortfarande upp",
|
||||
"document_upload_in_progress_description": "Vänta tills uppladdningen är klar innan du registrerar fakturan.",
|
||||
"document_upload_failed_title": "Underlaget kunde inte laddas upp",
|
||||
@@ -4137,7 +4134,6 @@
|
||||
"row_dimensions_aria": "Kostnadsställe/Projekt för rad {index}",
|
||||
"row_dimensions_title": "Kostnadsställe/Projekt",
|
||||
"row_dimensions_inherit_hint": "Tomma fält ärver fakturans standard ({dims}).",
|
||||
"dimensions_default_hint": "Kostnadsställe/projekt gäller alla rader utan egen märkning.",
|
||||
"ai_totals_label": "Från fakturan (AI):",
|
||||
"ai_net": "Netto {amount}",
|
||||
"ai_vat": "Moms {amount}",
|
||||
@@ -4452,7 +4448,6 @@
|
||||
"sort_description_asc": "Beskrivning, A till Ö",
|
||||
"sort_description_desc": "Beskrivning, Ö till A",
|
||||
"sort_by": "Sortera efter {column}",
|
||||
"sort_stack_hint": "Tips: Shift-klicka på en kolumnrubrik för att lägga till den som sekundär sortering.",
|
||||
"date_from_placeholder": "Från YYYY-MM-DD",
|
||||
"date_to_placeholder": "Till YYYY-MM-DD",
|
||||
"filter": "Filtrera",
|
||||
@@ -4600,7 +4595,6 @@
|
||||
"replace_failed": "Kunde inte ladda upp ny version.",
|
||||
"choose_from_inbox": "Välj från inkorgen",
|
||||
"picker_title": "Välj underlag från inkorgen",
|
||||
"picker_description": "Underlag som kommit in via e-post eller uppladdning och ännu inte använts.",
|
||||
"picker_search_placeholder": "Sök leverantör eller filnamn…",
|
||||
"picker_results": "{count} st",
|
||||
"picker_empty": "Inga oanvända underlag i inkorgen.",
|
||||
@@ -4704,7 +4698,6 @@
|
||||
"no_attachments": "Inga underlag bifogade",
|
||||
"references_count": "{count, plural, one {# hänvisning} other {# hänvisningar}}",
|
||||
"references_title": "Kopplat underlag",
|
||||
"references_subtitle": "Hänvisning till fakturan som identifierar affärshändelsen, del av verifieringskedjan.",
|
||||
"reference_invoice": "Kundfaktura {number}",
|
||||
"reference_supplier_invoice": "Leverantörsfaktura {number}",
|
||||
"currency_title": "Valutaomräkning",
|
||||
@@ -4817,12 +4810,10 @@
|
||||
"toast_draft_missing_docs_description": "Utkastet sparades, men {count} underlag kunde inte bifogas: {files}. Öppna utkastet för att bifoga på nytt.",
|
||||
"toast_open_entry": "Öppna verifikatet",
|
||||
"fill_balance_tooltip": "Dubbelklicka för att fylla i balanserande belopp",
|
||||
"fill_balance_hint": "Tips: dubbelklicka på debet eller kredit för att fylla i differensen.",
|
||||
"keyboard_hint": "Enter hoppar till nästa fält: när verifikatet balanserar öppnar Enter granskningen.",
|
||||
"review_month_changed": "Obs: annan månad än föregående verifikat ({prev} → {current}).",
|
||||
"review_period_locked": "Perioden är stängd eller låst: bokföring kan nekas.",
|
||||
"add_dimensions": "Kostnadsställe/Projekt",
|
||||
"dimensions_apply_all_hint": "Gäller alla rader utan egen märkning.",
|
||||
"row_dimensions_aria": "Kostnadsställe/Projekt för raden"
|
||||
},
|
||||
"chart_of_accounts": {
|
||||
|
||||
Reference in New Issue
Block a user