@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,161 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import type { TimeEntry } from '../types'
5
+ import { C, s } from '../constants'
6
+
7
+ interface TimeTrackingPanelProps {
8
+ timeEntries: TimeEntry[]
9
+ totalMinutes: number
10
+ // Timer
11
+ timerRunning: boolean
12
+ timerSeconds: number
13
+ setTimerSeconds: React.Dispatch<React.SetStateAction<number>>
14
+ timerDescription: string
15
+ setTimerDescription: (v: string) => void
16
+ handleTimerStart: (reset?: boolean) => void
17
+ handleTimerStop: () => void
18
+ handleTimerSave: () => void
19
+ handleTimerDiscard: () => void
20
+ // Manual entry
21
+ duration: string
22
+ setDuration: (v: string) => void
23
+ timeDescription: string
24
+ setTimeDescription: (v: string) => void
25
+ handleAddTime: () => void
26
+ addingTime: boolean
27
+ timeSuccess: string
28
+ }
29
+
30
+ export function TimeTrackingPanel({
31
+ timeEntries, totalMinutes,
32
+ timerRunning, timerSeconds, setTimerSeconds, timerDescription, setTimerDescription,
33
+ handleTimerStart, handleTimerStop, handleTimerSave, handleTimerDiscard,
34
+ duration, setDuration, timeDescription, setTimeDescription,
35
+ handleAddTime, addingTime, timeSuccess,
36
+ }: TimeTrackingPanelProps) {
37
+ const totalH = Math.floor(totalMinutes / 60)
38
+ const totalM = totalMinutes % 60
39
+
40
+ return (
41
+ <div style={s.section}>
42
+ <h4 style={s.sectionTitle}>
43
+ Temps
44
+ {totalMinutes > 0 && <span style={s.badge('#fef3c7', '#92400e')}>{totalH}h{String(totalM).padStart(2, '0')} total</span>}
45
+ </h4>
46
+
47
+ {/* Timer */}
48
+ <div style={{
49
+ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap',
50
+ padding: '12px 16px', borderRadius: '10px', marginBottom: '14px',
51
+ backgroundColor: timerRunning ? '#fef2f2' : 'var(--theme-elevation-100)',
52
+ border: timerRunning ? '1px solid #fca5a5' : '1px solid var(--theme-elevation-300)',
53
+ }}>
54
+ <div style={{
55
+ fontFamily: 'monospace', fontSize: '24px', fontWeight: 700,
56
+ color: timerRunning ? '#dc2626' : 'var(--theme-text)',
57
+ minWidth: '90px',
58
+ }}>
59
+ {String(Math.floor(timerSeconds / 3600)).padStart(2, '0')}:{String(Math.floor((timerSeconds % 3600) / 60)).padStart(2, '0')}:{String(timerSeconds % 60).padStart(2, '0')}
60
+ </div>
61
+
62
+ {!timerRunning && timerSeconds > 0 && (
63
+ <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
64
+ {[-5, -1, 1, 5, 15, 30].map((m) => (
65
+ <button
66
+ key={m}
67
+ onClick={() => setTimerSeconds((p) => Math.max(0, p + m * 60))}
68
+ style={{ width: Math.abs(m) >= 15 ? '32px' : '28px', height: '28px', borderRadius: '6px', border: '1px solid #e2e8f0', background: 'white', cursor: 'pointer', fontSize: Math.abs(m) >= 15 ? '12px' : '14px', fontWeight: 700, color: '#64748b' }}
69
+ title={`${m > 0 ? '+' : ''}${m} min`}
70
+ >
71
+ {m > 0 ? `+${m}` : String(m)}
72
+ </button>
73
+ ))}
74
+ </div>
75
+ )}
76
+
77
+ {!timerRunning && timerSeconds === 0 && (
78
+ <button onClick={() => handleTimerStart(true)} style={{ ...s.btn('#dc2626', false), fontSize: '12px', padding: '6px 16px', display: 'flex', alignItems: 'center', gap: '6px' }}>
79
+ &#9654; Démarrer
80
+ </button>
81
+ )}
82
+ {timerRunning && (
83
+ <button onClick={handleTimerStop} style={{ ...s.btn('#374151', false), fontSize: '12px', padding: '6px 16px', display: 'flex', alignItems: 'center', gap: '6px' }}>
84
+ &#9208; Pause
85
+ </button>
86
+ )}
87
+ {!timerRunning && timerSeconds > 0 && (
88
+ <>
89
+ <button onClick={() => handleTimerStart(false)} style={{ ...s.btn('#dc2626', false), fontSize: '12px', padding: '6px 14px' }}>
90
+ &#9654; Reprendre
91
+ </button>
92
+ <input
93
+ type="text"
94
+ value={timerDescription}
95
+ onChange={(e) => setTimerDescription(e.target.value)}
96
+ placeholder="Description..."
97
+ style={{ ...s.input, fontSize: '12px', flex: 1, minWidth: '120px' }}
98
+ />
99
+ <button
100
+ onClick={handleTimerSave}
101
+ disabled={addingTime || timerSeconds < 60}
102
+ style={{ ...s.btn('#16a34a', addingTime || timerSeconds < 60), fontSize: '12px', padding: '6px 14px' }}
103
+ title={timerSeconds < 60 ? 'Minimum 1 minute' : `Sauvegarder ${Math.round(timerSeconds / 60)} min`}
104
+ >
105
+ &#128190; {Math.round(timerSeconds / 60)} min
106
+ </button>
107
+ <button onClick={handleTimerDiscard} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '14px', color: '#94a3b8', padding: '4px' }} title="Annuler">
108
+ &#10005;
109
+ </button>
110
+ </>
111
+ )}
112
+ {timerRunning && (
113
+ <span style={{ fontSize: '11px', color: '#dc2626', fontWeight: 600 }}>
114
+ &#9679; Enregistrement en cours — session maintenue active
115
+ </span>
116
+ )}
117
+ </div>
118
+
119
+ {/* Manual time entry */}
120
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end', flexWrap: 'wrap', marginBottom: '14px' }}>
121
+ <div>
122
+ <label style={{ display: 'block', fontSize: '11px', color: C.textSecondary, marginBottom: '3px', fontWeight: 600 }}>Min</label>
123
+ <input type="number" min="1" value={duration} onChange={(e) => setDuration(e.target.value)} placeholder="30" style={{ ...s.input, width: '80px' }} />
124
+ </div>
125
+ <div style={{ flex: 1 }}>
126
+ <label style={{ display: 'block', fontSize: '11px', color: C.textSecondary, marginBottom: '3px', fontWeight: 600 }}>Description</label>
127
+ <input type="text" value={timeDescription} onChange={(e) => setTimeDescription(e.target.value)} placeholder="Travail effectué..." style={{ ...s.input, width: '100%' }} />
128
+ </div>
129
+ <button onClick={handleAddTime} disabled={addingTime || !duration} style={s.btn(C.amber, addingTime || !duration)}>
130
+ {addingTime ? '...' : '+ Temps'}
131
+ </button>
132
+ {timeSuccess && <span style={{ fontSize: '12px', color: '#16a34a', fontWeight: 600 }}>{timeSuccess}</span>}
133
+ </div>
134
+
135
+ {timeEntries.length > 0 && (
136
+ <div style={{ fontSize: '12px' }}>
137
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
138
+ <thead>
139
+ <tr style={{ borderBottom: `1px solid ${C.border}` }}>
140
+ <th style={{ textAlign: 'left', padding: '6px 8px', fontWeight: 600, fontSize: '11px', color: C.textSecondary }}>Date</th>
141
+ <th style={{ textAlign: 'left', padding: '6px 8px', fontWeight: 600, fontSize: '11px', color: C.textSecondary }}>Durée</th>
142
+ <th style={{ textAlign: 'left', padding: '6px 8px', fontWeight: 600, fontSize: '11px', color: C.textSecondary }}>Description</th>
143
+ </tr>
144
+ </thead>
145
+ <tbody>
146
+ {timeEntries.map((entry) => (
147
+ <tr key={entry.id} style={{ borderBottom: '1px solid #f1f5f9' }}>
148
+ <td style={{ padding: '6px 8px', color: '#374151' }}>
149
+ {new Date(entry.date).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
150
+ </td>
151
+ <td style={{ padding: '6px 8px', fontWeight: 600, color: '#374151' }}>{entry.duration} min</td>
152
+ <td style={{ padding: '6px 8px', color: C.textSecondary }}>{entry.description || '—'}</td>
153
+ </tr>
154
+ ))}
155
+ </tbody>
156
+ </table>
157
+ </div>
158
+ )}
159
+ </div>
160
+ )
161
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Feature flags for the ticketing module.
3
+ * Each feature can be enabled/disabled by the admin.
4
+ * When disabled, the corresponding UI section is hidden entirely.
5
+ */
6
+ export interface TicketingFeatures {
7
+ /** Time tracking: timer, manual entries, billing */
8
+ timeTracking: boolean
9
+ /** AI features: sentiment, synthesis, suggestion, rewrite */
10
+ ai: boolean
11
+ /** Satisfaction surveys: CSAT rating after resolution */
12
+ satisfaction: boolean
13
+ /** Live chat integration: chat → ticket conversion */
14
+ chat: boolean
15
+ /** Email tracking: pixel tracking, open/sent status per message */
16
+ emailTracking: boolean
17
+ /** Canned responses: quick reply templates */
18
+ canned: boolean
19
+ /** Ticket merge: combine two tickets into one */
20
+ merge: boolean
21
+ /** Snooze: temporarily hide a ticket */
22
+ snooze: boolean
23
+ /** External messages: add messages received outside the system */
24
+ externalMessages: boolean
25
+ /** Client history: past tickets, projects, notes sidebar */
26
+ clientHistory: boolean
27
+ /** Activity log: audit trail of actions on the ticket */
28
+ activityLog: boolean
29
+ /** Split ticket: extract a message into a new ticket */
30
+ splitTicket: boolean
31
+ /** Scheduled replies: send a message at a future date */
32
+ scheduledReplies: boolean
33
+ /** Auto-close: automatically resolve inactive tickets */
34
+ autoClose: boolean
35
+ /** Auto-close delay in days (used by auto-close cron) */
36
+ autoCloseDays: number
37
+ /** Round-robin: distribute new tickets evenly among agents */
38
+ roundRobin: boolean
39
+ }
40
+
41
+ /** Default features — all enabled */
42
+ export const DEFAULT_FEATURES: TicketingFeatures = {
43
+ timeTracking: true,
44
+ ai: true,
45
+ satisfaction: true,
46
+ chat: true,
47
+ emailTracking: true,
48
+ canned: true,
49
+ merge: true,
50
+ snooze: true,
51
+ externalMessages: true,
52
+ clientHistory: true,
53
+ activityLog: true,
54
+ splitTicket: true,
55
+ scheduledReplies: true,
56
+ autoClose: true,
57
+ autoCloseDays: 7,
58
+ roundRobin: false,
59
+ }
60
+
61
+ const STORAGE_KEY = 'ticketing_features'
62
+
63
+ /** Read features from localStorage (falls back to defaults) */
64
+ export function getFeatures(): TicketingFeatures {
65
+ if (typeof window === 'undefined') return DEFAULT_FEATURES
66
+ try {
67
+ const stored = localStorage.getItem(STORAGE_KEY)
68
+ if (stored) {
69
+ const parsed = JSON.parse(stored)
70
+ return { ...DEFAULT_FEATURES, ...parsed }
71
+ }
72
+ } catch { /* ignore */ }
73
+ return DEFAULT_FEATURES
74
+ }
75
+
76
+ /** Save features to localStorage */
77
+ export function saveFeatures(features: TicketingFeatures): void {
78
+ if (typeof window === 'undefined') return
79
+ try {
80
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(features))
81
+ } catch { /* ignore */ }
82
+ }
@@ -0,0 +1,74 @@
1
+ import type React from 'react'
2
+
3
+ export const statusLabels: Record<string, { label: string; bg: string; color: string }> = {
4
+ open: { label: 'Ouvert', bg: '#dbeafe', color: '#1e40af' },
5
+ waiting_client: { label: 'Attente', bg: '#fef3c7', color: '#92400e' },
6
+ resolved: { label: 'Résolu', bg: '#dcfce7', color: '#166534' },
7
+ }
8
+
9
+ export const projectStatusLabels: Record<string, { label: string; bg: string; color: string }> = {
10
+ active: { label: 'Actif', bg: '#dcfce7', color: '#166534' },
11
+ paused: { label: 'En pause', bg: '#fef3c7', color: '#92400e' },
12
+ completed: { label: 'Terminé', bg: '#e5e7eb', color: '#374151' },
13
+ }
14
+
15
+ // Color palette — Payload theme compatible
16
+ export const C = {
17
+ blue: '#2563eb',
18
+ amber: '#d97706',
19
+ orange: '#ea580c',
20
+ black: 'var(--theme-text)',
21
+ white: '#fff',
22
+ bg: '#fafafa',
23
+ textPrimary: 'var(--theme-text)',
24
+ textSecondary: '#6b7280',
25
+ textMuted: '#9ca3af',
26
+ border: '#e2e8f0',
27
+ statusOpen: '#2563eb',
28
+ statusProgress: '#ea580c',
29
+ statusWaiting: '#d97706',
30
+ statusResolved: '#16a34a',
31
+ statusClosed: '#6b7280',
32
+ adminBg: '#f8fafc',
33
+ adminBorder: '#2563eb',
34
+ clientBg: '#fafafa',
35
+ clientBorder: '#e2e8f0',
36
+ emailBg: '#fffbeb',
37
+ emailBorder: '#ea580c',
38
+ internalBg: '#fefce8',
39
+ internalBorder: '#d97706',
40
+ }
41
+
42
+ // Style factories
43
+ export const s = {
44
+ section: { borderTop: `1px solid ${C.border}`, paddingTop: '18px', marginTop: '20px' } as React.CSSProperties,
45
+ sectionTitle: { fontSize: '14px', fontWeight: 600, marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' } as React.CSSProperties,
46
+ btn: (bg: string, disabled?: boolean): React.CSSProperties => {
47
+ const hex = bg.replace('#', '')
48
+ const r = parseInt(hex.substring(0, 2), 16) || 0
49
+ const g = parseInt(hex.substring(2, 4), 16) || 0
50
+ const b = parseInt(hex.substring(4, 6), 16) || 0
51
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
52
+ const textColor = luminance > 0.6 ? '#374151' : C.white
53
+ return {
54
+ padding: '7px 14px', borderRadius: '6px', border: `1px solid ${C.border}`, backgroundColor: bg,
55
+ color: textColor, fontWeight: 600, fontSize: '13px', cursor: disabled ? 'not-allowed' : 'pointer',
56
+ opacity: disabled ? 0.5 : 1, whiteSpace: 'nowrap',
57
+ }
58
+ },
59
+ ghostBtn: (color: string, disabled?: boolean): React.CSSProperties => ({
60
+ padding: '7px 14px', borderRadius: '6px', border: `1px solid ${C.border}`, backgroundColor: 'transparent',
61
+ color, fontWeight: 600, fontSize: '13px', cursor: disabled ? 'not-allowed' : 'pointer',
62
+ opacity: disabled ? 0.5 : 1, whiteSpace: 'nowrap',
63
+ }),
64
+ outlineBtn: (color: string, disabled?: boolean): React.CSSProperties => ({
65
+ padding: '7px 14px', borderRadius: '6px', border: `1px solid ${color}`, backgroundColor: 'transparent',
66
+ color, fontWeight: 600, fontSize: '13px', cursor: disabled ? 'not-allowed' : 'pointer',
67
+ opacity: disabled ? 0.5 : 1, whiteSpace: 'nowrap',
68
+ }),
69
+ input: { padding: '8px 12px', borderRadius: '6px', border: `1px solid ${C.border}`, fontSize: '14px', color: '#374151', backgroundColor: C.white } as React.CSSProperties,
70
+ badge: (bg: string, color: string): React.CSSProperties => ({
71
+ display: 'inline-block', padding: '2px 8px', borderRadius: '4px', fontSize: '11px',
72
+ fontWeight: 600, backgroundColor: bg, color,
73
+ }),
74
+ }
@@ -0,0 +1,63 @@
1
+ import { createContext, useContext } from 'react'
2
+ import type { Message, TimeEntry, ClientInfo, CannedResponse, ActivityEntry, SatisfactionSurvey } from './types'
3
+
4
+ /**
5
+ * Handle interface for the RichTextEditor component.
6
+ * When used inside the plugin, the consumer must provide a compatible editor
7
+ * or leave the ref unused.
8
+ */
9
+ export interface RichTextEditorHandle {
10
+ setContent: (html: string) => void
11
+ clear: () => void
12
+ focus: () => void
13
+ }
14
+
15
+ export interface TicketContextValue {
16
+ // Identity
17
+ id: string | number | undefined
18
+
19
+ // Core data
20
+ messages: Message[]
21
+ timeEntries: TimeEntry[]
22
+ client: ClientInfo | null
23
+ cannedResponses: CannedResponse[]
24
+ activityLog: ActivityEntry[]
25
+ satisfaction: SatisfactionSurvey | null
26
+ loading: boolean
27
+
28
+ // Ticket metadata
29
+ currentStatus: string
30
+ ticketNumber: string
31
+ ticketSubject: string
32
+ ticketSource: string
33
+ chatSession: string
34
+ snoozeUntil: string | null
35
+ lastClientReadAt: string | null
36
+
37
+ // Client history
38
+ clientTickets: Array<{ id: number; ticketNumber: string; subject: string; status: string; createdAt: string }>
39
+ clientProjects: Array<{ id: number; name: string; status: string }>
40
+ clientNotes: string
41
+
42
+ // AI
43
+ clientSentiment: { emoji: string; label: string; color: string } | null
44
+
45
+ // Typing
46
+ clientTyping: boolean
47
+ clientTypingName: string
48
+
49
+ // Actions
50
+ fetchAll: () => void
51
+ sendAdminTyping: () => void
52
+
53
+ // Reply editor ref
54
+ replyEditorRef: React.RefObject<RichTextEditorHandle | null>
55
+ }
56
+
57
+ export const TicketContext = createContext<TicketContextValue | null>(null)
58
+
59
+ export function useTicketContext(): TicketContextValue {
60
+ const ctx = useContext(TicketContext)
61
+ if (!ctx) throw new Error('useTicketContext must be used within TicketContext.Provider')
62
+ return ctx
63
+ }
@@ -0,0 +1,180 @@
1
+ import { useState, useEffect } from 'react'
2
+ import type { Message, ClientInfo } from '../types'
3
+ import type { RichTextEditorHandle } from '../context'
4
+
5
+ export function useAI(
6
+ messages: Message[],
7
+ client: ClientInfo | null,
8
+ ticketSubject: string,
9
+ replyBody: string,
10
+ setReplyBody: (v: string) => void,
11
+ setReplyHtml: (v: string) => void,
12
+ replyEditorRef: React.RefObject<RichTextEditorHandle | null>,
13
+ ) {
14
+ const [clientSentiment, setClientSentiment] = useState<{ emoji: string; label: string; color: string } | null>(null)
15
+ const [aiReplying, setAiReplying] = useState(false)
16
+ const [aiRewriting, setAiRewriting] = useState(false)
17
+ const [showAiSummary, setShowAiSummary] = useState(false)
18
+ const [aiSummary, setAiSummary] = useState('')
19
+ const [aiGenerating, setAiGenerating] = useState(false)
20
+ const [aiSaving, setAiSaving] = useState(false)
21
+ const [aiSaved, setAiSaved] = useState(false)
22
+
23
+ // Sentiment analysis on last client message
24
+ useEffect(() => {
25
+ if (messages.length === 0) return
26
+ const lastClientMsg = [...messages].reverse().find((m) => m.authorType === 'client' || m.authorType === 'email')
27
+ if (!lastClientMsg) return
28
+
29
+ const analyze = async () => {
30
+ try {
31
+ const res = await fetch('/api/support/ai', {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ credentials: 'include',
35
+ body: JSON.stringify({ action: 'sentiment', text: lastClientMsg.body.slice(0, 500) }),
36
+ })
37
+ if (res.ok) {
38
+ const data = await res.json()
39
+ const raw = (data.sentiment || '').toLowerCase().trim()
40
+ const sentimentMap: Record<string, { emoji: string; label: string; color: string }> = {
41
+ 'frustré': { emoji: '\uD83D\uDE24', label: 'Frustré', color: '#dc2626' },
42
+ 'frustre': { emoji: '\uD83D\uDE24', label: 'Frustré', color: '#dc2626' },
43
+ 'mécontent': { emoji: '\uD83D\uDE20', label: 'Mécontent', color: '#ea580c' },
44
+ 'mecontent': { emoji: '\uD83D\uDE20', label: 'Mécontent', color: '#ea580c' },
45
+ 'urgent': { emoji: '\uD83D\uDD25', label: 'Urgent', color: '#dc2626' },
46
+ 'neutre': { emoji: '\uD83D\uDE10', label: 'Neutre', color: '#6b7280' },
47
+ 'satisfait': { emoji: '\uD83D\uDE0A', label: 'Satisfait', color: '#16a34a' },
48
+ }
49
+ const match = Object.keys(sentimentMap).find((k) => raw.includes(k))
50
+ if (match) setClientSentiment(sentimentMap[match])
51
+ else setClientSentiment({ emoji: '\uD83D\uDE10', label: 'Neutre', color: '#6b7280' })
52
+ }
53
+ } catch { /* silent */ }
54
+ }
55
+ analyze()
56
+ }, [messages.length]) // eslint-disable-line react-hooks/exhaustive-deps
57
+
58
+ const handleAiSuggestReply = async () => {
59
+ if (messages.length === 0) return
60
+ setAiReplying(true)
61
+ try {
62
+ const res = await fetch('/api/support/ai', {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ credentials: 'include',
66
+ body: JSON.stringify({
67
+ action: 'suggest_reply',
68
+ messages: messages.slice(-10).map((m) => ({ authorType: m.authorType, body: m.body })),
69
+ clientName: `${client?.firstName || ''} ${client?.lastName || ''}`.trim(),
70
+ clientCompany: client?.company,
71
+ }),
72
+ })
73
+ if (res.ok) {
74
+ const data = await res.json()
75
+ const suggestion = data.reply || ''
76
+ if (suggestion) {
77
+ setReplyBody(suggestion)
78
+ setReplyHtml(suggestion.replace(/\n/g, '<br/>'))
79
+ if (replyEditorRef.current?.setContent) {
80
+ replyEditorRef.current.setContent(suggestion.replace(/\n/g, '<br/>'))
81
+ }
82
+ }
83
+ }
84
+ } catch (err) {
85
+ console.error('AI suggest error:', err)
86
+ }
87
+ setAiReplying(false)
88
+ }
89
+
90
+ const handleAiRewrite = async () => {
91
+ if (!replyBody.trim()) return
92
+ setAiRewriting(true)
93
+ try {
94
+ const res = await fetch('/api/support/ai', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ credentials: 'include',
98
+ body: JSON.stringify({ action: 'rewrite', text: replyBody }),
99
+ })
100
+ if (res.ok) {
101
+ const data = await res.json()
102
+ const rewritten = data.rewritten || ''
103
+ if (rewritten) {
104
+ setReplyBody(rewritten)
105
+ setReplyHtml(rewritten.replace(/\n/g, '<br/>'))
106
+ if (replyEditorRef.current?.setContent) {
107
+ replyEditorRef.current.setContent(rewritten.replace(/\n/g, '<br/>'))
108
+ }
109
+ }
110
+ }
111
+ } catch (err) {
112
+ console.error('AI rewrite error:', err)
113
+ }
114
+ setAiRewriting(false)
115
+ }
116
+
117
+ const handleAiGenerate = async () => {
118
+ if (messages.length === 0) return
119
+ setAiGenerating(true)
120
+ setAiSummary('')
121
+ setAiSaved(false)
122
+ try {
123
+ const res = await fetch('/api/support/ai', {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ credentials: 'include',
127
+ body: JSON.stringify({
128
+ action: 'synthesis',
129
+ messages: messages.map((m) => ({ authorType: m.authorType, body: m.body, createdAt: m.createdAt })),
130
+ ticketSubject,
131
+ clientName: `${client?.firstName || ''} ${client?.lastName || ''}`.trim(),
132
+ clientCompany: client?.company,
133
+ }),
134
+ })
135
+ if (res.ok) {
136
+ const data = await res.json()
137
+ setAiSummary(data.synthesis || 'Aucune réponse générée')
138
+ } else {
139
+ setAiSummary('Erreur lors de la génération de la synthèse.')
140
+ }
141
+ } catch (err) {
142
+ setAiSummary(`Erreur de connexion : ${err instanceof Error ? err.message : 'erreur inconnue'}`)
143
+ } finally {
144
+ setAiGenerating(false)
145
+ }
146
+ }
147
+
148
+ const handleAiSave = async (id: string | number | undefined, fetchAll: () => void) => {
149
+ if (!id || !aiSummary) return
150
+ setAiSaving(true)
151
+ try {
152
+ const res = await fetch('/api/ticket-messages', {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ credentials: 'include',
156
+ body: JSON.stringify({
157
+ ticket: id,
158
+ body: `\uD83D\uDCCB **Synthèse IA (${new Date().toLocaleDateString('fr-FR')})**\n\n${aiSummary}`,
159
+ authorType: 'admin',
160
+ isInternal: true,
161
+ }),
162
+ })
163
+ if (res.ok) {
164
+ setAiSaved(true)
165
+ fetchAll()
166
+ }
167
+ } catch { /* ignore */ } finally {
168
+ setAiSaving(false)
169
+ }
170
+ }
171
+
172
+ return {
173
+ clientSentiment,
174
+ aiReplying, handleAiSuggestReply,
175
+ aiRewriting, handleAiRewrite,
176
+ showAiSummary, setShowAiSummary,
177
+ aiSummary, aiGenerating, handleAiGenerate,
178
+ aiSaving, aiSaved, handleAiSave,
179
+ }
180
+ }