공용 — 영업 4 + 프로젝트 2 + 개발 5메뉴 PageHeader + CompactFilterBar 일괄 적용

총 11개 페이지를 동일한 페이지 구조 표준으로 마이그레이션. 페이지 메뉴명은
PageHeader 가 useMenu() 자동 매칭, 검색 영역은 CompactFilterBar/CompactFilterField,
날짜 범위는 CompactDateRange 로 통일. 모든 자체 grid 검색폼 + 자체 h1 + 자체 액션
버튼 그룹 제거.

영업관리 4:
  - sales/estimate (견적관리) — 7필드 + 결재상태 SmartSelect
  - sales/order    (주문서관리) — 9필드 (날짜 2종)
  - sales/sale     (판매관리)   — 10필드 (출하지시상태 SmartSelect)
  - sales/revenue  (매출관리)   — 11필드 (날짜 3종)

프로젝트관리 2:
  - project/progress     (진행관리)         — 11필드 (그리드 6→자동 wrap)
  - project/wbs-template (제품구분_WBS관리) — 1필드

개발관리 5:
  - development/part-regist  (PART 등록)      — 2필드 (자동완성) + 7 액션
  - development/part-search  (PART 조회)      — 2필드 + 5 액션
  - development/ebom-regist  (E-BOM 등록)     — 4필드 + 3 액션 (잔재 Field helper 제거)
  - development/ebom-search  (E-BOM 조회)     — 3필드 + 4 액션 (정/역전개)
  - development/change-list  (설계변경 리스트) — 8필드 (read-only)

DB:
  - menu_info.menu_desc 11개 메뉴 보강 (PageHeader 자동 표시)
  - docs/migration/common/menu_desc_sync.sql (멱등 UPDATE)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hjjeong
