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:
184
src/lib/db/schema/documents.ts
Normal file
184
src/lib/db/schema/documents.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
boolean,
|
||||
integer,
|
||||
timestamp,
|
||||
jsonb,
|
||||
index,
|
||||
uniqueIndex,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { ports } from './ports';
|
||||
import { clients } from './clients';
|
||||
|
||||
export const files = pgTable(
|
||||
'files',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
portId: text('port_id')
|
||||
.notNull()
|
||||
.references(() => ports.id),
|
||||
clientId: text('client_id').references(() => clients.id),
|
||||
filename: text('filename').notNull(),
|
||||
originalName: text('original_name').notNull(),
|
||||
mimeType: text('mime_type'),
|
||||
sizeBytes: text('size_bytes'), // stored as text to avoid bigint issues; parse as number in app
|
||||
storagePath: text('storage_path').notNull(),
|
||||
storageBucket: text('storage_bucket').notNull().default('crm-files'),
|
||||
category: text('category'), // eoi, contract, image, receipt, correspondence, misc
|
||||
uploadedBy: text('uploaded_by').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_files_port').on(table.portId),
|
||||
index('idx_files_client').on(table.clientId),
|
||||
],
|
||||
);
|
||||
|
||||
export const documents = pgTable(
|
||||
'documents',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
portId: text('port_id')
|
||||
.notNull()
|
||||
.references(() => ports.id),
|
||||
interestId: text('interest_id'), // references interests.id
|
||||
clientId: text('client_id').references(() => clients.id),
|
||||
documentType: text('document_type').notNull(), // eoi, contract, nda, reservation_agreement, other
|
||||
title: text('title').notNull(),
|
||||
status: text('status').notNull().default('draft'), // draft, sent, partially_signed, completed, expired, cancelled
|
||||
documensoId: text('documenso_id'),
|
||||
fileId: text('file_id').references(() => files.id),
|
||||
signedFileId: text('signed_file_id').references(() => files.id),
|
||||
isManualUpload: boolean('is_manual_upload').notNull().default(false),
|
||||
notes: text('notes'),
|
||||
createdBy: text('created_by').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_docs_port').on(table.portId),
|
||||
index('idx_docs_interest').on(table.interestId),
|
||||
index('idx_docs_client').on(table.clientId),
|
||||
index('idx_docs_type').on(table.portId, table.documentType),
|
||||
],
|
||||
);
|
||||
|
||||
export const documentSigners = pgTable(
|
||||
'document_signers',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
documentId: text('document_id')
|
||||
.notNull()
|
||||
.references(() => documents.id, { onDelete: 'cascade' }),
|
||||
signerName: text('signer_name').notNull(),
|
||||
signerEmail: text('signer_email').notNull(),
|
||||
signerRole: text('signer_role').notNull(), // client, developer, sales, approver, other
|
||||
signingOrder: integer('signing_order').notNull(),
|
||||
status: text('status').notNull().default('pending'), // pending, signed, declined
|
||||
signedAt: timestamp('signed_at', { withTimezone: true }),
|
||||
signingUrl: text('signing_url'),
|
||||
embeddedUrl: text('embedded_url'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [index('idx_ds_doc').on(table.documentId)],
|
||||
);
|
||||
|
||||
export const documentEvents = pgTable(
|
||||
'document_events',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
documentId: text('document_id')
|
||||
.notNull()
|
||||
.references(() => documents.id, { onDelete: 'cascade' }),
|
||||
eventType: text('event_type').notNull(), // created, sent, viewed, signed, completed, expired, reminder_sent
|
||||
signerId: text('signer_id').references(() => documentSigners.id),
|
||||
eventData: jsonb('event_data').default({}),
|
||||
signatureHash: text('signature_hash'), // deduplication
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_de_doc').on(table.documentId),
|
||||
uniqueIndex('idx_de_dedup').on(table.documentId, table.signatureHash).where(
|
||||
sql`${table.signatureHash} IS NOT NULL`
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const documentTemplates = pgTable(
|
||||
'document_templates',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
portId: text('port_id')
|
||||
.notNull()
|
||||
.references(() => ports.id),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
templateType: text('template_type').notNull(), // welcome_letter, handover_checklist, acknowledgment, correspondence, custom
|
||||
bodyHtml: text('body_html').notNull(),
|
||||
mergeFields: jsonb('merge_fields').notNull().default([]),
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
createdBy: text('created_by').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_dt_port').on(table.portId),
|
||||
index('idx_dt_type').on(table.portId, table.templateType),
|
||||
],
|
||||
);
|
||||
|
||||
export const formTemplates = pgTable(
|
||||
'form_templates',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
portId: text('port_id')
|
||||
.notNull()
|
||||
.references(() => ports.id),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
fields: jsonb('fields').notNull(),
|
||||
branding: jsonb('branding').default({}),
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
createdBy: text('created_by').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [index('idx_ft_port').on(table.portId)],
|
||||
);
|
||||
|
||||
export const formSubmissions = pgTable(
|
||||
'form_submissions',
|
||||
{
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
formTemplateId: text('form_template_id')
|
||||
.notNull()
|
||||
.references(() => formTemplates.id),
|
||||
clientId: text('client_id').references(() => clients.id),
|
||||
interestId: text('interest_id'), // references interests.id
|
||||
token: text('token').notNull().unique(),
|
||||
prefilledData: jsonb('prefilled_data').default({}),
|
||||
submittedData: jsonb('submitted_data'),
|
||||
status: text('status').notNull().default('pending'), // pending, submitted, expired
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
||||
submittedAt: timestamp('submitted_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [uniqueIndex('idx_fs_token').on(table.token)],
|
||||
);
|
||||
|
||||
export type File = typeof files.$inferSelect;
|
||||
export type NewFile = typeof files.$inferInsert;
|
||||
export type Document = typeof documents.$inferSelect;
|
||||
export type NewDocument = typeof documents.$inferInsert;
|
||||
export type DocumentSigner = typeof documentSigners.$inferSelect;
|
||||
export type NewDocumentSigner = typeof documentSigners.$inferInsert;
|
||||
export type DocumentEvent = typeof documentEvents.$inferSelect;
|
||||
export type NewDocumentEvent = typeof documentEvents.$inferInsert;
|
||||
export type DocumentTemplate = typeof documentTemplates.$inferSelect;
|
||||
export type NewDocumentTemplate = typeof documentTemplates.$inferInsert;
|
||||
export type FormTemplate = typeof formTemplates.$inferSelect;
|
||||
export type NewFormTemplate = typeof formTemplates.$inferInsert;
|
||||
export type FormSubmission = typeof formSubmissions.$inferSelect;
|
||||
export type NewFormSubmission = typeof formSubmissions.$inferInsert;
|
||||
Reference in New Issue
Block a user