db.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. import { eq, desc, asc, and, sql, like, or, lt, gte, lte, isNotNull } from "drizzle-orm";
  2. import { drizzle } from "drizzle-orm/postgres-js";
  3. import {
  4. InsertUser, users,
  5. conversations, InsertConversation, Conversation,
  6. messages, InsertMessage,
  7. workflowNodes, InsertWorkflowNode,
  8. workflowEdges, InsertWorkflowEdge,
  9. workflowSuggestions, InsertWorkflowSuggestion,
  10. invitations, InsertInvitation,
  11. auditLogs, InsertAuditLog,
  12. passwordResetTokens, InsertPasswordResetToken,
  13. analyticsEvents, InsertAnalyticsEvent,
  14. dataSources, InsertDataSource,
  15. apiConnections, InsertApiConnection,
  16. } from "../drizzle/schema";
  17. import { ENV } from './_core/env';
  18. let _db: ReturnType<typeof drizzle> | null = null;
  19. export async function getDb() {
  20. if (!_db && process.env.DATABASE_URL) {
  21. try {
  22. // Set search_path so unqualified queries resolve to the chatbot schema
  23. _db = drizzle(process.env.DATABASE_URL, {
  24. connection: { options: "-c search_path=chatbot,public" },
  25. });
  26. } catch (error) {
  27. console.warn("[Database] Failed to connect:", error);
  28. _db = null;
  29. }
  30. }
  31. return _db;
  32. }
  33. /* ─── User helpers ─── */
  34. export async function upsertUser(user: InsertUser): Promise<void> {
  35. if (!user.openId) {
  36. throw new Error("User openId is required for upsert");
  37. }
  38. const db = await getDb();
  39. if (!db) { console.warn("[Database] Cannot upsert user: database not available"); return; }
  40. try {
  41. const values: InsertUser = { openId: user.openId };
  42. const updateSet: Record<string, unknown> = {};
  43. const textFields = ["name", "email", "loginMethod"] as const;
  44. type TextField = (typeof textFields)[number];
  45. const assignNullable = (field: TextField) => {
  46. const value = user[field];
  47. if (value === undefined) return;
  48. const normalized = value ?? null;
  49. values[field] = normalized;
  50. updateSet[field] = normalized;
  51. };
  52. textFields.forEach(assignNullable);
  53. if (user.lastSignedIn !== undefined) { values.lastSignedIn = user.lastSignedIn; updateSet.lastSignedIn = user.lastSignedIn; }
  54. if (user.role !== undefined) { values.role = user.role; updateSet.role = user.role; }
  55. else if (user.openId === ENV.ownerOpenId) { values.role = 'admin'; updateSet.role = 'admin'; }
  56. if (!values.lastSignedIn) { values.lastSignedIn = new Date(); }
  57. if (Object.keys(updateSet).length === 0) { updateSet.lastSignedIn = new Date(); }
  58. await db.insert(users).values(values).onDuplicateKeyUpdate({ set: updateSet });
  59. } catch (error) { console.error("[Database] Failed to upsert user:", error); throw error; }
  60. }
  61. export async function getUserByOpenId(openId: string) {
  62. const db = await getDb();
  63. if (!db) { console.warn("[Database] Cannot get user: database not available"); return undefined; }
  64. const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
  65. return result.length > 0 ? result[0] : undefined;
  66. }
  67. /* ─── User management helpers ─── */
  68. export async function getAllUsers() {
  69. const db = await getDb();
  70. if (!db) return [];
  71. return db.select({
  72. id: users.id,
  73. openId: users.openId,
  74. name: users.name,
  75. email: users.email,
  76. role: users.role,
  77. createdAt: users.createdAt,
  78. lastSignedIn: users.lastSignedIn,
  79. }).from(users).orderBy(desc(users.lastSignedIn));
  80. }
  81. export async function updateUserRole(userId: number, role: "user" | "agent" | "admin") {
  82. const db = await getDb();
  83. if (!db) throw new Error("Database not available");
  84. await db.update(users).set({ role }).where(eq(users.id, userId));
  85. const result = await db.select().from(users).where(eq(users.id, userId)).limit(1);
  86. return result[0];
  87. }
  88. export async function getUserById(userId: number) {
  89. const db = await getDb();
  90. if (!db) return undefined;
  91. const result = await db.select().from(users).where(eq(users.id, userId)).limit(1);
  92. return result[0];
  93. }
  94. export async function deleteUser(userId: number) {
  95. const db = await getDb();
  96. if (!db) throw new Error("Database not available");
  97. // Get user before deleting
  98. const user = await getUserById(userId);
  99. if (!user) return null;
  100. await db.delete(users).where(eq(users.id, userId));
  101. return user;
  102. }
  103. export async function getUserByEmail(email: string) {
  104. const db = await getDb();
  105. if (!db) return undefined;
  106. const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
  107. return result[0];
  108. }
  109. export async function getUserByEmailWithPassword(email: string) {
  110. const db = await getDb();
  111. if (!db) return undefined;
  112. const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
  113. return result[0]; // includes passwordHash
  114. }
  115. export async function createUserWithPassword(data: {
  116. email: string;
  117. name: string;
  118. passwordHash: string;
  119. role?: "user" | "agent" | "admin";
  120. }) {
  121. const db = await getDb();
  122. if (!db) throw new Error("Database not available");
  123. const openId = `local_${data.email}`; // local users get a synthetic openId
  124. await db.insert(users).values({
  125. openId,
  126. email: data.email,
  127. name: data.name,
  128. passwordHash: data.passwordHash,
  129. loginMethod: "email",
  130. role: data.role || "user",
  131. lastSignedIn: new Date(),
  132. });
  133. const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
  134. return result[0];
  135. }
  136. export async function updateUserPassword(userId: number, passwordHash: string) {
  137. const db = await getDb();
  138. if (!db) throw new Error("Database not available");
  139. await db.update(users).set({ passwordHash }).where(eq(users.id, userId));
  140. }
  141. /* ─── Password reset token helpers ─── */
  142. export async function createPasswordResetToken(data: InsertPasswordResetToken) {
  143. const db = await getDb();
  144. if (!db) throw new Error("Database not available");
  145. await db.insert(passwordResetTokens).values(data);
  146. const result = await db.select().from(passwordResetTokens).where(eq(passwordResetTokens.token, data.token)).limit(1);
  147. return result[0];
  148. }
  149. export async function getPasswordResetToken(token: string) {
  150. const db = await getDb();
  151. if (!db) return undefined;
  152. const result = await db.select().from(passwordResetTokens).where(eq(passwordResetTokens.token, token)).limit(1);
  153. return result[0];
  154. }
  155. export async function markPasswordResetTokenUsed(tokenId: number) {
  156. const db = await getDb();
  157. if (!db) throw new Error("Database not available");
  158. await db.update(passwordResetTokens).set({ usedAt: new Date() }).where(eq(passwordResetTokens.id, tokenId));
  159. }
  160. /* ─── Invitation helpers ─── */
  161. export async function createInvitation(data: InsertInvitation) {
  162. const db = await getDb();
  163. if (!db) throw new Error("Database not available");
  164. await db.insert(invitations).values(data);
  165. const result = await db.select().from(invitations).where(eq(invitations.token, data.token)).limit(1);
  166. return result[0];
  167. }
  168. export async function getAllInvitations() {
  169. const db = await getDb();
  170. if (!db) return [];
  171. return db.select().from(invitations).orderBy(desc(invitations.createdAt));
  172. }
  173. export async function getInvitationByToken(token: string) {
  174. const db = await getDb();
  175. if (!db) return undefined;
  176. const result = await db.select().from(invitations).where(eq(invitations.token, token)).limit(1);
  177. return result[0];
  178. }
  179. export async function getInvitationByEmail(email: string) {
  180. const db = await getDb();
  181. if (!db) return [];
  182. return db.select().from(invitations)
  183. .where(eq(invitations.email, email))
  184. .orderBy(desc(invitations.createdAt));
  185. }
  186. export async function updateInvitationStatus(
  187. id: number,
  188. status: "pending" | "accepted" | "expired" | "revoked",
  189. acceptedByUserId?: number,
  190. ) {
  191. const db = await getDb();
  192. if (!db) throw new Error("Database not available");
  193. const updateData: Record<string, unknown> = { status };
  194. if (status === "accepted") {
  195. updateData.acceptedAt = new Date();
  196. if (acceptedByUserId) updateData.acceptedByUserId = acceptedByUserId;
  197. }
  198. await db.update(invitations).set(updateData).where(eq(invitations.id, id));
  199. const result = await db.select().from(invitations).where(eq(invitations.id, id)).limit(1);
  200. return result[0];
  201. }
  202. export async function expireOldInvitations() {
  203. const db = await getDb();
  204. if (!db) return 0;
  205. const result = await db.update(invitations)
  206. .set({ status: "expired" })
  207. .where(
  208. and(
  209. eq(invitations.status, "pending"),
  210. lt(invitations.expiresAt, new Date()),
  211. )
  212. );
  213. return result[0]?.affectedRows || 0;
  214. }
  215. /* ─── Audit log helpers ─── */
  216. export async function createAuditLog(data: InsertAuditLog) {
  217. const db = await getDb();
  218. if (!db) { console.warn("[Database] Cannot create audit log: database not available"); return; }
  219. await db.insert(auditLogs).values(data);
  220. }
  221. export async function getAuditLogs(limit = 50) {
  222. const db = await getDb();
  223. if (!db) return [];
  224. return db.select().from(auditLogs).orderBy(desc(auditLogs.createdAt)).limit(limit);
  225. }
  226. /* ─── Conversation helpers ─── */
  227. export async function createConversation(data: InsertConversation) {
  228. const db = await getDb();
  229. if (!db) throw new Error("Database not available");
  230. await db.insert(conversations).values(data);
  231. const result = await db.select().from(conversations).where(eq(conversations.sessionId, data.sessionId)).limit(1);
  232. return result[0];
  233. }
  234. export async function getConversations(status?: string) {
  235. const db = await getDb();
  236. if (!db) return [];
  237. if (status) {
  238. return db.select().from(conversations)
  239. .where(eq(conversations.status, status as any))
  240. .orderBy(desc(conversations.updatedAt));
  241. }
  242. return db.select().from(conversations).orderBy(desc(conversations.updatedAt));
  243. }
  244. /** Advanced conversation query with pagination, search, agent filter, date range, and sorting */
  245. export async function getConversationsAdvanced(params: {
  246. page?: number;
  247. pageSize?: number;
  248. status?: string;
  249. search?: string;
  250. agentId?: number;
  251. dateFrom?: string;
  252. dateTo?: string;
  253. sortBy?: string;
  254. sortOrder?: "asc" | "desc";
  255. }) {
  256. const db = await getDb();
  257. if (!db) return { conversations: [], total: 0, page: 1, pageSize: 20, totalPages: 0 };
  258. const page = params.page || 1;
  259. const pageSize = params.pageSize || 20;
  260. const offset = (page - 1) * pageSize;
  261. // Build conditions
  262. const conditions = [];
  263. if (params.status) {
  264. conditions.push(eq(conversations.status, params.status as any));
  265. }
  266. if (params.agentId) {
  267. conditions.push(eq(conversations.assignedAgentId, params.agentId));
  268. }
  269. if (params.search) {
  270. const searchTerm = `%${params.search}%`;
  271. conditions.push(
  272. or(
  273. like(conversations.visitorName, searchTerm),
  274. like(conversations.visitorEmail, searchTerm),
  275. like(conversations.sessionId, searchTerm),
  276. like(conversations.customerId, searchTerm),
  277. like(conversations.salesRep, searchTerm),
  278. )!
  279. );
  280. }
  281. if (params.dateFrom) {
  282. conditions.push(gte(conversations.createdAt, new Date(params.dateFrom)));
  283. }
  284. if (params.dateTo) {
  285. const endDate = new Date(params.dateTo);
  286. endDate.setHours(23, 59, 59, 999);
  287. conditions.push(lte(conversations.createdAt, endDate));
  288. }
  289. const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
  290. // Sort
  291. const sortFn = params.sortOrder === "asc" ? asc : desc;
  292. let orderByCol;
  293. switch (params.sortBy) {
  294. case "created": orderByCol = sortFn(conversations.createdAt); break;
  295. case "visitor": orderByCol = sortFn(conversations.visitorName); break;
  296. case "status": orderByCol = sortFn(conversations.status); break;
  297. case "customerId": orderByCol = sortFn(conversations.customerId); break;
  298. case "salesRep": orderByCol = sortFn(conversations.salesRep); break;
  299. case "agent": orderByCol = sortFn(users.name); break;
  300. default: orderByCol = sortFn(conversations.updatedAt); break;
  301. }
  302. // Count
  303. const countResult = whereClause
  304. ? await db.select({ count: sql<number>`COUNT(*)` }).from(conversations).where(whereClause)
  305. : await db.select({ count: sql<number>`COUNT(*)` }).from(conversations);
  306. const total = Number(countResult[0]?.count || 0);
  307. const totalPages = Math.ceil(total / pageSize);
  308. // Fetch page with agent name join
  309. const baseQuery = db.select({
  310. id: conversations.id,
  311. sessionId: conversations.sessionId,
  312. visitorName: conversations.visitorName,
  313. visitorEmail: conversations.visitorEmail,
  314. customerId: conversations.customerId,
  315. salesRep: conversations.salesRep,
  316. status: conversations.status,
  317. assignedAgentId: conversations.assignedAgentId,
  318. metadata: conversations.metadata,
  319. createdAt: conversations.createdAt,
  320. updatedAt: conversations.updatedAt,
  321. agentName: users.name,
  322. })
  323. .from(conversations)
  324. .leftJoin(users, eq(conversations.assignedAgentId, users.id));
  325. const rows = whereClause
  326. ? await baseQuery.where(whereClause).orderBy(orderByCol).limit(pageSize).offset(offset)
  327. : await baseQuery.orderBy(orderByCol).limit(pageSize).offset(offset);
  328. return { conversations: rows, total, page, pageSize, totalPages };
  329. }
  330. /** Get all agents (users with agent or admin role) for filter dropdown */
  331. export async function getAgentUsers() {
  332. const db = await getDb();
  333. if (!db) return [];
  334. return db.select({
  335. id: users.id,
  336. name: users.name,
  337. email: users.email,
  338. role: users.role,
  339. }).from(users).where(
  340. or(
  341. eq(users.role, "agent"),
  342. eq(users.role, "admin"),
  343. )!
  344. ).orderBy(users.name);
  345. }
  346. /** Get message count per conversation */
  347. export async function getConversationMessageCounts(conversationIds: number[]) {
  348. const db = await getDb();
  349. if (!db || conversationIds.length === 0) return {};
  350. const result = await db.select({
  351. conversationId: messages.conversationId,
  352. count: sql<number>`COUNT(*)`,
  353. }).from(messages)
  354. .where(sql`${messages.conversationId} IN (${sql.join(conversationIds.map(id => sql`${id}`), sql`, `)})`)
  355. .groupBy(messages.conversationId);
  356. const counts: Record<number, number> = {};
  357. for (const row of result) {
  358. counts[row.conversationId] = Number(row.count);
  359. }
  360. return counts;
  361. }
  362. /** Bulk update conversation status */
  363. export async function bulkUpdateConversationStatus(
  364. ids: number[],
  365. status: "active" | "escalated" | "resolved" | "closed",
  366. agentId?: number
  367. ) {
  368. const db = await getDb();
  369. if (!db) throw new Error("Database not available");
  370. const updateData: Record<string, unknown> = { status };
  371. if (agentId !== undefined) updateData.assignedAgentId = agentId;
  372. for (const id of ids) {
  373. await db.update(conversations).set(updateData).where(eq(conversations.id, id));
  374. }
  375. return { updated: ids.length };
  376. }
  377. /** Delete conversations */
  378. export async function deleteConversations(ids: number[]) {
  379. const db = await getDb();
  380. if (!db) throw new Error("Database not available");
  381. for (const id of ids) {
  382. await db.delete(messages).where(eq(messages.conversationId, id));
  383. await db.delete(conversations).where(eq(conversations.id, id));
  384. }
  385. return { deleted: ids.length };
  386. }
  387. export async function getConversationById(id: number) {
  388. const db = await getDb();
  389. if (!db) return undefined;
  390. const result = await db.select().from(conversations).where(eq(conversations.id, id)).limit(1);
  391. return result[0];
  392. }
  393. export async function getConversationBySessionId(sessionId: string) {
  394. const db = await getDb();
  395. if (!db) return undefined;
  396. const result = await db.select().from(conversations).where(eq(conversations.sessionId, sessionId)).limit(1);
  397. return result[0];
  398. }
  399. export async function updateConversationStatus(id: number, status: "active" | "escalated" | "resolved" | "closed", agentId?: number) {
  400. const db = await getDb();
  401. if (!db) throw new Error("Database not available");
  402. const updateData: Record<string, unknown> = { status };
  403. if (agentId !== undefined) updateData.assignedAgentId = agentId;
  404. await db.update(conversations).set(updateData).where(eq(conversations.id, id));
  405. return getConversationById(id);
  406. }
  407. export async function getConversationStats() {
  408. const db = await getDb();
  409. if (!db) return { total: 0, active: 0, escalated: 0, resolved: 0, closed: 0 };
  410. const result = await db.select({
  411. status: conversations.status,
  412. count: sql<number>`COUNT(*)`,
  413. }).from(conversations).groupBy(conversations.status);
  414. const stats = { total: 0, active: 0, escalated: 0, resolved: 0, closed: 0 };
  415. for (const row of result) {
  416. const count = Number(row.count);
  417. stats[row.status as keyof typeof stats] = count;
  418. stats.total += count;
  419. }
  420. return stats;
  421. }
  422. /* ─── Message helpers ─── */
  423. export async function addMessage(data: InsertMessage) {
  424. const db = await getDb();
  425. if (!db) throw new Error("Database not available");
  426. const result = await db.insert(messages).values(data);
  427. await db.update(conversations).set({ updatedAt: new Date() }).where(eq(conversations.id, data.conversationId));
  428. return { id: Number(result[0].insertId), ...data };
  429. }
  430. export async function getMessagesByConversation(conversationId: number) {
  431. const db = await getDb();
  432. if (!db) return [];
  433. return db.select().from(messages)
  434. .where(eq(messages.conversationId, conversationId))
  435. .orderBy(messages.createdAt);
  436. }
  437. /* ─── Workflow helpers ─── */
  438. export async function saveWorkflow(workflowId: string, nodes: InsertWorkflowNode[], edges: InsertWorkflowEdge[]) {
  439. const db = await getDb();
  440. if (!db) throw new Error("Database not available");
  441. await db.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId));
  442. await db.delete(workflowNodes).where(eq(workflowNodes.workflowId, workflowId));
  443. if (nodes.length > 0) await db.insert(workflowNodes).values(nodes);
  444. if (edges.length > 0) await db.insert(workflowEdges).values(edges);
  445. return { workflowId, nodeCount: nodes.length, edgeCount: edges.length };
  446. }
  447. export async function getWorkflow(workflowId: string) {
  448. const db = await getDb();
  449. if (!db) return { nodes: [], edges: [] };
  450. const nodes = await db.select().from(workflowNodes).where(eq(workflowNodes.workflowId, workflowId));
  451. const edges = await db.select().from(workflowEdges).where(eq(workflowEdges.workflowId, workflowId));
  452. return { nodes, edges };
  453. }
  454. /* ─── Workflow suggestion helpers ─── */
  455. export async function createWorkflowSuggestion(data: InsertWorkflowSuggestion) {
  456. const db = await getDb();
  457. if (!db) throw new Error("Database not available");
  458. await db.insert(workflowSuggestions).values(data);
  459. return data;
  460. }
  461. export async function getWorkflowSuggestions(workflowId: string, status?: string) {
  462. const db = await getDb();
  463. if (!db) return [];
  464. const conditions = [eq(workflowSuggestions.workflowId, workflowId)];
  465. if (status) conditions.push(eq(workflowSuggestions.status, status as any));
  466. return db.select().from(workflowSuggestions).where(and(...conditions)).orderBy(desc(workflowSuggestions.frequency));
  467. }
  468. export async function updateWorkflowSuggestionStatus(id: number, status: "approved" | "declined" | "waiting", reviewedById: number) {
  469. const db = await getDb();
  470. if (!db) throw new Error("Database not available");
  471. await db.update(workflowSuggestions)
  472. .set({ status, reviewedById, reviewedAt: new Date() })
  473. .where(eq(workflowSuggestions.id, id));
  474. return { id, status };
  475. }
  476. export async function bulkCreateWorkflowSuggestions(suggestions: InsertWorkflowSuggestion[]) {
  477. const db = await getDb();
  478. if (!db) throw new Error("Database not available");
  479. if (suggestions.length === 0) return { created: 0 };
  480. await db.insert(workflowSuggestions).values(suggestions);
  481. return { created: suggestions.length };
  482. }
  483. /* ─── Analytics helpers ─── */
  484. export async function trackAnalyticsEvent(event: Omit<InsertAnalyticsEvent, "id" | "createdAt">) {
  485. const db = await getDb();
  486. if (!db) return null;
  487. const [result] = await db.insert(analyticsEvents).values(event as any);
  488. return result.insertId;
  489. }
  490. export async function getAnalyticsEvents(filters?: {
  491. eventType?: string;
  492. category?: string;
  493. startDate?: Date;
  494. endDate?: Date;
  495. }) {
  496. const db = await getDb();
  497. if (!db) return [];
  498. const conditions: any[] = [];
  499. if (filters?.eventType) conditions.push(eq(analyticsEvents.eventType, filters.eventType as any));
  500. if (filters?.category) conditions.push(eq(analyticsEvents.category, filters.category));
  501. if (filters?.startDate) conditions.push(gte(analyticsEvents.createdAt, filters.startDate));
  502. if (filters?.endDate) conditions.push(lte(analyticsEvents.createdAt, filters.endDate));
  503. const query = db.select().from(analyticsEvents);
  504. if (conditions.length > 0) {
  505. return query.where(and(...conditions)).orderBy(desc(analyticsEvents.createdAt)).limit(1000);
  506. }
  507. return query.orderBy(desc(analyticsEvents.createdAt)).limit(1000);
  508. }
  509. export async function getAnalyticsSummary(startDate?: Date, endDate?: Date) {
  510. const db = await getDb();
  511. if (!db) return null;
  512. const conditions: any[] = [];
  513. if (startDate) conditions.push(gte(analyticsEvents.createdAt, startDate));
  514. if (endDate) conditions.push(lte(analyticsEvents.createdAt, endDate));
  515. const baseQuery = conditions.length > 0
  516. ? db.select().from(analyticsEvents).where(and(...conditions))
  517. : db.select().from(analyticsEvents);
  518. const allEvents = await baseQuery;
  519. const totalSessions = allEvents.filter(e => e.eventType === "session_start").length;
  520. const resolvedByBot = allEvents.filter(e => e.eventType === "resolved_by_bot").length;
  521. const resolvedByAgent = allEvents.filter(e => e.eventType === "resolved_by_agent").length;
  522. const escalated = allEvents.filter(e => e.eventType === "escalated").length;
  523. const abandoned = allEvents.filter(e => e.eventType === "abandoned").length;
  524. const messagesSent = allEvents.filter(e => e.eventType === "message_sent").length;
  525. const messagesReceived = allEvents.filter(e => e.eventType === "message_received").length;
  526. const buttonClicks = allEvents.filter(e => e.eventType === "button_clicked").length;
  527. const positiveFeedback = allEvents.filter(e => e.eventType === "feedback_positive").length;
  528. const negativeFeedback = allEvents.filter(e => e.eventType === "feedback_negative").length;
  529. // Category breakdown
  530. const categories = ["orders", "shipping", "returning", "cancelling"];
  531. const categoryBreakdown = categories.map(cat => ({
  532. category: cat,
  533. count: allEvents.filter(e => e.category === cat).length,
  534. resolved: allEvents.filter(e => e.category === cat && (e.eventType === "resolved_by_bot" || e.eventType === "resolved_by_agent")).length,
  535. }));
  536. return {
  537. totalSessions,
  538. resolvedByBot,
  539. resolvedByAgent,
  540. escalated,
  541. abandoned,
  542. messagesSent,
  543. messagesReceived,
  544. buttonClicks,
  545. positiveFeedback,
  546. negativeFeedback,
  547. resolutionRate: totalSessions > 0 ? Math.round(((resolvedByBot + resolvedByAgent) / totalSessions) * 100) : 0,
  548. botResolutionRate: totalSessions > 0 ? Math.round((resolvedByBot / totalSessions) * 100) : 0,
  549. categoryBreakdown,
  550. totalEvents: allEvents.length,
  551. };
  552. }
  553. /* ─── Data Sources helpers ─── */
  554. export async function createDataSource(source: Omit<InsertDataSource, "id" | "createdAt" | "updatedAt">) {
  555. const db = await getDb();
  556. if (!db) return null;
  557. const [result] = await db.insert(dataSources).values(source as any);
  558. return result.insertId;
  559. }
  560. export async function getDataSources() {
  561. const db = await getDb();
  562. if (!db) return [];
  563. return db.select().from(dataSources).orderBy(desc(dataSources.createdAt));
  564. }
  565. export async function getDataSourceById(id: number) {
  566. const db = await getDb();
  567. if (!db) return null;
  568. const [source] = await db.select().from(dataSources).where(eq(dataSources.id, id));
  569. return source || null;
  570. }
  571. export async function updateDataSource(id: number, updates: Partial<InsertDataSource>) {
  572. const db = await getDb();
  573. if (!db) return null;
  574. await db.update(dataSources).set(updates as any).where(eq(dataSources.id, id));
  575. return getDataSourceById(id);
  576. }
  577. export async function deleteDataSource(id: number) {
  578. const db = await getDb();
  579. if (!db) return;
  580. await db.delete(dataSources).where(eq(dataSources.id, id));
  581. }
  582. /* ─── API Connections helpers ─── */
  583. export async function createApiConnection(conn: Omit<InsertApiConnection, "id" | "createdAt" | "updatedAt">) {
  584. const db = await getDb();
  585. if (!db) return null;
  586. const [result] = await db.insert(apiConnections).values(conn as any);
  587. return result.insertId;
  588. }
  589. export async function getApiConnections() {
  590. const db = await getDb();
  591. if (!db) return [];
  592. return db.select().from(apiConnections).orderBy(desc(apiConnections.createdAt));
  593. }
  594. export async function getApiConnectionById(id: number) {
  595. const db = await getDb();
  596. if (!db) return null;
  597. const [conn] = await db.select().from(apiConnections).where(eq(apiConnections.id, id));
  598. return conn || null;
  599. }
  600. export async function updateApiConnection(id: number, updates: Partial<InsertApiConnection>) {
  601. const db = await getDb();
  602. if (!db) return null;
  603. await db.update(apiConnections).set(updates as any).where(eq(apiConnections.id, id));
  604. return getApiConnectionById(id);
  605. }
  606. export async function deleteApiConnection(id: number) {
  607. const db = await getDb();
  608. if (!db) return;
  609. await db.delete(apiConnections).where(eq(apiConnections.id, id));
  610. }
  611. export async function incrementApiConnectionExecution(id: number) {
  612. const db = await getDb();
  613. if (!db) return;
  614. const conn = await getApiConnectionById(id);
  615. if (conn) {
  616. await db.update(apiConnections).set({
  617. executionCount: conn.executionCount + 1,
  618. lastExecutedAt: new Date(),
  619. } as any).where(eq(apiConnections.id, id));
  620. }
  621. }