task-d5.5.1
This commit is contained in:
@@ -28,7 +28,7 @@ export async function getQuotations(filters: QuotationFilters): Promise<Quotatio
|
||||
if (filters.quotationType) searchParams.set('quotationType', filters.quotationType);
|
||||
if (filters.branch) searchParams.set('branch', filters.branch);
|
||||
if (filters.customer) searchParams.set('customer', filters.customer);
|
||||
if (filters.enquiry) searchParams.set('enquiry', filters.enquiry);
|
||||
if (filters.opportunity) searchParams.set('opportunity', filters.opportunity);
|
||||
if (filters.hot) searchParams.set('hot', filters.hot);
|
||||
if (filters.sort) searchParams.set('sort', filters.sort);
|
||||
|
||||
@@ -212,3 +212,4 @@ export async function createQuotationRevision(id: string, revisionRemark?: strin
|
||||
body: JSON.stringify({ revisionRemark })
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface QuotationContactLookup {
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
export interface QuotationEnquiryLookup {
|
||||
export interface QuotationOpportunityLookup {
|
||||
id: string;
|
||||
code: string;
|
||||
customerId: string;
|
||||
@@ -64,7 +64,7 @@ export interface QuotationRecord {
|
||||
organizationId: string;
|
||||
branchId: string | null;
|
||||
code: string;
|
||||
enquiryId: string | null;
|
||||
opportunityId: string | null;
|
||||
customerId: string;
|
||||
contactId: string | null;
|
||||
quotationDate: string;
|
||||
@@ -115,7 +115,7 @@ export interface QuotationRecord {
|
||||
export interface QuotationListItem extends QuotationRecord {
|
||||
customerName: string;
|
||||
contactName: string | null;
|
||||
enquiryCode: string | null;
|
||||
opportunityCode: string | null;
|
||||
itemCount: number;
|
||||
revisionLabel: string;
|
||||
}
|
||||
@@ -253,7 +253,7 @@ export interface QuotationReferenceData {
|
||||
productTypes: QuotationOption[];
|
||||
customers: QuotationCustomerLookup[];
|
||||
contacts: QuotationContactLookup[];
|
||||
enquiries: QuotationEnquiryLookup[];
|
||||
opportunities: QuotationOpportunityLookup[];
|
||||
salesmen: QuotationSalesmanLookup[];
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ export interface QuotationFilters {
|
||||
quotationType?: string;
|
||||
branch?: string;
|
||||
customer?: string;
|
||||
enquiry?: string;
|
||||
opportunity?: string;
|
||||
hot?: string;
|
||||
sort?: string;
|
||||
}
|
||||
@@ -331,7 +331,7 @@ export interface QuotationRevisionsResponse {
|
||||
}
|
||||
|
||||
export interface QuotationMutationPayload {
|
||||
enquiryId?: string | null;
|
||||
opportunityId?: string | null;
|
||||
customerId: string;
|
||||
contactId?: string | null;
|
||||
quotationDate: string;
|
||||
@@ -420,3 +420,4 @@ export interface MutationSuccessResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,16 +114,16 @@ export function getQuotationColumns({
|
||||
enableColumnFilter: true
|
||||
},
|
||||
{
|
||||
id: 'enquiry',
|
||||
accessorFn: (row) => row.enquiryId ?? '',
|
||||
id: 'opportunity',
|
||||
accessorFn: (row) => row.opportunityId ?? '',
|
||||
header: ({ column }: { column: Column<QuotationListItem, unknown> }) => (
|
||||
<DataTableColumnHeader column={column} title='โอกาสขาย' />
|
||||
),
|
||||
cell: ({ row }) => row.original.enquiryCode ?? '-',
|
||||
cell: ({ row }) => row.original.opportunityCode ?? '-',
|
||||
meta: {
|
||||
label: 'โอกาสขาย',
|
||||
variant: 'multiSelect' as const,
|
||||
options: referenceData.enquiries.map((item) => ({ value: item.id, label: `${item.code} - ${item.title}` }))
|
||||
options: referenceData.opportunities.map((item) => ({ value: item.id, label: `${item.code} - ${item.title}` }))
|
||||
},
|
||||
enableColumnFilter: true
|
||||
},
|
||||
@@ -165,3 +165,4 @@ export function getQuotationColumns({
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ import { createQuotationMutation, updateQuotationMutation } from '../api/mutatio
|
||||
import type { QuotationMutationPayload, QuotationRecord, QuotationReferenceData } from '../api/types';
|
||||
|
||||
type FormState = {
|
||||
enquiryId: string;
|
||||
opportunityId: string;
|
||||
customerId: string;
|
||||
contactId: string;
|
||||
quotationDate: string;
|
||||
@@ -71,7 +71,7 @@ function toFormState(
|
||||
referenceData: QuotationReferenceData
|
||||
): FormState {
|
||||
return {
|
||||
enquiryId: quotation?.enquiryId ?? '',
|
||||
opportunityId: quotation?.opportunityId ?? '',
|
||||
customerId: quotation?.customerId ?? referenceData.customers[0]?.id ?? '',
|
||||
contactId: quotation?.contactId ?? '',
|
||||
quotationDate:
|
||||
@@ -190,16 +190,16 @@ export function QuotationFormSheet({
|
||||
setState((current) => ({ ...current, [key]: value }));
|
||||
}
|
||||
|
||||
function handleEnquiryChange(value: string) {
|
||||
const enquiry = referenceData.enquiries.find((item) => item.id === value);
|
||||
function handleOpportunityChange(value: string) {
|
||||
const opportunity = referenceData.opportunities.find((item) => item.id === value);
|
||||
setState((current) => ({
|
||||
...current,
|
||||
enquiryId: value === '__none__' ? '' : value,
|
||||
customerId: enquiry?.customerId ?? current.customerId,
|
||||
contactId: enquiry?.contactId ?? '',
|
||||
projectName: enquiry?.projectName ?? current.projectName,
|
||||
projectLocation: enquiry?.projectLocation ?? current.projectLocation,
|
||||
branchId: enquiry?.branchId ?? current.branchId
|
||||
opportunityId: value === '__none__' ? '' : value,
|
||||
customerId: opportunity?.customerId ?? current.customerId,
|
||||
contactId: opportunity?.contactId ?? '',
|
||||
projectName: opportunity?.projectName ?? current.projectName,
|
||||
projectLocation: opportunity?.projectLocation ?? current.projectLocation,
|
||||
branchId: opportunity?.branchId ?? current.branchId
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ export function QuotationFormSheet({
|
||||
event.preventDefault();
|
||||
|
||||
const payload: QuotationMutationPayload = {
|
||||
enquiryId: state.enquiryId || null,
|
||||
opportunityId: state.opportunityId || null,
|
||||
customerId: state.customerId,
|
||||
contactId: state.contactId || null,
|
||||
quotationDate: state.quotationDate,
|
||||
@@ -263,14 +263,14 @@ export function QuotationFormSheet({
|
||||
|
||||
<form id='quotation-form-sheet' onSubmit={onSubmit} className='flex-1 overflow-auto'>
|
||||
<div className='grid gap-4 py-4 md:grid-cols-2'>
|
||||
<Field label='Enquiry'>
|
||||
<Select value={state.enquiryId || '__none__'} onValueChange={handleEnquiryChange}>
|
||||
<Field label='Opportunity'>
|
||||
<Select value={state.opportunityId || '__none__'} onValueChange={handleOpportunityChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select enquiry' />
|
||||
<SelectValue placeholder='Select opportunity' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='__none__'>No linked enquiry</SelectItem>
|
||||
{referenceData.enquiries.map((item) => (
|
||||
<SelectItem value='__none__'>No linked opportunity</SelectItem>
|
||||
{referenceData.opportunities.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
{item.code} - {item.title}
|
||||
</SelectItem>
|
||||
@@ -541,3 +541,4 @@ export function QuotationFormSheetTrigger({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function QuotationListing({
|
||||
const quotationType = searchParamsCache.get('quotationType');
|
||||
const branch = searchParamsCache.get('branch');
|
||||
const customer = searchParamsCache.get('customer');
|
||||
const enquiry = searchParamsCache.get('enquiry');
|
||||
const opportunity = searchParamsCache.get('opportunity');
|
||||
const hot = searchParamsCache.get('hot');
|
||||
const sort = searchParamsCache.get('sort');
|
||||
const filters = {
|
||||
@@ -32,7 +32,7 @@ export default function QuotationListing({
|
||||
...(quotationType && { quotationType }),
|
||||
...(branch && { branch }),
|
||||
...(customer && { customer }),
|
||||
...(enquiry && { enquiry }),
|
||||
...(opportunity && { opportunity }),
|
||||
...(hot && { hot }),
|
||||
...(sort && { sort })
|
||||
};
|
||||
@@ -46,3 +46,4 @@ export default function QuotationListing({
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export function QuotationsTable({
|
||||
quotationType: parseAsString,
|
||||
branch: parseAsString,
|
||||
customer: parseAsString,
|
||||
enquiry: parseAsString,
|
||||
opportunity: parseAsString,
|
||||
hot: parseAsString,
|
||||
sort: getSortingStateParser(columnIds).withDefault([])
|
||||
});
|
||||
@@ -46,7 +46,7 @@ export function QuotationsTable({
|
||||
...(params.quotationType && { quotationType: params.quotationType }),
|
||||
...(params.branch && { branch: params.branch }),
|
||||
...(params.customer && { customer: params.customer }),
|
||||
...(params.enquiry && { enquiry: params.enquiry }),
|
||||
...(params.opportunity && { opportunity: params.opportunity }),
|
||||
...(params.hot && { hot: params.hot }),
|
||||
...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })
|
||||
};
|
||||
@@ -70,3 +70,4 @@ export function QuotationsTable({
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ function coerceRequiredNumber(message: string) {
|
||||
}
|
||||
|
||||
export const quotationSchema = z.object({
|
||||
enquiryId: z.string().optional().nullable(),
|
||||
opportunityId: z.string().optional().nullable(),
|
||||
customerId: z.string().min(1, 'Please select a customer'),
|
||||
contactId: z.string().optional().nullable(),
|
||||
quotationDate: z.string().min(1, 'Quotation date is required'),
|
||||
@@ -159,3 +159,4 @@ export type QuotationCustomerFormValues = z.input<typeof quotationCustomerSchema
|
||||
export type QuotationTopicFormValues = z.input<typeof quotationTopicSchema>;
|
||||
export type QuotationFollowupFormValues = z.input<typeof quotationFollowupSchema>;
|
||||
export type QuotationAttachmentFormValues = z.input<typeof quotationAttachmentSchema>;
|
||||
|
||||
|
||||
@@ -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