Skip to content

web: the app's own date field and one date format for the dealer's locale - #156

Merged
futurebuildai merged 33 commits into
refactor/v1from
refactor/g1-dates
Oct 15, 2026
Merged

futurebuildai merged 33 commits into
refactor/v1from
refactor/g1-dates

Conversation

@futurebuildai

Copy link
Copy Markdown
Contributor

Why

The statement's From and To opened the browser's white system calendar, AR Aging's As of read "mm/dd/yyyy" when empty, and dates showed in three formats across the app (G1 walk note N15). This change gives the app its own date field and one date format, set for the dealer's locale.

What

  • core: the dealer profile carries locale (a BCP 47 tag, migration 119, default en-US, the PUT validates the tag in words and a write without the field keeps the stored one; the demo seed sets en-CA and lifts an already seeded demo profile still on the default). GET /api/v1/dealer-profile/locale answers the locale alone to every signed in desk role (the profile read's own guard is unchanged), and the portal's public config and login response carry the same locale. The contract fragment, the assembled openapi.yaml, the census, the generated TypeScript schema and a CONTRACT-CHANGES row naming item 130 and N15 ride along; the portal goldens gain only the locale line.
  • web: lib/dates.ts is the one date module. The locale is read once at start (the desk route for the desk's pages, the portal config for the portal's; en-US on any failure, quietly). formatDay keeps the business day rule (built from its parts, so a west of UTC machine never shows the day before) and moves here, re-exported from utils.ts; formatDate, formatDateTime and formatTime read a moment in the person's own time zone. One style for every page: the dealer's locale with the month as a short word, never a numeric month first or day first form.
  • web: <gable-date-field> is the app's own date field: a text box in the module's style that also accepts YYYY-MM-DD, a calendar in the app's own popover (the week starts as the locale starts it; arrows move a day or a week, Page Up and Page Down a month, Home and End the week, Enter picks, Escape closes; today marked; min and max honoured), the value always YYYY-MM-DD for the API, change fired on a commit, and an empty field saying "Choose a date".
  • The sweep: all 19 native date and datetime-local inputs across 13 files are the field now, with the same values, bounds, defaults and change handling; every date shown to a person goes through the module, including the raw YYYY-MM-DD renderings (the statement's lines, the aging header, the quote analytics axis, the daily till) and the two date-fns call sites. ISO slices that go to the API stay ISO. The guard (dates.guard.test.ts, committed red first) fails any new native date input and any own date formatting outside the module, naming each file and line.

Tests

  • The guard is red on the old tree and green here; the module's legs pin the style in en-US, en-CA and en-GB and the west of UTC rule; the field's legs cover typing both forms, the calendar keys, min and max, and empty; the composed-server leg proves every desk role reads the locale and no role is refused; the migration's test covers the default, the shape check, the down and a re-apply.
  • Gates: go build, go vet, gofmt clean; the touched packages plus ./internal/app/serve/..., ./internal/app/seed/... and ./internal/characterization/... against a fresh database; make contract-go and make contract-ts; the web typecheck, npx eslint ., npm test and npm run build all clean.

Written by Kimi K3 for the Gable lead.

A Vitest leg over every .ts source under web/apps/ (tests excluded) that
fails on a native date, datetime-local, month or time input, and on
toLocaleDateString(, toLocaleTimeString( or Intl.DateTimeFormat( outside
lib/dates.ts, naming each file and line. It fails on today's code: the
native date inputs are still in the pages and dates are formatted on
their own all over the app.

Signed-off-by: colton <colton@futurebuild.ai>
…l config (G1 walk note N15)

The dealer profile carries locale (a BCP 47 tag the write validates,
refused in words otherwise; a write without the field keeps the stored
one), added by migration 119 with the en-US default; the demo seed sets
en-CA and lifts an already seeded demo profile still on the default,
keeping an operator's own tag. GET /api/v1/dealer-profile/locale answers
the locale alone to every signed in desk role (the profile read's own
guard is unchanged; a branch bound key's class is stated in the
allowlist), and the portal's public config and login response carry the
same locale for the portal's pages. The contract fragment, the assembled
openapi.yaml, the census, the generated TypeScript schema, the portal
goldens (the locale line only) and a CONTRACT-CHANGES row naming item
130 and N15 ride along, with wire legs on the module and the composed
server and the migration's test.

Signed-off-by: colton <colton@futurebuild.ai>
…e N15)

lib/dates.ts reads the dealer's locale once at start (the desk route for
the desk's pages, the portal's public config for the portal's; en-US on
any failure, quietly) and formats every date in one style: the dealer's
locale with the month as a short word, never a numeric month first or
day first form. formatDay keeps the business day rule (built from its
parts, so a west of UTC machine never shows the day before) and moves
here from utils.ts, which re-exports it; formatDate and formatDateTime
read a moment in the person's own time zone; parseDay reads a typed day
in the module's style or YYYY-MM-DD.

gable-date-field is the app's own date field: a text box in the module's
style with the calendar in the app's own popover (the week starts as the
locale starts it; arrows a day or a week, Page Up and Page Down a month,
Home and End the week, Enter picks, Escape closes; today marked; min and
max honoured), the value always YYYY-MM-DD for the API, change fired on
a commit, and an empty field saying Choose a date, never mm/dd/yyyy.

The sweep: every native date and datetime-local input in the desk is the
field now, with the same values, defaults and change handling (the
defaults that built today from UTC take todayDay/addDays, so they hold
west of UTC too); every date shown to a person goes through the module,
including the spots that showed the raw YYYY-MM-DD (the statement's
lines, the aging header, the quote analytics axis, the daily till) and
the two date-fns call sites. The guard (dates.guard.test.ts) is green on
this tree.

Signed-off-by: colton <colton@futurebuild.ai>
The hand-written desk type for the portal's public config gains the
locale the wire now answers.

Signed-off-by: colton <colton@futurebuild.ai>
PR 148 is in; both sides kept where they meet.

Signed-off-by: colton <colton@futurebuild.ai>
A business day sent as a midnight UTC timestamp must format from its day
part, and formatDay must never show raw machine text for a value that
parses. On today's code all three legs fail: the raw timestamp passes
through to the screen, so the journal entries list and the bank
reconciliation rows read 2026-10-14T00:00:00Z.

Signed-off-by: colton <colton@futurebuild.ai>
…ew P1)

A DATE column the server sends as a midnight UTC timestamp (the journal
entry's entry_date, the bank transaction's transaction_date, the session's
period start and end, the portal's due_date and scheduled_date) is
formatted from its day part, and any other value that parses is formatted
as a moment's day in the person's own zone: formatDay never puts raw
machine text on the screen. Only a value that does not parse at all comes
back as sent. The portal's three call sites drop their own slices. Every
other formatDay caller was walked against its Go type: all carry plain
YYYY-MM-DD strings (invoice, credit memo, payment, statement, reporting,
daily till, delivery strings), so nothing else shifts.

Signed-off-by: colton <colton@futurebuild.ai>
…ed, PR 156 review P2-1)

On today's code PUT /api/v1/dealer-profile with locale en_US (or root,
x-whatever, any tag the column's shape check refuses) answers 500
internal_error, and und and qaa are accepted though no person means them.
The legs want a 400 naming locale for each.

Signed-off-by: colton <colton@futurebuild.ai>
…156 review P2-1)

CheckLocale now requires the column's own shape (letters and hyphens, no
singleton subtags, no underscore), a successful parse, the canonical form
up to case, and refuses the root tag and the local use block qaa through
qtz. A tag like en_US is refused in words (400 naming locale) instead of
failing the shape check as a 500.

Signed-off-by: colton <colton@futurebuild.ai>
…d, PR 156 review P2-2)

At Jan 31 with the calendar open and the 31st focused, ArrowRight turns
the grid to February; the old cell's node is gone and focus drops to the
body, leaving the keyboard person stranded with an open calendar. The leg
asserts the Feb 1 button holds the focus; it fails on today's code.

Signed-off-by: colton <colton@futurebuild.ai>
…P2-2)

The grid tracks whether it holds the keyboard focus (focusin/focusout);
when a move turns the month and the old cell's node goes away, the render
gives the focus to the day the move landed on. The arrow, Page Up and Page
Down and Home and End moves clamp to min and max, so the focus never sits
on a disabled day; the min and max leg now pins the clamp.

Signed-off-by: colton <colton@futurebuild.ai>
…, toUTCString, date-fns (red, PR 156 review P2-3)

The guard also reads unquoted and bound native date inputs now. It fails
on today's code for the two inventory tabs' Generated lines, which still
show the browser's numeric format.

Signed-off-by: colton <colton@futurebuild.ai>
…dropped (PR 156 review P2-3)

ProductCollateralTab and ProductContentTab showed the browser's numeric
form; both now use formatDateTime. date-fns was unused after the sweep and
is removed from the desk's dependencies; the strengthened guard keeps all
of it from returning.

Signed-off-by: colton <colton@futurebuild.ai>
… PR 156 review P2-4)

An activity is an event with a time; the sweep left the form saving every
activity at noon. The legs want a time entry (HH:MM, defaulting to now,
refused in words) and the payload carrying the picked time; both fail on
today's code.

Signed-off-by: colton <colton@futurebuild.ai>
The form pairs the day's field with the app's own time entry (HH:MM, the
current hour and minute by default, refused in words when it is not a
time), and the payload carries the picked day at the picked time in the
person's own zone, so the feed shows when the call or meeting happened.

Signed-off-by: colton <colton@futurebuild.ai>
…156 review P2-5)

The popover is absolute under the field today, clipped by a scrolling or
hidden overflow modal and overflowing at a right edge. The legs pin the
placement: fixed at the field's box, clamped sideways, flipped upward when
the space below is short. They fail on today's code.

Signed-off-by: colton <colton@futurebuild.ai>
…6 review P2-5)

The popover is placed from the field's own box with fixed positioning, so
a modal's scroll or hidden overflow never clips it; placePopover clamps it
into the viewport sideways and flips it above the field when the space
below is short (clamped to the top margin when neither side fits). A
scroll or resize closes it rather than leaving it floating free of the
field.

Signed-off-by: colton <colton@futurebuild.ai>
…56 review P2-6)

On a light card (the Log Activity modal) the field draws white text on
white. The leg wants tone light to draw it in the dark-on-light tokens; it
fails on today's code.

Signed-off-by: colton <colton@futurebuild.ai>
A tone property: the dark tokens by default, tone light on a light card
(the Log Activity modal), where it draws dark on the zinc border and ink
text. The popover stays the app's own dark card, readable over either.

Signed-off-by: colton <colton@futurebuild.ai>
…red, PR 156 review P2-7)

Late in the day west of UTC the UTC slice is already tomorrow; the leg
wants the balance sheet, the profit and loss, the trial balance and the
journal entries' date defaults on the local day. It fails on today's code.

Signed-off-by: colton <colton@futurebuild.ai>
…6 review P2-7)

Balance Sheet, Profit and Loss, Trial Balance and the journal entry form
take todayDay() from the date module: the local day, not the UTC day's
tomorrow late in the day west of UTC.

Signed-off-by: colton <colton@futurebuild.ai>
…156 r2 P1)

A card or dialog with a blur or transform style becomes the reference for
the calendar's fixed position, so on the statement, the journal entry card
and Create Route it opens away from its field or is clipped. The leg mounts
the field inside a transform and overflow hidden ancestor and wants the
dialog to carry popover=manual with showPopover called; it fails on today's
code (jsdom has no popover API, so the calls are stubbed).

