@consilioweb/payload-support 0.5.1

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 (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +525 -0
  3. package/dist/client.cjs +7 -0
  4. package/dist/client.d.cts +3 -0
  5. package/dist/client.d.ts +3 -0
  6. package/dist/client.js +5 -0
  7. package/dist/index.cjs +7766 -0
  8. package/dist/index.d.cts +384 -0
  9. package/dist/index.d.ts +384 -0
  10. package/dist/index.js +7730 -0
  11. package/dist/views.d.cts +30 -0
  12. package/dist/views.d.ts +30 -0
  13. package/package.json +131 -0
  14. package/src/client.ts +1 -0
  15. package/src/collections/AuthLogs.ts +65 -0
  16. package/src/collections/CannedResponses.ts +69 -0
  17. package/src/collections/ChatMessages.ts +98 -0
  18. package/src/collections/EmailLogs.ts +94 -0
  19. package/src/collections/KnowledgeBase.ts +99 -0
  20. package/src/collections/Macros.ts +98 -0
  21. package/src/collections/PendingEmails.ts +122 -0
  22. package/src/collections/SatisfactionSurveys.ts +98 -0
  23. package/src/collections/SlaPolicies.ts +123 -0
  24. package/src/collections/SupportClients.ts +210 -0
  25. package/src/collections/TicketActivityLog.ts +81 -0
  26. package/src/collections/TicketMessages.ts +364 -0
  27. package/src/collections/TicketStatuses.ts +108 -0
  28. package/src/collections/Tickets.ts +704 -0
  29. package/src/collections/TimeEntries.ts +105 -0
  30. package/src/collections/WebhookEndpoints.ts +96 -0
  31. package/src/collections/index.ts +16 -0
  32. package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
  33. package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
  34. package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
  35. package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
  36. package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
  37. package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
  38. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
  39. package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
  40. package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
  41. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
  42. package/src/components/TicketConversation/config.ts +82 -0
  43. package/src/components/TicketConversation/constants.ts +74 -0
  44. package/src/components/TicketConversation/context.ts +63 -0
  45. package/src/components/TicketConversation/hooks/useAI.ts +180 -0
  46. package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
  47. package/src/components/TicketConversation/hooks/useReply.ts +190 -0
  48. package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
  49. package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
  50. package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
  51. package/src/components/TicketConversation/index.tsx +1110 -0
  52. package/src/components/TicketConversation/locales/en.json +878 -0
  53. package/src/components/TicketConversation/locales/fr.json +878 -0
  54. package/src/components/TicketConversation/types.ts +54 -0
  55. package/src/components/TicketConversation/utils.ts +25 -0
  56. package/src/endpoints/admin-chat-stream.ts +238 -0
  57. package/src/endpoints/admin-chat.ts +263 -0
  58. package/src/endpoints/admin-stats.ts +200 -0
  59. package/src/endpoints/ai.ts +199 -0
  60. package/src/endpoints/apply-macro.ts +144 -0
  61. package/src/endpoints/auth-2fa.ts +163 -0
  62. package/src/endpoints/auto-close.ts +175 -0
  63. package/src/endpoints/billing.ts +167 -0
  64. package/src/endpoints/bulk-action.ts +103 -0
  65. package/src/endpoints/chat-stream.ts +127 -0
  66. package/src/endpoints/chat.ts +188 -0
  67. package/src/endpoints/chatbot.ts +113 -0
  68. package/src/endpoints/delete-account.ts +129 -0
  69. package/src/endpoints/email-stats.ts +109 -0
  70. package/src/endpoints/export-csv.ts +84 -0
  71. package/src/endpoints/export-data.ts +104 -0
  72. package/src/endpoints/import-conversation.ts +307 -0
  73. package/src/endpoints/index.ts +154 -0
  74. package/src/endpoints/login.ts +92 -0
  75. package/src/endpoints/merge-clients.ts +132 -0
  76. package/src/endpoints/merge-tickets.ts +137 -0
  77. package/src/endpoints/oauth-google.ts +179 -0
  78. package/src/endpoints/pending-emails-process.ts +224 -0
  79. package/src/endpoints/presence.ts +104 -0
  80. package/src/endpoints/process-scheduled.ts +144 -0
  81. package/src/endpoints/purge-logs.ts +58 -0
  82. package/src/endpoints/resend-notification.ts +99 -0
  83. package/src/endpoints/round-robin-config.ts +92 -0
  84. package/src/endpoints/satisfaction.ts +93 -0
  85. package/src/endpoints/search.ts +106 -0
  86. package/src/endpoints/seed-kb.ts +153 -0
  87. package/src/endpoints/settings.ts +144 -0
  88. package/src/endpoints/signature.ts +93 -0
  89. package/src/endpoints/sla-check.ts +124 -0
  90. package/src/endpoints/split-ticket.ts +131 -0
  91. package/src/endpoints/statuses.ts +45 -0
  92. package/src/endpoints/track-open.ts +154 -0
  93. package/src/endpoints/typing.ts +101 -0
  94. package/src/endpoints/user-prefs.ts +125 -0
  95. package/src/hooks/checkSLA.ts +414 -0
  96. package/src/hooks/ticketStatusEmail.ts +182 -0
  97. package/src/index.ts +51 -0
  98. package/src/plugin.ts +157 -0
  99. package/src/portal/LiveChat.tsx +1353 -0
  100. package/src/portal/auth/ChatWidget.tsx +350 -0
  101. package/src/portal/auth/ChatbotWidget.tsx +285 -0
  102. package/src/portal/auth/SupportHeader.tsx +409 -0
  103. package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
  104. package/src/portal/auth/dashboard/page.tsx +84 -0
  105. package/src/portal/auth/faq/FAQSearch.tsx +117 -0
  106. package/src/portal/auth/faq/page.tsx +199 -0
  107. package/src/portal/auth/layout.tsx +61 -0
  108. package/src/portal/auth/profile/page.tsx +705 -0
  109. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
  110. package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
  111. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
  112. package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
  113. package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
  114. package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
  115. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
  116. package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
  117. package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
  118. package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
  119. package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
  120. package/src/portal/auth/tickets/detail/page.tsx +738 -0
  121. package/src/portal/auth/tickets/new/page.tsx +515 -0
  122. package/src/portal/forgot-password/page.tsx +114 -0
  123. package/src/portal/layout.tsx +26 -0
  124. package/src/portal/locales/en.json +374 -0
  125. package/src/portal/locales/fr.json +374 -0
  126. package/src/portal/login/page.tsx +351 -0
  127. package/src/portal/page.tsx +162 -0
  128. package/src/portal/register/page.tsx +281 -0
  129. package/src/portal/reset-password/page.tsx +152 -0
  130. package/src/styles/BillingView.module.scss +311 -0
  131. package/src/styles/ChatView.module.scss +438 -0
  132. package/src/styles/CommandPalette.module.scss +160 -0
  133. package/src/styles/CrmView.module.scss +554 -0
  134. package/src/styles/EmailTracking.module.scss +238 -0
  135. package/src/styles/ImportConversation.module.scss +267 -0
  136. package/src/styles/Layout.module.scss +55 -0
  137. package/src/styles/Logs.module.scss +164 -0
  138. package/src/styles/NewTicket.module.scss +143 -0
  139. package/src/styles/PendingEmails.module.scss +629 -0
  140. package/src/styles/SupportDashboard.module.scss +649 -0
  141. package/src/styles/TicketDetail.module.scss +1043 -0
  142. package/src/styles/TicketInbox.module.scss +296 -0
  143. package/src/styles/TicketingSettings.module.scss +358 -0
  144. package/src/styles/TimeDashboard.module.scss +287 -0
  145. package/src/styles/_tokens.scss +78 -0
  146. package/src/styles/theme.css +633 -0
  147. package/src/types.ts +255 -0
  148. package/src/utils/adminNotification.ts +38 -0
  149. package/src/utils/auth.ts +46 -0
  150. package/src/utils/emailTemplate.ts +343 -0
  151. package/src/utils/fireWebhooks.ts +84 -0
  152. package/src/utils/index.ts +22 -0
  153. package/src/utils/rateLimiter.ts +52 -0
  154. package/src/utils/readSettings.ts +67 -0
  155. package/src/utils/slugs.ts +54 -0
  156. package/src/utils/webhookDispatcher.ts +120 -0
  157. package/src/views/BillingView/client.tsx +137 -0
  158. package/src/views/BillingView/index.tsx +33 -0
  159. package/src/views/ChatView/client.tsx +294 -0
  160. package/src/views/ChatView/index.tsx +33 -0
  161. package/src/views/CrmView/client.tsx +206 -0
  162. package/src/views/CrmView/index.tsx +33 -0
  163. package/src/views/EmailTrackingView/client.tsx +124 -0
  164. package/src/views/EmailTrackingView/index.tsx +33 -0
  165. package/src/views/ImportConversationView/client.tsx +133 -0
  166. package/src/views/ImportConversationView/index.tsx +33 -0
  167. package/src/views/LogsView/client.tsx +151 -0
  168. package/src/views/LogsView/index.tsx +30 -0
  169. package/src/views/NewTicketView/client.tsx +227 -0
  170. package/src/views/NewTicketView/index.tsx +30 -0
  171. package/src/views/PendingEmailsView/client.tsx +177 -0
  172. package/src/views/PendingEmailsView/index.tsx +33 -0
  173. package/src/views/SupportDashboardView/client.tsx +424 -0
  174. package/src/views/SupportDashboardView/index.tsx +33 -0
  175. package/src/views/TicketDetailView/client.tsx +775 -0
  176. package/src/views/TicketDetailView/index.tsx +33 -0
  177. package/src/views/TicketInboxView/client.tsx +313 -0
  178. package/src/views/TicketInboxView/index.tsx +30 -0
  179. package/src/views/TicketingSettingsView/client.tsx +866 -0
  180. package/src/views/TicketingSettingsView/index.tsx +33 -0
  181. package/src/views/TimeDashboardView/client.tsx +144 -0
  182. package/src/views/TimeDashboardView/index.tsx +33 -0
  183. package/src/views/shared/AdminViewHeader.tsx +69 -0
  184. package/src/views/shared/ErrorBoundary.tsx +68 -0
  185. package/src/views/shared/Skeleton.tsx +125 -0
  186. package/src/views/shared/adminTokens.ts +37 -0
  187. package/src/views/shared/config.ts +82 -0
  188. package/src/views/shared/index.ts +6 -0
  189. package/src/views.ts +16 -0
@@ -0,0 +1,866 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import { Settings, Mail, Bot, Clock, Timer, Globe, FileSignature } from 'lucide-react'
5
+ import { V, btnStyle } from '../shared/adminTokens'
6
+ import { AdminViewHeader } from '../shared/AdminViewHeader'
7
+ import { getFeatures, saveFeatures, DEFAULT_FEATURES, type TicketingFeatures } from '../TicketConversation/config'
8
+ import ts from './TicketingSettings.module.scss'
9
+
10
+ /* ============================================
11
+ * Types
12
+ * ============================================ */
13
+
14
+ interface FeatureConfig {
15
+ key: keyof TicketingFeatures
16
+ label: string
17
+ description: string
18
+ category: 'core' | 'communication' | 'productivity' | 'advanced'
19
+ }
20
+
21
+ interface EmailSettings {
22
+ fromAddress: string
23
+ fromName: string
24
+ replyToAddress: string
25
+ }
26
+
27
+ interface AISettings {
28
+ provider: 'anthropic' | 'openai' | 'gemini' | 'ollama'
29
+ apiKey: string
30
+ model: string
31
+ enableSentiment: boolean
32
+ enableSynthesis: boolean
33
+ enableSuggestion: boolean
34
+ enableRewrite: boolean
35
+ }
36
+
37
+ interface SLASettings {
38
+ firstResponseMinutes: number
39
+ resolutionMinutes: number
40
+ businessHoursOnly: boolean
41
+ escalationEmail: string
42
+ }
43
+
44
+ interface AutoCloseSettings {
45
+ enabled: boolean
46
+ daysBeforeClose: number
47
+ reminderDaysBefore: number
48
+ }
49
+
50
+ interface LocaleSettings {
51
+ language: 'fr' | 'en'
52
+ }
53
+
54
+ interface AllSettings {
55
+ email: EmailSettings
56
+ ai: AISettings
57
+ sla: SLASettings
58
+ autoClose: AutoCloseSettings
59
+ locale: LocaleSettings
60
+ }
61
+
62
+ /* ============================================
63
+ * Constants
64
+ * ============================================ */
65
+
66
+ const DEFAULT_SETTINGS: AllSettings = {
67
+ email: {
68
+ fromAddress: '',
69
+ fromName: 'Support ConsilioWEB',
70
+ replyToAddress: '',
71
+ },
72
+ ai: {
73
+ provider: 'ollama',
74
+ apiKey: '',
75
+ model: 'qwen2.5:32b',
76
+ enableSentiment: true,
77
+ enableSynthesis: true,
78
+ enableSuggestion: true,
79
+ enableRewrite: true,
80
+ },
81
+ sla: {
82
+ firstResponseMinutes: 120,
83
+ resolutionMinutes: 1440,
84
+ businessHoursOnly: true,
85
+ escalationEmail: '',
86
+ },
87
+ autoClose: {
88
+ enabled: true,
89
+ daysBeforeClose: 7,
90
+ reminderDaysBefore: 2,
91
+ },
92
+ locale: {
93
+ language: 'fr',
94
+ },
95
+ }
96
+
97
+ /** Fetch global settings from the backend API */
98
+ async function fetchSettingsFromAPI(): Promise<AllSettings> {
99
+ try {
100
+ const res = await fetch('/api/support/settings', { credentials: 'include' })
101
+ if (res.ok) {
102
+ const data = await res.json()
103
+ return {
104
+ email: { ...DEFAULT_SETTINGS.email, ...data.email },
105
+ ai: { ...DEFAULT_SETTINGS.ai, apiKey: '', ...data.ai },
106
+ sla: { ...DEFAULT_SETTINGS.sla, ...data.sla },
107
+ autoClose: { ...DEFAULT_SETTINGS.autoClose, ...data.autoClose },
108
+ locale: DEFAULT_SETTINGS.locale, // locale is now per-user
109
+ }
110
+ }
111
+ } catch { /* ignore */ }
112
+ return DEFAULT_SETTINGS
113
+ }
114
+
115
+ /** Save global settings to the backend API */
116
+ async function saveSettingsToAPI(settings: AllSettings): Promise<boolean> {
117
+ try {
118
+ const toSave = {
119
+ email: settings.email,
120
+ ai: { ...settings.ai, apiKey: undefined },
121
+ sla: settings.sla,
122
+ autoClose: settings.autoClose,
123
+ // locale excluded — saved per-user via user-prefs
124
+ }
125
+ const res = await fetch('/api/support/settings', {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ credentials: 'include',
129
+ body: JSON.stringify(toSave),
130
+ })
131
+ return res.ok
132
+ } catch {
133
+ return false
134
+ }
135
+ }
136
+
137
+ /** Fetch per-user preferences (locale + signature) */
138
+ async function fetchUserPrefs(): Promise<{ locale: string; signature: string }> {
139
+ try {
140
+ const res = await fetch('/api/support/user-prefs', { credentials: 'include' })
141
+ if (res.ok) return await res.json()
142
+ } catch { /* ignore */ }
143
+ return { locale: 'fr', signature: '' }
144
+ }
145
+
146
+ /** Save per-user preferences */
147
+ async function saveUserPrefs(prefs: { locale: string; signature: string }): Promise<boolean> {
148
+ try {
149
+ const res = await fetch('/api/support/user-prefs', {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ credentials: 'include',
153
+ body: JSON.stringify(prefs),
154
+ })
155
+ return res.ok
156
+ } catch { return false }
157
+ }
158
+
159
+ /** @deprecated — kept for backward compat, use fetchUserPrefs instead */
160
+ async function fetchSignature(): Promise<string> {
161
+ const prefs = await fetchUserPrefs()
162
+ return prefs.signature
163
+ return ''
164
+ }
165
+
166
+ /** Save email signature to backend */
167
+ async function saveSignatureToAPI(signature: string): Promise<boolean> {
168
+ try {
169
+ const res = await fetch('/api/support/signature', {
170
+ method: 'POST',
171
+ headers: { 'Content-Type': 'application/json' },
172
+ credentials: 'include',
173
+ body: JSON.stringify({ signature }),
174
+ })
175
+ return res.ok
176
+ } catch {
177
+ return false
178
+ }
179
+ }
180
+
181
+ const FEATURE_LIST: FeatureConfig[] = [
182
+ // Core
183
+ { key: 'canned', label: 'Réponses rapides', description: 'Templates de réponses pré-enregistrées avec variables dynamiques', category: 'core' },
184
+ { key: 'scheduledReplies', label: 'Réponses programmées', description: 'Envoyer une réponse à une date/heure future', category: 'core' },
185
+ { key: 'activityLog', label: 'Journal d\'activité', description: 'Timeline des actions sur chaque ticket (changements de statut, assignation...)', category: 'core' },
186
+ // Communication
187
+ { key: 'emailTracking', label: 'Suivi des emails', description: 'Tracking d\'envoi et d\'ouverture des notifications email', category: 'communication' },
188
+ { key: 'chat', label: 'Live Chat', description: 'Chat en temps réel avec conversion en ticket', category: 'communication' },
189
+ { key: 'externalMessages', label: 'Messages externes', description: 'Ajouter manuellement des messages reçus par email, SMS, WhatsApp...', category: 'communication' },
190
+ // Productivity
191
+ { key: 'ai', label: 'Intelligence Artificielle', description: 'Analyse de sentiment, synthèse, suggestion de réponse, reformulation', category: 'productivity' },
192
+ { key: 'timeTracking', label: 'Suivi du temps', description: 'Timer, entrées manuelles, facturation', category: 'productivity' },
193
+ { key: 'satisfaction', label: 'Enquêtes satisfaction', description: 'Score CSAT après résolution du ticket', category: 'productivity' },
194
+ // Advanced
195
+ { key: 'merge', label: 'Fusion de tickets', description: 'Combiner deux tickets en un seul', category: 'advanced' },
196
+ { key: 'splitTicket', label: 'Extraction de message', description: 'Extraire un message en nouveau ticket lié', category: 'advanced' },
197
+ { key: 'snooze', label: 'Snooze', description: 'Masquer temporairement un ticket et rappel automatique', category: 'advanced' },
198
+ { key: 'clientHistory', label: 'Historique client', description: 'Tickets passés, projets et notes internes du client', category: 'advanced' },
199
+ ]
200
+
201
+ const CATEGORIES = [
202
+ { key: 'core', label: 'Fonctionnalités de base', color: V.blue },
203
+ { key: 'communication', label: 'Communication', color: V.green },
204
+ { key: 'productivity', label: 'Productivité', color: V.amber },
205
+ { key: 'advanced', label: 'Avancé', color: '#7c3aed' },
206
+ ]
207
+
208
+ /* ============================================
209
+ * Small reusable components
210
+ * ============================================ */
211
+
212
+ const Toggle: React.FC<{
213
+ checked: boolean
214
+ onChange: () => void
215
+ color?: string
216
+ size?: 'sm' | 'md'
217
+ }> = ({ checked, onChange, color = V.blue, size = 'md' }) => {
218
+ const w = size === 'sm' ? 36 : 40
219
+ const h = size === 'sm' ? 20 : 22
220
+ const knob = size === 'sm' ? 16 : 18
221
+ return (
222
+ <div
223
+ role="switch"
224
+ aria-checked={checked}
225
+ tabIndex={0}
226
+ onClick={onChange}
227
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onChange() } }}
228
+ style={{
229
+ width: w, height: h, borderRadius: h / 2, flexShrink: 0,
230
+ backgroundColor: checked ? color : 'var(--theme-elevation-300)',
231
+ position: 'relative', transition: 'background 150ms',
232
+ cursor: 'pointer',
233
+ }}
234
+ >
235
+ <div style={{
236
+ width: knob, height: knob, borderRadius: '50%', backgroundColor: '#fff',
237
+ position: 'absolute', top: (h - knob) / 2,
238
+ left: checked ? w - knob - (h - knob) / 2 : (h - knob) / 2,
239
+ transition: 'left 150ms',
240
+ boxShadow: '0 1px 3px rgba(0,0,0,0.15)',
241
+ }} />
242
+ </div>
243
+ )
244
+ }
245
+
246
+ const CollapsibleSection: React.FC<{
247
+ title: string
248
+ icon: React.ReactNode
249
+ color: string
250
+ defaultOpen?: boolean
251
+ children: React.ReactNode
252
+ badge?: React.ReactNode
253
+ }> = ({ title, icon, color, defaultOpen = true, children, badge }) => {
254
+ const [open, setOpen] = useState(defaultOpen)
255
+ return (
256
+ <div className={ts.sectionWrapper}>
257
+ <div
258
+ className={ts.sectionHeader}
259
+ onClick={() => setOpen(!open)}
260
+ role="button"
261
+ tabIndex={0}
262
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open) } }}
263
+ aria-expanded={open}
264
+ >
265
+ <div className={ts.sectionIcon} style={{ backgroundColor: color }}>{icon}</div>
266
+ <span className={ts.sectionTitle}>{title}</span>
267
+ {badge}
268
+ <span className={`${ts.sectionChevron} ${open ? ts.open : ''}`}>
269
+ &#9660;
270
+ </span>
271
+ </div>
272
+ {open && <div className={ts.sectionBody}>{children}</div>}
273
+ </div>
274
+ )
275
+ }
276
+
277
+ const FieldRow: React.FC<{
278
+ label: string
279
+ description?: string
280
+ children: React.ReactNode
281
+ }> = ({ label, description, children }) => (
282
+ <div className={ts.fieldRow}>
283
+ <div className={ts.fieldLabel}>
284
+ {label}
285
+ {description && <div className={ts.fieldDescription}>{description}</div>}
286
+ </div>
287
+ <div className={ts.fieldContent}>{children}</div>
288
+ </div>
289
+ )
290
+
291
+ /* ============================================
292
+ * Main Component
293
+ * ============================================ */
294
+
295
+ export const TicketingSettingsClient: React.FC = () => {
296
+ const [features, setFeatures] = useState<TicketingFeatures>(() => getFeatures())
297
+ const [settings, setSettings] = useState<AllSettings>(DEFAULT_SETTINGS)
298
+ const [signature, setSignature] = useState('')
299
+ const [saved, setSaved] = useState(false)
300
+ const [saving, setSaving] = useState(false)
301
+ const [showApiKey, setShowApiKey] = useState(false)
302
+ const [loadingSettings, setLoadingSettings] = useState(true)
303
+
304
+ // Load settings + signature from backend on mount
305
+ useEffect(() => {
306
+ let cancelled = false
307
+ Promise.all([fetchSettingsFromAPI(), fetchUserPrefs()]).then(([s, prefs]) => {
308
+ if (!cancelled) {
309
+ setSettings({ ...s, locale: { language: (prefs.locale as 'fr' | 'en') || 'fr' } })
310
+ setSignature(prefs.signature || '')
311
+ setLoadingSettings(false)
312
+ }
313
+ })
314
+ return () => { cancelled = true }
315
+ }, [])
316
+
317
+ const handleToggle = (key: keyof TicketingFeatures) => {
318
+ const updated = { ...features, [key]: !features[key] }
319
+ setFeatures(updated)
320
+ setSaved(false)
321
+ }
322
+
323
+ const updateEmail = (field: keyof EmailSettings, value: string) => {
324
+ setSettings((prev) => ({ ...prev, email: { ...prev.email, [field]: value } }))
325
+ setSaved(false)
326
+ }
327
+
328
+ const updateAI = <K extends keyof AISettings>(field: K, value: AISettings[K]) => {
329
+ setSettings((prev) => ({ ...prev, ai: { ...prev.ai, [field]: value } }))
330
+ setSaved(false)
331
+ }
332
+
333
+ const updateSLA = <K extends keyof SLASettings>(field: K, value: SLASettings[K]) => {
334
+ setSettings((prev) => ({ ...prev, sla: { ...prev.sla, [field]: value } }))
335
+ setSaved(false)
336
+ }
337
+
338
+ const updateAutoClose = <K extends keyof AutoCloseSettings>(field: K, value: AutoCloseSettings[K]) => {
339
+ setSettings((prev) => ({ ...prev, autoClose: { ...prev.autoClose, [field]: value } }))
340
+ setSaved(false)
341
+ }
342
+
343
+ const updateLocale = <K extends keyof LocaleSettings>(field: K, value: LocaleSettings[K]) => {
344
+ setSettings((prev) => ({ ...prev, locale: { ...prev.locale, [field]: value } }))
345
+ setSaved(false)
346
+ }
347
+
348
+ const handleSave = async () => {
349
+ setSaving(true)
350
+ // Save features to localStorage (UI-only flags)
351
+ saveFeatures(features)
352
+ // Save global settings + per-user prefs
353
+ const [settingsOk, prefsOk] = await Promise.all([
354
+ saveSettingsToAPI(settings),
355
+ saveUserPrefs({ locale: settings.locale.language, signature }),
356
+ ])
357
+ setSaving(false)
358
+ if (settingsOk && prefsOk) {
359
+ setSaved(true)
360
+ setTimeout(() => setSaved(false), 3000)
361
+ }
362
+ }
363
+
364
+ const handleReset = () => {
365
+ setFeatures({ ...DEFAULT_FEATURES })
366
+ setSettings({ ...DEFAULT_SETTINGS })
367
+ setSignature('')
368
+ setSaved(false)
369
+ }
370
+
371
+ const enabledCount = Object.entries(features).filter(([k, v]) => typeof v === 'boolean' && v).length
372
+ const totalCount = Object.entries(features).filter(([k, v]) => typeof v === 'boolean').length
373
+
374
+ // Read SMTP info from env (displayed as read-only)
375
+ const smtpHost = process.env.NEXT_PUBLIC_SMTP_HOST || '(non configure)'
376
+ const smtpPort = process.env.NEXT_PUBLIC_SMTP_PORT || '—'
377
+
378
+ return (
379
+ <div className={ts.page}>
380
+ <AdminViewHeader
381
+ icon={<Settings size={24} />}
382
+ title="Configuration du module Support"
383
+ subtitle={`${enabledCount}/${totalCount} fonctionnalites activees`}
384
+ actions={
385
+ <div style={{ display: 'flex', gap: 8 }}>
386
+ <button onClick={handleReset} style={btnStyle('var(--theme-elevation-400)', { small: true })}>
387
+ Reinitialiser
388
+ </button>
389
+ <button
390
+ onClick={handleSave}
391
+ disabled={saving}
392
+ style={btnStyle(saved ? V.green : V.blue, { small: true })}
393
+ >
394
+ {saving ? '...' : saved ? '\u2713 Sauvegarde' : 'Sauvegarder'}
395
+ </button>
396
+ </div>
397
+ }
398
+ />
399
+
400
+ <p className={ts.intro}>
401
+ Configurez le module de support : fonctionnalités, email, IA, SLA et fermeture automatique.
402
+ Les changements prennent effet immédiatement après sauvegarde (rechargez la page du ticket).
403
+ </p>
404
+
405
+ {/* ========================================
406
+ * SECTION 1 — Feature Flags
407
+ * ======================================== */}
408
+ <CollapsibleSection
409
+ title="Fonctionnalités"
410
+ icon={<Settings size={16} />}
411
+ color={V.blue}
412
+ badge={
413
+ <span className={ts.badge} style={{ backgroundColor: '#dbeafe', color: '#1e40af' }}>
414
+ {enabledCount}/{totalCount}
415
+ </span>
416
+ }
417
+ >
418
+ <p className={ts.sectionDescription}>
419
+ Activez ou desactivez les fonctionnalites du module. Les fonctionnalites desactivees sont completement masquees de l&apos;interface.
420
+ </p>
421
+
422
+ {CATEGORIES.map((cat) => {
423
+ const categoryFeatures = FEATURE_LIST.filter((f) => f.category === cat.key)
424
+ return (
425
+ <div key={cat.key} className={ts.categoryGroup}>
426
+ <h3 className={ts.categoryHeading} style={{ color: cat.color }}>
427
+ <span className={ts.categoryDot} style={{ backgroundColor: cat.color }} />
428
+ {cat.label}
429
+ </h3>
430
+ <div className={ts.featureList}>
431
+ {categoryFeatures.map((feat) => {
432
+ const enabled = features[feat.key]
433
+ return (
434
+ <div
435
+ key={feat.key}
436
+ onClick={() => handleToggle(feat.key)}
437
+ className={`${ts.featureCard} ${enabled ? ts.enabled : ''}`}
438
+ role="switch"
439
+ aria-checked={!!enabled}
440
+ tabIndex={0}
441
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleToggle(feat.key) } }}
442
+ >
443
+ <Toggle checked={!!enabled} onChange={() => handleToggle(feat.key)} color={cat.color} />
444
+ <div style={{ flex: 1 }}>
445
+ <div className={ts.featureLabel}>{feat.label}</div>
446
+ <div className={ts.featureDesc}>{feat.description}</div>
447
+ </div>
448
+ </div>
449
+ )
450
+ })}
451
+ </div>
452
+ </div>
453
+ )
454
+ })}
455
+ </CollapsibleSection>
456
+
457
+ {/* ========================================
458
+ * SECTION 2 — Email Configuration
459
+ * ======================================== */}
460
+ <CollapsibleSection
461
+ title="Configuration Email"
462
+ icon={<Mail size={16} />}
463
+ color="#ea580c"
464
+ defaultOpen={false}
465
+ >
466
+ <p className={ts.sectionDescription}>
467
+ Parametres d&apos;envoi des notifications email. L&apos;adresse SMTP est configuree via les variables d&apos;environnement du serveur.
468
+ </p>
469
+
470
+ <FieldRow label="Adresse expediteur" description="Adresse email affichee dans le champ From">
471
+ <input
472
+ type="email"
473
+ value={settings.email.fromAddress}
474
+ onChange={(e) => updateEmail('fromAddress', e.target.value)}
475
+ placeholder="support@example.com"
476
+ className={ts.input}
477
+ />
478
+ </FieldRow>
479
+
480
+ <FieldRow label="Nom expediteur" description="Nom affiche a cote de l'adresse email">
481
+ <input
482
+ type="text"
483
+ value={settings.email.fromName}
484
+ onChange={(e) => updateEmail('fromName', e.target.value)}
485
+ placeholder="Support ConsilioWEB"
486
+ className={ts.input}
487
+ />
488
+ </FieldRow>
489
+
490
+ <FieldRow label="Adresse Reply-To" description="Si differente de l'adresse expediteur">
491
+ <input
492
+ type="email"
493
+ value={settings.email.replyToAddress}
494
+ onChange={(e) => updateEmail('replyToAddress', e.target.value)}
495
+ placeholder="(identique a l'expediteur)"
496
+ className={ts.input}
497
+ />
498
+ </FieldRow>
499
+
500
+ <div className={ts.separator} />
501
+
502
+ <FieldRow label="Serveur SMTP" description="Configure via variables d'environnement">
503
+ <input
504
+ type="text"
505
+ value={smtpHost}
506
+ readOnly
507
+ className={ts.inputReadonly}
508
+ />
509
+ </FieldRow>
510
+
511
+ <FieldRow label="Port SMTP">
512
+ <input
513
+ type="text"
514
+ value={smtpPort}
515
+ readOnly
516
+ className={ts.inputReadonly}
517
+ style={{ maxWidth: 100 }}
518
+ />
519
+ </FieldRow>
520
+ </CollapsibleSection>
521
+
522
+ {/* ========================================
523
+ * SECTION 3 — AI Configuration
524
+ * ======================================== */}
525
+ <CollapsibleSection
526
+ title="Intelligence Artificielle"
527
+ icon={<Bot size={16} />}
528
+ color="#7c3aed"
529
+ defaultOpen={false}
530
+ badge={
531
+ features.ai
532
+ ? <span className={ts.badge} style={{ backgroundColor: '#dcfce7', color: '#166534' }}>Active</span>
533
+ : <span className={ts.badge} style={{ backgroundColor: '#fee2e2', color: '#991b1b' }}>Inactive</span>
534
+ }
535
+ >
536
+ <p className={ts.sectionDescription}>
537
+ Configurez le fournisseur d&apos;IA et activez/désactivez chaque fonctionnalité indépendamment.
538
+ Les fonctionnalités IA nécessitent que le flag &quot;Intelligence Artificielle&quot; soit actif dans la section précédente.
539
+ </p>
540
+
541
+ <FieldRow label="Fournisseur" description="Service d'IA utilise pour l'analyse">
542
+ <select
543
+ value={settings.ai.provider}
544
+ onChange={(e) => updateAI('provider', e.target.value as AISettings['provider'])}
545
+ className={ts.select}
546
+ >
547
+ <option value="ollama">Ollama (local / tunnel)</option>
548
+ <option value="anthropic">Anthropic (Claude)</option>
549
+ <option value="openai">OpenAI (GPT)</option>
550
+ <option value="gemini">Google (Gemini)</option>
551
+ </select>
552
+ </FieldRow>
553
+
554
+ {settings.ai.provider !== 'ollama' && (
555
+ <FieldRow label="Cle API" description="Cle secrete du fournisseur (non stockee en clair)">
556
+ <div className={ts.apiKeyRow}>
557
+ <input
558
+ type={showApiKey ? 'text' : 'password'}
559
+ value={settings.ai.apiKey}
560
+ onChange={(e) => updateAI('apiKey', e.target.value)}
561
+ placeholder="sk-..."
562
+ className={ts.input}
563
+ style={{ flex: 1 }}
564
+ />
565
+ <button
566
+ onClick={() => setShowApiKey(!showApiKey)}
567
+ className={ts.apiKeyToggle}
568
+ >
569
+ {showApiKey ? 'Masquer' : 'Afficher'}
570
+ </button>
571
+ </div>
572
+ </FieldRow>
573
+ )}
574
+
575
+ <FieldRow label="Modele" description="Nom du modele a utiliser">
576
+ <input
577
+ type="text"
578
+ value={settings.ai.model}
579
+ onChange={(e) => updateAI('model', e.target.value)}
580
+ placeholder="qwen2.5:32b"
581
+ className={ts.input}
582
+ />
583
+ </FieldRow>
584
+
585
+ <div className={ts.separator} />
586
+
587
+ <p className={ts.aiSubFeaturesLabel}>
588
+ Fonctionnalités IA individuelles
589
+ </p>
590
+
591
+ {([
592
+ { key: 'enableSentiment' as const, label: 'Analyse de sentiment', desc: 'Detecte le niveau de frustration ou satisfaction du client' },
593
+ { key: 'enableSynthesis' as const, label: 'Synthese automatique', desc: 'Resume les conversations longues en quelques phrases' },
594
+ { key: 'enableSuggestion' as const, label: 'Suggestion de reponse', desc: 'Propose un brouillon de reponse base sur le contexte' },
595
+ { key: 'enableRewrite' as const, label: 'Reformulation', desc: 'Reformule un message pour le rendre plus professionnel' },
596
+ ]).map((item) => (
597
+ <div key={item.key} className={ts.aiToggleRow}>
598
+ <Toggle
599
+ checked={settings.ai[item.key]}
600
+ onChange={() => updateAI(item.key, !settings.ai[item.key])}
601
+ color="#7c3aed"
602
+ size="sm"
603
+ />
604
+ <div style={{ flex: 1 }}>
605
+ <span className={ts.aiToggleLabel}>{item.label}</span>
606
+ <span className={ts.aiToggleDesc}>{item.desc}</span>
607
+ </div>
608
+ </div>
609
+ ))}
610
+ </CollapsibleSection>
611
+
612
+ {/* ========================================
613
+ * SECTION 4 — SLA Configuration
614
+ * ======================================== */}
615
+ <CollapsibleSection
616
+ title="SLA (Accords de niveau de service)"
617
+ icon={<Clock size={16} />}
618
+ color="#0891b2"
619
+ defaultOpen={false}
620
+ >
621
+ <p className={ts.sectionDescription}>
622
+ Definissez les delais de reponse et de resolution attendus. Ces seuils sont utilises pour le suivi de performance et les alertes d&apos;escalade.
623
+ </p>
624
+
625
+ <FieldRow label="Premiere reponse" description="Délai maximum en minutes (défaut : 120 = 2h)">
626
+ <div className={ts.slaInline}>
627
+ <input
628
+ type="number"
629
+ min={1}
630
+ value={settings.sla.firstResponseMinutes}
631
+ onChange={(e) => updateSLA('firstResponseMinutes', parseInt(e.target.value) || 0)}
632
+ className={ts.numberInput}
633
+ />
634
+ <span className={ts.slaHint}>
635
+ minutes ({Math.floor(settings.sla.firstResponseMinutes / 60)}h{String(settings.sla.firstResponseMinutes % 60).padStart(2, '0')})
636
+ </span>
637
+ </div>
638
+ </FieldRow>
639
+
640
+ <FieldRow label="Résolution" description="Délai maximum en minutes (défaut : 1440 = 24h)">
641
+ <div className={ts.slaInline}>
642
+ <input
643
+ type="number"
644
+ min={1}
645
+ value={settings.sla.resolutionMinutes}
646
+ onChange={(e) => updateSLA('resolutionMinutes', parseInt(e.target.value) || 0)}
647
+ className={ts.numberInput}
648
+ />
649
+ <span className={ts.slaHint}>
650
+ minutes ({Math.floor(settings.sla.resolutionMinutes / 60)}h{String(settings.sla.resolutionMinutes % 60).padStart(2, '0')})
651
+ </span>
652
+ </div>
653
+ </FieldRow>
654
+
655
+ <div className={ts.toggleRow}>
656
+ <Toggle
657
+ checked={settings.sla.businessHoursOnly}
658
+ onChange={() => updateSLA('businessHoursOnly', !settings.sla.businessHoursOnly)}
659
+ color="#0891b2"
660
+ size="sm"
661
+ />
662
+ <div>
663
+ <span className={ts.inlineLabel}>Heures ouvrables uniquement</span>
664
+ <div className={ts.inlineDesc}>
665
+ Le decompte SLA est suspendu en dehors des heures de bureau (Lun-Ven, 9h-18h)
666
+ </div>
667
+ </div>
668
+ </div>
669
+
670
+ <div className={ts.separator} />
671
+
672
+ <FieldRow label="Email d'escalade" description="Adresse notifiee en cas de depassement SLA">
673
+ <input
674
+ type="email"
675
+ value={settings.sla.escalationEmail}
676
+ onChange={(e) => updateSLA('escalationEmail', e.target.value)}
677
+ placeholder="admin@example.com"
678
+ className={ts.input}
679
+ />
680
+ </FieldRow>
681
+ </CollapsibleSection>
682
+
683
+ {/* ========================================
684
+ * SECTION 5 — Auto-Close
685
+ * ======================================== */}
686
+ <CollapsibleSection
687
+ title="Fermeture automatique"
688
+ icon={<Timer size={16} />}
689
+ color="#d97706"
690
+ defaultOpen={false}
691
+ badge={
692
+ settings.autoClose.enabled
693
+ ? <span className={ts.badge} style={{ backgroundColor: '#dcfce7', color: '#166534' }}>{settings.autoClose.daysBeforeClose}j</span>
694
+ : <span className={ts.badge} style={{ backgroundColor: '#e5e7eb', color: '#374151' }}>Off</span>
695
+ }
696
+ >
697
+ <p className={ts.sectionDescription}>
698
+ Les tickets en attente client sans reponse seront automatiquement resolus apres le delai configure.
699
+ Un email de rappel est envoye avant la fermeture.
700
+ </p>
701
+
702
+ <div className={ts.toggleRow} style={{ paddingBottom: 14 }}>
703
+ <Toggle
704
+ checked={settings.autoClose.enabled}
705
+ onChange={() => updateAutoClose('enabled', !settings.autoClose.enabled)}
706
+ color="#d97706"
707
+ />
708
+ <span className={ts.inlineLabel}>
709
+ Activer la fermeture automatique
710
+ </span>
711
+ </div>
712
+
713
+ {settings.autoClose.enabled && (
714
+ <>
715
+ <FieldRow label="Delai avant fermeture" description="Nombre de jours sans reponse du client">
716
+ <div className={ts.slaInline}>
717
+ <input
718
+ type="number"
719
+ min={1}
720
+ max={90}
721
+ value={settings.autoClose.daysBeforeClose}
722
+ onChange={(e) => updateAutoClose('daysBeforeClose', parseInt(e.target.value) || 7)}
723
+ className={ts.numberInput}
724
+ />
725
+ <span className={ts.slaHint}>jours</span>
726
+ </div>
727
+ </FieldRow>
728
+
729
+ <FieldRow label="Rappel avant fermeture" description="Email de rappel envoye X jours avant">
730
+ <div className={ts.slaInline}>
731
+ <input
732
+ type="number"
733
+ min={1}
734
+ max={settings.autoClose.daysBeforeClose - 1}
735
+ value={settings.autoClose.reminderDaysBefore}
736
+ onChange={(e) => updateAutoClose('reminderDaysBefore', parseInt(e.target.value) || 2)}
737
+ className={ts.numberInput}
738
+ />
739
+ <span className={ts.slaHint}>
740
+ jours avant (rappel a J-{settings.autoClose.reminderDaysBefore})
741
+ </span>
742
+ </div>
743
+ </FieldRow>
744
+ </>
745
+ )}
746
+ </CollapsibleSection>
747
+
748
+ {/* ────────────────────────────────────────
749
+ * MES PRÉFÉRENCES (per-user)
750
+ * ──────────────────────────────────────── */}
751
+ <div style={{
752
+ marginTop: 32,
753
+ marginBottom: 16,
754
+ padding: '12px 16px',
755
+ borderRadius: 8,
756
+ background: 'linear-gradient(135deg, #dbeafe 0%, #ede9fe 100%)',
757
+ border: '1px solid #c7d2fe',
758
+ }}>
759
+ <div style={{ fontWeight: 700, fontSize: 15, color: '#1e293b' }}>Mes preferences</div>
760
+ <div style={{ fontSize: 13, color: '#64748b', marginTop: 2 }}>
761
+ Ces reglages sont propres a votre compte et ne s&apos;appliquent qu&apos;a vous.
762
+ </div>
763
+ </div>
764
+
765
+ {/* ========================================
766
+ * SECTION 6 — Locale (per-user)
767
+ * ======================================== */}
768
+ <CollapsibleSection
769
+ title="Langue et localisation"
770
+ icon={<Globe size={16} />}
771
+ color="#16a34a"
772
+ defaultOpen={false}
773
+ >
774
+ <p className={ts.sectionDescription}>
775
+ Langue de l&apos;interface du module de support et des notifications email envoyees aux clients.
776
+ </p>
777
+
778
+ <FieldRow label="Langue" description="Langue principale du module">
779
+ <select
780
+ value={settings.locale.language}
781
+ onChange={(e) => updateLocale('language', e.target.value as 'fr' | 'en')}
782
+ className={ts.select}
783
+ >
784
+ <option value="fr">Francais</option>
785
+ <option value="en">English</option>
786
+ </select>
787
+ </FieldRow>
788
+ </CollapsibleSection>
789
+
790
+ {/* ========================================
791
+ * SECTION 7 — Email Signature
792
+ * ======================================== */}
793
+ <CollapsibleSection
794
+ title="Signature email"
795
+ icon={<FileSignature size={16} />}
796
+ color="#6366f1"
797
+ defaultOpen={false}
798
+ >
799
+ <p className={ts.sectionDescription}>
800
+ Signature ajoutee automatiquement en bas de chaque reponse email envoyee au client.
801
+ Supporte le texte brut et le HTML basique.
802
+ </p>
803
+
804
+ <textarea
805
+ value={signature}
806
+ onChange={(e) => { setSignature(e.target.value); setSaved(false) }}
807
+ placeholder="Cordialement,&#10;L'equipe ConsilioWEB"
808
+ rows={6}
809
+ className={ts.signatureTextarea}
810
+ />
811
+ </CollapsibleSection>
812
+
813
+ {/* Purge logs section */}
814
+ <CollapsibleSection title="Purge des logs" icon={<Settings size={18} />} color="#ef4444" defaultOpen={false}>
815
+ <p className={ts.sectionDescription}>
816
+ Supprimez les anciens logs pour libérer de l&apos;espace. Cette action est irréversible.
817
+ </p>
818
+ <div className={ts.purgeGroup}>
819
+ {['email-logs', 'auth-logs'].map((col) => (
820
+ <div key={col} className={ts.purgeCategory}>
821
+ <span className={ts.purgeCategoryLabel}>
822
+ {col === 'email-logs' ? 'Logs Email' : 'Logs Auth'}
823
+ </span>
824
+ <div className={ts.purgeButtons}>
825
+ {[
826
+ { label: '7 jours', days: 7 },
827
+ { label: '30 jours', days: 30 },
828
+ { label: '90 jours', days: 90 },
829
+ { label: 'Tout', days: 0 },
830
+ ].map((opt) => (
831
+ <button
832
+ key={opt.days}
833
+ onClick={async () => {
834
+ if (!window.confirm(`Supprimer les logs ${col} de plus de ${opt.days || 'tous les'} jours ?`)) return
835
+ try {
836
+ const res = await fetch(`/api/support/purge-logs?collection=${col}&days=${opt.days}`, { method: 'DELETE', credentials: 'include' })
837
+ if (res.ok) { const d = await res.json(); alert(`${d.purged} log(s) supprimé(s)`) }
838
+ } catch { alert('Erreur') }
839
+ }}
840
+ style={{ ...btnStyle(opt.days === 0 ? '#ef4444' : 'var(--theme-elevation-500)', { small: true }), fontSize: 11 }}
841
+ >
842
+ {opt.label}
843
+ </button>
844
+ ))}
845
+ </div>
846
+ </div>
847
+ ))}
848
+ </div>
849
+ </CollapsibleSection>
850
+
851
+ {/* Bottom save bar */}
852
+ <div className={ts.bottomBar}>
853
+ <button onClick={handleReset} style={btnStyle('var(--theme-elevation-400)', { small: true })}>
854
+ Réinitialiser tout
855
+ </button>
856
+ <button
857
+ onClick={handleSave}
858
+ disabled={saving}
859
+ style={btnStyle(saved ? V.green : V.blue, { small: true })}
860
+ >
861
+ {saving ? 'Sauvegarde...' : saved ? '\u2713 Sauvegardé' : 'Sauvegarder les modifications'}
862
+ </button>
863
+ </div>
864
+ </div>
865
+ )
866
+ }