@greatapps/greatagents-ui 0.1.0 → 0.2.0

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.
@@ -0,0 +1,572 @@
1
+ import { useMemo, useState } from "react";
2
+ import type { ColumnDef } from "@tanstack/react-table";
3
+ import type { Tool, ToolCredential } from "../../types";
4
+ import type { GagentsHookConfig } from "../../hooks/types";
5
+ import {
6
+ useCreateToolCredential,
7
+ useUpdateToolCredential,
8
+ useDeleteToolCredential,
9
+ } from "../../hooks";
10
+ import { useTools } from "../../hooks";
11
+ import { DataTable } from "@greatapps/greatauth-ui";
12
+ import {
13
+ Input,
14
+ Button,
15
+ Badge,
16
+ Tooltip,
17
+ TooltipTrigger,
18
+ TooltipContent,
19
+ Dialog,
20
+ DialogContent,
21
+ DialogHeader,
22
+ DialogTitle,
23
+ DialogFooter,
24
+ AlertDialog,
25
+ AlertDialogAction,
26
+ AlertDialogCancel,
27
+ AlertDialogContent,
28
+ AlertDialogDescription,
29
+ AlertDialogFooter,
30
+ AlertDialogHeader,
31
+ AlertDialogTitle,
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue,
37
+ } from "@greatapps/greatauth-ui/ui";
38
+ import { Trash2, Pencil, Link, Search } from "lucide-react";
39
+ import { format } from "date-fns";
40
+ import { ptBR } from "date-fns/locale";
41
+ import { toast } from "sonner";
42
+
43
+ interface ToolCredentialsFormProps {
44
+ credentials: ToolCredential[];
45
+ isLoading: boolean;
46
+ config: GagentsHookConfig;
47
+ gagentsApiUrl: string;
48
+ createOpen?: boolean;
49
+ onCreateOpenChange?: (open: boolean) => void;
50
+ }
51
+
52
+ function formatDate(dateStr: string | null): string {
53
+ if (!dateStr) return "Sem expiração";
54
+ return format(new Date(dateStr), "dd/MM/yyyy", { locale: ptBR });
55
+ }
56
+
57
+ function useColumns(
58
+ tools: Tool[],
59
+ onEdit: (cred: ToolCredential) => void,
60
+ onConnect: (cred: ToolCredential) => void,
61
+ onRemove: (cred: ToolCredential) => void,
62
+ ): ColumnDef<ToolCredential>[] {
63
+ function getToolName(idTool: number | null): string {
64
+ if (!idTool) return "\u2014";
65
+ const tool = tools.find((t) => t.id === idTool);
66
+ return tool?.name || `Ferramenta #${idTool}`;
67
+ }
68
+
69
+ function getToolType(idTool: number | null): string | null {
70
+ if (!idTool) return null;
71
+ const tool = tools.find((t) => t.id === idTool);
72
+ return tool?.type || null;
73
+ }
74
+
75
+ return [
76
+ {
77
+ accessorKey: "label",
78
+ header: "Label",
79
+ cell: ({ row }) => (
80
+ <span className="font-medium">{row.original.label || "\u2014"}</span>
81
+ ),
82
+ },
83
+ {
84
+ accessorKey: "id_tool",
85
+ header: "Ferramenta",
86
+ cell: ({ row }) => (
87
+ <span className="text-sm">{getToolName(row.original.id_tool)}</span>
88
+ ),
89
+ },
90
+ {
91
+ accessorKey: "status",
92
+ header: "Status",
93
+ cell: ({ row }) => (
94
+ <Badge
95
+ variant={row.original.status === "active" ? "default" : "destructive"}
96
+ >
97
+ {row.original.status === "active" ? "Ativo" : "Expirado"}
98
+ </Badge>
99
+ ),
100
+ },
101
+ {
102
+ accessorKey: "expires_at",
103
+ header: "Expira em",
104
+ cell: ({ row }) => (
105
+ <span className="text-muted-foreground text-sm">
106
+ {formatDate(row.original.expires_at)}
107
+ </span>
108
+ ),
109
+ },
110
+ {
111
+ accessorKey: "datetime_add",
112
+ header: "Criado em",
113
+ cell: ({ row }) => (
114
+ <span className="text-muted-foreground text-sm">
115
+ {formatDate(row.original.datetime_add)}
116
+ </span>
117
+ ),
118
+ },
119
+ {
120
+ id: "actions",
121
+ header: "Ações",
122
+ size: 100,
123
+ enableSorting: false,
124
+ cell: ({ row }) => (
125
+ <div className="flex items-center gap-1">
126
+ {getToolType(row.original.id_tool) === "oauth2" && (
127
+ <Tooltip>
128
+ <TooltipTrigger asChild>
129
+ <Button
130
+ variant="ghost"
131
+ size="icon"
132
+ className="h-8 w-8"
133
+ disabled
134
+ >
135
+ <Link className="h-4 w-4" />
136
+ </Button>
137
+ </TooltipTrigger>
138
+ <TooltipContent>Em breve</TooltipContent>
139
+ </Tooltip>
140
+ )}
141
+ <Tooltip>
142
+ <TooltipTrigger asChild>
143
+ <Button
144
+ variant="ghost"
145
+ size="icon"
146
+ className="h-8 w-8"
147
+ onClick={() => onEdit(row.original)}
148
+ >
149
+ <Pencil className="h-4 w-4" />
150
+ </Button>
151
+ </TooltipTrigger>
152
+ <TooltipContent>Editar</TooltipContent>
153
+ </Tooltip>
154
+ <Tooltip>
155
+ <TooltipTrigger asChild>
156
+ <Button
157
+ variant="ghost"
158
+ size="icon"
159
+ className="h-8 w-8 text-destructive hover:text-destructive"
160
+ onClick={() => onRemove(row.original)}
161
+ >
162
+ <Trash2 className="h-4 w-4" />
163
+ </Button>
164
+ </TooltipTrigger>
165
+ <TooltipContent>Remover</TooltipContent>
166
+ </Tooltip>
167
+ </div>
168
+ ),
169
+ },
170
+ ];
171
+ }
172
+
173
+ export function ToolCredentialsForm({
174
+ credentials,
175
+ isLoading,
176
+ config,
177
+ gagentsApiUrl,
178
+ createOpen: externalCreateOpen,
179
+ onCreateOpenChange,
180
+ }: ToolCredentialsFormProps) {
181
+ const createMutation = useCreateToolCredential(config);
182
+ const updateMutation = useUpdateToolCredential(config);
183
+ const deleteMutation = useDeleteToolCredential(config);
184
+ const { data: toolsData } = useTools(config);
185
+ const tools: Tool[] = toolsData?.data || [];
186
+
187
+ const [search, setSearch] = useState("");
188
+ const [internalCreateOpen, setInternalCreateOpen] = useState(false);
189
+ const showCreateDialog = externalCreateOpen ?? internalCreateOpen;
190
+ const setShowCreateDialog = onCreateOpenChange ?? setInternalCreateOpen;
191
+ const [createForm, setCreateForm] = useState({
192
+ id_tool: "",
193
+ label: "",
194
+ credentials_encrypted: "",
195
+ expires_at: "",
196
+ });
197
+
198
+ const [editTarget, setEditTarget] = useState<ToolCredential | null>(null);
199
+ const [editForm, setEditForm] = useState({
200
+ id_tool: "",
201
+ label: "",
202
+ credentials_encrypted: "",
203
+ expires_at: "",
204
+ status: "" as "active" | "expired" | "",
205
+ });
206
+
207
+ const [removeTarget, setRemoveTarget] = useState<ToolCredential | null>(null);
208
+
209
+ const filteredCredentials = useMemo(() => {
210
+ if (!search) return credentials;
211
+ const term = search.toLowerCase();
212
+ return credentials.filter((cred) => {
213
+ const toolName = tools.find((t) => t.id === cred.id_tool)?.name || "";
214
+ return (
215
+ (cred.label || "").toLowerCase().includes(term) ||
216
+ toolName.toLowerCase().includes(term)
217
+ );
218
+ });
219
+ }, [credentials, search, tools]);
220
+
221
+ const columns = useColumns(
222
+ tools,
223
+ (cred) => startEdit(cred),
224
+ (cred) => handleConnect(cred),
225
+ (cred) => setRemoveTarget(cred),
226
+ );
227
+
228
+ async function handleCreate() {
229
+ const idTool = parseInt(createForm.id_tool, 10);
230
+ if (!idTool || !createForm.label.trim() || !createForm.credentials_encrypted.trim()) return;
231
+
232
+ try {
233
+ const result = await createMutation.mutateAsync({
234
+ id_tool: idTool,
235
+ label: createForm.label.trim(),
236
+ credentials_encrypted: createForm.credentials_encrypted.trim(),
237
+ ...(createForm.expires_at ? { expires_at: createForm.expires_at } : {}),
238
+ });
239
+ if (result.status === 1) {
240
+ toast.success("Credencial criada");
241
+ setShowCreateDialog(false);
242
+ setCreateForm({ id_tool: "", label: "", credentials_encrypted: "", expires_at: "" });
243
+ } else {
244
+ toast.error(result.message || "Erro ao criar credencial");
245
+ }
246
+ } catch {
247
+ toast.error("Erro ao criar credencial");
248
+ }
249
+ }
250
+
251
+ function startEdit(cred: ToolCredential) {
252
+ setEditTarget(cred);
253
+ setEditForm({
254
+ id_tool: cred.id_tool ? String(cred.id_tool) : "",
255
+ label: cred.label || "",
256
+ credentials_encrypted: "",
257
+ expires_at: cred.expires_at || "",
258
+ status: cred.status,
259
+ });
260
+ }
261
+
262
+ async function handleSaveEdit() {
263
+ if (!editTarget) return;
264
+ const body: Record<string, unknown> = {};
265
+ const newIdTool = editForm.id_tool ? parseInt(editForm.id_tool, 10) : null;
266
+ if (newIdTool && newIdTool !== editTarget.id_tool) {
267
+ body.id_tool = newIdTool;
268
+ }
269
+ if (editForm.label.trim() && editForm.label.trim() !== (editTarget.label || "")) {
270
+ body.label = editForm.label.trim();
271
+ }
272
+ if (editForm.credentials_encrypted.trim()) {
273
+ body.credentials_encrypted = editForm.credentials_encrypted.trim();
274
+ }
275
+ if (editForm.expires_at !== (editTarget.expires_at || "")) {
276
+ body.expires_at = editForm.expires_at || null;
277
+ }
278
+ if (editForm.status && editForm.status !== editTarget.status) {
279
+ body.status = editForm.status;
280
+ }
281
+
282
+ if (Object.keys(body).length === 0) {
283
+ setEditTarget(null);
284
+ return;
285
+ }
286
+
287
+ try {
288
+ const result = await updateMutation.mutateAsync({
289
+ id: editTarget.id,
290
+ body: body as Parameters<typeof updateMutation.mutateAsync>[0]["body"],
291
+ });
292
+ if (result.status === 1) {
293
+ toast.success("Credencial atualizada");
294
+ setEditTarget(null);
295
+ } else {
296
+ toast.error(result.message || "Erro ao atualizar credencial");
297
+ }
298
+ } catch {
299
+ toast.error("Erro ao atualizar credencial");
300
+ }
301
+ }
302
+
303
+ async function handleRemove() {
304
+ if (!removeTarget) return;
305
+ try {
306
+ const result = await deleteMutation.mutateAsync(removeTarget.id);
307
+ if (result.status === 1) {
308
+ toast.success("Credencial removida");
309
+ } else {
310
+ toast.error(result.message || "Erro ao remover credencial");
311
+ }
312
+ } catch {
313
+ toast.error("Erro ao remover credencial");
314
+ } finally {
315
+ setRemoveTarget(null);
316
+ }
317
+ }
318
+
319
+ function handleConnect(cred: ToolCredential) {
320
+ if (!config.accountId || !config.token) return;
321
+ const language = config.language ?? "pt-br";
322
+ const idWl = config.idWl ?? 1;
323
+ const url = `${gagentsApiUrl}/v1/${language}/${idWl}/accounts/${config.accountId}/oauth/connect?id_tool=${cred.id_tool}`;
324
+ window.open(url, "_blank");
325
+ }
326
+
327
+ return (
328
+ <div className="space-y-4">
329
+ <div className="flex items-center gap-3">
330
+ <div className="relative flex-1 max-w-md">
331
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
332
+ <Input
333
+ placeholder="Buscar credenciais..."
334
+ value={search}
335
+ onChange={(e) => setSearch(e.target.value)}
336
+ className="pl-9"
337
+ />
338
+ </div>
339
+ </div>
340
+
341
+ <DataTable
342
+ columns={columns}
343
+ data={filteredCredentials}
344
+ isLoading={isLoading}
345
+ emptyMessage="Nenhuma credencial encontrada"
346
+ />
347
+
348
+ {/* Create Dialog */}
349
+ <Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
350
+ <DialogContent>
351
+ <DialogHeader>
352
+ <DialogTitle>Nova Credencial</DialogTitle>
353
+ </DialogHeader>
354
+ <div className="space-y-4">
355
+ <div>
356
+ <label className="mb-1 block text-sm font-medium">
357
+ Ferramenta *
358
+ </label>
359
+ <Select
360
+ value={createForm.id_tool}
361
+ onValueChange={(val) =>
362
+ setCreateForm((f) => ({ ...f, id_tool: val }))
363
+ }
364
+ >
365
+ <SelectTrigger>
366
+ <SelectValue placeholder="Selecione a ferramenta" />
367
+ </SelectTrigger>
368
+ <SelectContent>
369
+ {tools.map((tool) => (
370
+ <SelectItem key={tool.id} value={String(tool.id)}>
371
+ {tool.name}
372
+ </SelectItem>
373
+ ))}
374
+ </SelectContent>
375
+ </Select>
376
+ </div>
377
+ <div>
378
+ <label className="mb-1 block text-sm font-medium">
379
+ Label *
380
+ </label>
381
+ <Input
382
+ value={createForm.label}
383
+ onChange={(e) =>
384
+ setCreateForm((f) => ({ ...f, label: e.target.value }))
385
+ }
386
+ placeholder="Ex: Google Calendar - Clínica São Paulo"
387
+ />
388
+ </div>
389
+ <div>
390
+ <label className="mb-1 block text-sm font-medium">
391
+ Credencial *
392
+ </label>
393
+ <Input
394
+ type="password"
395
+ value={createForm.credentials_encrypted}
396
+ onChange={(e) =>
397
+ setCreateForm((f) => ({
398
+ ...f,
399
+ credentials_encrypted: e.target.value,
400
+ }))
401
+ }
402
+ placeholder="Credencial encriptada"
403
+ />
404
+ </div>
405
+ <div>
406
+ <label className="mb-1 block text-sm font-medium">
407
+ Data de Expiração (opcional)
408
+ </label>
409
+ <Input
410
+ type="date"
411
+ value={createForm.expires_at}
412
+ onChange={(e) =>
413
+ setCreateForm((f) => ({ ...f, expires_at: e.target.value }))
414
+ }
415
+ />
416
+ </div>
417
+ </div>
418
+ <DialogFooter>
419
+ <Button
420
+ variant="outline"
421
+ onClick={() => setShowCreateDialog(false)}
422
+ >
423
+ Cancelar
424
+ </Button>
425
+ <Button
426
+ onClick={handleCreate}
427
+ disabled={
428
+ !createForm.id_tool ||
429
+ !createForm.label.trim() ||
430
+ !createForm.credentials_encrypted.trim() ||
431
+ createMutation.isPending
432
+ }
433
+ >
434
+ Criar
435
+ </Button>
436
+ </DialogFooter>
437
+ </DialogContent>
438
+ </Dialog>
439
+
440
+ {/* Edit Dialog */}
441
+ <Dialog
442
+ open={!!editTarget}
443
+ onOpenChange={(open) => !open && setEditTarget(null)}
444
+ >
445
+ <DialogContent>
446
+ <DialogHeader>
447
+ <DialogTitle>Editar Credencial</DialogTitle>
448
+ </DialogHeader>
449
+ <div className="space-y-4">
450
+ <div>
451
+ <label className="mb-1 block text-sm font-medium">
452
+ Ferramenta *
453
+ </label>
454
+ <Select
455
+ value={editForm.id_tool}
456
+ onValueChange={(val) =>
457
+ setEditForm((f) => ({ ...f, id_tool: val }))
458
+ }
459
+ >
460
+ <SelectTrigger>
461
+ <SelectValue placeholder="Selecione a ferramenta" />
462
+ </SelectTrigger>
463
+ <SelectContent>
464
+ {tools.map((tool) => (
465
+ <SelectItem key={tool.id} value={String(tool.id)}>
466
+ {tool.name}
467
+ </SelectItem>
468
+ ))}
469
+ </SelectContent>
470
+ </Select>
471
+ </div>
472
+ <div>
473
+ <label className="mb-1 block text-sm font-medium">
474
+ Label
475
+ </label>
476
+ <Input
477
+ value={editForm.label}
478
+ onChange={(e) =>
479
+ setEditForm((f) => ({ ...f, label: e.target.value }))
480
+ }
481
+ placeholder="Label da credencial"
482
+ />
483
+ </div>
484
+ <div>
485
+ <label className="mb-1 block text-sm font-medium">
486
+ Nova Credencial (vazio = manter atual)
487
+ </label>
488
+ <Input
489
+ type="password"
490
+ value={editForm.credentials_encrypted}
491
+ onChange={(e) =>
492
+ setEditForm((f) => ({
493
+ ...f,
494
+ credentials_encrypted: e.target.value,
495
+ }))
496
+ }
497
+ placeholder="Nova credencial"
498
+ />
499
+ </div>
500
+ <div>
501
+ <label className="mb-1 block text-sm font-medium">
502
+ Data de Expiração
503
+ </label>
504
+ <Input
505
+ type="date"
506
+ value={editForm.expires_at}
507
+ onChange={(e) =>
508
+ setEditForm((f) => ({ ...f, expires_at: e.target.value }))
509
+ }
510
+ />
511
+ </div>
512
+ <div>
513
+ <label className="mb-1 block text-sm font-medium">Status</label>
514
+ <Select
515
+ value={editForm.status || undefined}
516
+ onValueChange={(val) =>
517
+ setEditForm((f) => ({
518
+ ...f,
519
+ status: val as "active" | "expired",
520
+ }))
521
+ }
522
+ >
523
+ <SelectTrigger>
524
+ <SelectValue />
525
+ </SelectTrigger>
526
+ <SelectContent>
527
+ <SelectItem value="active">Ativo</SelectItem>
528
+ <SelectItem value="expired">Expirado</SelectItem>
529
+ </SelectContent>
530
+ </Select>
531
+ </div>
532
+ </div>
533
+ <DialogFooter>
534
+ <Button variant="outline" onClick={() => setEditTarget(null)}>
535
+ Cancelar
536
+ </Button>
537
+ <Button
538
+ onClick={handleSaveEdit}
539
+ disabled={updateMutation.isPending}
540
+ >
541
+ Salvar
542
+ </Button>
543
+ </DialogFooter>
544
+ </DialogContent>
545
+ </Dialog>
546
+
547
+ {/* Delete confirmation */}
548
+ <AlertDialog
549
+ open={!!removeTarget}
550
+ onOpenChange={(open) => !open && setRemoveTarget(null)}
551
+ >
552
+ <AlertDialogContent>
553
+ <AlertDialogHeader>
554
+ <AlertDialogTitle>Remover credencial?</AlertDialogTitle>
555
+ <AlertDialogDescription>
556
+ A credencial será removida permanentemente.
557
+ </AlertDialogDescription>
558
+ </AlertDialogHeader>
559
+ <AlertDialogFooter>
560
+ <AlertDialogCancel>Cancelar</AlertDialogCancel>
561
+ <AlertDialogAction
562
+ onClick={handleRemove}
563
+ disabled={deleteMutation.isPending}
564
+ >
565
+ Remover
566
+ </AlertDialogAction>
567
+ </AlertDialogFooter>
568
+ </AlertDialogContent>
569
+ </AlertDialog>
570
+ </div>
571
+ );
572
+ }