@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,186 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+
5
+ const LANG_CONFIG: Record<string, { label: string; accent: string }> = {
6
+ javascript: { label: 'JavaScript', accent: '#f7df1e' },
7
+ js: { label: 'JavaScript', accent: '#f7df1e' },
8
+ typescript: { label: 'TypeScript', accent: '#3178c6' },
9
+ ts: { label: 'TypeScript', accent: '#3178c6' },
10
+ tsx: { label: 'TSX', accent: '#3178c6' },
11
+ jsx: { label: 'JSX', accent: '#f7df1e' },
12
+ php: { label: 'PHP', accent: '#777bb4' },
13
+ python: { label: 'Python', accent: '#3776ab' },
14
+ py: { label: 'Python', accent: '#3776ab' },
15
+ html: { label: 'HTML', accent: '#e34f26' },
16
+ css: { label: 'CSS', accent: '#1572b6' },
17
+ scss: { label: 'SCSS', accent: '#cc6699' },
18
+ json: { label: 'JSON', accent: '#292929' },
19
+ sql: { label: 'SQL', accent: '#e48e00' },
20
+ bash: { label: 'Bash', accent: '#4eaa25' },
21
+ sh: { label: 'Shell', accent: '#4eaa25' },
22
+ yaml: { label: 'YAML', accent: '#cb171e' },
23
+ yml: { label: 'YAML', accent: '#cb171e' },
24
+ xml: { label: 'XML', accent: '#f16529' },
25
+ markdown: { label: 'Markdown', accent: '#083fa1' },
26
+ md: { label: 'Markdown', accent: '#083fa1' },
27
+ diff: { label: 'Diff', accent: '#41b883' },
28
+ go: { label: 'Go', accent: '#00add8' },
29
+ rust: { label: 'Rust', accent: '#dea584' },
30
+ java: { label: 'Java', accent: '#ed8b00' },
31
+ c: { label: 'C', accent: '#555555' },
32
+ cpp: { label: 'C++', accent: '#00599c' },
33
+ ruby: { label: 'Ruby', accent: '#cc342d' },
34
+ swift: { label: 'Swift', accent: '#f05138' },
35
+ nginx: { label: 'Nginx', accent: '#009639' },
36
+ docker: { label: 'Docker', accent: '#2496ed' },
37
+ dockerfile: { label: 'Dockerfile', accent: '#2496ed' },
38
+ env: { label: '.env', accent: '#ecd53f' },
39
+ }
40
+
41
+ const DEFAULT_CONFIG = { label: 'Code', accent: '#6b7280' }
42
+
43
+ function SingleCodeBlock({ lang, code }: { lang: string; code: string }) {
44
+ const [copied, setCopied] = useState(false)
45
+ const config = LANG_CONFIG[lang.toLowerCase()] || (lang ? { label: lang.toUpperCase(), accent: '#6b7280' } : DEFAULT_CONFIG)
46
+
47
+ const handleCopy = () => {
48
+ navigator.clipboard.writeText(code)
49
+ setCopied(true)
50
+ setTimeout(() => setCopied(false), 1500)
51
+ }
52
+
53
+ const lines = code.split('\n')
54
+
55
+ return (
56
+ <div style={{
57
+ marginTop: 8,
58
+ marginBottom: 8,
59
+ borderRadius: 8,
60
+ overflow: 'hidden',
61
+ border: '1px solid #334155',
62
+ backgroundColor: '#0f172a',
63
+ fontSize: 13,
64
+ }}>
65
+ {/* Header */}
66
+ <div style={{
67
+ display: 'flex',
68
+ alignItems: 'center',
69
+ justifyContent: 'space-between',
70
+ padding: '6px 12px',
71
+ backgroundColor: '#1e293b',
72
+ borderBottom: '1px solid #334155',
73
+ }}>
74
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
75
+ <span style={{
76
+ display: 'inline-block',
77
+ width: 8,
78
+ height: 8,
79
+ borderRadius: '50%',
80
+ backgroundColor: config.accent,
81
+ flexShrink: 0,
82
+ }} />
83
+ <span style={{
84
+ fontSize: 11,
85
+ fontWeight: 700,
86
+ color: '#e2e8f0',
87
+ letterSpacing: '0.02em',
88
+ }}>
89
+ {config.label}
90
+ </span>
91
+ </div>
92
+ <button
93
+ onClick={handleCopy}
94
+ style={{
95
+ background: 'none',
96
+ border: '1px solid #475569',
97
+ borderRadius: 4,
98
+ padding: '2px 10px',
99
+ fontSize: 11,
100
+ color: copied ? '#4ade80' : '#94a3b8',
101
+ cursor: 'pointer',
102
+ fontWeight: 600,
103
+ transition: 'color 150ms',
104
+ }}
105
+ >
106
+ {copied ? 'Copié !' : 'Copier'}
107
+ </button>
108
+ </div>
109
+ {/* Code with line numbers */}
110
+ <div style={{ display: 'flex', overflowX: 'auto' }}>
111
+ <div style={{
112
+ padding: '12px 0',
113
+ borderRight: '1px solid #334155',
114
+ userSelect: 'none',
115
+ flexShrink: 0,
116
+ }}>
117
+ {lines.map((_, i) => (
118
+ <div key={i} style={{
119
+ padding: '0 12px',
120
+ fontSize: 12,
121
+ lineHeight: '20px',
122
+ color: '#475569',
123
+ textAlign: 'right',
124
+ fontFamily: '"SF Mono", "Fira Code", "JetBrains Mono", monospace',
125
+ }}>
126
+ {i + 1}
127
+ </div>
128
+ ))}
129
+ </div>
130
+ <pre style={{
131
+ margin: 0,
132
+ padding: 12,
133
+ overflow: 'auto',
134
+ flex: 1,
135
+ }}>
136
+ <code style={{
137
+ fontSize: 12,
138
+ lineHeight: '20px',
139
+ fontFamily: '"SF Mono", "Fira Code", "JetBrains Mono", monospace',
140
+ color: '#e2e8f0',
141
+ whiteSpace: 'pre',
142
+ }}>
143
+ {code}
144
+ </code>
145
+ </pre>
146
+ </div>
147
+ </div>
148
+ )
149
+ }
150
+
151
+ /**
152
+ * Renders text with fenced code blocks (```lang ... ```) styled per language.
153
+ * Non-code text is passed through as-is.
154
+ */
155
+ export function CodeBlockRenderer({ text }: { text: string }) {
156
+ // Match ```lang\n...\n``` blocks
157
+ const parts = text.split(/(```[\s\S]*?```)/g)
158
+ const hasCodeBlock = parts.some((p) => p.startsWith('```'))
159
+ if (!hasCodeBlock) return null
160
+
161
+ return (
162
+ <>
163
+ {parts.map((part, i) => {
164
+ if (part.startsWith('```')) {
165
+ const match = part.match(/^```(\w*)\n?([\s\S]*?)```$/)
166
+ if (match) {
167
+ const lang = match[1] || ''
168
+ const code = match[2].replace(/\n$/, '')
169
+ return <SingleCodeBlock key={i} lang={lang} code={code} />
170
+ }
171
+ }
172
+ return null
173
+ })}
174
+ </>
175
+ )
176
+ }
177
+
178
+ /**
179
+ * For HTML content, parses <pre><code> blocks from the RTE.
180
+ * Also handles ```lang blocks that survived as text in HTML.
181
+ */
182
+ export function CodeBlockRendererHtml({ html }: { html: string }) {
183
+ // Check for fenced code blocks in raw text within HTML
184
+ const stripped = html.replace(/<[^>]+>/g, '')
185
+ return <CodeBlockRenderer text={stripped} />
186
+ }
@@ -0,0 +1,166 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useEffect } from 'react'
4
+
5
+ const LANGUAGES = [
6
+ { value: 'javascript', label: 'JavaScript' },
7
+ { value: 'typescript', label: 'TypeScript' },
8
+ { value: 'php', label: 'PHP' },
9
+ { value: 'python', label: 'Python' },
10
+ { value: 'html', label: 'HTML' },
11
+ { value: 'css', label: 'CSS' },
12
+ { value: 'scss', label: 'SCSS' },
13
+ { value: 'json', label: 'JSON' },
14
+ { value: 'sql', label: 'SQL' },
15
+ { value: 'bash', label: 'Bash' },
16
+ { value: 'yaml', label: 'YAML' },
17
+ { value: 'xml', label: 'XML' },
18
+ { value: 'markdown', label: 'Markdown' },
19
+ { value: 'diff', label: 'Diff' },
20
+ { value: 'go', label: 'Go' },
21
+ { value: 'rust', label: 'Rust' },
22
+ { value: 'java', label: 'Java' },
23
+ { value: 'ruby', label: 'Ruby' },
24
+ { value: 'swift', label: 'Swift' },
25
+ { value: 'docker', label: 'Dockerfile' },
26
+ { value: 'nginx', label: 'Nginx' },
27
+ { value: 'env', label: '.env' },
28
+ ]
29
+
30
+ interface CodeBlockInserterProps {
31
+ onInsert: (block: string) => void
32
+ className?: string
33
+ style?: React.CSSProperties
34
+ }
35
+
36
+ /**
37
+ * Toolbar button that opens a language picker dropdown.
38
+ * When a language is selected, calls onInsert with the fenced code block template.
39
+ */
40
+ export function CodeBlockInserter({ onInsert, className, style }: CodeBlockInserterProps) {
41
+ const [open, setOpen] = useState(false)
42
+ const [filter, setFilter] = useState('')
43
+ const dropdownRef = useRef<HTMLDivElement>(null)
44
+ const inputRef = useRef<HTMLInputElement>(null)
45
+
46
+ useEffect(() => {
47
+ if (open && inputRef.current) inputRef.current.focus()
48
+ }, [open])
49
+
50
+ useEffect(() => {
51
+ const handleClickOutside = (e: MouseEvent) => {
52
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
53
+ setOpen(false)
54
+ setFilter('')
55
+ }
56
+ }
57
+ if (open) document.addEventListener('mousedown', handleClickOutside)
58
+ return () => document.removeEventListener('mousedown', handleClickOutside)
59
+ }, [open])
60
+
61
+ const filtered = filter
62
+ ? LANGUAGES.filter((l) => l.label.toLowerCase().includes(filter.toLowerCase()) || l.value.includes(filter.toLowerCase()))
63
+ : LANGUAGES
64
+
65
+ const handleSelect = (lang: string) => {
66
+ onInsert(`\n\`\`\`${lang}\n\n\`\`\`\n`)
67
+ setOpen(false)
68
+ setFilter('')
69
+ }
70
+
71
+ return (
72
+ <div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
73
+ <button
74
+ onClick={() => setOpen(!open)}
75
+ className={className}
76
+ style={{
77
+ fontSize: 11,
78
+ fontWeight: 700,
79
+ padding: '4px 10px',
80
+ width: 'auto',
81
+ cursor: 'pointer',
82
+ ...style,
83
+ }}
84
+ type="button"
85
+ aria-label="Insérer un bloc de code"
86
+ data-tooltip="Bloc de code"
87
+ >
88
+ &lt;/&gt; Code
89
+ </button>
90
+
91
+ {open && (
92
+ <div style={{
93
+ position: 'absolute',
94
+ bottom: '100%',
95
+ left: 0,
96
+ marginBottom: 4,
97
+ width: 200,
98
+ maxHeight: 280,
99
+ backgroundColor: '#1e293b',
100
+ border: '1px solid #334155',
101
+ borderRadius: 8,
102
+ boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
103
+ zIndex: 1000,
104
+ overflow: 'hidden',
105
+ display: 'flex',
106
+ flexDirection: 'column',
107
+ }}>
108
+ <div style={{ padding: '8px 8px 4px' }}>
109
+ <input
110
+ ref={inputRef}
111
+ type="text"
112
+ value={filter}
113
+ onChange={(e) => setFilter(e.target.value)}
114
+ onKeyDown={(e) => {
115
+ if (e.key === 'Escape') { setOpen(false); setFilter('') }
116
+ if (e.key === 'Enter' && filtered.length > 0) handleSelect(filtered[0].value)
117
+ }}
118
+ placeholder="Rechercher..."
119
+ style={{
120
+ width: '100%',
121
+ padding: '6px 10px',
122
+ fontSize: 12,
123
+ backgroundColor: '#0f172a',
124
+ border: '1px solid #334155',
125
+ borderRadius: 6,
126
+ color: '#e2e8f0',
127
+ outline: 'none',
128
+ boxSizing: 'border-box',
129
+ }}
130
+ />
131
+ </div>
132
+ <div style={{ overflowY: 'auto', maxHeight: 230, padding: '4px 0' }}>
133
+ {filtered.map((lang) => (
134
+ <button
135
+ key={lang.value}
136
+ onClick={() => handleSelect(lang.value)}
137
+ style={{
138
+ display: 'block',
139
+ width: '100%',
140
+ padding: '6px 12px',
141
+ fontSize: 12,
142
+ fontWeight: 500,
143
+ color: '#e2e8f0',
144
+ backgroundColor: 'transparent',
145
+ border: 'none',
146
+ cursor: 'pointer',
147
+ textAlign: 'left',
148
+ }}
149
+ onMouseEnter={(e) => { (e.target as HTMLElement).style.backgroundColor = '#334155' }}
150
+ onMouseLeave={(e) => { (e.target as HTMLElement).style.backgroundColor = 'transparent' }}
151
+ type="button"
152
+ >
153
+ {lang.label}
154
+ </button>
155
+ ))}
156
+ {filtered.length === 0 && (
157
+ <div style={{ padding: '12px', fontSize: 12, color: '#64748b', textAlign: 'center' }}>
158
+ Aucun langage trouvé
159
+ </div>
160
+ )}
161
+ </div>
162
+ </div>
163
+ )}
164
+ </div>
165
+ )
166
+ }
@@ -0,0 +1,82 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { C, s } from '../constants'
5
+
6
+ interface QuickActionsProps {
7
+ statusTransitions: Array<{ status: string; label: string; color: string }>
8
+ statusUpdating: boolean
9
+ onStatusChange: (status: string) => void
10
+ snoozeUntil: string | null
11
+ snoozeSaving: boolean
12
+ onCancelSnooze: () => void
13
+ // Panel toggles
14
+ showMerge: boolean
15
+ showExtMsg: boolean
16
+ showSnooze: boolean
17
+ onToggleMerge: () => void
18
+ onToggleExtMsg: () => void
19
+ onToggleSnooze: () => void
20
+ onNextTicket: () => void
21
+ // Next ticket banner
22
+ showNextTicket: boolean
23
+ nextTicketId: number | null
24
+ nextTicketInfo: string
25
+ onCloseNextTicket: () => void
26
+ }
27
+
28
+ export function QuickActions({
29
+ statusTransitions, statusUpdating, onStatusChange,
30
+ snoozeUntil, snoozeSaving, onCancelSnooze,
31
+ showMerge: _showMerge, showExtMsg: _showExtMsg, showSnooze: _showSnooze,
32
+ onToggleMerge, onToggleExtMsg, onToggleSnooze, onNextTicket,
33
+ showNextTicket, nextTicketId, nextTicketInfo, onCloseNextTicket,
34
+ }: QuickActionsProps) {
35
+ return (
36
+ <>
37
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '16px', alignItems: 'center' }}>
38
+ {statusTransitions.map((a) => (
39
+ <button
40
+ key={a.status}
41
+ onClick={() => onStatusChange(a.status)}
42
+ disabled={statusUpdating}
43
+ style={s.outlineBtn(a.color, statusUpdating)}
44
+ >
45
+ {a.label}
46
+ </button>
47
+ ))}
48
+ {snoozeUntil && new Date(snoozeUntil) > new Date() && (
49
+ <button onClick={onCancelSnooze} disabled={snoozeSaving} style={{ ...s.ghostBtn('#7c3aed', snoozeSaving), fontSize: '12px', padding: '5px 10px' }}>
50
+ Annuler snooze
51
+ </button>
52
+ )}
53
+ <span style={{ borderLeft: `1px solid ${C.border}`, height: '20px', margin: '0 4px' }} />
54
+ <button onClick={onToggleMerge} style={s.ghostBtn('#be185d')}>Fusionner</button>
55
+ <button onClick={onToggleExtMsg} style={s.ghostBtn('#4f46e5')}>+ Message reçu</button>
56
+ <button onClick={onToggleSnooze} style={s.ghostBtn('#7c3aed')}>Snooze</button>
57
+ <button onClick={onNextTicket} style={s.ghostBtn('#16a34a')}>Ticket suivant</button>
58
+ </div>
59
+
60
+ {showNextTicket && (
61
+ <div style={{ padding: '10px 14px', borderRadius: '8px', backgroundColor: '#f0fdf4', border: `1px solid ${C.border}`, marginBottom: '14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
62
+ {nextTicketId ? (
63
+ <>
64
+ <span style={{ fontSize: '13px', color: '#166534', fontWeight: 600 }}>
65
+ Ticket suivant : <strong>{nextTicketInfo}</strong>
66
+ </span>
67
+ <button
68
+ onClick={() => { window.location.href = `/admin/support/ticket?id=${nextTicketId}` }}
69
+ style={{ ...s.btn(C.statusResolved), color: C.white, fontSize: '12px', padding: '5px 14px' }}
70
+ >
71
+ Ouvrir
72
+ </button>
73
+ </>
74
+ ) : (
75
+ <span style={{ fontSize: '13px', color: '#166534', fontWeight: 700 }}>{nextTicketInfo}</span>
76
+ )}
77
+ <button onClick={onCloseNextTicket} style={{ border: 'none', background: 'none', color: C.textMuted, cursor: 'pointer', fontSize: '16px', fontWeight: 700, marginLeft: '8px' }}>&times;</button>
78
+ </div>
79
+ )}
80
+ </>
81
+ )
82
+ }
@@ -0,0 +1,91 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import type { SatisfactionSurvey } from '../types'
5
+ import { statusLabels, C, s } from '../constants'
6
+
7
+ const sourceLabels: Record<string, string> = {
8
+ email: 'Email',
9
+ 'live-chat': 'Live Chat',
10
+ portal: 'Portail',
11
+ admin: 'Admin',
12
+ }
13
+
14
+ interface TicketHeaderProps {
15
+ ticketNumber: string
16
+ currentStatus: string
17
+ clientSentiment: { emoji: string; label: string; color: string } | null
18
+ ticketSource: string
19
+ chatSession: string
20
+ snoozeUntil: string | null
21
+ satisfaction: SatisfactionSurvey | null
22
+ copiedLink: 'admin' | 'client' | null
23
+ onCopyLink: (type: 'admin' | 'client') => void
24
+ }
25
+
26
+ export function TicketHeader({
27
+ ticketNumber, currentStatus, clientSentiment, ticketSource, chatSession,
28
+ snoozeUntil, satisfaction, copiedLink, onCopyLink,
29
+ }: TicketHeaderProps) {
30
+ return (
31
+ <div style={{
32
+ display: 'flex', alignItems: 'center', gap: '10px', padding: '10px 14px',
33
+ borderRadius: '8px', backgroundColor: 'var(--theme-elevation-100)',
34
+ marginBottom: '12px', flexWrap: 'wrap',
35
+ }}>
36
+ {ticketNumber && (
37
+ <span style={{ fontFamily: "'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace", fontWeight: 700, fontSize: '14px', color: C.textPrimary }}>{ticketNumber}</span>
38
+ )}
39
+ {currentStatus && (() => {
40
+ const st = statusLabels[currentStatus] || statusLabels.open
41
+ return <span style={s.badge(st.bg, st.color)}>{st.label}</span>
42
+ })()}
43
+ {clientSentiment && (
44
+ <span
45
+ style={{ ...s.badge(`${clientSentiment.color}15`, clientSentiment.color), fontSize: '11px' }}
46
+ title={`Sentiment : ${clientSentiment.label}`}
47
+ >
48
+ {clientSentiment.emoji} {clientSentiment.label}
49
+ </span>
50
+ )}
51
+ {ticketSource && ticketSource !== 'portal' && (
52
+ <span style={s.badge('#f1f5f9', '#475569')}>{sourceLabels[ticketSource] || ticketSource}</span>
53
+ )}
54
+ {chatSession && ticketSource === 'live-chat' && (
55
+ <span style={{ fontSize: '11px', color: C.textMuted }}>
56
+ Session : {chatSession.slice(0, 16)}...
57
+ </span>
58
+ )}
59
+ {snoozeUntil && new Date(snoozeUntil) > new Date() && (
60
+ <span style={s.badge('#f5f3ff', '#7c3aed')}>
61
+ Snoozé {new Date(snoozeUntil).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}
62
+ </span>
63
+ )}
64
+ {satisfaction && (
65
+ <span style={s.badge(
66
+ satisfaction.rating >= 4 ? '#dcfce7' : satisfaction.rating >= 3 ? '#fef9c3' : '#fee2e2',
67
+ satisfaction.rating >= 4 ? '#166534' : satisfaction.rating >= 3 ? '#854d0e' : '#991b1b',
68
+ )}>
69
+ {Array.from({ length: 5 }, (_, i) => i < satisfaction.rating ? '\u2605' : '\u2606').join('')} {satisfaction.rating}/5
70
+ </span>
71
+ )}
72
+ <span style={{ marginLeft: 'auto', display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
73
+ <button
74
+ onClick={() => onCopyLink('admin')}
75
+ title="Copier le lien admin"
76
+ style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '13px', opacity: copiedLink === 'admin' ? 1 : 0.5, padding: '2px 4px' }}
77
+ >
78
+ {copiedLink === 'admin' ? '\u2705' : '\uD83D\uDCCB'}
79
+ </button>
80
+ <button
81
+ onClick={() => onCopyLink('client')}
82
+ title="Copier le lien client"
83
+ style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '13px', opacity: copiedLink === 'client' ? 1 : 0.5, padding: '2px 4px' }}
84
+ >
85
+ {copiedLink === 'client' ? '\u2705' : '\uD83D\uDD17'}
86
+ </button>
87
+ {copiedLink && <span style={{ fontSize: '11px', color: '#16a34a', fontWeight: 600 }}>Copié</span>}
88
+ </span>
89
+ </div>
90
+ )
91
+ }