@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,205 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Page,
5
+ PageHeader,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
9
+ import { Button } from '@/components/ui/button';
10
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
11
+ import {
12
+ Table,
13
+ TableBody,
14
+ TableCell,
15
+ TableHead,
16
+ TableHeader,
17
+ TableRow,
18
+ } from '@/components/ui/table';
19
+ import { useMemo, useState } from 'react';
20
+ import { useTranslations } from 'next-intl';
21
+ import { AIDraftEditorDialog } from '../_components/ai-draft-editor-dialog';
22
+ import { ticketDrafts } from '../_lib/mock-data';
23
+
24
+ export default function TicketAiReviewPage() {
25
+ const t = useTranslations('ticket.TicketModule');
26
+ const [search, setSearch] = useState('');
27
+ const [status, setStatus] = useState('pending_review');
28
+ const [reviewStatus, setReviewStatus] = useState<
29
+ Record<number, 'pending_review' | 'approved' | 'rejected'>
30
+ >({});
31
+ const [openEditor, setOpenEditor] = useState(false);
32
+ const [selectedDraftId, setSelectedDraftId] = useState<number | null>(null);
33
+ const [page, setPage] = useState(1);
34
+ const [pageSize, setPageSize] = useState(10);
35
+
36
+ const filtered = useMemo(
37
+ () =>
38
+ ticketDrafts.filter((draft) => {
39
+ const effectiveStatus = reviewStatus[draft.id] ?? draft.status;
40
+ const matchesSearch =
41
+ `${draft.ticketCode} ${draft.context} ${draft.channel}`
42
+ .toLowerCase()
43
+ .includes(search.toLowerCase());
44
+
45
+ return (
46
+ matchesSearch && (status === 'all' || effectiveStatus === status)
47
+ );
48
+ }),
49
+ [search, status, reviewStatus]
50
+ );
51
+
52
+ const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
53
+ const safePage = Math.min(page, totalPages);
54
+ const paged = filtered.slice((safePage - 1) * pageSize, safePage * pageSize);
55
+
56
+ const getDraftStatus = (
57
+ id: number,
58
+ fallback: (typeof ticketDrafts)[number]['status']
59
+ ) => reviewStatus[id] ?? fallback;
60
+
61
+ const selectedDraft = ticketDrafts.find(
62
+ (item) => item.id === selectedDraftId
63
+ );
64
+
65
+ return (
66
+ <Page>
67
+ <PageHeader
68
+ title={t('aiReview.title')}
69
+ description={t('aiReview.description')}
70
+ breadcrumbs={[
71
+ { label: t('common.home'), href: '/' },
72
+ { label: t('common.tickets'), href: '/ticket' },
73
+ { label: t('aiReview.breadcrumb') },
74
+ ]}
75
+ />
76
+
77
+ <SearchBar
78
+ searchQuery={search}
79
+ onSearchChange={(value) => {
80
+ setSearch(value);
81
+ setPage(1);
82
+ }}
83
+ onSearch={() => setPage(1)}
84
+ placeholder={t('aiReview.searchPlaceholder')}
85
+ controls={[
86
+ {
87
+ id: 'status',
88
+ type: 'select',
89
+ value: status,
90
+ onChange: (value) => {
91
+ setStatus(value);
92
+ setPage(1);
93
+ },
94
+ options: [
95
+ { value: 'all', label: t('aiReview.allStatus') },
96
+ { value: 'pending_review', label: t('aiReview.pendingReview') },
97
+ { value: 'approved', label: t('aiReview.approved') },
98
+ { value: 'rejected', label: t('aiReview.rejected') },
99
+ ],
100
+ },
101
+ ]}
102
+ />
103
+
104
+ <Card>
105
+ <CardHeader>
106
+ <CardTitle className="text-base">{t('aiReview.tableTitle')}</CardTitle>
107
+ </CardHeader>
108
+ <CardContent>
109
+ <Table>
110
+ <TableHeader>
111
+ <TableRow>
112
+ <TableHead>{t('aiReview.columns.ticket')}</TableHead>
113
+ <TableHead>{t('aiReview.columns.channel')}</TableHead>
114
+ <TableHead>{t('aiReview.columns.context')}</TableHead>
115
+ <TableHead>{t('aiReview.columns.model')}</TableHead>
116
+ <TableHead>{t('aiReview.columns.status')}</TableHead>
117
+ <TableHead>{t('aiReview.columns.original')}</TableHead>
118
+ <TableHead>{t('aiReview.columns.suggestion')}</TableHead>
119
+ <TableHead>{t('aiReview.columns.actions')}</TableHead>
120
+ </TableRow>
121
+ </TableHeader>
122
+ <TableBody>
123
+ {paged.map((draft) => {
124
+ const currentStatus = getDraftStatus(draft.id, draft.status);
125
+
126
+ return (
127
+ <TableRow key={draft.id}>
128
+ <TableCell>{draft.ticketCode}</TableCell>
129
+ <TableCell>{draft.channel}</TableCell>
130
+ <TableCell>{draft.context}</TableCell>
131
+ <TableCell>{draft.modelName}</TableCell>
132
+ <TableCell>
133
+ {currentStatus === 'pending_review'
134
+ ? t('aiReview.pendingReview')
135
+ : currentStatus === 'approved'
136
+ ? t('aiReview.approved')
137
+ : t('aiReview.rejected')}
138
+ </TableCell>
139
+ <TableCell className="max-w-[220px] text-xs text-muted-foreground">
140
+ {draft.originalSnippet}
141
+ </TableCell>
142
+ <TableCell className="max-w-[260px] text-xs text-muted-foreground">
143
+ {draft.suggestion}
144
+ </TableCell>
145
+ <TableCell>
146
+ <div className="flex gap-2">
147
+ <Button
148
+ size="sm"
149
+ onClick={() =>
150
+ setReviewStatus((current) => ({
151
+ ...current,
152
+ [draft.id]: 'approved',
153
+ }))
154
+ }
155
+ >
156
+ {t('aiReview.actions.approve')}
157
+ </Button>
158
+ <Button
159
+ size="sm"
160
+ variant="destructive"
161
+ onClick={() =>
162
+ setReviewStatus((current) => ({
163
+ ...current,
164
+ [draft.id]: 'rejected',
165
+ }))
166
+ }
167
+ >
168
+ {t('aiReview.actions.reject')}
169
+ </Button>
170
+ <Button
171
+ size="sm"
172
+ variant="outline"
173
+ onClick={() => {
174
+ setSelectedDraftId(draft.id);
175
+ setOpenEditor(true);
176
+ }}
177
+ >
178
+ {t('aiReview.actions.edit')}
179
+ </Button>
180
+ </div>
181
+ </TableCell>
182
+ </TableRow>
183
+ );
184
+ })}
185
+ </TableBody>
186
+ </Table>
187
+
188
+ <PaginationFooter
189
+ currentPage={safePage}
190
+ pageSize={pageSize}
191
+ totalItems={filtered.length}
192
+ onPageChange={setPage}
193
+ onPageSizeChange={setPageSize}
194
+ />
195
+ </CardContent>
196
+ </Card>
197
+
198
+ <AIDraftEditorDialog
199
+ open={openEditor}
200
+ onOpenChange={setOpenEditor}
201
+ draft={selectedDraft}
202
+ />
203
+ </Page>
204
+ );
205
+ }
@@ -0,0 +1,476 @@
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 { Switch } from '@/components/ui/switch';
38
+ import {
39
+ Table,
40
+ TableBody,
41
+ TableCell,
42
+ TableHead,
43
+ TableHeader,
44
+ TableRow,
45
+ } from '@/components/ui/table';
46
+ import { zodResolver } from '@hookform/resolvers/zod';
47
+ import { Layers } from 'lucide-react';
48
+ import { useTranslations } from 'next-intl';
49
+ import { useMemo, useState } from 'react';
50
+ import { useForm } from 'react-hook-form';
51
+ import { toast } from 'sonner';
52
+ import { z } from 'zod';
53
+ import {
54
+ getChannelById,
55
+ ticketChannelAccounts,
56
+ ticketChannels,
57
+ } from '../_lib/mock-data';
58
+
59
+ const PAGE_SIZE_OPTIONS = [10, 20, 30, 50];
60
+
61
+ export default function TicketChannelAccountsPage() {
62
+ const t = useTranslations('ticket.TicketModule');
63
+ const [accounts, setAccounts] = useState(ticketChannelAccounts);
64
+ const [search, setSearch] = useState('');
65
+ const [page, setPage] = useState(1);
66
+ const [pageSize, setPageSize] = useState(PAGE_SIZE_OPTIONS[0]);
67
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
68
+ const [editingId, setEditingId] = useState<number | null>(null);
69
+
70
+ const accountSchema = useMemo(
71
+ () =>
72
+ z.object({
73
+ code: z
74
+ .string()
75
+ .trim()
76
+ .min(1, t('channelAccounts.validation.codeRequired')),
77
+ channelId: z
78
+ .string()
79
+ .trim()
80
+ .min(1, t('channelAccounts.validation.channelRequired')),
81
+ name: z
82
+ .string()
83
+ .trim()
84
+ .min(1, t('channelAccounts.validation.nameRequired')),
85
+ externalIdentifier: z
86
+ .string()
87
+ .trim()
88
+ .min(1, t('channelAccounts.validation.externalIdentifierRequired')),
89
+ status: z.enum(['active', 'inactive']),
90
+ aiEnabled: z.boolean(),
91
+ }),
92
+ [t]
93
+ );
94
+
95
+ type AccountForm = z.infer<typeof accountSchema>;
96
+
97
+ const form = useForm<AccountForm>({
98
+ resolver: zodResolver(accountSchema),
99
+ defaultValues: {
100
+ code: '',
101
+ channelId: String(ticketChannels[0]?.id ?? 1),
102
+ name: '',
103
+ externalIdentifier: '',
104
+ status: 'active',
105
+ aiEnabled: true,
106
+ },
107
+ });
108
+
109
+ const filteredAccounts = useMemo(() => {
110
+ const query = search.toLowerCase();
111
+ return accounts.filter((item) => {
112
+ const channelName = getChannelById(item.channelId)?.name ?? '';
113
+ return `${item.code} ${item.name} ${item.externalIdentifier} ${channelName}`
114
+ .toLowerCase()
115
+ .includes(query);
116
+ });
117
+ }, [accounts, search]);
118
+
119
+ const totalPages = Math.max(1, Math.ceil(filteredAccounts.length / pageSize));
120
+ const safePage = Math.min(page, totalPages);
121
+ const paginatedAccounts = filteredAccounts.slice(
122
+ (safePage - 1) * pageSize,
123
+ safePage * pageSize
124
+ );
125
+
126
+ const handleOpenCreate = () => {
127
+ setEditingId(null);
128
+ form.reset({
129
+ code: '',
130
+ channelId: String(ticketChannels[0]?.id ?? 1),
131
+ name: '',
132
+ externalIdentifier: '',
133
+ status: 'active',
134
+ aiEnabled: true,
135
+ });
136
+ setIsSheetOpen(true);
137
+ };
138
+
139
+ const handleOpenEdit = (id: number) => {
140
+ const current = accounts.find((item) => item.id === id);
141
+ if (!current) return;
142
+
143
+ setEditingId(id);
144
+ form.reset({
145
+ code: current.code,
146
+ channelId: String(current.channelId),
147
+ name: current.name,
148
+ externalIdentifier: current.externalIdentifier,
149
+ status: current.status,
150
+ aiEnabled: current.aiEnabled,
151
+ });
152
+ setIsSheetOpen(true);
153
+ };
154
+
155
+ const handleSave = async (values: AccountForm) => {
156
+ const payload = {
157
+ code: values.code,
158
+ channelId: Number(values.channelId),
159
+ name: values.name,
160
+ externalIdentifier: values.externalIdentifier,
161
+ status: values.status,
162
+ aiEnabled: values.aiEnabled,
163
+ aiModel: values.aiEnabled ? 'gpt-5.3-mini' : 'disabled',
164
+ };
165
+
166
+ if (editingId) {
167
+ setAccounts((current) =>
168
+ current.map((item) =>
169
+ item.id === editingId ? { ...item, ...payload } : item
170
+ )
171
+ );
172
+ toast.success(t('channelAccounts.messages.updated'));
173
+ } else {
174
+ setAccounts((current) => [
175
+ ...current,
176
+ {
177
+ id: Math.max(0, ...current.map((item) => item.id)) + 1,
178
+ ...payload,
179
+ },
180
+ ]);
181
+ toast.success(t('channelAccounts.messages.created'));
182
+ }
183
+
184
+ setIsSheetOpen(false);
185
+ };
186
+
187
+ const handleTestConnection = () => {
188
+ toast.success(t('channelAccounts.messages.connectionTested'));
189
+ };
190
+
191
+ return (
192
+ <Page>
193
+ <PageHeader
194
+ title={t('channelAccounts.title')}
195
+ description={t('channelAccounts.description')}
196
+ breadcrumbs={[
197
+ { label: t('common.home'), href: '/' },
198
+ { label: t('common.tickets'), href: '/ticket' },
199
+ { label: t('channelAccounts.breadcrumb') },
200
+ ]}
201
+ actions={[
202
+ { label: t('channelAccounts.new'), onClick: handleOpenCreate },
203
+ ]}
204
+ />
205
+
206
+ <SearchBar
207
+ searchQuery={search}
208
+ onSearchChange={(value) => {
209
+ setSearch(value);
210
+ setPage(1);
211
+ }}
212
+ onSearch={() => setPage(1)}
213
+ placeholder={t('channelAccounts.searchPlaceholder')}
214
+ />
215
+
216
+ <Card>
217
+ <CardHeader>
218
+ <CardTitle className="text-base">
219
+ {t('channelAccounts.tableTitle')}
220
+ </CardTitle>
221
+ </CardHeader>
222
+ <CardContent className="space-y-4">
223
+ {filteredAccounts.length === 0 ? (
224
+ <EmptyState
225
+ icon={<Layers className="h-6 w-6" />}
226
+ title={t('channelAccounts.emptyState.title')}
227
+ description={t('channelAccounts.emptyState.description')}
228
+ actionLabel={t('channelAccounts.emptyState.action')}
229
+ onAction={handleOpenCreate}
230
+ />
231
+ ) : (
232
+ <>
233
+ <Table>
234
+ <TableHeader>
235
+ <TableRow>
236
+ <TableHead>{t('common.code')}</TableHead>
237
+ <TableHead>{t('common.channel')}</TableHead>
238
+ <TableHead>{t('common.name')}</TableHead>
239
+ <TableHead>
240
+ {t('channelAccounts.externalIdentifier')}
241
+ </TableHead>
242
+ <TableHead>{t('common.status')}</TableHead>
243
+ <TableHead>{t('channelAccounts.aiEnabled')}</TableHead>
244
+ <TableHead>{t('common.actions')}</TableHead>
245
+ </TableRow>
246
+ </TableHeader>
247
+ <TableBody>
248
+ {paginatedAccounts.map((item) => (
249
+ <TableRow key={item.id}>
250
+ <TableCell className="font-medium">{item.code}</TableCell>
251
+ <TableCell>
252
+ {getChannelById(item.channelId)?.name}
253
+ </TableCell>
254
+ <TableCell>{item.name}</TableCell>
255
+ <TableCell>{item.externalIdentifier}</TableCell>
256
+ <TableCell>
257
+ <Badge
258
+ variant={
259
+ item.status === 'active' ? 'default' : 'secondary'
260
+ }
261
+ >
262
+ {item.status === 'active'
263
+ ? t('common.enabled')
264
+ : t('common.disabled')}
265
+ </Badge>
266
+ </TableCell>
267
+ <TableCell>
268
+ <Badge
269
+ variant={item.aiEnabled ? 'default' : 'secondary'}
270
+ >
271
+ {item.aiEnabled
272
+ ? t('common.enabled')
273
+ : t('common.disabled')}
274
+ </Badge>
275
+ </TableCell>
276
+ <TableCell>
277
+ <div className="flex gap-2">
278
+ <Button
279
+ size="sm"
280
+ variant="outline"
281
+ onClick={() => handleOpenEdit(item.id)}
282
+ >
283
+ {t('common.edit')}
284
+ </Button>
285
+ <Button
286
+ size="sm"
287
+ variant="outline"
288
+ onClick={handleTestConnection}
289
+ >
290
+ {t('channelAccounts.testConnection')}
291
+ </Button>
292
+ </div>
293
+ </TableCell>
294
+ </TableRow>
295
+ ))}
296
+ </TableBody>
297
+ </Table>
298
+
299
+ <PaginationFooter
300
+ currentPage={safePage}
301
+ pageSize={pageSize}
302
+ totalItems={filteredAccounts.length}
303
+ onPageChange={setPage}
304
+ onPageSizeChange={setPageSize}
305
+ pageSizeOptions={PAGE_SIZE_OPTIONS}
306
+ />
307
+ </>
308
+ )}
309
+ </CardContent>
310
+ </Card>
311
+
312
+ <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
313
+ <SheetContent className="w-full sm:max-w-lg">
314
+ <SheetHeader>
315
+ <SheetTitle>
316
+ {editingId
317
+ ? t('channelAccounts.sheet.editTitle')
318
+ : t('channelAccounts.sheet.createTitle')}
319
+ </SheetTitle>
320
+ <SheetDescription>
321
+ {editingId
322
+ ? t('channelAccounts.sheet.editDescription')
323
+ : t('channelAccounts.sheet.createDescription')}
324
+ </SheetDescription>
325
+ </SheetHeader>
326
+
327
+ <Form {...form}>
328
+ <form
329
+ onSubmit={form.handleSubmit(handleSave)}
330
+ className="mt-6 space-y-4"
331
+ >
332
+ <FormField
333
+ control={form.control}
334
+ name="channelId"
335
+ render={({ field }) => (
336
+ <FormItem>
337
+ <FormLabel>{t('common.channel')}</FormLabel>
338
+ <FormControl>
339
+ <Select
340
+ value={field.value}
341
+ onValueChange={field.onChange}
342
+ >
343
+ <SelectTrigger>
344
+ <SelectValue />
345
+ </SelectTrigger>
346
+ <SelectContent>
347
+ {ticketChannels.map((channel) => (
348
+ <SelectItem
349
+ key={channel.id}
350
+ value={String(channel.id)}
351
+ >
352
+ {channel.name}
353
+ </SelectItem>
354
+ ))}
355
+ </SelectContent>
356
+ </Select>
357
+ </FormControl>
358
+ <FormMessage />
359
+ </FormItem>
360
+ )}
361
+ />
362
+
363
+ <FormField
364
+ control={form.control}
365
+ name="code"
366
+ render={({ field }) => (
367
+ <FormItem>
368
+ <FormLabel>{t('common.code')}</FormLabel>
369
+ <FormControl>
370
+ <Input
371
+ {...field}
372
+ placeholder={t('channelAccounts.form.codePlaceholder')}
373
+ />
374
+ </FormControl>
375
+ <FormMessage />
376
+ </FormItem>
377
+ )}
378
+ />
379
+
380
+ <FormField
381
+ control={form.control}
382
+ name="name"
383
+ render={({ field }) => (
384
+ <FormItem>
385
+ <FormLabel>{t('common.name')}</FormLabel>
386
+ <FormControl>
387
+ <Input
388
+ {...field}
389
+ placeholder={t('channelAccounts.form.namePlaceholder')}
390
+ />
391
+ </FormControl>
392
+ <FormMessage />
393
+ </FormItem>
394
+ )}
395
+ />
396
+
397
+ <FormField
398
+ control={form.control}
399
+ name="externalIdentifier"
400
+ render={({ field }) => (
401
+ <FormItem>
402
+ <FormLabel>
403
+ {t('channelAccounts.externalIdentifier')}
404
+ </FormLabel>
405
+ <FormControl>
406
+ <Input
407
+ {...field}
408
+ placeholder={t(
409
+ 'channelAccounts.form.externalIdentifierPlaceholder'
410
+ )}
411
+ />
412
+ </FormControl>
413
+ <FormMessage />
414
+ </FormItem>
415
+ )}
416
+ />
417
+
418
+ <FormField
419
+ control={form.control}
420
+ name="status"
421
+ render={({ field }) => (
422
+ <FormItem>
423
+ <FormLabel>{t('common.status')}</FormLabel>
424
+ <FormControl>
425
+ <Select
426
+ value={field.value}
427
+ onValueChange={field.onChange}
428
+ >
429
+ <SelectTrigger>
430
+ <SelectValue />
431
+ </SelectTrigger>
432
+ <SelectContent>
433
+ <SelectItem value="active">
434
+ {t('common.enabled')}
435
+ </SelectItem>
436
+ <SelectItem value="inactive">
437
+ {t('common.disabled')}
438
+ </SelectItem>
439
+ </SelectContent>
440
+ </Select>
441
+ </FormControl>
442
+ <FormMessage />
443
+ </FormItem>
444
+ )}
445
+ />
446
+
447
+ <FormField
448
+ control={form.control}
449
+ name="aiEnabled"
450
+ render={({ field }) => (
451
+ <FormItem className="flex items-center justify-between rounded border p-3">
452
+ <FormLabel>{t('channelAccounts.aiEnabled')}</FormLabel>
453
+ <FormControl>
454
+ <Switch
455
+ checked={field.value}
456
+ onCheckedChange={field.onChange}
457
+ />
458
+ </FormControl>
459
+ </FormItem>
460
+ )}
461
+ />
462
+
463
+ <SheetFooter>
464
+ <Button type="submit" className="w-full">
465
+ {editingId
466
+ ? t('channelAccounts.form.submitUpdate')
467
+ : t('channelAccounts.form.submitCreate')}
468
+ </Button>
469
+ </SheetFooter>
470
+ </form>
471
+ </Form>
472
+ </SheetContent>
473
+ </Sheet>
474
+ </Page>
475
+ );
476
+ }