diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx deleted file mode 100644 index 224632669a..0000000000 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ /dev/null @@ -1,290 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { ActivateClientDialog, DeactivateClientDialog } from "../../../components/clients/dialogs" -import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" -import { useToast } from "@/components/ui/use-toast" -import type { Client } from "@/app/types/clients" -import { Search, ArrowUpDown, Loader2 } from "lucide-react" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from "@/components/ui/pagination" - -const ITEMS_PER_PAGE = 8 - -const formatDate = (dateString: string | undefined): string => { - if (!dateString) return "N/A" - const date = new Date(dateString) - if (isNaN(date.getTime())) return "Invalid Date" - - const now = new Date() - const diffTime = date.getTime() - now.getTime() - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - - if (diffDays < 0) return "Expired" - if (diffDays === 0) return "Expires today" - if (diffDays === 1) return "Expires tomorrow" - return `Expires in ${diffDays} days` -} - - -const ClientManagement = () => { - const [clients, setClients] = useState([]) - const [loading, setLoading] = useState(false) - const [selectedClient, setSelectedClient] = useState(null) - const [activateDialogOpen, setActivateDialogOpen] = useState(false) - const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false) - const [searchQuery, setSearchQuery] = useState("") - const [currentPage, setCurrentPage] = useState(1) - const [sortField, setSortField] = useState("name") - const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") - const { toast } = useToast() - - const fetchClients = async () => { - setLoading(true) - try { - const response = await getClientsApi() - setClients(response.clients) - } catch (error) { - toast({ - title: "Error", - description: "Failed to fetch clients", - variant: "destructive", - }) - } finally { - setLoading(false) - } - } - - const handleCopyClientId = (clientId: string) => { - navigator.clipboard.writeText(clientId) - toast({ - title: "Client ID Copied", - description: "The client ID has been copied to your clipboard.", - }) - } - - useEffect(() => { - fetchClients() - }, []) - - const handleActivateDeactivate = async (clientId: string, activate: boolean) => { - try { - await activateUserClientApi({ _id: clientId, isActive: activate }) - await fetchClients() - toast({ - title: "Success", - description: `Client ${activate ? "activated" : "deactivated"} successfully`, - }) - } catch (error) { - toast({ - title: "Error", - description: `Failed to ${activate ? "activate" : "deactivate"} client`, - variant: "destructive", - }) - } finally { - setActivateDialogOpen(false) - setDeactivateDialogOpen(false) - setSelectedClient(null) - } - } - - const handleActivateClick = (client: Client) => { - setSelectedClient(client) - setActivateDialogOpen(true) - } - - const handleDeactivateClick = (client: Client) => { - setSelectedClient(client) - setDeactivateDialogOpen(true) - } - - const handleSort = (field: keyof Client) => { - if (sortField === field) { - setSortOrder(sortOrder === "asc" ? "desc" : "asc") - } else { - setSortField(field) - setSortOrder("asc") - } - } - - const filteredClients = clients.filter( - (client) => - client.name.toLowerCase().includes(searchQuery.toLowerCase()) || - client.user.email.toLowerCase().includes(searchQuery.toLowerCase()), - ) - - const sortedClients = [...filteredClients].sort((a, b) => { - if (a[sortField] < b[sortField]) return sortOrder === "asc" ? -1 : 1 - if (a[sortField] > b[sortField]) return sortOrder === "asc" ? 1 : -1 - return 0 - }) - - const totalPages = Math.ceil(sortedClients.length / ITEMS_PER_PAGE) - const paginatedClients = sortedClients.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE) - - const activatedClients = clients.filter((client) => client.isActive).length - const deactivatedClients = clients.filter((client) => !client.isActive).length - - - - return ( -
- {loading ? ( -
- -
- ) : ( - <> -
-

Client Management

- -
- -
-
-

Activated Clients

-

{activatedClients}

-
-
-

Deactivated Clients

-

{deactivatedClients}

-
- -
- -
-
- - setSearchQuery(e.target.value)} - /> -
- - - - - - handleSort("name")}> - Name {sortField === "name" && (sortOrder === "asc" ? "↑" : "↓")} - - handleSort("isActive")}> - Status {sortField === "isActive" && (sortOrder === "asc" ? "↑" : "↓")} - - - -
- -
- - - - Client Name - User Email - Token Expiry - Status - Actions - - - - {paginatedClients.map((client) => ( - - -
{client.name}
-
{client._id}
-
- {client.user.email} - - {client.access_token?.expires - ? formatDate(client.access_token.expires) - : "N/A"} - - - - {client.isActive ? "Activated" : "Not Activated"} - - - - - - -
- ))} -
-
-
- -
- - - - setCurrentPage((prev) => Math.max(prev - 1, 1))} - className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} - /> - - {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( - - setCurrentPage(page)} isActive={currentPage === page}> - {page} - - - ))} - - setCurrentPage((prev) => Math.min(prev + 1, totalPages))} - className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} - /> - - - -
- - selectedClient && handleActivateDeactivate(selectedClient._id, true)} - clientName={selectedClient?.name} - /> - - selectedClient && handleActivateDeactivate(selectedClient._id, false)} - clientName={selectedClient?.name} - /> - - )} -
- ) -} - -export default ClientManagement - diff --git a/netmanager-app/app/(authenticated)/profile/page.tsx b/netmanager-app/app/(authenticated)/profile/page.tsx new file mode 100644 index 0000000000..4ae36c19d2 --- /dev/null +++ b/netmanager-app/app/(authenticated)/profile/page.tsx @@ -0,0 +1,11 @@ +import ProfileTabs from "@/components/Settings/ProfileTabs" + +export default function ProfilePage() { + return ( +
+

User Profile

+ +
+ ) +} + diff --git a/netmanager-app/app/types/clients.ts b/netmanager-app/app/types/clients.ts index 9e0235a51c..bf4dca3510 100644 --- a/netmanager-app/app/types/clients.ts +++ b/netmanager-app/app/types/clients.ts @@ -23,5 +23,3 @@ import { UserDetails } from './users'; user: UserDetails access_token: AccessToken } - - \ No newline at end of file diff --git a/netmanager-app/components/Settings/ApiTokens.tsx b/netmanager-app/components/Settings/ApiTokens.tsx new file mode 100644 index 0000000000..87ecd2e4da --- /dev/null +++ b/netmanager-app/components/Settings/ApiTokens.tsx @@ -0,0 +1,322 @@ +import type React from "react" +import { useEffect, useState } from "react" +import moment from "moment" +import { useAppDispatch, useAppSelector } from "@/core/redux/hooks" +import { useToast } from "@/components/ui/use-toast" +import { Button } from "@/components/ui/button" +import { performRefresh } from "@/core/redux/slices/clientsSlice" +import EditClientForm from "./EditClientForm" +import CreateClientForm from "./CreateClientForm" +import DialogWrapper from "./DialogWrapper" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Skeleton } from "@/components/ui/skeleton" +import { Edit, Copy, Info, Plus } from "lucide-react" +import { users } from "@/core/apis/users" +import type { Client } from "@/app/types/clients" +import { settings } from "@/core/apis/settings" + + +const UserClientsTable = () => { + const dispatch = useAppDispatch() + const { toast } = useToast() + const [showInfoModal, setShowInfoModal] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [isLoadingToken, setIsLoadingToken] = useState(false) + const [isLoadingActivationRequest, setIsLoadingActivationRequest] = useState(false) + const [openEditForm, setOpenEditForm] = useState(false) + const [openCreateForm, setOpenCreateForm] = useState(false) + const [selectedClient, setSelectedClient] = useState({} as Client) + const userInfo = useAppSelector((state) => state.user.userDetails) + const [clients, setClients] = useState([]) + const [clientsDetails, setClientsDetails] = useState([]) + const [currentPage, setCurrentPage] = useState(1) + const itemsPerPage = 4 + + const fetchClients = async () => { + setIsLoading(true) + try { + const res = await users.getUserDetails(userInfo?._id || "") + if (res) { + dispatch({ type: "ADD_CLIENTS", payload: res.users[0].clients }) + setCurrentPage(1) + } + setClients(res.users[0].clients) + } catch (error) { + toast({ + title: "Error", + description: "Failed to fetch user details", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + const fetchClientDetails = async () => { + setIsLoading(true) + try { + const response = await settings.getUserClientsApi(userInfo?._id || "") + if (response) { + dispatch({ type: "ADD_CLIENTS_DETAILS", payload: response }) + } + setClientsDetails(response.clients) + } catch (error) { + console.error(error) + toast({ + title: "Error", + description: "Failed to fetch client details", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + fetchClients() + fetchClientDetails() + }, [userInfo?._id, dispatch]) + + const hasAccessToken = (clientId: string): boolean => { + const client = + Array.isArray(clientsDetails) && clientsDetails + ? clientsDetails?.find((client: Client) => client._id === clientId) + : undefined + return client?.access_token?.token ? true : false + } + + const getClientToken = (clientID: string) => { + const client = + Array.isArray(clientsDetails) && clientsDetails + ? clientsDetails?.find((client: Client) => client._id === clientID) + : undefined + return client && client.access_token && client.access_token.token + } + const getClientTokenExpiryDate = (clientID: string) => { + const client = + Array.isArray(clientsDetails) && clientsDetails + ? clientsDetails?.find((client: Client) => client._id === clientID) + : undefined + return client && client.access_token && client.access_token.expires + } + + const getClientTokenCreateAt = (clientID: string) => { + const client = + Array.isArray(clientsDetails) && clientsDetails + ? clientsDetails?.find((client: Client) => client._id === clientID) + : undefined + return client && client.access_token && client.access_token.createdAt + } + + const handleGenerateToken = async (res: Client) => { + setIsLoadingToken(true) + if (!res?.isActive) { + setShowInfoModal(true) + setIsLoadingToken(false) + return; + } else { + try { + const response = await settings.generateTokenApi(res) + if (response) { + toast({ + title: "Success", + description: "Token generated successfully", + }) + } + dispatch(performRefresh()) + } catch (error: any) { + const errorMessage = error?.response?.data?.message || error.message || "Failed to generate token"; + toast({ + title: "Error", + description: errorMessage, + }) + } finally { + setIsLoadingToken(false) + } + } + } + + const handleActivationRequest = async () => { + setIsLoadingActivationRequest(true) + try { + const clientID = selectedClient?._id + const response = await settings.activationRequestApi(clientID) + if (response) { + setShowInfoModal(false) + setTimeout(() => { + toast({ + title: "Success", + description: "Activation request sent successfully", + }) + }, 3000) + } + } catch (error: any) { + setShowInfoModal(false) + setTimeout(() => { + toast({ + title: "Error", + description: error.message || "Failed to send activation request", + variant: "destructive", + }) + }, 3000) + } finally { + setIsLoadingActivationRequest(false) + } + } + + const displayIPAddresses = (client: Client) => { + return Array.isArray(client.ip_addresses) ? client.ip_addresses.join(", ") : client.ip_addresses + } + + const handleClientCreated = () => { + fetchClients() + fetchClientDetails() + } + + return ( +
+
+

Clients

+ +
+ + + + Client name + IP Address + Client Status + Created + Token + Expires + + + + + {isLoading ? ( + + + + + + ) : clients?.length > 0 ? ( + clients + .slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage) + .map((client: Client, index: number) => ( + + {client?.name} + {displayIPAddresses(client)} + + + {client?.isActive ? "Activated" : "Not Activated"} + + + + {getClientTokenCreateAt(client._id) && moment(getClientTokenCreateAt(client._id)).format("MMM DD, YYYY")} + + + {hasAccessToken(client._id) ? ( +
+ + {getClientToken(client._id)?.slice(0, 2)} + ••••••• + {getClientToken(client._id)?.slice(-2)} + + +
+ ) : ( + + )} +
+ + + {getClientTokenExpiryDate(client._id) && + moment(getClientTokenExpiryDate(client._id)).format("MMM DD, YYYY")} + + + + +
+ )) + ) : ( + + + No data found + + + )} +
+
+ setOpenEditForm(false)} + data={selectedClient} + onClientUpdated={handleClientCreated} + /> + setOpenCreateForm(false)} + onClientCreated={handleClientCreated} + /> + setShowInfoModal(false)} + handleClick={handleActivationRequest} + primaryButtonText={"Send activation request"} + loading={isLoadingActivationRequest} + > +
+ +

+ You cannot generate a token for an inactive client. Reach out to support for assistance at support@airqo.net + or send an activation request. +

+
+
+
+ ) +} + +export default UserClientsTable + diff --git a/netmanager-app/components/Settings/ClientTableRow.tsx b/netmanager-app/components/Settings/ClientTableRow.tsx new file mode 100644 index 0000000000..611dd798ff --- /dev/null +++ b/netmanager-app/components/Settings/ClientTableRow.tsx @@ -0,0 +1,93 @@ +import type { FC } from "react" +import moment from "moment" +import {Button} from "@/components/ui/button" +import CopyIcon from "@/icons/Common/copy.svg" +import EditIcon from "@/icons/Common/edit-pencil.svg" + +interface ClientTableRowProps { + client: any + onGenerateToken: (client: any) => void + onEditClient: (client: any) => void + onCopyToken: (token: string) => void + getClientToken: (clientId: string) => string | null + getClientTokenExpiryDate: (clientId: string) => string | null + isLoadingToken: boolean +} + +export const ClientTableRow: FC = ({ + client, + onGenerateToken, + onEditClient, + onCopyToken, + getClientToken, + getClientTokenExpiryDate, + isLoadingToken, +}) => { + const displayIPAddresses = (client) => { + return Array.isArray(client.ip_addresses) ? client.ip_addresses.join(", ") : client.ip_addresses + } + + return ( + + + {client?.name} + + + {displayIPAddresses(client)} + + +
+ {client?.isActive ? "Activated" : "Not Activated"} +
+ + + {moment(client?.createdAt).format("MMM DD, YYYY")} + + + {getClientToken(client._id) ? ( + + {getClientToken(client._id).slice(0, 2)}.... + {getClientToken(client._id).slice(-2)} +
onCopyToken(getClientToken(client._id))} + > + +
+
+ ) : ( + + )} + + + {getClientTokenExpiryDate(client._id) && moment(getClientTokenExpiryDate(client._id)).format("MMM DD, YYYY")} + + +
onEditClient(client)} + > + +
+ + + ) +} + diff --git a/netmanager-app/components/Settings/Clients.tsx b/netmanager-app/components/Settings/Clients.tsx new file mode 100644 index 0000000000..aca3037804 --- /dev/null +++ b/netmanager-app/components/Settings/Clients.tsx @@ -0,0 +1,382 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + ActivateClientDialog, + DeactivateClientDialog, +} from "@/components/clients/dialogs"; +import { getClientsApi } from "@/core/apis/analytics"; +import { settings } from "@/core/apis/settings"; +import { useToast } from "@/components/ui/use-toast"; +import type { Client } from "@/app/types/clients"; +import { Search, ArrowUpDown, Loader2 } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; + +const ITEMS_PER_PAGE = 8; + +const formatDate = (dateString: string | undefined): string => { + if (!dateString) return "N/A"; + const date = new Date(dateString); + if (isNaN(date.getTime())) return "Invalid Date"; + + const now = new Date(); + const diffTime = date.getTime() - now.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays < 0) return "Expired"; + if (diffDays === 0) return "Expires today"; + if (diffDays === 1) return "Expires tomorrow"; + return `Expires in ${diffDays} days`; +}; + +const ClientManagement = () => { + const [clients, setClients] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedClient, setSelectedClient] = useState(null); + const [activateDialogOpen, setActivateDialogOpen] = useState(false); + const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [sortField, setSortField] = useState("name"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const { toast } = useToast(); + + const fetchClients = async () => { + setLoading(true); + try { + const response = await getClientsApi(); + setClients(response.clients); + } catch (error) { + toast({ + title: "Error", + description: "Failed to fetch clients", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchClients(); + }, []); + + const handleActivateDeactivate = async ( + clientId: string, + activate: boolean + ) => { + const data = { + _id: clientId, + isActive: activate ? true : false, + }; + try { + await settings.activateUserClientApi(data); + await fetchClients(); + toast({ + title: "Success", + description: `Client ${ + activate ? "activated" : "deactivated" + } successfully`, + }); + } catch (error) { + toast({ + title: "Error", + description: `Failed to ${activate ? "activate" : "deactivate"} client`, + variant: "destructive", + }); + } finally { + setActivateDialogOpen(false); + setDeactivateDialogOpen(false); + setSelectedClient(null); + } + }; + + const handleActivateClick = (client: Client) => { + setSelectedClient(client); + setActivateDialogOpen(true); + }; + + const handleDeactivateClick = (client: Client) => { + setSelectedClient(client); + setDeactivateDialogOpen(true); + }; + + const handleSort = (field: keyof Client) => { + if (sortField === field) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortField(field); + setSortOrder("asc"); + } + }; + + const filteredClients = clients.filter( + (client) => + client.name.toLowerCase().includes(searchQuery.toLowerCase()) || + (client.user?.email?.toLowerCase().includes(searchQuery.toLowerCase()) ?? + false) + ); + + const sortedClients = [...filteredClients].sort((a, b) => { + if (a[sortField] < b[sortField]) return sortOrder === "asc" ? -1 : 1; + if (a[sortField] > b[sortField]) return sortOrder === "asc" ? 1 : -1; + return 0; + }); + + const totalPages = Math.ceil(sortedClients.length / ITEMS_PER_PAGE); + const paginatedClients = sortedClients.slice( + (currentPage - 1) * ITEMS_PER_PAGE, + currentPage * ITEMS_PER_PAGE + ); + + const activatedClients = clients.filter((client) => client.isActive).length; + const deactivatedClients = clients.filter( + (client) => !client.isActive + ).length; + + const getPageNumbers = () => { + const pageNumbers = []; + const maxVisiblePages = 5; + + if (totalPages <= maxVisiblePages) { + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + if (currentPage <= 3) { + for (let i = 1; i <= 4; i++) { + pageNumbers.push(i); + } + pageNumbers.push("ellipsis"); + pageNumbers.push(totalPages); + } else if (currentPage >= totalPages - 2) { + pageNumbers.push(1); + pageNumbers.push("ellipsis"); + for (let i = totalPages - 3; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + pageNumbers.push(1); + pageNumbers.push("ellipsis"); + for (let i = currentPage - 1; i <= currentPage + 1; i++) { + pageNumbers.push(i); + } + pageNumbers.push("ellipsis"); + pageNumbers.push(totalPages); + } + } + return pageNumbers; + }; + + return ( +
+ {loading ? ( +
+ +
+ ) : ( + <> +
+

Client Management

+ +
+ +
+
+

Activated Clients

+

{activatedClients}

+
+
+

Deactivated Clients

+

{deactivatedClients}

+
+
+ +
+
+ + setSearchQuery(e.target.value)} + /> +
+ + + + + + handleSort("name")}> + Name{" "} + {sortField === "name" && (sortOrder === "asc" ? "↑" : "↓")} + + handleSort("isActive")}> + Status{" "} + {sortField === "isActive" && + (sortOrder === "asc" ? "↑" : "↓")} + + + +
+ +
+ + + + Client Name + User Email + Token Expiry + Status + Actions + + + + {paginatedClients.map((client) => ( + + +
{client.name}
+
+ {client._id} +
+
+ + {client.user && client.user.email + ? client.user.email + : "N/A"} + + + + {client.access_token?.expires + ? formatDate(client.access_token.expires) + : "N/A"} + + + + {client.isActive ? "Activated" : "Not Activated"} + + + + + +
+ ))} +
+
+
+ +
+ + + + + setCurrentPage((prev) => Math.max(prev - 1, 1)) + } + className={ + currentPage === 1 + ? "pointer-events-none opacity-50" + : "cursor-pointer" + } + /> + + + {getPageNumbers().map((pageNumber, index) => ( + + {pageNumber === "ellipsis" ? ( + + ) : ( + setCurrentPage(pageNumber as number)} + isActive={currentPage === pageNumber} + className="cursor-pointer" + > + {pageNumber} + + )} + + ))} + + + + setCurrentPage((prev) => Math.min(prev + 1, totalPages)) + } + className={ + currentPage === totalPages + ? "pointer-events-none opacity-50" + : "cursor-pointer" + } + /> + + + +
+ + + selectedClient && + handleActivateDeactivate(selectedClient._id, true) + } + clientName={selectedClient?.name} + /> + + + selectedClient && + handleActivateDeactivate(selectedClient._id, false) + } + clientName={selectedClient?.name} + /> + + )} +
+ ); +}; + +export default ClientManagement; diff --git a/netmanager-app/components/Settings/CreateClientForm.tsx b/netmanager-app/components/Settings/CreateClientForm.tsx new file mode 100644 index 0000000000..2946705286 --- /dev/null +++ b/netmanager-app/components/Settings/CreateClientForm.tsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react' +import { useAppSelector } from "@/core/redux/hooks" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Plus, X } from 'lucide-react' +import { settings } from "@/core/apis/settings" +import { useToast } from "@/components/ui/use-toast" + +interface CreateClientFormProps { + open: boolean + onClose: () => void + onClientCreated: () => void +} + +const CreateClientForm: React.FC = ({ open, onClose, onClientCreated }) => { + const { toast } = useToast() + const userInfo = useAppSelector((state) => state.user.userDetails) + const [clientName, setClientName] = useState('') + const [ipAddresses, setIpAddresses] = useState(['']) + const [isLoading, setIsLoading] = useState(false) + + const handleInputValueChange = (type: string, value: string, index?: number) => { + if (type === 'clientName') { + setClientName(value) + } else if (type === 'ipAddress' && index !== undefined) { + const newIpAddresses = [...ipAddresses] + newIpAddresses[index] = value + setIpAddresses(newIpAddresses) + } + } + + const handleRemoveInputValue = (index: number) => { + const newIpAddresses = ipAddresses.filter((_, i) => i !== index) + setIpAddresses(newIpAddresses) + } + + const handleAddIpAddress = () => { + setIpAddresses([...ipAddresses, '']) + } + + const handleSubmit = async () => { + setIsLoading(true) + try { + if (!clientName) { + throw new Error("Client name can't be empty") + } + + const data = { + name: clientName, + user_id: userInfo?._id, + ip_addresses: ipAddresses.filter((ip) => ip.trim() !== ''), + } + + const response = await settings.createClientApi(data) + if (response) { + toast({ + title: "Success", + description: "Client created successfully", + }) + onClientCreated() + onClose() + } + } catch (error: any) { + toast({ + title: "Error", + description: error.message || "Failed to create client", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + return ( + + + + Create New Client + +
+
+ + handleInputValueChange('clientName', e.target.value)} + placeholder="Enter client name" + /> +
+ {ipAddresses.map((ip, index) => ( +
+ +
+ handleInputValueChange('ipAddress', e.target.value, index)} + placeholder={`Enter IP address ${index + 1}`} + /> + {index > 0 && ( + + )} +
+
+ ))} + +
+ + + + +
+
+ ) +} + +export default CreateClientForm diff --git a/netmanager-app/components/Settings/DialogWrapper.tsx b/netmanager-app/components/Settings/DialogWrapper.tsx new file mode 100644 index 0000000000..5a020d8c20 --- /dev/null +++ b/netmanager-app/components/Settings/DialogWrapper.tsx @@ -0,0 +1,54 @@ +import type React from "react" +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" + +interface DialogWrapperProps { + children: React.ReactNode + ModalIcon?: React.ComponentType<{ className?: string }> + handleClick: () => void + open: boolean + onClose: () => void + primaryButtonText?: string + loading?: boolean + title?: string +} + +export const DialogWrapper: React.FC = ({ + children, + ModalIcon, + handleClick, + open, + onClose, + primaryButtonText = "Submit", + loading = false, + title, +}) => { + return ( + + + + {ModalIcon && ( +
+
+ +
+
+ )} + {title && {title}} +
+
{children}
+ + + + +
+
+ ) +} + +export default DialogWrapper + diff --git a/netmanager-app/components/Settings/EditClientForm.tsx b/netmanager-app/components/Settings/EditClientForm.tsx new file mode 100644 index 0000000000..236b87a0bd --- /dev/null +++ b/netmanager-app/components/Settings/EditClientForm.tsx @@ -0,0 +1,187 @@ +import React, { useState, useEffect, useCallback } from "react" +import { useSelector, useDispatch } from "react-redux" +import DialogWrapper from "./DialogWrapper" +import { Toast } from "@/components/ui/toast" +import { addClients, addClientsDetails, performRefresh } from "@/core/redux/slices/clientsSlice" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { X, Plus } from "lucide-react" +import { users } from "@/core/apis/users" +import type { RootState } from "@/core/redux/store" +import { Client } from "@/app/types/clients" +import { settings } from "@/core/apis/settings" + +interface EditClientFormProps { + open: boolean + onClose: () => void; + data: Client +} + +const EditClientForm: React.FC = ({ open, onClose, data }) => { + const dispatch = useDispatch() + const userInfo = useSelector((state: RootState) => state.user.userDetails) + const clientID = data?._id + const [loading, setLoading] = useState(false) + const [isError, setIsError] = useState({ isError: false, message: "", type: "" }) + const [clientName, setClientName] = useState("") + const [ipAddresses, setIpAddresses] = useState([""]) + + const handleInitialData = useCallback(() => { + setClientName(data?.name || ""); + const ipAddresses = Array.isArray(data?.ip_addresses) + ? data?.ip_addresses + : data?.ip_addresses + ? [data?.ip_addresses] + : [""]; + setIpAddresses(ipAddresses); + }, [data]); + + useEffect(() => { + handleInitialData(); + }, [handleInitialData]); + + const handleInputValueChange = useCallback((type: string, value: string, index?: number) => { + if (type === "clientName") { + setClientName(value) + } else if (type === "ipAddress" && index !== undefined) { + setIpAddresses((prev) => { + const newIpAddresses = [...prev] + newIpAddresses[index] = value + return newIpAddresses + }) + } + }, []) + + const handleRemoveInputValue = useCallback((type: string, index?: number) => { + if (type === "clientName") { + setClientName("") + } else if (type === "ipAddress" && index !== undefined) { + setIpAddresses((prev) => prev.filter((_, i) => i !== index)) + } + }, []) + + const handleAddIpAddress = useCallback(() => { + setIpAddresses((prev) => [...prev, ""]) + }, []) + + const handleSubmit = async () => { + setLoading(true) + + if (!clientName) { + setIsError({ + isError: true, + message: "Client name can't be empty", + type: "error", + }) + setLoading(false) + return + } + + try { + const data = { + name: clientName, + user_id: userInfo?._id, + ip_addresses: ipAddresses.filter((ip) => ip.trim() !== ""), + } + + const response = await settings.updateClientApi(data, clientID) + if (!response) { + throw new Error("Failed to update client") + } + const res = await users.getUserDetails(userInfo?._id || "") + const resp = await settings.getUserClientsApi(userInfo?._id || "") + dispatch(addClients(res.users[0].clients)) + dispatch(addClientsDetails(resp.clients)) + dispatch(performRefresh()) + closeModal() + } catch (error: any) { + setIsError({ + isError: true, + message: error?.response?.data?.message || "Failed to Edit client", + type: "error", + }) + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (isError.isError) { + const timer = setTimeout(() => { + setIsError({ + isError: false, + message: "", + type: "", + }) + }, 2000) + + return () => clearTimeout(timer) + } + }, [isError.isError]) + + return ( + + {isError.isError && } +

Edit client

+ +
+
+ + handleInputValueChange("clientName", e.target.value)} + /> + {clientName && ( + + )} +
+ + {ipAddresses.map((ip, index) => ( +
+ + handleInputValueChange("ipAddress", e.target.value, index)} + /> + +
+ ))} + + +
+
+ ) +} + +export default React.memo(EditClientForm) + diff --git a/netmanager-app/components/Settings/MyProfile.tsx b/netmanager-app/components/Settings/MyProfile.tsx new file mode 100644 index 0000000000..966f11e48f --- /dev/null +++ b/netmanager-app/components/Settings/MyProfile.tsx @@ -0,0 +1,391 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { settings } from "@/core/apis/settings"; +import { Avatar, AvatarImage } from "@/components/ui/avatar"; +import { getCountries } from "@/utils/countries"; +import { getTimezones } from "@/utils/timezones"; +import { users } from "@/core/apis/users"; +import { useAppSelector, useAppDispatch } from "@/core/redux/hooks"; +import { useToast } from "@/components/ui/use-toast"; +import { Toaster } from "@/components/ui/toaster"; +import { Loader2, Upload, Trash2 } from "lucide-react"; +import { cloudinaryImageUpload } from "@/core/apis/cloudinary"; +import { setUserDetails } from "@/core/redux/slices/userSlice"; + +interface Profile { + firstName: string; + lastName: string; + email: string; + jobTitle?: string; + country?: string; + timezone?: string; + description?: string; + profilePicture?: string; +} + +export default function MyProfile() { + const currentUser = useAppSelector((state) => state.user.userDetails); + const { toast } = useToast(); + const dispatch = useAppDispatch(); + + const [profile, setProfile] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [countries, setCountries] = useState< + { value: string; label: string }[] + >([]); + const [timezones, setTimezones] = useState< + { value: string; label: string }[] + >([]); + const [profileUploading, setProfileUploading] = useState(false); + + const fetchUserData = useCallback(async () => { + if (!currentUser) return; + + setIsLoading(true); + setError(null); + + try { + const response = await users.getUserDetails(currentUser._id); + const userData = response?.users?.[0]; + + if (userData) { + setProfile({ + firstName: userData.firstName || "", + lastName: userData.lastName || "", + email: userData.email || "", + jobTitle: userData.jobTitle || "", + country: userData.country || "", + timezone: userData.timezone || "", + description: userData.description || "", + profilePicture: userData.profilePicture || "", + }); + } else { + setError("User data not found"); + } + } catch (error) { + setError("Failed to fetch user data"); + toast({ + title: "Error", + description: "Failed to fetch user data", + }); + } finally { + setIsLoading(false); + } + }, [currentUser, toast]); + + useEffect(() => { + fetchUserData(); + }, [fetchUserData]); + + useEffect(() => { + setCountries(getCountries()); + setTimezones(getTimezones()); + }, []); + + const handleInputChange = ( + e: React.ChangeEvent + ) => { + if (profile) { + setProfile({ ...profile, [e.target.name]: e.target.value }); + } + }; + + const handleSelectChange = (name: string) => (value: string) => { + if (profile) { + setProfile({ ...profile, [name]: value }); + } + }; + + const handleProfileImageUpdate = async ( + e: React.ChangeEvent + ) => { + const file = e.target.files?.[0]; + if (!file) return; + + const formData = new FormData(); + formData.append("file", file); + formData.append( + "upload_preset", + process.env.NEXT_PUBLIC_CLOUDINARY_PRESET || "" + ); + formData.append("folder", "profiles"); + + setProfileUploading(true); + try { + const responseData = await cloudinaryImageUpload(formData); + if (profile && currentUser) { + const updatedProfile = { + ...profile, + profilePicture: responseData.secure_url, + }; + setProfile(updatedProfile); + await settings.updateUserDetailsApi( + { profilePicture: responseData.secure_url }, + currentUser._id + ); + dispatch( + setUserDetails({ + ...currentUser, + profilePicture: responseData.secure_url, + }) + ); + toast({ + title: "Success", + description: "Profile picture updated successfully", + }); + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to update profile picture", + }); + } finally { + setProfileUploading(false); + } + }; + + const deleteProfileImage = async () => { + if (!profile || !currentUser) return; + + try { + const updatedProfile = { ...profile, profilePicture: "" }; + setProfile(updatedProfile); + await settings.updateUserDetailsApi( + { profilePicture: "" }, + currentUser._id + ); + dispatch(setUserDetails({ ...currentUser, profilePicture: "" })); + toast({ + title: "Success", + description: "Profile picture deleted successfully", + }); + } catch (error) { + toast({ + title: "Error", + description: "Failed to delete profile picture", + }); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!profile || !currentUser) return; + + setIsLoading(true); + try { + await settings.updateUserDetailsApi(profile, currentUser._id); + const res = await users.getUserDetails(currentUser._id); + const updatedUser = res.users[0]; + + if (!updatedUser) { + throw new Error("User details not updated"); + } + + dispatch(setUserDetails({ ...currentUser, ...updatedUser })); + toast({ + title: "Success", + description: "User details updated successfully", + }); + } catch (error) { + setError("Failed to update user details"); + toast({ + title: "Error", + description: "Failed to update user details", + }); + } finally { + setIsLoading(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( + <> +
{error}
+ + + ); + } + + if (!profile) { + return
No profile data available.
; + } + + return ( + <> +
+

Personal Information

+

+ Update your photo and personal details. +

+ +
+
+ + {profile.profilePicture ? ( + + ) : ( +
+ No Image +
+ )} +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +