- BillManagement: BillDetail 리팩토링, sections/hooks 분리, constants 추가 - BillManagement types 대폭 확장, actions 개선 - GiftCertificateManagement: actions/types 확장 - CEO 대시보드: SummaryNavBar 컴포넌트 추가, useSectionSummary 훅 - bill-prototype 개발 페이지 업데이트
151 lines
7.7 KiB
TypeScript
151 lines
7.7 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo } from 'react';
|
|
import { Plus, Trash2, AlertTriangle } from 'lucide-react';
|
|
import { Input } from '@/components/ui/input';
|
|
import { DatePicker } from '@/components/ui/date-picker';
|
|
import { Label } from '@/components/ui/label';
|
|
import { CurrencyInput } from '@/components/ui/currency-input';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import {
|
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
|
} from '@/components/ui/table';
|
|
import type { BillFormData } from '../types';
|
|
import { HISTORY_TYPE_OPTIONS } from '../constants';
|
|
|
|
interface HistorySectionProps {
|
|
formData: BillFormData;
|
|
updateField: <K extends keyof BillFormData>(field: K, value: BillFormData[K]) => void;
|
|
isViewMode: boolean;
|
|
isElectronic: boolean;
|
|
maxSplitCount: number;
|
|
onAddInstallment: () => void;
|
|
onRemoveInstallment: (id: string) => void;
|
|
onUpdateInstallment: (id: string, field: string, value: string | number) => void;
|
|
}
|
|
|
|
export function HistorySection({
|
|
formData, updateField, isViewMode, isElectronic, maxSplitCount,
|
|
onAddInstallment, onRemoveInstallment, onUpdateInstallment,
|
|
}: HistorySectionProps) {
|
|
const splitEndorsementStats = useMemo(() => {
|
|
const splits = formData.installments.filter(inst => inst.type === 'splitEndorsement');
|
|
const totalAmount = splits.reduce((sum, inst) => sum + inst.amount, 0);
|
|
return { count: splits.length, totalAmount, remaining: formData.amount - totalAmount };
|
|
}, [formData.installments, formData.amount]);
|
|
|
|
return (
|
|
<Card className="mb-6">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle className="text-lg">이력 관리</CardTitle>
|
|
{!isViewMode && (
|
|
<Button variant="outline" size="sm" onClick={onAddInstallment} className="text-orange-500 border-orange-300 hover:bg-orange-50">
|
|
<Plus className="h-4 w-4 mr-1" />추가
|
|
</Button>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{/* 분할배서 토글 */}
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-3">
|
|
<Switch checked={formData.isSplit} onCheckedChange={(c) => updateField('isSplit', c)} disabled={isViewMode} />
|
|
<Label>분할배서 허용</Label>
|
|
{formData.isSplit && (
|
|
<Badge variant="outline" className="text-xs text-amber-600 border-amber-300 bg-amber-50">
|
|
최대 {maxSplitCount}회
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{formData.isSplit && isElectronic && (
|
|
<div className="flex items-center gap-2 text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-md px-3 py-2">
|
|
<AlertTriangle className="h-3.5 w-3.5 flex-shrink-0" />
|
|
<span>전자어음 분할배서: 최초 배서인에 한해 5회 미만 가능 (전자어음법 제6조)</span>
|
|
</div>
|
|
)}
|
|
{formData.isSplit && splitEndorsementStats.count > 0 && (
|
|
<div className="flex items-center gap-4 text-sm bg-gray-50 rounded-md px-3 py-2">
|
|
<span className="text-muted-foreground">원금액:</span>
|
|
<span className="font-semibold">₩ {formData.amount.toLocaleString()}</span>
|
|
<span className="text-muted-foreground">| 분할배서 합계:</span>
|
|
<span className="font-semibold text-blue-600">₩ {splitEndorsementStats.totalAmount.toLocaleString()}</span>
|
|
<span className="text-muted-foreground">| 잔액:</span>
|
|
<span className={`font-semibold ${splitEndorsementStats.remaining < 0 ? 'text-red-600' : 'text-green-600'}`}>
|
|
₩ {splitEndorsementStats.remaining.toLocaleString()}
|
|
</span>
|
|
{splitEndorsementStats.remaining < 0 && (
|
|
<span className="text-red-500 text-xs flex items-center gap-1"><AlertTriangle className="h-3 w-3" />금액 초과</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 이력 테이블 */}
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[50px]">No</TableHead>
|
|
<TableHead className="min-w-[130px]">일자</TableHead>
|
|
<TableHead className="min-w-[130px]">처리구분</TableHead>
|
|
<TableHead className="min-w-[120px]">금액</TableHead>
|
|
<TableHead className="min-w-[120px]">상대처</TableHead>
|
|
<TableHead className="min-w-[120px]">비고</TableHead>
|
|
{!isViewMode && <TableHead className="w-[60px]">삭제</TableHead>}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{formData.installments.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={isViewMode ? 6 : 7} className="text-center text-gray-500 py-8">등록된 이력이 없습니다</TableCell>
|
|
</TableRow>
|
|
) : formData.installments.map((inst, idx) => (
|
|
<TableRow key={inst.id} className={inst.type === 'splitEndorsement' ? 'bg-amber-50/50' : ''}>
|
|
<TableCell className="text-center">{idx + 1}</TableCell>
|
|
<TableCell>
|
|
<DatePicker value={inst.date} onChange={(d) => onUpdateInstallment(inst.id, 'date', d)} size="sm" disabled={isViewMode} />
|
|
</TableCell>
|
|
<TableCell>
|
|
<Select value={inst.type} onValueChange={(v) => onUpdateInstallment(inst.id, 'type', v)} disabled={isViewMode}>
|
|
<SelectTrigger className="h-8 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
{HISTORY_TYPE_OPTIONS
|
|
.filter(o => o.value !== 'splitEndorsement' || formData.isSplit)
|
|
.map(o => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)
|
|
}
|
|
</SelectContent>
|
|
</Select>
|
|
</TableCell>
|
|
<TableCell>
|
|
<CurrencyInput value={inst.amount} onChange={(v) => onUpdateInstallment(inst.id, 'amount', v ?? 0)} className="h-8 text-sm" disabled={isViewMode} />
|
|
</TableCell>
|
|
<TableCell>
|
|
<Input value={inst.counterparty} onChange={(e) => onUpdateInstallment(inst.id, 'counterparty', e.target.value)} placeholder="거래처/은행" className="h-8 text-sm" disabled={isViewMode} />
|
|
</TableCell>
|
|
<TableCell>
|
|
<Input value={inst.note} onChange={(e) => onUpdateInstallment(inst.id, 'note', e.target.value)} className="h-8 text-sm" disabled={isViewMode} />
|
|
</TableCell>
|
|
{!isViewMode && (
|
|
<TableCell>
|
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50" onClick={() => onRemoveInstallment(inst.id)}>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|