feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.
EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.
Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.
Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.
Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).
Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).
Berth interest list overhaul:
- Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
- Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
- Per-letter row tinting via colored left-border accent + dot in cell
- Documents tab merged Files (single attachments section)
Topbar improvements:
- Always-visible back arrow on detail pages (path depth > 2)
- Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
push their entity hierarchy (Clients › Mary Smith › Interest › B17)
- Tighter spacing, softer separators, 160px crumb truncation
DataTable upgrades:
- Page-size selector with All option (validator cap raised to 1000)
- getRowClassName slot for per-row styling (used by berth tinting)
- Fixed Radix SelectItem crash on empty-string values via __any__
sentinel (was crashing every list page that opened a select filter)
Interest list:
- Configurable columns picker
- Stage cell clickable into detail
- TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
- Save view moved into ColumnPicker menu; Views button hidden when
no views are saved
- Pipeline kanban board endpoint at /api/v1/interests/board with
minimal projection, 5000-row cap + truncated banner, filter
pass-through
Mobile chrome + sidebar collapse removed (always-expanded design choice).
User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,45 @@ function OverviewTab({ yachtId, yacht }: { yachtId: string; yacht: YachtTabsYach
|
||||
const value = transform ? transform(next) : next;
|
||||
await mutation.mutateAsync({ [field]: value });
|
||||
};
|
||||
const numericString = (next: string | null) => (next === null ? null : next);
|
||||
/**
|
||||
* Bidirectional dimension save: when the rep edits Length/Width/Draft
|
||||
* in feet, also write the metric counterpart (and vice versa). Avoids
|
||||
* the "I entered ft but the m row still says '-'" surprise.
|
||||
*
|
||||
* If the rep clears a field (next === null), only that side is
|
||||
* cleared — we never overwrite their other-unit value with a derived
|
||||
* one, since they may have intentionally entered a more precise
|
||||
* metric figure.
|
||||
*/
|
||||
function saveDimension(
|
||||
primaryField: 'lengthFt' | 'widthFt' | 'draftFt' | 'lengthM' | 'widthM' | 'draftM',
|
||||
) {
|
||||
const isFt = primaryField.endsWith('Ft');
|
||||
const counterpart = (
|
||||
isFt ? primaryField.replace('Ft', 'M') : primaryField.replace('M', 'Ft')
|
||||
) as YachtPatchField;
|
||||
return async (next: string | null) => {
|
||||
if (next === null || next === '') {
|
||||
await mutation.mutateAsync({ [primaryField]: null });
|
||||
return;
|
||||
}
|
||||
const n = Number.parseFloat(next);
|
||||
if (!Number.isFinite(n)) {
|
||||
await mutation.mutateAsync({ [primaryField]: next });
|
||||
return;
|
||||
}
|
||||
const FT_PER_M = 3.28084;
|
||||
const converted = isFt ? n / FT_PER_M : n * FT_PER_M;
|
||||
const convertedStr = converted
|
||||
.toFixed(2)
|
||||
.replace(/\.0+$/, '')
|
||||
.replace(/(\.\d)0$/, '$1');
|
||||
await mutation.mutateAsync({
|
||||
[primaryField]: next,
|
||||
[counterpart]: convertedStr,
|
||||
});
|
||||
};
|
||||
}
|
||||
const yearTransform = (next: string | null) => {
|
||||
if (next === null) return null;
|
||||
const n = Number.parseInt(next, 10);
|
||||
@@ -157,13 +195,13 @@ function OverviewTab({ yachtId, yacht }: { yachtId: string; yacht: YachtTabsYach
|
||||
<h3 className="text-sm font-medium mb-2">Dimensions (ft)</h3>
|
||||
<dl>
|
||||
<EditableRow label="Length (ft)">
|
||||
<InlineEditableField value={yacht.lengthFt} onSave={save('lengthFt', numericString)} />
|
||||
<InlineEditableField value={yacht.lengthFt} onSave={saveDimension('lengthFt')} />
|
||||
</EditableRow>
|
||||
<EditableRow label="Width (ft)">
|
||||
<InlineEditableField value={yacht.widthFt} onSave={save('widthFt', numericString)} />
|
||||
<InlineEditableField value={yacht.widthFt} onSave={saveDimension('widthFt')} />
|
||||
</EditableRow>
|
||||
<EditableRow label="Draft (ft)">
|
||||
<InlineEditableField value={yacht.draftFt} onSave={save('draftFt', numericString)} />
|
||||
<InlineEditableField value={yacht.draftFt} onSave={saveDimension('draftFt')} />
|
||||
</EditableRow>
|
||||
</dl>
|
||||
</div>
|
||||
@@ -173,13 +211,13 @@ function OverviewTab({ yachtId, yacht }: { yachtId: string; yacht: YachtTabsYach
|
||||
<h3 className="text-sm font-medium mb-2">Dimensions (m)</h3>
|
||||
<dl>
|
||||
<EditableRow label="Length (m)">
|
||||
<InlineEditableField value={yacht.lengthM} onSave={save('lengthM', numericString)} />
|
||||
<InlineEditableField value={yacht.lengthM} onSave={saveDimension('lengthM')} />
|
||||
</EditableRow>
|
||||
<EditableRow label="Width (m)">
|
||||
<InlineEditableField value={yacht.widthM} onSave={save('widthM', numericString)} />
|
||||
<InlineEditableField value={yacht.widthM} onSave={saveDimension('widthM')} />
|
||||
</EditableRow>
|
||||
<EditableRow label="Draft (m)">
|
||||
<InlineEditableField value={yacht.draftM} onSave={save('draftM', numericString)} />
|
||||
<InlineEditableField value={yacht.draftM} onSave={saveDimension('draftM')} />
|
||||
</EditableRow>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user