refactor: Improve table header and cell styling for production plan management and item inspection pages

- Updated the table header and cell styles to enhance visibility and usability, including adjustments to z-index and sticky positioning.
- Implemented dynamic label mapping for inspection types in the item inspection page to improve clarity.
- Enhanced the sales order page by including management item filters in server queries, allowing for better data handling and user experience.
- These changes aim to provide a more intuitive interface and improve data representation across multiple company implementations.
This commit is contained in:
kjs
2026-04-13 11:58:26 +09:00
parent b28e8e206c
commit 9272ddb345
19 changed files with 235 additions and 268 deletions
@@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() {
return (
<Table style={{ minWidth: "900px" }}>
<TableHeader className="sticky top-0 z-10">
<TableHeader className="sticky top-0 z-20">
<TableRow className="bg-muted hover:bg-muted">
<TableHead style={{ width: "30px", minWidth: "30px" }}>
<TableHead className="sticky left-0 z-20 bg-muted" style={{ width: "30px", minWidth: "30px" }}>
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead style={{ width: "40px", minWidth: "40px" }} />
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[30px] z-20 bg-muted" style={{ width: "40px", minWidth: "40px" }} />
<TableHead className="sticky left-[70px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={{ width: "140px", minWidth: "140px" }}></TableHead>
<TableHead className="sticky left-[210px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground border-r" style={{ width: "140px", minWidth: "140px" }}></TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
{col.label}
@@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() {
if (item._isGroupSummary) {
return (
<TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
<TableCell />
<TableCell />
<TableCell colSpan={2} />
<TableCell className="sticky left-0 z-10 bg-muted/60" />
<TableCell className="sticky left-[30px] z-10 bg-muted/60" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-muted/60 border-r" />
{ts.visibleColumns.map((col) => {
const v = (item as any)[col.key];
return (
@@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() {
return (
<React.Fragment key={item.item_code || rowIdx}>
<TableRow className={cn("cursor-pointer border-t-2 border-t-primary/30 bg-primary/5 font-semibold hover:bg-primary/10", selectedItemGroups.has(item.item_code) && "bg-primary/10")}>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<TableCell className="sticky left-0 z-10 bg-background text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox checked={selectedItemGroups.has(item.item_code)} onCheckedChange={() => toggleItemGroupSelect(item.item_code)} className="h-4 w-4" />
</TableCell>
<TableCell className="text-center" onClick={() => toggleItemExpand(item.item_code)}>
<TableCell className="sticky left-[30px] z-10 bg-background text-center" onClick={() => toggleItemExpand(item.item_code)}>
<ChevronRight className={cn("h-4 w-4 transition-transform duration-200", expandedItems.has(item.item_code) && "rotate-90")} />
</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
<TableCell className="sticky left-[70px] z-10 bg-background text-[13px] text-primary truncate" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell className="sticky left-[210px] z-10 bg-background text-[13px] text-primary truncate border-r" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
{ts.visibleColumns.map((col) => renderGroupCell(col, item))}
</TableRow>
{expandedItems.has(item.item_code) && item.orders?.map((detail: any) => {
@@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() {
}
return (
<TableRow key={detail.id || detail.order_no} className="cursor-pointer hover:bg-muted/50">
<TableCell />
<TableCell />
<TableCell colSpan={2} className="pl-10">
<TableCell className="sticky left-0 z-10 bg-card" />
<TableCell className="sticky left-[30px] z-10 bg-card" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-card pl-10 border-r">
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{detail.order_no}</span>
@@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() {
case "inspection_type": return (
<TableCell key={col.key}>
<div className="flex gap-1 flex-wrap">
{group.types.map((t: string) => (
<Badge key={t} variant="secondary" className="text-[10px]">{t}</Badge>
))}
{group.types.map((t: string) => {
const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t;
return <Badge key={t} variant="secondary" className="text-[10px]">{label}</Badge>;
})}
</div>
</TableCell>
);
@@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() {
: "bg-muted/50 text-muted-foreground border-border hover:bg-muted"
)}
>
{type}
{inspTypeCatOptions.find((o) => o.code === type)?.label || type}
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[16px] rounded-full text-[10px] font-bold px-1",
selectedTypeTab === type ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted-foreground/20 text-muted-foreground"
@@ -624,6 +624,15 @@ export default function SalesOrderPage() {
const filters: any[] = [];
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
// 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
const partnerId = masterForm.partner_id;
@@ -632,7 +641,7 @@ export default function SalesOrderPage() {
if (isCustomerPrice && partnerId) {
try {
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
autoFilter: true,
});
@@ -642,31 +651,22 @@ export default function SalesOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
let rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
// 거래처우선일 때 연결된 품목만 표시
// 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터)
if (customerItemIds) {
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
}
// 관리품목 필터 (코드/라벨 혼재 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -1474,15 +1474,7 @@ export default function SalesOrderPage() {
onKeyDown={(e) => e.key === "Enter" && triggerNewSearch()}
className="h-9 flex-1"
/>
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<div className="h-9 px-3 flex items-center rounded-md border border-input bg-muted text-xs font-medium text-muted-foreground whitespace-nowrap"></div>
<Button size="sm" onClick={triggerNewSearch} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /></>}
</Button>
@@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() {
return (
<Table style={{ minWidth: "900px" }}>
<TableHeader className="sticky top-0 z-10">
<TableHeader className="sticky top-0 z-20">
<TableRow className="bg-muted hover:bg-muted">
<TableHead style={{ width: "30px", minWidth: "30px" }}>
<TableHead className="sticky left-0 z-20 bg-muted" style={{ width: "30px", minWidth: "30px" }}>
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead style={{ width: "40px", minWidth: "40px" }} />
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[30px] z-20 bg-muted" style={{ width: "40px", minWidth: "40px" }} />
<TableHead className="sticky left-[70px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={{ width: "140px", minWidth: "140px" }}></TableHead>
<TableHead className="sticky left-[210px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground border-r" style={{ width: "140px", minWidth: "140px" }}></TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
{col.label}
@@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() {
if (item._isGroupSummary) {
return (
<TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
<TableCell />
<TableCell />
<TableCell colSpan={2} />
<TableCell className="sticky left-0 z-10 bg-muted/60" />
<TableCell className="sticky left-[30px] z-10 bg-muted/60" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-muted/60 border-r" />
{ts.visibleColumns.map((col) => {
const v = (item as any)[col.key];
return (
@@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() {
return (
<React.Fragment key={item.item_code || rowIdx}>
<TableRow className={cn("cursor-pointer border-t-2 border-t-primary/30 bg-primary/5 font-semibold hover:bg-primary/10", selectedItemGroups.has(item.item_code) && "bg-primary/10")}>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<TableCell className="sticky left-0 z-10 bg-background text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox checked={selectedItemGroups.has(item.item_code)} onCheckedChange={() => toggleItemGroupSelect(item.item_code)} className="h-4 w-4" />
</TableCell>
<TableCell className="text-center" onClick={() => toggleItemExpand(item.item_code)}>
<TableCell className="sticky left-[30px] z-10 bg-background text-center" onClick={() => toggleItemExpand(item.item_code)}>
<ChevronRight className={cn("h-4 w-4 transition-transform duration-200", expandedItems.has(item.item_code) && "rotate-90")} />
</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
<TableCell className="sticky left-[70px] z-10 bg-background text-[13px] text-primary truncate" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell className="sticky left-[210px] z-10 bg-background text-[13px] text-primary truncate border-r" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
{ts.visibleColumns.map((col) => renderGroupCell(col, item))}
</TableRow>
{expandedItems.has(item.item_code) && item.orders?.map((detail: any) => {
@@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() {
}
return (
<TableRow key={detail.id || detail.order_no} className="cursor-pointer hover:bg-muted/50">
<TableCell />
<TableCell />
<TableCell colSpan={2} className="pl-10">
<TableCell className="sticky left-0 z-10 bg-card" />
<TableCell className="sticky left-[30px] z-10 bg-card" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-card pl-10 border-r">
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{detail.order_no}</span>
@@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() {
case "inspection_type": return (
<TableCell key={col.key}>
<div className="flex gap-1 flex-wrap">
{group.types.map((t: string) => (
<Badge key={t} variant="secondary" className="text-[10px]">{t}</Badge>
))}
{group.types.map((t: string) => {
const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t;
return <Badge key={t} variant="secondary" className="text-[10px]">{label}</Badge>;
})}
</div>
</TableCell>
);
@@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() {
: "bg-muted/50 text-muted-foreground border-border hover:bg-muted"
)}
>
{type}
{inspTypeCatOptions.find((o) => o.code === type)?.label || type}
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[16px] rounded-full text-[10px] font-bold px-1",
selectedTypeTab === type ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted-foreground/20 text-muted-foreground"
@@ -624,6 +624,15 @@ export default function SalesOrderPage() {
const filters: any[] = [];
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
// 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
const partnerId = masterForm.partner_id;
@@ -632,7 +641,7 @@ export default function SalesOrderPage() {
if (isCustomerPrice && partnerId) {
try {
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
autoFilter: true,
});
@@ -642,31 +651,22 @@ export default function SalesOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
let rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
// 거래처우선일 때 연결된 품목만 표시
// 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터)
if (customerItemIds) {
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
}
// 관리품목 필터 (코드/라벨 혼재 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -1474,15 +1474,7 @@ export default function SalesOrderPage() {
onKeyDown={(e) => e.key === "Enter" && triggerNewSearch()}
className="h-9 flex-1"
/>
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<div className="h-9 px-3 flex items-center rounded-md border border-input bg-muted text-xs font-medium text-muted-foreground whitespace-nowrap"></div>
<Button size="sm" onClick={triggerNewSearch} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /></>}
</Button>
@@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() {
return (
<Table style={{ minWidth: "900px" }}>
<TableHeader className="sticky top-0 z-10">
<TableHeader className="sticky top-0 z-20">
<TableRow className="bg-muted hover:bg-muted">
<TableHead style={{ width: "30px", minWidth: "30px" }}>
<TableHead className="sticky left-0 z-20 bg-muted" style={{ width: "30px", minWidth: "30px" }}>
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead style={{ width: "40px", minWidth: "40px" }} />
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[30px] z-20 bg-muted" style={{ width: "40px", minWidth: "40px" }} />
<TableHead className="sticky left-[70px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={{ width: "140px", minWidth: "140px" }}></TableHead>
<TableHead className="sticky left-[210px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground border-r" style={{ width: "140px", minWidth: "140px" }}></TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
{col.label}
@@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() {
if (item._isGroupSummary) {
return (
<TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
<TableCell />
<TableCell />
<TableCell colSpan={2} />
<TableCell className="sticky left-0 z-10 bg-muted/60" />
<TableCell className="sticky left-[30px] z-10 bg-muted/60" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-muted/60 border-r" />
{ts.visibleColumns.map((col) => {
const v = (item as any)[col.key];
return (
@@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() {
return (
<React.Fragment key={item.item_code || rowIdx}>
<TableRow className={cn("cursor-pointer border-t-2 border-t-primary/30 bg-primary/5 font-semibold hover:bg-primary/10", selectedItemGroups.has(item.item_code) && "bg-primary/10")}>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<TableCell className="sticky left-0 z-10 bg-background text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox checked={selectedItemGroups.has(item.item_code)} onCheckedChange={() => toggleItemGroupSelect(item.item_code)} className="h-4 w-4" />
</TableCell>
<TableCell className="text-center" onClick={() => toggleItemExpand(item.item_code)}>
<TableCell className="sticky left-[30px] z-10 bg-background text-center" onClick={() => toggleItemExpand(item.item_code)}>
<ChevronRight className={cn("h-4 w-4 transition-transform duration-200", expandedItems.has(item.item_code) && "rotate-90")} />
</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
<TableCell className="sticky left-[70px] z-10 bg-background text-[13px] text-primary truncate" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell className="sticky left-[210px] z-10 bg-background text-[13px] text-primary truncate border-r" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
{ts.visibleColumns.map((col) => renderGroupCell(col, item))}
</TableRow>
{expandedItems.has(item.item_code) && item.orders?.map((detail: any) => {
@@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() {
}
return (
<TableRow key={detail.id || detail.order_no} className="cursor-pointer hover:bg-muted/50">
<TableCell />
<TableCell />
<TableCell colSpan={2} className="pl-10">
<TableCell className="sticky left-0 z-10 bg-card" />
<TableCell className="sticky left-[30px] z-10 bg-card" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-card pl-10 border-r">
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{detail.order_no}</span>
@@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() {
case "inspection_type": return (
<TableCell key={col.key}>
<div className="flex gap-1 flex-wrap">
{group.types.map((t: string) => (
<Badge key={t} variant="secondary" className="text-[10px]">{t}</Badge>
))}
{group.types.map((t: string) => {
const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t;
return <Badge key={t} variant="secondary" className="text-[10px]">{label}</Badge>;
})}
</div>
</TableCell>
);
@@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() {
: "bg-muted/50 text-muted-foreground border-border hover:bg-muted"
)}
>
{type}
{inspTypeCatOptions.find((o) => o.code === type)?.label || type}
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[16px] rounded-full text-[10px] font-bold px-1",
selectedTypeTab === type ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted-foreground/20 text-muted-foreground"
@@ -624,6 +624,15 @@ export default function SalesOrderPage() {
const filters: any[] = [];
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
// 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
const partnerId = masterForm.partner_id;
@@ -632,7 +641,7 @@ export default function SalesOrderPage() {
if (isCustomerPrice && partnerId) {
try {
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
autoFilter: true,
});
@@ -642,31 +651,22 @@ export default function SalesOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
let rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
// 거래처우선일 때 연결된 품목만 표시
// 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터)
if (customerItemIds) {
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
}
// 관리품목 필터 (코드/라벨 혼재 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -1474,15 +1474,7 @@ export default function SalesOrderPage() {
onKeyDown={(e) => e.key === "Enter" && triggerNewSearch()}
className="h-9 flex-1"
/>
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<div className="h-9 px-3 flex items-center rounded-md border border-input bg-muted text-xs font-medium text-muted-foreground whitespace-nowrap"></div>
<Button size="sm" onClick={triggerNewSearch} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /></>}
</Button>
@@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() {
return (
<Table style={{ minWidth: "900px" }}>
<TableHeader className="sticky top-0 z-10">
<TableHeader className="sticky top-0 z-20">
<TableRow className="bg-muted hover:bg-muted">
<TableHead style={{ width: "30px", minWidth: "30px" }}>
<TableHead className="sticky left-0 z-20 bg-muted" style={{ width: "30px", minWidth: "30px" }}>
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead style={{ width: "40px", minWidth: "40px" }} />
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[30px] z-20 bg-muted" style={{ width: "40px", minWidth: "40px" }} />
<TableHead className="sticky left-[70px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={{ width: "140px", minWidth: "140px" }}></TableHead>
<TableHead className="sticky left-[210px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground border-r" style={{ width: "140px", minWidth: "140px" }}></TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
{col.label}
@@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() {
if (item._isGroupSummary) {
return (
<TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
<TableCell />
<TableCell />
<TableCell colSpan={2} />
<TableCell className="sticky left-0 z-10 bg-muted/60" />
<TableCell className="sticky left-[30px] z-10 bg-muted/60" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-muted/60 border-r" />
{ts.visibleColumns.map((col) => {
const v = (item as any)[col.key];
return (
@@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() {
return (
<React.Fragment key={item.item_code || rowIdx}>
<TableRow className={cn("cursor-pointer border-t-2 border-t-primary/30 bg-primary/5 font-semibold hover:bg-primary/10", selectedItemGroups.has(item.item_code) && "bg-primary/10")}>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<TableCell className="sticky left-0 z-10 bg-background text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox checked={selectedItemGroups.has(item.item_code)} onCheckedChange={() => toggleItemGroupSelect(item.item_code)} className="h-4 w-4" />
</TableCell>
<TableCell className="text-center" onClick={() => toggleItemExpand(item.item_code)}>
<TableCell className="sticky left-[30px] z-10 bg-background text-center" onClick={() => toggleItemExpand(item.item_code)}>
<ChevronRight className={cn("h-4 w-4 transition-transform duration-200", expandedItems.has(item.item_code) && "rotate-90")} />
</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
<TableCell className="sticky left-[70px] z-10 bg-background text-[13px] text-primary truncate" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell className="sticky left-[210px] z-10 bg-background text-[13px] text-primary truncate border-r" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
{ts.visibleColumns.map((col) => renderGroupCell(col, item))}
</TableRow>
{expandedItems.has(item.item_code) && item.orders?.map((detail: any) => {
@@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() {
}
return (
<TableRow key={detail.id || detail.order_no} className="cursor-pointer hover:bg-muted/50">
<TableCell />
<TableCell />
<TableCell colSpan={2} className="pl-10">
<TableCell className="sticky left-0 z-10 bg-card" />
<TableCell className="sticky left-[30px] z-10 bg-card" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-card pl-10 border-r">
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{detail.order_no}</span>
@@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() {
case "inspection_type": return (
<TableCell key={col.key}>
<div className="flex gap-1 flex-wrap">
{group.types.map((t: string) => (
<Badge key={t} variant="secondary" className="text-[10px]">{t}</Badge>
))}
{group.types.map((t: string) => {
const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t;
return <Badge key={t} variant="secondary" className="text-[10px]">{label}</Badge>;
})}
</div>
</TableCell>
);
@@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() {
: "bg-muted/50 text-muted-foreground border-border hover:bg-muted"
)}
>
{type}
{inspTypeCatOptions.find((o) => o.code === type)?.label || type}
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[16px] rounded-full text-[10px] font-bold px-1",
selectedTypeTab === type ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted-foreground/20 text-muted-foreground"
@@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() {
return (
<Table style={{ minWidth: "900px" }}>
<TableHeader className="sticky top-0 z-10">
<TableHeader className="sticky top-0 z-20">
<TableRow className="bg-muted hover:bg-muted">
<TableHead style={{ width: "30px", minWidth: "30px" }}>
<TableHead className="sticky left-0 z-20 bg-muted" style={{ width: "30px", minWidth: "30px" }}>
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead style={{ width: "40px", minWidth: "40px" }} />
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[30px] z-20 bg-muted" style={{ width: "40px", minWidth: "40px" }} />
<TableHead className="sticky left-[70px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={{ width: "140px", minWidth: "140px" }}></TableHead>
<TableHead className="sticky left-[210px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground border-r" style={{ width: "140px", minWidth: "140px" }}></TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
{col.label}
@@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() {
if (item._isGroupSummary) {
return (
<TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
<TableCell />
<TableCell />
<TableCell colSpan={2} />
<TableCell className="sticky left-0 z-10 bg-muted/60" />
<TableCell className="sticky left-[30px] z-10 bg-muted/60" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-muted/60 border-r" />
{ts.visibleColumns.map((col) => {
const v = (item as any)[col.key];
return (
@@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() {
return (
<React.Fragment key={item.item_code || rowIdx}>
<TableRow className={cn("cursor-pointer border-t-2 border-t-primary/30 bg-primary/5 font-semibold hover:bg-primary/10", selectedItemGroups.has(item.item_code) && "bg-primary/10")}>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<TableCell className="sticky left-0 z-10 bg-background text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox checked={selectedItemGroups.has(item.item_code)} onCheckedChange={() => toggleItemGroupSelect(item.item_code)} className="h-4 w-4" />
</TableCell>
<TableCell className="text-center" onClick={() => toggleItemExpand(item.item_code)}>
<TableCell className="sticky left-[30px] z-10 bg-background text-center" onClick={() => toggleItemExpand(item.item_code)}>
<ChevronRight className={cn("h-4 w-4 transition-transform duration-200", expandedItems.has(item.item_code) && "rotate-90")} />
</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
<TableCell className="sticky left-[70px] z-10 bg-background text-[13px] text-primary truncate" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell className="sticky left-[210px] z-10 bg-background text-[13px] text-primary truncate border-r" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
{ts.visibleColumns.map((col) => renderGroupCell(col, item))}
</TableRow>
{expandedItems.has(item.item_code) && item.orders?.map((detail: any) => {
@@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() {
}
return (
<TableRow key={detail.id || detail.order_no} className="cursor-pointer hover:bg-muted/50">
<TableCell />
<TableCell />
<TableCell colSpan={2} className="pl-10">
<TableCell className="sticky left-0 z-10 bg-card" />
<TableCell className="sticky left-[30px] z-10 bg-card" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-card pl-10 border-r">
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{detail.order_no}</span>
@@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() {
case "inspection_type": return (
<TableCell key={col.key}>
<div className="flex gap-1 flex-wrap">
{group.types.map((t: string) => (
<Badge key={t} variant="secondary" className="text-[10px]">{t}</Badge>
))}
{group.types.map((t: string) => {
const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t;
return <Badge key={t} variant="secondary" className="text-[10px]">{label}</Badge>;
})}
</div>
</TableCell>
);
@@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() {
: "bg-muted/50 text-muted-foreground border-border hover:bg-muted"
)}
>
{type}
{inspTypeCatOptions.find((o) => o.code === type)?.label || type}
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[16px] rounded-full text-[10px] font-bold px-1",
selectedTypeTab === type ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted-foreground/20 text-muted-foreground"
@@ -624,6 +624,15 @@ export default function SalesOrderPage() {
const filters: any[] = [];
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
// 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
const partnerId = masterForm.partner_id;
@@ -632,7 +641,7 @@ export default function SalesOrderPage() {
if (isCustomerPrice && partnerId) {
try {
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
autoFilter: true,
});
@@ -642,31 +651,22 @@ export default function SalesOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
let rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
// 거래처우선일 때 연결된 품목만 표시
// 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터)
if (customerItemIds) {
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
}
// 관리품목 필터 (코드/라벨 혼재 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -1474,15 +1474,7 @@ export default function SalesOrderPage() {
onKeyDown={(e) => e.key === "Enter" && triggerNewSearch()}
className="h-9 flex-1"
/>
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<div className="h-9 px-3 flex items-center rounded-md border border-input bg-muted text-xs font-medium text-muted-foreground whitespace-nowrap"></div>
<Button size="sm" onClick={triggerNewSearch} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /></>}
</Button>
@@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() {
return (
<Table style={{ minWidth: "900px" }}>
<TableHeader className="sticky top-0 z-10">
<TableHeader className="sticky top-0 z-20">
<TableRow className="bg-muted hover:bg-muted">
<TableHead style={{ width: "30px", minWidth: "30px" }}>
<TableHead className="sticky left-0 z-20 bg-muted" style={{ width: "30px", minWidth: "30px" }}>
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead style={{ width: "40px", minWidth: "40px" }} />
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[30px] z-20 bg-muted" style={{ width: "40px", minWidth: "40px" }} />
<TableHead className="sticky left-[70px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={{ width: "140px", minWidth: "140px" }}></TableHead>
<TableHead className="sticky left-[210px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground border-r" style={{ width: "140px", minWidth: "140px" }}></TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
{col.label}
@@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() {
if (item._isGroupSummary) {
return (
<TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
<TableCell />
<TableCell />
<TableCell colSpan={2} />
<TableCell className="sticky left-0 z-10 bg-muted/60" />
<TableCell className="sticky left-[30px] z-10 bg-muted/60" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-muted/60 border-r" />
{ts.visibleColumns.map((col) => {
const v = (item as any)[col.key];
return (
@@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() {
return (
<React.Fragment key={item.item_code || rowIdx}>
<TableRow className={cn("cursor-pointer border-t-2 border-t-primary/30 bg-primary/5 font-semibold hover:bg-primary/10", selectedItemGroups.has(item.item_code) && "bg-primary/10")}>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<TableCell className="sticky left-0 z-10 bg-background text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox checked={selectedItemGroups.has(item.item_code)} onCheckedChange={() => toggleItemGroupSelect(item.item_code)} className="h-4 w-4" />
</TableCell>
<TableCell className="text-center" onClick={() => toggleItemExpand(item.item_code)}>
<TableCell className="sticky left-[30px] z-10 bg-background text-center" onClick={() => toggleItemExpand(item.item_code)}>
<ChevronRight className={cn("h-4 w-4 transition-transform duration-200", expandedItems.has(item.item_code) && "rotate-90")} />
</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
<TableCell className="sticky left-[70px] z-10 bg-background text-[13px] text-primary truncate" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell className="sticky left-[210px] z-10 bg-background text-[13px] text-primary truncate border-r" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
{ts.visibleColumns.map((col) => renderGroupCell(col, item))}
</TableRow>
{expandedItems.has(item.item_code) && item.orders?.map((detail: any) => {
@@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() {
}
return (
<TableRow key={detail.id || detail.order_no} className="cursor-pointer hover:bg-muted/50">
<TableCell />
<TableCell />
<TableCell colSpan={2} className="pl-10">
<TableCell className="sticky left-0 z-10 bg-card" />
<TableCell className="sticky left-[30px] z-10 bg-card" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-card pl-10 border-r">
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{detail.order_no}</span>
@@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() {
case "inspection_type": return (
<TableCell key={col.key}>
<div className="flex gap-1 flex-wrap">
{group.types.map((t: string) => (
<Badge key={t} variant="secondary" className="text-[10px]">{t}</Badge>
))}
{group.types.map((t: string) => {
const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t;
return <Badge key={t} variant="secondary" className="text-[10px]">{label}</Badge>;
})}
</div>
</TableCell>
);
@@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() {
: "bg-muted/50 text-muted-foreground border-border hover:bg-muted"
)}
>
{type}
{inspTypeCatOptions.find((o) => o.code === type)?.label || type}
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[16px] rounded-full text-[10px] font-bold px-1",
selectedTypeTab === type ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted-foreground/20 text-muted-foreground"
@@ -624,6 +624,15 @@ export default function SalesOrderPage() {
const filters: any[] = [];
if (itemSearchKeyword) filters.push({ columnName: "item_name", operator: "contains", value: itemSearchKeyword });
// 관리품목 필터를 서버 쿼리에 포함 (코드 + 라벨 양쪽 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
// 코드 또는 라벨이 저장된 경우 모두 조회하기 위해 in 연산자 사용
const divValues = [itemSearchDivision];
if (divLabel) divValues.push(divLabel);
filters.push({ columnName: "division", operator: "in", value: divValues });
}
// 거래처우선 단가방식일 때 거래처에 연결된 품목만 필터링
const isCustomerPrice = masterForm.price_mode === "CAT_MM0BV3OS_41DX" || masterForm.price_mode === "CAT_MLKG7D8K_N8SI";
const partnerId = masterForm.partner_id;
@@ -632,7 +641,7 @@ export default function SalesOrderPage() {
if (isCustomerPrice && partnerId) {
try {
const mappingRes = await apiClient.post(`/table-management/tables/customer_item_mapping/data`, {
page: 1, size: 500,
page: 1, size: 5000,
dataFilter: { enabled: true, filters: [{ columnName: "customer_id", operator: "equals", value: partnerId }] },
autoFilter: true,
});
@@ -642,31 +651,22 @@ export default function SalesOrderPage() {
}
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
page: 1, size: 500,
page: p, size: s,
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
autoFilter: true,
});
const resData = res.data?.data;
let allRows = resData?.data || resData?.rows || [];
let rows = resData?.data || resData?.rows || [];
const serverTotal = resData?.total || resData?.totalCount || rows.length;
// 거래처우선일 때 연결된 품목만 표시
// 거래처우선일 때 연결된 품목만 표시 (클라이언트 필터)
if (customerItemIds) {
allRows = allRows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
rows = rows.filter((item: any) => customerItemIds!.has(item.item_number) || customerItemIds!.has(item.id));
}
// 관리품목 필터 (코드/라벨 혼재 대응)
if (itemSearchDivision !== "all") {
const divLabel = categoryOptions["item_division"]?.find((o) => o.code === itemSearchDivision)?.label || "";
allRows = allRows.filter((item: any) => {
const div = item.division || "";
return div.includes(itemSearchDivision) || (divLabel && div.includes(divLabel));
});
}
const total = allRows.length;
const start = (p - 1) * s;
setItemSearchResults(allRows.slice(start, start + s));
setItemTotal(total);
setItemTotalPages(Math.max(1, Math.ceil(total / s)));
setItemSearchResults(rows);
setItemTotal(serverTotal);
setItemTotalPages(Math.max(1, Math.ceil(serverTotal / s)));
} catch { /* skip */ } finally {
setItemSearchLoading(false);
}
@@ -1474,15 +1474,7 @@ export default function SalesOrderPage() {
onKeyDown={(e) => e.key === "Enter" && triggerNewSearch()}
className="h-9 flex-1"
/>
<Select value={itemSearchDivision} onValueChange={setItemSearchDivision}>
<SelectTrigger className="h-9 w-[130px]"><SelectValue placeholder="관리품목" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{(categoryOptions["item_division"] || []).map((o) => (
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<div className="h-9 px-3 flex items-center rounded-md border border-input bg-muted text-xs font-medium text-muted-foreground whitespace-nowrap"></div>
<Button size="sm" onClick={triggerNewSearch} disabled={itemSearchLoading} className="h-9">
{itemSearchLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <><Search className="w-4 h-4 mr-1" /></>}
</Button>
@@ -1053,14 +1053,14 @@ export default function ProductionPlanManagementPage() {
return (
<Table style={{ minWidth: "900px" }}>
<TableHeader className="sticky top-0 z-10">
<TableHeader className="sticky top-0 z-20">
<TableRow className="bg-muted hover:bg-muted">
<TableHead style={{ width: "30px", minWidth: "30px" }}>
<TableHead className="sticky left-0 z-20 bg-muted" style={{ width: "30px", minWidth: "30px" }}>
<Checkbox checked={selectedItemGroups.size === orderItems.length && orderItems.length > 0} onCheckedChange={(c) => toggleAllItemGroups(!!c)} className="h-4 w-4" />
</TableHead>
<TableHead style={{ width: "40px", minWidth: "40px" }} />
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead style={{ width: "140px", minWidth: "140px" }} className="text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground"></TableHead>
<TableHead className="sticky left-[30px] z-20 bg-muted" style={{ width: "40px", minWidth: "40px" }} />
<TableHead className="sticky left-[70px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground" style={{ width: "140px", minWidth: "140px" }}></TableHead>
<TableHead className="sticky left-[210px] z-20 bg-muted text-xs font-bold whitespace-nowrap text-[11px] font-bold uppercase tracking-wide text-muted-foreground border-r" style={{ width: "140px", minWidth: "140px" }}></TableHead>
{ts.visibleColumns.map((col) => (
<TableHead key={col.key} style={{ ...ts.thStyle(col.key), minWidth: "80px" }} className="text-xs font-bold whitespace-nowrap text-right text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
{col.label}
@@ -1073,9 +1073,9 @@ export default function ProductionPlanManagementPage() {
if (item._isGroupSummary) {
return (
<TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
<TableCell />
<TableCell />
<TableCell colSpan={2} />
<TableCell className="sticky left-0 z-10 bg-muted/60" />
<TableCell className="sticky left-[30px] z-10 bg-muted/60" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-muted/60 border-r" />
{ts.visibleColumns.map((col) => {
const v = (item as any)[col.key];
return (
@@ -1090,14 +1090,14 @@ export default function ProductionPlanManagementPage() {
return (
<React.Fragment key={item.item_code || rowIdx}>
<TableRow className={cn("cursor-pointer border-t-2 border-t-primary/30 bg-primary/5 font-semibold hover:bg-primary/10", selectedItemGroups.has(item.item_code) && "bg-primary/10")}>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<TableCell className="sticky left-0 z-10 bg-background text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox checked={selectedItemGroups.has(item.item_code)} onCheckedChange={() => toggleItemGroupSelect(item.item_code)} className="h-4 w-4" />
</TableCell>
<TableCell className="text-center" onClick={() => toggleItemExpand(item.item_code)}>
<TableCell className="sticky left-[30px] z-10 bg-background text-center" onClick={() => toggleItemExpand(item.item_code)}>
<ChevronRight className={cn("h-4 w-4 transition-transform duration-200", expandedItems.has(item.item_code) && "rotate-90")} />
</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell style={{ width: "140px", minWidth: "140px" }} className="text-[13px] text-primary truncate" onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
<TableCell className="sticky left-[70px] z-10 bg-background text-[13px] text-primary truncate" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_code}</TableCell>
<TableCell className="sticky left-[210px] z-10 bg-background text-[13px] text-primary truncate border-r" style={{ width: "140px", minWidth: "140px" }} onClick={() => toggleItemExpand(item.item_code)}>{item.item_name}</TableCell>
{ts.visibleColumns.map((col) => renderGroupCell(col, item))}
</TableRow>
{expandedItems.has(item.item_code) && item.orders?.map((detail: any) => {
@@ -1107,9 +1107,9 @@ export default function ProductionPlanManagementPage() {
}
return (
<TableRow key={detail.id || detail.order_no} className="cursor-pointer hover:bg-muted/50">
<TableCell />
<TableCell />
<TableCell colSpan={2} className="pl-10">
<TableCell className="sticky left-0 z-10 bg-card" />
<TableCell className="sticky left-[30px] z-10 bg-card" />
<TableCell colSpan={2} className="sticky left-[70px] z-10 bg-card pl-10 border-r">
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{detail.order_no}</span>
@@ -424,9 +424,10 @@ export default function ItemInspectionInfoPage() {
case "inspection_type": return (
<TableCell key={col.key}>
<div className="flex gap-1 flex-wrap">
{group.types.map((t: string) => (
<Badge key={t} variant="secondary" className="text-[10px]">{t}</Badge>
))}
{group.types.map((t: string) => {
const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t;
return <Badge key={t} variant="secondary" className="text-[10px]">{label}</Badge>;
})}
</div>
</TableCell>
);
@@ -494,7 +495,7 @@ export default function ItemInspectionInfoPage() {
: "bg-muted/50 text-muted-foreground border-border hover:bg-muted"
)}
>
{type}
{inspTypeCatOptions.find((o) => o.code === type)?.label || type}
<span className={cn(
"inline-flex items-center justify-center h-4 min-w-[16px] rounded-full text-[10px] font-bold px-1",
selectedTypeTab === type ? "bg-primary-foreground/20 text-primary-foreground" : "bg-muted-foreground/20 text-muted-foreground"