diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/docker_build.sh b/docker_build.sh new file mode 100644 index 0000000..cf75d21 --- /dev/null +++ b/docker_build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +VERSION=$1 + +docker build -t atendai/evolution-manager-v2:${VERSION} . +docker push atendai/evolution-manager-v2:${VERSION} diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index f0c4f97..e3bb417 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -1,4 +1,10 @@ -import { ChevronsUpDown, CircleUser, Cog, MessageCircle, RefreshCw } from "lucide-react"; +import { + ChevronsUpDown, + CircleUser, + Cog, + MessageCircle, + RefreshCw, +} from "lucide-react"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; @@ -8,9 +14,25 @@ import { InstanceStatus } from "@/components/instance-status"; import { InstanceToken } from "@/components/instance-token"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; -import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@/components/ui/dialog"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from "@/components/ui/card"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuCheckboxItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { useFetchInstances } from "@/lib/queries/instance/fetchInstances"; @@ -23,7 +45,9 @@ import { NewInstance } from "./NewInstance"; function Dashboard() { const { t } = useTranslation(); - const [deleteConfirmation, setDeleteConfirmation] = useState(null); + const [deleteConfirmation, setDeleteConfirmation] = useState( + null, + ); const { deleteInstance, logout } = useManageInstance(); const { data: instances, refetch } = useFetchInstances(); const [deleting, setDeleting] = useState([]); @@ -58,11 +82,15 @@ function Dashboard() { const filteredInstances = useMemo(() => { let instancesList = instances ? [...instances] : []; if (searchStatus !== "all") { - instancesList = instancesList.filter((instance) => instance.connectionStatus === searchStatus); + instancesList = instancesList.filter( + (instance) => instance.connectionStatus === searchStatus, + ); } if (nameSearch !== "") { - instancesList = instancesList.filter((instance) => instance.name.toLowerCase().includes(nameSearch.toLowerCase())); + instancesList = instancesList.filter((instance) => + instance.name.toLowerCase().includes(nameSearch.toLowerCase()), + ); } return instancesList; @@ -88,7 +116,11 @@ function Dashboard() {
- setNameSearch(e.target.value)} /> + setNameSearch(e.target.value)} + />
@@ -105,7 +137,8 @@ function Dashboard() { if (checked) { setSearchStatus(status.value); } - }}> + }} + > {status.label} ))} @@ -114,11 +147,14 @@ function Dashboard() {
{filteredInstances.length > 0 && - Array.isArray(instances) && - instances.map((instance: Instance) => ( + Array.isArray(filteredInstances) && + filteredInstances.map((instance: Instance) => ( - +

{instance.name}

@@ -169,13 +228,22 @@ function Dashboard() { {t("modal.delete.title")} -

{t("modal.delete.message", { instanceName: deleteConfirmation })}

+

+ {t("modal.delete.message", { instanceName: deleteConfirmation })} +

- -
diff --git a/src/pages/instance/Dify/SessionsDify.tsx b/src/pages/instance/Dify/SessionsDify.tsx index 06a6692..b061372 100644 --- a/src/pages/instance/Dify/SessionsDify.tsx +++ b/src/pages/instance/Dify/SessionsDify.tsx @@ -1,40 +1,232 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ColumnDef, SortingState } from "@tanstack/react-table"; -import { Delete, ListCollapse, MoreHorizontal, Pause, Play, RotateCcw, StopCircle } from "lucide-react"; -import { useState } from "react"; + +import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; +import { + Check, + ChevronDown, + Clock, + Delete, + Filter, + ListCollapse, + MessageSquare, + MoreHorizontal, + Pause, + Play, + RotateCcw, + StopCircle, + Trash2, + User, + X, +} from "lucide-react"; import { Button } from "@/components/ui/button"; -import { DataTable } from "@/components/ui/data-table"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; import { useInstance } from "@/contexts/InstanceContext"; +import { api } from "@/lib/queries/api"; import { useFetchSessionsDify } from "@/lib/queries/dify/fetchSessionsDify"; import { useManageDify } from "@/lib/queries/dify/manageDify"; import { IntegrationSession } from "@/types/evolution.types"; +interface FilterState { + name: string; + number: string; + status: string; + time: string; + customTime?: { + condition: "more" | "less"; + value: number; + unit: "minutes" | "hours" | "days"; + }; +} + function SessionsDify({ difyId }: { difyId?: string }) { const { t } = useTranslation(); const { instance } = useInstance(); - const { changeStatusDify } = useManageDify(); - const [sorting, setSorting] = useState([]); - const { data: sessions, refetch } = useFetchSessionsDify({ - difyId, - instanceName: instance?.name, - }); const [open, setOpen] = useState(false); - const [globalFilter, setGlobalFilter] = useState(""); + const [filterState, setFilterState] = useState({ + name: "", + number: "", + status: "all", + time: "all", + }); + const [selectedSessions, setSelectedSessions] = useState([]); + const [newStatus, setNewStatus] = useState("opened"); + const [sessionsPerPage, setSessionsPerPage] = useState(9); + const [sessionsDisplayed, setSessionsDisplayed] = useState(0); + const [showBots, setShowBots] = useState(true); + const [filteredSessions, setFilteredSessions] = useState([]); + const [sendMessageOpen, setSendMessageOpen] = useState(false); + const [selectedSessionForMessage, setSelectedSessionForMessage] = useState(""); + const [messageText, setMessageText] = useState(""); + + const { data: sessions = [], refetch: refetchSessions } = + useFetchSessionsDify({ + instanceName: instance?.name, + difyId, + enabled: !!instance?.name && !!difyId, + }); + + console.log("Hook result:", { + sessions, + instanceName: instance?.name, + difyId, + enabled: !!instance?.name && !!difyId + }); - function onReset() { - refetch(); - } + const { changeStatusDify } = useManageDify(); + + // Time filter functions + const parseTimeFilter = ( + value: string, + customValue?: number, + customUnit?: string, + customCondition?: string + ) => { + if (value === "custom") { + if (!customValue || isNaN(customValue) || customValue <= 0) return null; + const minutes = convertCustomTime(customValue, customUnit || "minutes"); + return { minutes, condition: customCondition }; + } + if (!value || value === "all") return null; + + if (value.startsWith(">")) { + const val = parseInt(value.slice(1)); + return { moreThan: val }; + } else { + return parseInt(value); + } + }; + + const convertCustomTime = (value: number, unit: string) => { + if (unit === "minutes") return value; + if (unit === "hours") return value * 60; + if (unit === "days") return value * 1440; + return null; + }; + + const checkTimeCondition = (diffMinutes: number, timeFilter: any) => { + if (typeof timeFilter === "object" && timeFilter.moreThan !== undefined) { + return diffMinutes > timeFilter.moreThan; + } else if ( + typeof timeFilter === "object" && + timeFilter.minutes !== undefined && + timeFilter.condition + ) { + if (timeFilter.condition === "more") { + return diffMinutes > timeFilter.minutes; + } else { + return diffMinutes <= timeFilter.minutes; + } + } else if (typeof timeFilter === "number") { + return diffMinutes <= timeFilter; + } + return true; + }; + + // Apply filters + const applyFilters = () => { + const { name, number, status, time, customTime } = filterState; + const parsedTime = parseTimeFilter( + time, + customTime?.value, + customTime?.unit, + customTime?.condition + ); + + const filtered = sessions.filter((session) => { + const matchesName = session.pushName + ?.toLowerCase() + .includes(name.toLowerCase()); + const matchesNumber = session.remoteJid.includes(number); + const matchesStatus = status === "all" || !status || session.status === status; + + let matchesTime = true; + if (parsedTime !== null) { + const diffMinutes = + (Date.now() - new Date(session.updatedAt).getTime()) / 60000; + matchesTime = checkTimeCondition(diffMinutes, parsedTime); + } + + return matchesName && matchesNumber && matchesStatus && matchesTime; + }); + + setFilteredSessions(filtered); + // Reset pagination to show first batch of filtered sessions + setSessionsDisplayed(Math.min(sessionsPerPage, filtered.length)); + }; + + // Mass actions + const handleSelectAll = (checked: boolean) => { + if (checked) { + // Select all displayed sessions (filtered) + setSelectedSessions(displayedSessions.map((s) => s.remoteJid)); + } else { + setSelectedSessions([]); + } + }; + + const handleMassStatusChange = async () => { + if (selectedSessions.length === 0) { + toast.error("Select at least one session."); + return; + } + + try { + await Promise.all( + selectedSessions.map((remoteJid) => + changeStatusDify({ + instanceName: instance?.name || "", + token: instance?.token || "", + remoteJid, + status: newStatus, + }) + ) + ); + + toast.success("Status updated for selected sessions."); + setSelectedSessions([]); + refetchSessions(); + } catch (error: any) { + console.error(error); + toast.error(`Error: ${error?.response?.data?.response?.message}`); + } + }; + + // Individual actions const changeStatus = async (remoteJid: string, status: string) => { try { if (!instance) return; @@ -46,81 +238,83 @@ function SessionsDify({ difyId }: { difyId?: string }) { status, }); - toast.success(t("dify.toast.success.status")); - onReset(); + toast.success("Status changed successfully."); + refetchSessions(); } catch (error: any) { console.error("Error:", error); - toast.error(`Error : ${error?.response?.data?.response?.message}`); + toast.error(`Error: ${error?.response?.data?.response?.message}`); } }; - const columns: ColumnDef[] = [ - { - accessorKey: "remoteJid", - header: () =>
{t("dify.sessions.table.remoteJid")}
, - cell: ({ row }) =>
{row.getValue("remoteJid")}
, - }, - { - accessorKey: "pushName", - header: () =>
{t("dify.sessions.table.pushName")}
, - cell: ({ row }) =>
{row.getValue("pushName")}
, - }, - { - accessorKey: "sessionId", - header: () =>
{t("dify.sessions.table.sessionId")}
, - cell: ({ row }) =>
{row.getValue("sessionId")}
, - }, - { - accessorKey: "status", - header: () =>
{t("dify.sessions.table.status")}
, - cell: ({ row }) =>
{row.getValue("status")}
, - }, - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - const session = row.original; - - return ( - - - - - - {t("dify.sessions.table.actions.title")} - - {session.status !== "opened" && ( - changeStatus(session.remoteJid, "opened")}> - - {t("dify.sessions.table.actions.open")} - - )} - {session.status !== "paused" && session.status !== "closed" && ( - changeStatus(session.remoteJid, "paused")}> - - {t("dify.sessions.table.actions.pause")} - - )} - {session.status !== "closed" && ( - changeStatus(session.remoteJid, "closed")}> - - {t("dify.sessions.table.actions.close")} - - )} - - changeStatus(session.remoteJid, "delete")}> - - {t("dify.sessions.table.actions.delete")} - - - - ); - }, - }, - ]; + const openSendMessageModal = (remoteJid: string) => { + setSelectedSessionForMessage(remoteJid); + setMessageText(""); + setSendMessageOpen(true); + }; + + const sendMessage = async () => { + if (!messageText.trim()) { + toast.error("Please enter a message."); + return; + } + + try { + if (!instance) return; + + await api.post(`/message/sendText/${instance.name}`, { + number: selectedSessionForMessage, + text: messageText + }, { + headers: { + apikey: instance.token + } + }); + + toast.success("Message sent successfully."); + setSendMessageOpen(false); + setMessageText(""); + setSelectedSessionForMessage(""); + } catch (error: any) { + console.error("Error:", error); + toast.error(`Error: ${error?.response?.data?.response?.message || error?.message || 'Failed to send message'}`); + } + }; + + + + // Pagination + const showMore = () => { + setSessionsDisplayed((prev) => Math.min(prev + sessionsPerPage, filteredSessions.length)); + }; + + const showAll = () => { + setSessionsDisplayed(filteredSessions.length); + }; + + const showLess = () => { + setSessionsDisplayed(Math.min(sessionsPerPage, filteredSessions.length)); + }; + + // Initialize filtered sessions when sessions change + useEffect(() => { + console.log("Sessions changed:", sessions); + if (sessions.length > 0) { + setFilteredSessions(sessions); + // Show first batch of sessions automatically + setSessionsDisplayed(Math.min(sessionsPerPage, sessions.length)); + console.log("Filtered sessions set:", sessions.length); + } + }, [sessions, sessionsPerPage]); + + const displayedSessions = filteredSessions.slice(0, sessionsDisplayed); + + console.log("Debug info:", { + sessionsCount: sessions.length, + filteredCount: filteredSessions.length, + displayedCount: displayedSessions.length, + sessionsDisplayed, + sessionsPerPage + }); return ( @@ -130,28 +324,421 @@ function SessionsDify({ difyId }: { difyId?: string }) { {t("dify.sessions.label")} - + - {t("dify.sessions.label")} + Dify Sessions -
-
- setGlobalFilter(event.target.value)} /> - +
+ + + + {/* Toggle Bots Button */} + + + {/* Advanced Filters */} + + + + + Advanced Filters + + + +
+
+ + + setFilterState((prev) => ({ ...prev, name: e.target.value })) + } + /> +
+
+ + + setFilterState((prev) => ({ ...prev, number: e.target.value })) + } + /> +
+
+ + +
+
+ + +
+
+ + {/* Custom Time Filter */} + {filterState.time === "custom" && ( +
+ + + setFilterState((prev) => ({ + ...prev, + customTime: { + condition: prev.customTime?.condition || "more", + value: parseInt(e.target.value) || 0, + unit: prev.customTime?.unit || "minutes", + }, + })) + } + /> + +
+ )} + + +
+
+ + {/* Mass Actions */} + + + Mass Actions + + +
+
+ 0 && + displayedSessions.every((session) => + selectedSessions.includes(session.remoteJid) + ) + } + onChange={(e) => handleSelectAll(e.target.checked)} + className="h-4 w-4" + /> + +
+ + +
+
+
+ + {/* Sessions Display */} + + + + Sessions ({filteredSessions.length} total) + + + +
+ {displayedSessions.length === 0 ? ( +
+ {sessions.length === 0 ? ( +
+

No sessions found.

+

Instance: {instance?.name || "None"}

+

Dify ID: {difyId || "None"}

+
+ ) : ( +

No sessions match the current filters.

+ )} +
+ ) : ( + displayedSessions.map((session) => ( + + +
+ { + if (e.target.checked) { + setSelectedSessions((prev) => [ + ...prev, + session.remoteJid, + ]); + } else { + setSelectedSessions((prev) => + prev.filter((id) => id !== session.remoteJid) + ); + } + }} + className="h-4 w-4" + />
- -
+ +
+

+ {session.pushName || "No Name"} +

+

+ Number: {session.remoteJid} +

+

+ Status: {session.status} +

+

+ Updated:{" "} + {new Date(session.updatedAt).toLocaleString()} +

+
+ +
+ + + + + + Actions + + {session.status !== "opened" && ( + + changeStatus(session.remoteJid, "opened") + } + > + + Open + + )} + {session.status !== "paused" && + session.status !== "closed" && ( + + changeStatus(session.remoteJid, "paused") + } + > + + Pause + + )} + {session.status !== "closed" && ( + + changeStatus(session.remoteJid, "closed") + } + > + + Close + + )} + + changeStatus(session.remoteJid, "delete") + } + > + + Delete + + + + openSendMessageModal(session.remoteJid) + } + > + + Send Message + + + +
+
+
+ )) + )} +
+ + {/* Pagination */} +
+ + + +
+ + + +
+ + {/* Send Message Modal */} + + + + Send Message + +
+
+ +
+
+ +