489fa50d11
- 검색 폼 정합성: wace JSP `<!-- 주석처리된 검색필터 -->` 블록까지 잘못 이식했던 부분 정정 - 견적: 11→7개 (제품구분/국내해외/유무상/요청납기 제거) - 주문: 13→9개 (제품구분/국내해외/유무상/견적환종 제거) - 매출: 10→11개 (출하지시상태 제거 + 제품구분·국내/해외 추가, JSP 순서로 재배치) - 판매: 변경 없음 (원본 그대로 일치) - 매출 백엔드: SaleListFilter에 productType/nation 추가, getRevenueList에 partObjId/serialNo/orderDate/productType/nation 5개 필터 처리 - 공통 UX - 초기화 버튼을 4개 메뉴 동일하게 통일 (variant=ghost, 버튼 영역 끝) - <Input type="date">는 빈 값 placeholder 숨김 + 캘린더 아이콘 숨김 + 영역 클릭으로 picker 자동(showPicker) - 신규 공통 컴포넌트: CommCodeSelect/CustomerSelect/CustomerSearchDialog/PartSelect/ItemSearchDialog + backend salesCommonRoutes - 문서: 01/02/04 검색 폼 표를 활성/비활성 분리 형식으로 정정, README에 8. 공통 UX 규칙 섹션 신설 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
5.2 KiB
TypeScript
151 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Loader2 } from "lucide-react";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { toast } from "sonner";
|
|
|
|
export interface CustomerRow {
|
|
objid?: string | number;
|
|
id?: string | number;
|
|
customer_name?: string;
|
|
contact_person?: string;
|
|
business_number?: string;
|
|
contact_phone?: string;
|
|
address?: string;
|
|
customer_code?: string;
|
|
[key: string]: any;
|
|
}
|
|
|
|
/**
|
|
* customer_mng.id (정수) → contract_mgmt.customer_objid 'C_xxxxxxxxxx' (10자리 padded)
|
|
* 영업관리(contract_mgmt) 테이블이 사용하는 포맷.
|
|
*/
|
|
export const toContractCustomerObjid = (id: number | string | null | undefined) => {
|
|
if (id === null || id === undefined || id === "") return "";
|
|
return `C_${String(id).padStart(10, "0")}`;
|
|
};
|
|
|
|
interface CustomerSearchDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSelect: (customer: CustomerRow) => void;
|
|
title?: string;
|
|
description?: string;
|
|
}
|
|
|
|
export function CustomerSearchDialog({
|
|
open, onOpenChange, onSelect,
|
|
title = "거래처 검색",
|
|
description = "거래처를 검색하여 선택하세요.",
|
|
}: CustomerSearchDialogProps) {
|
|
const [keyword, setKeyword] = useState("");
|
|
const [results, setResults] = useState<CustomerRow[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setKeyword("");
|
|
setResults([]);
|
|
void search("");
|
|
}
|
|
}, [open]);
|
|
|
|
const search = async (kw?: string) => {
|
|
const k = kw ?? keyword;
|
|
setLoading(true);
|
|
try {
|
|
const filters: any[] = [];
|
|
if (k) filters.push({ columnName: "customer_name", operator: "contains", value: k });
|
|
const res = await apiClient.post("/table-management/tables/customer_mng/data", {
|
|
page: 1, size: 50,
|
|
dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined,
|
|
autoFilter: true,
|
|
});
|
|
const resData = res.data?.data;
|
|
setResults(resData?.data || resData?.rows || []);
|
|
} catch {
|
|
toast.error("거래처 조회 실패");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSelect = (cust: CustomerRow) => {
|
|
onSelect(cust);
|
|
onOpenChange(false);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="flex max-h-[80vh] max-w-2xl flex-col overflow-hidden">
|
|
<DialogHeader>
|
|
<DialogTitle>{title}</DialogTitle>
|
|
<DialogDescription>{description}</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
placeholder="거래처명 검색"
|
|
value={keyword}
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && search()}
|
|
className="text-sm"
|
|
/>
|
|
<Button onClick={() => search()} disabled={loading} className="shrink-0 gap-1">
|
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "검색"}
|
|
</Button>
|
|
</div>
|
|
<div className="flex-1 overflow-auto rounded border">
|
|
<table className="w-full text-xs">
|
|
<thead className="sticky top-0 bg-gray-50">
|
|
<tr>
|
|
<th className="px-3 py-2 text-left">거래처명</th>
|
|
<th className="px-3 py-2 text-left">대표자</th>
|
|
<th className="px-3 py-2 text-left">사업자번호</th>
|
|
<th className="px-3 py-2 text-left">연락처</th>
|
|
<th className="w-16 px-2 py-2 text-center">선택</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{results.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-gray-400">
|
|
{loading ? "검색 중..." : "검색 결과가 없습니다."}
|
|
</td>
|
|
</tr>
|
|
) : results.map((row, i) => (
|
|
<tr
|
|
key={row.objid ?? row.id ?? i}
|
|
className="cursor-pointer border-t hover:bg-blue-50"
|
|
onClick={() => handleSelect(row)}
|
|
>
|
|
<td className="px-3 py-2 font-medium">{row.customer_name || "-"}</td>
|
|
<td className="px-3 py-2">{row.contact_person || "-"}</td>
|
|
<td className="px-3 py-2">{row.business_number || "-"}</td>
|
|
<td className="px-3 py-2">{row.contact_phone || "-"}</td>
|
|
<td className="px-2 py-2 text-center">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="h-6 text-xs"
|
|
onClick={(e) => { e.stopPropagation(); handleSelect(row); }}
|
|
>선택</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>취소</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|