@greatapps/greatchat-ui 0.1.5 → 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.
Files changed (42) hide show
  1. package/dist/index.d.ts +106 -5
  2. package/dist/index.js +1787 -1116
  3. package/dist/index.js.map +1 -1
  4. package/package.json +17 -13
  5. package/src/components/channel-card.tsx +1 -1
  6. package/src/components/channel-create-dialog.tsx +126 -0
  7. package/src/components/channel-edit-dialog.tsx +132 -0
  8. package/src/components/channels-page.tsx +242 -0
  9. package/src/components/chat-dashboard.tsx +433 -0
  10. package/src/components/chat-input.tsx +1 -2
  11. package/src/components/chat-view.tsx +1 -8
  12. package/src/components/contact-avatar.tsx +1 -2
  13. package/src/components/contact-form-dialog.tsx +139 -0
  14. package/src/components/contact-info-panel.tsx +1 -4
  15. package/src/components/contacts-page.tsx +41 -0
  16. package/src/components/contacts-table.tsx +216 -0
  17. package/src/components/data-table.tsx +185 -0
  18. package/src/components/inbox-item.tsx +6 -4
  19. package/src/components/inbox-page.tsx +167 -0
  20. package/src/components/inbox-sidebar.tsx +1 -5
  21. package/src/components/message-bubble.tsx +4 -6
  22. package/src/components/new-conversation-dialog.tsx +2 -6
  23. package/src/components/whatsapp-icon.tsx +21 -0
  24. package/src/components/whatsapp-qr-dialog.tsx +147 -0
  25. package/src/components/whatsapp-status-badge.tsx +1 -1
  26. package/src/index.ts +37 -2
  27. package/src/components/ui/alert-dialog.tsx +0 -167
  28. package/src/components/ui/avatar.tsx +0 -51
  29. package/src/components/ui/badge.tsx +0 -44
  30. package/src/components/ui/button.tsx +0 -62
  31. package/src/components/ui/command.tsx +0 -106
  32. package/src/components/ui/dialog.tsx +0 -133
  33. package/src/components/ui/dropdown-menu.tsx +0 -173
  34. package/src/components/ui/input.tsx +0 -19
  35. package/src/components/ui/scroll-area.tsx +0 -50
  36. package/src/components/ui/select.tsx +0 -156
  37. package/src/components/ui/separator.tsx +0 -26
  38. package/src/components/ui/skeleton.tsx +0 -16
  39. package/src/components/ui/tabs.tsx +0 -64
  40. package/src/components/ui/textarea.tsx +0 -18
  41. package/src/components/ui/tooltip.tsx +0 -58
  42. package/src/lib/utils.ts +0 -6
