ERP-node/frontend/components/barcode/designer/BarcodeTemplatePalette.tsx

133 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-03-04 20:51:00 +09:00
"use client";
import { useState, useEffect, useMemo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Loader2, Search } from "lucide-react";
import { useBarcodeDesigner } from "@/contexts/BarcodeDesignerContext";
import { barcodeApi, BarcodeLabelTemplate } from "@/lib/api/barcodeApi";
type Category = "all" | "basic" | "zebra";
export function BarcodeTemplatePalette() {
const { applyTemplate } = useBarcodeDesigner();
const [templates, setTemplates] = useState<BarcodeLabelTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [category, setCategory] = useState<Category>("all");
const [searchText, setSearchText] = useState("");
useEffect(() => {
(async () => {
try {
const res = await barcodeApi.getTemplates();
if (res.success && res.data) setTemplates(res.data);
} catch {
setTemplates([]);
} finally {
setLoading(false);
}
})();
}, []);
const filtered = useMemo(() => {
let list = templates;
if (category === "basic") {
list = list.filter((t) => t.template_id.startsWith("TMPL_"));
} else if (category === "zebra") {
list = list.filter((t) => t.template_id.startsWith("ZJ"));
}
const q = searchText.trim().toLowerCase();
if (q) {
list = list.filter(
(t) =>
t.template_id.toLowerCase().includes(q) ||
(t.template_name_kor && t.template_name_kor.toLowerCase().includes(q)) ||
(t.template_name_eng && t.template_name_eng.toLowerCase().includes(q))
);
}
return list;
}, [templates, category, searchText]);
if (loading) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm"> </CardTitle>
</CardHeader>
<CardContent className="flex justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm"> </CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="relative">
<Search className="text-muted-foreground absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2" />
<Input
placeholder="코드·이름으로 찾기"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="h-8 pl-8 text-xs"
/>
</div>
<div className="flex gap-1">
<Button
variant={category === "all" ? "secondary" : "ghost"}
size="sm"
className="flex-1 text-xs"
onClick={() => setCategory("all")}
>
</Button>
<Button
variant={category === "basic" ? "secondary" : "ghost"}
size="sm"
className="flex-1 text-xs"
onClick={() => setCategory("basic")}
>
</Button>
<Button
variant={category === "zebra" ? "secondary" : "ghost"}
size="sm"
className="flex-1 text-xs"
onClick={() => setCategory("zebra")}
>
</Button>
</div>
<ScrollArea className="h-[280px] pr-2">
<div className="space-y-1">
{filtered.length === 0 ? (
<p className="text-muted-foreground py-2 text-center text-xs"> </p>
) : (
filtered.map((t) => (
<Button
key={t.template_id}
variant="outline"
size="sm"
className="h-auto w-full justify-start py-1.5 text-left"
onClick={() => applyTemplate(t.template_id)}
>
<span className="truncate">{t.template_name_kor}</span>
<span className="text-muted-foreground ml-1 shrink-0 text-xs">
{t.width_mm}×{t.height_mm}
</span>
</Button>
))
)}
</div>
</ScrollArea>
</CardContent>
</Card>
);
}