feat(WEB): 견적 시스템 개선, 엑셀 다운로드, PDF 생성 기능 추가
견적 시스템: - QuoteRegistrationV2: 할인 모달, 거래명세서 모달, vatType 필드 추가 - DiscountModal: 할인율/할인금액 상호 계산 모달 - QuoteTransactionModal: 거래명세서 미리보기 모달 - LocationDetailPanel, LocationListPanel 개선 템플릿 기능: - UniversalListPage: 엑셀 다운로드 기능 추가 (전체/선택 다운로드) - DocumentViewer: PDF 생성 기능 개선 신규 API: - /api/pdf/generate: Puppeteer 기반 PDF 생성 엔드포인트 UI 개선: - 입력 컴포넌트 placeholder 스타일 개선 (opacity 50%) - 각종 리스트 컴포넌트 정렬/필터링 개선 패키지 추가: - html2canvas, jspdf, puppeteer, dom-to-image-more Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useTransition } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfMonth, endOfMonth } from 'date-fns';
|
||||
import {
|
||||
FileText,
|
||||
Edit,
|
||||
@@ -23,6 +24,13 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { getQuoteStatusBadge } from '@/components/atoms/BadgeSm';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import {
|
||||
@@ -37,7 +45,6 @@ import {
|
||||
type UniversalListConfig,
|
||||
type SelectionHandlers,
|
||||
type RowClickHandlers,
|
||||
type TabOption,
|
||||
type StatCard,
|
||||
type ListParams,
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
@@ -64,6 +71,15 @@ export function QuoteManagementClient({
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
// ===== 날짜 필터 상태 =====
|
||||
const today = new Date();
|
||||
const [startDate, setStartDate] = useState(format(startOfMonth(today), 'yyyy-MM-dd'));
|
||||
const [endDate, setEndDate] = useState(format(endOfMonth(today), 'yyyy-MM-dd'));
|
||||
|
||||
// ===== 필터 상태 =====
|
||||
const [productCategoryFilter, setProductCategoryFilter] = useState<string>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
|
||||
// ===== 산출내역서 다이얼로그 상태 =====
|
||||
const [isCalculationDialogOpen, setIsCalculationDialogOpen] = useState(false);
|
||||
const [calculationQuote, setCalculationQuote] = useState<Quote | null>(null);
|
||||
@@ -156,35 +172,6 @@ export function QuoteManagementClient({
|
||||
return getQuoteStatusBadge(legacyQuote as any);
|
||||
}, []);
|
||||
|
||||
// ===== 탭 옵션 =====
|
||||
const tabs: TabOption[] = useMemo(() => [
|
||||
{ value: 'all', label: '전체', count: allQuotes.length, color: 'blue' },
|
||||
{
|
||||
value: 'initial',
|
||||
label: '최초작성',
|
||||
count: allQuotes.filter((q) => q.currentRevision === 0 && !q.isFinal && q.status !== 'converted').length,
|
||||
color: 'gray',
|
||||
},
|
||||
{
|
||||
value: 'revising',
|
||||
label: '수정중',
|
||||
count: allQuotes.filter((q) => q.currentRevision > 0 && !q.isFinal && q.status !== 'converted').length,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
value: 'final',
|
||||
label: '최종확정',
|
||||
count: allQuotes.filter((q) => q.isFinal && q.status !== 'converted').length,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: 'converted',
|
||||
label: '수주전환',
|
||||
count: allQuotes.filter((q) => q.status === 'converted').length,
|
||||
color: 'purple',
|
||||
},
|
||||
], [allQuotes]);
|
||||
|
||||
// ===== 통계 카드 계산 =====
|
||||
const computeStats = useCallback((data: Quote[]): StatCard[] => {
|
||||
const now = new Date();
|
||||
@@ -307,22 +294,41 @@ export function QuoteManagementClient({
|
||||
clientSideFiltering: true,
|
||||
itemsPerPage: 20,
|
||||
|
||||
// 탭 필터 함수
|
||||
tabFilter: (item: Quote, activeTab: string) => {
|
||||
if (activeTab === 'all') return true;
|
||||
if (activeTab === 'initial') {
|
||||
return item.currentRevision === 0 && !item.isFinal && item.status !== 'converted';
|
||||
}
|
||||
if (activeTab === 'revising') {
|
||||
return item.currentRevision > 0 && !item.isFinal && item.status !== 'converted';
|
||||
}
|
||||
if (activeTab === 'final') {
|
||||
return item.isFinal && item.status !== 'converted';
|
||||
}
|
||||
if (activeTab === 'converted') {
|
||||
return item.status === 'converted';
|
||||
}
|
||||
return true;
|
||||
// 필터링 함수 (날짜 + 제품분류 + 상태)
|
||||
customFilterFn: (items: Quote[]) => {
|
||||
return items.filter((item) => {
|
||||
// 날짜 필터
|
||||
const itemDate = item.registrationDate;
|
||||
if (itemDate) {
|
||||
if (startDate && itemDate < startDate) return false;
|
||||
if (endDate && itemDate > endDate) return false;
|
||||
}
|
||||
|
||||
// 제품분류 필터
|
||||
if (productCategoryFilter !== 'all') {
|
||||
if (productCategoryFilter === 'STEEL' && item.productCategory !== 'STEEL') return false;
|
||||
if (productCategoryFilter === 'SCREEN' && item.productCategory !== 'SCREEN') return false;
|
||||
if (productCategoryFilter === 'MIXED' && item.productCategory !== 'MIXED') return false;
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (statusFilter !== 'all') {
|
||||
if (statusFilter === 'initial') {
|
||||
// 최초작성: currentRevision === 0 && !isFinal
|
||||
if (!(item.currentRevision === 0 && !item.isFinal)) return false;
|
||||
}
|
||||
if (statusFilter === 'revising') {
|
||||
// N차수정: currentRevision > 0 && !isFinal
|
||||
if (!(item.currentRevision > 0 && !item.isFinal)) return false;
|
||||
}
|
||||
if (statusFilter === 'final') {
|
||||
// 최종확정: isFinal === true
|
||||
if (!item.isFinal) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
// 검색 필터 함수
|
||||
@@ -344,13 +350,53 @@ export function QuoteManagementClient({
|
||||
);
|
||||
},
|
||||
|
||||
// 탭 설정
|
||||
tabs,
|
||||
defaultTab: 'all',
|
||||
// 탭 비활성화 (필터로 대체)
|
||||
tabs: [],
|
||||
|
||||
// 통계 카드
|
||||
computeStats,
|
||||
|
||||
// 테이블 우측 필터 (탭 영역에 표시)
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 제품분류 필터 */}
|
||||
<Select value={productCategoryFilter} onValueChange={setProductCategoryFilter}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="제품분류" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="STEEL">철재</SelectItem>
|
||||
<SelectItem value="SCREEN">스크린</SelectItem>
|
||||
<SelectItem value="MIXED">혼합</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 상태 필터 */}
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="initial">최초작성</SelectItem>
|
||||
<SelectItem value="revising">N차수정</SelectItem>
|
||||
<SelectItem value="final">최종확정</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 날짜 필터
|
||||
dateRangeSelector: {
|
||||
enabled: true,
|
||||
showPresets: true,
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange: setStartDate,
|
||||
onEndDateChange: setEndDate,
|
||||
},
|
||||
|
||||
// 검색
|
||||
searchPlaceholder: '견적번호, 발주처, 담당자, 현장코드, 현장명 검색...',
|
||||
|
||||
@@ -553,7 +599,7 @@ export function QuoteManagementClient({
|
||||
);
|
||||
},
|
||||
}),
|
||||
[tabs, computeStats, router, handleView, handleEdit, handleDeleteClick, handleViewHistory, handleBulkDelete, getRevisionBadge, isPending]
|
||||
[computeStats, router, handleView, handleEdit, handleDeleteClick, handleViewHistory, handleBulkDelete, getRevisionBadge, isPending, startDate, endDate, productCategoryFilter, statusFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user