Signed-off-by: colton <colton@futurebuild.ai>
The dialog carries popover=manual and is shown with showPopover() while
open, so no ancestor's transform, filter, backdrop blur or overflow can
offset or clip it (the statement's card, the New Journal Entry card and
Create Route were all containing blocks for the fixed position). Manual
keeps dismissal with the field's own outside click and Escape handlers;
the coordinates still come from the field's box through placePopover. The
top layer needs no z-index arms race. K1's PR 155 has not landed yet, so
no shared helper exists; placePopover stays in gable-date-field.ts.

Signed-off-by: colton <colton@futurebuild.ai>
A planted leg runs the rule against the forms this codebase writes:
type=date alone on its own line (the usual multi-line Lit form) matches
nothing today, because the rule demands a trailing space, brace or slash.

Signed-off-by: colton <colton@futurebuild.ai>
…6 r2 P2)

The rule's terminator becomes a lookahead that also accepts the end of the
line, so the multi-line form (type=date alone on its line) is caught again;
the planted leg covers the line end, the inline, the unquoted and the bound
forms and the near misses (text, number, daterange, prototype).

Signed-off-by: colton <colton@futurebuild.ai>
…(red, PR 156 r2 P2)

Chromium fires focusout (relatedTarget null) on the focused cell when the
render removes it, clearing the grid's flag before updated() runs; jsdom
fires nothing, so the existing leg cannot see it. The leg replays the
sequence by hand and fails on today's code.

Signed-off-by: colton <colton@futurebuild.ai>
…2 P2)

A refocus flag is set by the grid's key handler and the month buttons (a
key or a click there proves the calendar had the interaction) and consumed
in updated(), so the focus lands on the moved-to day whatever focus events
the engine fires while the old cell's node goes away; a focusout whose
target is already disconnected is ignored. The flag clears on close.

Signed-off-by: colton <colton@futurebuild.ai>
The comment, the refusal toast and the placeholder said the pattern in
letters; they now say it in words or by example (the placeholder shows
09:30), so the pre-push names grep no longer trips on the diff.

Signed-off-by: colton <colton@futurebuild.ai>
…eared (red, PR 156 r3)

The browser centres every popover (inset 0, margin auto); setting only top
and left leaves the calendar centred in the space below and right of its
field. The leg wants margin cleared to zero and inset cleared to auto ahead
of the placement; it fails on today's code.

Signed-off-by: colton <colton@futurebuild.ai>
…56 r3)

The user agent stylesheet centres every popover (inset 0, margin auto);
writing inset auto and margin 0 ahead of top and left puts the calendar at
the field's box, as the app's select list in PR 155 does.

Signed-off-by: colton <colton@futurebuild.ai>
Assigning the field's value property fires no change, so the modal kept
its default day; the leg passed only while the runner's day equalled the
picked day and failed in CI after the day turned. The leg types the day
and leaves the box, and runs under a western zone asserting the UTC
literal, so the zone handling is what is proven.

Signed-off-by: colton <colton@futurebuild.ai>
The light sweep's tokens kept on the pages both sides touch; the date
field and module stay on top.

Signed-off-by: colton <colton@futurebuild.ai>

# Conflicts:
#	web/apps/desk/src/components/crm/LogActivityModal.ts
#	web/apps/desk/src/components/logistics/CreateRouteModal.ts
#	web/apps/desk/src/pages/reports/CustomerStatementPage.ts
The style leg now asserts the unused edges directly: the calendar's inline
style writes margin 0 and every edge (the unused two auto), the same
guarantee the inset shorthand gave, in a form the test environment can
read. The legs that set the runner's zone restore it by deleting the
variable when it was unset (assigning back an unset one stored the word
undefined, an unknown zone that reads as UTC). And the field's tone switch
is removed: the merged Log Activity modal is the desk's card on tokens, so
the field's default follows the mode and the light variant is dead.

Signed-off-by: colton <colton@futurebuild.ai>
@futurebuildai
futurebuildai merged commit 9699d33 into refactor/v1 Oct 15, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant