@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.
- package/LICENSE +21 -0
- package/README.md +525 -0
- package/dist/client.cjs +7 -0
- package/dist/client.d.cts +3 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/index.cjs +7766 -0
- package/dist/index.d.cts +384 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +7730 -0
- package/dist/views.d.cts +30 -0
- package/dist/views.d.ts +30 -0
- package/package.json +131 -0
- package/src/client.ts +1 -0
- package/src/collections/AuthLogs.ts +65 -0
- package/src/collections/CannedResponses.ts +69 -0
- package/src/collections/ChatMessages.ts +98 -0
- package/src/collections/EmailLogs.ts +94 -0
- package/src/collections/KnowledgeBase.ts +99 -0
- package/src/collections/Macros.ts +98 -0
- package/src/collections/PendingEmails.ts +122 -0
- package/src/collections/SatisfactionSurveys.ts +98 -0
- package/src/collections/SlaPolicies.ts +123 -0
- package/src/collections/SupportClients.ts +210 -0
- package/src/collections/TicketActivityLog.ts +81 -0
- package/src/collections/TicketMessages.ts +364 -0
- package/src/collections/TicketStatuses.ts +108 -0
- package/src/collections/Tickets.ts +704 -0
- package/src/collections/TimeEntries.ts +105 -0
- package/src/collections/WebhookEndpoints.ts +96 -0
- package/src/collections/index.ts +16 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
- package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
- package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
- package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
- package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
- package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
- package/src/components/TicketConversation/config.ts +82 -0
- package/src/components/TicketConversation/constants.ts +74 -0
- package/src/components/TicketConversation/context.ts +63 -0
- package/src/components/TicketConversation/hooks/useAI.ts +180 -0
- package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
- package/src/components/TicketConversation/hooks/useReply.ts +190 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
- package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
- package/src/components/TicketConversation/index.tsx +1110 -0
- package/src/components/TicketConversation/locales/en.json +878 -0
- package/src/components/TicketConversation/locales/fr.json +878 -0
- package/src/components/TicketConversation/types.ts +54 -0
- package/src/components/TicketConversation/utils.ts +25 -0
- package/src/endpoints/admin-chat-stream.ts +238 -0
- package/src/endpoints/admin-chat.ts +263 -0
- package/src/endpoints/admin-stats.ts +200 -0
- package/src/endpoints/ai.ts +199 -0
- package/src/endpoints/apply-macro.ts +144 -0
- package/src/endpoints/auth-2fa.ts +163 -0
- package/src/endpoints/auto-close.ts +175 -0
- package/src/endpoints/billing.ts +167 -0
- package/src/endpoints/bulk-action.ts +103 -0
- package/src/endpoints/chat-stream.ts +127 -0
- package/src/endpoints/chat.ts +188 -0
- package/src/endpoints/chatbot.ts +113 -0
- package/src/endpoints/delete-account.ts +129 -0
- package/src/endpoints/email-stats.ts +109 -0
- package/src/endpoints/export-csv.ts +84 -0
- package/src/endpoints/export-data.ts +104 -0
- package/src/endpoints/import-conversation.ts +307 -0
- package/src/endpoints/index.ts +154 -0
- package/src/endpoints/login.ts +92 -0
- package/src/endpoints/merge-clients.ts +132 -0
- package/src/endpoints/merge-tickets.ts +137 -0
- package/src/endpoints/oauth-google.ts +179 -0
- package/src/endpoints/pending-emails-process.ts +224 -0
- package/src/endpoints/presence.ts +104 -0
- package/src/endpoints/process-scheduled.ts +144 -0
- package/src/endpoints/purge-logs.ts +58 -0
- package/src/endpoints/resend-notification.ts +99 -0
- package/src/endpoints/round-robin-config.ts +92 -0
- package/src/endpoints/satisfaction.ts +93 -0
- package/src/endpoints/search.ts +106 -0
- package/src/endpoints/seed-kb.ts +153 -0
- package/src/endpoints/settings.ts +144 -0
- package/src/endpoints/signature.ts +93 -0
- package/src/endpoints/sla-check.ts +124 -0
- package/src/endpoints/split-ticket.ts +131 -0
- package/src/endpoints/statuses.ts +45 -0
- package/src/endpoints/track-open.ts +154 -0
- package/src/endpoints/typing.ts +101 -0
- package/src/endpoints/user-prefs.ts +125 -0
- package/src/hooks/checkSLA.ts +414 -0
- package/src/hooks/ticketStatusEmail.ts +182 -0
- package/src/index.ts +51 -0
- package/src/plugin.ts +157 -0
- package/src/portal/LiveChat.tsx +1353 -0
- package/src/portal/auth/ChatWidget.tsx +350 -0
- package/src/portal/auth/ChatbotWidget.tsx +285 -0
- package/src/portal/auth/SupportHeader.tsx +409 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
- package/src/portal/auth/dashboard/page.tsx +84 -0
- package/src/portal/auth/faq/FAQSearch.tsx +117 -0
- package/src/portal/auth/faq/page.tsx +199 -0
- package/src/portal/auth/layout.tsx +61 -0
- package/src/portal/auth/profile/page.tsx +705 -0
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
- package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
- package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
- package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
- package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
- package/src/portal/auth/tickets/detail/page.tsx +738 -0
- package/src/portal/auth/tickets/new/page.tsx +515 -0
- package/src/portal/forgot-password/page.tsx +114 -0
- package/src/portal/layout.tsx +26 -0
- package/src/portal/locales/en.json +374 -0
- package/src/portal/locales/fr.json +374 -0
- package/src/portal/login/page.tsx +351 -0
- package/src/portal/page.tsx +162 -0
- package/src/portal/register/page.tsx +281 -0
- package/src/portal/reset-password/page.tsx +152 -0
- package/src/styles/BillingView.module.scss +311 -0
- package/src/styles/ChatView.module.scss +438 -0
- package/src/styles/CommandPalette.module.scss +160 -0
- package/src/styles/CrmView.module.scss +554 -0
- package/src/styles/EmailTracking.module.scss +238 -0
- package/src/styles/ImportConversation.module.scss +267 -0
- package/src/styles/Layout.module.scss +55 -0
- package/src/styles/Logs.module.scss +164 -0
- package/src/styles/NewTicket.module.scss +143 -0
- package/src/styles/PendingEmails.module.scss +629 -0
- package/src/styles/SupportDashboard.module.scss +649 -0
- package/src/styles/TicketDetail.module.scss +1043 -0
- package/src/styles/TicketInbox.module.scss +296 -0
- package/src/styles/TicketingSettings.module.scss +358 -0
- package/src/styles/TimeDashboard.module.scss +287 -0
- package/src/styles/_tokens.scss +78 -0
- package/src/styles/theme.css +633 -0
- package/src/types.ts +255 -0
- package/src/utils/adminNotification.ts +38 -0
- package/src/utils/auth.ts +46 -0
- package/src/utils/emailTemplate.ts +343 -0
- package/src/utils/fireWebhooks.ts +84 -0
- package/src/utils/index.ts +22 -0
- package/src/utils/rateLimiter.ts +52 -0
- package/src/utils/readSettings.ts +67 -0
- package/src/utils/slugs.ts +54 -0
- package/src/utils/webhookDispatcher.ts +120 -0
- package/src/views/BillingView/client.tsx +137 -0
- package/src/views/BillingView/index.tsx +33 -0
- package/src/views/ChatView/client.tsx +294 -0
- package/src/views/ChatView/index.tsx +33 -0
- package/src/views/CrmView/client.tsx +206 -0
- package/src/views/CrmView/index.tsx +33 -0
- package/src/views/EmailTrackingView/client.tsx +124 -0
- package/src/views/EmailTrackingView/index.tsx +33 -0
- package/src/views/ImportConversationView/client.tsx +133 -0
- package/src/views/ImportConversationView/index.tsx +33 -0
- package/src/views/LogsView/client.tsx +151 -0
- package/src/views/LogsView/index.tsx +30 -0
- package/src/views/NewTicketView/client.tsx +227 -0
- package/src/views/NewTicketView/index.tsx +30 -0
- package/src/views/PendingEmailsView/client.tsx +177 -0
- package/src/views/PendingEmailsView/index.tsx +33 -0
- package/src/views/SupportDashboardView/client.tsx +424 -0
- package/src/views/SupportDashboardView/index.tsx +33 -0
- package/src/views/TicketDetailView/client.tsx +775 -0
- package/src/views/TicketDetailView/index.tsx +33 -0
- package/src/views/TicketInboxView/client.tsx +313 -0
- package/src/views/TicketInboxView/index.tsx +30 -0
- package/src/views/TicketingSettingsView/client.tsx +866 -0
- package/src/views/TicketingSettingsView/index.tsx +33 -0
- package/src/views/TimeDashboardView/client.tsx +144 -0
- package/src/views/TimeDashboardView/index.tsx +33 -0
- package/src/views/shared/AdminViewHeader.tsx +69 -0
- package/src/views/shared/ErrorBoundary.tsx +68 -0
- package/src/views/shared/Skeleton.tsx +125 -0
- package/src/views/shared/adminTokens.ts +37 -0
- package/src/views/shared/config.ts +82 -0
- package/src/views/shared/index.ts +6 -0
- 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
|
+
▼
|
|
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'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'envoi des notifications email. L'adresse SMTP est configuree via les variables d'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'IA et activez/désactivez chaque fonctionnalité indépendamment.
|
|
538
|
+
Les fonctionnalités IA nécessitent que le flag "Intelligence Artificielle" 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'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'appliquent qu'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'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, 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'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
|
+
}
|