@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,227 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { useRouter } from 'next/navigation'
|
|
6
|
+
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
7
|
+
import s from '../../styles/NewTicket.module.scss'
|
|
8
|
+
|
|
9
|
+
interface ClientOption { id: number; firstName?: string; lastName?: string; company?: string; email?: string }
|
|
10
|
+
interface ProjectOption { id: number; name: string }
|
|
11
|
+
|
|
12
|
+
const CATEGORY_KEYS = [
|
|
13
|
+
{ value: '', key: 'ticket.category.select' },
|
|
14
|
+
{ value: 'bug', key: 'ticket.category.bugFull' },
|
|
15
|
+
{ value: 'content', key: 'ticket.category.contentFull' },
|
|
16
|
+
{ value: 'feature', key: 'ticket.category.featureFull' },
|
|
17
|
+
{ value: 'question', key: 'ticket.category.questionFull' },
|
|
18
|
+
{ value: 'hosting', key: 'ticket.category.hostingFull' },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const PRIORITY_KEYS = [
|
|
22
|
+
{ value: 'low', key: 'ticket.priority.low' },
|
|
23
|
+
{ value: 'normal', key: 'ticket.priority.normal' },
|
|
24
|
+
{ value: 'high', key: 'ticket.priority.high' },
|
|
25
|
+
{ value: 'urgent', key: 'ticket.priority.urgent' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export const NewTicketClient: React.FC = () => {
|
|
29
|
+
const { t } = useTranslation()
|
|
30
|
+
const router = useRouter()
|
|
31
|
+
const [subject, setSubject] = useState('')
|
|
32
|
+
const [description, setDescription] = useState('')
|
|
33
|
+
const [category, setCategory] = useState('')
|
|
34
|
+
const [priority, setPriority] = useState('normal')
|
|
35
|
+
const [clientSearch, setClientSearch] = useState('')
|
|
36
|
+
const [clientId, setClientId] = useState<number | null>(null)
|
|
37
|
+
const [clientResults, setClientResults] = useState<ClientOption[]>([])
|
|
38
|
+
const [selectedClient, setSelectedClient] = useState<ClientOption | null>(null)
|
|
39
|
+
const [projectId, setProjectId] = useState<number | null>(null)
|
|
40
|
+
const [projects, setProjects] = useState<ProjectOption[]>([])
|
|
41
|
+
const [submitting, setSubmitting] = useState(false)
|
|
42
|
+
const [error, setError] = useState('')
|
|
43
|
+
|
|
44
|
+
// Search clients
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (clientSearch.length < 2) { setClientResults([]); return }
|
|
47
|
+
const timer = setTimeout(async () => {
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`/api/support-clients?where[or][0][email][contains]=${encodeURIComponent(clientSearch)}&where[or][1][firstName][contains]=${encodeURIComponent(clientSearch)}&where[or][2][company][contains]=${encodeURIComponent(clientSearch)}&limit=8&depth=0`, { credentials: 'include' })
|
|
50
|
+
if (res.ok) { const d = await res.json(); setClientResults(d.docs || []) }
|
|
51
|
+
} catch (err) { console.warn('[support] client search error:', err) }
|
|
52
|
+
}, 300)
|
|
53
|
+
return () => clearTimeout(timer)
|
|
54
|
+
}, [clientSearch])
|
|
55
|
+
|
|
56
|
+
// Fetch projects
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
fetch('/api/projects?where[status][equals]=active&limit=50&depth=0', { credentials: 'include' })
|
|
59
|
+
.then((r) => r.json())
|
|
60
|
+
.then((d) => setProjects(d.docs || []))
|
|
61
|
+
.catch(() => {})
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
setError('')
|
|
67
|
+
|
|
68
|
+
if (!subject.trim()) { setError(t('newTicket.errors.subjectRequired')); return }
|
|
69
|
+
if (!clientId) { setError(t('newTicket.errors.clientRequired')); return }
|
|
70
|
+
|
|
71
|
+
setSubmitting(true)
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch('/api/tickets', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
credentials: 'include',
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
subject: subject.trim(),
|
|
79
|
+
client: clientId,
|
|
80
|
+
category: category || undefined,
|
|
81
|
+
priority,
|
|
82
|
+
project: projectId || undefined,
|
|
83
|
+
source: 'admin',
|
|
84
|
+
status: 'open',
|
|
85
|
+
}),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const d = await res.json().catch(() => ({}))
|
|
90
|
+
setError(d.errors?.[0]?.message || t('newTicket.errors.creationError'))
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ticket = await res.json()
|
|
95
|
+
|
|
96
|
+
if (description.trim()) {
|
|
97
|
+
await fetch('/api/ticket-messages', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
credentials: 'include',
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
ticket: ticket.doc.id,
|
|
103
|
+
body: description.trim(),
|
|
104
|
+
authorType: 'admin',
|
|
105
|
+
isInternal: false,
|
|
106
|
+
}),
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
router.push(`/admin/support/ticket?id=${ticket.doc.id}`)
|
|
111
|
+
} catch {
|
|
112
|
+
setError(t('newTicket.errors.networkError'))
|
|
113
|
+
} finally {
|
|
114
|
+
setSubmitting(false)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const S: Record<string, React.CSSProperties> = {
|
|
119
|
+
page: { padding: '20px 30px', maxWidth: 720, margin: '0 auto' },
|
|
120
|
+
header: { marginBottom: 24 },
|
|
121
|
+
backLink: { fontSize: 13, color: '#2563eb', textDecoration: 'none' },
|
|
122
|
+
title: { fontSize: 24, fontWeight: 700, margin: '8px 0 4px', color: 'var(--theme-text)' },
|
|
123
|
+
subtitle: { fontSize: 14, color: 'var(--theme-elevation-500)' },
|
|
124
|
+
error: { padding: '10px 14px', borderRadius: 8, background: '#fef2f2', color: '#dc2626', fontSize: 13, marginBottom: 16, border: '1px solid #fecaca' },
|
|
125
|
+
fieldGroup: { marginBottom: 16 },
|
|
126
|
+
label: { display: 'block', fontSize: 13, fontWeight: 600, marginBottom: 6, color: 'var(--theme-text)' },
|
|
127
|
+
required: { color: '#dc2626' },
|
|
128
|
+
input: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' },
|
|
129
|
+
select: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' },
|
|
130
|
+
textarea: { width: '100%', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--theme-elevation-200)', fontSize: 13, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)', minHeight: 120, fontFamily: 'inherit', resize: 'vertical' as const },
|
|
131
|
+
row3: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12, marginBottom: 16 },
|
|
132
|
+
submitBtn: { padding: '10px 20px', borderRadius: 8, background: '#2563eb', color: '#fff', fontSize: 14, fontWeight: 600, border: 'none', cursor: 'pointer' },
|
|
133
|
+
searchResults: { position: 'absolute' as const, top: '100%', left: 0, right: 0, background: 'var(--theme-elevation-0)', border: '1px solid var(--theme-elevation-200)', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.1)', zIndex: 50, maxHeight: 200, overflowY: 'auto' as const },
|
|
134
|
+
searchItem: { padding: '8px 12px', cursor: 'pointer', fontSize: 13, borderBottom: '1px solid var(--theme-elevation-100)' },
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div style={S.page}>
|
|
139
|
+
<div style={S.header}>
|
|
140
|
+
<Link href="/admin/support/inbox" style={S.backLink}>← {t('newTicket.backToInbox')}</Link>
|
|
141
|
+
<h1 style={S.title}>{t('newTicket.title')}</h1>
|
|
142
|
+
<p style={S.subtitle}>{t('newTicket.subtitle')}</p>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{error && <div style={S.error}>{error}</div>}
|
|
146
|
+
|
|
147
|
+
<form onSubmit={handleSubmit}>
|
|
148
|
+
{/* Client search */}
|
|
149
|
+
<div style={S.fieldGroup}>
|
|
150
|
+
<label style={S.label}>{t('newTicket.clientLabel')} <span style={S.required}>*</span></label>
|
|
151
|
+
{selectedClient ? (
|
|
152
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderRadius: 10, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-50)' }}>
|
|
153
|
+
<span style={{ fontWeight: 600, fontSize: 13, color: 'var(--theme-text)' }}>
|
|
154
|
+
{selectedClient.firstName} {selectedClient.lastName} -- {selectedClient.company}
|
|
155
|
+
</span>
|
|
156
|
+
<span style={{ fontSize: 12, color: 'var(--theme-elevation-500)' }}>{selectedClient.email}</span>
|
|
157
|
+
<button type="button" onClick={() => { setSelectedClient(null); setClientId(null); setClientSearch('') }} style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--theme-elevation-400)', fontSize: 16 }}>×</button>
|
|
158
|
+
</div>
|
|
159
|
+
) : (
|
|
160
|
+
<div style={{ position: 'relative' }}>
|
|
161
|
+
<input
|
|
162
|
+
type="text"
|
|
163
|
+
style={S.input}
|
|
164
|
+
placeholder={t('newTicket.clientSearchPlaceholder')}
|
|
165
|
+
value={clientSearch}
|
|
166
|
+
onChange={(e) => setClientSearch(e.target.value)}
|
|
167
|
+
/>
|
|
168
|
+
{clientResults.length > 0 && (
|
|
169
|
+
<div style={S.searchResults}>
|
|
170
|
+
{clientResults.map((c) => (
|
|
171
|
+
<div key={c.id} style={S.searchItem} onClick={() => {
|
|
172
|
+
setSelectedClient(c)
|
|
173
|
+
setClientId(c.id)
|
|
174
|
+
setClientSearch('')
|
|
175
|
+
setClientResults([])
|
|
176
|
+
}}>
|
|
177
|
+
<strong>{c.firstName} {c.lastName}</strong> -- {c.company} <span style={{ color: 'var(--theme-elevation-400)', fontSize: 12 }}>{c.email}</span>
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* Subject */}
|
|
187
|
+
<div style={S.fieldGroup}>
|
|
188
|
+
<label style={S.label}>{t('newTicket.subjectLabel')} <span style={S.required}>*</span></label>
|
|
189
|
+
<input type="text" style={S.input} placeholder={t('newTicket.subjectPlaceholder')} value={subject} onChange={(e) => setSubject(e.target.value)} />
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Category + Priority + Project */}
|
|
193
|
+
<div style={S.row3}>
|
|
194
|
+
<div style={S.fieldGroup}>
|
|
195
|
+
<label style={S.label}>{t('newTicket.categoryLabel')}</label>
|
|
196
|
+
<select style={S.select} value={category} onChange={(e) => setCategory(e.target.value)}>
|
|
197
|
+
{CATEGORY_KEYS.map((c) => <option key={c.value} value={c.value}>{t(c.key)}</option>)}
|
|
198
|
+
</select>
|
|
199
|
+
</div>
|
|
200
|
+
<div style={S.fieldGroup}>
|
|
201
|
+
<label style={S.label}>{t('newTicket.priorityLabel')}</label>
|
|
202
|
+
<select style={S.select} value={priority} onChange={(e) => setPriority(e.target.value)}>
|
|
203
|
+
{PRIORITY_KEYS.map((p) => <option key={p.value} value={p.value}>{t(p.key)}</option>)}
|
|
204
|
+
</select>
|
|
205
|
+
</div>
|
|
206
|
+
<div style={S.fieldGroup}>
|
|
207
|
+
<label style={S.label}>{t('newTicket.projectLabel')}</label>
|
|
208
|
+
<select style={S.select} value={projectId || ''} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : null)}>
|
|
209
|
+
<option value="">{t('ticket.noProject')}</option>
|
|
210
|
+
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
211
|
+
</select>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Description */}
|
|
216
|
+
<div style={S.fieldGroup}>
|
|
217
|
+
<label style={S.label}>{t('newTicket.descriptionLabel')}</label>
|
|
218
|
+
<textarea style={S.textarea} placeholder={t('newTicket.descriptionPlaceholder')} value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<button type="submit" style={S.submitBtn} disabled={submitting}>
|
|
222
|
+
{submitting ? t('newTicket.submitting') : t('newTicket.submitButton')}
|
|
223
|
+
</button>
|
|
224
|
+
</form>
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { NewTicketClient } from './client'
|
|
7
|
+
|
|
8
|
+
export const NewTicketView: React.FC<AdminViewServerProps> = ({ initPageResult }) => {
|
|
9
|
+
const { req, visibleEntities } = initPageResult
|
|
10
|
+
if (!req.user) redirect('/admin/login')
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<DefaultTemplate
|
|
14
|
+
i18n={req.i18n}
|
|
15
|
+
locale={initPageResult.locale}
|
|
16
|
+
params={{}}
|
|
17
|
+
payload={req.payload}
|
|
18
|
+
permissions={initPageResult.permissions}
|
|
19
|
+
searchParams={{}}
|
|
20
|
+
user={req.user}
|
|
21
|
+
visibleEntities={visibleEntities}
|
|
22
|
+
>
|
|
23
|
+
<AdminErrorBoundary viewName="NewTicketView">
|
|
24
|
+
<NewTicketClient />
|
|
25
|
+
</AdminErrorBoundary>
|
|
26
|
+
</DefaultTemplate>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default NewTicketView
|
|
@@ -0,0 +1,177 @@
|
|
|
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/PendingEmails.module.scss'
|
|
6
|
+
|
|
7
|
+
interface SuggestedTicket { id: number; ticketNumber: string; subject: string; score: number }
|
|
8
|
+
interface PendingEmail {
|
|
9
|
+
id: number; senderEmail: string; senderName?: string; subject: string; body: string
|
|
10
|
+
client?: { id: number; firstName?: string; lastName?: string; email?: string; company?: string } | number
|
|
11
|
+
attachments?: Array<{ file: { id: number; filename?: string } | number }>
|
|
12
|
+
status: 'pending' | 'processed' | 'ignored'
|
|
13
|
+
processedAction?: 'ticket_created' | 'message_added' | 'ignored'
|
|
14
|
+
processedTicket?: { id: number; ticketNumber?: string } | number
|
|
15
|
+
suggestedTickets?: SuggestedTicket[]
|
|
16
|
+
createdAt: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Tab = 'pending' | 'processed' | 'ignored'
|
|
20
|
+
|
|
21
|
+
function timeAgo(dateStr: string): string {
|
|
22
|
+
const diff = Date.now() - new Date(dateStr).getTime()
|
|
23
|
+
const mins = Math.floor(diff / 60000)
|
|
24
|
+
if (mins < 1) return "a l'instant"
|
|
25
|
+
if (mins < 60) return `il y a ${mins}min`
|
|
26
|
+
const hours = Math.floor(mins / 60)
|
|
27
|
+
if (hours < 24) return `il y a ${hours}h`
|
|
28
|
+
return `il y a ${Math.floor(hours / 24)}j`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function EmailCard({ email, onProcess, processing, t }: { email: PendingEmail; onProcess: (action: 'create_ticket' | 'add_to_ticket' | 'ignore', ticketId?: number, clientId?: number) => void; processing: boolean; t: (key: string, vars?: Record<string, string | number>) => string }) {
|
|
32
|
+
const [expanded, setExpanded] = useState(false)
|
|
33
|
+
const [showLinkModal, setShowLinkModal] = useState(false)
|
|
34
|
+
const [linkSearch, setLinkSearch] = useState('')
|
|
35
|
+
const [linkResults, setLinkResults] = useState<Array<{ id: number; ticketNumber: string; subject: string }>>([])
|
|
36
|
+
const isPending = email.status === 'pending'
|
|
37
|
+
const preview = email.body.slice(0, 200) + (email.body.length > 200 ? '...' : '')
|
|
38
|
+
const suggestions = email.suggestedTickets || []
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!linkSearch || linkSearch.length < 2) { setLinkResults([]); return }
|
|
42
|
+
const timer = setTimeout(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`/api/tickets?where[or][0][ticketNumber][contains]=${encodeURIComponent(linkSearch)}&where[or][1][subject][contains]=${encodeURIComponent(linkSearch)}&limit=10&sort=-updatedAt&depth=0`)
|
|
45
|
+
if (res.ok) { const data = await res.json(); setLinkResults(data.docs.map((d: Record<string, unknown>) => ({ id: d.id, ticketNumber: d.ticketNumber, subject: d.subject }))) }
|
|
46
|
+
} catch (err) { console.warn('[support] ticket search error:', err) }
|
|
47
|
+
}, 300)
|
|
48
|
+
return () => clearTimeout(timer)
|
|
49
|
+
}, [linkSearch])
|
|
50
|
+
|
|
51
|
+
const S: Record<string, React.CSSProperties> = {
|
|
52
|
+
card: { padding: 16, borderRadius: 10, border: '1px solid var(--theme-elevation-150)', marginBottom: 12, opacity: processing ? 0.5 : 1 },
|
|
53
|
+
senderName: { fontWeight: 600, fontSize: 14 },
|
|
54
|
+
senderEmail: { fontSize: 12, color: 'var(--theme-elevation-500)' },
|
|
55
|
+
subject: { fontWeight: 600, fontSize: 13, marginTop: 4 },
|
|
56
|
+
meta: { fontSize: 12, color: 'var(--theme-elevation-400)', marginTop: 2 },
|
|
57
|
+
body: { fontSize: 13, color: 'var(--theme-text)', padding: '8px 0', whiteSpace: 'pre-wrap' as const, lineHeight: 1.5 },
|
|
58
|
+
actions: { display: 'flex', gap: 8, marginTop: 8 },
|
|
59
|
+
btn: { padding: '6px 14px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, fontWeight: 600, cursor: 'pointer', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' },
|
|
60
|
+
btnCreate: { background: '#2563eb', color: '#fff', border: 'none' },
|
|
61
|
+
btnIgnore: { color: '#dc2626', borderColor: '#dc2626' },
|
|
62
|
+
overlay: { position: 'fixed' as const, inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 },
|
|
63
|
+
modal: { background: 'var(--theme-elevation-0)', borderRadius: 12, padding: 24, maxWidth: 480, width: '100%', maxHeight: '80vh', overflowY: 'auto' as const },
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div style={S.card}>
|
|
68
|
+
<div>
|
|
69
|
+
<span style={S.senderName}>{email.senderName || email.senderEmail}</span>
|
|
70
|
+
{email.senderName && <span style={S.senderEmail}> <{email.senderEmail}></span>}
|
|
71
|
+
</div>
|
|
72
|
+
<div style={S.subject}>{email.subject}</div>
|
|
73
|
+
<div style={S.meta}>{timeAgo(email.createdAt)} {email.attachments?.length ? `-- ${email.attachments.length} PJ` : ''}</div>
|
|
74
|
+
|
|
75
|
+
{suggestions.length > 0 && isPending && (
|
|
76
|
+
<div style={{ display: 'flex', gap: 6, marginTop: 6, flexWrap: 'wrap' }}>
|
|
77
|
+
{suggestions.map((s) => (
|
|
78
|
+
<span key={s.id} style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, background: s.score >= 0.7 ? '#dcfce7' : '#fef3c7', color: s.score >= 0.7 ? '#166534' : '#92400e' }}>
|
|
79
|
+
Similaire a {s.ticketNumber} ({Math.round(s.score * 100)}%)
|
|
80
|
+
</span>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<div style={S.body}>{expanded ? email.body : preview}</div>
|
|
86
|
+
{email.body.length > 200 && <button onClick={() => setExpanded(!expanded)} style={{ ...S.btn, fontSize: 11, padding: '2px 8px' }}>{expanded ? t('pendingEmails.collapse') : t('pendingEmails.expand')}</button>}
|
|
87
|
+
|
|
88
|
+
{isPending && (
|
|
89
|
+
<div style={S.actions}>
|
|
90
|
+
<button onClick={() => {
|
|
91
|
+
const clientId = typeof email.client === 'object' && email.client ? email.client.id : undefined
|
|
92
|
+
onProcess('create_ticket', undefined, clientId)
|
|
93
|
+
}} disabled={processing} style={{ ...S.btn, ...S.btnCreate }}>{t('pendingEmails.actions.createTicket')}</button>
|
|
94
|
+
<button onClick={() => setShowLinkModal(true)} disabled={processing} style={S.btn}>{t('pendingEmails.actions.linkToTicket')}</button>
|
|
95
|
+
<button onClick={() => onProcess('ignore')} disabled={processing} style={{ ...S.btn, ...S.btnIgnore }}>{t('pendingEmails.actions.ignore')}</button>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{showLinkModal && (
|
|
100
|
+
<div style={S.overlay} onClick={() => setShowLinkModal(false)}>
|
|
101
|
+
<div style={S.modal} onClick={(e) => e.stopPropagation()}>
|
|
102
|
+
<h3 style={{ margin: '0 0 12px', fontSize: 16, fontWeight: 700 }}>{t('pendingEmails.linkModal.title')}</h3>
|
|
103
|
+
{suggestions.length > 0 && (
|
|
104
|
+
<div style={{ marginBottom: 12 }}>
|
|
105
|
+
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 4, color: 'var(--theme-elevation-500)' }}>{t('pendingEmails.linkModal.suggestions')}</div>
|
|
106
|
+
{suggestions.map((s) => (
|
|
107
|
+
<button key={s.id} onClick={() => { setShowLinkModal(false); onProcess('add_to_ticket', s.id) }} style={{ display: 'block', width: '100%', padding: '8px 12px', border: '1px solid var(--theme-elevation-200)', borderRadius: 6, background: 'var(--theme-elevation-0)', cursor: 'pointer', textAlign: 'left', marginBottom: 4, fontSize: 13 }}>
|
|
108
|
+
<strong>{s.ticketNumber}</strong> {s.subject} <span style={{ fontSize: 11, color: '#16a34a' }}>{Math.round(s.score * 100)}%</span>
|
|
109
|
+
</button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
<input type="text" placeholder={t('pendingEmails.linkModal.searchPlaceholder')} value={linkSearch} onChange={(e) => setLinkSearch(e.target.value)} style={{ width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 13, marginBottom: 8, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }} />
|
|
114
|
+
{linkResults.map((r) => (
|
|
115
|
+
<button key={r.id} onClick={() => { setShowLinkModal(false); onProcess('add_to_ticket', r.id) }} style={{ display: 'block', width: '100%', padding: '8px 12px', border: '1px solid var(--theme-elevation-200)', borderRadius: 6, background: 'var(--theme-elevation-0)', cursor: 'pointer', textAlign: 'left', marginBottom: 4, fontSize: 13 }}>
|
|
116
|
+
<strong>{r.ticketNumber}</strong> {r.subject}
|
|
117
|
+
</button>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const PendingEmailsClient: React.FC = () => {
|
|
127
|
+
const { t } = useTranslation()
|
|
128
|
+
const [emails, setEmails] = useState<PendingEmail[]>([])
|
|
129
|
+
const [loading, setLoading] = useState(true)
|
|
130
|
+
const [tab, setTab] = useState<Tab>('pending')
|
|
131
|
+
const [processing, setProcessing] = useState<number | null>(null)
|
|
132
|
+
|
|
133
|
+
const fetchEmails = useCallback(async () => {
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch(`/api/pending-emails?where[status][equals]=${tab}&sort=-createdAt&limit=50&depth=1`)
|
|
136
|
+
if (res.ok) { const data = await res.json(); setEmails(data.docs) }
|
|
137
|
+
} catch { /* ignore */ }
|
|
138
|
+
setLoading(false)
|
|
139
|
+
}, [tab])
|
|
140
|
+
|
|
141
|
+
useEffect(() => { setLoading(true); fetchEmails() }, [fetchEmails])
|
|
142
|
+
useEffect(() => { if (tab !== 'pending') return; const iv = setInterval(fetchEmails, 30000); return () => clearInterval(iv) }, [fetchEmails, tab])
|
|
143
|
+
|
|
144
|
+
const handleProcess = async (emailId: number, action: 'create_ticket' | 'add_to_ticket' | 'ignore', ticketId?: number, clientId?: number) => {
|
|
145
|
+
setProcessing(emailId)
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch(`/api/support/pending-emails/${emailId}/process`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, ticketId, clientId }) })
|
|
148
|
+
if (res.ok) { setEmails((prev) => prev.filter((e) => e.id !== emailId)) }
|
|
149
|
+
else { const err = await res.json().catch(() => ({ error: 'Unknown error' })); alert(`Erreur : ${err.error || res.statusText}`) }
|
|
150
|
+
} catch { alert('Erreur reseau') }
|
|
151
|
+
setProcessing(null)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tabs: { key: Tab; label: string }[] = [{ key: 'pending', label: t('pendingEmails.tabs.pending') }, { key: 'processed', label: t('pendingEmails.tabs.processed') }, { key: 'ignored', label: t('pendingEmails.tabs.ignored') }]
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div style={{ padding: '20px 30px', maxWidth: 900, margin: '0 auto' }}>
|
|
158
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
|
159
|
+
<div>
|
|
160
|
+
<h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, color: 'var(--theme-text)' }}>{t('pendingEmails.title')}</h1>
|
|
161
|
+
<p style={{ fontSize: 13, color: 'var(--theme-elevation-500)', margin: '4px 0 0' }}>{t('pendingEmails.subtitle')}</p>
|
|
162
|
+
</div>
|
|
163
|
+
{tab === 'pending' && emails.length > 0 && <span style={{ padding: '4px 10px', borderRadius: 10, background: '#fef2f2', color: '#dc2626', fontSize: 12, fontWeight: 700 }}>{t('pendingEmails.pendingCount', { count: String(emails.length) })}</span>}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
|
|
167
|
+
{tabs.map((tb) => (
|
|
168
|
+
<button key={tb.key} onClick={() => setTab(tb.key)} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: tab === tb.key ? 'var(--theme-elevation-100)' : 'none', cursor: 'pointer', fontSize: 13, fontWeight: tab === tb.key ? 700 : 500, color: tab === tb.key ? 'var(--theme-text)' : 'var(--theme-elevation-500)' }}>{tb.label}</button>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{loading ? <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t('common.loading')}</div>
|
|
173
|
+
: emails.length === 0 ? <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>{t(`pendingEmails.empty.${tab}`)}</div>
|
|
174
|
+
: emails.map((email) => <EmailCard key={email.id} email={email} onProcess={(action, ticketId, clientId) => handleProcess(email.id, action, ticketId, clientId)} processing={processing === email.id} t={t} />)}
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -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 { PendingEmailsClient } from './client'
|
|
7
|
+
|
|
8
|
+
export const PendingEmailsView: 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="PendingEmailsView">
|
|
27
|
+
<PendingEmailsClient />
|
|
28
|
+
</AdminErrorBoundary>
|
|
29
|
+
</DefaultTemplate>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default PendingEmailsView
|