Commit Graph

8 Commits

Author SHA1 Message Date
Matt Ciaccio
b4776b4c3c feat(interests): linked berths list with role-flag toggles + EOI bypass
Implements plan §5.5: a per-interest "Linked berths" panel mounted above the
recommender on the interest detail Overview tab. Each junction row exposes
the role-flag controls reps need to manage the M:M `interest_berths` link
without the legacy single-berth flow.

UI (`src/components/interests/linked-berths-list.tsx`)

* Rows ordered with primary first; mooring number links to /berths/[id], with
  area + a status pill (available/under_offer/sold) and a "Primary" chip.
* "Specifically pitching" Switch (writes `is_specific_interest`) with the
  consequence text from §1: "This berth will appear as under interest on the
  public map" / "This berth is hidden from the public map".
* "Mark in EOI bundle" Switch (writes `is_in_eoi_bundle`).
* "Set as primary" button when the row isn't primary - the existing
  `upsertInterestBerth` helper demotes the prior primary in the same tx.
* "Bypass EOI for this berth" with reason textarea, ONLY rendered when the
  parent interest's `eoiStatus === 'signed'`. Writes the bypass triple
  (`eoi_bypass_reason`, `eoi_bypassed_by` = caller, `eoi_bypassed_at` = now);
  also supports clearing.
* Remove-from-interest action gated by a confirmation dialog.

API (`src/app/api/v1/interests/[id]/berths/...`)

* `GET /` - list endpoint returning `listBerthsForInterest` plus the parent
  interest's `eoiStatus` in `meta.eoiStatus` so the UI can decide whether to
  show the bypass control.
* `PATCH /[berthId]` - partial update of the junction row's flags + bypass
  fields. Server-side guard: rejects bypass writes when `eoiStatus !==
  'signed'` (defence in depth - never trust the UI to gate this).
* `DELETE /[berthId]` - calls `removeInterestBerth`.
* The existing POST stays unchanged. All routes wrapped with
  `withAuth(withPermission('interests', view|edit, ...))`. portId from ctx;
  cross-port reads/writes return 404 for enumeration prevention (§14.10).

Service changes (`src/lib/services/interest-berths.service.ts`)

* `upsertInterestBerth` now accepts `eoiBypassReason` (tri-state: omit = no
  change, non-empty = record, null = clear) and `eoiBypassedBy`. The bypass
  triple moves as a unit, with `eoi_bypassed_at` stamped server-side.
* `listBerthsForInterest` now returns berth detail (area, status, dimensions)
  alongside the junction row, typed as `InterestBerthWithDetails`.

Socket: added `interest:berthLinkUpdated` event for live UI refreshes.

Tests: 18 new integration tests in `tests/integration/api/interest-berths.test.ts`
covering happy paths, primary-demotion in same tx, bypass write/clear, the
"requires signed EOI" guard, cross-port 404s, missing-link 404s, empty-body
400, and viewer 403 through the permission gate.
2026-05-05 04:01:56 +02:00
Matt Ciaccio
15d4849030 feat(recommender): API endpoint + interest-detail panel + add-to-interest dialog 2026-05-05 03:05:22 +02:00
Matt Ciaccio
8699f81879 chore(style): codebase em-dash sweep + minor layout polish
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped
Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).

Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
  pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
  port switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:57:01 +02:00
