task-d.5.3
This commit is contained in:
@@ -4,10 +4,14 @@ export const leadSchema = z.object({
|
||||
branchId: z.string().nullable().optional(),
|
||||
customerId: z.string().nullable().optional(),
|
||||
contactId: z.string().nullable().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
projectName: z.string().nullable().optional(),
|
||||
projectLocation: 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 a lead status'),
|
||||
status: z.string().min(1, 'Please select lead status'),
|
||||
followupStatus: z.string().nullable().optional(),
|
||||
lostReason: z.string().nullable().optional(),
|
||||
outcome: z.enum(['open', 'won', 'lost']).default('open'),
|
||||
@@ -16,9 +20,14 @@ export const leadSchema = z.object({
|
||||
|
||||
export const updateLeadSchema = leadSchema.partial();
|
||||
|
||||
export const assignLeadSchema = z.object({
|
||||
salesOwnerId: z.string().min(1, 'Sales owner is required'),
|
||||
assignmentRemark: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const leadFollowupSchema = z.object({
|
||||
followupDate: z.string().min(1, 'Follow-up date is required'),
|
||||
followupStatus: z.string().min(1, 'Please select a follow-up status'),
|
||||
followupDate: z.string().min(1, 'Follow-up date required'),
|
||||
followupStatus: z.string().min(1, 'Please select follow-up status'),
|
||||
note: z.string().nullable().optional(),
|
||||
nextFollowupDate: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
300
src/features/crm/leads/server/assignment.service.ts
Normal file
300
src/features/crm/leads/server/assignment.service.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { and, asc, eq, inArray, isNull } from 'drizzle-orm';
|
||||
import { crmEnquiries, crmLeads } from '@/db/schema';
|
||||
import { db } from '@/lib/db';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
|
||||
import {
|
||||
canAccessScopedCrmRecord,
|
||||
type CrmSecurityContext
|
||||
} from '@/features/crm/security/server/service';
|
||||
import {
|
||||
assertAssignableUserBelongsToOrganization,
|
||||
assertContactBelongsToOrganization,
|
||||
assertCustomerBelongsToOrganization
|
||||
} from '@/features/crm/enquiries/server/service';
|
||||
import type { AssignLeadInput, AssignLeadResult } from '../types';
|
||||
|
||||
export type LeadAssignmentAccessContext = CrmSecurityContext;
|
||||
|
||||
type LeadRow = typeof crmLeads.$inferSelect;
|
||||
type EnquiryRow = typeof crmEnquiries.$inferSelect;
|
||||
|
||||
function canAccessLeadRow(row: LeadRow, accessContext: LeadAssignmentAccessContext) {
|
||||
return canAccessScopedCrmRecord(accessContext, {
|
||||
branchId: row.branchId,
|
||||
createdBy: row.createdBy,
|
||||
ownerUserId: row.ownerMarketingUserId
|
||||
});
|
||||
}
|
||||
|
||||
async function assertLeadBelongsToOrganization(
|
||||
leadId: string,
|
||||
organizationId: string,
|
||||
accessContext?: LeadAssignmentAccessContext
|
||||
) {
|
||||
const [lead] = await db
|
||||
.select()
|
||||
.from(crmLeads)
|
||||
.where(
|
||||
and(
|
||||
eq(crmLeads.id, leadId),
|
||||
eq(crmLeads.organizationId, organizationId),
|
||||
isNull(crmLeads.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!lead) {
|
||||
throw new AuthError('Lead not found', 404);
|
||||
}
|
||||
|
||||
if (accessContext && !canAccessLeadRow(lead, accessContext)) {
|
||||
throw new AuthError('Forbidden', 403);
|
||||
}
|
||||
|
||||
return lead;
|
||||
}
|
||||
|
||||
async function resolveDefaultEnquiryStatusId(organizationId: string) {
|
||||
const options = await getActiveOptionsByCategory('crm_enquiry_status', { organizationId });
|
||||
const preferred = options.find((option) => option.code === 'new');
|
||||
const resolved = preferred ?? options[0];
|
||||
|
||||
if (!resolved) {
|
||||
throw new AuthError('Enquiry status options are not configured', 400);
|
||||
}
|
||||
|
||||
return resolved.id;
|
||||
}
|
||||
|
||||
async function findActiveEnquiryByLead(leadId: string, organizationId: string) {
|
||||
const [enquiry] = await db
|
||||
.select()
|
||||
.from(crmEnquiries)
|
||||
.where(
|
||||
and(
|
||||
eq(crmEnquiries.leadId, leadId),
|
||||
eq(crmEnquiries.organizationId, organizationId),
|
||||
inArray(crmEnquiries.pipelineStage, ['lead', 'enquiry']),
|
||||
isNull(crmEnquiries.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(crmEnquiries.createdAt))
|
||||
.limit(1);
|
||||
|
||||
return enquiry ?? null;
|
||||
}
|
||||
|
||||
async function syncLeadAssignment(input: {
|
||||
leadId: string;
|
||||
status: string;
|
||||
salesOwnerId: string;
|
||||
assignedBy: string;
|
||||
assignmentRemark?: string | null;
|
||||
}) {
|
||||
const now = new Date();
|
||||
|
||||
await db
|
||||
.update(crmLeads)
|
||||
.set({
|
||||
status: input.status,
|
||||
assignedSalesOwnerId: input.salesOwnerId,
|
||||
assignedAt: now,
|
||||
assignedBy: input.assignedBy,
|
||||
assignmentRemark: input.assignmentRemark?.trim() || null,
|
||||
updatedAt: now
|
||||
})
|
||||
.where(eq(crmLeads.id, input.leadId));
|
||||
}
|
||||
|
||||
async function createEnquiryFromLead(input: {
|
||||
lead: LeadRow;
|
||||
organizationId: string;
|
||||
actorUserId: string;
|
||||
salesOwnerId: string;
|
||||
assignmentRemark?: string | null;
|
||||
}) {
|
||||
if (!input.lead.customerId) {
|
||||
throw new AuthError('Lead requires a customer before assignment', 400);
|
||||
}
|
||||
|
||||
if (!input.lead.productType) {
|
||||
throw new AuthError('Lead requires a product type before assignment', 400);
|
||||
}
|
||||
|
||||
if (!input.lead.priority) {
|
||||
throw new AuthError('Lead requires a priority before assignment', 400);
|
||||
}
|
||||
|
||||
await assertCustomerBelongsToOrganization(input.lead.customerId, input.organizationId);
|
||||
|
||||
if (input.lead.contactId) {
|
||||
await assertContactBelongsToOrganization(
|
||||
input.lead.contactId,
|
||||
input.organizationId,
|
||||
input.lead.customerId
|
||||
);
|
||||
}
|
||||
|
||||
const [statusId, documentCode] = await Promise.all([
|
||||
resolveDefaultEnquiryStatusId(input.organizationId),
|
||||
generateNextDocumentCode({
|
||||
organizationId: input.organizationId,
|
||||
documentType: 'crm_enquiry',
|
||||
branchId: input.lead.branchId ?? null
|
||||
})
|
||||
]);
|
||||
|
||||
const now = new Date();
|
||||
const [created] = await db
|
||||
.insert(crmEnquiries)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
organizationId: input.organizationId,
|
||||
branchId: input.lead.branchId ?? null,
|
||||
leadId: input.lead.id,
|
||||
code: documentCode.code,
|
||||
customerId: input.lead.customerId,
|
||||
contactId: input.lead.contactId ?? null,
|
||||
title: input.lead.projectName?.trim() || `Lead ${input.lead.code}`,
|
||||
description: input.lead.description?.trim() || null,
|
||||
requirement: null,
|
||||
projectName: input.lead.projectName?.trim() || null,
|
||||
projectLocation: input.lead.projectLocation?.trim() || null,
|
||||
productType: input.lead.productType,
|
||||
status: statusId,
|
||||
priority: input.lead.priority,
|
||||
leadChannel: null,
|
||||
estimatedValue: input.lead.estimatedValue ?? null,
|
||||
chancePercent: null,
|
||||
expectedCloseDate: null,
|
||||
competitor: null,
|
||||
source: `lead:${input.lead.code}`,
|
||||
notes: null,
|
||||
isHotProject: false,
|
||||
isActive: true,
|
||||
pipelineStage: 'enquiry',
|
||||
assignedToUserId: input.salesOwnerId,
|
||||
assignedAt: now,
|
||||
assignedBy: input.actorUserId,
|
||||
assignmentRemark: input.assignmentRemark?.trim() || null,
|
||||
createdBy: input.actorUserId,
|
||||
updatedBy: input.actorUserId
|
||||
})
|
||||
.returning();
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function assignLead(
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
input: AssignLeadInput,
|
||||
accessContext?: LeadAssignmentAccessContext
|
||||
): Promise<AssignLeadResult> {
|
||||
const lead = await assertLeadBelongsToOrganization(input.leadId, organizationId, accessContext);
|
||||
await assertAssignableUserBelongsToOrganization(input.salesOwnerId, organizationId);
|
||||
|
||||
const existingEnquiry = await findActiveEnquiryByLead(input.leadId, organizationId);
|
||||
|
||||
if (existingEnquiry) {
|
||||
const resolvedSalesOwnerId = existingEnquiry.assignedToUserId ?? input.salesOwnerId;
|
||||
|
||||
if (!existingEnquiry.assignedToUserId) {
|
||||
await db
|
||||
.update(crmEnquiries)
|
||||
.set({
|
||||
pipelineStage: 'enquiry',
|
||||
assignedToUserId: input.salesOwnerId,
|
||||
assignedAt: new Date(),
|
||||
assignedBy: actorUserId,
|
||||
assignmentRemark: input.assignmentRemark?.trim() || null,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: actorUserId
|
||||
})
|
||||
.where(eq(crmEnquiries.id, existingEnquiry.id));
|
||||
}
|
||||
|
||||
await syncLeadAssignment({
|
||||
leadId: input.leadId,
|
||||
status: 'assigned',
|
||||
salesOwnerId: resolvedSalesOwnerId,
|
||||
assignedBy: actorUserId,
|
||||
assignmentRemark: input.assignmentRemark
|
||||
});
|
||||
|
||||
await auditAction({
|
||||
organizationId,
|
||||
branchId: lead.branchId,
|
||||
userId: actorUserId,
|
||||
entityType: 'crm_lead',
|
||||
entityId: input.leadId,
|
||||
action: 'assign_lead',
|
||||
afterData: {
|
||||
leadId: input.leadId,
|
||||
enquiryId: existingEnquiry.id,
|
||||
salesOwnerId: resolvedSalesOwnerId,
|
||||
reusedExistingEnquiry: true
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
leadId: input.leadId,
|
||||
enquiryId: existingEnquiry.id,
|
||||
enquiryCode: existingEnquiry.code
|
||||
};
|
||||
}
|
||||
|
||||
const createdEnquiry = await createEnquiryFromLead({
|
||||
lead,
|
||||
organizationId,
|
||||
actorUserId,
|
||||
salesOwnerId: input.salesOwnerId,
|
||||
assignmentRemark: input.assignmentRemark
|
||||
});
|
||||
|
||||
await syncLeadAssignment({
|
||||
leadId: input.leadId,
|
||||
status: 'assigned',
|
||||
salesOwnerId: input.salesOwnerId,
|
||||
assignedBy: actorUserId,
|
||||
assignmentRemark: input.assignmentRemark
|
||||
});
|
||||
|
||||
await auditAction({
|
||||
organizationId,
|
||||
branchId: lead.branchId,
|
||||
userId: actorUserId,
|
||||
entityType: 'crm_enquiry',
|
||||
entityId: createdEnquiry.id,
|
||||
action: 'create_enquiry_from_lead',
|
||||
afterData: {
|
||||
leadId: input.leadId,
|
||||
enquiryId: createdEnquiry.id,
|
||||
salesOwnerId: input.salesOwnerId
|
||||
}
|
||||
});
|
||||
|
||||
await auditAction({
|
||||
organizationId,
|
||||
branchId: lead.branchId,
|
||||
userId: actorUserId,
|
||||
entityType: 'crm_lead',
|
||||
entityId: input.leadId,
|
||||
action: 'assign_lead',
|
||||
afterData: {
|
||||
leadId: input.leadId,
|
||||
enquiryId: createdEnquiry.id,
|
||||
salesOwnerId: input.salesOwnerId,
|
||||
reusedExistingEnquiry: false
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
leadId: input.leadId,
|
||||
enquiryId: createdEnquiry.id,
|
||||
enquiryCode: createdEnquiry.code
|
||||
};
|
||||
}
|
||||
@@ -49,7 +49,9 @@ const LEAD_OPTION_CATEGORIES = {
|
||||
awareness: 'crm_lead_awareness',
|
||||
status: 'crm_lead_status',
|
||||
followupStatus: 'crm_lead_followup_status',
|
||||
lostReason: 'crm_lead_lost_reason'
|
||||
lostReason: 'crm_lead_lost_reason',
|
||||
productType: 'crm_product_type',
|
||||
priority: 'crm_priority'
|
||||
} as const;
|
||||
|
||||
export type LeadAccessContext = CrmSecurityContext;
|
||||
@@ -61,6 +63,8 @@ type LeadLookupMaps = {
|
||||
statusLabels: Map<string, string>;
|
||||
followupStatusLabels: Map<string, string>;
|
||||
lostReasonLabels: Map<string, string>;
|
||||
productTypeLabels: Map<string, string>;
|
||||
priorityLabels: Map<string, string>;
|
||||
customerNames: Map<string, string>;
|
||||
contactNames: Map<string, string>;
|
||||
userNames: Map<string, string>;
|
||||
@@ -121,8 +125,12 @@ function mapLeadSummary(row: LeadRow, lookups: LeadLookupMaps): LeadSummary {
|
||||
customerName: row.customerId ? (lookups.customerNames.get(row.customerId) ?? null) : null,
|
||||
contactId: row.contactId ?? null,
|
||||
contactName: row.contactId ? (lookups.contactNames.get(row.contactId) ?? null) : null,
|
||||
description: row.description ?? null,
|
||||
projectName: row.projectName ?? null,
|
||||
projectLocation: row.projectLocation ?? null,
|
||||
productType: row.productType ?? null,
|
||||
priority: row.priority ?? null,
|
||||
estimatedValue: row.estimatedValue ?? null,
|
||||
awarenessId: row.awarenessId ?? null,
|
||||
awarenessLabel: row.awarenessId ? (lookups.awarenessLabels.get(row.awarenessId) ?? null) : null,
|
||||
status: row.status,
|
||||
@@ -138,6 +146,14 @@ function mapLeadSummary(row: LeadRow, lookups: LeadLookupMaps): LeadSummary {
|
||||
ownerMarketingName: row.ownerMarketingUserId
|
||||
? (lookups.userNames.get(row.ownerMarketingUserId) ?? null)
|
||||
: null,
|
||||
assignedSalesOwnerId: row.assignedSalesOwnerId ?? null,
|
||||
assignedSalesOwnerName: row.assignedSalesOwnerId
|
||||
? (lookups.userNames.get(row.assignedSalesOwnerId) ?? null)
|
||||
: null,
|
||||
assignedAt: toIsoDateTime(row.assignedAt),
|
||||
assignedBy: row.assignedBy ?? null,
|
||||
assignedByName: row.assignedBy ? (lookups.userNames.get(row.assignedBy) ?? null) : null,
|
||||
assignmentRemark: row.assignmentRemark ?? null,
|
||||
createdBy: row.createdBy,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
@@ -339,12 +355,15 @@ async function assertLeadBelongsToOrganization(
|
||||
}
|
||||
|
||||
async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise<LeadLookupMaps> {
|
||||
const [awarenesses, statuses, followupStatuses, lostReasons] = await Promise.all([
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId })
|
||||
]);
|
||||
const [awarenesses, statuses, followupStatuses, lostReasons, productTypes, priorities] =
|
||||
await Promise.all([
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.productType, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.priority, { organizationId })
|
||||
]);
|
||||
const customerIds = [
|
||||
...new Set(rows.map((row) => row.customerId).filter((value): value is string => Boolean(value)))
|
||||
];
|
||||
@@ -354,7 +373,12 @@ async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise
|
||||
const userIds = [
|
||||
...new Set(
|
||||
rows
|
||||
.flatMap((row) => [row.ownerMarketingUserId, row.createdBy])
|
||||
.flatMap((row) => [
|
||||
row.ownerMarketingUserId,
|
||||
row.assignedSalesOwnerId,
|
||||
row.assignedBy,
|
||||
row.createdBy
|
||||
])
|
||||
.filter((value): value is string => Boolean(value))
|
||||
)
|
||||
];
|
||||
@@ -385,6 +409,8 @@ async function buildLookupMaps(organizationId: string, rows: LeadRow[]): Promise
|
||||
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])),
|
||||
productTypeLabels: new Map(productTypes.map((item) => [item.id, item.label])),
|
||||
priorityLabels: new Map(priorities.map((item) => [item.id, item.label])),
|
||||
customerNames: new Map(customerRows.map((item) => [item.id, item.name])),
|
||||
contactNames: new Map(contactRows.map((item) => [item.id, item.name])),
|
||||
userNames: new Map(userRows.map((item) => [item.id, item.name]))
|
||||
@@ -399,6 +425,12 @@ async function validateLeadPayload(
|
||||
) {
|
||||
await Promise.all([
|
||||
assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.awareness, payload.awarenessId),
|
||||
assertMasterOptionValue(
|
||||
organizationId,
|
||||
LEAD_OPTION_CATEGORIES.productType,
|
||||
payload.productType
|
||||
),
|
||||
assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.priority, payload.priority),
|
||||
payload.status
|
||||
? assertMasterOptionValue(organizationId, LEAD_OPTION_CATEGORIES.status, payload.status)
|
||||
: Promise.resolve(),
|
||||
@@ -454,19 +486,24 @@ async function validateLeadPayload(
|
||||
}
|
||||
|
||||
export async function getLeadReferenceData(organizationId: string): Promise<LeadReferenceData> {
|
||||
const [awarenesses, statuses, followupStatuses, lostReasons, branches] = await Promise.all([
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
|
||||
getUserBranches()
|
||||
]);
|
||||
const [awarenesses, statuses, followupStatuses, lostReasons, productTypes, priorities, branches] =
|
||||
await Promise.all([
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.awareness, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.status, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.followupStatus, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.lostReason, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.productType, { organizationId }),
|
||||
getActiveOptionsByCategory(LEAD_OPTION_CATEGORIES.priority, { organizationId }),
|
||||
getUserBranches()
|
||||
]);
|
||||
|
||||
return {
|
||||
awarenesses: awarenesses.map(mapOption),
|
||||
statuses: statuses.map(mapOption),
|
||||
followupStatuses: followupStatuses.map(mapOption),
|
||||
lostReasons: lostReasons.map(mapOption),
|
||||
productTypes: productTypes.map(mapOption),
|
||||
priorities: priorities.map(mapOption),
|
||||
branches: branches.map(mapBranch)
|
||||
};
|
||||
}
|
||||
@@ -570,8 +607,12 @@ export async function createLead(
|
||||
code: codeResult.code,
|
||||
customerId: payload.customerId ?? null,
|
||||
contactId: payload.contactId ?? null,
|
||||
description: payload.description?.trim() || null,
|
||||
projectName: payload.projectName?.trim() || null,
|
||||
projectLocation: payload.projectLocation?.trim() || null,
|
||||
productType: payload.productType ?? null,
|
||||
priority: payload.priority ?? null,
|
||||
estimatedValue: payload.estimatedValue ?? null,
|
||||
awarenessId: payload.awarenessId ?? null,
|
||||
status: payload.status,
|
||||
followupStatus: payload.followupStatus ?? null,
|
||||
@@ -600,9 +641,14 @@ export async function updateLead(
|
||||
payload.contactId === undefined
|
||||
? current.contactId
|
||||
: payload.contactId,
|
||||
description: payload.description === undefined ? current.description : payload.description,
|
||||
projectName: payload.projectName === undefined ? current.projectName : payload.projectName,
|
||||
projectLocation:
|
||||
payload.projectLocation === undefined ? current.projectLocation : payload.projectLocation,
|
||||
productType: payload.productType === undefined ? current.productType : payload.productType,
|
||||
priority: payload.priority === undefined ? current.priority : payload.priority,
|
||||
estimatedValue:
|
||||
payload.estimatedValue === undefined ? current.estimatedValue : payload.estimatedValue,
|
||||
awarenessId: payload.awarenessId === undefined ? current.awarenessId : payload.awarenessId,
|
||||
status: payload.status ?? current.status,
|
||||
followupStatus:
|
||||
@@ -623,8 +669,12 @@ export async function updateLead(
|
||||
branchId: nextPayload.branchId ?? null,
|
||||
customerId: nextPayload.customerId ?? null,
|
||||
contactId: nextPayload.contactId ?? null,
|
||||
description: nextPayload.description?.trim() || null,
|
||||
projectName: nextPayload.projectName?.trim() || null,
|
||||
projectLocation: nextPayload.projectLocation?.trim() || null,
|
||||
productType: nextPayload.productType ?? null,
|
||||
priority: nextPayload.priority ?? null,
|
||||
estimatedValue: nextPayload.estimatedValue ?? null,
|
||||
awarenessId: nextPayload.awarenessId ?? null,
|
||||
status: nextPayload.status,
|
||||
followupStatus: nextPayload.followupStatus ?? null,
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface LeadReferenceData {
|
||||
statuses: LeadOption[];
|
||||
followupStatuses: LeadOption[];
|
||||
lostReasons: LeadOption[];
|
||||
productTypes: LeadOption[];
|
||||
priorities: LeadOption[];
|
||||
branches: LeadBranchOption[];
|
||||
}
|
||||
|
||||
@@ -24,8 +26,12 @@ export interface CreateLeadInput {
|
||||
branchId?: string | null;
|
||||
customerId?: string | null;
|
||||
contactId?: string | null;
|
||||
description?: string | null;
|
||||
projectName?: string | null;
|
||||
projectLocation?: string | null;
|
||||
productType?: string | null;
|
||||
priority?: string | null;
|
||||
estimatedValue?: number | null;
|
||||
awarenessId?: string | null;
|
||||
status: string;
|
||||
followupStatus?: string | null;
|
||||
@@ -38,8 +44,12 @@ export interface UpdateLeadInput {
|
||||
branchId?: string | null;
|
||||
customerId?: string | null;
|
||||
contactId?: string | null;
|
||||
description?: string | null;
|
||||
projectName?: string | null;
|
||||
projectLocation?: string | null;
|
||||
productType?: string | null;
|
||||
priority?: string | null;
|
||||
estimatedValue?: number | null;
|
||||
awarenessId?: string | null;
|
||||
status?: string;
|
||||
followupStatus?: string | null;
|
||||
@@ -69,6 +79,12 @@ export interface CreateLeadFollowupInput {
|
||||
nextFollowupDate?: string | null;
|
||||
}
|
||||
|
||||
export interface AssignLeadInput {
|
||||
leadId: string;
|
||||
salesOwnerId: string;
|
||||
assignmentRemark?: string | null;
|
||||
}
|
||||
|
||||
export interface LeadSummary {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
@@ -78,8 +94,12 @@ export interface LeadSummary {
|
||||
customerName: string | null;
|
||||
contactId: string | null;
|
||||
contactName: string | null;
|
||||
description: string | null;
|
||||
projectName: string | null;
|
||||
projectLocation: string | null;
|
||||
productType: string | null;
|
||||
priority: string | null;
|
||||
estimatedValue: number | null;
|
||||
awarenessId: string | null;
|
||||
awarenessLabel: string | null;
|
||||
status: string;
|
||||
@@ -91,6 +111,12 @@ export interface LeadSummary {
|
||||
outcome: string;
|
||||
ownerMarketingUserId: string | null;
|
||||
ownerMarketingName: string | null;
|
||||
assignedSalesOwnerId: string | null;
|
||||
assignedSalesOwnerName: string | null;
|
||||
assignedAt: string | null;
|
||||
assignedBy: string | null;
|
||||
assignedByName: string | null;
|
||||
assignmentRemark: string | null;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -119,3 +145,9 @@ export interface LeadFollowupSummary {
|
||||
createdBy: string;
|
||||
createdByName: string | null;
|
||||
}
|
||||
|
||||
export interface AssignLeadResult {
|
||||
leadId: string;
|
||||
enquiryId: string;
|
||||
enquiryCode: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user