239 lines
9.8 KiB
TypeScript
Executable File
239 lines
9.8 KiB
TypeScript
Executable File
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 "{debouncedSearchTerm}"</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;
|