Files
accounted/components/onboarding/journey/JourneyDatePicker.tsx
T
Jakob WennbergandClaude Fable 5 b771c1f923 feat(onboarding): journey PR B — reducer, orb, track, question primitives (behind /sandbox demo) (#1145)
* refactor(onboarding): extract first-year defaults + shared TIC lookup client (journey PR A)

First of four PRs replacing the onboarding wizard with the journey flow
(dev_docs/onboarding_migration_plan.md, local). No UI change.

- Move deriveFirstYearDefaults + parseStartMonthDay out of
  WelcomeOnboarding into lib/company/first-year-defaults.ts and unit-test
  them (11-vs-13-months boundary, UTC month seeding, malformed input).
- Add the missing computeFiscalPeriod unit tests (calendar year, brutet
  ar, first year short/extended, EF calendar-year rule, period names,
  BFL 3 kap. 6-18 month window errors).
- New shared fetchCompanyLookup() client: the single client path to the
  Lens-backed /lookup, typed outcomes (found / not_found / disabled /
  error / aborted), never throws. Fixes the 403/404 conflation: the
  dispatcher's 404 ("Extension not found") and feature-flag 503
  (EXTENSION_DISABLED) now degrade silently instead of rendering as
  "company not found"; only the TIC handler's own 404 does.
- Step2CompanyDetails consumes the helper; identical UX otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(onboarding): journey state machine reducer with full branch coverage (journey PR B, 1/3)

Pure reducer for the journey onboarding: owns every transition and every
CompanySettings write; the component layer only renders steps, runs the
single TIC lookup, and calls the server action.

Encodes the plan's invariants: entry-snapshot history (Back rolls answers
AND stations), lookupRan gates fact-vs-question per field (BankID prefill
without lookup degrades to questions), vat_registered is never defaulted
without lookup data or an explicit answer, entity change wipes downstream,
org_number_invalid bounces to the Företaget station, station jumps rewind
to a station's first step.

34 unit tests: AB/EF found, not-found manual, ceased, BankID prefill
(found + degraded + disabled), first year, brutet år, moms nej, Back from
every step, station jumps, server-error bounces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(onboarding): journey visual primitives + sandbox gallery (journey PR B, 2/3)

Ports the founder-approved concept (artifact c82c9358) to React:

- JourneyOrb: 320-particle canvas sphere with comet travel, check morph
  and the monogram finale (glyph sampled live from --font-display). Own
  component per plan, NOT thinking-orbs. rAF pauses on document.hidden;
  reduced motion renders static frames.
- JourneyTrack: five stations with inked answers; completed stations are
  keyboard-accessible jump-back buttons; answers mirrored to an aria-live
  region.
- Question primitives: Question (ink title + "?" popover, Esc closes),
  ChipRow (fly-to-orb ghost), YearBand (springy fiscal-year preview),
  JourneyDatePicker (year -> month by name -> day), AddressFields
  (Enter-chained, skippable).
- journey.css: concept stylesheet namespaced under .jny on app tokens,
  incl. the no-scroll composition (100dvh + optical-centering balance
  spacer) and the dawn layer.
- /sandbox/journey: internal primitive gallery (auth-free sandbox path),
  demo data only: this page makes ZERO TIC calls.

i18n note: primitives are copy-agnostic (strings via props); the real
flow's sv/en keys land with their consumer in PR C.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: log journey reducer location decision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(onboarding): annotate ENTITY_PICKED settings as Partial<CompanySettings>

The wipeDownstream return narrows against the inferred initializer type;
tsc strict rejects the reassignment without the explicit annotation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:32:45 +02:00

90 lines
2.6 KiB
TypeScript

'use client'
import { useState } from 'react'
import { InkText } from './ink'
/**
* Tap-only date picker: year → month BY NAME → day. No dd/mm ambiguity.
* Fires onPick("YYYY-MM-DD") shortly after the day is tapped (immediately
* under reduced motion).
*/
const MONTHS_SV = [
'januari', 'februari', 'mars', 'april', 'maj', 'juni',
'juli', 'augusti', 'september', 'oktober', 'november', 'december',
]
const MONTHS_SHORT = ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec']
function lastDay(y: number, m: number) {
return new Date(y, m, 0).getDate()
}
interface JourneyDatePickerProps {
years: number[]
onPick: (date: string) => void
}
export default function JourneyDatePicker({ years, onPick }: JourneyDatePickerProps) {
const [y, setY] = useState<number>(years[years.length - 1])
const [m, setM] = useState<number>(0)
const [d, setD] = useState<number>(0)
function pickDay(day: number) {
setD(day)
const iso = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
window.setTimeout(() => onPick(iso), reduced ? 0 : 650)
}
const label = m && d ? `${d} ${MONTHS_SV[m - 1]} ${y}` : m ? `${MONTHS_SV[m - 1]} ${y}` : ''
return (
<div>
<div className="jny-mchips" style={{ marginTop: 10 }}>
{years.map((yy) => (
<button
key={yy}
type="button"
className={`jny-mchip${yy === y ? ' is-sel' : ''}`}
onClick={() => {
setY(yy)
setD(0)
}}
>
{yy}
</button>
))}
</div>
<div className="jny-mchips">
{MONTHS_SHORT.map((name, i) => (
<button
key={name}
type="button"
className={`jny-mchip${i + 1 === m ? ' is-sel' : ''}`}
onClick={() => {
setM(i + 1)
setD(0)
}}
>
{name}
</button>
))}
</div>
{m ? (
<div className="jny-mchips jny-dp-days">
{Array.from({ length: lastDay(y, m) }, (_, i) => i + 1).map((day) => (
<button
key={day}
type="button"
className={`jny-mchip${day === d ? ' is-sel' : ''}`}
onClick={() => pickDay(day)}
>
{day}
</button>
))}
</div>
) : null}
<div className="jny-dp-label">{label ? <InkText text={label} step={40} /> : null}</div>
</div>
)
}