@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,33 @@
1
+ import type { AdminViewServerProps } from 'payload'
2
+ import { DefaultTemplate } from '@payloadcms/next/templates'
3
+ import { redirect } from 'next/navigation'
4
+ import React from 'react'
5
+ import { AdminErrorBoundary } from '../shared/ErrorBoundary'
6
+ import { TicketingSettingsClient } from './client'
7
+
8
+ export const TicketingSettingsView: React.FC<AdminViewServerProps> = ({ initPageResult }) => {
9
+ const { req, visibleEntities } = initPageResult
10
+
11
+ if (!req.user) {
12
+ redirect('/admin/login')
13
+ }
14
+
15
+ return (
16
+ <DefaultTemplate
17
+ i18n={req.i18n}
18
+ locale={initPageResult.locale}
19
+ params={{}}
20
+ payload={req.payload}
21
+ permissions={initPageResult.permissions}
22
+ searchParams={{}}
23
+ user={req.user}
24
+ visibleEntities={visibleEntities}
25
+ >
26
+ <AdminErrorBoundary viewName="TicketingSettingsView">
27
+ <TicketingSettingsClient />
28
+ </AdminErrorBoundary>
29
+ </DefaultTemplate>
30
+ )
31
+ }
32
+
33
+ export default TicketingSettingsView
@@ -0,0 +1,144 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useCallback } from 'react'
4
+ import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
5
+ import s from '../../styles/TimeDashboard.module.scss'
6
+
7
+ interface TimeEntry {
8
+ id: number
9
+ ticket: number | { id: number; ticketNumber?: string; subject?: string; project?: number | { id: number; name?: string } }
10
+ duration: number
11
+ description: string
12
+ date: string
13
+ }
14
+
15
+ interface GroupedData { label: string; entries: TimeEntry[]; totalMinutes: number }
16
+
17
+ function formatDuration(minutes: number): string { const h = Math.floor(minutes / 60); const m = minutes % 60; if (h === 0) return `${m}min`; if (m === 0) return `${h}h`; return `${h}h${m}m` }
18
+ function getWeekNumber(d: Date): number { const oneJan = new Date(d.getFullYear(), 0, 1); return Math.ceil(((d.getTime() - oneJan.getTime()) / 86400000 + oneJan.getDay() + 1) / 7) }
19
+ function getMonthRange(offset: number): { from: string; to: string } { const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth() + offset, 1); const end = new Date(now.getFullYear(), now.getMonth() + offset + 1, 0); return { from: start.toISOString().split('T')[0], to: end.toISOString().split('T')[0] } }
20
+ const MONTHS_FR = ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aou', 'Sep', 'Oct', 'Nov', 'Dec']
21
+
22
+ export const TimeDashboardClient: React.FC = () => {
23
+ const { t } = useTranslation()
24
+ const [entries, setEntries] = useState<TimeEntry[]>([])
25
+ const [loading, setLoading] = useState(true)
26
+ const [from, setFrom] = useState(() => getMonthRange(0).from)
27
+ const [to, setTo] = useState(() => getMonthRange(0).to)
28
+ const [groupBy, setGroupBy] = useState<'day' | 'week' | 'project'>('day')
29
+
30
+ const fetchEntries = useCallback(async () => {
31
+ setLoading(true)
32
+ try {
33
+ const params = new URLSearchParams({ limit: '500', depth: '2', sort: '-date' })
34
+ if (from) params.set('where[date][greater_than_equal]', from)
35
+ if (to) params.set('where[date][less_than_equal]', to)
36
+ const res = await fetch(`/api/time-entries?${params}`)
37
+ if (res.ok) { const json = await res.json(); setEntries(json.docs || []) }
38
+ } catch { /* silent */ }
39
+ setLoading(false)
40
+ }, [from, to])
41
+
42
+ useEffect(() => { fetchEntries() }, [fetchEntries])
43
+
44
+ const totalMinutes = entries.reduce((sum, e) => sum + (e.duration || 0), 0)
45
+
46
+ const grouped: GroupedData[] = React.useMemo(() => {
47
+ const map = new Map<string, TimeEntry[]>()
48
+ for (const entry of entries) {
49
+ let key: string
50
+ if (groupBy === 'day') { key = entry.date ? entry.date.split('T')[0] : 'Sans date' }
51
+ else if (groupBy === 'week') { const d = new Date(entry.date); key = `Semaine ${getWeekNumber(d)} (${MONTHS_FR[d.getMonth()]} ${d.getFullYear()})` }
52
+ else { const ticket = typeof entry.ticket === 'object' ? entry.ticket : null; const project = ticket && typeof ticket.project === 'object' ? ticket.project : null; key = project?.name || 'Sans projet' }
53
+ if (!map.has(key)) map.set(key, [])
54
+ map.get(key)!.push(entry)
55
+ }
56
+ return Array.from(map.entries()).map(([label, items]) => ({ label, entries: items, totalMinutes: items.reduce((sum, e) => sum + (e.duration || 0), 0) }))
57
+ }, [entries, groupBy])
58
+
59
+ const dailyChart = React.useMemo(() => {
60
+ const dayMap = new Map<string, number>()
61
+ for (const entry of entries) { const day = entry.date ? entry.date.split('T')[0] : null; if (day) dayMap.set(day, (dayMap.get(day) || 0) + (entry.duration || 0)) }
62
+ return Array.from(dayMap.entries()).sort(([a], [b]) => a.localeCompare(b)).slice(-30).map(([day, mins]) => ({ day, minutes: mins }))
63
+ }, [entries])
64
+
65
+ const maxDailyMinutes = Math.max(...dailyChart.map((d) => d.minutes), 1)
66
+ const setPeriod = (range: { from: string; to: string }) => { setFrom(range.from); setTo(range.to) }
67
+
68
+ const S: Record<string, React.CSSProperties> = {
69
+ page: { padding: '20px 30px', maxWidth: 1100, margin: '0 auto' },
70
+ btn: { padding: '6px 12px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, cursor: 'pointer', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' },
71
+ btnPrimary: { padding: '6px 12px', borderRadius: 6, border: 'none', fontSize: 12, cursor: 'pointer', background: '#2563eb', color: '#fff', fontWeight: 600 },
72
+ kpis: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 20 },
73
+ kpiCard: { padding: '16px 20px', borderRadius: 10, border: '1px solid var(--theme-elevation-150)' },
74
+ groupCard: { marginBottom: 12, borderRadius: 10, border: '1px solid var(--theme-elevation-150)', overflow: 'hidden' },
75
+ groupHeader: { display: 'flex', justifyContent: 'space-between', padding: '10px 16px', background: 'var(--theme-elevation-50)', borderBottom: '1px solid var(--theme-elevation-150)' },
76
+ table: { width: '100%', borderCollapse: 'collapse' as const, fontSize: 12 },
77
+ td: { padding: '6px 8px', borderBottom: '1px solid var(--theme-elevation-100)' },
78
+ }
79
+
80
+ return (
81
+ <div style={S.page}>
82
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
83
+ <div>
84
+ <h1 style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>{t('timeDashboard.title')}</h1>
85
+ <p style={{ fontSize: 13, color: 'var(--theme-elevation-500)', margin: '4px 0 0' }}>{t('timeDashboard.subtitle')}</p>
86
+ </div>
87
+ <a href="/admin/collections/time-entries/create" style={{ ...S.btnPrimary, textDecoration: 'none', display: 'inline-block' }}>{t('timeDashboard.newEntry')}</a>
88
+ </div>
89
+
90
+ <div style={{ marginBottom: 16 }}>
91
+ <div style={{ display: 'flex', gap: 6, marginBottom: 8 }}>
92
+ <button style={S.btnPrimary} onClick={() => setPeriod(getMonthRange(0))}>{t('timeDashboard.filters.thisMonth')}</button>
93
+ <button style={S.btn} onClick={() => setPeriod(getMonthRange(-1))}>{t('timeDashboard.filters.lastMonth')}</button>
94
+ <button style={S.btn} onClick={() => setPeriod(getMonthRange(-2))}>{t('timeDashboard.filters.twoMonthsAgo')}</button>
95
+ </div>
96
+ <div style={{ display: 'flex', gap: 12, alignItems: 'flex-end' }}>
97
+ <div><label style={{ fontSize: 11, fontWeight: 600, display: 'block', marginBottom: 4, color: 'var(--theme-elevation-500)' }}>{t('timeDashboard.filters.from')}</label><input type="date" value={from} onChange={(e) => setFrom(e.target.value)} style={{ padding: '6px 10px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }} /></div>
98
+ <div><label style={{ fontSize: 11, fontWeight: 600, display: 'block', marginBottom: 4, color: 'var(--theme-elevation-500)' }}>{t('timeDashboard.filters.to')}</label><input type="date" value={to} onChange={(e) => setTo(e.target.value)} style={{ padding: '6px 10px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }} /></div>
99
+ <div><label style={{ fontSize: 11, fontWeight: 600, display: 'block', marginBottom: 4, color: 'var(--theme-elevation-500)' }}>{t('timeDashboard.filters.groupBy')}</label><select value={groupBy} onChange={(e) => setGroupBy(e.target.value as 'day' | 'week' | 'project')} style={{ padding: '6px 10px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }}><option value="day">{t('timeDashboard.filters.day')}</option><option value="week">{t('timeDashboard.filters.week')}</option><option value="project">{t('timeDashboard.filters.project')}</option></select></div>
100
+ </div>
101
+ </div>
102
+
103
+ {loading ? <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('common.loading')}</div> : (
104
+ <>
105
+ <div style={S.kpis}>
106
+ <div style={S.kpiCard}><div style={{ fontSize: 11, color: 'var(--theme-elevation-500)' }}>{t('timeDashboard.kpis.totalTime')}</div><div style={{ fontSize: 24, fontWeight: 700, color: '#2563eb' }}>{formatDuration(totalMinutes)}</div></div>
107
+ <div style={S.kpiCard}><div style={{ fontSize: 11, color: 'var(--theme-elevation-500)' }}>{t('timeDashboard.kpis.entries')}</div><div style={{ fontSize: 24, fontWeight: 700, color: '#d97706' }}>{entries.length}</div></div>
108
+ <div style={S.kpiCard}><div style={{ fontSize: 11, color: 'var(--theme-elevation-500)' }}>{t('timeDashboard.kpis.dailyAverage')}</div><div style={{ fontSize: 24, fontWeight: 700, color: '#ea580c' }}>{dailyChart.length > 0 ? formatDuration(Math.round(totalMinutes / dailyChart.length)) : '-'}</div></div>
109
+ </div>
110
+
111
+ {dailyChart.length > 0 && (
112
+ <div style={{ padding: 16, borderRadius: 10, border: '1px solid var(--theme-elevation-150)', marginBottom: 20 }}>
113
+ <div style={{ fontSize: 13, fontWeight: 700, marginBottom: 8 }}>{t('timeDashboard.chart.title')}</div>
114
+ <div style={{ display: 'flex', alignItems: 'flex-end', gap: 3, height: 80 }}>
115
+ {dailyChart.map((d) => <div key={d.day} style={{ flex: 1, background: '#3b82f6', borderRadius: '3px 3px 0 0', height: `${Math.max((d.minutes / maxDailyMinutes) * 100, 4)}%` }} title={`${d.day}: ${formatDuration(d.minutes)}`} />)}
116
+ </div>
117
+ </div>
118
+ )}
119
+
120
+ {grouped.length === 0 ? <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('timeDashboard.empty')}</div> : grouped.map((group) => (
121
+ <div key={group.label} style={S.groupCard}>
122
+ <div style={S.groupHeader}><span style={{ fontWeight: 700 }}>{group.label}</span><span style={{ fontWeight: 700, color: '#2563eb' }}>{formatDuration(group.totalMinutes)}</span></div>
123
+ <table style={S.table}>
124
+ <tbody>
125
+ {group.entries.map((entry) => {
126
+ const ticket = typeof entry.ticket === 'object' ? entry.ticket : null
127
+ return (
128
+ <tr key={entry.id}>
129
+ <td style={S.td}>{ticket ? <a href={`/admin/collections/tickets/${ticket.id}`} style={{ color: '#2563eb', textDecoration: 'none', fontWeight: 600 }}>{ticket.ticketNumber || `#${ticket.id}`}</a> : '-'}</td>
130
+ <td style={S.td}>{ticket?.subject || ''}</td>
131
+ <td style={{ ...S.td, color: 'var(--theme-elevation-500)' }}>{entry.description || '-'}</td>
132
+ <td style={{ ...S.td, textAlign: 'right', fontWeight: 600 }}>{formatDuration(entry.duration)}</td>
133
+ </tr>
134
+ )
135
+ })}
136
+ </tbody>
137
+ </table>
138
+ </div>
139
+ ))}
140
+ </>
141
+ )}
142
+ </div>
143
+ )
144
+ }
@@ -0,0 +1,33 @@
1
+ import type { AdminViewServerProps } from 'payload'
2
+ import { DefaultTemplate } from '@payloadcms/next/templates'
3
+ import { redirect } from 'next/navigation'
4
+ import React from 'react'
5
+ import { AdminErrorBoundary } from '../shared/ErrorBoundary'
6
+ import { TimeDashboardClient } from './client'
7
+
8
+ export const TimeDashboardView: React.FC<AdminViewServerProps> = ({ initPageResult }) => {
9
+ const { req, visibleEntities } = initPageResult
10
+
11
+ if (!req.user) {
12
+ redirect('/admin/login')
13
+ }
14
+
15
+ return (
16
+ <DefaultTemplate
17
+ i18n={req.i18n}
18
+ locale={initPageResult.locale}
19
+ params={{}}
20
+ payload={req.payload}
21
+ permissions={initPageResult.permissions}
22
+ searchParams={{}}
23
+ user={req.user}
24
+ visibleEntities={visibleEntities}
25
+ >
26
+ <AdminErrorBoundary viewName="TimeDashboardView">
27
+ <TimeDashboardClient />
28
+ </AdminErrorBoundary>
29
+ </DefaultTemplate>
30
+ )
31
+ }
32
+
33
+ export default TimeDashboardView
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { V } from './adminTokens'
5
+
6
+ interface AdminViewHeaderProps {
7
+ icon: React.ReactNode
8
+ title: string
9
+ subtitle?: string
10
+ breadcrumb?: { label: string; href: string }
11
+ actions?: React.ReactNode
12
+ }
13
+
14
+ export const AdminViewHeader: React.FC<AdminViewHeaderProps> = ({
15
+ icon,
16
+ title,
17
+ subtitle,
18
+ breadcrumb,
19
+ actions,
20
+ }) => {
21
+ return (
22
+ <div
23
+ style={{
24
+ display: 'flex',
25
+ justifyContent: 'space-between',
26
+ alignItems: 'flex-start',
27
+ marginBottom: 20,
28
+ }}
29
+ >
30
+ <div>
31
+ {breadcrumb && (
32
+ <div style={{ marginBottom: 6 }}>
33
+ <a
34
+ href={breadcrumb.href}
35
+ style={{
36
+ fontSize: 12,
37
+ fontWeight: 700,
38
+ color: V.cyan,
39
+ textDecoration: 'none',
40
+ textTransform: 'uppercase',
41
+ }}
42
+ >
43
+ &larr; {breadcrumb.label}
44
+ </a>
45
+ </div>
46
+ )}
47
+ <h1
48
+ style={{
49
+ fontSize: 28,
50
+ fontWeight: 600,
51
+ letterSpacing: 0,
52
+ margin: 0,
53
+ color: V.text,
54
+ display: 'flex',
55
+ alignItems: 'center',
56
+ gap: 10,
57
+ }}
58
+ >
59
+ <span style={{ color: V.cyan, display: 'flex', alignItems: 'center' }}>{icon}</span>
60
+ {title}
61
+ </h1>
62
+ {subtitle && (
63
+ <p style={{ color: V.textSecondary, margin: '4px 0 0', fontSize: 14 }}>{subtitle}</p>
64
+ )}
65
+ </div>
66
+ {actions && <div style={{ display: 'flex', gap: 8 }}>{actions}</div>}
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,68 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+
5
+ interface ErrorBoundaryProps {
6
+ children: React.ReactNode
7
+ fallback?: React.ReactNode
8
+ viewName?: string
9
+ }
10
+
11
+ interface ErrorBoundaryState {
12
+ hasError: boolean
13
+ error: Error | null
14
+ }
15
+
16
+ export class AdminErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
17
+ constructor(props: ErrorBoundaryProps) {
18
+ super(props)
19
+ this.state = { hasError: false, error: null }
20
+ }
21
+
22
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
23
+ return { hasError: true, error }
24
+ }
25
+
26
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
27
+ console.error(`[${this.props.viewName || 'AdminView'}] Error:`, error, errorInfo)
28
+ }
29
+
30
+ render(): React.ReactNode {
31
+ if (this.state.hasError) {
32
+ if (this.props.fallback) {
33
+ return this.props.fallback
34
+ }
35
+
36
+ return (
37
+ <div style={{
38
+ padding: '40px',
39
+ textAlign: 'center',
40
+ color: '#dc2626',
41
+ }}>
42
+ <h2 style={{ marginBottom: '16px', fontSize: '18px' }}>
43
+ Une erreur est survenue
44
+ </h2>
45
+ <p style={{ marginBottom: '24px', color: '#6b7280', fontSize: '14px' }}>
46
+ {this.state.error?.message || 'Erreur inattendue'}
47
+ </p>
48
+ <button
49
+ onClick={() => this.setState({ hasError: false, error: null })}
50
+ style={{
51
+ padding: '8px 20px',
52
+ backgroundColor: '#2563eb',
53
+ color: '#fff',
54
+ border: 'none',
55
+ borderRadius: '6px',
56
+ cursor: 'pointer',
57
+ fontSize: '14px',
58
+ }}
59
+ >
60
+ Reessayer
61
+ </button>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ return this.props.children
67
+ }
68
+ }
@@ -0,0 +1,125 @@
1
+ 'use client'
2
+
3
+ import React, { useEffect } from 'react'
4
+
5
+ // ---- Style injection ----
6
+
7
+ const styleId = 'skeleton-shimmer-style'
8
+
9
+ function injectSkeletonStyles() {
10
+ if (typeof document === 'undefined') return
11
+ if (document.getElementById(styleId)) return
12
+ const style = document.createElement('style')
13
+ style.id = styleId
14
+ style.textContent = `
15
+ @keyframes skeleton-shimmer {
16
+ 0% { background-position: 200% 0; }
17
+ 100% { background-position: -200% 0; }
18
+ }
19
+ `
20
+ document.head.appendChild(style)
21
+ }
22
+
23
+ // ---- Base Skeleton ----
24
+
25
+ interface SkeletonProps {
26
+ width?: string | number
27
+ height?: string | number
28
+ borderRadius?: string
29
+ style?: React.CSSProperties
30
+ }
31
+
32
+ export function Skeleton({ width = '100%', height = '20px', borderRadius = '4px', style }: SkeletonProps) {
33
+ useEffect(() => {
34
+ injectSkeletonStyles()
35
+ }, [])
36
+
37
+ return (
38
+ <div
39
+ style={{
40
+ width,
41
+ height,
42
+ borderRadius,
43
+ background: 'linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%)',
44
+ backgroundSize: '200% 100%',
45
+ animation: 'skeleton-shimmer 1.5s infinite',
46
+ ...style,
47
+ }}
48
+ />
49
+ )
50
+ }
51
+
52
+ // ---- Skeleton Text ----
53
+
54
+ export function SkeletonText({ lines = 3, width = '100%' }: { lines?: number; width?: string | number }) {
55
+ return (
56
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', width }}>
57
+ {Array.from({ length: lines }).map((_, i) => (
58
+ <Skeleton key={i} height="16px" width={i === lines - 1 ? '60%' : '100%'} />
59
+ ))}
60
+ </div>
61
+ )
62
+ }
63
+
64
+ // ---- Skeleton Card ----
65
+
66
+ export function SkeletonCard({ height = '200px' }: { height?: string }) {
67
+ return (
68
+ <div style={{
69
+ border: '1px solid #e5e7eb',
70
+ borderRadius: '8px',
71
+ padding: '20px',
72
+ display: 'flex',
73
+ flexDirection: 'column',
74
+ gap: '12px',
75
+ minHeight: height,
76
+ }}>
77
+ <Skeleton height="24px" width="40%" />
78
+ <SkeletonText lines={3} />
79
+ <Skeleton height="32px" width="120px" borderRadius="6px" />
80
+ </div>
81
+ )
82
+ }
83
+
84
+ // ---- Skeleton Table ----
85
+
86
+ export function SkeletonTable({ rows = 5, columns = 4 }: { rows?: number; columns?: number }) {
87
+ return (
88
+ <div style={{ width: '100%' }}>
89
+ {/* Header */}
90
+ <div style={{ display: 'flex', gap: '16px', padding: '12px 0', borderBottom: '2px solid #e5e7eb' }}>
91
+ {Array.from({ length: columns }).map((_, i) => (
92
+ <Skeleton key={i} height="16px" width={`${100 / columns}%`} />
93
+ ))}
94
+ </div>
95
+ {/* Rows */}
96
+ {Array.from({ length: rows }).map((_, rowIdx) => (
97
+ <div key={rowIdx} style={{ display: 'flex', gap: '16px', padding: '12px 0', borderBottom: '1px solid #f3f4f6' }}>
98
+ {Array.from({ length: columns }).map((_, colIdx) => (
99
+ <Skeleton key={colIdx} height="14px" width={`${100 / columns}%`} />
100
+ ))}
101
+ </div>
102
+ ))}
103
+ </div>
104
+ )
105
+ }
106
+
107
+ // ---- Skeleton Dashboard ----
108
+
109
+ export function SkeletonDashboard() {
110
+ return (
111
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', padding: '20px 0' }}>
112
+ {/* Stats row */}
113
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' }}>
114
+ {Array.from({ length: 4 }).map((_, i) => (
115
+ <div key={i} style={{ padding: '20px', border: '1px solid #e5e7eb', borderRadius: '8px' }}>
116
+ <Skeleton height="14px" width="60%" style={{ marginBottom: '8px' }} />
117
+ <Skeleton height="32px" width="40%" />
118
+ </div>
119
+ ))}
120
+ </div>
121
+ {/* Table */}
122
+ <SkeletonTable rows={5} columns={4} />
123
+ </div>
124
+ )
125
+ }
@@ -0,0 +1,37 @@
1
+ import type React from 'react'
2
+
3
+ // Shared design tokens for all admin views (professional style)
4
+ export const V = {
5
+ text: 'var(--theme-text)',
6
+ textSecondary: 'var(--theme-elevation-500)',
7
+ bg: 'var(--theme-elevation-50)',
8
+ bgCard: 'var(--theme-elevation-100)',
9
+ border: 'var(--theme-elevation-300)',
10
+ // Professional palette
11
+ blue: '#2563eb',
12
+ amber: '#d97706',
13
+ orange: '#ea580c',
14
+ green: '#16a34a',
15
+ red: '#dc2626',
16
+ // Legacy aliases (backward compat for views not yet updated)
17
+ cyan: '#2563eb',
18
+ yellow: '#d97706',
19
+ } as const
20
+
21
+ // Button style factory
22
+ export const btnStyle = (
23
+ bg: string,
24
+ opts?: { disabled?: boolean; small?: boolean },
25
+ ): React.CSSProperties => ({
26
+ padding: opts?.small ? '6px 12px' : '8px 14px',
27
+ borderRadius: 6,
28
+ border: `1px solid var(--theme-elevation-300)`,
29
+ backgroundColor: bg,
30
+ color: '#fff',
31
+ fontWeight: 600,
32
+ fontSize: opts?.small ? 12 : 13,
33
+ cursor: opts?.disabled ? 'not-allowed' : 'pointer',
34
+ opacity: opts?.disabled ? 0.5 : 1,
35
+ textDecoration: 'none',
36
+ whiteSpace: 'nowrap' as const,
37
+ })
@@ -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,6 @@
1
+ export { AdminErrorBoundary } from './ErrorBoundary'
2
+ export { V, btnStyle } from './adminTokens'
3
+ export { AdminViewHeader } from './AdminViewHeader'
4
+ export { Skeleton, SkeletonText, SkeletonCard, SkeletonTable, SkeletonDashboard } from './Skeleton'
5
+ export { getFeatures, saveFeatures, DEFAULT_FEATURES } from './config'
6
+ export type { TicketingFeatures } from './config'
package/src/views.ts ADDED
@@ -0,0 +1,16 @@
1
+ // Admin view components — server wrappers + client components
2
+ // Each view follows the pattern: index.tsx (server) + client.tsx ('use client')
3
+
4
+ export { TicketInboxView } from './views/TicketInboxView'
5
+ export { TicketDetailView } from './views/TicketDetailView'
6
+ export { SupportDashboardView } from './views/SupportDashboardView'
7
+ export { NewTicketView } from './views/NewTicketView'
8
+ export { TicketingSettingsView } from './views/TicketingSettingsView'
9
+ export { LogsView } from './views/LogsView'
10
+ export { ChatView } from './views/ChatView'
11
+ export { CrmView } from './views/CrmView'
12
+ export { PendingEmailsView } from './views/PendingEmailsView'
13
+ export { EmailTrackingView } from './views/EmailTrackingView'
14
+ export { BillingView } from './views/BillingView'
15
+ export { TimeDashboardView } from './views/TimeDashboardView'
16
+ export { ImportConversationView } from './views/ImportConversationView'