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>
2026-03-26 11:52:51 +01:00
|
|
|
import { z } from 'zod';
|
|
|
|
|
import { BERTH_STATUSES } from '@/lib/constants';
|
|
|
|
|
import { baseListQuerySchema } from '@/lib/api/route-helpers';
|
|
|
|
|
|
Add user settings, audit log, berth CRUD, and missing endpoints
- PATCH /api/v1/me: self-service profile update (name, phone, timezone)
- User settings page with profile editor + notification preferences
- Audit log API with filtering (entity, action, user, date range)
- Audit log page with search, entity type, and action filters
- Berth create/delete: POST /api/v1/berths + DELETE /api/v1/berths/[id]
- Client duplicates endpoint: GET /api/v1/clients/duplicates?name=
- Replace settings and audit stub pages with real implementations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:45:56 -04:00
|
|
|
// ─── Create Berth ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const createBerthSchema = z.object({
|
|
|
|
|
mooringNumber: z.string().min(1),
|
|
|
|
|
area: z.string().min(1),
|
|
|
|
|
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(),
|
|
|
|
|
price: z.coerce.number().optional(),
|
|
|
|
|
priceCurrency: z.string().optional(),
|
|
|
|
|
status: z.enum(BERTH_STATUSES).default('available'),
|
|
|
|
|
tenureType: z.enum(['permanent', 'fixed_term']).optional(),
|
|
|
|
|
mooringType: z.string().optional(),
|
|
|
|
|
powerCapacity: z.string().optional(),
|
|
|
|
|
voltage: z.string().optional(),
|
|
|
|
|
access: z.string().optional(),
|
|
|
|
|
bowFacing: z.string().optional(),
|
|
|
|
|
sidePontoon: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export type CreateBerthInput = z.infer<typeof createBerthSchema>;
|
|
|
|
|
|
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>
2026-03-26 11:52:51 +01:00
|
|
|
// ─── 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>;
|