pushy

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-28 11:30:01 +08:00
parent b03b204861
commit bac7168b1e
100 changed files with 5958 additions and 1976 deletions
+42 -15
View File
@@ -6,6 +6,7 @@ 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";
import { AlertTriangle } from "lucide-react";
/**
* Generic sheet for selecting and adding users to any entity
@@ -25,6 +26,8 @@ import { Spinner } from "@/components/ui/spinner";
* @param {string} [props.idKey] Key for the user id. Default: "user_id"
* @param {string} [props.labelKey] Key for the display name. Default: "full_name"
* @param {string} [props.subLabelKey] Optional secondary line (e.g. "email")
* @param {string} [props.warningKey] If set, users with a truthy value at this key show
* a "will be moved" warning (e.g. "current_group")
*
* @param {Function} props.onSubmit Called with selected ids[]
*
@@ -63,6 +66,7 @@ export function AddSheet({
idKey = "user_id",
labelKey = "full_name",
subLabelKey = null,
warningKey = null,
onSubmit,
}) {
@@ -102,6 +106,14 @@ export function AddSheet({
const allFilteredSelected =
filtered.length > 0 && filtered.every((u) => selected.includes(u[idKey]));
const movingCount = useMemo(() => {
if (!warningKey) return 0;
return selected.filter((id) => {
const user = users.find((u) => u[idKey] === id);
return !!user?.[warningKey];
}).length;
}, [selected, users, warningKey, idKey]);
async function handleSubmit() {
if (!selected.length) return;
await onSubmit(selected);
@@ -166,9 +178,10 @@ export function AddSheet({
</p>
) : (
filtered.map((user) => {
const id = user[idKey];
const label = user[labelKey];
const sub = subLabelKey ? user[subLabelKey] : null;
const id = user[idKey];
const label = user[labelKey];
const sub = subLabelKey ? user[subLabelKey] : null;
const warning = warningKey ? user[warningKey] : null;
return (
<label
@@ -180,13 +193,19 @@ export function AddSheet({
checked={selected.includes(id)}
onChange={() => toggle(id)}
/>
<div className="flex flex-col min-w-0">
<div className="flex flex-col min-w-0 flex-1">
<span className="truncate">{label}</span>
{sub && (
<span className="text-xs text-muted-foreground truncate">
{sub}
</span>
)}
{warning && (
<span className="text-xs text-amber-600 flex items-center gap-1 mt-0.5">
<AlertTriangle className="size-3 shrink-0" />
Also in: {warning}
</span>
)}
</div>
</label>
);
@@ -197,17 +216,25 @@ export function AddSheet({
</div>
{/* Footer */}
<div className="shrink-0 flex gap-2 border-t px-6 py-4">
<Button variant="outline" className="flex-1" onClick={handleClose}>
Cancel
</Button>
<Button
className="flex-1"
disabled={!selected.length || loading}
onClick={handleSubmit}
>
{selected.length > 0 ? `${submitLabel} (${selected.length})` : submitLabel}
</Button>
<div className="shrink-0 flex flex-col gap-2 border-t px-6 py-4">
{movingCount > 0 && (
<p className="text-xs text-amber-600 flex items-center gap-1.5">
<AlertTriangle className="size-3 shrink-0" />
{movingCount} user{movingCount > 1 ? 's' : ''} already belong{movingCount === 1 ? 's' : ''} to other group{movingCount > 1 ? 's' : ''}.
</p>
)}
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={handleClose}>
Cancel
</Button>
<Button
className="flex-1"
disabled={!selected.length || loading}
onClick={handleSubmit}
>
{selected.length > 0 ? `${submitLabel} (${selected.length})` : submitLabel}
</Button>
</div>
</div>
</SheetContent>
+5 -5
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useMemo } from "react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -14,14 +15,12 @@ const FIELD_DISPLAY_MAP = {
const BOOLEAN_FIELDS = Object.keys(FIELD_DISPLAY_MAP);
// ─── Generic formatter ────────────────────────────────────────────────────────
const formatFilterItem = (item, field, type) => {
const formatFilterItem = (item, field, type, fmtDate) => {
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 fmtDate(item);
}
return item;
};
@@ -35,6 +34,7 @@ const EmptyState = ({ search }) => (
// ─── Reusable item list renderer ──────────────────────────────────────────────
const FilterList = ({ items, field, type, selected, onToggle, inputType = "checkbox" }) => {
const { fmtDate } = useDateFormat();
if (items.length === 0) return <EmptyState />;
return items.map((item) => (
@@ -45,7 +45,7 @@ const FilterList = ({ items, field, type, selected, onToggle, inputType = "check
checked={selected.includes(String(item))}
onChange={() => onToggle(item)}
/>
{formatFilterItem(item, field, type)}
{formatFilterItem(item, field, type, fmtDate)}
</label>
));
};