Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
2026-05-14 23:42:27 +02:00
|
|
|
import { toast } from 'sonner';
|
2026-05-02 03:33:13 +02:00
|
|
|
import {
|
|
|
|
|
Pencil,
|
|
|
|
|
Archive,
|
|
|
|
|
RotateCcw,
|
|
|
|
|
Trophy,
|
|
|
|
|
XCircle,
|
|
|
|
|
RefreshCcw,
|
|
|
|
|
Mail,
|
|
|
|
|
Phone,
|
feat(sales-ux): triage signals, reminders, realtime toasts, mobile FAB
Sales-CRM workflow batch — closes audit recommendations #2, #3, #4, #6,
#7, #8, #9, #10, #13, #15. Skips #11 (My-pipeline filter — needs a real
assignee column on interests, defer until ownership model lands) and #12
(keyboard shortcuts — explicit user call).
Interest list (the rep's main triage surface):
- Last activity column replaces Created (sortable by
dateLastContact). Postgres NULLs-last on DESC means
never-contacted leads sort to the bottom — exactly the right
triage default.
- Comment-icon next to client name when notesCount > 0, with a
tooltip showing the count. Cheap, glanceable signal that the
lead has correspondence to peek at.
- Urgency badges under stage when criteria fire: "Silent Nd"
for mid-funnel interests with no contact in 7+ days,
"EOI Nd" for EOIs awaiting signature 14+ days, "Deposit Nd"
for eoi_signed interests with no deposit after 21 days.
Pure derived — no extra fetch, computed from the dates the
row already returns.
- Bulk select checkbox column with bulk-archive (existing
DataTable.bulkActions API; just wired with a confirm-dialog
and a Promise.all fan-out).
- Mobile FAB (+) for new interest, anchored above the bottom-tab
bar with safe-area inset awareness.
All four signals mirrored on the mobile InterestCard (comment
icon, urgency badges, last-activity footer).
Interest detail:
- Reminder bell badge in the header showing pending/snoozed
reminder count linked to the interest. Surfaced via
getInterestById's new `activeReminderCount`.
- "Latest note" teaser on the Overview tab — truncated 3-line
preview of the most recent threaded note + relative time +
"View all" link to the Notes tab. Saves a click for the
common "what was discussed last?" peek.
- Color-block swatches in InlineStagePicker dropdown (rounded-sm
mini-bars in the stage's progressive saturation color, replacing
the previous tiny dots). Reads as a visual scan instead of a
list.
Dashboard:
- MyRemindersRail on the right sidebar above the existing
AlertRail. Shows pending+snoozed reminders for the current
user (overdue first), each with priority pill, relative due
time, and click-through to the linked interest/client/berth.
Berth detail:
- BerthInterestPulse card at the top of the Overview tab,
replacing the old "buried in tab" pattern. Shows up to 5
active interests with avatar, stage pill, urgency badges, and
last-activity. Mirrors the old Nuxt CRM's beloved "Interested
Parties" panel but with the new triage signals.
Realtime toasts:
- New <RealtimeToasts /> mounted inside SocketProvider in the
dashboard layout. Subscribes to interest:stageChanged,
document:completed, document:signer:signed, and
interest:outcomeSet — fires sonner toasts so reps watching any
page learn about pipeline events without refreshing.
Service layer:
- listInterests: notesCount per row (left join + count + groupBy).
- getInterestById: clientPrimaryPhone + clientPrimaryPhoneE164
(for the Email/Call/WhatsApp buttons added last commit; phone
pieces were missing), notesCount, recentNote, activeReminderCount.
- sortColumn switch handles 'dateLastContact' explicitly; default
stays 'updatedAt'.
tsc clean. vitest 835/835 pass. ESLint clean on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 04:09:51 +02:00
|
|
|
AlarmClock,
|
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).
CRITICAL (3):
- C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
no longer silently drop interest links
- C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
- C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
callers must go through /stage with the override-guard chain
HIGH (14/15):
- H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
interests/documents/reservations/reminders/invoices (migration 0070)
- H-02 login page reads ?redirect= param with same-origin guard
- H-03 CRM invite token moves to URL fragment so it never lands in
nginx access logs / Referer headers
- H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
- H-05 toggleAccount writes an audit row
- H-06 upsertSetting masks any value whose key ends with _encrypted
- H-07 archiveClient cascade fires per-interest audit rows
- H-08 createSalesTransporter applies SMTP_TIMEOUTS
- H-09 AppShell stable children — viewport flip across breakpoint no
longer destroys in-progress form drafts
- H-10 portal documents page swaps Unicode glyph status icons for
Lucide CheckCircle2/XCircle/Circle + aria-labels
- H-12 list components swap alert(...) for toast.warning(...)
- H-13 5 icon-only buttons gain aria-label
- H-14 parseBody treats empty bodies as {}
- H-15 admin layout renders a 403 panel instead of silent bounce
- H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet
MEDIUM (28+):
- M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
WHEREs across custom-fields, notes (all 6 entity types x update +
delete), client-contacts, yacht ownerClient lookup, webhook reads
- M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
- M-EM01 portal-auth emails thread through portId
- M-EM02 sendEmail accepts cc/bcc params
- M-EM04 notification_digest catalog key
- M-IN01 portal presigned download URLs use 4h TTL
- M-IN02 OpenAI client lazy-instantiated
- M-IN04 stale pdfme refs updated to pdf-lib AcroForm
- M-IN05 umami.testConnection returns tagged union
- M-L01 reservations tenure_type unified with berths
- M-L02 report-generators canonicalize stage values
- M-AU01 audit log placeholder copy fixed
- M-AU04 outcome_set / outcome_cleared distinct audit verbs
- M-NEW-2 activity feed entity name+type separator
- M-R01 portal allowlist narrowed + portal_session backstop in proxy
- M-SC02 companies archived partial index
- M-SC04 audit_logs.searchText documented as DB-managed
- M-S01 storage_s3_access_key_encrypted admin field
- M-U01 audit log empty state uses <EmptyState>
- M-U09 invoice delete dialog -> <AlertDialog>
- M-U10 toast.success on ClientForm + InterestForm create/edit
- M-U11 settings-form-card logo preview alt text
- M-U14 mobile topbar title on clients/yachts/interests/berths
- M-U15 Invoices in mobile More-sheet
LOW (6/8):
- L-AU01 severity defaults for security-relevant verbs
- L-AU02 +13 missing actions in admin audit filter
- L-AU03 +7 missing entity types in admin audit filter
- L-AU04 dead listAuditLogs stubbed
- L-D02 CLAUDE.md Owner-wins chain tightened
Bonus — Document detail polish (#67 partial, 3/6 deliverables):
- state-aware action button per signer
- watcher Add UI with display-name resolution
- cleanSignerName cleanup
Prior session work bundled in:
- Documenso v2 webhook + envelope-ID normalization + sequential signing
- SigningProgress UI redesign (avatars, per-signer state, timestamps)
- env->admin settings registry + RegistryDrivenForm + encrypted creds
- Embedded-signing card + Test connection + setup help
- Dev-mode EMAIL_REDIRECT_TO banner
- Pipeline rules admin page
- Sales email config card
- Audit log details Sheet
- EOI tab: Finalising badge, absolute timestamps, sequential indicator
- Notes pipeline_stage_at_creation (migration 0069)
- Documenso numeric ID dual-key webhook (migration 0068)
- Dimensions criterion copy (migration 0067)
Tests: 1374/1374 vitest pass. tsc clean. lint clean.
See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
|
|
|
User,
|
2026-05-02 03:33:13 +02:00
|
|
|
} from 'lucide-react';
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
import { WhatsAppIcon } from '@/components/icons/whatsapp';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import Link from 'next/link';
|
|
|
|
|
|
2026-05-02 03:33:13 +02:00
|
|
|
import { Button } from '@/components/ui/button';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
|
import { TagBadge } from '@/components/shared/tag-badge';
|
|
|
|
|
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
|
2026-04-28 12:09:47 +02:00
|
|
|
import { DetailHeaderStrip } from '@/components/shared/detail-header-strip';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { PermissionGate } from '@/components/shared/permission-gate';
|
|
|
|
|
import { InterestForm } from '@/components/interests/interest-form';
|
2026-05-02 00:01:33 +02:00
|
|
|
import { InlineStagePicker } from '@/components/interests/inline-stage-picker';
|
|
|
|
|
import { InterestOutcomeDialog } from '@/components/interests/interest-outcome-dialog';
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
import { AssignedToChip } from '@/components/interests/assigned-to-chip';
|
|
|
|
|
import { MultiEoiChip } from '@/components/interests/multi-eoi-chip';
|
|
|
|
|
import { DealPulseChip } from '@/components/interests/deal-pulse-chip';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { apiFetch } from '@/lib/api/client';
|
2026-05-12 16:52:35 +02:00
|
|
|
import { formatOutcome } from '@/lib/constants';
|
2026-05-02 00:01:33 +02:00
|
|
|
import { cn } from '@/lib/utils';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
2026-05-02 00:01:33 +02:00
|
|
|
const OUTCOME_BADGE: Record<string, { label: string; className: string }> = {
|
|
|
|
|
won: { label: 'Won', className: 'bg-emerald-100 text-emerald-700' },
|
2026-05-04 22:57:01 +02:00
|
|
|
lost_other_marina: { label: 'Lost - other marina', className: 'bg-rose-100 text-rose-700' },
|
|
|
|
|
lost_unqualified: { label: 'Lost - unqualified', className: 'bg-rose-100 text-rose-700' },
|
|
|
|
|
lost_no_response: { label: 'Lost - no response', className: 'bg-rose-100 text-rose-700' },
|
2026-05-12 14:50:58 +02:00
|
|
|
lost_other: { label: 'Lost - other', className: 'bg-rose-100 text-rose-700' },
|
2026-05-02 00:01:33 +02:00
|
|
|
cancelled: { label: 'Cancelled', className: 'bg-slate-200 text-slate-700' },
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
};
|
|
|
|
|
|
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton
- Documents hub signer status now renders via a label map (`Pending`,
`Signed`, `Declined`, …) instead of the raw lowercase enum value.
- Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy`
via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and
swaps the "Payment Method" free-text input for a `Select` of labelled
options (`Bank transfer`, `Credit card`, …) so we never store
`bank_transfer` from a hand-typed field again.
- Interest tabs `MilestoneSection` status badge uses a `humanizeStatus`
helper so values like `waiting_for_signatures` show as
`Waiting For Signatures` (correctly title-cased) instead of being a
lower-snake-case fragment inside an ALL-CAPS pill.
- `OUTCOME_BADGE` in the interest header now has a fall-through that
renders any unknown outcome as a closed-state badge, preventing a
closed interest from looking open just because its enum was added
upstream without a matching label entry.
- Interest timeline route joins the `user` table and returns
`userName` alongside `userId`; the client renders the resolved name
instead of a 36-char UUID. Falls back to `'a teammate'` if the user
row was deleted.
- Invoice "New / Step 3 — Review" replaces the truncated UUID display
with a server-resolved client/company name via a small `useQuery`,
so users can confirm they picked the right billing entity before
submitting.
- New `loading.tsx` for client detail renders a header / tab strip /
card skeleton during the server-component / initial-query window
that previously flashed empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
|
|
|
// Catch-all so an unknown outcome (e.g. a future `lost_no_berth` enum) still
|
|
|
|
|
// renders as a closed-state badge instead of falling back to the open-state
|
|
|
|
|
// stage picker. Lost-* gets a rose tint; everything else gets neutral slate.
|
|
|
|
|
function resolveOutcomeBadge(outcome: string | null | undefined) {
|
|
|
|
|
if (!outcome) return null;
|
|
|
|
|
const known = OUTCOME_BADGE[outcome];
|
|
|
|
|
if (known) return known;
|
|
|
|
|
const isLoss = outcome.startsWith('lost');
|
|
|
|
|
return {
|
2026-05-12 16:52:35 +02:00
|
|
|
label: formatOutcome(outcome) ?? outcome,
|
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton
- Documents hub signer status now renders via a label map (`Pending`,
`Signed`, `Declined`, …) instead of the raw lowercase enum value.
- Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy`
via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and
swaps the "Payment Method" free-text input for a `Select` of labelled
options (`Bank transfer`, `Credit card`, …) so we never store
`bank_transfer` from a hand-typed field again.
- Interest tabs `MilestoneSection` status badge uses a `humanizeStatus`
helper so values like `waiting_for_signatures` show as
`Waiting For Signatures` (correctly title-cased) instead of being a
lower-snake-case fragment inside an ALL-CAPS pill.
- `OUTCOME_BADGE` in the interest header now has a fall-through that
renders any unknown outcome as a closed-state badge, preventing a
closed interest from looking open just because its enum was added
upstream without a matching label entry.
- Interest timeline route joins the `user` table and returns
`userName` alongside `userId`; the client renders the resolved name
instead of a 36-char UUID. Falls back to `'a teammate'` if the user
row was deleted.
- Invoice "New / Step 3 — Review" replaces the truncated UUID display
with a server-resolved client/company name via a small `useQuery`,
so users can confirm they picked the right billing entity before
submitting.
- New `loading.tsx` for client detail renders a header / tab strip /
card skeleton during the server-component / initial-query window
that previously flashed empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
|
|
|
className: isLoss ? 'bg-rose-100 text-rose-700' : 'bg-slate-200 text-slate-700',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
const CATEGORY_LABELS: Record<string, string> = {
|
2026-05-02 00:01:33 +02:00
|
|
|
general_interest: 'General',
|
fix(audit-wave-9): copy/terminology sweep (copy-auditor)
Address the highest-impact items from the copy-auditor's CRITICAL +
HIGH + MEDIUM bands:
**C2 portal raw-status leak**
- Drop the staff-only `leadCategory` chip from the portal interests
page entirely. Privacy + optics: clients should never see "hot lead"
in their own portal. `eoiStatus` was already wrapped in
`portalSigningLabel`; only the categorical chip remained.
**C3 signing-status label drift**
- Add `src/lib/labels/document-status.ts` as the single source of
truth for the {draft, sent, partially_signed, completed, expired,
cancelled} lifecycle: labels (CRM + portal variants), StatusPill
variant, and the "active / in-flight" set.
- Wire it into interest-eoi-tab, interest-contract-tab,
interest-reservation-tab — they previously redefined identical
STATUS_LABELS / ACTIVE_STATUSES blocks per-file.
**H1 + M3 verbiage codemod**
- `Save Changes` → `Save changes` (sentence case, matches the
surrounding admin/CRM pattern).
- `Saving...` (ASCII three dots) → `Saving…` (Unicode ellipsis).
Matches the project's UTF-8-elsewhere convention and reads
correctly via screen-readers.
**M1 envelope jargon → signing request**
- smart-archive-dialog: "Leave envelope pending" → "Leave signing
request pending"; "Void the signing envelope" → "Cancel the signing
request"; section header updated to match.
- document-detail: "voids the signing envelope" → "cancels the signing
request".
- bulk-archive-wizard: "leave invoices/signing envelopes alone" →
"leave invoices/signing requests alone".
- Documenso admin page intentionally keeps `envelope` (dev/integration
vocabulary).
**M5 Hot Lead casing**
- Normalize `Hot Lead` / `General Interest` / `Specific Qualified` to
sentence case in `constants.ts` LABEL_OVERRIDES and all per-file
lead-category maps so the CRM trend (sentence case) is consistent.
**C1 surface-level rename**
- "Linked prospect (optional)" → "Linked interest (optional)" on the
berth status-change dialog.
- "Deal Documents" tab → "Interest Documents" (URL/route kept as
`/deal-documents` to avoid breaking deep links; rename deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:12:40 +02:00
|
|
|
specific_qualified: 'Specific qualified',
|
|
|
|
|
hot_lead: 'Hot lead',
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface InterestDetailHeaderProps {
|
|
|
|
|
portSlug: string;
|
|
|
|
|
interest: {
|
|
|
|
|
id: string;
|
|
|
|
|
clientId: string;
|
|
|
|
|
clientName: string | null;
|
2026-05-02 03:33:13 +02:00
|
|
|
/** Primary contact channels resolved from the linked client. The header
|
|
|
|
|
* uses these to render Email / Call / WhatsApp buttons so the rep
|
|
|
|
|
* doesn't have to navigate to the client page just to reach out. */
|
|
|
|
|
clientPrimaryEmail?: string | null;
|
|
|
|
|
clientPrimaryPhone?: string | null;
|
|
|
|
|
clientPrimaryPhoneE164?: string | null;
|
feat(sales-ux): triage signals, reminders, realtime toasts, mobile FAB
Sales-CRM workflow batch — closes audit recommendations #2, #3, #4, #6,
#7, #8, #9, #10, #13, #15. Skips #11 (My-pipeline filter — needs a real
assignee column on interests, defer until ownership model lands) and #12
(keyboard shortcuts — explicit user call).
Interest list (the rep's main triage surface):
- Last activity column replaces Created (sortable by
dateLastContact). Postgres NULLs-last on DESC means
never-contacted leads sort to the bottom — exactly the right
triage default.
- Comment-icon next to client name when notesCount > 0, with a
tooltip showing the count. Cheap, glanceable signal that the
lead has correspondence to peek at.
- Urgency badges under stage when criteria fire: "Silent Nd"
for mid-funnel interests with no contact in 7+ days,
"EOI Nd" for EOIs awaiting signature 14+ days, "Deposit Nd"
for eoi_signed interests with no deposit after 21 days.
Pure derived — no extra fetch, computed from the dates the
row already returns.
- Bulk select checkbox column with bulk-archive (existing
DataTable.bulkActions API; just wired with a confirm-dialog
and a Promise.all fan-out).
- Mobile FAB (+) for new interest, anchored above the bottom-tab
bar with safe-area inset awareness.
All four signals mirrored on the mobile InterestCard (comment
icon, urgency badges, last-activity footer).
Interest detail:
- Reminder bell badge in the header showing pending/snoozed
reminder count linked to the interest. Surfaced via
getInterestById's new `activeReminderCount`.
- "Latest note" teaser on the Overview tab — truncated 3-line
preview of the most recent threaded note + relative time +
"View all" link to the Notes tab. Saves a click for the
common "what was discussed last?" peek.
- Color-block swatches in InlineStagePicker dropdown (rounded-sm
mini-bars in the stage's progressive saturation color, replacing
the previous tiny dots). Reads as a visual scan instead of a
list.
Dashboard:
- MyRemindersRail on the right sidebar above the existing
AlertRail. Shows pending+snoozed reminders for the current
user (overdue first), each with priority pill, relative due
time, and click-through to the linked interest/client/berth.
Berth detail:
- BerthInterestPulse card at the top of the Overview tab,
replacing the old "buried in tab" pattern. Shows up to 5
active interests with avatar, stage pill, urgency badges, and
last-activity. Mirrors the old Nuxt CRM's beloved "Interested
Parties" panel but with the new triage signals.
Realtime toasts:
- New <RealtimeToasts /> mounted inside SocketProvider in the
dashboard layout. Subscribes to interest:stageChanged,
document:completed, document:signer:signed, and
interest:outcomeSet — fires sonner toasts so reps watching any
page learn about pipeline events without refreshing.
Service layer:
- listInterests: notesCount per row (left join + count + groupBy).
- getInterestById: clientPrimaryPhone + clientPrimaryPhoneE164
(for the Email/Call/WhatsApp buttons added last commit; phone
pieces were missing), notesCount, recentNote, activeReminderCount.
- sortColumn switch handles 'dateLastContact' explicitly; default
stays 'updatedAt'.
tsc clean. vitest 835/835 pass. ESLint clean on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 04:09:51 +02:00
|
|
|
/** Pending/snoozed reminders attached to this interest. Drives the
|
2026-05-04 22:57:01 +02:00
|
|
|
* alarm-bell badge on the header - surfaces follow-ups so the rep
|
feat(sales-ux): triage signals, reminders, realtime toasts, mobile FAB
Sales-CRM workflow batch — closes audit recommendations #2, #3, #4, #6,
#7, #8, #9, #10, #13, #15. Skips #11 (My-pipeline filter — needs a real
assignee column on interests, defer until ownership model lands) and #12
(keyboard shortcuts — explicit user call).
Interest list (the rep's main triage surface):
- Last activity column replaces Created (sortable by
dateLastContact). Postgres NULLs-last on DESC means
never-contacted leads sort to the bottom — exactly the right
triage default.
- Comment-icon next to client name when notesCount > 0, with a
tooltip showing the count. Cheap, glanceable signal that the
lead has correspondence to peek at.
- Urgency badges under stage when criteria fire: "Silent Nd"
for mid-funnel interests with no contact in 7+ days,
"EOI Nd" for EOIs awaiting signature 14+ days, "Deposit Nd"
for eoi_signed interests with no deposit after 21 days.
Pure derived — no extra fetch, computed from the dates the
row already returns.
- Bulk select checkbox column with bulk-archive (existing
DataTable.bulkActions API; just wired with a confirm-dialog
and a Promise.all fan-out).
- Mobile FAB (+) for new interest, anchored above the bottom-tab
bar with safe-area inset awareness.
All four signals mirrored on the mobile InterestCard (comment
icon, urgency badges, last-activity footer).
Interest detail:
- Reminder bell badge in the header showing pending/snoozed
reminder count linked to the interest. Surfaced via
getInterestById's new `activeReminderCount`.
- "Latest note" teaser on the Overview tab — truncated 3-line
preview of the most recent threaded note + relative time +
"View all" link to the Notes tab. Saves a click for the
common "what was discussed last?" peek.
- Color-block swatches in InlineStagePicker dropdown (rounded-sm
mini-bars in the stage's progressive saturation color, replacing
the previous tiny dots). Reads as a visual scan instead of a
list.
Dashboard:
- MyRemindersRail on the right sidebar above the existing
AlertRail. Shows pending+snoozed reminders for the current
user (overdue first), each with priority pill, relative due
time, and click-through to the linked interest/client/berth.
Berth detail:
- BerthInterestPulse card at the top of the Overview tab,
replacing the old "buried in tab" pattern. Shows up to 5
active interests with avatar, stage pill, urgency badges, and
last-activity. Mirrors the old Nuxt CRM's beloved "Interested
Parties" panel but with the new triage signals.
Realtime toasts:
- New <RealtimeToasts /> mounted inside SocketProvider in the
dashboard layout. Subscribes to interest:stageChanged,
document:completed, document:signer:signed, and
interest:outcomeSet — fires sonner toasts so reps watching any
page learn about pipeline events without refreshing.
Service layer:
- listInterests: notesCount per row (left join + count + groupBy).
- getInterestById: clientPrimaryPhone + clientPrimaryPhoneE164
(for the Email/Call/WhatsApp buttons added last commit; phone
pieces were missing), notesCount, recentNote, activeReminderCount.
- sortColumn switch handles 'dateLastContact' explicitly; default
stays 'updatedAt'.
tsc clean. vitest 835/835 pass. ESLint clean on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 04:09:51 +02:00
|
|
|
* doesn't have to remember to check /reminders. */
|
|
|
|
|
activeReminderCount?: number;
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
berthId: string | null;
|
|
|
|
|
berthMooringNumber: string | null;
|
2026-05-14 23:46:36 +02:00
|
|
|
yachtId: string | null;
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
pipelineStage: string;
|
|
|
|
|
leadCategory: string | null;
|
|
|
|
|
source: string | null;
|
|
|
|
|
notes: string | null;
|
|
|
|
|
reminderEnabled: boolean;
|
|
|
|
|
reminderDays: number | null;
|
|
|
|
|
archivedAt: string | null;
|
2026-05-02 00:01:33 +02:00
|
|
|
outcome?: string | null;
|
|
|
|
|
outcomeReason?: string | null;
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
dateLastContact?: string | null;
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
dateFirstContact?: string | null;
|
|
|
|
|
dateEoiSent?: string | null;
|
|
|
|
|
dateEoiSigned?: string | null;
|
|
|
|
|
dateReservationSigned?: string | null;
|
|
|
|
|
dateContractSent?: string | null;
|
|
|
|
|
dateContractSigned?: string | null;
|
|
|
|
|
dateDepositReceived?: string | null;
|
|
|
|
|
eoiDocStatus?: string | null;
|
|
|
|
|
reservationDocStatus?: string | null;
|
|
|
|
|
contractDocStatus?: string | null;
|
|
|
|
|
/** Activity-log entries in the last 7 days — drives deal-pulse +5 signal. */
|
|
|
|
|
recentActivityCount?: number | null;
|
|
|
|
|
/** Sales rep who owns this deal — populated by the AssignedToChip. */
|
|
|
|
|
assignedTo?: string | null;
|
|
|
|
|
assignedToName?: string | null;
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
tags?: Array<{ id: string; name: string; color: string }>;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
function formatLastContactAge(iso: string): string {
|
|
|
|
|
const days = Math.floor((Date.now() - new Date(iso).getTime()) / 86_400_000);
|
|
|
|
|
if (days <= 0) return 'today';
|
|
|
|
|
if (days === 1) return 'yesterday';
|
|
|
|
|
if (days < 30) return `${days}d ago`;
|
|
|
|
|
if (days < 365) return `${Math.floor(days / 30)}mo ago`;
|
|
|
|
|
return `${Math.floor(days / 365)}y ago`;
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeaderProps) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
|
|
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
2026-05-02 00:01:33 +02:00
|
|
|
const [outcomeDialog, setOutcomeDialog] = useState<null | 'won' | 'lost'>(null);
|
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>
2026-05-07 20:59:28 +02:00
|
|
|
// (Upload-paper-signed-EOI dialog moved to the EOI tab.)
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
const isArchived = !!interest.archivedAt;
|
fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton
- Documents hub signer status now renders via a label map (`Pending`,
`Signed`, `Declined`, …) instead of the raw lowercase enum value.
- Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy`
via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and
swaps the "Payment Method" free-text input for a `Select` of labelled
options (`Bank transfer`, `Credit card`, …) so we never store
`bank_transfer` from a hand-typed field again.
- Interest tabs `MilestoneSection` status badge uses a `humanizeStatus`
helper so values like `waiting_for_signatures` show as
`Waiting For Signatures` (correctly title-cased) instead of being a
lower-snake-case fragment inside an ALL-CAPS pill.
- `OUTCOME_BADGE` in the interest header now has a fall-through that
renders any unknown outcome as a closed-state badge, preventing a
closed interest from looking open just because its enum was added
upstream without a matching label entry.
- Interest timeline route joins the `user` table and returns
`userName` alongside `userId`; the client renders the resolved name
instead of a 36-char UUID. Falls back to `'a teammate'` if the user
row was deleted.
- Invoice "New / Step 3 — Review" replaces the truncated UUID display
with a server-resolved client/company name via a small `useQuery`,
so users can confirm they picked the right billing entity before
submitting.
- New `loading.tsx` for client detail renders a header / tab strip /
card skeleton during the server-component / initial-query window
that previously flashed empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
|
|
|
const outcomeBadge = resolveOutcomeBadge(interest.outcome);
|
2026-05-02 00:01:33 +02:00
|
|
|
const isClosed = !!interest.outcome;
|
|
|
|
|
|
2026-05-04 22:57:01 +02:00
|
|
|
// Contact deep-links - resolved from the linked client's primary channels.
|
2026-05-02 03:33:13 +02:00
|
|
|
// wa.me requires the digits-only E.164 number (no leading "+"); fall back to
|
|
|
|
|
// stripping non-digits from the display value when the canonical form is
|
|
|
|
|
// missing.
|
|
|
|
|
const whatsappNumber = interest.clientPrimaryPhoneE164
|
|
|
|
|
? interest.clientPrimaryPhoneE164.replace(/^\+/, '')
|
|
|
|
|
: interest.clientPrimaryPhone
|
|
|
|
|
? interest.clientPrimaryPhone.replace(/[^\d]/g, '')
|
|
|
|
|
: null;
|
|
|
|
|
|
2026-05-02 00:01:33 +02:00
|
|
|
const reopenMutation = useMutation({
|
|
|
|
|
mutationFn: () =>
|
|
|
|
|
apiFetch(`/api/v1/interests/${interest.id}/outcome`, { method: 'DELETE', body: {} }),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests', interest.id] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests'] });
|
2026-05-14 23:42:27 +02:00
|
|
|
// F26: confirm to the user that the action ran — pre-fix the
|
|
|
|
|
// button gave no feedback and reps weren't sure if it took.
|
|
|
|
|
toast.success('Outcome cleared — interest is open again.');
|
2026-05-02 00:01:33 +02:00
|
|
|
},
|
|
|
|
|
});
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
const archiveMutation = useMutation({
|
2026-04-28 12:09:47 +02:00
|
|
|
mutationFn: () => apiFetch(`/api/v1/interests/${interest.id}`, { method: 'DELETE' }),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests', interest.id] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests'] });
|
|
|
|
|
setArchiveOpen(false);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const restoreMutation = useMutation({
|
2026-04-28 12:09:47 +02:00
|
|
|
mutationFn: () => apiFetch(`/api/v1/interests/${interest.id}/restore`, { method: 'POST' }),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests', interest.id] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['interests'] });
|
|
|
|
|
setArchiveOpen(false);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-02 00:01:33 +02:00
|
|
|
const meta: Array<{ key: string; node: React.ReactNode }> = [];
|
|
|
|
|
if (interest.berthMooringNumber) {
|
|
|
|
|
meta.push({
|
|
|
|
|
key: 'berth',
|
|
|
|
|
node: (
|
|
|
|
|
<Link
|
|
|
|
|
href={`/${portSlug}/berths/${interest.berthId}`}
|
|
|
|
|
className="text-foreground hover:underline"
|
|
|
|
|
>
|
|
|
|
|
Berth {interest.berthMooringNumber}
|
|
|
|
|
</Link>
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (interest.leadCategory) {
|
|
|
|
|
meta.push({
|
|
|
|
|
key: 'cat',
|
|
|
|
|
node: <span>{CATEGORY_LABELS[interest.leadCategory] ?? interest.leadCategory}</span>,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (interest.source) {
|
|
|
|
|
meta.push({
|
|
|
|
|
key: 'src',
|
|
|
|
|
node: <span className="capitalize">{interest.source}</span>,
|
|
|
|
|
});
|
|
|
|
|
}
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
if (interest.dateLastContact) {
|
|
|
|
|
meta.push({
|
|
|
|
|
key: 'last',
|
|
|
|
|
node: (
|
|
|
|
|
<span className="text-foreground/70">
|
|
|
|
|
Last contact {formatLastContactAge(interest.dateLastContact)}
|
|
|
|
|
</span>
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-05-02 00:01:33 +02:00
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
return (
|
|
|
|
|
<>
|
2026-04-28 12:09:47 +02:00
|
|
|
<DetailHeaderStrip>
|
2026-05-02 00:01:33 +02:00
|
|
|
<div className="flex items-start gap-2">
|
|
|
|
|
<div className="min-w-0 flex-1 space-y-1.5">
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
2026-05-02 00:01:33 +02:00
|
|
|
<h1 className="truncate text-lg font-bold text-foreground sm:text-xl">
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
{interest.clientName ?? 'Unknown Client'}
|
|
|
|
|
</h1>
|
|
|
|
|
{isArchived && (
|
2026-04-28 12:09:47 +02:00
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
Archived
|
|
|
|
|
</Badge>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
2026-05-02 00:01:33 +02:00
|
|
|
{outcomeBadge ? (
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
|
|
|
|
|
outcomeBadge.className,
|
|
|
|
|
)}
|
|
|
|
|
title={interest.outcomeReason ?? undefined}
|
|
|
|
|
>
|
|
|
|
|
{outcomeBadge.label}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</span>
|
2026-05-02 00:01:33 +02:00
|
|
|
) : (
|
|
|
|
|
<PermissionGate
|
|
|
|
|
resource="interests"
|
|
|
|
|
action="change_stage"
|
|
|
|
|
fallback={
|
|
|
|
|
<span className="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-700">
|
|
|
|
|
{interest.pipelineStage}
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<InlineStagePicker
|
|
|
|
|
interestId={interest.id}
|
|
|
|
|
currentStage={interest.pipelineStage}
|
2026-05-14 23:46:36 +02:00
|
|
|
currentYachtId={interest.yachtId}
|
|
|
|
|
clientId={interest.clientId}
|
2026-05-02 00:01:33 +02:00
|
|
|
/>
|
|
|
|
|
</PermissionGate>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
feat(sales-ux): triage signals, reminders, realtime toasts, mobile FAB
Sales-CRM workflow batch — closes audit recommendations #2, #3, #4, #6,
#7, #8, #9, #10, #13, #15. Skips #11 (My-pipeline filter — needs a real
assignee column on interests, defer until ownership model lands) and #12
(keyboard shortcuts — explicit user call).
Interest list (the rep's main triage surface):
- Last activity column replaces Created (sortable by
dateLastContact). Postgres NULLs-last on DESC means
never-contacted leads sort to the bottom — exactly the right
triage default.
- Comment-icon next to client name when notesCount > 0, with a
tooltip showing the count. Cheap, glanceable signal that the
lead has correspondence to peek at.
- Urgency badges under stage when criteria fire: "Silent Nd"
for mid-funnel interests with no contact in 7+ days,
"EOI Nd" for EOIs awaiting signature 14+ days, "Deposit Nd"
for eoi_signed interests with no deposit after 21 days.
Pure derived — no extra fetch, computed from the dates the
row already returns.
- Bulk select checkbox column with bulk-archive (existing
DataTable.bulkActions API; just wired with a confirm-dialog
and a Promise.all fan-out).
- Mobile FAB (+) for new interest, anchored above the bottom-tab
bar with safe-area inset awareness.
All four signals mirrored on the mobile InterestCard (comment
icon, urgency badges, last-activity footer).
Interest detail:
- Reminder bell badge in the header showing pending/snoozed
reminder count linked to the interest. Surfaced via
getInterestById's new `activeReminderCount`.
- "Latest note" teaser on the Overview tab — truncated 3-line
preview of the most recent threaded note + relative time +
"View all" link to the Notes tab. Saves a click for the
common "what was discussed last?" peek.
- Color-block swatches in InlineStagePicker dropdown (rounded-sm
mini-bars in the stage's progressive saturation color, replacing
the previous tiny dots). Reads as a visual scan instead of a
list.
Dashboard:
- MyRemindersRail on the right sidebar above the existing
AlertRail. Shows pending+snoozed reminders for the current
user (overdue first), each with priority pill, relative due
time, and click-through to the linked interest/client/berth.
Berth detail:
- BerthInterestPulse card at the top of the Overview tab,
replacing the old "buried in tab" pattern. Shows up to 5
active interests with avatar, stage pill, urgency badges, and
last-activity. Mirrors the old Nuxt CRM's beloved "Interested
Parties" panel but with the new triage signals.
Realtime toasts:
- New <RealtimeToasts /> mounted inside SocketProvider in the
dashboard layout. Subscribes to interest:stageChanged,
document:completed, document:signer:signed, and
interest:outcomeSet — fires sonner toasts so reps watching any
page learn about pipeline events without refreshing.
Service layer:
- listInterests: notesCount per row (left join + count + groupBy).
- getInterestById: clientPrimaryPhone + clientPrimaryPhoneE164
(for the Email/Call/WhatsApp buttons added last commit; phone
pieces were missing), notesCount, recentNote, activeReminderCount.
- sortColumn switch handles 'dateLastContact' explicitly; default
stays 'updatedAt'.
tsc clean. vitest 835/835 pass. ESLint clean on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 04:09:51 +02:00
|
|
|
{(interest.activeReminderCount ?? 0) > 0 ? (
|
|
|
|
|
<span
|
|
|
|
|
className="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-amber-800"
|
|
|
|
|
title={`${interest.activeReminderCount} pending reminder${
|
|
|
|
|
interest.activeReminderCount === 1 ? '' : 's'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<AlarmClock className="size-3" aria-hidden />
|
feat(sales-ux): triage signals, reminders, realtime toasts, mobile FAB
Sales-CRM workflow batch — closes audit recommendations #2, #3, #4, #6,
#7, #8, #9, #10, #13, #15. Skips #11 (My-pipeline filter — needs a real
assignee column on interests, defer until ownership model lands) and #12
(keyboard shortcuts — explicit user call).
Interest list (the rep's main triage surface):
- Last activity column replaces Created (sortable by
dateLastContact). Postgres NULLs-last on DESC means
never-contacted leads sort to the bottom — exactly the right
triage default.
- Comment-icon next to client name when notesCount > 0, with a
tooltip showing the count. Cheap, glanceable signal that the
lead has correspondence to peek at.
- Urgency badges under stage when criteria fire: "Silent Nd"
for mid-funnel interests with no contact in 7+ days,
"EOI Nd" for EOIs awaiting signature 14+ days, "Deposit Nd"
for eoi_signed interests with no deposit after 21 days.
Pure derived — no extra fetch, computed from the dates the
row already returns.
- Bulk select checkbox column with bulk-archive (existing
DataTable.bulkActions API; just wired with a confirm-dialog
and a Promise.all fan-out).
- Mobile FAB (+) for new interest, anchored above the bottom-tab
bar with safe-area inset awareness.
All four signals mirrored on the mobile InterestCard (comment
icon, urgency badges, last-activity footer).
Interest detail:
- Reminder bell badge in the header showing pending/snoozed
reminder count linked to the interest. Surfaced via
getInterestById's new `activeReminderCount`.
- "Latest note" teaser on the Overview tab — truncated 3-line
preview of the most recent threaded note + relative time +
"View all" link to the Notes tab. Saves a click for the
common "what was discussed last?" peek.
- Color-block swatches in InlineStagePicker dropdown (rounded-sm
mini-bars in the stage's progressive saturation color, replacing
the previous tiny dots). Reads as a visual scan instead of a
list.
Dashboard:
- MyRemindersRail on the right sidebar above the existing
AlertRail. Shows pending+snoozed reminders for the current
user (overdue first), each with priority pill, relative due
time, and click-through to the linked interest/client/berth.
Berth detail:
- BerthInterestPulse card at the top of the Overview tab,
replacing the old "buried in tab" pattern. Shows up to 5
active interests with avatar, stage pill, urgency badges, and
last-activity. Mirrors the old Nuxt CRM's beloved "Interested
Parties" panel but with the new triage signals.
Realtime toasts:
- New <RealtimeToasts /> mounted inside SocketProvider in the
dashboard layout. Subscribes to interest:stageChanged,
document:completed, document:signer:signed, and
interest:outcomeSet — fires sonner toasts so reps watching any
page learn about pipeline events without refreshing.
Service layer:
- listInterests: notesCount per row (left join + count + groupBy).
- getInterestById: clientPrimaryPhone + clientPrimaryPhoneE164
(for the Email/Call/WhatsApp buttons added last commit; phone
pieces were missing), notesCount, recentNote, activeReminderCount.
- sortColumn switch handles 'dateLastContact' explicitly; default
stays 'updatedAt'.
tsc clean. vitest 835/835 pass. ESLint clean on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 04:09:51 +02:00
|
|
|
{interest.activeReminderCount}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
<PermissionGate resource="interests" action="edit">
|
|
|
|
|
<AssignedToChip
|
|
|
|
|
interestId={interest.id}
|
|
|
|
|
currentAssignedTo={interest.assignedTo ?? null}
|
|
|
|
|
currentAssignedToName={interest.assignedToName ?? null}
|
|
|
|
|
/>
|
|
|
|
|
</PermissionGate>
|
|
|
|
|
<MultiEoiChip interestId={interest.id} />
|
|
|
|
|
<DealPulseChip
|
|
|
|
|
interest={{
|
|
|
|
|
pipelineStage: interest.pipelineStage,
|
|
|
|
|
outcome: interest.outcome,
|
|
|
|
|
archivedAt: interest.archivedAt,
|
|
|
|
|
dateFirstContact: interest.dateFirstContact,
|
|
|
|
|
dateLastContact: interest.dateLastContact,
|
|
|
|
|
dateEoiSent: interest.dateEoiSent,
|
|
|
|
|
dateEoiSigned: interest.dateEoiSigned,
|
|
|
|
|
dateReservationSigned: interest.dateReservationSigned,
|
|
|
|
|
dateContractSent: interest.dateContractSent,
|
|
|
|
|
dateContractSigned: interest.dateContractSigned,
|
|
|
|
|
dateDepositReceived: interest.dateDepositReceived,
|
|
|
|
|
eoiDocStatus: interest.eoiDocStatus,
|
|
|
|
|
reservationDocStatus: interest.reservationDocStatus,
|
|
|
|
|
contractDocStatus: interest.contractDocStatus,
|
|
|
|
|
recentActivityCount: interest.recentActivityCount,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-02 00:01:33 +02:00
|
|
|
{meta.length > 0 ? (
|
|
|
|
|
<p className="text-xs text-muted-foreground sm:text-sm">
|
|
|
|
|
{meta.map((m, i) => (
|
|
|
|
|
<span key={m.key}>
|
|
|
|
|
{i > 0 ? (
|
|
|
|
|
<span aria-hidden className="mx-1.5">
|
|
|
|
|
·
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
{m.node}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
{interest.tags && interest.tags.length > 0 && (
|
2026-05-02 00:01:33 +02:00
|
|
|
<div className="flex flex-wrap gap-1 pt-0.5">
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
{interest.tags.map((tag) => (
|
|
|
|
|
<TagBadge key={tag.id} name={tag.name} color={tag.color} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-02 03:33:13 +02:00
|
|
|
|
2026-05-04 22:57:01 +02:00
|
|
|
{/* Contact deep-links - let the rep email / call / WhatsApp the
|
2026-05-02 03:33:13 +02:00
|
|
|
client without leaving the interest workspace. Resolved from
|
|
|
|
|
the linked client's primary contact channels (server-side
|
|
|
|
|
fetch in getInterestById). */}
|
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).
CRITICAL (3):
- C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
no longer silently drop interest links
- C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
- C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
callers must go through /stage with the override-guard chain
HIGH (14/15):
- H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
interests/documents/reservations/reminders/invoices (migration 0070)
- H-02 login page reads ?redirect= param with same-origin guard
- H-03 CRM invite token moves to URL fragment so it never lands in
nginx access logs / Referer headers
- H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
- H-05 toggleAccount writes an audit row
- H-06 upsertSetting masks any value whose key ends with _encrypted
- H-07 archiveClient cascade fires per-interest audit rows
- H-08 createSalesTransporter applies SMTP_TIMEOUTS
- H-09 AppShell stable children — viewport flip across breakpoint no
longer destroys in-progress form drafts
- H-10 portal documents page swaps Unicode glyph status icons for
Lucide CheckCircle2/XCircle/Circle + aria-labels
- H-12 list components swap alert(...) for toast.warning(...)
- H-13 5 icon-only buttons gain aria-label
- H-14 parseBody treats empty bodies as {}
- H-15 admin layout renders a 403 panel instead of silent bounce
- H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet
MEDIUM (28+):
- M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
WHEREs across custom-fields, notes (all 6 entity types x update +
delete), client-contacts, yacht ownerClient lookup, webhook reads
- M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
- M-EM01 portal-auth emails thread through portId
- M-EM02 sendEmail accepts cc/bcc params
- M-EM04 notification_digest catalog key
- M-IN01 portal presigned download URLs use 4h TTL
- M-IN02 OpenAI client lazy-instantiated
- M-IN04 stale pdfme refs updated to pdf-lib AcroForm
- M-IN05 umami.testConnection returns tagged union
- M-L01 reservations tenure_type unified with berths
- M-L02 report-generators canonicalize stage values
- M-AU01 audit log placeholder copy fixed
- M-AU04 outcome_set / outcome_cleared distinct audit verbs
- M-NEW-2 activity feed entity name+type separator
- M-R01 portal allowlist narrowed + portal_session backstop in proxy
- M-SC02 companies archived partial index
- M-SC04 audit_logs.searchText documented as DB-managed
- M-S01 storage_s3_access_key_encrypted admin field
- M-U01 audit log empty state uses <EmptyState>
- M-U09 invoice delete dialog -> <AlertDialog>
- M-U10 toast.success on ClientForm + InterestForm create/edit
- M-U11 settings-form-card logo preview alt text
- M-U14 mobile topbar title on clients/yachts/interests/berths
- M-U15 Invoices in mobile More-sheet
LOW (6/8):
- L-AU01 severity defaults for security-relevant verbs
- L-AU02 +13 missing actions in admin audit filter
- L-AU03 +7 missing entity types in admin audit filter
- L-AU04 dead listAuditLogs stubbed
- L-D02 CLAUDE.md Owner-wins chain tightened
Bonus — Document detail polish (#67 partial, 3/6 deliverables):
- state-aware action button per signer
- watcher Add UI with display-name resolution
- cleanSignerName cleanup
Prior session work bundled in:
- Documenso v2 webhook + envelope-ID normalization + sequential signing
- SigningProgress UI redesign (avatars, per-signer state, timestamps)
- env->admin settings registry + RegistryDrivenForm + encrypted creds
- Embedded-signing card + Test connection + setup help
- Dev-mode EMAIL_REDIRECT_TO banner
- Pipeline rules admin page
- Sales email config card
- Audit log details Sheet
- EOI tab: Finalising badge, absolute timestamps, sequential indicator
- Notes pipeline_stage_at_creation (migration 0069)
- Documenso numeric ID dual-key webhook (migration 0068)
- Dimensions criterion copy (migration 0067)
Tests: 1374/1374 vitest pass. tsc clean. lint clean.
See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
|
|
|
{interest.clientPrimaryEmail ||
|
|
|
|
|
interest.clientPrimaryPhone ||
|
|
|
|
|
whatsappNumber ||
|
|
|
|
|
interest.clientId ? (
|
2026-05-02 03:33:13 +02:00
|
|
|
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).
CRITICAL (3):
- C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
no longer silently drop interest links
- C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
- C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
callers must go through /stage with the override-guard chain
HIGH (14/15):
- H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
interests/documents/reservations/reminders/invoices (migration 0070)
- H-02 login page reads ?redirect= param with same-origin guard
- H-03 CRM invite token moves to URL fragment so it never lands in
nginx access logs / Referer headers
- H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
- H-05 toggleAccount writes an audit row
- H-06 upsertSetting masks any value whose key ends with _encrypted
- H-07 archiveClient cascade fires per-interest audit rows
- H-08 createSalesTransporter applies SMTP_TIMEOUTS
- H-09 AppShell stable children — viewport flip across breakpoint no
longer destroys in-progress form drafts
- H-10 portal documents page swaps Unicode glyph status icons for
Lucide CheckCircle2/XCircle/Circle + aria-labels
- H-12 list components swap alert(...) for toast.warning(...)
- H-13 5 icon-only buttons gain aria-label
- H-14 parseBody treats empty bodies as {}
- H-15 admin layout renders a 403 panel instead of silent bounce
- H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet
MEDIUM (28+):
- M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
WHEREs across custom-fields, notes (all 6 entity types x update +
delete), client-contacts, yacht ownerClient lookup, webhook reads
- M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
- M-EM01 portal-auth emails thread through portId
- M-EM02 sendEmail accepts cc/bcc params
- M-EM04 notification_digest catalog key
- M-IN01 portal presigned download URLs use 4h TTL
- M-IN02 OpenAI client lazy-instantiated
- M-IN04 stale pdfme refs updated to pdf-lib AcroForm
- M-IN05 umami.testConnection returns tagged union
- M-L01 reservations tenure_type unified with berths
- M-L02 report-generators canonicalize stage values
- M-AU01 audit log placeholder copy fixed
- M-AU04 outcome_set / outcome_cleared distinct audit verbs
- M-NEW-2 activity feed entity name+type separator
- M-R01 portal allowlist narrowed + portal_session backstop in proxy
- M-SC02 companies archived partial index
- M-SC04 audit_logs.searchText documented as DB-managed
- M-S01 storage_s3_access_key_encrypted admin field
- M-U01 audit log empty state uses <EmptyState>
- M-U09 invoice delete dialog -> <AlertDialog>
- M-U10 toast.success on ClientForm + InterestForm create/edit
- M-U11 settings-form-card logo preview alt text
- M-U14 mobile topbar title on clients/yachts/interests/berths
- M-U15 Invoices in mobile More-sheet
LOW (6/8):
- L-AU01 severity defaults for security-relevant verbs
- L-AU02 +13 missing actions in admin audit filter
- L-AU03 +7 missing entity types in admin audit filter
- L-AU04 dead listAuditLogs stubbed
- L-D02 CLAUDE.md Owner-wins chain tightened
Bonus — Document detail polish (#67 partial, 3/6 deliverables):
- state-aware action button per signer
- watcher Add UI with display-name resolution
- cleanSignerName cleanup
Prior session work bundled in:
- Documenso v2 webhook + envelope-ID normalization + sequential signing
- SigningProgress UI redesign (avatars, per-signer state, timestamps)
- env->admin settings registry + RegistryDrivenForm + encrypted creds
- Embedded-signing card + Test connection + setup help
- Dev-mode EMAIL_REDIRECT_TO banner
- Pipeline rules admin page
- Sales email config card
- Audit log details Sheet
- EOI tab: Finalising badge, absolute timestamps, sequential indicator
- Notes pipeline_stage_at_creation (migration 0069)
- Documenso numeric ID dual-key webhook (migration 0068)
- Dimensions criterion copy (migration 0067)
Tests: 1374/1374 vitest pass. tsc clean. lint clean.
See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
|
|
|
{interest.clientId ? (
|
|
|
|
|
<Button
|
|
|
|
|
asChild
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-8 gap-1.5 px-2.5 [&_svg]:size-3.5"
|
|
|
|
|
>
|
|
|
|
|
<Link
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
href={`/${portSlug}/clients/${interest.clientId}` as any}
|
|
|
|
|
aria-label="Open client page"
|
|
|
|
|
>
|
|
|
|
|
<User />
|
|
|
|
|
Client page
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
) : null}
|
2026-05-02 03:33:13 +02:00
|
|
|
{interest.clientPrimaryEmail ? (
|
|
|
|
|
<Button
|
|
|
|
|
asChild
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-8 gap-1.5 px-2.5 [&_svg]:size-3.5"
|
|
|
|
|
>
|
|
|
|
|
<a
|
|
|
|
|
href={`mailto:${interest.clientPrimaryEmail}`}
|
|
|
|
|
aria-label={`Email ${interest.clientPrimaryEmail}`}
|
|
|
|
|
>
|
|
|
|
|
<Mail />
|
|
|
|
|
Email
|
|
|
|
|
</a>
|
|
|
|
|
</Button>
|
|
|
|
|
) : null}
|
|
|
|
|
{interest.clientPrimaryPhone ? (
|
|
|
|
|
<Button
|
|
|
|
|
asChild
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-8 gap-1.5 px-2.5 [&_svg]:size-3.5"
|
|
|
|
|
>
|
|
|
|
|
<a
|
|
|
|
|
href={`tel:${interest.clientPrimaryPhone}`}
|
|
|
|
|
aria-label={`Call ${interest.clientPrimaryPhone}`}
|
|
|
|
|
>
|
|
|
|
|
<Phone />
|
|
|
|
|
Call
|
|
|
|
|
</a>
|
|
|
|
|
</Button>
|
|
|
|
|
) : null}
|
|
|
|
|
{whatsappNumber ? (
|
|
|
|
|
<Button
|
|
|
|
|
asChild
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-8 gap-1.5 px-2.5 [&_svg]:size-3.5"
|
|
|
|
|
>
|
|
|
|
|
<a
|
|
|
|
|
href={`https://wa.me/${whatsappNumber}`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
aria-label={`Message on WhatsApp`}
|
|
|
|
|
>
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
<WhatsAppIcon className="h-4 w-4" />
|
2026-05-02 03:33:13 +02:00
|
|
|
WhatsApp
|
|
|
|
|
</a>
|
|
|
|
|
</Button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
|
|
|
|
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
{/* Top-right actions. Won/Lost are sales-critical and read as text
|
|
|
|
|
buttons on desktop; Edit/Archive stay icon-only. On mobile,
|
|
|
|
|
Won/Lost shrink to icon buttons to keep the cluster from
|
|
|
|
|
wrapping. */}
|
|
|
|
|
<div className="flex shrink-0 items-center gap-1">
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
<PermissionGate resource="interests" action="change_stage">
|
2026-05-02 00:01:33 +02:00
|
|
|
{isClosed ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => reopenMutation.mutate()}
|
|
|
|
|
disabled={reopenMutation.isPending}
|
|
|
|
|
className={cn(
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
'inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1 text-xs font-medium text-foreground transition-colors',
|
|
|
|
|
'hover:bg-foreground/5 disabled:opacity-50',
|
2026-05-02 00:01:33 +02:00
|
|
|
)}
|
|
|
|
|
>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<RefreshCcw className="size-3.5" aria-hidden />
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
Reopen
|
2026-05-02 00:01:33 +02:00
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
fix(ux): pass-2 audit fixes — admin grouping, Duplicates entry, header tooltips
Three small but high-leverage fixes from the second audit pass on main:
Admin index (src/app/(dashboard)/[portSlug]/admin/page.tsx):
- Grouped 21 sections into 7 categories: Access, Configuration, Content,
Data Quality, Operations, Tenancy, Integrations. Each group has a
one-line description so first-time admins can orient themselves
without reading every card.
- Added the missing Duplicates entry (links to /admin/duplicates from
the dedup-migration work) under Data Quality.
More sheet (mobile bottom-drawer nav):
- "Email" -> "Inbox". The page that opens is an email-inbox surface
(Inbox + Accounts tabs), not a generic email composer. The previous
label was ambiguous.
Interest detail header (Won / Lost outcome buttons):
- Added title="Mark as won" / "Close as lost" so the icon-only buttons
on mobile have a tooltip on long-press / desktop hover.
- Tightened mobile padding (px-2 vs px-2.5) so the full-text desktop
labels still fit on sm+ without re-introducing a regression where a
visible mobile "Won"/"Lost" inline label crowded the right cluster
enough to push Email/Call/WhatsApp action chips into a vertical
stack.
Verification: 0 tsc errors, 926/926 vitest passing, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:35:32 +02:00
|
|
|
{/* Mobile: icon-only with title tooltip + colored fill carries
|
|
|
|
|
the won/lost meaning (green vs rose). Adding a "Won" /
|
|
|
|
|
"Lost" text label inline blew out the cluster width and
|
|
|
|
|
forced the Email/Call/WhatsApp action-chip row above to
|
2026-05-04 22:57:01 +02:00
|
|
|
stack vertically - bad trade. From sm up, the full
|
fix(ux): pass-2 audit fixes — admin grouping, Duplicates entry, header tooltips
Three small but high-leverage fixes from the second audit pass on main:
Admin index (src/app/(dashboard)/[portSlug]/admin/page.tsx):
- Grouped 21 sections into 7 categories: Access, Configuration, Content,
Data Quality, Operations, Tenancy, Integrations. Each group has a
one-line description so first-time admins can orient themselves
without reading every card.
- Added the missing Duplicates entry (links to /admin/duplicates from
the dedup-migration work) under Data Quality.
More sheet (mobile bottom-drawer nav):
- "Email" -> "Inbox". The page that opens is an email-inbox surface
(Inbox + Accounts tabs), not a generic email composer. The previous
label was ambiguous.
Interest detail header (Won / Lost outcome buttons):
- Added title="Mark as won" / "Close as lost" so the icon-only buttons
on mobile have a tooltip on long-press / desktop hover.
- Tightened mobile padding (px-2 vs px-2.5) so the full-text desktop
labels still fit on sm+ without re-introducing a regression where a
visible mobile "Won"/"Lost" inline label crowded the right cluster
enough to push Email/Call/WhatsApp action chips into a vertical
stack.
Verification: 0 tsc errors, 926/926 vitest passing, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:35:32 +02:00
|
|
|
"Mark won" / "Close as lost" labels read clearly. */}
|
2026-05-02 00:01:33 +02:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setOutcomeDialog('won')}
|
|
|
|
|
aria-label="Mark as won"
|
fix(ux): pass-2 audit fixes — admin grouping, Duplicates entry, header tooltips
Three small but high-leverage fixes from the second audit pass on main:
Admin index (src/app/(dashboard)/[portSlug]/admin/page.tsx):
- Grouped 21 sections into 7 categories: Access, Configuration, Content,
Data Quality, Operations, Tenancy, Integrations. Each group has a
one-line description so first-time admins can orient themselves
without reading every card.
- Added the missing Duplicates entry (links to /admin/duplicates from
the dedup-migration work) under Data Quality.
More sheet (mobile bottom-drawer nav):
- "Email" -> "Inbox". The page that opens is an email-inbox surface
(Inbox + Accounts tabs), not a generic email composer. The previous
label was ambiguous.
Interest detail header (Won / Lost outcome buttons):
- Added title="Mark as won" / "Close as lost" so the icon-only buttons
on mobile have a tooltip on long-press / desktop hover.
- Tightened mobile padding (px-2 vs px-2.5) so the full-text desktop
labels still fit on sm+ without re-introducing a regression where a
visible mobile "Won"/"Lost" inline label crowded the right cluster
enough to push Email/Call/WhatsApp action chips into a vertical
stack.
Verification: 0 tsc errors, 926/926 vitest passing, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:35:32 +02:00
|
|
|
title="Mark as won"
|
2026-05-02 00:01:33 +02:00
|
|
|
className={cn(
|
fix(ux): pass-2 audit fixes — admin grouping, Duplicates entry, header tooltips
Three small but high-leverage fixes from the second audit pass on main:
Admin index (src/app/(dashboard)/[portSlug]/admin/page.tsx):
- Grouped 21 sections into 7 categories: Access, Configuration, Content,
Data Quality, Operations, Tenancy, Integrations. Each group has a
one-line description so first-time admins can orient themselves
without reading every card.
- Added the missing Duplicates entry (links to /admin/duplicates from
the dedup-migration work) under Data Quality.
More sheet (mobile bottom-drawer nav):
- "Email" -> "Inbox". The page that opens is an email-inbox surface
(Inbox + Accounts tabs), not a generic email composer. The previous
label was ambiguous.
Interest detail header (Won / Lost outcome buttons):
- Added title="Mark as won" / "Close as lost" so the icon-only buttons
on mobile have a tooltip on long-press / desktop hover.
- Tightened mobile padding (px-2 vs px-2.5) so the full-text desktop
labels still fit on sm+ without re-introducing a regression where a
visible mobile "Won"/"Lost" inline label crowded the right cluster
enough to push Email/Call/WhatsApp action chips into a vertical
stack.
Verification: 0 tsc errors, 926/926 vitest passing, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:35:32 +02:00
|
|
|
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-2.5',
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
'border border-emerald-200 bg-emerald-50 text-emerald-700',
|
|
|
|
|
'hover:bg-emerald-100',
|
2026-05-02 00:01:33 +02:00
|
|
|
)}
|
|
|
|
|
>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Trophy className="size-3.5" aria-hidden />
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
<span className="hidden sm:inline">Mark won</span>
|
2026-05-02 00:01:33 +02:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setOutcomeDialog('lost')}
|
|
|
|
|
aria-label="Close as lost"
|
fix(ux): pass-2 audit fixes — admin grouping, Duplicates entry, header tooltips
Three small but high-leverage fixes from the second audit pass on main:
Admin index (src/app/(dashboard)/[portSlug]/admin/page.tsx):
- Grouped 21 sections into 7 categories: Access, Configuration, Content,
Data Quality, Operations, Tenancy, Integrations. Each group has a
one-line description so first-time admins can orient themselves
without reading every card.
- Added the missing Duplicates entry (links to /admin/duplicates from
the dedup-migration work) under Data Quality.
More sheet (mobile bottom-drawer nav):
- "Email" -> "Inbox". The page that opens is an email-inbox surface
(Inbox + Accounts tabs), not a generic email composer. The previous
label was ambiguous.
Interest detail header (Won / Lost outcome buttons):
- Added title="Mark as won" / "Close as lost" so the icon-only buttons
on mobile have a tooltip on long-press / desktop hover.
- Tightened mobile padding (px-2 vs px-2.5) so the full-text desktop
labels still fit on sm+ without re-introducing a regression where a
visible mobile "Won"/"Lost" inline label crowded the right cluster
enough to push Email/Call/WhatsApp action chips into a vertical
stack.
Verification: 0 tsc errors, 926/926 vitest passing, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:35:32 +02:00
|
|
|
title="Close as lost"
|
2026-05-02 00:01:33 +02:00
|
|
|
className={cn(
|
fix(ux): pass-2 audit fixes — admin grouping, Duplicates entry, header tooltips
Three small but high-leverage fixes from the second audit pass on main:
Admin index (src/app/(dashboard)/[portSlug]/admin/page.tsx):
- Grouped 21 sections into 7 categories: Access, Configuration, Content,
Data Quality, Operations, Tenancy, Integrations. Each group has a
one-line description so first-time admins can orient themselves
without reading every card.
- Added the missing Duplicates entry (links to /admin/duplicates from
the dedup-migration work) under Data Quality.
More sheet (mobile bottom-drawer nav):
- "Email" -> "Inbox". The page that opens is an email-inbox surface
(Inbox + Accounts tabs), not a generic email composer. The previous
label was ambiguous.
Interest detail header (Won / Lost outcome buttons):
- Added title="Mark as won" / "Close as lost" so the icon-only buttons
on mobile have a tooltip on long-press / desktop hover.
- Tightened mobile padding (px-2 vs px-2.5) so the full-text desktop
labels still fit on sm+ without re-introducing a regression where a
visible mobile "Won"/"Lost" inline label crowded the right cluster
enough to push Email/Call/WhatsApp action chips into a vertical
stack.
Verification: 0 tsc errors, 926/926 vitest passing, lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:35:32 +02:00
|
|
|
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors sm:px-2.5',
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
'border border-rose-200 text-rose-700',
|
|
|
|
|
'hover:bg-rose-50',
|
2026-05-02 00:01:33 +02:00
|
|
|
)}
|
|
|
|
|
>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<XCircle className="size-3.5" aria-hidden />
|
fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.
Bugs:
- dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
sm:top-[50%]) were being silently stripped by tailwind-merge because
the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
sm:right-auto sm:bottom-auto). Every Dialog instance now centers
correctly on desktop. (Affected 16 dialog consumers.)
- interest-documents-tab.tsx — useQuery shared the queryKey
['interests', interestId] with the parent InterestDetail's query but
returned a different shape ({ data: ... } envelope vs unwrapped).
They clobbered each other's cache on tab mount, degenerating the
parent header to "Unknown Client" / "Open" briefly. Unified the
queryFn shape so the cache stays consistent.
- interest-tabs.tsx — milestone steps now derive done-state from
PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
well as from the date stamp. Stage truth > date truth. Seeded /
imported interests that arrived past `open` without per-step dates
now correctly show their milestone steps as checked.
- interest-detail.tsx — wires useMobileChrome so the mobile topbar
shows the client name instead of the interest UUID.
- interest-documents-tab.tsx — empty state restructured to a centered
"No documents yet — Generate EOI" CTA card instead of a small
primary button floating in the corner.
- timeline/route.ts — synthesizes a "Created at <stage>" event when
no audit-log rows exist for the interest, so the Activity tab
isn't empty for seeded interests.
- lead-source-chart.tsx — pie radii switched from fixed 90px/50px
to "70%"/"40%" so the pie scales with the container instead of
being clipped at narrow widths; reserved 40px for the legend.
Visual / clarity:
- interest-detail-header.tsx — Won/Lost rendered as branded text
buttons on desktop ("Mark won", "Close as lost") and icon-only on
mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
promoted to a labeled button when the interest is closed. Added
"Last contact Xd ago" to the meta row.
- detail-header-strip.tsx — py-4 → py-3 (tighter strip).
- interest-tabs.tsx — milestone cards: the next pending milestone
gets a brand-blue ring + "NEXT" pill so the user can see at a
glance which lifecycle to act on. Its primary action gets the
filled button variant.
- interest-tabs.tsx — Deposit milestone: invoice flow promoted to
primary CTA ("Create deposit invoice"), manual stage advance
demoted to a small text link ("Mark received manually"). Reflects
the actual recommended path now that recordPayment auto-advances
on payment.
- inline-editable-field.tsx — pencil affordance shown faintly
(opacity-20) at rest so users discover that fields are editable
without having to hover-test every label. Lifts to opacity-60 on
hover.
- constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
via useIsMobile, so the rotated 9-stage axis isn't a wall of
overlap on a 393px screen.
- client-pipeline-summary.tsx — StageStepper rebuilt as a single
segmented progress bar instead of 9 micro-dots + connectors that
rendered inconsistently at tight widths. Each stage is an equal
slice that lights up as the interest reaches it; tooltips on hover
give the full stage name. Also dropped a pre-existing dead `br`
variable.
- dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
Funnel, and Recent Activity now have helpful descriptions explaining
what populates them, instead of bare "No interests in range".
- use-paginated-query.ts — reuses `&` when the endpoint already has
`?`, so callers like the documents hub don't generate
`…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
rejected as 400). Caught while testing the now-removed EOI route
but applies broadly.
tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
|
|
|
<span className="hidden sm:inline">Close as lost</span>
|
2026-05-02 00:01:33 +02:00
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</PermissionGate>
|
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>
2026-05-07 20:59:28 +02:00
|
|
|
{/* The "Upload paper-signed EOI" button used to live here.
|
|
|
|
|
It's now on the dedicated EOI tab (in both the active-EOI
|
|
|
|
|
hero and the empty-state CTA row), where it sits next to
|
|
|
|
|
the document it relates to. The header was a shotgun of
|
|
|
|
|
actions that didn't all belong; collecting them per-tab
|
|
|
|
|
is the cleaner UX. */}
|
2026-05-02 00:01:33 +02:00
|
|
|
<PermissionGate resource="interests" action="edit">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setEditOpen(true)}
|
|
|
|
|
aria-label="Edit interest"
|
|
|
|
|
title="Edit interest"
|
|
|
|
|
className={cn(
|
|
|
|
|
'rounded-md p-1.5 text-muted-foreground/70 transition-colors',
|
|
|
|
|
'hover:bg-foreground/5 hover:text-foreground',
|
|
|
|
|
)}
|
|
|
|
|
>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Pencil className="size-4" aria-hidden />
|
2026-05-02 00:01:33 +02:00
|
|
|
</button>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</PermissionGate>
|
|
|
|
|
<PermissionGate resource="interests" action="delete">
|
2026-05-02 00:01:33 +02:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setArchiveOpen(true)}
|
|
|
|
|
aria-label={isArchived ? 'Restore interest' : 'Archive interest'}
|
|
|
|
|
title={isArchived ? 'Restore interest' : 'Archive interest'}
|
|
|
|
|
className={cn(
|
|
|
|
|
'rounded-md p-1.5 text-muted-foreground/70 transition-colors',
|
|
|
|
|
'hover:bg-foreground/5',
|
|
|
|
|
isArchived ? 'hover:text-foreground' : 'hover:text-destructive',
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
2026-05-02 00:01:33 +02:00
|
|
|
>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
{isArchived ? (
|
|
|
|
|
<RotateCcw className="size-4" aria-hidden />
|
|
|
|
|
) : (
|
|
|
|
|
<Archive className="size-4" aria-hidden />
|
|
|
|
|
)}
|
2026-05-02 00:01:33 +02:00
|
|
|
</button>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</PermissionGate>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-28 12:09:47 +02:00
|
|
|
</DetailHeaderStrip>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
2026-05-02 00:01:33 +02:00
|
|
|
{outcomeDialog && (
|
|
|
|
|
<InterestOutcomeDialog
|
|
|
|
|
interestId={interest.id}
|
|
|
|
|
mode={outcomeDialog}
|
|
|
|
|
open={outcomeDialog !== null}
|
|
|
|
|
onOpenChange={(open) => !open && setOutcomeDialog(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
<InterestForm
|
|
|
|
|
open={editOpen}
|
|
|
|
|
onOpenChange={setEditOpen}
|
2026-03-26 12:06:18 +01:00
|
|
|
interest={interest as unknown as Parameters<typeof InterestForm>[0]['interest']}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<ArchiveConfirmDialog
|
|
|
|
|
open={archiveOpen}
|
|
|
|
|
onOpenChange={setArchiveOpen}
|
|
|
|
|
entityName={interest.clientName ?? 'Interest'}
|
|
|
|
|
entityType="Interest"
|
|
|
|
|
isArchived={isArchived}
|
|
|
|
|
onConfirm={() => {
|
|
|
|
|
if (isArchived) {
|
|
|
|
|
restoreMutation.mutate();
|
|
|
|
|
} else {
|
|
|
|
|
archiveMutation.mutate();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
isLoading={archiveMutation.isPending || restoreMutation.isPending}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|