2026-05-13 17:10:07 +09:00
parent e208d26e51
commit 4f5dd8b47f
12 changed files with 736 additions and 889 deletions
+20
View File
@@ -0,0 +1,20 @@
-- ============================================================
-- 영업관리 4 + 프로젝트관리 2 + M-BOM 2 메뉴 menu_desc 보강
-- (PageHeader 자동 매칭용)
-- 2026-05-13
-- ============================================================
UPDATE menu_info SET menu_desc='고객사 견적 작성 · 발송 · 승인 (wace estimateList 1:1)' WHERE objid=100002;
UPDATE menu_info SET menu_desc='고객사 주문서 등록 · 항목 관리 (wace orderList 1:1)' WHERE objid=100003;
UPDATE menu_info SET menu_desc='출하 · 판매 처리 및 시리얼 · 송장 관리 (wace saleList 1:1)' WHERE objid=100004;
UPDATE menu_info SET menu_desc='매출 등록 · 세금계산서 · 수금 관리 (wace revenueList 1:1)' WHERE objid=100005;
UPDATE menu_info SET menu_desc='제품구분 별 WBS 템플릿 + 표준 작업 (wace 1:1)' WHERE objid=100007;
UPDATE menu_info SET menu_desc='프로젝트 진행 현황 + 8그룹 컬럼 (wace projectMgmtWbsList3 1:1)' WHERE objid=100008;
UPDATE menu_info SET menu_desc='생산용 BOM 트리 + read-only 조회 (운영판 mBomMgmtList 1:1)' WHERE objid IN (100016, 100032);
-- 개발관리 5메뉴
UPDATE menu_info SET menu_desc='PART 마스터 등록 · 수정 (wace partMngList 1:1)' WHERE objid=100010;
UPDATE menu_info SET menu_desc='PART 마스터 조회 (wace partMngListSearch 1:1)' WHERE objid=100011;
UPDATE menu_info SET menu_desc='E-BOM 등록 · CSV Import (wace structureList 1:1)' WHERE objid=100012;
UPDATE menu_info SET menu_desc='E-BOM 트리 정/역전개 조회 + Excel 다운로드 (wace 1:1)' WHERE objid=100013;
UPDATE menu_info SET menu_desc='설계변경 리스트 (read-only, wace partMngHisList 1:1)' WHERE objid=100014;
@@ -6,14 +6,13 @@
// 참조: docs/migration/development/03-eo-history.md
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Search, Loader2, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
import { devEoHistoryApi, EoHistoryListFilter, EoHistoryRow } from "@/lib/api/devEoHistory";
import { PartHisDetailDialog } from "@/components/development/PartHisDetailDialog";
@@ -92,73 +91,62 @@ export default function EoHistoryPage() {
);
return (
<div className="flex h-full flex-col">
<div className="border-b bg-card px-4 py-3">
<div className="grid grid-cols-4 gap-3 text-sm">
<Field label="년도">
<SmartSelect
options={YEAR_OPTIONS}
value={filter.Year ?? ""}
onValueChange={(v) => setFilter({ ...filter, Year: v })}
placeholder="전체"
/>
</Field>
<Field label="프로젝트 OBJID">
<Input value={filter.contract_objid ?? ""}
onChange={(e) => setFilter({ ...filter, contract_objid: e.target.value })}
placeholder="project_mgmt.objid" />
</Field>
<Field label="품번">
<Input value={filter.part_no ?? ""}
onChange={(e) => setFilter({ ...filter, part_no: e.target.value })}
placeholder="part_no LIKE" />
</Field>
<Field label="품명">
<Input value={filter.part_name ?? ""}
onChange={(e) => setFilter({ ...filter, part_name: e.target.value })}
placeholder="part_name LIKE" />
</Field>
<div className="flex h-full flex-col gap-2 p-2">
<PageHeader />
<Field label="EO Date 시작">
<Input type="date" value={filter.eo_start_date ?? ""}
onChange={(e) => setFilter({ ...filter, eo_start_date: e.target.value })} />
</Field>
<Field label="EO Date 종료">
<Input type="date" value={filter.eo_end_date ?? ""}
onChange={(e) => setFilter({ ...filter, eo_end_date: e.target.value })} />
</Field>
<Field label="PART구분">
<CommCodeSelect groupId={GROUP_PART_TYPE}
value={filter.part_type ?? ""}
onValueChange={(v) => setFilter({ ...filter, part_type: v })} />
</Field>
<Field label="EO구분 / EO사유 (code_id)">
<div className="flex items-center gap-1">
<Input value={filter.change_type ?? ""}
onChange={(e) => setFilter({ ...filter, change_type: e.target.value })}
placeholder="EO구분 code_id" />
<Input value={filter.change_option ?? ""}
onChange={(e) => setFilter({ ...filter, change_option: e.target.value })}
placeholder="EO사유 code_id" />
</div>
</Field>
</div>
<div className="mt-3 flex items-center justify-between">
<div className="text-xs text-muted-foreground"> {total.toLocaleString()} (read-only)</div>
<div className="flex items-end gap-2">
<Button variant="outline" size="sm"
onClick={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}>
<RotateCcw className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" onClick={() => fetchList()} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
</div>
</div>
</div>
<CompactFilterBar
loading={loading}
onSearch={() => fetchList()}
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
totalText={<> {total.toLocaleString()} (read-only)</>}
>
<CompactFilterField label="년도" width={100}>
<SmartSelect
options={YEAR_OPTIONS}
value={filter.Year ?? ""}
onValueChange={(v) => setFilter({ ...filter, Year: v })}
placeholder="전체"
/>
</CompactFilterField>
<CompactFilterField label="프로젝트 OBJID" width={180}>
<Input value={filter.contract_objid ?? ""}
onChange={(e) => setFilter({ ...filter, contract_objid: e.target.value })}
placeholder="project_mgmt.objid" />
</CompactFilterField>
<CompactFilterField label="품번" width={140}>
<Input value={filter.part_no ?? ""}
onChange={(e) => setFilter({ ...filter, part_no: e.target.value })}
placeholder="part_no LIKE" />
</CompactFilterField>
<CompactFilterField label="품명" width={150}>
<Input value={filter.part_name ?? ""}
onChange={(e) => setFilter({ ...filter, part_name: e.target.value })}
placeholder="part_name LIKE" />
</CompactFilterField>
<CompactFilterField label="EO Date" width={280}>
<CompactDateRange
from={filter.eo_start_date ?? ""}
setFrom={(v) => setFilter({ ...filter, eo_start_date: v })}
to={filter.eo_end_date ?? ""}
setTo={(v) => setFilter({ ...filter, eo_end_date: v })}
/>
</CompactFilterField>
<CompactFilterField label="PART구분" width={140}>
<CommCodeSelect groupId={GROUP_PART_TYPE}
value={filter.part_type ?? ""}
onValueChange={(v) => setFilter({ ...filter, part_type: v })} />
</CompactFilterField>
<CompactFilterField label="EO구분 code_id" width={140}>
<Input value={filter.change_type ?? ""}
onChange={(e) => setFilter({ ...filter, change_type: e.target.value })} />
</CompactFilterField>
<CompactFilterField label="EO사유 code_id" width={140}>
<Input value={filter.change_option ?? ""}
onChange={(e) => setFilter({ ...filter, change_option: e.target.value })} />
</CompactFilterField>
</CompactFilterBar>
<div className="min-h-0 flex-1 p-2">
<div className="min-h-0 flex-1">
<DataGrid
columns={columns}
data={rows}
@@ -178,11 +166,3 @@ export default function EoHistoryPage() {
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<Label className="mb-1 block text-xs text-muted-foreground">{label}</Label>
{children}
</div>
);
}
@@ -7,15 +7,13 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Search, Loader2, RotateCcw, Trash2, Settings, FileSpreadsheet,
} from "lucide-react";
import { Trash2, Settings, FileSpreadsheet } from "lucide-react";
import { toast } from "sonner";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
import { devBomApi, BomReportListFilter, BomReportRow } from "@/lib/api/devBom";
import { BomReportStatusDialog } from "@/components/development/BomReportStatusDialog";
import { DevPartSelect } from "@/components/development/DevPartSelect";
@@ -116,72 +114,66 @@ export default function EbomRegistPage() {
const gridRows = useMemo(() => rows.map((r) => ({ ...r, id: r.objid })), [rows]);
return (
<div className="flex h-full flex-col">
<div className="border-b bg-card px-4 py-3">
<div className="grid grid-cols-4 gap-3 text-sm">
<Field label="제품구분">
<CommCodeSelect
groupId={PRODUCT_GROUP}
value={filter.product_cd ?? ""}
onValueChange={(v) => setFilter({ ...filter, product_cd: v })}
/>
</Field>
<Field label="상태">
<SmartSelect
options={STATUS_OPTIONS}
value={filter.status ?? ""}
onValueChange={(v) => setFilter({ ...filter, status: v })}
placeholder="전체"
/>
</Field>
{/* wace structureList.jsp 1:1 — select2-part 자동완성 (양방향 동기) */}
<Field label="품번">
<DevPartSelect mode="partNo"
value={filter.search_part_no ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_no: v,
search_part_name: row?.part_name ?? prev.search_part_name,
}))} />
</Field>
<Field label="품명">
<DevPartSelect mode="partName"
value={filter.search_part_name ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_name: v,
search_part_no: row?.part_no ?? prev.search_part_no,
}))} />
</Field>
</div>
<div className="mt-3 flex items-center justify-between">
<div className="text-xs text-muted-foreground"> {total.toLocaleString()}</div>
<div className="flex items-end gap-2">
<Button variant="outline" size="sm"
onClick={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}>
<RotateCcw className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" onClick={() => fetchList()} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
<Button size="sm" onClick={() => setExcelOpen(true)}
className="bg-emerald-600 hover:bg-emerald-700 text-white">
<FileSpreadsheet className="h-4 w-4" /><span className="ml-1">E-BOM (Excel)</span>
</Button>
<Button size="sm" variant="secondary" onClick={handleStatusChange}
disabled={checkedIds.length !== 1}>
<Settings className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" variant="destructive" onClick={handleDelete}
disabled={checkedIds.length === 0}>
<Trash2 className="h-4 w-4" /><span className="ml-1"></span>
</Button>
</div>
</div>
</div>
<div className="flex h-full flex-col gap-2 p-2">
<PageHeader actions={
<>
<Button size="sm" onClick={() => setExcelOpen(true)}
className="h-8 gap-1 bg-emerald-600 hover:bg-emerald-700 text-white text-xs">
<FileSpreadsheet className="h-3.5 w-3.5" />E-BOM (Excel)
</Button>
<Button size="sm" variant="secondary" className="h-8 gap-1 text-xs" onClick={handleStatusChange}
disabled={checkedIds.length !== 1}>
<Settings className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="destructive" className="h-8 gap-1 text-xs" onClick={handleDelete}
disabled={checkedIds.length === 0}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</>
} />
<div className="min-h-0 flex-1 p-2">
<CompactFilterBar
loading={loading}
onSearch={() => fetchList()}
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
totalText={<> {total.toLocaleString()}</>}
>
<CompactFilterField label="제품구분" width={160}>
<CommCodeSelect
groupId={PRODUCT_GROUP}
value={filter.product_cd ?? ""}
onValueChange={(v) => setFilter({ ...filter, product_cd: v })}
/>
</CompactFilterField>
<CompactFilterField label="상태" width={140}>
<SmartSelect
options={STATUS_OPTIONS}
value={filter.status ?? ""}
onValueChange={(v) => setFilter({ ...filter, status: v })}
placeholder="전체"
/>
</CompactFilterField>
<CompactFilterField label="품번" width={200}>
<DevPartSelect mode="partNo"
value={filter.search_part_no ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_no: v,
search_part_name: row?.part_name ?? prev.search_part_name,
}))} />
</CompactFilterField>
<CompactFilterField label="품명" width={220}>
<DevPartSelect mode="partName"
value={filter.search_part_name ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_name: v,
search_part_no: row?.part_no ?? prev.search_part_no,
}))} />
</CompactFilterField>
</CompactFilterBar>
<div className="min-h-0 flex-1">
<DataGrid
columns={columns}
data={gridRows}
@@ -216,11 +208,3 @@ export default function EbomRegistPage() {
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<Label className="mb-1 block text-xs text-muted-foreground">{label}</Label>
{children}
</div>
);
}
@@ -6,11 +6,9 @@
import React, { useCallback, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Search, Loader2, RotateCcw, ChevronsRight, ChevronsLeft, FileSpreadsheet,
} from "lucide-react";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
import { Loader2, ChevronsRight, ChevronsLeft, FileSpreadsheet } from "lucide-react";
import { toast } from "sonner";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { devBomApi, BomTreeFilter, BomTreeRow } from "@/lib/api/devBom";
@@ -186,81 +184,80 @@ export default function EbomSearchPage() {
}, [rows, maxLevel, hasChildSet, ancestorsByChildId, collapsedChildIds]);
return (
<div className="flex h-full flex-col">
<div className="border-b bg-card px-4 py-3">
{/* 운영판 wace structureAscendingList.jsp 1:1 — 노출 검색 필드 3개
(고객사/프로젝트번호/유닛명 은 운영판에서도 주석 처리되어 노출 안 됨) */}
<div className="grid grid-cols-3 gap-3 text-sm">
<Field label="품번">
<DevPartSelect mode="partNo"
value={filter.search_part_no ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_no: v,
// 품번 선택 시 품명 자동 채움 (wace select2-part 1:1)
search_part_name: row?.part_name ?? prev.search_part_name,
}))} />
</Field>
<Field label="품명">
<DevPartSelect mode="partName"
value={filter.search_part_name ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_name: v,
// 품명 선택 시 품번 자동 채움
search_part_no: row?.part_no ?? prev.search_part_no,
}))} />
</Field>
<Field label="표시 레벨">
<SmartSelect
options={LEVEL_OPTIONS}
value={String(filter.search_level ?? "")}
onValueChange={(v) => setFilter({ ...filter, search_level: v })}
placeholder="전체"
/>
</Field>
</div>
<div className="mt-3 flex items-center justify-between">
<div className="text-xs text-muted-foreground">
: {direction === "ascending" ? "정전개 (루트 → 리프)" : "역전개 (리프 → 부모)"} · {rows.length.toLocaleString()} · MAX_LEVEL = {maxLevel}
</div>
<div className="flex items-end gap-2">
<Button variant="outline" size="sm"
onClick={() => { setFilter(EMPTY_FILTER); setRows([]); setMaxLevel(0); }}>
<RotateCcw className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" onClick={() => runQuery("ascending")} disabled={loading}
variant={direction === "ascending" ? "default" : "secondary"}>
{loading && direction === "ascending"
? <Loader2 className="h-4 w-4 animate-spin" />
: <ChevronsRight className="h-4 w-4" />}
<span className="ml-1"> </span>
</Button>
<Button size="sm" onClick={() => runQuery("descending")} disabled={loading}
variant={direction === "descending" ? "default" : "secondary"}>
{loading && direction === "descending"
? <Loader2 className="h-4 w-4 animate-spin" />
: <ChevronsLeft className="h-4 w-4" />}
<span className="ml-1"> </span>
</Button>
<Button size="sm" variant="outline" onClick={() => downloadExcel("ascending")} disabled={exporting}>
{exporting ? <Loader2 className="h-4 w-4 animate-spin" /> : <FileSpreadsheet className="h-4 w-4" />}
<span className="ml-1"> </span>
</Button>
<Button size="sm" variant="outline" onClick={() => downloadExcel("descending")} disabled={exporting}>
{exporting ? <Loader2 className="h-4 w-4 animate-spin" /> : <FileSpreadsheet className="h-4 w-4" />}
<span className="ml-1"> </span>
</Button>
</div>
</div>
{direction === "descending" && (
<div className="mt-2 text-xs text-amber-600">
.
</div>
)}
</div>
<div className="flex h-full flex-col gap-2 p-2">
<PageHeader actions={
<>
<Button size="sm" onClick={() => runQuery("ascending")} disabled={loading}
variant={direction === "ascending" ? "default" : "secondary"}
className="h-8 gap-1 text-xs">
{loading && direction === "ascending"
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
: <ChevronsRight className="h-3.5 w-3.5" />}
</Button>
<Button size="sm" onClick={() => runQuery("descending")} disabled={loading}
variant={direction === "descending" ? "default" : "secondary"}
className="h-8 gap-1 text-xs">
{loading && direction === "descending"
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
: <ChevronsLeft className="h-3.5 w-3.5" />}
</Button>
<Button size="sm" variant="outline" className="h-8 gap-1 text-xs" onClick={() => downloadExcel("ascending")} disabled={exporting}>
{exporting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <FileSpreadsheet className="h-3.5 w-3.5" />}
</Button>
<Button size="sm" variant="outline" className="h-8 gap-1 text-xs" onClick={() => downloadExcel("descending")} disabled={exporting}>
{exporting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <FileSpreadsheet className="h-3.5 w-3.5" />}
</Button>
</>
} />
<div className="min-h-0 flex-1 p-2">
{/* 운영판 wace structureAscendingList.jsp 1:1 — 노출 검색 필드 3개
(고객사/프로젝트번호/유닛명 은 운영판에서도 주석 처리되어 노출 안 됨) */}
<CompactFilterBar
loading={loading}
onReset={() => { setFilter(EMPTY_FILTER); setRows([]); setMaxLevel(0); }}
totalText={<>: {direction === "ascending" ? "정전개 (루트 → 리프)" : "역전개 (리프 → 부모)"} · {rows.length.toLocaleString()} · MAX_LEVEL = {maxLevel}</>}
>
<CompactFilterField label="품번" width={200}>
<DevPartSelect mode="partNo"
value={filter.search_part_no ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_no: v,
// 품번 선택 시 품명 자동 채움 (wace select2-part 1:1)
search_part_name: row?.part_name ?? prev.search_part_name,
}))} />
</CompactFilterField>
<CompactFilterField label="품명" width={220}>
<DevPartSelect mode="partName"
value={filter.search_part_name ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_name: v,
// 품명 선택 시 품번 자동 채움
search_part_no: row?.part_no ?? prev.search_part_no,
}))} />
</CompactFilterField>
<CompactFilterField label="표시 레벨" width={120}>
<SmartSelect
options={LEVEL_OPTIONS}
value={String(filter.search_level ?? "")}
onValueChange={(v) => setFilter({ ...filter, search_level: v })}
placeholder="전체"
/>
</CompactFilterField>
</CompactFilterBar>
{direction === "descending" && (
<div className="text-xs text-amber-600 px-2">
.
</div>
)}
<div className="min-h-0 flex-1">
<DataGrid
columns={columns}
data={gridData}
@@ -280,11 +277,3 @@ export default function EbomSearchPage() {
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<Label className="mb-1 block text-xs text-muted-foreground">{label}</Label>
{children}
</div>
);
}
@@ -7,10 +7,10 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
import {
Search, Loader2, RotateCcw, Plus, Pencil, Trash2, CheckSquare, FileSpreadsheet,
Plus, Pencil, Trash2, CheckSquare, FileSpreadsheet,
} from "lucide-react";
import { toast } from "sonner";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
@@ -155,71 +155,62 @@ export default function PartRegistPage() {
};
return (
<div className="flex h-full flex-col">
{/* 검색폼 — wace partMngTempList.jsp 활성 2필드 */}
<div className="border-b bg-card px-4 py-3">
<div className="flex flex-wrap items-end gap-4">
{/* wace partMngTempList.jsp 1:1 — select2-part 자동완성 (양방향 동기) */}
<div className="min-w-[220px]">
<Label className="mb-1 block text-xs text-muted-foreground"></Label>
<DevPartSelect mode="partNo"
value={filter.search_part_no ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_no: v,
search_part_name: row?.part_name ?? prev.search_part_name,
}))} />
</div>
<div className="min-w-[220px]">
<Label className="mb-1 block text-xs text-muted-foreground"></Label>
<DevPartSelect mode="partName"
value={filter.search_part_name ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_name: v,
search_part_no: row?.part_no ?? prev.search_part_no,
}))} />
</div>
<div className="ml-auto flex items-end gap-2">
<Button variant="outline" size="sm"
onClick={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}>
<RotateCcw className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" onClick={() => fetchList()} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
<Button size="sm" variant="default" onClick={handleCreate}>
<Plus className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" variant="secondary" onClick={handleEdit}
disabled={checkedIds.length !== 1}>
<Pencil className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" variant="destructive" onClick={handleDelete}
disabled={checkedIds.length === 0}>
<Trash2 className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" variant="outline" onClick={() => setExcelOpen(true)}>
<FileSpreadsheet className="h-4 w-4" /><span className="ml-1">Excel Upload</span>
</Button>
<PartDrawingMultiUploadButton
partNoList={rows.map((r) => r.part_no).filter(Boolean) as string[]}
onUploaded={() => fetchList()}
/>
<Button size="sm" onClick={handleDeploy}
disabled={checkedIds.length === 0}
className="bg-emerald-600 hover:bg-emerald-700 text-white">
<CheckSquare className="h-4 w-4" /><span className="ml-1"></span>
</Button>
</div>
</div>
<div className="mt-2 text-xs text-muted-foreground">
{total.toLocaleString()} (M1: status 'release')
</div>
</div>
<div className="flex h-full flex-col gap-2 p-2">
<PageHeader actions={
<>
<Button size="sm" className="h-8 gap-1 text-xs" onClick={handleCreate}>
<Plus className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="secondary" className="h-8 gap-1 text-xs" onClick={handleEdit}
disabled={checkedIds.length !== 1}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="destructive" className="h-8 gap-1 text-xs" onClick={handleDelete}
disabled={checkedIds.length === 0}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="outline" className="h-8 gap-1 text-xs" onClick={() => setExcelOpen(true)}>
<FileSpreadsheet className="h-3.5 w-3.5" />Excel Upload
</Button>
<PartDrawingMultiUploadButton
partNoList={rows.map((r) => r.part_no).filter(Boolean) as string[]}
onUploaded={() => fetchList()}
/>
<Button size="sm" onClick={handleDeploy}
disabled={checkedIds.length === 0}
className="h-8 gap-1 bg-emerald-600 hover:bg-emerald-700 text-white text-xs">
<CheckSquare className="h-3.5 w-3.5" />
</Button>
</>
} />
<div className="min-h-0 flex-1 p-2">
<CompactFilterBar
loading={loading}
onSearch={() => fetchList()}
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
totalText={<> {total.toLocaleString()} (M1: status 'release')</>}
>
<CompactFilterField label="품번" width={220}>
<DevPartSelect mode="partNo"
value={filter.search_part_no ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_no: v,
search_part_name: row?.part_name ?? prev.search_part_name,
}))} />
</CompactFilterField>
<CompactFilterField label="품명" width={220}>
<DevPartSelect mode="partName"
value={filter.search_part_name ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_name: v,
search_part_no: row?.part_no ?? prev.search_part_no,
}))} />
</CompactFilterField>
</CompactFilterBar>
<div className="min-h-0 flex-1">
<DataGrid
columns={columns}
data={gridRows}
@@ -7,10 +7,10 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
import {
Search, Loader2, RotateCcw, Plus, Pencil, Trash2, FileSpreadsheet,
Plus, Pencil, Trash2, FileSpreadsheet,
} from "lucide-react";
import { toast } from "sonner";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
@@ -124,63 +124,55 @@ export default function PartSearchPage() {
};
return (
<div className="flex h-full flex-col">
<div className="border-b bg-card px-4 py-3">
<div className="flex flex-wrap items-end gap-4">
{/* wace partMngList.jsp 1:1 — select2-part 자동완성 (양방향 동기) */}
<div className="min-w-[220px]">
<Label className="mb-1 block text-xs text-muted-foreground"></Label>
<DevPartSelect mode="partNo"
value={filter.search_part_no ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_no: v,
search_part_name: row?.part_name ?? prev.search_part_name,
}))} />
</div>
<div className="min-w-[220px]">
<Label className="mb-1 block text-xs text-muted-foreground"></Label>
<DevPartSelect mode="partName"
value={filter.search_part_name ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_name: v,
search_part_no: row?.part_no ?? prev.search_part_no,
}))} />
</div>
<div className="ml-auto flex items-end gap-2">
<Button variant="outline" size="sm"
onClick={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}>
<RotateCcw className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" onClick={() => fetchList()} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
<Button size="sm" variant="default" onClick={handleCreate}>
<Plus className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" variant="secondary" onClick={handleEdit}
disabled={checkedIds.length !== 1}>
<Pencil className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" variant="destructive" onClick={handleDelete}
disabled={checkedIds.length === 0}>
<Trash2 className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" variant="outline" onClick={() => setExcelOpen(true)}>
<FileSpreadsheet className="h-4 w-4" /><span className="ml-1">Excel Upload</span>
</Button>
{/* M2 조회 — partNoList 미전달: IS_LAST='1' 전체 part_mng 매칭 (페이지 밖도 허용) */}
<PartDrawingMultiUploadButton onUploaded={() => fetchList()} />
</div>
</div>
<div className="mt-2 text-xs text-muted-foreground">
{total.toLocaleString()} (M2: status = 'release')
</div>
</div>
<div className="flex h-full flex-col gap-2 p-2">
<PageHeader actions={
<>
<Button size="sm" className="h-8 gap-1 text-xs" onClick={handleCreate}>
<Plus className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="secondary" className="h-8 gap-1 text-xs" onClick={handleEdit}
disabled={checkedIds.length !== 1}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="destructive" className="h-8 gap-1 text-xs" onClick={handleDelete}
disabled={checkedIds.length === 0}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="outline" className="h-8 gap-1 text-xs" onClick={() => setExcelOpen(true)}>
<FileSpreadsheet className="h-3.5 w-3.5" />Excel Upload
</Button>
{/* M2 조회 — partNoList 미전달: IS_LAST='1' 전체 part_mng 매칭 (페이지 밖도 허용) */}
<PartDrawingMultiUploadButton onUploaded={() => fetchList()} />
</>
} />
<div className="min-h-0 flex-1 p-2">
<CompactFilterBar
loading={loading}
onSearch={() => fetchList()}
onReset={() => { setFilter(EMPTY_FILTER); fetchList(EMPTY_FILTER); }}
totalText={<> {total.toLocaleString()} (M2: status = 'release')</>}
>
<CompactFilterField label="품번" width={220}>
<DevPartSelect mode="partNo"
value={filter.search_part_no ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_no: v,
search_part_name: row?.part_name ?? prev.search_part_name,
}))} />
</CompactFilterField>
<CompactFilterField label="품명" width={220}>
<DevPartSelect mode="partName"
value={filter.search_part_name ?? ""}
onValueChange={(v, row) => setFilter((prev) => ({
...prev,
search_part_name: v,
search_part_no: row?.part_no ?? prev.search_part_no,
}))} />
</CompactFilterField>
</CompactFilterBar>
<div className="min-h-0 flex-1">
<DataGrid
columns={columns}
data={gridRows}
@@ -11,16 +11,15 @@
// 행 클릭: P1.5에서 영업관리 OrderRegistDialog 재사용 검토 — 현재 미연결
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Search, Loader2, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { CustomerSelect } from "@/components/common/CustomerSelect";
import { PartSelect } from "@/components/common/PartSelect";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog";
import { projectMgmtApi, ProgressListFilter, ProgressRow } from "@/lib/api/projectMgmt";
@@ -147,109 +146,99 @@ export default function ProjectProgressPage() {
};
return (
<div className="flex flex-col h-full">
{/* 검색폼 — wace projectMgmtWbsList3.jsp:222-313 활성 11필드 */}
<div className="border-b bg-card px-4 py-3">
<div className="grid grid-cols-6 gap-3 text-sm">
{/* 1행 */}
<Field label="년도">
<SmartSelect
options={YEAR_OPTIONS}
value={filter.Year ?? ""}
onValueChange={(v) => setFilter({ ...filter, Year: v })}
placeholder="전체"
/>
</Field>
<Field label="프로젝트번호">
<SmartSelect
options={projectNoOptions}
value={filter.project_nos ?? ""}
onValueChange={(v) => setFilter({ ...filter, project_nos: v })}
placeholder="전체"
/>
</Field>
<Field label="주문유형">
<CommCodeSelect
groupId={CATEGORY_GROUP}
value={filter.category_cd ?? ""}
onValueChange={(v) => setFilter({ ...filter, category_cd: v })}
/>
</Field>
<Field label="고객사">
<CustomerSelect
value={filter.customer_objid ?? ""}
onValueChange={(v) => setFilter({ ...filter, customer_objid: v })}
/>
</Field>
<Field label="제품구분">
<CommCodeSelect
groupId={PRODUCT_GROUP}
value={filter.product ?? ""}
onValueChange={(v) => setFilter({ ...filter, product: v })}
/>
</Field>
<Field label="요청납기일">
<div className="flex items-center gap-1">
<Input type="date" value={filter.contract_start_date ?? ""} onChange={(e) => setFilter({ ...filter, contract_start_date: e.target.value })} />
<span className="text-xs text-muted-foreground">~</span>
<Input type="date" value={filter.contract_end_date ?? ""} onChange={(e) => setFilter({ ...filter, contract_end_date: e.target.value })} />
</div>
</Field>
<div className="flex flex-col h-full gap-2 p-2">
<PageHeader />
{/* 2행 */}
<Field label="국내/해외">
<SmartSelect
options={AREA_OPTIONS}
value={filter.area_cd ?? ""}
onValueChange={(v) => setFilter({ ...filter, area_cd: v })}
placeholder="전체"
/>
</Field>
<Field label="유/무상">
<SmartSelect
options={PAID_OPTIONS}
value={filter.free_of_charge ?? ""}
onValueChange={(v) => setFilter({ ...filter, free_of_charge: v })}
placeholder="전체"
/>
</Field>
<Field label="품번">
<PartSelect
mode="partNo"
value={filter.search_partObjId ?? ""}
onValueChange={(v) => setFilter({ ...filter, search_partObjId: v })}
/>
</Field>
<Field label="품명">
<PartSelect
mode="partName"
value={filter.search_partObjId ?? ""}
onValueChange={(v) => setFilter({ ...filter, search_partObjId: v })}
/>
</Field>
<Field label="S/N">
<Input
value={filter.serial_no ?? ""}
onChange={(e) => setFilter({ ...filter, serial_no: e.target.value })}
placeholder="S/N LIKE"
/>
</Field>
<CompactFilterBar
loading={loading}
onSearch={fetchList}
onReset={handleReset}
totalText={<> {rows.length.toLocaleString()}</>}
>
<CompactFilterField label="년도" width={100}>
<SmartSelect
options={YEAR_OPTIONS}
value={filter.Year ?? ""}
onValueChange={(v) => setFilter({ ...filter, Year: v })}
placeholder="전체"
/>
</CompactFilterField>
<CompactFilterField label="프로젝트번호" width={160}>
<SmartSelect
options={projectNoOptions}
value={filter.project_nos ?? ""}
onValueChange={(v) => setFilter({ ...filter, project_nos: v })}
placeholder="전체"
/>
</CompactFilterField>
<CompactFilterField label="주문유형" width={130}>
<CommCodeSelect
groupId={CATEGORY_GROUP}
value={filter.category_cd ?? ""}
onValueChange={(v) => setFilter({ ...filter, category_cd: v })}
/>
</CompactFilterField>
<CompactFilterField label="고객사" width={160}>
<CustomerSelect
value={filter.customer_objid ?? ""}
onValueChange={(v) => setFilter({ ...filter, customer_objid: v })}
/>
</CompactFilterField>
<CompactFilterField label="제품구분" width={130}>
<CommCodeSelect
groupId={PRODUCT_GROUP}
value={filter.product ?? ""}
onValueChange={(v) => setFilter({ ...filter, product: v })}
/>
</CompactFilterField>
<CompactFilterField label="국내/해외" width={100}>
<SmartSelect
options={AREA_OPTIONS}
value={filter.area_cd ?? ""}
onValueChange={(v) => setFilter({ ...filter, area_cd: v })}
placeholder="전체"
/>
</CompactFilterField>
<CompactFilterField label="유/무상" width={100}>
<SmartSelect
options={PAID_OPTIONS}
value={filter.free_of_charge ?? ""}
onValueChange={(v) => setFilter({ ...filter, free_of_charge: v })}
placeholder="전체"
/>
</CompactFilterField>
<CompactFilterField label="품번" width={130}>
<PartSelect
mode="partNo"
value={filter.search_partObjId ?? ""}
onValueChange={(v) => setFilter({ ...filter, search_partObjId: v })}
/>
</CompactFilterField>
<CompactFilterField label="품명" width={150}>
<PartSelect
mode="partName"
value={filter.search_partObjId ?? ""}
onValueChange={(v) => setFilter({ ...filter, search_partObjId: v })}
/>
</CompactFilterField>
<CompactFilterField label="S/N" width={120}>
<Input
value={filter.serial_no ?? ""}
onChange={(e) => setFilter({ ...filter, serial_no: e.target.value })}
placeholder="S/N LIKE"
/>
</CompactFilterField>
<CompactFilterField label="요청납기일" width={280}>
<CompactDateRange
from={filter.contract_start_date ?? ""}
setFrom={(v) => setFilter({ ...filter, contract_start_date: v })}
to={filter.contract_end_date ?? ""}
setTo={(v) => setFilter({ ...filter, contract_end_date: v })}
/>
</CompactFilterField>
</CompactFilterBar>
{/* 액션 */}
<div className="flex items-end justify-end gap-2">
<Button variant="outline" size="sm" onClick={handleReset}>
<RotateCcw className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" onClick={fetchList} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
</div>
</div>
</div>
{/* 그리드 (8그룹 18셀 평탄화) */}
<div className="flex-1 min-h-0 p-2">
<div className="flex-1 min-h-0">
<DataGrid
columns={columns}
data={rows}
@@ -264,12 +253,3 @@ export default function ProjectProgressPage() {
</div>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<Label className="mb-1 block text-xs text-muted-foreground">{label}</Label>
{children}
</div>
);
}
@@ -12,11 +12,12 @@
import React, { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Search, Loader2, RotateCcw, Plus, Trash2 } from "lucide-react";
import { Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { Label } from "@/components/ui/label";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField } from "@/components/common/CompactFilterBar";
import { wbsTemplateApi, TemplateRow } from "@/lib/api/wbsTemplate";
import { WbsTemplateDialog } from "@/components/project/WbsTemplateDialog";
@@ -110,43 +111,34 @@ export default function WbsTemplatePage() {
);
return (
<div className="flex flex-col h-full">
{/* 검색폼 — wace wbsTemplateMngList.jsp:361-371 (제품구분 1필드) */}
<div className="border-b bg-card px-4 py-3">
<div className="flex items-end gap-4">
<div className="min-w-[260px]">
<Label className="mb-1 block text-xs text-muted-foreground"></Label>
<CommCodeSelect
groupId={PRODUCT_GROUP}
value={filterProduct}
onValueChange={setFilterProduct}
/>
</div>
<div className="ml-auto flex items-end gap-2">
<Button variant="outline" size="sm" onClick={handleReset}>
<RotateCcw className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button size="sm" onClick={handleSearch} disabled={loading}>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
<span className="ml-1"></span>
</Button>
<Button size="sm" variant="default" onClick={handleRegist}>
<Plus className="h-4 w-4" /><span className="ml-1"></span>
</Button>
<Button
size="sm"
variant="destructive"
onClick={handleDelete}
disabled={checkedIds.length === 0}
>
<Trash2 className="h-4 w-4" /><span className="ml-1"></span>
</Button>
</div>
</div>
</div>
<div className="flex flex-col h-full gap-2 p-2">
<PageHeader actions={
<>
<Button size="sm" className="h-8 gap-1 text-xs" onClick={handleRegist}>
<Plus className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="destructive" className="h-8 gap-1 text-xs" onClick={handleDelete} disabled={checkedIds.length === 0}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</>
} />
{/* 그리드 (5컬럼) */}
<div className="flex-1 min-h-0 p-2">
<CompactFilterBar
loading={loading}
onSearch={handleSearch}
onReset={handleReset}
totalText={<> {rows.length.toLocaleString()}</>}
>
<CompactFilterField label="제품구분" width={200}>
<CommCodeSelect
groupId={PRODUCT_GROUP}
value={filterProduct}
onValueChange={setFilterProduct}
/>
</CompactFilterField>
</CompactFilterBar>
<div className="flex-1 min-h-0">
<DataGrid
columns={columns}
data={rows}
@@ -19,6 +19,9 @@ import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CustomerSelect } from "@/components/common/CustomerSelect";
import { PartSelect } from "@/components/common/PartSelect";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
import { ItemSearchDialog, ItemRow } from "@/components/common/ItemSearchDialog";
import { AttachmentDialog } from "@/components/common/AttachmentDialog";
import { EstimateMailDialog } from "@/components/sales/EstimateMailDialog";
@@ -472,112 +475,94 @@ export default function SalesEstimatePage() {
// ─── 렌더 ───────────────────────────────────────────────────
const apprStatusOpts: SmartSelectOption[] = [
{ code: "작성중", label: "작성중" },
{ code: "결재중", label: "결재중" },
{ code: "결재완료", label: "결재완료" },
{ code: "반려", label: "반려" },
{ code: "결재불필요", label: "결재불필요" },
];
const handleReset = () => setSearchForm({
category_cd: "", customer_objid: "",
search_partObjId: "", search_partName: "", search_serialNo: "",
appr_status: "",
receipt_start_date: "", receipt_end_date: "",
});
return (
<div className="flex h-full flex-col overflow-hidden p-4 gap-4">
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
{ConfirmDialogComponent}
{/* 헤더 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold"> _ </h1>
<p className="text-sm text-muted-foreground"> {rows.length}</p>
</div>
<div className="flex gap-2 flex-wrap">
<Button variant="outline" size="sm" onClick={fetchList} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Search className="w-4 h-4 mr-1" />}
<PageHeader actions={
<>
<Button size="sm" variant="destructive" className="h-8 gap-1 text-xs" onClick={handleDelete} disabled={!selected}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="destructive" onClick={handleDelete} disabled={!selected}>
<Trash2 className="w-4 h-4 mr-1" />
</Button>
<Button size="sm" onClick={() => { if (selected) openEdit(); else openCreate(); }}>
{selected ? <Pencil className="w-4 h-4 mr-1" /> : <Plus className="w-4 h-4 mr-1" />}
<Button size="sm" className="h-8 gap-1 text-xs" onClick={() => { if (selected) openEdit(); else openCreate(); }}>
{selected ? <Pencil className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
{selected ? "견적요청수정" : "견적요청등록"}
</Button>
<Button size="sm" variant="outline" onClick={openTemplateChoice} disabled={!selected}>
<Pencil className="w-4 h-4 mr-1" />
<Button size="sm" variant="outline" className="h-8 gap-1 text-xs" onClick={openTemplateChoice} disabled={!selected}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button size="sm" className="bg-sky-600 hover:bg-sky-700 text-white"
<Button size="sm" className="h-8 gap-1 bg-sky-600 hover:bg-sky-700 text-white text-xs"
onClick={handleAmaranthApproval} disabled={!selected}>
<Send className="w-4 h-4 mr-1" />
<Send className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="outline" disabled={!selected}
onClick={openMailDialog}>
<Button size="sm" variant="outline" className="h-8 gap-1 text-xs" disabled={!selected} onClick={openMailDialog}>
</Button>
<Button size="sm" variant="ghost"
onClick={() => setSearchForm({
category_cd: "", customer_objid: "",
search_partObjId: "", search_partName: "", search_serialNo: "",
appr_status: "",
receipt_start_date: "", receipt_end_date: "",
})}>
</Button>
</div>
</div>
</>
} />
{/* 검색 폼 — wace 원본 estimateList_new.jsp 활성 7개 */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-x-2 gap-y-1.5 p-2 border rounded-md bg-muted/30">
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<CompactFilterBar
loading={loading}
onSearch={fetchList}
onReset={handleReset}
totalText={<> {rows.length.toLocaleString()}</>}
>
<CompactFilterField label="주문유형" width={130}>
<CommCodeSelect groupId="0000167"
value={searchForm.category_cd}
onValueChange={(v) => setSearchForm({ ...searchForm, category_cd: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, category_cd: v })} />
</CompactFilterField>
<CompactFilterField label="고객사" width={160}>
<CustomerSelect
value={searchForm.customer_objid}
onValueChange={(v) => setSearchForm({ ...searchForm, customer_objid: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, customer_objid: v })} />
</CompactFilterField>
<CompactFilterField label="품번" width={130}>
<PartSelect mode="partNo"
value={searchForm.search_partObjId}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })} />
</CompactFilterField>
<CompactFilterField label="품명" width={150}>
<PartSelect mode="partName"
value={searchForm.search_partObjId}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground">S/N</Label>
<Input className="h-8 text-xs" value={searchForm.search_serialNo}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })} />
</CompactFilterField>
<CompactFilterField label="S/N" width={120}>
<Input value={searchForm.search_serialNo}
onChange={(e) => setSearchForm({ ...searchForm, search_serialNo: e.target.value })} />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<Select value={searchForm.appr_status || "all"}
onValueChange={(v) => setSearchForm({ ...searchForm, appr_status: v === "all" ? "" : v })}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="전체" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="작성중"></SelectItem>
<SelectItem value="결재중"></SelectItem>
<SelectItem value="결재완료"></SelectItem>
<SelectItem value="반려"></SelectItem>
<SelectItem value="결재불필요"></SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<div className="flex gap-0.5 items-center">
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.receipt_start_date}
onChange={(e) => setSearchForm({ ...searchForm, receipt_start_date: e.target.value })} />
<span className="text-[11px] text-muted-foreground">~</span>
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.receipt_end_date}
onChange={(e) => setSearchForm({ ...searchForm, receipt_end_date: e.target.value })} />
</div>
</div>
</div>
</CompactFilterField>
<CompactFilterField label="결재상태" width={120}>
<SmartSelect
options={apprStatusOpts}
value={searchForm.appr_status}
onValueChange={(v) => setSearchForm({ ...searchForm, appr_status: v })}
placeholder="전체"
/>
</CompactFilterField>
<CompactFilterField label="접수일" width={280}>
<CompactDateRange
from={searchForm.receipt_start_date}
setFrom={(v) => setSearchForm({ ...searchForm, receipt_start_date: v })}
to={searchForm.receipt_end_date}
setTo={(v) => setSearchForm({ ...searchForm, receipt_end_date: v })}
/>
</CompactFilterField>
</CompactFilterBar>
{/* 그리드 — 첫 컬럼 체크박스 (행 아무 셀 클릭으로 단일 선택, 클립/폴더 등 팝업 컬럼은 stopPropagation으로 제외) */}
<DataGrid
@@ -19,6 +19,8 @@ import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CustomerSelect } from "@/components/common/CustomerSelect";
import { PartSelect } from "@/components/common/PartSelect";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
import { ItemSearchDialog, ItemRow } from "@/components/common/ItemSearchDialog";
import { AttachmentDialog } from "@/components/common/AttachmentDialog";
import { OrderFormViewDialog } from "@/components/sales/OrderFormViewDialog";
@@ -524,120 +526,98 @@ export default function SalesOrderPage() {
}), { qty: 0, supply: 0, vat: 0, total: 0 });
}, [form.items]);
const handleReset = () => setSearchForm({
category_cd: "", search_poNo: "", customer_objid: "",
search_partObjId: "", search_partName: "", search_serialNo: "", contract_result: "",
order_start_date: "", order_end_date: "",
due_start_date: "", due_end_date: "",
});
return (
<div className="flex h-full flex-col overflow-hidden p-4 gap-4">
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
{ConfirmDialogComponent}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold"></h1>
<p className="text-sm text-muted-foreground"> {rows.length}</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={fetchList} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Search className="w-4 h-4 mr-1" />}
</Button>
<Button size="sm" onClick={() => { if (selected) openEdit(); else openCreate(); }}>
{selected ? <Pencil className="w-4 h-4 mr-1" /> : <Plus className="w-4 h-4 mr-1" />}
<PageHeader actions={
<>
<Button size="sm" className="h-8 gap-1 text-xs" onClick={() => { if (selected) openEdit(); else openCreate(); }}>
{selected ? <Pencil className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
{selected ? "수주수정" : "수주입력"}
</Button>
<Button size="sm" className="bg-emerald-600 hover:bg-emerald-700 text-white" onClick={handleConfirmOrder} disabled={!selected}>
<CheckCircle2 className="w-4 h-4 mr-1" />
<Button size="sm" className="h-8 gap-1 bg-emerald-600 hover:bg-emerald-700 text-white text-xs" onClick={handleConfirmOrder} disabled={!selected}>
<CheckCircle2 className="h-3.5 w-3.5" />
</Button>
<Button size="sm" className="bg-rose-600 hover:bg-rose-700 text-white" onClick={handleCancelOrder} disabled={!selected}>
<XCircle className="w-4 h-4 mr-1" />
<Button size="sm" className="h-8 gap-1 bg-rose-600 hover:bg-rose-700 text-white text-xs" onClick={handleCancelOrder} disabled={!selected}>
<XCircle className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="outline" onClick={handleCopyOrder} disabled={!selected}>
<Copy className="w-4 h-4 mr-1" />
<Button size="sm" variant="outline" className="h-8 gap-1 text-xs" onClick={handleCopyOrder} disabled={!selected}>
<Copy className="h-3.5 w-3.5" />
</Button>
<Button size="sm" className="bg-sky-600 hover:bg-sky-700 text-white" onClick={handleAmaranthApproval} disabled={!selected}>
<Send className="w-4 h-4 mr-1" />
<Button size="sm" className="h-8 gap-1 bg-sky-600 hover:bg-sky-700 text-white text-xs" onClick={handleAmaranthApproval} disabled={!selected}>
<Send className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="destructive" onClick={handleDelete} disabled={!selected}>
<Trash2 className="w-4 h-4 mr-1" />
<Button size="sm" variant="destructive" className="h-8 gap-1 text-xs" onClick={handleDelete} disabled={!selected}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="ghost"
onClick={() => setSearchForm({
category_cd: "", search_poNo: "", customer_objid: "",
search_partObjId: "", search_partName: "", search_serialNo: "", contract_result: "",
order_start_date: "", order_end_date: "",
due_start_date: "", due_end_date: "",
})}>
</Button>
</div>
</div>
</>
} />
{/* 검색 폼 — wace 원본 orderMgmtList.jsp 활성 9개 */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-x-2 gap-y-1.5 p-2 border rounded-md bg-muted/30">
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<CompactFilterBar
loading={loading}
onSearch={fetchList}
onReset={handleReset}
totalText={<> {rows.length.toLocaleString()}</>}
>
<CompactFilterField label="주문유형" width={130}>
<CommCodeSelect groupId="0000167"
value={searchForm.category_cd}
onValueChange={(v) => setSearchForm({ ...searchForm, category_cd: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<Input className="h-8 text-xs" placeholder="발주번호 검색"
onValueChange={(v) => setSearchForm({ ...searchForm, category_cd: v })} />
</CompactFilterField>
<CompactFilterField label="발주번호" width={140}>
<Input placeholder="발주번호 검색"
value={searchForm.search_poNo}
onChange={(e) => setSearchForm({ ...searchForm, search_poNo: e.target.value })} />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
</CompactFilterField>
<CompactFilterField label="고객사" width={160}>
<CustomerSelect
value={searchForm.customer_objid}
onValueChange={(v) => setSearchForm({ ...searchForm, customer_objid: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, customer_objid: v })} />
</CompactFilterField>
<CompactFilterField label="품번" width={130}>
<PartSelect mode="partNo"
value={searchForm.search_partObjId}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })} />
</CompactFilterField>
<CompactFilterField label="품명" width={150}>
<PartSelect mode="partName"
value={searchForm.search_partObjId}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground">S/N</Label>
<Input className="h-8 text-xs" value={searchForm.search_serialNo}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })} />
</CompactFilterField>
<CompactFilterField label="S/N" width={120}>
<Input value={searchForm.search_serialNo}
onChange={(e) => setSearchForm({ ...searchForm, search_serialNo: e.target.value })} />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
</CompactFilterField>
<CompactFilterField label="수주상태" width={130}>
<CommCodeSelect groupId="0000963"
value={searchForm.contract_result}
onValueChange={(v) => setSearchForm({ ...searchForm, contract_result: v })}
className="h-8 text-xs" />
</div>
{/* 2줄 */}
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<div className="flex gap-0.5 items-center">
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.order_start_date}
onChange={(e) => setSearchForm({ ...searchForm, order_start_date: e.target.value })} />
<span className="text-[11px] text-muted-foreground">~</span>
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.order_end_date}
onChange={(e) => setSearchForm({ ...searchForm, order_end_date: e.target.value })} />
</div>
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<div className="flex gap-0.5 items-center">
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.due_start_date}
onChange={(e) => setSearchForm({ ...searchForm, due_start_date: e.target.value })} />
<span className="text-[11px] text-muted-foreground">~</span>
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.due_end_date}
onChange={(e) => setSearchForm({ ...searchForm, due_end_date: e.target.value })} />
</div>
</div>
</div>
onValueChange={(v) => setSearchForm({ ...searchForm, contract_result: v })} />
</CompactFilterField>
<CompactFilterField label="발주일" width={280}>
<CompactDateRange
from={searchForm.order_start_date}
setFrom={(v) => setSearchForm({ ...searchForm, order_start_date: v })}
to={searchForm.order_end_date}
setTo={(v) => setSearchForm({ ...searchForm, order_end_date: v })}
/>
</CompactFilterField>
<CompactFilterField label="요청납기" width={280}>
<CompactDateRange
from={searchForm.due_start_date}
setFrom={(v) => setSearchForm({ ...searchForm, due_start_date: v })}
to={searchForm.due_end_date}
setTo={(v) => setSearchForm({ ...searchForm, due_end_date: v })}
/>
</CompactFilterField>
</CompactFilterBar>
<DataGrid
columns={gridColumns}
@@ -19,6 +19,8 @@ import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CustomerSelect } from "@/components/common/CustomerSelect";
import { PartSelect } from "@/components/common/PartSelect";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog";
import { salesSaleApi, RevenueListRow, DeadlineInfoBody } from "@/lib/api/salesSale";
@@ -176,127 +178,100 @@ export default function SalesRevenuePage() {
}
};
const handleReset = () => setSearchForm({
orderType: "", poNo: "", customer_objid: "",
productType: "", search_partObjId: "", nation: "",
serialNo: "",
salesDeadlineFrom: "", salesDeadlineTo: "",
orderDateFrom: "", orderDateTo: "",
shippingDateFrom: "", shippingDateTo: "",
});
return (
<div className="flex h-full flex-col overflow-hidden p-4 gap-4">
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
{ConfirmDialogComponent}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold"></h1>
<p className="text-sm text-muted-foreground"> {rows.length} (/ )</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={fetchList} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Search className="w-4 h-4 mr-1" />}
<PageHeader actions={
<>
<Button size="sm" className="h-8 gap-1 bg-blue-600 hover:bg-blue-700 text-white text-xs" onClick={openDeadline} disabled={!selected}>
<FileCheck2 className="h-3.5 w-3.5" />
</Button>
<Button size="sm" className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openDeadline} disabled={!selected}>
<FileCheck2 className="w-4 h-4 mr-1" />
<Button size="sm" className="h-8 gap-1 bg-emerald-600 hover:bg-emerald-700 text-white text-xs" onClick={handleConfirmDeadline} disabled={checkedIds.length === 0}>
<CheckCircle2 className="h-3.5 w-3.5" />
</Button>
<Button size="sm" className="bg-emerald-600 hover:bg-emerald-700 text-white" onClick={handleConfirmDeadline} disabled={checkedIds.length === 0}>
<CheckCircle2 className="w-4 h-4 mr-1" />
</Button>
<Button size="sm" variant="ghost"
onClick={() => setSearchForm({
orderType: "", poNo: "", customer_objid: "",
productType: "", search_partObjId: "", nation: "",
serialNo: "",
salesDeadlineFrom: "", salesDeadlineTo: "",
orderDateFrom: "", orderDateTo: "",
shippingDateFrom: "", shippingDateTo: "",
})}>
</Button>
</div>
</div>
</>
} />
{/* 검색 폼 — wace 원본 revenueMgmtList.jsp 활성 11개 */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-x-2 gap-y-1.5 p-2 border rounded-md bg-muted/30">
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<CompactFilterBar
loading={loading}
onSearch={fetchList}
onReset={handleReset}
totalText={<> {rows.length.toLocaleString()} (/ )</>}
>
<CompactFilterField label="주문유형" width={130}>
<CommCodeSelect groupId="0000167"
value={searchForm.orderType}
onValueChange={(v) => setSearchForm({ ...searchForm, orderType: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<Input className="h-8 text-xs" placeholder="발주번호 검색"
onValueChange={(v) => setSearchForm({ ...searchForm, orderType: v })} />
</CompactFilterField>
<CompactFilterField label="발주번호" width={140}>
<Input placeholder="발주번호 검색"
value={searchForm.poNo}
onChange={(e) => setSearchForm({ ...searchForm, poNo: e.target.value })} />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
</CompactFilterField>
<CompactFilterField label="고객사" width={160}>
<CustomerSelect
value={searchForm.customer_objid}
onValueChange={(v) => setSearchForm({ ...searchForm, customer_objid: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, customer_objid: v })} />
</CompactFilterField>
<CompactFilterField label="제품구분" width={130}>
<CommCodeSelect groupId="0000001"
value={searchForm.productType}
onValueChange={(v) => setSearchForm({ ...searchForm, productType: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, productType: v })} />
</CompactFilterField>
<CompactFilterField label="품번" width={130}>
<PartSelect mode="partNo"
value={searchForm.search_partObjId}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })} />
</CompactFilterField>
<CompactFilterField label="품명" width={150}>
<PartSelect mode="partName"
value={searchForm.search_partObjId}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground">/</Label>
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })} />
</CompactFilterField>
<CompactFilterField label="국내/해외" width={120}>
<CommCodeSelect groupId="0001219"
value={searchForm.nation}
onValueChange={(v) => setSearchForm({ ...searchForm, nation: v })}
className="h-8 text-xs" />
</div>
{/* 2줄 */}
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground">S/N</Label>
<Input className="h-8 text-xs"
value={searchForm.serialNo}
onValueChange={(v) => setSearchForm({ ...searchForm, nation: v })} />
</CompactFilterField>
<CompactFilterField label="S/N" width={120}>
<Input value={searchForm.serialNo}
onChange={(e) => setSearchForm({ ...searchForm, serialNo: e.target.value })} />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<div className="flex gap-0.5 items-center">
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.salesDeadlineFrom}
onChange={(e) => setSearchForm({ ...searchForm, salesDeadlineFrom: e.target.value })} />
<span className="text-[11px] text-muted-foreground">~</span>
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.salesDeadlineTo}
onChange={(e) => setSearchForm({ ...searchForm, salesDeadlineTo: e.target.value })} />
</div>
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<div className="flex gap-0.5 items-center">
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.orderDateFrom}
onChange={(e) => setSearchForm({ ...searchForm, orderDateFrom: e.target.value })} />
<span className="text-[11px] text-muted-foreground">~</span>
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.orderDateTo}
onChange={(e) => setSearchForm({ ...searchForm, orderDateTo: e.target.value })} />
</div>
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<div className="flex gap-0.5 items-center">
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.shippingDateFrom}
onChange={(e) => setSearchForm({ ...searchForm, shippingDateFrom: e.target.value })} />
<span className="text-[11px] text-muted-foreground">~</span>
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.shippingDateTo}
onChange={(e) => setSearchForm({ ...searchForm, shippingDateTo: e.target.value })} />
</div>
</div>
</div>
</CompactFilterField>
<CompactFilterField label="매출마감" width={280}>
<CompactDateRange
from={searchForm.salesDeadlineFrom}
setFrom={(v) => setSearchForm({ ...searchForm, salesDeadlineFrom: v })}
to={searchForm.salesDeadlineTo}
setTo={(v) => setSearchForm({ ...searchForm, salesDeadlineTo: v })}
/>
</CompactFilterField>
<CompactFilterField label="발주일" width={280}>
<CompactDateRange
from={searchForm.orderDateFrom}
setFrom={(v) => setSearchForm({ ...searchForm, orderDateFrom: v })}
to={searchForm.orderDateTo}
setTo={(v) => setSearchForm({ ...searchForm, orderDateTo: v })}
/>
</CompactFilterField>
<CompactFilterField label="출하일" width={280}>
<CompactDateRange
from={searchForm.shippingDateFrom}
setFrom={(v) => setSearchForm({ ...searchForm, shippingDateFrom: v })}
to={searchForm.shippingDateTo}
setTo={(v) => setSearchForm({ ...searchForm, shippingDateTo: v })}
/>
</CompactFilterField>
</CompactFilterBar>
<DataGrid
columns={columns}
@@ -17,6 +17,9 @@ import { DataGrid, DataGridColumn } from "@/components/common/DataGrid";
import { CustomerSelect } from "@/components/common/CustomerSelect";
import { PartSelect } from "@/components/common/PartSelect";
import { CommCodeSelect } from "@/components/common/CommCodeSelect";
import { SmartSelect, SmartSelectOption } from "@/components/common/SmartSelect";
import { PageHeader } from "@/components/common/PageHeader";
import { CompactFilterBar, CompactFilterField, CompactDateRange } from "@/components/common/CompactFilterBar";
import { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog";
import { salesSaleApi, SaleListRow, SaleRegisterBody } from "@/lib/api/salesSale";
@@ -167,116 +170,92 @@ export default function SalesSalePage() {
} finally { setSaving(false); }
};
return (
<div className="flex h-full flex-col overflow-hidden p-4 gap-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold"></h1>
<p className="text-sm text-muted-foreground"> {rows.length} ( )</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={fetchList} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : <Search className="w-4 h-4 mr-1" />}
</Button>
<Button size="sm" className="bg-emerald-600 hover:bg-emerald-700 text-white" onClick={openRegister} disabled={!selected}>
<Truck className="w-4 h-4 mr-1" />/
</Button>
<Button size="sm" variant="ghost"
onClick={() => setSearchForm({
orderType: "", poNo: "", customer_objid: "", search_partObjId: "",
serialNo: "", shippingStatus: "", salesStatus: "",
orderDateFrom: "", orderDateTo: "",
shippingDateFrom: "", shippingDateTo: "",
})}>
</Button>
</div>
</div>
const shippingStatusOpts: SmartSelectOption[] = [
{ code: "PENDING", label: "대기" },
{ code: "COMPLETED", label: "완료" },
{ code: "CANCELLED", label: "취소" },
];
{/* 검색 폼 — wace 원본 salesMgmtList.jsp 재현 (1줄 7개 / 2줄 3개) */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-x-2 gap-y-1.5 p-2 border rounded-md bg-muted/30">
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
const handleReset = () => setSearchForm({
orderType: "", poNo: "", customer_objid: "", search_partObjId: "",
serialNo: "", shippingStatus: "", salesStatus: "",
orderDateFrom: "", orderDateTo: "",
shippingDateFrom: "", shippingDateTo: "",
});
return (
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
<PageHeader actions={
<Button size="sm" className="h-8 gap-1 bg-emerald-600 hover:bg-emerald-700 text-white text-xs" onClick={openRegister} disabled={!selected}>
<Truck className="h-3.5 w-3.5" />/
</Button>
} />
<CompactFilterBar
loading={loading}
onSearch={fetchList}
onReset={handleReset}
totalText={<> {rows.length.toLocaleString()} ( )</>}
>
<CompactFilterField label="주문유형" width={130}>
<CommCodeSelect groupId="0000167"
value={searchForm.orderType}
onValueChange={(v) => setSearchForm({ ...searchForm, orderType: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<Input className="h-8 text-xs" placeholder="발주번호 검색"
onValueChange={(v) => setSearchForm({ ...searchForm, orderType: v })} />
</CompactFilterField>
<CompactFilterField label="발주번호" width={140}>
<Input placeholder="발주번호 검색"
value={searchForm.poNo}
onChange={(e) => setSearchForm({ ...searchForm, poNo: e.target.value })} />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
</CompactFilterField>
<CompactFilterField label="고객사" width={160}>
<CustomerSelect
value={searchForm.customer_objid}
onValueChange={(v) => setSearchForm({ ...searchForm, customer_objid: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, customer_objid: v })} />
</CompactFilterField>
<CompactFilterField label="품번" width={130}>
<PartSelect mode="partNo"
value={searchForm.search_partObjId}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })} />
</CompactFilterField>
<CompactFilterField label="품명" width={150}>
<PartSelect mode="partName"
value={searchForm.search_partObjId}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })}
className="h-8 text-xs" />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground">S/N</Label>
<Input className="h-8 text-xs"
value={searchForm.serialNo}
onValueChange={(v) => setSearchForm({ ...searchForm, search_partObjId: v })} />
</CompactFilterField>
<CompactFilterField label="S/N" width={120}>
<Input value={searchForm.serialNo}
onChange={(e) => setSearchForm({ ...searchForm, serialNo: e.target.value })} />
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<Select value={searchForm.shippingStatus || "all"}
onValueChange={(v) => setSearchForm({ ...searchForm, shippingStatus: v === "all" ? "" : v })}>
<SelectTrigger className="h-8 text-xs"><SelectValue placeholder="전체" /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="PENDING"></SelectItem>
<SelectItem value="COMPLETED"></SelectItem>
<SelectItem value="CANCELLED"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 2줄 */}
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<div className="flex gap-0.5 items-center">
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.orderDateFrom}
onChange={(e) => setSearchForm({ ...searchForm, orderDateFrom: e.target.value })} />
<span className="text-[11px] text-muted-foreground">~</span>
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.orderDateTo}
onChange={(e) => setSearchForm({ ...searchForm, orderDateTo: e.target.value })} />
</div>
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
<div className="flex gap-0.5 items-center">
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.shippingDateFrom}
onChange={(e) => setSearchForm({ ...searchForm, shippingDateFrom: e.target.value })} />
<span className="text-[11px] text-muted-foreground">~</span>
<Input type="date" className="h-8 text-xs px-1 flex-1 min-w-0" value={searchForm.shippingDateTo}
onChange={(e) => setSearchForm({ ...searchForm, shippingDateTo: e.target.value })} />
</div>
</div>
<div>
<Label className="text-[11px] mb-0.5 block text-muted-foreground"></Label>
</CompactFilterField>
<CompactFilterField label="출하지시상태" width={130}>
<SmartSelect
options={shippingStatusOpts}
value={searchForm.shippingStatus}
onValueChange={(v) => setSearchForm({ ...searchForm, shippingStatus: v })}
placeholder="전체"
/>
</CompactFilterField>
<CompactFilterField label="판매상태" width={130}>
<CommCodeSelect groupId="0900207"
value={searchForm.salesStatus}
onValueChange={(v) => setSearchForm({ ...searchForm, salesStatus: v })}
className="h-8 text-xs" />
</div>
</div>
onValueChange={(v) => setSearchForm({ ...searchForm, salesStatus: v })} />
</CompactFilterField>
<CompactFilterField label="발주일" width={280}>
<CompactDateRange
from={searchForm.orderDateFrom}
setFrom={(v) => setSearchForm({ ...searchForm, orderDateFrom: v })}
to={searchForm.orderDateTo}
setTo={(v) => setSearchForm({ ...searchForm, orderDateTo: v })}
/>
</CompactFilterField>
<CompactFilterField label="출하일" width={280}>
<CompactDateRange
from={searchForm.shippingDateFrom}
setFrom={(v) => setSearchForm({ ...searchForm, shippingDateFrom: v })}
to={searchForm.shippingDateTo}
setTo={(v) => setSearchForm({ ...searchForm, shippingDateTo: v })}
/>
</CompactFilterField>
</CompactFilterBar>
<DataGrid
columns={columns}