feat: 견적확정 밸리데이션, 수주등록 개소그룹, 작업지시 개선
- 견적확정 시 업체명/현장명/담당자/연락처 프론트 밸리데이션 추가 - 견적확정 후 수주등록 버튼 동적 전환 - 수주등록 품목 개소별(floor+code) 그룹핑 수정 - 작업지시 상세 quantity 문자열→숫자 변환 (formatQuantity) - 작업지시 탭 카운트 초기 로딩 시 전체 표시 (by_process 활용) - 작업지시 상세 개소별/품목별 합산 테이블 추가 - 작업자 화면 API 연동 및 목업 데이터 분리 - 입고관리 완료건 수정, 재고현황 개선
This commit is contained in:
@@ -234,93 +234,58 @@ export function OrderRegistration({
|
||||
}, [])
|
||||
);
|
||||
|
||||
// 제품코드에서 그룹핑 키 추출: FG-KWE01-벽면형-SUS → KWE01-SUS
|
||||
const extractGroupKey = useCallback((productName: string): string => {
|
||||
const parts = productName.split('-');
|
||||
if (parts.length >= 4) {
|
||||
// FG-{model}-{installationType}-{finishType}
|
||||
return `${parts[1]}-${parts[3]}`;
|
||||
}
|
||||
return productName;
|
||||
}, []);
|
||||
|
||||
// 아이템을 제품 모델+타입별로 그룹핑 (제품 단위 집약)
|
||||
// 아이템을 개소별(floor+code)로 그룹핑
|
||||
const itemGroups = useMemo(() => {
|
||||
const calcItems = form.selectedQuotation?.calculationInputs?.items;
|
||||
if (!calcItems || calcItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// floor+code → productCode 매핑
|
||||
const locationProductMap = new Map<string, string>();
|
||||
// floor+code → calculationInput 매핑 (개소 메타정보)
|
||||
const locationMetaMap = new Map<string, {
|
||||
productCode: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
floor: string;
|
||||
code: string;
|
||||
}>();
|
||||
calcItems.forEach(ci => {
|
||||
if (ci.floor && ci.code && ci.productCode) {
|
||||
locationProductMap.set(`${ci.floor}|${ci.code}`, ci.productCode);
|
||||
if (ci.floor && ci.code) {
|
||||
const locKey = `${ci.floor}|${ci.code}`;
|
||||
locationMetaMap.set(locKey, {
|
||||
productCode: ci.productCode || '',
|
||||
productName: ci.productName || '',
|
||||
quantity: ci.quantity ?? 1,
|
||||
floor: ci.floor,
|
||||
code: ci.code,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 그룹별 데이터 집계
|
||||
// 개소별 그룹
|
||||
const groups = new Map<string, {
|
||||
items: OrderItem[];
|
||||
productCode: string;
|
||||
locations: Set<string>; // 개소 목록
|
||||
quantity: number; // 개소별 수량 합계 (calculation_inputs 기준)
|
||||
meta: { productCode: string; productName: string; quantity: number; floor: string; code: string };
|
||||
}>();
|
||||
const ungrouped: OrderItem[] = [];
|
||||
|
||||
form.items.forEach(item => {
|
||||
const locKey = `${item.type}|${item.symbol}`;
|
||||
const productCode = locationProductMap.get(locKey);
|
||||
if (productCode) {
|
||||
const groupKey = extractGroupKey(productCode);
|
||||
if (!groups.has(groupKey)) {
|
||||
groups.set(groupKey, { items: [], productCode, locations: new Set(), quantity: 0 });
|
||||
const meta = locationMetaMap.get(locKey);
|
||||
if (meta) {
|
||||
if (!groups.has(locKey)) {
|
||||
groups.set(locKey, { items: [], meta });
|
||||
}
|
||||
const g = groups.get(groupKey)!;
|
||||
g.items.push(item);
|
||||
g.locations.add(locKey);
|
||||
groups.get(locKey)!.items.push(item);
|
||||
} else {
|
||||
ungrouped.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// calculation_inputs에서 개소별 수량 합산
|
||||
calcItems.forEach(ci => {
|
||||
if (ci.productCode) {
|
||||
const groupKey = extractGroupKey(ci.productCode);
|
||||
const g = groups.get(groupKey);
|
||||
if (g) {
|
||||
g.quantity += ci.quantity ?? 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (groups.size <= 1 && ungrouped.length === 0) {
|
||||
if (groups.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 그룹 내 동일 품목(item_code) 합산
|
||||
const aggregateItems = (items: OrderItem[]) => {
|
||||
const map = new Map<string, OrderItem & { _sourceIds: string[] }>();
|
||||
items.forEach(item => {
|
||||
const code = item.itemCode || item.itemName;
|
||||
if (map.has(code)) {
|
||||
const existing = map.get(code)!;
|
||||
existing.quantity += item.quantity;
|
||||
existing.amount = (existing.amount ?? 0) + (item.amount ?? 0);
|
||||
existing._sourceIds.push(item.id);
|
||||
} else {
|
||||
map.set(code, {
|
||||
...item,
|
||||
quantity: item.quantity,
|
||||
amount: item.amount ?? 0,
|
||||
_sourceIds: [item.id],
|
||||
});
|
||||
}
|
||||
});
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
const result: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -329,20 +294,19 @@ export function OrderRegistration({
|
||||
quantity: number;
|
||||
amount: number;
|
||||
items: OrderItem[];
|
||||
aggregatedItems: (OrderItem & { _sourceIds: string[] })[];
|
||||
}> = [];
|
||||
|
||||
let orderNum = 1;
|
||||
groups.forEach((value, key) => {
|
||||
const amount = value.items.reduce((sum, item) => sum + (item.amount ?? 0), 0);
|
||||
result.push({
|
||||
key,
|
||||
label: `수주 ${orderNum}: ${key}`,
|
||||
productCode: key,
|
||||
locationCount: value.locations.size,
|
||||
quantity: value.quantity,
|
||||
label: `${orderNum}. ${value.meta.floor} / ${value.meta.code}`,
|
||||
productCode: value.meta.productName || value.meta.productCode,
|
||||
locationCount: 1,
|
||||
quantity: value.meta.quantity,
|
||||
amount,
|
||||
items: value.items,
|
||||
aggregatedItems: aggregateItems(value.items),
|
||||
});
|
||||
orderNum++;
|
||||
});
|
||||
@@ -357,12 +321,11 @@ export function OrderRegistration({
|
||||
quantity: ungrouped.length,
|
||||
amount,
|
||||
items: ungrouped,
|
||||
aggregatedItems: aggregateItems(ungrouped),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [form.items, form.selectedQuotation?.calculationInputs, extractGroupKey]);
|
||||
}, [form.items, form.selectedQuotation?.calculationInputs]);
|
||||
|
||||
// 견적 선택 핸들러
|
||||
const handleQuotationSelect = (quotation: QuotationForSelect) => {
|
||||
@@ -903,17 +866,18 @@ export function OrderRegistration({
|
||||
{itemGroups ? (
|
||||
// 그룹핑 표시
|
||||
<div className="space-y-4">
|
||||
{itemGroups.map((group) => {
|
||||
return (
|
||||
{itemGroups.map((group) => (
|
||||
<div key={group.key} className={cn("border rounded-lg overflow-hidden", fieldErrors.items && "border-red-500")}>
|
||||
<div className="bg-blue-50 px-4 py-2 border-b flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className={getPresetStyle('info')}>
|
||||
{group.label}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({group.locationCount}개소 / {group.quantity}대)
|
||||
</span>
|
||||
{group.productCode && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{group.productCode} ({group.quantity}대)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
소계: {formatAmount(group.amount)}
|
||||
@@ -934,8 +898,8 @@ export function OrderRegistration({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{group.aggregatedItems.map((item, index) => (
|
||||
<TableRow key={`agg-${item.itemCode || item.id}`}>
|
||||
{group.items.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="text-center">{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-xs bg-gray-100 px-1.5 py-0.5 rounded">
|
||||
@@ -960,8 +924,7 @@ export function OrderRegistration({
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// 기본 플랫 리스트
|
||||
|
||||
Reference in New Issue
Block a user