@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,360 @@
1
+ 'use client';
2
+
3
+ import { Page, PageHeader } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
7
+ import {
8
+ Form,
9
+ FormControl,
10
+ FormField,
11
+ FormItem,
12
+ FormLabel,
13
+ FormMessage,
14
+ } from '@/components/ui/form';
15
+ import { Input } from '@/components/ui/input';
16
+ import { Textarea } from '@/components/ui/textarea';
17
+ import { zodResolver } from '@hookform/resolvers/zod';
18
+ import { useTranslations } from 'next-intl';
19
+ import { useParams } from 'next/navigation';
20
+ import { useMemo } from 'react';
21
+ import { useForm } from 'react-hook-form';
22
+ import { toast } from 'sonner';
23
+ import { z } from 'zod';
24
+ import {
25
+ TicketAiReviewModeBadge,
26
+ TicketPriorityBadge,
27
+ TicketStatusBadge,
28
+ } from '../_components/ticket-badges';
29
+ import { TicketContextCard } from '../_components/ticket-context-card';
30
+ import { TicketTimeline } from '../_components/ticket-timeline';
31
+ import {
32
+ getChannelAccountById,
33
+ getChannelById,
34
+ getPersonById,
35
+ getTicketContextByTicketId,
36
+ getTicketMessages,
37
+ ticketDrafts,
38
+ ticketItems,
39
+ } from '../_lib/mock-data';
40
+
41
+ export default function TicketDetailPage() {
42
+ const t = useTranslations('ticket.TicketModule');
43
+ const params = useParams<{ id: string }>();
44
+ const id = Number(params.id);
45
+ const ticket = ticketItems.find((item) => item.id === id) ?? ticketItems[0];
46
+
47
+ const replySchema = useMemo(
48
+ () =>
49
+ z.object({
50
+ subject: z
51
+ .string()
52
+ .trim()
53
+ .min(1, t('detail.validation.replySubjectRequired')),
54
+ body: z
55
+ .string()
56
+ .trim()
57
+ .min(1, t('detail.validation.replyBodyRequired')),
58
+ }),
59
+ [t]
60
+ );
61
+
62
+ const internalNoteSchema = useMemo(
63
+ () =>
64
+ z.object({
65
+ body: z
66
+ .string()
67
+ .trim()
68
+ .min(1, t('detail.validation.internalNoteBodyRequired')),
69
+ }),
70
+ [t]
71
+ );
72
+
73
+ type ReplyForm = z.infer<typeof replySchema>;
74
+ type InternalNoteForm = z.infer<typeof internalNoteSchema>;
75
+
76
+ const replyForm = useForm<ReplyForm>({
77
+ resolver: zodResolver(replySchema),
78
+ defaultValues: {
79
+ subject: ticket ? `Re: ${ticket.subject}` : '',
80
+ body: '',
81
+ },
82
+ });
83
+
84
+ const internalNoteForm = useForm<InternalNoteForm>({
85
+ resolver: zodResolver(internalNoteSchema),
86
+ defaultValues: {
87
+ body: '',
88
+ },
89
+ });
90
+
91
+ if (!ticket) {
92
+ return null;
93
+ }
94
+
95
+ const requester = getPersonById(ticket.requesterId);
96
+ const owner = getPersonById(ticket.ownerId);
97
+ const channel = getChannelById(ticket.channelId);
98
+ const account = getChannelAccountById(ticket.channelAccountId);
99
+ const context = getTicketContextByTicketId(ticket.id);
100
+ const messages = getTicketMessages(ticket.id);
101
+ const draft = ticketDrafts.find((item) => item.ticketId === ticket.id);
102
+
103
+ const handleReplySubmit = async (values: ReplyForm) => {
104
+ console.log('reply payload', values);
105
+ toast.success(t('detail.messages.replySendSuccess'));
106
+ replyForm.reset({
107
+ subject: `Re: ${ticket.subject}`,
108
+ body: '',
109
+ });
110
+ };
111
+
112
+ const handleInternalNoteSubmit = async (values: InternalNoteForm) => {
113
+ console.log('internal note payload', values);
114
+ toast.success(t('detail.messages.internalNoteSaveSuccess'));
115
+ internalNoteForm.reset({ body: '' });
116
+ };
117
+
118
+ return (
119
+ <Page>
120
+ <PageHeader
121
+ title={`${ticket.code} - ${ticket.subject}`}
122
+ description={t('detail.description')}
123
+ breadcrumbs={[
124
+ { label: t('common.home'), href: '/' },
125
+ { label: t('common.tickets'), href: '/ticket' },
126
+ { label: t('inbox.breadcrumb'), href: '/ticket/inbox' },
127
+ { label: ticket.code },
128
+ ]}
129
+ actions={[
130
+ { label: t('detail.takeOwnership'), onClick: () => {} },
131
+ { label: t('detail.assign'), onClick: () => {}, variant: 'outline' },
132
+ {
133
+ label: t('detail.changeStatus'),
134
+ onClick: () => {},
135
+ variant: 'outline',
136
+ },
137
+ ]}
138
+ />
139
+
140
+ <div className="flex flex-wrap items-center gap-2">
141
+ <TicketStatusBadge value={ticket.status} />
142
+ <TicketPriorityBadge value={ticket.priority} />
143
+ <TicketAiReviewModeBadge value={ticket.aiReviewMode} />
144
+ <Badge variant="outline">
145
+ {t('detail.ownerBadge', { name: owner?.name || '-' })}
146
+ </Badge>
147
+ </div>
148
+
149
+ <div className="grid gap-4 lg:grid-cols-3">
150
+ <Card>
151
+ <CardHeader>
152
+ <CardTitle className="text-base">
153
+ {t('detail.requesterCard.title')}
154
+ </CardTitle>
155
+ </CardHeader>
156
+ <CardContent className="space-y-1 text-sm">
157
+ <div className="font-semibold">{requester?.name}</div>
158
+ <div className="text-muted-foreground">{requester?.email}</div>
159
+ <div className="text-muted-foreground">
160
+ {t('detail.requesterCard.role', {
161
+ role: requester?.roleLabel || '-',
162
+ })}
163
+ </div>
164
+ </CardContent>
165
+ </Card>
166
+
167
+ <TicketContextCard context={context} />
168
+
169
+ <Card>
170
+ <CardHeader>
171
+ <CardTitle className="text-base">
172
+ {t('detail.sourceCard.title')}
173
+ </CardTitle>
174
+ </CardHeader>
175
+ <CardContent className="space-y-2 text-sm">
176
+ <div>
177
+ <span className="text-muted-foreground">
178
+ {t('detail.sourceCard.channel')}{' '}
179
+ </span>
180
+ <span className="font-medium">{channel?.name}</span>
181
+ </div>
182
+ <div>
183
+ <span className="text-muted-foreground">
184
+ {t('detail.sourceCard.account')}{' '}
185
+ </span>
186
+ <span className="font-medium">{account?.code}</span>
187
+ </div>
188
+ <div className="flex flex-wrap gap-1">
189
+ {ticket.tags.map((tag) => (
190
+ <Badge key={tag} variant="outline">
191
+ {t('detail.sourceCard.tag', { value: tag })}
192
+ </Badge>
193
+ ))}
194
+ {ticket.categories.map((category) => (
195
+ <Badge key={category} variant="outline">
196
+ {t('detail.sourceCard.category', { value: category })}
197
+ </Badge>
198
+ ))}
199
+ </div>
200
+ </CardContent>
201
+ </Card>
202
+ </div>
203
+
204
+ <div className="grid gap-4 lg:grid-cols-3">
205
+ <div className="space-y-4 lg:col-span-2">
206
+ <TicketTimeline messages={messages} />
207
+
208
+ <Card>
209
+ <CardHeader>
210
+ <CardTitle className="text-base">
211
+ {t('detail.reply.title')}
212
+ </CardTitle>
213
+ </CardHeader>
214
+ <CardContent>
215
+ <Form {...replyForm}>
216
+ <form
217
+ onSubmit={replyForm.handleSubmit(handleReplySubmit)}
218
+ className="space-y-3"
219
+ >
220
+ <FormField
221
+ control={replyForm.control}
222
+ name="subject"
223
+ render={({ field }) => (
224
+ <FormItem>
225
+ <FormLabel className="sr-only">subject</FormLabel>
226
+ <FormControl>
227
+ <Input
228
+ {...field}
229
+ placeholder={t('detail.reply.subjectPlaceholder')}
230
+ />
231
+ </FormControl>
232
+ <FormMessage />
233
+ </FormItem>
234
+ )}
235
+ />
236
+
237
+ <FormField
238
+ control={replyForm.control}
239
+ name="body"
240
+ render={({ field }) => (
241
+ <FormItem>
242
+ <FormLabel className="sr-only">body</FormLabel>
243
+ <FormControl>
244
+ <Textarea
245
+ {...field}
246
+ className="min-h-[140px]"
247
+ placeholder={t('detail.reply.bodyPlaceholder')}
248
+ />
249
+ </FormControl>
250
+ <FormMessage />
251
+ </FormItem>
252
+ )}
253
+ />
254
+
255
+ <Button type="submit">{t('detail.reply.send')}</Button>
256
+ </form>
257
+ </Form>
258
+ </CardContent>
259
+ </Card>
260
+
261
+ <Card>
262
+ <CardHeader>
263
+ <CardTitle className="text-base">
264
+ {t('detail.internalNote.title')}
265
+ </CardTitle>
266
+ </CardHeader>
267
+ <CardContent>
268
+ <Form {...internalNoteForm}>
269
+ <form
270
+ onSubmit={internalNoteForm.handleSubmit(
271
+ handleInternalNoteSubmit
272
+ )}
273
+ className="space-y-3"
274
+ >
275
+ <FormField
276
+ control={internalNoteForm.control}
277
+ name="body"
278
+ render={({ field }) => (
279
+ <FormItem>
280
+ <FormLabel className="sr-only">internal note</FormLabel>
281
+ <FormControl>
282
+ <Textarea
283
+ {...field}
284
+ className="min-h-[120px]"
285
+ placeholder={t(
286
+ 'detail.internalNote.bodyPlaceholder'
287
+ )}
288
+ />
289
+ </FormControl>
290
+ <FormMessage />
291
+ </FormItem>
292
+ )}
293
+ />
294
+
295
+ <Button type="submit" variant="outline">
296
+ {t('detail.internalNote.save')}
297
+ </Button>
298
+ </form>
299
+ </Form>
300
+ </CardContent>
301
+ </Card>
302
+ </div>
303
+
304
+ <Card>
305
+ <CardHeader>
306
+ <CardTitle className="text-base">
307
+ {t('detail.aiPanel.title')}
308
+ </CardTitle>
309
+ </CardHeader>
310
+ <CardContent className="space-y-3 text-sm">
311
+ <div className="rounded border border-fuchsia-200 bg-fuchsia-50 p-3">
312
+ <div className="font-semibold">
313
+ {t('detail.aiPanel.draftTitle')}
314
+ </div>
315
+ <div className="mt-1 text-muted-foreground">
316
+ {draft?.suggestion}
317
+ </div>
318
+ </div>
319
+ <div className="space-y-2">
320
+ <Button className="w-full">{t('detail.aiPanel.approve')}</Button>
321
+ <Button className="w-full" variant="outline">
322
+ {t('detail.aiPanel.edit')}
323
+ </Button>
324
+ <Button className="w-full" variant="destructive">
325
+ {t('detail.aiPanel.reject')}
326
+ </Button>
327
+ </div>
328
+ <div className="rounded border p-3">
329
+ <div className="font-semibold">
330
+ {t('detail.aiPanel.participants')}
331
+ </div>
332
+ <ul className="mt-2 space-y-1 text-muted-foreground">
333
+ <li>
334
+ {t('detail.aiPanel.participantRequester', {
335
+ name: requester?.name || '-',
336
+ })}
337
+ </li>
338
+ <li>
339
+ {t('detail.aiPanel.participantAgent', {
340
+ name: owner?.name || '-',
341
+ })}
342
+ </li>
343
+ <li>{t('detail.aiPanel.participantCollaborator')}</li>
344
+ </ul>
345
+ </div>
346
+ <div className="rounded border p-3">
347
+ <div className="font-semibold">
348
+ {t('detail.aiPanel.attachments')}
349
+ </div>
350
+ <ul className="mt-2 space-y-1 text-muted-foreground">
351
+ <li>erro-jwt-refresh.png</li>
352
+ <li>trecho-token-service.ts</li>
353
+ </ul>
354
+ </div>
355
+ </CardContent>
356
+ </Card>
357
+ </div>
358
+ </Page>
359
+ );
360
+ }
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from '@/components/ui/dialog';
12
+ import { Textarea } from '@/components/ui/textarea';
13
+ import { useEffect, useState } from 'react';
14
+ import { useTranslations } from 'next-intl';
15
+ import { TicketDraft } from '../_lib/types';
16
+
17
+ export function AIDraftEditorDialog({
18
+ open,
19
+ onOpenChange,
20
+ draft,
21
+ }: {
22
+ open: boolean;
23
+ onOpenChange: (value: boolean) => void;
24
+ draft?: TicketDraft;
25
+ }) {
26
+ const t = useTranslations('ticket.TicketModule');
27
+ const [content, setContent] = useState('');
28
+
29
+ useEffect(() => {
30
+ setContent(draft?.suggestion || '');
31
+ }, [draft]);
32
+
33
+ return (
34
+ <Dialog open={open} onOpenChange={onOpenChange}>
35
+ <DialogContent className="max-w-3xl">
36
+ <DialogHeader>
37
+ <DialogTitle>{t('draftDialog.title')}</DialogTitle>
38
+ <DialogDescription>
39
+ {t('draftDialog.description')}
40
+ </DialogDescription>
41
+ </DialogHeader>
42
+ <div className="space-y-3">
43
+ <div className="rounded-md border bg-slate-50 p-3 text-sm">
44
+ <div className="font-semibold">{t('draftDialog.originalTitle')}</div>
45
+ <p className="mt-1 text-muted-foreground">
46
+ {draft?.originalSnippet}
47
+ </p>
48
+ </div>
49
+ <Textarea
50
+ value={content}
51
+ onChange={(event) => setContent(event.target.value)}
52
+ className="min-h-[220px]"
53
+ placeholder={t('draftDialog.placeholder')}
54
+ />
55
+ </div>
56
+ <DialogFooter>
57
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
58
+ {t('draftDialog.cancel')}
59
+ </Button>
60
+ <Button onClick={() => onOpenChange(false)}>
61
+ {t('draftDialog.save')}
62
+ </Button>
63
+ </DialogFooter>
64
+ </DialogContent>
65
+ </Dialog>
66
+ );
67
+ }
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import {
5
+ TicketAiReviewMode,
6
+ TicketPriority,
7
+ TicketStatus,
8
+ } from '../_lib/types';
9
+ import { useTranslations } from 'next-intl';
10
+
11
+ const statusVariant: Record<TicketStatus, string> = {
12
+ open: 'bg-blue-50 text-blue-700 border-blue-200',
13
+ pending: 'bg-amber-50 text-amber-700 border-amber-200',
14
+ waiting_customer: 'bg-violet-50 text-violet-700 border-violet-200',
15
+ resolved: 'bg-emerald-50 text-emerald-700 border-emerald-200',
16
+ closed: 'bg-slate-100 text-slate-700 border-slate-300',
17
+ spam: 'bg-rose-50 text-rose-700 border-rose-200',
18
+ };
19
+
20
+ const priorityVariant: Record<TicketPriority, string> = {
21
+ low: 'bg-slate-100 text-slate-700 border-slate-300',
22
+ normal: 'bg-sky-50 text-sky-700 border-sky-200',
23
+ high: 'bg-orange-50 text-orange-700 border-orange-200',
24
+ urgent: 'bg-red-50 text-red-700 border-red-200',
25
+ };
26
+
27
+ const aiReviewVariant: Record<TicketAiReviewMode, string> = {
28
+ disabled: 'bg-slate-100 text-slate-700 border-slate-300',
29
+ optional: 'bg-indigo-50 text-indigo-700 border-indigo-200',
30
+ required: 'bg-fuchsia-50 text-fuchsia-700 border-fuchsia-200',
31
+ };
32
+
33
+ export function TicketStatusBadge({ value }: { value: TicketStatus }) {
34
+ const t = useTranslations('ticket.TicketModule');
35
+
36
+ return (
37
+ <Badge variant="outline" className={statusVariant[value]}>
38
+ {t(`badges.status.${value}`)}
39
+ </Badge>
40
+ );
41
+ }
42
+
43
+ export function TicketPriorityBadge({ value }: { value: TicketPriority }) {
44
+ const t = useTranslations('ticket.TicketModule');
45
+
46
+ return (
47
+ <Badge variant="outline" className={priorityVariant[value]}>
48
+ {t(`badges.priority.${value}`)}
49
+ </Badge>
50
+ );
51
+ }
52
+
53
+ export function TicketAiReviewModeBadge({
54
+ value,
55
+ }: {
56
+ value: TicketAiReviewMode;
57
+ }) {
58
+ const t = useTranslations('ticket.TicketModule');
59
+
60
+ return (
61
+ <Badge variant="outline" className={aiReviewVariant[value]}>
62
+ {t(`badges.aiReviewMode.${value}`)}
63
+ </Badge>
64
+ );
65
+ }
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
4
+ import { useTranslations } from 'next-intl';
5
+ import { TicketContext } from '../_lib/types';
6
+
7
+ export function TicketContextCard({ context }: { context?: TicketContext }) {
8
+ const t = useTranslations('ticket.TicketModule');
9
+
10
+ if (!context) {
11
+ return (
12
+ <Card>
13
+ <CardHeader>
14
+ <CardTitle className="text-base">{t('contextCard.title')}</CardTitle>
15
+ </CardHeader>
16
+ <CardContent className="text-sm text-muted-foreground">
17
+ {t('contextCard.empty')}
18
+ </CardContent>
19
+ </Card>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <Card>
25
+ <CardHeader>
26
+ <CardTitle className="text-base">{t('contextCard.title')}</CardTitle>
27
+ </CardHeader>
28
+ <CardContent className="space-y-2 text-sm">
29
+ <div>
30
+ <span className="text-muted-foreground">{t('contextCard.type')} </span>
31
+ <span className="font-medium">{context.contextType}</span>
32
+ </div>
33
+ <div>
34
+ <span className="text-muted-foreground">{t('contextCard.source')} </span>
35
+ <span className="font-medium">{context.sourceLabel}</span>
36
+ </div>
37
+ {context.courseName && (
38
+ <div>
39
+ <span className="text-muted-foreground">{t('contextCard.course')} </span>
40
+ <span className="font-medium">{context.courseName}</span>
41
+ </div>
42
+ )}
43
+ {context.sectionName && (
44
+ <div>
45
+ <span className="text-muted-foreground">{t('contextCard.section')} </span>
46
+ <span className="font-medium">{context.sectionName}</span>
47
+ </div>
48
+ )}
49
+ {context.lessonName && (
50
+ <div>
51
+ <span className="text-muted-foreground">{t('contextCard.lesson')} </span>
52
+ <span className="font-medium">{context.lessonName}</span>
53
+ </div>
54
+ )}
55
+ {context.videoName && (
56
+ <div>
57
+ <span className="text-muted-foreground">{t('contextCard.video')} </span>
58
+ <span className="font-medium">{context.videoName}</span>
59
+ </div>
60
+ )}
61
+ {context.pageName && (
62
+ <div>
63
+ <span className="text-muted-foreground">{t('contextCard.page')} </span>
64
+ <span className="font-medium">{context.pageName}</span>
65
+ </div>
66
+ )}
67
+ {context.externalUrl && (
68
+ <a
69
+ className="text-sm text-blue-600 hover:underline"
70
+ href={context.externalUrl}
71
+ target="_blank"
72
+ rel="noreferrer"
73
+ >
74
+ {t('contextCard.openSource')}
75
+ </a>
76
+ )}
77
+ </CardContent>
78
+ </Card>
79
+ );
80
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { useTranslations } from 'next-intl';
6
+ import { TicketMessage } from '../_lib/types';
7
+
8
+ function tone(message: TicketMessage) {
9
+ if (message.audience === 'internal') return 'border-amber-200 bg-amber-50';
10
+ if (message.direction === 'inbound') return 'border-sky-200 bg-sky-50';
11
+ if (message.direction === 'outbound')
12
+ return 'border-emerald-200 bg-emerald-50';
13
+ return 'border-slate-200 bg-slate-50';
14
+ }
15
+
16
+ export function TicketTimeline({ messages }: { messages: TicketMessage[] }) {
17
+ const t = useTranslations('ticket.TicketModule');
18
+
19
+ return (
20
+ <Card>
21
+ <CardHeader>
22
+ <CardTitle className="text-base">{t('timeline.title')}</CardTitle>
23
+ </CardHeader>
24
+ <CardContent className="space-y-3">
25
+ {messages.map((message) => (
26
+ <div
27
+ key={message.id}
28
+ className={`rounded-md border p-3 ${tone(message)}`}
29
+ >
30
+ <div className="mb-2 flex items-center justify-between gap-2">
31
+ <div className="text-sm font-semibold">{message.authorName}</div>
32
+ <div className="flex items-center gap-2">
33
+ <Badge variant="outline">
34
+ {t(`timeline.directions.${message.direction}`)}
35
+ </Badge>
36
+ {message.audience === 'internal' && (
37
+ <Badge variant="secondary">{t('timeline.internalNote')}</Badge>
38
+ )}
39
+ <span className="text-xs text-muted-foreground">
40
+ {message.createdAt}
41
+ </span>
42
+ </div>
43
+ </div>
44
+ <p className="text-sm leading-relaxed">{message.body}</p>
45
+ {message.attachments && message.attachments.length > 0 && (
46
+ <div className="mt-2 flex flex-wrap gap-2">
47
+ {message.attachments.map((attachment) => (
48
+ <Badge key={attachment.name} variant="outline">
49
+ {attachment.kind}: {attachment.name}
50
+ </Badge>
51
+ ))}
52
+ </div>
53
+ )}
54
+ </div>
55
+ ))}
56
+ </CardContent>
57
+ </Card>
58
+ );
59
+ }