satupeta-main/app/(modules)/maps/components/mapset-dialog/mapset-list/index.tsx
2026-01-27 09:31:12 +07:00

239 lines
9.8 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Layers, Filter } from "lucide-react";
import SearchInput from "@/shared/components/search-input";
import { useQueryParam, StringParam } from "use-query-params";
import GroupMapset from "./group-mapset";
import { useQuery } from "@tanstack/react-query";
import organizationApi from "@/shared/services/organization";
import categoryApi from "@/shared/services/category"; // Add this import
import { useAtom } from "jotai";
import { activeTabAtom } from "../../../state/active-tab";
import { selectedMapsetAtom } from "../../../state/mapset-dialog";
import { Organization } from "@/shared/types/organization";
import { Category } from "@/shared/types/category";
import { Button } from "@/shared/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select";
const MapsetList: React.FC = () => {
const [query] = useQueryParam("query", StringParam);
const [producerId, setProducerId] = useQueryParam("producer_id", StringParam);
const [categoryId, setCategoryId] = useQueryParam("category_id", StringParam);
const [searchTerm, setSearchTerm] = useState(query || "");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
const [isSearching, setIsSearching] = useState(false);
const [activeTab] = useAtom(activeTabAtom);
const [selectedMapset, setSelectedMapset] = useAtom(selectedMapsetAtom);
const [orgFilterEnabled, setOrgFilterEnabled] = useState<boolean>(false);
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
const [pendingOrgId, setPendingOrgId] = useState<string | undefined>(undefined);
const { data: organizations, isLoading: isLoadingOrganizations } = useQuery({
queryKey: ["organizations"],
queryFn: () => organizationApi.getOrganizations(undefined, { skipAuth: true }).then((res) => res.items),
enabled: activeTab === "organization",
});
const { data: singleOrganization, isLoading: isLoadingSingleOrganization } = useQuery({
queryKey: ["organization", producerId],
queryFn: () => organizationApi.getOrganizationById(producerId as string, { skipAuth: true }),
enabled: activeTab === "organization" && !!producerId && orgFilterEnabled,
});
// When a specific category_id is provided (coming from CategorySection), fetch only that category
const { data: singleCategory, isLoading: isLoadingSingleCategory } = useQuery({
queryKey: ["category", categoryId],
queryFn: () => categoryApi.getCategoryById(categoryId as string, { skipAuth: true }),
enabled: activeTab === "category" && !!categoryId,
});
const { data: categories, isLoading: isLoadingCategories } = useQuery({
queryKey: ["categories"],
queryFn: () => categoryApi.getCategories(undefined, { skipAuth: true }).then((res) => res.items),
enabled: activeTab === "category" && !categoryId,
});
const isDataLoading =
(activeTab === "organization" && (isLoadingOrganizations || (producerId && orgFilterEnabled ? isLoadingSingleOrganization : false))) ||
(activeTab === "category" && (categoryId ? isLoadingSingleCategory : isLoadingCategories));
let items: (Organization | Category)[] = [];
if (activeTab === "organization" && producerId && orgFilterEnabled) {
items = singleOrganization ? [singleOrganization] : [];
} else if (activeTab === "organization") {
items = organizations || [];
} else if (activeTab === "category") {
items = categoryId ? (singleCategory ? [singleCategory] : []) : categories || [];
}
useEffect(() => {
setSearchTerm(query || "");
}, [query]);
useEffect(() => {
setIsSearching(true);
const handler = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
setIsSearching(false);
}, 300);
return () => {
clearTimeout(handler);
};
}, [searchTerm]);
useEffect(() => {
if (activeTab === "organization" && producerId) {
setOrgFilterEnabled(true);
}
}, [activeTab, producerId]);
const handleSearch = (value: string) => {
setSearchTerm(value);
};
if (isDataLoading) {
return (
<div className="p-4 h-full flex flex-col max-w-xl mx-auto rounded-l-lg">
<div className="mb-4">
<SearchInput value={searchTerm} onChange={handleSearch} placeholder="Cari dataset" />
</div>
<div className="flex flex-1 flex-col items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
<p className="mt-4 text-gray-500">{`Loading ${activeTab === "organization" ? "organizations" : "categories"}...`}</p>
</div>
</div>
);
}
return (
<div className="p-4 h-full flex flex-col max-w-xl mx-auto rounded-l-lg">
<div className="mb-4 flex justify-between items-center">
<div className="w-full mr-3">
<SearchInput value={searchTerm} onChange={handleSearch} placeholder="Cari dataset" />
{activeTab === "category" && categoryId && (
<div className="mt-2 flex items-center gap-2 text-xs text-zinc-700">
<span className="inline-flex items-center rounded-full border border-zinc-300 bg-white px-2 py-0.5">
<span className="font-medium mr-1">Filter:</span>
<span>Kategori</span>
{singleCategory?.name ? <span className="mx-1"></span> : null}
{singleCategory?.name ? <span className="truncate max-w-[240px]">{singleCategory.name}</span> : null}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCategoryId(undefined)}
className="h-6 px-2 py-0"
>
Hapus Filter
</Button>
</div>
)}
</div>
{activeTab === "organization" && (
<Popover
open={popoverOpen}
onOpenChange={(open) => {
setPopoverOpen(open);
if (open) {
setPendingOrgId((producerId as string) || undefined);
}
}}
>
{false && <></>}
<PopoverTrigger asChild>
<Button size="icon" variant={producerId && orgFilterEnabled ? "default" : "outline"} className="w-9 h-9" title="Filter organisasi">
<Filter className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-64 p-3 z-[600]"
align="end"
onInteractOutside={(e) => {
const target = e.target as HTMLElement;
if (target && target.closest('[data-slot="select-content"]')) {
e.preventDefault();
}
}}
>
<div className="space-y-3">
<div className="text-sm font-medium">Filter organisasi</div>
<Select value={pendingOrgId} onValueChange={(val) => setPendingOrgId(val)}>
<SelectTrigger className="w-full min-w-0">
<SelectValue className="truncate flex-1" placeholder="Pilih organisasi" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto z-[700]">
{organizations?.map((org) => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex justify-between gap-4">
<Button
size="sm"
variant="outline"
onClick={() => {
setProducerId(undefined);
setOrgFilterEnabled(false);
setPendingOrgId(undefined);
}}
>
Reset
</Button>
<Button
size="sm"
variant="default"
onClick={() => {
if (pendingOrgId) {
setProducerId(pendingOrgId);
setOrgFilterEnabled(true);
}
setPopoverOpen(false);
}}
disabled={!pendingOrgId}
>
Terapkan
</Button>
</div>
</div>
</PopoverContent>
</Popover>
)}
</div>
{isSearching ? (
<div className="flex flex-1 flex-col items-center justify-center text-gray-500 bg-gray-50 rounded-lg">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-gray-500"></div>
<p className="mt-3">Mencari dataset...</p>
</div>
) : items?.length === 0 ? (
<div className="flex flex-1 flex-col items-center justify-center text-gray-500 bg-gray-50 rounded-lg">
<Layers size={48} className="mb-2 opacity-50" />
<p>No layers found matching &quot;{debouncedSearchTerm}&quot;</p>
</div>
) : (
<div className="overflow-y-auto pr-1 flex flex-col space-y-2">
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items?.map((item: any) => (
<GroupMapset
key={item.id}
item={item}
type={activeTab}
// When filtering to a single category from CategorySection, don't apply the global search to mapsets
search={activeTab === "category" && categoryId ? "" : debouncedSearchTerm}
selectedMapset={selectedMapset}
setSelectedMapset={setSelectedMapset}
/>
))
}
</div>
)}
</div>
);
};
export default MapsetList;