공용 — 영업 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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user