task-i
This commit is contained in:
@@ -44,6 +44,21 @@ export interface QuotationSalesmanLookup {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface QuotationApprovedArtifactSummary {
|
||||
id: string;
|
||||
fileName: string;
|
||||
contentType: string;
|
||||
fileSize: number | null;
|
||||
checksum: string | null;
|
||||
status: string;
|
||||
generatedAt: string;
|
||||
generatedBy: string;
|
||||
generatedByName: string | null;
|
||||
lockedAt: string | null;
|
||||
lockedBy: string | null;
|
||||
lockedByName: string | null;
|
||||
}
|
||||
|
||||
export interface QuotationRecord {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
@@ -80,9 +95,12 @@ export interface QuotationRecord {
|
||||
sentAt: string | null;
|
||||
sentVia: string | null;
|
||||
approvedAt: string | null;
|
||||
approvedArtifactId: string | null;
|
||||
approvedPdfUrl: string | null;
|
||||
approvedSnapshot: unknown | null;
|
||||
approvedTemplateVersionId: string | null;
|
||||
approvedArtifact: QuotationApprovedArtifactSummary | null;
|
||||
hasLegacyApprovedPdf: boolean;
|
||||
acceptedAt: string | null;
|
||||
rejectedAt: string | null;
|
||||
rejectionReason: string | null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,6 @@
|
||||
import { and, asc, count, desc, eq, ilike, inArray, isNull, or, type SQL } from 'drizzle-orm';
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
ilike,
|
||||
inArray,
|
||||
isNull,
|
||||
or,
|
||||
type SQL
|
||||
} from 'drizzle-orm';
|
||||
import {
|
||||
crmDocumentArtifacts,
|
||||
crmCustomerContacts,
|
||||
crmCustomers,
|
||||
crmEnquiries,
|
||||
@@ -34,6 +24,7 @@ import type {
|
||||
QuotationActivityRecord,
|
||||
QuotationAttachmentMutationPayload,
|
||||
QuotationAttachmentRecord,
|
||||
QuotationApprovedArtifactSummary,
|
||||
QuotationBranchOption,
|
||||
QuotationContactLookup,
|
||||
QuotationCustomerListItem,
|
||||
@@ -95,7 +86,30 @@ function mapBranch(
|
||||
};
|
||||
}
|
||||
|
||||
function mapQuotationRecord(row: typeof crmQuotations.$inferSelect): QuotationRecord {
|
||||
function mapApprovedArtifactSummary(
|
||||
row: typeof crmDocumentArtifacts.$inferSelect,
|
||||
options?: { generatedByName?: string | null; lockedByName?: string | null }
|
||||
): QuotationApprovedArtifactSummary {
|
||||
return {
|
||||
id: row.id,
|
||||
fileName: row.fileName,
|
||||
contentType: row.contentType,
|
||||
fileSize: row.fileSize,
|
||||
checksum: row.checksum,
|
||||
status: row.status,
|
||||
generatedAt: row.generatedAt.toISOString(),
|
||||
generatedBy: row.generatedBy,
|
||||
generatedByName: options?.generatedByName ?? null,
|
||||
lockedAt: row.lockedAt?.toISOString() ?? null,
|
||||
lockedBy: row.lockedBy,
|
||||
lockedByName: options?.lockedByName ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function mapQuotationRecord(
|
||||
row: typeof crmQuotations.$inferSelect,
|
||||
options?: { approvedArtifact?: QuotationApprovedArtifactSummary | null }
|
||||
): QuotationRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
@@ -132,9 +146,15 @@ function mapQuotationRecord(row: typeof crmQuotations.$inferSelect): QuotationRe
|
||||
sentAt: row.sentAt?.toISOString() ?? null,
|
||||
sentVia: row.sentVia,
|
||||
approvedAt: row.approvedAt?.toISOString() ?? null,
|
||||
approvedArtifactId: row.approvedArtifactId,
|
||||
approvedPdfUrl: row.approvedPdfUrl,
|
||||
approvedSnapshot: row.approvedSnapshot ?? null,
|
||||
approvedTemplateVersionId: row.approvedTemplateVersionId,
|
||||
approvedArtifact: options?.approvedArtifact ?? null,
|
||||
hasLegacyApprovedPdf:
|
||||
!row.approvedArtifactId &&
|
||||
typeof row.approvedPdfUrl === 'string' &&
|
||||
row.approvedPdfUrl.startsWith('/generated/'),
|
||||
acceptedAt: row.acceptedAt?.toISOString() ?? null,
|
||||
rejectedAt: row.rejectedAt?.toISOString() ?? null,
|
||||
rejectionReason: row.rejectionReason,
|
||||
@@ -334,11 +354,7 @@ async function resolveOptionCodeById(
|
||||
return options.find((option) => option.id === optionId)?.code ?? null;
|
||||
}
|
||||
|
||||
async function resolveOptionIdByCode(
|
||||
organizationId: string,
|
||||
category: string,
|
||||
code: string
|
||||
) {
|
||||
async function resolveOptionIdByCode(organizationId: string, category: string, code: string) {
|
||||
const options = await getActiveOptionsByCategory(category, { organizationId });
|
||||
return options.find((option) => option.code === code)?.id ?? null;
|
||||
}
|
||||
@@ -724,7 +740,11 @@ function buildQuotationFilters(organizationId: string, filters: QuotationFilters
|
||||
];
|
||||
}
|
||||
|
||||
async function refreshQuotationTotals(quotationId: string, organizationId: string, userId?: string) {
|
||||
async function refreshQuotationTotals(
|
||||
quotationId: string,
|
||||
organizationId: string,
|
||||
userId?: string
|
||||
) {
|
||||
const quotation = await assertQuotationBelongsToOrganization(quotationId, organizationId);
|
||||
const items = await listQuotationItems(quotationId, organizationId);
|
||||
const subtotal = items.reduce((sum, item) => sum + item.totalPrice, 0);
|
||||
@@ -903,7 +923,9 @@ export async function listQuotations(
|
||||
contactIds.length
|
||||
? db.select().from(crmCustomerContacts).where(inArray(crmCustomerContacts.id, contactIds))
|
||||
: [],
|
||||
enquiryIds.length ? db.select().from(crmEnquiries).where(inArray(crmEnquiries.id, enquiryIds)) : [],
|
||||
enquiryIds.length
|
||||
? db.select().from(crmEnquiries).where(inArray(crmEnquiries.id, enquiryIds))
|
||||
: [],
|
||||
quotationIds.length
|
||||
? db
|
||||
.select({ quotationId: crmQuotationItems.quotationId, value: count() })
|
||||
@@ -942,7 +964,47 @@ export async function getQuotationDetail(
|
||||
organizationId: string
|
||||
): Promise<QuotationRecord> {
|
||||
const quotation = await assertQuotationBelongsToOrganization(id, organizationId);
|
||||
return mapQuotationRecord(quotation);
|
||||
const approvedArtifactId =
|
||||
quotation.approvedArtifactId ??
|
||||
(quotation.approvedPdfUrl?.startsWith('artifact:')
|
||||
? quotation.approvedPdfUrl.replace('artifact:', '')
|
||||
: null);
|
||||
|
||||
if (!approvedArtifactId) {
|
||||
return mapQuotationRecord(quotation);
|
||||
}
|
||||
|
||||
const [artifact] = await db
|
||||
.select()
|
||||
.from(crmDocumentArtifacts)
|
||||
.where(
|
||||
and(
|
||||
eq(crmDocumentArtifacts.id, approvedArtifactId),
|
||||
eq(crmDocumentArtifacts.organizationId, organizationId),
|
||||
isNull(crmDocumentArtifacts.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!artifact) {
|
||||
return mapQuotationRecord(quotation);
|
||||
}
|
||||
|
||||
const relatedUserIds = [artifact.generatedBy, artifact.lockedBy].filter(Boolean) as string[];
|
||||
const actorRows = relatedUserIds.length
|
||||
? await db
|
||||
.select({ id: users.id, name: users.name })
|
||||
.from(users)
|
||||
.where(inArray(users.id, relatedUserIds))
|
||||
: [];
|
||||
const actorMap = new Map(actorRows.map((row) => [row.id, row.name]));
|
||||
|
||||
return mapQuotationRecord(quotation, {
|
||||
approvedArtifact: mapApprovedArtifactSummary(artifact, {
|
||||
generatedByName: actorMap.get(artifact.generatedBy) ?? null,
|
||||
lockedByName: artifact.lockedBy ? (actorMap.get(artifact.lockedBy) ?? null) : null
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function getQuotationActivity(
|
||||
@@ -961,11 +1023,15 @@ export async function getQuotationActivity(
|
||||
log.entityType === 'crm_quotation_customer' ||
|
||||
log.entityType === 'crm_quotation_topic' ||
|
||||
log.entityType === 'crm_quotation_followup' ||
|
||||
log.entityType === 'crm_quotation_attachment'
|
||||
log.entityType === 'crm_quotation_attachment' ||
|
||||
log.entityType === 'crm_document_artifact'
|
||||
);
|
||||
const userIds = [...new Set(filtered.map((log) => log.userId))];
|
||||
const actorRows = userIds.length
|
||||
? await db.select({ id: users.id, name: users.name }).from(users).where(inArray(users.id, userIds))
|
||||
? await db
|
||||
.select({ id: users.id, name: users.name })
|
||||
.from(users)
|
||||
.where(inArray(users.id, userIds))
|
||||
: [];
|
||||
const actorMap = new Map(actorRows.map((row) => [row.id, row.name]));
|
||||
|
||||
@@ -1029,6 +1095,7 @@ export async function createQuotation(
|
||||
isSent: false,
|
||||
sentVia: payload.sentVia ?? null,
|
||||
approvedAt: null,
|
||||
approvedArtifactId: null,
|
||||
approvedPdfUrl: null,
|
||||
approvedSnapshot: null,
|
||||
approvedTemplateVersionId: null,
|
||||
@@ -1119,23 +1186,48 @@ export async function softDeleteQuotation(id: string, organizationId: string, us
|
||||
db
|
||||
.update(crmQuotationItems)
|
||||
.set({ deletedAt: now, updatedAt: now })
|
||||
.where(and(eq(crmQuotationItems.quotationId, id), eq(crmQuotationItems.organizationId, organizationId))),
|
||||
.where(
|
||||
and(
|
||||
eq(crmQuotationItems.quotationId, id),
|
||||
eq(crmQuotationItems.organizationId, organizationId)
|
||||
)
|
||||
),
|
||||
db
|
||||
.update(crmQuotationCustomers)
|
||||
.set({ deletedAt: now, updatedAt: now })
|
||||
.where(and(eq(crmQuotationCustomers.quotationId, id), eq(crmQuotationCustomers.organizationId, organizationId))),
|
||||
.where(
|
||||
and(
|
||||
eq(crmQuotationCustomers.quotationId, id),
|
||||
eq(crmQuotationCustomers.organizationId, organizationId)
|
||||
)
|
||||
),
|
||||
db
|
||||
.update(crmQuotationTopics)
|
||||
.set({ deletedAt: now, updatedAt: now })
|
||||
.where(and(eq(crmQuotationTopics.quotationId, id), eq(crmQuotationTopics.organizationId, organizationId))),
|
||||
.where(
|
||||
and(
|
||||
eq(crmQuotationTopics.quotationId, id),
|
||||
eq(crmQuotationTopics.organizationId, organizationId)
|
||||
)
|
||||
),
|
||||
db
|
||||
.update(crmQuotationFollowups)
|
||||
.set({ deletedAt: now, updatedAt: now, updatedBy: userId })
|
||||
.where(and(eq(crmQuotationFollowups.quotationId, id), eq(crmQuotationFollowups.organizationId, organizationId))),
|
||||
.where(
|
||||
and(
|
||||
eq(crmQuotationFollowups.quotationId, id),
|
||||
eq(crmQuotationFollowups.organizationId, organizationId)
|
||||
)
|
||||
),
|
||||
db
|
||||
.update(crmQuotationAttachments)
|
||||
.set({ deletedAt: now, updatedAt: now })
|
||||
.where(and(eq(crmQuotationAttachments.quotationId, id), eq(crmQuotationAttachments.organizationId, organizationId)))
|
||||
.where(
|
||||
and(
|
||||
eq(crmQuotationAttachments.quotationId, id),
|
||||
eq(crmQuotationAttachments.organizationId, organizationId)
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
return updated;
|
||||
@@ -1506,7 +1598,12 @@ export async function softDeleteQuotationTopic(
|
||||
await db
|
||||
.update(crmQuotationTopicItems)
|
||||
.set({ deletedAt: now, updatedAt: now })
|
||||
.where(and(eq(crmQuotationTopicItems.topicId, topicId), eq(crmQuotationTopicItems.organizationId, organizationId)));
|
||||
.where(
|
||||
and(
|
||||
eq(crmQuotationTopicItems.topicId, topicId),
|
||||
eq(crmQuotationTopicItems.organizationId, organizationId)
|
||||
)
|
||||
);
|
||||
|
||||
return updated;
|
||||
}
|
||||
@@ -1708,11 +1805,8 @@ export async function createQuotationRevision(
|
||||
);
|
||||
}
|
||||
const revisedStatusId =
|
||||
(await resolveOptionIdByCode(
|
||||
organizationId,
|
||||
QUOTATION_OPTION_CATEGORIES.status,
|
||||
'revised'
|
||||
)) ?? parent.status;
|
||||
(await resolveOptionIdByCode(organizationId, QUOTATION_OPTION_CATEGORIES.status, 'revised')) ??
|
||||
parent.status;
|
||||
|
||||
const familyRootId = parent.parentQuotationId ?? parent.id;
|
||||
const familyRows = await db
|
||||
@@ -1774,6 +1868,7 @@ export async function createQuotationRevision(
|
||||
isSent: false,
|
||||
sentVia: parent.sentVia,
|
||||
approvedAt: null,
|
||||
approvedArtifactId: null,
|
||||
approvedPdfUrl: null,
|
||||
approvedSnapshot: null,
|
||||
approvedTemplateVersionId: null,
|
||||
|
||||
Reference in New Issue
Block a user