Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into mhkim-node
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user