crm finish
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE "Customer" ADD COLUMN "lifecycleStage" TEXT NOT NULL DEFAULT 'ACTIVE';
|
||||
ALTER TABLE "Customer" ADD COLUMN "preferredAccount" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Customer" ADD COLUMN "strategicAccount" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Customer" ADD COLUMN "requiresApproval" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Customer" ADD COLUMN "blockedAccount" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE "Vendor" ADD COLUMN "lifecycleStage" TEXT NOT NULL DEFAULT 'ACTIVE';
|
||||
ALTER TABLE "Vendor" ADD COLUMN "preferredAccount" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Vendor" ADD COLUMN "strategicAccount" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Vendor" ADD COLUMN "requiresApproval" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Vendor" ADD COLUMN "blockedAccount" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -113,6 +113,7 @@ model Customer {
|
||||
postalCode String
|
||||
country String
|
||||
status String @default("ACTIVE")
|
||||
lifecycleStage String @default("ACTIVE")
|
||||
isReseller Boolean @default(false)
|
||||
resellerDiscountPercent Float @default(0)
|
||||
parentCustomerId String?
|
||||
@@ -120,6 +121,10 @@ model Customer {
|
||||
currencyCode String? @default("USD")
|
||||
taxExempt Boolean @default(false)
|
||||
creditHold Boolean @default(false)
|
||||
preferredAccount Boolean @default(false)
|
||||
strategicAccount Boolean @default(false)
|
||||
requiresApproval Boolean @default(false)
|
||||
blockedAccount Boolean @default(false)
|
||||
notes String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -141,10 +146,15 @@ model Vendor {
|
||||
postalCode String
|
||||
country String
|
||||
status String @default("ACTIVE")
|
||||
lifecycleStage String @default("ACTIVE")
|
||||
paymentTerms String?
|
||||
currencyCode String? @default("USD")
|
||||
taxExempt Boolean @default(false)
|
||||
creditHold Boolean @default(false)
|
||||
preferredAccount Boolean @default(false)
|
||||
strategicAccount Boolean @default(false)
|
||||
requiresApproval Boolean @default(false)
|
||||
blockedAccount Boolean @default(false)
|
||||
notes String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { permissions } from "@mrp/shared";
|
||||
import { crmContactEntryTypes, crmContactRoles, crmRecordStatuses } from "@mrp/shared/dist/crm/types.js";
|
||||
import { crmContactEntryTypes, crmContactRoles, crmLifecycleStages, crmRecordStatuses } from "@mrp/shared/dist/crm/types.js";
|
||||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -32,6 +32,7 @@ const crmRecordSchema = z.object({
|
||||
postalCode: z.string().trim().min(1),
|
||||
country: z.string().trim().min(1),
|
||||
status: z.enum(crmRecordStatuses),
|
||||
lifecycleStage: z.enum(crmLifecycleStages).optional(),
|
||||
notes: z.string(),
|
||||
isReseller: z.boolean().optional(),
|
||||
resellerDiscountPercent: z.number().min(0).max(100).nullable().optional(),
|
||||
@@ -40,12 +41,18 @@ const crmRecordSchema = z.object({
|
||||
currencyCode: z.string().max(8).nullable().optional(),
|
||||
taxExempt: z.boolean().optional(),
|
||||
creditHold: z.boolean().optional(),
|
||||
preferredAccount: z.boolean().optional(),
|
||||
strategicAccount: z.boolean().optional(),
|
||||
requiresApproval: z.boolean().optional(),
|
||||
blockedAccount: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const crmListQuerySchema = z.object({
|
||||
q: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
status: z.enum(crmRecordStatuses).optional(),
|
||||
lifecycleStage: z.enum(crmLifecycleStages).optional(),
|
||||
flag: z.enum(["PREFERRED", "STRATEGIC", "REQUIRES_APPROVAL", "BLOCKED"]).optional(),
|
||||
});
|
||||
|
||||
const crmContactEntrySchema = z.object({
|
||||
@@ -81,6 +88,8 @@ crmRouter.get("/customers", requirePermissions([permissions.crmRead]), async (_r
|
||||
query: parsed.data.q,
|
||||
status: parsed.data.status,
|
||||
state: parsed.data.state,
|
||||
lifecycleStage: parsed.data.lifecycleStage,
|
||||
flag: parsed.data.flag,
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -192,6 +201,8 @@ crmRouter.get("/vendors", requirePermissions([permissions.crmRead]), async (_req
|
||||
query: parsed.data.q,
|
||||
status: parsed.data.status,
|
||||
state: parsed.data.state,
|
||||
lifecycleStage: parsed.data.lifecycleStage,
|
||||
flag: parsed.data.flag,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ import type {
|
||||
CrmCustomerChildDto,
|
||||
CrmRecordDetailDto,
|
||||
CrmRecordInput,
|
||||
CrmLifecycleStage,
|
||||
CrmRecordRollupsDto,
|
||||
CrmRecordStatus,
|
||||
CrmRecordSummaryDto,
|
||||
} from "@mrp/shared/dist/crm/types.js";
|
||||
@@ -25,6 +27,11 @@ function mapSummary(record: Customer | Vendor): CrmRecordSummaryDto {
|
||||
state: record.state,
|
||||
country: record.country,
|
||||
status: record.status as CrmRecordStatus,
|
||||
lifecycleStage: record.lifecycleStage as CrmLifecycleStage,
|
||||
preferredAccount: record.preferredAccount,
|
||||
strategicAccount: record.strategicAccount,
|
||||
requiresApproval: record.requiresApproval,
|
||||
blockedAccount: record.blockedAccount,
|
||||
updatedAt: record.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -43,6 +50,12 @@ function mapDetail(record: Customer | Vendor): CrmRecordDetailDto {
|
||||
|
||||
type CustomerSummaryRecord = Customer & {
|
||||
parentCustomer: Pick<Customer, "id" | "name"> | null;
|
||||
_count: {
|
||||
contactEntries: number;
|
||||
contacts: number;
|
||||
childCustomers: number;
|
||||
};
|
||||
contactEntries: Array<Pick<ContactEntryWithAuthor, "contactAt">>;
|
||||
};
|
||||
|
||||
type CustomerDetailedRecord = Customer & {
|
||||
@@ -57,6 +70,14 @@ type VendorDetailedRecord = Vendor & {
|
||||
contacts: ContactRecord[];
|
||||
};
|
||||
|
||||
type VendorSummaryRecord = Vendor & {
|
||||
_count: {
|
||||
contactEntries: number;
|
||||
contacts: number;
|
||||
};
|
||||
contactEntries: Array<Pick<ContactEntryWithAuthor, "contactAt">>;
|
||||
};
|
||||
|
||||
type ContactRecord = {
|
||||
id: string;
|
||||
fullName: string;
|
||||
@@ -75,16 +96,39 @@ function mapCustomerChild(record: Pick<Customer, "id" | "name" | "status">): Crm
|
||||
};
|
||||
}
|
||||
|
||||
function mapCustomerSummary(record: CustomerSummaryRecord): CrmRecordSummaryDto {
|
||||
function mapRollups(input: {
|
||||
lastContactAt?: Date | null;
|
||||
contactHistoryCount: number;
|
||||
contactCount: number;
|
||||
attachmentCount: number;
|
||||
childCustomerCount?: number;
|
||||
}): CrmRecordRollupsDto {
|
||||
return {
|
||||
lastContactAt: input.lastContactAt ? input.lastContactAt.toISOString() : null,
|
||||
contactHistoryCount: input.contactHistoryCount,
|
||||
contactCount: input.contactCount,
|
||||
attachmentCount: input.attachmentCount,
|
||||
childCustomerCount: input.childCustomerCount,
|
||||
};
|
||||
}
|
||||
|
||||
function mapCustomerSummary(record: CustomerSummaryRecord, attachmentCount: number): CrmRecordSummaryDto {
|
||||
return {
|
||||
...mapSummary(record),
|
||||
isReseller: record.isReseller,
|
||||
parentCustomerId: record.parentCustomer?.id ?? null,
|
||||
parentCustomerName: record.parentCustomer?.name ?? null,
|
||||
rollups: mapRollups({
|
||||
lastContactAt: record.contactEntries[0]?.contactAt ?? null,
|
||||
contactHistoryCount: record._count.contactEntries,
|
||||
contactCount: record._count.contacts,
|
||||
attachmentCount,
|
||||
childCustomerCount: record._count.childCustomers,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function mapCustomerDetail(record: CustomerDetailedRecord): CrmRecordDetailDto {
|
||||
function mapCustomerDetail(record: CustomerDetailedRecord, attachmentCount: number): CrmRecordDetailDto {
|
||||
return {
|
||||
...mapDetailedRecord(record),
|
||||
isReseller: record.isReseller,
|
||||
@@ -96,7 +140,19 @@ function mapCustomerDetail(record: CustomerDetailedRecord): CrmRecordDetailDto {
|
||||
currencyCode: record.currencyCode,
|
||||
taxExempt: record.taxExempt,
|
||||
creditHold: record.creditHold,
|
||||
lifecycleStage: record.lifecycleStage as CrmLifecycleStage,
|
||||
preferredAccount: record.preferredAccount,
|
||||
strategicAccount: record.strategicAccount,
|
||||
requiresApproval: record.requiresApproval,
|
||||
blockedAccount: record.blockedAccount,
|
||||
contacts: record.contacts.map(mapCrmContact),
|
||||
rollups: mapRollups({
|
||||
lastContactAt: record.contactEntries[0]?.contactAt ?? null,
|
||||
contactHistoryCount: record.contactEntries.length,
|
||||
contactCount: record.contacts.length,
|
||||
attachmentCount,
|
||||
childCustomerCount: record.childCustomers.length,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,14 +168,37 @@ function mapCrmContact(record: ContactRecord): CrmContactDto {
|
||||
};
|
||||
}
|
||||
|
||||
function mapVendorDetail(record: VendorDetailedRecord): CrmRecordDetailDto {
|
||||
function mapVendorSummary(record: VendorSummaryRecord, attachmentCount: number): CrmRecordSummaryDto {
|
||||
return {
|
||||
...mapSummary(record),
|
||||
rollups: mapRollups({
|
||||
lastContactAt: record.contactEntries[0]?.contactAt ?? null,
|
||||
contactHistoryCount: record._count.contactEntries,
|
||||
contactCount: record._count.contacts,
|
||||
attachmentCount,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function mapVendorDetail(record: VendorDetailedRecord, attachmentCount: number): CrmRecordDetailDto {
|
||||
return {
|
||||
...mapDetailedRecord(record),
|
||||
paymentTerms: record.paymentTerms,
|
||||
currencyCode: record.currencyCode,
|
||||
taxExempt: record.taxExempt,
|
||||
creditHold: record.creditHold,
|
||||
lifecycleStage: record.lifecycleStage as CrmLifecycleStage,
|
||||
preferredAccount: record.preferredAccount,
|
||||
strategicAccount: record.strategicAccount,
|
||||
requiresApproval: record.requiresApproval,
|
||||
blockedAccount: record.blockedAccount,
|
||||
contacts: record.contacts.map(mapCrmContact),
|
||||
rollups: mapRollups({
|
||||
lastContactAt: record.contactEntries[0]?.contactAt ?? null,
|
||||
contactHistoryCount: record.contactEntries.length,
|
||||
contactCount: record.contacts.length,
|
||||
attachmentCount,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -177,15 +256,50 @@ function mapDetailedRecord(record: DetailedRecord): CrmRecordDetailDto {
|
||||
interface CrmListFilters {
|
||||
query?: string;
|
||||
status?: CrmRecordStatus;
|
||||
lifecycleStage?: CrmLifecycleStage;
|
||||
state?: string;
|
||||
flag?: "PREFERRED" | "STRATEGIC" | "REQUIRES_APPROVAL" | "BLOCKED";
|
||||
}
|
||||
|
||||
async function getAttachmentCountMap(ownerType: string, ownerIds: string[]) {
|
||||
if (ownerIds.length === 0) {
|
||||
return new Map<string, number>();
|
||||
}
|
||||
|
||||
const groupedAttachments = await prisma.fileAttachment.groupBy({
|
||||
by: ["ownerId"],
|
||||
where: {
|
||||
ownerType,
|
||||
ownerId: {
|
||||
in: ownerIds,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
});
|
||||
|
||||
return new Map(groupedAttachments.map((entry) => [entry.ownerId, entry._count._all]));
|
||||
}
|
||||
|
||||
function buildWhereClause(filters: CrmListFilters) {
|
||||
const trimmedQuery = filters.query?.trim();
|
||||
const trimmedState = filters.state?.trim();
|
||||
const flagFilter =
|
||||
filters.flag === "PREFERRED"
|
||||
? { preferredAccount: true }
|
||||
: filters.flag === "STRATEGIC"
|
||||
? { strategicAccount: true }
|
||||
: filters.flag === "REQUIRES_APPROVAL"
|
||||
? { requiresApproval: true }
|
||||
: filters.flag === "BLOCKED"
|
||||
? { blockedAccount: true }
|
||||
: {};
|
||||
|
||||
return {
|
||||
...(filters.status ? { status: filters.status } : {}),
|
||||
...(filters.lifecycleStage ? { lifecycleStage: filters.lifecycleStage } : {}),
|
||||
...flagFilter,
|
||||
...(trimmedState ? { state: { contains: trimmedState } } : {}),
|
||||
...(trimmedQuery
|
||||
? {
|
||||
@@ -213,11 +327,28 @@ export async function listCustomers(filters: CrmListFilters = {}) {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
contactEntries: {
|
||||
select: {
|
||||
contactAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
contactAt: "desc",
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
contactEntries: true,
|
||||
contacts: true,
|
||||
childCustomers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
|
||||
return customers.map(mapCustomerSummary);
|
||||
const attachmentCountMap = await getAttachmentCountMap("crm-customer", customers.map((customer) => customer.id));
|
||||
return customers.map((customer) => mapCustomerSummary(customer, attachmentCountMap.get(customer.id) ?? 0));
|
||||
}
|
||||
|
||||
export async function getCustomerById(customerId: string) {
|
||||
@@ -252,7 +383,18 @@ export async function getCustomerById(customerId: string) {
|
||||
},
|
||||
});
|
||||
|
||||
return customer ? mapCustomerDetail(customer) : null;
|
||||
if (!customer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attachmentCount = await prisma.fileAttachment.count({
|
||||
where: {
|
||||
ownerType: "crm-customer",
|
||||
ownerId: customerId,
|
||||
},
|
||||
});
|
||||
|
||||
return mapCustomerDetail(customer, attachmentCount);
|
||||
}
|
||||
|
||||
export async function createCustomer(payload: CrmRecordInput) {
|
||||
@@ -279,6 +421,7 @@ export async function createCustomer(payload: CrmRecordInput) {
|
||||
country: payload.country,
|
||||
status: payload.status,
|
||||
notes: payload.notes,
|
||||
lifecycleStage: payload.lifecycleStage ?? "ACTIVE",
|
||||
isReseller: payload.isReseller ?? false,
|
||||
resellerDiscountPercent: payload.resellerDiscountPercent ?? 0,
|
||||
parentCustomerId: payload.parentCustomerId ?? null,
|
||||
@@ -286,6 +429,10 @@ export async function createCustomer(payload: CrmRecordInput) {
|
||||
currencyCode: payload.currencyCode ?? "USD",
|
||||
taxExempt: payload.taxExempt ?? false,
|
||||
creditHold: payload.creditHold ?? false,
|
||||
preferredAccount: payload.preferredAccount ?? false,
|
||||
strategicAccount: payload.strategicAccount ?? false,
|
||||
requiresApproval: payload.requiresApproval ?? false,
|
||||
blockedAccount: payload.blockedAccount ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -300,7 +447,19 @@ export async function createCustomer(payload: CrmRecordInput) {
|
||||
currencyCode: customer.currencyCode,
|
||||
taxExempt: customer.taxExempt,
|
||||
creditHold: customer.creditHold,
|
||||
lifecycleStage: customer.lifecycleStage as CrmLifecycleStage,
|
||||
preferredAccount: customer.preferredAccount,
|
||||
strategicAccount: customer.strategicAccount,
|
||||
requiresApproval: customer.requiresApproval,
|
||||
blockedAccount: customer.blockedAccount,
|
||||
contacts: [],
|
||||
rollups: mapRollups({
|
||||
lastContactAt: null,
|
||||
contactHistoryCount: 0,
|
||||
contactCount: 0,
|
||||
attachmentCount: 0,
|
||||
childCustomerCount: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -341,6 +500,7 @@ export async function updateCustomer(customerId: string, payload: CrmRecordInput
|
||||
country: payload.country,
|
||||
status: payload.status,
|
||||
notes: payload.notes,
|
||||
lifecycleStage: payload.lifecycleStage ?? "ACTIVE",
|
||||
isReseller: payload.isReseller ?? false,
|
||||
resellerDiscountPercent: payload.resellerDiscountPercent ?? 0,
|
||||
parentCustomerId: payload.parentCustomerId ?? null,
|
||||
@@ -348,6 +508,10 @@ export async function updateCustomer(customerId: string, payload: CrmRecordInput
|
||||
currencyCode: payload.currencyCode ?? "USD",
|
||||
taxExempt: payload.taxExempt ?? false,
|
||||
creditHold: payload.creditHold ?? false,
|
||||
preferredAccount: payload.preferredAccount ?? false,
|
||||
strategicAccount: payload.strategicAccount ?? false,
|
||||
requiresApproval: payload.requiresApproval ?? false,
|
||||
blockedAccount: payload.blockedAccount ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -362,17 +526,47 @@ export async function updateCustomer(customerId: string, payload: CrmRecordInput
|
||||
currencyCode: customer.currencyCode,
|
||||
taxExempt: customer.taxExempt,
|
||||
creditHold: customer.creditHold,
|
||||
lifecycleStage: customer.lifecycleStage as CrmLifecycleStage,
|
||||
preferredAccount: customer.preferredAccount,
|
||||
strategicAccount: customer.strategicAccount,
|
||||
requiresApproval: customer.requiresApproval,
|
||||
blockedAccount: customer.blockedAccount,
|
||||
contacts: [],
|
||||
rollups: mapRollups({
|
||||
lastContactAt: null,
|
||||
contactHistoryCount: 0,
|
||||
contactCount: 0,
|
||||
attachmentCount: 0,
|
||||
childCustomerCount: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listVendors(filters: CrmListFilters = {}) {
|
||||
const vendors = await prisma.vendor.findMany({
|
||||
where: buildWhereClause(filters),
|
||||
include: {
|
||||
contactEntries: {
|
||||
select: {
|
||||
contactAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
contactAt: "desc",
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
contactEntries: true,
|
||||
contacts: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
|
||||
return vendors.map(mapSummary);
|
||||
const attachmentCountMap = await getAttachmentCountMap("crm-vendor", vendors.map((vendor) => vendor.id));
|
||||
return vendors.map((vendor) => mapVendorSummary(vendor, attachmentCountMap.get(vendor.id) ?? 0));
|
||||
}
|
||||
|
||||
export async function listCustomerHierarchyOptions(excludeCustomerId?: string) {
|
||||
@@ -422,7 +616,18 @@ export async function getVendorById(vendorId: string) {
|
||||
},
|
||||
});
|
||||
|
||||
return vendor ? mapVendorDetail(vendor) : null;
|
||||
if (!vendor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attachmentCount = await prisma.fileAttachment.count({
|
||||
where: {
|
||||
ownerType: "crm-vendor",
|
||||
ownerId: vendorId,
|
||||
},
|
||||
});
|
||||
|
||||
return mapVendorDetail(vendor, attachmentCount);
|
||||
}
|
||||
|
||||
export async function createVendor(payload: CrmRecordInput) {
|
||||
@@ -438,11 +643,16 @@ export async function createVendor(payload: CrmRecordInput) {
|
||||
postalCode: payload.postalCode,
|
||||
country: payload.country,
|
||||
status: payload.status,
|
||||
lifecycleStage: payload.lifecycleStage ?? "ACTIVE",
|
||||
notes: payload.notes,
|
||||
paymentTerms: payload.paymentTerms ?? null,
|
||||
currencyCode: payload.currencyCode ?? "USD",
|
||||
taxExempt: payload.taxExempt ?? false,
|
||||
creditHold: payload.creditHold ?? false,
|
||||
preferredAccount: payload.preferredAccount ?? false,
|
||||
strategicAccount: payload.strategicAccount ?? false,
|
||||
requiresApproval: payload.requiresApproval ?? false,
|
||||
blockedAccount: payload.blockedAccount ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -452,7 +662,18 @@ export async function createVendor(payload: CrmRecordInput) {
|
||||
currencyCode: vendor.currencyCode,
|
||||
taxExempt: vendor.taxExempt,
|
||||
creditHold: vendor.creditHold,
|
||||
lifecycleStage: vendor.lifecycleStage as CrmLifecycleStage,
|
||||
preferredAccount: vendor.preferredAccount,
|
||||
strategicAccount: vendor.strategicAccount,
|
||||
requiresApproval: vendor.requiresApproval,
|
||||
blockedAccount: vendor.blockedAccount,
|
||||
contacts: [],
|
||||
rollups: mapRollups({
|
||||
lastContactAt: null,
|
||||
contactHistoryCount: 0,
|
||||
contactCount: 0,
|
||||
attachmentCount: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -478,11 +699,16 @@ export async function updateVendor(vendorId: string, payload: CrmRecordInput) {
|
||||
postalCode: payload.postalCode,
|
||||
country: payload.country,
|
||||
status: payload.status,
|
||||
lifecycleStage: payload.lifecycleStage ?? "ACTIVE",
|
||||
notes: payload.notes,
|
||||
paymentTerms: payload.paymentTerms ?? null,
|
||||
currencyCode: payload.currencyCode ?? "USD",
|
||||
taxExempt: payload.taxExempt ?? false,
|
||||
creditHold: payload.creditHold ?? false,
|
||||
preferredAccount: payload.preferredAccount ?? false,
|
||||
strategicAccount: payload.strategicAccount ?? false,
|
||||
requiresApproval: payload.requiresApproval ?? false,
|
||||
blockedAccount: payload.blockedAccount ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -492,7 +718,18 @@ export async function updateVendor(vendorId: string, payload: CrmRecordInput) {
|
||||
currencyCode: vendor.currencyCode,
|
||||
taxExempt: vendor.taxExempt,
|
||||
creditHold: vendor.creditHold,
|
||||
lifecycleStage: vendor.lifecycleStage as CrmLifecycleStage,
|
||||
preferredAccount: vendor.preferredAccount,
|
||||
strategicAccount: vendor.strategicAccount,
|
||||
requiresApproval: vendor.requiresApproval,
|
||||
blockedAccount: vendor.blockedAccount,
|
||||
contacts: [],
|
||||
rollups: mapRollups({
|
||||
lastContactAt: null,
|
||||
contactHistoryCount: 0,
|
||||
contactCount: 0,
|
||||
attachmentCount: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
|
||||
import { fail, ok } from "../../lib/http.js";
|
||||
import { requirePermissions } from "../../lib/rbac.js";
|
||||
import { createAttachment, getAttachmentContent, getAttachmentMetadata, listAttachmentsByOwner } from "./service.js";
|
||||
import { createAttachment, deleteAttachment, getAttachmentContent, getAttachmentMetadata, listAttachmentsByOwner } from "./service.js";
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
@@ -71,3 +71,7 @@ filesRouter.get("/:id/content", requirePermissions([permissions.filesRead]), asy
|
||||
response.setHeader("Content-Disposition", `inline; filename="${file.originalName}"`);
|
||||
return response.send(content);
|
||||
});
|
||||
|
||||
filesRouter.delete("/:id", requirePermissions([permissions.filesWrite]), async (request, response) => {
|
||||
return ok(response, await deleteAttachment(String(request.params.id)));
|
||||
});
|
||||
|
||||
@@ -78,3 +78,23 @@ export async function getAttachmentContent(id: string) {
|
||||
content: await fs.readFile(path.join(paths.dataDir, file.relativePath)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteAttachment(id: string) {
|
||||
const file = await prisma.fileAttachment.findUniqueOrThrow({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
try {
|
||||
await fs.unlink(path.join(paths.dataDir, file.relativePath));
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.fileAttachment.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return mapFile(file);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user