@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,515 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useRef, useCallback } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+
6
+ const categories = [
7
+ { label: 'Bug / Dysfonctionnement', value: 'bug', icon: '🐛' },
8
+ { label: 'Modification de contenu', value: 'content', icon: '📝' },
9
+ { label: 'Nouvelle fonctionnalité', value: 'feature', icon: '✨' },
10
+ { label: 'Question / Aide', value: 'question', icon: '💬' },
11
+ { label: 'Hébergement / Domaine', value: 'hosting', icon: '🌐' },
12
+ ]
13
+
14
+ const templates: Record<string, { subject: string; message: string }> = {
15
+ bug: {
16
+ subject: '',
17
+ message: `**Description du bug :**\n\n**Étapes pour reproduire :**\n1. \n2. \n3. \n\n**Résultat attendu :**\n\n**Résultat actuel :**\n\n**Navigateur / Appareil :**\n`,
18
+ },
19
+ feature: {
20
+ subject: '',
21
+ message: `**Fonctionnalité souhaitée :**\n\n**Contexte / besoin :**\n\n**Comportement attendu :**\n\n**Priorité souhaitée :**\n`,
22
+ },
23
+ content: {
24
+ subject: '',
25
+ message: `**Page concernée :**\n\n**Modification demandée :**\n\n**Nouveau contenu :**\n`,
26
+ },
27
+ hosting: {
28
+ subject: '',
29
+ message: `**Nom de domaine :**\n\n**Description du problème :**\n\n**Message d'erreur (si applicable) :**\n`,
30
+ },
31
+ }
32
+
33
+ const priorities = [
34
+ { label: 'Basse', value: 'low', color: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300' },
35
+ { label: 'Normale', value: 'normal', color: 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' },
36
+ { label: 'Haute', value: 'high', color: 'bg-amber-50 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400' },
37
+ { label: 'Urgente', value: 'urgent', color: 'bg-red-50 text-red-600 dark:bg-red-900/30 dark:text-red-400' },
38
+ ]
39
+
40
+ interface Project {
41
+ id: number | string
42
+ name: string
43
+ }
44
+
45
+ const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB
46
+ const MAX_FILE_SIZE_LABEL = '5 Mo'
47
+
48
+ export default function NewTicketPage() {
49
+ const router = useRouter()
50
+ const fileInputRef = useRef<HTMLInputElement>(null)
51
+ const [subject, setSubject] = useState('')
52
+ const [category, setCategory] = useState('')
53
+ const [priority, setPriority] = useState('normal')
54
+ const [project, setProject] = useState('')
55
+ const [projects, setProjects] = useState<Project[]>([])
56
+ const [message, setMessage] = useState('')
57
+ const [files, setFiles] = useState<File[]>([])
58
+ const [error, setError] = useState('')
59
+ const [loading, setLoading] = useState(false)
60
+ const [isDragging, setIsDragging] = useState(false)
61
+ const [showAdvanced, setShowAdvanced] = useState(false)
62
+
63
+ useEffect(() => {
64
+ const fetchProjects = async () => {
65
+ try {
66
+ const res = await fetch('/api/projects?where[status][equals]=active&limit=50', {
67
+ credentials: 'include',
68
+ })
69
+ if (res.ok) {
70
+ const data = await res.json()
71
+ setProjects(data.docs || [])
72
+ }
73
+ } catch { /* ignore */ }
74
+ }
75
+ fetchProjects()
76
+ }, [])
77
+
78
+ const addFiles = useCallback((newFiles: File[]) => {
79
+ const tooLarge = newFiles.filter((f) => f.size > MAX_FILE_SIZE)
80
+ if (tooLarge.length > 0) {
81
+ setError(`Fichier(s) trop volumineux (max ${MAX_FILE_SIZE_LABEL}) : ${tooLarge.map((f) => f.name).join(', ')}`)
82
+ }
83
+ setFiles((prev) => [...prev, ...newFiles.filter((f) => f.size <= MAX_FILE_SIZE)])
84
+ }, [])
85
+
86
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
87
+ if (e.target.files) {
88
+ addFiles(Array.from(e.target.files))
89
+ }
90
+ if (fileInputRef.current) fileInputRef.current.value = ''
91
+ }
92
+
93
+ const handleDragOver = useCallback((e: React.DragEvent) => {
94
+ e.preventDefault()
95
+ e.stopPropagation()
96
+ setIsDragging(true)
97
+ }, [])
98
+
99
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
100
+ e.preventDefault()
101
+ e.stopPropagation()
102
+ setIsDragging(false)
103
+ }, [])
104
+
105
+ const handleDrop = useCallback((e: React.DragEvent) => {
106
+ e.preventDefault()
107
+ e.stopPropagation()
108
+ setIsDragging(false)
109
+ if (e.dataTransfer.files?.length) {
110
+ addFiles(Array.from(e.dataTransfer.files))
111
+ }
112
+ }, [addFiles])
113
+
114
+ const removeFile = (index: number) => {
115
+ setFiles((prev) => prev.filter((_, i) => i !== index))
116
+ }
117
+
118
+ const handleSubmit = async (e: React.FormEvent) => {
119
+ e.preventDefault()
120
+ setError('')
121
+
122
+ if (!subject.trim() || !message.trim()) {
123
+ setError('Le sujet et le message sont obligatoires.')
124
+ return
125
+ }
126
+
127
+ setLoading(true)
128
+
129
+ try {
130
+ // Step 1: Upload files if any
131
+ const uploadedMediaIds: number[] = []
132
+ for (const file of files) {
133
+ const formData = new FormData()
134
+ formData.append('file', file)
135
+ formData.append('_payload', JSON.stringify({ alt: file.name }))
136
+
137
+ const uploadRes = await fetch('/api/media', {
138
+ method: 'POST',
139
+ credentials: 'include',
140
+ body: formData,
141
+ })
142
+
143
+ if (uploadRes.ok) {
144
+ const uploadData = await uploadRes.json()
145
+ if (uploadData.doc?.id) {
146
+ uploadedMediaIds.push(uploadData.doc.id)
147
+ }
148
+ }
149
+ }
150
+
151
+ // Step 2: Create the ticket via Payload REST API
152
+ const ticketRes = await fetch('/api/tickets', {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ credentials: 'include',
156
+ body: JSON.stringify({
157
+ subject: subject.trim(),
158
+ category: category || undefined,
159
+ priority,
160
+ project: project || undefined,
161
+ status: 'open',
162
+ }),
163
+ })
164
+
165
+ const ticketData = await ticketRes.json()
166
+
167
+ if (!ticketRes.ok || ticketData.errors) {
168
+ setError(ticketData.errors?.[0]?.message || 'Erreur lors de la création du ticket.')
169
+ return
170
+ }
171
+
172
+ // Step 3: Create the first message with attachments
173
+ const messagePayload: Record<string, unknown> = {
174
+ ticket: ticketData.doc.id,
175
+ body: message.trim(),
176
+ authorType: 'client',
177
+ }
178
+
179
+ if (uploadedMediaIds.length > 0) {
180
+ messagePayload.attachments = uploadedMediaIds.map((id) => ({ file: id }))
181
+ }
182
+
183
+ const messageRes = await fetch('/api/ticket-messages', {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ credentials: 'include',
187
+ body: JSON.stringify(messagePayload),
188
+ })
189
+
190
+ if (!messageRes.ok) {
191
+ setError('Le ticket a été créé mais le message n\'a pas pu être envoyé.')
192
+ return
193
+ }
194
+
195
+ router.push(`/support/tickets/${ticketData.doc.id}`)
196
+ router.refresh()
197
+ } catch {
198
+ setError('Erreur de connexion. Veuillez réessayer.')
199
+ } finally {
200
+ setLoading(false)
201
+ }
202
+ }
203
+
204
+ const formatFileSize = (size: number) => {
205
+ if (size < 1024) return `${size} o`
206
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(0)} Ko`
207
+ return `${(size / (1024 * 1024)).toFixed(1)} Mo`
208
+ }
209
+
210
+ return (
211
+ <div className="mx-auto max-w-3xl px-4 pb-12">
212
+ {/* Back navigation */}
213
+ <button
214
+ onClick={() => router.back()}
215
+ className="group mb-8 inline-flex items-center gap-2 text-sm font-medium text-slate-500 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
216
+ >
217
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 transition-transform group-hover:-translate-x-0.5">
218
+ <path fillRule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" clipRule="evenodd" />
219
+ </svg>
220
+ Retour aux tickets
221
+ </button>
222
+
223
+ {/* Header */}
224
+ <div className="mb-8">
225
+ <div className="mb-3 inline-flex items-center gap-2 rounded-full bg-blue-50 dark:bg-blue-900/20 px-3 py-1">
226
+ <div className="h-1.5 w-1.5 rounded-full bg-blue-500 animate-pulse" />
227
+ <span className="text-xs font-semibold text-blue-600 dark:text-blue-400">Nouveau ticket</span>
228
+ </div>
229
+ <h1 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-3xl">
230
+ Comment pouvons-nous vous aider ?
231
+ </h1>
232
+ <p className="mt-2 text-base text-slate-500 dark:text-slate-400">
233
+ Decrivez votre besoin en detail et nous vous repondrons dans les plus brefs delais.
234
+ </p>
235
+ </div>
236
+
237
+ {/* Form card */}
238
+ <form
239
+ onSubmit={handleSubmit}
240
+ onDragOver={handleDragOver}
241
+ onDragLeave={handleDragLeave}
242
+ onDrop={handleDrop}
243
+ className="space-y-6"
244
+ >
245
+ {/* Error alert */}
246
+ {error && (
247
+ <div className="flex items-start gap-3 rounded-xl border border-red-200 dark:border-red-800/50 bg-red-50 dark:bg-red-900/20 px-4 py-3.5">
248
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="mt-0.5 h-5 w-5 flex-shrink-0 text-red-500">
249
+ <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
250
+ </svg>
251
+ <p className="text-sm font-medium text-red-700 dark:text-red-400">{error}</p>
252
+ </div>
253
+ )}
254
+
255
+ {/* Main card */}
256
+ <div className={`rounded-2xl border bg-white dark:bg-slate-800/50 shadow-sm backdrop-blur-sm transition-all duration-200 ${
257
+ isDragging
258
+ ? 'border-blue-400 dark:border-blue-500 ring-4 ring-blue-100 dark:ring-blue-900/30 shadow-blue-100/50'
259
+ : 'border-slate-200 dark:border-slate-700/50'
260
+ }`}>
261
+ <div className="p-6 sm:p-8">
262
+ {/* Subject */}
263
+ <div className="mb-6">
264
+ <label htmlFor="subject" className="mb-2 block text-sm font-semibold text-slate-700 dark:text-slate-200">
265
+ Sujet <span className="text-red-400">*</span>
266
+ </label>
267
+ <input
268
+ id="subject"
269
+ type="text"
270
+ required
271
+ value={subject}
272
+ onChange={(e) => setSubject(e.target.value)}
273
+ placeholder="Ex: Erreur 404 sur la page contact"
274
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
275
+ />
276
+ </div>
277
+
278
+ {/* Description */}
279
+ <div className="mb-6">
280
+ <label htmlFor="message" className="mb-2 block text-sm font-semibold text-slate-700 dark:text-slate-200">
281
+ Description <span className="text-red-400">*</span>
282
+ </label>
283
+ <textarea
284
+ id="message"
285
+ required
286
+ rows={7}
287
+ value={message}
288
+ onChange={(e) => setMessage(e.target.value)}
289
+ placeholder="Decrivez votre probleme en detail. Plus vous donnez d'informations, plus nous pourrons vous aider rapidement."
290
+ className="w-full resize-y rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm leading-relaxed text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
291
+ />
292
+ </div>
293
+
294
+ {/* File attachments */}
295
+ <div className="mb-6">
296
+ <label className="mb-2 block text-sm font-semibold text-slate-700 dark:text-slate-200">
297
+ Pieces jointes
298
+ </label>
299
+ <input
300
+ ref={fileInputRef}
301
+ type="file"
302
+ multiple
303
+ onChange={handleFileChange}
304
+ className="hidden"
305
+ accept="image/*,.pdf,.doc,.docx,.txt,.zip,.xls,.xlsx,.csv"
306
+ />
307
+
308
+ {/* Drop zone overlay */}
309
+ {isDragging && (
310
+ <div className="mb-4 flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-blue-400 bg-blue-50/80 dark:bg-blue-900/20 p-10">
311
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="mb-2 h-8 w-8 text-blue-500">
312
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
313
+ </svg>
314
+ <p className="text-sm font-semibold text-blue-600 dark:text-blue-400">Deposez vos fichiers ici</p>
315
+ </div>
316
+ )}
317
+
318
+ {/* Upload button */}
319
+ <button
320
+ type="button"
321
+ onClick={() => fileInputRef.current?.click()}
322
+ className="group flex w-full items-center justify-center gap-3 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-600 bg-slate-50/50 dark:bg-slate-900/30 px-6 py-4 transition-all duration-200 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-blue-50/50 dark:hover:bg-blue-900/10"
323
+ >
324
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-slate-100 dark:bg-slate-700 transition-colors group-hover:bg-blue-100 dark:group-hover:bg-blue-900/40">
325
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="h-5 w-5 text-slate-400 group-hover:text-blue-500 transition-colors">
326
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
327
+ </svg>
328
+ </div>
329
+ <div className="text-left">
330
+ <p className="text-sm font-semibold text-slate-600 dark:text-slate-300 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
331
+ Cliquez ou glissez-deposez vos fichiers
332
+ </p>
333
+ <p className="text-xs text-slate-400 dark:text-slate-500">
334
+ Images, PDF, documents — max {MAX_FILE_SIZE_LABEL} par fichier
335
+ </p>
336
+ </div>
337
+ </button>
338
+
339
+ {/* File list */}
340
+ {files.length > 0 && (
341
+ <div className="mt-3 space-y-2">
342
+ {files.map((file, index) => {
343
+ const isImage = file.type.startsWith('image/')
344
+ return (
345
+ <div
346
+ key={index}
347
+ className="flex items-center gap-3 rounded-lg border border-slate-100 dark:border-slate-700 bg-white dark:bg-slate-800 px-3 py-2.5 transition-colors hover:border-slate-200 dark:hover:border-slate-600"
348
+ >
349
+ {isImage ? (
350
+ // eslint-disable-next-line @next/next/no-img-element
351
+ <img
352
+ src={URL.createObjectURL(file)}
353
+ alt={file.name}
354
+ className="h-9 w-9 flex-shrink-0 rounded-lg object-cover"
355
+ />
356
+ ) : (
357
+ <div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-slate-100 dark:bg-slate-700">
358
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 text-slate-400">
359
+ <path d="M3 3.5A1.5 1.5 0 014.5 2h6.879a1.5 1.5 0 011.06.44l4.122 4.12A1.5 1.5 0 0117 7.622V16.5a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 013 16.5v-13z" />
360
+ </svg>
361
+ </div>
362
+ )}
363
+ <div className="min-w-0 flex-1">
364
+ <p className="truncate text-sm font-medium text-slate-700 dark:text-slate-200">{file.name}</p>
365
+ <p className="text-xs text-slate-400 dark:text-slate-500">{formatFileSize(file.size)}</p>
366
+ </div>
367
+ <button
368
+ type="button"
369
+ onClick={() => removeFile(index)}
370
+ className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg text-slate-300 transition-colors hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-500"
371
+ >
372
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
373
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
374
+ </svg>
375
+ </button>
376
+ </div>
377
+ )
378
+ })}
379
+ </div>
380
+ )}
381
+ </div>
382
+
383
+ {/* Advanced options toggle */}
384
+ <div className="border-t border-slate-100 dark:border-slate-700/50 pt-5">
385
+ <button
386
+ type="button"
387
+ onClick={() => setShowAdvanced(!showAdvanced)}
388
+ className="group flex w-full items-center justify-between rounded-lg px-1 py-1 text-sm font-medium text-slate-500 dark:text-slate-400 transition-colors hover:text-slate-700 dark:hover:text-slate-200"
389
+ >
390
+ <span className="flex items-center gap-2">
391
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
392
+ <path d="M17 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM17 15.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM3.75 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5a.75.75 0 01.75-.75zM4.5 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM10 11a.75.75 0 01.75.75v5.5a.75.75 0 01-1.5 0v-5.5A.75.75 0 0110 11zM10.75 2.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM10 6a2 2 0 100 4 2 2 0 000-4zM3.75 10a2 2 0 100 4 2 2 0 000-4zM16.25 10a2 2 0 100 4 2 2 0 000-4z" />
393
+ </svg>
394
+ Options avancees
395
+ </span>
396
+ <svg
397
+ xmlns="http://www.w3.org/2000/svg"
398
+ viewBox="0 0 20 20"
399
+ fill="currentColor"
400
+ className={`h-4 w-4 transition-transform duration-200 ${showAdvanced ? 'rotate-180' : ''}`}
401
+ >
402
+ <path fillRule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clipRule="evenodd" />
403
+ </svg>
404
+ </button>
405
+
406
+ {showAdvanced && (
407
+ <div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3 rounded-xl border border-slate-100 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/30 p-5">
408
+ {/* Category */}
409
+ <div>
410
+ <label htmlFor="category" className="mb-2 block text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
411
+ Categorie
412
+ </label>
413
+ <select
414
+ id="category"
415
+ value={category}
416
+ onChange={(e) => {
417
+ const val = e.target.value
418
+ setCategory(val)
419
+ const tmpl = templates[val]
420
+ if (tmpl) {
421
+ if (!message.trim()) setMessage(tmpl.message)
422
+ if (!subject.trim() && tmpl.subject) setSubject(tmpl.subject)
423
+ }
424
+ }}
425
+ className="w-full rounded-lg border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2.5 text-sm text-slate-900 dark:text-white outline-none transition-all focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
426
+ >
427
+ <option value="">Selectionner</option>
428
+ {categories.map((cat) => (
429
+ <option key={cat.value} value={cat.value}>
430
+ {cat.icon} {cat.label}
431
+ </option>
432
+ ))}
433
+ </select>
434
+ </div>
435
+
436
+ {/* Priority */}
437
+ <div>
438
+ <label htmlFor="priority" className="mb-2 block text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
439
+ Priorite
440
+ </label>
441
+ <select
442
+ id="priority"
443
+ value={priority}
444
+ onChange={(e) => setPriority(e.target.value)}
445
+ className="w-full rounded-lg border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2.5 text-sm text-slate-900 dark:text-white outline-none transition-all focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
446
+ >
447
+ {priorities.map((p) => (
448
+ <option key={p.value} value={p.value}>
449
+ {p.label}
450
+ </option>
451
+ ))}
452
+ </select>
453
+ </div>
454
+
455
+ {/* Project */}
456
+ {projects.length > 0 && (
457
+ <div>
458
+ <label htmlFor="project" className="mb-2 block text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
459
+ Projet
460
+ </label>
461
+ <select
462
+ id="project"
463
+ value={project}
464
+ onChange={(e) => setProject(e.target.value)}
465
+ className="w-full rounded-lg border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2.5 text-sm text-slate-900 dark:text-white outline-none transition-all focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
466
+ >
467
+ <option value="">Selectionner</option>
468
+ {projects.map((p) => (
469
+ <option key={p.id} value={p.id}>
470
+ {p.name}
471
+ </option>
472
+ ))}
473
+ </select>
474
+ </div>
475
+ )}
476
+ </div>
477
+ )}
478
+ </div>
479
+ </div>
480
+
481
+ {/* Submit footer */}
482
+ <div className="border-t border-slate-100 dark:border-slate-700/50 bg-slate-50/50 dark:bg-slate-900/20 px-6 py-4 sm:px-8 rounded-b-2xl">
483
+ <div className="flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between">
484
+ <p className="text-xs text-slate-400 dark:text-slate-500">
485
+ Les champs marques d&apos;un <span className="text-red-400">*</span> sont obligatoires
486
+ </p>
487
+ <button
488
+ type="submit"
489
+ disabled={loading}
490
+ className="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-8 py-3 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
491
+ >
492
+ {loading ? (
493
+ <>
494
+ <svg className="h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
495
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
496
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
497
+ </svg>
498
+ Envoi en cours...
499
+ </>
500
+ ) : (
501
+ <>
502
+ Creer le ticket
503
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
504
+ <path fillRule="evenodd" d="M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z" clipRule="evenodd" />
505
+ </svg>
506
+ </>
507
+ )}
508
+ </button>
509
+ </div>
510
+ </div>
511
+ </div>
512
+ </form>
513
+ </div>
514
+ )
515
+ }
@@ -0,0 +1,114 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import Link from 'next/link'
5
+
6
+ export default function ForgotPasswordPage() {
7
+ const [email, setEmail] = useState('')
8
+ const [sent, setSent] = useState(false)
9
+ const [error, setError] = useState('')
10
+ const [loading, setLoading] = useState(false)
11
+
12
+ const handleSubmit = async (e: React.FormEvent) => {
13
+ e.preventDefault()
14
+ setError('')
15
+ setLoading(true)
16
+
17
+ try {
18
+ const res = await fetch('/api/support-clients/forgot-password', {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({ email }),
22
+ })
23
+
24
+ if (res.ok) {
25
+ setSent(true)
26
+ } else {
27
+ // Payload returns 200 even if email doesn't exist (security)
28
+ setSent(true)
29
+ }
30
+ } catch {
31
+ setError('Erreur de connexion. Veuillez réessayer.')
32
+ } finally {
33
+ setLoading(false)
34
+ }
35
+ }
36
+
37
+ return (
38
+ <div className="flex min-h-screen items-center justify-center px-4 py-12">
39
+ <div className="w-full max-w-md">
40
+ <div className="mb-8 text-center">
41
+ <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl border-4 border-black bg-[#FFD600] shadow-[4px_4px_0px_#000]">
42
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-8 w-8">
43
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
44
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
45
+ </svg>
46
+ </div>
47
+ <h1 className="text-3xl font-black text-black">Mot de passe oublié</h1>
48
+ <p className="mt-2 text-gray-600">Portail client</p>
49
+ </div>
50
+
51
+ {sent ? (
52
+ <div className="rounded-2xl border-4 border-black bg-white p-8 shadow-[6px_6px_0px_#000]">
53
+ <div className="mb-4 rounded-xl border-2 border-green-500 bg-green-50 px-4 py-3 text-sm font-semibold text-green-700">
54
+ Si un compte existe avec cette adresse, vous recevrez un email avec un lien de réinitialisation.
55
+ </div>
56
+ <p className="mb-6 text-sm text-gray-500">
57
+ Vérifiez votre boîte de réception et vos spams. Le lien est valable 1 heure.
58
+ </p>
59
+ <Link
60
+ href="/support/login"
61
+ className="block w-full rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-center text-base font-black text-black shadow-[4px_4px_0px_#000] transition-all hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000]"
62
+ >
63
+ Retour à la connexion
64
+ </Link>
65
+ </div>
66
+ ) : (
67
+ <form
68
+ onSubmit={handleSubmit}
69
+ className="rounded-2xl border-4 border-black bg-white p-8 shadow-[6px_6px_0px_#000]"
70
+ >
71
+ <p className="mb-6 text-sm text-gray-600">
72
+ Entrez votre adresse email. Vous recevrez un lien pour réinitialiser votre mot de passe.
73
+ </p>
74
+
75
+ {error && (
76
+ <div className="mb-6 rounded-xl border-2 border-red-500 bg-red-50 px-4 py-3 text-sm font-semibold text-red-700">
77
+ {error}
78
+ </div>
79
+ )}
80
+
81
+ <div className="mb-6">
82
+ <label htmlFor="email" className="mb-2 block text-sm font-bold text-black">
83
+ Email
84
+ </label>
85
+ <input
86
+ id="email"
87
+ type="email"
88
+ required
89
+ value={email}
90
+ onChange={(e) => setEmail(e.target.value)}
91
+ placeholder="vous@entreprise.fr"
92
+ className="w-full rounded-xl border-3 border-black px-4 py-3 text-base outline-none transition-shadow focus:shadow-[3px_3px_0px_#00E5FF]"
93
+ />
94
+ </div>
95
+
96
+ <button
97
+ type="submit"
98
+ disabled={loading}
99
+ className="w-full rounded-xl border-3 border-black bg-[#FFD600] px-6 py-3 text-base font-black text-black shadow-[4px_4px_0px_#000] transition-all hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000] disabled:opacity-50"
100
+ >
101
+ {loading ? 'Envoi...' : 'Envoyer le lien'}
102
+ </button>
103
+ </form>
104
+ )}
105
+
106
+ <p className="mt-6 text-center text-sm text-gray-500">
107
+ <Link href="/support/login" className="font-semibold text-black hover:underline">
108
+ &larr; Retour à la connexion
109
+ </Link>
110
+ </p>
111
+ </div>
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+
3
+ export const metadata = {
4
+ title: 'Support Client',
5
+ description: 'Portail de support client',
6
+ robots: { index: false, follow: false },
7
+ }
8
+
9
+ export default function SupportRootLayout({ children }: { children: React.ReactNode }) {
10
+ return (
11
+ <html lang="fr" suppressHydrationWarning>
12
+ <head>
13
+ <link href="/favicon.ico" rel="icon" sizes="32x32" />
14
+ <meta name="theme-color" content="#2563eb" />
15
+ <script
16
+ dangerouslySetInnerHTML={{
17
+ __html: `(function(){try{if(localStorage.getItem('support-dark-mode')==='true'){document.documentElement.setAttribute('data-theme','dark')}}catch(e){console.error('[support-layout] Failed to get user:',e)}})()`,
18
+ }}
19
+ />
20
+ </head>
21
+ <body className="min-h-screen bg-gray-50 dark:bg-gray-950" suppressHydrationWarning>
22
+ {children}
23
+ </body>
24
+ </html>
25
+ )
26
+ }