elevenlabs-webhook-nodejs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (969) hide show
  1. package/.claude/settings.local.json +27 -0
  2. package/.dockerignore +6 -0
  3. package/.env.example +43 -0
  4. package/BEFORE-PRODUCTION.md +32 -0
  5. package/Dockerfile +23 -0
  6. package/SYSTEM_OVERVIEW.md +942 -0
  7. package/SYSTEM_STATUS.md +332 -0
  8. package/backup_script/main.py +146 -0
  9. package/baileys/.dockerignore +7 -0
  10. package/baileys/Dockerfile +13 -0
  11. package/baileys/README.md +412 -0
  12. package/baileys/index.js +499 -0
  13. package/baileys/package-lock.json +2532 -0
  14. package/baileys/package.json +25 -0
  15. package/baileys/server.js +96 -0
  16. package/baileys/src/config.js +55 -0
  17. package/baileys/src/middleware/api-key.js +16 -0
  18. package/baileys/src/routes/accounts.js +51 -0
  19. package/baileys/src/routes/chats.js +103 -0
  20. package/baileys/src/routes/webhooks.js +34 -0
  21. package/baileys/src/services/account-store.js +259 -0
  22. package/baileys/src/services/webhook-dispatcher.js +161 -0
  23. package/baileys/src/services/worker-manager.js +597 -0
  24. package/baileys/src/utils/jid.js +79 -0
  25. package/baileys/src/utils/logger.js +16 -0
  26. package/baileys/src/utils/use-mongodb-auth-state.js +122 -0
  27. package/baileys/worker.js +721 -0
  28. package/dist/app.d.ts +1 -0
  29. package/dist/app.js +84 -0
  30. package/dist/app.js.map +1 -0
  31. package/dist/config/agentPrompts.json +19 -0
  32. package/dist/config/database.d.ts +1 -0
  33. package/dist/config/database.js +26 -0
  34. package/dist/config/database.js.map +1 -0
  35. package/dist/config/env.d.ts +41 -0
  36. package/dist/config/env.js +81 -0
  37. package/dist/config/env.js.map +1 -0
  38. package/dist/config/index.d.ts +3 -0
  39. package/dist/config/index.js +73 -0
  40. package/dist/config/index.js.map +1 -0
  41. package/dist/controllers/webhook.controller.d.ts +10 -0
  42. package/dist/controllers/webhook.controller.js +128 -0
  43. package/dist/controllers/webhook.controller.js.map +1 -0
  44. package/dist/errors/index.d.ts +4 -0
  45. package/dist/errors/index.js +13 -0
  46. package/dist/errors/index.js.map +1 -0
  47. package/dist/hooks/webhookValidator.d.ts +2 -0
  48. package/dist/hooks/webhookValidator.js +47 -0
  49. package/dist/hooks/webhookValidator.js.map +1 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +55 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/migrations/001_session-architecture.d.ts +3 -0
  54. package/dist/migrations/001_session-architecture.js +119 -0
  55. package/dist/migrations/001_session-architecture.js.map +1 -0
  56. package/dist/migrations/002_agent-ref.d.ts +3 -0
  57. package/dist/migrations/002_agent-ref.js +55 -0
  58. package/dist/migrations/002_agent-ref.js.map +1 -0
  59. package/dist/migrations/003_shift-schedule.d.ts +3 -0
  60. package/dist/migrations/003_shift-schedule.js +48 -0
  61. package/dist/migrations/003_shift-schedule.js.map +1 -0
  62. package/dist/migrations/004_invert-shift-to-clinic-hours.d.ts +3 -0
  63. package/dist/migrations/004_invert-shift-to-clinic-hours.js +27 -0
  64. package/dist/migrations/004_invert-shift-to-clinic-hours.js.map +1 -0
  65. package/dist/migrations/005_composite-baileys-chat-ids.d.ts +3 -0
  66. package/dist/migrations/005_composite-baileys-chat-ids.js +47 -0
  67. package/dist/migrations/005_composite-baileys-chat-ids.js.map +1 -0
  68. package/dist/migrations/migration.model.d.ts +18 -0
  69. package/dist/migrations/migration.model.js +45 -0
  70. package/dist/migrations/migration.model.js.map +1 -0
  71. package/dist/migrations/migration.routes.d.ts +2 -0
  72. package/dist/migrations/migration.routes.js +37 -0
  73. package/dist/migrations/migration.routes.js.map +1 -0
  74. package/dist/migrations/migration.runner.d.ts +19 -0
  75. package/dist/migrations/migration.runner.js +74 -0
  76. package/dist/migrations/migration.runner.js.map +1 -0
  77. package/dist/models/ConversationClaim.d.ts +13 -0
  78. package/dist/models/ConversationClaim.js +44 -0
  79. package/dist/models/ConversationClaim.js.map +1 -0
  80. package/dist/models/ConversationEvaluation.d.ts +23 -0
  81. package/dist/models/ConversationEvaluation.js +92 -0
  82. package/dist/models/ConversationEvaluation.js.map +1 -0
  83. package/dist/models/Summary.d.ts +37 -0
  84. package/dist/models/Summary.js +78 -0
  85. package/dist/models/Summary.js.map +1 -0
  86. package/dist/models/Transcription.d.ts +34 -0
  87. package/dist/models/Transcription.js +71 -0
  88. package/dist/models/Transcription.js.map +1 -0
  89. package/dist/models/WhatsAppRecipient.d.ts +23 -0
  90. package/dist/models/WhatsAppRecipient.js +53 -0
  91. package/dist/models/WhatsAppRecipient.js.map +1 -0
  92. package/dist/modules/admin/admin.routes.d.ts +2 -0
  93. package/dist/modules/admin/admin.routes.js +1966 -0
  94. package/dist/modules/admin/admin.routes.js.map +1 -0
  95. package/dist/modules/admin/elevenlabs-test-chat.routes.d.ts +2 -0
  96. package/dist/modules/admin/elevenlabs-test-chat.routes.js +158 -0
  97. package/dist/modules/admin/elevenlabs-test-chat.routes.js.map +1 -0
  98. package/dist/modules/admin/whatsapp-test-chat.routes.d.ts +2 -0
  99. package/dist/modules/admin/whatsapp-test-chat.routes.js +204 -0
  100. package/dist/modules/admin/whatsapp-test-chat.routes.js.map +1 -0
  101. package/dist/modules/agents/agent.model.d.ts +38 -0
  102. package/dist/modules/agents/agent.model.js +92 -0
  103. package/dist/modules/agents/agent.model.js.map +1 -0
  104. package/dist/modules/agents/agent.routes.d.ts +2 -0
  105. package/dist/modules/agents/agent.routes.js +61 -0
  106. package/dist/modules/agents/agent.routes.js.map +1 -0
  107. package/dist/modules/appointment-validation/appointment-validation.model.d.ts +76 -0
  108. package/dist/modules/appointment-validation/appointment-validation.model.js +118 -0
  109. package/dist/modules/appointment-validation/appointment-validation.model.js.map +1 -0
  110. package/dist/modules/appointment-validation/appointment-validation.routes.d.ts +2 -0
  111. package/dist/modules/appointment-validation/appointment-validation.routes.js +202 -0
  112. package/dist/modules/appointment-validation/appointment-validation.routes.js.map +1 -0
  113. package/dist/modules/appointment-validation/appointment-validation.service.d.ts +53 -0
  114. package/dist/modules/appointment-validation/appointment-validation.service.js +827 -0
  115. package/dist/modules/appointment-validation/appointment-validation.service.js.map +1 -0
  116. package/dist/modules/auth/auth.model.d.ts +17 -0
  117. package/dist/modules/auth/auth.model.js +64 -0
  118. package/dist/modules/auth/auth.model.js.map +1 -0
  119. package/dist/modules/auth/auth.routes.d.ts +2 -0
  120. package/dist/modules/auth/auth.routes.js +202 -0
  121. package/dist/modules/auth/auth.routes.js.map +1 -0
  122. package/dist/modules/auth/auth.service.d.ts +28 -0
  123. package/dist/modules/auth/auth.service.js +183 -0
  124. package/dist/modules/auth/auth.service.js.map +1 -0
  125. package/dist/modules/auth/refresh-token.model.d.ts +13 -0
  126. package/dist/modules/auth/refresh-token.model.js +52 -0
  127. package/dist/modules/auth/refresh-token.model.js.map +1 -0
  128. package/dist/modules/billing/billing-alert.model.d.ts +16 -0
  129. package/dist/modules/billing/billing-alert.model.js +47 -0
  130. package/dist/modules/billing/billing-alert.model.js.map +1 -0
  131. package/dist/modules/billing/billing-period-snapshot.model.d.ts +35 -0
  132. package/dist/modules/billing/billing-period-snapshot.model.js +68 -0
  133. package/dist/modules/billing/billing-period-snapshot.model.js.map +1 -0
  134. package/dist/modules/billing/billing.model.d.ts +18 -0
  135. package/dist/modules/billing/billing.model.js +62 -0
  136. package/dist/modules/billing/billing.model.js.map +1 -0
  137. package/dist/modules/billing/billing.routes.d.ts +2 -0
  138. package/dist/modules/billing/billing.routes.js +63 -0
  139. package/dist/modules/billing/billing.routes.js.map +1 -0
  140. package/dist/modules/billing/billing.service.d.ts +69 -0
  141. package/dist/modules/billing/billing.service.js +498 -0
  142. package/dist/modules/billing/billing.service.js.map +1 -0
  143. package/dist/modules/billing/payment.model.d.ts +24 -0
  144. package/dist/modules/billing/payment.model.js +57 -0
  145. package/dist/modules/billing/payment.model.js.map +1 -0
  146. package/dist/modules/calls/call.model.d.ts +41 -0
  147. package/dist/modules/calls/call.model.js +97 -0
  148. package/dist/modules/calls/call.model.js.map +1 -0
  149. package/dist/modules/calls/call.routes.d.ts +2 -0
  150. package/dist/modules/calls/call.routes.js +103 -0
  151. package/dist/modules/calls/call.routes.js.map +1 -0
  152. package/dist/modules/campaigns/campaign.model.d.ts +45 -0
  153. package/dist/modules/campaigns/campaign.model.js +98 -0
  154. package/dist/modules/campaigns/campaign.model.js.map +1 -0
  155. package/dist/modules/campaigns/campaign.routes.d.ts +2 -0
  156. package/dist/modules/campaigns/campaign.routes.js +323 -0
  157. package/dist/modules/campaigns/campaign.routes.js.map +1 -0
  158. package/dist/modules/campaigns/campaign.service.d.ts +11 -0
  159. package/dist/modules/campaigns/campaign.service.js +86 -0
  160. package/dist/modules/campaigns/campaign.service.js.map +1 -0
  161. package/dist/modules/google-calendar/google-calendar.routes.d.ts +2 -0
  162. package/dist/modules/google-calendar/google-calendar.routes.js +32 -0
  163. package/dist/modules/google-calendar/google-calendar.routes.js.map +1 -0
  164. package/dist/modules/inbound-call/inbound-call-config.model.d.ts +20 -0
  165. package/dist/modules/inbound-call/inbound-call-config.model.js +68 -0
  166. package/dist/modules/inbound-call/inbound-call-config.model.js.map +1 -0
  167. package/dist/modules/inbound-call/inbound-call.routes.d.ts +2 -0
  168. package/dist/modules/inbound-call/inbound-call.routes.js +243 -0
  169. package/dist/modules/inbound-call/inbound-call.routes.js.map +1 -0
  170. package/dist/modules/leads/lead.model.d.ts +24 -0
  171. package/dist/modules/leads/lead.model.js +54 -0
  172. package/dist/modules/leads/lead.model.js.map +1 -0
  173. package/dist/modules/leads/lead.routes.d.ts +2 -0
  174. package/dist/modules/leads/lead.routes.js +201 -0
  175. package/dist/modules/leads/lead.routes.js.map +1 -0
  176. package/dist/modules/plans/plan.model.d.ts +25 -0
  177. package/dist/modules/plans/plan.model.js +59 -0
  178. package/dist/modules/plans/plan.model.js.map +1 -0
  179. package/dist/modules/surveys/survey.model.d.ts +30 -0
  180. package/dist/modules/surveys/survey.model.js +75 -0
  181. package/dist/modules/surveys/survey.model.js.map +1 -0
  182. package/dist/modules/surveys/survey.routes.d.ts +2 -0
  183. package/dist/modules/surveys/survey.routes.js +153 -0
  184. package/dist/modules/surveys/survey.routes.js.map +1 -0
  185. package/dist/modules/tenants/tenant.model.d.ts +86 -0
  186. package/dist/modules/tenants/tenant.model.js +127 -0
  187. package/dist/modules/tenants/tenant.model.js.map +1 -0
  188. package/dist/modules/tenants/tenant.routes.d.ts +2 -0
  189. package/dist/modules/tenants/tenant.routes.js +65 -0
  190. package/dist/modules/tenants/tenant.routes.js.map +1 -0
  191. package/dist/modules/users/user.routes.d.ts +2 -0
  192. package/dist/modules/users/user.routes.js +106 -0
  193. package/dist/modules/users/user.routes.js.map +1 -0
  194. package/dist/modules/webhooks/elevenlabs-tool.routes.d.ts +20 -0
  195. package/dist/modules/webhooks/elevenlabs-tool.routes.js +85 -0
  196. package/dist/modules/webhooks/elevenlabs-tool.routes.js.map +1 -0
  197. package/dist/modules/webhooks/elevenlabs-tool.service.d.ts +11 -0
  198. package/dist/modules/webhooks/elevenlabs-tool.service.js +360 -0
  199. package/dist/modules/webhooks/elevenlabs-tool.service.js.map +1 -0
  200. package/dist/modules/webhooks/elevenlabs.routes.d.ts +2 -0
  201. package/dist/modules/webhooks/elevenlabs.routes.js +34 -0
  202. package/dist/modules/webhooks/elevenlabs.routes.js.map +1 -0
  203. package/dist/modules/webhooks/elevenlabs.service.d.ts +6 -0
  204. package/dist/modules/webhooks/elevenlabs.service.js +512 -0
  205. package/dist/modules/webhooks/elevenlabs.service.js.map +1 -0
  206. package/dist/modules/webhooks/unipile.routes.d.ts +2 -0
  207. package/dist/modules/webhooks/unipile.routes.js +780 -0
  208. package/dist/modules/webhooks/unipile.routes.js.map +1 -0
  209. package/dist/modules/whatsapp/appointment.model.d.ts +27 -0
  210. package/dist/modules/whatsapp/appointment.model.js +58 -0
  211. package/dist/modules/whatsapp/appointment.model.js.map +1 -0
  212. package/dist/modules/whatsapp/operator-request.model.d.ts +29 -0
  213. package/dist/modules/whatsapp/operator-request.model.js +65 -0
  214. package/dist/modules/whatsapp/operator-request.model.js.map +1 -0
  215. package/dist/modules/whatsapp/stt-usage.model.d.ts +16 -0
  216. package/dist/modules/whatsapp/stt-usage.model.js +53 -0
  217. package/dist/modules/whatsapp/stt-usage.model.js.map +1 -0
  218. package/dist/modules/whatsapp/whatsapp-chat.model.d.ts +23 -0
  219. package/dist/modules/whatsapp/whatsapp-chat.model.js +55 -0
  220. package/dist/modules/whatsapp/whatsapp-chat.model.js.map +1 -0
  221. package/dist/modules/whatsapp/whatsapp-contact-profile.model.d.ts +23 -0
  222. package/dist/modules/whatsapp/whatsapp-contact-profile.model.js +54 -0
  223. package/dist/modules/whatsapp/whatsapp-contact-profile.model.js.map +1 -0
  224. package/dist/modules/whatsapp/whatsapp-message.model.d.ts +30 -0
  225. package/dist/modules/whatsapp/whatsapp-message.model.js +52 -0
  226. package/dist/modules/whatsapp/whatsapp-message.model.js.map +1 -0
  227. package/dist/modules/whatsapp/whatsapp-session.model.d.ts +33 -0
  228. package/dist/modules/whatsapp/whatsapp-session.model.js +65 -0
  229. package/dist/modules/whatsapp/whatsapp-session.model.js.map +1 -0
  230. package/dist/modules/whatsapp/whatsapp.routes.d.ts +2 -0
  231. package/dist/modules/whatsapp/whatsapp.routes.js +1237 -0
  232. package/dist/modules/whatsapp/whatsapp.routes.js.map +1 -0
  233. package/dist/plugins/cors.d.ts +3 -0
  234. package/dist/plugins/cors.js +11 -0
  235. package/dist/plugins/cors.js.map +1 -0
  236. package/dist/plugins/jwt.d.ts +3 -0
  237. package/dist/plugins/jwt.js +22 -0
  238. package/dist/plugins/jwt.js.map +1 -0
  239. package/dist/plugins/rawBody.d.ts +3 -0
  240. package/dist/plugins/rawBody.js +16 -0
  241. package/dist/plugins/rawBody.js.map +1 -0
  242. package/dist/plugins/rbac.d.ts +5 -0
  243. package/dist/plugins/rbac.js +29 -0
  244. package/dist/plugins/rbac.js.map +1 -0
  245. package/dist/routes/admin.routes.d.ts +2 -0
  246. package/dist/routes/admin.routes.js +169 -0
  247. package/dist/routes/admin.routes.js.map +1 -0
  248. package/dist/routes/health.routes.d.ts +2 -0
  249. package/dist/routes/health.routes.js +16 -0
  250. package/dist/routes/health.routes.js.map +1 -0
  251. package/dist/routes/webhook.routes.d.ts +2 -0
  252. package/dist/routes/webhook.routes.js +17 -0
  253. package/dist/routes/webhook.routes.js.map +1 -0
  254. package/dist/services/ai/base.ai.d.ts +19 -0
  255. package/dist/services/ai/base.ai.js +120 -0
  256. package/dist/services/ai/base.ai.js.map +1 -0
  257. package/dist/services/ai/gemini.service.d.ts +11 -0
  258. package/dist/services/ai/gemini.service.js +43 -0
  259. package/dist/services/ai/gemini.service.js.map +1 -0
  260. package/dist/services/ai/index.d.ts +2 -0
  261. package/dist/services/ai/index.js +26 -0
  262. package/dist/services/ai/index.js.map +1 -0
  263. package/dist/services/ai/openai.service.d.ts +11 -0
  264. package/dist/services/ai/openai.service.js +50 -0
  265. package/dist/services/ai/openai.service.js.map +1 -0
  266. package/dist/services/elevenlabs.service.d.ts +52 -0
  267. package/dist/services/elevenlabs.service.js +447 -0
  268. package/dist/services/elevenlabs.service.js.map +1 -0
  269. package/dist/services/google-calendar.service.d.ts +60 -0
  270. package/dist/services/google-calendar.service.js +494 -0
  271. package/dist/services/google-calendar.service.js.map +1 -0
  272. package/dist/services/inbound-call-schedule.service.d.ts +11 -0
  273. package/dist/services/inbound-call-schedule.service.js +162 -0
  274. package/dist/services/inbound-call-schedule.service.js.map +1 -0
  275. package/dist/services/netgsm.service.d.ts +41 -0
  276. package/dist/services/netgsm.service.js +89 -0
  277. package/dist/services/netgsm.service.js.map +1 -0
  278. package/dist/services/unipile.service.d.ts +41 -0
  279. package/dist/services/unipile.service.js +149 -0
  280. package/dist/services/unipile.service.js.map +1 -0
  281. package/dist/services/whatsapp-agent.service.d.ts +139 -0
  282. package/dist/services/whatsapp-agent.service.js +2055 -0
  283. package/dist/services/whatsapp-agent.service.js.map +1 -0
  284. package/dist/services/whatsapp.service.d.ts +26 -0
  285. package/dist/services/whatsapp.service.js +206 -0
  286. package/dist/services/whatsapp.service.js.map +1 -0
  287. package/dist/templates/index.d.ts +39 -0
  288. package/dist/templates/index.js +35 -0
  289. package/dist/templates/index.js.map +1 -0
  290. package/dist/templates/receptionist.d.ts +2 -0
  291. package/dist/templates/receptionist.js +39 -0
  292. package/dist/templates/receptionist.js.map +1 -0
  293. package/dist/templates/survey.d.ts +2 -0
  294. package/dist/templates/survey.js +41 -0
  295. package/dist/templates/survey.js.map +1 -0
  296. package/dist/types/index.d.ts +173 -0
  297. package/dist/types/index.js +3 -0
  298. package/dist/types/index.js.map +1 -0
  299. package/dist/utils/logger.d.ts +3 -0
  300. package/dist/utils/logger.js +105 -0
  301. package/dist/utils/logger.js.map +1 -0
  302. package/docker-compose.nestjs.yml +89 -0
  303. package/docker-compose.yml +78 -0
  304. package/docs/AI_AGENT_ENHANCEMENT_PLAN.md +164 -0
  305. package/docs/API.md +1193 -0
  306. package/docs/API_ENDPOINTS.md +344 -0
  307. package/docs/ARCHITECTURE.md +305 -0
  308. package/docs/AUTH_API.md +252 -0
  309. package/docs/BILLING_SMS_ALERTS.md +94 -0
  310. package/docs/CHAT_ASSIGNMENT_SYSTEM.md +118 -0
  311. package/docs/CLIENT_TOOLS_AND_FEATURES.md +337 -0
  312. package/docs/ELEVENLABS_WEBHOOK_TOOLS.md +644 -0
  313. package/docs/FRONTEND_CHECKLIST.md +227 -0
  314. package/docs/IMPLEMENTATION_STATUS.md +470 -0
  315. package/docs/MIGRATION_GUIDE.md +96 -0
  316. package/docs/MISSINGS_REPORT.md +507 -0
  317. package/docs/NESTJS_MIGRATION_REFERENCE.md +5136 -0
  318. package/docs/PROJECT_DESCRIPTION.md +1038 -0
  319. package/docs/SCALING.md +148 -0
  320. package/docs/SESSION_SUMMARY_2026_03_17.md +135 -0
  321. package/docs/WHATSAPP_AGENT.md +1086 -0
  322. package/docs/architecture/00-SYSTEM-OVERVIEW.md +318 -0
  323. package/docs/architecture/01-DATABASE-SCHEMA.md +2564 -0
  324. package/docs/architecture/02-AUTHENTICATION.md +1040 -0
  325. package/docs/architecture/03-MULTI-CLINIC.md +742 -0
  326. package/docs/architecture/04-WHATSAPP-AGENT.md +608 -0
  327. package/docs/architecture/05-OPERATOR-WORKFLOW.md +444 -0
  328. package/docs/architecture/06-BAILEYS-MICROSERVICE.md +616 -0
  329. package/docs/architecture/07-APPOINTMENTS.md +849 -0
  330. package/docs/architecture/08-VOICE-CALLS.md +470 -0
  331. package/docs/architecture/09-OUTBOUND-CAMPAIGNS.md +542 -0
  332. package/docs/architecture/10-CLIENT-TOOLS.md +665 -0
  333. package/docs/architecture/11-BILLING.md +458 -0
  334. package/docs/architecture/12-SECURITY.md +216 -0
  335. package/docs/architecture/13-LOGGING-AUDIT.md +549 -0
  336. package/docs/architecture/14-META-BUSINESS-API.md +454 -0
  337. package/docs/architecture/15-AI-MODULE.md +479 -0
  338. package/docs/architecture/16-BACKGROUND-JOBS.md +469 -0
  339. package/docs/architecture/17-REALTIME-LIVECHAT.md +447 -0
  340. package/docs/architecture/18-FILE-STORAGE.md +410 -0
  341. package/docs/architecture/19-PATIENTS.md +1034 -0
  342. package/docs/architecture/20-TREATMENTS-AND-PLANS.md +774 -0
  343. package/docs/architecture/21-BEFORE-AFTER-PHOTOS.md +519 -0
  344. package/docs/database.md +456 -0
  345. package/docs/ornek-randevu-onay.csv +3 -0
  346. package/ecosystem.config.js +16 -0
  347. package/elevenlabs-convai-api-reference.md +1171 -0
  348. package/frontend/.dockerignore +2 -0
  349. package/frontend/Dockerfile +24 -0
  350. package/frontend/README.md +75 -0
  351. package/frontend/components.json +25 -0
  352. package/frontend/eslint.config.js +23 -0
  353. package/frontend/index.html +13 -0
  354. package/frontend/nginx.conf +37 -0
  355. package/frontend/package-lock.json +8709 -0
  356. package/frontend/package.json +71 -0
  357. package/frontend/public/favicon.svg +1 -0
  358. package/frontend/public/icons.svg +24 -0
  359. package/frontend/src/App.tsx +125 -0
  360. package/frontend/src/components/error-boundary.tsx +50 -0
  361. package/frontend/src/components/shared/activity-timeline.tsx +66 -0
  362. package/frontend/src/components/shared/appointment-calendar.css +80 -0
  363. package/frontend/src/components/shared/appointment-calendar.tsx +245 -0
  364. package/frontend/src/components/shared/chat-bubble.tsx +72 -0
  365. package/frontend/src/components/shared/combobox.tsx +119 -0
  366. package/frontend/src/components/shared/confirm-dialog.tsx +57 -0
  367. package/frontend/src/components/shared/data-table-pagination.tsx +97 -0
  368. package/frontend/src/components/shared/data-table-toolbar.tsx +39 -0
  369. package/frontend/src/components/shared/empty-state.tsx +27 -0
  370. package/frontend/src/components/shared/file-upload.tsx +140 -0
  371. package/frontend/src/components/shared/index.ts +20 -0
  372. package/frontend/src/components/shared/language-switcher.tsx +29 -0
  373. package/frontend/src/components/shared/notification-dropdown.tsx +115 -0
  374. package/frontend/src/components/shared/page-header.tsx +23 -0
  375. package/frontend/src/components/shared/stat-card.tsx +37 -0
  376. package/frontend/src/components/shared/status-badge.tsx +70 -0
  377. package/frontend/src/components/shared/status-dot.tsx +43 -0
  378. package/frontend/src/components/ui/alert-dialog.tsx +187 -0
  379. package/frontend/src/components/ui/alert.tsx +76 -0
  380. package/frontend/src/components/ui/avatar.tsx +109 -0
  381. package/frontend/src/components/ui/badge.tsx +52 -0
  382. package/frontend/src/components/ui/breadcrumb.tsx +125 -0
  383. package/frontend/src/components/ui/button.tsx +60 -0
  384. package/frontend/src/components/ui/calendar.tsx +219 -0
  385. package/frontend/src/components/ui/card.tsx +103 -0
  386. package/frontend/src/components/ui/chart.tsx +371 -0
  387. package/frontend/src/components/ui/checkbox.tsx +29 -0
  388. package/frontend/src/components/ui/collapsible.tsx +19 -0
  389. package/frontend/src/components/ui/command.tsx +194 -0
  390. package/frontend/src/components/ui/dialog.tsx +158 -0
  391. package/frontend/src/components/ui/dropdown-menu.tsx +268 -0
  392. package/frontend/src/components/ui/input-group.tsx +156 -0
  393. package/frontend/src/components/ui/input.tsx +20 -0
  394. package/frontend/src/components/ui/label.tsx +20 -0
  395. package/frontend/src/components/ui/pagination.tsx +130 -0
  396. package/frontend/src/components/ui/popover.tsx +90 -0
  397. package/frontend/src/components/ui/progress.tsx +83 -0
  398. package/frontend/src/components/ui/radio-group.tsx +36 -0
  399. package/frontend/src/components/ui/scroll-area.tsx +54 -0
  400. package/frontend/src/components/ui/select.tsx +199 -0
  401. package/frontend/src/components/ui/separator.tsx +23 -0
  402. package/frontend/src/components/ui/sheet.tsx +138 -0
  403. package/frontend/src/components/ui/sidebar.tsx +723 -0
  404. package/frontend/src/components/ui/skeleton.tsx +13 -0
  405. package/frontend/src/components/ui/sonner.tsx +47 -0
  406. package/frontend/src/components/ui/switch.tsx +30 -0
  407. package/frontend/src/components/ui/table.tsx +114 -0
  408. package/frontend/src/components/ui/tabs.tsx +82 -0
  409. package/frontend/src/components/ui/textarea.tsx +18 -0
  410. package/frontend/src/components/ui/toggle-group.tsx +89 -0
  411. package/frontend/src/components/ui/toggle.tsx +43 -0
  412. package/frontend/src/components/ui/tooltip.tsx +64 -0
  413. package/frontend/src/hooks/use-mobile.ts +19 -0
  414. package/frontend/src/i18n/index.ts +20 -0
  415. package/frontend/src/i18n/locales/en.json +786 -0
  416. package/frontend/src/i18n/locales/tr.json +786 -0
  417. package/frontend/src/index.css +134 -0
  418. package/frontend/src/layouts/admin-layout.tsx +111 -0
  419. package/frontend/src/layouts/app-header.tsx +130 -0
  420. package/frontend/src/layouts/app-layout.tsx +19 -0
  421. package/frontend/src/layouts/app-sidebar.tsx +212 -0
  422. package/frontend/src/layouts/auth-guard.tsx +31 -0
  423. package/frontend/src/layouts/mobile-sidebar.tsx +120 -0
  424. package/frontend/src/lib/api.ts +68 -0
  425. package/frontend/src/lib/hooks/use-appointments.ts +224 -0
  426. package/frontend/src/lib/hooks/use-availability-blocks.ts +83 -0
  427. package/frontend/src/lib/hooks/use-chats.ts +236 -0
  428. package/frontend/src/lib/hooks/use-clinic-detail.ts +171 -0
  429. package/frontend/src/lib/hooks/use-clinics.ts +37 -0
  430. package/frontend/src/lib/hooks/use-doctor-calendar.ts +80 -0
  431. package/frontend/src/lib/hooks/use-examinations.ts +89 -0
  432. package/frontend/src/lib/hooks/use-medical-history.ts +52 -0
  433. package/frontend/src/lib/hooks/use-patients.ts +296 -0
  434. package/frontend/src/lib/hooks/use-photo-sets.ts +118 -0
  435. package/frontend/src/lib/hooks/use-treatment-plans.ts +152 -0
  436. package/frontend/src/lib/hooks/use-treatments.ts +125 -0
  437. package/frontend/src/lib/hooks/use-users.ts +172 -0
  438. package/frontend/src/lib/utils.ts +6 -0
  439. package/frontend/src/main.tsx +17 -0
  440. package/frontend/src/pages/admin/agents/detail.tsx +774 -0
  441. package/frontend/src/pages/admin/agents/import.tsx +280 -0
  442. package/frontend/src/pages/admin/agents/index.tsx +245 -0
  443. package/frontend/src/pages/admin/ai-playground.tsx +543 -0
  444. package/frontend/src/pages/appointments/create-appointment-dialog.tsx +390 -0
  445. package/frontend/src/pages/appointments/index.tsx +860 -0
  446. package/frontend/src/pages/audit/index.tsx +24 -0
  447. package/frontend/src/pages/auth/login.tsx +194 -0
  448. package/frontend/src/pages/billing/index.tsx +24 -0
  449. package/frontend/src/pages/calendar/index.tsx +704 -0
  450. package/frontend/src/pages/calendar-connections/index.tsx +295 -0
  451. package/frontend/src/pages/calls/index.tsx +24 -0
  452. package/frontend/src/pages/campaigns/index.tsx +24 -0
  453. package/frontend/src/pages/chats/index.tsx +981 -0
  454. package/frontend/src/pages/clinics/index.tsx +224 -0
  455. package/frontend/src/pages/clinics/settings.tsx +412 -0
  456. package/frontend/src/pages/components-showcase.tsx +773 -0
  457. package/frontend/src/pages/connections/index.tsx +328 -0
  458. package/frontend/src/pages/dashboard.tsx +50 -0
  459. package/frontend/src/pages/leads/index.tsx +24 -0
  460. package/frontend/src/pages/my-schedule/index.tsx +496 -0
  461. package/frontend/src/pages/patients/create-patient-dialog.tsx +358 -0
  462. package/frontend/src/pages/patients/detail.tsx +1195 -0
  463. package/frontend/src/pages/patients/edit-patient-dialog.tsx +387 -0
  464. package/frontend/src/pages/patients/examinations-tab.tsx +460 -0
  465. package/frontend/src/pages/patients/index.tsx +381 -0
  466. package/frontend/src/pages/patients/medical-history-dialog.tsx +207 -0
  467. package/frontend/src/pages/patients/photo-sets-tab.tsx +616 -0
  468. package/frontend/src/pages/patients/timeline-tab.tsx +164 -0
  469. package/frontend/src/pages/patients/treatment-plans-tab.tsx +598 -0
  470. package/frontend/src/pages/phone-numbers/detail.tsx +427 -0
  471. package/frontend/src/pages/phone-numbers/index.tsx +455 -0
  472. package/frontend/src/pages/platform/index.tsx +454 -0
  473. package/frontend/src/pages/settings/index.tsx +126 -0
  474. package/frontend/src/pages/treatments/index.tsx +487 -0
  475. package/frontend/src/pages/users/doctor-profile.tsx +672 -0
  476. package/frontend/src/pages/users/edit.tsx +329 -0
  477. package/frontend/src/pages/users/index.tsx +407 -0
  478. package/frontend/src/pages/validation/index.tsx +24 -0
  479. package/frontend/src/stores/auth.store.ts +108 -0
  480. package/frontend/src/stores/ui.store.ts +41 -0
  481. package/frontend/tsconfig.app.json +32 -0
  482. package/frontend/tsconfig.json +13 -0
  483. package/frontend/tsconfig.node.json +26 -0
  484. package/frontend/vite.config.ts +29 -0
  485. package/nestjs/.dockerignore +5 -0
  486. package/nestjs/.env.docker +64 -0
  487. package/nestjs/.prettierrc +4 -0
  488. package/nestjs/Dockerfile +36 -0
  489. package/nestjs/README.md +98 -0
  490. package/nestjs/eslint.config.mjs +35 -0
  491. package/nestjs/nest-cli.json +8 -0
  492. package/nestjs/package-lock.json +13390 -0
  493. package/nestjs/package.json +114 -0
  494. package/nestjs/prisma/migrations/20260409161536_add_message_metadata_fields/migration.sql +1746 -0
  495. package/nestjs/prisma/migrations/20260410140436_add_agent_ai_fields/migration.sql +36 -0
  496. package/nestjs/prisma/migrations/20260410175519_add_agent_clinic_assignments/migration.sql +21 -0
  497. package/nestjs/prisma/migrations/20260412094344_make_agent_tenant_optional/migration.sql +2 -0
  498. package/nestjs/prisma/migrations/20260412110008_add_admin_chat_sessions/migration.sql +47 -0
  499. package/nestjs/prisma/migrations/migration_lock.toml +3 -0
  500. package/nestjs/prisma/schema.prisma +1843 -0
  501. package/nestjs/prisma/seed.ts +375 -0
  502. package/nestjs/prisma.config.ts +14 -0
  503. package/nestjs/src/admin/admin.controller.ts +27 -0
  504. package/nestjs/src/admin/admin.module.ts +16 -0
  505. package/nestjs/src/admin/admin.service.ts +91 -0
  506. package/nestjs/src/admin/ai-chat-session.service.ts +454 -0
  507. package/nestjs/src/admin/ai-test.controller.ts +191 -0
  508. package/nestjs/src/admin/superadmin.controller.ts +106 -0
  509. package/nestjs/src/agents/agents.controller.ts +262 -0
  510. package/nestjs/src/agents/agents.module.ts +13 -0
  511. package/nestjs/src/agents/agents.service.ts +733 -0
  512. package/nestjs/src/agents/dto/create-agent.dto.ts +99 -0
  513. package/nestjs/src/agents/dto/index.ts +2 -0
  514. package/nestjs/src/agents/dto/update-agent.dto.ts +148 -0
  515. package/nestjs/src/app.module.ts +115 -0
  516. package/nestjs/src/appointment-validation/appointment-validation.controller.ts +194 -0
  517. package/nestjs/src/appointment-validation/appointment-validation.module.ts +16 -0
  518. package/nestjs/src/appointment-validation/appointment-validation.service.ts +450 -0
  519. package/nestjs/src/appointment-validation/dto/create-batch.dto.ts +105 -0
  520. package/nestjs/src/appointment-validation/dto/index.ts +1 -0
  521. package/nestjs/src/appointment-validation/processors/validation-dispatch.processor.ts +26 -0
  522. package/nestjs/src/appointment-validation/processors/validation-sync.processor.ts +23 -0
  523. package/nestjs/src/appointments/appointments.controller.ts +268 -0
  524. package/nestjs/src/appointments/appointments.module.ts +13 -0
  525. package/nestjs/src/appointments/appointments.service.ts +773 -0
  526. package/nestjs/src/appointments/dto/create-appointment.dto.ts +72 -0
  527. package/nestjs/src/appointments/dto/index.ts +4 -0
  528. package/nestjs/src/appointments/dto/query-appointments.dto.ts +60 -0
  529. package/nestjs/src/appointments/dto/update-appointment.dto.ts +43 -0
  530. package/nestjs/src/appointments/dto/update-status.dto.ts +18 -0
  531. package/nestjs/src/appointments/processors/reminder.processor.ts +243 -0
  532. package/nestjs/src/audit/audit.controller.ts +84 -0
  533. package/nestjs/src/audit/audit.decorator.ts +20 -0
  534. package/nestjs/src/audit/audit.interceptor.ts +67 -0
  535. package/nestjs/src/audit/audit.interfaces.ts +15 -0
  536. package/nestjs/src/audit/audit.module.ts +12 -0
  537. package/nestjs/src/audit/audit.service.ts +177 -0
  538. package/nestjs/src/auth/auth.controller.ts +116 -0
  539. package/nestjs/src/auth/auth.module.ts +25 -0
  540. package/nestjs/src/auth/auth.service.ts +612 -0
  541. package/nestjs/src/auth/dto/change-password.dto.ts +13 -0
  542. package/nestjs/src/auth/dto/forgot-password.dto.ts +8 -0
  543. package/nestjs/src/auth/dto/index.ts +6 -0
  544. package/nestjs/src/auth/dto/login.dto.ts +13 -0
  545. package/nestjs/src/auth/dto/refresh.dto.ts +8 -0
  546. package/nestjs/src/auth/dto/register.dto.ts +28 -0
  547. package/nestjs/src/auth/dto/reset-password.dto.ts +13 -0
  548. package/nestjs/src/auth/permissions.config.ts +85 -0
  549. package/nestjs/src/availability-blocks/availability-blocks.controller.ts +83 -0
  550. package/nestjs/src/availability-blocks/availability-blocks.module.ts +10 -0
  551. package/nestjs/src/availability-blocks/availability-blocks.service.ts +202 -0
  552. package/nestjs/src/billing/billing.controller.ts +104 -0
  553. package/nestjs/src/billing/billing.module.ts +12 -0
  554. package/nestjs/src/billing/billing.service.ts +398 -0
  555. package/nestjs/src/billing/dto/index.ts +2 -0
  556. package/nestjs/src/billing/dto/query-billing.dto.ts +29 -0
  557. package/nestjs/src/billing/dto/record-payment.dto.ts +60 -0
  558. package/nestjs/src/billing/processors/alerts.processor.ts +216 -0
  559. package/nestjs/src/billing/processors/snapshot.processor.ts +181 -0
  560. package/nestjs/src/calls/calls.controller.ts +92 -0
  561. package/nestjs/src/calls/calls.module.ts +10 -0
  562. package/nestjs/src/calls/calls.service.ts +359 -0
  563. package/nestjs/src/calls/dto/index.ts +1 -0
  564. package/nestjs/src/calls/dto/query-calls.dto.ts +46 -0
  565. package/nestjs/src/clinics/clinics.controller.ts +226 -0
  566. package/nestjs/src/clinics/clinics.module.ts +11 -0
  567. package/nestjs/src/clinics/clinics.service.ts +203 -0
  568. package/nestjs/src/clinics/dto/create-clinic.dto.ts +40 -0
  569. package/nestjs/src/clinics/dto/create-meta-connection.dto.ts +44 -0
  570. package/nestjs/src/clinics/dto/index.ts +4 -0
  571. package/nestjs/src/clinics/dto/update-clinic.dto.ts +133 -0
  572. package/nestjs/src/clinics/dto/update-meta-connection.dto.ts +22 -0
  573. package/nestjs/src/clinics/meta-connections.service.ts +210 -0
  574. package/nestjs/src/common/decorators/accessible-clinics.decorator.ts +9 -0
  575. package/nestjs/src/common/decorators/current-user.decorator.ts +10 -0
  576. package/nestjs/src/common/decorators/features.decorator.ts +7 -0
  577. package/nestjs/src/common/decorators/index.ts +4 -0
  578. package/nestjs/src/common/decorators/permissions.decorator.ts +13 -0
  579. package/nestjs/src/common/gateways/events.gateway.ts +157 -0
  580. package/nestjs/src/common/guards/clinic-access.guard.ts +40 -0
  581. package/nestjs/src/common/guards/features.guard.ts +38 -0
  582. package/nestjs/src/common/guards/index.ts +5 -0
  583. package/nestjs/src/common/guards/jwt-auth.guard.ts +54 -0
  584. package/nestjs/src/common/guards/permissions.guard.ts +50 -0
  585. package/nestjs/src/common/guards/superadmin.guard.ts +35 -0
  586. package/nestjs/src/common/interceptors/request-context.interceptor.ts +32 -0
  587. package/nestjs/src/common/interceptors/superadmin-tenant.interceptor.ts +42 -0
  588. package/nestjs/src/common/interfaces/jwt-payload.interface.ts +11 -0
  589. package/nestjs/src/config/config.module.ts +13 -0
  590. package/nestjs/src/database/database.module.ts +9 -0
  591. package/nestjs/src/database/prisma.service.ts +20 -0
  592. package/nestjs/src/database/tenant-context.ts +27 -0
  593. package/nestjs/src/google-calendar/google-calendar.controller.ts +259 -0
  594. package/nestjs/src/google-calendar/google-calendar.module.ts +10 -0
  595. package/nestjs/src/google-calendar/google-calendar.service.ts +811 -0
  596. package/nestjs/src/integrations/ai/ai.interface.ts +74 -0
  597. package/nestjs/src/integrations/ai/ai.module.ts +11 -0
  598. package/nestjs/src/integrations/ai/ai.service.ts +148 -0
  599. package/nestjs/src/integrations/ai/providers/anthropic.provider.ts +146 -0
  600. package/nestjs/src/integrations/ai/providers/openai.provider.ts +158 -0
  601. package/nestjs/src/integrations/elevenlabs/elevenlabs.module.ts +8 -0
  602. package/nestjs/src/integrations/elevenlabs/elevenlabs.service.ts +226 -0
  603. package/nestjs/src/integrations/encryption/encryption.module.ts +9 -0
  604. package/nestjs/src/integrations/encryption/encryption.service.ts +31 -0
  605. package/nestjs/src/integrations/image/image.module.ts +9 -0
  606. package/nestjs/src/integrations/image/image.service.ts +61 -0
  607. package/nestjs/src/integrations/meta-business/meta-business.module.ts +10 -0
  608. package/nestjs/src/integrations/meta-business/meta-instagram.service.ts +94 -0
  609. package/nestjs/src/integrations/meta-business/meta-webhook.service.ts +52 -0
  610. package/nestjs/src/integrations/meta-business/meta-whatsapp.service.ts +254 -0
  611. package/nestjs/src/integrations/minio/minio.module.ts +9 -0
  612. package/nestjs/src/integrations/minio/minio.service.ts +88 -0
  613. package/nestjs/src/integrations/netgsm/netgsm.module.ts +8 -0
  614. package/nestjs/src/integrations/netgsm/netgsm.service.ts +17 -0
  615. package/nestjs/src/integrations/tektippay/tektippay.module.ts +8 -0
  616. package/nestjs/src/integrations/tektippay/tektippay.service.ts +23 -0
  617. package/nestjs/src/leads/dto/index.ts +2 -0
  618. package/nestjs/src/leads/dto/query-leads.dto.ts +50 -0
  619. package/nestjs/src/leads/dto/update-lead.dto.ts +26 -0
  620. package/nestjs/src/leads/leads.controller.ts +184 -0
  621. package/nestjs/src/leads/leads.module.ts +10 -0
  622. package/nestjs/src/leads/leads.service.ts +375 -0
  623. package/nestjs/src/logging/logging.controller.ts +82 -0
  624. package/nestjs/src/logging/logging.module.ts +11 -0
  625. package/nestjs/src/logging/logging.service.ts +180 -0
  626. package/nestjs/src/main.ts +86 -0
  627. package/nestjs/src/outbound-campaigns/dto/create-campaign.dto.ts +47 -0
  628. package/nestjs/src/outbound-campaigns/dto/index.ts +3 -0
  629. package/nestjs/src/outbound-campaigns/dto/update-campaign.dto.ts +31 -0
  630. package/nestjs/src/outbound-campaigns/dto/upload-entries.dto.ts +35 -0
  631. package/nestjs/src/outbound-campaigns/outbound-campaigns.controller.ts +307 -0
  632. package/nestjs/src/outbound-campaigns/outbound-campaigns.module.ts +13 -0
  633. package/nestjs/src/outbound-campaigns/outbound-campaigns.service.ts +471 -0
  634. package/nestjs/src/outbound-campaigns/processors/campaign-dispatch.processor.ts +366 -0
  635. package/nestjs/src/patients/documents.service.ts +231 -0
  636. package/nestjs/src/patients/dto/create-examination.dto.ts +34 -0
  637. package/nestjs/src/patients/dto/create-note.dto.ts +14 -0
  638. package/nestjs/src/patients/dto/create-patient.dto.ts +86 -0
  639. package/nestjs/src/patients/dto/create-photo-set.dto.ts +32 -0
  640. package/nestjs/src/patients/dto/index.ts +10 -0
  641. package/nestjs/src/patients/dto/update-examination.dto.ts +29 -0
  642. package/nestjs/src/patients/dto/update-medical-history.dto.ts +47 -0
  643. package/nestjs/src/patients/dto/update-patient.dto.ts +87 -0
  644. package/nestjs/src/patients/dto/update-photo-set.dto.ts +28 -0
  645. package/nestjs/src/patients/dto/upload-document.dto.ts +31 -0
  646. package/nestjs/src/patients/dto/upload-photo.dto.ts +27 -0
  647. package/nestjs/src/patients/examinations.service.ts +271 -0
  648. package/nestjs/src/patients/medical-history.service.ts +149 -0
  649. package/nestjs/src/patients/notes.service.ts +172 -0
  650. package/nestjs/src/patients/patients.controller.ts +485 -0
  651. package/nestjs/src/patients/patients.module.ts +22 -0
  652. package/nestjs/src/patients/patients.service.ts +412 -0
  653. package/nestjs/src/patients/photo-sets.service.ts +389 -0
  654. package/nestjs/src/phone-numbers/dto/create-phone-number.dto.ts +57 -0
  655. package/nestjs/src/phone-numbers/dto/index.ts +3 -0
  656. package/nestjs/src/phone-numbers/dto/update-phone-number.dto.ts +38 -0
  657. package/nestjs/src/phone-numbers/dto/update-schedule.dto.ts +39 -0
  658. package/nestjs/src/phone-numbers/phone-numbers.controller.ts +125 -0
  659. package/nestjs/src/phone-numbers/phone-numbers.module.ts +10 -0
  660. package/nestjs/src/phone-numbers/phone-numbers.service.ts +209 -0
  661. package/nestjs/src/plans/dto/create-plan.dto.ts +70 -0
  662. package/nestjs/src/plans/dto/index.ts +2 -0
  663. package/nestjs/src/plans/dto/update-plan.dto.ts +80 -0
  664. package/nestjs/src/plans/plans.controller.ts +50 -0
  665. package/nestjs/src/plans/plans.module.ts +10 -0
  666. package/nestjs/src/plans/plans.service.ts +115 -0
  667. package/nestjs/src/platform/dto/create-tenant.dto.ts +36 -0
  668. package/nestjs/src/platform/dto/index.ts +2 -0
  669. package/nestjs/src/platform/dto/update-tenant-platform.dto.ts +44 -0
  670. package/nestjs/src/platform/platform.controller.ts +79 -0
  671. package/nestjs/src/platform/platform.module.ts +10 -0
  672. package/nestjs/src/platform/platform.service.ts +301 -0
  673. package/nestjs/src/queue/queue.module.ts +56 -0
  674. package/nestjs/src/redis/redis.module.ts +20 -0
  675. package/nestjs/src/tenants/dto/index.ts +1 -0
  676. package/nestjs/src/tenants/dto/update-tenant.dto.ts +15 -0
  677. package/nestjs/src/tenants/tenants.controller.ts +45 -0
  678. package/nestjs/src/tenants/tenants.module.ts +10 -0
  679. package/nestjs/src/tenants/tenants.service.ts +41 -0
  680. package/nestjs/src/tools/adapters/elevenlabs-http.adapter.ts +51 -0
  681. package/nestjs/src/tools/adapters/elevenlabs-ws.adapter.ts +59 -0
  682. package/nestjs/src/tools/handlers/calendar.tools.ts +441 -0
  683. package/nestjs/src/tools/handlers/notification.tools.ts +34 -0
  684. package/nestjs/src/tools/handlers/operator.tools.ts +303 -0
  685. package/nestjs/src/tools/handlers/patient.tools.ts +242 -0
  686. package/nestjs/src/tools/handlers/payment.tools.ts +43 -0
  687. package/nestjs/src/tools/handlers/validation.tools.ts +152 -0
  688. package/nestjs/src/tools/tool-registry.service.ts +221 -0
  689. package/nestjs/src/tools/tool.decorator.ts +16 -0
  690. package/nestjs/src/tools/tool.interfaces.ts +26 -0
  691. package/nestjs/src/tools/tools.module.ts +50 -0
  692. package/nestjs/src/treatments/dto/create-plan-item.dto.ts +27 -0
  693. package/nestjs/src/treatments/dto/create-treatment-plan.dto.ts +69 -0
  694. package/nestjs/src/treatments/dto/create-treatment.dto.ts +59 -0
  695. package/nestjs/src/treatments/dto/index.ts +6 -0
  696. package/nestjs/src/treatments/dto/update-plan-item.dto.ts +23 -0
  697. package/nestjs/src/treatments/dto/update-treatment-plan.dto.ts +22 -0
  698. package/nestjs/src/treatments/dto/update-treatment.dto.ts +70 -0
  699. package/nestjs/src/treatments/treatment-plans.service.ts +362 -0
  700. package/nestjs/src/treatments/treatments.controller.ts +265 -0
  701. package/nestjs/src/treatments/treatments.module.ts +14 -0
  702. package/nestjs/src/treatments/treatments.service.ts +165 -0
  703. package/nestjs/src/users/doctor-profiles.service.ts +202 -0
  704. package/nestjs/src/users/dto/index.ts +4 -0
  705. package/nestjs/src/users/dto/invite-user.dto.ts +52 -0
  706. package/nestjs/src/users/dto/update-clinic-assignments.dto.ts +9 -0
  707. package/nestjs/src/users/dto/update-doctor-profile.dto.ts +49 -0
  708. package/nestjs/src/users/dto/update-user.dto.ts +41 -0
  709. package/nestjs/src/users/users.controller.ts +142 -0
  710. package/nestjs/src/users/users.module.ts +11 -0
  711. package/nestjs/src/users/users.service.ts +250 -0
  712. package/nestjs/src/webhooks/elevenlabs-tool.controller.ts +66 -0
  713. package/nestjs/src/webhooks/elevenlabs-webhook.controller.ts +60 -0
  714. package/nestjs/src/webhooks/meta-webhook.controller.ts +178 -0
  715. package/nestjs/src/webhooks/processors/elevenlabs-webhook.processor.ts +28 -0
  716. package/nestjs/src/webhooks/webhooks.module.ts +17 -0
  717. package/nestjs/src/whatsapp/chat-context.service.ts +281 -0
  718. package/nestjs/src/whatsapp/dto/add-blacklist.dto.ts +13 -0
  719. package/nestjs/src/whatsapp/dto/index.ts +2 -0
  720. package/nestjs/src/whatsapp/dto/send-message.dto.ts +14 -0
  721. package/nestjs/src/whatsapp/listeners/meta-message.listener.ts +579 -0
  722. package/nestjs/src/whatsapp/meta-auth.controller.ts +268 -0
  723. package/nestjs/src/whatsapp/meta-instagram-auth.controller.ts +244 -0
  724. package/nestjs/src/whatsapp/operator.service.ts +692 -0
  725. package/nestjs/src/whatsapp/processors/cost-sweep.processor.ts +130 -0
  726. package/nestjs/src/whatsapp/processors/grace.processor.ts +191 -0
  727. package/nestjs/src/whatsapp/processors/message-buffer.processor.ts +138 -0
  728. package/nestjs/src/whatsapp/processors/message-cleanup.processor.ts +148 -0
  729. package/nestjs/src/whatsapp/processors/meta-token-refresh.processor.ts +114 -0
  730. package/nestjs/src/whatsapp/processors/operator-expiry.processor.ts +105 -0
  731. package/nestjs/src/whatsapp/processors/profile-update.processor.ts +234 -0
  732. package/nestjs/src/whatsapp/processors/session-cleanup.processor.ts +178 -0
  733. package/nestjs/src/whatsapp/processors/session-labels.processor.ts +248 -0
  734. package/nestjs/src/whatsapp/whatsapp-agent.service.ts +2506 -0
  735. package/nestjs/src/whatsapp/whatsapp-recovery.service.ts +117 -0
  736. package/nestjs/src/whatsapp/whatsapp.controller.ts +398 -0
  737. package/nestjs/src/whatsapp/whatsapp.module.ts +51 -0
  738. package/nestjs/src/whatsapp/whatsapp.service.ts +592 -0
  739. package/nestjs/test/app.e2e-spec.ts +25 -0
  740. package/nestjs/test/jest-e2e.json +9 -0
  741. package/nestjs/tsconfig.build.json +4 -0
  742. package/nestjs/tsconfig.json +25 -0
  743. package/nginx.example.conf +18 -0
  744. package/package.json +47 -0
  745. package/scripts/addRecipient.ts +48 -0
  746. package/scripts/listRecipients.ts +31 -0
  747. package/scripts/migrate-agent-ref.ts +86 -0
  748. package/scripts/migrate-sessions.ts +183 -0
  749. package/scripts/promote.ts +27 -0
  750. package/scripts/retrigger.ts +84 -0
  751. package/scripts/seed.ts +435 -0
  752. package/scripts/testSend.ts +63 -0
  753. package/src/app.ts +85 -0
  754. package/src/config/agentPrompts.json +19 -0
  755. package/src/config/database.ts +21 -0
  756. package/src/config/env.ts +94 -0
  757. package/src/config/index.ts +86 -0
  758. package/src/controllers/webhook.controller.ts +150 -0
  759. package/src/errors/index.ts +9 -0
  760. package/src/hooks/webhookValidator.ts +55 -0
  761. package/src/index.ts +68 -0
  762. package/src/migrations/001_session-architecture.ts +138 -0
  763. package/src/migrations/002_agent-ref.ts +65 -0
  764. package/src/migrations/003_shift-schedule.ts +55 -0
  765. package/src/migrations/004_invert-shift-to-clinic-hours.ts +30 -0
  766. package/src/migrations/005_composite-baileys-chat-ids.ts +60 -0
  767. package/src/migrations/migration.model.ts +27 -0
  768. package/src/migrations/migration.routes.ts +40 -0
  769. package/src/migrations/migration.runner.ts +112 -0
  770. package/src/models/ConversationClaim.ts +17 -0
  771. package/src/models/ConversationEvaluation.ts +91 -0
  772. package/src/models/Summary.ts +77 -0
  773. package/src/models/Transcription.ts +68 -0
  774. package/src/models/WhatsAppRecipient.ts +37 -0
  775. package/src/modules/admin/admin.routes.ts +2385 -0
  776. package/src/modules/admin/elevenlabs-test-chat.routes.ts +193 -0
  777. package/src/modules/admin/whatsapp-test-chat.routes.ts +244 -0
  778. package/src/modules/agents/agent.model.ts +93 -0
  779. package/src/modules/agents/agent.routes.ts +65 -0
  780. package/src/modules/appointment-validation/appointment-validation.model.ts +163 -0
  781. package/src/modules/appointment-validation/appointment-validation.routes.ts +275 -0
  782. package/src/modules/appointment-validation/appointment-validation.service.ts +1028 -0
  783. package/src/modules/auth/auth.model.ts +42 -0
  784. package/src/modules/auth/auth.routes.ts +199 -0
  785. package/src/modules/auth/auth.service.ts +210 -0
  786. package/src/modules/auth/refresh-token.model.ts +26 -0
  787. package/src/modules/billing/billing-alert.model.ts +28 -0
  788. package/src/modules/billing/billing-period-snapshot.model.ts +68 -0
  789. package/src/modules/billing/billing.model.ts +42 -0
  790. package/src/modules/billing/billing.routes.ts +67 -0
  791. package/src/modules/billing/billing.service.ts +562 -0
  792. package/src/modules/billing/payment.model.ts +42 -0
  793. package/src/modules/calls/call.model.ts +102 -0
  794. package/src/modules/calls/call.routes.ts +118 -0
  795. package/src/modules/campaigns/campaign.model.ts +111 -0
  796. package/src/modules/campaigns/campaign.routes.ts +402 -0
  797. package/src/modules/campaigns/campaign.service.ts +99 -0
  798. package/src/modules/google-calendar/google-calendar.routes.ts +31 -0
  799. package/src/modules/inbound-call/inbound-call-config.model.ts +49 -0
  800. package/src/modules/inbound-call/inbound-call.routes.ts +289 -0
  801. package/src/modules/leads/lead.model.ts +40 -0
  802. package/src/modules/leads/lead.routes.ts +246 -0
  803. package/src/modules/logs/log.model.ts +27 -0
  804. package/src/modules/logs/log.routes.ts +102 -0
  805. package/src/modules/plans/plan.model.ts +45 -0
  806. package/src/modules/surveys/survey.model.ts +70 -0
  807. package/src/modules/surveys/survey.routes.ts +187 -0
  808. package/src/modules/tenants/tenant.model.ts +181 -0
  809. package/src/modules/tenants/tenant.routes.ts +78 -0
  810. package/src/modules/users/user.routes.ts +126 -0
  811. package/src/modules/webhooks/elevenlabs-tool.routes.ts +94 -0
  812. package/src/modules/webhooks/elevenlabs-tool.service.ts +491 -0
  813. package/src/modules/webhooks/elevenlabs.routes.ts +34 -0
  814. package/src/modules/webhooks/elevenlabs.service.ts +565 -0
  815. package/src/modules/webhooks/unipile.routes.ts +917 -0
  816. package/src/modules/whatsapp/appointment.model.ts +47 -0
  817. package/src/modules/whatsapp/operator-request.model.ts +58 -0
  818. package/src/modules/whatsapp/stt-usage.model.ts +30 -0
  819. package/src/modules/whatsapp/whatsapp-chat.model.ts +39 -0
  820. package/src/modules/whatsapp/whatsapp-contact-profile.model.ts +41 -0
  821. package/src/modules/whatsapp/whatsapp-message.model.ts +41 -0
  822. package/src/modules/whatsapp/whatsapp-session.model.ts +60 -0
  823. package/src/modules/whatsapp/whatsapp.routes.ts +1435 -0
  824. package/src/plugins/cors.ts +7 -0
  825. package/src/plugins/jwt.ts +18 -0
  826. package/src/plugins/rawBody.ts +12 -0
  827. package/src/plugins/rbac.ts +24 -0
  828. package/src/routes/admin.routes.ts +208 -0
  829. package/src/routes/health.routes.ts +12 -0
  830. package/src/routes/webhook.routes.ts +12 -0
  831. package/src/services/ai/base.ai.ts +132 -0
  832. package/src/services/ai/gemini.service.ts +41 -0
  833. package/src/services/ai/index.ts +24 -0
  834. package/src/services/ai/openai.service.ts +48 -0
  835. package/src/services/elevenlabs.service.ts +532 -0
  836. package/src/services/google-calendar.service.ts +656 -0
  837. package/src/services/inbound-call-schedule.service.ts +174 -0
  838. package/src/services/netgsm.service.ts +128 -0
  839. package/src/services/unipile.service.ts +200 -0
  840. package/src/services/whatsapp-agent.service.ts +2479 -0
  841. package/src/services/whatsapp.service.ts +245 -0
  842. package/src/templates/index.ts +71 -0
  843. package/src/templates/receptionist.ts +44 -0
  844. package/src/templates/survey.ts +44 -0
  845. package/src/types/index.ts +218 -0
  846. package/src/utils/logger.ts +83 -0
  847. package/tsconfig.json +19 -0
  848. package/web/.dockerignore +4 -0
  849. package/web/Dockerfile +18 -0
  850. package/web/README.md +73 -0
  851. package/web/components.json +23 -0
  852. package/web/eslint.config.js +23 -0
  853. package/web/index.html +14 -0
  854. package/web/nginx.conf +18 -0
  855. package/web/package-lock.json +10292 -0
  856. package/web/package.json +48 -0
  857. package/web/public/favicon.ico +0 -0
  858. package/web/public/vite.svg +1 -0
  859. package/web/src/App.tsx +191 -0
  860. package/web/src/assets/react.svg +1 -0
  861. package/web/src/components/Layout.tsx +261 -0
  862. package/web/src/components/LeadConversation.tsx +251 -0
  863. package/web/src/components/ProtectedRoute.tsx +20 -0
  864. package/web/src/components/conversation-review/CardAudioPlayer.tsx +200 -0
  865. package/web/src/components/conversation-review/CardEvaluation.tsx +351 -0
  866. package/web/src/components/conversation-review/CardMetadata.tsx +44 -0
  867. package/web/src/components/conversation-review/ConversationCard.tsx +95 -0
  868. package/web/src/components/conversation-review/ReviewContainer.tsx +120 -0
  869. package/web/src/components/conversation-review/ReviewFilters.tsx +88 -0
  870. package/web/src/components/empty-state.tsx +15 -0
  871. package/web/src/components/page-header.tsx +19 -0
  872. package/web/src/components/page-loader.tsx +32 -0
  873. package/web/src/components/pagination.tsx +38 -0
  874. package/web/src/components/stat-card.tsx +27 -0
  875. package/web/src/components/status-badge.tsx +125 -0
  876. package/web/src/components/ui/alert.tsx +66 -0
  877. package/web/src/components/ui/badge.tsx +48 -0
  878. package/web/src/components/ui/button.tsx +64 -0
  879. package/web/src/components/ui/card.tsx +92 -0
  880. package/web/src/components/ui/checkbox.tsx +32 -0
  881. package/web/src/components/ui/dialog.tsx +158 -0
  882. package/web/src/components/ui/dropdown-menu.tsx +255 -0
  883. package/web/src/components/ui/input.tsx +21 -0
  884. package/web/src/components/ui/label.tsx +22 -0
  885. package/web/src/components/ui/progress.tsx +29 -0
  886. package/web/src/components/ui/select.tsx +188 -0
  887. package/web/src/components/ui/separator.tsx +28 -0
  888. package/web/src/components/ui/sheet.tsx +123 -0
  889. package/web/src/components/ui/skeleton.tsx +13 -0
  890. package/web/src/components/ui/sonner.tsx +35 -0
  891. package/web/src/components/ui/table.tsx +116 -0
  892. package/web/src/components/ui/tabs.tsx +89 -0
  893. package/web/src/components/ui/textarea.tsx +18 -0
  894. package/web/src/components/ui/tooltip.tsx +57 -0
  895. package/web/src/components/whatsapp/ChatDetail.tsx +417 -0
  896. package/web/src/components/whatsapp/ChatHeader.tsx +78 -0
  897. package/web/src/components/whatsapp/ChatList.tsx +107 -0
  898. package/web/src/components/whatsapp/ChatListItem.tsx +60 -0
  899. package/web/src/components/whatsapp/MessageBubble.tsx +46 -0
  900. package/web/src/components/whatsapp/MessageInput.tsx +63 -0
  901. package/web/src/components/whatsapp/MessageStream.tsx +135 -0
  902. package/web/src/components/whatsapp/SessionDivider.tsx +65 -0
  903. package/web/src/components/whatsapp/SessionHistory.tsx +119 -0
  904. package/web/src/components/whatsapp/settings/AiSettingsTab.tsx +268 -0
  905. package/web/src/components/whatsapp/settings/CalendarTab.tsx +339 -0
  906. package/web/src/components/whatsapp/settings/ConnectionTab.tsx +236 -0
  907. package/web/src/components/whatsapp/settings/NotificationsTab.tsx +109 -0
  908. package/web/src/components/whatsapp/settings/OperatorTab.tsx +303 -0
  909. package/web/src/contexts/AuthContext.tsx +103 -0
  910. package/web/src/index.css +130 -0
  911. package/web/src/lib/api.ts +92 -0
  912. package/web/src/lib/utils.ts +6 -0
  913. package/web/src/main.tsx +10 -0
  914. package/web/src/pages/AppointmentDetail.tsx +206 -0
  915. package/web/src/pages/AppointmentValidation.tsx +157 -0
  916. package/web/src/pages/AppointmentValidationDetail.tsx +617 -0
  917. package/web/src/pages/AppointmentValidationNew.tsx +1005 -0
  918. package/web/src/pages/Appointments.tsx +283 -0
  919. package/web/src/pages/Billing.tsx +126 -0
  920. package/web/src/pages/CallDetail.tsx +293 -0
  921. package/web/src/pages/CallSettings.tsx +313 -0
  922. package/web/src/pages/Calls.tsx +188 -0
  923. package/web/src/pages/CampaignDetail.tsx +216 -0
  924. package/web/src/pages/CampaignNew.tsx +277 -0
  925. package/web/src/pages/CampaignResults.tsx +185 -0
  926. package/web/src/pages/Campaigns.tsx +171 -0
  927. package/web/src/pages/Dashboard.tsx +336 -0
  928. package/web/src/pages/InboundSchedule.tsx +246 -0
  929. package/web/src/pages/LeadDetail.tsx +183 -0
  930. package/web/src/pages/Leads.tsx +258 -0
  931. package/web/src/pages/Login.tsx +99 -0
  932. package/web/src/pages/Register.tsx +129 -0
  933. package/web/src/pages/Settings.tsx +133 -0
  934. package/web/src/pages/SurveyDetail.tsx +232 -0
  935. package/web/src/pages/SurveyPreview.tsx +179 -0
  936. package/web/src/pages/Surveys.tsx +207 -0
  937. package/web/src/pages/Users.tsx +199 -0
  938. package/web/src/pages/WhatsApp.tsx +147 -0
  939. package/web/src/pages/WhatsAppSettings.tsx +215 -0
  940. package/web/src/pages/admin/AdminBaileysMessageLog.tsx +331 -0
  941. package/web/src/pages/admin/AdminBaileysRawMessages.tsx +318 -0
  942. package/web/src/pages/admin/AdminConversationReview.tsx +116 -0
  943. package/web/src/pages/admin/AdminCredits.tsx +467 -0
  944. package/web/src/pages/admin/AdminElevenLabsAgentDetail.tsx +332 -0
  945. package/web/src/pages/admin/AdminElevenLabsAgents.tsx +164 -0
  946. package/web/src/pages/admin/AdminElevenLabsBatchCallDetail.tsx +214 -0
  947. package/web/src/pages/admin/AdminElevenLabsBatchCalls.tsx +293 -0
  948. package/web/src/pages/admin/AdminElevenLabsConversationDetail.tsx +230 -0
  949. package/web/src/pages/admin/AdminElevenLabsConversations.tsx +228 -0
  950. package/web/src/pages/admin/AdminElevenLabsInboundCalls.tsx +293 -0
  951. package/web/src/pages/admin/AdminElevenLabsPhoneNumbers.tsx +506 -0
  952. package/web/src/pages/admin/AdminElevenLabsTestChat.tsx +258 -0
  953. package/web/src/pages/admin/AdminElevenLabsWebhooks.tsx +289 -0
  954. package/web/src/pages/admin/AdminEvaluationDashboard.tsx +520 -0
  955. package/web/src/pages/admin/AdminLeads.tsx +339 -0
  956. package/web/src/pages/admin/AdminLogs.tsx +283 -0
  957. package/web/src/pages/admin/AdminMigrations.tsx +247 -0
  958. package/web/src/pages/admin/AdminPlans.tsx +313 -0
  959. package/web/src/pages/admin/AdminSystem.tsx +391 -0
  960. package/web/src/pages/admin/AdminTenantBillableItems.tsx +464 -0
  961. package/web/src/pages/admin/AdminTenantDetail.tsx +1317 -0
  962. package/web/src/pages/admin/AdminTenants.tsx +274 -0
  963. package/web/src/pages/admin/AdminWhatsApp.tsx +618 -0
  964. package/web/src/pages/admin/AdminWhatsAppTestChat.tsx +328 -0
  965. package/web/src/types/index.ts +242 -0
  966. package/web/tsconfig.app.json +32 -0
  967. package/web/tsconfig.json +13 -0
  968. package/web/tsconfig.node.json +26 -0
  969. package/web/vite.config.ts +23 -0
