task-d5.5.1
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
||||
crmDocumentArtifacts,
|
||||
crmCustomerContacts,
|
||||
crmCustomers,
|
||||
crmEnquiries,
|
||||
crmOpportunities,
|
||||
crmQuotationAttachments,
|
||||
crmQuotationCustomers,
|
||||
crmQuotationFollowups,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '@/features/crm/security/server/service';
|
||||
import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service';
|
||||
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
|
||||
import { syncEnquiryPipelineStageFromQuotationStatus } from '@/features/crm/enquiries/server/service';
|
||||
import { syncOpportunityPipelineStageFromQuotationStatus } from '@/features/crm/opportunities/server/service';
|
||||
import type {
|
||||
QuotationActivityRecord,
|
||||
QuotationAttachmentMutationPayload,
|
||||
@@ -37,7 +37,7 @@ import type {
|
||||
QuotationCustomerLookup,
|
||||
QuotationCustomerMutationPayload,
|
||||
QuotationCustomerRecord,
|
||||
QuotationEnquiryLookup,
|
||||
QuotationOpportunityLookup,
|
||||
QuotationFilters,
|
||||
QuotationFollowupMutationPayload,
|
||||
QuotationFollowupRecord,
|
||||
@@ -128,7 +128,7 @@ function mapQuotationRecord(
|
||||
organizationId: row.organizationId,
|
||||
branchId: row.branchId,
|
||||
code: row.code,
|
||||
enquiryId: row.enquiryId,
|
||||
opportunityId: row.opportunityId,
|
||||
customerId: row.customerId,
|
||||
contactId: row.contactId,
|
||||
quotationDate: row.quotationDate.toISOString(),
|
||||
@@ -437,24 +437,24 @@ export async function assertContactBelongsToOrganization(
|
||||
return contact;
|
||||
}
|
||||
|
||||
async function assertEnquiryBelongsToOrganization(id: string, organizationId: string) {
|
||||
const [enquiry] = await db
|
||||
async function assertOpportunityBelongsToOrganization(id: string, organizationId: string) {
|
||||
const [opportunity] = await db
|
||||
.select()
|
||||
.from(crmEnquiries)
|
||||
.from(crmOpportunities)
|
||||
.where(
|
||||
and(
|
||||
eq(crmEnquiries.id, id),
|
||||
eq(crmEnquiries.organizationId, organizationId),
|
||||
isNull(crmEnquiries.deletedAt)
|
||||
eq(crmOpportunities.id, id),
|
||||
eq(crmOpportunities.organizationId, organizationId),
|
||||
isNull(crmOpportunities.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!enquiry) {
|
||||
throw new AuthError('Enquiry not found', 404);
|
||||
if (!opportunity) {
|
||||
throw new AuthError('Opportunity not found', 404);
|
||||
}
|
||||
|
||||
return enquiry;
|
||||
return opportunity;
|
||||
}
|
||||
|
||||
async function assertQuotationBelongsToOrganization(
|
||||
@@ -665,11 +665,11 @@ async function validateQuotationPayload(
|
||||
await assertContactBelongsToOrganization(payload.contactId, organizationId, payload.customerId);
|
||||
}
|
||||
|
||||
if (payload.enquiryId) {
|
||||
const enquiry = await assertEnquiryBelongsToOrganization(payload.enquiryId, organizationId);
|
||||
if (payload.opportunityId) {
|
||||
const opportunity = await assertOpportunityBelongsToOrganization(payload.opportunityId, organizationId);
|
||||
|
||||
if (enquiry.customerId !== payload.customerId) {
|
||||
throw new AuthError('Selected enquiry belongs to a different customer', 400);
|
||||
if (opportunity.customerId !== payload.customerId) {
|
||||
throw new AuthError('Selected opportunity belongs to a different customer', 400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -941,7 +941,7 @@ function buildQuotationFilters(
|
||||
const quotationTypes = splitFilterValue(filters.quotationType);
|
||||
const branches = splitFilterValue(filters.branch);
|
||||
const customers = splitFilterValue(filters.customer);
|
||||
const enquiries = splitFilterValue(filters.enquiry);
|
||||
const opportunities = splitFilterValue(filters.opportunity);
|
||||
|
||||
return [
|
||||
eq(crmQuotations.organizationId, organizationId),
|
||||
@@ -951,7 +951,7 @@ function buildQuotationFilters(
|
||||
...(quotationTypes.length ? [inArray(crmQuotations.quotationType, quotationTypes)] : []),
|
||||
...(branches.length ? [inArray(crmQuotations.branchId, branches)] : []),
|
||||
...(customers.length ? [inArray(crmQuotations.customerId, customers)] : []),
|
||||
...(enquiries.length ? [inArray(crmQuotations.enquiryId, enquiries)] : []),
|
||||
...(opportunities.length ? [inArray(crmQuotations.opportunityId, opportunities)] : []),
|
||||
...(filters.hot === 'true' ? [eq(crmQuotations.isHotProject, true)] : []),
|
||||
...(filters.hot === 'false' ? [eq(crmQuotations.isHotProject, false)] : []),
|
||||
...(filters.search
|
||||
@@ -1035,7 +1035,7 @@ export async function getQuotationReferenceData(
|
||||
productTypes,
|
||||
customers,
|
||||
contacts,
|
||||
enquiries,
|
||||
opportunities,
|
||||
salesmen
|
||||
] = await Promise.all([
|
||||
getUserBranches(),
|
||||
@@ -1066,9 +1066,9 @@ export async function getQuotationReferenceData(
|
||||
.orderBy(desc(crmCustomerContacts.isPrimary), asc(crmCustomerContacts.name)),
|
||||
db
|
||||
.select()
|
||||
.from(crmEnquiries)
|
||||
.where(and(eq(crmEnquiries.organizationId, organizationId), isNull(crmEnquiries.deletedAt)))
|
||||
.orderBy(desc(crmEnquiries.updatedAt)),
|
||||
.from(crmOpportunities)
|
||||
.where(and(eq(crmOpportunities.organizationId, organizationId), isNull(crmOpportunities.deletedAt)))
|
||||
.orderBy(desc(crmOpportunities.updatedAt)),
|
||||
db
|
||||
.select({ id: users.id, name: users.name })
|
||||
.from(memberships)
|
||||
@@ -1103,15 +1103,15 @@ export async function getQuotationReferenceData(
|
||||
mobile: contact.mobile,
|
||||
isPrimary: contact.isPrimary
|
||||
})),
|
||||
enquiries: enquiries.map<QuotationEnquiryLookup>((enquiry) => ({
|
||||
id: enquiry.id,
|
||||
code: enquiry.code,
|
||||
customerId: enquiry.customerId,
|
||||
contactId: enquiry.contactId,
|
||||
title: enquiry.title,
|
||||
projectName: enquiry.projectName,
|
||||
projectLocation: enquiry.projectLocation,
|
||||
branchId: enquiry.branchId
|
||||
opportunities: opportunities.map<QuotationOpportunityLookup>((opportunity) => ({
|
||||
id: opportunity.id,
|
||||
code: opportunity.code,
|
||||
customerId: opportunity.customerId,
|
||||
contactId: opportunity.contactId,
|
||||
title: opportunity.title,
|
||||
projectName: opportunity.projectName,
|
||||
projectLocation: opportunity.projectLocation,
|
||||
branchId: opportunity.branchId
|
||||
})),
|
||||
salesmen: salesmen.map<QuotationSalesmanLookup>((salesman) => ({
|
||||
id: salesman.id,
|
||||
@@ -1142,18 +1142,18 @@ export async function listQuotations(
|
||||
|
||||
const customerIds = [...new Set(rows.map((row) => row.customerId))];
|
||||
const contactIds = [...new Set(rows.map((row) => row.contactId).filter(Boolean))] as string[];
|
||||
const enquiryIds = [...new Set(rows.map((row) => row.enquiryId).filter(Boolean))] as string[];
|
||||
const opportunityIds = [...new Set(rows.map((row) => row.opportunityId).filter(Boolean))] as string[];
|
||||
const quotationIds = rows.map((row) => row.id);
|
||||
|
||||
const [customers, contacts, enquiries, itemCounts] = await Promise.all([
|
||||
const [customers, contacts, opportunities, itemCounts] = await Promise.all([
|
||||
customerIds.length
|
||||
? db.select().from(crmCustomers).where(inArray(crmCustomers.id, customerIds))
|
||||
: [],
|
||||
contactIds.length
|
||||
? db.select().from(crmCustomerContacts).where(inArray(crmCustomerContacts.id, contactIds))
|
||||
: [],
|
||||
enquiryIds.length
|
||||
? db.select().from(crmEnquiries).where(inArray(crmEnquiries.id, enquiryIds))
|
||||
opportunityIds.length
|
||||
? db.select().from(crmOpportunities).where(inArray(crmOpportunities.id, opportunityIds))
|
||||
: [],
|
||||
quotationIds.length
|
||||
? db
|
||||
@@ -1172,7 +1172,7 @@ export async function listQuotations(
|
||||
|
||||
const customerMap = new Map(customers.map((item) => [item.id, item]));
|
||||
const contactMap = new Map(contacts.map((item) => [item.id, item]));
|
||||
const enquiryMap = new Map(enquiries.map((item) => [item.id, item]));
|
||||
const opportunityMap = new Map(opportunities.map((item) => [item.id, item]));
|
||||
const itemCountMap = new Map(itemCounts.map((item) => [item.quotationId, item.value]));
|
||||
|
||||
return {
|
||||
@@ -1180,7 +1180,7 @@ export async function listQuotations(
|
||||
...mapQuotationRecord(row),
|
||||
customerName: customerMap.get(row.customerId)?.name ?? 'Unknown customer',
|
||||
contactName: row.contactId ? (contactMap.get(row.contactId)?.name ?? null) : null,
|
||||
enquiryCode: row.enquiryId ? (enquiryMap.get(row.enquiryId)?.code ?? null) : null,
|
||||
opportunityCode: row.opportunityId ? (opportunityMap.get(row.opportunityId)?.code ?? null) : null,
|
||||
itemCount: itemCountMap.get(row.id) ?? 0,
|
||||
revisionLabel: toRevisionLabel(row.revision)
|
||||
})),
|
||||
@@ -1289,13 +1289,13 @@ export async function createQuotation(
|
||||
payload.status
|
||||
);
|
||||
|
||||
const enquiry = payload.enquiryId
|
||||
? await assertEnquiryBelongsToOrganization(payload.enquiryId, organizationId)
|
||||
const opportunity = payload.opportunityId
|
||||
? await assertOpportunityBelongsToOrganization(payload.opportunityId, organizationId)
|
||||
: null;
|
||||
const documentCode = await generateNextDocumentCode({
|
||||
organizationId,
|
||||
documentType: 'quotation',
|
||||
branchId: payload.branchId ?? enquiry?.branchId ?? ''
|
||||
branchId: payload.branchId ?? opportunity?.branchId ?? ''
|
||||
});
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
@@ -1304,16 +1304,16 @@ export async function createQuotation(
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
organizationId,
|
||||
branchId: payload.branchId ?? enquiry?.branchId ?? null,
|
||||
branchId: payload.branchId ?? opportunity?.branchId ?? null,
|
||||
code: documentCode.code,
|
||||
enquiryId: payload.enquiryId ?? null,
|
||||
opportunityId: payload.opportunityId ?? null,
|
||||
customerId: payload.customerId,
|
||||
contactId: payload.contactId ?? enquiry?.contactId ?? null,
|
||||
contactId: payload.contactId ?? opportunity?.contactId ?? null,
|
||||
quotationDate: new Date(payload.quotationDate),
|
||||
validUntil: payload.validUntil ? new Date(payload.validUntil) : null,
|
||||
quotationType: payload.quotationType,
|
||||
projectName: payload.projectName?.trim() || enquiry?.projectName || null,
|
||||
projectLocation: payload.projectLocation?.trim() || enquiry?.projectLocation || null,
|
||||
projectName: payload.projectName?.trim() || opportunity?.projectName || null,
|
||||
projectLocation: payload.projectLocation?.trim() || opportunity?.projectLocation || null,
|
||||
attention: payload.attention?.trim() || null,
|
||||
reference: payload.reference?.trim() || null,
|
||||
notes: payload.notes?.trim() || null,
|
||||
@@ -1325,9 +1325,9 @@ export async function createQuotation(
|
||||
discount: payload.discount ?? 0,
|
||||
discountType: payload.discountType ?? null,
|
||||
taxRate: payload.taxRate ?? 0,
|
||||
chancePercent: payload.chancePercent ?? enquiry?.chancePercent ?? null,
|
||||
isHotProject: payload.isHotProject ?? enquiry?.isHotProject ?? false,
|
||||
competitor: payload.competitor?.trim() || enquiry?.competitor || null,
|
||||
chancePercent: payload.chancePercent ?? opportunity?.chancePercent ?? null,
|
||||
isHotProject: payload.isHotProject ?? opportunity?.isHotProject ?? false,
|
||||
competitor: payload.competitor?.trim() || opportunity?.competitor || null,
|
||||
salesmanId: payload.salesmanId ?? null,
|
||||
isSent: false,
|
||||
sentVia: payload.sentVia ?? null,
|
||||
@@ -1350,9 +1350,9 @@ export async function createQuotation(
|
||||
payload.projectParties ?? []
|
||||
);
|
||||
|
||||
if (created.enquiryId) {
|
||||
await syncEnquiryPipelineStageFromQuotationStatus(
|
||||
created.enquiryId,
|
||||
if (created.opportunityId) {
|
||||
await syncOpportunityPipelineStageFromQuotationStatus(
|
||||
created.opportunityId,
|
||||
organizationId,
|
||||
quotationStatusCode
|
||||
);
|
||||
@@ -1376,23 +1376,23 @@ export async function updateQuotation(
|
||||
payload.status
|
||||
);
|
||||
const existing = await assertQuotationBelongsToOrganization(id, organizationId, accessContext);
|
||||
const enquiry = payload.enquiryId
|
||||
? await assertEnquiryBelongsToOrganization(payload.enquiryId, organizationId)
|
||||
const opportunity = payload.opportunityId
|
||||
? await assertOpportunityBelongsToOrganization(payload.opportunityId, organizationId)
|
||||
: null;
|
||||
|
||||
const updated = await db.transaction(async (tx) => {
|
||||
const [row] = await tx
|
||||
.update(crmQuotations)
|
||||
.set({
|
||||
branchId: payload.branchId ?? enquiry?.branchId ?? null,
|
||||
enquiryId: payload.enquiryId ?? null,
|
||||
branchId: payload.branchId ?? opportunity?.branchId ?? null,
|
||||
opportunityId: payload.opportunityId ?? null,
|
||||
customerId: payload.customerId,
|
||||
contactId: payload.contactId ?? enquiry?.contactId ?? null,
|
||||
contactId: payload.contactId ?? opportunity?.contactId ?? null,
|
||||
quotationDate: new Date(payload.quotationDate),
|
||||
validUntil: payload.validUntil ? new Date(payload.validUntil) : null,
|
||||
quotationType: payload.quotationType,
|
||||
projectName: payload.projectName?.trim() || enquiry?.projectName || null,
|
||||
projectLocation: payload.projectLocation?.trim() || enquiry?.projectLocation || null,
|
||||
projectName: payload.projectName?.trim() || opportunity?.projectName || null,
|
||||
projectLocation: payload.projectLocation?.trim() || opportunity?.projectLocation || null,
|
||||
attention: payload.attention?.trim() || null,
|
||||
reference: payload.reference?.trim() || null,
|
||||
notes: payload.notes?.trim() || null,
|
||||
@@ -1403,9 +1403,9 @@ export async function updateQuotation(
|
||||
discount: payload.discount ?? 0,
|
||||
discountType: payload.discountType ?? null,
|
||||
taxRate: payload.taxRate ?? 0,
|
||||
chancePercent: payload.chancePercent ?? enquiry?.chancePercent ?? null,
|
||||
chancePercent: payload.chancePercent ?? opportunity?.chancePercent ?? null,
|
||||
isHotProject: payload.isHotProject ?? false,
|
||||
competitor: payload.competitor?.trim() || enquiry?.competitor || null,
|
||||
competitor: payload.competitor?.trim() || opportunity?.competitor || null,
|
||||
salesmanId: payload.salesmanId ?? null,
|
||||
sentVia: payload.sentVia ?? null,
|
||||
isActive: payload.isActive ?? true,
|
||||
@@ -1423,9 +1423,9 @@ export async function updateQuotation(
|
||||
payload.projectParties ?? []
|
||||
);
|
||||
|
||||
if (row.enquiryId) {
|
||||
await syncEnquiryPipelineStageFromQuotationStatus(
|
||||
row.enquiryId,
|
||||
if (row.opportunityId) {
|
||||
await syncOpportunityPipelineStageFromQuotationStatus(
|
||||
row.opportunityId,
|
||||
organizationId,
|
||||
quotationStatusCode
|
||||
);
|
||||
@@ -2160,7 +2160,7 @@ export async function createQuotationRevision(
|
||||
organizationId,
|
||||
branchId: parent.branchId,
|
||||
code: documentCode.code,
|
||||
enquiryId: parent.enquiryId,
|
||||
opportunityId: parent.opportunityId,
|
||||
customerId: parent.customerId,
|
||||
contactId: parent.contactId,
|
||||
quotationDate: new Date(),
|
||||
@@ -2291,8 +2291,8 @@ export async function listQuotationRevisions(id: string, organizationId: string)
|
||||
}));
|
||||
}
|
||||
|
||||
export async function listEnquiryQuotationRelations(
|
||||
enquiryId: string,
|
||||
export async function listOpportunityQuotationRelations(
|
||||
opportunityId: string,
|
||||
organizationId: string
|
||||
): Promise<QuotationRelationItem[]> {
|
||||
const rows = await db
|
||||
@@ -2301,7 +2301,7 @@ export async function listEnquiryQuotationRelations(
|
||||
.where(
|
||||
and(
|
||||
eq(crmQuotations.organizationId, organizationId),
|
||||
eq(crmQuotations.enquiryId, enquiryId),
|
||||
eq(crmQuotations.opportunityId, opportunityId),
|
||||
isNull(crmQuotations.deletedAt)
|
||||
)
|
||||
)
|
||||
@@ -2345,3 +2345,4 @@ export async function listCustomerQuotationRelations(
|
||||
updatedAt: row.updatedAt.toISOString()
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user