11
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -127,6 +127,7 @@ export default function SupplierManagementPage() {
|
||||
const [itemSelectOpen, setItemSelectOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [itemSearchResults, setItemSearchResults] = useState<any[]>([]);
|
||||
const [itemTotalCount, setItemTotalCount] = useState(0);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemCheckedIds, setItemCheckedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
@@ -817,12 +818,13 @@ export default function SupplierManagementPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const allItems = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
setItemTotalCount(allItems.length);
|
||||
const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number));
|
||||
const PURCHASE_CODE = "CAT_MMDJB7R4_TO3T";
|
||||
const PURCHASE_CODES = ["s"]; // 구매관리 카테고리 코드
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const div = item.division || "";
|
||||
return div.includes(PURCHASE_CODE) || div.includes("구매");
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => PURCHASE_CODES.includes(code));
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -911,27 +913,27 @@ export default function SupplierManagementPage() {
|
||||
[itemKey]: (prev[itemKey] || []).map((r) => {
|
||||
if (r._id !== rowId) return r;
|
||||
const updated = { ...r, [field]: value };
|
||||
if (["base_price", "discount_type", "discount_value", "rounding_unit_value"].includes(field)) {
|
||||
if (["base_price", "discount_type", "discount_value", "rounding_unit_value", "rounding_type"].includes(field)) {
|
||||
const bp = Number(updated.base_price) || 0;
|
||||
const dv = Number(updated.discount_value) || 0;
|
||||
const dt = updated.discount_type;
|
||||
let calc = bp;
|
||||
if (dt === "CAT_MLAMBEC8_URQA") calc = bp * (1 - dv / 100);
|
||||
else if (dt === "CAT_MLAMBLFM_JTLO") calc = bp - dv;
|
||||
// 절삭/반올림 적용
|
||||
// 반올림 유형 + 단위 적용
|
||||
const rv = updated.rounding_unit_value;
|
||||
const rt = updated.rounding_type;
|
||||
const roundOpts = priceCategoryOptions["rounding_unit_value"] || [];
|
||||
const roundLabel = roundOpts.find((o) => o.code === rv)?.label || "";
|
||||
if (roundLabel.includes("절삭") || roundLabel.includes("버림") || roundLabel.includes("floor")) {
|
||||
calc = Math.floor(calc);
|
||||
} else if (roundLabel.includes("올림") || roundLabel.includes("ceil")) {
|
||||
calc = Math.ceil(calc);
|
||||
} else if (roundLabel.includes("반올림") || roundLabel.includes("round")) {
|
||||
calc = Math.round(calc);
|
||||
} else if (rv) {
|
||||
// 단위 값 기반 (예: 10원 단위 절삭)
|
||||
const unit = Number(rv);
|
||||
if (!isNaN(unit) && unit > 0) calc = Math.floor(calc / unit) * unit;
|
||||
const unitOpts = priceCategoryOptions["rounding_type"] || [];
|
||||
const unitLabel = unitOpts.find((o) => o.code === rt)?.label || "";
|
||||
const unit = parseInt(unitLabel) || 1; // "10원" → 10, "100원" → 100
|
||||
if (roundLabel === "반올림") {
|
||||
calc = Math.round(calc / unit) * unit;
|
||||
} else if (roundLabel === "절삭") {
|
||||
calc = Math.floor(calc / unit) * unit;
|
||||
} else if (roundLabel === "올림") {
|
||||
calc = Math.ceil(calc / unit) * unit;
|
||||
}
|
||||
updated.calculated_price = String(Math.floor(calc));
|
||||
}
|
||||
@@ -2370,7 +2372,7 @@ export default function SupplierManagementPage() {
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>거래처에 추가할 품목을 선택하세요.</DialogDescription>
|
||||
<DialogDescription>공급업체에 추가할 품목을 선택하세요. (전체: {itemTotalCount}건 / 대상: {itemSearchResults.length}건{itemCheckedIds.size > 0 ? ` / 선택: ${itemCheckedIds.size}건` : ""})</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2 mb-3">
|
||||
<Input
|
||||
@@ -2616,66 +2618,74 @@ export default function SupplierManagementPage() {
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 기준가/할인/반올림 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.base_price_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "base_price_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="기준" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* 기준유형 + 기준가 */}
|
||||
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
|
||||
<Select
|
||||
value={price.base_price_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "base_price_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="기준유형" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={price.base_price ? Number(price.base_price).toLocaleString() : ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/[^\d.-]/g, "");
|
||||
updatePriceRow(itemKey, price._id, "base_price", raw);
|
||||
}}
|
||||
className="h-9 text-[13px] text-right flex-1"
|
||||
className="h-9 text-[13px] text-right col-span-3"
|
||||
placeholder="기준가"
|
||||
/>
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.discount_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "discount_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="할인" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">할인없음</SelectItem>
|
||||
{(priceCategoryOptions["discount_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 할인 + 반올림 */}
|
||||
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
|
||||
<Select
|
||||
value={price.discount_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "discount_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="할인" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">할인없음</SelectItem>
|
||||
{(priceCategoryOptions["discount_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={price.discount_value ? Number(price.discount_value).toLocaleString() : ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/[^\d.-]/g, "");
|
||||
updatePriceRow(itemKey, price._id, "discount_value", raw);
|
||||
}}
|
||||
className="h-9 text-[13px] text-right w-[60px]"
|
||||
className="h-9 text-[13px] text-right"
|
||||
placeholder="0"
|
||||
/>
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.rounding_unit_value}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_unit_value", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="반올림" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select
|
||||
value={price.rounding_unit_value}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_unit_value", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="반올림" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={price.rounding_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* 계산 단가 */}
|
||||
<div className="flex items-center justify-end gap-1.5 pt-2 border-t">
|
||||
|
||||
@@ -127,6 +127,7 @@ export default function CustomerManagementPage() {
|
||||
const [itemSelectOpen, setItemSelectOpen] = useState(false);
|
||||
const [itemSearchKeyword, setItemSearchKeyword] = useState("");
|
||||
const [itemSearchResults, setItemSearchResults] = useState<any[]>([]);
|
||||
const [itemTotalCount, setItemTotalCount] = useState(0);
|
||||
const [itemSearchLoading, setItemSearchLoading] = useState(false);
|
||||
const [itemCheckedIds, setItemCheckedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
@@ -817,12 +818,13 @@ export default function CustomerManagementPage() {
|
||||
autoFilter: true,
|
||||
});
|
||||
const allItems = res.data?.data?.data || res.data?.data?.rows || [];
|
||||
setItemTotalCount(allItems.length);
|
||||
const existingItemIds = new Set(priceItems.map((p: any) => p.item_id || p.item_number));
|
||||
const SALES_CODE = "CAT_ML8ZFVEL_1TOR";
|
||||
const SALES_CODES = ["CAT_ML8ZFVEL_1TOR"]; // 영업관리 카테고리 코드
|
||||
setItemSearchResults(allItems.filter((item: any) => {
|
||||
if (existingItemIds.has(item.item_number) || existingItemIds.has(item.id)) return false;
|
||||
const div = item.division || "";
|
||||
return div.includes(SALES_CODE) || div.includes("영업");
|
||||
const divCodes = (item.division || "").split(",").map((c: string) => c.trim());
|
||||
return divCodes.some((code: string) => SALES_CODES.includes(code));
|
||||
}));
|
||||
} catch { /* skip */ } finally { setItemSearchLoading(false); }
|
||||
};
|
||||
@@ -911,27 +913,27 @@ export default function CustomerManagementPage() {
|
||||
[itemKey]: (prev[itemKey] || []).map((r) => {
|
||||
if (r._id !== rowId) return r;
|
||||
const updated = { ...r, [field]: value };
|
||||
if (["base_price", "discount_type", "discount_value", "rounding_unit_value"].includes(field)) {
|
||||
if (["base_price", "discount_type", "discount_value", "rounding_unit_value", "rounding_type"].includes(field)) {
|
||||
const bp = Number(updated.base_price) || 0;
|
||||
const dv = Number(updated.discount_value) || 0;
|
||||
const dt = updated.discount_type;
|
||||
let calc = bp;
|
||||
if (dt === "CAT_MLAMBEC8_URQA") calc = bp * (1 - dv / 100);
|
||||
else if (dt === "CAT_MLAMBLFM_JTLO") calc = bp - dv;
|
||||
// 절삭/반올림 적용
|
||||
// 반올림 유형 + 단위 적용
|
||||
const rv = updated.rounding_unit_value;
|
||||
const rt = updated.rounding_type;
|
||||
const roundOpts = priceCategoryOptions["rounding_unit_value"] || [];
|
||||
const roundLabel = roundOpts.find((o) => o.code === rv)?.label || "";
|
||||
if (roundLabel.includes("절삭") || roundLabel.includes("버림") || roundLabel.includes("floor")) {
|
||||
calc = Math.floor(calc);
|
||||
} else if (roundLabel.includes("올림") || roundLabel.includes("ceil")) {
|
||||
calc = Math.ceil(calc);
|
||||
} else if (roundLabel.includes("반올림") || roundLabel.includes("round")) {
|
||||
calc = Math.round(calc);
|
||||
} else if (rv) {
|
||||
// 단위 값 기반 (예: 10원 단위 절삭)
|
||||
const unit = Number(rv);
|
||||
if (!isNaN(unit) && unit > 0) calc = Math.floor(calc / unit) * unit;
|
||||
const unitOpts = priceCategoryOptions["rounding_type"] || [];
|
||||
const unitLabel = unitOpts.find((o) => o.code === rt)?.label || "";
|
||||
const unit = parseInt(unitLabel) || 1; // "10원" → 10, "100원" → 100
|
||||
if (roundLabel === "반올림") {
|
||||
calc = Math.round(calc / unit) * unit;
|
||||
} else if (roundLabel === "절삭") {
|
||||
calc = Math.floor(calc / unit) * unit;
|
||||
} else if (roundLabel === "올림") {
|
||||
calc = Math.ceil(calc / unit) * unit;
|
||||
}
|
||||
updated.calculated_price = String(Math.floor(calc));
|
||||
}
|
||||
@@ -2368,7 +2370,7 @@ export default function CustomerManagementPage() {
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>품목 선택</DialogTitle>
|
||||
<DialogDescription>거래처에 추가할 품목을 선택하세요.</DialogDescription>
|
||||
<DialogDescription>거래처에 추가할 품목을 선택하세요. (전체: {itemTotalCount}건 / 대상: {itemSearchResults.length}건{itemCheckedIds.size > 0 ? ` / 선택: ${itemCheckedIds.size}건` : ""})</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex gap-2 mb-3">
|
||||
<Input
|
||||
@@ -2614,66 +2616,74 @@ export default function CustomerManagementPage() {
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 기준가/할인/반올림 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.base_price_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "base_price_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="기준" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* 기준유형 + 기준가 */}
|
||||
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
|
||||
<Select
|
||||
value={price.base_price_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "base_price_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="기준유형" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={price.base_price ? Number(price.base_price).toLocaleString() : ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/[^\d.-]/g, "");
|
||||
updatePriceRow(itemKey, price._id, "base_price", raw);
|
||||
}}
|
||||
className="h-9 text-[13px] text-right flex-1"
|
||||
className="h-9 text-[13px] text-right col-span-3"
|
||||
placeholder="기준가"
|
||||
/>
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.discount_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "discount_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="할인" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">할인없음</SelectItem>
|
||||
{(priceCategoryOptions["discount_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 할인 + 반올림 */}
|
||||
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
|
||||
<Select
|
||||
value={price.discount_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "discount_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="할인" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">할인없음</SelectItem>
|
||||
{(priceCategoryOptions["discount_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={price.discount_value ? Number(price.discount_value).toLocaleString() : ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/[^\d.-]/g, "");
|
||||
updatePriceRow(itemKey, price._id, "discount_value", raw);
|
||||
}}
|
||||
className="h-9 text-[13px] text-right w-[60px]"
|
||||
className="h-9 text-[13px] text-right"
|
||||
placeholder="0"
|
||||
/>
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.rounding_unit_value}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_unit_value", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="반올림" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select
|
||||
value={price.rounding_unit_value}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_unit_value", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="반올림" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={price.rounding_type}
|
||||
onValueChange={(v) => updatePriceRow(itemKey, price._id, "rounding_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-[13px]"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* 계산 단가 */}
|
||||
<div className="flex items-center justify-end gap-1.5 pt-2 border-t">
|
||||
|
||||
@@ -13,12 +13,13 @@ import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Users, Search, X, Settings2 } from "lucide-react";
|
||||
import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Users, Package, Search, X, Settings2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
@@ -55,7 +56,7 @@ const ITEM_GRID_COLUMNS = [
|
||||
|
||||
export default function SalesItemPage() {
|
||||
const { user } = useAuth();
|
||||
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
|
||||
const { confirm, ConfirmDialogComponent, isConfirmOpenRef } = useConfirmDialog();
|
||||
const ts = useTableSettings("c16-sales-item", ITEM_TABLE, ITEM_GRID_COLUMNS);
|
||||
|
||||
// 좌측: 품목
|
||||
@@ -361,14 +362,25 @@ export default function SalesItemPage() {
|
||||
[custKey]: (prev[custKey] || []).map((r) => {
|
||||
if (r._id !== rowId) return r;
|
||||
const updated = { ...r, [field]: value };
|
||||
if (["base_price", "discount_type", "discount_value"].includes(field)) {
|
||||
if (["base_price", "discount_type", "discount_value", "rounding_unit_value", "rounding_type"].includes(field)) {
|
||||
const bp = Number(updated.base_price) || 0;
|
||||
const dv = Number(updated.discount_value) || 0;
|
||||
const dt = updated.discount_type;
|
||||
let calc = bp;
|
||||
if (dt === "CAT_MLAMBEC8_URQA") calc = bp * (1 - dv / 100);
|
||||
else if (dt === "CAT_MLAMBLFM_JTLO") calc = bp - dv;
|
||||
updated.calculated_price = String(Math.round(calc));
|
||||
// 반올림 유형 + 단위 적용
|
||||
const rv = updated.rounding_unit_value;
|
||||
const rt = updated.rounding_type;
|
||||
const roundOpts = priceCategoryOptions["rounding_unit_value"] || [];
|
||||
const roundLabel = roundOpts.find((o) => o.code === rv)?.label || "";
|
||||
const unitOpts = priceCategoryOptions["rounding_type"] || [];
|
||||
const unitLabel = unitOpts.find((o) => o.code === rt)?.label || "";
|
||||
const unit = parseInt(unitLabel) || 1;
|
||||
if (roundLabel === "반올림") calc = Math.round(calc / unit) * unit;
|
||||
else if (roundLabel === "절삭") calc = Math.floor(calc / unit) * unit;
|
||||
else if (roundLabel === "올림") calc = Math.ceil(calc / unit) * unit;
|
||||
updated.calculated_price = String(Math.floor(calc));
|
||||
}
|
||||
return updated;
|
||||
}),
|
||||
@@ -619,8 +631,7 @@ export default function SalesItemPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-3 p-3">
|
||||
|
||||
<div className="flex h-full flex-col gap-3 p-4">
|
||||
{/* 검색 필터 (DynamicSearchFilter) */}
|
||||
<DynamicSearchFilter
|
||||
tableName={ITEM_TABLE}
|
||||
@@ -628,40 +639,48 @@ export default function SalesItemPage() {
|
||||
onFilterChange={setSearchFilters}
|
||||
dataCount={items.length}
|
||||
externalFilterConfig={ts.filterConfig}
|
||||
extraActions={
|
||||
<div className="flex gap-1.5">
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={() => setExcelUploadOpen(true)}>
|
||||
<FileSpreadsheet className="w-3.5 h-3.5" />
|
||||
엑셀 업로드
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={handleExcelDownload}>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* ── 마스터-디테일 분할 패널 ── */}
|
||||
<div className="flex-1 overflow-hidden border rounded-lg bg-background">
|
||||
{/* 액션 버튼 영역 */}
|
||||
<div className="flex items-center gap-2 px-4 shrink-0">
|
||||
<div className="flex gap-1.5 ml-auto">
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={() => setExcelUploadOpen(true)}>
|
||||
<FileSpreadsheet className="w-3.5 h-3.5 mr-1" />
|
||||
엑셀 업로드
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-8" onClick={handleExcelDownload}>
|
||||
<Download className="w-3.5 h-3.5 mr-1" />
|
||||
엑셀 다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 마스터-디테일 분할 패널 */}
|
||||
<div className="flex-1 overflow-hidden border rounded-lg bg-background shadow-sm">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
|
||||
{/* 좌측: 판매품목 목록 (마스터) */}
|
||||
{/* 좌측: 판매품목 목록 */}
|
||||
<ResizablePanel defaultSize={55} minSize={30}>
|
||||
<div className="flex flex-col h-full bg-muted">
|
||||
<div className="flex flex-col h-full">
|
||||
{/* 패널 헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b bg-muted shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-bold text-foreground">판매품목 목록</span>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 px-2 py-0.5 rounded-full">
|
||||
<div className="flex items-center justify-between px-4 h-[42px] border-b bg-muted shrink-0">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="text-[13px] font-bold">판매품목 목록</span>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/[0.08] px-2 py-0.5 rounded-full">
|
||||
{itemCount}건
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => ts.setOpen(true)}>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button variant="outline" size="sm" disabled={!selectedItemId} onClick={openEditItem}>
|
||||
<Pencil className="w-3.5 h-3.5 mr-1" /> 수정
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => ts.setOpen(true)}>
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* 테이블 영역 */}
|
||||
|
||||
{/* 거래처 테이블 */}
|
||||
<EDataTable
|
||||
columns={itemColumns}
|
||||
data={ts.groupData(items)}
|
||||
@@ -672,7 +691,8 @@ export default function SalesItemPage() {
|
||||
onSelect={(id) => setSelectedItemId(id)}
|
||||
onRowDoubleClick={() => openEditItem()}
|
||||
showRowNumber
|
||||
showPagination={false}
|
||||
showPagination
|
||||
defaultPageSize={20}
|
||||
draggableColumns={false}
|
||||
columnOrderKey="c16-sales-item"
|
||||
/>
|
||||
@@ -681,69 +701,49 @@ export default function SalesItemPage() {
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
{/* 우측: 거래처 정보 (디테일) */}
|
||||
{/* 우측: 디테일 패널 */}
|
||||
<ResizablePanel defaultSize={45} minSize={25}>
|
||||
<div className="flex flex-col h-full bg-muted">
|
||||
<div className="flex flex-col h-full">
|
||||
{!selectedItemId ? (
|
||||
/* 빈 상태 */
|
||||
<div className="flex-1 flex items-center justify-center m-5">
|
||||
<div className="flex flex-col items-center gap-4 border-2 border-dashed border-border rounded-lg p-10 text-center">
|
||||
<Users className="w-12 h-12 text-muted-foreground/40" />
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-muted-foreground">품목을 선택해주세요</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">좌측에서 품목을 선택하면 거래처 정보가 표시돼요</div>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center p-5">
|
||||
<div className="flex flex-col items-center justify-center text-center border-2 border-dashed border-border rounded-lg px-10 py-16">
|
||||
<Package className="w-12 h-12 text-muted-foreground/40 mb-4" />
|
||||
<div className="text-sm font-semibold text-muted-foreground mb-1.5">품목을 선택해주세요</div>
|
||||
<div className="text-xs text-muted-foreground">좌측에서 품목을 선택하면 상세 정보가 표시돼요</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 선택 품목 상세 헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 border-b bg-muted shrink-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-[13px] font-bold text-foreground truncate">{selectedItem?.item_name || "-"}</span>
|
||||
<span className="text-[11px] font-mono text-muted-foreground bg-muted-foreground/10 px-2 py-0.5 rounded-full shrink-0">
|
||||
{selectedItem?.item_number || ""}
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="h-8 shrink-0" onClick={openEditItem}>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
수정
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 거래처별 단가 서브헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-muted-foreground">거래처별 단가</span>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 px-1.5 py-0.5 rounded-full">
|
||||
{customerItems.length}건
|
||||
</span>
|
||||
{/* 거래처별 단가 헤더 */}
|
||||
<div className="flex items-center justify-between h-[42px] border-b bg-muted shrink-0 pr-3">
|
||||
<div className="flex items-center gap-2.5 px-4">
|
||||
<Users className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-semibold">거래처별 단가</span>
|
||||
{customerItems.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-0.5 text-[10px] px-1.5 py-0">{customerItems.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => { setCustCheckedIds(new Set()); setCustSelectOpen(true); searchCustomers(); }}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
거래처 추가
|
||||
<Plus className="w-3.5 h-3.5" /> 거래처 추가
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
disabled={customerCheckedIds.length === 0}
|
||||
onClick={handleCustomerMappingDelete}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
삭제
|
||||
<Trash2 className="w-3.5 h-3.5" /> 삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 거래처 테이블 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="flex-1 min-h-0 overflow-auto pt-px">
|
||||
{customerLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
||||
@@ -755,8 +755,8 @@ export default function SalesItemPage() {
|
||||
) : (
|
||||
<Table noWrapper>
|
||||
<TableHeader className="sticky top-0 z-10">
|
||||
<TableRow className="bg-muted hover:bg-muted">
|
||||
<TableHead className="sticky top-0 z-10 bg-card w-[36px] text-center">
|
||||
<TableRow className="bg-muted hover:bg-muted h-10">
|
||||
<TableHead className="w-[40px] text-center px-2">
|
||||
<Checkbox
|
||||
checked={customerItems.length > 0 && customerCheckedIds.length === customerItems.length}
|
||||
onCheckedChange={(checked) => {
|
||||
@@ -765,13 +765,13 @@ export default function SalesItemPage() {
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[110px]">거래처코드</TableHead>
|
||||
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground min-w-[120px]">거래처명</TableHead>
|
||||
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[100px]">거래처품번</TableHead>
|
||||
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[100px]">거래처품명</TableHead>
|
||||
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[80px] text-right">기준가</TableHead>
|
||||
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[80px] text-right">단가</TableHead>
|
||||
<TableHead className="sticky top-0 z-10 bg-card text-[11px] font-bold uppercase tracking-wide text-muted-foreground w-[50px]">통화</TableHead>
|
||||
<TableHead className="w-[110px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처코드</TableHead>
|
||||
<TableHead className="min-w-[120px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처명</TableHead>
|
||||
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처품번</TableHead>
|
||||
<TableHead className="w-[100px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">거래처품명</TableHead>
|
||||
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">기준가</TableHead>
|
||||
<TableHead className="w-[80px] text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">단가</TableHead>
|
||||
<TableHead className="w-[50px] text-[11px] font-bold uppercase tracking-wide text-muted-foreground">통화</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -779,12 +779,20 @@ export default function SalesItemPage() {
|
||||
<TableRow
|
||||
key={row.id}
|
||||
className={cn(
|
||||
"cursor-pointer transition-all",
|
||||
"cursor-pointer h-[41px]",
|
||||
customerCheckedIds.includes(row.id) ? "bg-primary/[0.08]" : "hover:bg-accent"
|
||||
)}
|
||||
onDoubleClick={() => openEditCust(row)}
|
||||
>
|
||||
<TableCell className="text-center">
|
||||
<TableCell
|
||||
className="text-center px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCustomerCheckedIds((prev) =>
|
||||
prev.includes(row.id) ? prev.filter((id) => id !== row.id) : [...prev, row.id]
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={customerCheckedIds.includes(row.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
@@ -1104,66 +1112,74 @@ export default function SalesItemPage() {
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 기준가 + 할인 + 반올림 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.base_price_type}
|
||||
onValueChange={(v) => updatePriceRow(custKey, price._id, "base_price_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="기준" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* 기준유형 + 기준가 */}
|
||||
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
|
||||
<Select
|
||||
value={price.base_price_type}
|
||||
onValueChange={(v) => updatePriceRow(custKey, price._id, "base_price_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="기준유형" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["base_price_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={price.base_price}
|
||||
onChange={(e) => updatePriceRow(custKey, price._id, "base_price", e.target.value)}
|
||||
className="h-8 text-xs text-right flex-1"
|
||||
value={price.base_price ? Number(price.base_price).toLocaleString() : ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/[^\d.-]/g, "");
|
||||
updatePriceRow(custKey, price._id, "base_price", raw);
|
||||
}}
|
||||
className="h-8 text-xs text-right col-span-3"
|
||||
placeholder="기준가"
|
||||
/>
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.discount_type}
|
||||
onValueChange={(v) => updatePriceRow(custKey, price._id, "discount_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="할인" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">할인없음</SelectItem>
|
||||
{(priceCategoryOptions["discount_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 할인 + 반올림 */}
|
||||
<div className="grid grid-cols-[1fr_70px_1fr_85px] gap-2 items-center">
|
||||
<Select
|
||||
value={price.discount_type}
|
||||
onValueChange={(v) => updatePriceRow(custKey, price._id, "discount_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="할인" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">할인없음</SelectItem>
|
||||
{(priceCategoryOptions["discount_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={price.discount_value}
|
||||
onChange={(e) => updatePriceRow(custKey, price._id, "discount_value", e.target.value)}
|
||||
className="h-8 text-xs text-right w-[60px]"
|
||||
value={price.discount_value ? Number(price.discount_value).toLocaleString() : ""}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/[^\d.-]/g, "");
|
||||
updatePriceRow(custKey, price._id, "discount_value", raw);
|
||||
}}
|
||||
className="h-8 text-xs text-right"
|
||||
placeholder="0"
|
||||
/>
|
||||
<div className="w-[90px]">
|
||||
<Select
|
||||
value={price.rounding_unit_value}
|
||||
onValueChange={(v) => updatePriceRow(custKey, price._id, "rounding_unit_value", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="반올림" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select
|
||||
value={price.rounding_unit_value}
|
||||
onValueChange={(v) => updatePriceRow(custKey, price._id, "rounding_unit_value", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="반올림" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_unit_value"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={price.rounding_type}
|
||||
onValueChange={(v) => updatePriceRow(custKey, price._id, "rounding_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="단위" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(priceCategoryOptions["rounding_type"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* 계산 단가 */}
|
||||
<div className="flex items-center justify-end gap-2 pt-1 border-t">
|
||||
|
||||
Reference in New Issue
Block a user