import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { Library, Loader2 } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Skeleton } from "@/components/ui/skeleton"; import { resourcesService } from "@/services/resources.service"; import type { Resource } from "@/types"; /** * Generic, reusable picker over `/api/resources`. * * Two callers wire this up today: * * 1. **AdminCoursePlanDetail** — for an *existing* plan, so it forwards * the picked resources to `coursePlanService.attachResources` in the * `onConfirm` handler and invalidates the sources query. * * 2. **CoursePlanWizard** — the plan does not exist yet at pick time, * so the wizard simply pushes the picks into its draft state and * attaches them in one batch after the plan is created. * * The dialog itself is purposefully agnostic: it only reports the * selected `Resource` rows; the parent decides what to do. */ export function LibraryPickerDialog({ open, onOpenChange, alreadyLinkedIds, onConfirm, isPending, }: { open: boolean; onOpenChange: (open: boolean) => void; /** Resources whose ids are in this set render disabled + checked. */ alreadyLinkedIds?: Set; /** Called when the admin clicks "Attach selected". */ onConfirm: (resources: Resource[]) => void | Promise; /** Optional spinner state from the parent's mutation. */ isPending?: boolean; }) { const { t } = useTranslation(); const [search, setSearch] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [selected, setSelected] = useState>(new Set()); // Reset every time the dialog re-opens so a previous session's // selection doesn't bleed into the next attach. useEffect(() => { if (open) { setSelected(new Set()); setSearch(""); setTypeFilter("all"); } }, [open]); const { data, isLoading } = useQuery({ queryKey: ["library-resources", { search, type: typeFilter }], queryFn: () => resourcesService.list({ search: search || undefined, resource_type: typeFilter === "all" ? undefined : typeFilter, review_status: "approved", }), enabled: open, }); const resources: Resource[] = data?.items ?? []; const toggle = (id: number) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const handleConfirm = async () => { const picked = resources.filter((r) => selected.has(r.id)); if (!picked.length) return; await onConfirm(picked); }; return ( {t("coursePlan.sources.libraryTitle")} {t("coursePlan.sources.libraryDescription")}
setSearch(e.target.value)} className="flex-1" />
{isLoading && } {!isLoading && resources.length === 0 && (

{t("coursePlan.sources.libraryEmpty")}

)} {!isLoading && resources.map((r) => { const isLinked = alreadyLinkedIds?.has(r.id) ?? false; const isSelected = selected.has(r.id); return ( ); })}
{t("coursePlan.sources.librarySelectedCount", { count: selected.size, })}
); }