@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,227 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect } from 'react'
4
+ import Link from 'next/link'
5
+ import { useRouter } from 'next/navigation'
6
+ import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
7
+ import s from '../../styles/NewTicket.module.scss'
8
+
9
+ interface ClientOption { id: number; firstName?: string; lastName?: string; company?: string; email?: string }
10
+ interface ProjectOption { id: number; name: string }
11
+
12
+ const CATEGORY_KEYS = [
13
+ { value: '', key: 'ticket.category.select' },
14
+ { value: 'bug', key: 'ticket.category.bugFull' },
15
+ { value: 'content', key: 'ticket.category.contentFull' },
16
+ { value: 'feature', key: 'ticket.category.featureFull' },
17
+ { value: 'question', key: 'ticket.category.questionFull' },
18
+ { value: 'hosting', key: 'ticket.category.hostingFull' },
19
+ ]
20
+
21
+ const PRIORITY_KEYS = [
22
+ { value: 'low', key: 'ticket.priority.low' },
23
+ { value: 'normal', key: 'ticket.priority.normal' },
24
+ { value: 'high', key: 'ticket.priority.high' },
25
+ { value: 'urgent', key: 'ticket.priority.urgent' },
26
+ ]
27
+
28
+ export const NewTicketClient: React.FC = () => {
29
+ const { t } = useTranslation()
30
+ const router = useRouter()
31
+ const [subject, setSubject] = useState('')
32
+ const [description, setDescription] = useState('')
33
+ const [category, setCategory] = useState('')
34
+ const [priority, setPriority] = useState('normal')
35
+ const [clientSearch, setClientSearch] = useState('')
36
+ const [clientId, setClientId] = useState<number | null>(null)
37
+ const [clientResults, setClientResults] = useState<ClientOption[]>([])
38
+ const [selectedClient, setSelectedClient] = useState<ClientOption | null>(null)
39
+ const [projectId, setProjectId] = useState<number | null>(null)
40
+ const [projects, setProjects] = useState<ProjectOption[]>([])
41
+ const [submitting, setSubmitting] = useState(false)
42
+ const [error, setError] = useState('')
43
+
44
+ // Search clients
45
+ useEffect(() => {
46
+ if (clientSearch.length < 2) { setClientResults([]); return }
47
+ const timer = setTimeout(async () => {
48
+ try {
49
+ const res = await fetch(`/api/support-clients?where[or][0][email][contains]=${encodeURIComponent(clientSearch)}&where[or][1][firstName][contains]=${encodeURIComponent(clientSearch)}&where[or][2][company][contains]=${encodeURIComponent(clientSearch)}&limit=8&depth=0`, { credentials: 'include' })
50
+ if (res.ok) { const d = await res.json(); setClientResults(d.docs || []) }
51
+ } catch (err) { console.warn('[support] client search error:', err) }
52
+ }, 300)
53
+ return () => clearTimeout(timer)
54
+ }, [clientSearch])
55
+
56
+ // Fetch projects
57
+ useEffect(() => {
58
+ fetch('/api/projects?where[status][equals]=active&limit=50&depth=0', { credentials: 'include' })
59
+ .then((r) => r.json())
60
+ .then((d) => setProjects(d.docs || []))
61
+ .catch(() => {})
62
+ }, [])
63
+
64
+ const handleSubmit = async (e: React.FormEvent) => {
65
+ e.preventDefault()
66
+ setError('')
67
+
68
+ if (!subject.trim()) { setError(t('newTicket.errors.subjectRequired')); return }
69
+ if (!clientId) { setError(t('newTicket.errors.clientRequired')); return }
70
+
71
+ setSubmitting(true)
72
+ try {
73
+ const res = await fetch('/api/tickets', {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ credentials: 'include',
77
+ body: JSON.stringify({
78
+ subject: subject.trim(),
79
+ client: clientId,
80
+ category: category || undefined,
81
+ priority,
82
+ project: projectId || undefined,
83
+ source: 'admin',
84
+ status: 'open',
85
+ }),
86
+ })
87
+
88
+ if (!res.ok) {
89
+ const d = await res.json().catch(() => ({}))
90
+ setError(d.errors?.[0]?.message || t('newTicket.errors.creationError'))
91
+ return
92
+ }
93
+
94
+ const ticket = await res.json()
95
+
96
+ if (description.trim()) {
97
+ await fetch('/api/ticket-messages', {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ credentials: 'include',
101
+ body: JSON.stringify({
102
+ ticket: ticket.doc.id,
103
+ body: description.trim(),
104
+ authorType: 'admin',
105
+ isInternal: false,
106
+ }),
107
+ })
108
+ }
109
+
110
+ router.push(`/admin/support/ticket?id=${ticket.doc.id}`)
111
+ } catch {
112
+ setError(t('newTicket.errors.networkError'))
113
+ } finally {
114
+ setSubmitting(false)
115
+ }
116
+ }
117
+
118
+ const S: Record<string, React.CSSProperties> = {
119
+ page: { padding: '20px 30px', maxWidth: 720, margin: '0 auto' },
120
+ header: { marginBottom: 24 },
121
+ backLink: { fontSize: 13, color: '#2563eb', textDecoration: 'none' },
122
+ title: { fontSize: 24, fontWeight: 700, margin: '8px 0 4px', color: 'var(--theme-text)' },
123
+ subtitle: { fontSize: 14, color: 'var(--theme-elevation-500)' },
124
+ error: { padding: '10px 14px', borderRadius: 8, background: '#fef2f2', color: '#dc2626', fontSize: 13, marginBottom: 16, border: '1px solid #fecaca' },
125
+ fieldGroup: { marginBottom: 16 },
126
+ label: { display: 'block', fontSize: 13, fontWeight: 600, marginBottom: 6, color: 'var(--theme-text)' },
127
+ required: { color: '#dc2626' },
128
+ input: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' },
129
+ select: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' },
130
+ textarea: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)', minHeight: 120, fontFamily: 'inherit', resize: 'vertical' as const },
131
+ row3: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12, marginBottom: 16 },
132
+ submitBtn: { padding: '10px 20px', borderRadius: 8, background: '#2563eb', color: '#fff', fontSize: 14, fontWeight: 600, border: 'none', cursor: 'pointer' },
133
+ searchResults: { position: 'absolute' as const, top: '100%', left: 0, right: 0, background: 'var(--theme-elevation-0)', border: '1px solid var(--theme-elevation-200)', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.1)', zIndex: 50, maxHeight: 200, overflowY: 'auto' as const },
134
+ searchItem: { padding: '8px 12px', cursor: 'pointer', fontSize: 13, borderBottom: '1px solid var(--theme-elevation-100)' },
135
+ }
136
+
137
+ return (
138
+ <div style={S.page}>
139
+ <div style={S.header}>
140
+ <Link href="/admin/support/inbox" style={S.backLink}>&larr; {t('newTicket.backToInbox')}</Link>
141
+ <h1 style={S.title}>{t('newTicket.title')}</h1>
142
+ <p style={S.subtitle}>{t('newTicket.subtitle')}</p>
143
+ </div>
144
+
145
+ {error && <div style={S.error}>{error}</div>}
146
+
147
+ <form onSubmit={handleSubmit}>
148
+ {/* Client search */}
149
+ <div style={S.fieldGroup}>
150
+ <label style={S.label}>{t('newTicket.clientLabel')} <span style={S.required}>*</span></label>
151
+ {selectedClient ? (
152
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderRadius: 10, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-50)' }}>
153
+ <span style={{ fontWeight: 600, fontSize: 13, color: 'var(--theme-text)' }}>
154
+ {selectedClient.firstName} {selectedClient.lastName} -- {selectedClient.company}
155
+ </span>
156
+ <span style={{ fontSize: 12, color: 'var(--theme-elevation-500)' }}>{selectedClient.email}</span>
157
+ <button type="button" onClick={() => { setSelectedClient(null); setClientId(null); setClientSearch('') }} style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--theme-elevation-400)', fontSize: 16 }}>&times;</button>
158
+ </div>
159
+ ) : (
160
+ <div style={{ position: 'relative' }}>
161
+ <input
162
+ type="text"
163
+ style={S.input}
164
+ placeholder={t('newTicket.clientSearchPlaceholder')}
165
+ value={clientSearch}
166
+ onChange={(e) => setClientSearch(e.target.value)}
167
+ />
168
+ {clientResults.length > 0 && (
169
+ <div style={S.searchResults}>
170
+ {clientResults.map((c) => (
171
+ <div key={c.id} style={S.searchItem} onClick={() => {
172
+ setSelectedClient(c)
173
+ setClientId(c.id)
174
+ setClientSearch('')
175
+ setClientResults([])
176
+ }}>
177
+ <strong>{c.firstName} {c.lastName}</strong> -- {c.company} <span style={{ color: 'var(--theme-elevation-400)', fontSize: 12 }}>{c.email}</span>
178
+ </div>
179
+ ))}
180
+ </div>
181
+ )}
182
+ </div>
183
+ )}
184
+ </div>
185
+
186
+ {/* Subject */}
187
+ <div style={S.fieldGroup}>
188
+ <label style={S.label}>{t('newTicket.subjectLabel')} <span style={S.required}>*</span></label>
189
+ <input type="text" style={S.input} placeholder={t('newTicket.subjectPlaceholder')} value={subject} onChange={(e) => setSubject(e.target.value)} />
190
+ </div>
191
+
192
+ {/* Category + Priority + Project */}
193
+ <div style={S.row3}>
194
+ <div style={S.fieldGroup}>
195
+ <label style={S.label}>{t('newTicket.categoryLabel')}</label>
196
+ <select style={S.select} value={category} onChange={(e) => setCategory(e.target.value)}>
197
+ {CATEGORY_KEYS.map((c) => <option key={c.value} value={c.value}>{t(c.key)}</option>)}
198
+ </select>
199
+ </div>
200
+ <div style={S.fieldGroup}>
201
+ <label style={S.label}>{t('newTicket.priorityLabel')}</label>
202
+ <select style={S.select} value={priority} onChange={(e) => setPriority(e.target.value)}>
203
+ {PRIORITY_KEYS.map((p) => <option key={p.value} value={p.value}>{t(p.key)}</option>)}
204
+ </select>
205
+ </div>
206
+ <div style={S.fieldGroup}>
207
+ <label style={S.label}>{t('newTicket.projectLabel')}</label>
208
+ <select style={S.select} value={projectId || ''} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : null)}>
209
+ <option value="">{t('ticket.noProject')}</option>
210
+ {projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
211
+ </select>
212
+ </div>
213
+ </div>
214
+
215
+ {/* Description */}
216
+ <div style={S.fieldGroup}>
217
+ <label style={S.label}>{t('newTicket.descriptionLabel')}</label>
218
+ <textarea style={S.textarea} placeholder={t('newTicket.descriptionPlaceholder')} value={description} onChange={(e) => setDescription(e.target.value)} />
219
+ </div>
220
+
221
+ <button type="submit" style={S.submitBtn} disabled={submitting}>
222
+ {submitting ? t('newTicket.submitting') : t('newTicket.submitButton')}
223
+ </button>
224
+ </form>
225
+ </div>
226
+ )
227
+ }
@@ -0,0 +1,30 @@
1
+ import type { AdminViewServerProps } from 'payload'
2
+ import { DefaultTemplate } from '@payloadcms/next/templates'
3
+ import { redirect } from 'next/navigation'
4
+ import React from 'react'
5
+ import { AdminErrorBoundary } from '../shared/ErrorBoundary'
6
+ import { NewTicketClient } from './client'
7
+
8
+ export const NewTicketView: React.FC<AdminViewServerProps> = ({ initPageResult }) => {
9
+ const { req, visibleEntities } = initPageResult
10
+ if (!req.user) redirect('/admin/login')
11
+
12
+ return (
13
+ <DefaultTemplate
14
+ i18n={req.i18n}
15
+ locale={initPageResult.locale}
16
+ params={{}}
17
+ payload={req.payload}
18
+ permissions={initPageResult.permissions}
19
+ searchParams={{}}
20
+ user={req.user}
21
+ visibleEntities={visibleEntities}
22
+ >
23
+ <AdminErrorBoundary viewName="NewTicketView">
24
+ <NewTicketClient />
25
+ </AdminErrorBoundary>
26
+ </DefaultTemplate>
27
+ )
28
+ }
29
+
30
+ export default NewTicketView
@@ -0,0 +1,177 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useCallback } from 'react'
4
+ import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
5
+ import s from '../../styles/PendingEmails.module.scss'
6
+
7
+ interface SuggestedTicket { id: number; ticketNumber: string; subject: string; score: number }
8
+ interface PendingEmail {
9
+ id: number; senderEmail: string; senderName?: string; subject: string; body: string
10
+ client?: { id: number; firstName?: string; lastName?: string; email?: string; company?: string } | number
11
+ attachments?: Array<{ file: { id: number; filename?: string } | number }>
12
+ status: 'pending' | 'processed' | 'ignored'
13
+ processedAction?: 'ticket_created' | 'message_added' | 'ignored'
14
+ processedTicket?: { id: number; ticketNumber?: string } | number
15
+ suggestedTickets?: SuggestedTicket[]
16
+ createdAt: string
17
+ }
18
+
19
+ type Tab = 'pending' | 'processed' | 'ignored'
20
+
21
+ function timeAgo(dateStr: string): string {
22
+ const diff = Date.now() - new Date(dateStr).getTime()
23
+ const mins = Math.floor(diff / 60000)
24
+ if (mins < 1) return "a l'instant"
25
+ if (mins < 60) return `il y a ${mins}min`
26
+ const hours = Math.floor(mins / 60)
27
+ if (hours < 24) return `il y a ${hours}h`
28
+ return `il y a ${Math.floor(hours / 24)}j`
29
+ }
30
+
31
+ function EmailCard({ email, onProcess, processing, t }: { email: PendingEmail; onProcess: (action: 'create_ticket' | 'add_to_ticket' | 'ignore', ticketId?: number, clientId?: number) => void; processing: boolean; t: (key: string, vars?: Record<string, string | number>) => string }) {
32
+ const [expanded, setExpanded] = useState(false)
33
+ const [showLinkModal, setShowLinkModal] = useState(false)
34
+ const [linkSearch, setLinkSearch] = useState('')
35
+ const [linkResults, setLinkResults] = useState<Array<{ id: number; ticketNumber: string; subject: string }>>([])
36
+ const isPending = email.status === 'pending'
37
+ const preview = email.body.slice(0, 200) + (email.body.length > 200 ? '...' : '')
38
+ const suggestions = email.suggestedTickets || []
39
+
40
+ useEffect(() => {
41
+ if (!linkSearch || linkSearch.length < 2) { setLinkResults([]); return }
42
+ const timer = setTimeout(async () => {
43
+ try {
44
+ const res = await fetch(`/api/tickets?where[or][0][ticketNumber][contains]=${encodeURIComponent(linkSearch)}&where[or][1][subject][contains]=${encodeURIComponent(linkSearch)}&limit=10&sort=-updatedAt&depth=0`)
45
+ if (res.ok) { const data = await res.json(); setLinkResults(data.docs.map((d: Record<string, unknown>) => ({ id: d.id, ticketNumber: d.ticketNumber, subject: d.subject }))) }
46
+ } catch (err) { console.warn('[support] ticket search error:', err) }
47
+ }, 300)
48
+ return () => clearTimeout(timer)
49
+ }, [linkSearch])
50
+
51
+ const S: Record<string, React.CSSProperties> = {
52
+ card: { padding: 16, borderRadius: 10, border: '1px solid var(--theme-elevation-150)', marginBottom: 12, opacity: processing ? 0.5 : 1 },
53
+ senderName: { fontWeight: 600, fontSize: 14 },
54
+ senderEmail: { fontSize: 12, color: 'var(--theme-elevation-500)' },
55
+ subject: { fontWeight: 600, fontSize: 13, marginTop: 4 },
56
+ meta: { fontSize: 12, color: 'var(--theme-elevation-400)', marginTop: 2 },
57
+ body: { fontSize: 13, color: 'var(--theme-text)', padding: '8px 0', whiteSpace: 'pre-wrap' as const, lineHeight: 1.5 },
58
+ actions: { display: 'flex', gap: 8, marginTop: 8 },
59
+ btn: { padding: '6px 14px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, fontWeight: 600, cursor: 'pointer', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' },
60
+ btnCreate: { background: '#2563eb', color: '#fff', border: 'none' },
61
+ btnIgnore: { color: '#dc2626', borderColor: '#dc2626' },
62
+ overlay: { position: 'fixed' as const, inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 },
63
+ modal: { background: 'var(--theme-elevation-0)', borderRadius: 12, padding: 24, maxWidth: 480, width: '100%', maxHeight: '80vh', overflowY: 'auto' as const },
64
+ }
65
+
66
+ return (
67
+ <div style={S.card}>
68
+ <div>
69
+ <span style={S.senderName}>{email.senderName || email.senderEmail}</span>
70
+ {email.senderName && <span style={S.senderEmail}> &lt;{email.senderEmail}&gt;</span>}
71
+ </div>
72
+ <div style={S.subject}>{email.subject}</div>
73
+ <div style={S.meta}>{timeAgo(email.createdAt)} {email.attachments?.length ? `-- ${email.attachments.length} PJ` : ''}</div>
74
+
75
+ {suggestions.length > 0 && isPending && (
76
+ <div style={{ display: 'flex', gap: 6, marginTop: 6, flexWrap: 'wrap' }}>
77
+ {suggestions.map((s) => (
78
+ <span key={s.id} style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, background: s.score >= 0.7 ? '#dcfce7' : '#fef3c7', color: s.score >= 0.7 ? '#166534' : '#92400e' }}>
79
+ Similaire a {s.ticketNumber} ({Math.round(s.score * 100)}%)
80
+ </span>
81
+ ))}
82
+ </div>
83
+ )}
84
+
85
+ <div style={S.body}>{expanded ? email.body : preview}</div>
86
+ {email.body.length > 200 && <button onClick={() => setExpanded(!expanded)} style={{ ...S.btn, fontSize: 11, padding: '2px 8px' }}>{expanded ? t('pendingEmails.collapse') : t('pendingEmails.expand')}</button>}
87
+
88
+ {isPending && (
89
+ <div style={S.actions}>
90
+ <button onClick={() => {
91
+ const clientId = typeof email.client === 'object' && email.client ? email.client.id : undefined
92
+ onProcess('create_ticket', undefined, clientId)
93
+ }} disabled={processing} style={{ ...S.btn, ...S.btnCreate }}>{t('pendingEmails.actions.createTicket')}</button>
94
+ <button onClick={() => setShowLinkModal(true)} disabled={processing} style={S.btn}>{t('pendingEmails.actions.linkToTicket')}</button>
95
+ <button onClick={() => onProcess('ignore')} disabled={processing} style={{ ...S.btn, ...S.btnIgnore }}>{t('pendingEmails.actions.ignore')}</button>
96
+ </div>
97
+ )}
98
+
99
+ {showLinkModal && (
100
+ <div style={S.overlay} onClick={() => setShowLinkModal(false)}>
101
+ <div style={S.modal} onClick={(e) => e.stopPropagation()}>
102
+ <h3 style={{ margin: '0 0 12px', fontSize: 16, fontWeight: 700 }}>{t('pendingEmails.linkModal.title')}</h3>
103
+ {suggestions.length > 0 && (
104
+ <div style={{ marginBottom: 12 }}>
105
+ <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4, color: 'var(--theme-elevation-500)' }}>{t('pendingEmails.linkModal.suggestions')}</div>
106
+ {suggestions.map((s) => (
107
+ <button key={s.id} onClick={() => { setShowLinkModal(false); onProcess('add_to_ticket', s.id) }} style={{ display: 'block', width: '100%', padding: '8px 12px', border: '1px solid var(--theme-elevation-200)', borderRadius: 6, background: 'var(--theme-elevation-0)', cursor: 'pointer', textAlign: 'left', marginBottom: 4, fontSize: 13 }}>
108
+ <strong>{s.ticketNumber}</strong> {s.subject} <span style={{ fontSize: 11, color: '#16a34a' }}>{Math.round(s.score * 100)}%</span>
109
+ </button>
110
+ ))}
111
+ </div>
112
+ )}
113
+ <input type="text" placeholder={t('pendingEmails.linkModal.searchPlaceholder')} value={linkSearch} onChange={(e) => setLinkSearch(e.target.value)} style={{ width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 13, marginBottom: 8, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }} />
114
+ {linkResults.map((r) => (
115
+ <button key={r.id} onClick={() => { setShowLinkModal(false); onProcess('add_to_ticket', r.id) }} style={{ display: 'block', width: '100%', padding: '8px 12px', border: '1px solid var(--theme-elevation-200)', borderRadius: 6, background: 'var(--theme-elevation-0)', cursor: 'pointer', textAlign: 'left', marginBottom: 4, fontSize: 13 }}>
116
+ <strong>{r.ticketNumber}</strong> {r.subject}
117
+ </button>
118
+ ))}
119
+ </div>
120
+ </div>
121
+ )}
122
+ </div>
123
+ )
124
+ }
125
+
126
+ export const PendingEmailsClient: React.FC = () => {
127
+ const { t } = useTranslation()
128
+ const [emails, setEmails] = useState<PendingEmail[]>([])
129
+ const [loading, setLoading] = useState(true)
130
+ const [tab, setTab] = useState<Tab>('pending')
131
+ const [processing, setProcessing] = useState<number | null>(null)
132
+
133
+ const fetchEmails = useCallback(async () => {
134
+ try {
135
+ const res = await fetch(`/api/pending-emails?where[status][equals]=${tab}&sort=-createdAt&limit=50&depth=1`)
136
+ if (res.ok) { const data = await res.json(); setEmails(data.docs) }
137
+ } catch { /* ignore */ }
138
+ setLoading(false)
139
+ }, [tab])
140
+
141
+ useEffect(() => { setLoading(true); fetchEmails() }, [fetchEmails])
142
+ useEffect(() => { if (tab !== 'pending') return; const iv = setInterval(fetchEmails, 30000); return () => clearInterval(iv) }, [fetchEmails, tab])
143
+
144
+ const handleProcess = async (emailId: number, action: 'create_ticket' | 'add_to_ticket' | 'ignore', ticketId?: number, clientId?: number) => {
145
+ setProcessing(emailId)
146
+ try {
147
+ const res = await fetch(`/api/support/pending-emails/${emailId}/process`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, ticketId, clientId }) })
148
+ if (res.ok) { setEmails((prev) => prev.filter((e) => e.id !== emailId)) }
149
+ else { const err = await res.json().catch(() => ({ error: 'Unknown error' })); alert(`Erreur : ${err.error || res.statusText}`) }
150
+ } catch { alert('Erreur reseau') }
151
+ setProcessing(null)
152
+ }
153
+
154
+ const tabs: { key: Tab; label: string }[] = [{ key: 'pending', label: t('pendingEmails.tabs.pending') }, { key: 'processed', label: t('pendingEmails.tabs.processed') }, { key: 'ignored', label: t('pendingEmails.tabs.ignored') }]
155
+
156
+ return (
157
+ <div style={{ padding: '20px 30px', maxWidth: 900, margin: '0 auto' }}>
158
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
159
+ <div>
160
+ <h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, color: 'var(--theme-text)' }}>{t('pendingEmails.title')}</h1>
161
+ <p style={{ fontSize: 13, color: 'var(--theme-elevation-500)', margin: '4px 0 0' }}>{t('pendingEmails.subtitle')}</p>
162
+ </div>
163
+ {tab === 'pending' && emails.length > 0 && <span style={{ padding: '4px 10px', borderRadius: 10, background: '#fef2f2', color: '#dc2626', fontSize: 12, fontWeight: 700 }}>{t('pendingEmails.pendingCount', { count: String(emails.length) })}</span>}
164
+ </div>
165
+
166
+ <div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
167
+ {tabs.map((tb) => (
168
+ <button key={tb.key} onClick={() => setTab(tb.key)} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: tab === tb.key ? 'var(--theme-elevation-100)' : 'none', cursor: 'pointer', fontSize: 13, fontWeight: tab === tb.key ? 700 : 500, color: tab === tb.key ? 'var(--theme-text)' : 'var(--theme-elevation-500)' }}>{tb.label}</button>
169
+ ))}
170
+ </div>
171
+
172
+ {loading ? <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('common.loading')}</div>
173
+ : emails.length === 0 ? <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t(`pendingEmails.empty.${tab}`)}</div>
174
+ : emails.map((email) => <EmailCard key={email.id} email={email} onProcess={(action, ticketId, clientId) => handleProcess(email.id, action, ticketId, clientId)} processing={processing === email.id} t={t} />)}
175
+ </div>
176
+ )
177
+ }
@@ -0,0 +1,33 @@
1
+ import type { AdminViewServerProps } from 'payload'
2
+ import { DefaultTemplate } from '@payloadcms/next/templates'
3
+ import { redirect } from 'next/navigation'
4
+ import React from 'react'
5
+ import { AdminErrorBoundary } from '../shared/ErrorBoundary'
6
+ import { PendingEmailsClient } from './client'
7
+
8
+ export const PendingEmailsView: React.FC<AdminViewServerProps> = ({ initPageResult }) => {
9
+ const { req, visibleEntities } = initPageResult
10
+
11
+ if (!req.user) {
12
+ redirect('/admin/login')
13
+ }
14
+
15
+ return (
16
+ <DefaultTemplate
17
+ i18n={req.i18n}
18
+ locale={initPageResult.locale}
19
+ params={{}}
20
+ payload={req.payload}
21
+ permissions={initPageResult.permissions}
22
+ searchParams={{}}
23
+ user={req.user}
24
+ visibleEntities={visibleEntities}
25
+ >
26
+ <AdminErrorBoundary viewName="PendingEmailsView">
27
+ <PendingEmailsClient />
28
+ </AdminErrorBoundary>
29
+ </DefaultTemplate>
30
+ )
31
+ }
32
+
33
+ export default PendingEmailsView