db.ts 25 KB

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