1095 lines
44 KiB
Markdown
1095 lines
44 KiB
Markdown
|
|
# Port Nimara CRM — Database Schema (PostgreSQL + Drizzle ORM)
|
||
|
|
|
||
|
|
**Compiled:** 2026-03-11
|
||
|
|
**ORM:** Drizzle ORM with TypeScript strict mode
|
||
|
|
**Database:** PostgreSQL 16 (Docker container)
|
||
|
|
**Conventions:** snake_case for all columns and tables, UUID primary keys, `created_at`/`updated_at` on every table, `port_id` on every port-scoped table, soft deletes via `archived_at` where applicable.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Schema Overview
|
||
|
|
|
||
|
|
### Multi-Tenancy Core
|
||
|
|
|
||
|
|
- `ports` — Marina ports (tenants)
|
||
|
|
- `users` — CRM user accounts (managed by Better Auth)
|
||
|
|
- `roles` — Permission role definitions
|
||
|
|
- `port_role_overrides` — Per-port tweaks to global roles
|
||
|
|
- `user_port_roles` — User ↔ port ↔ role assignments
|
||
|
|
- `sessions` — Better Auth session management
|
||
|
|
|
||
|
|
### Client Domain
|
||
|
|
|
||
|
|
- `clients` — Anchor records for people/entities
|
||
|
|
- `client_contacts` — Multi-channel contact entries per client
|
||
|
|
- `client_relationships` — Relationships between clients (referrals, broker, family)
|
||
|
|
- `client_notes` — Timestamped notes on clients
|
||
|
|
- `client_tags` — Tags assigned to clients
|
||
|
|
- `client_merge_log` — Audit trail of client merges
|
||
|
|
|
||
|
|
### Interest Domain
|
||
|
|
|
||
|
|
- `interests` — Per-berth pipeline records, each belonging to a client (milestone dates are inline columns)
|
||
|
|
- `interest_notes` — Timestamped notes on interests
|
||
|
|
- `interest_tags` — Tags assigned to interests
|
||
|
|
|
||
|
|
### Berth Domain
|
||
|
|
|
||
|
|
- `berths` — Physical marina berths
|
||
|
|
- `berth_map_data` — SVG coordinates and visual data for berth map rendering
|
||
|
|
- `berth_recommendations` — AI/manual berth recommendations for interests
|
||
|
|
- `berth_waiting_list` — Waiting list entries per berth
|
||
|
|
- `berth_maintenance_log` — Maintenance, repair, and inspection records
|
||
|
|
- `berth_tags` — Tags assigned to berths
|
||
|
|
|
||
|
|
### Document Domain
|
||
|
|
|
||
|
|
- `documents` — All documents (EOIs, contracts, NDAs, custom)
|
||
|
|
- `document_signers` — Signers assigned to each document
|
||
|
|
- `document_events` — Signature events and status changes
|
||
|
|
- `document_templates` — Reusable document templates with merge fields
|
||
|
|
- `form_templates` — Pre-filled data collection form definitions
|
||
|
|
- `form_submissions` — Client form submission data
|
||
|
|
|
||
|
|
### Financial Domain
|
||
|
|
|
||
|
|
- `expenses` — Operational expenses with receipt tracking
|
||
|
|
- `invoices` — Generated invoices
|
||
|
|
- `invoice_line_items` — Line items per invoice
|
||
|
|
- `invoice_expenses` — Junction table linking invoices to expenses
|
||
|
|
|
||
|
|
### Communication Domain
|
||
|
|
|
||
|
|
- `email_accounts` — User SMTP/IMAP connection configs
|
||
|
|
- `email_threads` — Email conversation threads linked to clients
|
||
|
|
- `email_messages` — Individual email messages within threads
|
||
|
|
|
||
|
|
### Operations Domain
|
||
|
|
|
||
|
|
- `reminders` — CRM reminders and follow-ups (replaces tasks)
|
||
|
|
- `google_calendar_tokens` — Per-user Google Calendar OAuth tokens
|
||
|
|
- `google_calendar_cache` — Cached Google Calendar events for display
|
||
|
|
- `notifications` — In-app notification records
|
||
|
|
- `scheduled_reports` — Report schedule configurations
|
||
|
|
- `report_recipients` — Recipients per scheduled report
|
||
|
|
|
||
|
|
### System Domain
|
||
|
|
|
||
|
|
- `audit_logs` — Deep audit log with before/after values
|
||
|
|
- `tags` — Tag definitions (color, name)
|
||
|
|
- `files` — File metadata (MinIO references)
|
||
|
|
- `webhooks` — Outbound webhook configurations
|
||
|
|
- `webhook_deliveries` — Webhook delivery log
|
||
|
|
- `system_settings` — Key-value system settings (per-user preferences stored inline on `user_profiles.preferences`)
|
||
|
|
- `saved_views` — Saved filter/view configurations
|
||
|
|
- `scratchpad_notes` — Per-user quick notes not tied to records
|
||
|
|
- `user_notification_preferences` — Per-user per-type notification toggles (in-app, email)
|
||
|
|
- `currency_rates` — Exchange rates cache (Frankfurter API + manual overrides)
|
||
|
|
- `custom_field_definitions` — Admin-defined custom fields per entity type
|
||
|
|
- `custom_field_values` — Custom field data per entity record
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Detailed Table Definitions
|
||
|
|
|
||
|
|
### ports
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE ports (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
slug TEXT NOT NULL UNIQUE, -- URL-safe identifier
|
||
|
|
logo_url TEXT,
|
||
|
|
primary_color TEXT, -- hex color for branding
|
||
|
|
default_currency TEXT NOT NULL DEFAULT 'USD',
|
||
|
|
timezone TEXT NOT NULL DEFAULT 'America/Anguilla',
|
||
|
|
settings JSONB NOT NULL DEFAULT '{}', -- port-specific config overrides
|
||
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### users
|
||
|
|
|
||
|
|
Better Auth manages the core user table. Additional CRM-specific fields:
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- Better Auth creates its own user/session tables.
|
||
|
|
-- We extend with:
|
||
|
|
|
||
|
|
CREATE TABLE user_profiles (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id TEXT NOT NULL UNIQUE, -- references Better Auth user ID
|
||
|
|
display_name TEXT NOT NULL,
|
||
|
|
avatar_url TEXT,
|
||
|
|
phone TEXT,
|
||
|
|
is_super_admin BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
last_login_at TIMESTAMPTZ,
|
||
|
|
preferences JSONB NOT NULL DEFAULT '{}', -- dark mode, locale, etc.
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### roles
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE roles (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
permissions JSONB NOT NULL DEFAULT '{}', -- { "clients.view": true, "clients.create": true, ... }
|
||
|
|
is_global BOOLEAN NOT NULL DEFAULT true, -- available at all ports
|
||
|
|
is_system BOOLEAN NOT NULL DEFAULT false, -- protected from deletion (super_admin, director)
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### port_role_overrides
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE port_role_overrides (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id) ON DELETE CASCADE,
|
||
|
|
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||
|
|
permission_overrides JSONB NOT NULL DEFAULT '{}', -- overrides specific permissions for this port
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(port_id, role_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### user_port_roles
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE user_port_roles (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id TEXT NOT NULL, -- references Better Auth user ID
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id) ON DELETE CASCADE,
|
||
|
|
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||
|
|
assigned_by TEXT, -- user ID of who assigned this
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(user_id, port_id, role_id)
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_upr_user ON user_port_roles(user_id);
|
||
|
|
CREATE INDEX idx_upr_port ON user_port_roles(port_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### clients
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE clients (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
full_name TEXT NOT NULL,
|
||
|
|
company_name TEXT,
|
||
|
|
nationality TEXT,
|
||
|
|
is_proxy BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
proxy_type TEXT, -- broker, representative, family_member, legal_counsel, other
|
||
|
|
actual_owner_name TEXT, -- if known, the real owner behind the proxy
|
||
|
|
relationship_notes TEXT,
|
||
|
|
yacht_name TEXT,
|
||
|
|
yacht_length_ft NUMERIC,
|
||
|
|
yacht_width_ft NUMERIC,
|
||
|
|
yacht_draft_ft NUMERIC,
|
||
|
|
yacht_length_m NUMERIC,
|
||
|
|
yacht_width_m NUMERIC,
|
||
|
|
yacht_draft_m NUMERIC,
|
||
|
|
berth_size_desired TEXT,
|
||
|
|
preferred_contact_method TEXT, -- email, phone, whatsapp
|
||
|
|
preferred_language TEXT,
|
||
|
|
timezone TEXT,
|
||
|
|
source TEXT, -- website, manual, referral, broker
|
||
|
|
source_details TEXT, -- who referred, which broker, etc.
|
||
|
|
archived_at TIMESTAMPTZ, -- soft delete / archive
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_clients_port ON clients(port_id);
|
||
|
|
CREATE INDEX idx_clients_name ON clients(port_id, full_name);
|
||
|
|
CREATE INDEX idx_clients_archived ON clients(port_id, archived_at);
|
||
|
|
```
|
||
|
|
|
||
|
|
### client_contacts
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE client_contacts (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||
|
|
channel TEXT NOT NULL, -- email, phone, whatsapp, other
|
||
|
|
value TEXT NOT NULL, -- the actual address/number
|
||
|
|
label TEXT, -- primary, secondary, work, personal, broker, assistant
|
||
|
|
is_primary BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
notes TEXT,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_cc_client ON client_contacts(client_id);
|
||
|
|
CREATE INDEX idx_cc_email ON client_contacts(channel, value) WHERE channel = 'email';
|
||
|
|
CREATE INDEX idx_cc_phone ON client_contacts(channel, value) WHERE channel = 'phone';
|
||
|
|
```
|
||
|
|
|
||
|
|
### client_relationships
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE client_relationships (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
client_a_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||
|
|
client_b_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||
|
|
relationship_type TEXT NOT NULL, -- referred_by, broker_for, family_member, same_vessel, custom
|
||
|
|
description TEXT,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
CHECK (client_a_id != client_b_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### client_merge_log
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE client_merge_log (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
surviving_client_id UUID NOT NULL REFERENCES clients(id),
|
||
|
|
merged_client_id UUID NOT NULL, -- the client that was merged away (may no longer exist)
|
||
|
|
merged_by TEXT NOT NULL, -- user ID
|
||
|
|
merge_details JSONB NOT NULL, -- which fields were kept from which record
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### interests
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE interests (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
client_id UUID NOT NULL REFERENCES clients(id),
|
||
|
|
berth_id UUID REFERENCES berths(id), -- nullable for general inquiries
|
||
|
|
pipeline_stage TEXT NOT NULL DEFAULT 'open', -- open, details_sent, in_communication, visited, signed_eoi_nda, deposit_10pct, contract, completed
|
||
|
|
lead_category TEXT, -- general_interest, specific_qualified, hot_lead
|
||
|
|
source TEXT, -- website, manual, referral, broker
|
||
|
|
eoi_status TEXT, -- null, waiting_for_signatures, signed, expired
|
||
|
|
documenso_id TEXT, -- Documenso document ID
|
||
|
|
contract_status TEXT,
|
||
|
|
deposit_status TEXT,
|
||
|
|
reservation_status TEXT,
|
||
|
|
date_first_contact TIMESTAMPTZ,
|
||
|
|
date_last_contact TIMESTAMPTZ,
|
||
|
|
date_eoi_sent TIMESTAMPTZ,
|
||
|
|
date_eoi_signed TIMESTAMPTZ,
|
||
|
|
date_contract_sent TIMESTAMPTZ,
|
||
|
|
date_contract_signed TIMESTAMPTZ,
|
||
|
|
date_deposit_received TIMESTAMPTZ,
|
||
|
|
reminder_enabled BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
reminder_days INTEGER, -- follow up in X days if no activity
|
||
|
|
reminder_last_fired TIMESTAMPTZ,
|
||
|
|
notes TEXT, -- quick notes field
|
||
|
|
archived_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_interests_port ON interests(port_id);
|
||
|
|
CREATE INDEX idx_interests_client ON interests(client_id);
|
||
|
|
CREATE INDEX idx_interests_berth ON interests(berth_id);
|
||
|
|
CREATE INDEX idx_interests_stage ON interests(port_id, pipeline_stage);
|
||
|
|
CREATE INDEX idx_interests_archived ON interests(port_id, archived_at);
|
||
|
|
```
|
||
|
|
|
||
|
|
### interest_notes
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE interest_notes (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
interest_id UUID NOT NULL REFERENCES interests(id) ON DELETE CASCADE,
|
||
|
|
author_id TEXT NOT NULL, -- user ID
|
||
|
|
content TEXT NOT NULL,
|
||
|
|
mentions TEXT[], -- array of mentioned user IDs
|
||
|
|
is_locked BOOLEAN NOT NULL DEFAULT false, -- locked after 15 min edit window
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_in_interest ON interest_notes(interest_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### client_notes
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE client_notes (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||
|
|
author_id TEXT NOT NULL,
|
||
|
|
content TEXT NOT NULL,
|
||
|
|
mentions TEXT[],
|
||
|
|
is_locked BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_cn_client ON client_notes(client_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### berths
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE berths (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
mooring_number TEXT NOT NULL,
|
||
|
|
area TEXT,
|
||
|
|
status TEXT NOT NULL DEFAULT 'available', -- available, under_offer, sold
|
||
|
|
length_ft NUMERIC,
|
||
|
|
width_ft NUMERIC,
|
||
|
|
draft_ft NUMERIC,
|
||
|
|
length_m NUMERIC,
|
||
|
|
width_m NUMERIC,
|
||
|
|
draft_m NUMERIC,
|
||
|
|
width_is_minimum BOOLEAN DEFAULT false,
|
||
|
|
nominal_boat_size TEXT,
|
||
|
|
nominal_boat_size_m TEXT,
|
||
|
|
water_depth NUMERIC,
|
||
|
|
water_depth_m NUMERIC,
|
||
|
|
water_depth_is_minimum BOOLEAN DEFAULT false,
|
||
|
|
side_pontoon TEXT,
|
||
|
|
power_capacity TEXT,
|
||
|
|
voltage TEXT,
|
||
|
|
mooring_type TEXT,
|
||
|
|
cleat_type TEXT,
|
||
|
|
cleat_capacity TEXT,
|
||
|
|
bollard_type TEXT,
|
||
|
|
bollard_capacity TEXT,
|
||
|
|
access TEXT,
|
||
|
|
price NUMERIC,
|
||
|
|
price_currency TEXT NOT NULL DEFAULT 'USD',
|
||
|
|
bow_facing TEXT,
|
||
|
|
berth_approved BOOLEAN DEFAULT false,
|
||
|
|
tenure_type TEXT NOT NULL DEFAULT 'permanent', -- permanent, fixed_term
|
||
|
|
tenure_years INTEGER, -- for fixed_term
|
||
|
|
tenure_start_date DATE,
|
||
|
|
tenure_end_date DATE,
|
||
|
|
status_last_changed_by TEXT, -- user ID who last changed status (manual or accepted suggestion)
|
||
|
|
status_last_changed_reason TEXT, -- 'manual', 'rule:interest_linked', 'rule:eoi_sent', etc.
|
||
|
|
status_last_modified TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_berths_port ON berths(port_id);
|
||
|
|
CREATE INDEX idx_berths_status ON berths(port_id, status);
|
||
|
|
CREATE INDEX idx_berths_area ON berths(port_id, area);
|
||
|
|
CREATE UNIQUE INDEX idx_berths_mooring ON berths(port_id, mooring_number);
|
||
|
|
```
|
||
|
|
|
||
|
|
### berth_map_data
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE berth_map_data (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
berth_id UUID NOT NULL UNIQUE REFERENCES berths(id) ON DELETE CASCADE,
|
||
|
|
svg_path TEXT, -- SVG path string
|
||
|
|
x NUMERIC,
|
||
|
|
y NUMERIC,
|
||
|
|
transform TEXT,
|
||
|
|
font_size NUMERIC,
|
||
|
|
extra_data JSONB DEFAULT '{}', -- any additional map rendering data
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### berth_recommendations
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE berth_recommendations (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
interest_id UUID NOT NULL REFERENCES interests(id) ON DELETE CASCADE,
|
||
|
|
berth_id UUID NOT NULL REFERENCES berths(id) ON DELETE CASCADE,
|
||
|
|
match_score NUMERIC, -- 0-100
|
||
|
|
match_reasons JSONB, -- { "dimensional_fit": 95, "power_match": 80, ... }
|
||
|
|
source TEXT NOT NULL DEFAULT 'ai', -- ai, manual
|
||
|
|
created_by TEXT, -- user ID for manual recommendations
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(interest_id, berth_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### berth_waiting_list
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE berth_waiting_list (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
berth_id UUID NOT NULL REFERENCES berths(id) ON DELETE CASCADE,
|
||
|
|
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||
|
|
position INTEGER NOT NULL,
|
||
|
|
priority TEXT NOT NULL DEFAULT 'normal', -- normal, high
|
||
|
|
notify_pref TEXT DEFAULT 'email', -- email, in_app, both
|
||
|
|
notes TEXT,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(berth_id, client_id)
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_bwl_berth ON berth_waiting_list(berth_id, position);
|
||
|
|
```
|
||
|
|
|
||
|
|
### berth_maintenance_log
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE berth_maintenance_log (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
berth_id UUID NOT NULL REFERENCES berths(id) ON DELETE CASCADE,
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
category TEXT NOT NULL, -- routine, repair, inspection, upgrade
|
||
|
|
description TEXT NOT NULL,
|
||
|
|
cost NUMERIC,
|
||
|
|
cost_currency TEXT DEFAULT 'USD',
|
||
|
|
responsible_party TEXT,
|
||
|
|
performed_date DATE NOT NULL,
|
||
|
|
photo_file_ids UUID[], -- references to files table
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### documents
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE documents (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
interest_id UUID REFERENCES interests(id),
|
||
|
|
client_id UUID REFERENCES clients(id),
|
||
|
|
document_type TEXT NOT NULL, -- eoi, contract, nda, reservation_agreement, other
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
status TEXT NOT NULL DEFAULT 'draft', -- draft, sent, partially_signed, completed, expired, cancelled
|
||
|
|
documenso_id TEXT, -- Documenso document ID (null for manual uploads)
|
||
|
|
file_id UUID REFERENCES files(id), -- MinIO file reference
|
||
|
|
signed_file_id UUID REFERENCES files(id), -- signed version file reference
|
||
|
|
is_manual_upload BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
notes TEXT,
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_docs_port ON documents(port_id);
|
||
|
|
CREATE INDEX idx_docs_interest ON documents(interest_id);
|
||
|
|
CREATE INDEX idx_docs_client ON documents(client_id);
|
||
|
|
CREATE INDEX idx_docs_type ON documents(port_id, document_type);
|
||
|
|
```
|
||
|
|
|
||
|
|
### document_signers
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE document_signers (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||
|
|
signer_name TEXT NOT NULL,
|
||
|
|
signer_email TEXT NOT NULL,
|
||
|
|
signer_role TEXT NOT NULL, -- client, developer, sales, approver, other
|
||
|
|
signing_order INTEGER NOT NULL, -- sequential order
|
||
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending, signed, declined
|
||
|
|
signed_at TIMESTAMPTZ,
|
||
|
|
signing_url TEXT,
|
||
|
|
embedded_url TEXT,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_ds_doc ON document_signers(document_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### document_events
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE document_events (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||
|
|
event_type TEXT NOT NULL, -- created, sent, viewed, signed, completed, expired, reminder_sent
|
||
|
|
signer_id UUID REFERENCES document_signers(id),
|
||
|
|
event_data JSONB DEFAULT '{}',
|
||
|
|
signature_hash TEXT, -- deduplication
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_de_doc ON document_events(document_id);
|
||
|
|
CREATE UNIQUE INDEX idx_de_dedup ON document_events(document_id, signature_hash) WHERE signature_hash IS NOT NULL;
|
||
|
|
```
|
||
|
|
|
||
|
|
### document_templates
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE document_templates (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
template_type TEXT NOT NULL, -- welcome_letter, handover_checklist, acknowledgment, correspondence, custom
|
||
|
|
body_html TEXT NOT NULL, -- rich text with merge field tokens: {{client.full_name}}, {{berth.mooring_number}}, etc.
|
||
|
|
merge_fields JSONB NOT NULL DEFAULT '[]', -- array of field definitions: [{ "token": "client.full_name", "label": "Client Name", "source": "client" }]
|
||
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_dt_port ON document_templates(port_id);
|
||
|
|
CREATE INDEX idx_dt_type ON document_templates(port_id, template_type);
|
||
|
|
```
|
||
|
|
|
||
|
|
### form_templates
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE form_templates (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
fields JSONB NOT NULL, -- field definitions with types, labels, validation
|
||
|
|
branding JSONB DEFAULT '{}', -- logo, colors, header text
|
||
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### form_submissions
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE form_submissions (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
form_template_id UUID NOT NULL REFERENCES form_templates(id),
|
||
|
|
client_id UUID REFERENCES clients(id),
|
||
|
|
interest_id UUID REFERENCES interests(id),
|
||
|
|
token TEXT NOT NULL UNIQUE, -- secure token for form URL
|
||
|
|
prefilled_data JSONB DEFAULT '{}',
|
||
|
|
submitted_data JSONB,
|
||
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending, submitted, expired
|
||
|
|
expires_at TIMESTAMPTZ,
|
||
|
|
submitted_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE UNIQUE INDEX idx_fs_token ON form_submissions(token);
|
||
|
|
```
|
||
|
|
|
||
|
|
### expenses
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE expenses (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
establishment_name TEXT,
|
||
|
|
amount NUMERIC NOT NULL,
|
||
|
|
currency TEXT NOT NULL DEFAULT 'USD',
|
||
|
|
amount_usd NUMERIC, -- converted equivalent
|
||
|
|
exchange_rate NUMERIC, -- rate used for conversion
|
||
|
|
payment_method TEXT,
|
||
|
|
category TEXT,
|
||
|
|
payer TEXT,
|
||
|
|
expense_date TIMESTAMPTZ NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
receipt_file_ids UUID[], -- references to files table
|
||
|
|
payment_status TEXT DEFAULT 'unpaid', -- unpaid, paid, partial
|
||
|
|
payment_date DATE,
|
||
|
|
payment_reference TEXT,
|
||
|
|
payment_notes TEXT,
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
archived_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_expenses_port ON expenses(port_id);
|
||
|
|
CREATE INDEX idx_expenses_date ON expenses(port_id, expense_date);
|
||
|
|
CREATE INDEX idx_expenses_category ON expenses(port_id, category);
|
||
|
|
```
|
||
|
|
|
||
|
|
### invoices
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE invoices (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
invoice_number TEXT NOT NULL, -- INV-YYYYMM-### auto-generated
|
||
|
|
client_name TEXT NOT NULL,
|
||
|
|
billing_email TEXT,
|
||
|
|
billing_address TEXT,
|
||
|
|
due_date DATE NOT NULL,
|
||
|
|
payment_terms TEXT NOT NULL DEFAULT 'net30', -- immediate, net10, net15, net30, net45, net60
|
||
|
|
currency TEXT NOT NULL DEFAULT 'USD',
|
||
|
|
subtotal NUMERIC NOT NULL,
|
||
|
|
discount_pct NUMERIC DEFAULT 0, -- e.g., 2 for net10
|
||
|
|
discount_amount NUMERIC DEFAULT 0,
|
||
|
|
fee_pct NUMERIC DEFAULT 0, -- e.g., 5 for processing fee
|
||
|
|
fee_amount NUMERIC DEFAULT 0,
|
||
|
|
total NUMERIC NOT NULL,
|
||
|
|
status TEXT NOT NULL DEFAULT 'draft', -- draft, sent, paid, overdue, cancelled
|
||
|
|
payment_status TEXT DEFAULT 'unpaid',
|
||
|
|
payment_date DATE,
|
||
|
|
payment_method TEXT,
|
||
|
|
payment_reference TEXT,
|
||
|
|
pdf_file_id UUID REFERENCES files(id),
|
||
|
|
notes TEXT,
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
archived_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE UNIQUE INDEX idx_invoices_number ON invoices(port_id, invoice_number);
|
||
|
|
CREATE INDEX idx_invoices_port ON invoices(port_id);
|
||
|
|
CREATE INDEX idx_invoices_status ON invoices(port_id, status);
|
||
|
|
```
|
||
|
|
|
||
|
|
### invoice_line_items
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE invoice_line_items (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
||
|
|
description TEXT NOT NULL,
|
||
|
|
quantity NUMERIC NOT NULL DEFAULT 1,
|
||
|
|
unit_price NUMERIC NOT NULL,
|
||
|
|
total NUMERIC NOT NULL,
|
||
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### invoice_expenses
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE invoice_expenses (
|
||
|
|
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
||
|
|
expense_id UUID NOT NULL REFERENCES expenses(id) ON DELETE CASCADE,
|
||
|
|
PRIMARY KEY (invoice_id, expense_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### email_accounts
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE email_accounts (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id TEXT NOT NULL,
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
provider TEXT NOT NULL, -- google, outlook, custom
|
||
|
|
email_address TEXT NOT NULL,
|
||
|
|
smtp_host TEXT NOT NULL,
|
||
|
|
smtp_port INTEGER NOT NULL,
|
||
|
|
imap_host TEXT NOT NULL,
|
||
|
|
imap_port INTEGER NOT NULL,
|
||
|
|
credentials_enc BYTEA NOT NULL, -- encrypted credentials
|
||
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
last_sync_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### email_threads
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE email_threads (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
client_id UUID REFERENCES clients(id),
|
||
|
|
subject TEXT,
|
||
|
|
last_message_at TIMESTAMPTZ,
|
||
|
|
message_count INTEGER NOT NULL DEFAULT 0,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_et_client ON email_threads(client_id);
|
||
|
|
CREATE INDEX idx_et_port ON email_threads(port_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### email_messages
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE email_messages (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
thread_id UUID NOT NULL REFERENCES email_threads(id) ON DELETE CASCADE,
|
||
|
|
message_id_header TEXT, -- email Message-ID header
|
||
|
|
from_address TEXT NOT NULL,
|
||
|
|
to_addresses TEXT[] NOT NULL,
|
||
|
|
cc_addresses TEXT[],
|
||
|
|
subject TEXT,
|
||
|
|
body_text TEXT,
|
||
|
|
body_html TEXT,
|
||
|
|
direction TEXT NOT NULL, -- inbound, outbound
|
||
|
|
sent_at TIMESTAMPTZ NOT NULL,
|
||
|
|
attachment_file_ids UUID[], -- references to files table
|
||
|
|
raw_file_id UUID REFERENCES files(id), -- full raw email stored in MinIO
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_em_thread ON email_messages(thread_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### reminders
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE reminders (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
note TEXT,
|
||
|
|
due_at TIMESTAMPTZ NOT NULL,
|
||
|
|
priority TEXT NOT NULL DEFAULT 'medium', -- low, medium, high, urgent
|
||
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending, snoozed, completed, dismissed
|
||
|
|
assigned_to TEXT, -- user ID
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
client_id UUID REFERENCES clients(id),
|
||
|
|
interest_id UUID REFERENCES interests(id),
|
||
|
|
berth_id UUID REFERENCES berths(id),
|
||
|
|
auto_generated BOOLEAN NOT NULL DEFAULT false, -- true if created by follow-up reminder system
|
||
|
|
google_calendar_event_id TEXT, -- Google Calendar event ID (if synced)
|
||
|
|
google_calendar_synced BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
snoozed_until TIMESTAMPTZ, -- if snoozed, when to resurface
|
||
|
|
completed_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_reminders_port ON reminders(port_id);
|
||
|
|
CREATE INDEX idx_reminders_assigned ON reminders(assigned_to, status);
|
||
|
|
CREATE INDEX idx_reminders_due ON reminders(port_id, due_at) WHERE status IN ('pending', 'snoozed');
|
||
|
|
```
|
||
|
|
|
||
|
|
### google_calendar_tokens
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE google_calendar_tokens (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id TEXT NOT NULL UNIQUE,
|
||
|
|
access_token TEXT NOT NULL, -- encrypted (pgcrypto)
|
||
|
|
refresh_token TEXT NOT NULL, -- encrypted (pgcrypto)
|
||
|
|
token_expiry TIMESTAMPTZ NOT NULL,
|
||
|
|
calendar_id TEXT NOT NULL DEFAULT 'primary', -- which calendar to sync
|
||
|
|
connected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
last_sync_at TIMESTAMPTZ,
|
||
|
|
sync_enabled BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### google_calendar_cache
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE google_calendar_cache (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id TEXT NOT NULL,
|
||
|
|
event_id TEXT NOT NULL, -- Google Calendar event ID
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
start_at TIMESTAMPTZ NOT NULL,
|
||
|
|
end_at TIMESTAMPTZ,
|
||
|
|
location TEXT,
|
||
|
|
description TEXT,
|
||
|
|
is_crm_pushed BOOLEAN NOT NULL DEFAULT false, -- true if this event originated from a CRM reminder
|
||
|
|
reminder_id UUID REFERENCES reminders(id), -- linked CRM reminder (if is_crm_pushed)
|
||
|
|
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(user_id, event_id)
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_gcal_cache_user ON google_calendar_cache(user_id, start_at);
|
||
|
|
```
|
||
|
|
|
||
|
|
### notifications
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE notifications (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
user_id TEXT NOT NULL,
|
||
|
|
type TEXT NOT NULL, -- reminder_due, reminder_overdue, new_registration, eoi_signed, eoi_completed, email_received, duplicate_alert, invoice_overdue, waiting_list, system_alert, follow_up_created, tenure_expiring
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
link TEXT, -- URL to relevant record
|
||
|
|
entity_type TEXT, -- client, interest, berth, invoice, etc.
|
||
|
|
entity_id UUID,
|
||
|
|
is_read BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
email_sent BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_notif_user ON notifications(user_id, is_read);
|
||
|
|
CREATE INDEX idx_notif_port ON notifications(port_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### audit_logs
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE audit_logs (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID REFERENCES ports(id), -- null for system-level events
|
||
|
|
user_id TEXT, -- null for system-generated events
|
||
|
|
action TEXT NOT NULL, -- create, update, delete, archive, restore, merge, login, logout, revert
|
||
|
|
entity_type TEXT NOT NULL, -- client, interest, berth, expense, invoice, file, user, role, etc.
|
||
|
|
entity_id UUID,
|
||
|
|
field_changed TEXT, -- specific field for update actions
|
||
|
|
old_value JSONB,
|
||
|
|
new_value JSONB,
|
||
|
|
ip_address TEXT,
|
||
|
|
user_agent TEXT,
|
||
|
|
reverted_by TEXT, -- user ID if this change was reverted
|
||
|
|
reverted_at TIMESTAMPTZ,
|
||
|
|
revert_of UUID REFERENCES audit_logs(id), -- points to the audit entry this reverts
|
||
|
|
metadata JSONB DEFAULT '{}', -- extra context
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_al_port ON audit_logs(port_id, created_at DESC);
|
||
|
|
CREATE INDEX idx_al_entity ON audit_logs(entity_type, entity_id);
|
||
|
|
CREATE INDEX idx_al_user ON audit_logs(user_id, created_at DESC);
|
||
|
|
CREATE INDEX idx_al_created ON audit_logs(created_at DESC);
|
||
|
|
```
|
||
|
|
|
||
|
|
### tags
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE tags (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
color TEXT NOT NULL DEFAULT '#6B7280', -- hex color
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(port_id, name)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### client_tags
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE client_tags (
|
||
|
|
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||
|
|
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||
|
|
PRIMARY KEY (client_id, tag_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### interest_tags
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE interest_tags (
|
||
|
|
interest_id UUID NOT NULL REFERENCES interests(id) ON DELETE CASCADE,
|
||
|
|
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||
|
|
PRIMARY KEY (interest_id, tag_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### berth_tags
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE berth_tags (
|
||
|
|
berth_id UUID NOT NULL REFERENCES berths(id) ON DELETE CASCADE,
|
||
|
|
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||
|
|
PRIMARY KEY (berth_id, tag_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### files
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE files (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
client_id UUID REFERENCES clients(id),
|
||
|
|
filename TEXT NOT NULL,
|
||
|
|
original_name TEXT NOT NULL,
|
||
|
|
mime_type TEXT,
|
||
|
|
size_bytes BIGINT,
|
||
|
|
storage_path TEXT NOT NULL, -- MinIO object key
|
||
|
|
storage_bucket TEXT NOT NULL DEFAULT 'crm-files',
|
||
|
|
category TEXT, -- eoi, contract, image, receipt, correspondence, misc
|
||
|
|
uploaded_by TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_files_port ON files(port_id);
|
||
|
|
CREATE INDEX idx_files_client ON files(client_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### webhooks
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE webhooks (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
url TEXT NOT NULL,
|
||
|
|
secret TEXT, -- HMAC signing secret
|
||
|
|
events TEXT[] NOT NULL, -- array of event types to subscribe to
|
||
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### webhook_deliveries
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE webhook_deliveries (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
|
||
|
|
event_type TEXT NOT NULL,
|
||
|
|
payload JSONB NOT NULL,
|
||
|
|
response_status INTEGER,
|
||
|
|
response_body TEXT,
|
||
|
|
attempt INTEGER NOT NULL DEFAULT 1,
|
||
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending, success, failed, dead_letter
|
||
|
|
delivered_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_wd_webhook ON webhook_deliveries(webhook_id, created_at DESC);
|
||
|
|
```
|
||
|
|
|
||
|
|
### scheduled_reports
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE scheduled_reports (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
report_type TEXT NOT NULL, -- pipeline_summary, expense_summary, berth_occupancy, activity_log, overdue_items, revenue_forecast
|
||
|
|
schedule TEXT NOT NULL, -- cron expression
|
||
|
|
last_run_at TIMESTAMPTZ,
|
||
|
|
next_run_at TIMESTAMPTZ,
|
||
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
config JSONB DEFAULT '{}', -- report-specific config options
|
||
|
|
created_by TEXT NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### report_recipients
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE report_recipients (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
report_id UUID NOT NULL REFERENCES scheduled_reports(id) ON DELETE CASCADE,
|
||
|
|
email TEXT NOT NULL,
|
||
|
|
user_id TEXT, -- null for external recipients
|
||
|
|
UNIQUE(report_id, email)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### system_settings
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE system_settings (
|
||
|
|
key TEXT PRIMARY KEY,
|
||
|
|
value JSONB NOT NULL,
|
||
|
|
port_id UUID REFERENCES ports(id), -- null for global settings
|
||
|
|
updated_by TEXT,
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(key, port_id)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Key system_settings entries include:**
|
||
|
|
|
||
|
|
- `berth_status_rules` (per-port) — JSON array of `{ trigger, mode, target_status }` objects defining berth auto-status transition behavior. Modes: `auto`, `suggest`, `off`. See BR-001.
|
||
|
|
- `follow_up_defaults` (per-port) — Default reminder_days, send window hours, cooldown period
|
||
|
|
- `eoi_reminder_settings` (per-port) — EOI signing reminder schedule, cooldown, send window
|
||
|
|
- `currency_display` (global) — Primary currency, secondary currency, decimal places
|
||
|
|
|
||
|
|
### saved_views
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE saved_views (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
user_id TEXT NOT NULL,
|
||
|
|
entity_type TEXT NOT NULL, -- clients, interests, berths, expenses, invoices
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
filters JSONB NOT NULL,
|
||
|
|
sort_config JSONB,
|
||
|
|
column_config JSONB, -- which columns visible, column order
|
||
|
|
is_shared BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
is_default BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_sv_user ON saved_views(user_id, entity_type);
|
||
|
|
```
|
||
|
|
|
||
|
|
### scratchpad_notes
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE scratchpad_notes (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id TEXT NOT NULL,
|
||
|
|
content TEXT NOT NULL,
|
||
|
|
linked_client_id UUID REFERENCES clients(id), -- null until dragged onto a client
|
||
|
|
linked_at TIMESTAMPTZ,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_sp_user ON scratchpad_notes(user_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
### user_notification_preferences
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE user_notification_preferences (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id TEXT NOT NULL,
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
notification_type TEXT NOT NULL,
|
||
|
|
in_app BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
email BOOLEAN NOT NULL DEFAULT true,
|
||
|
|
UNIQUE(user_id, port_id, notification_type)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### currency_rates
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE currency_rates (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
base_currency TEXT NOT NULL,
|
||
|
|
target_currency TEXT NOT NULL,
|
||
|
|
rate NUMERIC NOT NULL,
|
||
|
|
source TEXT NOT NULL DEFAULT 'frankfurter', -- frankfurter, manual
|
||
|
|
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(base_currency, target_currency)
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
### custom_fields
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE custom_field_definitions (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
port_id UUID NOT NULL REFERENCES ports(id),
|
||
|
|
entity_type TEXT NOT NULL, -- client, interest, berth
|
||
|
|
field_name TEXT NOT NULL,
|
||
|
|
field_label TEXT NOT NULL,
|
||
|
|
field_type TEXT NOT NULL, -- text, number, date, boolean, select
|
||
|
|
select_options JSONB, -- for select type: ["option1", "option2"]
|
||
|
|
is_required BOOLEAN NOT NULL DEFAULT false,
|
||
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(port_id, entity_type, field_name)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE custom_field_values (
|
||
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
field_id UUID NOT NULL REFERENCES custom_field_definitions(id) ON DELETE CASCADE,
|
||
|
|
entity_id UUID NOT NULL, -- references the client/interest/berth ID
|
||
|
|
value JSONB NOT NULL,
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
|
|
UNIQUE(field_id, entity_id)
|
||
|
|
);
|
||
|
|
CREATE INDEX idx_cfv_entity ON custom_field_values(entity_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Table Count Summary
|
||
|
|
|
||
|
|
| Category | Tables | Count |
|
||
|
|
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||
|
|
| Multi-Tenancy Core | ports, user_profiles, roles, port_role_overrides, user_port_roles | 5 |
|
||
|
|
| Client Domain | clients, client_contacts, client_relationships, client_merge_log, client_notes, client_tags | 6 |
|
||
|
|
| Interest Domain | interests, interest_notes, interest_tags | 3 |
|
||
|
|
| Berth Domain | berths, berth_map_data, berth_recommendations, berth_waiting_list, berth_maintenance_log, berth_tags | 6 |
|
||
|
|
| Document Domain | documents, document_signers, document_events, document_templates, form_templates, form_submissions | 6 |
|
||
|
|
| Financial Domain | expenses, invoices, invoice_line_items, invoice_expenses | 4 |
|
||
|
|
| Communication Domain | email_accounts, email_threads, email_messages | 3 |
|
||
|
|
| Operations Domain | reminders, google_calendar_tokens, google_calendar_cache, notifications, scheduled_reports, report_recipients | 6 |
|
||
|
|
| System Domain | audit_logs, tags, files, webhooks, webhook_deliveries, system_settings, saved_views, scratchpad_notes, user_notification_preferences, currency_rates, custom_field_definitions, custom_field_values | 12 |
|
||
|
|
| **TOTAL** | | **51 tables** |
|