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

@@ -23,7 +23,7 @@ import { FieldHistoryProvider, FieldHistoryIcon } from '@/components/shared/fiel
import { ClientChannelEditor } from '@/components/clients/client-channel-editor';
import { InlineTagEditor } from '@/components/shared/inline-tag-editor';
import { RemindersInline } from '@/components/reminders/reminders-inline';
// Legacy `RecommendationList` removed 2026-05-15 replaced by the same
// Legacy `RecommendationList` removed 2026-05-15 - replaced by the same
// rule-based `BerthRecommenderPanel` (already imported above) used on the
// Overview tab so the scoring + UI stay consistent. The old component
// pulled stale "AI"-style rows that all scored 50% because the underlying
@@ -111,7 +111,7 @@ interface InterestTabsOptions {
desiredLengthM?: string | null;
desiredWidthM?: string | null;
desiredDraftM?: string | null;
/** Unit the rep originally entered the dims in drives the
/** Unit the rep originally entered the dims in - drives the
* recommender header's display so a metric-entered deal doesn't
* render as ft. The three columns share an entry unit in practice. */
desiredLengthUnit?: string | null;
@@ -125,25 +125,25 @@ interface InterestTabsOptions {
* auto-advance once payment totals catch up. */
depositExpectedAmount?: string | null;
depositExpectedCurrency?: string | null;
/** Doc-bearing stage sub-status badges drive the milestone past/current
/** Doc-bearing stage sub-status badges - drive the milestone past/current
* classification independently of the pipeline stage. NULL until the
* matching stage is reached. */
eoiDocStatus?: string | null;
reservationDocStatus?: string | null;
contractDocStatus?: string | null;
/** Final outcome 'won' surfaces the wrap-up checklist panel. */
/** Final outcome - 'won' surfaces the wrap-up checklist panel. */
outcome?: string | null;
/** Interest id needed for the queryClient.invalidateQueries calls
/** Interest id - needed for the queryClient.invalidateQueries calls
* that fire after an inline contact edit. The parent passes this
* through `interestId` already, but the inline-edit handlers below
* use the structured object form. */
id: string;
/** Linked client id required for the PATCH /api/v1/clients/[id]/
/** Linked client id - required for the PATCH /api/v1/clients/[id]/
* contacts/[contactId] flow that the inline Email + Phone editors
* use. Null on an unlinked interest (rare but possible). */
clientId: string | null;
/** Primary contact channels resolved from the linked client record by
* getInterestById both editable inline. The contact row's id is
* getInterestById - both editable inline. The contact row's id is
* exposed alongside so the inline editor can PATCH the right row
* without an extra fetch. */
clientPrimaryEmail?: string | null;
@@ -163,7 +163,7 @@ interface InterestTabsOptions {
reminderEnabled: boolean;
reminderDays: number | null;
reminderLastFired: string | null;
/** Count of berths linked via the interest_berths junction
/** Count of berths linked via the interest_berths junction -
* drives the "Berth Interest" milestone on the Overview tab. */
linkedBerthCount?: number;
notes: string | null;
@@ -391,7 +391,7 @@ function MilestoneAdvanceButton({
* Skip-ahead backfill control: shown next to past milestones whose
* date column is null. Opens the same date popover as
* MilestoneAdvanceButton but PATCHes the date column directly without
* triggering a stage transition the stage was already advanced
* triggering a stage transition - the stage was already advanced
* manually upstream.
*/
function MilestoneBackfillButton({
@@ -640,7 +640,7 @@ function OverviewTab({
const portSlug = params?.portSlug ?? '';
// QueryClient lifted to the top of the tab so the inline-edit email +
// Lift the EOI generate dialog into the Overview so the milestone card
// can launch it inline same dialog the dedicated EOI tab uses, so the
// can launch it inline - same dialog the dedicated EOI tab uses, so the
// editing/confirmation flow is identical regardless of entry point.
const [eoiGenerateOpen, setEoiGenerateOpen] = useState(false);
const mutation = useInterestPatch(interestId);
@@ -681,14 +681,14 @@ function OverviewTab({
};
// Determine each milestone's phase relative to the current pipeline
// stage. The overview hides future-phase milestones by default it
// stage. The overview hides future-phase milestones by default - it
// was visually noisy to see Deposit + Contract cards on a deal still
// at the EOI stage, and the empty cards invited mis-clicks.
//
// Past milestones still render (collapsed history) so reps can see
// what's been completed. Future milestones are gated behind a "Show
// upcoming milestones" toggle so the rep CAN reach them when a deal
// genuinely skips stages the click then routes through the same
// genuinely skips stages - the click then routes through the same
// override-confirm flow as the inline stage picker.
const stageIdx = PIPELINE_STAGES.indexOf(interest.pipelineStage as PipelineStage);
const reservationIdx = PIPELINE_STAGES.indexOf('reservation');
@@ -707,7 +707,7 @@ function OverviewTab({
// pipeline-stage column happens to sit. The previous "phase === current
// only when stageIdx exactly matches" rule produced an empty Overview
// for the qualified + nurturing stages (no milestone marked current, EOI
// hidden under "show upcoming") exactly the gap the rep complained
// hidden under "show upcoming") - exactly the gap the rep complained
// about. New model: the FIRST not-yet-complete milestone in the fixed
// berth_interest → eoi → reservation → deposit → contract order is
// 'current'. Everything before is 'past'; everything after is 'future'.
@@ -727,7 +727,7 @@ function OverviewTab({
// the deal's current pipelineStage column. When the rep manually
// jumps the stage forward (Reservation+) but earlier sub-statuses
// are still un-signed, we need the current-stage milestone to stay
// marked `'current'` regardless of completion otherwise EOI gets
// marked `'current'` regardless of completion - otherwise EOI gets
// flagged as NEXT STEP and the actual current stage hides under
// "Upcoming milestones". Earlier-than-stage milestones go to
// `'past'` so the rep can render backfill controls against them.
@@ -749,7 +749,7 @@ function OverviewTab({
if (k === firstIncompleteKey) return 'current';
return 'future';
}
// A stage DOES own a different milestone bucket by position
// A stage DOES own a different milestone - bucket by position
// relative to it. Earlier slots go to `past` even if incomplete
// (the backfill controls live there); later slots go to `future`.
const idx = order.indexOf(k);
@@ -797,12 +797,12 @@ function OverviewTab({
phase: berthInterestPhase,
title: 'Berth Interest',
icon: Anchor,
// No status badge the count IS the status. Showing "0 berths"
// No status badge - the count IS the status. Showing "0 berths"
// would just duplicate the empty-state copy below.
status: hasLinkedBerth
? `${interest.linkedBerthCount} berth${(interest.linkedBerthCount ?? 0) === 1 ? '' : 's'}`
: null,
// No advanceStage step the milestone tracks a state (berths
// No advanceStage step - the milestone tracks a state (berths
// linked) rather than a stage transition. Hide the row chrome by
// passing an empty steps array; the footer renders the action.
steps: [],
@@ -846,8 +846,8 @@ function OverviewTab({
// When the EOI milestone is the active next step but nothing's been
// sent yet, surface the actual generation entry points instead of
// making the rep navigate to the EOI tab first. Mirrors the EOI
// tab's Generate flow exactly same dialog component, same
// confirmation step so behaviour stays consistent.
// tab's Generate flow exactly - same dialog component, same
// confirmation step - so behaviour stays consistent.
footer:
eoiPhase === 'current' && !interest.dateEoiSent ? (
<div className="flex flex-wrap items-center gap-2 pt-1">
@@ -1062,7 +1062,7 @@ function OverviewTab({
</AccordionTrigger>
<AccordionContent>
{/* Reuse the same MilestoneSection layout used for the
current milestone the steps list, sub-status badge,
current milestone - the steps list, sub-status badge,
and any inline doc actions all render the same way.
`isActive={false}` keeps the NEXT-STEP pill off. */}
<MilestoneSection
@@ -1329,7 +1329,7 @@ function OverviewTab({
stands today; reading it on Overview, "current stage"
answers the implicit "where in the deal is this?". A
historical "stage-at-note-time" lookup would need an
audit_logs read per teaser render over-engineered for
audit_logs read per teaser render - over-engineered for
a context hint. */}
<span
className={cn(
@@ -1424,7 +1424,7 @@ export function getInterestTabs({
clientId = null,
interest,
}: InterestTabsOptions): DetailTab[] {
// The EOI / Contract / Reservation tabs are stage-conditional
// The EOI / Contract / Reservation tabs are stage-conditional -
// each appears only at the stages where the rep is likely to act
// on it. Hides clutter from later-stage deals where earlier docs
// are ancient history. Each tab still queries for its own past
@@ -1437,7 +1437,7 @@ export function getInterestTabs({
const contractIdx = PIPELINE_STAGES.indexOf('contract');
// EOI: from qualified through contract (the deal's whole life past lead-only).
const showEoiTab = stageIdx >= qualifiedIdx;
// Reservation: once the EOI is signed onward the reservation agreement
// Reservation: once the EOI is signed onward - the reservation agreement
// is the v1 step between EOI and deposit. Stays visible through contract
// so the rep can re-open the signed reservation later.
const showReservationTab = stageIdx >= reservationIdx;