Files
pn-new-crm/src/lib/db/schema/berths.ts

187 lines
6.9 KiB
TypeScript

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' }),
yachtId: text('yacht_id'), // FK added via relation; nullable (waiting for this yacht)
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;