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:
@@ -31,7 +31,7 @@ interface Props {
|
||||
*/
|
||||
export function ActiveInterestsPopover({ berthId, portSlug, count }: Props) {
|
||||
// Lazy-load: only fetch when the popover opens. Pattern from the
|
||||
// detail-label fallback queries elsewhere in the codebase — the
|
||||
// detail-label fallback queries elsewhere in the codebase - the
|
||||
// `enabled` flag flips on first open.
|
||||
const { data, isLoading, isError } = useQuery<{ data: ActiveInterestRow[] }>({
|
||||
queryKey: ['berth', berthId, 'active-interests'],
|
||||
|
||||
@@ -44,7 +44,7 @@ export function BerthCard({ berth }: BerthCardProps) {
|
||||
// already conveyed by the pill below, so the stripe is dock-keyed.
|
||||
const accentClass = mooringLetterDot(berth.mooringNumber) ?? 'bg-slate-300';
|
||||
|
||||
// Dimensions string — Length × Width × Draft (each segment is optional).
|
||||
// Dimensions string - Length × Width × Draft (each segment is optional).
|
||||
// The avatar already conveys the mooring number, so this becomes the
|
||||
// primary "what is this berth" line.
|
||||
const dimParts: string[] = [];
|
||||
@@ -53,7 +53,7 @@ export function BerthCard({ berth }: BerthCardProps) {
|
||||
if (berth.draftM) dimParts.push(`${berth.draftM}m draft`);
|
||||
const dimText = dimParts.length > 0 ? dimParts.join(' × ') : null;
|
||||
|
||||
// Recommended boat size — the most rep-actionable signal in a glance
|
||||
// Recommended boat size - the most rep-actionable signal in a glance
|
||||
// ("can my client's yacht park here?"). Tenure was previously here but
|
||||
// dropped: tenure is set per EOI/contract, not per berth, so showing
|
||||
// it as a berth property was misleading.
|
||||
@@ -64,7 +64,7 @@ export function BerthCard({ berth }: BerthCardProps) {
|
||||
boatCapacityText = `Fits up to ${berth.nominalBoatSize}ft`;
|
||||
}
|
||||
|
||||
// Water depth — operational; matters for deep-keel yachts.
|
||||
// Water depth - operational; matters for deep-keel yachts.
|
||||
let waterDepthText: string | null = null;
|
||||
if (berth.waterDepthM) {
|
||||
const prefix = berth.waterDepthIsMinimum ? '≥ ' : '';
|
||||
@@ -134,7 +134,7 @@ export function BerthCard({ berth }: BerthCardProps) {
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* The mooring number IS the avatar — recognisable at a glance
|
||||
{/* The mooring number IS the avatar - recognisable at a glance
|
||||
(A1, B12, …) and eliminates the duplicate berth-number heading
|
||||
that previously sat to the right of an anchor icon. */}
|
||||
<ListCardAvatar
|
||||
|
||||
@@ -82,7 +82,7 @@ export type BerthRow = {
|
||||
/**
|
||||
* Toggleable columns for the berth list ColumnPicker. Heavy NocoDB
|
||||
* fields default to hidden; reps can switch them on per-table-view.
|
||||
* `mooringNumber` is intentionally omitted from this list — it's the
|
||||
* `mooringNumber` is intentionally omitted from this list - it's the
|
||||
* primary identifier and always visible.
|
||||
*/
|
||||
export const BERTH_COLUMN_OPTIONS: Array<{ id: string; label: string }> = [
|
||||
@@ -108,7 +108,7 @@ export const BERTH_COLUMN_OPTIONS: Array<{ id: string; label: string }> = [
|
||||
{ id: 'tags', label: 'Tags' },
|
||||
];
|
||||
|
||||
/** Hidden by default — power-users turn them on via the picker. */
|
||||
/** Hidden by default - power-users turn them on via the picker. */
|
||||
export const BERTH_DEFAULT_HIDDEN: string[] = [
|
||||
'tenure',
|
||||
'sidePontoon',
|
||||
@@ -148,14 +148,14 @@ function StatusBadge({ status }: { status: string }) {
|
||||
/**
|
||||
* #67 Phase 2: small amber chip beside the status pill flagging rows
|
||||
* whose status was set manually and has no backing interest. These are
|
||||
* the candidates for the catch-up wizard — the rep flipped a berth to
|
||||
* the candidates for the catch-up wizard - the rep flipped a berth to
|
||||
* "Under Offer" or "Sold" without ever creating the matching deal.
|
||||
*/
|
||||
function ManualBadge() {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-800"
|
||||
title="Status set manually with no backing interest — needs catch-up"
|
||||
title="Status set manually with no backing interest - needs catch-up"
|
||||
>
|
||||
Manual
|
||||
</span>
|
||||
@@ -470,7 +470,7 @@ export const berthColumns: ColumnDef<BerthRow, unknown>[] = [
|
||||
* cell renderer reading a context.
|
||||
*
|
||||
* Imperial columns assume the canonical `*Ft` columns are populated
|
||||
* (true by default — the import pipeline + bulk-add wizard write both,
|
||||
* (true by default - the import pipeline + bulk-add wizard write both,
|
||||
* and the inline editor in yacht-tabs.tsx auto-fills the counterpart).
|
||||
* Rows with only the metric counterpart fall through to `?` for that
|
||||
* dimension; the cell still renders so the rep sees what's set.
|
||||
|
||||
@@ -105,7 +105,7 @@ interface InterestOption {
|
||||
id: string;
|
||||
clientName: string;
|
||||
pipelineStage: string;
|
||||
/** Used to sort the picker — most recently interacted with floats to the top. */
|
||||
/** Used to sort the picker - most recently interacted with floats to the top. */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ function StatusChangeDialog({
|
||||
const interestId = watch('interestId');
|
||||
const showInterestPicker = status === 'under_offer' || status === 'sold';
|
||||
|
||||
// Active interests for this port — used to populate the prospect
|
||||
// Active interests for this port - used to populate the prospect
|
||||
// selector when status moves to under_offer / sold. Only fetched when
|
||||
// the picker is actually visible to avoid an unnecessary round-trip
|
||||
// for available-status changes.
|
||||
@@ -317,7 +317,7 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
|
||||
/**
|
||||
* Searchable combobox for picking a linked prospect when changing berth
|
||||
* status. Replaces the bare Select which had no filter, no stage colours,
|
||||
* and no recency sort — for ports with 200+ active interests that became
|
||||
* and no recency sort - for ports with 200+ active interests that became
|
||||
* a scroll-fest. Stage labels render with the same coloured pill the rest
|
||||
* of the CRM uses for stage badges so the rep can scan the list visually.
|
||||
*/
|
||||
@@ -332,7 +332,7 @@ function InterestLinkPicker({
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
// Sort with the most recently updated interest first so reps see the
|
||||
// active deals at the top of the list — older / dormant ones drop
|
||||
// active deals at the top of the list - older / dormant ones drop
|
||||
// beneath. `updatedAt` is set on every patch + every stage advance.
|
||||
const sorted = [...options].sort((a, b) => {
|
||||
if (!a.updatedAt && !b.updatedAt) return 0;
|
||||
|
||||
@@ -51,7 +51,7 @@ export function BerthDetail({ berthId }: BerthDetailProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('edit') === 'true') {
|
||||
// setState in effect is the right shape here — the URL is an
|
||||
// setState in effect is the right shape here - the URL is an
|
||||
// external store and the trigger is a query-param change, not a
|
||||
// prop in the React tree.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Documents tab on the berth detail page (Phase 6b — see plan §5.6).
|
||||
* Documents tab on the berth detail page (Phase 6b - see plan §5.6).
|
||||
*
|
||||
* Sections:
|
||||
* - Current PDF panel (download link, "Replace PDF" button, parse-engine chip).
|
||||
* - Version history list — newest first, with rollback affordance on every
|
||||
* - Version history list - newest first, with rollback affordance on every
|
||||
* non-current row.
|
||||
* - Reconcile-diff dialog (PdfReconcileDialog), opened after a successful
|
||||
* upload + parse. Shows auto-applied vs conflicted fields and lets the
|
||||
|
||||
@@ -47,7 +47,7 @@ export function BerthInterestPulse({ berthId }: { berthId: string }) {
|
||||
// Stay in sync with the linked-berths list + add-to-interest dialog.
|
||||
// Each of those flows emits a realtime socket event but does NOT
|
||||
// invalidate this exact query key (it's berth-scoped, theirs are
|
||||
// interest-scoped) — bridge via the invalidation hook.
|
||||
// interest-scoped) - bridge via the invalidation hook.
|
||||
useRealtimeInvalidation({
|
||||
'interest:berthLinked': [queryKey],
|
||||
'interest:berthUnlinked': [queryKey],
|
||||
|
||||
@@ -90,7 +90,7 @@ export function BerthList() {
|
||||
'berth:statusChanged': [['berths']],
|
||||
});
|
||||
|
||||
// Persisted column visibility + row density + dimension unit — same
|
||||
// Persisted column visibility + row density + dimension unit - same
|
||||
// pattern as ClientList / InterestList; density falls back to
|
||||
// 'comfortable' and dimensionUnit to 'ft' for users who haven't picked.
|
||||
const { hidden, setHidden, density, setDensity, dimensionUnit, setDimensionUnit } =
|
||||
@@ -98,7 +98,7 @@ export function BerthList() {
|
||||
const columnVisibility = Object.fromEntries(hidden.map((id) => [id, false]));
|
||||
const berthColumns = getBerthColumns(dimensionUnit);
|
||||
|
||||
// Bulk-action state — one dialog per action (status / tenure type /
|
||||
// Bulk-action state - one dialog per action (status / tenure type /
|
||||
// tag add+remove). Mirrors the InterestList pattern so reps already
|
||||
// know the idiom from there.
|
||||
const qc = useQueryClient();
|
||||
@@ -143,16 +143,16 @@ export function BerthList() {
|
||||
// No "New" button - berths are import-only
|
||||
/>
|
||||
|
||||
{/* Toolbar — two halves separated by `justify-between` so the
|
||||
{/* Toolbar - two halves separated by `justify-between` so the
|
||||
Columns + Saved-views actions stay pinned to the right edge of
|
||||
the row at every width. The previous `ml-auto` trick didn't
|
||||
survive flex-wrap on intermediate widths — the actions ended
|
||||
survive flex-wrap on intermediate widths - the actions ended
|
||||
up centered. */}
|
||||
<div className="flex items-center gap-2 flex-wrap justify-between">
|
||||
<div className="flex items-center gap-2 flex-wrap min-w-0 flex-1">
|
||||
<FilterBar
|
||||
// Search is hoisted out of the popover into the inline input
|
||||
// below — keeps the daily "find by mooring/area" lookup one
|
||||
// below - keeps the daily "find by mooring/area" lookup one
|
||||
// tap away instead of buried behind the Filters dropdown.
|
||||
filters={berthFilterDefinitions.filter((d) => d.key !== 'search')}
|
||||
values={filters}
|
||||
@@ -302,7 +302,7 @@ export function BerthList() {
|
||||
: undefined
|
||||
}
|
||||
cardRender={(row) => <BerthCard berth={row.original} />}
|
||||
// Group adjacent cards by dock letter (area) on mobile — adds a
|
||||
// Group adjacent cards by dock letter (area) on mobile - adds a
|
||||
// dim divider + uppercased label above the first card of each
|
||||
// group. Data is already sorted by mooringNumber (A1, A2, …, B1,
|
||||
// B2, …) so consecutive rows naturally share dock letters.
|
||||
@@ -436,7 +436,7 @@ export function BerthList() {
|
||||
toast.error('Pick at least one tag.');
|
||||
return;
|
||||
}
|
||||
// Per-tag bulk call — the endpoint takes one tagId at a
|
||||
// Per-tag bulk call - the endpoint takes one tagId at a
|
||||
// time. For the typical 1-2 tag case the round-trips are
|
||||
// cheap; multi-tag UX can come later.
|
||||
const action = tagDialog?.mode === 'add' ? 'add_tag' : 'remove_tag';
|
||||
|
||||
@@ -81,7 +81,7 @@ function SpecRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
/**
|
||||
* Tags Card for the berth overview. Wraps the InlineTagEditor in a Card so
|
||||
* the section header uses CardTitle styling; mirrors the visibility rule
|
||||
* the editor itself uses — hides entirely when the port has no tags
|
||||
* the editor itself uses - hides entirely when the port has no tags
|
||||
* defined AND this berth has none applied.
|
||||
*/
|
||||
function BerthTagsCard({ berth }: { berth: BerthData }) {
|
||||
@@ -215,7 +215,7 @@ function OverviewTab({ berth }: { berth: BerthData }) {
|
||||
const patch = useBerthPatch(berth.id);
|
||||
// User-selected display unit for dimensions. Persisted in localStorage
|
||||
// so reps' preferred unit sticks across navigations + sessions.
|
||||
// Lazy initializer reads localStorage on first render — avoids the
|
||||
// Lazy initializer reads localStorage on first render - avoids the
|
||||
// mount-effect-setState shape the compiler flags.
|
||||
const [units, setUnits] = useState<'ft' | 'm'>(() => {
|
||||
if (typeof window === 'undefined') return 'ft';
|
||||
|
||||
@@ -61,7 +61,7 @@ const STATUS_TO_STAGES: Record<string, readonly string[]> = {
|
||||
* under_offer → enquiry...reservation, available → any)
|
||||
*
|
||||
* Doc upload + payment recording (Phases 4.4 / 4.5 of the spec) are
|
||||
* out of scope for the initial cut — once the interest exists, the rep
|
||||
* out of scope for the initial cut - once the interest exists, the rep
|
||||
* has the standard interest detail page to upload contracts and record
|
||||
* payments. The wizard's job is to get them from "manual berth, no
|
||||
* interest" to "interest exists, override cleared" in one round-trip.
|
||||
@@ -94,7 +94,7 @@ export function CatchUpWizard({ berthId, open, onOpenChange }: CatchUpWizardProp
|
||||
});
|
||||
|
||||
const allowedStages = berth ? (STATUS_TO_STAGES[berth.data.status] ?? PIPELINE_STAGES) : [];
|
||||
// Default the stage picker to the "right" default for each status —
|
||||
// Default the stage picker to the "right" default for each status -
|
||||
// sold defaults to contract (and we auto-set outcome=won server-side),
|
||||
// under_offer defaults to eoi since that's the most common pre-deal
|
||||
// status that reps mark manually.
|
||||
@@ -124,7 +124,7 @@ export function CatchUpWizard({ berthId, open, onOpenChange }: CatchUpWizardProp
|
||||
);
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
toast.success('Berth reconciled — new interest created');
|
||||
toast.success('Berth reconciled - new interest created');
|
||||
queryClient.invalidateQueries({ queryKey: ['berths'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['berths', 'reconcile-queue'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['interests'] });
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* Maps a berth's mooring-letter prefix (A, B, C…) to a subtle visual
|
||||
* accent. Pontoons cluster physically — A row is one dock, B another
|
||||
* — so the berth grid reads at a glance when each pontoon's rows
|
||||
* accent. Pontoons cluster physically - A row is one dock, B another
|
||||
* - so the berth grid reads at a glance when each pontoon's rows
|
||||
* share a colour cue. Earlier iteration tinted the entire row
|
||||
* background; that proved visually noisy. This version keeps rows
|
||||
* white and surfaces the colour as a coloured left border, plus a
|
||||
* matching dot the column factory uses inside the Mooring # cell.
|
||||
*
|
||||
* Cycle wraps at the 8th letter; ports with more pontoons get
|
||||
* repeats (fine in practice — they don't sit adjacent on the page).
|
||||
* repeats (fine in practice - they don't sit adjacent on the page).
|
||||
*/
|
||||
const BORDER_CYCLE = [
|
||||
'border-l-4 border-l-rose-400',
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Reconcile-diff dialog (Phase 6b — see plan §4.7b, §14.6).
|
||||
* Reconcile-diff dialog (Phase 6b - see plan §4.7b, §14.6).
|
||||
*
|
||||
* Shown after a successful per-berth PDF upload + parse. Surfaces three
|
||||
* sections:
|
||||
* - Warnings (mooring-number mismatch, imperial-vs-metric drift, etc.)
|
||||
* so the rep can abort before applying.
|
||||
* - Auto-applied fields — fields the parser found that the CRM had as null;
|
||||
* - Auto-applied fields - fields the parser found that the CRM had as null;
|
||||
* these are pre-checked and applied on confirm.
|
||||
* - Conflicts — fields where CRM and PDF disagree on a non-null value.
|
||||
* - Conflicts - fields where CRM and PDF disagree on a non-null value.
|
||||
* The rep picks "Keep CRM" or "Use PDF" per row before confirming.
|
||||
*
|
||||
* On confirm, the dialog POSTs to /pdf-versions/parse-results/apply with the
|
||||
|
||||
Reference in New Issue
Block a user