feat(WEB): 공사관리 시스템 및 CEO 대시보드 기능 확장
- 공사현장관리: 프로젝트 상세, 공정관리, 칸반보드 구현 - 이슈관리: 현장 이슈 등록/조회 기능 추가 - 근로자현황: 일별 근로자 출역 현황 페이지 추가 - 유틸리티관리: 현장 유틸리티 관리 페이지 추가 - 기성청구: 기성청구 관리 페이지 추가 - CEO 대시보드: 현황판(StatusBoardSection) 추가, 설정 다이얼로그 개선 - 발주관리: 모바일 필터 적용, 리스트 UI 개선 - 공용 컴포넌트: MobileFilter, IntegratedListTemplateV2 개선, CalendarHeader 반응형 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user