d41ef2a909
* feat(sandbox): seed payroll, articles and a year of ledger history; calm the connect CTAs The sandbox showed neither Löner nor a usable set of reports, and the "connect X" surfaces were oversized boxed cards. Sandbox seed: - pays_salaries + employer_registered, so Löner and Anställda appear at all (an enskild firma is not an employer by default). Both seeded employees are employment_type 'employee': an EF may employ staff, just not its own owner. - Two employees, one booked and one open lönekörning, and the three verifikat the booked run must have posted (7210/2710/1930, 7510/2731, 7290+7519/ 2920+2940). Skatteavdrag comes from the real Skatteverket 2026 tables. - Year-to-date ledger history, January through last month, with the quarterly momsredovisning cleared to 2650 and paid on the SFL deadline. Without the settlement the demo collected VAT all year and never remitted it, which left an implausible bank balance and 155 813 kr of moms "att betala". - The history is exempted through journal_entry_no_doc_required, the same way the SIE-import opt-in treats imported books: its kvitton live in the previous system, and unflagged it put 39 "verifikat utan underlag" on the home screen. - Artikelregister, and the BAS accounts the K1 chart omits for an enskild firma. - History is numbered before the invoice and payroll vouchers so the series runs forwards through the year, and its writes are batched. Connect CTAs: - Bank picker: a two-column grid of 95px bordered logo cards becomes flat hairline rows, Lucide icons, and a quiet inline connecting state. - Cloud backup: each provider collapses to one row; the BFL note is shown once for the section and names only configured destinations. - Hem first-run: only the active step argues its case, but every not-done step keeps a reachable action. The Skatteverket nudge becomes one quiet sentence. Mobile assistant FAB: a fresh open is desktop-only, since the bottom nav already has an Assistent tab. A collapsed session keeps its handle everywhere except /chat, which is itself the way back to the conversation. Also closes a real hole: /api/salary/runs/[id]/payslips/send had no sandbox guard, and a seeded booked run put "Skicka lönebesked" one click from an anonymous visitor with live Resend behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sandbox): check the two unchecked Supabase errors and tighten review nits CodeRabbit review on #1437. Major: two calls discarded their error and continued with null data. A failed chart_of_accounts re-select would have written account_id: null onto every ledger-history and salary voucher line, and a failed next_voucher_number would have inserted a posted verifikat with no number, which is a hole in the verifikationsserie (BFNAR 2013:2). Both now throw, and a null voucher number is rejected explicitly. Minor: the A-004 note claimed a 10 % markup on numbers that are 11.1 %; the salary breakdown test's name said the opposite of its assertions after the switch to the real tax table; the ledger-history doc still said 4 to 6 verifikat per month before the quarterly momsredovisning added a seventh in March, May and June. Bank picker: the spinner is aria-hidden, so loading and connecting had no text equivalent and a failed bank fetch was never announced. Added role="status" with an sr-only label, and role="alert" on the error line. Declined: confirm-before-disconnect on the cloud-backup row. Disconnect was unconfirmed before this PR too, so adding a dialog is a behaviour change beyond the redesign rather than a fix to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
/**
|
|
* Demo articles (artikelregister) for the sandbox company.
|
|
*
|
|
* /articles renders the Package empty state in a fresh sandbox, which hides an
|
|
* entire surface: reusable invoice-line presets are how most users actually
|
|
* build a faktura. Five rows are enough to show the register's shape (numbering,
|
|
* vara vs tjanst, unit, price excl VAT, VAT rate, active flag) without turning
|
|
* the demo into a catalogue.
|
|
*
|
|
* Modelled on a one-person Swedish IT/design consultancy (enskild firma), which
|
|
* is what the rest of the seed portrays: hourly work on löpande räkning, one
|
|
* fixed-price package, one rebilled licence, and one printed book.
|
|
*
|
|
* VAT rates are law, not decoration:
|
|
* - Consulting and design services, the fixed-price package and the rebilled
|
|
* software licence are all ordinary 25 % supplies (ML 6 kap.).
|
|
* - The printed handbook is 6 %: böcker, broschyrer och häften carry the
|
|
* reduced rate (ML 9 kap., the 6 % rates sit in §§ 8-15). No other article
|
|
* here qualifies for a reduced rate, so the rest stay at 25 %.
|
|
*
|
|
* revenue_account is left NULL on every row on purpose: NULL means "derive the
|
|
* revenue account from the VAT treatment at line-create time", which is the
|
|
* behaviour we want to demonstrate. Pinning an override would freeze a 25 %
|
|
* account onto the 6 % book line and misreport ruta 05.
|
|
*
|
|
* Every row sets every optional column explicitly (null where empty). PostgREST
|
|
* normalizes columns across the rows of a bulk insert, so a key present on one
|
|
* row and absent on another is sent as NULL rather than falling through to the
|
|
* schema default: the same gotcha documented on the journal lines in route.ts.
|
|
*/
|
|
|
|
export interface SandboxArticlesInput {
|
|
userId: string
|
|
companyId: string
|
|
}
|
|
|
|
export function buildSandboxArticles({ userId, companyId }: SandboxArticlesInput) {
|
|
const base = { user_id: userId, company_id: companyId, currency: 'SEK', active: true }
|
|
|
|
return [
|
|
{
|
|
...base,
|
|
article_number: 'A-001',
|
|
name: 'Konsulttimme, systemutveckling',
|
|
name_en: 'Consulting hour, software development',
|
|
type: 'tjanst',
|
|
unit: 'tim',
|
|
price_excl_vat: 1150,
|
|
vat_rate: 25,
|
|
revenue_account: null,
|
|
cost_price: null,
|
|
ean: null,
|
|
housework_type: null,
|
|
notes: 'Löpande räkning. Faktureras månadsvis i efterskott.',
|
|
},
|
|
{
|
|
...base,
|
|
article_number: 'A-002',
|
|
name: 'Konsulttimme, UX och gränssnittsdesign',
|
|
name_en: 'Consulting hour, UX and interface design',
|
|
type: 'tjanst',
|
|
unit: 'tim',
|
|
price_excl_vat: 995,
|
|
vat_rate: 25,
|
|
revenue_account: null,
|
|
cost_price: null,
|
|
ean: null,
|
|
housework_type: null,
|
|
notes: 'Löpande räkning. Faktureras månadsvis i efterskott.',
|
|
},
|
|
{
|
|
...base,
|
|
article_number: 'A-003',
|
|
name: 'Startpaket webbplats, fast pris',
|
|
name_en: 'Website starter package, fixed price',
|
|
type: 'tjanst',
|
|
unit: 'st',
|
|
price_excl_vat: 38000,
|
|
vat_rate: 25,
|
|
revenue_account: null,
|
|
cost_price: null,
|
|
ean: null,
|
|
housework_type: null,
|
|
notes: 'Fast pris: design, uppsättning och överlämning. Halva beloppet vid start.',
|
|
},
|
|
{
|
|
...base,
|
|
article_number: 'A-004',
|
|
name: 'Vidarefakturerad licens, designverktyg (1 plats, 12 mån)',
|
|
name_en: 'Rebilled licence, design tool (1 seat, 12 months)',
|
|
// A resold licence is a vara in the register's sense: a unit the customer
|
|
// buys, not time we spend. cost_price is display/margin only and is never
|
|
// posted to the ledger.
|
|
type: 'vara',
|
|
unit: 'st',
|
|
price_excl_vat: 5400,
|
|
vat_rate: 25,
|
|
revenue_account: null,
|
|
cost_price: 4860,
|
|
ean: null,
|
|
housework_type: null,
|
|
notes: 'Inköpspris plus påslag. Faktureras när licensen förnyas.',
|
|
},
|
|
{
|
|
...base,
|
|
article_number: 'A-005',
|
|
name: 'Handbok: Designsystem i praktiken (tryckt)',
|
|
name_en: 'Handbook: Design systems in practice (print)',
|
|
type: 'vara',
|
|
unit: 'st',
|
|
price_excl_vat: 320,
|
|
vat_rate: 6,
|
|
revenue_account: null,
|
|
cost_price: 118,
|
|
ean: null,
|
|
housework_type: null,
|
|
// The one reduced rate in the register, and the reason it is here: a
|
|
// printed book is 6 % under ML 9 kap., so the demo shows a mixed-rate
|
|
// register instead of a wall of 25 %. The note stays at chapter level:
|
|
// a paragraph-precise citation in demo copy is a liability if it drifts.
|
|
notes: 'Tryckt bok: 6 % moms enligt ML 9 kap.',
|
|
},
|
|
]
|
|
}
|