@greatapps/greatchat-ui 0.1.4 → 0.1.6
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.
- package/dist/index.d.ts +122 -1
- package/dist/index.js +1692 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/components/channel-card.tsx +100 -0
- package/src/components/channel-create-dialog.tsx +126 -0
- package/src/components/channel-edit-dialog.tsx +132 -0
- package/src/components/channels-page.tsx +242 -0
- package/src/components/chat-dashboard.tsx +433 -0
- package/src/components/contact-form-dialog.tsx +139 -0
- package/src/components/contacts-page.tsx +41 -0
- package/src/components/contacts-table.tsx +218 -0
- package/src/components/data-table.tsx +185 -0
- package/src/components/inbox-page.tsx +174 -0
- package/src/components/index.ts +6 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/table.tsx +101 -0
- package/src/components/whatsapp-icon.tsx +21 -0
- package/src/components/whatsapp-qr-dialog.tsx +147 -0
- package/src/components/whatsapp-status-badge.tsx +72 -0
- package/src/index.ts +40 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greatapps/greatchat-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -23,11 +23,13 @@
|
|
|
23
23
|
"react": "^19",
|
|
24
24
|
"react-dom": "^19",
|
|
25
25
|
"@tanstack/react-query": "^5",
|
|
26
|
+
"@tanstack/react-table": "^8",
|
|
26
27
|
"lucide-react": "*",
|
|
27
28
|
"date-fns": "*",
|
|
28
29
|
"sonner": "*"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
32
|
+
"@greatapps/greatauth-ui": "^0.1.4",
|
|
31
33
|
"class-variance-authority": "^0.7.1",
|
|
32
34
|
"clsx": "^2",
|
|
33
35
|
"cmdk": "^1.1.1",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
"react-dom": "^19",
|
|
42
44
|
"@types/react": "latest",
|
|
43
45
|
"@types/react-dom": "latest",
|
|
44
|
-
"@tanstack/react-query": "latest"
|
|
46
|
+
"@tanstack/react-query": "latest",
|
|
47
|
+
"@tanstack/react-table": "latest"
|
|
45
48
|
}
|
|
46
49
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { Channel } from "../types";
|
|
5
|
+
import type { GchatHookConfig } from "../hooks/types";
|
|
6
|
+
import { useChannelWhatsappStatus } from "../hooks/use-channels";
|
|
7
|
+
import { WhatsappStatusBadge } from "./whatsapp-status-badge";
|
|
8
|
+
import { Button } from "./ui/button";
|
|
9
|
+
|
|
10
|
+
export interface ChannelCardProps {
|
|
11
|
+
channel: Channel;
|
|
12
|
+
config: GchatHookConfig;
|
|
13
|
+
onEdit?: () => void;
|
|
14
|
+
onDelete?: () => void;
|
|
15
|
+
linkedAgentName?: string;
|
|
16
|
+
linkedAgentActive?: boolean;
|
|
17
|
+
actions?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ChannelCard({
|
|
21
|
+
channel,
|
|
22
|
+
config,
|
|
23
|
+
onEdit,
|
|
24
|
+
onDelete,
|
|
25
|
+
linkedAgentName,
|
|
26
|
+
linkedAgentActive = true,
|
|
27
|
+
actions,
|
|
28
|
+
}: ChannelCardProps) {
|
|
29
|
+
const { data: status } = useChannelWhatsappStatus(config, channel.id);
|
|
30
|
+
const hasSession = !!channel.external_id;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
|
|
34
|
+
<div className="flex flex-row items-start justify-between p-4 pb-2">
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-md bg-green-500/10">
|
|
37
|
+
<svg className="h-5 w-5 text-green-600" viewBox="0 0 24 24" fill="currentColor">
|
|
38
|
+
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" />
|
|
39
|
+
</svg>
|
|
40
|
+
</div>
|
|
41
|
+
<div>
|
|
42
|
+
<h3 className="text-sm font-medium">{channel.name}</h3>
|
|
43
|
+
<p className="text-xs text-muted-foreground">
|
|
44
|
+
{channel.identifier || "Sem número"}
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex items-center gap-1">
|
|
49
|
+
{onEdit && (
|
|
50
|
+
<Button
|
|
51
|
+
variant="ghost"
|
|
52
|
+
size="icon"
|
|
53
|
+
className="h-7 w-7"
|
|
54
|
+
onClick={onEdit}
|
|
55
|
+
>
|
|
56
|
+
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
57
|
+
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
|
58
|
+
<path d="m15 5 4 4" />
|
|
59
|
+
</svg>
|
|
60
|
+
</Button>
|
|
61
|
+
)}
|
|
62
|
+
{onDelete && (
|
|
63
|
+
<Button
|
|
64
|
+
variant="ghost"
|
|
65
|
+
size="icon"
|
|
66
|
+
className="h-7 w-7 text-destructive hover:text-destructive"
|
|
67
|
+
onClick={onDelete}
|
|
68
|
+
>
|
|
69
|
+
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
70
|
+
<path d="M3 6h18" />
|
|
71
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
72
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
73
|
+
</svg>
|
|
74
|
+
</Button>
|
|
75
|
+
)}
|
|
76
|
+
<WhatsappStatusBadge status={status} hasSession={hasSession} />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="p-4 pt-0 space-y-3">
|
|
80
|
+
{linkedAgentName && (
|
|
81
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
82
|
+
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
83
|
+
<path d="M12 8V4H8" />
|
|
84
|
+
<rect width="16" height="12" x="4" y="8" rx="2" />
|
|
85
|
+
<path d="M2 14h2" />
|
|
86
|
+
<path d="M20 14h2" />
|
|
87
|
+
<path d="M15 13v2" />
|
|
88
|
+
<path d="M9 13v2" />
|
|
89
|
+
</svg>
|
|
90
|
+
<span>Agent: <strong>{linkedAgentName}</strong></span>
|
|
91
|
+
{!linkedAgentActive && (
|
|
92
|
+
<span className="text-[10px] px-1 py-0 border rounded">inativo</span>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
{actions}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import type { GchatHookConfig } from "../hooks/types";
|
|
6
|
+
import { useCreateChannel } from "../hooks/use-channels";
|
|
7
|
+
import { WhatsappIcon } from "./whatsapp-icon";
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
} from "./ui/dialog";
|
|
15
|
+
import { Button } from "./ui/button";
|
|
16
|
+
import { Input } from "./ui/input";
|
|
17
|
+
import { Label } from "./ui/label";
|
|
18
|
+
import { Loader2 } from "lucide-react";
|
|
19
|
+
import { toast } from "sonner";
|
|
20
|
+
|
|
21
|
+
export interface ChannelCreateDialogProps {
|
|
22
|
+
open: boolean;
|
|
23
|
+
onOpenChange: (open: boolean) => void;
|
|
24
|
+
config: GchatHookConfig;
|
|
25
|
+
renderAgentSelect?: (props: {
|
|
26
|
+
value: number | null;
|
|
27
|
+
onChange: (value: number | null) => void;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}) => ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ChannelCreateDialog({
|
|
33
|
+
open,
|
|
34
|
+
onOpenChange,
|
|
35
|
+
config,
|
|
36
|
+
renderAgentSelect,
|
|
37
|
+
}: ChannelCreateDialogProps) {
|
|
38
|
+
const createChannel = useCreateChannel(config);
|
|
39
|
+
const [name, setName] = useState("");
|
|
40
|
+
const [identifier, setIdentifier] = useState("");
|
|
41
|
+
const [idAgent, setIdAgent] = useState<number | null>(null);
|
|
42
|
+
|
|
43
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
if (!name.trim()) return;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await createChannel.mutateAsync({
|
|
49
|
+
name: name.trim(),
|
|
50
|
+
type: "whatsapp_unofficial",
|
|
51
|
+
provider: "gwhatsmeow",
|
|
52
|
+
identifier: identifier.trim() || undefined,
|
|
53
|
+
id_agent: idAgent,
|
|
54
|
+
});
|
|
55
|
+
toast.success("Canal criado! Agora conecte via QR code.");
|
|
56
|
+
setName("");
|
|
57
|
+
setIdentifier("");
|
|
58
|
+
setIdAgent(null);
|
|
59
|
+
onOpenChange(false);
|
|
60
|
+
} catch {
|
|
61
|
+
toast.error("Erro ao criar canal");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
67
|
+
<DialogContent>
|
|
68
|
+
<DialogHeader>
|
|
69
|
+
<DialogTitle className="flex items-center gap-2">
|
|
70
|
+
<WhatsappIcon className="h-5 w-5 text-green-600" />
|
|
71
|
+
Novo Canal WhatsApp
|
|
72
|
+
</DialogTitle>
|
|
73
|
+
</DialogHeader>
|
|
74
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<Label htmlFor="channel-name">Nome do canal *</Label>
|
|
77
|
+
<Input
|
|
78
|
+
id="channel-name"
|
|
79
|
+
value={name}
|
|
80
|
+
onChange={(e) => setName(e.target.value)}
|
|
81
|
+
placeholder="Ex: WhatsApp Principal"
|
|
82
|
+
required
|
|
83
|
+
disabled={createChannel.isPending}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="space-y-2">
|
|
87
|
+
<Label htmlFor="channel-identifier">
|
|
88
|
+
Número do WhatsApp (opcional)
|
|
89
|
+
</Label>
|
|
90
|
+
<Input
|
|
91
|
+
id="channel-identifier"
|
|
92
|
+
value={identifier}
|
|
93
|
+
onChange={(e) => setIdentifier(e.target.value)}
|
|
94
|
+
placeholder="5511999999999"
|
|
95
|
+
disabled={createChannel.isPending}
|
|
96
|
+
/>
|
|
97
|
+
<p className="text-xs text-muted-foreground">
|
|
98
|
+
Será detectado automaticamente ao escanear o QR code
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
{renderAgentSelect?.({
|
|
102
|
+
value: idAgent,
|
|
103
|
+
onChange: setIdAgent,
|
|
104
|
+
disabled: createChannel.isPending,
|
|
105
|
+
})}
|
|
106
|
+
<DialogFooter>
|
|
107
|
+
<Button
|
|
108
|
+
type="button"
|
|
109
|
+
variant="outline"
|
|
110
|
+
onClick={() => onOpenChange(false)}
|
|
111
|
+
disabled={createChannel.isPending}
|
|
112
|
+
>
|
|
113
|
+
Cancelar
|
|
114
|
+
</Button>
|
|
115
|
+
<Button type="submit" disabled={createChannel.isPending || !name.trim()}>
|
|
116
|
+
{createChannel.isPending && (
|
|
117
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
118
|
+
)}
|
|
119
|
+
Criar Canal
|
|
120
|
+
</Button>
|
|
121
|
+
</DialogFooter>
|
|
122
|
+
</form>
|
|
123
|
+
</DialogContent>
|
|
124
|
+
</Dialog>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import type { Channel } from "../types";
|
|
6
|
+
import type { GchatHookConfig } from "../hooks/types";
|
|
7
|
+
import { useUpdateChannel } from "../hooks/use-channels";
|
|
8
|
+
import { WhatsappIcon } from "./whatsapp-icon";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
} from "./ui/dialog";
|
|
16
|
+
import { Button } from "./ui/button";
|
|
17
|
+
import { Input } from "./ui/input";
|
|
18
|
+
import { Label } from "./ui/label";
|
|
19
|
+
import { Loader2 } from "lucide-react";
|
|
20
|
+
import { toast } from "sonner";
|
|
21
|
+
|
|
22
|
+
export interface ChannelEditDialogProps {
|
|
23
|
+
open: boolean;
|
|
24
|
+
onOpenChange: (open: boolean) => void;
|
|
25
|
+
channel: Channel;
|
|
26
|
+
config: GchatHookConfig;
|
|
27
|
+
renderAgentSelect?: (props: {
|
|
28
|
+
value: number | null;
|
|
29
|
+
onChange: (value: number | null) => void;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
}) => ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ChannelEditDialog({
|
|
35
|
+
open,
|
|
36
|
+
onOpenChange,
|
|
37
|
+
channel,
|
|
38
|
+
config,
|
|
39
|
+
renderAgentSelect,
|
|
40
|
+
}: ChannelEditDialogProps) {
|
|
41
|
+
const updateChannel = useUpdateChannel(config);
|
|
42
|
+
const [name, setName] = useState(channel.name);
|
|
43
|
+
const [identifier, setIdentifier] = useState(channel.identifier || "");
|
|
44
|
+
const [idAgent, setIdAgent] = useState<number | null>(channel.id_agent);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (open) {
|
|
48
|
+
setName(channel.name);
|
|
49
|
+
setIdentifier(channel.identifier || "");
|
|
50
|
+
setIdAgent(channel.id_agent);
|
|
51
|
+
}
|
|
52
|
+
}, [open, channel]);
|
|
53
|
+
|
|
54
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
if (!name.trim()) return;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await updateChannel.mutateAsync({
|
|
60
|
+
id: channel.id,
|
|
61
|
+
body: {
|
|
62
|
+
name: name.trim(),
|
|
63
|
+
identifier: identifier.trim() || undefined,
|
|
64
|
+
id_agent: idAgent,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
toast.success("Canal atualizado");
|
|
68
|
+
onOpenChange(false);
|
|
69
|
+
} catch {
|
|
70
|
+
toast.error("Erro ao atualizar canal");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
76
|
+
<DialogContent>
|
|
77
|
+
<DialogHeader>
|
|
78
|
+
<DialogTitle className="flex items-center gap-2">
|
|
79
|
+
<WhatsappIcon className="h-5 w-5 text-green-600" />
|
|
80
|
+
Editar Canal
|
|
81
|
+
</DialogTitle>
|
|
82
|
+
</DialogHeader>
|
|
83
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
84
|
+
<div className="space-y-2">
|
|
85
|
+
<Label htmlFor="edit-channel-name">Nome do canal *</Label>
|
|
86
|
+
<Input
|
|
87
|
+
id="edit-channel-name"
|
|
88
|
+
value={name}
|
|
89
|
+
onChange={(e) => setName(e.target.value)}
|
|
90
|
+
placeholder="Ex: WhatsApp Principal"
|
|
91
|
+
required
|
|
92
|
+
disabled={updateChannel.isPending}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
<Label htmlFor="edit-channel-identifier">
|
|
97
|
+
Número do WhatsApp (opcional)
|
|
98
|
+
</Label>
|
|
99
|
+
<Input
|
|
100
|
+
id="edit-channel-identifier"
|
|
101
|
+
value={identifier}
|
|
102
|
+
onChange={(e) => setIdentifier(e.target.value)}
|
|
103
|
+
placeholder="5511999999999"
|
|
104
|
+
disabled={updateChannel.isPending}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
{renderAgentSelect?.({
|
|
108
|
+
value: idAgent,
|
|
109
|
+
onChange: setIdAgent,
|
|
110
|
+
disabled: updateChannel.isPending,
|
|
111
|
+
})}
|
|
112
|
+
<DialogFooter>
|
|
113
|
+
<Button
|
|
114
|
+
type="button"
|
|
115
|
+
variant="outline"
|
|
116
|
+
onClick={() => onOpenChange(false)}
|
|
117
|
+
disabled={updateChannel.isPending}
|
|
118
|
+
>
|
|
119
|
+
Cancelar
|
|
120
|
+
</Button>
|
|
121
|
+
<Button type="submit" disabled={updateChannel.isPending || !name.trim()}>
|
|
122
|
+
{updateChannel.isPending && (
|
|
123
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
124
|
+
)}
|
|
125
|
+
Salvar
|
|
126
|
+
</Button>
|
|
127
|
+
</DialogFooter>
|
|
128
|
+
</form>
|
|
129
|
+
</DialogContent>
|
|
130
|
+
</Dialog>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import type { Channel } from "../types";
|
|
6
|
+
import type { GchatHookConfig } from "../hooks/types";
|
|
7
|
+
import { useChannels, useDisconnectChannel, useLogoutChannel, useDeleteChannel, useChannelWhatsappStatus } from "../hooks/use-channels";
|
|
8
|
+
import { ChannelCard } from "./channel-card";
|
|
9
|
+
import { ChannelCreateDialog } from "./channel-create-dialog";
|
|
10
|
+
import { ChannelEditDialog } from "./channel-edit-dialog";
|
|
11
|
+
import { WhatsappQrDialog } from "./whatsapp-qr-dialog";
|
|
12
|
+
import { WhatsappIcon } from "./whatsapp-icon";
|
|
13
|
+
import { Button } from "./ui/button";
|
|
14
|
+
import {
|
|
15
|
+
AlertDialog,
|
|
16
|
+
AlertDialogAction,
|
|
17
|
+
AlertDialogCancel,
|
|
18
|
+
AlertDialogContent,
|
|
19
|
+
AlertDialogDescription,
|
|
20
|
+
AlertDialogFooter,
|
|
21
|
+
AlertDialogHeader,
|
|
22
|
+
AlertDialogTitle,
|
|
23
|
+
} from "./ui/alert-dialog";
|
|
24
|
+
import { Skeleton } from "./ui/skeleton";
|
|
25
|
+
import { Plus, QrCode, Unplug, LogOut } from "lucide-react";
|
|
26
|
+
import { toast } from "sonner";
|
|
27
|
+
|
|
28
|
+
export interface ChannelsPageProps {
|
|
29
|
+
config: GchatHookConfig;
|
|
30
|
+
renderAgentSelect?: (props: {
|
|
31
|
+
value: number | null;
|
|
32
|
+
onChange: (value: number | null) => void;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
}) => ReactNode;
|
|
35
|
+
/** Optional: resolve agent info for a channel. Returns { name, active } */
|
|
36
|
+
getAgentInfo?: (idAgent: number | null) => { name: string; active: boolean } | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ChannelCardWrapper({
|
|
40
|
+
channel,
|
|
41
|
+
config,
|
|
42
|
+
renderAgentSelect,
|
|
43
|
+
getAgentInfo,
|
|
44
|
+
}: {
|
|
45
|
+
channel: Channel;
|
|
46
|
+
config: GchatHookConfig;
|
|
47
|
+
renderAgentSelect?: ChannelsPageProps["renderAgentSelect"];
|
|
48
|
+
getAgentInfo?: ChannelsPageProps["getAgentInfo"];
|
|
49
|
+
}) {
|
|
50
|
+
const { data: status } = useChannelWhatsappStatus(config, channel.id);
|
|
51
|
+
const disconnectChannel = useDisconnectChannel(config);
|
|
52
|
+
const logoutChannel = useLogoutChannel(config);
|
|
53
|
+
const deleteChannel = useDeleteChannel(config);
|
|
54
|
+
const [qrOpen, setQrOpen] = useState(false);
|
|
55
|
+
const [editOpen, setEditOpen] = useState(false);
|
|
56
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
57
|
+
|
|
58
|
+
const isFullyConnected = status?.connected === true && status?.logged_in === true;
|
|
59
|
+
const hasSession = !!channel.external_id;
|
|
60
|
+
const agentInfo = getAgentInfo?.(channel.id_agent);
|
|
61
|
+
|
|
62
|
+
function handleDisconnect() {
|
|
63
|
+
disconnectChannel.mutate(channel.id, {
|
|
64
|
+
onSuccess: () => toast.success("Canal desconectado"),
|
|
65
|
+
onError: () => toast.error("Erro ao desconectar"),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleLogout() {
|
|
70
|
+
logoutChannel.mutate(channel.id, {
|
|
71
|
+
onSuccess: () => toast.success("Logout realizado. Será necessário novo QR code."),
|
|
72
|
+
onError: () => toast.error("Erro ao fazer logout"),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleDelete() {
|
|
77
|
+
deleteChannel.mutate(channel.id, {
|
|
78
|
+
onSuccess: () => {
|
|
79
|
+
toast.success("Canal excluído");
|
|
80
|
+
setDeleteOpen(false);
|
|
81
|
+
},
|
|
82
|
+
onError: () => toast.error("Erro ao excluir canal"),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<ChannelCard
|
|
89
|
+
channel={channel}
|
|
90
|
+
config={config}
|
|
91
|
+
onEdit={() => setEditOpen(true)}
|
|
92
|
+
onDelete={() => setDeleteOpen(true)}
|
|
93
|
+
linkedAgentName={agentInfo?.name}
|
|
94
|
+
linkedAgentActive={agentInfo?.active !== false}
|
|
95
|
+
actions={
|
|
96
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
97
|
+
{!isFullyConnected && (
|
|
98
|
+
<Button
|
|
99
|
+
size="sm"
|
|
100
|
+
variant="outline"
|
|
101
|
+
onClick={() => setQrOpen(true)}
|
|
102
|
+
>
|
|
103
|
+
<QrCode className="mr-1.5 h-3.5 w-3.5" />
|
|
104
|
+
Conectar via QR
|
|
105
|
+
</Button>
|
|
106
|
+
)}
|
|
107
|
+
{isFullyConnected && (
|
|
108
|
+
<Button
|
|
109
|
+
size="sm"
|
|
110
|
+
variant="outline"
|
|
111
|
+
onClick={handleDisconnect}
|
|
112
|
+
disabled={disconnectChannel.isPending}
|
|
113
|
+
>
|
|
114
|
+
<Unplug className="mr-1.5 h-3.5 w-3.5" />
|
|
115
|
+
Desconectar
|
|
116
|
+
</Button>
|
|
117
|
+
)}
|
|
118
|
+
{hasSession && (
|
|
119
|
+
<Button
|
|
120
|
+
size="sm"
|
|
121
|
+
variant="outline"
|
|
122
|
+
className="text-destructive hover:text-destructive"
|
|
123
|
+
onClick={handleLogout}
|
|
124
|
+
disabled={logoutChannel.isPending}
|
|
125
|
+
>
|
|
126
|
+
<LogOut className="mr-1.5 h-3.5 w-3.5" />
|
|
127
|
+
Logout
|
|
128
|
+
</Button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<WhatsappQrDialog
|
|
135
|
+
open={qrOpen}
|
|
136
|
+
onOpenChange={setQrOpen}
|
|
137
|
+
channelId={channel.id}
|
|
138
|
+
config={config}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<ChannelEditDialog
|
|
142
|
+
open={editOpen}
|
|
143
|
+
onOpenChange={setEditOpen}
|
|
144
|
+
channel={channel}
|
|
145
|
+
config={config}
|
|
146
|
+
renderAgentSelect={renderAgentSelect}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
150
|
+
<AlertDialogContent>
|
|
151
|
+
<AlertDialogHeader>
|
|
152
|
+
<AlertDialogTitle>Excluir canal?</AlertDialogTitle>
|
|
153
|
+
<AlertDialogDescription>
|
|
154
|
+
O canal "{channel.name}" será removido permanentemente.
|
|
155
|
+
Todas as conversas associadas serão perdidas.
|
|
156
|
+
</AlertDialogDescription>
|
|
157
|
+
</AlertDialogHeader>
|
|
158
|
+
<AlertDialogFooter>
|
|
159
|
+
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
160
|
+
<AlertDialogAction
|
|
161
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
162
|
+
onClick={handleDelete}
|
|
163
|
+
>
|
|
164
|
+
Excluir
|
|
165
|
+
</AlertDialogAction>
|
|
166
|
+
</AlertDialogFooter>
|
|
167
|
+
</AlertDialogContent>
|
|
168
|
+
</AlertDialog>
|
|
169
|
+
</>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function ChannelsPage({
|
|
174
|
+
config,
|
|
175
|
+
renderAgentSelect,
|
|
176
|
+
getAgentInfo,
|
|
177
|
+
}: ChannelsPageProps) {
|
|
178
|
+
const { data: channels, isLoading } = useChannels(config);
|
|
179
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="flex flex-col gap-6 p-4 flex-1 min-h-0 overflow-y-auto">
|
|
183
|
+
<div className="flex items-center justify-between">
|
|
184
|
+
<div>
|
|
185
|
+
<h1 className="text-xl font-semibold">Canais</h1>
|
|
186
|
+
<p className="text-sm text-muted-foreground">
|
|
187
|
+
Gerencie seus canais de comunicação
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
<Button onClick={() => setCreateOpen(true)}>
|
|
191
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
192
|
+
Novo Canal
|
|
193
|
+
</Button>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{isLoading ? (
|
|
197
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
198
|
+
{[1, 2, 3].map((i) => (
|
|
199
|
+
<div key={i} className="rounded-lg border p-4 space-y-3">
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<Skeleton className="h-9 w-9 rounded-md" />
|
|
202
|
+
<div className="space-y-1">
|
|
203
|
+
<Skeleton className="h-4 w-24" />
|
|
204
|
+
<Skeleton className="h-3 w-32" />
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<Skeleton className="h-8 w-full" />
|
|
208
|
+
</div>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
) : !channels?.length ? (
|
|
212
|
+
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground gap-3">
|
|
213
|
+
<WhatsappIcon className="h-12 w-12 opacity-20" />
|
|
214
|
+
<p className="text-sm">Nenhum canal configurado</p>
|
|
215
|
+
<Button variant="outline" onClick={() => setCreateOpen(true)}>
|
|
216
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
217
|
+
Criar primeiro canal
|
|
218
|
+
</Button>
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
222
|
+
{channels.map((channel) => (
|
|
223
|
+
<ChannelCardWrapper
|
|
224
|
+
key={channel.id}
|
|
225
|
+
channel={channel}
|
|
226
|
+
config={config}
|
|
227
|
+
renderAgentSelect={renderAgentSelect}
|
|
228
|
+
getAgentInfo={getAgentInfo}
|
|
229
|
+
/>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
<ChannelCreateDialog
|
|
235
|
+
open={createOpen}
|
|
236
|
+
onOpenChange={setCreateOpen}
|
|
237
|
+
config={config}
|
|
238
|
+
renderAgentSelect={renderAgentSelect}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|