@consilioweb/payload-support 0.15.0 → 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 (202) hide show
  1. package/README.md +347 -435
  2. package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
  4. package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
  5. package/dist/components/TicketConversation/SkeletonText.js +20 -0
  6. package/dist/components/TicketConversation/components/ActionPanels.cjs +72 -0
  7. package/dist/components/TicketConversation/components/ActionPanels.js +73 -2
  8. package/dist/components/TicketConversation/components/QuickActions.cjs +22 -1
  9. package/dist/components/TicketConversation/components/QuickActions.js +22 -1
  10. package/dist/components/TicketConversation/constants.cjs +27 -0
  11. package/dist/components/TicketConversation/constants.js +26 -1
  12. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
  13. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
  14. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +59 -0
  15. package/dist/components/TicketConversation/hooks/useTicketActions.js +59 -0
  16. package/dist/components/TicketConversation/index.cjs +42 -149
  17. package/dist/components/TicketConversation/index.js +37 -144
  18. package/dist/index.cjs +1835 -741
  19. package/dist/index.d.cts +3 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +1833 -741
  22. package/dist/views/BillingView/client.cjs +40 -54
  23. package/dist/views/BillingView/client.js +40 -54
  24. package/dist/views/ChatView/client.cjs +4 -3
  25. package/dist/views/ChatView/client.js +4 -3
  26. package/dist/views/CrmView/client.cjs +7 -7
  27. package/dist/views/CrmView/client.js +7 -7
  28. package/dist/views/EmailTrackingView/client.cjs +6 -5
  29. package/dist/views/EmailTrackingView/client.js +6 -5
  30. package/dist/views/ImportConversationView/client.cjs +4 -4
  31. package/dist/views/ImportConversationView/client.js +4 -4
  32. package/dist/views/LogsView/client.cjs +3 -2
  33. package/dist/views/LogsView/client.js +3 -2
  34. package/dist/views/NewTicketView/client.cjs +1 -1
  35. package/dist/views/NewTicketView/client.js +1 -1
  36. package/dist/views/PendingEmailsView/client.cjs +7 -6
  37. package/dist/views/PendingEmailsView/client.js +7 -6
  38. package/dist/views/SupportDashboardView/client.cjs +19 -22
  39. package/dist/views/SupportDashboardView/client.js +19 -22
  40. package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
  41. package/dist/views/TicketDetailView/NextActionItem.js +34 -0
  42. package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
  43. package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
  44. package/dist/views/TicketDetailView/client.cjs +59 -190
  45. package/dist/views/TicketDetailView/client.js +52 -183
  46. package/dist/views/TicketDetailView/constants.cjs +20 -0
  47. package/dist/views/TicketDetailView/constants.js +16 -0
  48. package/dist/views/TicketDetailView/helpers.cjs +23 -0
  49. package/dist/views/TicketDetailView/helpers.js +20 -0
  50. package/dist/views/TicketDetailView/types.cjs +2 -0
  51. package/dist/views/TicketDetailView/types.js +1 -0
  52. package/dist/views/TicketInboxView/client.cjs +14 -10
  53. package/dist/views/TicketInboxView/client.js +14 -10
  54. package/dist/views/TicketingSettingsView/client.cjs +101 -96
  55. package/dist/views/TicketingSettingsView/client.js +92 -88
  56. package/dist/views/TimeDashboardView/client.cjs +2 -2
  57. package/dist/views/TimeDashboardView/client.js +2 -2
  58. package/dist/views/shared/StatusPill.cjs +23 -12
  59. package/dist/views/shared/StatusPill.js +23 -12
  60. package/dist/views/shared/dateLocale.cjs +5 -0
  61. package/dist/views/shared/dateLocale.js +3 -0
  62. package/package.json +23 -15
  63. package/src/__tests__/generateTrackingToken.test.ts +53 -0
  64. package/src/__tests__/integration/ai-agent.test.ts +46 -0
  65. package/src/__tests__/integration/auth.test.ts +43 -0
  66. package/src/__tests__/integration/automation-rules.test.ts +68 -0
  67. package/src/__tests__/integration/buildTestPayload.ts +52 -0
  68. package/src/__tests__/integration/channels.test.ts +52 -0
  69. package/src/__tests__/integration/core-behaviors.test.ts +58 -0
  70. package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
  71. package/src/__tests__/integration/digest.test.ts +48 -0
  72. package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
  73. package/src/__tests__/integration/isolation.test.ts +81 -0
  74. package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
  75. package/src/__tests__/integration/nps.test.ts +37 -0
  76. package/src/__tests__/integration/sanitization.test.ts +35 -0
  77. package/src/__tests__/integration/sla-pause.test.ts +39 -0
  78. package/src/__tests__/integration/smoke.test.ts +26 -0
  79. package/src/__tests__/integration/snooze.test.ts +68 -0
  80. package/src/__tests__/integration/teams.test.ts +38 -0
  81. package/src/__tests__/rateLimiter.test.ts +89 -0
  82. package/src/__tests__/sanitizeHtml.test.ts +165 -0
  83. package/src/__tests__/sla.test.ts +258 -0
  84. package/src/collections/AutomationRules.ts +66 -0
  85. package/src/collections/NotificationQueue.ts +30 -0
  86. package/src/collections/SatisfactionSurveys.ts +11 -2
  87. package/src/collections/SupportClients.ts +58 -1
  88. package/src/collections/SupportTeam.ts +27 -0
  89. package/src/collections/TicketActivityLog.ts +9 -4
  90. package/src/collections/TicketMessages.ts +137 -31
  91. package/src/collections/Tickets.ts +31 -72
  92. package/src/collections/index.ts +3 -0
  93. package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
  94. package/src/components/TicketConversation/SkeletonText.tsx +23 -0
  95. package/src/components/TicketConversation/components/ActionPanels.tsx +82 -1
  96. package/src/components/TicketConversation/components/QuickActions.tsx +25 -0
  97. package/src/components/TicketConversation/constants.ts +29 -0
  98. package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
  99. package/src/components/TicketConversation/hooks/useTicketActions.ts +51 -0
  100. package/src/components/TicketConversation/index.tsx +29 -157
  101. package/src/endpoints/admin-chat-stream.ts +3 -4
  102. package/src/endpoints/admin-chat.ts +11 -20
  103. package/src/endpoints/admin-stats.ts +70 -32
  104. package/src/endpoints/ai-agent.ts +36 -0
  105. package/src/endpoints/apply-macro.ts +8 -14
  106. package/src/endpoints/auth-2fa.ts +12 -12
  107. package/src/endpoints/auto-close.ts +38 -18
  108. package/src/endpoints/billing.ts +17 -4
  109. package/src/endpoints/bulk-action.ts +7 -12
  110. package/src/endpoints/channels.ts +89 -0
  111. package/src/endpoints/chat-stream.ts +3 -4
  112. package/src/endpoints/chat.ts +7 -12
  113. package/src/endpoints/chatbot.ts +2 -2
  114. package/src/endpoints/client-intelligence.ts +4 -4
  115. package/src/endpoints/delete-account.ts +9 -16
  116. package/src/endpoints/email-stats.ts +2 -2
  117. package/src/endpoints/escalate.ts +2 -2
  118. package/src/endpoints/export-csv.ts +2 -2
  119. package/src/endpoints/export-data.ts +5 -8
  120. package/src/endpoints/import-conversation.ts +6 -10
  121. package/src/endpoints/index.ts +24 -2
  122. package/src/endpoints/invite-collaborator.ts +30 -15
  123. package/src/endpoints/invoice.ts +127 -0
  124. package/src/endpoints/kb-search.ts +2 -2
  125. package/src/endpoints/login.ts +12 -4
  126. package/src/endpoints/merge-clients.ts +14 -19
  127. package/src/endpoints/merge-tickets.ts +11 -18
  128. package/src/endpoints/oauth-google.ts +86 -39
  129. package/src/endpoints/pending-emails-process.ts +15 -25
  130. package/src/endpoints/process-digests.ts +93 -0
  131. package/src/endpoints/process-scheduled.ts +6 -10
  132. package/src/endpoints/process-snooze.ts +78 -0
  133. package/src/endpoints/purge-logs.ts +2 -2
  134. package/src/endpoints/resend-notification.ts +3 -4
  135. package/src/endpoints/round-robin-config.ts +3 -4
  136. package/src/endpoints/satisfaction.ts +21 -14
  137. package/src/endpoints/search.ts +5 -8
  138. package/src/endpoints/seed-kb.ts +3 -4
  139. package/src/endpoints/send-reminder.ts +186 -0
  140. package/src/endpoints/settings.ts +5 -2
  141. package/src/endpoints/signature.ts +3 -4
  142. package/src/endpoints/sla-check.ts +2 -2
  143. package/src/endpoints/split-ticket.ts +7 -12
  144. package/src/endpoints/statuses.ts +2 -2
  145. package/src/endpoints/ticket-feedback.ts +4 -6
  146. package/src/endpoints/ticket-synthesis.ts +2 -2
  147. package/src/endpoints/track-open.ts +16 -28
  148. package/src/endpoints/transfer-ticket.ts +21 -9
  149. package/src/endpoints/user-prefs.ts +3 -4
  150. package/src/hooks/automationRules.ts +84 -0
  151. package/src/hooks/checkSLA.ts +48 -16
  152. package/src/plugin.ts +6 -0
  153. package/src/portal/LiveChat.tsx +2 -63
  154. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
  155. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
  156. package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
  157. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
  158. package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
  159. package/src/portal/auth/tickets/detail/page.tsx +2 -2
  160. package/src/portal/helpers.ts +46 -0
  161. package/src/portal/login/page.tsx +3 -8
  162. package/src/portal/types.ts +15 -0
  163. package/src/types/lucide-react.d.ts +34 -0
  164. package/src/types/payload-config.d.ts +10 -0
  165. package/src/utils/adminNotification.ts +2 -2
  166. package/src/utils/aiAgent.ts +147 -0
  167. package/src/utils/auth.ts +5 -4
  168. package/src/utils/channels.ts +36 -0
  169. package/src/utils/db.ts +73 -0
  170. package/src/utils/emailTemplate.ts +8 -1
  171. package/src/utils/fireWebhooks.ts +4 -6
  172. package/src/utils/generateTicketSynthesis.ts +7 -12
  173. package/src/utils/invoicePdf.ts +88 -0
  174. package/src/utils/notificationQueue.ts +36 -0
  175. package/src/utils/readSettings.ts +22 -6
  176. package/src/utils/sanitizeHtml.ts +57 -0
  177. package/src/utils/slugs.ts +6 -0
  178. package/src/utils/teamAccess.ts +27 -0
  179. package/src/utils/ticketAccess.ts +53 -0
  180. package/src/utils/webhookDispatcher.ts +4 -6
  181. package/src/views/BillingView/client.tsx +49 -37
  182. package/src/views/ChatView/client.tsx +4 -3
  183. package/src/views/CrmView/client.tsx +7 -7
  184. package/src/views/EmailTrackingView/client.tsx +7 -6
  185. package/src/views/ImportConversationView/client.tsx +4 -4
  186. package/src/views/LogsView/client.tsx +3 -2
  187. package/src/views/NewTicketView/client.tsx +1 -1
  188. package/src/views/PendingEmailsView/client.tsx +7 -6
  189. package/src/views/SupportDashboardView/client.tsx +21 -21
  190. package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
  191. package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
  192. package/src/views/TicketDetailView/client.tsx +54 -179
  193. package/src/views/TicketDetailView/constants.ts +23 -0
  194. package/src/views/TicketDetailView/helpers.ts +17 -0
  195. package/src/views/TicketDetailView/types.ts +10 -0
  196. package/src/views/TicketInboxView/client.tsx +16 -10
  197. package/src/views/TicketingSettingsView/client.tsx +90 -87
  198. package/src/views/TimeDashboardView/client.tsx +2 -2
  199. package/src/views/shared/StatusPill.tsx +24 -12
  200. package/src/views/shared/dateLocale.ts +5 -0
  201. package/src/views/shared/locales/en.json +708 -0
  202. package/src/views/shared/locales/fr.json +708 -0
