@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,105 @@
1
+ import type { CollectionConfig, CollectionAfterChangeHook } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+
4
+ // ─── Hooks ───────────────────────────────────────────────
5
+
6
+ function createRecalculateTicketTime(slugs: CollectionSlugs): CollectionAfterChangeHook {
7
+ return async ({ doc, req }) => {
8
+ if (!doc.ticket) return
9
+
10
+ const { payload } = req
11
+ const ticketId = typeof doc.ticket === 'object' ? doc.ticket.id : doc.ticket
12
+
13
+ // Sum all time entries for this ticket, paginating to avoid loading all at once
14
+ let totalMinutes = 0
15
+ let page = 1
16
+ let hasMore = true
17
+
18
+ while (hasMore) {
19
+ const entries = await payload.find({
20
+ collection: slugs.timeEntries,
21
+ where: { ticket: { equals: ticketId } },
22
+ limit: 100,
23
+ page,
24
+ depth: 0,
25
+ overrideAccess: true,
26
+ select: { duration: true },
27
+ })
28
+
29
+ for (const entry of entries.docs) {
30
+ totalMinutes += ((entry.duration as number) || 0)
31
+ }
32
+
33
+ hasMore = entries.hasNextPage ?? false
34
+ page++
35
+ }
36
+
37
+ await payload.update({
38
+ collection: slugs.tickets,
39
+ id: ticketId,
40
+ data: { totalTimeMinutes: totalMinutes },
41
+ overrideAccess: true,
42
+ })
43
+ }
44
+ }
45
+
46
+ // ─── Collection factory ──────────────────────────────────
47
+
48
+ export function createTimeEntriesCollection(slugs: CollectionSlugs): CollectionConfig {
49
+ return {
50
+ slug: slugs.timeEntries,
51
+ labels: {
52
+ singular: 'Entrée de temps',
53
+ plural: 'Entrées de temps',
54
+ },
55
+ admin: {
56
+ group: 'Gestion',
57
+ defaultColumns: ['ticket', 'duration', 'description', 'date'],
58
+ },
59
+ fields: [
60
+ {
61
+ name: 'ticket',
62
+ type: 'relationship',
63
+ relationTo: slugs.tickets,
64
+ required: true,
65
+ label: 'Ticket',
66
+ },
67
+ {
68
+ type: 'row',
69
+ fields: [
70
+ {
71
+ name: 'duration',
72
+ type: 'number',
73
+ required: true,
74
+ label: 'Durée (minutes)',
75
+ min: 1,
76
+ admin: { width: '50%' },
77
+ },
78
+ {
79
+ name: 'date',
80
+ type: 'date',
81
+ required: true,
82
+ label: 'Date',
83
+ defaultValue: () => new Date().toISOString(),
84
+ admin: { width: '50%', date: { displayFormat: 'dd/MM/yyyy' } },
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ name: 'description',
90
+ type: 'textarea',
91
+ label: 'Description du travail',
92
+ },
93
+ ],
94
+ hooks: {
95
+ afterChange: [createRecalculateTicketTime(slugs)],
96
+ },
97
+ access: {
98
+ create: ({ req }) => req.user?.collection === slugs.users,
99
+ read: ({ req }) => req.user?.collection === slugs.users,
100
+ update: ({ req }) => req.user?.collection === slugs.users,
101
+ delete: ({ req }) => req.user?.collection === slugs.users,
102
+ },
103
+ timestamps: true,
104
+ }
105
+ }
@@ -0,0 +1,96 @@
1
+ import type { CollectionConfig } from 'payload'
2
+ import type { CollectionSlugs } from '../utils/slugs'
3
+
4
+ // ─── Collection factory ──────────────────────────────────
5
+
6
+ export function createWebhookEndpointsCollection(slugs: CollectionSlugs): CollectionConfig {
7
+ return {
8
+ slug: slugs.webhookEndpoints,
9
+ labels: {
10
+ singular: 'Webhook',
11
+ plural: 'Webhooks',
12
+ },
13
+ admin: {
14
+ useAsTitle: 'name',
15
+ group: 'Support',
16
+ defaultColumns: ['name', 'url', 'events', 'active', 'lastTriggeredAt', 'lastStatus'],
17
+ },
18
+ access: {
19
+ create: ({ req }) => req.user?.collection === slugs.users,
20
+ read: ({ req }) => req.user?.collection === slugs.users,
21
+ update: ({ req }) => req.user?.collection === slugs.users,
22
+ delete: ({ req }) => req.user?.collection === slugs.users,
23
+ },
24
+ fields: [
25
+ {
26
+ name: 'name',
27
+ type: 'text',
28
+ required: true,
29
+ label: 'Nom',
30
+ admin: {
31
+ description: 'Ex: Slack notifications, n8n workflow…',
32
+ },
33
+ },
34
+ {
35
+ name: 'url',
36
+ type: 'text',
37
+ required: true,
38
+ label: 'URL',
39
+ admin: {
40
+ description: 'URL du webhook à appeler (POST)',
41
+ },
42
+ },
43
+ {
44
+ name: 'secret',
45
+ type: 'text',
46
+ label: 'Secret HMAC',
47
+ admin: {
48
+ description: 'Secret optionnel pour signer les payloads (HMAC-SHA256, header X-Webhook-Signature)',
49
+ },
50
+ },
51
+ {
52
+ name: 'events',
53
+ type: 'select',
54
+ hasMany: true,
55
+ required: true,
56
+ label: 'Événements',
57
+ options: [
58
+ { label: 'Ticket créé', value: 'ticket_created' },
59
+ { label: 'Ticket résolu', value: 'ticket_resolved' },
60
+ { label: 'Réponse au ticket', value: 'ticket_replied' },
61
+ { label: 'Ticket assigné', value: 'ticket_assigned' },
62
+ { label: 'SLA dépassé', value: 'sla_breached' },
63
+ ],
64
+ admin: {
65
+ description: 'Événements qui déclenchent ce webhook',
66
+ },
67
+ },
68
+ {
69
+ name: 'active',
70
+ type: 'checkbox',
71
+ defaultValue: true,
72
+ label: 'Actif',
73
+ },
74
+ {
75
+ name: 'lastTriggeredAt',
76
+ type: 'date',
77
+ label: 'Dernier déclenchement',
78
+ admin: {
79
+ readOnly: true,
80
+ date: { displayFormat: 'dd/MM/yyyy HH:mm' },
81
+ position: 'sidebar',
82
+ },
83
+ },
84
+ {
85
+ name: 'lastStatus',
86
+ type: 'number',
87
+ label: 'Dernier statut HTTP',
88
+ admin: {
89
+ readOnly: true,
90
+ position: 'sidebar',
91
+ },
92
+ },
93
+ ],
94
+ timestamps: true,
95
+ }
96
+ }
@@ -0,0 +1,16 @@
1
+ export { createTicketsCollection } from './Tickets'
2
+ export { createTicketMessagesCollection } from './TicketMessages'
3
+ export { createSupportClientsCollection } from './SupportClients'
4
+ export { createTimeEntriesCollection } from './TimeEntries'
5
+ export { createCannedResponsesCollection } from './CannedResponses'
6
+ export { createTicketActivityLogCollection } from './TicketActivityLog'
7
+ export { createSatisfactionSurveysCollection } from './SatisfactionSurveys'
8
+ export { createKnowledgeBaseCollection } from './KnowledgeBase'
9
+ export { createChatMessagesCollection } from './ChatMessages'
10
+ export { createPendingEmailsCollection } from './PendingEmails'
11
+ export { createEmailLogsCollection } from './EmailLogs'
12
+ export { createAuthLogsCollection } from './AuthLogs'
13
+ export { createWebhookEndpointsCollection } from './WebhookEndpoints'
14
+ export { createSlaPoliciesCollection } from './SlaPolicies'
15
+ export { createMacrosCollection } from './Macros'
16
+ export { createTicketStatusesCollection } from './TicketStatuses'
@@ -0,0 +1,85 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { s } from '../constants'
5
+
6
+ interface AISummaryPanelProps {
7
+ showAiSummary: boolean
8
+ setShowAiSummary: (v: boolean) => void
9
+ aiSummary: string
10
+ aiGenerating: boolean
11
+ aiSaving: boolean
12
+ aiSaved: boolean
13
+ handleAiGenerate: () => void
14
+ handleAiSave: () => void
15
+ }
16
+
17
+ export function AISummaryPanel({
18
+ showAiSummary, setShowAiSummary, aiSummary, aiGenerating, aiSaving, aiSaved,
19
+ handleAiGenerate, handleAiSave,
20
+ }: AISummaryPanelProps) {
21
+ return (
22
+ <div style={{ marginBottom: '14px' }}>
23
+ <button
24
+ onClick={() => { setShowAiSummary(!showAiSummary); if (!showAiSummary && !aiSummary) handleAiGenerate() }}
25
+ style={{
26
+ ...s.ghostBtn('#7c3aed', false),
27
+ fontSize: '12px',
28
+ display: 'inline-flex',
29
+ alignItems: 'center',
30
+ gap: '6px',
31
+ }}
32
+ >
33
+ {showAiSummary ? 'Masquer la synthèse IA' : 'Synthèse IA'}
34
+ </button>
35
+ {showAiSummary && (
36
+ <div style={{
37
+ marginTop: '10px', padding: '14px 18px', borderRadius: '8px',
38
+ backgroundColor: '#faf5ff', border: '1px solid #e9d5ff',
39
+ }}>
40
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
41
+ <h4 style={{ fontSize: '13px', fontWeight: 600, color: '#7c3aed', margin: 0 }}>
42
+ Synthèse IA
43
+ </h4>
44
+ <div style={{ display: 'flex', gap: '6px' }}>
45
+ <button
46
+ onClick={handleAiGenerate}
47
+ disabled={aiGenerating}
48
+ style={{ ...s.outlineBtn('#7c3aed', aiGenerating), fontSize: '11px', padding: '4px 10px' }}
49
+ >
50
+ {aiGenerating ? 'Génération...' : 'Régénérer'}
51
+ </button>
52
+ {aiSummary && !aiGenerating && (
53
+ <button
54
+ onClick={handleAiSave}
55
+ disabled={aiSaving || aiSaved}
56
+ style={{ ...s.btn(aiSaved ? '#16a34a' : '#2563eb', aiSaving), fontSize: '11px', padding: '4px 10px' }}
57
+ >
58
+ {aiSaved ? 'Sauvegardé' : aiSaving ? 'Sauvegarde...' : 'Sauvegarder (note interne)'}
59
+ </button>
60
+ )}
61
+ </div>
62
+ </div>
63
+ {aiGenerating ? (
64
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#7c3aed', fontSize: '13px' }}>
65
+ Analyse de la conversation en cours...
66
+ </div>
67
+ ) : aiSummary ? (
68
+ <div
69
+ style={{ fontSize: '13px', lineHeight: '1.7', color: '#1e1b4b', whiteSpace: 'pre-wrap' }}
70
+ dangerouslySetInnerHTML={{
71
+ __html: aiSummary
72
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
73
+ .replace(/\n/g, '<br/>'),
74
+ }}
75
+ />
76
+ ) : (
77
+ <p style={{ color: '#999', fontStyle: 'italic', fontSize: '13px', margin: 0 }}>
78
+ Cliquez sur &quot;Régénérer&quot; pour lancer l&apos;analyse
79
+ </p>
80
+ )}
81
+ </div>
82
+ )}
83
+ </div>
84
+ )
85
+ }
@@ -0,0 +1,140 @@
1
+ 'use client'
2
+
3
+ import React, { useRef } from 'react'
4
+ import { s } from '../constants'
5
+
6
+ // ========== MERGE PANEL ==========
7
+ interface MergePanelProps {
8
+ mergeTarget: string
9
+ setMergeTarget: (v: string) => void
10
+ mergeTargetInfo: { id: number; ticketNumber: string; subject: string } | null
11
+ setMergeTargetInfo: (v: null) => void
12
+ mergeError: string
13
+ setMergeError: (v: string) => void
14
+ merging: boolean
15
+ handleMergeLookup: () => void
16
+ handleMerge: () => void
17
+ }
18
+
19
+ export function MergePanel({
20
+ mergeTarget, setMergeTarget, mergeTargetInfo, setMergeTargetInfo,
21
+ mergeError, setMergeError, merging, handleMergeLookup, handleMerge,
22
+ }: MergePanelProps) {
23
+ return (
24
+ <div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#fdf2f8', border: '1px solid #fbcfe8', marginBottom: '14px' }}>
25
+ <h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#831843' }}>Fusionner ce ticket dans un autre</h4>
26
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
27
+ <input
28
+ type="text"
29
+ value={mergeTarget}
30
+ onChange={(e) => { setMergeTarget(e.target.value); setMergeTargetInfo(null); setMergeError('') }}
31
+ placeholder="TK-0001"
32
+ style={{ ...s.input, width: '130px' }}
33
+ />
34
+ <button onClick={handleMergeLookup} style={{ ...s.outlineBtn('#ec4899'), fontSize: '12px', padding: '6px 14px' }}>
35
+ Rechercher
36
+ </button>
37
+ {mergeError && <span style={{ fontSize: '12px', color: '#be185d', fontWeight: 600 }}>{mergeError}</span>}
38
+ </div>
39
+ {mergeTargetInfo && (
40
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
41
+ <span style={{ fontSize: '13px', fontWeight: 600 }}>
42
+ {mergeTargetInfo.ticketNumber} — {mergeTargetInfo.subject}
43
+ </span>
44
+ <button onClick={handleMerge} disabled={merging} style={{ ...s.btn('#ec4899', merging), color: '#fff', fontSize: '12px', padding: '6px 14px' }}>
45
+ {merging ? 'Fusion...' : 'Confirmer la fusion'}
46
+ </button>
47
+ </div>
48
+ )}
49
+ </div>
50
+ )
51
+ }
52
+
53
+ // ========== EXTERNAL MESSAGE PANEL ==========
54
+ interface ExtMessagePanelProps {
55
+ extMsgBody: string
56
+ setExtMsgBody: (v: string) => void
57
+ extMsgAuthor: 'client' | 'admin'
58
+ setExtMsgAuthor: (v: 'client' | 'admin') => void
59
+ extMsgDate: string
60
+ setExtMsgDate: (v: string) => void
61
+ extMsgFiles: File[]
62
+ setExtMsgFiles: React.Dispatch<React.SetStateAction<File[]>>
63
+ sendingExtMsg: boolean
64
+ handleSendExtMsg: () => void
65
+ handleExtFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void
66
+ }
67
+
68
+ export function ExtMessagePanel({
69
+ extMsgBody, setExtMsgBody, extMsgAuthor, setExtMsgAuthor,
70
+ extMsgDate, setExtMsgDate, extMsgFiles, setExtMsgFiles,
71
+ sendingExtMsg, handleSendExtMsg, handleExtFileChange,
72
+ }: ExtMessagePanelProps) {
73
+ const extFileInputRef = useRef<HTMLInputElement>(null)
74
+
75
+ return (
76
+ <div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#eef2ff', border: '1px solid #c7d2fe', marginBottom: '14px' }}>
77
+ <h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#312e81' }}>Ajouter un message reçu (email, SMS, WhatsApp...)</h4>
78
+ <textarea
79
+ value={extMsgBody}
80
+ onChange={(e) => setExtMsgBody(e.target.value)}
81
+ placeholder="Coller le contenu du message reçu..."
82
+ rows={3}
83
+ style={{ ...s.input, width: '100%', resize: 'vertical', marginBottom: '10px' }}
84
+ />
85
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginBottom: '10px' }}>
86
+ <select value={extMsgAuthor} onChange={(e) => setExtMsgAuthor(e.target.value as 'client' | 'admin')} style={{ ...s.input, fontSize: '12px' }}>
87
+ <option value="client">Envoyé par le client</option>
88
+ <option value="admin">Envoyé par le support</option>
89
+ </select>
90
+ <input
91
+ type="datetime-local"
92
+ value={extMsgDate}
93
+ onChange={(e) => setExtMsgDate(e.target.value)}
94
+ style={{ ...s.input, fontSize: '12px' }}
95
+ />
96
+ <input ref={extFileInputRef} type="file" multiple onChange={handleExtFileChange} style={{ display: 'none' }} accept="image/*,.pdf,.doc,.docx,.txt,.zip" />
97
+ <button type="button" onClick={() => extFileInputRef.current?.click()} style={{ ...s.ghostBtn('#6b7280'), fontSize: '12px', padding: '6px 12px' }}>
98
+ + PJ
99
+ </button>
100
+ </div>
101
+ {extMsgFiles.length > 0 && (
102
+ <div style={{ marginBottom: '10px', display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
103
+ {extMsgFiles.map((file, i) => (
104
+ <span key={i} style={{ ...s.badge('#f1f5f9', '#374151'), display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
105
+ {file.name}
106
+ <button type="button" onClick={() => setExtMsgFiles((prev) => prev.filter((_, idx) => idx !== i))} style={{ border: 'none', background: 'none', color: '#ef4444', fontWeight: 700, cursor: 'pointer', fontSize: '14px', lineHeight: 1 }}>&times;</button>
107
+ </span>
108
+ ))}
109
+ </div>
110
+ )}
111
+ <button onClick={handleSendExtMsg} disabled={sendingExtMsg || !extMsgBody.trim()} style={s.btn('#818cf8', sendingExtMsg || !extMsgBody.trim())}>
112
+ {sendingExtMsg ? 'Ajout...' : 'Ajouter (sans notification)'}
113
+ </button>
114
+ </div>
115
+ )
116
+ }
117
+
118
+ // ========== SNOOZE PANEL ==========
119
+ interface SnoozePanelProps {
120
+ snoozeSaving: boolean
121
+ handleSnooze: (days: number | null, customDate?: string) => void
122
+ }
123
+
124
+ export function SnoozePanel({ snoozeSaving, handleSnooze }: SnoozePanelProps) {
125
+ return (
126
+ <div style={{ padding: '14px 18px', borderRadius: '8px', backgroundColor: '#faf5ff', border: '1px solid #e9d5ff', marginBottom: '14px' }}>
127
+ <h4 style={{ fontSize: '13px', fontWeight: 600, marginBottom: '10px', color: '#5b21b6' }}>Snooze — masquer temporairement</h4>
128
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
129
+ <button onClick={() => handleSnooze(1)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#8b5cf6', snoozeSaving), fontSize: '12px' }}>1 jour</button>
130
+ <button onClick={() => handleSnooze(3)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#8b5cf6', snoozeSaving), fontSize: '12px' }}>3 jours</button>
131
+ <button onClick={() => handleSnooze(7)} disabled={snoozeSaving} style={{ ...s.outlineBtn('#8b5cf6', snoozeSaving), fontSize: '12px' }}>1 semaine</button>
132
+ <input
133
+ type="datetime-local"
134
+ onChange={(e) => { if (e.target.value) handleSnooze(null, e.target.value) }}
135
+ style={{ ...s.input, fontSize: '12px' }}
136
+ />
137
+ </div>
138
+ </div>
139
+ )
140
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import type { ActivityEntry } from '../types'
5
+ import { C, s } from '../constants'
6
+
7
+ export function ActivityLog({ activityLog }: { activityLog: ActivityEntry[] }) {
8
+ const [showActivity, setShowActivity] = useState(false)
9
+
10
+ return (
11
+ <div style={s.section}>
12
+ <h4 style={{ ...s.sectionTitle, cursor: 'pointer' }} onClick={() => setShowActivity(!showActivity)}>
13
+ Historique <span style={s.badge('#f1f5f9', '#475569')}>{activityLog.length}</span>
14
+ <span style={{ fontSize: '12px', color: C.textMuted, transition: 'transform 0.2s', display: 'inline-block', transform: showActivity ? 'rotate(90deg)' : 'none' }}>&#9654;</span>
15
+ </h4>
16
+
17
+ {showActivity && (
18
+ activityLog.length === 0 ? (
19
+ <p style={{ fontSize: '12px', color: C.textMuted, fontStyle: 'italic' }}>Aucune activité enregistrée.</p>
20
+ ) : (
21
+ <div style={{ fontSize: '12px', display: 'flex', flexDirection: 'column', gap: '2px', borderLeft: '2px solid #bfdbfe', paddingLeft: '14px', marginLeft: '4px' }}>
22
+ {activityLog.map((entry) => (
23
+ <div key={entry.id} style={{ display: 'flex', gap: '10px', alignItems: 'center', padding: '5px 0', borderBottom: '1px solid #f8fafc' }}>
24
+ <span style={{ color: C.textMuted, fontSize: '11px', whiteSpace: 'nowrap', fontWeight: 500 }}>
25
+ {new Date(entry.createdAt).toLocaleString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}
26
+ </span>
27
+ <span style={s.badge(entry.actorType === 'admin' ? '#eff6ff' : '#dcfce7', entry.actorType === 'admin' ? '#1e40af' : '#166534')}>
28
+ {entry.actorType || 'system'}
29
+ </span>
30
+ <span style={{ color: '#374151', fontWeight: 500 }}>{(entry.detail || entry.action).replace(/\[object Object\]/g, '(utilisateur)')}</span>
31
+ <span style={{ color: C.textMuted, fontSize: '11px' }}>{entry.actorEmail}</span>
32
+ </div>
33
+ ))}
34
+ </div>
35
+ )
36
+ )}
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import type { ClientInfo } from '../types'
5
+ import { C, s } from '../constants'
6
+
7
+ export function ClientBar({ client }: { client: ClientInfo }) {
8
+ return (
9
+ <div style={{
10
+ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 16px',
11
+ borderRadius: '8px', backgroundColor: 'var(--theme-elevation-100)',
12
+ marginBottom: '16px', flexWrap: 'wrap',
13
+ }}>
14
+ <span style={{ fontSize: '16px' }}>&#128100;</span>
15
+ <span style={{ fontWeight: 700, fontSize: '13px', color: C.textPrimary }}>{client.company}</span>
16
+ <span style={{ color: C.textSecondary, fontSize: '13px' }}>
17
+ {client.firstName} {client.lastName}
18
+ </span>
19
+ <span style={{ color: C.textMuted, fontSize: '12px' }}>|</span>
20
+ <a href={`mailto:${client.email}`} style={{ color: '#2563eb', fontSize: '12px', fontWeight: 600, textDecoration: 'none' }}>{client.email}</a>
21
+ {client.phone && <span style={{ color: C.textSecondary, fontSize: '12px' }}>{client.phone}</span>}
22
+ <span style={{ marginLeft: 'auto', display: 'inline-flex', gap: '8px', alignItems: 'center' }}>
23
+ <a href={`/admin/collections/support-clients/${client.id}`} style={{ ...s.ghostBtn('#475569'), fontSize: '11px', padding: '4px 10px', textDecoration: 'none' }}>
24
+ Fiche client
25
+ </a>
26
+ <button
27
+ type="button"
28
+ onClick={() => window.open(`/api/admin/impersonate?clientId=${client.id}`, '_blank')}
29
+ style={{ ...s.ghostBtn('#7c3aed'), fontSize: '11px', padding: '4px 10px' }}
30
+ title="Se connecter au portail support en tant que ce client"
31
+ >
32
+ Voir en tant que client
33
+ </button>
34
+ </span>
35
+ </div>
36
+ )
37
+ }
@@ -0,0 +1,117 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import type { ClientInfo } from '../types'
5
+ import { statusLabels, projectStatusLabels, C, s } from '../constants'
6
+
7
+ interface ClientHistoryProps {
8
+ client: ClientInfo
9
+ clientTickets: Array<{ id: number; ticketNumber: string; subject: string; status: string; createdAt: string }>
10
+ clientProjects: Array<{ id: number; name: string; status: string }>
11
+ clientNotes: string
12
+ onNotesChange: (notes: string) => void
13
+ onNotesSave: () => void
14
+ savingNotes: boolean
15
+ notesSaved: boolean
16
+ }
17
+
18
+ export function ClientHistory({
19
+ client: _client, clientTickets, clientProjects, clientNotes, onNotesChange, onNotesSave, savingNotes, notesSaved,
20
+ }: ClientHistoryProps) {
21
+ const [showClientHistory, setShowClientHistory] = useState(false)
22
+
23
+ return (
24
+ <div style={s.section}>
25
+ <button
26
+ type="button"
27
+ onClick={() => setShowClientHistory(!showClientHistory)}
28
+ style={{
29
+ background: 'none', border: `1px dashed ${C.border}`, borderRadius: '6px',
30
+ padding: '8px 14px', cursor: 'pointer', color: C.textSecondary, fontSize: '13px',
31
+ fontWeight: 600, width: '100%', textAlign: 'left',
32
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
33
+ }}
34
+ >
35
+ <span>Historique client ({clientTickets.length} ticket{clientTickets.length !== 1 ? 's' : ''}, {clientProjects.length} projet{clientProjects.length !== 1 ? 's' : ''})</span>
36
+ <span style={{ fontSize: '12px', transition: 'transform 0.2s', display: 'inline-block', transform: showClientHistory ? 'rotate(90deg)' : 'none' }}>&#9654;</span>
37
+ </button>
38
+ {showClientHistory && (
39
+ <div style={{ marginTop: '10px', padding: '14px 18px', borderRadius: '8px', backgroundColor: C.white, border: `1px solid ${C.border}` }}>
40
+ {/* Past tickets */}
41
+ <h5 style={{ fontSize: '12px', fontWeight: 600, color: C.textSecondary, marginBottom: '8px' }}>Derniers tickets</h5>
42
+ {clientTickets.length === 0 ? (
43
+ <p style={{ fontSize: '12px', color: C.textMuted, fontStyle: 'italic', marginBottom: '14px' }}>Aucun autre ticket</p>
44
+ ) : (
45
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', marginBottom: '14px' }}>
46
+ {clientTickets.map((t) => {
47
+ const st = statusLabels[t.status] || statusLabels.open
48
+ return (
49
+ <a
50
+ key={t.id}
51
+ href={`/admin/support/ticket?id=${t.id}`}
52
+ style={{
53
+ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 10px',
54
+ borderRadius: '6px', border: `1px solid ${C.border}`, textDecoration: 'none',
55
+ fontSize: '12px', color: '#374151', backgroundColor: '#fafafa',
56
+ }}
57
+ >
58
+ <span style={{ fontFamily: "'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace", fontWeight: 600, color: C.textMuted, fontSize: '11px', whiteSpace: 'nowrap' }}>{t.ticketNumber}</span>
59
+ <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 600 }}>{t.subject}</span>
60
+ <span style={s.badge(st.bg, st.color)}>{st.label}</span>
61
+ <span style={{ fontSize: '10px', color: C.textMuted, whiteSpace: 'nowrap' }}>
62
+ {new Date(t.createdAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
63
+ </span>
64
+ </a>
65
+ )
66
+ })}
67
+ </div>
68
+ )}
69
+
70
+ {/* Projects */}
71
+ <h5 style={{ fontSize: '12px', fontWeight: 600, color: C.textSecondary, marginBottom: '8px' }}>Projets</h5>
72
+ {clientProjects.length === 0 ? (
73
+ <p style={{ fontSize: '12px', color: C.textMuted, fontStyle: 'italic', marginBottom: '14px' }}>Aucun projet</p>
74
+ ) : (
75
+ <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '14px' }}>
76
+ {clientProjects.map((p) => {
77
+ const ps = projectStatusLabels[p.status] || projectStatusLabels.active
78
+ return (
79
+ <a
80
+ key={p.id}
81
+ href={`/admin/collections/projects/${p.id}`}
82
+ style={{
83
+ display: 'inline-flex', alignItems: 'center', gap: '6px', padding: '5px 10px',
84
+ borderRadius: '6px', border: `1px solid ${C.border}`, textDecoration: 'none',
85
+ fontSize: '12px', fontWeight: 600, color: '#374151', backgroundColor: '#fafafa',
86
+ }}
87
+ >
88
+ {p.name}
89
+ <span style={s.badge(ps.bg, ps.color)}>{ps.label}</span>
90
+ </a>
91
+ )
92
+ })}
93
+ </div>
94
+ )}
95
+
96
+ {/* Notes */}
97
+ <h5 style={{ fontSize: '12px', fontWeight: 600, color: C.textSecondary, marginBottom: '8px' }}>Notes internes</h5>
98
+ <textarea
99
+ value={clientNotes}
100
+ onChange={(e) => onNotesChange(e.target.value)}
101
+ rows={3}
102
+ style={{ ...s.input, width: '100%', resize: 'vertical', fontSize: '12px', marginBottom: '8px' }}
103
+ placeholder="Notes sur ce client..."
104
+ />
105
+ <button
106
+ type="button"
107
+ onClick={onNotesSave}
108
+ disabled={savingNotes}
109
+ style={{ ...s.btn(notesSaved ? '#16a34a' : C.blue, savingNotes), fontSize: '11px', padding: '5px 12px' }}
110
+ >
111
+ {notesSaved ? 'Sauvegarde OK' : savingNotes ? 'Sauvegarde...' : 'Sauvegarder les notes'}
112
+ </button>
113
+ </div>
114
+ )}
115
+ </div>
116
+ )
117
+ }