@consilioweb/payload-support 0.15.0 → 1.0.0
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.
- package/README.md +347 -435
- package/dist/components/TicketConversation/RewriteDropdown.cjs +98 -0
- package/dist/components/TicketConversation/RewriteDropdown.js +93 -0
- package/dist/components/TicketConversation/SkeletonText.cjs +21 -0
- package/dist/components/TicketConversation/SkeletonText.js +20 -0
- package/dist/components/TicketConversation/components/ActionPanels.cjs +72 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +73 -2
- package/dist/components/TicketConversation/components/QuickActions.cjs +22 -1
- package/dist/components/TicketConversation/components/QuickActions.js +22 -1
- package/dist/components/TicketConversation/constants.cjs +27 -0
- package/dist/components/TicketConversation/constants.js +26 -1
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +20 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.js +19 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.cjs +59 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.js +59 -0
- package/dist/components/TicketConversation/index.cjs +42 -149
- package/dist/components/TicketConversation/index.js +37 -144
- package/dist/index.cjs +1835 -741
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1833 -741
- package/dist/views/BillingView/client.cjs +40 -54
- package/dist/views/BillingView/client.js +40 -54
- package/dist/views/ChatView/client.cjs +4 -3
- package/dist/views/ChatView/client.js +4 -3
- package/dist/views/CrmView/client.cjs +7 -7
- package/dist/views/CrmView/client.js +7 -7
- package/dist/views/EmailTrackingView/client.cjs +6 -5
- package/dist/views/EmailTrackingView/client.js +6 -5
- package/dist/views/ImportConversationView/client.cjs +4 -4
- package/dist/views/ImportConversationView/client.js +4 -4
- package/dist/views/LogsView/client.cjs +3 -2
- package/dist/views/LogsView/client.js +3 -2
- package/dist/views/NewTicketView/client.cjs +1 -1
- package/dist/views/NewTicketView/client.js +1 -1
- package/dist/views/PendingEmailsView/client.cjs +7 -6
- package/dist/views/PendingEmailsView/client.js +7 -6
- package/dist/views/SupportDashboardView/client.cjs +19 -22
- package/dist/views/SupportDashboardView/client.js +19 -22
- package/dist/views/TicketDetailView/NextActionItem.cjs +40 -0
- package/dist/views/TicketDetailView/NextActionItem.js +34 -0
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +80 -0
- package/dist/views/TicketDetailView/RewriteDropdown.js +78 -0
- package/dist/views/TicketDetailView/client.cjs +59 -190
- package/dist/views/TicketDetailView/client.js +52 -183
- package/dist/views/TicketDetailView/constants.cjs +20 -0
- package/dist/views/TicketDetailView/constants.js +16 -0
- package/dist/views/TicketDetailView/helpers.cjs +23 -0
- package/dist/views/TicketDetailView/helpers.js +20 -0
- package/dist/views/TicketDetailView/types.cjs +2 -0
- package/dist/views/TicketDetailView/types.js +1 -0
- package/dist/views/TicketInboxView/client.cjs +14 -10
- package/dist/views/TicketInboxView/client.js +14 -10
- package/dist/views/TicketingSettingsView/client.cjs +101 -96
- package/dist/views/TicketingSettingsView/client.js +92 -88
- package/dist/views/TimeDashboardView/client.cjs +2 -2
- package/dist/views/TimeDashboardView/client.js +2 -2
- package/dist/views/shared/StatusPill.cjs +23 -12
- package/dist/views/shared/StatusPill.js +23 -12
- package/dist/views/shared/dateLocale.cjs +5 -0
- package/dist/views/shared/dateLocale.js +3 -0
- package/package.json +23 -15
- package/src/__tests__/generateTrackingToken.test.ts +53 -0
- package/src/__tests__/integration/ai-agent.test.ts +46 -0
- package/src/__tests__/integration/auth.test.ts +43 -0
- package/src/__tests__/integration/automation-rules.test.ts +68 -0
- package/src/__tests__/integration/buildTestPayload.ts +52 -0
- package/src/__tests__/integration/channels.test.ts +52 -0
- package/src/__tests__/integration/core-behaviors.test.ts +58 -0
- package/src/__tests__/integration/dashboard-volume.test.ts +31 -0
- package/src/__tests__/integration/digest.test.ts +48 -0
- package/src/__tests__/integration/invoice-pdf.test.ts +30 -0
- package/src/__tests__/integration/isolation.test.ts +81 -0
- package/src/__tests__/integration/mentions-invoice.test.ts +50 -0
- package/src/__tests__/integration/nps.test.ts +37 -0
- package/src/__tests__/integration/sanitization.test.ts +35 -0
- package/src/__tests__/integration/sla-pause.test.ts +39 -0
- package/src/__tests__/integration/smoke.test.ts +26 -0
- package/src/__tests__/integration/snooze.test.ts +68 -0
- package/src/__tests__/integration/teams.test.ts +38 -0
- package/src/__tests__/rateLimiter.test.ts +89 -0
- package/src/__tests__/sanitizeHtml.test.ts +165 -0
- package/src/__tests__/sla.test.ts +258 -0
- package/src/collections/AutomationRules.ts +66 -0
- package/src/collections/NotificationQueue.ts +30 -0
- package/src/collections/SatisfactionSurveys.ts +11 -2
- package/src/collections/SupportClients.ts +58 -1
- package/src/collections/SupportTeam.ts +27 -0
- package/src/collections/TicketActivityLog.ts +9 -4
- package/src/collections/TicketMessages.ts +137 -31
- package/src/collections/Tickets.ts +31 -72
- package/src/collections/index.ts +3 -0
- package/src/components/TicketConversation/RewriteDropdown.tsx +86 -0
- package/src/components/TicketConversation/SkeletonText.tsx +23 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +82 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +25 -0
- package/src/components/TicketConversation/constants.ts +29 -0
- package/src/components/TicketConversation/hooks/useDocumentIdFromUrl.ts +21 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +51 -0
- package/src/components/TicketConversation/index.tsx +29 -157
- package/src/endpoints/admin-chat-stream.ts +3 -4
- package/src/endpoints/admin-chat.ts +11 -20
- package/src/endpoints/admin-stats.ts +70 -32
- package/src/endpoints/ai-agent.ts +36 -0
- package/src/endpoints/apply-macro.ts +8 -14
- package/src/endpoints/auth-2fa.ts +12 -12
- package/src/endpoints/auto-close.ts +38 -18
- package/src/endpoints/billing.ts +17 -4
- package/src/endpoints/bulk-action.ts +7 -12
- package/src/endpoints/channels.ts +89 -0
- package/src/endpoints/chat-stream.ts +3 -4
- package/src/endpoints/chat.ts +7 -12
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/client-intelligence.ts +4 -4
- package/src/endpoints/delete-account.ts +9 -16
- package/src/endpoints/email-stats.ts +2 -2
- package/src/endpoints/escalate.ts +2 -2
- package/src/endpoints/export-csv.ts +2 -2
- package/src/endpoints/export-data.ts +5 -8
- package/src/endpoints/import-conversation.ts +6 -10
- package/src/endpoints/index.ts +24 -2
- package/src/endpoints/invite-collaborator.ts +30 -15
- package/src/endpoints/invoice.ts +127 -0
- package/src/endpoints/kb-search.ts +2 -2
- package/src/endpoints/login.ts +12 -4
- package/src/endpoints/merge-clients.ts +14 -19
- package/src/endpoints/merge-tickets.ts +11 -18
- package/src/endpoints/oauth-google.ts +86 -39
- package/src/endpoints/pending-emails-process.ts +15 -25
- package/src/endpoints/process-digests.ts +93 -0
- package/src/endpoints/process-scheduled.ts +6 -10
- package/src/endpoints/process-snooze.ts +78 -0
- package/src/endpoints/purge-logs.ts +2 -2
- package/src/endpoints/resend-notification.ts +3 -4
- package/src/endpoints/round-robin-config.ts +3 -4
- package/src/endpoints/satisfaction.ts +21 -14
- package/src/endpoints/search.ts +5 -8
- package/src/endpoints/seed-kb.ts +3 -4
- package/src/endpoints/send-reminder.ts +186 -0
- package/src/endpoints/settings.ts +5 -2
- package/src/endpoints/signature.ts +3 -4
- package/src/endpoints/sla-check.ts +2 -2
- package/src/endpoints/split-ticket.ts +7 -12
- package/src/endpoints/statuses.ts +2 -2
- package/src/endpoints/ticket-feedback.ts +4 -6
- package/src/endpoints/ticket-synthesis.ts +2 -2
- package/src/endpoints/track-open.ts +16 -28
- package/src/endpoints/transfer-ticket.ts +21 -9
- package/src/endpoints/user-prefs.ts +3 -4
- package/src/hooks/automationRules.ts +84 -0
- package/src/hooks/checkSLA.ts +48 -16
- package/src/plugin.ts +6 -0
- package/src/portal/LiveChat.tsx +2 -63
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +14 -2
- package/src/portal/auth/tickets/detail/MessageActions.tsx +37 -18
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +10 -0
- package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +3 -2
- package/src/portal/auth/tickets/detail/page.tsx +2 -2
- package/src/portal/helpers.ts +46 -0
- package/src/portal/login/page.tsx +3 -8
- package/src/portal/types.ts +15 -0
- package/src/types/lucide-react.d.ts +34 -0
- package/src/types/payload-config.d.ts +10 -0
- package/src/utils/adminNotification.ts +2 -2
- package/src/utils/aiAgent.ts +147 -0
- package/src/utils/auth.ts +5 -4
- package/src/utils/channels.ts +36 -0
- package/src/utils/db.ts +73 -0
- package/src/utils/emailTemplate.ts +8 -1
- package/src/utils/fireWebhooks.ts +4 -6
- package/src/utils/generateTicketSynthesis.ts +7 -12
- package/src/utils/invoicePdf.ts +88 -0
- package/src/utils/notificationQueue.ts +36 -0
- package/src/utils/readSettings.ts +22 -6
- package/src/utils/sanitizeHtml.ts +57 -0
- package/src/utils/slugs.ts +6 -0
- package/src/utils/teamAccess.ts +27 -0
- package/src/utils/ticketAccess.ts +53 -0
- package/src/utils/webhookDispatcher.ts +4 -6
- package/src/views/BillingView/client.tsx +49 -37
- package/src/views/ChatView/client.tsx +4 -3
- package/src/views/CrmView/client.tsx +7 -7
- package/src/views/EmailTrackingView/client.tsx +7 -6
- package/src/views/ImportConversationView/client.tsx +4 -4
- package/src/views/LogsView/client.tsx +3 -2
- package/src/views/NewTicketView/client.tsx +1 -1
- package/src/views/PendingEmailsView/client.tsx +7 -6
- package/src/views/SupportDashboardView/client.tsx +21 -21
- package/src/views/TicketDetailView/NextActionItem.tsx +30 -0
- package/src/views/TicketDetailView/RewriteDropdown.tsx +63 -0
- package/src/views/TicketDetailView/client.tsx +54 -179
- package/src/views/TicketDetailView/constants.ts +23 -0
- package/src/views/TicketDetailView/helpers.ts +17 -0
- package/src/views/TicketDetailView/types.ts +10 -0
- package/src/views/TicketInboxView/client.tsx +16 -10
- package/src/views/TicketingSettingsView/client.tsx +90 -87
- package/src/views/TimeDashboardView/client.tsx +2 -2
- package/src/views/shared/StatusPill.tsx +24 -12
- package/src/views/shared/dateLocale.ts +5 -0
- package/src/views/shared/locales/en.json +708 -0
- package/src/views/shared/locales/fr.json +708 -0
|
@@ -71,14 +71,14 @@ function formatDuration(minutes: number): string {
|
|
|
71
71
|
return `${h}h${m}m`
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
function timeAgo(dateStr: string): string {
|
|
74
|
+
function timeAgo(dateStr: string, tFn: (k: string, vars?: Record<string, string | number>) => string): string {
|
|
75
75
|
const diff = Date.now() - new Date(dateStr).getTime()
|
|
76
76
|
const days = Math.floor(diff / 86400000)
|
|
77
|
-
if (days === 0) return
|
|
78
|
-
if (days === 1) return '
|
|
79
|
-
if (days < 30) return
|
|
77
|
+
if (days === 0) return tFn('crm.timeAgo.today')
|
|
78
|
+
if (days === 1) return tFn('crm.timeAgo.yesterday')
|
|
79
|
+
if (days < 30) return tFn('crm.timeAgo.daysAgo', { count: days })
|
|
80
80
|
const months = Math.floor(days / 30)
|
|
81
|
-
return
|
|
81
|
+
return tFn('crm.timeAgo.monthsAgo', { count: months })
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function getStatColorClass(key: string, detail: ClientDetail['stats']): string {
|
|
@@ -416,7 +416,7 @@ export const CrmClient: React.FC = () => {
|
|
|
416
416
|
{ key: 'open', label: t('crm.stats.openTickets'), value: String(detail.stats.openTickets) },
|
|
417
417
|
{ key: 'resolved', label: t('crm.stats.resolvedTickets'), value: String(detail.stats.resolvedTickets) },
|
|
418
418
|
{ key: 'time', label: t('crm.stats.timeSpent'), value: formatDuration(detail.stats.totalTimeMinutes) },
|
|
419
|
-
{ key: 'activity', label: t('crm.stats.lastActivity'), value: detail.stats.lastActivity ? timeAgo(detail.stats.lastActivity) : '-' },
|
|
419
|
+
{ key: 'activity', label: t('crm.stats.lastActivity'), value: detail.stats.lastActivity ? timeAgo(detail.stats.lastActivity, t) : '-' },
|
|
420
420
|
].map((stat) => (
|
|
421
421
|
<div key={stat.key} className={styles.statCard}>
|
|
422
422
|
<div className={styles.statLabel}>{stat.label}</div>
|
|
@@ -554,7 +554,7 @@ export const CrmClient: React.FC = () => {
|
|
|
554
554
|
</span>
|
|
555
555
|
</td>
|
|
556
556
|
<td className={styles.ticketDate}>
|
|
557
|
-
{timeAgo(tk.createdAt)}
|
|
557
|
+
{timeAgo(tk.createdAt, t)}
|
|
558
558
|
</td>
|
|
559
559
|
</tr>
|
|
560
560
|
))}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
4
4
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
5
|
+
import { DATE_LOCALE } from '../shared/dateLocale'
|
|
5
6
|
import s from '../../styles/EmailTracking.module.scss'
|
|
6
7
|
|
|
7
8
|
interface EmailLog {
|
|
@@ -30,14 +31,14 @@ interface Stats {
|
|
|
30
31
|
type StatusTab = 'all' | 'success' | 'error' | 'ignored'
|
|
31
32
|
type DateRange = 7 | 30 | 90
|
|
32
33
|
|
|
33
|
-
const STATUS_CFG: Record<string, {
|
|
34
|
-
success: {
|
|
35
|
-
error: {
|
|
36
|
-
ignored: {
|
|
34
|
+
const STATUS_CFG: Record<string, { labelKey: string; bg: string; color: string }> = {
|
|
35
|
+
success: { labelKey: 'logs.statusSuccess', bg: '#dcfce7', color: '#16a34a' },
|
|
36
|
+
error: { labelKey: 'emailTracking.errorLabel', bg: '#fef2f2', color: '#dc2626' },
|
|
37
|
+
ignored: { labelKey: 'logs.statusIgnored', bg: '#f3f4f6', color: '#6b7280' },
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function fmtDate(d: string): string {
|
|
40
|
-
return new Date(d).toLocaleDateString(
|
|
41
|
+
return new Date(d).toLocaleDateString(DATE_LOCALE, { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export const EmailTrackingClient: React.FC = () => {
|
|
@@ -129,7 +130,7 @@ export const EmailTrackingClient: React.FC = () => {
|
|
|
129
130
|
<td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.recipientEmail}>{log.recipientEmail || '—'}</td>
|
|
130
131
|
<td style={{ maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.subject}>{log.subject || '—'}</td>
|
|
131
132
|
<td>{log.ticketNumber ? <span className={s.ticketLink}>{log.ticketNumber}</span> : '—'}</td>
|
|
132
|
-
<td><span className={s.statusBadge} style={{ background: STATUS_CFG[log.status]?.bg, color: STATUS_CFG[log.status]?.color }}>{STATUS_CFG[log.status]?.
|
|
133
|
+
<td><span className={s.statusBadge} style={{ background: STATUS_CFG[log.status]?.bg, color: STATUS_CFG[log.status]?.color }}>{STATUS_CFG[log.status]?.labelKey ? t(STATUS_CFG[log.status].labelKey) : log.status}</span></td>
|
|
133
134
|
<td style={{ fontSize: 12 }}>{log.action || '—'}</td>
|
|
134
135
|
<td style={{ textAlign: 'right' }}>
|
|
135
136
|
{log.processingTimeMs != null ? (
|
|
@@ -96,13 +96,13 @@ export function ImportConversationClient() {
|
|
|
96
96
|
|
|
97
97
|
const data = await res.json()
|
|
98
98
|
if (!res.ok) {
|
|
99
|
-
setError(data.error || '
|
|
99
|
+
setError(data.error || t('import.analyzeError'))
|
|
100
100
|
return
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
setPreview(data)
|
|
104
104
|
} catch {
|
|
105
|
-
setError('
|
|
105
|
+
setError(t('import.networkError'))
|
|
106
106
|
} finally {
|
|
107
107
|
setLoading(false)
|
|
108
108
|
}
|
|
@@ -122,14 +122,14 @@ export function ImportConversationClient() {
|
|
|
122
122
|
|
|
123
123
|
const data = await res.json()
|
|
124
124
|
if (!res.ok) {
|
|
125
|
-
setError(data.error || '
|
|
125
|
+
setError(data.error || t('import.importError'))
|
|
126
126
|
return
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
setResult(data)
|
|
130
130
|
setPreview(null)
|
|
131
131
|
} catch {
|
|
132
|
-
setError('
|
|
132
|
+
setError(t('import.networkError'))
|
|
133
133
|
} finally {
|
|
134
134
|
setLoading(false)
|
|
135
135
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
4
4
|
import { useSearchParams } from 'next/navigation'
|
|
5
5
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
6
|
+
import { DATE_LOCALE } from '../shared/dateLocale'
|
|
6
7
|
import s from '../../styles/Logs.module.scss'
|
|
7
8
|
|
|
8
9
|
type LogType = 'email' | 'auth'
|
|
@@ -26,7 +27,7 @@ interface LogEntry {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function fmtDate(d: string): string {
|
|
29
|
-
return new Date(d).toLocaleDateString(
|
|
30
|
+
return new Date(d).toLocaleDateString(DATE_LOCALE, { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export const LogsClient: React.FC = () => {
|
|
@@ -83,7 +84,7 @@ export const LogsClient: React.FC = () => {
|
|
|
83
84
|
<div className={s.headerActions}>
|
|
84
85
|
<button className={s.purgeBtn} onClick={() => handlePurge(7)}>{t('logs.purge7')}</button>
|
|
85
86
|
<button className={s.purgeBtn} onClick={() => handlePurge(30)}>{t('logs.purge30')}</button>
|
|
86
|
-
<button className={s.purgeBtn} onClick={() => handlePurge(90)}>
|
|
87
|
+
<button className={s.purgeBtn} onClick={() => handlePurge(90)}>{t('logs.purge90')}</button>
|
|
87
88
|
<button className={`${s.purgeBtn} ${s.purgeBtnDanger}`} onClick={() => handlePurge(0)}>{t('logs.purgeAll')}</button>
|
|
88
89
|
</div>
|
|
89
90
|
</div>
|
|
@@ -190,7 +190,7 @@ export const NewTicketClient: React.FC = () => {
|
|
|
190
190
|
<div className={s.fieldGroup}>
|
|
191
191
|
<label className={s.label}>{t('newTicket.projectLabel')}</label>
|
|
192
192
|
<select className={s.select} value={projectId || ''} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : null)}>
|
|
193
|
-
<option value=""
|
|
193
|
+
<option value="">{t('newTicket.noProject')}</option>
|
|
194
194
|
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
195
195
|
</select>
|
|
196
196
|
</div>
|
|
@@ -32,15 +32,15 @@ interface PendingEmail {
|
|
|
32
32
|
|
|
33
33
|
type Tab = 'pending' | 'processed' | 'ignored'
|
|
34
34
|
|
|
35
|
-
function timeAgo(dateStr: string): string {
|
|
35
|
+
function timeAgo(dateStr: string, tFn: (k: string, vars?: Record<string, string | number>) => string): string {
|
|
36
36
|
const diff = Date.now() - new Date(dateStr).getTime()
|
|
37
37
|
const mins = Math.floor(diff / 60000)
|
|
38
|
-
if (mins < 1) return
|
|
39
|
-
if (mins < 60) return
|
|
38
|
+
if (mins < 1) return tFn('pendingEmails.timeAgo.justNow')
|
|
39
|
+
if (mins < 60) return tFn('pendingEmails.timeAgo.minutesAgo', { count: mins })
|
|
40
40
|
const hours = Math.floor(mins / 60)
|
|
41
|
-
if (hours < 24) return
|
|
41
|
+
if (hours < 24) return tFn('pendingEmails.timeAgo.hoursAgo', { count: hours })
|
|
42
42
|
const days = Math.floor(hours / 24)
|
|
43
|
-
return
|
|
43
|
+
return tFn('pendingEmails.timeAgo.daysAgo', { count: days })
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function TicketSearchModal({
|
|
@@ -333,6 +333,7 @@ function EmailCard({
|
|
|
333
333
|
onProcess: (action: 'create_ticket' | 'add_to_ticket' | 'ignore', ticketId?: number, clientId?: number) => void
|
|
334
334
|
processing: boolean
|
|
335
335
|
}) {
|
|
336
|
+
const { t } = useTranslation()
|
|
336
337
|
const [expanded, setExpanded] = useState(false)
|
|
337
338
|
const [showLinkModal, setShowLinkModal] = useState(false)
|
|
338
339
|
const [showClientPicker, setShowClientPicker] = useState(false)
|
|
@@ -364,7 +365,7 @@ function EmailCard({
|
|
|
364
365
|
</div>
|
|
365
366
|
<div className={styles.cardSubject}>{email.subject}</div>
|
|
366
367
|
<div className={styles.cardMeta}>
|
|
367
|
-
<span>{timeAgo(email.createdAt)}</span>
|
|
368
|
+
<span>{timeAgo(email.createdAt, t)}</span>
|
|
368
369
|
{attachmentCount > 0 && (
|
|
369
370
|
<span className={styles.attachment}>
|
|
370
371
|
<Paperclip size={12} /> {attachmentCount} PJ
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
5
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
6
|
+
import { DATE_LOCALE } from '../shared/dateLocale'
|
|
6
7
|
import styles from '../../styles/SupportDashboard.module.scss'
|
|
7
8
|
|
|
8
9
|
// ─── Types ──────────────────────────────────────────────────
|
|
@@ -14,6 +15,7 @@ interface Stats {
|
|
|
14
15
|
byCategory: Record<string, number>
|
|
15
16
|
createdLast7Days: number
|
|
16
17
|
createdLast30Days: number
|
|
18
|
+
volumeByDay?: Array<{ date: string; count: number }>
|
|
17
19
|
avgResponseTimeHours: number | null
|
|
18
20
|
avgResolutionTimeHours: number | null
|
|
19
21
|
totalTimeMinutes: number
|
|
@@ -92,6 +94,7 @@ function StatCard({ label, value, trend, accentColor, onClick }: {
|
|
|
92
94
|
accentColor?: string
|
|
93
95
|
onClick?: () => void
|
|
94
96
|
}) {
|
|
97
|
+
const { t } = useTranslation()
|
|
95
98
|
const style = accentColor ? { '--stat-accent': accentColor } as React.CSSProperties : undefined
|
|
96
99
|
const Tag = onClick ? 'button' : 'div'
|
|
97
100
|
|
|
@@ -114,7 +117,7 @@ function StatCard({ label, value, trend, accentColor, onClick }: {
|
|
|
114
117
|
)}
|
|
115
118
|
{trend && trend.dir === 'neutral' && (
|
|
116
119
|
<div className={`${styles.statTrend} ${styles.neutral}`}>
|
|
117
|
-
|
|
120
|
+
{t('dashboard.stable')}
|
|
118
121
|
</div>
|
|
119
122
|
)}
|
|
120
123
|
</Tag>
|
|
@@ -144,6 +147,7 @@ function VolumeChart({ data }: { data: { label: string; value: number }[] }) {
|
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
function CSATRing({ score, count }: { score: number; count: number }) {
|
|
150
|
+
const { t } = useTranslation()
|
|
147
151
|
// Ring progress: score is out of 5
|
|
148
152
|
const pct = score > 0 ? (score / 5) * 100 : 0
|
|
149
153
|
const radius = 42
|
|
@@ -171,10 +175,10 @@ function CSATRing({ score, count }: { score: number; count: number }) {
|
|
|
171
175
|
</div>
|
|
172
176
|
<div className={styles.csatMeta}>
|
|
173
177
|
<div className={styles.csatLabel}>
|
|
174
|
-
{score > 0 ? `${score.toFixed(1)} / 5` : '
|
|
178
|
+
{score > 0 ? `${score.toFixed(1)} / 5` : t('dashboard.csat.noData')}
|
|
175
179
|
</div>
|
|
176
180
|
<div className={styles.csatSub}>
|
|
177
|
-
{count > 0 ?
|
|
181
|
+
{count > 0 ? t('dashboard.csat.reviews', { count }) : t('dashboard.csat.noReviews')}
|
|
178
182
|
</div>
|
|
179
183
|
</div>
|
|
180
184
|
</div>
|
|
@@ -195,6 +199,7 @@ function formatSlaTime(minutes: number): string {
|
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
function SlaSection() {
|
|
202
|
+
const { t } = useTranslation()
|
|
198
203
|
const [sla, setSla] = useState<SlaData | null>(null)
|
|
199
204
|
const [slaLoading, setSlaLoading] = useState(true)
|
|
200
205
|
|
|
@@ -216,8 +221,8 @@ function SlaSection() {
|
|
|
216
221
|
if (slaLoading) {
|
|
217
222
|
return (
|
|
218
223
|
<div className={styles.slaSection}>
|
|
219
|
-
<h2 className={styles.slaSectionTitle}>
|
|
220
|
-
<div className={styles.slaLoading}>
|
|
224
|
+
<h2 className={styles.slaSectionTitle}>{t('dashboard.slaTitle')}</h2>
|
|
225
|
+
<div className={styles.slaLoading}>{t('dashboard.sla.loading')}</div>
|
|
221
226
|
</div>
|
|
222
227
|
)
|
|
223
228
|
}
|
|
@@ -230,7 +235,7 @@ function SlaSection() {
|
|
|
230
235
|
|
|
231
236
|
return (
|
|
232
237
|
<div className={styles.slaSection}>
|
|
233
|
-
<h2 className={styles.slaSectionTitle}>
|
|
238
|
+
<h2 className={styles.slaSectionTitle}>{t('dashboard.slaTitle')}</h2>
|
|
234
239
|
<div className={styles.slaGrid}>
|
|
235
240
|
{/* Breached */}
|
|
236
241
|
<div className={`${styles.slaCard} ${styles.slaBreach}`}>
|
|
@@ -241,14 +246,14 @@ function SlaSection() {
|
|
|
241
246
|
<line x1="12" y1="9" x2="12" y2="13" />
|
|
242
247
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
243
248
|
</svg>
|
|
244
|
-
|
|
249
|
+
{t('dashboard.sla.breached')}
|
|
245
250
|
</h3>
|
|
246
251
|
<span className={`${styles.slaCount} ${styles.slaCountBreach}`}>
|
|
247
252
|
{sla.breached.length}
|
|
248
253
|
</span>
|
|
249
254
|
</div>
|
|
250
255
|
{sla.breached.length === 0 ? (
|
|
251
|
-
<div className={styles.slaEmpty}>
|
|
256
|
+
<div className={styles.slaEmpty}>{t('dashboard.sla.noBreached')}</div>
|
|
252
257
|
) : (
|
|
253
258
|
<ul className={styles.slaList}>
|
|
254
259
|
{sla.breached.map((ticket) => {
|
|
@@ -284,14 +289,14 @@ function SlaSection() {
|
|
|
284
289
|
<circle cx="12" cy="12" r="10" />
|
|
285
290
|
<polyline points="12 6 12 12 16 14" />
|
|
286
291
|
</svg>
|
|
287
|
-
|
|
292
|
+
{t('dashboard.sla.atRisk')}
|
|
288
293
|
</h3>
|
|
289
294
|
<span className={`${styles.slaCount} ${styles.slaCountRisk}`}>
|
|
290
295
|
{sla.atRisk.length}
|
|
291
296
|
</span>
|
|
292
297
|
</div>
|
|
293
298
|
{sla.atRisk.length === 0 ? (
|
|
294
|
-
<div className={styles.slaEmpty}>
|
|
299
|
+
<div className={styles.slaEmpty}>{t('dashboard.sla.noAtRisk')}</div>
|
|
295
300
|
) : (
|
|
296
301
|
<ul className={styles.slaList}>
|
|
297
302
|
{sla.atRisk.map((ticket) => {
|
|
@@ -313,7 +318,7 @@ function SlaSection() {
|
|
|
313
318
|
<span className={styles.slaItemSubject}>{ticket.subject}</span>
|
|
314
319
|
</div>
|
|
315
320
|
<span className={`${styles.slaItemTime} ${styles.slaTimeRisk}`}>
|
|
316
|
-
{formatSlaTime(remainingMin)}
|
|
321
|
+
{t('dashboard.sla.remaining', { time: formatSlaTime(remainingMin) })}
|
|
317
322
|
</span>
|
|
318
323
|
</li>
|
|
319
324
|
)
|
|
@@ -372,17 +377,12 @@ export const SupportDashboardClient: React.FC = () => {
|
|
|
372
377
|
return () => window.removeEventListener('focus', onFocus)
|
|
373
378
|
}, [fetchData, sessionExpired])
|
|
374
379
|
|
|
375
|
-
//
|
|
380
|
+
// Real daily volume for the last 7 days (from admin-stats.volumeByDay).
|
|
376
381
|
const volumeData = useMemo(() => {
|
|
377
|
-
if (!stats) return []
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const base = Math.max(Math.floor(avg / 7), 0)
|
|
382
|
-
return days.map((label, i) => ({
|
|
383
|
-
label,
|
|
384
|
-
// Simple deterministic variation based on index
|
|
385
|
-
value: Math.max(base + ((i * 3 + 1) % 5) - 2, 0),
|
|
382
|
+
if (!stats?.volumeByDay?.length) return []
|
|
383
|
+
return stats.volumeByDay.map((d) => ({
|
|
384
|
+
label: new Date(d.date).toLocaleDateString(DATE_LOCALE, { weekday: 'short' }),
|
|
385
|
+
value: d.count,
|
|
386
386
|
}))
|
|
387
387
|
}, [stats])
|
|
388
388
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import s from '../../styles/TicketDetail.module.scss'
|
|
5
|
+
|
|
6
|
+
const labelMap: Record<string, string> = { now: 'Maintenant', today: "Aujourd'hui", 'this-week': 'Cette semaine' }
|
|
7
|
+
|
|
8
|
+
export function NextActionItem({ action, index, onToggle }: { action: any; index: number; onToggle: (done: boolean) => Promise<void> }) {
|
|
9
|
+
const [pending, setPending] = useState(false)
|
|
10
|
+
const done = !!action.done
|
|
11
|
+
return (
|
|
12
|
+
<li className={s.aiCardNextAction}>
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
className={`${s.aiCardNextCheck} ${done ? s.aiCardNextCheckDone : ''}`}
|
|
16
|
+
onClick={async () => {
|
|
17
|
+
setPending(true)
|
|
18
|
+
try { await onToggle(!done) } finally { setPending(false) }
|
|
19
|
+
}}
|
|
20
|
+
aria-pressed={done}
|
|
21
|
+
aria-label={done ? 'Marquer non fait' : 'Marquer fait'}
|
|
22
|
+
disabled={pending}
|
|
23
|
+
>
|
|
24
|
+
{done && <span aria-hidden>✓</span>}
|
|
25
|
+
</button>
|
|
26
|
+
<span className={`${s.aiCardNextLabel} ${done ? s.aiCardNextLabelDone : ''}`}>{action.label}</span>
|
|
27
|
+
<span className={s.aiCardNextWhen}>{labelMap[action.priority] || action.priority}</span>
|
|
28
|
+
</li>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
4
|
+
import { REWRITE_STYLES } from './constants'
|
|
5
|
+
|
|
6
|
+
export const RewriteDropdown: React.FC<{
|
|
7
|
+
disabled: boolean
|
|
8
|
+
loading: boolean
|
|
9
|
+
onSelect: (style: string) => void
|
|
10
|
+
toolbarBtnClass?: string
|
|
11
|
+
}> = ({ disabled, loading, onSelect, toolbarBtnClass }) => {
|
|
12
|
+
const [open, setOpen] = useState(false)
|
|
13
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!open) return
|
|
17
|
+
const close = (e: MouseEvent) => {
|
|
18
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
19
|
+
}
|
|
20
|
+
document.addEventListener('mousedown', close)
|
|
21
|
+
return () => document.removeEventListener('mousedown', close)
|
|
22
|
+
}, [open])
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
className={toolbarBtnClass}
|
|
29
|
+
onClick={() => setOpen(!open)}
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
style={{ fontSize: 11, fontWeight: 700, padding: '4px 10px', width: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}
|
|
32
|
+
>
|
|
33
|
+
{loading ? '...' : '✏️ Reformuler'}
|
|
34
|
+
{!loading && <span style={{ fontSize: 9, opacity: 0.6 }}>▼</span>}
|
|
35
|
+
</button>
|
|
36
|
+
{open && !disabled && !loading && (
|
|
37
|
+
<div style={{
|
|
38
|
+
position: 'absolute', bottom: '100%', left: 0, marginBottom: 4,
|
|
39
|
+
background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8,
|
|
40
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', zIndex: 100, minWidth: 180, overflow: 'hidden',
|
|
41
|
+
}}>
|
|
42
|
+
{REWRITE_STYLES.map((style) => (
|
|
43
|
+
<button
|
|
44
|
+
key={style.id}
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={() => { setOpen(false); onSelect(style.id) }}
|
|
47
|
+
style={{
|
|
48
|
+
display: 'flex', flexDirection: 'column', width: '100%', padding: '8px 12px',
|
|
49
|
+
border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'left',
|
|
50
|
+
borderBottom: '1px solid #f3f4f6',
|
|
51
|
+
}}
|
|
52
|
+
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = '#f9fafb' }}
|
|
53
|
+
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = 'transparent' }}
|
|
54
|
+
>
|
|
55
|
+
<span style={{ fontSize: 12, fontWeight: 600 }}>{style.label}</span>
|
|
56
|
+
<span style={{ fontSize: 10, color: '#9ca3af' }}>{style.desc}</span>
|
|
57
|
+
</button>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|