2026-05-05 02:22:11 +02:00
|
|
|
import {
|
|
|
|
|
pgTable,
|
|
|
|
|
text,
|
|
|
|
|
boolean,
|
|
|
|
|
integer,
|
|
|
|
|
numeric,
|
|
|
|
|
timestamp,
|
|
|
|
|
primaryKey,
|
|
|
|
|
index,
|
|
|
|
|
uniqueIndex,
|
|
|
|
|
} from 'drizzle-orm/pg-core';
|
|
|
|
|
import { sql } from 'drizzle-orm';
|
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 { ports } from './ports';
|
|
|
|
|
import { clients } from './clients';
|
2026-05-05 02:22:11 +02:00
|
|
|
import { berths } from './berths';
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
import { yachts } from './yachts';
|
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
|
|
|
// Pipeline stages: open, details_sent, in_communication, eoi_sent, eoi_signed, deposit_10pct, contract_sent, contract_signed, completed
|
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 const interests = pgTable(
|
|
|
|
|
'interests',
|
|
|
|
|
{
|
2026-04-23 17:57:29 +02:00
|
|
|
id: text('id')
|
|
|
|
|
.primaryKey()
|
|
|
|
|
.$defaultFn(() => crypto.randomUUID()),
|
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
|
|
|
portId: text('port_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => ports.id),
|
|
|
|
|
clientId: text('client_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => clients.id),
|
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings:
* fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps
Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can
no longer pin a worker concurrency slot indefinitely. OpenAI client
passes timeout: 30_000. ImapFlow gets socket / greeting / connection
timeouts.
* SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP,
closes Socket.io, and disconnects Redis before exit; compose
stop_grace_period bumped to 30s. Adds closeSocketServer() helper.
* env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and
filesystem.ts now reads from env (a typo can no longer silently
disable the multi-node guard).
* Per-port Documenso template + recipient IDs land in system_settings
with env fallback (PortDocumensoConfig now exposes eoiTemplateId,
clientRecipientId, developerRecipientId, approvalRecipientId).
document-templates.ts uses the per-port config and threads portId
into documensoGenerateFromTemplate().
* Migration 0042 wires the eleven HIGH-tier missing FK constraints
(documents/files/interests/reminders/berth_waiting_list/
form_submissions) plus polymorphic CHECK round 2
(yacht_ownership_history.owner_type, document_sends.document_kind),
invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK.
Drizzle schema columns updated to .references(...) where possible
so the misleading "FK wired in relations.ts" comments are gone.
Test status: 1168/1168 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 +
MED §§14,15,16,18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:52:58 +02:00
|
|
|
yachtId: text('yacht_id').references(() => yachts.id, { onDelete: 'set 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: text('pipeline_stage').notNull().default('open'),
|
|
|
|
|
leadCategory: text('lead_category'), // general_interest, specific_qualified, hot_lead
|
|
|
|
|
source: text('source'), // website, manual, referral, broker
|
|
|
|
|
eoiStatus: text('eoi_status'), // null, waiting_for_signatures, signed, expired
|
|
|
|
|
documensoId: text('documenso_id'),
|
|
|
|
|
contractStatus: text('contract_status'),
|
|
|
|
|
depositStatus: text('deposit_status'),
|
|
|
|
|
reservationStatus: text('reservation_status'),
|
|
|
|
|
dateFirstContact: timestamp('date_first_contact', { withTimezone: true }),
|
|
|
|
|
dateLastContact: timestamp('date_last_contact', { withTimezone: true }),
|
|
|
|
|
dateEoiSent: timestamp('date_eoi_sent', { withTimezone: true }),
|
|
|
|
|
dateEoiSigned: timestamp('date_eoi_signed', { withTimezone: true }),
|
|
|
|
|
dateContractSent: timestamp('date_contract_sent', { withTimezone: true }),
|
|
|
|
|
dateContractSigned: timestamp('date_contract_signed', { withTimezone: true }),
|
|
|
|
|
dateDepositReceived: timestamp('date_deposit_received', { withTimezone: true }),
|
|
|
|
|
reminderEnabled: boolean('reminder_enabled').notNull().default(false),
|
|
|
|
|
reminderDays: integer('reminder_days'),
|
|
|
|
|
reminderLastFired: timestamp('reminder_last_fired', { withTimezone: true }),
|
2026-05-04 22:57:01 +02:00
|
|
|
/** Terminal outcome. Independent of pipelineStage - `outcome` is set
|
2026-05-02 00:01:33 +02:00
|
|
|
* alongside the stage transition to `completed` to distinguish won
|
|
|
|
|
* deals from the various lost variants. NULL while the interest is
|
|
|
|
|
* still active. */
|
|
|
|
|
outcome: text('outcome'), // 'won' | 'lost_other_marina' | 'lost_unqualified' | 'lost_no_response' | 'cancelled'
|
|
|
|
|
/** Free-text reason captured at the time the outcome is set. Surfaces
|
|
|
|
|
* in the timeline + reports. */
|
|
|
|
|
outcomeReason: text('outcome_reason'),
|
|
|
|
|
/** When the outcome was decided. Lets us age 'how long ago did we lose'. */
|
|
|
|
|
outcomeAt: timestamp('outcome_at', { withTimezone: true }),
|
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units
Berth surfaces
- New compact mooring-chip header (colored plate + status pill, dock-label
in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack
- Berth list gains a "Latest deal stage" column showing the most-advanced
pipeline stage of any active linked interest (server-aggregated, ranks by
PIPELINE_STAGES index)
- "Linked prospect" Select on the status-change dialog rebuilt as a Command
combobox: search, recent-first sort, stage-coloured pills
Pipeline UX
- Reverting an interest to Open with linked berths now prompts: keep the
links, unlink and reset, or cancel. Silent when no berths are linked
- Activity feed + entity-activity feed normalise enum field values via
STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as
"10% Deposit → Contract Sent"
EOI generate dialog
- Inline-editable rows for client name, nationality (country combobox), and
yacht name — pencil affordance saves directly via clients/yachts PATCH
- Replaces the single "Edit on client's page" link with two contextual links
framed by short copy explaining what's inline vs what needs the canonical
page
- Backend EoiContext now includes client.id + yacht.id so the dialog can
PATCH without an extra round-trip
Company form
- New "Connections" section lets the rep attach members (clients) and yachts
during create. Yacht attach uses the existing transfer endpoint so audit
log + ownership history capture the change
- Inline "+ New client" / "+ New yacht" buttons open the canonical forms
stacked over the company sheet
- After save, the form chains to a yacht pull-in prompt (if any attached
client owns yachts not yet linked) and an optional "Create interest" step
pre-filled with the first attached client
Admin
- /admin landing gains a searchable index — typed query flattens groups into
a result list matching label + description + group title
- "Documenso & EOI" card relabelled to "EOI signing service" (consistent
with the user-facing language rename from round 1)
Measurement units (migration 0053)
- interests gains desired_*_m columns + desired_*_unit discriminators so
the rep's literal entry (ft OR m) is preserved verbatim instead of being
reconstructed from a single canonical column on every render
- yachts + berths gain matching *_unit columns alongside their existing
ft + m pairs; defaults to 'ft' so legacy rows still render normally
- Interest form POST/PATCH now sends both ft + m + unit; computed m is
derived from the ft canonical to keep the recommender SQL unchanged
Misc
- Active-deals tile + topbar type their Link href as `Route` instead of `any`
- Unused REPORT_TYPE_LABELS const dropped from generate-report-form
- Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated
to include the new id + unit fields on the EoiContext / Berth shapes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:28:22 +02:00
|
|
|
/** Recommender inputs - dual-stored. ft is the canonical unit the
|
|
|
|
|
* recommender SQL queries on; m is the human-friendly entry the rep
|
|
|
|
|
* may have actually typed. The matching `*_unit` column says which
|
|
|
|
|
* side is source-of-truth — display prefers that side and recomputes
|
|
|
|
|
* the other so the rep's literal entry doesn't drift through repeated
|
|
|
|
|
* conversions. Resolver treats nulls as "no constraint" on that axis. */
|
2026-05-05 02:22:11 +02:00
|
|
|
desiredLengthFt: numeric('desired_length_ft'),
|
|
|
|
|
desiredWidthFt: numeric('desired_width_ft'),
|
|
|
|
|
desiredDraftFt: numeric('desired_draft_ft'),
|
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units
Berth surfaces
- New compact mooring-chip header (colored plate + status pill, dock-label
in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack
- Berth list gains a "Latest deal stage" column showing the most-advanced
pipeline stage of any active linked interest (server-aggregated, ranks by
PIPELINE_STAGES index)
- "Linked prospect" Select on the status-change dialog rebuilt as a Command
combobox: search, recent-first sort, stage-coloured pills
Pipeline UX
- Reverting an interest to Open with linked berths now prompts: keep the
links, unlink and reset, or cancel. Silent when no berths are linked
- Activity feed + entity-activity feed normalise enum field values via
STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as
"10% Deposit → Contract Sent"
EOI generate dialog
- Inline-editable rows for client name, nationality (country combobox), and
yacht name — pencil affordance saves directly via clients/yachts PATCH
- Replaces the single "Edit on client's page" link with two contextual links
framed by short copy explaining what's inline vs what needs the canonical
page
- Backend EoiContext now includes client.id + yacht.id so the dialog can
PATCH without an extra round-trip
Company form
- New "Connections" section lets the rep attach members (clients) and yachts
during create. Yacht attach uses the existing transfer endpoint so audit
log + ownership history capture the change
- Inline "+ New client" / "+ New yacht" buttons open the canonical forms
stacked over the company sheet
- After save, the form chains to a yacht pull-in prompt (if any attached
client owns yachts not yet linked) and an optional "Create interest" step
pre-filled with the first attached client
Admin
- /admin landing gains a searchable index — typed query flattens groups into
a result list matching label + description + group title
- "Documenso & EOI" card relabelled to "EOI signing service" (consistent
with the user-facing language rename from round 1)
Measurement units (migration 0053)
- interests gains desired_*_m columns + desired_*_unit discriminators so
the rep's literal entry (ft OR m) is preserved verbatim instead of being
reconstructed from a single canonical column on every render
- yachts + berths gain matching *_unit columns alongside their existing
ft + m pairs; defaults to 'ft' so legacy rows still render normally
- Interest form POST/PATCH now sends both ft + m + unit; computed m is
derived from the ft canonical to keep the recommender SQL unchanged
Misc
- Active-deals tile + topbar type their Link href as `Route` instead of `any`
- Unused REPORT_TYPE_LABELS const dropped from generate-report-form
- Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated
to include the new id + unit fields on the EoiContext / Berth shapes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:28:22 +02:00
|
|
|
desiredLengthM: numeric('desired_length_m'),
|
|
|
|
|
desiredWidthM: numeric('desired_width_m'),
|
|
|
|
|
desiredDraftM: numeric('desired_draft_m'),
|
|
|
|
|
desiredLengthUnit: text('desired_length_unit').notNull().default('ft'),
|
|
|
|
|
desiredWidthUnit: text('desired_width_unit').notNull().default('ft'),
|
|
|
|
|
desiredDraftUnit: text('desired_draft_unit').notNull().default('ft'),
|
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
|
|
|
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
|
|
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
index('idx_interests_port').on(table.portId),
|
|
|
|
|
index('idx_interests_client').on(table.clientId),
|
2026-04-23 17:57:29 +02:00
|
|
|
index('idx_interests_yacht').on(table.yachtId),
|
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
|
|
|
index('idx_interests_stage').on(table.portId, table.pipelineStage),
|
fix(audit): backlog sweep — partial archived indexes, custom-fields per-entity gate, polish
Wave through the 2026-05-07 backlog of small/concrete audit-final-deferred
items (deferring the Documenso Phases 2-7 build and items needing design
decisions or live external instances).
DB schema:
- Migration 0046 converts 5 composite (port_id, archived_at) indexes to
partial WHERE archived_at IS NULL — clients, interests, yachts, and
both residential tables. Smaller, faster planner choice for the
dominant list-query shape.
Multi-tenant isolation:
- document_sends now verifies recipient.interestId belongs to the port
before landing on the audit row (the surrounding clientId check was
already port-scoped; interestId pollution was the gap).
Routes / API:
- /api/v1/custom-fields/[entityId] requires entityType query param and
gates on the matching resource permission (clients/interests/berths/
yachts/companies). Fixes the cross-resource gap where a user with
clients.view could read company custom-field values.
- Admin user list trash button wrapped in PermissionGate (edit was
already gated; remove was not).
Service polish:
- berth-recommender accepts string-shaped JSONB booleans
('true'/'false') so admin UIs that wrap values as strings don't
silently fall through to defaults.
- expense-pdf renderReceiptHeader anchors all text positions to a
captured baseY rather than reading mutating doc.y after rect+stroke.
Headers no longer drift on the first receipt page after a soft page
break.
- berth-pdf apply: collect non-finite numeric coercion drops + warn-log
them so partial silent drops are observable (was invisible because
the no-fields-supplied check only fires when ALL drop).
- Storage cache fingerprint comment documenting the encrypted-secret
invariant + the explicit invalidation hook.
UI polish:
- invoice-detail typed: replaced two `any` casts with a proper
InvoiceDetailData / LineItem / LinkedExpense interface set.
- YachtForm now accepts initialOwner prop. Wired through:
- client-yachts-tab passes { type: 'client', id: clientId }
- interest-form passes { type: 'client', id: selectedClientId }
- Interest-form yacht picker now includes company-owned yachts where
the selected client is a member (fetches client.companies and feeds
YachtPicker an array filter). Plus an inline "Add new" button that
opens YachtForm pre-bound to the client.
- YachtPicker accepts ownerFilter as single OR array for "match any"
semantics.
BACKLOG.md updated with what landed vs what's still deferred (and why
each deferred item is genuinely larger than this push warrants).
Tests: 1185/1185 vitest, tsc clean.
2026-05-07 21:45:42 +02:00
|
|
|
index('idx_interests_archived')
|
|
|
|
|
.on(table.portId)
|
|
|
|
|
.where(sql`${table.archivedAt} IS NULL`),
|
2026-05-02 00:01:33 +02:00
|
|
|
index('idx_interests_outcome').on(table.portId, table.outcome),
|
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-05 02:22:11 +02:00
|
|
|
/**
|
|
|
|
|
* Many-to-many junction between interests and berths.
|
|
|
|
|
*
|
|
|
|
|
* Replaces the old single-berth `interests.berth_id` column. Each row
|
|
|
|
|
* carries three role flags so a rep can model "actively pitching this
|
|
|
|
|
* berth" vs "covered by the EOI bundle but not pitched" vs "primary
|
|
|
|
|
* berth for the deal" independently:
|
|
|
|
|
*
|
|
|
|
|
* - is_primary : at most one row per interest is the primary;
|
|
|
|
|
* templates / forms / "the berth for this deal"
|
|
|
|
|
* semantics resolve through this row.
|
|
|
|
|
* - is_specific_interest : true = berth shows as "Under Offer" on the
|
|
|
|
|
* public map. false = legal/EOI-only link.
|
|
|
|
|
* - is_in_eoi_bundle : covered by the interest's EOI signature.
|
|
|
|
|
*
|
|
|
|
|
* EOI bypass: when the interest has a signed primary EOI but a specific
|
|
|
|
|
* berth in the bundle still needs its own EOI, a rep records the bypass
|
|
|
|
|
* reason here.
|
|
|
|
|
*/
|
|
|
|
|
export const interestBerths = pgTable(
|
|
|
|
|
'interest_berths',
|
|
|
|
|
{
|
|
|
|
|
id: text('id')
|
|
|
|
|
.primaryKey()
|
|
|
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
|
|
|
interestId: text('interest_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => interests.id, { onDelete: 'cascade' }),
|
|
|
|
|
berthId: text('berth_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => berths.id, { onDelete: 'restrict' }),
|
|
|
|
|
isPrimary: boolean('is_primary').notNull().default(false),
|
|
|
|
|
isSpecificInterest: boolean('is_specific_interest').notNull().default(true),
|
|
|
|
|
isInEoiBundle: boolean('is_in_eoi_bundle').notNull().default(false),
|
|
|
|
|
eoiBypassReason: text('eoi_bypass_reason'),
|
|
|
|
|
eoiBypassedBy: text('eoi_bypassed_by'),
|
|
|
|
|
eoiBypassedAt: timestamp('eoi_bypassed_at', { withTimezone: true }),
|
|
|
|
|
addedBy: text('added_by'),
|
|
|
|
|
addedAt: timestamp('added_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
notes: text('notes'),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
uniqueIndex('idx_ib_interest_berth').on(table.interestId, table.berthId),
|
|
|
|
|
uniqueIndex('idx_ib_one_primary')
|
|
|
|
|
.on(table.interestId)
|
|
|
|
|
.where(sql`${table.isPrimary} = true`),
|
|
|
|
|
index('idx_ib_berth').on(table.berthId),
|
|
|
|
|
index('idx_ib_specific')
|
|
|
|
|
.on(table.berthId)
|
|
|
|
|
.where(sql`${table.isSpecificInterest} = true`),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
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 const interestNotes = pgTable(
|
|
|
|
|
'interest_notes',
|
|
|
|
|
{
|
2026-04-23 17:57:29 +02:00
|
|
|
id: text('id')
|
|
|
|
|
.primaryKey()
|
|
|
|
|
.$defaultFn(() => crypto.randomUUID()),
|
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
|
|
|
interestId: text('interest_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => interests.id, { onDelete: 'cascade' }),
|
|
|
|
|
authorId: text('author_id').notNull(), // user ID
|
|
|
|
|
content: text('content').notNull(),
|
|
|
|
|
mentions: text('mentions').array(), // array of mentioned user IDs
|
|
|
|
|
isLocked: boolean('is_locked').notNull().default(false),
|
|
|
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
},
|
|
|
|
|
(table) => [index('idx_in_interest').on(table.interestId)],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const interestTags = pgTable(
|
|
|
|
|
'interest_tags',
|
|
|
|
|
{
|
|
|
|
|
interestId: text('interest_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => interests.id, { onDelete: 'cascade' }),
|
|
|
|
|
tagId: text('tag_id').notNull(), // references tags.id
|
|
|
|
|
},
|
|
|
|
|
(table) => [primaryKey({ columns: [table.interestId, table.tagId] })],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export type Interest = typeof interests.$inferSelect;
|
|
|
|
|
export type NewInterest = typeof interests.$inferInsert;
|
|
|
|
|
export type InterestNote = typeof interestNotes.$inferSelect;
|
|
|
|
|
export type NewInterestNote = typeof interestNotes.$inferInsert;
|
2026-05-05 02:22:11 +02:00
|
|
|
export type InterestBerth = typeof interestBerths.$inferSelect;
|
|
|
|
|
export type NewInterestBerth = typeof interestBerths.$inferInsert;
|