Merge pull request 'hjjeong' (#8) from hjjeong into main
Reviewed-on: https://g.wace.me/chpark/vexplor_rps/pulls/8
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
// 검색폼: 11필드 (1행 6 + 2행 5)
|
||||
// 행 클릭: P1.5에서 영업관리 OrderRegistDialog 재사용 검토 — 현재 미연결
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
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";
|
||||
@@ -21,8 +21,27 @@ 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 { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog";
|
||||
import { projectMgmtApi, ProgressListFilter, ProgressRow } from "@/lib/api/projectMgmt";
|
||||
|
||||
// 진행관리 row → 정규화된 ProjectInfoData 매핑
|
||||
const toProjectInfo = (r: ProgressRow): ProjectInfoData => ({
|
||||
orderType: r.category_name,
|
||||
productType: r.product_name,
|
||||
area: r.area_name,
|
||||
customer: r.customer_name,
|
||||
paidType: r.free_of_charge,
|
||||
regDate: r.reg_date,
|
||||
currency: r.contract_currency_name,
|
||||
exchangeRate: r.exchange_rate,
|
||||
partNo: r.product_item_code,
|
||||
partName: r.product_item_name,
|
||||
serialNo: r.serial_no,
|
||||
reqDelDate: r.req_del_date,
|
||||
customerRequest: r.customer_request,
|
||||
returnReason: r.return_reason_name,
|
||||
});
|
||||
|
||||
// wace projectMgmtWbsList3.jsp 컬럼 정의 1:1 (8그룹 → 평탄화, 그룹명은 라벨 prefix)
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[160px]", frozen: true },
|
||||
@@ -79,6 +98,20 @@ export default function ProjectProgressPage() {
|
||||
const [filter, setFilter] = useState<ProgressListFilter>(EMPTY_FILTER);
|
||||
const [projectNoOptions, setProjectNoOptions] = useState<SmartSelectOption[]>([]);
|
||||
|
||||
// wace `fn_openSaleRegPopup(PROJECT_NO, "detail")` 대응 — PROJECT_NO 셀 클릭 시 프로젝트 정보 다이얼로그
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
const [infoData, setInfoData] = useState<ProjectInfoData | null>(null);
|
||||
|
||||
// GRID_COLUMNS의 project_no 셀에만 onClick 주입 (DataGrid 컬럼별 onClick 패턴)
|
||||
const columns = useMemo(
|
||||
() => GRID_COLUMNS.map((col) =>
|
||||
col.key === "project_no"
|
||||
? { ...col, onClick: (row: any) => { setInfoData(toProjectInfo(row as ProgressRow)); setInfoOpen(true); } }
|
||||
: col,
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -216,7 +249,7 @@ export default function ProjectProgressPage() {
|
||||
{/* 그리드 (8그룹 18셀 평탄화) */}
|
||||
<div className="flex-1 min-h-0 p-2">
|
||||
<DataGrid
|
||||
columns={GRID_COLUMNS}
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
@@ -224,6 +257,8 @@ export default function ProjectProgressPage() {
|
||||
gridId="project-progress-wbslist3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ProjectInfoDialog open={infoOpen} onOpenChange={setInfoOpen} data={infoData} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
// 프로젝트관리 > 제품구분_WBS관리 (wace wbsTemplateMngList.jsp 1:1 이식)
|
||||
// 원본:
|
||||
// - JSP: /Users/jhj/wace_plm/WebContent/WEB-INF/view/project/wbsTemplateMngList.jsp (378줄)
|
||||
// - 매퍼: wace_plm/src/com/pms/mapper/project.xml:5552 wbsTemplateMngGridList
|
||||
// GAP: docs/migration/project/02-wbs-template.md
|
||||
//
|
||||
// 그리드: 5컬럼 (제품구분 / 제목 / WBS(folder) / 등록자 / 등록일)
|
||||
// 검색: 제품구분 단일
|
||||
// 등록/수정 통합 다이얼로그: WbsTemplateDialog (wace WBSExcelImportPopUp.jsp 1:1)
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, Loader2, RotateCcw, 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 { wbsTemplateApi, TemplateRow } from "@/lib/api/wbsTemplate";
|
||||
import { WbsTemplateDialog } from "@/components/project/WbsTemplateDialog";
|
||||
|
||||
const PRODUCT_GROUP = "0000001"; // 제품구분
|
||||
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "product_name", label: "제품구분", width: "w-[200px]", frozen: true },
|
||||
{ key: "title", label: "제목", minWidth: "min-w-[260px]" },
|
||||
{
|
||||
key: "wbs_task_cnt",
|
||||
label: "WBS",
|
||||
width: "w-[100px]",
|
||||
align: "center",
|
||||
renderType: "folder", // wace fnc_getFolderIcon
|
||||
},
|
||||
{ key: "writer_title", label: "등록자", width: "w-[180px]" },
|
||||
{ key: "reg_date_title", label: "등록일", width: "w-[130px]", align: "center" },
|
||||
];
|
||||
|
||||
export default function WbsTemplatePage() {
|
||||
const [rows, setRows] = useState<TemplateRow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filterProduct, setFilterProduct] = useState<string>("");
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editObjId, setEditObjId] = useState<string | null>(null);
|
||||
const [defaultProduct, setDefaultProduct] = useState<string>("");
|
||||
|
||||
const fetchList = useCallback(async (product?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await wbsTemplateApi.list(product || undefined);
|
||||
setRows(data);
|
||||
setCheckedIds([]);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchList();
|
||||
}, [fetchList]);
|
||||
|
||||
const handleSearch = () => fetchList(filterProduct);
|
||||
const handleReset = () => {
|
||||
setFilterProduct("");
|
||||
fetchList();
|
||||
};
|
||||
|
||||
// 등록 (wace btnRegist click — product 선택 필수)
|
||||
const handleRegist = () => {
|
||||
if (!filterProduct) {
|
||||
toast.error("제품은 필수값입니다. 제품을 선택해 주세요.");
|
||||
return;
|
||||
}
|
||||
setEditObjId(null);
|
||||
setDefaultProduct(filterProduct);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
// 수정 (wace fn_openWBSTaskListPopUp — WBS 폴더 컬럼 클릭)
|
||||
const handleOpenEdit = (row: any) => {
|
||||
setEditObjId(row.objid);
|
||||
setDefaultProduct("");
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
// 삭제 (wace fn_delete — 체크된 행 다건 삭제)
|
||||
const handleDelete = async () => {
|
||||
if (checkedIds.length === 0) {
|
||||
toast.error("선택된 대상이 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (!confirm("삭제하시겠습니까?")) return;
|
||||
try {
|
||||
const res = await wbsTemplateApi.remove(checkedIds);
|
||||
toast.success(res?.msg ?? "삭제하였습니다.");
|
||||
fetchList(filterProduct);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "삭제 실패");
|
||||
}
|
||||
};
|
||||
|
||||
// DataGrid 컬럼에 folder 클릭 핸들러 주입
|
||||
const columns: DataGridColumn[] = GRID_COLUMNS.map((c) =>
|
||||
c.key === "wbs_task_cnt" ? { ...c, onClick: handleOpenEdit } : c
|
||||
);
|
||||
|
||||
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>
|
||||
|
||||
{/* 그리드 (5컬럼) */}
|
||||
<div className="flex-1 min-h-0 p-2">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="등록된 WBS 템플릿이 없습니다."
|
||||
gridId="project-wbs-template"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 통합 팝업 */}
|
||||
<WbsTemplateDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
templateObjId={editObjId}
|
||||
defaultProduct={defaultProduct}
|
||||
onSaved={() => fetchList(filterProduct)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
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";
|
||||
@@ -19,8 +19,27 @@ 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 { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog";
|
||||
import { salesSaleApi, RevenueListRow, DeadlineInfoBody } from "@/lib/api/salesSale";
|
||||
|
||||
// RevenueListRow → 정규화된 ProjectInfoData (wace 운영판 다이얼로그 1:1)
|
||||
const toProjectInfo = (r: RevenueListRow): ProjectInfoData => ({
|
||||
orderType: r.order_type_name,
|
||||
productType: r.product_type_name,
|
||||
area: r.nation_name,
|
||||
customer: r.customer,
|
||||
paidType: r.payment_type_name,
|
||||
regDate: r.receipt_date,
|
||||
currency: r.contract_currency_name,
|
||||
exchangeRate: r.exchange_rate,
|
||||
partNo: r.product_no,
|
||||
partName: r.product_name,
|
||||
serialNo: r.serial_no,
|
||||
reqDelDate: r.request_date,
|
||||
customerRequest: r.customer_request,
|
||||
returnReason: r.return_reason_name,
|
||||
});
|
||||
|
||||
// wace_plm revenueMgmtList.jsp 컬럼 순서/라벨에 맞춤
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[170px]", frozen: true },
|
||||
@@ -70,6 +89,18 @@ export default function SalesRevenuePage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selected, setSelected] = useState<RevenueListRow | null>(null);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 프로젝트번호 셀 클릭 → 프로젝트 상세 정보 다이얼로그 (wace 운영판 1:1)
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
const [infoData, setInfoData] = useState<ProjectInfoData | null>(null);
|
||||
const columns = useMemo(
|
||||
() => GRID_COLUMNS.map((col) =>
|
||||
col.key === "project_no"
|
||||
? { ...col, onClick: (row: any) => { setInfoData(toProjectInfo(row as RevenueListRow)); setInfoOpen(true); } }
|
||||
: col,
|
||||
),
|
||||
[],
|
||||
);
|
||||
// wace revenueMgmtList.jsp 활성 11개
|
||||
const [searchForm, setSearchForm] = useState({
|
||||
orderType: "", poNo: "", customer_objid: "",
|
||||
@@ -268,7 +299,7 @@ export default function SalesRevenuePage() {
|
||||
</div>
|
||||
|
||||
<DataGrid
|
||||
columns={GRID_COLUMNS}
|
||||
columns={columns}
|
||||
data={rows}
|
||||
selectedId={selected ? String(selected.log_id) : null}
|
||||
onSelect={(id) => setSelected(id ? rows.find((r) => r.id === id) ?? null : null)}
|
||||
@@ -280,6 +311,8 @@ export default function SalesRevenuePage() {
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
<ProjectInfoDialog open={infoOpen} onOpenChange={setInfoOpen} data={infoData} />
|
||||
|
||||
{/* 마감정보 입력 Dialog */}
|
||||
<Dialog open={deadlineOpen} onOpenChange={setDeadlineOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
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";
|
||||
@@ -17,8 +17,27 @@ 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 { ProjectInfoDialog, ProjectInfoData } from "@/components/project/ProjectInfoDialog";
|
||||
import { salesSaleApi, SaleListRow, SaleRegisterBody } from "@/lib/api/salesSale";
|
||||
|
||||
// SaleListRow → 정규화된 ProjectInfoData (wace 운영판 다이얼로그 1:1)
|
||||
const toProjectInfo = (r: SaleListRow): ProjectInfoData => ({
|
||||
orderType: r.order_type_name,
|
||||
productType: r.product_type_name,
|
||||
area: r.nation_name,
|
||||
customer: r.customer,
|
||||
paidType: r.payment_type_name,
|
||||
regDate: r.receipt_date,
|
||||
currency: r.contract_currency_name,
|
||||
exchangeRate: r.exchange_rate,
|
||||
partNo: r.product_no,
|
||||
partName: r.product_name,
|
||||
serialNo: r.serial_no,
|
||||
reqDelDate: r.request_date,
|
||||
customerRequest: r.customer_request,
|
||||
returnReason: r.return_reason_name,
|
||||
});
|
||||
|
||||
// wace_plm salesMgmtList.jsp 컬럼 순서/라벨에 맞춤
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[170px]", frozen: true },
|
||||
@@ -75,6 +94,18 @@ export default function SalesSalePage() {
|
||||
shippingDateFrom: "", shippingDateTo: "",
|
||||
});
|
||||
|
||||
// 프로젝트번호 셀 클릭 → 프로젝트 상세 정보 다이얼로그 (wace 운영판 1:1)
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
const [infoData, setInfoData] = useState<ProjectInfoData | null>(null);
|
||||
const columns = useMemo(
|
||||
() => GRID_COLUMNS.map((col) =>
|
||||
col.key === "project_no"
|
||||
? { ...col, onClick: (row: any) => { setInfoData(toProjectInfo(row as SaleListRow)); setInfoOpen(true); } }
|
||||
: col,
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const [registerOpen, setRegisterOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState<SaleRegisterBody>({
|
||||
@@ -248,7 +279,7 @@ export default function SalesSalePage() {
|
||||
</div>
|
||||
|
||||
<DataGrid
|
||||
columns={GRID_COLUMNS}
|
||||
columns={columns}
|
||||
data={rows}
|
||||
selectedId={selected ? `${selected.project_no}-${selected.contract_item_objid}-0` : null}
|
||||
onSelect={(id) => setSelected(id ? rows.find((r) => r.id === id) ?? null : null)}
|
||||
@@ -257,6 +288,8 @@ export default function SalesSalePage() {
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
<ProjectInfoDialog open={infoOpen} onOpenChange={setInfoOpen} data={infoData} />
|
||||
|
||||
{/* 출하지시/판매등록 Dialog */}
|
||||
<Dialog open={registerOpen} onOpenChange={setRegisterOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
|
||||
Reference in New Issue
Block a user