feat(clients): multi-contact support — co-buyers, auto-seed document signers from client contacts
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { clients } from "@/lib/db/schema";
|
||||
import type { ClientContact } from "@/lib/db/schema";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -13,14 +14,28 @@ const clientSchema = z.object({
|
||||
propertyAddress: z.string().optional(),
|
||||
});
|
||||
|
||||
/** Parse the hidden `contacts` JSON field from the form — returns null on invalid/empty */
|
||||
function parseContacts(raw: FormDataEntryValue | null): ClientContact[] | null {
|
||||
if (!raw || typeof raw !== 'string' || !raw.trim()) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
const valid = parsed.filter((c): c is ClientContact =>
|
||||
typeof c?.name === 'string' && typeof c?.email === 'string' &&
|
||||
c.name.trim() !== '' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(c.email)
|
||||
);
|
||||
return valid.length > 0 ? valid : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createClient(
|
||||
_prevState: { error?: string; success?: boolean } | null,
|
||||
formData: FormData
|
||||
): Promise<{ error?: string; success?: boolean }> {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
if (!session) return { error: "Unauthorized" };
|
||||
|
||||
const parsed = clientSchema.safeParse({
|
||||
name: formData.get("name"),
|
||||
@@ -28,14 +43,15 @@ export async function createClient(
|
||||
propertyAddress: formData.get("propertyAddress"),
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.issues[0].message };
|
||||
}
|
||||
if (!parsed.success) return { error: parsed.error.issues[0].message };
|
||||
|
||||
const contacts = parseContacts(formData.get("contacts"));
|
||||
|
||||
await db.insert(clients).values({
|
||||
name: parsed.data.name,
|
||||
email: parsed.data.email,
|
||||
propertyAddress: parsed.data.propertyAddress || null,
|
||||
contacts,
|
||||
});
|
||||
|
||||
revalidatePath("/portal/clients");
|
||||
@@ -48,9 +64,7 @@ export async function updateClient(
|
||||
formData: FormData
|
||||
): Promise<{ error?: string; success?: boolean }> {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
if (!session) return { error: "Unauthorized" };
|
||||
|
||||
const parsed = clientSchema.safeParse({
|
||||
name: formData.get("name"),
|
||||
@@ -58,13 +72,19 @@ export async function updateClient(
|
||||
propertyAddress: formData.get("propertyAddress"),
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.issues[0].message };
|
||||
}
|
||||
if (!parsed.success) return { error: parsed.error.issues[0].message };
|
||||
|
||||
const contacts = parseContacts(formData.get("contacts"));
|
||||
|
||||
await db
|
||||
.update(clients)
|
||||
.set({ name: parsed.data.name, email: parsed.data.email, propertyAddress: parsed.data.propertyAddress || null, updatedAt: new Date() })
|
||||
.set({
|
||||
name: parsed.data.name,
|
||||
email: parsed.data.email,
|
||||
propertyAddress: parsed.data.propertyAddress || null,
|
||||
contacts,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(clients.id, id));
|
||||
|
||||
revalidatePath("/portal/clients");
|
||||
@@ -76,12 +96,9 @@ export async function deleteClient(
|
||||
id: string
|
||||
): Promise<{ error?: string; success?: boolean }> {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
if (!session) return { error: "Unauthorized" };
|
||||
|
||||
await db.delete(clients).where(eq(clients.id, id));
|
||||
|
||||
revalidatePath("/portal/clients");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -66,10 +66,22 @@ export const documentStatusEnum = pgEnum("document_status", [
|
||||
"Signed",
|
||||
]);
|
||||
|
||||
/**
|
||||
* A single person on a client record (primary or co-buyer).
|
||||
* Primary contact uses the top-level clients.name / clients.email.
|
||||
* Additional contacts (co-buyers, co-sellers) live in clients.contacts[].
|
||||
*/
|
||||
export interface ClientContact {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export const clients = pgTable("clients", {
|
||||
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
/** Additional contacts on this client record (co-buyers, partners, etc.) */
|
||||
contacts: jsonb("contacts").$type<ClientContact[]>(),
|
||||
propertyAddress: text("property_address"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
|
||||
Reference in New Issue
Block a user