@hed-hog/ticket 0.0.285

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.
Files changed (134) hide show
  1. package/README.md +480 -0
  2. package/dist/dto/assign-ticket-owner.dto.d.ts +4 -0
  3. package/dist/dto/assign-ticket-owner.dto.d.ts.map +1 -0
  4. package/dist/dto/assign-ticket-owner.dto.js +7 -0
  5. package/dist/dto/assign-ticket-owner.dto.js.map +1 -0
  6. package/dist/dto/create-ticket-channel-account.dto.d.ts +10 -0
  7. package/dist/dto/create-ticket-channel-account.dto.d.ts.map +1 -0
  8. package/dist/dto/create-ticket-channel-account.dto.js +7 -0
  9. package/dist/dto/create-ticket-channel-account.dto.js.map +1 -0
  10. package/dist/dto/create-ticket-channel.dto.d.ts +7 -0
  11. package/dist/dto/create-ticket-channel.dto.d.ts.map +1 -0
  12. package/dist/dto/create-ticket-channel.dto.js +7 -0
  13. package/dist/dto/create-ticket-channel.dto.js.map +1 -0
  14. package/dist/dto/create-ticket-internal-note.dto.d.ts +4 -0
  15. package/dist/dto/create-ticket-internal-note.dto.d.ts.map +1 -0
  16. package/dist/dto/create-ticket-internal-note.dto.js +7 -0
  17. package/dist/dto/create-ticket-internal-note.dto.js.map +1 -0
  18. package/dist/dto/create-ticket-reply.dto.d.ts +5 -0
  19. package/dist/dto/create-ticket-reply.dto.d.ts.map +1 -0
  20. package/dist/dto/create-ticket-reply.dto.js +7 -0
  21. package/dist/dto/create-ticket-reply.dto.js.map +1 -0
  22. package/dist/dto/update-ticket-channel-account.dto.d.ts +8 -0
  23. package/dist/dto/update-ticket-channel-account.dto.d.ts.map +1 -0
  24. package/dist/dto/update-ticket-channel-account.dto.js +7 -0
  25. package/dist/dto/update-ticket-channel-account.dto.js.map +1 -0
  26. package/dist/dto/update-ticket-channel.dto.d.ts +5 -0
  27. package/dist/dto/update-ticket-channel.dto.d.ts.map +1 -0
  28. package/dist/dto/update-ticket-channel.dto.js +7 -0
  29. package/dist/dto/update-ticket-channel.dto.js.map +1 -0
  30. package/dist/dto/update-ticket-priority.dto.d.ts +4 -0
  31. package/dist/dto/update-ticket-priority.dto.d.ts.map +1 -0
  32. package/dist/dto/update-ticket-priority.dto.js +7 -0
  33. package/dist/dto/update-ticket-priority.dto.js.map +1 -0
  34. package/dist/dto/update-ticket-settings.dto.d.ts +13 -0
  35. package/dist/dto/update-ticket-settings.dto.d.ts.map +1 -0
  36. package/dist/dto/update-ticket-settings.dto.js +7 -0
  37. package/dist/dto/update-ticket-settings.dto.js.map +1 -0
  38. package/dist/dto/update-ticket-status.dto.d.ts +4 -0
  39. package/dist/dto/update-ticket-status.dto.d.ts.map +1 -0
  40. package/dist/dto/update-ticket-status.dto.js +7 -0
  41. package/dist/dto/update-ticket-status.dto.js.map +1 -0
  42. package/dist/index.d.ts +19 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +35 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/ticket-ai-review.controller.d.ts +9 -0
  47. package/dist/ticket-ai-review.controller.d.ts.map +1 -0
  48. package/dist/ticket-ai-review.controller.js +36 -0
  49. package/dist/ticket-ai-review.controller.js.map +1 -0
  50. package/dist/ticket-channel-accounts.controller.d.ts +24 -0
  51. package/dist/ticket-channel-accounts.controller.d.ts.map +1 -0
  52. package/dist/ticket-channel-accounts.controller.js +72 -0
  53. package/dist/ticket-channel-accounts.controller.js.map +1 -0
  54. package/dist/ticket-channels.controller.d.ts +21 -0
  55. package/dist/ticket-channels.controller.d.ts.map +1 -0
  56. package/dist/ticket-channels.controller.js +72 -0
  57. package/dist/ticket-channels.controller.js.map +1 -0
  58. package/dist/ticket-data.controller.d.ts +24 -0
  59. package/dist/ticket-data.controller.d.ts.map +1 -0
  60. package/dist/ticket-data.controller.js +81 -0
  61. package/dist/ticket-data.controller.js.map +1 -0
  62. package/dist/ticket-items.controller.d.ts +35 -0
  63. package/dist/ticket-items.controller.d.ts.map +1 -0
  64. package/dist/ticket-items.controller.js +100 -0
  65. package/dist/ticket-items.controller.js.map +1 -0
  66. package/dist/ticket-settings.controller.d.ts +11 -0
  67. package/dist/ticket-settings.controller.d.ts.map +1 -0
  68. package/dist/ticket-settings.controller.js +50 -0
  69. package/dist/ticket-settings.controller.js.map +1 -0
  70. package/dist/ticket.module.d.ts +3 -0
  71. package/dist/ticket.module.d.ts.map +1 -0
  72. package/dist/ticket.module.js +45 -0
  73. package/dist/ticket.module.js.map +1 -0
  74. package/dist/ticket.service.d.ts +88 -0
  75. package/dist/ticket.service.d.ts.map +1 -0
  76. package/dist/ticket.service.js +83 -0
  77. package/dist/ticket.service.js.map +1 -0
  78. package/hedhog/data/menu.yaml +164 -0
  79. package/hedhog/data/role.yaml +23 -0
  80. package/hedhog/data/route.yaml +225 -0
  81. package/hedhog/data/setting_group.yaml +574 -0
  82. package/hedhog/frontend/app/[id]/page.tsx.ejs +360 -0
  83. package/hedhog/frontend/app/_components/ai-draft-editor-dialog.tsx.ejs +67 -0
  84. package/hedhog/frontend/app/_components/ticket-badges.tsx.ejs +65 -0
  85. package/hedhog/frontend/app/_components/ticket-context-card.tsx.ejs +80 -0
  86. package/hedhog/frontend/app/_components/ticket-timeline.tsx.ejs +59 -0
  87. package/hedhog/frontend/app/_lib/mock-data.ts.ejs +474 -0
  88. package/hedhog/frontend/app/_lib/types.ts.ejs +134 -0
  89. package/hedhog/frontend/app/ai-review/page.tsx.ejs +205 -0
  90. package/hedhog/frontend/app/channel-accounts/page.tsx.ejs +476 -0
  91. package/hedhog/frontend/app/channels/page.tsx.ejs +406 -0
  92. package/hedhog/frontend/app/dashboard/page.tsx.ejs +178 -0
  93. package/hedhog/frontend/app/inbox/page.tsx.ejs +500 -0
  94. package/hedhog/frontend/app/page.tsx.ejs +5 -0
  95. package/hedhog/frontend/app/reports/page.tsx.ejs +151 -0
  96. package/hedhog/frontend/app/roles/page.tsx.ejs +110 -0
  97. package/hedhog/frontend/app/settings/page.tsx.ejs +484 -0
  98. package/hedhog/frontend/messages/en.json +412 -0
  99. package/hedhog/frontend/messages/pt.json +412 -0
  100. package/hedhog/table/channel.yaml +23 -0
  101. package/hedhog/table/channel_account.yaml +34 -0
  102. package/hedhog/table/ticket.yaml +73 -0
  103. package/hedhog/table/ticket_ai_draft.yaml +51 -0
  104. package/hedhog/table/ticket_attachment.yaml +40 -0
  105. package/hedhog/table/ticket_category.yaml +22 -0
  106. package/hedhog/table/ticket_context.yaml +64 -0
  107. package/hedhog/table/ticket_message.yaml +49 -0
  108. package/hedhog/table/ticket_message_source.yaml +50 -0
  109. package/hedhog/table/ticket_metadata.yaml +21 -0
  110. package/hedhog/table/ticket_participant.yaml +25 -0
  111. package/hedhog/table/ticket_source.yaml +43 -0
  112. package/hedhog/table/ticket_tag.yaml +22 -0
  113. package/package.json +40 -0
  114. package/src/dto/assign-ticket-owner.dto.ts +3 -0
  115. package/src/dto/create-ticket-channel-account.dto.ts +9 -0
  116. package/src/dto/create-ticket-channel.dto.ts +6 -0
  117. package/src/dto/create-ticket-internal-note.dto.ts +3 -0
  118. package/src/dto/create-ticket-reply.dto.ts +4 -0
  119. package/src/dto/update-ticket-channel-account.dto.ts +7 -0
  120. package/src/dto/update-ticket-channel.dto.ts +4 -0
  121. package/src/dto/update-ticket-priority.dto.ts +3 -0
  122. package/src/dto/update-ticket-settings.dto.ts +12 -0
  123. package/src/dto/update-ticket-status.dto.ts +3 -0
  124. package/src/index.ts +19 -0
  125. package/src/language/en.json +8 -0
  126. package/src/language/pt.json +8 -0
  127. package/src/ticket-ai-review.controller.ts +14 -0
  128. package/src/ticket-channel-accounts.controller.ts +34 -0
  129. package/src/ticket-channels.controller.ts +31 -0
  130. package/src/ticket-data.controller.ts +39 -0
  131. package/src/ticket-items.controller.ts +56 -0
  132. package/src/ticket-settings.controller.ts +20 -0
  133. package/src/ticket.module.ts +32 -0
  134. package/src/ticket.service.ts +102 -0
