영업관리 — 주문/판매/매출에 logicstudio 스타일 DataGrid 적용
견적관리(36c1f357)와 동일한 패턴을 나머지 3개 메뉴로 확장:
공통 DataGrid props:
- gridId (sales-order / sales-sale / sales-revenue) 로 컬럼 visibility·순서·너비·차트 영속
- showColumnSettings, paginationStyle="range", pageSizeOptions=[10,15,20,50,100]
- onRefresh = fetchList, onDownload = exportToExcel(GRID_COLUMNS 라벨 매핑)
- showChart
도메인별 summaryStats (하단 통계 행):
- 주문: 수주 건수 / 수주수량·수주취소 합계 / 공급가액·부가세·총액·원화총액 합계
- 판매: 판매 라인 / 수주·판매·잔량 수량 합계 / 판매공급가액·판매원화·잔량원화 합계
- 매출: 매출 이력 / 수량 합계 / 공급가액·부가세·총액·원화총액 합계
컬럼 폭 보정:
- ⋮⋮ 드래그 핸들 추가로 좁아진 4글자 한국어 라벨이 잘리지 않도록 95~135px로 확대
- 시스템 컬럼: 주문관리 writer_name (systemColumnKeys 분리)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,33 +26,34 @@ import { AttachmentDialog } from "@/components/common/AttachmentDialog";
|
||||
import { OrderFormViewDialog } from "@/components/sales/OrderFormViewDialog";
|
||||
import { OrderRegistDialog } from "@/components/sales/OrderRegistDialog";
|
||||
import { salesOrderMgmtApi, OrderRow, OrderBody, OrderItem } from "@/lib/api/salesOrderMgmt";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// wace_plm orderMgmtList.jsp 컬럼 순서/라벨에 맞춤
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "contract_no", label: "영업번호", width: "w-[125px]", frozen: true },
|
||||
{ key: "category_name", label: "주문유형", width: "w-[90px]", align: "center" },
|
||||
{ key: "order_date", label: "발주일", width: "w-[120px]", align: "center" },
|
||||
{ key: "po_no", label: "발주번호", width: "w-[130px]" },
|
||||
{ key: "category_name", label: "주문유형", width: "w-[115px]", align: "center" },
|
||||
{ key: "order_date", label: "발주일", width: "w-[115px]", align: "center" },
|
||||
{ key: "po_no", label: "발주번호", width: "w-[140px]" },
|
||||
{ key: "earliest_due_date_label", label: "요청납기", width: "w-[160px]", align: "center" },
|
||||
{ key: "customer_name", label: "고객사", width: "w-[160px]" },
|
||||
{ key: "item_summary", label: "품명", width: "w-[200px]" },
|
||||
{ key: "order_quantity", label: "수주수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "cancel_qty_sum", label: "수주취소", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "paid_type_name", label: "유/무상", width: "w-[80px]", align: "center" },
|
||||
{ key: "contract_result_name", label: "수주상태", width: "w-[100px]", align: "center" },
|
||||
{ key: "order_supply_price_sum", label: "공급가액", width: "w-[120px]", formatMoney: true },
|
||||
{ key: "order_vat_sum", label: "부가세", width: "w-[100px]", formatMoney: true },
|
||||
{ key: "order_quantity", label: "수주수량", width: "w-[115px]", align: "right", formatNumber: true },
|
||||
{ key: "cancel_qty_sum", label: "수주취소", width: "w-[115px]", align: "right", formatNumber: true },
|
||||
{ key: "paid_type_name", label: "유/무상", width: "w-[100px]", align: "center" },
|
||||
{ key: "contract_result_name", label: "수주상태", width: "w-[115px]", align: "center" },
|
||||
{ key: "order_supply_price_sum", label: "공급가액", width: "w-[130px]", formatMoney: true },
|
||||
{ key: "order_vat_sum", label: "부가세", width: "w-[115px]", formatMoney: true },
|
||||
{ key: "order_total_amount_sum", label: "총액", width: "w-[120px]", formatMoney: true },
|
||||
{ key: "order_total_amount_krw", label: "원화총액", width: "w-[120px]", formatMoney: true },
|
||||
{ key: "cu01_cnt", label: "주문서첨부", width: "w-[90px]", align: "center", renderType: "clip" },
|
||||
{ key: "has_order_data", label: "주문서", width: "w-[80px]", align: "center", renderType: "folder" },
|
||||
{ key: "order_total_amount_krw", label: "원화총액", width: "w-[130px]", formatMoney: true },
|
||||
{ key: "cu01_cnt", label: "주문서첨부", width: "w-[115px]", align: "center", renderType: "clip" },
|
||||
{ key: "has_order_data", label: "주문서", width: "w-[100px]", align: "center", renderType: "folder" },
|
||||
{ key: "customer_request", label: "고객사요청사항", width: "w-[180px]" },
|
||||
{ key: "order_appr_status", label: "결재상태", width: "w-[90px]", align: "center" },
|
||||
{ key: "contract_currency_name", label: "환종", width: "w-[70px]", align: "center" },
|
||||
{ key: "exchange_rate", label: "환율", width: "w-[80px]", formatMoney: true },
|
||||
{ key: "order_appr_status", label: "결재상태", width: "w-[115px]", align: "center" },
|
||||
{ key: "contract_currency_name", label: "환종", width: "w-[95px]", align: "center" },
|
||||
{ key: "exchange_rate", label: "환율", width: "w-[95px]", formatMoney: true },
|
||||
{ key: "serial_no", label: "S/N", width: "w-[140px]" },
|
||||
{ key: "part_no", label: "품번", width: "w-[120px]" },
|
||||
{ key: "writer_name", label: "작성자", width: "w-[110px]" },
|
||||
{ key: "writer_name", label: "작성자", width: "w-[115px]" },
|
||||
/* wace orderMgmtList.jsp 429~434 — 비활성(주석) 컬럼. 활성화 시 아래 주석 해제.
|
||||
{ key: "product_name", label: "제품구분", width: "w-[90px]", align: "center" },
|
||||
{ key: "area_name", label: "국내/해외", width: "w-[90px]", align: "center" },
|
||||
@@ -209,6 +210,29 @@ export default function SalesOrderPage() {
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
// ─── 하단 통계 ──────────────────────────────────────────────
|
||||
// 수주 건수 / 수주수량·취소 합계 / 공급가액·부가세·총액·원화총액 합계
|
||||
const orderSummary = useMemo(() => {
|
||||
const count = rows.length;
|
||||
const qtySum = rows.reduce((acc, r) => acc + Number(r.order_quantity || 0), 0);
|
||||
const cancelSum = rows.reduce((acc, r) => acc + Number(r.cancel_qty_sum || 0), 0);
|
||||
const supplySum = rows.reduce((acc, r) => acc + Number(r.order_supply_price_sum || 0), 0);
|
||||
const vatSum = rows.reduce((acc, r) => acc + Number(r.order_vat_sum || 0), 0);
|
||||
const totalSum = rows.reduce((acc, r) => acc + Number(r.order_total_amount_sum || 0), 0);
|
||||
const krwSum = rows.reduce((acc, r) => acc + Number(r.order_total_amount_krw || 0), 0);
|
||||
const money = (n: number) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
const intFmt = (n: number) => n.toLocaleString();
|
||||
return [
|
||||
{ label: "수주 건수", value: intFmt(count), suffix: "건" },
|
||||
{ label: "수주수량 합계", value: intFmt(qtySum) },
|
||||
{ label: "수주취소 합계", value: intFmt(cancelSum) },
|
||||
{ label: "공급가액 합계", value: money(supplySum) },
|
||||
{ label: "부가세 합계", value: money(vatSum) },
|
||||
{ label: "총액 합계", value: money(totalSum) },
|
||||
{ label: "원화총액 합계", value: money(krwSum) },
|
||||
];
|
||||
}, [rows]);
|
||||
|
||||
const openCreate = async () => {
|
||||
setDialogMode("create");
|
||||
const contractNo = await salesOrderMgmtApi.generateNumber().catch(() => "");
|
||||
@@ -619,6 +643,7 @@ export default function SalesOrderPage() {
|
||||
</CompactFilterBar>
|
||||
|
||||
<DataGrid
|
||||
gridId="sales-order"
|
||||
columns={gridColumns}
|
||||
data={rows}
|
||||
showCheckbox
|
||||
@@ -634,6 +659,22 @@ export default function SalesOrderPage() {
|
||||
onRowDoubleClick={(row) => { setSelected(row); openEdit(); }}
|
||||
emptyMessage={loading ? "조회 중..." : "데이터가 없습니다"}
|
||||
loading={loading}
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={orderSummary}
|
||||
systemColumnKeys={["writer_name"]}
|
||||
onRefresh={fetchList}
|
||||
onDownload={() => {
|
||||
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = rows.map((r) => {
|
||||
const out: Record<string, any> = {};
|
||||
GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "주문관리.xlsx", "주문관리");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
|
||||
@@ -23,6 +23,7 @@ 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";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// RevenueListRow → 정규화된 ProjectInfoData (wace 운영판 다이얼로그 1:1)
|
||||
const toProjectInfo = (r: RevenueListRow): ProjectInfoData => ({
|
||||
@@ -45,31 +46,31 @@ const toProjectInfo = (r: RevenueListRow): ProjectInfoData => ({
|
||||
// wace_plm revenueMgmtList.jsp 컬럼 순서/라벨에 맞춤
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[170px]", frozen: true },
|
||||
{ key: "order_type_name", label: "주문유형", width: "w-[90px]", align: "center" },
|
||||
{ key: "sales_deadline_date", label: "매출마감", width: "w-[110px]", align: "center" },
|
||||
{ key: "order_type_name", label: "주문유형", width: "w-[115px]", align: "center" },
|
||||
{ key: "sales_deadline_date", label: "매출마감", width: "w-[115px]", align: "center" },
|
||||
{ key: "order_date", label: "발주일", width: "w-[115px]", align: "center" },
|
||||
{ key: "po_no", label: "발주번호", width: "w-[140px]" },
|
||||
{ key: "customer", label: "고객사", width: "w-[160px]" },
|
||||
{ key: "product_type_name", label: "제품구분", width: "w-[90px]", align: "center" },
|
||||
{ key: "product_type_name", label: "제품구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "product_name", label: "품명", width: "w-[180px]" },
|
||||
{ key: "sales_quantity", label: "수량", width: "w-[80px]", align: "right", formatNumber: true },
|
||||
{ key: "sales_unit_price", label: "단가", width: "w-[110px]", formatMoney: true },
|
||||
{ key: "sales_supply_price", label: "공급가액", width: "w-[120px]", formatMoney: true },
|
||||
{ key: "sales_vat", label: "부가세", width: "w-[100px]", formatMoney: true },
|
||||
{ key: "sales_quantity", label: "수량", width: "w-[95px]", align: "right", formatNumber: true },
|
||||
{ key: "sales_unit_price", label: "단가", width: "w-[115px]", formatMoney: true },
|
||||
{ key: "sales_supply_price", label: "공급가액", width: "w-[130px]", formatMoney: true },
|
||||
{ key: "sales_vat", label: "부가세", width: "w-[115px]", formatMoney: true },
|
||||
{ key: "sales_total_amount", label: "총액", width: "w-[120px]", formatMoney: true },
|
||||
{ key: "sales_total_amount_krw", label: "원화총액", width: "w-[120px]", formatMoney: true },
|
||||
{ key: "sales_total_amount_krw", label: "원화총액", width: "w-[130px]", formatMoney: true },
|
||||
{ key: "shipping_date", label: "출하일", width: "w-[115px]", align: "center" },
|
||||
{ key: "nation_name", label: "국내/해외", width: "w-[90px]", align: "center" },
|
||||
{ key: "sales_currency_name", label: "환종", width: "w-[70px]", align: "center" },
|
||||
{ key: "sales_exchange_rate", label: "환율", width: "w-[80px]", formatMoney: true },
|
||||
{ key: "nation_name", label: "국내/해외", width: "w-[115px]", align: "center" },
|
||||
{ key: "sales_currency_name", label: "환종", width: "w-[95px]", align: "center" },
|
||||
{ key: "sales_exchange_rate", label: "환율", width: "w-[95px]", formatMoney: true },
|
||||
{ key: "serial_no", label: "S/N", width: "w-[140px]" },
|
||||
{ key: "split_serial_no", label: "분할S/N", width: "w-[140px]" },
|
||||
{ key: "product_no", label: "품번", width: "w-[120px]" },
|
||||
{ key: "tax_type", label: "과세구분", width: "w-[100px]", align: "center" },
|
||||
{ key: "tax_invoice_date", label: "세금계산서발행일", width: "w-[140px]", align: "center" },
|
||||
{ key: "export_decl_no", label: "수출신고필증신고번호", width: "w-[160px]" },
|
||||
{ key: "loading_date", label: "선적일자", width: "w-[100px]", align: "center" },
|
||||
{ key: "has_transaction_statement", label: "거래명세서", width: "w-[100px]", align: "center" },
|
||||
{ key: "tax_type", label: "과세구분", width: "w-[115px]", align: "center" },
|
||||
{ key: "tax_invoice_date", label: "세금계산서발행일", width: "w-[160px]", align: "center" },
|
||||
{ key: "export_decl_no", label: "수출신고필증신고번호", width: "w-[180px]" },
|
||||
{ key: "loading_date", label: "선적일자", width: "w-[115px]", align: "center" },
|
||||
{ key: "has_transaction_statement", label: "거래명세서", width: "w-[115px]", align: "center" },
|
||||
/* wace revenueMgmtList.jsp 615~632 — 비활성(주석) 컬럼. 활성화 시 아래 주석 해제.
|
||||
{ key: "receipt_date", label: "접수일", width: "w-[115px]", align: "center" },
|
||||
{ key: "payment_type_name", label: "유/무상", width: "w-[80px]", align: "center" },
|
||||
@@ -134,6 +135,27 @@ export default function SalesRevenuePage() {
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
// ─── 하단 통계 ──────────────────────────────────────────────
|
||||
// 매출 이력 건수 / 수량 합계 / 공급가액·부가세·총액·원화총액 합계
|
||||
const revenueSummary = useMemo(() => {
|
||||
const count = rows.length;
|
||||
const qtySum = rows.reduce((acc, r) => acc + Number(r.sales_quantity || 0), 0);
|
||||
const supplySum = rows.reduce((acc, r) => acc + Number(r.sales_supply_price || 0), 0);
|
||||
const vatSum = rows.reduce((acc, r) => acc + Number(r.sales_vat || 0), 0);
|
||||
const totalSum = rows.reduce((acc, r) => acc + Number(r.sales_total_amount || 0), 0);
|
||||
const krwSum = rows.reduce((acc, r) => acc + Number(r.sales_total_amount_krw || 0), 0);
|
||||
const money = (n: number) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
const intFmt = (n: number) => n.toLocaleString();
|
||||
return [
|
||||
{ label: "매출 이력", value: intFmt(count), suffix: "건" },
|
||||
{ label: "수량 합계", value: intFmt(qtySum) },
|
||||
{ label: "공급가액 합계", value: money(supplySum) },
|
||||
{ label: "부가세 합계", value: money(vatSum) },
|
||||
{ label: "총액 합계", value: money(totalSum) },
|
||||
{ label: "원화총액 합계", value: money(krwSum) },
|
||||
];
|
||||
}, [rows]);
|
||||
|
||||
const openDeadline = () => {
|
||||
if (!selected) { toast.warning("마감정보를 입력할 행을 선택하세요."); return; }
|
||||
setForm({
|
||||
@@ -273,6 +295,7 @@ export default function SalesRevenuePage() {
|
||||
</CompactFilterBar>
|
||||
|
||||
<DataGrid
|
||||
gridId="sales-revenue"
|
||||
columns={columns}
|
||||
data={rows}
|
||||
selectedId={selected ? String(selected.log_id) : null}
|
||||
@@ -283,6 +306,21 @@ export default function SalesRevenuePage() {
|
||||
onRowDoubleClick={(row) => { setSelected(row); openDeadline(); }}
|
||||
emptyMessage={loading ? "조회 중..." : "데이터가 없습니다"}
|
||||
loading={loading}
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={revenueSummary}
|
||||
onRefresh={fetchList}
|
||||
onDownload={() => {
|
||||
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = rows.map((r) => {
|
||||
const out: Record<string, any> = {};
|
||||
GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "매출관리.xlsx", "매출관리");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<ProjectInfoDialog open={infoOpen} onOpenChange={setInfoOpen} data={infoData} />
|
||||
|
||||
@@ -22,6 +22,7 @@ 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";
|
||||
import { exportToExcel } from "@/lib/utils/excelExport";
|
||||
|
||||
// SaleListRow → 정규화된 ProjectInfoData (wace 운영판 다이얼로그 1:1)
|
||||
const toProjectInfo = (r: SaleListRow): ProjectInfoData => ({
|
||||
@@ -44,29 +45,29 @@ const toProjectInfo = (r: SaleListRow): ProjectInfoData => ({
|
||||
// wace_plm salesMgmtList.jsp 컬럼 순서/라벨에 맞춤
|
||||
const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "project_no", label: "프로젝트번호", width: "w-[170px]", frozen: true },
|
||||
{ key: "order_type_name", label: "주문유형", width: "w-[90px]", align: "center" },
|
||||
{ key: "order_type_name", label: "주문유형", width: "w-[115px]", align: "center" },
|
||||
{ key: "order_date", label: "발주일", width: "w-[115px]", align: "center" },
|
||||
{ key: "po_no", label: "발주번호", width: "w-[140px]" },
|
||||
{ key: "request_date", label: "요청납기", width: "w-[115px]", align: "center" },
|
||||
{ key: "shipping_date", label: "출하일", width: "w-[115px]", align: "center" },
|
||||
{ key: "customer", label: "고객사", width: "w-[160px]" },
|
||||
{ key: "product_name", label: "품명", width: "w-[180px]" },
|
||||
{ key: "order_quantity", label: "수주수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "sales_quantity", label: "판매수량", width: "w-[90px]", align: "right", formatNumber: true },
|
||||
{ key: "remaining_quantity", label: "잔량", width: "w-[80px]", align: "right", formatNumber: true },
|
||||
{ key: "sales_unit_price", label: "판매단가", width: "w-[110px]", formatMoney: true },
|
||||
{ key: "sales_supply_price", label: "판매공급가액", width: "w-[130px]", formatMoney: true },
|
||||
{ key: "sales_vat", label: "부가세", width: "w-[100px]", formatMoney: true },
|
||||
{ key: "sales_total_amount", label: "판매총액", width: "w-[120px]", formatMoney: true },
|
||||
{ key: "sales_total_amount_krw", label: "판매원화총액", width: "w-[130px]", formatMoney: true },
|
||||
{ key: "remaining_amount_krw", label: "잔량원화총액", width: "w-[130px]", formatMoney: true },
|
||||
{ key: "order_status_name", label: "수주상태", width: "w-[90px]", align: "center" },
|
||||
{ key: "sales_status", label: "판매상태", width: "w-[100px]", align: "center" },
|
||||
{ key: "production_status", label: "생산상태", width: "w-[100px]", align: "center" },
|
||||
{ key: "shipping_order_status", label: "출하지시상태", width: "w-[110px]", align: "center" },
|
||||
{ key: "payment_type_name", label: "유/무상", width: "w-[80px]", align: "center" },
|
||||
{ key: "sales_currency_name", label: "환종", width: "w-[70px]", align: "center" },
|
||||
{ key: "sales_exchange_rate", label: "환율", width: "w-[80px]", formatMoney: true },
|
||||
{ key: "order_quantity", label: "수주수량", width: "w-[115px]", align: "right", formatNumber: true },
|
||||
{ key: "sales_quantity", label: "판매수량", width: "w-[115px]", align: "right", formatNumber: true },
|
||||
{ key: "remaining_quantity", label: "잔량", width: "w-[95px]", align: "right", formatNumber: true },
|
||||
{ key: "sales_unit_price", label: "판매단가", width: "w-[115px]", formatMoney: true },
|
||||
{ key: "sales_supply_price", label: "판매공급가액", width: "w-[140px]", formatMoney: true },
|
||||
{ key: "sales_vat", label: "부가세", width: "w-[115px]", formatMoney: true },
|
||||
{ key: "sales_total_amount", label: "판매총액", width: "w-[130px]", formatMoney: true },
|
||||
{ key: "sales_total_amount_krw", label: "판매원화총액", width: "w-[140px]", formatMoney: true },
|
||||
{ key: "remaining_amount_krw", label: "잔량원화총액", width: "w-[140px]", formatMoney: true },
|
||||
{ key: "order_status_name", label: "수주상태", width: "w-[115px]", align: "center" },
|
||||
{ key: "sales_status", label: "판매상태", width: "w-[115px]", align: "center" },
|
||||
{ key: "production_status", label: "생산상태", width: "w-[115px]", align: "center" },
|
||||
{ key: "shipping_order_status", label: "출하지시상태", width: "w-[135px]", align: "center" },
|
||||
{ key: "payment_type_name", label: "유/무상", width: "w-[100px]", align: "center" },
|
||||
{ key: "sales_currency_name", label: "환종", width: "w-[95px]", align: "center" },
|
||||
{ key: "sales_exchange_rate", label: "환율", width: "w-[95px]", formatMoney: true },
|
||||
{ key: "serial_no", label: "S/N", width: "w-[140px]" },
|
||||
{ key: "split_serial_no", label: "분할S/N", width: "w-[140px]" },
|
||||
{ key: "product_no", label: "품번", width: "w-[120px]" },
|
||||
@@ -80,7 +81,7 @@ const GRID_COLUMNS: DataGridColumn[] = [
|
||||
{ key: "manager_name", label: "담당자", width: "w-[100px]", align: "center" },
|
||||
{ key: "incoterms", label: "인도조건", width: "w-[90px]", align: "center" },
|
||||
*/
|
||||
{ key: "has_transaction_statement", label: "거래명세서", width: "w-[100px]", align: "center" },
|
||||
{ key: "has_transaction_statement", label: "거래명세서", width: "w-[115px]", align: "center" },
|
||||
];
|
||||
|
||||
export default function SalesSalePage() {
|
||||
@@ -130,6 +131,29 @@ export default function SalesSalePage() {
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
// ─── 하단 통계 ──────────────────────────────────────────────
|
||||
// 판매 라인 수 / 수주·판매·잔량 수량 합계 / 판매공급가액·판매원화총액·잔량원화총액 합계
|
||||
const saleSummary = useMemo(() => {
|
||||
const count = rows.length;
|
||||
const ordQty = rows.reduce((acc, r) => acc + Number(r.order_quantity || 0), 0);
|
||||
const salQty = rows.reduce((acc, r) => acc + Number(r.sales_quantity || 0), 0);
|
||||
const remQty = rows.reduce((acc, r) => acc + Number(r.remaining_quantity || 0), 0);
|
||||
const supplySum = rows.reduce((acc, r) => acc + Number(r.sales_supply_price || 0), 0);
|
||||
const totalKrw = rows.reduce((acc, r) => acc + Number(r.sales_total_amount_krw || 0), 0);
|
||||
const remKrw = rows.reduce((acc, r) => acc + Number(r.remaining_amount_krw || 0), 0);
|
||||
const money = (n: number) => n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
const intFmt = (n: number) => n.toLocaleString();
|
||||
return [
|
||||
{ label: "판매 라인", value: intFmt(count), suffix: "건" },
|
||||
{ label: "수주수량 합계", value: intFmt(ordQty) },
|
||||
{ label: "판매수량 합계", value: intFmt(salQty) },
|
||||
{ label: "잔량 합계", value: intFmt(remQty) },
|
||||
{ label: "판매공급가액 합계", value: money(supplySum) },
|
||||
{ label: "판매원화총액 합계", value: money(totalKrw) },
|
||||
{ label: "잔량원화총액 합계", value: money(remKrw) },
|
||||
];
|
||||
}, [rows]);
|
||||
|
||||
const openRegister = () => {
|
||||
if (!selected) { toast.warning("판매등록할 행을 선택하세요."); return; }
|
||||
setForm({
|
||||
@@ -257,6 +281,7 @@ export default function SalesSalePage() {
|
||||
</CompactFilterBar>
|
||||
|
||||
<DataGrid
|
||||
gridId="sales-sale"
|
||||
columns={columns}
|
||||
data={rows}
|
||||
selectedId={selected ? `${selected.project_no}-${selected.contract_item_objid}-0` : null}
|
||||
@@ -264,6 +289,21 @@ export default function SalesSalePage() {
|
||||
onRowDoubleClick={(row) => { setSelected(row); openRegister(); }}
|
||||
emptyMessage={loading ? "조회 중..." : "데이터가 없습니다"}
|
||||
loading={loading}
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={saleSummary}
|
||||
onRefresh={fetchList}
|
||||
onDownload={() => {
|
||||
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = rows.map((r) => {
|
||||
const out: Record<string, any> = {};
|
||||
GRID_COLUMNS.forEach((col) => { out[col.label] = r[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "판매관리.xlsx", "판매관리");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<ProjectInfoDialog open={infoOpen} onOpenChange={setInfoOpen} data={infoData} />
|
||||
|
||||
Reference in New Issue
Block a user