x
This commit is contained in:
1 parent
d5ea45a536
commit
afd6904c48
20 files changed
+5435
-51
No files matched your search
@@ -1872,6 +1872,12 @@ export const businessAnalyticsRoute = new Elysia({ prefix: "/business-analytics"
|
||||
.slice(0, 3)
|
||||
.map(([name]) => name);
|
||||
|
||||
let predictedNextOrderDate = "";
|
||||
if (c.lastTimestamp > 0 && c.daysBetweenOrders > 0) {
|
||||
const predictedTs = c.lastTimestamp + c.daysBetweenOrders * 24 * 60 * 60 * 1000;
|
||||
predictedNextOrderDate = new Date(predictedTs).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
return {
|
||||
id: c.id,
|
||||
identifier: c.identifier,
|
||||
@@ -1885,6 +1891,7 @@ export const businessAnalyticsRoute = new Elysia({ prefix: "/business-analytics"
|
||||
firstOrderDate: c.firstOrderDate,
|
||||
lastOrderDate: c.lastOrderDate,
|
||||
daysBetweenOrders: c.daysBetweenOrders,
|
||||
predictedNextOrderDate,
|
||||
location: c.location,
|
||||
topProducts: sortedProds,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import prisma from "../lib/prisma";
|
||||
|
||||
export const crmRoute = new Elysia({ prefix: "/crm" })
|
||||
// ── 1. Import Single / Multiple Customers ─────────────────────────────────
|
||||
.post(
|
||||
"/import",
|
||||
async ({ body, set }) => {
|
||||
try {
|
||||
const { customers } = body as {
|
||||
customers: Array<{
|
||||
channel: string;
|
||||
shop?: string;
|
||||
identifier: string;
|
||||
displayName: string;
|
||||
location?: string;
|
||||
province?: string;
|
||||
district?: string;
|
||||
orderCount?: number;
|
||||
totalSpent?: number;
|
||||
avgIntervalDays?: number;
|
||||
firstOrderDate?: string;
|
||||
lastOrderDate?: string;
|
||||
predictedNextOrderDate?: string;
|
||||
topProducts?: string[];
|
||||
initialStatus?: string;
|
||||
strategyType?: string;
|
||||
promoCode?: string;
|
||||
discountOffered?: string;
|
||||
notes?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
if (!customers || !Array.isArray(customers) || customers.length === 0) {
|
||||
set.status = 400;
|
||||
return { error: "customers array is required" };
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const c of customers) {
|
||||
const channel = (c.channel || "tiktok").toLowerCase();
|
||||
const identifier = (c.identifier || "").trim();
|
||||
if (!identifier) continue;
|
||||
|
||||
// Upsert MarketplaceCustomer
|
||||
const cust = await prisma.marketplaceCustomer.upsert({
|
||||
where: {
|
||||
channel_identifier: {
|
||||
channel,
|
||||
identifier,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
displayName: c.displayName || identifier,
|
||||
shop: c.shop || "hom-aroma",
|
||||
location: c.location || undefined,
|
||||
province: c.province || undefined,
|
||||
district: c.district || undefined,
|
||||
orderCount: typeof c.orderCount === "number" ? c.orderCount : undefined,
|
||||
totalSpent: typeof c.totalSpent === "number" ? c.totalSpent : undefined,
|
||||
avgIntervalDays: typeof c.avgIntervalDays === "number" ? c.avgIntervalDays : undefined,
|
||||
firstOrderDate: c.firstOrderDate || undefined,
|
||||
lastOrderDate: c.lastOrderDate || undefined,
|
||||
predictedNextOrderDate: c.predictedNextOrderDate || undefined,
|
||||
topProducts: c.topProducts ? (c.topProducts as any) : undefined,
|
||||
},
|
||||
create: {
|
||||
channel,
|
||||
shop: c.shop || "hom-aroma",
|
||||
identifier,
|
||||
displayName: c.displayName || identifier,
|
||||
location: c.location || "",
|
||||
province: c.province || "",
|
||||
district: c.district || "",
|
||||
orderCount: c.orderCount || 0,
|
||||
totalSpent: c.totalSpent || 0,
|
||||
avgIntervalDays: c.avgIntervalDays || 0,
|
||||
firstOrderDate: c.firstOrderDate || "",
|
||||
lastOrderDate: c.lastOrderDate || "",
|
||||
predictedNextOrderDate: c.predictedNextOrderDate || "",
|
||||
topProducts: c.topProducts ? (c.topProducts as any) : [],
|
||||
},
|
||||
});
|
||||
|
||||
// Check if CRM record exists
|
||||
let crm = await prisma.marketplaceCustomerCrm.findUnique({
|
||||
where: { customerId: cust.id },
|
||||
});
|
||||
|
||||
if (!crm) {
|
||||
crm = await prisma.marketplaceCustomerCrm.create({
|
||||
data: {
|
||||
customerId: cust.id,
|
||||
status: c.initialStatus || "pending",
|
||||
strategyType: c.strategyType || "due_restock",
|
||||
promoCodeSent: c.promoCode || null,
|
||||
discountOffered: c.discountOffered || null,
|
||||
notes: c.notes || null,
|
||||
activities: {
|
||||
create: {
|
||||
action: "IMPORT",
|
||||
content: `นำเข้าลูกค้าจาก Action Plan (${channel})`,
|
||||
author: "System",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
results.push({
|
||||
id: cust.id,
|
||||
identifier: cust.identifier,
|
||||
channel: cust.channel,
|
||||
displayName: cust.displayName,
|
||||
crmId: crm.id,
|
||||
status: crm.status,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
importedCount: results.length,
|
||||
results,
|
||||
};
|
||||
} catch (err: any) {
|
||||
set.status = 500;
|
||||
return { error: err.message || "Failed to import customers" };
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
customers: t.Array(
|
||||
t.Object({
|
||||
channel: t.String(),
|
||||
shop: t.Optional(t.String()),
|
||||
identifier: t.String(),
|
||||
displayName: t.String(),
|
||||
location: t.Optional(t.String()),
|
||||
province: t.Optional(t.String()),
|
||||
district: t.Optional(t.String()),
|
||||
orderCount: t.Optional(t.Number()),
|
||||
totalSpent: t.Optional(t.Number()),
|
||||
avgIntervalDays: t.Optional(t.Number()),
|
||||
firstOrderDate: t.Optional(t.String()),
|
||||
lastOrderDate: t.Optional(t.String()),
|
||||
predictedNextOrderDate: t.Optional(t.String()),
|
||||
topProducts: t.Optional(t.Array(t.String())),
|
||||
initialStatus: t.Optional(t.String()),
|
||||
strategyType: t.Optional(t.String()),
|
||||
promoCode: t.Optional(t.String()),
|
||||
discountOffered: t.Optional(t.String()),
|
||||
notes: t.Optional(t.String()),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
// ── 2. Check Which Customers are Already Imported ─────────────────────────
|
||||
.post(
|
||||
"/check-imported",
|
||||
async ({ body, set }) => {
|
||||
try {
|
||||
const { keys } = body as {
|
||||
keys: Array<{ channel: string; identifier: string }>;
|
||||
};
|
||||
|
||||
if (!keys || !Array.isArray(keys) || keys.length === 0) {
|
||||
return { importedMap: {} };
|
||||
}
|
||||
|
||||
const conditions = keys.map((k) => ({
|
||||
channel: k.channel.toLowerCase(),
|
||||
identifier: k.identifier.trim(),
|
||||
}));
|
||||
|
||||
const existing = await prisma.marketplaceCustomer.findMany({
|
||||
where: {
|
||||
OR: conditions,
|
||||
},
|
||||
include: {
|
||||
crm: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Map key format: "channel:identifier" => { customerId, crmId, status, notes, promoCodeSent }
|
||||
const importedMap: Record<string, any> = {};
|
||||
for (const item of existing) {
|
||||
const mapKey = `${item.channel}:${item.identifier.toLowerCase()}`;
|
||||
importedMap[mapKey] = {
|
||||
id: item.id,
|
||||
channel: item.channel,
|
||||
identifier: item.identifier,
|
||||
displayName: item.displayName,
|
||||
crmId: item.crm?.id,
|
||||
status: item.crm?.status || "pending",
|
||||
notes: item.crm?.notes || "",
|
||||
promoCodeSent: item.crm?.promoCodeSent || "",
|
||||
discountOffered: item.crm?.discountOffered || "",
|
||||
strategyType: item.crm?.strategyType || "due_restock",
|
||||
updatedAt: item.crm?.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
return { importedMap };
|
||||
} catch (err: any) {
|
||||
set.status = 500;
|
||||
return { error: err.message || "Failed to check imported customers" };
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
keys: t.Array(
|
||||
t.Object({
|
||||
channel: t.String(),
|
||||
identifier: t.String(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
// ── 3. List CRM Customers with Stats, Filters, and Pagination ─────────────
|
||||
.get("/customers", async ({ query, set }) => {
|
||||
try {
|
||||
const channel = (query.channel as string) || "all";
|
||||
const status = (query.status as string) || "all";
|
||||
const strategyType = (query.strategyType as string) || "all";
|
||||
const search = (query.search as string) || "";
|
||||
const page = Math.max(1, parseInt((query.page as string) || "1"));
|
||||
const limit = Math.max(1, Math.min(100, parseInt((query.limit as string) || "20")));
|
||||
const sortBy = (query.sortBy as string) || "updatedAt"; // 'updatedAt' | 'totalSpent' | 'orderCount' | 'nextFollowUpDate'
|
||||
const sortOrder = (query.sortOrder as string) || "desc";
|
||||
|
||||
const where: any = {};
|
||||
|
||||
if (channel !== "all") {
|
||||
where.channel = channel;
|
||||
}
|
||||
|
||||
if (status !== "all") {
|
||||
where.crm = { ...(where.crm || {}), status };
|
||||
}
|
||||
|
||||
if (strategyType !== "all") {
|
||||
where.crm = { ...(where.crm || {}), strategyType };
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ displayName: { contains: search, mode: "insensitive" } },
|
||||
{ identifier: { contains: search, mode: "insensitive" } },
|
||||
{ location: { contains: search, mode: "insensitive" } },
|
||||
{ crm: { notes: { contains: search, mode: "insensitive" } } },
|
||||
];
|
||||
}
|
||||
|
||||
// Total count
|
||||
const totalCount = await prisma.marketplaceCustomer.count({ where });
|
||||
|
||||
// Build Order By
|
||||
let orderBy: any = {};
|
||||
if (sortBy === "totalSpent") {
|
||||
orderBy = { totalSpent: sortOrder };
|
||||
} else if (sortBy === "orderCount") {
|
||||
orderBy = { orderCount: sortOrder };
|
||||
} else if (sortBy === "nextFollowUpDate") {
|
||||
orderBy = { crm: { nextFollowUpDate: sortOrder } };
|
||||
} else {
|
||||
orderBy = { updatedAt: sortOrder };
|
||||
}
|
||||
|
||||
const customers = await prisma.marketplaceCustomer.findMany({
|
||||
where,
|
||||
include: {
|
||||
crm: {
|
||||
include: {
|
||||
activities: {
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy,
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
// Overall CRM summary metrics
|
||||
const [
|
||||
totalInCrm,
|
||||
pendingCount,
|
||||
contactedCount,
|
||||
convertedCount,
|
||||
unresponsiveCount,
|
||||
aggregateStats,
|
||||
] = await Promise.all([
|
||||
prisma.marketplaceCustomerCrm.count(),
|
||||
prisma.marketplaceCustomerCrm.count({ where: { status: "pending" } }),
|
||||
prisma.marketplaceCustomerCrm.count({ where: { status: "contacted" } }),
|
||||
prisma.marketplaceCustomerCrm.count({ where: { status: "converted" } }),
|
||||
prisma.marketplaceCustomerCrm.count({ where: { status: "unresponsive" } }),
|
||||
prisma.marketplaceCustomer.aggregate({
|
||||
_sum: { totalSpent: true },
|
||||
_avg: { avgIntervalDays: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const conversionRate =
|
||||
contactedCount + convertedCount > 0
|
||||
? Number(((convertedCount / (contactedCount + convertedCount)) * 100).toFixed(1))
|
||||
: 0;
|
||||
|
||||
return {
|
||||
summary: {
|
||||
totalInCrm,
|
||||
pendingCount,
|
||||
contactedCount,
|
||||
convertedCount,
|
||||
unresponsiveCount,
|
||||
conversionRate,
|
||||
totalRevenueInCrm: aggregateStats._sum.totalSpent || 0,
|
||||
},
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
},
|
||||
customers: customers.map((c) => ({
|
||||
id: c.id,
|
||||
channel: c.channel,
|
||||
shop: c.shop,
|
||||
identifier: c.identifier,
|
||||
displayName: c.displayName,
|
||||
location: c.location,
|
||||
province: c.province,
|
||||
district: c.district,
|
||||
orderCount: c.orderCount,
|
||||
totalSpent: c.totalSpent,
|
||||
avgIntervalDays: c.avgIntervalDays,
|
||||
firstOrderDate: c.firstOrderDate,
|
||||
lastOrderDate: c.lastOrderDate,
|
||||
predictedNextOrderDate: c.predictedNextOrderDate,
|
||||
topProducts: c.topProducts,
|
||||
crm: c.crm
|
||||
? {
|
||||
id: c.crm.id,
|
||||
status: c.crm.status,
|
||||
strategyType: c.crm.strategyType,
|
||||
promoCodeSent: c.crm.promoCodeSent,
|
||||
discountOffered: c.crm.discountOffered,
|
||||
lastContactedAt: c.crm.lastContactedAt,
|
||||
nextFollowUpDate: c.crm.nextFollowUpDate,
|
||||
notes: c.crm.notes,
|
||||
assignedTo: c.crm.assignedTo,
|
||||
activities: c.crm.activities,
|
||||
updatedAt: c.crm.updatedAt,
|
||||
}
|
||||
: null,
|
||||
})),
|
||||
};
|
||||
} catch (err: any) {
|
||||
set.status = 500;
|
||||
return { error: err.message || "Failed to list CRM customers" };
|
||||
}
|
||||
})
|
||||
|
||||
// ── 3.1 Get Single CRM Customer Detail with Order History ─────────────────
|
||||
.get("/customers/:id", async ({ params, set }) => {
|
||||
try {
|
||||
const id = parseInt(params.id);
|
||||
const cust = await prisma.marketplaceCustomer.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
crm: {
|
||||
include: {
|
||||
activities: {
|
||||
orderBy: { createdAt: "desc" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!cust) {
|
||||
set.status = 404;
|
||||
return { error: "Customer not found" };
|
||||
}
|
||||
|
||||
// Fetch order history for this customer based on channel & identifier
|
||||
let orders: any[] = [];
|
||||
const channel = cust.channel.toLowerCase();
|
||||
const identifier = cust.identifier;
|
||||
|
||||
if (channel === "tiktok") {
|
||||
const dbOrders = await prisma.tiktokOrder.findMany({
|
||||
where: {
|
||||
buyerUsername: identifier,
|
||||
},
|
||||
orderBy: { createdTime: "desc" },
|
||||
include: {
|
||||
items: true,
|
||||
},
|
||||
});
|
||||
orders = dbOrders.map((o) => ({
|
||||
orderId: o.orderId,
|
||||
channel: "tiktok",
|
||||
shop: o.shop,
|
||||
date: o.createdTime,
|
||||
status: o.status,
|
||||
amount: Number(o.orderAmount) || 0,
|
||||
shippingFee: Number(o.shippingFee) || 0,
|
||||
trackingId: o.trackingId,
|
||||
deliveryOption: o.deliveryOption,
|
||||
items: o.items.map((it) => ({
|
||||
name: it.productName,
|
||||
variation: it.variation,
|
||||
quantity: it.quantity,
|
||||
price: Number(it.subtotalAfterDiscount || it.unitOriginalPrice) || 0,
|
||||
})),
|
||||
}));
|
||||
} else if (channel === "shopee") {
|
||||
const dbOrders = await prisma.shopeeOrder.findMany({
|
||||
where: {
|
||||
buyerUsername: identifier,
|
||||
},
|
||||
orderBy: { orderCreatedAt: "desc" },
|
||||
include: {
|
||||
items: true,
|
||||
},
|
||||
});
|
||||
orders = dbOrders.map((o) => ({
|
||||
orderId: o.orderId,
|
||||
channel: "shopee",
|
||||
shop: o.shop,
|
||||
date: o.orderCreatedAt,
|
||||
status: o.status,
|
||||
amount: Number(o.buyerPaidAmount || o.totalAmount) || 0,
|
||||
shippingFee: Number(o.shippingFee) || 0,
|
||||
trackingId: o.trackingNo,
|
||||
items: o.items.map((it) => ({
|
||||
name: it.productName,
|
||||
variation: it.variantName,
|
||||
quantity: it.quantity,
|
||||
price: Number(it.dealPrice || it.originalPrice) || 0,
|
||||
})),
|
||||
}));
|
||||
} else if (channel === "lazada") {
|
||||
const parts = identifier.split("_");
|
||||
const namePart = parts[0];
|
||||
const postCodePart = parts[1] || "";
|
||||
const where: any = {
|
||||
customerName: { equals: namePart, mode: "insensitive" },
|
||||
};
|
||||
if (postCodePart) {
|
||||
where.shippingPostCode = postCodePart;
|
||||
}
|
||||
const dbOrders = await prisma.lazadaOrder.findMany({
|
||||
where,
|
||||
orderBy: { createTime: "desc" },
|
||||
include: {
|
||||
items: true,
|
||||
},
|
||||
});
|
||||
orders = dbOrders.map((o) => ({
|
||||
orderId: o.orderNumber,
|
||||
channel: "lazada",
|
||||
shop: o.shop,
|
||||
date: o.createTime,
|
||||
status: o.status || "Completed",
|
||||
amount: Number(o.orderAmount) || 0,
|
||||
shippingFee: Number(o.shippingFee) || 0,
|
||||
trackingId: o.trackingCode,
|
||||
items: o.items.map((it) => ({
|
||||
name: it.itemName,
|
||||
variation: it.variation,
|
||||
quantity: 1,
|
||||
price: Number(it.paidPrice || it.unitPrice) || 0,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
customer: cust,
|
||||
orders,
|
||||
};
|
||||
} catch (err: any) {
|
||||
set.status = 500;
|
||||
return { error: err.message || "Failed to fetch customer detail" };
|
||||
}
|
||||
})
|
||||
|
||||
// ── 4. Update CRM Status & Notes ──────────────────────────────────────────
|
||||
.patch(
|
||||
"/customers/:id",
|
||||
async ({ params, body, set }) => {
|
||||
try {
|
||||
const id = parseInt(params.id);
|
||||
const {
|
||||
status,
|
||||
strategyType,
|
||||
notes,
|
||||
promoCodeSent,
|
||||
discountOffered,
|
||||
nextFollowUpDate,
|
||||
assignedTo,
|
||||
activityAction,
|
||||
activityContent,
|
||||
author,
|
||||
} = body as {
|
||||
status?: string;
|
||||
strategyType?: string;
|
||||
notes?: string;
|
||||
promoCodeSent?: string;
|
||||
discountOffered?: string;
|
||||
nextFollowUpDate?: string | null;
|
||||
assignedTo?: string;
|
||||
activityAction?: string;
|
||||
activityContent?: string;
|
||||
author?: string;
|
||||
};
|
||||
|
||||
// Find customer
|
||||
const cust = await prisma.marketplaceCustomer.findUnique({
|
||||
where: { id },
|
||||
include: { crm: true },
|
||||
});
|
||||
|
||||
if (!cust) {
|
||||
set.status = 404;
|
||||
return { error: "Marketplace customer not found" };
|
||||
}
|
||||
|
||||
const updateData: any = {};
|
||||
if (status) {
|
||||
updateData.status = status;
|
||||
if (status === "contacted" || status === "converted") {
|
||||
updateData.lastContactedAt = new Date();
|
||||
}
|
||||
}
|
||||
if (strategyType) updateData.strategyType = strategyType;
|
||||
if (typeof notes !== "undefined") updateData.notes = notes;
|
||||
if (typeof promoCodeSent !== "undefined") updateData.promoCodeSent = promoCodeSent;
|
||||
if (typeof discountOffered !== "undefined") updateData.discountOffered = discountOffered;
|
||||
if (typeof assignedTo !== "undefined") updateData.assignedTo = assignedTo;
|
||||
if (typeof nextFollowUpDate !== "undefined") {
|
||||
updateData.nextFollowUpDate = nextFollowUpDate ? new Date(nextFollowUpDate) : null;
|
||||
}
|
||||
|
||||
let crm = cust.crm;
|
||||
|
||||
if (!crm) {
|
||||
crm = await prisma.marketplaceCustomerCrm.create({
|
||||
data: {
|
||||
customerId: cust.id,
|
||||
status: status || "pending",
|
||||
strategyType: strategyType || "due_restock",
|
||||
...updateData,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
crm = await prisma.marketplaceCustomerCrm.update({
|
||||
where: { id: crm.id },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
// Add Activity Log if supplied or if status changed
|
||||
if (activityAction && activityContent) {
|
||||
await prisma.customerCrmActivity.create({
|
||||
data: {
|
||||
crmId: crm.id,
|
||||
action: activityAction,
|
||||
content: activityContent,
|
||||
author: author || "Admin",
|
||||
},
|
||||
});
|
||||
} else if (status && status !== cust.crm?.status) {
|
||||
const statusLabels: Record<string, string> = {
|
||||
pending: "⏳ รอดำเนินการ",
|
||||
contacted: "💬 ส่งโปรแล้ว",
|
||||
converted: "✅ ซื้อซ้ำสำเร็จ",
|
||||
unresponsive: "❌ ลูกค้าไม่ตอบ/ไม่สนใจ",
|
||||
do_not_disturb: "⛔ ขอไม่รับการติดต่อ",
|
||||
};
|
||||
await prisma.customerCrmActivity.create({
|
||||
data: {
|
||||
crmId: crm.id,
|
||||
action: "STATUS_CHANGE",
|
||||
content: `เปลี่ยนสถานะเป็น: ${statusLabels[status] || status}`,
|
||||
author: author || "Admin",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
customer: {
|
||||
...cust,
|
||||
crm,
|
||||
},
|
||||
};
|
||||
} catch (err: any) {
|
||||
set.status = 500;
|
||||
return { error: err.message || "Failed to update CRM status" };
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
status: t.Optional(t.String()),
|
||||
strategyType: t.Optional(t.String()),
|
||||
notes: t.Optional(t.String()),
|
||||
promoCodeSent: t.Optional(t.String()),
|
||||
discountOffered: t.Optional(t.String()),
|
||||
nextFollowUpDate: t.Optional(t.Union([t.String(), t.Null()])),
|
||||
assignedTo: t.Optional(t.String()),
|
||||
activityAction: t.Optional(t.String()),
|
||||
activityContent: t.Optional(t.String()),
|
||||
author: t.Optional(t.String()),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
// ── 5. Add Custom Activity Note ───────────────────────────────────────────
|
||||
.post(
|
||||
"/customers/:id/activities",
|
||||
async ({ params, body, set }) => {
|
||||
try {
|
||||
const customerId = parseInt(params.id);
|
||||
const { action, content, author } = body as {
|
||||
action: string;
|
||||
content: string;
|
||||
author?: string;
|
||||
};
|
||||
|
||||
const crm = await prisma.marketplaceCustomerCrm.findUnique({
|
||||
where: { customerId },
|
||||
});
|
||||
|
||||
if (!crm) {
|
||||
set.status = 404;
|
||||
return { error: "Customer CRM record not found" };
|
||||
}
|
||||
|
||||
const activity = await prisma.customerCrmActivity.create({
|
||||
data: {
|
||||
crmId: crm.id,
|
||||
action: action || "NOTE",
|
||||
content,
|
||||
author: author || "Admin",
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, activity };
|
||||
} catch (err: any) {
|
||||
set.status = 500;
|
||||
return { error: err.message || "Failed to add activity" };
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
action: t.String(),
|
||||
content: t.String(),
|
||||
author: t.Optional(t.String()),
|
||||
}),
|
||||
}
|
||||
);
|
||||
Reference in new issue
Block a user