@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.
- package/LICENSE +21 -0
- package/README.md +525 -0
- package/dist/client.cjs +7 -0
- package/dist/client.d.cts +3 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +5 -0
- package/dist/index.cjs +7766 -0
- package/dist/index.d.cts +384 -0
- package/dist/index.d.ts +384 -0
- package/dist/index.js +7730 -0
- package/dist/views.d.cts +30 -0
- package/dist/views.d.ts +30 -0
- package/package.json +131 -0
- package/src/client.ts +1 -0
- package/src/collections/AuthLogs.ts +65 -0
- package/src/collections/CannedResponses.ts +69 -0
- package/src/collections/ChatMessages.ts +98 -0
- package/src/collections/EmailLogs.ts +94 -0
- package/src/collections/KnowledgeBase.ts +99 -0
- package/src/collections/Macros.ts +98 -0
- package/src/collections/PendingEmails.ts +122 -0
- package/src/collections/SatisfactionSurveys.ts +98 -0
- package/src/collections/SlaPolicies.ts +123 -0
- package/src/collections/SupportClients.ts +210 -0
- package/src/collections/TicketActivityLog.ts +81 -0
- package/src/collections/TicketMessages.ts +364 -0
- package/src/collections/TicketStatuses.ts +108 -0
- package/src/collections/Tickets.ts +704 -0
- package/src/collections/TimeEntries.ts +105 -0
- package/src/collections/WebhookEndpoints.ts +96 -0
- package/src/collections/index.ts +16 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +85 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +140 -0
- package/src/components/TicketConversation/components/ActivityLog.tsx +39 -0
- package/src/components/TicketConversation/components/ClientBar.tsx +37 -0
- package/src/components/TicketConversation/components/ClientHistory.tsx +117 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +186 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +166 -0
- package/src/components/TicketConversation/components/QuickActions.tsx +82 -0
- package/src/components/TicketConversation/components/TicketHeader.tsx +91 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +161 -0
- package/src/components/TicketConversation/config.ts +82 -0
- package/src/components/TicketConversation/constants.ts +74 -0
- package/src/components/TicketConversation/context.ts +63 -0
- package/src/components/TicketConversation/hooks/useAI.ts +180 -0
- package/src/components/TicketConversation/hooks/useMessageActions.ts +131 -0
- package/src/components/TicketConversation/hooks/useReply.ts +190 -0
- package/src/components/TicketConversation/hooks/useTicketActions.ts +205 -0
- package/src/components/TicketConversation/hooks/useTimeTracking.ts +107 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +116 -0
- package/src/components/TicketConversation/index.tsx +1110 -0
- package/src/components/TicketConversation/locales/en.json +878 -0
- package/src/components/TicketConversation/locales/fr.json +878 -0
- package/src/components/TicketConversation/types.ts +54 -0
- package/src/components/TicketConversation/utils.ts +25 -0
- package/src/endpoints/admin-chat-stream.ts +238 -0
- package/src/endpoints/admin-chat.ts +263 -0
- package/src/endpoints/admin-stats.ts +200 -0
- package/src/endpoints/ai.ts +199 -0
- package/src/endpoints/apply-macro.ts +144 -0
- package/src/endpoints/auth-2fa.ts +163 -0
- package/src/endpoints/auto-close.ts +175 -0
- package/src/endpoints/billing.ts +167 -0
- package/src/endpoints/bulk-action.ts +103 -0
- package/src/endpoints/chat-stream.ts +127 -0
- package/src/endpoints/chat.ts +188 -0
- package/src/endpoints/chatbot.ts +113 -0
- package/src/endpoints/delete-account.ts +129 -0
- package/src/endpoints/email-stats.ts +109 -0
- package/src/endpoints/export-csv.ts +84 -0
- package/src/endpoints/export-data.ts +104 -0
- package/src/endpoints/import-conversation.ts +307 -0
- package/src/endpoints/index.ts +154 -0
- package/src/endpoints/login.ts +92 -0
- package/src/endpoints/merge-clients.ts +132 -0
- package/src/endpoints/merge-tickets.ts +137 -0
- package/src/endpoints/oauth-google.ts +179 -0
- package/src/endpoints/pending-emails-process.ts +224 -0
- package/src/endpoints/presence.ts +104 -0
- package/src/endpoints/process-scheduled.ts +144 -0
- package/src/endpoints/purge-logs.ts +58 -0
- package/src/endpoints/resend-notification.ts +99 -0
- package/src/endpoints/round-robin-config.ts +92 -0
- package/src/endpoints/satisfaction.ts +93 -0
- package/src/endpoints/search.ts +106 -0
- package/src/endpoints/seed-kb.ts +153 -0
- package/src/endpoints/settings.ts +144 -0
- package/src/endpoints/signature.ts +93 -0
- package/src/endpoints/sla-check.ts +124 -0
- package/src/endpoints/split-ticket.ts +131 -0
- package/src/endpoints/statuses.ts +45 -0
- package/src/endpoints/track-open.ts +154 -0
- package/src/endpoints/typing.ts +101 -0
- package/src/endpoints/user-prefs.ts +125 -0
- package/src/hooks/checkSLA.ts +414 -0
- package/src/hooks/ticketStatusEmail.ts +182 -0
- package/src/index.ts +51 -0
- package/src/plugin.ts +157 -0
- package/src/portal/LiveChat.tsx +1353 -0
- package/src/portal/auth/ChatWidget.tsx +350 -0
- package/src/portal/auth/ChatbotWidget.tsx +285 -0
- package/src/portal/auth/SupportHeader.tsx +409 -0
- package/src/portal/auth/dashboard/DashboardClient.tsx +650 -0
- package/src/portal/auth/dashboard/page.tsx +84 -0
- package/src/portal/auth/faq/FAQSearch.tsx +117 -0
- package/src/portal/auth/faq/page.tsx +199 -0
- package/src/portal/auth/layout.tsx +61 -0
- package/src/portal/auth/profile/page.tsx +705 -0
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/CollapsibleMessages.tsx +46 -0
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +50 -0
- package/src/portal/auth/tickets/detail/MessageActions.tsx +158 -0
- package/src/portal/auth/tickets/detail/PrintButton.tsx +16 -0
- package/src/portal/auth/tickets/detail/ReadReceipt.tsx +34 -0
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +74 -0
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +156 -0
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +57 -0
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +294 -0
- package/src/portal/auth/tickets/detail/TypingIndicator.tsx +58 -0
- package/src/portal/auth/tickets/detail/page.tsx +738 -0
- package/src/portal/auth/tickets/new/page.tsx +515 -0
- package/src/portal/forgot-password/page.tsx +114 -0
- package/src/portal/layout.tsx +26 -0
- package/src/portal/locales/en.json +374 -0
- package/src/portal/locales/fr.json +374 -0
- package/src/portal/login/page.tsx +351 -0
- package/src/portal/page.tsx +162 -0
- package/src/portal/register/page.tsx +281 -0
- package/src/portal/reset-password/page.tsx +152 -0
- package/src/styles/BillingView.module.scss +311 -0
- package/src/styles/ChatView.module.scss +438 -0
- package/src/styles/CommandPalette.module.scss +160 -0
- package/src/styles/CrmView.module.scss +554 -0
- package/src/styles/EmailTracking.module.scss +238 -0
- package/src/styles/ImportConversation.module.scss +267 -0
- package/src/styles/Layout.module.scss +55 -0
- package/src/styles/Logs.module.scss +164 -0
- package/src/styles/NewTicket.module.scss +143 -0
- package/src/styles/PendingEmails.module.scss +629 -0
- package/src/styles/SupportDashboard.module.scss +649 -0
- package/src/styles/TicketDetail.module.scss +1043 -0
- package/src/styles/TicketInbox.module.scss +296 -0
- package/src/styles/TicketingSettings.module.scss +358 -0
- package/src/styles/TimeDashboard.module.scss +287 -0
- package/src/styles/_tokens.scss +78 -0
- package/src/styles/theme.css +633 -0
- package/src/types.ts +255 -0
- package/src/utils/adminNotification.ts +38 -0
- package/src/utils/auth.ts +46 -0
- package/src/utils/emailTemplate.ts +343 -0
- package/src/utils/fireWebhooks.ts +84 -0
- package/src/utils/index.ts +22 -0
- package/src/utils/rateLimiter.ts +52 -0
- package/src/utils/readSettings.ts +67 -0
- package/src/utils/slugs.ts +54 -0
- package/src/utils/webhookDispatcher.ts +120 -0
- package/src/views/BillingView/client.tsx +137 -0
- package/src/views/BillingView/index.tsx +33 -0
- package/src/views/ChatView/client.tsx +294 -0
- package/src/views/ChatView/index.tsx +33 -0
- package/src/views/CrmView/client.tsx +206 -0
- package/src/views/CrmView/index.tsx +33 -0
- package/src/views/EmailTrackingView/client.tsx +124 -0
- package/src/views/EmailTrackingView/index.tsx +33 -0
- package/src/views/ImportConversationView/client.tsx +133 -0
- package/src/views/ImportConversationView/index.tsx +33 -0
- package/src/views/LogsView/client.tsx +151 -0
- package/src/views/LogsView/index.tsx +30 -0
- package/src/views/NewTicketView/client.tsx +227 -0
- package/src/views/NewTicketView/index.tsx +30 -0
- package/src/views/PendingEmailsView/client.tsx +177 -0
- package/src/views/PendingEmailsView/index.tsx +33 -0
- package/src/views/SupportDashboardView/client.tsx +424 -0
- package/src/views/SupportDashboardView/index.tsx +33 -0
- package/src/views/TicketDetailView/client.tsx +775 -0
- package/src/views/TicketDetailView/index.tsx +33 -0
- package/src/views/TicketInboxView/client.tsx +313 -0
- package/src/views/TicketInboxView/index.tsx +30 -0
- package/src/views/TicketingSettingsView/client.tsx +866 -0
- package/src/views/TicketingSettingsView/index.tsx +33 -0
- package/src/views/TimeDashboardView/client.tsx +144 -0
- package/src/views/TimeDashboardView/index.tsx +33 -0
- package/src/views/shared/AdminViewHeader.tsx +69 -0
- package/src/views/shared/ErrorBoundary.tsx +68 -0
- package/src/views/shared/Skeleton.tsx +125 -0
- package/src/views/shared/adminTokens.ts +37 -0
- package/src/views/shared/config.ts +82 -0
- package/src/views/shared/index.ts +6 -0
- 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
|
+
← {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'
|