feat(reconciliation): absorb the last bank-view tools and retire /reports/bank-reconciliation (#1871)
The old Bankavstämning report page was the only place a user could still tag a bank row as ingående balans or move it to another bank account, so the new /reconciliation page kept linking out to it and the reconciliation lived in two places. Both row tools now live on the account overview (hover-revealed, same endpoints), the ?autorun=1 deep link from the transactions inbox runs the matcher on the new page, and the report slug redirects: old links, ⌘K, the bokslut readiness wizard and the ignore toast all land on /reconciliation. BankReconciliationView and its FocusedReport branches (own range preset, help popover, autoRun plumbing) are removed; the source-grepping parity test for its quick-book path goes with it. 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:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
d88df74b85
commit
9ebb2e518f
@@ -19,7 +19,11 @@ export default async function ReportSlugPage({
|
||||
const [{ slug }, query] = await Promise.all([params, searchParams])
|
||||
const report = getReport(slug)
|
||||
if (!report) notFound()
|
||||
if (report.route) redirect(report.route)
|
||||
if (report.route) {
|
||||
// The old bankavstämning deep link (?autorun=1 from the transactions
|
||||
// inbox) keeps working on the page that absorbed it.
|
||||
redirect(slug === 'bank-reconciliation' && query.autorun === '1' ? `${report.route}?autorun=1` : report.route)
|
||||
}
|
||||
|
||||
const [{ supabase }, companyId] = await Promise.all([
|
||||
getDashboardAuthContext(),
|
||||
@@ -39,9 +43,6 @@ export default async function ReportSlugPage({
|
||||
slug={slug}
|
||||
initialPeriods={(periods ?? []) as FiscalPeriod[]}
|
||||
initialCompanyId={companyId}
|
||||
// ?autorun=1 deep-links (e.g. the transactions inbox banner) ask the
|
||||
// bank-reconciliation view to run its dry-run preview once on load.
|
||||
autoRun={query.autorun === '1'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3180,7 +3180,7 @@ export default function TransactionsPage() {
|
||||
description: `${successes} transaktioner ignorerade`,
|
||||
action: (
|
||||
<ToastAction altText="Öppna Bankavstämning" asChild>
|
||||
<Link href="/reports/bank-reconciliation">Bankavstämning</Link>
|
||||
<Link href="/reconciliation">Avstämning</Link>
|
||||
</ToastAction>
|
||||
),
|
||||
})
|
||||
@@ -3852,7 +3852,7 @@ export default function TransactionsPage() {
|
||||
className="px-1 pt-3"
|
||||
action={{
|
||||
label: t('recon_attn_action'),
|
||||
href: '/reports/bank-reconciliation?autorun=1',
|
||||
href: '/reconciliation?autorun=1',
|
||||
}}
|
||||
>
|
||||
{t('recon_attn', { count: selectableInboxIds.length })}
|
||||
@@ -3900,7 +3900,7 @@ export default function TransactionsPage() {
|
||||
<BankSyncNowButton />
|
||||
<BankSyncSinceLastVisit />
|
||||
<Link
|
||||
href="/reports/bank-reconciliation"
|
||||
href="/reconciliation"
|
||||
className="ml-auto transition-colors duration-150 hover:text-foreground"
|
||||
>
|
||||
Bankavstämning →
|
||||
|
||||
@@ -70,7 +70,7 @@ const PAGE_ENTRIES: Entry[] = [
|
||||
{ id: 'rapport-huvudbok', label: 'Visa rapport: Huvudbok', icon: BookOpen, href: '/reports/huvudbok', keywords: 'rapport huvudbok ledger general konto saldo transaktioner per konto verifikat verifikationer verifikationer per konto kontoutdrag kontoanalys kontokort kontohistorik balance account statement transactions vouchers' },
|
||||
{ id: 'rapport-kundreskontra', label: 'Visa rapport: Kundreskontra', icon: Users, href: '/reports/kundreskontra', keywords: 'rapport kundreskontra ar kundfordringar' },
|
||||
{ id: 'avstamning', label: 'Avstämning', hint: 'Stäm av bank och skattekonto', icon: Scale, href: '/reconciliation', keywords: 'avstämning stäm av bank skattekonto matcha reconcile reconciliation 1630 1930' },
|
||||
{ id: 'rapport-bankavstamning', label: 'Bankavstämning', hint: 'Stäm av bank mot bokföring', icon: ArrowLeftRight, href: '/reports/bank-reconciliation', keywords: 'avstämning stäm av bank matcha banktransaktioner reconcile reconciliation 1930' },
|
||||
{ id: 'rapport-bankavstamning', label: 'Bankavstämning', hint: 'Stäm av bank mot bokföring', icon: ArrowLeftRight, href: '/reconciliation', keywords: 'avstämning stäm av bank matcha banktransaktioner reconcile reconciliation 1930' },
|
||||
{ id: 'importera', label: 'Importera', icon: Upload, href: '/import' },
|
||||
{ id: 'granskning', label: 'Granskning', icon: ClipboardCheck, href: '/pending', keywords: 'pending review' },
|
||||
{ id: 'löner', label: 'Löner', icon: HandCoins, href: '/salary' },
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AttnLine } from '@/components/ui/attn-line'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
@@ -67,6 +68,8 @@ export interface ReconciliationWindow {
|
||||
|
||||
interface AccountOverviewProps {
|
||||
account: ReconciliationAccount
|
||||
/** The other bank accounts in the rail: targets for "Flytta till konto". */
|
||||
otherBankAccounts?: ReconciliationAccount[]
|
||||
/** The account rail. Rendered inside the summary grid so the items table below can span the full page width (the approved layout). */
|
||||
rail: ReactNode
|
||||
/** The selected period: scopes the bank bridge and the item windows; its end is the default sign-off date. */
|
||||
@@ -75,7 +78,7 @@ interface AccountOverviewProps {
|
||||
onChanged: () => void
|
||||
}
|
||||
|
||||
export function AccountOverview({ account, rail, window, onChanged }: AccountOverviewProps) {
|
||||
export function AccountOverview({ account, rail, otherBankAccounts = [], window, onChanged }: AccountOverviewProps) {
|
||||
const t = useTranslations('reconciliation')
|
||||
const locale = useLocale()
|
||||
const { toast } = useToast()
|
||||
@@ -87,6 +90,9 @@ export function AccountOverview({ account, rail, window, onChanged }: AccountOve
|
||||
const [bookRow, setBookRow] = useState<ReconciliationItem | null>(null)
|
||||
const [signoffOpen, setSignoffOpen] = useState(false)
|
||||
const [matcher, setMatcher] = useState<MatcherMatch[] | null>(null)
|
||||
const searchParams = useSearchParams()
|
||||
const autorunRequested = searchParams.get('autorun') === '1'
|
||||
const autorunDone = useRef(false)
|
||||
|
||||
const isSkv = account.kind === 'skattekonto'
|
||||
const base = `/api/reconciliation/accounts/${encodeURIComponent(account.account_key)}`
|
||||
@@ -317,6 +323,48 @@ export function AccountOverview({ account, rail, window, onChanged }: AccountOve
|
||||
}
|
||||
}
|
||||
|
||||
async function markOpeningBalance(item: ReconciliationItem) {
|
||||
setBusy(item.item_id)
|
||||
try {
|
||||
const data = await postJson('/api/reconciliation/bank/mark-opening-balance', { journal_entry_id: item.item_id })
|
||||
if (data !== null) {
|
||||
toast({ title: t('toast_marked_ib') })
|
||||
await refresh()
|
||||
}
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToAccount(item: ReconciliationItem, target: ReconciliationAccount) {
|
||||
setBusy(item.item_id)
|
||||
try {
|
||||
const res = await fetch(`/api/transactions/${item.item_id}/cash-account`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ account_number: target.account_number }),
|
||||
})
|
||||
const json = await res.json().catch(() => ({}))
|
||||
if (!res.ok) {
|
||||
toast({ title: t('toast_failed'), description: getUserErrorMessage(json, { statusCode: res.status }), variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
toast({ title: t('toast_moved', { account: `${target.name} (${target.account_number})` }) })
|
||||
await refresh()
|
||||
} finally {
|
||||
setBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
// ?autorun=1 (the old bankavstämning deep link from the transactions inbox):
|
||||
// run the matcher preview once the bridge is up, once per mount.
|
||||
useEffect(() => {
|
||||
if (!autorunRequested || autorunDone.current || isSkv || !status) return
|
||||
autorunDone.current = true
|
||||
void runMatcher()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- fire once when the status first loads
|
||||
}, [autorunRequested, isSkv, status])
|
||||
|
||||
// ---- render -------------------------------------------------------------
|
||||
|
||||
if (loadError) {
|
||||
@@ -431,7 +479,6 @@ export function AccountOverview({ account, rail, window, onChanged }: AccountOve
|
||||
(byBucket.get('unmatched_external')?.length ?? 0) +
|
||||
(byBucket.get('unmatched_ledger')?.length ?? 0)
|
||||
|
||||
const bankViewHref = '/reports/bank-reconciliation'
|
||||
|
||||
// Default sign-off date: the window end, never past today nor past the
|
||||
// skattekonto snapshot. The button hides when that date is already signed.
|
||||
@@ -537,20 +584,26 @@ export function AccountOverview({ account, rail, window, onChanged }: AccountOve
|
||||
{t('signoff_button', { date: formatDate(signoffDefaultDate) })}
|
||||
</Button>
|
||||
)}
|
||||
<span className="ml-auto">
|
||||
<Link href={isSkv ? '/skattekonto' : bankViewHref} className={QUIET_LINK_CLASS}>
|
||||
{isSkv ? t('action_open_skattekonto') : t('action_open_bank_view')}
|
||||
</Link>
|
||||
</span>
|
||||
{isSkv && (
|
||||
<span className="ml-auto">
|
||||
<Link href="/skattekonto" className={QUIET_LINK_CLASS}>
|
||||
{t('action_open_skattekonto')}
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{items.older_unmatched_count > 0 && (
|
||||
<p className="text-[12.5px] text-muted-foreground">
|
||||
{t('older_unmatched', { count: items.older_unmatched_count })}
|
||||
{' · '}
|
||||
<Link href={isSkv ? '/skattekonto' : bankViewHref} className={QUIET_LINK_CLASS}>
|
||||
{t('older_show')}
|
||||
</Link>
|
||||
{isSkv && (
|
||||
<>
|
||||
{' · '}
|
||||
<Link href="/skattekonto" className={QUIET_LINK_CLASS}>
|
||||
{t('older_show')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -635,6 +688,9 @@ export function AccountOverview({ account, rail, window, onChanged }: AccountOve
|
||||
onIgnore={() => void setIgnored(item, true)}
|
||||
onUnignore={() => void setIgnored(item, false)}
|
||||
onBook={() => setBookRow(item)}
|
||||
onMarkIb={!isSkv && item.side === 'ledger' && item.bucket === 'unmatched_ledger' ? () => void markOpeningBalance(item) : undefined}
|
||||
moveTargets={!isSkv && item.item_type === 'transaction' && item.bucket === 'unmatched_external' ? otherBankAccounts : []}
|
||||
onMove={(target) => void moveToAccount(item, target)}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
@@ -710,6 +766,11 @@ interface ItemRowProps {
|
||||
onIgnore: () => void
|
||||
onUnignore: () => void
|
||||
onBook: () => void
|
||||
/** "Märk som IB" for a ledger row without a bank counterpart (bank accounts). */
|
||||
onMarkIb?: () => void
|
||||
/** Other bank accounts a stray transaction can be moved to. */
|
||||
moveTargets: ReconciliationAccount[]
|
||||
onMove: (target: ReconciliationAccount) => void
|
||||
}
|
||||
|
||||
function ItemRow({
|
||||
@@ -724,6 +785,9 @@ function ItemRow({
|
||||
onIgnore,
|
||||
onUnignore,
|
||||
onBook,
|
||||
onMarkIb,
|
||||
moveTargets,
|
||||
onMove,
|
||||
}: ItemRowProps) {
|
||||
const t = useTranslations('reconciliation')
|
||||
const can = (a: ReconciliationItem['actions'][number]) => item.actions.includes(a)
|
||||
@@ -837,6 +901,30 @@ function ItemRow({
|
||||
{t('row_unignore')}
|
||||
</button>
|
||||
)}
|
||||
{onMarkIb && (
|
||||
<button type="button" onClick={onMarkIb} disabled={anyBusy} className={cn(QUIET_LINK_CLASS, HOVER_REVEAL_CLASS)}>
|
||||
{t('row_mark_ib')}
|
||||
</button>
|
||||
)}
|
||||
{moveTargets.length > 0 && (
|
||||
<select
|
||||
aria-label={t('row_move')}
|
||||
value=""
|
||||
disabled={anyBusy}
|
||||
onChange={(e) => {
|
||||
const target = moveTargets.find((a) => a.account_key === e.target.value)
|
||||
if (target) onMove(target)
|
||||
}}
|
||||
className={cn('h-7 rounded-full border border-border bg-background px-2 text-[11.5px] text-muted-foreground', HOVER_REVEAL_CLASS)}
|
||||
>
|
||||
<option value="">{t('row_move')}</option>
|
||||
{moveTargets.map((a) => (
|
||||
<option key={a.account_key} value={a.account_key}>
|
||||
{a.name} ({a.account_number})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -214,6 +214,7 @@ export function ReconciliationWorkspace({ initialPeriods, initialCompanyId }: Re
|
||||
key={selected.account_key}
|
||||
account={selected}
|
||||
rail={<ReconciliationRail accounts={accounts} selectedKey={selected.account_key} onSelect={select} />}
|
||||
otherBankAccounts={accounts.filter((a) => a.kind === 'bank' && a.account_key !== selected.account_key && !a.superseded_by)}
|
||||
window={window}
|
||||
onChanged={() => void load()}
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@ import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
import { HelpPopover } from '@/components/ui/help-popover'
|
||||
import { EmptyState } from '@/components/ui/empty-state'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
@@ -18,10 +17,6 @@ import { DimensionFilter, type DimensionFilterValue } from '@/components/reports
|
||||
import { DATE_RANGE_SLUGS, DIMENSION_FILTER_SLUGS, getReport } from '@/lib/reports/catalog'
|
||||
import type { FiscalPeriod } from '@/types'
|
||||
|
||||
/** Preset memory for the bank-reconciliation range, deliberately separate from
|
||||
* the shared report-family key so the two cannot steer each other. */
|
||||
const RECONCILIATION_RANGE_KEY_PREFIX = 'Accounted:recon-range-preset:'
|
||||
|
||||
function ReportViewLoading() {
|
||||
return (
|
||||
<Card>
|
||||
@@ -60,10 +55,6 @@ const BehandlingshistorikView = dynamic(() =>
|
||||
import('./BehandlingshistorikView').then((module) => ({ default: module.BehandlingshistorikView })),
|
||||
{ loading: ReportViewLoading },
|
||||
)
|
||||
const BankReconciliationView = dynamic(() =>
|
||||
import('./BankReconciliationView').then((module) => ({ default: module.BankReconciliationView })),
|
||||
{ loading: ReportViewLoading },
|
||||
)
|
||||
|
||||
/**
|
||||
* The focused single-report experience at /reports/[slug]. Carries one report:
|
||||
@@ -76,13 +67,10 @@ function FocusedReportInner({
|
||||
slug,
|
||||
initialPeriods,
|
||||
initialCompanyId,
|
||||
autoRun,
|
||||
}: {
|
||||
slug: string
|
||||
initialPeriods: FiscalPeriod[]
|
||||
initialCompanyId: string | null
|
||||
/** ?autorun=1: the bank-reconciliation view runs its preview once on load. */
|
||||
autoRun?: boolean
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
@@ -134,16 +122,7 @@ function FocusedReportInner({
|
||||
// Page help behind a "?" (UI-migration convention 7): the report
|
||||
// bodies carry no instructional copy in the page flow.
|
||||
help={
|
||||
slug === 'bank-reconciliation' ? (
|
||||
<HelpPopover>
|
||||
<div className="space-y-2">
|
||||
<p>{t('help_bank_reconciliation_scope')}</p>
|
||||
<p>{t('help_bank_reconciliation_preview')}</p>
|
||||
<p>{t('help_bank_reconciliation_ib')}</p>
|
||||
<p>{t('help_bank_reconciliation_ignored')}</p>
|
||||
</div>
|
||||
</HelpPopover>
|
||||
) : undefined
|
||||
undefined
|
||||
}
|
||||
action={
|
||||
<FyPicker
|
||||
@@ -171,14 +150,6 @@ function FocusedReportInner({
|
||||
periodEnd={selectedPeriodBounds.end}
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
// A reconciliation is carried out over a whole räkenskapsår, so it
|
||||
// opens on the full year and keeps its own preset memory: inheriting
|
||||
// a "Denna månad" left over from Resultatrapport would show an
|
||||
// alarming difference for a window the user never chose here.
|
||||
defaultPreset={slug === 'bank-reconciliation' ? 'full_year' : undefined}
|
||||
storageKeyPrefix={
|
||||
slug === 'bank-reconciliation' ? RECONCILIATION_RANGE_KEY_PREFIX : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -198,14 +169,12 @@ function FocusedReportInner({
|
||||
slug={slug}
|
||||
reportName={reportName}
|
||||
periodId={selectedPeriod}
|
||||
periodBounds={selectedPeriodBounds}
|
||||
dateRange={dateRange}
|
||||
dimensionFilter={dimensionFilter}
|
||||
accountFilter={accountFilter}
|
||||
isEnskildFirma={isEnskildFirma}
|
||||
isAktiebolag={isAktiebolag}
|
||||
onNavigateToAccount={navigateToAccount}
|
||||
autoRun={autoRun}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
@@ -223,26 +192,22 @@ function FocusedView({
|
||||
slug,
|
||||
reportName,
|
||||
periodId,
|
||||
periodBounds,
|
||||
dateRange,
|
||||
dimensionFilter,
|
||||
accountFilter,
|
||||
isEnskildFirma,
|
||||
isAktiebolag,
|
||||
onNavigateToAccount,
|
||||
autoRun,
|
||||
}: {
|
||||
slug: string
|
||||
reportName: string
|
||||
periodId: string
|
||||
periodBounds: { start: string; end: string } | null
|
||||
dateRange: DateRangeValue
|
||||
dimensionFilter: DimensionFilterValue | null
|
||||
accountFilter: string | null
|
||||
isEnskildFirma: boolean
|
||||
isAktiebolag: boolean
|
||||
onNavigateToAccount: (account: string) => void
|
||||
autoRun?: boolean
|
||||
}) {
|
||||
switch (slug) {
|
||||
case 'resultatrapport':
|
||||
@@ -275,15 +240,6 @@ function FocusedView({
|
||||
return <SupplierLedgerView periodId={periodId} />
|
||||
case 'behandlingshistorik':
|
||||
return <BehandlingshistorikView periodId={periodId} dateRange={dateRange} />
|
||||
case 'bank-reconciliation':
|
||||
return (
|
||||
<BankReconciliationView
|
||||
periodId={periodId}
|
||||
periodBounds={periodBounds}
|
||||
dateRange={dateRange}
|
||||
autoRun={autoRun}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -293,13 +249,10 @@ export function FocusedReport({
|
||||
slug,
|
||||
initialPeriods,
|
||||
initialCompanyId,
|
||||
autoRun,
|
||||
}: {
|
||||
slug: string
|
||||
initialPeriods: FiscalPeriod[]
|
||||
initialCompanyId: string | null
|
||||
/** ?autorun=1: the bank-reconciliation view runs its preview once on load. */
|
||||
autoRun?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Suspense fallback={<div className="space-y-8" />}>
|
||||
@@ -307,7 +260,6 @@ export function FocusedReport({
|
||||
slug={slug}
|
||||
initialPeriods={initialPeriods}
|
||||
initialCompanyId={initialCompanyId}
|
||||
autoRun={autoRun}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
@@ -112,14 +112,11 @@ describe('transactions page booking feedback', () => {
|
||||
* Duplicate-guard feedback parity: every client of POST /categorize must route
|
||||
* a TRANSACTION_BOOK_POSSIBLE_DUPLICATE 409 into DuplicateBookingDialog (which
|
||||
* offers match / ignore / book-anyway), never into a destructive toast that
|
||||
* names no way forward. Two clients used to dead-end: the counterparty-
|
||||
* template branch of handleQuickReviewConfirm, and BankReconciliationView's
|
||||
* quick-book.
|
||||
* names no way forward. The counterparty-template branch of
|
||||
* handleQuickReviewConfirm used to dead-end. (The old BankReconciliationView
|
||||
* quick-book was retired with the view on 2026-08-25; /reconciliation books
|
||||
* through the shared transactions inbox flow instead.)
|
||||
*/
|
||||
const BANK_RECON_SRC = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../reports/BankReconciliationView.tsx'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
describe('duplicate-guard 409 routing parity', () => {
|
||||
it('handles the duplicate code on both booking paths of the transactions page', () => {
|
||||
@@ -136,14 +133,4 @@ describe('duplicate-guard 409 routing parity', () => {
|
||||
/TRANSACTION_BOOK_POSSIBLE_DUPLICATE'[\s\S]{0,600}cpCategorize\(\{\s*\n?\s*expectedDuplicateJournalEntryId: candidate\.journal_entry_id,/,
|
||||
)
|
||||
})
|
||||
|
||||
it('routes the quick-book 409 on the reconciliation page into the shared dialog', () => {
|
||||
expect(BANK_RECON_SRC).toContain("'TRANSACTION_BOOK_POSSIBLE_DUPLICATE'")
|
||||
expect(BANK_RECON_SRC).toContain('<DuplicateBookingDialog')
|
||||
expect(BANK_RECON_SRC).toMatch(/setDuplicateWarning\(\{/)
|
||||
// The retry re-runs the quick-book with force bound to the candidate.
|
||||
expect(BANK_RECON_SRC).toMatch(
|
||||
/handleQuickBook\(transactionId, templateId, \{\s*\n?\s*expectedDuplicateJournalEntryId: candidate\.journal_entry_id,/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -152,10 +152,7 @@ export async function buildBokslutReadinessReport(
|
||||
reconciliation.unmatched_transaction_count > 0
|
||||
? `${reconciliation.unmatched_transaction_count} banktransaktioner är inte matchade. Avstäm banken innan bokslut.`
|
||||
: `Bankavstämningen visar en differens på ${reconciliation.difference.toFixed(2)} kr.`,
|
||||
// Bankavstämning's real route: the earlier '/reconciliation/bank' href
|
||||
// pointed at a page that has never existed, so the wizard's "Öppna"
|
||||
// link 404ed.
|
||||
href: '/reports/bank-reconciliation',
|
||||
href: '/reconciliation',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -305,6 +305,10 @@ export const REPORT_CATALOG: ReportDescriptor[] = [
|
||||
// uses the shared ReportDateRange like every other report, mounted with a
|
||||
// full-year default and its own preset memory (see FocusedReport).
|
||||
params: 'fiscal-range',
|
||||
// 2026-08-25: the bank view was absorbed by /reconciliation (matcher,
|
||||
// manual N:1 matching, residual booking, IB tag, move-to-account all live
|
||||
// there). The slug stays for old links and the report library; it redirects.
|
||||
route: '/reconciliation',
|
||||
},
|
||||
|
||||
// --- Export & arkiv: library-only ---
|
||||
|
||||
+5
-1
@@ -7922,7 +7922,11 @@
|
||||
"residual_interest_expense": "Interest expense (8410)",
|
||||
"residual_interest_income": "Interest income (8310)",
|
||||
"residual_rounding": "Rounding (3740)",
|
||||
"toast_residual_booked": "{amount} booked and linked"
|
||||
"toast_residual_booked": "{amount} booked and linked",
|
||||
"row_mark_ib": "Mark as opening balance",
|
||||
"row_move": "Move to account",
|
||||
"toast_marked_ib": "The voucher was marked as opening balance",
|
||||
"toast_moved": "The transaction was moved to {account}"
|
||||
},
|
||||
"skattekonto": {
|
||||
"help_text": "The balance and events are fetched from Skatteverket and synced automatically every night. Completed events are booked against 1630 Skattekonto, usually automatically; anything that cannot be matched is flagged in the list. Pay in via bankgiro 5050-1055 with your OCR number.",
|
||||
|
||||
+5
-1
@@ -7922,7 +7922,11 @@
|
||||
"residual_interest_expense": "Räntekostnad (8410)",
|
||||
"residual_interest_income": "Ränteintäkt (8310)",
|
||||
"residual_rounding": "Öresavrundning (3740)",
|
||||
"toast_residual_booked": "{amount} bokfört och kopplat"
|
||||
"toast_residual_booked": "{amount} bokfört och kopplat",
|
||||
"row_mark_ib": "Märk som IB",
|
||||
"row_move": "Flytta till konto",
|
||||
"toast_marked_ib": "Verifikatet markerades som ingående balans",
|
||||
"toast_moved": "Transaktionen flyttades till {account}"
|
||||
},
|
||||
"skattekonto": {
|
||||
"help_text": "Saldot och händelserna hämtas från Skatteverket och synkas automatiskt varje natt. Genomförda händelser bokförs mot 1630 Skattekonto, oftast automatiskt; det som inte kan matchas flaggas i listan. Betala in via bankgiro 5050-1055 med ditt OCR-nummer.",
|
||||
|
||||
Reference in New Issue
Block a user