@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,406 @@
1
+ 'use client';
2
+
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ SearchBar,
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
+ Form,
15
+ FormControl,
16
+ FormField,
17
+ FormItem,
18
+ FormLabel,
19
+ FormMessage,
20
+ } from '@/components/ui/form';
21
+ import { Input } from '@/components/ui/input';
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from '@/components/ui/select';
29
+ import {
30
+ Sheet,
31
+ SheetContent,
32
+ SheetDescription,
33
+ SheetFooter,
34
+ SheetHeader,
35
+ SheetTitle,
36
+ } from '@/components/ui/sheet';
37
+ import {
38
+ Table,
39
+ TableBody,
40
+ TableCell,
41
+ TableHead,
42
+ TableHeader,
43
+ TableRow,
44
+ } from '@/components/ui/table';
45
+ import { zodResolver } from '@hookform/resolvers/zod';
46
+ import { Layers } from 'lucide-react';
47
+ import { useTranslations } from 'next-intl';
48
+ import { useMemo, useState } from 'react';
49
+ import { useForm } from 'react-hook-form';
50
+ import { toast } from 'sonner';
51
+ import { z } from 'zod';
52
+ import { ticketChannels } from '../_lib/mock-data';
53
+
54
+ const PAGE_SIZE_OPTIONS = [10, 20, 30, 50];
55
+ const DEFAULT_PAGE_SIZE = PAGE_SIZE_OPTIONS[0] ?? 10;
56
+
57
+ export default function TicketChannelsPage() {
58
+ const t = useTranslations('ticket.TicketModule');
59
+ const [channels, setChannels] = useState(ticketChannels);
60
+ const [search, setSearch] = useState('');
61
+ const [page, setPage] = useState(1);
62
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
63
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
64
+ const [editingId, setEditingId] = useState<number | null>(null);
65
+
66
+ const channelSchema = useMemo(
67
+ () =>
68
+ z.object({
69
+ code: z.string().trim().min(1, t('channels.validation.codeRequired')),
70
+ name: z.string().trim().min(1, t('channels.validation.nameRequired')),
71
+ kind: z.enum([
72
+ 'portal',
73
+ 'marketplace',
74
+ 'social',
75
+ 'form',
76
+ 'email',
77
+ 'whatsapp',
78
+ 'api',
79
+ 'other',
80
+ ]),
81
+ status: z.enum(['active', 'inactive']),
82
+ }),
83
+ [t]
84
+ );
85
+
86
+ type ChannelForm = z.infer<typeof channelSchema>;
87
+
88
+ const form = useForm<ChannelForm>({
89
+ resolver: zodResolver(channelSchema),
90
+ defaultValues: {
91
+ code: '',
92
+ name: '',
93
+ kind: 'portal',
94
+ status: 'active',
95
+ },
96
+ });
97
+
98
+ const filteredChannels = useMemo(() => {
99
+ const query = search.toLowerCase();
100
+ return channels.filter((item) =>
101
+ `${item.code} ${item.name} ${item.kind}`.toLowerCase().includes(query)
102
+ );
103
+ }, [channels, search]);
104
+
105
+ const totalPages = Math.max(1, Math.ceil(filteredChannels.length / pageSize));
106
+ const safePage = Math.min(page, totalPages);
107
+ const paginatedChannels = filteredChannels.slice(
108
+ (safePage - 1) * pageSize,
109
+ safePage * pageSize
110
+ );
111
+
112
+ const handleOpenCreate = () => {
113
+ setEditingId(null);
114
+ form.reset({
115
+ code: '',
116
+ name: '',
117
+ kind: 'portal',
118
+ status: 'active',
119
+ });
120
+ setIsSheetOpen(true);
121
+ };
122
+
123
+ const handleOpenEdit = (id: number) => {
124
+ const current = channels.find((item) => item.id === id);
125
+ if (!current) return;
126
+
127
+ setEditingId(id);
128
+ form.reset({
129
+ code: current.code,
130
+ name: current.name,
131
+ kind: current.kind,
132
+ status: current.status,
133
+ });
134
+ setIsSheetOpen(true);
135
+ };
136
+
137
+ const handleSave = async (values: ChannelForm) => {
138
+ if (editingId) {
139
+ setChannels((current) =>
140
+ current.map((item) =>
141
+ item.id === editingId ? { ...item, ...values } : item
142
+ )
143
+ );
144
+ toast.success(t('channels.messages.updated'));
145
+ } else {
146
+ setChannels((current) => [
147
+ ...current,
148
+ {
149
+ id: Math.max(0, ...current.map((item) => item.id)) + 1,
150
+ ...values,
151
+ },
152
+ ]);
153
+ toast.success(t('channels.messages.created'));
154
+ }
155
+
156
+ setIsSheetOpen(false);
157
+ };
158
+
159
+ const handleToggleStatus = (id: number) => {
160
+ setChannels((current) =>
161
+ current.map((item) =>
162
+ item.id === id
163
+ ? {
164
+ ...item,
165
+ status: item.status === 'active' ? 'inactive' : 'active',
166
+ }
167
+ : item
168
+ )
169
+ );
170
+ };
171
+
172
+ return (
173
+ <Page>
174
+ <PageHeader
175
+ title={t('channels.title')}
176
+ description={t('channels.description')}
177
+ breadcrumbs={[
178
+ { label: t('common.home'), href: '/' },
179
+ { label: t('common.tickets'), href: '/ticket' },
180
+ { label: t('channels.breadcrumb') },
181
+ ]}
182
+ actions={[{ label: t('channels.new'), onClick: handleOpenCreate }]}
183
+ />
184
+
185
+ <SearchBar
186
+ searchQuery={search}
187
+ onSearchChange={(value) => {
188
+ setSearch(value);
189
+ setPage(1);
190
+ }}
191
+ onSearch={() => setPage(1)}
192
+ placeholder={t('channels.searchPlaceholder')}
193
+ />
194
+
195
+ <Card>
196
+ <CardHeader>
197
+ <CardTitle className="text-base">
198
+ {t('channels.tableTitle')}
199
+ </CardTitle>
200
+ </CardHeader>
201
+ <CardContent className="space-y-4">
202
+ {filteredChannels.length === 0 ? (
203
+ <EmptyState
204
+ icon={<Layers className="h-6 w-6" />}
205
+ title={t('channels.emptyState.title')}
206
+ description={t('channels.emptyState.description')}
207
+ actionLabel={t('channels.emptyState.action')}
208
+ onAction={handleOpenCreate}
209
+ />
210
+ ) : (
211
+ <>
212
+ <Table>
213
+ <TableHeader>
214
+ <TableRow>
215
+ <TableHead>{t('common.code')}</TableHead>
216
+ <TableHead>{t('common.name')}</TableHead>
217
+ <TableHead>{t('channels.kind')}</TableHead>
218
+ <TableHead>{t('common.status')}</TableHead>
219
+ <TableHead>{t('common.actions')}</TableHead>
220
+ </TableRow>
221
+ </TableHeader>
222
+ <TableBody>
223
+ {paginatedChannels.map((item) => (
224
+ <TableRow key={item.id}>
225
+ <TableCell className="font-medium">{item.code}</TableCell>
226
+ <TableCell>{item.name}</TableCell>
227
+ <TableCell>{item.kind}</TableCell>
228
+ <TableCell>
229
+ <Badge
230
+ variant={
231
+ item.status === 'active' ? 'default' : 'secondary'
232
+ }
233
+ >
234
+ {item.status === 'active'
235
+ ? t('common.enabled')
236
+ : t('common.disabled')}
237
+ </Badge>
238
+ </TableCell>
239
+ <TableCell>
240
+ <div className="flex gap-2">
241
+ <Button
242
+ size="sm"
243
+ variant="outline"
244
+ onClick={() => handleOpenEdit(item.id)}
245
+ >
246
+ {t('common.edit')}
247
+ </Button>
248
+ <Button
249
+ size="sm"
250
+ variant="outline"
251
+ onClick={() => handleToggleStatus(item.id)}
252
+ >
253
+ {item.status === 'active'
254
+ ? t('channels.disable')
255
+ : t('common.enabled')}
256
+ </Button>
257
+ </div>
258
+ </TableCell>
259
+ </TableRow>
260
+ ))}
261
+ </TableBody>
262
+ </Table>
263
+
264
+ <PaginationFooter
265
+ currentPage={safePage}
266
+ pageSize={pageSize}
267
+ totalItems={filteredChannels.length}
268
+ onPageChange={setPage}
269
+ onPageSizeChange={setPageSize}
270
+ pageSizeOptions={PAGE_SIZE_OPTIONS}
271
+ />
272
+ </>
273
+ )}
274
+ </CardContent>
275
+ </Card>
276
+
277
+ <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
278
+ <SheetContent className="w-full sm:max-w-lg">
279
+ <SheetHeader>
280
+ <SheetTitle>
281
+ {editingId
282
+ ? t('channels.sheet.editTitle')
283
+ : t('channels.sheet.createTitle')}
284
+ </SheetTitle>
285
+ <SheetDescription>
286
+ {editingId
287
+ ? t('channels.sheet.editDescription')
288
+ : t('channels.sheet.createDescription')}
289
+ </SheetDescription>
290
+ </SheetHeader>
291
+
292
+ <Form {...form}>
293
+ <form
294
+ onSubmit={form.handleSubmit(handleSave)}
295
+ className="mt-6 space-y-4"
296
+ >
297
+ <FormField
298
+ control={form.control}
299
+ name="code"
300
+ render={({ field }) => (
301
+ <FormItem>
302
+ <FormLabel>{t('common.code')}</FormLabel>
303
+ <FormControl>
304
+ <Input
305
+ {...field}
306
+ placeholder={t('channels.form.codePlaceholder')}
307
+ />
308
+ </FormControl>
309
+ <FormMessage />
310
+ </FormItem>
311
+ )}
312
+ />
313
+
314
+ <FormField
315
+ control={form.control}
316
+ name="name"
317
+ render={({ field }) => (
318
+ <FormItem>
319
+ <FormLabel>{t('common.name')}</FormLabel>
320
+ <FormControl>
321
+ <Input
322
+ {...field}
323
+ placeholder={t('channels.form.namePlaceholder')}
324
+ />
325
+ </FormControl>
326
+ <FormMessage />
327
+ </FormItem>
328
+ )}
329
+ />
330
+
331
+ <FormField
332
+ control={form.control}
333
+ name="kind"
334
+ render={({ field }) => (
335
+ <FormItem>
336
+ <FormLabel>{t('channels.kind')}</FormLabel>
337
+ <FormControl>
338
+ <Select
339
+ value={field.value}
340
+ onValueChange={field.onChange}
341
+ >
342
+ <SelectTrigger>
343
+ <SelectValue />
344
+ </SelectTrigger>
345
+ <SelectContent>
346
+ <SelectItem value="portal">portal</SelectItem>
347
+ <SelectItem value="marketplace">
348
+ marketplace
349
+ </SelectItem>
350
+ <SelectItem value="social">social</SelectItem>
351
+ <SelectItem value="form">form</SelectItem>
352
+ <SelectItem value="email">email</SelectItem>
353
+ <SelectItem value="whatsapp">whatsapp</SelectItem>
354
+ <SelectItem value="api">api</SelectItem>
355
+ <SelectItem value="other">other</SelectItem>
356
+ </SelectContent>
357
+ </Select>
358
+ </FormControl>
359
+ <FormMessage />
360
+ </FormItem>
361
+ )}
362
+ />
363
+
364
+ <FormField
365
+ control={form.control}
366
+ name="status"
367
+ render={({ field }) => (
368
+ <FormItem>
369
+ <FormLabel>{t('common.status')}</FormLabel>
370
+ <FormControl>
371
+ <Select
372
+ value={field.value}
373
+ onValueChange={field.onChange}
374
+ >
375
+ <SelectTrigger>
376
+ <SelectValue />
377
+ </SelectTrigger>
378
+ <SelectContent>
379
+ <SelectItem value="active">
380
+ {t('common.enabled')}
381
+ </SelectItem>
382
+ <SelectItem value="inactive">
383
+ {t('common.disabled')}
384
+ </SelectItem>
385
+ </SelectContent>
386
+ </Select>
387
+ </FormControl>
388
+ <FormMessage />
389
+ </FormItem>
390
+ )}
391
+ />
392
+
393
+ <SheetFooter>
394
+ <Button type="submit" className="w-full">
395
+ {editingId
396
+ ? t('channels.form.submitUpdate')
397
+ : t('channels.form.submitCreate')}
398
+ </Button>
399
+ </SheetFooter>
400
+ </form>
401
+ </Form>
402
+ </SheetContent>
403
+ </Sheet>
404
+ </Page>
405
+ );
406
+ }
@@ -0,0 +1,178 @@
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 {
6
+ AlertTriangle,
7
+ Bot,
8
+ CheckCircle2,
9
+ Clock3,
10
+ Inbox,
11
+ Timer,
12
+ } from 'lucide-react';
13
+ import {
14
+ dashboardSummary,
15
+ latestActivity,
16
+ ticketPeople,
17
+ ticketsByChannel,
18
+ ticketsByCourse,
19
+ } from '../_lib/mock-data';
20
+ import { useTranslations } from 'next-intl';
21
+
22
+ export default function TicketDashboardPage() {
23
+ const t = useTranslations('ticket.TicketModule');
24
+ const maxChannel = Math.max(...ticketsByChannel.map((item) => item.value));
25
+
26
+ return (
27
+ <Page>
28
+ <PageHeader
29
+ title={t('dashboard.title')}
30
+ description={t('dashboard.description')}
31
+ breadcrumbs={[
32
+ { label: t('common.home'), href: '/' },
33
+ { label: t('common.tickets'), href: '/ticket' },
34
+ { label: t('dashboard.breadcrumb') },
35
+ ]}
36
+ />
37
+
38
+ <StatsCards
39
+ stats={[
40
+ {
41
+ title: t('dashboard.stats.totalOpen'),
42
+ value: dashboardSummary.totalOpen,
43
+ icon: <Inbox className="h-5 w-5" />,
44
+ iconBgColor: 'bg-blue-50',
45
+ iconColor: 'text-blue-600',
46
+ },
47
+ {
48
+ title: t('dashboard.stats.pending'),
49
+ value: dashboardSummary.pending,
50
+ icon: <Clock3 className="h-5 w-5" />,
51
+ iconBgColor: 'bg-amber-50',
52
+ iconColor: 'text-amber-600',
53
+ },
54
+ {
55
+ title: t('dashboard.stats.resolvedToday'),
56
+ value: dashboardSummary.resolvedToday,
57
+ icon: <CheckCircle2 className="h-5 w-5" />,
58
+ iconBgColor: 'bg-emerald-50',
59
+ iconColor: 'text-emerald-600',
60
+ },
61
+ {
62
+ title: t('dashboard.stats.aiPendingReview'),
63
+ value: dashboardSummary.aiPendingReview,
64
+ icon: <Bot className="h-5 w-5" />,
65
+ iconBgColor: 'bg-fuchsia-50',
66
+ iconColor: 'text-fuchsia-600',
67
+ },
68
+ {
69
+ title: t('dashboard.stats.waitingCustomer'),
70
+ value: dashboardSummary.waitingCustomer,
71
+ icon: <Timer className="h-5 w-5" />,
72
+ iconBgColor: 'bg-violet-50',
73
+ iconColor: 'text-violet-600',
74
+ },
75
+ {
76
+ title: t('dashboard.stats.urgent'),
77
+ value: dashboardSummary.urgent,
78
+ icon: <AlertTriangle className="h-5 w-5" />,
79
+ iconBgColor: 'bg-rose-50',
80
+ iconColor: 'text-rose-600',
81
+ },
82
+ ]}
83
+ className="sm:grid-cols-3 lg:grid-cols-6"
84
+ />
85
+
86
+ <div className="grid gap-4 lg:grid-cols-3">
87
+ <Card className="lg:col-span-1">
88
+ <CardHeader>
89
+ <CardTitle className="text-base">
90
+ {t('dashboard.cards.byChannel')}
91
+ </CardTitle>
92
+ </CardHeader>
93
+ <CardContent className="space-y-3">
94
+ {ticketsByChannel.map((item) => (
95
+ <div key={item.label}>
96
+ <div className="mb-1 flex items-center justify-between text-sm">
97
+ <span>{item.label}</span>
98
+ <span className="font-semibold">{item.value}</span>
99
+ </div>
100
+ <div className="h-2 rounded bg-slate-100">
101
+ <div
102
+ className={`h-2 rounded ${item.color}`}
103
+ style={{
104
+ width: `${Math.round((item.value / maxChannel) * 100)}%`,
105
+ }}
106
+ />
107
+ </div>
108
+ </div>
109
+ ))}
110
+ </CardContent>
111
+ </Card>
112
+
113
+ <Card className="lg:col-span-1">
114
+ <CardHeader>
115
+ <CardTitle className="text-base">
116
+ {t('dashboard.cards.byCourse')}
117
+ </CardTitle>
118
+ </CardHeader>
119
+ <CardContent className="space-y-2">
120
+ {ticketsByCourse.map((item) => (
121
+ <div
122
+ key={item.label}
123
+ className="flex items-center justify-between rounded border p-2 text-sm"
124
+ >
125
+ <span>{item.label}</span>
126
+ <span className="font-semibold">{item.value}</span>
127
+ </div>
128
+ ))}
129
+ </CardContent>
130
+ </Card>
131
+
132
+ <Card className="lg:col-span-1">
133
+ <CardHeader>
134
+ <CardTitle className="text-base">
135
+ {t('dashboard.cards.byOwner')}
136
+ </CardTitle>
137
+ </CardHeader>
138
+ <CardContent className="space-y-2">
139
+ {ticketPeople
140
+ .filter((person) => person.roleLabel !== 'Requester')
141
+ .map((person, index) => (
142
+ <div
143
+ key={person.id}
144
+ className="flex items-center justify-between rounded border p-2 text-sm"
145
+ >
146
+ <span>{person.name}</span>
147
+ <span className="font-semibold">
148
+ {index === 0 ? 44 : index === 1 ? 39 : 21}
149
+ </span>
150
+ </div>
151
+ ))}
152
+ </CardContent>
153
+ </Card>
154
+ </div>
155
+
156
+ <Card>
157
+ <CardHeader>
158
+ <CardTitle className="text-base">
159
+ {t('dashboard.cards.latestActivity')}
160
+ </CardTitle>
161
+ </CardHeader>
162
+ <CardContent className="space-y-2">
163
+ {latestActivity.map((item) => (
164
+ <div
165
+ key={item.time + item.label}
166
+ className="flex gap-3 rounded border p-3 text-sm"
167
+ >
168
+ <span className="w-14 font-semibold text-blue-600">
169
+ {item.time}
170
+ </span>
171
+ <span>{item.label}</span>
172
+ </div>
173
+ ))}
174
+ </CardContent>
175
+ </Card>
176
+ </Page>
177
+ );
178
+ }