@@ -0,0 +1,433 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "react";
4
+ import type { ReactNode } from "react";
5
+ import type { Inbox, Channel } from "../types";
6
+ import type { GchatHookConfig } from "../hooks/types";
7
+ import { useInboxStats, useInboxes } from "../hooks/use-inboxes";
8
+ import { useChannels, useChannelWhatsappStatus } from "../hooks/use-channels";
9
+ import { ContactAvatar } from "./contact-avatar";
10
+ import { WhatsappStatusBadge } from "./whatsapp-status-badge";
11
+ import { WhatsappIcon } from "./whatsapp-icon";
12
+ import {
13
+ Card,
14
+ CardContent,
15
+ CardHeader,
16
+ CardTitle,
17
+ Badge,
18
+ Progress,
19
+ Skeleton,
20
+ cn,
21
+ } from "@greatapps/greatauth-ui/ui";
22
+ import {
23
+ Inbox as InboxIcon,
24
+ MessageCircle,
25
+ Clock,
26
+ CircleCheck,
27
+ ArrowRight,
28
+ } from "lucide-react";
29
+ import { formatDistanceToNow } from "date-fns";
30
+ import { ptBR } from "date-fns/locale";
31
+
32
+ // --- Helpers ---
33
+
34
+ function getGreeting(userName?: string): string {
35
+ const hour = new Date().getHours();
36
+ const prefix = hour < 12 ? "Bom dia" : hour < 18 ? "Boa tarde" : "Boa noite";
37
+ return userName ? `${prefix}, ${userName}!` : `${prefix}!`;
38
+ }
39
+
40
+ const statusColors: Record<string, string> = {
41
+ open: "bg-green-500/10 text-green-600 border-green-200",
42
+ pending: "bg-yellow-500/10 text-yellow-600 border-yellow-200",
43
+ resolved: "bg-zinc-500/10 text-zinc-500 border-zinc-200",
44
+ };
45
+
46
+ const statusLabels: Record<string, string> = {
47
+ open: "Aberta",
48
+ pending: "Pendente",
49
+ resolved: "Resolvida",
50
+ };
51
+
52
+ // --- Sub-components ---
53
+
54
+ function StatCard({
55
+ title,
56
+ value,
57
+ total,
58
+ icon: Icon,
59
+ loading,
60
+ accentColor,
61
+ onClick,
62
+ }: {
63
+ title: string;
64
+ value: number | undefined;
65
+ total: number | undefined;
66
+ icon: React.ElementType;
67
+ loading: boolean;
68
+ accentColor: string;
69
+ onClick: () => void;
70
+ }) {
71
+ const pct = total && value ? Math.round((value / total) * 100) : 0;
72
+
73
+ return (
74
+ <Card
75
+ className={cn("cursor-pointer transition-shadow hover:shadow-md border-l-4", accentColor)}
76
+ onClick={onClick}
77
+ >
78
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
79
+ <CardTitle className="text-sm font-medium text-muted-foreground">
80
+ {title}
81
+ </CardTitle>
82
+ <Icon className="h-4 w-4 text-muted-foreground" />
83
+ </CardHeader>
84
+ <CardContent className="space-y-2">
85
+ {loading ? (
86
+ <Skeleton className="h-8 w-16" />
87
+ ) : (
88
+ <>
89
+ <div className="flex items-baseline gap-2">
90
+ <p className="text-2xl font-bold">{value ?? 0}</p>
91
+ {total !== undefined && total > 0 && title !== "Total" && (
92
+ <span className="text-xs text-muted-foreground">
93
+ de {total}
94
+ </span>
95
+ )}
96
+ </div>
97
+ {title !== "Total" && (
98
+ <Progress value={pct} className="h-1" />
99
+ )}
100
+ </>
101
+ )}
102
+ </CardContent>
103
+ </Card>
104
+ );
105
+ }
106
+
107
+ function ChannelHealthCard({
108
+ channel,
109
+ config,
110
+ onClick,
111
+ }: {
112
+ channel: Channel;
113
+ config: GchatHookConfig;
114
+ onClick: () => void;
115
+ }) {
116
+ const { data: status } = useChannelWhatsappStatus(config, channel.id);
117
+
118
+ return (
119
+ <div
120
+ className="flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors hover:bg-accent"
121
+ onClick={onClick}
122
+ >
123
+ <div className="flex h-9 w-9 items-center justify-center rounded-md bg-green-500/10">
124
+ <WhatsappIcon className="h-5 w-5 text-green-600" />
125
+ </div>
126
+ <div className="flex-1 min-w-0">
127
+ <p className="text-sm font-medium truncate">{channel.name}</p>
128
+ <p className="text-xs text-muted-foreground truncate">
129
+ {channel.identifier || "Sem número"}
130
+ </p>
131
+ </div>
132
+ <WhatsappStatusBadge status={status} hasSession={!!channel.external_id} />
133
+ </div>
134
+ );
135
+ }
136
+
137
+ function RecentConversationItem({
138
+ inbox,
139
+ onClick,
140
+ }: {
141
+ inbox: Inbox;
142
+ onClick: () => void;
143
+ }) {
144
+ const timestamp = inbox.last_message_at || inbox.datetime_add;
145
+ const timeAgo = formatDistanceToNow(new Date(timestamp), {
146
+ addSuffix: true,
147
+ locale: ptBR,
148
+ });
149
+
150
+ const lastMessage =
151
+ inbox.last_message_content_type === "text"
152
+ ? inbox.last_message_content
153
+ : inbox.last_message_content_type
154
+ ? `[${inbox.last_message_content_type}]`
155
+ : null;
156
+
157
+ const directionPrefix =
158
+ inbox.last_message_direction === "outbound" ? "Você: " : "";
159
+
160
+ return (
161
+ <div
162
+ className="flex items-center gap-3 rounded-lg p-3 cursor-pointer transition-colors hover:bg-accent"
163
+ onClick={onClick}
164
+ >
165
+ <ContactAvatar name={inbox.contact_name || "?"} className="h-9 w-9" />
166
+ <div className="flex-1 min-w-0">
167
+ <div className="flex items-center justify-between gap-2">
168
+ <div className="flex items-center gap-2 min-w-0">
169
+ <span className="text-sm font-medium truncate">
170
+ {inbox.contact_name || "Desconhecido"}
171
+ </span>
172
+ {inbox.channel_name && (
173
+ <Badge variant="outline" className="shrink-0 text-[10px] px-1.5 py-0 text-muted-foreground">
174
+ {inbox.channel_name}
175
+ </Badge>
176
+ )}
177
+ </div>
178
+ <span className="text-xs text-muted-foreground shrink-0">
179
+ {timeAgo}
180
+ </span>
181
+ </div>
182
+ <div className="flex items-center justify-between gap-2 mt-0.5">
183
+ <p className="text-xs text-muted-foreground truncate">
184
+ {lastMessage ? `${directionPrefix}${lastMessage}` : "Sem mensagens"}
185
+ </p>
186
+ <Badge
187
+ variant="outline"
188
+ className={cn("shrink-0 text-[10px] px-1.5 py-0", statusColors[inbox.status])}
189
+ >
190
+ {statusLabels[inbox.status] || inbox.status}
191
+ </Badge>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ // --- Main Component ---
199
+
200
+ export interface ChatDashboardProps {
201
+ config: GchatHookConfig;
202
+ userName?: string;
203
+ onNavigateToInbox?: (filters?: { status?: string; channelId?: number }) => void;
204
+ onNavigateToChannels?: () => void;
205
+ renderExtraSection?: () => ReactNode;
206
+ }
207
+
208
+ export function ChatDashboard({
209
+ config,
210
+ userName,
211
+ onNavigateToInbox,
212
+ onNavigateToChannels,
213
+ renderExtraSection,
214
+ }: ChatDashboardProps) {
215
+ const [selectedChannelId, setSelectedChannelId] = useState<number | null>(null);
216
+
217
+ const { data: stats, isLoading: statsLoading } = useInboxStats(config);
218
+ const { data: inboxes, isLoading: inboxesLoading } = useInboxes(config);
219
+ const { data: channels, isLoading: channelsLoading } = useChannels(config);
220
+
221
+ const filteredInboxes = useMemo(() => {
222
+ if (!inboxes) return [];
223
+ if (!selectedChannelId) return inboxes;
224
+ return inboxes.filter((i) => i.id_channel === selectedChannelId);
225
+ }, [inboxes, selectedChannelId]);
226
+
227
+ const filteredStats = useMemo(() => {
228
+ if (!selectedChannelId) return stats;
229
+ const open = filteredInboxes.filter((i) => i.status === "open").length;
230
+ const pending = filteredInboxes.filter((i) => i.status === "pending").length;
231
+ const resolved = filteredInboxes.filter((i) => i.status === "resolved").length;
232
+ return { total: filteredInboxes.length, open, pending, resolved };
233
+ }, [stats, filteredInboxes, selectedChannelId]);
234
+
235
+ const recentInboxes = useMemo(() => {
236
+ return [...filteredInboxes]
237
+ .sort((a, b) => {
238
+ const da = a.last_message_at || a.datetime_add;
239
+ const db = b.last_message_at || b.datetime_add;
240
+ return new Date(db).getTime() - new Date(da).getTime();
241
+ })
242
+ .slice(0, 5);
243
+ }, [filteredInboxes]);
244
+
245
+ return (
246
+ <div className="flex flex-col gap-6 p-4 flex-1 min-h-0 overflow-y-auto">
247
+ {/* Greeting */}
248
+ <div>
249
+ <h1 className="text-xl font-semibold">
250
+ {getGreeting(userName)}
251
+ </h1>
252
+ <p className="text-sm text-muted-foreground">
253
+ Aqui está o resumo das suas conversas e canais.
254
+ </p>
255
+ </div>
256
+
257
+ {/* Channel Filter */}
258
+ <div className="flex items-center gap-2 flex-wrap">
259
+ <button
260
+ className={cn(
261
+ "inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors",
262
+ !selectedChannelId
263
+ ? "bg-primary text-primary-foreground border-primary"
264
+ : "bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground"
265
+ )}
266
+ onClick={() => setSelectedChannelId(null)}
267
+ >
268
+ Todos os canais
269
+ </button>
270
+ {channelsLoading ? (
271
+ <>
272
+ <Skeleton className="h-7 w-24 rounded-full" />
273
+ <Skeleton className="h-7 w-28 rounded-full" />
274
+ </>
275
+ ) : (
276
+ channels?.map((channel) => (
277
+ <button
278
+ key={channel.id}
279
+ className={cn(
280
+ "inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors",
281
+ selectedChannelId === channel.id
282
+ ? "bg-primary text-primary-foreground border-primary"
283
+ : "bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground"
284
+ )}
285
+ onClick={() => setSelectedChannelId(channel.id)}
286
+ >
287
+ <WhatsappIcon className="h-3 w-3" />
288
+ {channel.name}
289
+ </button>
290
+ ))
291
+ )}
292
+ </div>
293
+
294
+ {/* Stat Cards */}
295
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
296
+ <StatCard
297
+ title="Total"
298
+ value={filteredStats?.total}
299
+ total={filteredStats?.total}
300
+ icon={InboxIcon}
301
+ loading={statsLoading && !selectedChannelId}
302
+ accentColor="border-l-blue-500"
303
+ onClick={() => onNavigateToInbox?.()}
304
+ />
305
+ <StatCard
306
+ title="Abertas"
307
+ value={filteredStats?.open}
308
+ total={filteredStats?.total}
309
+ icon={MessageCircle}
310
+ loading={statsLoading && !selectedChannelId}
311
+ accentColor="border-l-green-500"
312
+ onClick={() => onNavigateToInbox?.({ status: "open" })}
313
+ />
314
+ <StatCard
315
+ title="Pendentes"
316
+ value={filteredStats?.pending}
317
+ total={filteredStats?.total}
318
+ icon={Clock}
319
+ loading={statsLoading && !selectedChannelId}
320
+ accentColor="border-l-yellow-500"
321
+ onClick={() => onNavigateToInbox?.({ status: "pending" })}
322
+ />
323
+ <StatCard
324
+ title="Resolvidas"
325
+ value={filteredStats?.resolved}
326
+ total={filteredStats?.total}
327
+ icon={CircleCheck}
328
+ loading={statsLoading && !selectedChannelId}
329
+ accentColor="border-l-zinc-400"
330
+ onClick={() => onNavigateToInbox?.({ status: "resolved" })}
331
+ />
332
+ </div>
333
+
334
+ {/* Channel Health */}
335
+ <Card>
336
+ <CardHeader className="flex flex-row items-center justify-between">
337
+ <CardTitle className="text-sm font-medium">
338
+ Saúde dos Canais
339
+ </CardTitle>
340
+ {onNavigateToChannels && (
341
+ <button
342
+ onClick={onNavigateToChannels}
343
+ className="text-xs text-primary hover:underline flex items-center gap-1"
344
+ >
345
+ Gerenciar
346
+ <ArrowRight className="h-3 w-3" />
347
+ </button>
348
+ )}
349
+ </CardHeader>
350
+ <CardContent>
351
+ {channelsLoading ? (
352
+ <div className="space-y-3">
353
+ {[1, 2].map((i) => (
354
+ <div key={i} className="flex items-center gap-3 p-3">
355
+ <Skeleton className="h-8 w-8 rounded" />
356
+ <div className="flex-1 space-y-1">
357
+ <Skeleton className="h-4 w-24" />
358
+ <Skeleton className="h-3 w-32" />
359
+ </div>
360
+ </div>
361
+ ))}
362
+ </div>
363
+ ) : !channels?.length ? (
364
+ <div className="flex items-center justify-center h-full text-sm text-muted-foreground py-8">
365
+ Nenhum canal configurado
366
+ </div>
367
+ ) : (
368
+ <div className="space-y-2">
369
+ {channels.map((channel) => (
370
+ <ChannelHealthCard
371
+ key={channel.id}
372
+ channel={channel}
373
+ config={config}
374
+ onClick={() => onNavigateToInbox?.({ channelId: channel.id })}
375
+ />
376
+ ))}
377
+ </div>
378
+ )}
379
+ </CardContent>
380
+ </Card>
381
+
382
+ {/* Extra Section (e.g., MetaAgents in admin) */}
383
+ {renderExtraSection?.()}
384
+
385
+ {/* Recent Conversations */}
386
+ <Card>
387
+ <CardHeader className="flex flex-row items-center justify-between">
388
+ <CardTitle className="text-sm font-medium">
389
+ Conversas Recentes
390
+ </CardTitle>
391
+ {onNavigateToInbox && (
392
+ <button
393
+ onClick={() => onNavigateToInbox()}
394
+ className="text-xs text-primary hover:underline flex items-center gap-1"
395
+ >
396
+ Ver todas
397
+ <ArrowRight className="h-3 w-3" />
398
+ </button>
399
+ )}
400
+ </CardHeader>
401
+ <CardContent className="p-0">
402
+ {inboxesLoading ? (
403
+ <div className="space-y-2 p-4">
404
+ {[1, 2, 3].map((i) => (
405
+ <div key={i} className="flex items-center gap-3 p-3">
406
+ <Skeleton className="h-9 w-9 rounded-full shrink-0" />
407
+ <div className="flex-1 space-y-1.5">
408
+ <Skeleton className="h-4 w-28" />
409
+ <Skeleton className="h-3 w-40" />
410
+ </div>
411
+ </div>
412
+ ))}
413
+ </div>
414
+ ) : recentInboxes.length === 0 ? (
415
+ <div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
416
+ Nenhuma conversa encontrada
417
+ </div>
418
+ ) : (
419
+ <div className="divide-y">
420
+ {recentInboxes.map((inbox) => (
421
+ <RecentConversationItem
422
+ key={inbox.id}
423
+ inbox={inbox}
424
+ onClick={() => onNavigateToInbox?.()}
425
+ />
426
+ ))}
427
+ </div>
428
+ )}
429
+ </CardContent>
430
+ </Card>
431
+ </div>
432
+ );
433
+ }
@@ -1,7 +1,6 @@
1
1
  import { useState, useRef } from "react";
2
2
  import { Send } from "lucide-react";
3
- import { Button } from "./ui/button";
4
- import { Textarea } from "./ui/textarea";
3
+ import { Button, Textarea } from "@greatapps/greatauth-ui/ui";
5
4
 
6
5
  export interface ChatInputProps {
7
6
  onSend: (content: string) => void;
@@ -4,14 +4,7 @@ import { groupMessagesByDate } from "../utils/group-messages";
4
4
  import { formatDateGroup } from "../utils/format-date";
5
5
  import { MessageBubble } from "./message-bubble";
6
6
  import { ChatInput } from "./chat-input";
7
- import { Skeleton } from "./ui/skeleton";
8
- import {
9
- Select,
10
- SelectContent,
11
- SelectItem,
12
- SelectTrigger,
13
- SelectValue,
14
- } from "./ui/select";
7
+ import { Skeleton, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@greatapps/greatauth-ui/ui";
15
8
 
16
9
  export interface ChatViewProps {
17
10
  messages: InboxMessage[];
@@ -1,5 +1,4 @@
1
- import { Avatar, AvatarFallback } from "./ui/avatar";
2
- import { cn } from "../lib/utils";
1
+ import { Avatar, AvatarFallback, cn } from "@greatapps/greatauth-ui/ui";
3
2
 
4
3
  const COLORS = [
5
4
  "bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300",
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import type { Contact } from "../types";
5
+ import type { GchatHookConfig } from "../hooks/types";
6
+ import { useCreateContact, useUpdateContact } from "../hooks/use-contacts";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogFooter,
13
+ Button,
14
+ Input,
15
+ Label,
16
+ } from "@greatapps/greatauth-ui/ui";
17
+ import { Loader2 } from "lucide-react";
18
+ import { toast } from "sonner";
19
+
20
+ export interface ContactFormDialogProps {
21
+ open: boolean;
22
+ onOpenChange: (open: boolean) => void;
23
+ contact?: Contact;
24
+ config: GchatHookConfig;
25
+ }
26
+
27
+ export function ContactFormDialog({
28
+ open,
29
+ onOpenChange,
30
+ contact,
31
+ config,
32
+ }: ContactFormDialogProps) {
33
+ const isEditing = !!contact;
34
+ const createContact = useCreateContact(config);
35
+ const updateContact = useUpdateContact(config);
36
+
37
+ const [name, setName] = useState("");
38
+ const [phoneNumber, setPhoneNumber] = useState("");
39
+ const [identifier, setIdentifier] = useState("");
40
+
41
+ useEffect(() => {
42
+ if (contact) {
43
+ setName(contact.name);
44
+ setPhoneNumber(contact.phone_number || "");
45
+ setIdentifier(contact.identifier || "");
46
+ } else {
47
+ setName("");
48
+ setPhoneNumber("");
49
+ setIdentifier("");
50
+ }
51
+ }, [contact, open]);
52
+
53
+ const isPending = createContact.isPending || updateContact.isPending;
54
+
55
+ async function handleSubmit(e: React.FormEvent) {
56
+ e.preventDefault();
57
+ if (!name.trim()) return;
58
+
59
+ const body = {
60
+ name: name.trim(),
61
+ phone_number: phoneNumber.trim() || undefined,
62
+ identifier: identifier.trim() || undefined,
63
+ };
64
+
65
+ try {
66
+ if (isEditing) {
67
+ await updateContact.mutateAsync({ id: contact.id, body });
68
+ toast.success("Contato atualizado");
69
+ } else {
70
+ await createContact.mutateAsync(body);
71
+ toast.success("Contato criado");
72
+ }
73
+ onOpenChange(false);
74
+ } catch {
75
+ toast.error(isEditing ? "Erro ao atualizar" : "Erro ao criar contato");
76
+ }
77
+ }
78
+
79
+ return (
80
+ <Dialog open={open} onOpenChange={onOpenChange}>
81
+ <DialogContent>
82
+ <DialogHeader>
83
+ <DialogTitle>
84
+ {isEditing ? "Editar Contato" : "Novo Contato"}
85
+ </DialogTitle>
86
+ </DialogHeader>
87
+ <form onSubmit={handleSubmit} className="space-y-4">
88
+ <div className="space-y-2">
89
+ <Label htmlFor="contact-name">Nome *</Label>
90
+ <Input
91
+ id="contact-name"
92
+ value={name}
93
+ onChange={(e) => setName(e.target.value)}
94
+ placeholder="Nome do contato"
95
+ required
96
+ disabled={isPending}
97
+ />
98
+ </div>
99
+ <div className="space-y-2">
100
+ <Label htmlFor="contact-phone">Telefone</Label>
101
+ <Input
102
+ id="contact-phone"
103
+ value={phoneNumber}
104
+ onChange={(e) => setPhoneNumber(e.target.value)}
105
+ placeholder="5511999999999"
106
+ disabled={isPending}
107
+ />
108
+ </div>
109
+ <div className="space-y-2">
110
+ <Label htmlFor="contact-identifier">Identificador (WhatsApp ID)</Label>
111
+ <Input
112
+ id="contact-identifier"
113
+ value={identifier}
114
+ onChange={(e) => setIdentifier(e.target.value)}
115
+ placeholder="5511999999999@s.whatsapp.net"
116
+ disabled={isPending}
117
+ />
118
+ </div>
119
+ <DialogFooter>
120
+ <Button
121
+ type="button"
122
+ variant="outline"
123
+ onClick={() => onOpenChange(false)}
124
+ disabled={isPending}
125
+ >
126
+ Cancelar
127
+ </Button>
128
+ <Button type="submit" disabled={isPending || !name.trim()}>
129
+ {isPending ? (
130
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
131
+ ) : null}
132
+ {isEditing ? "Salvar" : "Criar"}
133
+ </Button>
134
+ </DialogFooter>
135
+ </form>
136
+ </DialogContent>
137
+ </Dialog>
138
+ );
139
+ }
@@ -1,8 +1,6 @@
1
1
  import type { Contact } from "../types";
2
2
  import { ContactAvatar } from "./contact-avatar";
3
- import { Button } from "./ui/button";
4
- import { Skeleton } from "./ui/skeleton";
5
- import { Separator } from "./ui/separator";
3
+ import { Button, Skeleton, Separator, cn } from "@greatapps/greatauth-ui/ui";
6
4
  import {
7
5
  X,
8
6
  Phone,
@@ -13,7 +11,6 @@ import {
13
11
  } from "lucide-react";
14
12
  import { format } from "date-fns";
15
13
  import { ptBR } from "date-fns/locale";
16
- import { cn } from "../lib/utils";
17
14
 
18
15
  function InfoRow({
19
16
  icon: Icon,
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { GchatHookConfig } from "../hooks/types";
5
+ import { ContactsTable } from "./contacts-table";
6
+ import { ContactFormDialog } from "./contact-form-dialog";
7
+ import { Button } from "@greatapps/greatauth-ui/ui";
8
+ import { Plus } from "lucide-react";
9
+
10
+ export interface ContactsPageProps {
11
+ config: GchatHookConfig;
12
+ }
13
+
14
+ export function ContactsPage({ config }: ContactsPageProps) {
15
+ const [createOpen, setCreateOpen] = useState(false);
16
+
17
+ return (
18
+ <div className="flex flex-col gap-6 p-4 flex-1 min-h-0 overflow-y-auto">
19
+ <div className="flex items-center justify-between">
20
+ <div>
21
+ <h1 className="text-xl font-semibold">Contatos</h1>
22
+ <p className="text-sm text-muted-foreground">
23
+ Gerencie seus contatos de conversas
24
+ </p>
25
+ </div>
26
+ <Button onClick={() => setCreateOpen(true)}>
27
+ <Plus className="mr-2 h-4 w-4" />
28
+ Novo Contato
29
+ </Button>
30
+ </div>
31
+
32
+ <ContactsTable config={config} />
33
+
34
+ <ContactFormDialog
35
+ open={createOpen}
36
+ onOpenChange={setCreateOpen}
37
+ config={config}
38
+ />
39
+ </div>
40
+ );
41
+ }