@@ -71,14 +71,14 @@ function formatDuration(minutes: number): string {
71
71
  return `${h}h${m}m`
72
72
  }
73
73
 
74
- function timeAgo(dateStr: string): string {
74
+ function timeAgo(dateStr: string, tFn: (k: string, vars?: Record<string, string | number>) => string): string {
75
75
  const diff = Date.now() - new Date(dateStr).getTime()
76
76
  const days = Math.floor(diff / 86400000)
77
- if (days === 0) return "Aujourd'hui"
78
- if (days === 1) return 'Hier'
79
- if (days < 30) return `Il y a ${days}j`
77
+ if (days === 0) return tFn('crm.timeAgo.today')
78
+ if (days === 1) return tFn('crm.timeAgo.yesterday')
79
+ if (days < 30) return tFn('crm.timeAgo.daysAgo', { count: days })
80
80
  const months = Math.floor(days / 30)
81
- return `Il y a ${months} mois`
81
+ return tFn('crm.timeAgo.monthsAgo', { count: months })
82
82
  }
83
83
 
84
84
  function getStatColorClass(key: string, detail: ClientDetail['stats']): string {
@@ -416,7 +416,7 @@ export const CrmClient: React.FC = () => {
416
416
  { key: 'open', label: t('crm.stats.openTickets'), value: String(detail.stats.openTickets) },
417
417
  { key: 'resolved', label: t('crm.stats.resolvedTickets'), value: String(detail.stats.resolvedTickets) },
418
418
  { key: 'time', label: t('crm.stats.timeSpent'), value: formatDuration(detail.stats.totalTimeMinutes) },
419
- { key: 'activity', label: t('crm.stats.lastActivity'), value: detail.stats.lastActivity ? timeAgo(detail.stats.lastActivity) : '-' },
419
+ { key: 'activity', label: t('crm.stats.lastActivity'), value: detail.stats.lastActivity ? timeAgo(detail.stats.lastActivity, t) : '-' },
420
420
  ].map((stat) => (
421
421
  <div key={stat.key} className={styles.statCard}>
422
422
  <div className={styles.statLabel}>{stat.label}</div>
@@ -554,7 +554,7 @@ export const CrmClient: React.FC = () => {
554
554
  </span>
555
555
  </td>
556
556
  <td className={styles.ticketDate}>
557
- {timeAgo(tk.createdAt)}
557
+ {timeAgo(tk.createdAt, t)}
558
558
  </td>
559
559
  </tr>
560
560
  ))}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { useState, useEffect, useCallback } from 'react'
4
4
  import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
5
+ import { DATE_LOCALE } from '../shared/dateLocale'
5
6
  import s from '../../styles/EmailTracking.module.scss'
6
7
 
7
8
  interface EmailLog {
@@ -30,14 +31,14 @@ interface Stats {
30
31
  type StatusTab = 'all' | 'success' | 'error' | 'ignored'
31
32
  type DateRange = 7 | 30 | 90
32
33
 
33
- const STATUS_CFG: Record<string, { label: string; bg: string; color: string }> = {
34
- success: { label: 'Succès', bg: '#dcfce7', color: '#16a34a' },
35
- error: { label: 'Erreur', bg: '#fef2f2', color: '#dc2626' },
36
- ignored: { label: 'Ignoré', bg: '#f3f4f6', color: '#6b7280' },
34
+ const STATUS_CFG: Record<string, { labelKey: string; bg: string; color: string }> = {
35
+ success: { labelKey: 'logs.statusSuccess', bg: '#dcfce7', color: '#16a34a' },
36
+ error: { labelKey: 'emailTracking.errorLabel', bg: '#fef2f2', color: '#dc2626' },
37
+ ignored: { labelKey: 'logs.statusIgnored', bg: '#f3f4f6', color: '#6b7280' },
37
38
  }
38
39
 
39
40
  function fmtDate(d: string): string {
40
- return new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
41
+ return new Date(d).toLocaleDateString(DATE_LOCALE, { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
41
42
  }
42
43
 
43
44
  export const EmailTrackingClient: React.FC = () => {
@@ -129,7 +130,7 @@ export const EmailTrackingClient: React.FC = () => {
129
130
  <td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.recipientEmail}>{log.recipientEmail || '—'}</td>
130
131
  <td style={{ maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.subject}>{log.subject || '—'}</td>
131
132
  <td>{log.ticketNumber ? <span className={s.ticketLink}>{log.ticketNumber}</span> : '—'}</td>
132
- <td><span className={s.statusBadge} style={{ background: STATUS_CFG[log.status]?.bg, color: STATUS_CFG[log.status]?.color }}>{STATUS_CFG[log.status]?.label || log.status}</span></td>
133
+ <td><span className={s.statusBadge} style={{ background: STATUS_CFG[log.status]?.bg, color: STATUS_CFG[log.status]?.color }}>{STATUS_CFG[log.status]?.labelKey ? t(STATUS_CFG[log.status].labelKey) : log.status}</span></td>
133
134
  <td style={{ fontSize: 12 }}>{log.action || '—'}</td>
134
135
  <td style={{ textAlign: 'right' }}>
135
136
  {log.processingTimeMs != null ? (
@@ -96,13 +96,13 @@ export function ImportConversationClient() {
96
96
 
97
97
  const data = await res.json()
98
98
  if (!res.ok) {
99
- setError(data.error || 'Erreur lors de l\'analyse')
99
+ setError(data.error || t('import.analyzeError'))
100
100
  return
101
101
  }
102
102
 
103
103
  setPreview(data)
104
104
  } catch {
105
- setError('Erreur reseau')
105
+ setError(t('import.networkError'))
106
106
  } finally {
107
107
  setLoading(false)
108
108
  }
@@ -122,14 +122,14 @@ export function ImportConversationClient() {
122
122
 
123
123
  const data = await res.json()
124
124
  if (!res.ok) {
125
- setError(data.error || 'Erreur lors de l\'import')
125
+ setError(data.error || t('import.importError'))
126
126
  return
127
127
  }
128
128
 
129
129
  setResult(data)
130
130
  setPreview(null)
131
131
  } catch {
132
- setError('Erreur reseau')
132
+ setError(t('import.networkError'))
133
133
  } finally {
134
134
  setLoading(false)
135
135
  }
@@ -3,6 +3,7 @@
3
3
  import React, { useState, useEffect, useCallback } from 'react'
4
4
  import { useSearchParams } from 'next/navigation'
5
5
  import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
6
+ import { DATE_LOCALE } from '../shared/dateLocale'
6
7
  import s from '../../styles/Logs.module.scss'
7
8
 
8
9
  type LogType = 'email' | 'auth'
@@ -26,7 +27,7 @@ interface LogEntry {
26
27
  }
27
28
 
28
29
  function fmtDate(d: string): string {
29
- return new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
30
+ return new Date(d).toLocaleDateString(DATE_LOCALE, { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
30
31
  }
31
32
 
32
33
  export const LogsClient: React.FC = () => {
@@ -83,7 +84,7 @@ export const LogsClient: React.FC = () => {
83
84
  <div className={s.headerActions}>
84
85
  <button className={s.purgeBtn} onClick={() => handlePurge(7)}>{t('logs.purge7')}</button>
85
86
  <button className={s.purgeBtn} onClick={() => handlePurge(30)}>{t('logs.purge30')}</button>
86
- <button className={s.purgeBtn} onClick={() => handlePurge(90)}>Purger +90j</button>
87
+ <button className={s.purgeBtn} onClick={() => handlePurge(90)}>{t('logs.purge90')}</button>
87
88
  <button className={`${s.purgeBtn} ${s.purgeBtnDanger}`} onClick={() => handlePurge(0)}>{t('logs.purgeAll')}</button>
88
89
  </div>
89
90
  </div>
@@ -190,7 +190,7 @@ export const NewTicketClient: React.FC = () => {
190
190
  <div className={s.fieldGroup}>
191
191
  <label className={s.label}>{t('newTicket.projectLabel')}</label>
192
192
  <select className={s.select} value={projectId || ''} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : null)}>
193
- <option value="">— Aucun —</option>
193
+ <option value="">{t('newTicket.noProject')}</option>
194
194
  {projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
195
195
  </select>
196
196
  </div>
@@ -32,15 +32,15 @@ interface PendingEmail {
32
32
 
33
33
  type Tab = 'pending' | 'processed' | 'ignored'
34
34
 
35
- function timeAgo(dateStr: string): string {
35
+ function timeAgo(dateStr: string, tFn: (k: string, vars?: Record<string, string | number>) => string): string {
36
36
  const diff = Date.now() - new Date(dateStr).getTime()
37
37
  const mins = Math.floor(diff / 60000)
38
- if (mins < 1) return "à l'instant"
39
- if (mins < 60) return `il y a ${mins}min`
38
+ if (mins < 1) return tFn('pendingEmails.timeAgo.justNow')
39
+ if (mins < 60) return tFn('pendingEmails.timeAgo.minutesAgo', { count: mins })
40
40
  const hours = Math.floor(mins / 60)
41
- if (hours < 24) return `il y a ${hours}h`
41
+ if (hours < 24) return tFn('pendingEmails.timeAgo.hoursAgo', { count: hours })
42
42
  const days = Math.floor(hours / 24)
43
- return `il y a ${days}j`
43
+ return tFn('pendingEmails.timeAgo.daysAgo', { count: days })
44
44
  }
45
45
 
46
46
  function TicketSearchModal({
@@ -333,6 +333,7 @@ function EmailCard({
333
333
  onProcess: (action: 'create_ticket' | 'add_to_ticket' | 'ignore', ticketId?: number, clientId?: number) => void
334
334
  processing: boolean
335
335
  }) {
336
+ const { t } = useTranslation()
336
337
  const [expanded, setExpanded] = useState(false)
337
338
  const [showLinkModal, setShowLinkModal] = useState(false)
338
339
  const [showClientPicker, setShowClientPicker] = useState(false)
@@ -364,7 +365,7 @@ function EmailCard({
364
365
  </div>
365
366
  <div className={styles.cardSubject}>{email.subject}</div>
366
367
  <div className={styles.cardMeta}>
367
- <span>{timeAgo(email.createdAt)}</span>
368
+ <span>{timeAgo(email.createdAt, t)}</span>
368
369
  {attachmentCount > 0 && (
369
370
  <span className={styles.attachment}>
370
371
  <Paperclip size={12} /> {attachmentCount} PJ
@@ -3,6 +3,7 @@
3
3
  import React, { useState, useEffect, useCallback, useMemo } from 'react'
4
4
  import Link from 'next/link'
5
5
  import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
6
+ import { DATE_LOCALE } from '../shared/dateLocale'
6
7
  import styles from '../../styles/SupportDashboard.module.scss'
7
8
 
8
9
  // ─── Types ──────────────────────────────────────────────────
@@ -14,6 +15,7 @@ interface Stats {
14
15
  byCategory: Record<string, number>
15
16
  createdLast7Days: number
16
17
  createdLast30Days: number
18
+ volumeByDay?: Array<{ date: string; count: number }>
17
19
  avgResponseTimeHours: number | null
18
20
  avgResolutionTimeHours: number | null
19
21
  totalTimeMinutes: number
@@ -92,6 +94,7 @@ function StatCard({ label, value, trend, accentColor, onClick }: {
92
94
  accentColor?: string
93
95
  onClick?: () => void
94
96
  }) {
97
+ const { t } = useTranslation()
95
98
  const style = accentColor ? { '--stat-accent': accentColor } as React.CSSProperties : undefined
96
99
  const Tag = onClick ? 'button' : 'div'
97
100
 
@@ -114,7 +117,7 @@ function StatCard({ label, value, trend, accentColor, onClick }: {
114
117
  )}
115
118
  {trend && trend.dir === 'neutral' && (
116
119
  <div className={`${styles.statTrend} ${styles.neutral}`}>
117
- stable
120
+ {t('dashboard.stable')}
118
121
  </div>
119
122
  )}
120
123
  </Tag>
@@ -144,6 +147,7 @@ function VolumeChart({ data }: { data: { label: string; value: number }[] }) {
144
147
  }
145
148
 
146
149
  function CSATRing({ score, count }: { score: number; count: number }) {
150
+ const { t } = useTranslation()
147
151
  // Ring progress: score is out of 5
148
152
  const pct = score > 0 ? (score / 5) * 100 : 0
149
153
  const radius = 42
@@ -171,10 +175,10 @@ function CSATRing({ score, count }: { score: number; count: number }) {
171
175
  </div>
172
176
  <div className={styles.csatMeta}>
173
177
  <div className={styles.csatLabel}>
174
- {score > 0 ? `${score.toFixed(1)} / 5` : 'Pas de données'}
178
+ {score > 0 ? `${score.toFixed(1)} / 5` : t('dashboard.csat.noData')}
175
179
  </div>
176
180
  <div className={styles.csatSub}>
177
- {count > 0 ? `${count} avis recueillis` : 'Aucun avis'}
181
+ {count > 0 ? t('dashboard.csat.reviews', { count }) : t('dashboard.csat.noReviews')}
178
182
  </div>
179
183
  </div>
180
184
  </div>
@@ -195,6 +199,7 @@ function formatSlaTime(minutes: number): string {
195
199
  }
196
200
 
197
201
  function SlaSection() {
202
+ const { t } = useTranslation()
198
203
  const [sla, setSla] = useState<SlaData | null>(null)
199
204
  const [slaLoading, setSlaLoading] = useState(true)
200
205
 
@@ -216,8 +221,8 @@ function SlaSection() {
216
221
  if (slaLoading) {
217
222
  return (
218
223
  <div className={styles.slaSection}>
219
- <h2 className={styles.slaSectionTitle}>SLA</h2>
220
- <div className={styles.slaLoading}>Chargement...</div>
224
+ <h2 className={styles.slaSectionTitle}>{t('dashboard.slaTitle')}</h2>
225
+ <div className={styles.slaLoading}>{t('dashboard.sla.loading')}</div>
221
226
  </div>
222
227
  )
223
228
  }
@@ -230,7 +235,7 @@ function SlaSection() {
230
235
 
231
236
  return (
232
237
  <div className={styles.slaSection}>
233
- <h2 className={styles.slaSectionTitle}>SLA</h2>
238
+ <h2 className={styles.slaSectionTitle}>{t('dashboard.slaTitle')}</h2>
234
239
  <div className={styles.slaGrid}>
235
240
  {/* Breached */}
236
241
  <div className={`${styles.slaCard} ${styles.slaBreach}`}>
@@ -241,14 +246,14 @@ function SlaSection() {
241
246
  <line x1="12" y1="9" x2="12" y2="13" />
242
247
  <line x1="12" y1="17" x2="12.01" y2="17" />
243
248
  </svg>
244
- En breach
249
+ {t('dashboard.sla.breached')}
245
250
  </h3>
246
251
  <span className={`${styles.slaCount} ${styles.slaCountBreach}`}>
247
252
  {sla.breached.length}
248
253
  </span>
249
254
  </div>
250
255
  {sla.breached.length === 0 ? (
251
- <div className={styles.slaEmpty}>Aucun ticket en breach</div>
256
+ <div className={styles.slaEmpty}>{t('dashboard.sla.noBreached')}</div>
252
257
  ) : (
253
258
  <ul className={styles.slaList}>
254
259
  {sla.breached.map((ticket) => {
@@ -284,14 +289,14 @@ function SlaSection() {
284
289
  <circle cx="12" cy="12" r="10" />
285
290
  <polyline points="12 6 12 12 16 14" />
286
291
  </svg>
287
- À risque
292
+ {t('dashboard.sla.atRisk')}
288
293
  </h3>
289
294
  <span className={`${styles.slaCount} ${styles.slaCountRisk}`}>
290
295
  {sla.atRisk.length}
291
296
  </span>
292
297
  </div>
293
298
  {sla.atRisk.length === 0 ? (
294
- <div className={styles.slaEmpty}>Aucun ticket à risque</div>
299
+ <div className={styles.slaEmpty}>{t('dashboard.sla.noAtRisk')}</div>
295
300
  ) : (
296
301
  <ul className={styles.slaList}>
297
302
  {sla.atRisk.map((ticket) => {
@@ -313,7 +318,7 @@ function SlaSection() {
313
318
  <span className={styles.slaItemSubject}>{ticket.subject}</span>
314
319
  </div>
315
320
  <span className={`${styles.slaItemTime} ${styles.slaTimeRisk}`}>
316
- {formatSlaTime(remainingMin)} restant
321
+ {t('dashboard.sla.remaining', { time: formatSlaTime(remainingMin) })}
317
322
  </span>
318
323
  </li>
319
324
  )
@@ -372,17 +377,12 @@ export const SupportDashboardClient: React.FC = () => {
372
377
  return () => window.removeEventListener('focus', onFocus)
373
378
  }, [fetchData, sessionExpired])
374
379
 
375
- // Generate fake daily volume from available stats (7 synthetic bars)
380
+ // Real daily volume for the last 7 days (from admin-stats.volumeByDay).
376
381
  const volumeData = useMemo(() => {
377
- if (!stats) return []
378
- const avg = stats.createdLast7Days
379
- const days = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']
380
- // Distribute the 7-day total across days with some variation
381
- const base = Math.max(Math.floor(avg / 7), 0)
382
- return days.map((label, i) => ({
383
- label,
384
- // Simple deterministic variation based on index
385
- value: Math.max(base + ((i * 3 + 1) % 5) - 2, 0),
382
+ if (!stats?.volumeByDay?.length) return []
383
+ return stats.volumeByDay.map((d) => ({
384
+ label: new Date(d.date).toLocaleDateString(DATE_LOCALE, { weekday: 'short' }),
385
+ value: d.count,
386
386
  }))
387
387
  }, [stats])
388
388
 
@@ -0,0 +1,30 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import s from '../../styles/TicketDetail.module.scss'
5
+
6
+ const labelMap: Record<string, string> = { now: 'Maintenant', today: "Aujourd'hui", 'this-week': 'Cette semaine' }
7
+
8
+ export function NextActionItem({ action, index, onToggle }: { action: any; index: number; onToggle: (done: boolean) => Promise<void> }) {
9
+ const [pending, setPending] = useState(false)
10
+ const done = !!action.done
11
+ return (
12
+ <li className={s.aiCardNextAction}>
13
+ <button
14
+ type="button"
15
+ className={`${s.aiCardNextCheck} ${done ? s.aiCardNextCheckDone : ''}`}
16
+ onClick={async () => {
17
+ setPending(true)
18
+ try { await onToggle(!done) } finally { setPending(false) }
19
+ }}
20
+ aria-pressed={done}
21
+ aria-label={done ? 'Marquer non fait' : 'Marquer fait'}
22
+ disabled={pending}
23
+ >
24
+ {done && <span aria-hidden>✓</span>}
25
+ </button>
26
+ <span className={`${s.aiCardNextLabel} ${done ? s.aiCardNextLabelDone : ''}`}>{action.label}</span>
27
+ <span className={s.aiCardNextWhen}>{labelMap[action.priority] || action.priority}</span>
28
+ </li>
29
+ )
30
+ }
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useRef } from 'react'
4
+ import { REWRITE_STYLES } from './constants'
5
+
6
+ export const RewriteDropdown: React.FC<{
7
+ disabled: boolean
8
+ loading: boolean
9
+ onSelect: (style: string) => void
10
+ toolbarBtnClass?: string
11
+ }> = ({ disabled, loading, onSelect, toolbarBtnClass }) => {
12
+ const [open, setOpen] = useState(false)
13
+ const ref = useRef<HTMLDivElement>(null)
14
+
15
+ useEffect(() => {
16
+ if (!open) return
17
+ const close = (e: MouseEvent) => {
18
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
19
+ }
20
+ document.addEventListener('mousedown', close)
21
+ return () => document.removeEventListener('mousedown', close)
22
+ }, [open])
23
+
24
+ return (
25
+ <div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
26
+ <button
27
+ type="button"
28
+ className={toolbarBtnClass}
29
+ onClick={() => setOpen(!open)}
30
+ disabled={disabled}
31
+ style={{ fontSize: 11, fontWeight: 700, padding: '4px 10px', width: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}
32
+ >
33
+ {loading ? '...' : '✏️ Reformuler'}
34
+ {!loading && <span style={{ fontSize: 9, opacity: 0.6 }}>▼</span>}
35
+ </button>
36
+ {open && !disabled && !loading && (
37
+ <div style={{
38
+ position: 'absolute', bottom: '100%', left: 0, marginBottom: 4,
39
+ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8,
40
+ boxShadow: '0 4px 16px rgba(0,0,0,0.12)', zIndex: 100, minWidth: 180, overflow: 'hidden',
41
+ }}>
42
+ {REWRITE_STYLES.map((style) => (
43
+ <button
44
+ key={style.id}
45
+ type="button"
46
+ onClick={() => { setOpen(false); onSelect(style.id) }}
47
+ style={{
48
+ display: 'flex', flexDirection: 'column', width: '100%', padding: '8px 12px',
49
+ border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'left',
50
+ borderBottom: '1px solid #f3f4f6',
51
+ }}
52
+ onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = '#f9fafb' }}
53
+ onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = 'transparent' }}
54
+ >
55
+ <span style={{ fontSize: 12, fontWeight: 600 }}>{style.label}</span>
56
+ <span style={{ fontSize: 10, color: '#9ca3af' }}>{style.desc}</span>
57
+ </button>
58
+ ))}
59
+ </div>
60
+ )}
61
+ </div>
62
+ )
63
+ }