Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into mhkim-node

This commit is contained in:
kmh
2026-03-25 14:36:47 +09:00
+95 -107
View File
@@ -15,10 +15,12 @@ import {
ResizableHandle, ResizablePanel, ResizablePanelGroup,
} from "@/components/ui/resizable";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandInput, CommandList, CommandEmpty, CommandItem } from "@/components/ui/command";
import { FullscreenDialog } from "@/components/common/FullscreenDialog";
import { useConfirmDialog } from "@/components/common/ConfirmDialog";
import {
Search, Plus, Trash2, RotateCcw, Loader2, Package, Box, X, Save, Edit2, Download,
Search, Plus, Trash2, RotateCcw, Loader2, Package, Box, X, Save, Edit2, Download, ChevronsUpDown, Check,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
@@ -85,13 +87,13 @@ export default function PackagingPage() {
const [pkgModalMode, setPkgModalMode] = useState<"create" | "edit">("create");
const [pkgForm, setPkgForm] = useState<Record<string, any>>({});
const [pkgItemOptions, setPkgItemOptions] = useState<ItemInfoForPkg[]>([]);
const [pkgItemSearchKw, setPkgItemSearchKw] = useState("");
const [pkgItemPopoverOpen, setPkgItemPopoverOpen] = useState(false);
const [loadModalOpen, setLoadModalOpen] = useState(false);
const [loadModalMode, setLoadModalMode] = useState<"create" | "edit">("create");
const [loadForm, setLoadForm] = useState<Record<string, any>>({});
const [loadItemOptions, setLoadItemOptions] = useState<ItemInfoForPkg[]>([]);
const [loadItemSearchKw, setLoadItemSearchKw] = useState("");
const [loadItemPopoverOpen, setLoadItemPopoverOpen] = useState(false);
const [itemMatchModalOpen, setItemMatchModalOpen] = useState(false);
const [itemMatchKeyword, setItemMatchKeyword] = useState("");
@@ -166,19 +168,16 @@ export default function PackagingPage() {
} else {
setPkgForm({ pkg_code: "", pkg_name: "", pkg_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", volume_l: "", remarks: "" });
}
setPkgItemSearchKw("");
setPkgItemOptions([]);
setPkgItemPopoverOpen(false);
try {
const res = await getItemsByDivision("포장재");
if (res.success) setPkgItemOptions(res.data);
} catch { setPkgItemOptions([]); }
setPkgModalOpen(true);
};
const searchPkgItems = async (kw?: string) => {
try {
const res = await getItemsByDivision("포장재", kw || undefined);
if (res.success) setPkgItemOptions(res.data);
} catch { setPkgItemOptions([]); }
};
const onPkgItemSelect = (item: ItemInfoForPkg) => {
setPkgItemPopoverOpen(false);
const dims = parseSpecDimensions(item.size);
setPkgForm((prev) => ({
...prev,
@@ -224,19 +223,16 @@ export default function PackagingPage() {
} else {
setLoadForm({ loading_code: "", loading_name: "", loading_type: "", status: "ACTIVE", width_mm: "", length_mm: "", height_mm: "", self_weight_kg: "", max_load_kg: "", max_stack: "", remarks: "" });
}
setLoadItemSearchKw("");
setLoadItemOptions([]);
setLoadItemPopoverOpen(false);
try {
const res = await getItemsByDivision("적재함");
if (res.success) setLoadItemOptions(res.data);
} catch { setLoadItemOptions([]); }
setLoadModalOpen(true);
};
const searchLoadItems = async (kw?: string) => {
try {
const res = await getItemsByDivision("적재함", kw || undefined);
if (res.success) setLoadItemOptions(res.data);
} catch { setLoadItemOptions([]); }
};
const onLoadItemSelect = (item: ItemInfoForPkg) => {
setLoadItemPopoverOpen(false);
const dims = parseSpecDimensions(item.size);
setLoadForm((prev) => ({
...prev,
@@ -275,9 +271,13 @@ export default function PackagingPage() {
};
// --- 품목 추가 모달 (포장재 매칭) ---
const openItemMatchModal = () => {
setItemMatchKeyword(""); setItemMatchResults([]); setItemMatchSelected(null); setItemMatchQty(1);
const openItemMatchModal = async () => {
setItemMatchKeyword(""); setItemMatchSelected(null); setItemMatchQty(1);
setItemMatchModalOpen(true);
try {
const res = await getGeneralItems();
if (res.success) setItemMatchResults(res.data);
} catch { setItemMatchResults([]); }
};
const searchItemsForMatch = async () => {
@@ -635,44 +635,37 @@ export default function PackagingPage() {
{pkgModalMode === "create" && (
<div className="rounded-lg border bg-blue-50 dark:bg-blue-950/20 p-4">
<Label className="text-xs font-semibold mb-2 block"> (구분: 포장재)</Label>
<div className="flex gap-2 mb-2">
<Input
placeholder="품목코드 또는 품목명으로 검색"
value={pkgItemSearchKw}
onChange={(e) => setPkgItemSearchKw(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && searchPkgItems(pkgItemSearchKw)}
className="h-9 text-xs flex-1"
/>
<Button size="sm" onClick={() => searchPkgItems(pkgItemSearchKw)} className="h-9">
<Search className="mr-1 h-3 w-3" />
</Button>
</div>
{pkgItemOptions.length > 0 && (
<div className="max-h-[150px] overflow-auto border rounded">
<Table>
<TableHeader>
<TableRow className="text-[10px]">
<TableHead className="p-1.5 w-[30px]" />
<TableHead className="p-1.5"></TableHead>
<TableHead className="p-1.5"></TableHead>
<TableHead className="p-1.5 w-[80px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<Popover open={pkgItemPopoverOpen} onOpenChange={setPkgItemPopoverOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="w-full justify-between h-9 text-sm font-normal">
{pkgForm.pkg_code
? `${pkgForm.pkg_name} (${pkgForm.pkg_code})`
: "품목정보에서 포장재를 선택하세요"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
<Command filter={(value, search) => {
const item = pkgItemOptions.find((i) => i.id === value);
if (!item) return 0;
const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase();
return target.includes(search.toLowerCase()) ? 1 : 0;
}}>
<CommandInput placeholder="품목코드 / 품목명 검색..." />
<CommandList className="max-h-[200px]">
<CommandEmpty> </CommandEmpty>
{pkgItemOptions.map((item) => (
<TableRow key={item.id} className={cn("cursor-pointer text-xs", pkgForm.pkg_code === item.item_number && "bg-primary/10")}
onClick={() => onPkgItemSelect(item)}>
<TableCell className="p-1.5 text-center text-[10px]">{pkgForm.pkg_code === item.item_number ? "✓" : ""}</TableCell>
<TableCell className="p-1.5 font-medium">{item.item_number}</TableCell>
<TableCell className="p-1.5">{item.item_name}</TableCell>
<TableCell className="p-1.5 text-[10px]">{item.size || "-"}</TableCell>
</TableRow>
<CommandItem key={item.id} value={item.id} onSelect={() => onPkgItemSelect(item)} className="text-xs">
<Check className={cn("mr-2 h-3 w-3", pkgForm.pkg_code === item.item_number ? "opacity-100" : "opacity-0")} />
<span className="font-medium">{item.item_name}</span>
<span className="ml-2 text-muted-foreground">{item.item_number}</span>
{item.size && <span className="ml-auto text-[10px] text-muted-foreground">{item.size}</span>}
</CommandItem>
))}
</TableBody>
</Table>
</div>
)}
{pkgItemOptions.length === 0 && <p className="text-xs text-muted-foreground"> </p>}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)}
<div className="grid grid-cols-2 gap-4">
@@ -723,44 +716,37 @@ export default function PackagingPage() {
{loadModalMode === "create" && (
<div className="rounded-lg border bg-green-50 dark:bg-green-950/20 p-4">
<Label className="text-xs font-semibold mb-2 block"> (구분: 적재함)</Label>
<div className="flex gap-2 mb-2">
<Input
placeholder="품목코드 또는 품목명으로 검색"
value={loadItemSearchKw}
onChange={(e) => setLoadItemSearchKw(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && searchLoadItems(loadItemSearchKw)}
className="h-9 text-xs flex-1"
/>
<Button size="sm" onClick={() => searchLoadItems(loadItemSearchKw)} className="h-9">
<Search className="mr-1 h-3 w-3" />
</Button>
</div>
{loadItemOptions.length > 0 && (
<div className="max-h-[150px] overflow-auto border rounded">
<Table>
<TableHeader>
<TableRow className="text-[10px]">
<TableHead className="p-1.5 w-[30px]" />
<TableHead className="p-1.5"></TableHead>
<TableHead className="p-1.5"></TableHead>
<TableHead className="p-1.5 w-[80px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<Popover open={loadItemPopoverOpen} onOpenChange={setLoadItemPopoverOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="w-full justify-between h-9 text-sm font-normal">
{loadForm.loading_code
? `${loadForm.loading_name} (${loadForm.loading_code})`
: "품목정보에서 적재함을 선택하세요"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
<Command filter={(value, search) => {
const item = loadItemOptions.find((i) => i.id === value);
if (!item) return 0;
const target = `${item.item_number} ${item.item_name} ${item.size || ""}`.toLowerCase();
return target.includes(search.toLowerCase()) ? 1 : 0;
}}>
<CommandInput placeholder="품목코드 / 품목명 검색..." />
<CommandList className="max-h-[200px]">
<CommandEmpty> </CommandEmpty>
{loadItemOptions.map((item) => (
<TableRow key={item.id} className={cn("cursor-pointer text-xs", loadForm.loading_code === item.item_number && "bg-primary/10")}
onClick={() => onLoadItemSelect(item)}>
<TableCell className="p-1.5 text-center text-[10px]">{loadForm.loading_code === item.item_number ? "✓" : ""}</TableCell>
<TableCell className="p-1.5 font-medium">{item.item_number}</TableCell>
<TableCell className="p-1.5">{item.item_name}</TableCell>
<TableCell className="p-1.5 text-[10px]">{item.size || "-"}</TableCell>
</TableRow>
<CommandItem key={item.id} value={item.id} onSelect={() => onLoadItemSelect(item)} className="text-xs">
<Check className={cn("mr-2 h-3 w-3", loadForm.loading_code === item.item_number ? "opacity-100" : "opacity-0")} />
<span className="font-medium">{item.item_name}</span>
<span className="ml-2 text-muted-foreground">{item.item_number}</span>
{item.size && <span className="ml-auto text-[10px] text-muted-foreground">{item.size}</span>}
</CommandItem>
))}
</TableBody>
</Table>
</div>
)}
{loadItemOptions.length === 0 && <p className="text-xs text-muted-foreground"> </p>}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
)}
<div className="grid grid-cols-2 gap-4">
@@ -798,7 +784,7 @@ export default function PackagingPage() {
{/* 품목 추가 모달 (포장재 매칭) */}
<Dialog open={itemMatchModalOpen} onOpenChange={setItemMatchModalOpen}>
<DialogContent className="max-w-[650px]">
<DialogContent className="max-w-[900px]">
<DialogHeader>
<DialogTitle> {selectedPkg?.pkg_name}</DialogTitle>
<DialogDescription> .</DialogDescription>
@@ -809,27 +795,29 @@ export default function PackagingPage() {
onKeyDown={(e) => e.key === "Enter" && searchItemsForMatch()} className="h-9 text-xs" />
<Button size="sm" onClick={searchItemsForMatch} className="h-9"><Search className="mr-1 h-3 w-3" /> </Button>
</div>
<div className="max-h-[200px] overflow-auto border rounded">
<div className="max-h-[300px] overflow-auto border rounded">
<Table>
<TableHeader>
<TableRow className="text-[11px]">
<TableHead className="p-2 w-[30px]" />
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[130px]"></TableHead>
<TableHead className="p-2"></TableHead>
<TableHead className="p-2 w-[70px]"></TableHead>
<TableHead className="p-2 w-[100px]"></TableHead>
<TableHead className="p-2 w-[80px]"></TableHead>
<TableHead className="p-2 w-[50px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{itemMatchResults.length === 0 ? (
<TableRow><TableCell colSpan={5} className="text-center text-muted-foreground text-xs h-16"> </TableCell></TableRow>
<TableRow><TableCell colSpan={6} className="text-center text-muted-foreground text-xs h-16"> </TableCell></TableRow>
) : itemMatchResults.map((item) => (
<TableRow key={item.id} className={cn("cursor-pointer text-xs", itemMatchSelected?.id === item.id && "bg-primary/10")}
onClick={() => setItemMatchSelected(item)}>
<TableCell className="p-2 text-center">{itemMatchSelected?.id === item.id ? "✓" : ""}</TableCell>
<TableCell className="p-2 font-medium">{item.item_number}</TableCell>
<TableCell className="p-2">{item.item_name}</TableCell>
<TableCell className="p-2">{item.spec || "-"}</TableCell>
<TableCell className="p-2 font-medium truncate max-w-[130px]">{item.item_number}</TableCell>
<TableCell className="p-2 truncate max-w-[200px]">{item.item_name}</TableCell>
<TableCell className="p-2 truncate">{item.spec || "-"}</TableCell>
<TableCell className="p-2 truncate">{item.material || "-"}</TableCell>
<TableCell className="p-2">{item.unit || "EA"}</TableCell>
</TableRow>
))}
@@ -848,8 +836,8 @@ export default function PackagingPage() {
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setItemMatchModalOpen(false)}></Button>
<Button onClick={saveItemMatch} disabled={saving || !itemMatchSelected}>{saving ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : null} </Button>
<Button type="button" variant="outline" onClick={() => setItemMatchModalOpen(false)}></Button>
<Button type="button" data-action-type="custom" onClick={saveItemMatch} disabled={saving || !itemMatchSelected}>{saving ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : null} </Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -899,8 +887,8 @@ export default function PackagingPage() {
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setPkgMatchModalOpen(false)}></Button>
<Button onClick={savePkgMatch} disabled={saving || !pkgMatchSelected}>{saving ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : null} </Button>
<Button type="button" variant="outline" onClick={() => setPkgMatchModalOpen(false)}></Button>
<Button type="button" data-action-type="custom" onClick={savePkgMatch} disabled={saving || !pkgMatchSelected}>{saving ? <Loader2 className="mr-1 h-4 w-4 animate-spin" /> : null} </Button>
</DialogFooter>
</DialogContent>
</Dialog>