@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.
- package/dist/index.d.ts +106 -5
- package/dist/index.js +1787 -1116
- package/dist/index.js.map +1 -1
- package/package.json +17 -13
- package/src/components/channel-card.tsx +1 -1
- 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/chat-input.tsx +1 -2
- package/src/components/chat-view.tsx +1 -8
- package/src/components/contact-avatar.tsx +1 -2
- package/src/components/contact-form-dialog.tsx +139 -0
- package/src/components/contact-info-panel.tsx +1 -4
- package/src/components/contacts-page.tsx +41 -0
- package/src/components/contacts-table.tsx +216 -0
- package/src/components/data-table.tsx +185 -0
- package/src/components/inbox-item.tsx +6 -4
- package/src/components/inbox-page.tsx +167 -0
- package/src/components/inbox-sidebar.tsx +1 -5
- package/src/components/message-bubble.tsx +4 -6
- package/src/components/new-conversation-dialog.tsx +2 -6
- package/src/components/whatsapp-icon.tsx +21 -0
- package/src/components/whatsapp-qr-dialog.tsx +147 -0
- package/src/components/whatsapp-status-badge.tsx +1 -1
- package/src/index.ts +37 -2
- package/src/components/ui/alert-dialog.tsx +0 -167
- package/src/components/ui/avatar.tsx +0 -51
- package/src/components/ui/badge.tsx +0 -44
- package/src/components/ui/button.tsx +0 -62
- package/src/components/ui/command.tsx +0 -106
- package/src/components/ui/dialog.tsx +0 -133
- package/src/components/ui/dropdown-menu.tsx +0 -173
- package/src/components/ui/input.tsx +0 -19
- package/src/components/ui/scroll-area.tsx +0 -50
- package/src/components/ui/select.tsx +0 -156
- package/src/components/ui/separator.tsx +0 -26
- package/src/components/ui/skeleton.tsx +0 -16
- package/src/components/ui/tabs.tsx +0 -64
- package/src/components/ui/textarea.tsx +0 -18
- package/src/components/ui/tooltip.tsx +0 -58
- package/src/lib/utils.ts +0 -6
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import type { ColumnDef } from "@tanstack/react-table";
|
|
5
|
+
import type { Contact } from "../types";
|
|
6
|
+
import type { GchatHookConfig } from "../hooks/types";
|
|
7
|
+
import { useContacts, useDeleteContact } from "../hooks/use-contacts";
|
|
8
|
+
import { ContactFormDialog } from "./contact-form-dialog";
|
|
9
|
+
import { DataTable } from "./data-table";
|
|
10
|
+
import {
|
|
11
|
+
Input,
|
|
12
|
+
Tooltip,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
TooltipContent,
|
|
15
|
+
AlertDialog,
|
|
16
|
+
AlertDialogAction,
|
|
17
|
+
AlertDialogCancel,
|
|
18
|
+
AlertDialogContent,
|
|
19
|
+
AlertDialogDescription,
|
|
20
|
+
AlertDialogFooter,
|
|
21
|
+
AlertDialogHeader,
|
|
22
|
+
AlertDialogTitle,
|
|
23
|
+
Button,
|
|
24
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
25
|
+
import { Pencil, Trash2, Search } from "lucide-react";
|
|
26
|
+
import { format } from "date-fns";
|
|
27
|
+
import { ptBR } from "date-fns/locale";
|
|
28
|
+
import { toast } from "sonner";
|
|
29
|
+
|
|
30
|
+
function useColumns(
|
|
31
|
+
onEdit: (contact: Contact) => void,
|
|
32
|
+
onDelete: (id: number) => void,
|
|
33
|
+
): ColumnDef<Contact>[] {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
accessorKey: "name",
|
|
37
|
+
header: "Nome",
|
|
38
|
+
cell: ({ row }) => (
|
|
39
|
+
<span className="font-medium">{row.original.name}</span>
|
|
40
|
+
),
|
|
41
|
+
sortingFn: (rowA, rowB) =>
|
|
42
|
+
rowA.original.name.toLowerCase().localeCompare(rowB.original.name.toLowerCase()),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
accessorKey: "phone_number",
|
|
46
|
+
header: "Telefone",
|
|
47
|
+
cell: ({ row }) => row.original.phone_number || "—",
|
|
48
|
+
sortingFn: (rowA, rowB) => {
|
|
49
|
+
const a = rowA.original.phone_number || "";
|
|
50
|
+
const b = rowB.original.phone_number || "";
|
|
51
|
+
return a.localeCompare(b);
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
accessorKey: "identifier",
|
|
56
|
+
header: "Identificador",
|
|
57
|
+
cell: ({ row }) => (
|
|
58
|
+
<span className="text-muted-foreground text-xs font-mono">
|
|
59
|
+
{row.original.identifier || "—"}
|
|
60
|
+
</span>
|
|
61
|
+
),
|
|
62
|
+
sortingFn: (rowA, rowB) => {
|
|
63
|
+
const a = rowA.original.identifier || "";
|
|
64
|
+
const b = rowB.original.identifier || "";
|
|
65
|
+
return a.localeCompare(b);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
accessorKey: "datetime_add",
|
|
70
|
+
header: "Criado em",
|
|
71
|
+
cell: ({ row }) => (
|
|
72
|
+
<span className="text-muted-foreground text-sm">
|
|
73
|
+
{format(new Date(row.original.datetime_add), "dd/MM/yyyy", { locale: ptBR })}
|
|
74
|
+
</span>
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "actions",
|
|
79
|
+
size: 80,
|
|
80
|
+
enableSorting: false,
|
|
81
|
+
cell: ({ row }) => (
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
<Tooltip>
|
|
84
|
+
<TooltipTrigger asChild>
|
|
85
|
+
<Button
|
|
86
|
+
variant="ghost"
|
|
87
|
+
size="icon"
|
|
88
|
+
className="h-8 w-8"
|
|
89
|
+
onClick={() => onEdit(row.original)}
|
|
90
|
+
>
|
|
91
|
+
<Pencil className="h-4 w-4" />
|
|
92
|
+
</Button>
|
|
93
|
+
</TooltipTrigger>
|
|
94
|
+
<TooltipContent>Editar</TooltipContent>
|
|
95
|
+
</Tooltip>
|
|
96
|
+
<Tooltip>
|
|
97
|
+
<TooltipTrigger asChild>
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="icon"
|
|
101
|
+
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
102
|
+
onClick={() => onDelete(row.original.id)}
|
|
103
|
+
>
|
|
104
|
+
<Trash2 className="h-4 w-4" />
|
|
105
|
+
</Button>
|
|
106
|
+
</TooltipTrigger>
|
|
107
|
+
<TooltipContent>Excluir</TooltipContent>
|
|
108
|
+
</Tooltip>
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ContactsTableProps {
|
|
116
|
+
config: GchatHookConfig;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function ContactsTable({ config }: ContactsTableProps) {
|
|
120
|
+
const [search, setSearch] = useState("");
|
|
121
|
+
const [page, setPage] = useState(1);
|
|
122
|
+
|
|
123
|
+
const queryParams = useMemo(() => {
|
|
124
|
+
const params: Record<string, string> = {
|
|
125
|
+
limit: "15",
|
|
126
|
+
page: String(page),
|
|
127
|
+
};
|
|
128
|
+
if (search) {
|
|
129
|
+
params.search = search;
|
|
130
|
+
}
|
|
131
|
+
return params;
|
|
132
|
+
}, [search, page]);
|
|
133
|
+
|
|
134
|
+
const { data, isLoading } = useContacts(config, queryParams);
|
|
135
|
+
const deleteContact = useDeleteContact(config);
|
|
136
|
+
const [editContact, setEditContact] = useState<Contact | null>(null);
|
|
137
|
+
const [deleteId, setDeleteId] = useState<number | null>(null);
|
|
138
|
+
|
|
139
|
+
const contacts = data?.data || [];
|
|
140
|
+
const total = data?.total || 0;
|
|
141
|
+
|
|
142
|
+
const columns = useColumns(
|
|
143
|
+
(contact) => setEditContact(contact),
|
|
144
|
+
(id) => setDeleteId(id),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
function handleDelete() {
|
|
148
|
+
if (!deleteId) return;
|
|
149
|
+
deleteContact.mutate(deleteId, {
|
|
150
|
+
onSuccess: () => {
|
|
151
|
+
toast.success("Contato excluído");
|
|
152
|
+
setDeleteId(null);
|
|
153
|
+
},
|
|
154
|
+
onError: () => toast.error("Erro ao excluir contato"),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleSearchChange(value: string) {
|
|
159
|
+
setSearch(value);
|
|
160
|
+
setPage(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
<div className="flex items-center gap-3">
|
|
166
|
+
<div className="relative flex-1 max-w-md">
|
|
167
|
+
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
168
|
+
<Input
|
|
169
|
+
placeholder="Buscar por nome, telefone ou identificador..."
|
|
170
|
+
value={search}
|
|
171
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
172
|
+
className="pl-9"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<DataTable
|
|
178
|
+
columns={columns}
|
|
179
|
+
data={contacts}
|
|
180
|
+
isLoading={isLoading}
|
|
181
|
+
emptyMessage="Nenhum contato encontrado"
|
|
182
|
+
total={total}
|
|
183
|
+
page={page}
|
|
184
|
+
onPageChange={setPage}
|
|
185
|
+
pageSize={15}
|
|
186
|
+
/>
|
|
187
|
+
|
|
188
|
+
<ContactFormDialog
|
|
189
|
+
open={!!editContact}
|
|
190
|
+
onOpenChange={(open) => !open && setEditContact(null)}
|
|
191
|
+
contact={editContact ?? undefined}
|
|
192
|
+
config={config}
|
|
193
|
+
/>
|
|
194
|
+
|
|
195
|
+
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
|
196
|
+
<AlertDialogContent>
|
|
197
|
+
<AlertDialogHeader>
|
|
198
|
+
<AlertDialogTitle>Excluir contato?</AlertDialogTitle>
|
|
199
|
+
<AlertDialogDescription>
|
|
200
|
+
Esta ação não pode ser desfeita. O contato será removido permanentemente.
|
|
201
|
+
</AlertDialogDescription>
|
|
202
|
+
</AlertDialogHeader>
|
|
203
|
+
<AlertDialogFooter>
|
|
204
|
+
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
205
|
+
<AlertDialogAction
|
|
206
|
+
onClick={handleDelete}
|
|
207
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
208
|
+
>
|
|
209
|
+
Excluir
|
|
210
|
+
</AlertDialogAction>
|
|
211
|
+
</AlertDialogFooter>
|
|
212
|
+
</AlertDialogContent>
|
|
213
|
+
</AlertDialog>
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ColumnDef,
|
|
5
|
+
type SortingState,
|
|
6
|
+
flexRender,
|
|
7
|
+
getCoreRowModel,
|
|
8
|
+
getSortedRowModel,
|
|
9
|
+
useReactTable,
|
|
10
|
+
} from "@tanstack/react-table";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import {
|
|
13
|
+
Table,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
TableHead,
|
|
17
|
+
TableHeader,
|
|
18
|
+
TableRow,
|
|
19
|
+
Button,
|
|
20
|
+
Skeleton,
|
|
21
|
+
cn,
|
|
22
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
23
|
+
import { ChevronLeft, ChevronRight, ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
|
|
24
|
+
|
|
25
|
+
export interface DataTableProps<TData, TValue> {
|
|
26
|
+
columns: ColumnDef<TData, TValue>[];
|
|
27
|
+
data: TData[];
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
emptyMessage?: string;
|
|
30
|
+
total?: number;
|
|
31
|
+
page?: number;
|
|
32
|
+
onPageChange?: (page: number) => void;
|
|
33
|
+
pageSize?: number;
|
|
34
|
+
onRowClick?: (row: TData) => void;
|
|
35
|
+
selectedRowId?: string | number | null;
|
|
36
|
+
getRowId?: (row: TData) => string | number;
|
|
37
|
+
compact?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DataTable<TData, TValue>({
|
|
41
|
+
columns,
|
|
42
|
+
data,
|
|
43
|
+
isLoading,
|
|
44
|
+
emptyMessage = "Nenhum registro encontrado",
|
|
45
|
+
total,
|
|
46
|
+
page = 1,
|
|
47
|
+
onPageChange,
|
|
48
|
+
pageSize = 15,
|
|
49
|
+
onRowClick,
|
|
50
|
+
selectedRowId,
|
|
51
|
+
getRowId,
|
|
52
|
+
compact,
|
|
53
|
+
}: DataTableProps<TData, TValue>) {
|
|
54
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
55
|
+
|
|
56
|
+
const table = useReactTable({
|
|
57
|
+
data,
|
|
58
|
+
columns,
|
|
59
|
+
getCoreRowModel: getCoreRowModel(),
|
|
60
|
+
getSortedRowModel: getSortedRowModel(),
|
|
61
|
+
onSortingChange: setSorting,
|
|
62
|
+
state: { sorting },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const totalPages = total ? Math.ceil(total / pageSize) : 1;
|
|
66
|
+
const showPagination = onPageChange && total && total > pageSize;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="space-y-4">
|
|
70
|
+
<div className="rounded-md border">
|
|
71
|
+
<Table>
|
|
72
|
+
<TableHeader>
|
|
73
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
74
|
+
<TableRow key={headerGroup.id}>
|
|
75
|
+
{headerGroup.headers.map((header) => (
|
|
76
|
+
<TableHead
|
|
77
|
+
key={header.id}
|
|
78
|
+
className={cn(compact && "py-1.5 text-xs")}
|
|
79
|
+
style={{ width: header.getSize() !== 150 ? header.getSize() : undefined }}
|
|
80
|
+
>
|
|
81
|
+
{header.isPlaceholder ? null : header.column.getCanSort() ? (
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
className="flex items-center gap-1 hover:text-foreground -ml-1 px-1 py-0.5 rounded cursor-pointer select-none"
|
|
85
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
86
|
+
>
|
|
87
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
88
|
+
{{
|
|
89
|
+
asc: <ArrowUp className="h-3.5 w-3.5" />,
|
|
90
|
+
desc: <ArrowDown className="h-3.5 w-3.5" />,
|
|
91
|
+
}[header.column.getIsSorted() as string] ?? (
|
|
92
|
+
<ArrowUpDown className="h-3.5 w-3.5 text-muted-foreground/50" />
|
|
93
|
+
)}
|
|
94
|
+
</button>
|
|
95
|
+
) : (
|
|
96
|
+
flexRender(header.column.columnDef.header, header.getContext())
|
|
97
|
+
)}
|
|
98
|
+
</TableHead>
|
|
99
|
+
))}
|
|
100
|
+
</TableRow>
|
|
101
|
+
))}
|
|
102
|
+
</TableHeader>
|
|
103
|
+
<TableBody>
|
|
104
|
+
{isLoading ? (
|
|
105
|
+
Array.from({ length: compact ? 3 : 5 }).map((_, i) => (
|
|
106
|
+
<TableRow key={i}>
|
|
107
|
+
{columns.map((_, j) => (
|
|
108
|
+
<TableCell key={j} className={cn(compact && "py-1.5")}>
|
|
109
|
+
<Skeleton className="h-4 w-full max-w-[120px]" />
|
|
110
|
+
</TableCell>
|
|
111
|
+
))}
|
|
112
|
+
</TableRow>
|
|
113
|
+
))
|
|
114
|
+
) : table.getRowModel().rows.length === 0 ? (
|
|
115
|
+
<TableRow>
|
|
116
|
+
<TableCell
|
|
117
|
+
colSpan={columns.length}
|
|
118
|
+
className={cn(
|
|
119
|
+
"text-center text-muted-foreground",
|
|
120
|
+
compact ? "py-4" : "py-8",
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{emptyMessage}
|
|
124
|
+
</TableCell>
|
|
125
|
+
</TableRow>
|
|
126
|
+
) : (
|
|
127
|
+
table.getRowModel().rows.map((row) => {
|
|
128
|
+
const rowId = getRowId ? getRowId(row.original) : undefined;
|
|
129
|
+
const isSelected = selectedRowId != null && rowId != null && rowId === selectedRowId;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<TableRow
|
|
133
|
+
key={row.id}
|
|
134
|
+
className={cn(
|
|
135
|
+
onRowClick && "cursor-pointer",
|
|
136
|
+
isSelected && "bg-accent",
|
|
137
|
+
)}
|
|
138
|
+
onClick={() => onRowClick?.(row.original)}
|
|
139
|
+
>
|
|
140
|
+
{row.getVisibleCells().map((cell) => (
|
|
141
|
+
<TableCell key={cell.id} className={cn(compact && "py-1.5")}>
|
|
142
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
143
|
+
</TableCell>
|
|
144
|
+
))}
|
|
145
|
+
</TableRow>
|
|
146
|
+
);
|
|
147
|
+
})
|
|
148
|
+
)}
|
|
149
|
+
</TableBody>
|
|
150
|
+
</Table>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{showPagination && (
|
|
154
|
+
<div className="flex items-center justify-between px-2">
|
|
155
|
+
<p className="text-sm text-muted-foreground">
|
|
156
|
+
{total} registro{total !== 1 ? "s" : ""}
|
|
157
|
+
</p>
|
|
158
|
+
<div className="flex items-center gap-2">
|
|
159
|
+
<Button
|
|
160
|
+
variant="outline"
|
|
161
|
+
size="sm"
|
|
162
|
+
onClick={() => onPageChange(page - 1)}
|
|
163
|
+
disabled={page <= 1}
|
|
164
|
+
>
|
|
165
|
+
<ChevronLeft className="h-4 w-4" />
|
|
166
|
+
Anterior
|
|
167
|
+
</Button>
|
|
168
|
+
<span className="text-sm text-muted-foreground">
|
|
169
|
+
{page} de {totalPages}
|
|
170
|
+
</span>
|
|
171
|
+
<Button
|
|
172
|
+
variant="outline"
|
|
173
|
+
size="sm"
|
|
174
|
+
onClick={() => onPageChange(page + 1)}
|
|
175
|
+
disabled={page >= totalPages}
|
|
176
|
+
>
|
|
177
|
+
Próximo
|
|
178
|
+
<ChevronRight className="h-4 w-4" />
|
|
179
|
+
</Button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { Inbox } from "../types";
|
|
3
3
|
import { ContactAvatar } from "./contact-avatar";
|
|
4
|
-
import { Badge } from "./ui/badge";
|
|
5
|
-
import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip";
|
|
6
4
|
import {
|
|
5
|
+
Badge,
|
|
6
|
+
Tooltip,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
TooltipContent,
|
|
7
9
|
AlertDialog,
|
|
8
10
|
AlertDialogAction,
|
|
9
11
|
AlertDialogCancel,
|
|
@@ -12,11 +14,11 @@ import {
|
|
|
12
14
|
AlertDialogFooter,
|
|
13
15
|
AlertDialogHeader,
|
|
14
16
|
AlertDialogTitle,
|
|
15
|
-
|
|
17
|
+
cn,
|
|
18
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
16
19
|
import { formatDistanceToNow } from "date-fns";
|
|
17
20
|
import { ptBR } from "date-fns/locale";
|
|
18
21
|
import { Trash2 } from "lucide-react";
|
|
19
|
-
import { cn } from "../lib/utils";
|
|
20
22
|
|
|
21
23
|
const statusColors: Record<string, string> = {
|
|
22
24
|
open: "bg-green-500/10 text-green-600 border-green-200",
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import type { Inbox, InboxMessage } from "../types";
|
|
5
|
+
import type { GchatHookConfig } from "../hooks/types";
|
|
6
|
+
import {
|
|
7
|
+
useInboxes,
|
|
8
|
+
useInboxStats,
|
|
9
|
+
useUpdateInbox,
|
|
10
|
+
useDeleteInbox,
|
|
11
|
+
} from "../hooks/use-inboxes";
|
|
12
|
+
import {
|
|
13
|
+
useInboxMessages,
|
|
14
|
+
useSendMessage,
|
|
15
|
+
useRetryMessage,
|
|
16
|
+
useRevokeMessage,
|
|
17
|
+
useEditMessage,
|
|
18
|
+
} from "../hooks/use-inbox-messages";
|
|
19
|
+
import { useGetContact } from "../hooks/use-contacts";
|
|
20
|
+
import { InboxSidebar } from "./inbox-sidebar";
|
|
21
|
+
import { ChatView } from "./chat-view";
|
|
22
|
+
import { ContactInfoPanel } from "./contact-info-panel";
|
|
23
|
+
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@greatapps/greatauth-ui/ui";
|
|
24
|
+
import { MessageCircle, User } from "lucide-react";
|
|
25
|
+
|
|
26
|
+
export interface InboxPageProps {
|
|
27
|
+
config: GchatHookConfig;
|
|
28
|
+
filterChannelId?: number | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function InboxPage({ config, filterChannelId }: InboxPageProps) {
|
|
32
|
+
const [selectedInbox, setSelectedInbox] = useState<Inbox | null>(null);
|
|
33
|
+
const [showContactPanel, setShowContactPanel] = useState(false);
|
|
34
|
+
|
|
35
|
+
const { data: inboxes, isLoading: inboxesLoading } = useInboxes(config);
|
|
36
|
+
const { data: messages = [], isLoading: messagesLoading } =
|
|
37
|
+
useInboxMessages(config, selectedInbox?.id ?? null);
|
|
38
|
+
const { data: contact, isLoading: contactLoading } = useGetContact(
|
|
39
|
+
config,
|
|
40
|
+
showContactPanel && selectedInbox?.id_contact
|
|
41
|
+
? selectedInbox.id_contact
|
|
42
|
+
: null,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const sendMessage = useSendMessage(config);
|
|
46
|
+
const retryMessage = useRetryMessage(config);
|
|
47
|
+
const revokeMessage = useRevokeMessage(config);
|
|
48
|
+
const editMessage = useEditMessage(config);
|
|
49
|
+
const updateInbox = useUpdateInbox(config);
|
|
50
|
+
const deleteInbox = useDeleteInbox(config);
|
|
51
|
+
|
|
52
|
+
const handleSend = useCallback(
|
|
53
|
+
(content: string) => {
|
|
54
|
+
if (!selectedInbox) return;
|
|
55
|
+
sendMessage.mutate({ idInbox: selectedInbox.id, content });
|
|
56
|
+
},
|
|
57
|
+
[selectedInbox, sendMessage],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleStatusChange = useCallback(
|
|
61
|
+
(status: "open" | "pending" | "resolved") => {
|
|
62
|
+
if (!selectedInbox) return;
|
|
63
|
+
updateInbox.mutate({ id: selectedInbox.id, body: { status } });
|
|
64
|
+
},
|
|
65
|
+
[selectedInbox, updateInbox],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const handleRevoke = useCallback(
|
|
69
|
+
(message: InboxMessage) => {
|
|
70
|
+
if (!selectedInbox) return;
|
|
71
|
+
revokeMessage.mutate({ id: message.id, idInbox: selectedInbox.id });
|
|
72
|
+
},
|
|
73
|
+
[selectedInbox, revokeMessage],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const handleEdit = useCallback(
|
|
77
|
+
(message: InboxMessage, newContent: string) => {
|
|
78
|
+
if (!selectedInbox) return;
|
|
79
|
+
editMessage.mutate({
|
|
80
|
+
id: message.id,
|
|
81
|
+
idInbox: selectedInbox.id,
|
|
82
|
+
content: newContent,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
[selectedInbox, editMessage],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="flex h-full">
|
|
90
|
+
<div className="w-[360px] shrink-0 border-r flex flex-col">
|
|
91
|
+
<InboxSidebar
|
|
92
|
+
inboxes={inboxes}
|
|
93
|
+
isLoading={inboxesLoading}
|
|
94
|
+
selectedInboxId={selectedInbox?.id ?? null}
|
|
95
|
+
onSelectInbox={(inbox) => {
|
|
96
|
+
setSelectedInbox(inbox);
|
|
97
|
+
setShowContactPanel(false);
|
|
98
|
+
}}
|
|
99
|
+
onDeleteInbox={(id) => deleteInbox.mutate(id)}
|
|
100
|
+
filterChannelId={filterChannelId}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="flex-1 flex min-w-0">
|
|
105
|
+
{selectedInbox ? (
|
|
106
|
+
<>
|
|
107
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
108
|
+
<ChatView
|
|
109
|
+
messages={messages}
|
|
110
|
+
isLoading={messagesLoading}
|
|
111
|
+
onSend={handleSend}
|
|
112
|
+
onRetry={retryMessage}
|
|
113
|
+
onRevoke={handleRevoke}
|
|
114
|
+
onEdit={handleEdit}
|
|
115
|
+
renderHeader={
|
|
116
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
117
|
+
<span className="text-sm font-medium">
|
|
118
|
+
{selectedInbox.contact_name || "Conversa"}
|
|
119
|
+
</span>
|
|
120
|
+
<div className="flex items-center gap-2">
|
|
121
|
+
<Select
|
|
122
|
+
value={selectedInbox.status}
|
|
123
|
+
onValueChange={handleStatusChange}
|
|
124
|
+
>
|
|
125
|
+
<SelectTrigger className="w-[130px] h-8 text-xs">
|
|
126
|
+
<SelectValue placeholder="Status" />
|
|
127
|
+
</SelectTrigger>
|
|
128
|
+
<SelectContent>
|
|
129
|
+
<SelectItem value="open">Aberta</SelectItem>
|
|
130
|
+
<SelectItem value="pending">Pendente</SelectItem>
|
|
131
|
+
<SelectItem value="resolved">Resolvida</SelectItem>
|
|
132
|
+
</SelectContent>
|
|
133
|
+
</Select>
|
|
134
|
+
{selectedInbox.id_contact && (
|
|
135
|
+
<Button
|
|
136
|
+
variant="ghost"
|
|
137
|
+
size="icon"
|
|
138
|
+
className="h-8 w-8"
|
|
139
|
+
onClick={() => setShowContactPanel((v) => !v)}
|
|
140
|
+
>
|
|
141
|
+
<User className="h-4 w-4" />
|
|
142
|
+
</Button>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{showContactPanel && selectedInbox.id_contact && (
|
|
151
|
+
<ContactInfoPanel
|
|
152
|
+
contact={contact ?? null}
|
|
153
|
+
isLoading={contactLoading}
|
|
154
|
+
onClose={() => setShowContactPanel(false)}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
</>
|
|
158
|
+
) : (
|
|
159
|
+
<div className="flex-1 flex flex-col items-center justify-center text-muted-foreground gap-3">
|
|
160
|
+
<MessageCircle className="h-12 w-12 opacity-20" />
|
|
161
|
+
<p className="text-sm">Selecione uma conversa para começar</p>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { useState, useMemo } from "react";
|
|
2
2
|
import type { Inbox } from "../types";
|
|
3
3
|
import { InboxItem } from "./inbox-item";
|
|
4
|
-
import { Input } from "
|
|
5
|
-
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
|
6
|
-
import { Button } from "./ui/button";
|
|
7
|
-
import { Skeleton } from "./ui/skeleton";
|
|
8
|
-
import { ScrollArea } from "./ui/scroll-area";
|
|
4
|
+
import { Input, Tabs, TabsList, TabsTrigger, Button, Skeleton, ScrollArea } from "@greatapps/greatauth-ui/ui";
|
|
9
5
|
import { Search, Plus, Inbox as InboxIcon } from "lucide-react";
|
|
10
6
|
|
|
11
7
|
const STATUS_TABS = [
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { InboxMessage } from "../types";
|
|
3
|
-
import { cn } from "../lib/utils";
|
|
4
3
|
import { formatMessageTime } from "../utils/format-date";
|
|
5
4
|
import {
|
|
6
5
|
Check,
|
|
@@ -18,12 +17,11 @@ import {
|
|
|
18
17
|
Ban,
|
|
19
18
|
} from "lucide-react";
|
|
20
19
|
import {
|
|
20
|
+
cn,
|
|
21
21
|
DropdownMenu,
|
|
22
22
|
DropdownMenuContent,
|
|
23
23
|
DropdownMenuItem,
|
|
24
24
|
DropdownMenuTrigger,
|
|
25
|
-
} from "./ui/dropdown-menu";
|
|
26
|
-
import {
|
|
27
25
|
AlertDialog,
|
|
28
26
|
AlertDialogAction,
|
|
29
27
|
AlertDialogCancel,
|
|
@@ -32,9 +30,9 @@ import {
|
|
|
32
30
|
AlertDialogFooter,
|
|
33
31
|
AlertDialogHeader,
|
|
34
32
|
AlertDialogTitle,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
Button,
|
|
34
|
+
Textarea,
|
|
35
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
38
36
|
|
|
39
37
|
const statusIcons: Record<string, React.ReactNode> = {
|
|
40
38
|
pending: <Clock className="h-3 w-3 animate-pulse" />,
|
|
@@ -8,23 +8,19 @@ import {
|
|
|
8
8
|
DialogTitle,
|
|
9
9
|
DialogDescription,
|
|
10
10
|
DialogFooter,
|
|
11
|
-
} from "./ui/dialog";
|
|
12
|
-
import {
|
|
13
11
|
Command,
|
|
14
12
|
CommandInput,
|
|
15
13
|
CommandList,
|
|
16
14
|
CommandEmpty,
|
|
17
15
|
CommandGroup,
|
|
18
16
|
CommandItem,
|
|
19
|
-
} from "./ui/command";
|
|
20
|
-
import {
|
|
21
17
|
Select,
|
|
22
18
|
SelectContent,
|
|
23
19
|
SelectItem,
|
|
24
20
|
SelectTrigger,
|
|
25
21
|
SelectValue,
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
Button,
|
|
23
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
28
24
|
import { Loader2 } from "lucide-react";
|
|
29
25
|
|
|
30
26
|
export interface NewConversationDialogProps {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
|
|
3
|
+
export function WhatsappIcon(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
width="24"
|
|
8
|
+
height="24"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
strokeWidth={2}
|
|
13
|
+
strokeLinecap="round"
|
|
14
|
+
strokeLinejoin="round"
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
<path d="M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9" />
|
|
18
|
+
<path d="M9 10a.5 .5 0 0 0 1 0v-1a.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a.5 .5 0 0 0 0 -1h-1a.5 .5 0 0 0 0 1" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
}
|