chore(autonomous-session): consolidate uncommitted work from prior session

Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
This commit is contained in:
2026-05-23 00:52:59 +02:00
parent 43719b49e9
commit 221ae5784e
749 changed files with 7440 additions and 3118 deletions

View File

@@ -72,7 +72,7 @@ export type RolePermissions = {
* an interest). Carved out from `invoices.record_payment` so a port
* that does not use the invoicing module at all can still grant
* payment-recording rights to sales reps. `view` follows interests.view
* at the route level this gate only governs the UI affordance.
* at the route level - this gate only governs the UI affordance.
*/
payments: {
view: boolean;
@@ -166,7 +166,7 @@ export type RolePermissions = {
};
/**
* Per-table column visibility drives the `<ColumnPicker>` and the
* Per-table column visibility - drives the `<ColumnPicker>` and the
* DataTable `columnVisibility` state. `hiddenColumns` is the source of
* truth; an entry's absence means "show this column" (so newly-added
* columns show by default for existing users without us having to
@@ -198,20 +198,27 @@ export type UserPreferences = {
/**
* Dashboard widget visibility, keyed by widget id from the registry
* in `src/components/dashboard/widget-registry.ts`. Missing keys fall
* back to `defaultVisible` from the registry so adding a new widget
* back to `defaultVisible` from the registry - so adding a new widget
* surfaces it for everyone without a migration. `false` hides it.
*/
dashboardWidgets?: Record<string, boolean>;
/**
* Ordered list of widget ids — drives the dashboard render order so a
* rep can drag tiles around and have the layout persist. Missing
* widgets (ids not in the array) render after the listed ones in
* registry order, so adding a new widget always surfaces it without
* a migration. Order is scoped per widget group implicitly — the
* shell groups by `widget.group` first (chart / rail / feed) then
* sorts within the group by this array.
* Ordered list of widget ids for the **desktop / xl layout** (charts
* column + rails aside + feed row, side-by-side). Drives the render
* order at viewport widths >= 1280px. Missing widgets fall through to
* registry order so newly-added widgets always surface.
*/
dashboardWidgetOrder?: string[];
/**
* Ordered list of widget ids for the **stacked layout** (single
* column at < xl). Reps reasonably want a different order on mobile
* vs desktop - Reminders + Activity top on the phone, Pipeline Funnel
* top on a 27" monitor. When unset, the dashboard falls back to
* `dashboardWidgetOrder` (then registry order) so a rep who only
* customized desktop sees the same order on a phone until they
* customize there too.
*/
dashboardWidgetOrderMobile?: string[];
[key: string]: unknown;
};
@@ -274,7 +281,7 @@ export const userProfiles = pgTable(
userId: text('user_id').notNull().unique(), // references Better Auth user ID
/**
* Canonical first/last name pair. Added 2026-05-09 as the primary
* source for greetings, invoicing, and DocSign field-merging the
* source for greetings, invoicing, and DocSign field-merging - the
* older `displayName` is now kept around as a derived/optional
* override (e.g. for nicknames or vanity formatting). When migrating
* production, backfill these columns from displayName by splitting
@@ -293,7 +300,7 @@ export const userProfiles = pgTable(
*/
username: text('username'),
avatarUrl: text('avatar_url'),
/** FK into the polymorphic `files` table the avatar is stored
/** FK into the polymorphic `files` table - the avatar is stored
* via getStorageBackend() so an S3↔filesystem swap carries it
* without breaking the URL. The legacy `avatarUrl` column is
* kept for any external photo sources but the file pointer wins
@@ -330,7 +337,7 @@ export const roles = pgTable('roles', {
* Per-user permission overrides layered on top of the role's baseline for
* a specific port. Each row carries a `Partial<RolePermissions>` map; any
* explicitly-set leaf wins over the role + port-role-override chain. Most
* users will never have a row here it exists for the rare "give Alice
* users will never have a row here - it exists for the rare "give Alice
* the same role as her team but let her run permanent deletes" case.
*
* Effective permission resolution lives in `getEffectivePermissions` in
@@ -344,7 +351,7 @@ export const userPermissionOverrides = pgTable(
.$defaultFn(() => crypto.randomUUID()),
// onDelete: 'cascade' is intentional here (not 'set null' as a stale 2026-05-12
// audit item suggested). A permission override has no semantic value without
// the user it grants permissions to preserving a row with user_id=NULL
// the user it grants permissions to - preserving a row with user_id=NULL
// would be an orphan with no audit value, since the override is per-user
// additive permissions, not a historical event we need to retain.
userId: text('user_id')