Matt Ciaccio
57a099acc4 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
Matt Ciaccio
76a7387dcc 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
Matt Ciaccio
dbbd03fd22 feat(sales): admin-configurable EOI signers + richer timeline events
1. Per-port EOI signer config

     - New `eoi_signers` system_settings key (JSON: { developer, approver },
       each `{ name, email }`). Settings UI exposes it under Admin → Settings.
     - getPortEoiSigners(portId) reads the setting with a typed validator;
       falls back to the legacy David Mizrahi / Abbie May defaults if the
       row is missing or malformed (so older ports keep working until an
       admin saves a value).
     - Both EOI generation pathways now read from the helper instead of
       hardcoded constants:
         * documenso-template path (generateAndSignViaDocumensoTemplate)
         * in-app PDF-fill path (generateAndSignViaInApp)

  2. Timeline upgrades

     The interest detail Activity tab now distinguishes the new automation
     events that arrived with sessions 1+2:

     - Stage auto-advances (userId='system') get a small "Auto" pill and
       carry their reason into the description (e.g. "Stage advanced to
       EOI Signed (auto-advanced — EOI signed via Documenso)").
     - outcome_set events show "Marked as Won" / "Marked as Lost — went
       to another marina" with optional reason; trophy/X icons.
     - outcome_cleared events show "Reopened to {stage}" with a refresh
       icon.
     - Document events humanized: "Document 'X' fully signed" instead
       of "Document X: completed".
     - Stage labels run through stageLabel() so the timeline shows the
       human label, not the enum key.
     - Timestamps switched to relative-time with full-date tooltip.
     - "by system" is rendered plainly (no longer the literal user-id).

tsc clean. vitest 832/832 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
Matt Ciaccio
ba5fb6db5e feat(sales): EOI queue route + invoice→deposit auto-advance + won/lost outcomes
Three independent strengthenings of the sales spine that the prior coherence
sweep made it possible to do cleanly.

  1. EOI queue page

     - Sidebar entry under Documents → "EOI queue".
     - Route /[port]/documents/eoi renders DocumentsHub with the existing
       eoi_queue tab pre-selected (filters in-flight EOIs only).
     - .gitignore: tightened root-only `eoi/` ignore so the documents/eoi
       route is no longer silently excluded.

  2. Invoice ↔ deposit link

     - invoices.interestId (FK, ON DELETE SET NULL) + invoices.kind
       ('general' | 'deposit'). Indexed on (port_id, interest_id).
     - createInvoiceSchema requires interestId when kind === 'deposit';
       the service validates the linked interest belongs to the same port
       before insert.
     - recordPayment auto-advances pipelineStage to deposit_10pct (via
       advanceStageIfBehind) when a paid invoice is kind=deposit and has
       an interestId. No-op if the interest is already further along.
     - "Create deposit invoice" link added to the Deposit milestone on the
       interest detail. Links to /invoices/new?interestId=…&kind=deposit;
       the form prefills the billing entity from the linked interest's
       client and shows a context banner.

  3. Won / lost terminal outcomes

     - interests.outcome ('won' | 'lost_other_marina' | 'lost_unqualified'
       | 'lost_no_response' | 'cancelled') + outcomeReason text +
       outcomeAt timestamp. Indexed on (port_id, outcome).
     - setInterestOutcome / clearInterestOutcome services + POST/DELETE
       /api/v1/interests/:id/outcome endpoints (gated by change_stage
       permission). Setting an outcome moves the interest to `completed`
       in the same write; clearing reopens to `in_communication` (or a
       caller-specified stage).
     - Mark Won / Mark Lost icon buttons on the interest detail header,
       plus an outcome badge that replaces the stage pill once a terminal
       outcome is set, plus a Reopen button.
     - Funnel + dashboard math updated to exclude lost/cancelled outcomes
       from active calculations (KPIs.activeInterests, pipelineValueUsd,
       getPipelineCounts, computePipelineFunnel, getRevenueForecast).
       The funnel now also returns a `lost` summary so callers can
       surface leakage without polluting conversion percentages.

Schema changes shipped via 0019_lazy_vampiro.sql; applied to dev DB
manually via psql because drizzle-kit push hits a pre-existing zod
parsing issue on the companies index. Dev server may need a restart
to flush prepared-statement caches.

tsc clean. vitest 832/832 pass. ESLint clean on every file touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:01:33 +02:00
67d7e6e3d5 Initial commit: Port Nimara CRM (Layers 0-4)
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled
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