2026-02-04 12:46:19 +09:00
|
|
|
/**
|
|
|
|
|
* 단가배포 상세/수정 페이지
|
|
|
|
|
*
|
|
|
|
|
* mode 패턴:
|
|
|
|
|
* - view: 상세 조회 (읽기 전용) → 하단: 단가표 보기, 최종확정, 수정
|
|
|
|
|
* - edit: 수정 모드 → 하단: 취소, 저장
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
|
|
|
import { ArrowLeft, FileText, CheckCircle2, Edit3, Save, X } from 'lucide-react';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2026-02-11 15:09:51 +09:00
|
|
|
import { useMenuStore } from '@/stores/menuStore';
|
2026-02-04 12:46:19 +09:00
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
feat(WEB): DatePicker 공통화 및 공정관리/작업자화면 대폭 개선
DatePicker 공통화:
- date-picker.tsx 공통 컴포넌트 신규 추가
- 전체 폼 컴포넌트 DatePicker 통일 적용 (50+ 파일)
- DateRangeSelector 개선
공정관리:
- RuleModal 대폭 리팩토링 (-592줄 → 간소화)
- ProcessForm, StepForm 개선
- ProcessDetail 수정, actions 확장
작업자화면:
- WorkerScreen 기능 대폭 확장 (+543줄)
- WorkItemCard 개선
- types 확장
회계/인사/영업/품질:
- BadDebtDetail, BillDetail, DepositDetail, SalesDetail 등 DatePicker 적용
- EmployeeForm, VacationDialog 등 DatePicker 적용
- OrderRegistration, QuoteRegistration DatePicker 적용
- InspectionCreate, InspectionDetail DatePicker 적용
공사관리/CEO대시보드:
- BiddingDetail, ContractDetail, HandoverReport 등 DatePicker 적용
- ScheduleDetailModal, TodayIssueSection 개선
기타:
- WorkOrderCreate/Edit/Detail/List 개선
- ShipmentCreate/Edit, ReceivingDetail 개선
- calendar, calendarEvents 수정
- datepicker 마이그레이션 체크리스트 추가
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:48:00 +09:00
|
|
|
import { DatePicker } from '@/components/ui/date-picker';
|
2026-02-04 12:46:19 +09:00
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
2026-02-05 15:57:49 +09:00
|
|
|
import { getPresetStyle } from '@/lib/utils/status-config';
|
2026-02-04 12:46:19 +09:00
|
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from '@/components/ui/table';
|
|
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
} from '@/components/ui/alert-dialog';
|
|
|
|
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
|
|
|
import { PageHeader } from '@/components/organisms/PageHeader';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
import type {
|
|
|
|
|
PriceDistributionDetail as DetailType,
|
|
|
|
|
PriceDistributionFormData,
|
|
|
|
|
DistributionStatus,
|
|
|
|
|
} from './types';
|
|
|
|
|
import {
|
|
|
|
|
DISTRIBUTION_STATUS_LABELS,
|
|
|
|
|
DISTRIBUTION_STATUS_STYLES,
|
|
|
|
|
TRADE_GRADE_OPTIONS,
|
|
|
|
|
} from './types';
|
|
|
|
|
import {
|
|
|
|
|
getPriceDistributionById,
|
|
|
|
|
updatePriceDistribution,
|
|
|
|
|
finalizePriceDistribution,
|
|
|
|
|
} from './actions';
|
|
|
|
|
import { PriceDistributionDocumentModal } from './PriceDistributionDocumentModal';
|
|
|
|
|
import { usePermission } from '@/hooks/usePermission';
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
id: string;
|
|
|
|
|
mode?: 'view' | 'edit';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function PriceDistributionDetail({ id, mode: propMode }: Props) {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const { canUpdate, canApprove } = usePermission();
|
|
|
|
|
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
|
|
|
|
|
const mode = propMode || (searchParams.get('mode') as 'view' | 'edit') || 'view';
|
|
|
|
|
const isEditMode = mode === 'edit';
|
|
|
|
|
const isViewMode = mode === 'view';
|
|
|
|
|
const [detail, setDetail] = useState<DetailType | null>(null);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
const [showFinalizeDialog, setShowFinalizeDialog] = useState(false);
|
|
|
|
|
const [showDocumentModal, setShowDocumentModal] = useState(false);
|
|
|
|
|
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
|
|
|
|
const [gradeFilter, setGradeFilter] = useState<string>('all');
|
|
|
|
|
|
|
|
|
|
// 수정 가능 폼 데이터
|
|
|
|
|
const [formData, setFormData] = useState<PriceDistributionFormData>({
|
|
|
|
|
distributionName: '',
|
|
|
|
|
documentNo: '',
|
|
|
|
|
effectiveDate: '',
|
|
|
|
|
officePhone: '',
|
|
|
|
|
orderPhone: '',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 데이터 로드
|
|
|
|
|
const loadData = useCallback(async () => {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const result = await getPriceDistributionById(id);
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
setDetail(result.data);
|
|
|
|
|
setFormData({
|
|
|
|
|
distributionName: result.data.distributionName,
|
|
|
|
|
documentNo: result.data.documentNo,
|
|
|
|
|
effectiveDate: result.data.effectiveDate,
|
|
|
|
|
officePhone: result.data.officePhone,
|
|
|
|
|
orderPhone: result.data.orderPhone,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(result.error || '데이터를 불러올 수 없습니다.');
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [id]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadData();
|
|
|
|
|
}, [loadData]);
|
|
|
|
|
|
|
|
|
|
// 폼 값 변경
|
|
|
|
|
const handleChange = (field: keyof PriceDistributionFormData, value: string) => {
|
|
|
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 저장
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
try {
|
|
|
|
|
const result = await updatePriceDistribution(id, formData);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
toast.success('저장되었습니다.');
|
|
|
|
|
router.push(`/master-data/price-distribution/${id}`);
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(result.error || '저장에 실패했습니다.');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
toast.error('저장 중 오류가 발생했습니다.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 최종확정
|
|
|
|
|
const handleFinalize = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await finalizePriceDistribution(id);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
toast.success('최종확정 되었습니다.');
|
|
|
|
|
setShowFinalizeDialog(false);
|
|
|
|
|
loadData();
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(result.error || '최종확정에 실패했습니다.');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
toast.error('최종확정 중 오류가 발생했습니다.');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 수정 모드 전환
|
|
|
|
|
const handleEditMode = () => {
|
|
|
|
|
router.push(`/master-data/price-distribution/${id}/edit`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 취소
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
router.push(`/master-data/price-distribution/${id}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 목록으로
|
|
|
|
|
const handleBack = () => {
|
|
|
|
|
router.push('/master-data/price-distribution');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 체크박스 전체 선택/해제
|
|
|
|
|
const handleSelectAll = (checked: boolean) => {
|
|
|
|
|
if (!detail) return;
|
|
|
|
|
if (checked) {
|
|
|
|
|
setSelectedItems(new Set(detail.items.map((item) => item.id)));
|
|
|
|
|
} else {
|
|
|
|
|
setSelectedItems(new Set());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 체크박스 개별 선택
|
|
|
|
|
const handleSelectItem = (itemId: string, checked: boolean) => {
|
|
|
|
|
setSelectedItems((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (checked) {
|
|
|
|
|
next.add(itemId);
|
|
|
|
|
} else {
|
|
|
|
|
next.delete(itemId);
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 상태 뱃지
|
|
|
|
|
const renderStatusBadge = (status: DistributionStatus) => {
|
|
|
|
|
const style = DISTRIBUTION_STATUS_STYLES[status];
|
|
|
|
|
const label = DISTRIBUTION_STATUS_LABELS[status];
|
|
|
|
|
return (
|
|
|
|
|
<Badge variant="outline" className={`${style.bg} ${style.text} ${style.border}`}>
|
|
|
|
|
{label}
|
|
|
|
|
</Badge>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 금액 포맷
|
|
|
|
|
const formatPrice = (price?: number) => {
|
|
|
|
|
if (price === undefined || price === null) return '-';
|
|
|
|
|
return price.toLocaleString();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<PageLayout>
|
|
|
|
|
<div className="flex items-center justify-center h-64">
|
|
|
|
|
<p className="text-muted-foreground">로딩 중...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</PageLayout>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!detail) {
|
|
|
|
|
return (
|
|
|
|
|
<PageLayout>
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
|
|
|
|
<p className="text-muted-foreground">데이터를 찾을 수 없습니다.</p>
|
|
|
|
|
<Button variant="outline" onClick={handleBack}>목록으로</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</PageLayout>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isAllSelected = detail.items.length > 0 && selectedItems.size === detail.items.length;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<PageLayout>
|
|
|
|
|
<PageHeader
|
|
|
|
|
title={`단가배포 ${isEditMode ? '수정' : '상세'}`}
|
|
|
|
|
description={`${detail.distributionName} (${detail.distributionNo})`}
|
|
|
|
|
icon={FileText}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 기본 정보 */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base">기본 정보</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
|
|
{/* 단가배포번호 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-muted-foreground text-xs">단가배포번호</Label>
|
|
|
|
|
<p className="text-sm font-medium">{detail.distributionNo}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 단가배포명 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-muted-foreground text-xs">단가배포명</Label>
|
|
|
|
|
{isEditMode ? (
|
|
|
|
|
<Input
|
|
|
|
|
value={formData.distributionName}
|
|
|
|
|
onChange={(e) => handleChange('distributionName', e.target.value)}
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm font-medium">{detail.distributionName}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 상태 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-muted-foreground text-xs">상태</Label>
|
|
|
|
|
<Select value={detail.status} disabled>
|
|
|
|
|
<SelectTrigger className="h-8 text-sm">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="initial">{DISTRIBUTION_STATUS_LABELS.initial}</SelectItem>
|
|
|
|
|
<SelectItem value="revision">{DISTRIBUTION_STATUS_LABELS.revision}</SelectItem>
|
|
|
|
|
<SelectItem value="finalized">{DISTRIBUTION_STATUS_LABELS.finalized}</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 작성자 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-muted-foreground text-xs">작성자</Label>
|
|
|
|
|
<p className="text-sm font-medium">{detail.author}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 등록일 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-muted-foreground text-xs">등록일</Label>
|
|
|
|
|
<p className="text-sm font-medium">{detail.createdAt}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 적용시점 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-muted-foreground text-xs">적용시점</Label>
|
|
|
|
|
{isEditMode ? (
|
feat(WEB): DatePicker 공통화 및 공정관리/작업자화면 대폭 개선
DatePicker 공통화:
- date-picker.tsx 공통 컴포넌트 신규 추가
- 전체 폼 컴포넌트 DatePicker 통일 적용 (50+ 파일)
- DateRangeSelector 개선
공정관리:
- RuleModal 대폭 리팩토링 (-592줄 → 간소화)
- ProcessForm, StepForm 개선
- ProcessDetail 수정, actions 확장
작업자화면:
- WorkerScreen 기능 대폭 확장 (+543줄)
- WorkItemCard 개선
- types 확장
회계/인사/영업/품질:
- BadDebtDetail, BillDetail, DepositDetail, SalesDetail 등 DatePicker 적용
- EmployeeForm, VacationDialog 등 DatePicker 적용
- OrderRegistration, QuoteRegistration DatePicker 적용
- InspectionCreate, InspectionDetail DatePicker 적용
공사관리/CEO대시보드:
- BiddingDetail, ContractDetail, HandoverReport 등 DatePicker 적용
- ScheduleDetailModal, TodayIssueSection 개선
기타:
- WorkOrderCreate/Edit/Detail/List 개선
- ShipmentCreate/Edit, ReceivingDetail 개선
- calendar, calendarEvents 수정
- datepicker 마이그레이션 체크리스트 추가
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:48:00 +09:00
|
|
|
<DatePicker
|
2026-02-04 12:46:19 +09:00
|
|
|
value={formData.effectiveDate}
|
feat(WEB): DatePicker 공통화 및 공정관리/작업자화면 대폭 개선
DatePicker 공통화:
- date-picker.tsx 공통 컴포넌트 신규 추가
- 전체 폼 컴포넌트 DatePicker 통일 적용 (50+ 파일)
- DateRangeSelector 개선
공정관리:
- RuleModal 대폭 리팩토링 (-592줄 → 간소화)
- ProcessForm, StepForm 개선
- ProcessDetail 수정, actions 확장
작업자화면:
- WorkerScreen 기능 대폭 확장 (+543줄)
- WorkItemCard 개선
- types 확장
회계/인사/영업/품질:
- BadDebtDetail, BillDetail, DepositDetail, SalesDetail 등 DatePicker 적용
- EmployeeForm, VacationDialog 등 DatePicker 적용
- OrderRegistration, QuoteRegistration DatePicker 적용
- InspectionCreate, InspectionDetail DatePicker 적용
공사관리/CEO대시보드:
- BiddingDetail, ContractDetail, HandoverReport 등 DatePicker 적용
- ScheduleDetailModal, TodayIssueSection 개선
기타:
- WorkOrderCreate/Edit/Detail/List 개선
- ShipmentCreate/Edit, ReceivingDetail 개선
- calendar, calendarEvents 수정
- datepicker 마이그레이션 체크리스트 추가
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:48:00 +09:00
|
|
|
onChange={(date) => handleChange('effectiveDate', date)}
|
|
|
|
|
size="sm"
|
2026-02-04 12:46:19 +09:00
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm font-medium">
|
|
|
|
|
{detail.effectiveDate ? new Date(detail.effectiveDate).toLocaleDateString('ko-KR') : '-'}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 사무실 연락처 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-muted-foreground text-xs">사무실 연락처</Label>
|
|
|
|
|
{isEditMode ? (
|
|
|
|
|
<Input
|
|
|
|
|
value={formData.officePhone}
|
|
|
|
|
onChange={(e) => handleChange('officePhone', e.target.value)}
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
placeholder="02-0000-0000"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm font-medium">{detail.officePhone || '-'}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 발주전용 연락처 */}
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-muted-foreground text-xs">발주전용 연락처</Label>
|
|
|
|
|
{isEditMode ? (
|
|
|
|
|
<Input
|
|
|
|
|
value={formData.orderPhone}
|
|
|
|
|
onChange={(e) => handleChange('orderPhone', e.target.value)}
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
placeholder="02-0000-0000"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm font-medium">{detail.orderPhone || '-'}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* 단가 목록 테이블 */}
|
|
|
|
|
<Card className="mt-4">
|
|
|
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
|
|
|
<CardTitle className="text-base">단가표 목록</CardTitle>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<Select value={gradeFilter} onValueChange={setGradeFilter}>
|
|
|
|
|
<SelectTrigger className="h-8 w-[120px] text-sm">
|
|
|
|
|
<SelectValue placeholder="등급" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="all">전체</SelectItem>
|
|
|
|
|
{TRADE_GRADE_OPTIONS.map((opt) => (
|
|
|
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
|
|
|
{opt.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
총 {detail.items.length}건
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="p-0">
|
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead className="w-[40px] text-center">
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={isAllSelected}
|
|
|
|
|
onCheckedChange={(checked) => handleSelectAll(!!checked)}
|
|
|
|
|
/>
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead className="w-[50px] text-center">번호</TableHead>
|
|
|
|
|
<TableHead className="min-w-[100px]">단가번호</TableHead>
|
|
|
|
|
<TableHead className="min-w-[100px]">품목코드</TableHead>
|
|
|
|
|
<TableHead className="min-w-[80px]">품목유형</TableHead>
|
|
|
|
|
<TableHead className="min-w-[120px]">품목명</TableHead>
|
|
|
|
|
<TableHead className="min-w-[80px]">규격</TableHead>
|
|
|
|
|
<TableHead className="min-w-[60px]">단위</TableHead>
|
|
|
|
|
<TableHead className="min-w-[100px] text-right">매입단가</TableHead>
|
|
|
|
|
<TableHead className="min-w-[80px] text-right">가공비</TableHead>
|
|
|
|
|
<TableHead className="min-w-[70px] text-right">마진율</TableHead>
|
|
|
|
|
<TableHead className="min-w-[100px] text-right">판매단가</TableHead>
|
|
|
|
|
<TableHead className="min-w-[80px]">상태</TableHead>
|
|
|
|
|
<TableHead className="min-w-[80px]">작성자</TableHead>
|
|
|
|
|
<TableHead className="min-w-[100px]">변경일</TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{detail.items.length === 0 ? (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell colSpan={15} className="text-center text-muted-foreground py-8">
|
|
|
|
|
단가 데이터가 없습니다.
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
) : (
|
|
|
|
|
detail.items.map((item, index) => (
|
|
|
|
|
<TableRow key={item.id}>
|
|
|
|
|
<TableCell className="text-center">
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={selectedItems.has(item.id)}
|
|
|
|
|
onCheckedChange={(checked) => handleSelectItem(item.id, !!checked)}
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-center text-muted-foreground">
|
|
|
|
|
{index + 1}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>{item.pricingCode}</TableCell>
|
|
|
|
|
<TableCell>{item.itemCode}</TableCell>
|
|
|
|
|
<TableCell className="text-muted-foreground">{item.itemType}</TableCell>
|
|
|
|
|
<TableCell className="font-medium">{item.itemName}</TableCell>
|
|
|
|
|
<TableCell className="text-muted-foreground">{item.specification}</TableCell>
|
|
|
|
|
<TableCell className="text-muted-foreground">{item.unit}</TableCell>
|
|
|
|
|
<TableCell className="text-right font-mono">{formatPrice(item.purchasePrice)}</TableCell>
|
|
|
|
|
<TableCell className="text-right font-mono">{formatPrice(item.processingCost)}</TableCell>
|
|
|
|
|
<TableCell className="text-right font-mono">{item.marginRate}%</TableCell>
|
|
|
|
|
<TableCell className="text-right font-mono font-semibold">{formatPrice(item.salesPrice)}</TableCell>
|
|
|
|
|
<TableCell>
|
2026-02-05 15:57:49 +09:00
|
|
|
<Badge variant="outline" className={getPresetStyle('success')}>
|
2026-02-04 12:46:19 +09:00
|
|
|
{item.status}
|
|
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-muted-foreground">{item.author}</TableCell>
|
|
|
|
|
<TableCell className="text-muted-foreground">{item.changedDate}</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* 하단 버튼 (sticky 하단 바) */}
|
|
|
|
|
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[48px] ${sidebarCollapsed ? 'md:left-[156px]' : 'md:left-[316px]'} flex items-center justify-between`}>
|
|
|
|
|
{/* 왼쪽: 목록으로 / 취소 */}
|
|
|
|
|
{isViewMode ? (
|
|
|
|
|
<Button variant="outline" onClick={handleBack} size="sm" className="md:size-default">
|
|
|
|
|
<ArrowLeft className="w-4 h-4 md:mr-2" />
|
|
|
|
|
<span className="hidden md:inline">목록으로</span>
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button variant="outline" onClick={handleCancel} disabled={isSaving} size="sm" className="md:size-default">
|
|
|
|
|
<X className="w-4 h-4 md:mr-2" />
|
|
|
|
|
<span className="hidden md:inline">취소</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 오른쪽: 액션 버튼들 */}
|
|
|
|
|
<div className="flex items-center gap-1 md:gap-2">
|
|
|
|
|
{isViewMode && (
|
|
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="md:size-default"
|
|
|
|
|
onClick={() => setShowDocumentModal(true)}
|
|
|
|
|
>
|
|
|
|
|
<FileText className="w-4 h-4 md:mr-2" />
|
|
|
|
|
<span className="hidden md:inline">단가표 보기</span>
|
|
|
|
|
</Button>
|
|
|
|
|
{canApprove && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="md:size-default"
|
|
|
|
|
onClick={() => setShowFinalizeDialog(true)}
|
|
|
|
|
disabled={detail.status === 'finalized'}
|
|
|
|
|
>
|
|
|
|
|
<CheckCircle2 className="w-4 h-4 md:mr-2" />
|
|
|
|
|
<span className="hidden md:inline">최종확정</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
{canUpdate && (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleEditMode}
|
|
|
|
|
size="sm"
|
|
|
|
|
className="md:size-default"
|
|
|
|
|
disabled={detail.status === 'finalized'}
|
|
|
|
|
>
|
|
|
|
|
<Edit3 className="w-4 h-4 md:mr-2" />
|
|
|
|
|
<span className="hidden md:inline">수정</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{isEditMode && canUpdate && (
|
|
|
|
|
<Button onClick={handleSave} disabled={isSaving} size="sm" className="md:size-default">
|
|
|
|
|
<Save className="w-4 h-4 md:mr-2" />
|
|
|
|
|
<span className="hidden md:inline">{isSaving ? '저장 중...' : '저장'}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 최종확정 다이얼로그 */}
|
|
|
|
|
<AlertDialog open={showFinalizeDialog} onOpenChange={setShowFinalizeDialog}>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>최종확정</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
단가배포를 최종 확정하시겠습니까? 확정 후에는 수정이 불가합니다.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction onClick={handleFinalize}>
|
|
|
|
|
확정
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
|
|
|
|
|
{/* 단가표 보기 모달 */}
|
|
|
|
|
<PriceDistributionDocumentModal
|
|
|
|
|
open={showDocumentModal}
|
|
|
|
|
onOpenChange={setShowDocumentModal}
|
|
|
|
|
detail={detail}
|
|
|
|
|
/>
|
|
|
|
|
</PageLayout>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default PriceDistributionDetail;
|