Files
pn-new-crm/src/lib/validators/residential.ts
Matt Ciaccio 8699f81879
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped
chore(style): codebase em-dash sweep + minor layout polish
Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).

Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
  pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
  port switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:57:01 +02:00

123 lines
5.3 KiB
TypeScript

import { z } from 'zod';
import { baseListQuerySchema } from '@/lib/api/route-helpers';
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']).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']).optional(),
});
// ─── Residential interest ────────────────────────────────────────────────────
export const PIPELINE_STAGES = [
'new',
'contacted',
'viewing_scheduled',
'offer_made',
'offer_accepted',
'closed_won',
'closed_lost',
] as const;
export const createResidentialInterestSchema = z.object({
residentialClientId: z.string().min(1),
pipelineStage: z.enum(PIPELINE_STAGES).optional().default('new'),
source: z.enum(['website', 'manual', 'referral', 'broker']).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: z.enum(PIPELINE_STAGES).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>;