@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,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
export function CloseTicketButton({ ticketId }: { ticketId: number | string }) {
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const [loading, setLoading] = useState(false)
|
|
9
|
+
const [confirming, setConfirming] = useState(false)
|
|
10
|
+
|
|
11
|
+
const handleClose = async () => {
|
|
12
|
+
if (!confirming) {
|
|
13
|
+
setConfirming(true)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setLoading(true)
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`/api/tickets/${ticketId}`, {
|
|
20
|
+
method: 'PATCH',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
credentials: 'include',
|
|
23
|
+
body: JSON.stringify({ status: 'resolved' }),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (res.ok) {
|
|
27
|
+
router.refresh()
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.warn('[CloseTicketButton] Failed to close ticket:', err)
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false)
|
|
33
|
+
setConfirming(false)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
onClick={handleClose}
|
|
40
|
+
disabled={loading}
|
|
41
|
+
className={`inline-flex items-center gap-1.5 rounded-xl border px-3.5 py-2 text-sm font-semibold transition-all disabled:opacity-50 ${
|
|
42
|
+
confirming
|
|
43
|
+
? 'border-red-200 dark:border-red-800/50 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30'
|
|
44
|
+
: 'border-emerald-200 dark:border-emerald-800/50 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/30'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
{loading ? (
|
|
48
|
+
<>
|
|
49
|
+
<svg className="h-3.5 w-3.5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
50
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
51
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
52
|
+
</svg>
|
|
53
|
+
Fermeture...
|
|
54
|
+
</>
|
|
55
|
+
) : confirming ? (
|
|
56
|
+
<>
|
|
57
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-3.5 w-3.5">
|
|
58
|
+
<circle cx="12" cy="12" r="10" />
|
|
59
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
60
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
61
|
+
</svg>
|
|
62
|
+
Confirmer ?
|
|
63
|
+
</>
|
|
64
|
+
) : (
|
|
65
|
+
<>
|
|
66
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-3.5 w-3.5">
|
|
67
|
+
<polyline points="20,6 9,17 4,12" />
|
|
68
|
+
</svg>
|
|
69
|
+
Résolu
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
interface CollapsibleMessagesProps {
|
|
6
|
+
children: React.ReactNode[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const VISIBLE_COUNT = 3
|
|
10
|
+
|
|
11
|
+
export const CollapsibleMessages: React.FC<CollapsibleMessagesProps> = ({ children }) => {
|
|
12
|
+
const [collapsed, setCollapsed] = useState(true)
|
|
13
|
+
|
|
14
|
+
if (!children || children.length <= VISIBLE_COUNT) {
|
|
15
|
+
return <>{children}</>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const hiddenCount = children.length - VISIBLE_COUNT
|
|
19
|
+
const lastMessages = children.slice(-VISIBLE_COUNT)
|
|
20
|
+
|
|
21
|
+
if (collapsed) {
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<button
|
|
25
|
+
onClick={() => setCollapsed(false)}
|
|
26
|
+
className="no-print w-full rounded-2xl border-2 border-dashed border-gray-300 px-4 py-3 text-center text-sm font-semibold text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-600"
|
|
27
|
+
>
|
|
28
|
+
Voir les {hiddenCount} message{hiddenCount > 1 ? 's' : ''} précédent{hiddenCount > 1 ? 's' : ''}
|
|
29
|
+
</button>
|
|
30
|
+
{lastMessages}
|
|
31
|
+
</>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => setCollapsed(true)}
|
|
39
|
+
className="no-print w-full rounded-2xl border-2 border-dashed border-gray-300 px-4 py-3 text-center text-sm font-semibold text-gray-400 transition-colors hover:border-gray-400 hover:text-gray-600"
|
|
40
|
+
>
|
|
41
|
+
Masquer les anciens messages
|
|
42
|
+
</button>
|
|
43
|
+
{children}
|
|
44
|
+
</>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
export function MarkSolutionButton({
|
|
7
|
+
messageId,
|
|
8
|
+
isSolution,
|
|
9
|
+
}: {
|
|
10
|
+
messageId: number | string
|
|
11
|
+
isSolution: boolean
|
|
12
|
+
}) {
|
|
13
|
+
const router = useRouter()
|
|
14
|
+
const [loading, setLoading] = useState(false)
|
|
15
|
+
|
|
16
|
+
const handleToggle = async () => {
|
|
17
|
+
setLoading(true)
|
|
18
|
+
try {
|
|
19
|
+
await fetch(`/api/ticket-messages/${messageId}`, {
|
|
20
|
+
method: 'PATCH',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
credentials: 'include',
|
|
23
|
+
body: JSON.stringify({ isSolution: !isSolution }),
|
|
24
|
+
})
|
|
25
|
+
router.refresh()
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.warn('[MarkSolutionButton] Failed to toggle solution:', err)
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
onClick={handleToggle}
|
|
36
|
+
disabled={loading}
|
|
37
|
+
className={`mt-2 flex items-center gap-1 rounded-lg px-2 py-1 text-xs font-bold transition-all ${
|
|
38
|
+
isSolution
|
|
39
|
+
? 'border-2 border-green-500 bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
40
|
+
: 'border-2 border-gray-200 text-gray-400 hover:border-green-500 hover:text-green-600 dark:border-gray-600 dark:hover:border-green-500'
|
|
41
|
+
} disabled:opacity-50`}
|
|
42
|
+
title={isSolution ? 'Retirer le marquage solution' : 'Marquer comme solution'}
|
|
43
|
+
>
|
|
44
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-3.5 w-3.5">
|
|
45
|
+
<polyline points="20,6 9,17 4,12" />
|
|
46
|
+
</svg>
|
|
47
|
+
{isSolution ? 'Solution' : 'Marquer comme solution'}
|
|
48
|
+
</button>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
const EDIT_WINDOW_MS = 5 * 60 * 1000 // 5 minutes
|
|
7
|
+
|
|
8
|
+
export function MessageActions({
|
|
9
|
+
messageId,
|
|
10
|
+
body,
|
|
11
|
+
createdAt,
|
|
12
|
+
}: {
|
|
13
|
+
messageId: number
|
|
14
|
+
body: string
|
|
15
|
+
createdAt: string
|
|
16
|
+
}) {
|
|
17
|
+
const router = useRouter()
|
|
18
|
+
const [editing, setEditing] = useState(false)
|
|
19
|
+
const [editText, setEditText] = useState(body)
|
|
20
|
+
const [saving, setSaving] = useState(false)
|
|
21
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
22
|
+
|
|
23
|
+
const elapsed = Date.now() - new Date(createdAt).getTime()
|
|
24
|
+
const canEdit = elapsed < EDIT_WINDOW_MS
|
|
25
|
+
|
|
26
|
+
if (!canEdit) return null
|
|
27
|
+
|
|
28
|
+
const remainingMin = Math.max(0, Math.ceil((EDIT_WINDOW_MS - elapsed) / 60000))
|
|
29
|
+
|
|
30
|
+
const handleSave = async () => {
|
|
31
|
+
if (!editText.trim() || editText === body) {
|
|
32
|
+
setEditing(false)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
setSaving(true)
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`/api/ticket-messages/${messageId}`, {
|
|
38
|
+
method: 'PATCH',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
credentials: 'include',
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
body: editText.trim(),
|
|
43
|
+
editedAt: new Date().toISOString(),
|
|
44
|
+
}),
|
|
45
|
+
})
|
|
46
|
+
if (res.ok) {
|
|
47
|
+
setEditing(false)
|
|
48
|
+
router.refresh()
|
|
49
|
+
}
|
|
50
|
+
} catch { /* ignore */ }
|
|
51
|
+
setSaving(false)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handleDelete = async () => {
|
|
55
|
+
setSaving(true)
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(`/api/ticket-messages/${messageId}`, {
|
|
58
|
+
method: 'PATCH',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
credentials: 'include',
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
body: '[Message supprimé]',
|
|
63
|
+
bodyHtml: '',
|
|
64
|
+
deletedAt: new Date().toISOString(),
|
|
65
|
+
}),
|
|
66
|
+
})
|
|
67
|
+
if (res.ok) {
|
|
68
|
+
setConfirmDelete(false)
|
|
69
|
+
router.refresh()
|
|
70
|
+
}
|
|
71
|
+
} catch { /* ignore */ }
|
|
72
|
+
setSaving(false)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (editing) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="mt-2 space-y-2">
|
|
78
|
+
<textarea
|
|
79
|
+
value={editText}
|
|
80
|
+
onChange={(e) => setEditText(e.target.value)}
|
|
81
|
+
rows={3}
|
|
82
|
+
className="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 px-3 py-2 text-sm text-slate-900 dark:text-white outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
|
|
83
|
+
/>
|
|
84
|
+
<div className="flex gap-2">
|
|
85
|
+
<button
|
|
86
|
+
onClick={handleSave}
|
|
87
|
+
disabled={saving}
|
|
88
|
+
className="rounded-md bg-blue-600 px-3 py-1 text-xs font-semibold text-white hover:bg-blue-700 disabled:opacity-50"
|
|
89
|
+
>
|
|
90
|
+
{saving ? 'Enregistrement...' : 'Enregistrer'}
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => { setEditing(false); setEditText(body) }}
|
|
94
|
+
className="rounded-md border border-slate-300 dark:border-slate-600 px-3 py-1 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
|
|
95
|
+
>
|
|
96
|
+
Annuler
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (confirmDelete) {
|
|
104
|
+
return (
|
|
105
|
+
<div className="mt-2 flex items-center gap-2">
|
|
106
|
+
<span className="text-xs text-red-500">Supprimer ce message ?</span>
|
|
107
|
+
<button
|
|
108
|
+
onClick={handleDelete}
|
|
109
|
+
disabled={saving}
|
|
110
|
+
className="rounded-md bg-red-600 px-2.5 py-1 text-xs font-semibold text-white hover:bg-red-700 disabled:opacity-50"
|
|
111
|
+
>
|
|
112
|
+
{saving ? '...' : 'Confirmer'}
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => setConfirmDelete(false)}
|
|
116
|
+
className="rounded-md border border-slate-300 dark:border-slate-600 px-2.5 py-1 text-xs text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
|
|
117
|
+
>
|
|
118
|
+
Annuler
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="mt-1.5 flex items-center gap-3">
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => setEditing(true)}
|
|
128
|
+
className="text-[11px] font-medium text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
|
129
|
+
>
|
|
130
|
+
Modifier
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
onClick={() => setConfirmDelete(true)}
|
|
134
|
+
className="text-[11px] font-medium text-slate-400 hover:text-red-500 transition-colors"
|
|
135
|
+
>
|
|
136
|
+
Supprimer
|
|
137
|
+
</button>
|
|
138
|
+
<span className="text-[10px] text-slate-300 dark:text-slate-600">{remainingMin}min restantes</span>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function EditedBadge({ editedAt }: { editedAt?: string | null }) {
|
|
144
|
+
if (!editedAt) return null
|
|
145
|
+
return (
|
|
146
|
+
<span className="ml-2 text-[10px] text-slate-400 dark:text-slate-500 italic">
|
|
147
|
+
(modifié)
|
|
148
|
+
</span>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function DeletedMessage() {
|
|
153
|
+
return (
|
|
154
|
+
<div className="italic text-sm text-slate-400 dark:text-slate-500">
|
|
155
|
+
Ce message a été supprimé.
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
export function PrintButton() {
|
|
4
|
+
return (
|
|
5
|
+
<button
|
|
6
|
+
onClick={() => window.print()}
|
|
7
|
+
className="no-print rounded-lg border border-slate-300 bg-white px-3 py-2 text-xs font-medium text-slate-600 hover:bg-slate-50 transition-colors"
|
|
8
|
+
title="Exporter en PDF"
|
|
9
|
+
>
|
|
10
|
+
<svg className="h-4 w-4 inline mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
11
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
12
|
+
</svg>
|
|
13
|
+
PDF
|
|
14
|
+
</button>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
const TZ = 'Europe/Paris'
|
|
4
|
+
|
|
5
|
+
export function ReadReceipt({
|
|
6
|
+
lastAdminReadAt,
|
|
7
|
+
messageCreatedAt,
|
|
8
|
+
}: {
|
|
9
|
+
lastAdminReadAt?: string | null
|
|
10
|
+
messageCreatedAt: string
|
|
11
|
+
}) {
|
|
12
|
+
if (!lastAdminReadAt) return null
|
|
13
|
+
|
|
14
|
+
const readTime = new Date(lastAdminReadAt).getTime()
|
|
15
|
+
const msgTime = new Date(messageCreatedAt).getTime()
|
|
16
|
+
|
|
17
|
+
// Only show "Lu" if the admin read after this message was created
|
|
18
|
+
if (readTime < msgTime) return null
|
|
19
|
+
|
|
20
|
+
const readDate = new Date(lastAdminReadAt)
|
|
21
|
+
const formatted = readDate.toLocaleTimeString('fr-FR', {
|
|
22
|
+
hour: '2-digit',
|
|
23
|
+
minute: '2-digit',
|
|
24
|
+
timeZone: TZ,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="mt-1 text-right">
|
|
29
|
+
<span className="text-[10px] text-blue-400 dark:text-blue-500 font-medium">
|
|
30
|
+
Lu {formatted}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
export function ReopenTicketButton({ ticketId }: { ticketId: number | string }) {
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const [loading, setLoading] = useState(false)
|
|
9
|
+
const [confirming, setConfirming] = useState(false)
|
|
10
|
+
|
|
11
|
+
const handleReopen = async () => {
|
|
12
|
+
if (!confirming) {
|
|
13
|
+
setConfirming(true)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setLoading(true)
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`/api/tickets/${ticketId}`, {
|
|
20
|
+
method: 'PATCH',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
credentials: 'include',
|
|
23
|
+
body: JSON.stringify({ status: 'open' }),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (res.ok) {
|
|
27
|
+
router.refresh()
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.warn('[ReopenTicketButton] Failed to reopen ticket:', err)
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false)
|
|
33
|
+
setConfirming(false)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
onClick={handleReopen}
|
|
40
|
+
disabled={loading}
|
|
41
|
+
className={`inline-flex items-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-semibold transition-all disabled:opacity-50 ${
|
|
42
|
+
confirming
|
|
43
|
+
? 'border-emerald-200 dark:border-emerald-800/50 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/30'
|
|
44
|
+
: 'border-blue-200 dark:border-blue-800/50 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
{loading ? (
|
|
48
|
+
<>
|
|
49
|
+
<svg className="h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
50
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
51
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
52
|
+
</svg>
|
|
53
|
+
Réouverture...
|
|
54
|
+
</>
|
|
55
|
+
) : confirming ? (
|
|
56
|
+
<>
|
|
57
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
|
|
58
|
+
<polyline points="23 4 23 10 17 10" />
|
|
59
|
+
<path d="M20.49 15a9 9 0 11-2.12-9.36L23 10" />
|
|
60
|
+
</svg>
|
|
61
|
+
Confirmer ?
|
|
62
|
+
</>
|
|
63
|
+
) : (
|
|
64
|
+
<>
|
|
65
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
|
|
66
|
+
<polyline points="23 4 23 10 17 10" />
|
|
67
|
+
<path d="M20.49 15a9 9 0 11-2.12-9.36L23 10" />
|
|
68
|
+
</svg>
|
|
69
|
+
Rouvrir le ticket
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
export function SatisfactionForm({ ticketId }: { ticketId: number | string }) {
|
|
6
|
+
const [rating, setRating] = useState(0)
|
|
7
|
+
const [hoveredRating, setHoveredRating] = useState(0)
|
|
8
|
+
const [comment, setComment] = useState('')
|
|
9
|
+
const [loading, setLoading] = useState(false)
|
|
10
|
+
const [submitted, setSubmitted] = useState(false)
|
|
11
|
+
const [error, setError] = useState('')
|
|
12
|
+
|
|
13
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
14
|
+
e.preventDefault()
|
|
15
|
+
if (rating === 0) {
|
|
16
|
+
setError('Veuillez sélectionner une note.')
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setLoading(true)
|
|
21
|
+
setError('')
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch('/api/support/satisfaction', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
credentials: 'include',
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
ticketId,
|
|
30
|
+
rating,
|
|
31
|
+
...(comment.trim() ? { comment: comment.trim() } : {}),
|
|
32
|
+
}),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (res.ok) {
|
|
36
|
+
setSubmitted(true)
|
|
37
|
+
} else {
|
|
38
|
+
const data = await res.json()
|
|
39
|
+
setError(data.error || 'Erreur lors de l\'envoi.')
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
setError('Erreur de connexion.')
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (submitted) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="rounded-2xl border border-emerald-200 dark:border-emerald-800/50 bg-gradient-to-br from-emerald-50 to-green-50 dark:from-emerald-900/20 dark:to-green-900/20 p-6 text-center">
|
|
51
|
+
<div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/20">
|
|
52
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5 text-white">
|
|
53
|
+
<polyline points="20,6 9,17 4,12" />
|
|
54
|
+
</svg>
|
|
55
|
+
</div>
|
|
56
|
+
<p className="text-base font-bold text-emerald-900 dark:text-emerald-200">Merci pour votre avis !</p>
|
|
57
|
+
<p className="mt-1 text-sm text-emerald-600 dark:text-emerald-400">Votre retour nous aide à nous améliorer.</p>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const stars = [1, 2, 3, 4, 5]
|
|
63
|
+
const labels = ['Très insatisfait', 'Insatisfait', 'Correct', 'Satisfait', 'Très satisfait']
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<form
|
|
67
|
+
onSubmit={handleSubmit}
|
|
68
|
+
className="rounded-2xl border border-amber-200/80 dark:border-amber-800/50 bg-gradient-to-br from-amber-50/80 to-orange-50/40 dark:from-amber-900/10 dark:to-orange-900/10 p-5 sm:p-6 shadow-sm"
|
|
69
|
+
>
|
|
70
|
+
<div className="mb-1 flex items-center gap-2">
|
|
71
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/30">
|
|
72
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4 text-amber-600 dark:text-amber-400">
|
|
73
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
74
|
+
</svg>
|
|
75
|
+
</div>
|
|
76
|
+
<h3 className="text-base font-bold text-slate-900 dark:text-white">Comment s'est passé le support ?</h3>
|
|
77
|
+
</div>
|
|
78
|
+
<p className="mb-5 ml-10 text-sm text-slate-500 dark:text-slate-400">Votre avis nous aide à améliorer notre service.</p>
|
|
79
|
+
|
|
80
|
+
{error && (
|
|
81
|
+
<div className="mb-4 flex items-center gap-2 rounded-xl border border-red-200 dark:border-red-800/50 bg-red-50 dark:bg-red-900/20 px-4 py-3 text-sm text-red-700 dark:text-red-400">
|
|
82
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4 flex-shrink-0">
|
|
83
|
+
<circle cx="12" cy="12" r="10" />
|
|
84
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
85
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
86
|
+
</svg>
|
|
87
|
+
{error}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{/* Star rating */}
|
|
92
|
+
<div className="mb-1 flex items-center gap-0.5">
|
|
93
|
+
{stars.map((star) => (
|
|
94
|
+
<button
|
|
95
|
+
key={star}
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={() => setRating(star)}
|
|
98
|
+
onMouseEnter={() => setHoveredRating(star)}
|
|
99
|
+
onMouseLeave={() => setHoveredRating(0)}
|
|
100
|
+
className="rounded-lg p-1 transition-all hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400"
|
|
101
|
+
>
|
|
102
|
+
<svg
|
|
103
|
+
className={`h-8 w-8 transition-colors ${
|
|
104
|
+
star <= (hoveredRating || rating)
|
|
105
|
+
? 'fill-amber-400 text-amber-400'
|
|
106
|
+
: 'fill-slate-200 dark:fill-slate-600 text-slate-300 dark:text-slate-500'
|
|
107
|
+
}`}
|
|
108
|
+
viewBox="0 0 24 24"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
strokeWidth={1.5}
|
|
111
|
+
>
|
|
112
|
+
<path
|
|
113
|
+
strokeLinecap="round"
|
|
114
|
+
strokeLinejoin="round"
|
|
115
|
+
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
|
|
116
|
+
/>
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
{(hoveredRating || rating) > 0 && (
|
|
122
|
+
<p className="mb-4 text-xs font-medium text-slate-500 dark:text-slate-400">
|
|
123
|
+
{labels[(hoveredRating || rating) - 1]}
|
|
124
|
+
</p>
|
|
125
|
+
)}
|
|
126
|
+
{(hoveredRating || rating) === 0 && <div className="mb-4" />}
|
|
127
|
+
|
|
128
|
+
{/* Comment */}
|
|
129
|
+
<textarea
|
|
130
|
+
rows={3}
|
|
131
|
+
value={comment}
|
|
132
|
+
onChange={(e) => setComment(e.target.value)}
|
|
133
|
+
placeholder="Un commentaire ? (optionnel)"
|
|
134
|
+
className="mb-4 w-full resize-y rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-3 text-sm text-slate-900 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 outline-none transition-all focus:border-blue-400 dark:focus:border-blue-500 focus:ring-2 focus:ring-blue-400/20 dark:focus:ring-blue-500/20"
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
<button
|
|
138
|
+
type="submit"
|
|
139
|
+
disabled={loading || rating === 0}
|
|
140
|
+
className="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-blue-700 hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed"
|
|
141
|
+
>
|
|
142
|
+
{loading ? (
|
|
143
|
+
<>
|
|
144
|
+
<svg className="h-4 w-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
145
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
146
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
147
|
+
</svg>
|
|
148
|
+
Envoi...
|
|
149
|
+
</>
|
|
150
|
+
) : (
|
|
151
|
+
'Envoyer mon avis'
|
|
152
|
+
)}
|
|
153
|
+
</button>
|
|
154
|
+
</form>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
const POLL_INTERVAL = 30_000 // 30 seconds
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Polls for new messages on the current ticket.
|
|
10
|
+
* Uses Payload REST API to check message count, then triggers a router.refresh()
|
|
11
|
+
* if new messages are detected. Stops polling on session expiry (401/403).
|
|
12
|
+
*/
|
|
13
|
+
export function TicketPolling({
|
|
14
|
+
ticketId,
|
|
15
|
+
messageCount,
|
|
16
|
+
}: {
|
|
17
|
+
ticketId: number | string
|
|
18
|
+
messageCount: number
|
|
19
|
+
}) {
|
|
20
|
+
const router = useRouter()
|
|
21
|
+
const lastCount = useRef(messageCount)
|
|
22
|
+
const [sessionExpired, setSessionExpired] = useState(false)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
lastCount.current = messageCount
|
|
26
|
+
}, [messageCount])
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (sessionExpired) return
|
|
30
|
+
|
|
31
|
+
const interval = setInterval(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(
|
|
34
|
+
`/api/ticket-messages?where[ticket][equals]=${ticketId}&limit=0&depth=0`,
|
|
35
|
+
{ credentials: 'include' },
|
|
36
|
+
)
|
|
37
|
+
if (res.status === 401 || res.status === 403) {
|
|
38
|
+
setSessionExpired(true)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (!res.ok) return
|
|
42
|
+
|
|
43
|
+
const data = await res.json()
|
|
44
|
+
if (data.totalDocs > lastCount.current) {
|
|
45
|
+
lastCount.current = data.totalDocs
|
|
46
|
+
router.refresh()
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.warn('[TicketPolling] Poll failed:', err)
|
|
50
|
+
}
|
|
51
|
+
}, POLL_INTERVAL)
|
|
52
|
+
|
|
53
|
+
return () => clearInterval(interval)
|
|
54
|
+
}, [ticketId, router, sessionExpired])
|
|
55
|
+
|
|
56
|
+
return null
|
|
57
|
+
}
|