integrate users

This commit is contained in:
rgrgogu
2026-05-06 14:14:54 +08:00
parent 569442bf33
commit f165e8b3bb
35 changed files with 2516 additions and 24 deletions
@@ -0,0 +1,201 @@
import { useState, useEffect, useMemo } from "react";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Spinner } from "@/components/ui/spinner";
// ─── Field-specific display maps ──────────────────────────────────────────────
const FIELD_DISPLAY_MAP = {
is_active: { true: "Active", false: "Inactive" },
is_verified: { true: "Verified", false: "Not Verified" },
};
const BOOLEAN_FIELDS = Object.keys(FIELD_DISPLAY_MAP);
// ─── Generic formatter ────────────────────────────────────────────────────────
const formatFilterItem = (item, field, type) => {
if (FIELD_DISPLAY_MAP[field]) {
return FIELD_DISPLAY_MAP[field][String(item)] ?? item;
}
if (type === "date" && item) {
return new Date(item).toLocaleDateString("en-US", {
year: "numeric", month: "long", day: "numeric",
});
}
return item;
};
// ─── Reusable empty state ─────────────────────────────────────────────────────
const EmptyState = ({ search }) => (
<p className="text-sm text-muted-foreground text-center py-4">
{search ? `No results for "${search}".` : "No data found."}
</p>
);
// ─── Reusable item list renderer ──────────────────────────────────────────────
const FilterList = ({ items, field, type, selected, onToggle, inputType = "checkbox" }) => {
if (items.length === 0) return <EmptyState />;
return items.map((item) => (
<label key={item} className="flex items-center gap-2 text-sm cursor-pointer">
<input
type={inputType}
name={inputType === "radio" ? field : undefined}
checked={selected.includes(String(item))}
onChange={() => onToggle(item)}
/>
{formatFilterItem(item, field, type)}
</label>
));
};
export function FilterSheet({ open, onOpenChange, column, attr, data = [], loading }) {
const type = attr?.type;
const field = attr?.field;
const enumData = attr?.options?.choices || [];
const [search, setSearch] = useState("");
const [selected, setSelected] = useState([]);
useEffect(() => {
const val = column?.getFilterValue();
setSelected(Array.isArray(val) ? val.map(String) : []);
}, [column]);
const sourceData = useMemo(() => {
if (BOOLEAN_FIELDS.includes(field)) return ["true", "false"];
if (type === "enum") return enumData;
return data || [];
}, [field, type, enumData, data]);
const filteredData = useMemo(() => {
if (!search) return sourceData;
return sourceData.filter((item) =>
formatFilterItem(item, field, type)
.toString()
.toLowerCase()
.includes(search.toLowerCase())
);
}, [search, sourceData, field, type]);
const toggle = (item) => {
setSelected((prev) =>
prev.includes(String(item))
? prev.filter((v) => v !== String(item))
: [...prev, String(item)]
);
};
const toggleRadio = (item) => setSelected([String(item)]);
const isBoolean = BOOLEAN_FIELDS.includes(field);
const isEnum = type === "enum" && !isBoolean;
const isList = !isBoolean && !isEnum;
const isEmpty = !loading && sourceData.length === 0;
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-[380px] flex flex-col">
<SheetHeader className="shrink-0 border-b">
<SheetTitle>Filter by {column?.columnDef?.header}</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-hidden flex flex-col gap-4 px-4 relative">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-background/60 backdrop-blur-sm z-10">
<Spinner className="size-8" />
</div>
)}
{/* ================= EMPTY STATE ================= */}
{isEmpty && <EmptyState />}
{/* ================= BOOLEAN ================= */}
{!isEmpty && isBoolean && (
<div className="space-y-2 overflow-auto pr-2 pt-2">
<FilterList
items={sourceData}
field={field}
type={type}
selected={selected}
onToggle={toggleRadio}
inputType="radio"
/>
</div>
)}
{/* ================= ENUM ================= */}
{!isEmpty && isEnum && (
<div className="space-y-2 overflow-auto pr-2 pt-2">
<FilterList
items={sourceData}
field={field}
type={type}
selected={selected}
onToggle={toggle}
/>
</div>
)}
{/* ========== TEXT / NUMBER / DATE ========== */}
{!isEmpty && isList && (
<>
<div>
<Input
placeholder={`Search ${type}...`}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<ScrollArea className="flex-1 h-[550px] pr-2">
<div className="space-y-2">
{filteredData.length === 0
? <EmptyState search={search} />
: <FilterList
items={filteredData}
field={field}
type={type}
selected={selected}
onToggle={toggle}
/>
}
</div>
<ScrollBar orientation="vertical" />
</ScrollArea>
</>
)}
</div>
{/* ─── Footer — hidden when empty ──────────────────────────────────── */}
{!isEmpty && (
<div className="shrink-0 flex gap-2 border-t p-4">
<Button
variant="outline"
className="flex-1"
onClick={() => {
setSelected([]);
column.setFilterValue(undefined);
onOpenChange(false);
}}
>
Clear
</Button>
<Button
className="flex-1"
onClick={() => {
column.setFilterValue(selected);
onOpenChange(false);
}}
>
Apply
</Button>
</div>
)}
</SheetContent>
</Sheet>
);
}