Initial commit: Port Nimara CRM (Layers 0-4)
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled

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:
2026-03-26 11:52:51 +01:00
commit 67d7e6e3d5
572 changed files with 86496 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
import {
pgTable,
text,
boolean,
integer,
timestamp,
primaryKey,
index,
} from 'drizzle-orm/pg-core';
import { ports } from './ports';
import { clients } from './clients';
// Pipeline stages: open, details_sent, in_communication, visited, signed_eoi_nda, deposit_10pct, contract, completed
export const interests = pgTable(
'interests',
{
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
portId: text('port_id')
.notNull()
.references(() => ports.id),
clientId: text('client_id')
.notNull()
.references(() => clients.id),
berthId: text('berth_id'), // nullable — FK to berths defined in berths.ts, added via relation
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 }),
notes: text('notes'),
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),
index('idx_interests_berth').on(table.berthId),
index('idx_interests_stage').on(table.portId, table.pipelineStage),
index('idx_interests_archived').on(table.portId, table.archivedAt),
],
);
export const interestNotes = pgTable(
'interest_notes',
{
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
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;