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 {
|
|
|
|
|
pgTable,
|
|
|
|
|
text,
|
|
|
|
|
boolean,
|
|
|
|
|
integer,
|
|
|
|
|
numeric,
|
|
|
|
|
timestamp,
|
|
|
|
|
date,
|
|
|
|
|
jsonb,
|
|
|
|
|
index,
|
|
|
|
|
uniqueIndex,
|
|
|
|
|
primaryKey,
|
|
|
|
|
} from 'drizzle-orm/pg-core';
|
|
|
|
|
import { ports } from './ports';
|
|
|
|
|
import { clients } from './clients';
|
|
|
|
|
|
|
|
|
|
export const berths = pgTable(
|
|
|
|
|
'berths',
|
|
|
|
|
{
|
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),
|
|
|
|
|
mooringNumber: text('mooring_number').notNull(),
|
|
|
|
|
area: text('area'),
|
|
|
|
|
status: text('status').notNull().default('available'), // available, under_offer, sold
|
|
|
|
|
lengthFt: numeric('length_ft'),
|
|
|
|
|
widthFt: numeric('width_ft'),
|
|
|
|
|
draftFt: numeric('draft_ft'),
|
|
|
|
|
lengthM: numeric('length_m'),
|
|
|
|
|
widthM: numeric('width_m'),
|
|
|
|
|
draftM: numeric('draft_m'),
|
|
|
|
|
widthIsMinimum: boolean('width_is_minimum').default(false),
|
feat(berths): full NocoDB field parity, numeric types, sales edit access
Aligns the berths schema with the 117 production rows in NocoDB and exposes
every field for editing via the BerthForm sheet.
Schema (migration 0020):
- power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric
(NocoDB stores plain numbers; text was wrong shape and broke filter/sort)
- ADD status_override_mode text (1/117 legacy rows have a value; carried
forward for parity but not yet wired into the UI)
- USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty
strings convert cleanly
Validator + service:
- updateBerthSchema / createBerthSchema use z.coerce.number() for the
four numeric fields
- berths.service stringifies numeric values for Drizzle's numeric type
Form (src/components/berths/berth-form.tsx):
- adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag,
side pontoon, cleat type/capacity, bollard type/capacity, bow facing
- converts to typed selects (with NocoDB option lists in src/lib/constants):
area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity,
access
- power capacity / voltage become numeric inputs (with kW / V hints)
Permissions (seed.ts + dev DB):
- sales_manager and sales_agent: berths.edit false -> true
("sales will sometimes have to update these and I cannot be the only one")
- super_admin / director already had it; viewer stays read-only
- dev DB updated in-place via UPDATE roles ... jsonb_set
Verification:
- pnpm exec vitest run: 858/858 passing
- pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing
on feat/mobile-foundation, none introduced)
- lint clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
|
|
|
// Numeric: ft (legacy NocoDB stored as plain numbers, no units in value).
|
|
|
|
|
nominalBoatSize: numeric('nominal_boat_size'),
|
|
|
|
|
nominalBoatSizeM: numeric('nominal_boat_size_m'),
|
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
|
|
|
waterDepth: numeric('water_depth'),
|
|
|
|
|
waterDepthM: numeric('water_depth_m'),
|
|
|
|
|
waterDepthIsMinimum: boolean('water_depth_is_minimum').default(false),
|
|
|
|
|
sidePontoon: text('side_pontoon'),
|
feat(berths): full NocoDB field parity, numeric types, sales edit access
Aligns the berths schema with the 117 production rows in NocoDB and exposes
every field for editing via the BerthForm sheet.
Schema (migration 0020):
- power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric
(NocoDB stores plain numbers; text was wrong shape and broke filter/sort)
- ADD status_override_mode text (1/117 legacy rows have a value; carried
forward for parity but not yet wired into the UI)
- USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty
strings convert cleanly
Validator + service:
- updateBerthSchema / createBerthSchema use z.coerce.number() for the
four numeric fields
- berths.service stringifies numeric values for Drizzle's numeric type
Form (src/components/berths/berth-form.tsx):
- adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag,
side pontoon, cleat type/capacity, bollard type/capacity, bow facing
- converts to typed selects (with NocoDB option lists in src/lib/constants):
area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity,
access
- power capacity / voltage become numeric inputs (with kW / V hints)
Permissions (seed.ts + dev DB):
- sales_manager and sales_agent: berths.edit false -> true
("sales will sometimes have to update these and I cannot be the only one")
- super_admin / director already had it; viewer stays read-only
- dev DB updated in-place via UPDATE roles ... jsonb_set
Verification:
- pnpm exec vitest run: 858/858 passing
- pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing
on feat/mobile-foundation, none introduced)
- lint clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
|
|
|
powerCapacity: numeric('power_capacity'), // kW
|
|
|
|
|
voltage: numeric('voltage'), // V at 60Hz
|
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
|
|
|
mooringType: text('mooring_type'),
|
|
|
|
|
cleatType: text('cleat_type'),
|
|
|
|
|
cleatCapacity: text('cleat_capacity'),
|
|
|
|
|
bollardType: text('bollard_type'),
|
|
|
|
|
bollardCapacity: text('bollard_capacity'),
|
|
|
|
|
access: text('access'),
|
|
|
|
|
price: numeric('price'),
|
|
|
|
|
priceCurrency: text('price_currency').notNull().default('USD'),
|
|
|
|
|
bowFacing: text('bow_facing'),
|
|
|
|
|
berthApproved: boolean('berth_approved').default(false),
|
|
|
|
|
tenureType: text('tenure_type').notNull().default('permanent'), // permanent, fixed_term
|
|
|
|
|
tenureYears: integer('tenure_years'),
|
|
|
|
|
tenureStartDate: date('tenure_start_date'),
|
|
|
|
|
tenureEndDate: date('tenure_end_date'),
|
|
|
|
|
statusLastChangedBy: text('status_last_changed_by'), // user ID
|
|
|
|
|
statusLastChangedReason: text('status_last_changed_reason'),
|
|
|
|
|
statusLastModified: timestamp('status_last_modified', { withTimezone: true }),
|
feat(berths): full NocoDB field parity, numeric types, sales edit access
Aligns the berths schema with the 117 production rows in NocoDB and exposes
every field for editing via the BerthForm sheet.
Schema (migration 0020):
- power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric
(NocoDB stores plain numbers; text was wrong shape and broke filter/sort)
- ADD status_override_mode text (1/117 legacy rows have a value; carried
forward for parity but not yet wired into the UI)
- USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty
strings convert cleanly
Validator + service:
- updateBerthSchema / createBerthSchema use z.coerce.number() for the
four numeric fields
- berths.service stringifies numeric values for Drizzle's numeric type
Form (src/components/berths/berth-form.tsx):
- adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag,
side pontoon, cleat type/capacity, bollard type/capacity, bow facing
- converts to typed selects (with NocoDB option lists in src/lib/constants):
area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity,
access
- power capacity / voltage become numeric inputs (with kW / V hints)
Permissions (seed.ts + dev DB):
- sales_manager and sales_agent: berths.edit false -> true
("sales will sometimes have to update these and I cannot be the only one")
- super_admin / director already had it; viewer stays read-only
- dev DB updated in-place via UPDATE roles ... jsonb_set
Verification:
- pnpm exec vitest run: 858/858 passing
- pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing
on feat/mobile-foundation, none introduced)
- lint clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00
|
|
|
// Optional override flag carried over from NocoDB ("auto" or null in legacy data).
|
|
|
|
|
// Reserved for future "manual override" semantics; not surfaced in the UI today.
|
|
|
|
|
statusOverrideMode: text('status_override_mode'),
|
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
|
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
index('idx_berths_port').on(table.portId),
|
|
|
|
|
index('idx_berths_status').on(table.portId, table.status),
|
|
|
|
|
index('idx_berths_area').on(table.portId, table.area),
|
|
|
|
|
uniqueIndex('idx_berths_mooring').on(table.portId, table.mooringNumber),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const berthMapData = pgTable(
|
|
|
|
|
'berth_map_data',
|
|
|
|
|
{
|
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
|
|
|
berthId: text('berth_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.unique()
|
|
|
|
|
.references(() => berths.id, { onDelete: 'cascade' }),
|
|
|
|
|
svgPath: text('svg_path'),
|
|
|
|
|
x: numeric('x'),
|
|
|
|
|
y: numeric('y'),
|
|
|
|
|
transform: text('transform'),
|
|
|
|
|
fontSize: numeric('font_size'),
|
|
|
|
|
extraData: jsonb('extra_data').default({}),
|
|
|
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
},
|
|
|
|
|
(table) => [uniqueIndex('berth_map_data_berth_id_idx').on(table.berthId)],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const berthRecommendations = pgTable(
|
|
|
|
|
'berth_recommendations',
|
|
|
|
|
{
|
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
|
|
|
|
|
berthId: text('berth_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => berths.id, { onDelete: 'cascade' }),
|
|
|
|
|
matchScore: numeric('match_score'), // 0-100
|
|
|
|
|
matchReasons: jsonb('match_reasons'), // { "dimensional_fit": 95, "power_match": 80, ... }
|
|
|
|
|
source: text('source').notNull().default('ai'), // ai, manual
|
|
|
|
|
createdBy: text('created_by'), // user ID for manual recommendations
|
|
|
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
uniqueIndex('berth_rec_interest_berth_idx').on(table.interestId, table.berthId),
|
|
|
|
|
index('idx_br_interest').on(table.interestId),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const berthWaitingList = pgTable(
|
|
|
|
|
'berth_waiting_list',
|
|
|
|
|
{
|
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
|
|
|
berthId: text('berth_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => berths.id, { onDelete: 'cascade' }),
|
|
|
|
|
clientId: text('client_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => clients.id, { onDelete: 'cascade' }),
|
2026-04-23 17:57:29 +02:00
|
|
|
yachtId: text('yacht_id'), // FK added via relation; nullable (waiting for this yacht)
|
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
|
|
|
position: integer('position').notNull(),
|
|
|
|
|
priority: text('priority').notNull().default('normal'), // normal, high
|
|
|
|
|
notifyPref: text('notify_pref').default('email'), // email, in_app, both
|
|
|
|
|
notes: text('notes'),
|
|
|
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
},
|
|
|
|
|
(table) => [
|
|
|
|
|
uniqueIndex('berth_waiting_list_berth_client_idx').on(table.berthId, table.clientId),
|
|
|
|
|
index('idx_bwl_berth').on(table.berthId, table.position),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const berthMaintenanceLog = pgTable(
|
|
|
|
|
'berth_maintenance_log',
|
|
|
|
|
{
|
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
|
|
|
berthId: text('berth_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => berths.id, { onDelete: 'cascade' }),
|
|
|
|
|
portId: text('port_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => ports.id),
|
|
|
|
|
category: text('category').notNull(), // routine, repair, inspection, upgrade
|
|
|
|
|
description: text('description').notNull(),
|
|
|
|
|
cost: numeric('cost'),
|
|
|
|
|
costCurrency: text('cost_currency').default('USD'),
|
|
|
|
|
responsibleParty: text('responsible_party'),
|
|
|
|
|
performedDate: date('performed_date').notNull(),
|
|
|
|
|
photoFileIds: text('photo_file_ids').array(), // references to files table
|
|
|
|
|
createdBy: text('created_by').notNull(),
|
|
|
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
|
|
|
},
|
2026-04-23 17:57:29 +02:00
|
|
|
(table) => [index('idx_bml_berth').on(table.berthId), index('idx_bml_port').on(table.portId)],
|
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 berthTags = pgTable(
|
|
|
|
|
'berth_tags',
|
|
|
|
|
{
|
|
|
|
|
berthId: text('berth_id')
|
|
|
|
|
.notNull()
|
|
|
|
|
.references(() => berths.id, { onDelete: 'cascade' }),
|
|
|
|
|
tagId: text('tag_id').notNull(), // references tags.id
|
|
|
|
|
},
|
|
|
|
|
(table) => [primaryKey({ columns: [table.berthId, table.tagId] })],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export type Berth = typeof berths.$inferSelect;
|
|
|
|
|
export type NewBerth = typeof berths.$inferInsert;
|
|
|
|
|
export type BerthMapData = typeof berthMapData.$inferSelect;
|
|
|
|
|
export type NewBerthMapData = typeof berthMapData.$inferInsert;
|
|
|
|
|
export type BerthRecommendation = typeof berthRecommendations.$inferSelect;
|
|
|
|
|
export type NewBerthRecommendation = typeof berthRecommendations.$inferInsert;
|
|
|
|
|
export type BerthWaitingList = typeof berthWaitingList.$inferSelect;
|
|
|
|
|
export type NewBerthWaitingList = typeof berthWaitingList.$inferInsert;
|
|
|
|
|
export type BerthMaintenanceLog = typeof berthMaintenanceLog.$inferSelect;
|
|
|
|
|
export type NewBerthMaintenanceLog = typeof berthMaintenanceLog.$inferInsert;
|