task-d.6.1

This commit is contained in:
phaichayon
2026-07-01 10:29:00 +07:00
parent 6d6cd3a6df
commit 0a144f0f9e
14 changed files with 364 additions and 129 deletions

View File

@@ -426,11 +426,12 @@ export const crmLeads = pgTable(
description: text('description'),
projectName: text('project_name'),
projectLocation: text('project_location'),
leadChannel: text('lead_channel'),
productType: text('product_type'),
priority: text('priority'),
estimatedValue: doublePrecision('estimated_value'),
awarenessId: text('awareness_id'),
status: text('status').default('new_job').notNull(),
status: text('status').notNull(),
followupStatus: text('followup_status'),
lostReason: text('lost_reason'),
outcome: text('outcome').default('open').notNull(),

View File

@@ -94,6 +94,7 @@ export function LeadDetail({
<FieldItem label='Contact' value={lead.contactName} />
<FieldItem label='Project Name' value={lead.projectName} />
<FieldItem label='Project Location' value={lead.projectLocation} />
<FieldItem label='Lead Source' value={lead.leadChannelLabel} />
<FieldItem label='Awareness' value={lead.awarenessLabel} />
<FieldItem label='Priority' value={lead.priority} />
<FieldItem label='Marketing Owner' value={lead.ownerMarketingName} />

View File

@@ -1,32 +1,39 @@
'use client';
"use client";
import * as React from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Icons } from '@/components/icons';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle
} from '@/components/ui/sheet';
import { useAppForm, useFormFields } from '@/components/ui/tanstack-form';
import { createLeadMutation, updateLeadMutation } from '../api/mutations';
import type { LeadReferenceData, LeadSummary } from '../api/types';
import { leadSchema } from '../schemas/lead.schema';
SheetTitle,
} from "@/components/ui/sheet";
import { useAppForm, useFormFields } from "@/components/ui/tanstack-form";
import { createLeadMutation, updateLeadMutation } from "../api/mutations";
import type { LeadReferenceData, LeadSummary } from "../api/types";
import { leadFormSchema } from "../schemas/lead.schema";
type LeadFormValues = {
type LeadFormValues = Record<string, unknown> & {
branchId: string | null;
customerId: string | null;
contactId: string | null;
description: string | null;
projectName: string | null;
projectLocation: string | null;
leadChannel: string | null;
productType: string | null;
priority: string | null;
estimatedValue: number | null;
@@ -34,11 +41,22 @@ type LeadFormValues = {
status: string;
followupStatus: string | null;
lostReason: string | null;
outcome: 'open' | 'won' | 'lost';
outcome: "open" | "won" | "lost";
ownerMarketingUserId: string | null;
};
function toDefaultValues(lead: LeadSummary | undefined, referenceData: LeadReferenceData): LeadFormValues {
function getNewJobStatusId(referenceData: LeadReferenceData) {
return (
referenceData.statuses.find((item) => item.code === "new_job")?.id ??
referenceData.statuses[0]?.id ??
""
);
}
function toDefaultValues(
lead: LeadSummary | undefined,
referenceData: LeadReferenceData,
): LeadFormValues {
return {
branchId: lead?.branchId ?? referenceData.branches[0]?.id ?? null,
customerId: lead?.customerId ?? null,
@@ -46,15 +64,19 @@ function toDefaultValues(lead: LeadSummary | undefined, referenceData: LeadRefer
description: lead?.description ?? null,
projectName: lead?.projectName ?? null,
projectLocation: lead?.projectLocation ?? null,
leadChannel: lead?.leadChannel ?? null,
productType: lead?.productType ?? referenceData.productTypes[0]?.id ?? null,
priority: lead?.priority ?? referenceData.priorities[0]?.id ?? null,
estimatedValue: lead?.estimatedValue ?? null,
awarenessId: lead?.awarenessId ?? referenceData.awarenesses[0]?.id ?? null,
status: lead?.status ?? referenceData.statuses[0]?.id ?? '',
status: lead?.status ?? getNewJobStatusId(referenceData),
followupStatus: lead?.followupStatus ?? null,
lostReason: lead?.lostReason ?? null,
outcome: (lead?.outcome as 'open' | 'won' | 'lost' | undefined) ?? 'open',
ownerMarketingUserId: lead?.ownerMarketingUserId ?? null
outcome:
lead?.outcome === "won" || lead?.outcome === "lost"
? lead.outcome
: "open",
ownerMarketingUserId: lead?.ownerMarketingUserId ?? null,
};
}
@@ -62,7 +84,7 @@ export function LeadForm({
open,
onOpenChange,
referenceData,
lead
lead,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -70,55 +92,73 @@ export function LeadForm({
lead?: LeadSummary;
}) {
const isEdit = Boolean(lead);
const defaultValues = useMemo(() => toDefaultValues(lead, referenceData), [lead, referenceData]);
const [selectedCustomerId, setSelectedCustomerId] = useState(defaultValues.customerId ?? '');
const { FormTextField, FormTextareaField, FormSelectField, FormCurrencyField } =
useFormFields<LeadFormValues>();
const [selectedCustomerId, setSelectedCustomerId] = useState(
lead?.customerId ?? "",
);
const defaultValues = useMemo(
() => toDefaultValues(lead, referenceData),
[lead, referenceData],
);
const {
FormCurrencyField,
FormSelectField,
FormTextField,
FormTextareaField,
} = useFormFields<LeadFormValues>();
const createMutation = useMutation({
...createLeadMutation,
onSuccess: () => {
toast.success('สร้างลีดสำเร็จ');
toast.success("Lead created successfully");
onOpenChange(false);
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : 'ไม่สามารถสร้างลีดได้');
}
toast.error(
error instanceof Error ? error.message : "Unable to create lead",
);
},
});
const updateMutation = useMutation({
...updateLeadMutation,
onSuccess: () => {
toast.success('อัปเดตลีดสำเร็จ');
toast.success("Lead updated successfully");
onOpenChange(false);
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : 'ไม่สามารถอัปเดตลีดได้');
}
toast.error(
error instanceof Error ? error.message : "Unable to update lead",
);
},
});
const contactsForCustomer = referenceData.contacts.filter(
(contact) => contact.customerId === selectedCustomerId
(contact) => contact.customerId === selectedCustomerId,
);
const form = useAppForm({
defaultValues,
validators: {
onSubmit: leadFormSchema,
},
onSubmit: async ({ value }) => {
const payload = {
...value,
branchId: value.branchId || null,
customerId: value.customerId || null,
contactId: value.contactId || null,
description: value.description || null,
projectName: value.projectName || null,
projectLocation: value.projectLocation || null,
leadChannel: value.leadChannel || null,
productType: value.productType || null,
priority: value.priority || null,
estimatedValue: value.estimatedValue ?? null,
awarenessId: value.awarenessId || null,
status: value.status,
followupStatus: value.followupStatus || null,
lostReason: value.lostReason || null,
ownerMarketingUserId: value.ownerMarketingUserId || null
outcome: value.outcome,
ownerMarketingUserId: value.ownerMarketingUserId || null,
};
if (isEdit && lead) {
@@ -127,47 +167,62 @@ export function LeadForm({
}
await createMutation.mutateAsync(payload);
}
},
});
useEffect(() => {
if (!open) return;
if (!open) {
return;
}
form.reset(defaultValues);
setSelectedCustomerId(defaultValues.customerId ?? '');
setSelectedCustomerId(defaultValues.customerId ?? "");
}, [defaultValues, form, open]);
const isPending = createMutation.isPending || updateMutation.isPending;
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className='flex w-full flex-col sm:max-w-3xl'>
<SheetContent className="flex w-full flex-col sm:max-w-3xl">
<SheetHeader>
<SheetTitle>{isEdit ? 'แก้ไขลีด' : 'สร้างลีด'}</SheetTitle>
<SheetDescription></SheetDescription>
<SheetTitle>{isEdit ? "Edit Lead" : "Create Lead"}</SheetTitle>
<SheetDescription>
Capture marketing lead information before assignment to the sales
workspace.
</SheetDescription>
</SheetHeader>
<div className='flex-1 overflow-auto'>
<div className="flex-1 overflow-auto">
<form.AppForm>
<form.Form id='lead-form' className='grid gap-4 md:grid-cols-2'>
<form.Form id="lead-form" className="grid gap-4 md:grid-cols-2">
<form.AppField
name='customerId'
name="customerId"
children={(field) => (
<field.FieldSet>
<field.Field>
<field.FieldLabel></field.FieldLabel>
<field.FieldLabel>Customer</field.FieldLabel>
<Select
value={field.state.value ?? ''}
value={
typeof field.state.value === "string"
? field.state.value
: "__none__"
}
onValueChange={(nextValue) => {
field.handleChange(nextValue === '__none__' ? null : nextValue);
const customerId =
nextValue === "__none__" ? null : nextValue;
field.handleChange(customerId);
field.handleBlur();
setSelectedCustomerId(nextValue === '__none__' ? '' : nextValue);
form.setFieldValue('contactId', null);
setSelectedCustomerId(customerId ?? "");
form.setFieldValue("contactId", null);
}}
>
<SelectTrigger aria-label='ลูกค้า'>
<SelectValue placeholder='เลือกลูกค้า' />
<SelectTrigger aria-label="Customer">
<SelectValue placeholder="Select customer" />
</SelectTrigger>
<SelectContent>
<SelectItem value='__none__'></SelectItem>
<SelectItem value="__none__">
No customer selected
</SelectItem>
{referenceData.customers.map((customer) => (
<SelectItem key={customer.id} value={customer.id}>
{customer.name} ({customer.code})
@@ -182,27 +237,35 @@ export function LeadForm({
/>
<form.AppField
name='contactId'
name="contactId"
children={(field) => (
<field.FieldSet>
<field.Field>
<field.FieldLabel></field.FieldLabel>
<field.FieldLabel>Contact</field.FieldLabel>
<Select
value={field.state.value ?? '__none__'}
value={
typeof field.state.value === "string"
? field.state.value
: "__none__"
}
onValueChange={(nextValue) => {
field.handleChange(nextValue === '__none__' ? null : nextValue);
field.handleChange(
nextValue === "__none__" ? null : nextValue,
);
field.handleBlur();
}}
>
<SelectTrigger aria-label='ผู้ติดต่อ'>
<SelectValue placeholder='เลือกผู้ติดต่อ' />
<SelectTrigger aria-label="Contact">
<SelectValue placeholder="Select contact" />
</SelectTrigger>
<SelectContent>
<SelectItem value='__none__'></SelectItem>
<SelectItem value="__none__">
No contact selected
</SelectItem>
{contactsForCustomer.map((contact) => (
<SelectItem key={contact.id} value={contact.id}>
{contact.name}
{contact.isPrimary ? ' (หลัก)' : ''}
{contact.isPrimary ? " (Primary)" : ""}
</SelectItem>
))}
</SelectContent>
@@ -212,95 +275,117 @@ export function LeadForm({
</field.FieldSet>
)}
/>
<FormTextField name='projectName' label='Project Name' />
<FormTextField name='projectLocation' label='Project Location' />
<FormTextareaField name='description' label='Description' className='md:col-span-2' />
<FormSelectField
name='branchId'
label='สาขา'
name="branchId"
label="Branch"
options={referenceData.branches.map((branch) => ({
value: branch.id,
label: branch.name
label: branch.name,
}))}
/>
<FormTextField name="projectName" label="Project Name" />
<FormTextField name="projectLocation" label="Project Location" />
<FormSelectField
name='productType'
label='Product Type'
name="productType"
label="Product Type"
options={referenceData.productTypes.map((item) => ({
value: item.id,
label: item.label
label: item.label,
}))}
/>
<FormSelectField
name='priority'
label='Priority'
options={referenceData.priorities.map((item) => ({
name="leadChannel"
label="Lead Source"
options={referenceData.leadChannels.map((item) => ({
value: item.id,
label: item.label
label: item.label,
}))}
/>
<FormCurrencyField name='estimatedValue' label='Estimated Value' currencyLabel='THB' />
<FormSelectField
name='awarenessId'
label='Awareness'
name="awarenessId"
label="Awareness"
options={referenceData.awarenesses.map((item) => ({
value: item.id,
label: item.label
label: item.label,
}))}
/>
<FormSelectField
name='status'
label='Status'
options={referenceData.statuses.map((item) => ({
name="priority"
label="Priority"
options={referenceData.priorities.map((item) => ({
value: item.id,
label: item.label
label: item.label,
}))}
/>
<FormCurrencyField
name="estimatedValue"
label="Estimated Value"
currencyLabel="THB"
/>
<FormSelectField
name='followupStatus'
label='Follow-up Status'
name="followupStatus"
label="Follow-up Status"
options={referenceData.followupStatuses.map((item) => ({
value: item.id,
label: item.label
}))}
/>
<FormSelectField
name='lostReason'
label='Lost Reason'
options={referenceData.lostReasons.map((item) => ({
value: item.id,
label: item.label
}))}
/>
<FormSelectField
name='ownerMarketingUserId'
label='Marketing Owner'
options={referenceData.assignableUsers.map((user) => ({
value: user.id,
label: user.name
label: item.label,
}))}
/>
{isEdit ? (
<>
<FormSelectField
name="status"
label="Status"
options={referenceData.statuses.map((item) => ({
value: item.id,
label: item.label,
}))}
/>
<FormSelectField
name="lostReason"
label="Lost Reason"
options={referenceData.lostReasons.map((item) => ({
value: item.id,
label: item.label,
}))}
/>
<FormSelectField
name="ownerMarketingUserId"
label="Marketing Owner"
options={referenceData.assignableUsers.map((user) => ({
value: user.id,
label: user.name,
}))}
/>
</>
) : null}
<div className="md:col-span-2">
<FormTextareaField
name="description"
label="Description"
size="md"
/>
</div>
</form.Form>
</form.AppForm>
</div>
<SheetFooter>
<Button
variant='outline'
variant="outline"
onClick={() => onOpenChange(false)}
disabled={createMutation.isPending || updateMutation.isPending}
disabled={isPending}
>
Cancel
</Button>
<Button
type='submit'
form='lead-form'
isLoading={createMutation.isPending || updateMutation.isPending}
>
<Icons.add className='mr-2 h-4 w-4' />
{isEdit ? 'อัปเดตลีด' : 'สร้างลีด'}
<Button type="submit" form="lead-form" isLoading={isPending}>
<Icons.add className="mr-2 h-4 w-4" />
{isEdit ? "Update Lead" : "Create Lead"}
</Button>
</SheetFooter>
</SheetContent>
@@ -308,16 +393,24 @@ export function LeadForm({
);
}
export function LeadFormTrigger({ referenceData }: { referenceData: LeadReferenceData }) {
export function LeadFormTrigger({
referenceData,
}: {
referenceData: LeadReferenceData;
}) {
const [open, setOpen] = React.useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>
<Icons.add className='mr-2 h-4 w-4' />
<Icons.add className="mr-2 h-4 w-4" />
Add Lead
</Button>
<LeadForm open={open} onOpenChange={setOpen} referenceData={referenceData} />
<LeadForm
open={open}
onOpenChange={setOpen}
referenceData={referenceData}
/>
</>
);
}

View File

@@ -7,17 +7,37 @@ export const leadSchema = z.object({
description: z.string().nullable().optional(),
projectName: z.string().nullable().optional(),
projectLocation: z.string().nullable().optional(),
leadChannel: z.string().nullable().optional(),
productType: z.string().nullable().optional(),
priority: z.string().nullable().optional(),
estimatedValue: z.number().nullable().optional(),
awarenessId: z.string().nullable().optional(),
status: z.string().min(1, 'Please select lead status'),
status: z.string().min(1, 'Please select lead status').default('new_job'),
followupStatus: z.string().nullable().optional(),
lostReason: z.string().nullable().optional(),
outcome: z.enum(['open', 'won', 'lost']).default('open'),
ownerMarketingUserId: z.string().nullable().optional()
});
export const leadFormSchema = z.object({
branchId: z.string().nullable(),
customerId: z.string().nullable(),
contactId: z.string().nullable(),
description: z.string().nullable(),
projectName: z.string().nullable(),
projectLocation: z.string().nullable(),
leadChannel: z.string().nullable(),
productType: z.string().nullable(),
priority: z.string().nullable(),
estimatedValue: z.number().nullable(),
awarenessId: z.string().nullable(),
status: z.string().min(1, 'Please select lead status'),
followupStatus: z.string().nullable(),
lostReason: z.string().nullable(),
outcome: z.enum(['open', 'won', 'lost']),
ownerMarketingUserId: z.string().nullable()
});
export const updateLeadSchema = leadSchema.partial();
export const assignLeadSchema = z.object({

View File

@@ -52,6 +52,7 @@ import type {
const LEAD_OPTION_CATEGORIES = {
awareness: 'crm_lead_awareness',
leadChannel: 'crm_lead_channel',
status: 'crm_lead_status',
followupStatus: 'crm_lead_followup_status',
lostReason: 'crm_lead_lost_reason',
@@ -65,6 +66,7 @@ type LeadRow = typeof crmLeads.$inferSelect;
type LeadLookupMaps = {
awarenessLabels: Map<string, string>;
leadChannelLabels: Map<string, string>;
statusLabels: Map<string, string>;
followupStatusLabels: Map<string, string>;
lostReasonLabels: Map<string, string>;
@@ -301,6 +303,10 @@ function mapLeadSummary(row: LeadRow, lookups: LeadLookupMaps): LeadSummary {
description: row.description ?? null,
projectName: row.projectName ?? null,
projectLocation: row.projectLocation ?? null,
leadChannel: row.leadChannel ?? null,
leadChannelLabel: row.leadChannel
? (lookups.leadChannelLabels.get(row.leadChannel) ?? null)
: null,
productType: row.productType ?? null,
priority: row.priority ?? null,
estimatedValue: row.estimatedValue ?? null,
@@ -394,6 +400,15 @@ async function resolveValidOptionIds(category: string, organizationId: string) {
return new Set(options.map((option) => option.id));
}
async function resolveOptionIdByCode(
organizationId: string,
category: string,
code: string
) {
const options = await getActiveOptionsByCategory(category, { organizationId });
return options.find((option) => option.code === code)?.id ?? null;
}
async function assertMasterOptionValue(
organizationId: string,
category: string,
@@ -535,9 +550,10 @@ async function assertLeadBelongsToOrganization(
}
async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise<LeadLookupMaps> {
const [awarenesses, statuses, followupStatuses, lostReasons, productTypes, priorities] =
const [awarenesses, leadChannels, statuses, followupStatuses, lostReasons, productTypes, priorities] =
await Promise.all([
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.leadChannel, { organizationId }),
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
@@ -595,6 +611,7 @@ async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise
return {
awarenessLabels: new Map(awarenesses.map((item) => [item.id, item.label])),
leadChannelLabels: new Map(leadChannels.map((item) => [item.id, item.label])),
statusLabels: new Map(statuses.map((item) => [item.id, item.label])),
followupStatusLabels: new Map(followupStatuses.map((item) => [item.id, item.label])),
lostReasonLabels: new Map(lostReasons.map((item) => [item.id, item.label])),
@@ -615,6 +632,11 @@ async function validateLeadPayload(
) {
await Promise.all([
assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.awareness, payload.awarenessId),
assertMasterOptionValue(
organizationId,
LEAD_OPTION_CATEGORIES.leadChannel,
payload.leadChannel
),
assertMasterOptionValue(
organizationId,
LEAD_OPTION_CATEGORIES.productType,
@@ -678,6 +700,7 @@ async function validateLeadPayload(
export async function getLeadReferenceData(organizationId: string): Promise<LeadReferenceData> {
const [
awarenesses,
leadChannels,
statuses,
followupStatuses,
lostReasons,
@@ -690,6 +713,7 @@ export async function getLeadReferenceData(organizationId: string): Promise<Lead
] =
await Promise.all([
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.leadChannel, { organizationId }),
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
@@ -703,6 +727,7 @@ export async function getLeadReferenceData(organizationId: string): Promise<Lead
return {
awarenesses: awarenesses.map(mapOption),
leadChannels: leadChannels.map(mapOption),
statuses: statuses.map(mapOption),
followupStatuses: followupStatuses.map(mapOption),
lostReasons: lostReasons.map(mapOption),
@@ -806,6 +831,14 @@ export async function createLead(
accessContext?: LeadAccessContext
): Promise<LeadDetail> {
await validateLeadPayload(organizationId, userId, payload, accessContext);
const defaultStatusId =
(await resolveOptionIdByCode(organizationId, LEAD_OPTION_CATEGORIES.status, 'new_job')) ??
payload.status;
if (!defaultStatusId) {
throw new AuthError('Lead status option new_job is not configured', 400);
}
const codeResult = await generateNextDocumentCode({
organizationId,
documentType: 'crm_lead',
@@ -824,15 +857,16 @@ export async function createLead(
description: payload.description?.trim() || null,
projectName: payload.projectName?.trim() || null,
projectLocation: payload.projectLocation?.trim() || null,
leadChannel: payload.leadChannel ?? null,
productType: payload.productType ?? null,
priority: payload.priority ?? null,
estimatedValue: payload.estimatedValue ?? null,
awarenessId: payload.awarenessId ?? null,
status: payload.status,
status: defaultStatusId,
followupStatus: payload.followupStatus ?? null,
lostReason: payload.lostReason ?? null,
outcome: normalizeLeadOutcome(payload.outcome),
ownerMarketingUserId: payload.ownerMarketingUserId ?? userId,
lostReason: null,
outcome: 'open',
ownerMarketingUserId: userId,
createdBy: userId
})
.returning();
@@ -859,6 +893,7 @@ export async function updateLead(
projectName: payload.projectName === undefined ? current.projectName : payload.projectName,
projectLocation:
payload.projectLocation === undefined ? current.projectLocation : payload.projectLocation,
leadChannel: payload.leadChannel === undefined ? current.leadChannel : payload.leadChannel,
productType: payload.productType === undefined ? current.productType : payload.productType,
priority: payload.priority === undefined ? current.priority : payload.priority,
estimatedValue:
@@ -886,6 +921,7 @@ export async function updateLead(
description: nextPayload.description?.trim() || null,
projectName: nextPayload.projectName?.trim() || null,
projectLocation: nextPayload.projectLocation?.trim() || null,
leadChannel: nextPayload.leadChannel ?? null,
productType: nextPayload.productType ?? null,
priority: nextPayload.priority ?? null,
estimatedValue: nextPayload.estimatedValue ?? null,

View File

@@ -51,6 +51,7 @@ export type LeadActivityRecord = CrmAuditLogRecord;
export interface LeadReferenceData {
awarenesses: LeadOption[];
leadChannels: LeadOption[];
statuses: LeadOption[];
followupStatuses: LeadOption[];
lostReasons: LeadOption[];
@@ -69,11 +70,12 @@ export interface CreateLeadInput {
description?: string | null;
projectName?: string | null;
projectLocation?: string | null;
leadChannel?: string | null;
productType?: string | null;
priority?: string | null;
estimatedValue?: number | null;
awarenessId?: string | null;
status: string;
status?: string;
followupStatus?: string | null;
lostReason?: string | null;
outcome?: 'open' | 'won' | 'lost';
@@ -87,6 +89,7 @@ export interface UpdateLeadInput {
description?: string | null;
projectName?: string | null;
projectLocation?: string | null;
leadChannel?: string | null;
productType?: string | null;
priority?: string | null;
estimatedValue?: number | null;
@@ -137,6 +140,8 @@ export interface LeadSummary {
description: string | null;
projectName: string | null;
projectLocation: string | null;
leadChannel: string | null;
leadChannelLabel: string | null;
productType: string | null;
priority: string | null;
estimatedValue: number | null;
@@ -218,4 +223,3 @@ export interface LeadMutationSuccessResponse {
followup?: LeadFollowupSummary;
}
import type { CrmAuditLogRecord } from '@/features/crm/audit-log/types';