@@ -0,0 +1,981 @@
1
+ import { useState, useRef, useEffect, useCallback, useMemo } from "react"
2
+ import { useQueryClient } from "@tanstack/react-query"
3
+ import { api } from "@/lib/api"
4
+ import { useTranslation } from "react-i18next"
5
+ import { format, formatDistanceToNow } from "date-fns"
6
+ import { tr } from "date-fns/locale"
7
+ import { TZDate } from "@date-fns/tz"
8
+ import { toast } from "sonner"
9
+ import {
10
+ Search,
11
+ Send,
12
+ ArrowDownLeft,
13
+ ArrowUpRight,
14
+ CheckCircle,
15
+ Bot,
16
+ User,
17
+ Phone,
18
+ MessageSquare,
19
+ Loader2,
20
+ Hash,
21
+ Paperclip,
22
+ } from "lucide-react"
23
+
24
+ import {
25
+ useChats,
26
+ useChatMessages,
27
+ useSendMessage,
28
+ useTakeover,
29
+ useRelease,
30
+ useResolve,
31
+ useAssignChat,
32
+ useUnassignChat,
33
+ useConnectionStatus,
34
+ } from "@/lib/hooks/use-chats"
35
+ import { useUsers } from "@/lib/hooks/use-users"
36
+ import type { Chat, ChatMessage } from "@/lib/hooks/use-chats"
37
+
38
+ import { Button } from "@/components/ui/button"
39
+ import { Input } from "@/components/ui/input"
40
+ import { Badge } from "@/components/ui/badge"
41
+ import { Skeleton } from "@/components/ui/skeleton"
42
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
43
+ import { ScrollArea } from "@/components/ui/scroll-area"
44
+ import {
45
+ Select,
46
+ SelectContent,
47
+ SelectItem,
48
+ SelectTrigger,
49
+ SelectValue,
50
+ } from "@/components/ui/select"
51
+ import { useClinicTimezone } from "@/lib/hooks/use-clinics"
52
+ import { cn } from "@/lib/utils"
53
+
54
+ // ─── Helpers ────────────────────────────────────────────────
55
+
56
+ function useMediaUrl(msgId: string | null) {
57
+ const [url, setUrl] = useState<string | null>(null)
58
+ useEffect(() => {
59
+ if (!msgId) return
60
+ let revoked = false
61
+ api.get(`/whatsapp/media/${msgId}`, { responseType: "blob" })
62
+ .then(({ data }) => {
63
+ if (!revoked) setUrl(URL.createObjectURL(data))
64
+ })
65
+ .catch(() => {})
66
+ return () => { revoked = true; if (url) URL.revokeObjectURL(url) }
67
+ }, [msgId])
68
+ return url
69
+ }
70
+
71
+ function safeDate(val: string | null | undefined): Date | null {
72
+ if (!val) return null
73
+ const d = new Date(val)
74
+ return isNaN(d.getTime()) ? null : d
75
+ }
76
+
77
+ function InstagramIcon({ className }: { className?: string }) {
78
+ return (
79
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={cn("text-pink-500", className)}>
80
+ <rect width="20" height="20" x="2" y="2" rx="5" ry="5" />
81
+ <path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
82
+ <line x1="17.5" x2="17.51" y1="6.5" y2="6.5" />
83
+ </svg>
84
+ )
85
+ }
86
+
87
+ function ChannelIcon({ channel, className }: { channel: string; className?: string }) {
88
+ if (channel === "instagram") {
89
+ return <InstagramIcon className={className} />
90
+ }
91
+ // WhatsApp SVG icon
92
+ return (
93
+ <svg viewBox="0 0 24 24" fill="currentColor" className={cn("text-green-500", className)}>
94
+ <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" />
95
+ </svg>
96
+ )
97
+ }
98
+
99
+ function channelColor(channel: string) {
100
+ return channel === "instagram" ? "bg-pink-500/10" : "bg-green-500/10"
101
+ }
102
+
103
+ // ─── Main Page ───────────────────────────────────────────────
104
+
105
+ export default function ChatsPage() {
106
+ const { t } = useTranslation()
107
+ const [search, setSearch] = useState("")
108
+ const [statusFilter, setStatusFilter] = useState("all")
109
+ const [channelFilter, setChannelFilter] = useState("all")
110
+ const [selectedChatId, setSelectedChatId] = useState<string | null>(null)
111
+
112
+ const { data, isLoading } = useChats({
113
+ search: search || undefined,
114
+ status: statusFilter !== "all" ? statusFilter : undefined,
115
+ limit: 50,
116
+ })
117
+
118
+ const { data: connections } = useConnectionStatus()
119
+
120
+ // Build receiver ID → label lookup from connections
121
+ const receiverLabels = useMemo(() => {
122
+ const map: Record<string, string> = {}
123
+ for (const c of connections ?? []) {
124
+ if (c.type === "whatsapp" && c.phoneNumberId) map[c.phoneNumberId] = c.label || c.phone || ""
125
+ if (c.type === "instagram" && c.instagramPageId) map[c.instagramPageId] = c.label || c.phone || ""
126
+ }
127
+ return map
128
+ }, [connections])
129
+
130
+ const allChats = data?.data ?? []
131
+ const chats = channelFilter === "all"
132
+ ? allChats
133
+ : allChats.filter((c) => c.chat_channel === channelFilter)
134
+ const selectedChat = chats.find((c) => c.chat_id === selectedChatId) ?? null
135
+
136
+ return (
137
+ <div className="flex h-full -m-4 sm:-m-6">
138
+ {/* Sidebar — Chat List */}
139
+ <div className="w-80 border-r flex flex-col bg-card shrink-0">
140
+ {/* Filters */}
141
+ <div className="p-3 space-y-2 border-b">
142
+ <div className="relative">
143
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
144
+ <Input
145
+ className="pl-8 h-8"
146
+ placeholder={t("chats.searchChats")}
147
+ value={search}
148
+ onChange={(e) => setSearch(e.target.value)}
149
+ />
150
+ </div>
151
+ <div className="flex gap-1.5">
152
+ {/* Channel tabs */}
153
+ <div className="flex rounded-lg border overflow-hidden flex-1">
154
+ {(["all", "whatsapp", "instagram"] as const).map((ch) => (
155
+ <button
156
+ key={ch}
157
+ onClick={() => setChannelFilter(ch)}
158
+ className={cn(
159
+ "flex-1 flex items-center justify-center gap-1 py-1 text-[10px] font-medium transition-colors",
160
+ channelFilter === ch
161
+ ? "bg-primary text-primary-foreground"
162
+ : "bg-background hover:bg-muted text-muted-foreground",
163
+ ch !== "all" && "border-l",
164
+ )}
165
+ >
166
+ {ch === "all" ? t("chats.allChats") : ch === "whatsapp" ? (
167
+ <><ChannelIcon channel="whatsapp" className="w-3 h-3" /> WA</>
168
+ ) : (
169
+ <><InstagramIcon className="w-3 h-3" /> IG</>
170
+ )}
171
+ </button>
172
+ ))}
173
+ </div>
174
+ {/* Status filter */}
175
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
176
+ <SelectTrigger className="h-7 text-[10px] w-[90px]">
177
+ <SelectValue>
178
+ {statusFilter === "all" ? t("common.all") : t(`chats.status_${statusFilter}`)}
179
+ </SelectValue>
180
+ </SelectTrigger>
181
+ <SelectContent>
182
+ <SelectItem value="all">{t("common.all")}</SelectItem>
183
+ <SelectItem value="active">{t("chats.status_active")}</SelectItem>
184
+ <SelectItem value="waiting">{t("chats.status_waiting")}</SelectItem>
185
+ </SelectContent>
186
+ </Select>
187
+ </div>
188
+ </div>
189
+
190
+ {/* Chat list */}
191
+ <ScrollArea className="flex-1">
192
+ {isLoading ? (
193
+ <div className="space-y-1 p-2">
194
+ {Array.from({ length: 8 }).map((_, i) => (
195
+ <Skeleton key={i} className="h-16 w-full rounded-lg" />
196
+ ))}
197
+ </div>
198
+ ) : chats.length === 0 ? (
199
+ <div className="flex flex-col items-center justify-center py-16 text-center px-4">
200
+ <MessageSquare className="w-8 h-8 text-muted-foreground/40 mb-2" />
201
+ <p className="text-sm text-muted-foreground">{t("chats.noChats")}</p>
202
+ </div>
203
+ ) : (
204
+ <div className="p-1">
205
+ {chats.map((chat) => (
206
+ <ChatListItem
207
+ key={chat.chat_id}
208
+ chat={chat}
209
+ isActive={selectedChatId === chat.chat_id}
210
+ onClick={() => setSelectedChatId(chat.chat_id)}
211
+ receiverLabels={receiverLabels}
212
+ />
213
+ ))}
214
+ </div>
215
+ )}
216
+ </ScrollArea>
217
+ </div>
218
+
219
+ {/* Main Area */}
220
+ <div className="flex-1 flex flex-col min-w-0">
221
+ {selectedChat ? (
222
+ <ChatView chat={selectedChat} receiverLabels={receiverLabels} />
223
+ ) : (
224
+ <div className="flex-1 flex items-center justify-center">
225
+ <div className="text-center space-y-2">
226
+ <MessageSquare className="w-12 h-12 text-muted-foreground/30 mx-auto" />
227
+ <p className="text-sm text-muted-foreground">{t("chats.selectChat")}</p>
228
+ </div>
229
+ </div>
230
+ )}
231
+ </div>
232
+ </div>
233
+ )
234
+ }
235
+
236
+ // ─── Chat List Item ──────────────────────────────────────────
237
+
238
+ function ChatListItem({
239
+ chat,
240
+ isActive,
241
+ onClick,
242
+ receiverLabels,
243
+ }: {
244
+ chat: Chat
245
+ isActive: boolean
246
+ onClick: () => void
247
+ receiverLabels: Record<string, string>
248
+ }) {
249
+ const { t } = useTranslation()
250
+ const session = chat.sessions[0]
251
+ const name = chat.patient?.patient_display_name || chat.chat_contact_name || chat.chat_contact_phone || "?"
252
+ const assignee = chat.assigned_to
253
+ const lastDate = safeDate(chat.chat_last_message_at)
254
+ const receiverId = chat.chat_external_id?.split("@")[1] || ""
255
+ const receiverLabel = chat.phone?.phone_label || chat.phone?.phone_number || receiverLabels[receiverId] || ""
256
+
257
+ return (
258
+ <button
259
+ className={cn(
260
+ "w-full text-left px-3 py-2.5 rounded-lg transition-colors",
261
+ isActive ? "bg-primary/10" : "hover:bg-muted/50",
262
+ )}
263
+ onClick={onClick}
264
+ >
265
+ <div className="flex items-start gap-2.5">
266
+ {/* Channel-colored avatar */}
267
+ <div className={cn(
268
+ "w-9 h-9 rounded-full flex items-center justify-center shrink-0 mt-0.5",
269
+ channelColor(chat.chat_channel),
270
+ )}>
271
+ <ChannelIcon channel={chat.chat_channel} className="w-4 h-4" />
272
+ </div>
273
+
274
+ <div className="flex-1 min-w-0">
275
+ <div className="flex items-center justify-between gap-1">
276
+ <span className="text-sm font-medium truncate">{name}</span>
277
+ <span className="text-[10px] text-muted-foreground shrink-0">
278
+ {lastDate ? formatDistanceToNow(lastDate, { addSuffix: true, locale: tr }) : ""}
279
+ </span>
280
+ </div>
281
+ <p className="text-xs text-muted-foreground truncate mt-0.5">
282
+ {chat.chat_last_message_preview || chat.chat_contact_phone}
283
+ </p>
284
+ <div className="flex items-center gap-1 mt-1">
285
+ {session && (
286
+ <Badge variant="outline" className={cn(
287
+ "text-[9px] px-1.5 py-0 h-4",
288
+ session.session_status === "active" && !session.session_taken_over_by_id && "border-blue-500/30 text-blue-600",
289
+ session.session_status === "active" && session.session_taken_over_by_id && "border-orange-500/30 text-orange-600",
290
+ session.session_status === "waiting" && "border-yellow-500/30 text-yellow-600",
291
+ )}>
292
+ {session.session_status === "active"
293
+ ? session.session_taken_over_by_id ? t("chats.human") : t("chats.ai")
294
+ : t(`chats.status_${session.session_status}`)}
295
+ </Badge>
296
+ )}
297
+ {assignee && (
298
+ <Badge variant="secondary" className="text-[9px] px-1.5 py-0 h-4">
299
+ {assignee.user_first_name}
300
+ </Badge>
301
+ )}
302
+ {receiverLabel && (
303
+ <Badge variant="outline" className="text-[9px] px-1.5 py-0 h-4 ml-auto font-mono">
304
+ {receiverLabel}
305
+ </Badge>
306
+ )}
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </button>
311
+ )
312
+ }
313
+
314
+ // ─── Chat View ───────────────────────────────────────────────
315
+
316
+ function ChatView({ chat, receiverLabels }: { chat: Chat; receiverLabels: Record<string, string> }) {
317
+ const { t } = useTranslation()
318
+ const clinicTZ = useClinicTimezone()
319
+ const { data: messages, isLoading } = useChatMessages(chat.chat_id)
320
+ const sendMessage = useSendMessage()
321
+ const takeover = useTakeover()
322
+ const release = useRelease()
323
+ const resolve = useResolve()
324
+ const assignChat = useAssignChat()
325
+ const unassignChat = useUnassignChat()
326
+ const { data: allUsers } = useUsers()
327
+ const [text, setText] = useState("")
328
+ const messagesContainerRef = useRef<HTMLDivElement>(null)
329
+
330
+ const session = chat.sessions[0]
331
+ const isTakenOver = !!session?.session_taken_over_by_id
332
+ const name = chat.patient?.patient_display_name || chat.chat_contact_name || chat.chat_contact_phone || "?"
333
+ const staffUsers = (allUsers ?? []).filter((u) => u.isActive)
334
+ const receiverId = chat.chat_external_id?.split("@")[1] || ""
335
+ const receiverLabel = chat.phone?.phone_label || chat.phone?.phone_number || receiverLabels[receiverId] || ""
336
+
337
+ useEffect(() => {
338
+ const el = messagesContainerRef.current
339
+ if (el) el.scrollTop = el.scrollHeight
340
+ }, [messages])
341
+
342
+ const handleSend = useCallback(() => {
343
+ if (!text.trim()) return
344
+ const msg = text.trim()
345
+ setText("") // Clear immediately
346
+ sendMessage.mutate(
347
+ { chatId: chat.chat_id, text: msg },
348
+ { onError: () => toast.error(t("chats.sendError")) },
349
+ )
350
+ }, [text, chat.chat_id, sendMessage, t])
351
+
352
+ const handleAssign = async (userId: string) => {
353
+ try {
354
+ if (userId === "unassigned") {
355
+ await unassignChat.mutateAsync(chat.chat_id)
356
+ toast.success(t("chats.chatUnassigned"))
357
+ } else {
358
+ await assignChat.mutateAsync({ chatId: chat.chat_id, userId })
359
+ toast.success(t("chats.chatAssigned"))
360
+ }
361
+ } catch {
362
+ toast.error(t("chats.assignError"))
363
+ }
364
+ }
365
+
366
+ return (
367
+ <div className="flex flex-col h-full min-h-0">
368
+ {/* Header */}
369
+ <div className="border-b flex items-center justify-between px-4 py-2.5 shrink-0 bg-card">
370
+ <div className="flex items-center gap-3 min-w-0">
371
+ <div className={cn(
372
+ "w-10 h-10 rounded-full flex items-center justify-center shrink-0",
373
+ channelColor(chat.chat_channel),
374
+ )}>
375
+ <ChannelIcon channel={chat.chat_channel} className="w-5 h-5" />
376
+ </div>
377
+ <div className="min-w-0">
378
+ <div className="flex items-center gap-2">
379
+ <p className="text-sm font-medium truncate">{name}</p>
380
+ {chat.clinic && (
381
+ <Badge variant="outline" className="text-[9px] px-1.5 py-0 h-4 shrink-0">
382
+ {chat.clinic.clinic_name}
383
+ </Badge>
384
+ )}
385
+ </div>
386
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
387
+ <span className="flex items-center gap-1">
388
+ {chat.chat_channel === "instagram"
389
+ ? <InstagramIcon className="w-3 h-3" />
390
+ : <Phone className="w-3 h-3" />}
391
+ {chat.chat_contact_phone}
392
+ </span>
393
+ {receiverLabel && (
394
+ <span className="flex items-center gap-1 font-mono text-[10px]">
395
+ → {receiverLabel}
396
+ </span>
397
+ )}
398
+ </div>
399
+ </div>
400
+ </div>
401
+ <div className="flex items-center gap-1.5">
402
+ {/* Assign */}
403
+ <Select
404
+ value={chat.chat_assigned_to_id || "unassigned"}
405
+ onValueChange={handleAssign}
406
+ >
407
+ <SelectTrigger className="h-8 w-[130px] text-xs">
408
+ <SelectValue>
409
+ {chat.assigned_to
410
+ ? `${chat.assigned_to.user_first_name} ${chat.assigned_to.user_last_name}`
411
+ : t("chats.unassigned_label")}
412
+ </SelectValue>
413
+ </SelectTrigger>
414
+ <SelectContent>
415
+ <SelectItem value="unassigned">{t("chats.unassigned_label")}</SelectItem>
416
+ {staffUsers.map((u) => (
417
+ <SelectItem key={u.id} value={u.id}>
418
+ {u.firstName} {u.lastName}
419
+ </SelectItem>
420
+ ))}
421
+ </SelectContent>
422
+ </Select>
423
+
424
+ {session?.session_status === "active" && !isTakenOver && (
425
+ <Button size="sm" variant="outline" className="h-8" onClick={() => takeover.mutateAsync(chat.chat_id).then(() => toast.success(t("chats.takenOver")))}>
426
+ <ArrowDownLeft className="w-3.5 h-3.5 mr-1" />
427
+ {t("chats.takeover")}
428
+ </Button>
429
+ )}
430
+ {session?.session_status === "active" && isTakenOver && (
431
+ <Button size="sm" variant="outline" className="h-8" onClick={() => release.mutateAsync(chat.chat_id).then(() => toast.success(t("chats.released")))}>
432
+ <ArrowUpRight className="w-3.5 h-3.5 mr-1" />
433
+ {t("chats.release")}
434
+ </Button>
435
+ )}
436
+ {session && session.session_status !== "resolved" && (
437
+ <Button size="sm" variant="ghost" className="h-8" onClick={() => resolve.mutateAsync(chat.chat_id).then(() => toast.success(t("chats.resolved_msg")))}>
438
+ <CheckCircle className="w-3.5 h-3.5 mr-1" />
439
+ {t("chats.resolve")}
440
+ </Button>
441
+ )}
442
+ </div>
443
+ </div>
444
+
445
+ {/* Messages */}
446
+ <div ref={messagesContainerRef} className="flex-1 overflow-y-auto px-4 py-3 min-h-0">
447
+ {isLoading ? (
448
+ <div className="space-y-3">
449
+ {Array.from({ length: 5 }).map((_, i) => (
450
+ <Skeleton key={i} className={`h-10 ${i % 2 === 0 ? "w-2/3" : "w-1/2 ml-auto"} rounded-xl`} />
451
+ ))}
452
+ </div>
453
+ ) : !messages?.length ? (
454
+ <div className="flex items-center justify-center h-full">
455
+ <p className="text-sm text-muted-foreground">{t("chats.noMessages")}</p>
456
+ </div>
457
+ ) : (
458
+ <div className="space-y-1.5">
459
+ {messages.map((msg) => (
460
+ <MessageBubble key={msg.id} message={msg} tz={clinicTZ} channel={chat.chat_channel} />
461
+ ))}
462
+ </div>
463
+ )}
464
+ </div>
465
+
466
+ {/* Input */}
467
+ {(!session || session.session_status !== "resolved") && (
468
+ <div className="border-t p-3 flex items-center gap-2 bg-card shrink-0">
469
+ {/* <FileAttachButton chatId={chat.chat_id} channel={chat.chat_channel} /> */}
470
+ <Input
471
+ className="flex-1"
472
+ placeholder={t("chats.typeMessage")}
473
+ value={text}
474
+ onChange={(e) => setText(e.target.value)}
475
+ onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
476
+ />
477
+ <Button size="icon" className="shrink-0" onClick={handleSend} disabled={!text.trim()}>
478
+ <Send className="w-4 h-4" />
479
+ </Button>
480
+ </div>
481
+ )}
482
+ </div>
483
+ )
484
+ }
485
+
486
+ // ─── Message Bubble ──────────────────────────────────────────
487
+
488
+ const MEDIA_LABELS: Record<string, string> = {
489
+ image: "📷 Fotoğraf",
490
+ video: "🎥 Video",
491
+ audio: "🎵 Ses",
492
+ voice: "🎤 Sesli mesaj",
493
+ document: "📎 Belge",
494
+ sticker: "🏷️ Sticker",
495
+ location: "📍 Konum",
496
+ contacts: "👤 Kişi",
497
+ reaction: "",
498
+ unreaction: "",
499
+ button_reply: "🔘",
500
+ list_reply: "📋",
501
+ button: "🔘",
502
+ order: "🛒 Sipariş",
503
+ view_once: "🔒 Tek seferlik",
504
+ unsupported: "🔒 Desteklenmeyen",
505
+ story_mention: "📖 Hikayede bahsetti",
506
+ story_reply: "💬 Hikayeye yanıt",
507
+ share: "🔗 Paylaşım",
508
+ reel: "🎬 Reels",
509
+ ig_reel: "🎬 Reels",
510
+ quick_reply: "⚡",
511
+ postback: "📌",
512
+ }
513
+
514
+ function MessageBubble({ message: msg, tz, channel }: { message: ChatMessage; tz: string; channel: string }) {
515
+ const isContact = msg.sender === "contact"
516
+ const isAI = msg.sender === "ai"
517
+ const isSystem = msg.sender === "system"
518
+ const date = safeDate(msg.createdAt)
519
+ const mt = msg.mediaType || ""
520
+
521
+ // System messages — centered pill
522
+ if (isSystem || mt === "system") {
523
+ return (
524
+ <div className="flex justify-center my-2">
525
+ <span className="text-[10px] text-muted-foreground bg-muted/50 rounded-full px-3 py-0.5">
526
+ {msg.text}
527
+ </span>
528
+ </div>
529
+ )
530
+ }
531
+
532
+ // Reactions — small inline
533
+ if (mt === "reaction") {
534
+ return (
535
+ <div className={cn("flex", isContact ? "justify-start" : "justify-end")}>
536
+ <div className="text-xl px-1" title="Tepki">
537
+ {msg.metadata?.emoji || msg.text || "❤️"}
538
+ </div>
539
+ </div>
540
+ )
541
+ }
542
+
543
+ // Unreaction — skip display
544
+ if (mt === "unreaction") return null
545
+
546
+ // Unsupported / view-once / deleted — can't display
547
+ if (mt === "unsupported" || mt === "view_once") {
548
+ return (
549
+ <div className={cn("flex", isContact ? "justify-start" : "justify-end")}>
550
+ <div className="rounded-2xl px-3.5 py-2 text-sm bg-muted/50 border border-dashed rounded-bl-sm">
551
+ <p className="text-xs text-muted-foreground italic flex items-center gap-1.5">
552
+ <span>🔒</span>
553
+ {msg.metadata?.is_deleted
554
+ ? "Bu mesaj silindi"
555
+ : msg.metadata?.original_type
556
+ ? `Tek seferlik ${msg.metadata.original_type === "video" ? "video" : "fotoğraf"}`
557
+ : "Desteklenmeyen mesaj türü"}
558
+ </p>
559
+ <p className="text-[10px] text-muted-foreground/60 mt-0.5">
560
+ Bu içerik yalnızca telefondan görüntülenebilir
561
+ </p>
562
+ {date && (
563
+ <p className="text-[10px] text-muted-foreground mt-1 text-right">
564
+ {format(new TZDate(date, tz || "UTC"), "HH:mm")}
565
+ </p>
566
+ )}
567
+ </div>
568
+ </div>
569
+ )
570
+ }
571
+
572
+ const mediaLabel = MEDIA_LABELS[mt] || ""
573
+ const hasMedia = mt && mt !== "text" && mt !== "transcribedAudio" && mt !== "reaction"
574
+ const hasVisualMedia = mt === "image" || mt === "video" || mt === "sticker" || mt === "reel" || mt === "story_mention" || mt === "ig_reel"
575
+ // Instagram stores direct URLs in mediaKey; WhatsApp stores media IDs that need proxy
576
+ const isDirectUrl = msg.mediaKey?.startsWith("http") || msg.mediaKey?.startsWith("blob:")
577
+ const proxyMediaUrl = useMediaUrl(hasVisualMedia && !isDirectUrl ? msg.id : null)
578
+ const mediaUrl = isDirectUrl ? msg.mediaKey : proxyMediaUrl
579
+
580
+ return (
581
+ <div className={cn("flex", isContact ? "justify-start" : "justify-end")}>
582
+ <div
583
+ className={cn(
584
+ "max-w-[75%] rounded-2xl px-3.5 py-2 text-sm",
585
+ isContact
586
+ ? "bg-muted text-foreground rounded-bl-sm"
587
+ : isAI
588
+ ? "bg-blue-500/10 text-foreground border border-blue-500/20 rounded-br-sm"
589
+ : "bg-primary text-primary-foreground rounded-br-sm",
590
+ )}
591
+ >
592
+ {isAI && (
593
+ <div className="flex items-center gap-1 mb-0.5">
594
+ <Bot className="w-3 h-3" />
595
+ <span className="text-[10px] font-medium opacity-70">AI</span>
596
+ </div>
597
+ )}
598
+ {!isContact && !isAI && msg.senderName && (
599
+ <div className="flex items-center gap-1 mb-0.5">
600
+ <User className="w-3 h-3" />
601
+ <span className="text-[10px] font-medium opacity-70">{msg.senderName}</span>
602
+ </div>
603
+ )}
604
+
605
+ {/* Ad context banner */}
606
+ {msg.adContext && (msg.adContext.headline || msg.adContext.source_type || msg.adContext.body) && (
607
+ <div className="rounded-lg bg-amber-500/10 border border-amber-500/20 px-2.5 py-1.5 mb-1.5">
608
+ <div className="flex items-center gap-1.5 mb-0.5">
609
+ <span className="text-[10px]">📢</span>
610
+ <span className="text-[10px] font-medium text-amber-700 dark:text-amber-400">
611
+ {msg.adContext.source_type === "ad" ? "Reklam tıklaması" : "Reklam üzerinden geldi"}
612
+ </span>
613
+ </div>
614
+ {msg.adContext.headline && (
615
+ <p className="text-xs font-medium">{msg.adContext.headline}</p>
616
+ )}
617
+ {msg.adContext.body && (
618
+ <p className="text-[10px] text-muted-foreground">{msg.adContext.body}</p>
619
+ )}
620
+ {msg.adContext.source_url && (
621
+ <a
622
+ href={msg.adContext.source_url}
623
+ target="_blank"
624
+ rel="noopener noreferrer"
625
+ className="text-[10px] text-primary underline mt-0.5 block"
626
+ >
627
+ Reklamı gör →
628
+ </a>
629
+ )}
630
+ </div>
631
+ )}
632
+
633
+ {/* Visual media (image/video/sticker) */}
634
+ {mediaUrl && (
635
+ <div className={cn("mb-1.5", mt === "sticker" ? "w-24 h-24" : "max-w-[240px]")}>
636
+ {(mt === "video" || mt === "reel" || mt === "ig_reel") ? (
637
+ <video
638
+ src={mediaUrl}
639
+ controls
640
+ className="rounded-lg w-full"
641
+ preload="metadata"
642
+ />
643
+ ) : (
644
+ <img
645
+ src={mediaUrl}
646
+ alt=""
647
+ className={cn("rounded-lg", mt === "sticker" ? "w-24 h-24 object-contain" : "w-full")}
648
+ loading="lazy"
649
+ onError={(e) => { (e.target as HTMLImageElement).style.display = "none" }}
650
+ />
651
+ )}
652
+ </div>
653
+ )}
654
+
655
+ {/* Document */}
656
+ {mt === "document" && (
657
+ <DocumentCard msgId={msg.id} filename={msg.mediaFilename} />
658
+ )}
659
+
660
+ {/* Audio / voice */}
661
+ {(mt === "audio" || mt === "voice") && (
662
+ <AudioPlayer msgId={msg.id} isVoice={mt === "voice"} />
663
+ )}
664
+
665
+ {/* Other non-visual media (order, button, etc.) */}
666
+ {hasMedia && !hasVisualMedia && mt !== "document" && mt !== "audio" && mt !== "voice" && mt !== "location" && mt !== "contacts" && mt !== "reaction" && (
667
+ <div className="flex items-center gap-1.5 mb-1">
668
+ <span className="text-xs opacity-70">{mediaLabel}</span>
669
+ </div>
670
+ )}
671
+
672
+ {/* Location map link */}
673
+ {mt === "location" && msg.metadata?.latitude && (
674
+ <a
675
+ href={msg.metadata.url || `https://maps.google.com/?q=${msg.metadata.latitude},${msg.metadata.longitude}`}
676
+ target="_blank"
677
+ rel="noopener noreferrer"
678
+ className="block mb-1.5 rounded-lg overflow-hidden border hover:border-primary/50 transition-colors"
679
+ >
680
+ <div className="bg-green-500/10 px-3 py-2.5 flex items-start gap-2.5">
681
+ <span className="text-lg mt-0.5">📍</span>
682
+ <div className="min-w-0">
683
+ {msg.metadata.name && <p className="text-xs font-medium truncate">{msg.metadata.name}</p>}
684
+ {msg.metadata.address && <p className="text-[10px] text-muted-foreground truncate">{msg.metadata.address}</p>}
685
+ {!msg.metadata.name && !msg.metadata.address && (
686
+ <p className="text-xs text-muted-foreground font-mono">{msg.metadata.latitude.toFixed(5)}, {msg.metadata.longitude.toFixed(5)}</p>
687
+ )}
688
+ <p className="text-[10px] text-primary mt-1">Haritada aç →</p>
689
+ </div>
690
+ </div>
691
+ </a>
692
+ )}
693
+
694
+ {/* Contact cards */}
695
+ {mt === "contacts" && msg.metadata?.contacts && (
696
+ <div className="space-y-1 mb-1">
697
+ {(msg.metadata.contacts as any[]).map((c: any, i: number) => (
698
+ <div key={i} className="text-xs">
699
+ <span className="font-medium">{c.name}</span>
700
+ {c.phones?.[0]?.phone && <span className="opacity-70 ml-1">{c.phones[0].phone}</span>}
701
+ </div>
702
+ ))}
703
+ </div>
704
+ )}
705
+
706
+ {/* Story mention / story reply */}
707
+ {(mt === "story_mention" || mt === "story_reply") && (
708
+ <div className="rounded-lg bg-gradient-to-r from-purple-500/10 to-pink-500/10 border border-purple-500/20 px-3 py-2 mb-1.5">
709
+ <p className="text-[10px] font-medium text-purple-600">
710
+ {mt === "story_mention" ? "📖 Hikayenizde bahsetti" : "💬 Hikayenize yanıt verdi"}
711
+ </p>
712
+ {msg.metadata?.story_url && (
713
+ <a href={msg.metadata.story_url} target="_blank" rel="noopener noreferrer" className="text-[10px] text-primary underline">
714
+ Hikayeyi gör
715
+ </a>
716
+ )}
717
+ </div>
718
+ )}
719
+
720
+ {/* IG Reel title + link */}
721
+ {mt === "ig_reel" && msg.metadata?.title && (
722
+ <p className="text-xs opacity-70 mb-1 truncate">{msg.metadata.title}</p>
723
+ )}
724
+ {mt === "ig_reel" && msg.metadata?.url && !mediaUrl && (
725
+ <a
726
+ href={msg.metadata.url}
727
+ target="_blank"
728
+ rel="noopener noreferrer"
729
+ className="block mb-1.5 rounded-lg border hover:border-primary/50 transition-colors overflow-hidden"
730
+ >
731
+ <div className="bg-pink-500/5 px-3 py-2 flex items-center gap-2">
732
+ <span className="text-base">🎬</span>
733
+ <div className="min-w-0 flex-1">
734
+ <p className="text-[10px] text-muted-foreground">Instagram Reels</p>
735
+ <p className="text-xs text-primary truncate">{msg.metadata.title || "Reels'i aç"}</p>
736
+ </div>
737
+ </div>
738
+ </a>
739
+ )}
740
+
741
+ {/* Share (post/reel link) */}
742
+ {mt === "share" && msg.metadata?.url && (
743
+ <a
744
+ href={msg.metadata.url}
745
+ target="_blank"
746
+ rel="noopener noreferrer"
747
+ className="block mb-1.5 rounded-lg border hover:border-primary/50 transition-colors overflow-hidden"
748
+ >
749
+ <div className="bg-purple-500/5 px-3 py-2 flex items-center gap-2">
750
+ <span className="text-base">🔗</span>
751
+ <div className="min-w-0 flex-1">
752
+ <p className="text-[10px] text-muted-foreground">Instagram gönderi paylaşıldı</p>
753
+ <p className="text-xs text-primary truncate">{msg.metadata.url}</p>
754
+ </div>
755
+ </div>
756
+ </a>
757
+ )}
758
+
759
+ {/* Button/list reply indicator */}
760
+ {(mt === "button_reply" || mt === "list_reply" || mt === "quick_reply" || mt === "postback") && (
761
+ <div className="text-[10px] opacity-60 mb-0.5">
762
+ {mt === "button_reply" ? "Butona basıldı" : mt === "list_reply" ? "Listeden seçildi" : mt === "quick_reply" ? "Hızlı yanıt" : "Menüden seçildi"}
763
+ </div>
764
+ )}
765
+
766
+ {/* Message text (skip for location/contacts/reaction — already rendered above) */}
767
+ {msg.text && mt !== "location" && mt !== "contacts" && mt !== "reaction" && (
768
+ <p className="whitespace-pre-wrap break-words">{msg.text}</p>
769
+ )}
770
+
771
+ {/* Timestamp */}
772
+ <p className={cn(
773
+ "text-[10px] mt-1 text-right",
774
+ isContact ? "text-muted-foreground" : "opacity-60",
775
+ )}>
776
+ {msg.metadata?._sending ? (
777
+ <Loader2 className="w-3 h-3 animate-spin inline-block" />
778
+ ) : date ? format(new TZDate(date, tz || "UTC"), "HH:mm") : ""}
779
+ </p>
780
+ </div>
781
+ </div>
782
+ )
783
+ }
784
+
785
+ // ─── Document Card ──────────────────────────────────────────
786
+
787
+ function DocumentCard({ msgId, filename }: { msgId: string; filename: string | null }) {
788
+ const [downloading, setDownloading] = useState(false)
789
+
790
+ const handleOpen = async () => {
791
+ setDownloading(true)
792
+ try {
793
+ const { data } = await api.get(`/whatsapp/media/${msgId}`, { responseType: "blob" })
794
+ const url = URL.createObjectURL(data)
795
+ window.open(url, "_blank")
796
+ } catch {}
797
+ setDownloading(false)
798
+ }
799
+
800
+ const ext = filename?.split(".").pop()?.toUpperCase() || "FILE"
801
+
802
+ return (
803
+ <button
804
+ onClick={handleOpen}
805
+ disabled={downloading}
806
+ className="flex items-center gap-2.5 rounded-lg border bg-background/50 px-3 py-2 mb-1.5 hover:bg-muted/50 transition-colors w-full text-left"
807
+ >
808
+ <div className="w-9 h-9 rounded-lg bg-blue-500/10 flex items-center justify-center shrink-0">
809
+ <span className="text-[10px] font-bold text-blue-600">{ext}</span>
810
+ </div>
811
+ <div className="min-w-0 flex-1">
812
+ <p className="text-xs font-medium truncate">{filename || "Belge"}</p>
813
+ <p className="text-[10px] text-muted-foreground">{downloading ? "Aciliyor..." : "Acmak icin tiklayin"}</p>
814
+ </div>
815
+ </button>
816
+ )
817
+ }
818
+
819
+ // ─── Audio Player ───────────────────────────────────────────
820
+
821
+ function AudioPlayer({ msgId, isVoice }: { msgId: string; isVoice: boolean }) {
822
+ const audioUrl = useMediaUrl(msgId)
823
+ const audioRef = useRef<HTMLAudioElement>(null)
824
+ const [playing, setPlaying] = useState(false)
825
+ const [progress, setProgress] = useState(0)
826
+ const [duration, setDuration] = useState(0)
827
+
828
+ useEffect(() => {
829
+ const audio = audioRef.current
830
+ if (!audio) return
831
+ const onTime = () => setProgress(audio.currentTime)
832
+ const onMeta = () => setDuration(audio.duration || 0)
833
+ const onEnd = () => { setPlaying(false); setProgress(0) }
834
+ audio.addEventListener("timeupdate", onTime)
835
+ audio.addEventListener("loadedmetadata", onMeta)
836
+ audio.addEventListener("ended", onEnd)
837
+ return () => {
838
+ audio.removeEventListener("timeupdate", onTime)
839
+ audio.removeEventListener("loadedmetadata", onMeta)
840
+ audio.removeEventListener("ended", onEnd)
841
+ }
842
+ }, [audioUrl])
843
+
844
+ const toggle = () => {
845
+ const audio = audioRef.current
846
+ if (!audio) return
847
+ if (playing) { audio.pause() } else { audio.play() }
848
+ setPlaying(!playing)
849
+ }
850
+
851
+ const seek = (e: React.MouseEvent<HTMLDivElement>) => {
852
+ const audio = audioRef.current
853
+ if (!audio || !duration) return
854
+ const rect = e.currentTarget.getBoundingClientRect()
855
+ const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
856
+ audio.currentTime = pct * duration
857
+ setProgress(pct * duration)
858
+ }
859
+
860
+ const fmt = (s: number) => {
861
+ const m = Math.floor(s / 60)
862
+ const sec = Math.floor(s % 60)
863
+ return `${m}:${sec.toString().padStart(2, "0")}`
864
+ }
865
+
866
+ if (!audioUrl) {
867
+ return <div className="w-[220px] h-10 rounded-full bg-muted/50 animate-pulse mb-1.5" />
868
+ }
869
+
870
+ return (
871
+ <div className="mb-1.5">
872
+ <audio ref={audioRef} src={audioUrl} preload="metadata" />
873
+ <div className="flex items-center gap-2 w-[220px]">
874
+ <button
875
+ onClick={toggle}
876
+ className="w-8 h-8 rounded-full bg-primary/10 hover:bg-primary/20 flex items-center justify-center shrink-0 transition-colors"
877
+ >
878
+ {playing ? (
879
+ <svg viewBox="0 0 24 24" fill="currentColor" className="w-3.5 h-3.5 text-primary">
880
+ <rect x="6" y="5" width="4" height="14" rx="1" />
881
+ <rect x="14" y="5" width="4" height="14" rx="1" />
882
+ </svg>
883
+ ) : (
884
+ <svg viewBox="0 0 24 24" fill="currentColor" className="w-3.5 h-3.5 text-primary ml-0.5">
885
+ <path d="M8 5v14l11-7z" />
886
+ </svg>
887
+ )}
888
+ </button>
889
+ <div className="flex-1 min-w-0">
890
+ <div
891
+ className="h-1.5 rounded-full bg-muted cursor-pointer relative"
892
+ onClick={seek}
893
+ >
894
+ <div
895
+ className="h-full rounded-full bg-primary/60 transition-all"
896
+ style={{ width: `${duration ? (progress / duration) * 100 : 0}%` }}
897
+ />
898
+ </div>
899
+ <div className="flex justify-between mt-0.5">
900
+ <span className="text-[9px] text-muted-foreground">{fmt(progress)}</span>
901
+ <span className="text-[9px] text-muted-foreground">{duration ? fmt(duration) : isVoice ? "Sesli mesaj" : "Ses"}</span>
902
+ </div>
903
+ </div>
904
+ </div>
905
+ </div>
906
+ )
907
+ }
908
+
909
+ // ─── File Attach Button ─────────────────────────────────────
910
+
911
+ function FileAttachButton({ chatId, channel }: { chatId: string; channel: string }) {
912
+ const { t } = useTranslation()
913
+ const fileRef = useRef<HTMLInputElement>(null)
914
+ const qc = useQueryClient()
915
+
916
+ const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
917
+ const file = e.target.files?.[0]
918
+ if (!file) return
919
+
920
+ // Optimistic: show file immediately
921
+ const mediaType = file.type.startsWith("image/") ? "image"
922
+ : file.type.startsWith("video/") ? "video"
923
+ : file.type.startsWith("audio/") ? "audio"
924
+ : "document"
925
+
926
+ const optimisticId = `optimistic-file-${Date.now()}`
927
+ const blobUrl = URL.createObjectURL(file)
928
+
929
+ const optimistic: ChatMessage = {
930
+ id: optimisticId,
931
+ sender: "human",
932
+ senderName: "",
933
+ text: "",
934
+ mediaType,
935
+ mediaKey: blobUrl,
936
+ mediaFilename: file.name,
937
+ metadata: { _sending: true },
938
+ adContext: null,
939
+ createdAt: new Date().toISOString(),
940
+ }
941
+
942
+ // Insert optimistic message
943
+ qc.setQueryData<ChatMessage[]>(["chat-messages", chatId], (old) => [...(old ?? []), optimistic])
944
+
945
+ try {
946
+ const form = new FormData()
947
+ form.append("file", file)
948
+ await api.post(`/whatsapp/chats/${chatId}/media`, form, {
949
+ headers: { "Content-Type": "multipart/form-data" },
950
+ })
951
+ } catch {
952
+ toast.error(t("chats.sendError"))
953
+ }
954
+
955
+ // Refetch real data
956
+ qc.invalidateQueries({ queryKey: ["chat-messages", chatId] })
957
+ qc.invalidateQueries({ queryKey: ["chats"] })
958
+ URL.revokeObjectURL(blobUrl)
959
+ if (fileRef.current) fileRef.current.value = ""
960
+ }
961
+
962
+ return (
963
+ <>
964
+ <input
965
+ ref={fileRef}
966
+ type="file"
967
+ className="hidden"
968
+ accept={channel === "instagram" ? "image/*,video/*" : "image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.csv,.zip"}
969
+ onChange={handleFile}
970
+ />
971
+ <Button
972
+ size="icon"
973
+ variant="ghost"
974
+ className="shrink-0"
975
+ onClick={() => fileRef.current?.click()}
976
+ >
977
+ <Paperclip className="w-4 h-4" />
978
+ </Button>
979
+ </>
980
+ )
981
+ }