Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
142 lines
6.4 KiB
TypeScript
142 lines
6.4 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
import { baseListQuerySchema } from '@/lib/api/list-query';
|
|
import {
|
|
optionalCountryIsoSchema,
|
|
optionalIanaTimezoneSchema,
|
|
optionalPhoneE164Schema,
|
|
optionalSubdivisionIsoSchema,
|
|
} from '@/lib/validators/i18n';
|
|
|
|
// ─── Residential client ──────────────────────────────────────────────────────
|
|
|
|
export const createResidentialClientSchema = z.object({
|
|
fullName: z.string().min(1).max(200),
|
|
email: z
|
|
.string()
|
|
.email()
|
|
.optional()
|
|
.or(z.literal('').transform(() => undefined)),
|
|
phone: z.string().optional(),
|
|
/** E.164-normalized phone alongside the legacy free-text `phone`. */
|
|
phoneE164: optionalPhoneE164Schema.optional(),
|
|
/** ISO-3166-1 alpha-2 the phone was parsed against. */
|
|
phoneCountry: optionalCountryIsoSchema.optional(),
|
|
/** ISO-3166-1 alpha-2 nationality. */
|
|
nationalityIso: optionalCountryIsoSchema.optional(),
|
|
/** IANA timezone. */
|
|
timezone: optionalIanaTimezoneSchema.optional(),
|
|
placeOfResidence: z.string().optional(),
|
|
/** ISO-3166-1 alpha-2 country of residence. */
|
|
placeOfResidenceCountryIso: optionalCountryIsoSchema.optional(),
|
|
/** ISO 3166-2 subdivision code for place of residence. */
|
|
subdivisionIso: optionalSubdivisionIsoSchema.optional(),
|
|
preferredContactMethod: z.enum(['email', 'phone']).optional(),
|
|
status: z.enum(['prospect', 'active', 'inactive']).optional().default('prospect'),
|
|
source: z.enum(['website', 'manual', 'referral', 'broker', 'other']).optional(),
|
|
notes: z.string().optional(),
|
|
});
|
|
|
|
export const updateResidentialClientSchema = createResidentialClientSchema.partial();
|
|
|
|
export const listResidentialClientsSchema = baseListQuerySchema.extend({
|
|
status: z.enum(['prospect', 'active', 'inactive']).optional(),
|
|
source: z.enum(['website', 'manual', 'referral', 'broker', 'other']).optional(),
|
|
});
|
|
|
|
// ─── Residential interest ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Default pipeline stages - used as the fallback when a port hasn't
|
|
* configured its own list via the residential admin page. Mirror the
|
|
* legacy hard-coded set so existing data continues to validate.
|
|
*
|
|
* Per-port stages are stored in `system_settings.residential_pipeline_stages`
|
|
* (JSON array of stage ids). The validators below accept any string and
|
|
* defer the membership check to a runtime helper that reads the live
|
|
* stage list. This lets admins add/rename stages without a deploy.
|
|
*/
|
|
export const DEFAULT_RESIDENTIAL_PIPELINE_STAGES = [
|
|
'new',
|
|
'contacted',
|
|
'viewing_scheduled',
|
|
'offer_made',
|
|
'offer_accepted',
|
|
'closed_won',
|
|
'closed_lost',
|
|
] as const;
|
|
|
|
/** Backwards-compat alias kept for any existing imports. */
|
|
export const PIPELINE_STAGES = DEFAULT_RESIDENTIAL_PIPELINE_STAGES;
|
|
|
|
export const createResidentialInterestSchema = z.object({
|
|
residentialClientId: z.string().min(1),
|
|
pipelineStage: z.string().optional().default('new'),
|
|
source: z.enum(['website', 'manual', 'referral', 'broker', 'other']).optional(),
|
|
notes: z.string().optional(),
|
|
preferences: z.string().optional(),
|
|
assignedTo: z.string().optional(),
|
|
});
|
|
|
|
export const updateResidentialInterestSchema = createResidentialInterestSchema
|
|
.omit({ residentialClientId: true })
|
|
.partial();
|
|
|
|
export const listResidentialInterestsSchema = baseListQuerySchema.extend({
|
|
// pipelineStage accepts a string OR string[] so the FilterBar can filter
|
|
// multi-select. The legacy single-string form stays accepted so existing
|
|
// callers (e.g. residential-client-tabs) don't need to migrate.
|
|
pipelineStage: z.union([z.string(), z.array(z.string())]).optional(),
|
|
// Source filter - mirrors the main interest list. Comma-separated when
|
|
// submitted as a query string ("website,referral").
|
|
source: z.union([z.string(), z.array(z.string())]).optional(),
|
|
assignedTo: z.string().optional(),
|
|
residentialClientId: z.string().optional(),
|
|
});
|
|
|
|
// ─── Public website inquiry ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Shape posted by the public website's residential interest form. Coerces
|
|
* to internal create-shapes inside the public route.
|
|
*
|
|
* The legacy `phone` field stays free-text - older website builds may post
|
|
* raw international strings ('+44 7700 900123'). The route handler parses
|
|
* it server-side into `phoneE164` + `phoneCountry`. Newer website builds
|
|
* can post normalized values directly.
|
|
*/
|
|
export const publicResidentialInquirySchema = z.object({
|
|
firstName: z.string().min(1),
|
|
lastName: z.string().min(1),
|
|
email: z.string().email(),
|
|
phone: z.string().min(1),
|
|
/** Pre-normalized E.164 form, optional for backwards compat. */
|
|
phoneE164: optionalPhoneE164Schema.optional(),
|
|
/** ISO-3166-1 alpha-2 the phone was parsed against. */
|
|
phoneCountry: optionalCountryIsoSchema.optional(),
|
|
/** ISO-3166-1 alpha-2 nationality. */
|
|
nationalityIso: optionalCountryIsoSchema.optional(),
|
|
/** IANA timezone. */
|
|
timezone: optionalIanaTimezoneSchema.optional(),
|
|
placeOfResidence: z.string().optional(),
|
|
/** ISO-3166-1 alpha-2 country of residence. */
|
|
placeOfResidenceCountryIso: optionalCountryIsoSchema.optional(),
|
|
/** ISO 3166-2 subdivision code for place of residence. */
|
|
subdivisionIso: optionalSubdivisionIsoSchema.optional(),
|
|
preferredContactMethod: z.enum(['email', 'phone']).optional(),
|
|
notes: z.string().optional(),
|
|
preferences: z.string().optional(),
|
|
});
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
export type CreateResidentialClientInput = z.infer<typeof createResidentialClientSchema>;
|
|
export type UpdateResidentialClientInput = z.infer<typeof updateResidentialClientSchema>;
|
|
export type ListResidentialClientsInput = z.infer<typeof listResidentialClientsSchema>;
|
|
|
|
export type CreateResidentialInterestInput = z.infer<typeof createResidentialInterestSchema>;
|
|
export type UpdateResidentialInterestInput = z.infer<typeof updateResidentialInterestSchema>;
|
|
export type ListResidentialInterestsInput = z.infer<typeof listResidentialInterestsSchema>;
|
|
|
|
export type PublicResidentialInquiryInput = z.infer<typeof publicResidentialInquirySchema>;
|