@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/greatchat-ui",
3
- "version": "0.1.4",
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 &quot;{channel.name}&quot; 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
+ }