- EstimateDetailTableSection: 하드코딩된 셀렉트 옵션 → API 데이터 연동 - 재료/도장/모터/제어기/시공비: getCommonCodeOptions() 사용 - 공과 품목: getExpenseItemOptions() 사용 - EstimateListClient: 거래처/견적자 필터 API 연동 - MOCK_PARTNERS → getClientOptions() - MOCK_ESTIMATORS → getUserOptions() - actions.ts: 공통코드/거래처/사용자/공과품목 API 함수 추가 - constants.ts: MOCK_MATERIALS 제거 - EstimateDetailForm: MOCK_MATERIALS import 제거
661 lines
32 KiB
TypeScript
661 lines
32 KiB
TypeScript
'use client';
|
||
|
||
import React from 'react';
|
||
import { X, HelpCircle } from 'lucide-react';
|
||
import {
|
||
Tooltip,
|
||
TooltipContent,
|
||
TooltipProvider,
|
||
TooltipTrigger,
|
||
} from '@/components/ui/tooltip';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/components/ui/select';
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from '@/components/ui/table';
|
||
import type { EstimateDetailItem } from '../types';
|
||
import { formatAmount } from '../utils';
|
||
import { calculateItemValuesWithApplied, calculateTotalsWithApplied } from '../hooks/useEstimateCalculations';
|
||
import type { CommonCodeOption } from '../actions';
|
||
|
||
// 계산식 정보
|
||
const FORMULA_INFO: Record<string, string> = {
|
||
weight: '면적 × 25',
|
||
area: '(가로 × 0.16) × (세로 × 0.5)',
|
||
steelScreen: '면적 × 47,500',
|
||
caulking: '(세로 × 4) × 조정단가',
|
||
rail: '(세로 × 0.2) × 조정단가',
|
||
bottom: '가로 × 조정단가',
|
||
boxReinforce: '가로 × 조정단가',
|
||
shaft: '가로 × 조정단가',
|
||
unitPrice: '철제스크린 + 코킹 + 레일 + 하장 + 박스보강 + 샤프트 + 도장 + 모터 + 제어기 + 가로시공비 + 세로시공비',
|
||
expense: '단가 × 공과율',
|
||
cost: '단가 + 공과',
|
||
costExecution: '원가 × 수량',
|
||
marginCost: '원가 × 마진율(1.03)',
|
||
marginCostExecution: '마진원가 × 수량',
|
||
expenseExecution: '공과 × 수량',
|
||
};
|
||
|
||
// 계산식 툴팁이 있는 헤더 컴포넌트
|
||
function FormulaHeader({ label, formulaKey, className }: { label: string; formulaKey: string; className?: string }) {
|
||
const formula = FORMULA_INFO[formulaKey];
|
||
if (!formula) {
|
||
return <span>{label}</span>;
|
||
}
|
||
return (
|
||
<TooltipProvider delayDuration={0}>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<span className={`inline-flex items-center gap-1 cursor-help ${className || ''}`}>
|
||
{label}
|
||
<HelpCircle className="h-3.5 w-3.5 text-gray-400 hover:text-gray-600" />
|
||
</span>
|
||
</TooltipTrigger>
|
||
<TooltipContent side="top" className="max-w-xs">
|
||
<p className="text-xs font-medium text-gray-700">{label} 계산식</p>
|
||
<p className="text-xs text-gray-500 mt-1">{formula}</p>
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</TooltipProvider>
|
||
);
|
||
}
|
||
|
||
// appliedPrices 타입 정의
|
||
export interface AppliedPrices {
|
||
caulking: number;
|
||
rail: number;
|
||
bottom: number;
|
||
boxReinforce: number;
|
||
shaft: number;
|
||
painting: number;
|
||
motor: number;
|
||
controller: number;
|
||
}
|
||
|
||
// 옵션 데이터 타입
|
||
export interface EstimateDetailOptions {
|
||
materials: CommonCodeOption[];
|
||
paintings: CommonCodeOption[];
|
||
motors: CommonCodeOption[];
|
||
controllers: CommonCodeOption[];
|
||
widthConstructions: CommonCodeOption[];
|
||
heightConstructions: CommonCodeOption[];
|
||
}
|
||
|
||
interface EstimateDetailTableSectionProps {
|
||
detailItems: EstimateDetailItem[];
|
||
appliedPrices: AppliedPrices | null;
|
||
isViewMode: boolean;
|
||
options?: EstimateDetailOptions;
|
||
onAddItems: (count: number) => void;
|
||
onRemoveItem: (id: string) => void;
|
||
onRemoveSelected: () => void;
|
||
onItemChange: (id: string, field: keyof EstimateDetailItem, value: string | number) => void;
|
||
onSelectItem: (id: string, selected: boolean) => void;
|
||
onSelectAll: (selected: boolean) => void;
|
||
onApplyAdjustedPrice: () => void;
|
||
onReset: () => void;
|
||
}
|
||
|
||
// API 데이터 로드 전 기본 옵션 (폴백용)
|
||
const DEFAULT_OPTIONS: EstimateDetailOptions = {
|
||
materials: [
|
||
{ value: 'screen', label: '스크린', code: 'screen' },
|
||
{ value: 'slat', label: '슬랫', code: 'slat' },
|
||
{ value: 'bending', label: '벤딩', code: 'bending' },
|
||
{ value: 'jointbar', label: '조인트바', code: 'jointbar' },
|
||
],
|
||
paintings: [
|
||
{ value: '0', label: '직접입력', code: '0', price: 0 },
|
||
{ value: '50000', label: '도장A', code: 'painting_a', price: 50000 },
|
||
{ value: '80000', label: '도장B', code: 'painting_b', price: 80000 },
|
||
],
|
||
motors: [
|
||
{ value: '300000', label: '모터 300,000', code: 'motor_300k', price: 300000 },
|
||
{ value: '500000', label: '모터 500,000', code: 'motor_500k', price: 500000 },
|
||
],
|
||
controllers: [
|
||
{ value: '150000', label: '제어기 150,000', code: 'ctrl_150k', price: 150000 },
|
||
{ value: '250000', label: '제어기 250,000', code: 'ctrl_250k', price: 250000 },
|
||
],
|
||
widthConstructions: [
|
||
{ value: '300000', label: '3.01~4.0M', code: 'w_3_4m', price: 300000 },
|
||
{ value: '400000', label: '4.01~5.0M', code: 'w_4_5m', price: 400000 },
|
||
{ value: '500000', label: '5.01~6.0M', code: 'w_5_6m', price: 500000 },
|
||
{ value: '600000', label: '6.01~7.0M', code: 'w_6_7m', price: 600000 },
|
||
],
|
||
heightConstructions: [
|
||
{ value: '5000', label: '3.51~4.5M', code: 'h_3_4m', price: 5000 },
|
||
{ value: '8000', label: '4.51~5.5M', code: 'h_4_5m', price: 8000 },
|
||
{ value: '10000', label: '5.51~6.5M', code: 'h_5_6m', price: 10000 },
|
||
],
|
||
};
|
||
|
||
export function EstimateDetailTableSection({
|
||
detailItems,
|
||
appliedPrices,
|
||
isViewMode,
|
||
options,
|
||
onAddItems,
|
||
onRemoveItem,
|
||
onRemoveSelected,
|
||
onItemChange,
|
||
onSelectItem,
|
||
onSelectAll,
|
||
onApplyAdjustedPrice,
|
||
onReset,
|
||
}: EstimateDetailTableSectionProps) {
|
||
// API 옵션이 없으면 기본 옵션 사용
|
||
const opts = options || DEFAULT_OPTIONS;
|
||
const selectedCount = detailItems.filter((item) => (item as unknown as { selected?: boolean }).selected).length;
|
||
const allSelected = detailItems.length > 0 && detailItems.every((item) => (item as unknown as { selected?: boolean }).selected);
|
||
const totals = calculateTotalsWithApplied(detailItems, appliedPrices);
|
||
|
||
return (
|
||
<Card>
|
||
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||
<div className="flex items-center gap-4">
|
||
<CardTitle className="text-lg whitespace-nowrap">견적 상세</CardTitle>
|
||
{!isViewMode && (
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-gray-600">{selectedCount}건 선택</span>
|
||
<Button
|
||
type="button"
|
||
variant="default"
|
||
size="sm"
|
||
className="bg-gray-900 hover:bg-gray-800"
|
||
onClick={onRemoveSelected}
|
||
>
|
||
삭제
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant="default"
|
||
size="sm"
|
||
className="bg-blue-500 hover:bg-blue-600"
|
||
onClick={onApplyAdjustedPrice}
|
||
>
|
||
조정 단가 적용
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{!isViewMode && (
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
type="number"
|
||
min={1}
|
||
defaultValue={1}
|
||
className="w-16 text-center"
|
||
id="detail-add-count"
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="default"
|
||
size="sm"
|
||
className="bg-gray-900 hover:bg-gray-800"
|
||
onClick={() => {
|
||
const countInput = document.getElementById('detail-add-count') as HTMLInputElement;
|
||
const count = Math.max(1, parseInt(countInput?.value || '1', 10));
|
||
onAddItems(count);
|
||
}}
|
||
>
|
||
추가
|
||
</Button>
|
||
{/* TODO: 견적 상세 기획서 수정 후 초기화 버튼 및 테이블 항목/데이터 재작업 필요
|
||
<Button type="button" variant="outline" size="sm" onClick={onReset}>
|
||
초기화
|
||
</Button>
|
||
*/}
|
||
</div>
|
||
)}
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="overflow-x-auto max-h-[600px]">
|
||
<Table>
|
||
<TableHeader className="sticky top-0 bg-white z-10">
|
||
<TableRow className="bg-gray-100">
|
||
{!isViewMode && (
|
||
<TableHead className="w-[40px] text-center sticky left-0 bg-gray-100 z-20">
|
||
<input
|
||
type="checkbox"
|
||
className="h-4 w-4 rounded border-gray-300"
|
||
checked={allSelected}
|
||
onChange={(e) => onSelectAll(e.target.checked)}
|
||
/>
|
||
</TableHead>
|
||
)}
|
||
<TableHead className="w-[100px] text-center">명칭</TableHead>
|
||
<TableHead className="w-[80px] text-center">제품</TableHead>
|
||
<TableHead className="w-[70px] text-right">가로</TableHead>
|
||
<TableHead className="w-[70px] text-right">세로</TableHead>
|
||
<TableHead className="w-[70px] text-right">
|
||
<FormulaHeader label="무게" formulaKey="weight" />
|
||
</TableHead>
|
||
<TableHead className="w-[70px] text-right">
|
||
<FormulaHeader label="면적" formulaKey="area" />
|
||
</TableHead>
|
||
<TableHead className="w-[90px] text-right">
|
||
<FormulaHeader label="철제,스크린" formulaKey="steelScreen" />
|
||
</TableHead>
|
||
<TableHead className="w-[80px] text-right">
|
||
<FormulaHeader label="코킹" formulaKey="caulking" />
|
||
</TableHead>
|
||
<TableHead className="w-[80px] text-right">
|
||
<FormulaHeader label="레일" formulaKey="rail" />
|
||
</TableHead>
|
||
<TableHead className="w-[80px] text-right">
|
||
<FormulaHeader label="하장" formulaKey="bottom" />
|
||
</TableHead>
|
||
<TableHead className="w-[90px] text-right">
|
||
<FormulaHeader label="박스+보강" formulaKey="boxReinforce" />
|
||
</TableHead>
|
||
<TableHead className="w-[80px] text-right">
|
||
<FormulaHeader label="샤프트" formulaKey="shaft" />
|
||
</TableHead>
|
||
<TableHead className="w-[80px] text-center">도장</TableHead>
|
||
<TableHead className="w-[80px] text-center">모터</TableHead>
|
||
<TableHead className="w-[80px] text-center">제어기</TableHead>
|
||
<TableHead className="w-[100px] text-center">가로시공비</TableHead>
|
||
<TableHead className="w-[100px] text-center">세로시공비</TableHead>
|
||
<TableHead className="w-[90px] text-right">
|
||
<FormulaHeader label="단가" formulaKey="unitPrice" />
|
||
</TableHead>
|
||
<TableHead className="w-[70px] text-right">공과율</TableHead>
|
||
<TableHead className="w-[80px] text-right">
|
||
<FormulaHeader label="공과" formulaKey="expense" />
|
||
</TableHead>
|
||
<TableHead className="w-[50px] text-right">수량</TableHead>
|
||
<TableHead className="w-[90px] text-right">
|
||
<FormulaHeader label="원가" formulaKey="cost" />
|
||
</TableHead>
|
||
<TableHead className="w-[90px] text-right">
|
||
<FormulaHeader label="원가실행" formulaKey="costExecution" />
|
||
</TableHead>
|
||
<TableHead className="w-[90px] text-right">
|
||
<FormulaHeader label="마진원가" formulaKey="marginCost" />
|
||
</TableHead>
|
||
<TableHead className="w-[100px] text-right">
|
||
<FormulaHeader label="마진원가실행" formulaKey="marginCostExecution" />
|
||
</TableHead>
|
||
<TableHead className="w-[90px] text-right">
|
||
<FormulaHeader label="공과실행" formulaKey="expenseExecution" />
|
||
</TableHead>
|
||
{!isViewMode && <TableHead className="w-[50px] text-center">삭제</TableHead>}
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{detailItems.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell
|
||
colSpan={isViewMode ? 27 : 29}
|
||
className="text-center text-gray-500 py-8"
|
||
>
|
||
등록된 항목이 없습니다.
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
<>
|
||
{detailItems.map((item) => {
|
||
const values = calculateItemValuesWithApplied(item, appliedPrices);
|
||
|
||
return (
|
||
<TableRow key={item.id}>
|
||
{!isViewMode && (
|
||
<TableCell className="text-center sticky left-0 bg-white">
|
||
<input
|
||
type="checkbox"
|
||
className="h-4 w-4 rounded border-gray-300"
|
||
checked={(item as unknown as { selected?: boolean }).selected || false}
|
||
onChange={(e) => onSelectItem(item.id, e.target.checked)}
|
||
/>
|
||
</TableCell>
|
||
)}
|
||
{/* 01: 명칭 */}
|
||
<TableCell>
|
||
<Input
|
||
value={item.name ?? ''}
|
||
onChange={(e) => onItemChange(item.id, 'name', e.target.value)}
|
||
disabled={isViewMode}
|
||
className={`w-full min-w-[80px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 02: 제품 */}
|
||
<TableCell>
|
||
<Select
|
||
value={item.material ?? ''}
|
||
onValueChange={(val) => onItemChange(item.id, 'material', val)}
|
||
disabled={isViewMode}
|
||
>
|
||
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||
<SelectValue placeholder="선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{opts.materials.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</TableCell>
|
||
{/* 03: 가로 */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
step="0.01"
|
||
value={item.width ?? 0}
|
||
onChange={(e) => onItemChange(item.id, 'width', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 04: 세로 */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
step="0.01"
|
||
value={item.height ?? 0}
|
||
onChange={(e) => onItemChange(item.id, 'height', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 05: 무게 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
step="0.01"
|
||
value={values.weight.toFixed(2)}
|
||
onChange={(e) => onItemChange(item.id, 'calcWeight', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 06: 면적 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
step="0.01"
|
||
value={values.area.toFixed(2)}
|
||
onChange={(e) => onItemChange(item.id, 'calcArea', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 07: 철제,스크린 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={values.steelScreen}
|
||
onChange={(e) => onItemChange(item.id, 'calcSteelScreen', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[80px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 08: 코킹 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={values.caulking}
|
||
onChange={(e) => onItemChange(item.id, 'calcCaulking', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 09: 레일 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={values.rail}
|
||
onChange={(e) => onItemChange(item.id, 'calcRail', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 10: 하장 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={values.bottom}
|
||
onChange={(e) => onItemChange(item.id, 'calcBottom', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 11: 박스+보강 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={values.boxReinforce}
|
||
onChange={(e) => onItemChange(item.id, 'calcBoxReinforce', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[80px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 12: 샤프트 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={values.shaft}
|
||
onChange={(e) => onItemChange(item.id, 'calcShaft', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 13: 도장 */}
|
||
<TableCell>
|
||
<Select
|
||
value={String(item.coating || '')}
|
||
onValueChange={(val) => onItemChange(item.id, 'coating', Number(val))}
|
||
disabled={isViewMode}
|
||
>
|
||
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||
<SelectValue placeholder="선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{opts.paintings.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</TableCell>
|
||
{/* 14: 모터 */}
|
||
<TableCell>
|
||
<Select
|
||
value={String(item.mounting || '300000')}
|
||
onValueChange={(val) => onItemChange(item.id, 'mounting', Number(val))}
|
||
disabled={isViewMode}
|
||
>
|
||
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||
<SelectValue placeholder="선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{opts.motors.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</TableCell>
|
||
{/* 15: 제어기 */}
|
||
<TableCell>
|
||
<Select
|
||
value={String(item.controller || '')}
|
||
onValueChange={(val) => onItemChange(item.id, 'controller', Number(val))}
|
||
disabled={isViewMode}
|
||
>
|
||
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||
<SelectValue placeholder="선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{opts.controllers.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</TableCell>
|
||
{/* 16: 가로시공비 */}
|
||
<TableCell>
|
||
<Select
|
||
value={String(item.widthConstruction || '')}
|
||
onValueChange={(val) => onItemChange(item.id, 'widthConstruction', Number(val))}
|
||
disabled={isViewMode}
|
||
>
|
||
<SelectTrigger className={`w-full min-w-[90px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||
<SelectValue placeholder="선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{opts.widthConstructions.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</TableCell>
|
||
{/* 17: 세로시공비 */}
|
||
<TableCell>
|
||
<Select
|
||
value={String(item.heightConstruction || '')}
|
||
onValueChange={(val) => onItemChange(item.id, 'heightConstruction', Number(val))}
|
||
disabled={isViewMode}
|
||
>
|
||
<SelectTrigger className={`w-full min-w-[90px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
|
||
<SelectValue placeholder="선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{opts.heightConstructions.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</TableCell>
|
||
{/* 18: 단가 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={values.unitPrice}
|
||
onChange={(e) => onItemChange(item.id, 'calcUnitPrice', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[80px] font-medium ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 19: 공과율 */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
step="0.01"
|
||
value={item.expense ?? 0}
|
||
onChange={(e) => onItemChange(item.id, 'expense', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 20: 공과 (인풋, 계산값 표시 + 수정 가능) */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
value={values.expense}
|
||
onChange={(e) => onItemChange(item.id, 'calcExpense', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 21: 수량 */}
|
||
<TableCell>
|
||
<Input
|
||
type="number"
|
||
min={1}
|
||
value={item.quantity ?? 0}
|
||
onChange={(e) => onItemChange(item.id, 'quantity', Number(e.target.value))}
|
||
disabled={isViewMode}
|
||
className={`text-right min-w-[40px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||
/>
|
||
</TableCell>
|
||
{/* 22: 원가 */}
|
||
<TableCell className="text-right bg-gray-50">{formatAmount(values.cost)}</TableCell>
|
||
{/* 23: 원가실행 */}
|
||
<TableCell className="text-right bg-gray-50">{formatAmount(values.costExecution)}</TableCell>
|
||
{/* 24: 마진원가 */}
|
||
<TableCell className="text-right bg-gray-50">{formatAmount(values.marginCost)}</TableCell>
|
||
{/* 25: 마진원가실행 */}
|
||
<TableCell className="text-right bg-gray-50 font-medium">{formatAmount(values.marginCostExecution)}</TableCell>
|
||
{/* 26: 공과실행 */}
|
||
<TableCell className="text-right bg-gray-50">{formatAmount(values.expenseExecution)}</TableCell>
|
||
{!isViewMode && (
|
||
<TableCell className="text-center">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 text-red-500 hover:text-red-600"
|
||
onClick={() => onRemoveItem(item.id)}
|
||
>
|
||
<X className="h-4 w-4" />
|
||
</Button>
|
||
</TableCell>
|
||
)}
|
||
</TableRow>
|
||
);
|
||
})}
|
||
{/* 합계 행 */}
|
||
{detailItems.length > 0 && (
|
||
<TableRow className="bg-orange-50 font-medium border-t-2 border-orange-300">
|
||
{!isViewMode && <TableCell className="sticky left-0 bg-orange-50"></TableCell>}
|
||
<TableCell colSpan={4} className="text-center font-bold">합계</TableCell>
|
||
<TableCell className="text-right">{totals.weight.toFixed(2)}</TableCell>
|
||
<TableCell className="text-right">{totals.area.toFixed(2)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.steelScreen)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.caulking)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.rail)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.bottom)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.boxReinforce)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.shaft)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.painting)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.motor)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.controller)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.widthConstruction)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.heightConstruction)}</TableCell>
|
||
<TableCell className="text-right font-bold">{formatAmount(totals.unitPrice)}</TableCell>
|
||
<TableCell className="text-right">-</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.expense)}</TableCell>
|
||
<TableCell className="text-right">{totals.quantity}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.cost)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.costExecution)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.marginCost)}</TableCell>
|
||
<TableCell className="text-right font-bold">{formatAmount(totals.marginCostExecution)}</TableCell>
|
||
<TableCell className="text-right">{formatAmount(totals.expenseExecution)}</TableCell>
|
||
{!isViewMode && <TableCell></TableCell>}
|
||
</TableRow>
|
||
)}
|
||
</>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
} |