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:
15
src/lib/validators/ai.ts
Normal file
15
src/lib/validators/ai.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const requestScoreSchema = z.object({
|
||||
interestId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const requestDraftSchema = z.object({
|
||||
interestId: z.string().uuid(),
|
||||
clientId: z.string().uuid(),
|
||||
context: z.enum(['follow_up', 'introduction', 'stage_update', 'general']),
|
||||
additionalInstructions: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
export type RequestScoreInput = z.infer<typeof requestScoreSchema>;
|
||||
export type RequestDraftInput = z.infer<typeof requestDraftSchema>;
|
||||
97
src/lib/validators/berths.ts
Normal file
97
src/lib/validators/berths.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { z } from 'zod';
|
||||
import { BERTH_STATUSES } from '@/lib/constants';
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
|
||||
// ─── Update Berth ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const updateBerthSchema = z.object({
|
||||
area: z.string().optional(),
|
||||
lengthFt: z.coerce.number().optional(),
|
||||
lengthM: z.coerce.number().optional(),
|
||||
widthFt: z.coerce.number().optional(),
|
||||
widthM: z.coerce.number().optional(),
|
||||
draftFt: z.coerce.number().optional(),
|
||||
draftM: z.coerce.number().optional(),
|
||||
widthIsMinimum: z.boolean().optional(),
|
||||
nominalBoatSize: z.string().optional(),
|
||||
nominalBoatSizeM: z.string().optional(),
|
||||
waterDepth: z.coerce.number().optional(),
|
||||
waterDepthM: z.coerce.number().optional(),
|
||||
waterDepthIsMinimum: z.boolean().optional(),
|
||||
sidePontoon: z.string().optional(),
|
||||
powerCapacity: z.string().optional(),
|
||||
voltage: z.string().optional(),
|
||||
mooringType: z.string().optional(),
|
||||
cleatType: z.string().optional(),
|
||||
cleatCapacity: z.string().optional(),
|
||||
bollardType: z.string().optional(),
|
||||
bollardCapacity: z.string().optional(),
|
||||
access: z.string().optional(),
|
||||
price: z.coerce.number().optional(),
|
||||
priceCurrency: z.string().optional(),
|
||||
bowFacing: z.string().optional(),
|
||||
berthApproved: z.boolean().optional(),
|
||||
tenureType: z.enum(['permanent', 'fixed_term']).optional(),
|
||||
tenureYears: z.coerce.number().int().optional(),
|
||||
tenureStartDate: z.string().optional(),
|
||||
tenureEndDate: z.string().optional(),
|
||||
});
|
||||
|
||||
export type UpdateBerthInput = z.infer<typeof updateBerthSchema>;
|
||||
|
||||
// ─── Update Berth Status ──────────────────────────────────────────────────────
|
||||
|
||||
export const updateBerthStatusSchema = z.object({
|
||||
status: z.enum(BERTH_STATUSES),
|
||||
reason: z.string().min(1, 'Reason is required'),
|
||||
});
|
||||
|
||||
export type UpdateBerthStatusInput = z.infer<typeof updateBerthStatusSchema>;
|
||||
|
||||
// ─── List Berths ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const listBerthsSchema = baseListQuerySchema.extend({
|
||||
status: z.enum(BERTH_STATUSES).optional(),
|
||||
area: z.string().optional(),
|
||||
minLength: z.coerce.number().optional(),
|
||||
maxLength: z.coerce.number().optional(),
|
||||
minPrice: z.coerce.number().optional(),
|
||||
maxPrice: z.coerce.number().optional(),
|
||||
tenureType: z.enum(['permanent', 'fixed_term']).optional(),
|
||||
tagIds: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((v) => (v ? v.split(',') : undefined)),
|
||||
});
|
||||
|
||||
export type ListBerthsQuery = z.infer<typeof listBerthsSchema>;
|
||||
|
||||
// ─── Add Maintenance Log ──────────────────────────────────────────────────────
|
||||
|
||||
export const addMaintenanceLogSchema = z.object({
|
||||
category: z.enum(['routine', 'repair', 'inspection', 'upgrade']),
|
||||
description: z.string().min(1),
|
||||
cost: z.coerce.number().optional(),
|
||||
costCurrency: z.string().optional(),
|
||||
responsibleParty: z.string().optional(),
|
||||
performedDate: z.string().min(1, 'Performed date is required'),
|
||||
photoFileIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type AddMaintenanceLogInput = z.infer<typeof addMaintenanceLogSchema>;
|
||||
|
||||
// ─── Update Waiting List ──────────────────────────────────────────────────────
|
||||
|
||||
export const updateWaitingListSchema = z.object({
|
||||
entries: z.array(
|
||||
z.object({
|
||||
clientId: z.string(),
|
||||
position: z.number().int().min(1),
|
||||
priority: z.enum(['normal', 'high']).optional(),
|
||||
notifyPref: z.enum(['email', 'in_app', 'both']).optional(),
|
||||
notes: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type UpdateWaitingListInput = z.infer<typeof updateWaitingListSchema>;
|
||||
65
src/lib/validators/clients.ts
Normal file
65
src/lib/validators/clients.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
|
||||
// ─── Contact sub-schema ──────────────────────────────────────────────────────
|
||||
|
||||
export const contactSchema = z.object({
|
||||
channel: z.enum(['email', 'phone', 'whatsapp', 'other']),
|
||||
value: z.string().min(1),
|
||||
label: z.string().optional(),
|
||||
isPrimary: z.boolean().optional().default(false),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
// ─── Create ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const createClientSchema = z.object({
|
||||
fullName: z.string().min(1).max(200),
|
||||
contacts: z.array(contactSchema).min(1, 'At least one contact is required'),
|
||||
companyName: z.string().optional(),
|
||||
nationality: z.string().optional(),
|
||||
isProxy: z.boolean().optional().default(false),
|
||||
proxyType: z.string().optional(),
|
||||
actualOwnerName: z.string().optional(),
|
||||
yachtName: z.string().optional(),
|
||||
yachtLengthFt: z.string().optional(),
|
||||
yachtWidthFt: z.string().optional(),
|
||||
yachtDraftFt: z.string().optional(),
|
||||
yachtLengthM: z.string().optional(),
|
||||
yachtWidthM: z.string().optional(),
|
||||
yachtDraftM: z.string().optional(),
|
||||
berthSizeDesired: z.string().optional(),
|
||||
preferredContactMethod: z.enum(['email', 'phone', 'whatsapp']).optional(),
|
||||
preferredLanguage: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
source: z.enum(['website', 'manual', 'referral', 'broker']).optional(),
|
||||
sourceDetails: z.string().optional(),
|
||||
tagIds: z.array(z.string()).optional().default([]),
|
||||
});
|
||||
|
||||
// ─── Update ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const updateClientSchema = createClientSchema.omit({ contacts: true, tagIds: true }).partial();
|
||||
|
||||
// ─── List ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const listClientsSchema = baseListQuerySchema.extend({
|
||||
source: z.enum(['website', 'manual', 'referral', 'broker']).optional(),
|
||||
nationality: z.string().optional(),
|
||||
isProxy: z
|
||||
.string()
|
||||
.transform((v) => v === 'true')
|
||||
.optional(),
|
||||
tagIds: z
|
||||
.string()
|
||||
.transform((v) => v.split(',').filter(Boolean))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type ContactInput = z.infer<typeof contactSchema>;
|
||||
export type CreateClientInput = z.infer<typeof createClientSchema>;
|
||||
export type UpdateClientInput = z.infer<typeof updateClientSchema>;
|
||||
export type ListClientsInput = z.infer<typeof listClientsSchema>;
|
||||
53
src/lib/validators/custom-fields.ts
Normal file
53
src/lib/validators/custom-fields.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const CUSTOM_FIELD_TYPES = ['text', 'number', 'date', 'boolean', 'select'] as const;
|
||||
const CUSTOM_FIELD_ENTITIES = ['client', 'interest', 'berth'] as const;
|
||||
|
||||
export const createFieldSchema = z
|
||||
.object({
|
||||
entityType: z.enum(CUSTOM_FIELD_ENTITIES),
|
||||
fieldName: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(50)
|
||||
.regex(/^[a-z_][a-z0-9_]*$/, 'Must be snake_case'),
|
||||
fieldLabel: z.string().min(1).max(100),
|
||||
fieldType: z.enum(CUSTOM_FIELD_TYPES),
|
||||
selectOptions: z
|
||||
.array(z.string().min(1).max(100))
|
||||
.min(1)
|
||||
.max(50)
|
||||
.optional(),
|
||||
isRequired: z.boolean().default(false),
|
||||
sortOrder: z.number().int().min(0).default(0),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
data.fieldType !== 'select' ||
|
||||
(data.selectOptions && data.selectOptions.length > 0),
|
||||
{
|
||||
message: 'Select fields must have at least one option',
|
||||
path: ['selectOptions'],
|
||||
},
|
||||
);
|
||||
|
||||
export const updateFieldSchema = z.object({
|
||||
fieldLabel: z.string().min(1).max(100).optional(),
|
||||
selectOptions: z.array(z.string().min(1).max(100)).optional(),
|
||||
isRequired: z.boolean().optional(),
|
||||
sortOrder: z.number().int().min(0).optional(),
|
||||
// fieldType intentionally omitted — cannot be changed after creation
|
||||
});
|
||||
|
||||
export const setValuesSchema = z.object({
|
||||
values: z.array(
|
||||
z.object({
|
||||
fieldId: z.string().uuid(),
|
||||
value: z.unknown(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type CreateFieldInput = z.infer<typeof createFieldSchema>;
|
||||
export type UpdateFieldInput = z.infer<typeof updateFieldSchema>;
|
||||
export type SetValuesInput = z.infer<typeof setValuesSchema>;
|
||||
105
src/lib/validators/document-templates.ts
Normal file
105
src/lib/validators/document-templates.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
|
||||
export const createTemplateSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().max(500).optional(),
|
||||
templateType: z.enum([
|
||||
'welcome_letter',
|
||||
'handover_checklist',
|
||||
'acknowledgment',
|
||||
'correspondence',
|
||||
'custom',
|
||||
]),
|
||||
bodyHtml: z.string().min(1),
|
||||
mergeFields: z.array(z.string()).optional().default([]),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const updateTemplateSchema = createTemplateSchema.partial();
|
||||
|
||||
export const listTemplatesSchema = baseListQuerySchema.extend({
|
||||
templateType: z.string().optional(),
|
||||
isActive: z
|
||||
.enum(['true', 'false'])
|
||||
.transform((v) => v === 'true')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const generateSchema = z.object({
|
||||
clientId: z.string().optional(),
|
||||
interestId: z.string().optional(),
|
||||
berthId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const generateAndSendSchema = generateSchema.extend({
|
||||
recipientEmail: z.string().email(),
|
||||
});
|
||||
|
||||
export const generateAndSignSchema = generateSchema.extend({
|
||||
signers: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
role: z.string().min(1),
|
||||
signingOrder: z.number().int().min(1),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
export type CreateTemplateInput = z.infer<typeof createTemplateSchema>;
|
||||
export type UpdateTemplateInput = z.infer<typeof updateTemplateSchema>;
|
||||
export type ListTemplatesInput = z.infer<typeof listTemplatesSchema>;
|
||||
export type GenerateInput = z.infer<typeof generateSchema>;
|
||||
export type GenerateAndSendInput = z.infer<typeof generateAndSendSchema>;
|
||||
export type GenerateAndSignInput = z.infer<typeof generateAndSignSchema>;
|
||||
|
||||
// ─── TipTap-based Admin Template Schemas ─────────────────────────────────────
|
||||
// Used by /api/v1/admin/templates — the TipTap JSON document store.
|
||||
|
||||
export const tiptapDocumentTypes = [
|
||||
'eoi',
|
||||
'contract',
|
||||
'nda',
|
||||
'reservation_agreement',
|
||||
'letter',
|
||||
'other',
|
||||
] as const;
|
||||
|
||||
export const createAdminTemplateSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
type: z.enum(tiptapDocumentTypes),
|
||||
content: z.record(z.unknown()), // TipTap JSON document
|
||||
});
|
||||
|
||||
export const updateAdminTemplateSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
content: z.record(z.unknown()).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const previewAdminTemplateSchema = z.object({
|
||||
content: z.record(z.unknown()),
|
||||
sampleData: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const rollbackAdminTemplateSchema = z.object({
|
||||
version: z.number().int().min(1),
|
||||
});
|
||||
|
||||
export const listAdminTemplatesSchema = baseListQuerySchema.extend({
|
||||
type: z.enum(tiptapDocumentTypes).optional(),
|
||||
isActive: z
|
||||
.enum(['true', 'false'])
|
||||
.transform((v) => v === 'true')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type CreateAdminTemplateInput = z.infer<typeof createAdminTemplateSchema>;
|
||||
export type UpdateAdminTemplateInput = z.infer<typeof updateAdminTemplateSchema>;
|
||||
export type PreviewAdminTemplateInput = z.infer<typeof previewAdminTemplateSchema>;
|
||||
export type RollbackAdminTemplateInput = z.infer<typeof rollbackAdminTemplateSchema>;
|
||||
export type ListAdminTemplatesInput = z.infer<typeof listAdminTemplatesSchema>;
|
||||
38
src/lib/validators/documents.ts
Normal file
38
src/lib/validators/documents.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
import { DOCUMENT_TYPES, DOCUMENT_STATUSES } from '@/lib/constants';
|
||||
|
||||
export const createDocumentSchema = z.object({
|
||||
interestId: z.string().optional(),
|
||||
clientId: z.string().optional(),
|
||||
documentType: z.enum(DOCUMENT_TYPES),
|
||||
title: z.string().min(1).max(200),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
export const updateDocumentSchema = z.object({
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
notes: z.string().optional(),
|
||||
status: z.enum(DOCUMENT_STATUSES).optional(),
|
||||
});
|
||||
|
||||
export const listDocumentsSchema = baseListQuerySchema.extend({
|
||||
interestId: z.string().optional(),
|
||||
clientId: z.string().optional(),
|
||||
documentType: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
});
|
||||
|
||||
export const generateEoiSchema = z.object({
|
||||
interestId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const uploadSignedSchema = z.object({
|
||||
documentId: z.string().min(1),
|
||||
});
|
||||
|
||||
export type CreateDocumentInput = z.infer<typeof createDocumentSchema>;
|
||||
export type UpdateDocumentInput = z.infer<typeof updateDocumentSchema>;
|
||||
export type ListDocumentsInput = z.infer<typeof listDocumentsSchema>;
|
||||
export type GenerateEoiInput = z.infer<typeof generateEoiSchema>;
|
||||
37
src/lib/validators/email.ts
Normal file
37
src/lib/validators/email.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const connectAccountSchema = z.object({
|
||||
provider: z.enum(['google', 'outlook', 'custom']),
|
||||
emailAddress: z.string().email(),
|
||||
smtpHost: z.string().min(1),
|
||||
smtpPort: z.number().int().positive(),
|
||||
imapHost: z.string().min(1),
|
||||
imapPort: z.number().int().positive(),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const toggleAccountSchema = z.object({
|
||||
isActive: z.boolean(),
|
||||
});
|
||||
|
||||
export const composeEmailSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
threadId: z.string().uuid().optional(),
|
||||
to: z.array(z.string().email()).min(1),
|
||||
cc: z.array(z.string().email()).optional(),
|
||||
subject: z.string().min(1),
|
||||
bodyHtml: z.string().min(1),
|
||||
inReplyToMessageId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const listThreadsSchema = z.object({
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20),
|
||||
clientId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export type ConnectAccountInput = z.infer<typeof connectAccountSchema>;
|
||||
export type ToggleAccountInput = z.infer<typeof toggleAccountSchema>;
|
||||
export type ComposeEmailInput = z.infer<typeof composeEmailSchema>;
|
||||
export type ListThreadsInput = z.infer<typeof listThreadsSchema>;
|
||||
34
src/lib/validators/expenses.ts
Normal file
34
src/lib/validators/expenses.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
import { EXPENSE_CATEGORIES, PAYMENT_METHODS } from '@/lib/constants';
|
||||
|
||||
export const createExpenseSchema = z.object({
|
||||
establishmentName: z.string().max(200).optional(),
|
||||
amount: z.coerce.number().positive(),
|
||||
currency: z.string().length(3).default('USD'),
|
||||
paymentMethod: z.enum(PAYMENT_METHODS).optional(),
|
||||
category: z.enum(EXPENSE_CATEGORIES).optional(),
|
||||
payer: z.string().max(200).optional(),
|
||||
expenseDate: z.coerce.date(),
|
||||
description: z.string().max(2000).optional(),
|
||||
receiptFileIds: z.array(z.string()).optional(),
|
||||
paymentStatus: z.enum(['unpaid', 'paid', 'partial']).default('unpaid'),
|
||||
paymentDate: z.string().optional(),
|
||||
paymentReference: z.string().optional(),
|
||||
paymentNotes: z.string().optional(),
|
||||
});
|
||||
|
||||
export const updateExpenseSchema = createExpenseSchema.partial();
|
||||
|
||||
export const listExpensesSchema = baseListQuerySchema.extend({
|
||||
category: z.string().optional(),
|
||||
paymentStatus: z.string().optional(),
|
||||
dateFrom: z.string().optional(),
|
||||
dateTo: z.string().optional(),
|
||||
currency: z.string().optional(),
|
||||
payer: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CreateExpenseInput = z.infer<typeof createExpenseSchema>;
|
||||
export type UpdateExpenseInput = z.infer<typeof updateExpenseSchema>;
|
||||
export type ListExpensesInput = z.infer<typeof listExpensesSchema>;
|
||||
27
src/lib/validators/files.ts
Normal file
27
src/lib/validators/files.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
|
||||
export const uploadFileSchema = z.object({
|
||||
filename: z.string().min(1).max(255),
|
||||
clientId: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
entityType: z.string().optional(),
|
||||
entityId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const updateFileSchema = z.object({
|
||||
filename: z.string().min(1).max(255).optional(),
|
||||
category: z.string().optional(),
|
||||
});
|
||||
|
||||
export const listFilesSchema = baseListQuerySchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
entityType: z.string().optional(),
|
||||
entityId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type UploadFileInput = z.infer<typeof uploadFileSchema>;
|
||||
export type UpdateFileInput = z.infer<typeof updateFileSchema>;
|
||||
export type ListFilesInput = z.infer<typeof listFilesSchema>;
|
||||
96
src/lib/validators/interests.ts
Normal file
96
src/lib/validators/interests.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
import { PIPELINE_STAGES, LEAD_CATEGORIES } from '@/lib/constants';
|
||||
|
||||
// ─── Create ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const createInterestSchema = z.object({
|
||||
clientId: z.string().min(1),
|
||||
berthId: z.string().optional(),
|
||||
pipelineStage: z.enum(PIPELINE_STAGES).default('open'),
|
||||
leadCategory: z.enum(LEAD_CATEGORIES).optional(),
|
||||
source: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
tagIds: z.array(z.string()).optional().default([]),
|
||||
reminderEnabled: z.boolean().optional().default(false),
|
||||
reminderDays: z.number().int().min(1).optional(),
|
||||
});
|
||||
|
||||
// ─── Update ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const updateInterestSchema = createInterestSchema
|
||||
.omit({ clientId: true, tagIds: true })
|
||||
.partial();
|
||||
|
||||
// ─── Change Stage ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const changeStageSchema = z.object({
|
||||
pipelineStage: z.enum(PIPELINE_STAGES),
|
||||
reason: z.string().optional(),
|
||||
});
|
||||
|
||||
// ─── List ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const listInterestsSchema = baseListQuerySchema.extend({
|
||||
clientId: z.string().optional(),
|
||||
berthId: z.string().optional(),
|
||||
pipelineStage: z
|
||||
.string()
|
||||
.transform((v) => v.split(',').filter(Boolean))
|
||||
.optional(),
|
||||
leadCategory: z.enum(LEAD_CATEGORIES).optional(),
|
||||
eoiStatus: z.string().optional(),
|
||||
tagIds: z
|
||||
.string()
|
||||
.transform((v) => v.split(',').filter(Boolean))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// ─── Waiting List ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const waitingListAddSchema = z.object({
|
||||
clientId: z.string().min(1),
|
||||
priority: z.enum(['normal', 'high']).default('normal'),
|
||||
notifyPref: z.enum(['email', 'in_app', 'both']).default('email'),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
// ─── Generate Recommendations ─────────────────────────────────────────────────
|
||||
|
||||
export const generateRecommendationsSchema = z.object({
|
||||
interestId: z.string().min(1),
|
||||
});
|
||||
|
||||
// ─── Public Interest ──────────────────────────────────────────────────────────
|
||||
|
||||
export const publicInterestSchema = z.object({
|
||||
fullName: z.string().min(1).max(200),
|
||||
email: z.string().email(),
|
||||
phone: z.string().optional(),
|
||||
companyName: z.string().optional(),
|
||||
yachtName: z.string().optional(),
|
||||
yachtLengthFt: z.coerce.number().positive().optional(),
|
||||
yachtWidthFt: z.coerce.number().positive().optional(),
|
||||
yachtDraftFt: z.coerce.number().positive().optional(),
|
||||
preferredBerthSize: z.string().optional(),
|
||||
source: z.literal('website').default('website'),
|
||||
notes: z.string().max(2000).optional(),
|
||||
});
|
||||
|
||||
// ─── Reorder Waiting List ─────────────────────────────────────────────────────
|
||||
|
||||
export const reorderWaitingListSchema = z.object({
|
||||
entryId: z.string().min(1),
|
||||
newPosition: z.coerce.number().int().min(1),
|
||||
});
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type CreateInterestInput = z.infer<typeof createInterestSchema>;
|
||||
export type UpdateInterestInput = z.infer<typeof updateInterestSchema>;
|
||||
export type ChangeStageInput = z.infer<typeof changeStageSchema>;
|
||||
export type ListInterestsInput = z.infer<typeof listInterestsSchema>;
|
||||
export type WaitingListAddInput = z.infer<typeof waitingListAddSchema>;
|
||||
export type PublicInterestInput = z.infer<typeof publicInterestSchema>;
|
||||
export type ReorderWaitingListInput = z.infer<typeof reorderWaitingListSchema>;
|
||||
71
src/lib/validators/invoices.ts
Normal file
71
src/lib/validators/invoices.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { z } from 'zod';
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
|
||||
export const createInvoiceSchema = z
|
||||
.object({
|
||||
clientName: z.string().min(1).max(200),
|
||||
billingEmail: z.string().email().optional(),
|
||||
billingAddress: z.string().max(500).optional(),
|
||||
dueDate: z.string().min(1),
|
||||
paymentTerms: z
|
||||
.enum(['immediate', 'net10', 'net15', 'net30', 'net45', 'net60'])
|
||||
.default('net30'),
|
||||
currency: z.string().length(3).default('USD'),
|
||||
notes: z.string().max(2000).optional(),
|
||||
lineItems: z
|
||||
.array(
|
||||
z.object({
|
||||
description: z.string().min(1),
|
||||
quantity: z.coerce.number().positive().default(1),
|
||||
unitPrice: z.coerce.number().min(0),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
expenseIds: z.array(z.string()).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.lineItems && data.lineItems.length > 0) ||
|
||||
(data.expenseIds && data.expenseIds.length > 0),
|
||||
{ message: 'Invoice must have at least one line item or linked expense' },
|
||||
);
|
||||
|
||||
export const updateInvoiceSchema = z.object({
|
||||
clientName: z.string().min(1).max(200).optional(),
|
||||
billingEmail: z.string().email().optional(),
|
||||
billingAddress: z.string().max(500).optional(),
|
||||
dueDate: z.string().min(1).optional(),
|
||||
paymentTerms: z
|
||||
.enum(['immediate', 'net10', 'net15', 'net30', 'net45', 'net60'])
|
||||
.optional(),
|
||||
currency: z.string().length(3).optional(),
|
||||
notes: z.string().max(2000).optional(),
|
||||
lineItems: z
|
||||
.array(
|
||||
z.object({
|
||||
description: z.string().min(1),
|
||||
quantity: z.coerce.number().positive().default(1),
|
||||
unitPrice: z.coerce.number().min(0),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
expenseIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const recordPaymentSchema = z.object({
|
||||
paymentDate: z.string().min(1),
|
||||
paymentMethod: z.string().optional(),
|
||||
paymentReference: z.string().optional(),
|
||||
});
|
||||
|
||||
export const listInvoicesSchema = baseListQuerySchema.extend({
|
||||
status: z.string().optional(),
|
||||
clientName: z.string().optional(),
|
||||
dateFrom: z.string().optional(),
|
||||
dateTo: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CreateInvoiceInput = z.infer<typeof createInvoiceSchema>;
|
||||
export type UpdateInvoiceInput = z.infer<typeof updateInvoiceSchema>;
|
||||
export type RecordPaymentInput = z.infer<typeof recordPaymentSchema>;
|
||||
export type ListInvoicesInput = z.infer<typeof listInvoicesSchema>;
|
||||
12
src/lib/validators/notes.ts
Normal file
12
src/lib/validators/notes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createNoteSchema = z.object({
|
||||
content: z.string().min(1),
|
||||
});
|
||||
|
||||
export const updateNoteSchema = z.object({
|
||||
content: z.string().min(1),
|
||||
});
|
||||
|
||||
export type CreateNoteInput = z.infer<typeof createNoteSchema>;
|
||||
export type UpdateNoteInput = z.infer<typeof updateNoteSchema>;
|
||||
20
src/lib/validators/notifications.ts
Normal file
20
src/lib/validators/notifications.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const listNotificationsSchema = z.object({
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20),
|
||||
unreadOnly: z.coerce.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const updatePreferencesSchema = z.object({
|
||||
preferences: z.array(
|
||||
z.object({
|
||||
notificationType: z.string(),
|
||||
inApp: z.boolean(),
|
||||
email: z.boolean(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type ListNotificationsInput = z.infer<typeof listNotificationsSchema>;
|
||||
export type UpdatePreferencesInput = z.infer<typeof updatePreferencesSchema>;
|
||||
22
src/lib/validators/reports.ts
Normal file
22
src/lib/validators/reports.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const requestReportSchema = z.object({
|
||||
reportType: z.enum(['pipeline', 'revenue', 'activity', 'occupancy']),
|
||||
name: z.string().min(1).max(200),
|
||||
parameters: z
|
||||
.object({
|
||||
dateFrom: z.string().optional(),
|
||||
dateTo: z.string().optional(),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
});
|
||||
|
||||
export const listReportsSchema = z.object({
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20),
|
||||
status: z.enum(['queued', 'processing', 'ready', 'failed']).optional(),
|
||||
});
|
||||
|
||||
export type RequestReportInput = z.infer<typeof requestReportSchema>;
|
||||
export type ListReportsInput = z.infer<typeof listReportsSchema>;
|
||||
21
src/lib/validators/saved-views.ts
Normal file
21
src/lib/validators/saved-views.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createSavedViewSchema = z.object({
|
||||
entityType: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
filters: z.record(z.unknown()).default({}),
|
||||
sortConfig: z
|
||||
.object({
|
||||
field: z.string(),
|
||||
direction: z.enum(['asc', 'desc']),
|
||||
})
|
||||
.optional(),
|
||||
columnConfig: z.record(z.unknown()).optional(),
|
||||
isShared: z.boolean().optional().default(false),
|
||||
isDefault: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const updateSavedViewSchema = createSavedViewSchema.partial();
|
||||
|
||||
export type CreateSavedViewInput = z.infer<typeof createSavedViewSchema>;
|
||||
export type UpdateSavedViewInput = z.infer<typeof updateSavedViewSchema>;
|
||||
7
src/lib/validators/search.ts
Normal file
7
src/lib/validators/search.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const searchQuerySchema = z.object({
|
||||
q: z.string().min(2).max(200),
|
||||
});
|
||||
|
||||
export type SearchQuery = z.infer<typeof searchQuerySchema>;
|
||||
15
src/lib/validators/tags.ts
Normal file
15
src/lib/validators/tags.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createTagSchema = z.object({
|
||||
name: z.string().min(1).max(50),
|
||||
color: z
|
||||
.string()
|
||||
.regex(/^#[0-9A-Fa-f]{6}$/, 'Color must be a valid hex color (e.g. #6B7280)')
|
||||
.optional()
|
||||
.default('#6B7280'),
|
||||
});
|
||||
|
||||
export const updateTagSchema = createTagSchema.partial();
|
||||
|
||||
export type CreateTagInput = z.infer<typeof createTagSchema>;
|
||||
export type UpdateTagInput = z.infer<typeof updateTagSchema>;
|
||||
43
src/lib/validators/webhooks.ts
Normal file
43
src/lib/validators/webhooks.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
||||
import { WEBHOOK_EVENTS } from '@/lib/services/webhook-event-map';
|
||||
|
||||
// ─── Create ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const createWebhookSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
url: z.string().url('Must be a valid HTTPS URL').refine(
|
||||
(u) => u.startsWith('https://'),
|
||||
'Webhook URL must use HTTPS',
|
||||
),
|
||||
events: z
|
||||
.array(z.enum(WEBHOOK_EVENTS))
|
||||
.min(1, 'At least one event must be selected'),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
// ─── Update ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const updateWebhookSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
url: z
|
||||
.string()
|
||||
.url('Must be a valid HTTPS URL')
|
||||
.refine((u) => u.startsWith('https://'), 'Webhook URL must use HTTPS')
|
||||
.optional(),
|
||||
events: z.array(z.enum(WEBHOOK_EVENTS)).min(1).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// ─── List Deliveries ──────────────────────────────────────────────────────────
|
||||
|
||||
export const listDeliveriesSchema = baseListQuerySchema.extend({
|
||||
status: z.enum(['pending', 'success', 'failed', 'dead_letter']).optional(),
|
||||
});
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type CreateWebhookInput = z.infer<typeof createWebhookSchema>;
|
||||
export type UpdateWebhookInput = z.infer<typeof updateWebhookSchema>;
|
||||
export type ListDeliveriesInput = z.infer<typeof listDeliveriesSchema>;
|
||||
Reference in New Issue
Block a user