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>
This commit is contained in:
178
src/lib/db/schema/berths.ts
Normal file
178
src/lib/db/schema/berths.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
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',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
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),
|
||||
nominalBoatSize: text('nominal_boat_size'),
|
||||
nominalBoatSizeM: text('nominal_boat_size_m'),
|
||||
waterDepth: numeric('water_depth'),
|
||||
waterDepthM: numeric('water_depth_m'),
|
||||
waterDepthIsMinimum: boolean('water_depth_is_minimum').default(false),
|
||||
sidePontoon: text('side_pontoon'),
|
||||
powerCapacity: text('power_capacity'),
|
||||
voltage: text('voltage'),
|
||||
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 }),
|
||||
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',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
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',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
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',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
berthId: text('berth_id')
|
||||
.notNull()
|
||||
.references(() => berths.id, { onDelete: 'cascade' }),
|
||||
clientId: text('client_id')
|
||||
.notNull()
|
||||
.references(() => clients.id, { onDelete: 'cascade' }),
|
||||
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',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
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(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_bml_berth').on(table.berthId),
|
||||
index('idx_bml_port').on(table.portId),
|
||||
],
|
||||
);
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user