fix(WEB): 작업일지 담당자 정보 및 슬랫 데이터 파이프라인 연동
- 작업일지(스크린/슬랫/절곡): 담당자→수주담당자, 연락처→담당자연락처, 생산담당자 분리 표시 - SlatWorkLogContent: 방화유리 수량을 slatInfo.glassQty에서 표시 - SlatInfo 타입에 glassQty 추가 (WorkOrders/types, WorkerScreen/types) - WorkerScreen: salesManager/managerPhone API 연동 - slat_info 변환 로직에 glass_qty 매핑 추가
This commit is contained in:
@@ -30,6 +30,8 @@ export interface WorkOrder {
|
||||
delayDays?: number; // 지연 일수
|
||||
instruction?: string; // 지시사항
|
||||
salesOrderNo?: string; // 수주번호
|
||||
salesManager?: string; // 수주 담당자 (orders.options.manager_name)
|
||||
managerPhone?: string; // 담당자 연락처 (orders.client_contact)
|
||||
teamId?: number | null; // 배정 부서 ID (work_orders.team_id)
|
||||
teamName?: string; // 배정 부서명
|
||||
processDepartment?: string; // 공정 담당부서명 (processes.department)
|
||||
|
||||
@@ -88,13 +88,13 @@ export function BendingWorkLogContent({ data: order }: BendingWorkLogContentProp
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">담당자</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.salesOrderWriter || '-'}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">제품 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">연락처</td>
|
||||
<td className="border border-gray-400 px-3 py-2">-</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.clientContact || '-'}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">생산담당자</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
|
||||
</tr>
|
||||
|
||||
@@ -223,13 +223,13 @@ export function ScreenWorkLogContent({ data: order, materialLots = [] }: ScreenW
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">담당자</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.salesOrderWriter || '-'}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">제품 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">연락처</td>
|
||||
<td className="border border-gray-400 px-3 py-2">-</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.clientContact || '-'}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">생산담당자</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
|
||||
</tr>
|
||||
|
||||
@@ -69,6 +69,26 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL
|
||||
const lotNoList = materialLots.map(lot => lot.lot_no).filter(Boolean);
|
||||
const lotNoDisplay = lotNoList.length > 0 ? lotNoList.join(', ') : '';
|
||||
|
||||
// 슬랫 계산: 매수(세로) = floor(height / 72) + 1
|
||||
const calcSlatCount = (height?: number) => height ? Math.floor(height / 72) + 1 : 0;
|
||||
|
||||
// 코일 사용량 = ((가로 + 4) × 매수) / 1000 + (304 × 3 × 조인트바) / 1000
|
||||
const calcCoilUsage = (width?: number, height?: number, jointBar?: number) => {
|
||||
const slatCount = calcSlatCount(height);
|
||||
const w = width || 0;
|
||||
const jb = jointBar || 0;
|
||||
return Math.round(((w + 4) * slatCount / 1000 + (304 * 3 * jb) / 1000) * 10) / 10;
|
||||
};
|
||||
|
||||
// 합계 계산
|
||||
let totalCoilUsage = 0;
|
||||
let totalJointBar = 0;
|
||||
items.forEach(item => {
|
||||
const jb = item.slatInfo?.jointBar || 0;
|
||||
totalJointBar += jb;
|
||||
totalCoilUsage += calcCoilUsage(item.width, item.height, jb);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
@@ -130,13 +150,13 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">담당자</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.salesOrderWriter || '-'}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">제품 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">연락처</td>
|
||||
<td className="border border-gray-400 px-3 py-2">-</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.clientContact || '-'}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">생산담당자</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{primaryAssignee}</td>
|
||||
</tr>
|
||||
@@ -153,40 +173,46 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-1 w-7" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 p-1 w-16" rowSpan={2}>입고 LOT<br/>NO</th>
|
||||
<th className="border border-gray-400 p-1 w-12" rowSpan={2}>방화유리<br/>수량</th>
|
||||
<th className="border border-gray-400 p-1" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 p-1" colSpan={3}>제작사이즈(mm) - 미미제외</th>
|
||||
<th className="border border-gray-400 p-1 w-12" rowSpan={2}>조인트바<br/>수량</th>
|
||||
<th className="border border-gray-400 p-1 w-12" rowSpan={2}>코일<br/>사용량</th>
|
||||
<th className="border border-gray-400 p-1 w-14" rowSpan={2}>설치홈/<br/>부호</th>
|
||||
<th className="border border-gray-400 px-2 py-1 w-7" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 px-2 py-1" rowSpan={2}>입고 LOT<br/>NO</th>
|
||||
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap" rowSpan={2}>방화유리<br/>수량</th>
|
||||
<th className="border border-gray-400 px-2 py-1" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 px-2 py-1" colSpan={3}>제작사이즈(mm) - 미미제외</th>
|
||||
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap" rowSpan={2}>조인트바<br/>수량</th>
|
||||
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap" rowSpan={2}>코일<br/>사용량</th>
|
||||
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap" rowSpan={2}>설치홈/<br/>부호</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-1 w-14">가로</th>
|
||||
<th className="border border-gray-400 p-1 w-14">세로</th>
|
||||
<th className="border border-gray-400 p-1 w-12">매수<br/>(세로)</th>
|
||||
<th className="border border-gray-400 px-2 py-1">가로</th>
|
||||
<th className="border border-gray-400 px-2 py-1">세로</th>
|
||||
<th className="border border-gray-400 px-2 py-1 whitespace-nowrap">매수<br/>(세로)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item, idx) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 p-1 text-center text-[10px]">{lotNoDisplay}</td>
|
||||
<td className="border border-gray-400 p-1 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-[10px]">{item.productName}</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{fmt(item.width)}</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{fmt(item.height)}</td>
|
||||
<td className="border border-gray-400 p-1 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-center">-</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-nowrap">{getSymbolCode(item.floorCode)}</td>
|
||||
</tr>
|
||||
))
|
||||
items.map((item, idx) => {
|
||||
const slatCount = calcSlatCount(item.height);
|
||||
const jointBar = item.slatInfo?.jointBar || 0;
|
||||
const glassQty = item.slatInfo?.glassQty || 0;
|
||||
const coilUsage = calcCoilUsage(item.width, item.height, jointBar);
|
||||
return (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">{lotNoDisplay}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">{glassQty > 0 ? fmt(glassQty) : '-'}</td>
|
||||
<td className="border border-gray-400 px-2 py-1">{item.productName}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center whitespace-nowrap">{fmt(item.width)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center whitespace-nowrap">{fmt(item.height)}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">{slatCount > 0 ? fmt(slatCount) : '-'}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">{jointBar > 0 ? fmt(jointBar) : '-'}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">{coilUsage > 0 ? coilUsage.toFixed(1) : '-'}</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center whitespace-nowrap">{getSymbolCode(item.floorCode)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={10} className="border border-gray-400 p-4 text-center text-gray-400">
|
||||
<td colSpan={10} className="border border-gray-400 px-2 py-4 text-center text-gray-400">
|
||||
등록된 품목이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -198,10 +224,10 @@ export function SlatWorkLogContent({ data: order, materialLots = [] }: SlatWorkL
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-36">생산량 합계 [m²]</td>
|
||||
<td className="border border-gray-400 px-3 py-2"></td>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-36">조인트바 합계</td>
|
||||
<td className="border border-gray-400 px-3 py-2"></td>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium whitespace-nowrap">생산량 합계 [m²]</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{totalCoilUsage > 0 ? totalCoilUsage.toFixed(1) : ''}</td>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium whitespace-nowrap">조인트바 합계</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{totalJointBar > 0 ? fmt(totalJointBar) : ''}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -118,6 +118,7 @@ export interface WorkOrderItem {
|
||||
unit: string; // 단위
|
||||
orderNodeId: number | null; // 개소 ID
|
||||
orderNodeName: string; // 개소명
|
||||
slatInfo?: { length: number; slatCount: number; jointBar: number; glassQty: number }; // 슬랫 공정 정보
|
||||
}
|
||||
|
||||
// 전개도 상세 (절곡용)
|
||||
@@ -347,6 +348,7 @@ export interface WorkOrderApi {
|
||||
created_at?: string;
|
||||
quantity?: number;
|
||||
root_nodes_count?: number;
|
||||
options?: { manager_name?: string; [key: string]: unknown };
|
||||
client?: { id: number; name: string };
|
||||
writer?: { id: number; name: string };
|
||||
};
|
||||
@@ -467,6 +469,10 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder {
|
||||
unit: item.unit || '-',
|
||||
orderNodeId: item.source_order_item?.order_node_id ?? null,
|
||||
orderNodeName: item.source_order_item?.node?.name || '-',
|
||||
slatInfo: item.options?.slat_info ? (() => {
|
||||
const si = item.options.slat_info as { length?: number; slat_count?: number; joint_bar?: number; glass_qty?: number };
|
||||
return { length: si.length || 0, slatCount: si.slat_count || 0, jointBar: si.joint_bar || 0, glassQty: si.glass_qty || 0 };
|
||||
})() : undefined,
|
||||
})),
|
||||
bendingDetails: api.bending_detail ? transformBendingDetail(api.bending_detail) : undefined,
|
||||
issues: (api.issues || []).map(issue => ({
|
||||
|
||||
@@ -39,6 +39,8 @@ interface WorkOrderApiItem {
|
||||
id: number;
|
||||
order_no: string;
|
||||
client?: { id: number; name: string };
|
||||
client_contact?: string;
|
||||
options?: { manager_name?: string; [key: string]: unknown };
|
||||
root_nodes_count?: number;
|
||||
};
|
||||
team_id?: number | null;
|
||||
@@ -189,6 +191,8 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
|
||||
delayDays,
|
||||
instruction: api.memo || undefined,
|
||||
salesOrderNo: api.sales_order?.order_no || undefined,
|
||||
salesManager: api.sales_order?.options?.manager_name as string || undefined,
|
||||
managerPhone: api.sales_order?.client_contact || undefined,
|
||||
teamId: api.team_id ?? null,
|
||||
teamName: api.team?.name || undefined,
|
||||
processDepartment: api.process?.department || undefined,
|
||||
@@ -635,8 +639,8 @@ export async function getWorkOrderDetail(
|
||||
workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets };
|
||||
}
|
||||
if (opts.slat_info) {
|
||||
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number };
|
||||
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar };
|
||||
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number; glass_qty: number };
|
||||
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar, glassQty: si.glass_qty || 0 };
|
||||
}
|
||||
if (opts.bending_info) {
|
||||
const bi = opts.bending_info as {
|
||||
|
||||
@@ -118,7 +118,7 @@ const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
|
||||
{
|
||||
id: 'mock-l1', itemNo: 1, itemCode: 'KQTS01', itemName: '슬랫코일', floor: '1층', code: 'FSS-01',
|
||||
width: 8260, height: 8350, quantity: 2, processType: 'slat',
|
||||
slatInfo: { length: 3910, slatCount: 40, jointBar: 4 },
|
||||
slatInfo: { length: 3910, slatCount: 40, jointBar: 4, glassQty: 2 },
|
||||
steps: [
|
||||
{ id: 'l1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
|
||||
{ id: 'l1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
@@ -132,7 +132,7 @@ const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
|
||||
{
|
||||
id: 'mock-l2', itemNo: 2, itemCode: 'KQTS03', itemName: '슬랫코일(광폭)', floor: '2층', code: 'FSS-02',
|
||||
width: 10500, height: 6200, quantity: 3, processType: 'slat',
|
||||
slatInfo: { length: 5200, slatCount: 55, jointBar: 6 },
|
||||
slatInfo: { length: 5200, slatCount: 55, jointBar: 6, glassQty: 3 },
|
||||
steps: [
|
||||
{ id: 'l2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'l2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
@@ -709,8 +709,8 @@ export default function WorkerScreen() {
|
||||
workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets };
|
||||
}
|
||||
if (opts.slat_info) {
|
||||
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number };
|
||||
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar };
|
||||
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number; glass_qty: number };
|
||||
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar, glassQty: si.glass_qty || 0 };
|
||||
}
|
||||
if (opts.bending_info) {
|
||||
const bi = opts.bending_info as {
|
||||
@@ -864,8 +864,8 @@ export default function WorkerScreen() {
|
||||
salesOrderNo: apiOrder.salesOrderNo || '-',
|
||||
siteName: apiOrder.projectName || '-',
|
||||
client: apiOrder.client || '-',
|
||||
salesManager: apiOrder.assignees?.[0] || '-',
|
||||
managerPhone: '-',
|
||||
salesManager: apiOrder.salesManager || '-',
|
||||
managerPhone: apiOrder.managerPhone || '-',
|
||||
shippingDate: apiOrder.dueDate ? new Date(apiOrder.dueDate).toLocaleDateString('ko-KR') : '-',
|
||||
};
|
||||
}
|
||||
@@ -892,8 +892,8 @@ export default function WorkerScreen() {
|
||||
salesOrderNo: first.salesOrderNo || '-',
|
||||
siteName: first.projectName || '-',
|
||||
client: first.client || '-',
|
||||
salesManager: first.assignees?.[0] || '-',
|
||||
managerPhone: '-',
|
||||
salesManager: first.salesManager || '-',
|
||||
managerPhone: first.managerPhone || '-',
|
||||
shippingDate: first.dueDate ? new Date(first.dueDate).toLocaleDateString('ko-KR') : '-',
|
||||
};
|
||||
}, [filteredWorkOrders, selectedSidebarOrderId, activeProcessTabKey]);
|
||||
|
||||
@@ -60,7 +60,7 @@ export const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
|
||||
{
|
||||
id: 'mock-l1', itemNo: 1, itemCode: 'KQTS01', itemName: '슬랫코일', floor: '1층', code: 'FSS-01',
|
||||
width: 8260, height: 8350, quantity: 2, processType: 'slat',
|
||||
slatInfo: { length: 3910, slatCount: 40, jointBar: 4 },
|
||||
slatInfo: { length: 3910, slatCount: 40, jointBar: 4, glassQty: 2 },
|
||||
steps: [
|
||||
{ id: 'l1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
|
||||
{ id: 'l1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
@@ -73,7 +73,7 @@ export const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
|
||||
{
|
||||
id: 'mock-l2', itemNo: 2, itemCode: 'KQTS03', itemName: '슬랫코일(광폭)', floor: '2층', code: 'FSS-02',
|
||||
width: 10500, height: 6200, quantity: 3, processType: 'slat',
|
||||
slatInfo: { length: 5200, slatCount: 55, jointBar: 6 },
|
||||
slatInfo: { length: 5200, slatCount: 55, jointBar: 6, glassQty: 3 },
|
||||
steps: [
|
||||
{ id: 'l2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'l2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface SlatInfo {
|
||||
length: number; // 길이 (mm)
|
||||
slatCount: number; // 슬랫 매수
|
||||
jointBar: number; // 조인트바 개수
|
||||
glassQty: number; // 방화유리 수량
|
||||
}
|
||||
|
||||
// ===== 슬랫 조인트바 전용 정보 =====
|
||||
|
||||
Reference in New Issue
Block a user