- 공사현장관리: 프로젝트 상세, 공정관리, 칸반보드 구현 - 이슈관리: 현장 이슈 등록/조회 기능 추가 - 근로자현황: 일별 근로자 출역 현황 페이지 추가 - 유틸리티관리: 현장 유틸리티 관리 페이지 추가 - 기성청구: 기성청구 관리 페이지 추가 - CEO 대시보드: 현황판(StatusBoardSection) 추가, 설정 다이얼로그 개선 - 발주관리: 모바일 필터 적용, 리스트 UI 개선 - 공용 컴포넌트: MobileFilter, IntegratedListTemplateV2 개선, CalendarHeader 반응형 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
268 lines
12 KiB
TypeScript
268 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogTitle,
|
|
VisuallyHidden,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Edit, Trash2, Printer, X } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { printArea } from '@/lib/print-utils';
|
|
import type { ProgressBillingDetailFormData } from '../types';
|
|
|
|
// 숫자 포맷팅 (천단위 콤마)
|
|
function formatNumber(num: number | undefined): string {
|
|
if (num === undefined || num === null) return '-';
|
|
return num.toLocaleString('ko-KR');
|
|
}
|
|
|
|
interface DirectConstructionModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
data: ProgressBillingDetailFormData;
|
|
}
|
|
|
|
// 직접 공사 내역 아이템 타입
|
|
interface DirectConstructionItem {
|
|
id: string;
|
|
name: string;
|
|
product: string;
|
|
width: number;
|
|
height: number;
|
|
quantity: number;
|
|
unit: string;
|
|
contractUnitPrice: number;
|
|
contractAmount: number;
|
|
prevQuantity: number;
|
|
prevAmount: number;
|
|
currentQuantity: number;
|
|
currentAmount: number;
|
|
cumulativeQuantity: number;
|
|
cumulativeAmount: number;
|
|
remark?: string;
|
|
}
|
|
|
|
// 목업 데이터 생성
|
|
function generateMockItems(billingItems: ProgressBillingDetailFormData['billingItems']): DirectConstructionItem[] {
|
|
return billingItems.map((item, index) => ({
|
|
id: item.id,
|
|
name: item.name || '명칭',
|
|
product: item.product || '제품명',
|
|
width: item.width || 2500,
|
|
height: item.height || 3200,
|
|
quantity: 1,
|
|
unit: 'EA',
|
|
contractUnitPrice: 2500000,
|
|
contractAmount: 2500000,
|
|
prevQuantity: index < 4 ? 0 : 0.8,
|
|
prevAmount: index < 4 ? 0 : 1900000,
|
|
currentQuantity: 0.8,
|
|
currentAmount: 1900000,
|
|
cumulativeQuantity: 0.8,
|
|
cumulativeAmount: 1900000,
|
|
remark: '',
|
|
}));
|
|
}
|
|
|
|
export function DirectConstructionModal({
|
|
open,
|
|
onOpenChange,
|
|
data,
|
|
}: DirectConstructionModalProps) {
|
|
// 핸들러
|
|
const handleEdit = () => {
|
|
toast.info('수정 기능은 준비 중입니다.');
|
|
};
|
|
|
|
const handleDelete = () => {
|
|
toast.info('삭제 기능은 준비 중입니다.');
|
|
};
|
|
|
|
const handlePrint = () => {
|
|
printArea({ title: '직접 공사 내역서 인쇄' });
|
|
};
|
|
|
|
// 목업 데이터
|
|
const items = generateMockItems(data.billingItems);
|
|
|
|
// 합계 계산
|
|
const totalContractAmount = items.reduce((sum, item) => sum + item.contractAmount, 0);
|
|
const totalCurrentAmount = items.reduce((sum, item) => sum + item.currentAmount, 0);
|
|
const totalCumulativeAmount = items.reduce((sum, item) => sum + item.cumulativeAmount, 0);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-[95vw] md:max-w-[1000px] lg:max-w-[1200px] h-[95vh] p-0 flex flex-col gap-0 overflow-hidden [&>button]:hidden">
|
|
<VisuallyHidden>
|
|
<DialogTitle>직접 공사 내역서</DialogTitle>
|
|
</VisuallyHidden>
|
|
|
|
{/* 헤더 영역 */}
|
|
<div className="print-hidden flex-shrink-0 flex items-center justify-between px-6 py-4 border-b bg-white">
|
|
<h2 className="text-lg font-semibold">직접 공사 내역서</h2>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => onOpenChange(false)}
|
|
className="h-8 w-8"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 버튼 영역 */}
|
|
<div className="print-hidden flex-shrink-0 flex items-center gap-2 px-6 py-3 bg-muted/30 border-b">
|
|
<Button variant="outline" size="sm" onClick={handleEdit}>
|
|
<Edit className="h-4 w-4 mr-1" />
|
|
수정
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={handleDelete}>
|
|
<Trash2 className="h-4 w-4 mr-1" />
|
|
삭제
|
|
</Button>
|
|
<Button variant="outline" size="sm" onClick={handlePrint}>
|
|
<Printer className="h-4 w-4 mr-1" />
|
|
인쇄
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 문서 영역 */}
|
|
<div className="print-area flex-1 overflow-y-auto bg-gray-100 p-6">
|
|
<div className="max-w-[297mm] mx-auto bg-white shadow-lg rounded-lg p-8">
|
|
{/* 상단: 제목 + 결재란 */}
|
|
<div className="flex items-start justify-between mb-6">
|
|
{/* 좌측: 제목 및 문서 정보 */}
|
|
<div className="flex-1">
|
|
<h1 className="text-2xl font-bold mb-2">직접 공사 내역서</h1>
|
|
<div className="text-sm text-gray-600">
|
|
문서번호: {data.billingNumber || 'ABC123'} | 작성일자: 2025년 11월 11일
|
|
</div>
|
|
</div>
|
|
|
|
{/* 우측: 결재란 */}
|
|
<table className="border-collapse border border-gray-400 text-sm">
|
|
<tbody>
|
|
<tr>
|
|
<th rowSpan={3} className="border border-gray-400 px-3 py-1 bg-gray-50 text-center align-middle w-12">
|
|
결<br />재
|
|
</th>
|
|
<th className="border border-gray-400 px-6 py-1 bg-gray-50 text-center whitespace-nowrap">작성</th>
|
|
<th className="border border-gray-400 px-6 py-1 bg-gray-50 text-center whitespace-nowrap">승인</th>
|
|
</tr>
|
|
<tr>
|
|
<td className="border border-gray-400 px-6 py-2 text-center h-10 whitespace-nowrap">홍길동</td>
|
|
<td className="border border-gray-400 px-6 py-2 text-center h-10 whitespace-nowrap">이름</td>
|
|
</tr>
|
|
<tr>
|
|
<td className="border border-gray-400 px-6 py-1 text-center text-xs text-gray-500 whitespace-nowrap">부서명</td>
|
|
<td className="border border-gray-400 px-6 py-1 text-center text-xs text-gray-500 whitespace-nowrap">부서명</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* 기성내역 제목 */}
|
|
<div className="text-center font-bold text-lg mb-4">
|
|
기성내역 제{data.billingRound || 1}회 ({data.billingYearMonth || '2025년 11월'})
|
|
</div>
|
|
|
|
{/* 현장 정보 */}
|
|
<div className="mb-4">
|
|
<span className="font-bold">■ 현장: {data.siteName || '현장명'}</span>
|
|
</div>
|
|
|
|
{/* 테이블 */}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse border border-gray-400 text-xs">
|
|
<thead>
|
|
{/* 1행: 상위 헤더 */}
|
|
<tr className="bg-gray-50">
|
|
<th rowSpan={2} className="border border-gray-400 px-2 py-2 text-center w-10">No.</th>
|
|
<th rowSpan={2} className="border border-gray-400 px-2 py-2 text-center min-w-[80px]">명칭</th>
|
|
<th rowSpan={2} className="border border-gray-400 px-2 py-2 text-center min-w-[60px]">제품</th>
|
|
<th colSpan={2} className="border border-gray-400 px-2 py-1 text-center bg-red-50">
|
|
<span className="inline-flex items-center gap-1">
|
|
규격 mm
|
|
</span>
|
|
</th>
|
|
<th rowSpan={2} className="border border-gray-400 px-2 py-2 text-center w-12">수량</th>
|
|
<th rowSpan={2} className="border border-gray-400 px-2 py-2 text-center w-12">단위</th>
|
|
<th colSpan={2} className="border border-gray-400 px-2 py-1 text-center bg-red-50">
|
|
<span className="inline-flex items-center gap-1">
|
|
계약금액
|
|
</span>
|
|
</th>
|
|
<th colSpan={2} className="border border-gray-400 px-2 py-1 text-center bg-red-50">
|
|
<span className="inline-flex items-center gap-1">
|
|
전회기성
|
|
</span>
|
|
</th>
|
|
<th colSpan={2} className="border border-gray-400 px-2 py-1 text-center bg-red-50">
|
|
<span className="inline-flex items-center gap-1">
|
|
금회기성
|
|
</span>
|
|
</th>
|
|
<th colSpan={2} className="border border-gray-400 px-2 py-1 text-center bg-red-50">
|
|
<span className="inline-flex items-center gap-1">
|
|
누계기성
|
|
</span>
|
|
</th>
|
|
<th rowSpan={2} className="border border-gray-400 px-2 py-2 text-center w-16">비고</th>
|
|
</tr>
|
|
{/* 2행: 하위 헤더 */}
|
|
<tr className="bg-gray-50">
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-14">가로</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-14">세로</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-16">단가</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-20">금액</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-12">수량</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-20">금액</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-12">수량</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-20">금액</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-12">수량</th>
|
|
<th className="border border-gray-400 px-2 py-1 text-center w-20">금액</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{items.map((item, index) => (
|
|
<tr key={item.id}>
|
|
<td className="border border-gray-400 px-2 py-2 text-center">{index + 1}</td>
|
|
<td className="border border-gray-400 px-2 py-2">{item.name}</td>
|
|
<td className="border border-gray-400 px-2 py-2">{item.product}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(item.width)}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(item.height)}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-center">{item.quantity}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-center">{item.unit}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(item.contractUnitPrice)}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(item.contractAmount)}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-center">{item.prevQuantity || '-'}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{item.prevAmount ? formatNumber(item.prevAmount) : '-'}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-center">{item.currentQuantity}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(item.currentAmount)}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-center">{item.cumulativeQuantity}</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(item.cumulativeAmount)}</td>
|
|
<td className="border border-gray-400 px-2 py-2">{item.remark || ''}</td>
|
|
</tr>
|
|
))}
|
|
{/* 합계 행 */}
|
|
<tr className="bg-gray-50 font-bold">
|
|
<td colSpan={8} className="border border-gray-400 px-2 py-2 text-center">합계</td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(totalContractAmount)}</td>
|
|
<td colSpan={2} className="border border-gray-400 px-2 py-2"></td>
|
|
<td className="border border-gray-400 px-2 py-2"></td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(totalCurrentAmount)}</td>
|
|
<td className="border border-gray-400 px-2 py-2"></td>
|
|
<td className="border border-gray-400 px-2 py-2 text-right">{formatNumber(totalCumulativeAmount)}</td>
|
|
<td className="border border-gray-400 px-2 py-2"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |