diff --git a/netmanager-app/app/(authenticated)/sites/page.tsx b/netmanager-app/app/(authenticated)/sites/page.tsx index b978d31cb3..54961d6384 100644 --- a/netmanager-app/app/(authenticated)/sites/page.tsx +++ b/netmanager-app/app/(authenticated)/sites/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Plus, Search, Trash2 } from "lucide-react"; +import { Plus, Search, Loader2, ArrowUpDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -20,53 +20,157 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; import { SiteForm } from "@/app/(authenticated)/sites/site-form"; import { useRouter } from "next/navigation"; import { RouteGuard } from "@/components/route-guard"; +import { useSites } from "@/core/hooks/useSites"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Site } from "@/core/redux/slices/sitesSlice"; + +const ITEMS_PER_PAGE = 8; -// Sample data -const sites = [ - { - id: "site_528", - name: "Water and Environment House, Luzira", - description: "Water and Environment House, Luzira", - country: "Uganda", - district: "Kampala", - region: "Central Region", - }, - { - id: "site_527", - name: "All Saints Church, Nakasero", - description: "All Saints Church, Nakasero, Kampala, Uganda", - country: "Uganda", - district: "Kampala", - region: "Central Region", - }, - { - id: "site_526", - name: "Namungona Primary school", - description: "Namungona Primary school", - country: "Uganda", - district: "Kampala", - region: "Central Region", - }, -]; +type SortField = "name" | "description" | "region" | "isOnline"; +type SortOrder = "asc" | "desc"; export default function SitesPage() { const router = useRouter(); const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [sortField, setSortField] = useState("name"); + const [sortOrder, setSortOrder] = useState("asc"); + const { sites, isLoading, error } = useSites(); - const filteredSites = sites.filter( - (site) => - site.name.toLowerCase().includes(searchQuery.toLowerCase()) || - site.description.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const handleSort = (field: SortField) => { + if (sortField === field) { + // Toggle order if same field + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + // New field, set to ascending + setSortField(field); + setSortOrder("asc"); + } + }; + + const sortSites = (sitesToSort: Site[]) => { + return [...sitesToSort].sort((a, b) => { + let compareA = a[sortField].toString(); + let compareB = b[sortField].toString(); + + // Handle special cases + if (sortField === "isOnline") { + return sortOrder === "asc" + ? Number(b.isOnline) - Number(a.isOnline) + : Number(a.isOnline) - Number(b.isOnline); + } + + // Normal string comparison + if (typeof compareA === "string") { + compareA = compareA.toLowerCase(); + compareB = compareB.toLowerCase(); + } + + if (compareA < compareB) return sortOrder === "asc" ? -1 : 1; + if (compareA > compareB) return sortOrder === "asc" ? 1 : -1; + return 0; + }); + }; + + const filteredSites = sites.filter((site: Site) => { + const searchLower = searchQuery.toLowerCase(); + return ( + site.name?.toLowerCase().includes(searchLower) || + site.location_name?.toLowerCase().includes(searchLower) || + site.generated_name?.toLowerCase().includes(searchLower) || + site.formatted_name?.toLowerCase().includes(searchLower) + ); + }); + + const sortedSites = sortSites(filteredSites); + + // Pagination calculations + const totalPages = Math.ceil(sortedSites.length / ITEMS_PER_PAGE); + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; + const endIndex = startIndex + ITEMS_PER_PAGE; + const currentSites = sortedSites.slice(startIndex, endIndex); + + // Generate page numbers for pagination + const getPageNumbers = () => { + const pageNumbers = []; + const maxVisiblePages = 5; + + if (totalPages <= maxVisiblePages) { + // Show all pages if total pages is less than max visible + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + // Show pages with ellipsis + 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; + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ + + Error + {error.message} + +
+ ); + } return (
-

Site Registry for AirQo

+

Site Registry

-
+
setSearchQuery(e.target.value)} />
+ + + + + + handleSort("name")}> + Name {sortField === "name" && (sortOrder === "asc" ? "↑" : "↓")} + + handleSort("description")}> + Description{" "} + {sortField === "description" && + (sortOrder === "asc" ? "↑" : "↓")} + + handleSort("region")}> + Region{" "} + {sortField === "region" && (sortOrder === "asc" ? "↑" : "↓")} + + handleSort("isOnline")}> + Status{" "} + {sortField === "isOnline" && (sortOrder === "asc" ? "↑" : "↓")} + + +
- Name + handleSort("name")} + > + Name{" "} + {sortField === "name" && (sortOrder === "asc" ? "↑" : "↓")} + Site ID - Description + handleSort("description")} + > + Description{" "} + {sortField === "description" && + (sortOrder === "asc" ? "↑" : "↓")} + Country District - Region - Actions + handleSort("region")} + > + Region{" "} + {sortField === "region" && (sortOrder === "asc" ? "↑" : "↓")} + + handleSort("isOnline")} + > + Status{" "} + {sortField === "isOnline" && + (sortOrder === "asc" ? "↑" : "↓")} + + {/* Actions */} - {filteredSites.map((site) => ( + {currentSites.map((site: Site) => ( router.push(`/sites/${site.id}`)} + onClick={() => router.push(`/sites/${site._id}`)} > {site.name} - {site.id} + {site.generated_name} {site.description} {site.country} {site.district} {site.region} + + {site.isOnline ? "Online" : "Offline"} + + + {/* - + */} ))} + {currentSites.length === 0 && ( + + + No sites found + + + )}
+ + {/* Pagination */} + {sortedSites.length > 0 && ( +
+ + + + + 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" + } + /> + + + +
+ )}
); diff --git a/netmanager-app/components/sidebar.tsx b/netmanager-app/components/sidebar.tsx index 0c8ff370c4..439b1ff697 100644 --- a/netmanager-app/components/sidebar.tsx +++ b/netmanager-app/components/sidebar.tsx @@ -53,7 +53,6 @@ const Sidebar = () => { const availableNetworks = useAppSelector( (state) => state.user.availableNetworks ); - console.log(availableNetworks); const activeNetwork = useAppSelector((state) => state.user.activeNetwork); const isActive = (path: string) => pathname?.startsWith(path); diff --git a/netmanager-app/components/ui/pagination.tsx b/netmanager-app/components/ui/pagination.tsx new file mode 100644 index 0000000000..46ae77674d --- /dev/null +++ b/netmanager-app/components/ui/pagination.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { ButtonProps, buttonVariants } from "@/components/ui/button"; + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +