@escalated-dev/escalated 0.8.0 → 0.11.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 -21
- package/README.md +388 -361
- package/package.json +85 -84
- package/src/components/ActiveChatsPanel.vue +246 -246
- package/src/components/AdminDashboard.stories.js +1423 -1423
- package/src/components/AttachmentList.vue +65 -65
- package/src/components/ChatActionBar.vue +226 -226
- package/src/components/ChatBubble.vue +162 -162
- package/src/components/ChatComposer.vue +308 -308
- package/src/components/ChatQueue.vue +222 -222
- package/src/components/ChatThread.vue +197 -197
- package/src/components/CustomTicketActions.stories.js +71 -0
- package/src/components/CustomTicketActions.vue +101 -0
- package/src/components/EscalatedLayout.vue +628 -604
- package/src/components/FollowButton.vue +20 -1
- package/src/components/MentionDropdown.vue +154 -154
- package/src/components/MetricComparison.vue +89 -89
- package/src/components/PercentileChart.vue +67 -67
- package/src/components/PeriodSelector.vue +98 -98
- package/src/components/PresenceIndicator.vue +228 -213
- package/src/components/ReplyComposer.vue +336 -336
- package/src/components/ReplyThread.vue +153 -153
- package/src/components/ReportChart.vue +395 -395
- package/src/components/ReportExportButton.vue +108 -108
- package/src/components/SaveViewDialog.vue +172 -172
- package/src/components/SavedViewSidebar.vue +208 -208
- package/src/components/SkillTagManager.vue +3 -0
- package/src/components/SnoozeButton.vue +247 -247
- package/src/components/TicketFilters.vue +270 -270
- package/src/components/TicketList.vue +655 -655
- package/src/components/TicketSidebar.vue +4 -0
- package/src/components/TicketSplitDialog.vue +98 -98
- package/src/components/TicketSubjects.stories.js +63 -0
- package/src/components/TicketSubjects.vue +107 -0
- package/src/components/TwoFactorSetup.vue +1 -37
- package/src/components/VariableTokenInput.vue +111 -111
- package/src/components/WorkflowActionConfig.vue +287 -287
- package/src/components/WorkflowActionList.vue +350 -350
- package/src/components/WorkflowConditionBuilder.vue +265 -265
- package/src/components/WorkflowTriggerSelector.vue +125 -125
- package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
- package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
- package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
- package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
- package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
- package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
- package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
- package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
- package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
- package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
- package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
- package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
- package/src/composables/useChat.js +187 -187
- package/src/composables/useI18n.js +102 -102
- package/src/composables/useMentions.js +134 -134
- package/src/composables/usePermissions.js +31 -0
- package/src/composables/useRealtime.js +182 -182
- package/src/index.js +85 -84
- package/src/locales/README.md +13 -13
- package/src/locales/ar.json +727 -727
- package/src/locales/de.json +690 -690
- package/src/locales/en.json +889 -729
- package/src/locales/es.json +691 -691
- package/src/locales/fr.json +691 -691
- package/src/locales/index.js +16 -16
- package/src/locales/it.json +727 -727
- package/src/locales/ja.json +727 -727
- package/src/locales/ko.json +727 -727
- package/src/locales/nl.json +727 -727
- package/src/locales/pl.json +727 -727
- package/src/locales/pt-BR.json +727 -727
- package/src/locales/ru.json +727 -727
- package/src/locales/tr.json +727 -727
- package/src/locales/zh-CN.json +727 -727
- package/src/pages/Admin/Automations/Form.vue +280 -0
- package/src/pages/Admin/Automations/Index.vue +230 -0
- package/src/pages/Admin/Newsletters/Compose.vue +192 -0
- package/src/pages/Admin/Newsletters/Edit.vue +24 -0
- package/src/pages/Admin/Newsletters/Index.vue +141 -0
- package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
- package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
- package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
- package/src/pages/Admin/Newsletters/Settings.vue +85 -0
- package/src/pages/Admin/Newsletters/Show.vue +140 -0
- package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
- package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
- package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
- package/src/pages/Admin/Newsletters/usePreview.js +44 -0
- package/src/pages/Admin/Plugins/Index.vue +10 -2
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
- package/src/pages/Admin/Reports/Cohorts.vue +180 -180
- package/src/pages/Admin/Reports/Comparison.vue +141 -141
- package/src/pages/Admin/Reports/Dashboard.vue +226 -226
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
- package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
- package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
- package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
- package/src/pages/Admin/Settings.vue +1008 -1008
- package/src/pages/Admin/Skills/Form.vue +30 -10
- package/src/pages/Admin/Tickets/Index.vue +126 -126
- package/src/pages/Admin/Tickets/Show.vue +254 -249
- package/src/pages/Admin/Users/Index.vue +145 -145
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
- package/src/pages/Admin/Workflows/Builder.vue +207 -207
- package/src/pages/Admin/Workflows/Form.vue +24 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -235
- package/src/pages/Admin/Workflows/Logs.vue +286 -286
- package/src/pages/Agent/TicketIndex.vue +112 -112
- package/src/pages/Agent/TicketShow.vue +547 -494
- package/src/pages/Customer/Index.vue +56 -56
- package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
- package/src/pages/Plugin/Page.vue +61 -0
- package/src/plugin.js +175 -175
- package/src/utils/mentionHighlight.js +24 -24
- package/src/widget/EscalatedWidget.vue +804 -804
- package/src/widget/index.js +254 -254
|
@@ -1,494 +1,547 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import EscalatedLayout from '../../components/EscalatedLayout.vue';
|
|
3
|
-
import StatusBadge from '../../components/StatusBadge.vue';
|
|
4
|
-
import PriorityBadge from '../../components/PriorityBadge.vue';
|
|
5
|
-
import ReplyThread from '../../components/ReplyThread.vue';
|
|
6
|
-
import ReplyComposer from '../../components/ReplyComposer.vue';
|
|
7
|
-
import ChatThread from '../../components/ChatThread.vue';
|
|
8
|
-
import ChatComposer from '../../components/ChatComposer.vue';
|
|
9
|
-
import ChatActionBar from '../../components/ChatActionBar.vue';
|
|
10
|
-
import TicketSidebar from '../../components/TicketSidebar.vue';
|
|
11
|
-
import AttachmentList from '../../components/AttachmentList.vue';
|
|
12
|
-
import MacroDropdown from '../../components/MacroDropdown.vue';
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
>
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
/>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
:
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
ticket.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
<
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<ContextPanelSection
|
|
470
|
-
title="
|
|
471
|
-
icon="
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
<span :class="escDark ? 'text-neutral-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../components/EscalatedLayout.vue';
|
|
3
|
+
import StatusBadge from '../../components/StatusBadge.vue';
|
|
4
|
+
import PriorityBadge from '../../components/PriorityBadge.vue';
|
|
5
|
+
import ReplyThread from '../../components/ReplyThread.vue';
|
|
6
|
+
import ReplyComposer from '../../components/ReplyComposer.vue';
|
|
7
|
+
import ChatThread from '../../components/ChatThread.vue';
|
|
8
|
+
import ChatComposer from '../../components/ChatComposer.vue';
|
|
9
|
+
import ChatActionBar from '../../components/ChatActionBar.vue';
|
|
10
|
+
import TicketSidebar from '../../components/TicketSidebar.vue';
|
|
11
|
+
import AttachmentList from '../../components/AttachmentList.vue';
|
|
12
|
+
import MacroDropdown from '../../components/MacroDropdown.vue';
|
|
13
|
+
import CustomTicketActions from '../../components/CustomTicketActions.vue';
|
|
14
|
+
import FollowButton from '../../components/FollowButton.vue';
|
|
15
|
+
import PresenceIndicator from '../../components/PresenceIndicator.vue';
|
|
16
|
+
import PinnedNotes from '../../components/PinnedNotes.vue';
|
|
17
|
+
import KeyboardShortcutHelp from '../../components/KeyboardShortcutHelp.vue';
|
|
18
|
+
import PluginSlot from '../../components/PluginSlot.vue';
|
|
19
|
+
import KnowledgePanel from '../../components/KnowledgePanel.vue';
|
|
20
|
+
import ContextPanel from '../../components/ContextPanel.vue';
|
|
21
|
+
import ContextPanelSection from '../../components/ContextPanelSection.vue';
|
|
22
|
+
import { useKeyboardShortcuts } from '../../composables/useKeyboardShortcuts';
|
|
23
|
+
import { usePluginExtensions } from '../../composables/usePluginExtensions';
|
|
24
|
+
import { useRealtime } from '../../composables/useRealtime';
|
|
25
|
+
import { useChat } from '../../composables/useChat';
|
|
26
|
+
import { router, useForm, usePage } from '@inertiajs/vue3';
|
|
27
|
+
import { ref, computed, inject, onUnmounted, watch } from 'vue';
|
|
28
|
+
|
|
29
|
+
const props = defineProps({
|
|
30
|
+
ticket: Object,
|
|
31
|
+
departments: Array,
|
|
32
|
+
tags: Array,
|
|
33
|
+
cannedResponses: Array,
|
|
34
|
+
macros: { type: Array, default: () => [] },
|
|
35
|
+
customActions: { type: Array, default: () => [] },
|
|
36
|
+
is_following: { type: Boolean, default: false },
|
|
37
|
+
followers_count: { type: Number, default: 0 },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const page = usePage();
|
|
41
|
+
const isLightAgent = computed(() => page.props.escalated?.agent_type === 'light');
|
|
42
|
+
// Route prefix the host framework mounts Escalated under. Defaults to
|
|
43
|
+
// `support`, matching every host-adapter plugin. NestJS uses `escalated`.
|
|
44
|
+
const routePrefix = computed(() => page.props.escalated?.prefix || 'support');
|
|
45
|
+
const widgetPath = computed(() => `/${routePrefix.value}/widget`);
|
|
46
|
+
const activeTab = ref(isLightAgent.value ? 'note' : 'reply');
|
|
47
|
+
const showShortcutHelp = ref(false);
|
|
48
|
+
const replyComposerRef = ref(null);
|
|
49
|
+
const statusSelectRef = ref(null);
|
|
50
|
+
const prioritySelectRef = ref(null);
|
|
51
|
+
|
|
52
|
+
const showContextPanel = ref(false);
|
|
53
|
+
|
|
54
|
+
const statusForm = useForm({ status: '' });
|
|
55
|
+
const priorityForm = useForm({ priority: '' });
|
|
56
|
+
const assignForm = useForm({ agent_id: '' });
|
|
57
|
+
|
|
58
|
+
function changeStatus(status) {
|
|
59
|
+
statusForm.status = status;
|
|
60
|
+
statusForm.post(route('escalated.agent.tickets.status', props.ticket.reference), { preserveScroll: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function changePriority(priority) {
|
|
64
|
+
priorityForm.priority = priority;
|
|
65
|
+
priorityForm.post(route('escalated.agent.tickets.priority', props.ticket.reference), { preserveScroll: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assignToMe() {
|
|
69
|
+
// auth.user may be absent if the host app hasn't shared it (other call
|
|
70
|
+
// sites in this page already guard with auth?.user?.id). Bail rather than
|
|
71
|
+
// crash on the click.
|
|
72
|
+
const userId = page.props.auth?.user?.id;
|
|
73
|
+
if (userId == null) return;
|
|
74
|
+
assignForm.agent_id = userId;
|
|
75
|
+
assignForm.post(route('escalated.agent.tickets.assign', props.ticket.reference), { preserveScroll: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toggleFollow() {
|
|
79
|
+
router.post(route('escalated.agent.tickets.follow', props.ticket.reference), {}, { preserveScroll: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const escDark = inject(
|
|
83
|
+
'esc-dark',
|
|
84
|
+
computed(() => false),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Live chat mode detection
|
|
88
|
+
const isChatMode = computed(() => props.ticket.channel === 'chat' && props.ticket.status === 'live');
|
|
89
|
+
const chatMessages = ref(props.ticket.chat_messages || []);
|
|
90
|
+
const chatTypingUser = ref(null);
|
|
91
|
+
|
|
92
|
+
// Subscribe to chat channel for real-time messages. Pass widgetPath so
|
|
93
|
+
// useChat's API calls resolve against whatever prefix the host framework
|
|
94
|
+
// serves Escalated under (config on NestJS, default /support elsewhere).
|
|
95
|
+
const { subscribeToChat } = useChat({ widgetPath: widgetPath.value });
|
|
96
|
+
let chatTypingTimer = null;
|
|
97
|
+
let chatUnsubscribe = null;
|
|
98
|
+
|
|
99
|
+
// Re-seed the chat thread whenever we navigate to a different ticket. Inertia
|
|
100
|
+
// reuses this page component across ticket navigation (setup() does not re-run),
|
|
101
|
+
// so without this the previous ticket's chat would persist.
|
|
102
|
+
watch(
|
|
103
|
+
() => props.ticket.id,
|
|
104
|
+
() => {
|
|
105
|
+
chatMessages.value = props.ticket.chat_messages || [];
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// (Re)subscribe to the chat channel whenever the live chat session changes —
|
|
110
|
+
// including navigating from a non-chat ticket to a chat ticket. The old
|
|
111
|
+
// subscription is torn down first so we never leak a channel or receive
|
|
112
|
+
// messages for the ticket we just left.
|
|
113
|
+
function setupChatSubscription() {
|
|
114
|
+
if (chatUnsubscribe) {
|
|
115
|
+
chatUnsubscribe();
|
|
116
|
+
chatUnsubscribe = null;
|
|
117
|
+
}
|
|
118
|
+
if (!isChatMode.value || !props.ticket.chat_session_id) return;
|
|
119
|
+
chatUnsubscribe = subscribeToChat(props.ticket.chat_session_id, {
|
|
120
|
+
onMessage(data) {
|
|
121
|
+
chatMessages.value.push(data.message);
|
|
122
|
+
},
|
|
123
|
+
onTyping(data) {
|
|
124
|
+
chatTypingUser.value = data.name;
|
|
125
|
+
clearTimeout(chatTypingTimer);
|
|
126
|
+
chatTypingTimer = setTimeout(() => {
|
|
127
|
+
chatTypingUser.value = null;
|
|
128
|
+
}, 4000);
|
|
129
|
+
},
|
|
130
|
+
onEnded() {
|
|
131
|
+
router.reload({ preserveScroll: true });
|
|
132
|
+
},
|
|
133
|
+
onAssigned() {
|
|
134
|
+
router.reload({ preserveScroll: true });
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
watch(() => [props.ticket.chat_session_id, isChatMode.value], setupChatSubscription, {
|
|
140
|
+
immediate: true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
onUnmounted(() => {
|
|
144
|
+
if (chatUnsubscribe) chatUnsubscribe();
|
|
145
|
+
clearTimeout(chatTypingTimer);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function onChatSend(payload) {
|
|
149
|
+
// Optimistically add message to thread
|
|
150
|
+
chatMessages.value.push({
|
|
151
|
+
id: Date.now(),
|
|
152
|
+
body: payload.body,
|
|
153
|
+
is_internal_note: payload.is_internal_note,
|
|
154
|
+
is_agent: true,
|
|
155
|
+
author: { id: page.props.auth?.user?.id, name: page.props.auth?.user?.name },
|
|
156
|
+
created_at: new Date().toISOString(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function onEndChat() {
|
|
161
|
+
router.reload({ preserveScroll: true });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { getPageComponents } = usePluginExtensions();
|
|
165
|
+
|
|
166
|
+
// Keyboard shortcuts
|
|
167
|
+
useKeyboardShortcuts({
|
|
168
|
+
r: () => {
|
|
169
|
+
activeTab.value = 'reply';
|
|
170
|
+
replyComposerRef.value?.$el?.querySelector('textarea')?.focus();
|
|
171
|
+
},
|
|
172
|
+
n: () => {
|
|
173
|
+
activeTab.value = 'note';
|
|
174
|
+
},
|
|
175
|
+
s: () => {
|
|
176
|
+
statusSelectRef.value?.focus();
|
|
177
|
+
},
|
|
178
|
+
p: () => {
|
|
179
|
+
prioritySelectRef.value?.focus();
|
|
180
|
+
},
|
|
181
|
+
f: () => {
|
|
182
|
+
toggleFollow();
|
|
183
|
+
},
|
|
184
|
+
'?': () => {
|
|
185
|
+
showShortcutHelp.value = true;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Real-time updates via WebSocket (gracefully degrades to no-op when Echo is absent)
|
|
190
|
+
const { echoAvailable, subscribeToTicket } = useRealtime();
|
|
191
|
+
let ticketUnsubscribe = null;
|
|
192
|
+
|
|
193
|
+
// (Re)subscribe whenever the ticket id changes. Because Inertia reuses this
|
|
194
|
+
// page across ticket navigation, an onMounted-only subscription would stay
|
|
195
|
+
// bound to the first ticket — realtime updates for the ticket you navigated to
|
|
196
|
+
// would never arrive, and the previous ticket's events would keep triggering
|
|
197
|
+
// reloads. Tear down the old subscription before binding the new one.
|
|
198
|
+
function setupTicketSubscription() {
|
|
199
|
+
if (ticketUnsubscribe) {
|
|
200
|
+
ticketUnsubscribe();
|
|
201
|
+
ticketUnsubscribe = null;
|
|
202
|
+
}
|
|
203
|
+
if (!echoAvailable.value || !props.ticket?.id) return;
|
|
204
|
+
|
|
205
|
+
ticketUnsubscribe = subscribeToTicket(props.ticket.id, {
|
|
206
|
+
onReplyCreated() {
|
|
207
|
+
router.reload({ only: ['ticket'], preserveScroll: true });
|
|
208
|
+
},
|
|
209
|
+
onStatusChanged() {
|
|
210
|
+
router.reload({ only: ['ticket'], preserveScroll: true });
|
|
211
|
+
},
|
|
212
|
+
onTicketUpdated() {
|
|
213
|
+
router.reload({ only: ['ticket'], preserveScroll: true });
|
|
214
|
+
},
|
|
215
|
+
onTicketAssigned() {
|
|
216
|
+
router.reload({ only: ['ticket'], preserveScroll: true });
|
|
217
|
+
},
|
|
218
|
+
onTicketEscalated() {
|
|
219
|
+
router.reload({ only: ['ticket'], preserveScroll: true });
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
watch(() => props.ticket?.id, setupTicketSubscription, { immediate: true });
|
|
225
|
+
|
|
226
|
+
onUnmounted(() => {
|
|
227
|
+
if (ticketUnsubscribe) ticketUnsubscribe();
|
|
228
|
+
});
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
<template>
|
|
232
|
+
<EscalatedLayout :title="ticket.subject">
|
|
233
|
+
<!-- Plugin: ticket show header slot -->
|
|
234
|
+
<PluginSlot slot="ticket.show.header" :components="getPageComponents('ticket.show', 'header')" />
|
|
235
|
+
|
|
236
|
+
<div class="mb-5 flex flex-wrap items-center gap-3">
|
|
237
|
+
<span :class="['text-sm font-mono font-medium', escDark ? 'text-white' : 'text-gray-900']">{{
|
|
238
|
+
ticket.reference
|
|
239
|
+
}}</span>
|
|
240
|
+
<StatusBadge :status="ticket.status" />
|
|
241
|
+
<PriorityBadge :priority="ticket.priority" />
|
|
242
|
+
<span class="text-sm text-neutral-500">by {{ ticket.requester?.name }}</span>
|
|
243
|
+
<PresenceIndicator
|
|
244
|
+
:ticket-reference="ticket.reference"
|
|
245
|
+
:ticket-id="ticket.id"
|
|
246
|
+
route-prefix="escalated.agent"
|
|
247
|
+
/>
|
|
248
|
+
<div class="ml-auto flex items-center gap-2">
|
|
249
|
+
<FollowButton
|
|
250
|
+
:is-following="is_following"
|
|
251
|
+
:followers-count="followers_count"
|
|
252
|
+
:action="route('escalated.agent.tickets.follow', ticket.reference)"
|
|
253
|
+
/>
|
|
254
|
+
<MacroDropdown
|
|
255
|
+
v-if="macros.length && !isLightAgent"
|
|
256
|
+
:macros="macros"
|
|
257
|
+
:action="route('escalated.agent.tickets.macro', ticket.reference)"
|
|
258
|
+
/>
|
|
259
|
+
<button
|
|
260
|
+
v-if="!ticket.assigned_to && !isLightAgent"
|
|
261
|
+
class="rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400"
|
|
262
|
+
@click="assignToMe"
|
|
263
|
+
>
|
|
264
|
+
Assign to Me
|
|
265
|
+
</button>
|
|
266
|
+
<template v-if="!isLightAgent">
|
|
267
|
+
<select
|
|
268
|
+
ref="statusSelectRef"
|
|
269
|
+
:class="[
|
|
270
|
+
'rounded-lg border px-3 py-1.5 text-sm focus:outline-none focus:ring-1',
|
|
271
|
+
escDark
|
|
272
|
+
? 'border-white/10 bg-neutral-950 text-neutral-200 focus:border-white/20 focus:ring-white/10'
|
|
273
|
+
: 'border-gray-300 bg-white text-gray-700 focus:border-gray-400 focus:ring-gray-200',
|
|
274
|
+
]"
|
|
275
|
+
@change="
|
|
276
|
+
changeStatus($event.target.value);
|
|
277
|
+
$event.target.value = '';
|
|
278
|
+
"
|
|
279
|
+
>
|
|
280
|
+
<option value="">Change Status...</option>
|
|
281
|
+
<option value="in_progress">In Progress</option>
|
|
282
|
+
<option value="waiting_on_customer">Waiting on Customer</option>
|
|
283
|
+
<option value="resolved">Resolved</option>
|
|
284
|
+
<option value="closed">Closed</option>
|
|
285
|
+
</select>
|
|
286
|
+
<select
|
|
287
|
+
ref="prioritySelectRef"
|
|
288
|
+
:class="[
|
|
289
|
+
'rounded-lg border px-3 py-1.5 text-sm focus:outline-none focus:ring-1',
|
|
290
|
+
escDark
|
|
291
|
+
? 'border-white/10 bg-neutral-950 text-neutral-200 focus:border-white/20 focus:ring-white/10'
|
|
292
|
+
: 'border-gray-300 bg-white text-gray-700 focus:border-gray-400 focus:ring-gray-200',
|
|
293
|
+
]"
|
|
294
|
+
@change="
|
|
295
|
+
changePriority($event.target.value);
|
|
296
|
+
$event.target.value = '';
|
|
297
|
+
"
|
|
298
|
+
>
|
|
299
|
+
<option value="">Change Priority...</option>
|
|
300
|
+
<option value="low">Low</option>
|
|
301
|
+
<option value="medium">Medium</option>
|
|
302
|
+
<option value="high">High</option>
|
|
303
|
+
<option value="urgent">Urgent</option>
|
|
304
|
+
<option value="critical">Critical</option>
|
|
305
|
+
</select>
|
|
306
|
+
</template>
|
|
307
|
+
<!-- Host-defined custom ticket actions -->
|
|
308
|
+
<CustomTicketActions :actions="customActions" />
|
|
309
|
+
<!-- Context Panel Toggle -->
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
:class="[
|
|
313
|
+
'rounded-lg border px-3 py-1.5 text-sm transition-colors',
|
|
314
|
+
showContextPanel
|
|
315
|
+
? escDark
|
|
316
|
+
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-400'
|
|
317
|
+
: 'border-blue-300 bg-blue-50 text-blue-600'
|
|
318
|
+
: escDark
|
|
319
|
+
? 'border-white/10 text-neutral-400 hover:border-white/20 hover:text-neutral-200'
|
|
320
|
+
: 'border-gray-300 text-gray-400 hover:border-gray-400 hover:text-gray-600',
|
|
321
|
+
]"
|
|
322
|
+
@click="showContextPanel = !showContextPanel"
|
|
323
|
+
>
|
|
324
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
325
|
+
<path
|
|
326
|
+
stroke-linecap="round"
|
|
327
|
+
stroke-linejoin="round"
|
|
328
|
+
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
|
329
|
+
/>
|
|
330
|
+
</svg>
|
|
331
|
+
</button>
|
|
332
|
+
<!-- Plugin: ticket show actions slot -->
|
|
333
|
+
<PluginSlot slot="ticket.show.actions" :components="getPageComponents('ticket.show', 'actions')" />
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
337
|
+
<div class="lg:col-span-2 space-y-6">
|
|
338
|
+
<!-- Chat Action Bar (live chat mode) -->
|
|
339
|
+
<ChatActionBar
|
|
340
|
+
v-if="isChatMode"
|
|
341
|
+
:session-id="ticket.chat_session_id"
|
|
342
|
+
:started-at="ticket.chat_started_at || ticket.created_at"
|
|
343
|
+
:customer="{
|
|
344
|
+
name: ticket.requester?.name,
|
|
345
|
+
email: ticket.requester?.email,
|
|
346
|
+
page_url: ticket.chat_metadata?.page_url,
|
|
347
|
+
}"
|
|
348
|
+
:agents="[]"
|
|
349
|
+
:departments="departments"
|
|
350
|
+
@end-chat="onEndChat"
|
|
351
|
+
/>
|
|
352
|
+
|
|
353
|
+
<PinnedNotes
|
|
354
|
+
v-if="ticket.pinned_notes?.length"
|
|
355
|
+
:notes="ticket.pinned_notes"
|
|
356
|
+
:ticket-reference="ticket.reference"
|
|
357
|
+
route-prefix="escalated.agent"
|
|
358
|
+
/>
|
|
359
|
+
<div
|
|
360
|
+
:class="[
|
|
361
|
+
'rounded-xl border p-5',
|
|
362
|
+
escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
|
|
363
|
+
]"
|
|
364
|
+
>
|
|
365
|
+
<p :class="['whitespace-pre-wrap text-sm', escDark ? 'text-neutral-300' : 'text-gray-700']">
|
|
366
|
+
{{ ticket.description }}
|
|
367
|
+
</p>
|
|
368
|
+
<AttachmentList v-if="ticket.attachments?.length" :attachments="ticket.attachments" class="mt-3" />
|
|
369
|
+
</div>
|
|
370
|
+
<!-- Live Chat Mode -->
|
|
371
|
+
<template v-if="isChatMode">
|
|
372
|
+
<div
|
|
373
|
+
:class="[
|
|
374
|
+
'overflow-hidden rounded-xl border',
|
|
375
|
+
escDark ? 'border-white/[0.06] bg-neutral-900/60' : 'border-gray-200 bg-white',
|
|
376
|
+
]"
|
|
377
|
+
>
|
|
378
|
+
<ChatThread
|
|
379
|
+
:messages="chatMessages"
|
|
380
|
+
:typing-user="chatTypingUser"
|
|
381
|
+
:current-user-id="page.props.auth?.user?.id"
|
|
382
|
+
/>
|
|
383
|
+
<ChatComposer
|
|
384
|
+
:session-id="ticket.chat_session_id"
|
|
385
|
+
:send-endpoint="
|
|
386
|
+
ticket.chat_session_id ? `${widgetPath}/chat/${ticket.chat_session_id}/messages` : ''
|
|
387
|
+
"
|
|
388
|
+
:typing-endpoint="
|
|
389
|
+
ticket.chat_session_id ? `${widgetPath}/chat/${ticket.chat_session_id}/typing` : ''
|
|
390
|
+
"
|
|
391
|
+
allow-notes
|
|
392
|
+
@send="onChatSend"
|
|
393
|
+
/>
|
|
394
|
+
</div>
|
|
395
|
+
</template>
|
|
396
|
+
|
|
397
|
+
<!-- Standard Reply Mode -->
|
|
398
|
+
<template v-else>
|
|
399
|
+
<div>
|
|
400
|
+
<div :class="['mb-4 flex gap-4 border-b', escDark ? 'border-white/[0.06]' : 'border-gray-200']">
|
|
401
|
+
<button
|
|
402
|
+
v-if="!isLightAgent"
|
|
403
|
+
:class="[
|
|
404
|
+
'pb-2 text-sm font-medium transition-colors',
|
|
405
|
+
activeTab === 'reply'
|
|
406
|
+
? escDark
|
|
407
|
+
? 'border-b-2 border-cyan-500 text-white'
|
|
408
|
+
: 'border-b-2 border-blue-500 text-gray-900'
|
|
409
|
+
: escDark
|
|
410
|
+
? 'text-neutral-500 hover:text-neutral-300'
|
|
411
|
+
: 'text-gray-400 hover:text-gray-600',
|
|
412
|
+
]"
|
|
413
|
+
@click="activeTab = 'reply'"
|
|
414
|
+
>
|
|
415
|
+
Reply
|
|
416
|
+
</button>
|
|
417
|
+
<button
|
|
418
|
+
:class="[
|
|
419
|
+
'pb-2 text-sm font-medium transition-colors',
|
|
420
|
+
activeTab === 'note'
|
|
421
|
+
? escDark
|
|
422
|
+
? 'border-b-2 border-amber-500 text-amber-400'
|
|
423
|
+
: 'border-b-2 border-amber-500 text-amber-700'
|
|
424
|
+
: escDark
|
|
425
|
+
? 'text-neutral-500 hover:text-neutral-300'
|
|
426
|
+
: 'text-gray-400 hover:text-gray-600',
|
|
427
|
+
]"
|
|
428
|
+
@click="activeTab = 'note'"
|
|
429
|
+
>
|
|
430
|
+
Internal Note
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
<ReplyComposer
|
|
434
|
+
v-if="activeTab === 'reply' && !isLightAgent"
|
|
435
|
+
ref="replyComposerRef"
|
|
436
|
+
:action="route('escalated.agent.tickets.reply', ticket.reference)"
|
|
437
|
+
:canned-responses="cannedResponses"
|
|
438
|
+
/>
|
|
439
|
+
<ReplyComposer
|
|
440
|
+
v-if="activeTab === 'note'"
|
|
441
|
+
:action="route('escalated.agent.tickets.note', ticket.reference)"
|
|
442
|
+
placeholder="Write an internal note..."
|
|
443
|
+
submit-label="Add Note"
|
|
444
|
+
/>
|
|
445
|
+
</div>
|
|
446
|
+
<div>
|
|
447
|
+
<h2 :class="['mb-4 text-lg font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']">
|
|
448
|
+
Conversation
|
|
449
|
+
</h2>
|
|
450
|
+
<ReplyThread
|
|
451
|
+
:replies="ticket.replies || []"
|
|
452
|
+
:current-user-id="page.props.auth?.user?.id"
|
|
453
|
+
:ticket-reference="ticket.reference"
|
|
454
|
+
route-prefix="escalated.agent"
|
|
455
|
+
pinnable
|
|
456
|
+
/>
|
|
457
|
+
</div>
|
|
458
|
+
</template>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="space-y-6">
|
|
461
|
+
<TicketSidebar :ticket="ticket" :tags="tags" :departments="departments" />
|
|
462
|
+
<KnowledgePanel :ticket-reference="ticket.reference" />
|
|
463
|
+
<!-- Plugin: ticket show sidebar slot -->
|
|
464
|
+
<PluginSlot slot="ticket.show.sidebar" :components="getPageComponents('ticket.show', 'sidebar')" />
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
<!-- Context Panel -->
|
|
468
|
+
<ContextPanel v-model:visible="showContextPanel">
|
|
469
|
+
<ContextPanelSection
|
|
470
|
+
title="Customer Info"
|
|
471
|
+
icon="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
|
|
472
|
+
>
|
|
473
|
+
<div class="space-y-2 text-sm">
|
|
474
|
+
<div class="flex justify-between">
|
|
475
|
+
<span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Name</span>
|
|
476
|
+
<span :class="escDark ? 'text-neutral-200' : 'text-gray-900'">{{
|
|
477
|
+
ticket.requester?.name || 'Unknown'
|
|
478
|
+
}}</span>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="flex justify-between">
|
|
481
|
+
<span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Email</span>
|
|
482
|
+
<span :class="['truncate', escDark ? 'text-neutral-200' : 'text-gray-900']">{{
|
|
483
|
+
ticket.requester?.email || '---'
|
|
484
|
+
}}</span>
|
|
485
|
+
</div>
|
|
486
|
+
<div class="flex justify-between">
|
|
487
|
+
<span :class="escDark ? 'text-neutral-500' : 'text-gray-500'">Tickets</span>
|
|
488
|
+
<span :class="escDark ? 'text-neutral-200' : 'text-gray-900'">{{
|
|
489
|
+
ticket.requester_ticket_count ?? '---'
|
|
490
|
+
}}</span>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
</ContextPanelSection>
|
|
494
|
+
|
|
495
|
+
<ContextPanelSection
|
|
496
|
+
title="Related Tickets"
|
|
497
|
+
icon="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
|
498
|
+
:default-open="false"
|
|
499
|
+
>
|
|
500
|
+
<div v-if="ticket.related_tickets?.length" class="space-y-2">
|
|
501
|
+
<div
|
|
502
|
+
v-for="rt in ticket.related_tickets"
|
|
503
|
+
:key="rt.reference"
|
|
504
|
+
:class="[
|
|
505
|
+
'rounded-lg border px-3 py-2',
|
|
506
|
+
escDark ? 'border-white/[0.06] bg-neutral-950' : 'border-gray-200 bg-gray-50',
|
|
507
|
+
]"
|
|
508
|
+
>
|
|
509
|
+
<span :class="['font-mono text-xs', escDark ? 'text-cyan-400' : 'text-blue-600']">{{
|
|
510
|
+
rt.reference
|
|
511
|
+
}}</span>
|
|
512
|
+
<p :class="['mt-0.5 truncate text-xs', escDark ? 'text-neutral-400' : 'text-gray-500']">
|
|
513
|
+
{{ rt.subject }}
|
|
514
|
+
</p>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
<p v-else :class="['text-xs', escDark ? 'text-neutral-600' : 'text-gray-400']">
|
|
518
|
+
No related tickets found.
|
|
519
|
+
</p>
|
|
520
|
+
</ContextPanelSection>
|
|
521
|
+
|
|
522
|
+
<ContextPanelSection
|
|
523
|
+
title="Recent Activity"
|
|
524
|
+
icon="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
525
|
+
:default-open="false"
|
|
526
|
+
>
|
|
527
|
+
<div v-if="ticket.activities?.length" class="space-y-2">
|
|
528
|
+
<div v-for="activity in ticket.activities.slice(0, 5)" :key="activity.id" class="text-xs">
|
|
529
|
+
<span :class="escDark ? 'text-neutral-500' : 'text-gray-600'">{{ activity.description }}</span>
|
|
530
|
+
<span :class="['ml-1', escDark ? 'text-neutral-600' : 'text-gray-400']">{{
|
|
531
|
+
activity.created_at_human
|
|
532
|
+
}}</span>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
<p v-else :class="['text-xs', escDark ? 'text-neutral-600' : 'text-gray-400']">No recent activity.</p>
|
|
536
|
+
</ContextPanelSection>
|
|
537
|
+
|
|
538
|
+
<!-- Plugin: context panel slot -->
|
|
539
|
+
<PluginSlot
|
|
540
|
+
slot="ticket.show.context-panel"
|
|
541
|
+
:components="getPageComponents('ticket.show', 'context-panel')"
|
|
542
|
+
/>
|
|
543
|
+
</ContextPanel>
|
|
544
|
+
|
|
545
|
+
<KeyboardShortcutHelp v-model:show="showShortcutHelp" context="detail" />
|
|
546
|
+
</EscalatedLayout>
|
|
547
|
+
</template>
|