@@ -0,0 +1,500 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Page,
5
+ PageHeader,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ StatsCards,
9
+ } from '@/components/entity-list';
10
+ import { Badge } from '@/components/ui/badge';
11
+ import { Button } from '@/components/ui/button';
12
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
13
+ import {
14
+ Table,
15
+ TableBody,
16
+ TableCell,
17
+ TableHead,
18
+ TableHeader,
19
+ TableRow,
20
+ } from '@/components/ui/table';
21
+ import Link from 'next/link';
22
+ import { useMemo, useState } from 'react';
23
+ import { useTranslations } from 'next-intl';
24
+ import {
25
+ TicketAiReviewModeBadge,
26
+ TicketPriorityBadge,
27
+ TicketStatusBadge,
28
+ } from '../_components/ticket-badges';
29
+ import {
30
+ dashboardSummary,
31
+ getChannelAccountById,
32
+ getChannelById,
33
+ getPersonById,
34
+ getTicketContextByTicketId,
35
+ ticketChannels,
36
+ ticketItems,
37
+ ticketPeople,
38
+ } from '../_lib/mock-data';
39
+
40
+ export default function TicketInboxPage() {
41
+ const t = useTranslations('ticket.TicketModule');
42
+ const [search, setSearch] = useState('');
43
+ const [status, setStatus] = useState('all');
44
+ const [priority, setPriority] = useState('all');
45
+ const [channel, setChannel] = useState('all');
46
+ const [owner, setOwner] = useState('all');
47
+ const [requester, setRequester] = useState('all');
48
+ const [course, setCourse] = useState('all');
49
+ const [section, setSection] = useState('all');
50
+ const [lesson, setLesson] = useState('all');
51
+ const [video, setVideo] = useState('all');
52
+ const [aiStatus, setAiStatus] = useState('all');
53
+ const [dateStart, setDateStart] = useState('');
54
+ const [dateEnd, setDateEnd] = useState('');
55
+ const [tags, setTags] = useState('all');
56
+ const [categories, setCategories] = useState('all');
57
+ const [quick, setQuick] = useState('all');
58
+ const [page, setPage] = useState(1);
59
+ const [pageSize, setPageSize] = useState(12);
60
+
61
+ const filtered = useMemo(() => {
62
+ return ticketItems.filter((item) => {
63
+ const text = `${item.code} ${item.subject}`.toLowerCase();
64
+ const context = getTicketContextByTicketId(item.id);
65
+
66
+ const aiStatusValue =
67
+ item.aiReviewMode === 'required'
68
+ ? 'pending_review'
69
+ : item.aiReviewMode === 'optional'
70
+ ? 'approved'
71
+ : 'all';
72
+
73
+ const itemDate = item.lastActivity.slice(0, 10);
74
+ const dateStartMatch = !dateStart || itemDate >= dateStart;
75
+ const dateEndMatch = !dateEnd || itemDate <= dateEnd;
76
+
77
+ const quickMatch =
78
+ quick === 'all' ||
79
+ (quick === 'my' && item.ownerId === 101) ||
80
+ (quick === 'urgent' && item.priority === 'urgent') ||
81
+ (quick === 'open' && item.status === 'open') ||
82
+ (quick === 'waiting_customer' && item.status === 'waiting_customer') ||
83
+ (quick === 'resolved' && item.status === 'resolved');
84
+
85
+ return (
86
+ text.includes(search.toLowerCase()) &&
87
+ (status === 'all' || item.status === status) &&
88
+ (priority === 'all' || item.priority === priority) &&
89
+ (channel === 'all' || String(item.channelId) === channel) &&
90
+ (owner === 'all' || String(item.ownerId) === owner) &&
91
+ (requester === 'all' || String(item.requesterId) === requester) &&
92
+ (course === 'all' || context?.courseName?.toLowerCase() === course) &&
93
+ (section === 'all' ||
94
+ context?.sectionName?.toLowerCase() === section) &&
95
+ (lesson === 'all' || context?.lessonName?.toLowerCase() === lesson) &&
96
+ (video === 'all' || context?.videoName?.toLowerCase() === video) &&
97
+ (aiStatus === 'all' || aiStatusValue === aiStatus) &&
98
+ (tags === 'all' || item.tags.includes(tags)) &&
99
+ (categories === 'all' || item.categories.includes(categories)) &&
100
+ dateStartMatch &&
101
+ dateEndMatch &&
102
+ quickMatch
103
+ );
104
+ });
105
+ }, [
106
+ search,
107
+ status,
108
+ priority,
109
+ channel,
110
+ owner,
111
+ requester,
112
+ course,
113
+ section,
114
+ lesson,
115
+ video,
116
+ aiStatus,
117
+ tags,
118
+ categories,
119
+ dateStart,
120
+ dateEnd,
121
+ quick,
122
+ ]);
123
+
124
+ const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
125
+ const safePage = Math.min(page, totalPages);
126
+ const paged = filtered.slice((safePage - 1) * pageSize, safePage * pageSize);
127
+
128
+ return (
129
+ <Page>
130
+ <PageHeader
131
+ title={t('inbox.title')}
132
+ description={t('inbox.description')}
133
+ breadcrumbs={[
134
+ { label: t('common.home'), href: '/' },
135
+ { label: t('common.tickets'), href: '/ticket' },
136
+ { label: t('inbox.breadcrumb') },
137
+ ]}
138
+ actions={[
139
+ {
140
+ label: t('inbox.newTicket'),
141
+ onClick: () => setSearch(''),
142
+ },
143
+ ]}
144
+ />
145
+
146
+ <StatsCards
147
+ stats={[
148
+ {
149
+ title: t('inbox.stats.open'),
150
+ value: dashboardSummary.totalOpen,
151
+ icon: <span>O</span>,
152
+ },
153
+ {
154
+ title: t('inbox.stats.pending'),
155
+ value: dashboardSummary.pending,
156
+ icon: <span>P</span>,
157
+ },
158
+ {
159
+ title: t('inbox.stats.waitingCustomer'),
160
+ value: dashboardSummary.waitingCustomer,
161
+ icon: <span>W</span>,
162
+ },
163
+ {
164
+ title: t('inbox.stats.urgent'),
165
+ value: dashboardSummary.urgent,
166
+ icon: <span>U</span>,
167
+ },
168
+ ]}
169
+ />
170
+
171
+ <div className="flex flex-wrap gap-2">
172
+ {[
173
+ { key: 'all', label: t('inbox.quick.all') },
174
+ { key: 'my', label: t('inbox.quick.my') },
175
+ { key: 'open', label: t('inbox.quick.open') },
176
+ { key: 'waiting_customer', label: t('inbox.quick.waiting_customer') },
177
+ { key: 'resolved', label: t('inbox.quick.resolved') },
178
+ { key: 'urgent', label: t('inbox.quick.urgent') },
179
+ ].map((tab) => (
180
+ <Button
181
+ key={tab.key}
182
+ variant={quick === tab.key ? 'default' : 'outline'}
183
+ size="sm"
184
+ onClick={() => {
185
+ setQuick(tab.key);
186
+ setPage(1);
187
+ }}
188
+ >
189
+ {tab.label}
190
+ </Button>
191
+ ))}
192
+ </div>
193
+
194
+ <SearchBar
195
+ searchQuery={search}
196
+ onSearchChange={setSearch}
197
+ onSearch={() => setPage(1)}
198
+ placeholder={t('inbox.searchPlaceholder')}
199
+ controls={[
200
+ {
201
+ id: 'status',
202
+ type: 'select',
203
+ value: status,
204
+ onChange: setStatus,
205
+ placeholder: t('inbox.filters.status'),
206
+ options: [
207
+ { value: 'all', label: t('inbox.filters.allStatus') },
208
+ { value: 'open', label: t('badges.status.open') },
209
+ { value: 'pending', label: t('badges.status.pending') },
210
+ {
211
+ value: 'waiting_customer',
212
+ label: t('badges.status.waiting_customer'),
213
+ },
214
+ { value: 'resolved', label: t('badges.status.resolved') },
215
+ ],
216
+ },
217
+ {
218
+ id: 'priority',
219
+ type: 'select',
220
+ value: priority,
221
+ onChange: setPriority,
222
+ placeholder: t('inbox.filters.priority'),
223
+ options: [
224
+ { value: 'all', label: t('inbox.filters.allPriority') },
225
+ { value: 'low', label: t('badges.priority.low') },
226
+ { value: 'normal', label: t('badges.priority.normal') },
227
+ { value: 'high', label: t('badges.priority.high') },
228
+ { value: 'urgent', label: t('badges.priority.urgent') },
229
+ ],
230
+ },
231
+ {
232
+ id: 'channel',
233
+ type: 'select',
234
+ value: channel,
235
+ onChange: setChannel,
236
+ placeholder: t('inbox.filters.channel'),
237
+ options: [
238
+ { value: 'all', label: t('inbox.filters.allChannels') },
239
+ ...ticketChannels.map((item) => ({
240
+ value: String(item.id),
241
+ label: item.name,
242
+ })),
243
+ ],
244
+ },
245
+ {
246
+ id: 'owner',
247
+ type: 'select',
248
+ value: owner,
249
+ onChange: (value) => {
250
+ setOwner(value);
251
+ setPage(1);
252
+ },
253
+ placeholder: t('inbox.filters.owner'),
254
+ options: [
255
+ { value: 'all', label: t('inbox.filters.allOwners') },
256
+ ...ticketPeople
257
+ .filter((item) => item.roleLabel !== 'Requester')
258
+ .map((item) => ({ value: String(item.id), label: item.name })),
259
+ ],
260
+ },
261
+ {
262
+ id: 'requester',
263
+ type: 'select',
264
+ value: requester,
265
+ onChange: (value) => {
266
+ setRequester(value);
267
+ setPage(1);
268
+ },
269
+ placeholder: t('inbox.filters.requester'),
270
+ options: [
271
+ { value: 'all', label: t('inbox.filters.allRequesters') },
272
+ ...ticketPeople
273
+ .filter((item) => item.roleLabel === 'Requester')
274
+ .map((item) => ({ value: String(item.id), label: item.name })),
275
+ ],
276
+ },
277
+ {
278
+ id: 'course',
279
+ type: 'select',
280
+ value: course,
281
+ onChange: (value) => {
282
+ setCourse(value);
283
+ setPage(1);
284
+ },
285
+ placeholder: t('inbox.filters.course'),
286
+ options: [
287
+ { value: 'all', label: t('inbox.filters.allCourses') },
288
+ { value: 'nestjs', label: 'NestJS' },
289
+ { value: 'sql completo', label: 'SQL Completo' },
290
+ { value: 'php', label: 'PHP' },
291
+ ],
292
+ },
293
+ {
294
+ id: 'section',
295
+ type: 'select',
296
+ value: section,
297
+ onChange: (value) => {
298
+ setSection(value);
299
+ setPage(1);
300
+ },
301
+ placeholder: t('inbox.filters.section'),
302
+ options: [
303
+ { value: 'all', label: t('inbox.filters.allSections') },
304
+ { value: 'auth', label: 'Autenticacao' },
305
+ { value: 'advanced-sql', label: 'Consultas Avancadas' },
306
+ ],
307
+ },
308
+ {
309
+ id: 'lesson',
310
+ type: 'select',
311
+ value: lesson,
312
+ onChange: (value) => {
313
+ setLesson(value);
314
+ setPage(1);
315
+ },
316
+ placeholder: t('inbox.filters.lesson'),
317
+ options: [
318
+ { value: 'all', label: t('inbox.filters.allLessons') },
319
+ {
320
+ value: 'jwt com refresh token',
321
+ label: 'JWT com Refresh Token',
322
+ },
323
+ {
324
+ value: 'inner join e left join',
325
+ label: 'INNER JOIN e LEFT JOIN',
326
+ },
327
+ ],
328
+ },
329
+ {
330
+ id: 'video',
331
+ type: 'select',
332
+ value: video,
333
+ onChange: (value) => {
334
+ setVideo(value);
335
+ setPage(1);
336
+ },
337
+ placeholder: t('inbox.filters.video'),
338
+ options: [
339
+ { value: 'all', label: t('inbox.filters.allVideos') },
340
+ {
341
+ value: 'curso de php - aula 10',
342
+ label: 'Curso de PHP - Aula 10',
343
+ },
344
+ ],
345
+ },
346
+ {
347
+ id: 'ai-status',
348
+ type: 'select',
349
+ value: aiStatus,
350
+ onChange: (value) => {
351
+ setAiStatus(value);
352
+ setPage(1);
353
+ },
354
+ placeholder: t('inbox.filters.aiStatus'),
355
+ options: [
356
+ { value: 'all', label: t('inbox.filters.allAiStatus') },
357
+ { value: 'pending_review', label: t('inbox.filters.pendingReview') },
358
+ { value: 'approved', label: t('inbox.filters.approved') },
359
+ ],
360
+ },
361
+ {
362
+ id: 'date-start',
363
+ type: 'date',
364
+ value: dateStart,
365
+ onChange: (value) => {
366
+ setDateStart(value);
367
+ setPage(1);
368
+ },
369
+ },
370
+ {
371
+ id: 'date-end',
372
+ type: 'date',
373
+ value: dateEnd,
374
+ onChange: (value) => {
375
+ setDateEnd(value);
376
+ setPage(1);
377
+ },
378
+ },
379
+ {
380
+ id: 'tags',
381
+ type: 'select',
382
+ value: tags,
383
+ onChange: (value) => {
384
+ setTags(value);
385
+ setPage(1);
386
+ },
387
+ placeholder: t('inbox.filters.tags'),
388
+ options: [
389
+ { value: 'all', label: t('inbox.filters.allTags') },
390
+ { value: 'nestjs', label: 'nestjs' },
391
+ { value: 'sql', label: 'sql' },
392
+ { value: 'financeiro', label: 'financeiro' },
393
+ ],
394
+ },
395
+ {
396
+ id: 'categories',
397
+ type: 'select',
398
+ value: categories,
399
+ onChange: (value) => {
400
+ setCategories(value);
401
+ setPage(1);
402
+ },
403
+ placeholder: t('inbox.filters.categories'),
404
+ options: [
405
+ { value: 'all', label: t('inbox.filters.allCategories') },
406
+ { value: 'duvida_tecnica', label: 'duvida_tecnica' },
407
+ { value: 'certificacao', label: 'certificacao' },
408
+ { value: 'financeiro', label: 'financeiro' },
409
+ ],
410
+ },
411
+ ]}
412
+ />
413
+
414
+ <Card>
415
+ <CardHeader>
416
+ <CardTitle className="text-base">{t('inbox.table.title')}</CardTitle>
417
+ </CardHeader>
418
+ <CardContent className="space-y-4">
419
+ <Table>
420
+ <TableHeader>
421
+ <TableRow>
422
+ <TableHead>{t('common.code')}</TableHead>
423
+ <TableHead>{t('inbox.table.subject')}</TableHead>
424
+ <TableHead>{t('common.requester')}</TableHead>
425
+ <TableHead>{t('common.channel')}</TableHead>
426
+ <TableHead>{t('inbox.table.channelAccount')}</TableHead>
427
+ <TableHead>{t('common.context')}</TableHead>
428
+ <TableHead>{t('common.owner')}</TableHead>
429
+ <TableHead>{t('common.priority')}</TableHead>
430
+ <TableHead>{t('common.status')}</TableHead>
431
+ <TableHead>{t('inbox.table.lastActivity')}</TableHead>
432
+ <TableHead>{t('inbox.table.aiReview')}</TableHead>
433
+ </TableRow>
434
+ </TableHeader>
435
+ <TableBody>
436
+ {paged.map((item) => {
437
+ const requester = getPersonById(item.requesterId);
438
+ const owner = getPersonById(item.ownerId);
439
+ const channelItem = getChannelById(item.channelId);
440
+ const account = getChannelAccountById(item.channelAccountId);
441
+ const context = getTicketContextByTicketId(item.id);
442
+
443
+ return (
444
+ <TableRow key={item.id}>
445
+ <TableCell>
446
+ <Link
447
+ className="text-blue-600 hover:underline"
448
+ href={`/ticket/${item.id}`}
449
+ >
450
+ {item.code}
451
+ </Link>
452
+ </TableCell>
453
+ <TableCell className="max-w-[280px]">
454
+ <div className="font-medium">{item.subject}</div>
455
+ <div className="text-xs text-muted-foreground">
456
+ {item.tags.map((tag) => (
457
+ <Badge key={tag} variant="outline" className="mr-1">
458
+ {tag}
459
+ </Badge>
460
+ ))}
461
+ </div>
462
+ </TableCell>
463
+ <TableCell>{requester?.name}</TableCell>
464
+ <TableCell>{channelItem?.name}</TableCell>
465
+ <TableCell>{account?.code}</TableCell>
466
+ <TableCell>
467
+ {context?.courseName ||
468
+ context?.videoName ||
469
+ context?.pageName ||
470
+ '-'}
471
+ </TableCell>
472
+ <TableCell>{owner?.name}</TableCell>
473
+ <TableCell>
474
+ <TicketPriorityBadge value={item.priority} />
475
+ </TableCell>
476
+ <TableCell>
477
+ <TicketStatusBadge value={item.status} />
478
+ </TableCell>
479
+ <TableCell>{item.lastActivity}</TableCell>
480
+ <TableCell>
481
+ <TicketAiReviewModeBadge value={item.aiReviewMode} />
482
+ </TableCell>
483
+ </TableRow>
484
+ );
485
+ })}
486
+ </TableBody>
487
+ </Table>
488
+
489
+ <PaginationFooter
490
+ currentPage={safePage}
491
+ pageSize={pageSize}
492
+ totalItems={filtered.length}
493
+ onPageChange={setPage}
494
+ onPageSizeChange={setPageSize}
495
+ />
496
+ </CardContent>
497
+ </Card>
498
+ </Page>
499
+ );
500
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ export default function TicketIndexPage() {
4
+ redirect('/ticket/dashboard');
5
+ }
@@ -0,0 +1,151 @@
1
+ 'use client';
2
+
3
+ import { Page, PageHeader, StatsCards } from '@/components/entity-list';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { useTranslations } from 'next-intl';
6
+ import {
7
+ dashboardSummary,
8
+ ticketsByChannel,
9
+ ticketsByCourse,
10
+ } from '../_lib/mock-data';
11
+
12
+ const byOperator = [
13
+ { label: 'Camila Moraes', value: 51, color: 'bg-sky-500' },
14
+ { label: 'Bruno Araujo', value: 43, color: 'bg-emerald-500' },
15
+ { label: 'Rodrigo Pires', value: 31, color: 'bg-orange-500' },
16
+ ];
17
+
18
+ export default function TicketReportsPage() {
19
+ const t = useTranslations('ticket.TicketModule');
20
+ const maxOperator = Math.max(...byOperator.map((item) => item.value));
21
+
22
+ return (
23
+ <Page>
24
+ <PageHeader
25
+ title={t('reports.title')}
26
+ description={t('reports.description')}
27
+ breadcrumbs={[
28
+ { label: t('common.home'), href: '/' },
29
+ { label: t('common.tickets'), href: '/ticket' },
30
+ { label: t('reports.breadcrumb') },
31
+ ]}
32
+ />
33
+
34
+ <StatsCards
35
+ stats={[
36
+ {
37
+ title: t('reports.stats.firstResponse'),
38
+ value: '11m',
39
+ icon: <span>FR</span>,
40
+ },
41
+ {
42
+ title: t('reports.stats.resolution'),
43
+ value: '6h 23m',
44
+ icon: <span>RT</span>,
45
+ },
46
+ {
47
+ title: t('reports.stats.approvalRate'),
48
+ value: '78%',
49
+ icon: <span>AI</span>,
50
+ },
51
+ {
52
+ title: t('reports.stats.usageByChannel'),
53
+ value: '64%',
54
+ icon: <span>UC</span>,
55
+ },
56
+ ]}
57
+ />
58
+
59
+ <div className="grid gap-4 lg:grid-cols-2">
60
+ <Card>
61
+ <CardHeader>
62
+ <CardTitle className="text-base">{t('reports.cards.byChannel')}</CardTitle>
63
+ </CardHeader>
64
+ <CardContent className="space-y-3">
65
+ {ticketsByChannel.map((item) => (
66
+ <div key={item.label}>
67
+ <div className="mb-1 flex items-center justify-between text-sm">
68
+ <span>{item.label}</span>
69
+ <span className="font-semibold">{item.value}</span>
70
+ </div>
71
+ <div className="h-2 rounded bg-slate-100">
72
+ <div
73
+ className={`h-2 rounded ${item.color}`}
74
+ style={{ width: `${item.value}%` }}
75
+ />
76
+ </div>
77
+ </div>
78
+ ))}
79
+ </CardContent>
80
+ </Card>
81
+
82
+ <Card>
83
+ <CardHeader>
84
+ <CardTitle className="text-base">{t('reports.cards.byCourse')}</CardTitle>
85
+ </CardHeader>
86
+ <CardContent className="space-y-2">
87
+ {ticketsByCourse.map((item) => (
88
+ <div
89
+ key={item.label}
90
+ className="flex items-center justify-between rounded border p-2 text-sm"
91
+ >
92
+ <span>{item.label}</span>
93
+ <span className="font-semibold">{item.value}</span>
94
+ </div>
95
+ ))}
96
+ </CardContent>
97
+ </Card>
98
+
99
+ <Card>
100
+ <CardHeader>
101
+ <CardTitle className="text-base">{t('reports.cards.byLesson')}</CardTitle>
102
+ </CardHeader>
103
+ <CardContent className="space-y-2 text-sm">
104
+ <div className="rounded border p-2">JWT com Refresh Token: 22</div>
105
+ <div className="rounded border p-2">INNER JOIN e LEFT JOIN: 17</div>
106
+ <div className="rounded border p-2">NestJS Guards e Roles: 14</div>
107
+ <div className="rounded border p-2">Curso de PHP - Aula 10: 9</div>
108
+ </CardContent>
109
+ </Card>
110
+
111
+ <Card>
112
+ <CardHeader>
113
+ <CardTitle className="text-base">{t('reports.cards.byOperator')}</CardTitle>
114
+ </CardHeader>
115
+ <CardContent className="space-y-3">
116
+ {byOperator.map((item) => (
117
+ <div key={item.label}>
118
+ <div className="mb-1 flex items-center justify-between text-sm">
119
+ <span>{item.label}</span>
120
+ <span className="font-semibold">{item.value}</span>
121
+ </div>
122
+ <div className="h-2 rounded bg-slate-100">
123
+ <div
124
+ className={`h-2 rounded ${item.color}`}
125
+ style={{
126
+ width: `${Math.round((item.value / maxOperator) * 100)}%`,
127
+ }}
128
+ />
129
+ </div>
130
+ </div>
131
+ ))}
132
+ </CardContent>
133
+ </Card>
134
+ </div>
135
+
136
+ <Card>
137
+ <CardHeader>
138
+ <CardTitle className="text-base">{t('reports.cards.summary')}</CardTitle>
139
+ </CardHeader>
140
+ <CardContent className="grid gap-2 text-sm md:grid-cols-2 lg:grid-cols-3">
141
+ <div className="rounded border p-3">{t('reports.summary.open', { value: dashboardSummary.totalOpen })}</div>
142
+ <div className="rounded border p-3">{t('reports.summary.pending', { value: dashboardSummary.pending })}</div>
143
+ <div className="rounded border p-3">{t('reports.summary.waiting', { value: dashboardSummary.waitingCustomer })}</div>
144
+ <div className="rounded border p-3">{t('reports.summary.resolved', { value: dashboardSummary.resolvedToday })}</div>
145
+ <div className="rounded border p-3">{t('reports.summary.urgent', { value: dashboardSummary.urgent })}</div>
146
+ <div className="rounded border p-3">{t('reports.summary.aiPending', { value: dashboardSummary.aiPendingReview })}</div>
147
+ </CardContent>
148
+ </Card>
149
+ </Page>
150
+ );
151
+ }