- 회계: 거래처, 매입/매출, 입출금 상세 페이지 개선 - HR: 직원 관리 및 출퇴근 설정 기능 수정 - 주문관리: 상세폼 구조 분리 (cards, dialogs, hooks, tables) - 알림설정: 컴포넌트 구조 단순화 및 리팩토링 - 캘린더: 헤더 및 일정 타입 개선 - 출고관리: 액션 및 타입 정의 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
757 lines
27 KiB
TypeScript
757 lines
27 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { format } from 'date-fns';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@/components/ui/alert-dialog';
|
|
import { FileText, Plus, X, Eye, Receipt, List } from 'lucide-react';
|
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
import { PageHeader } from '@/components/organisms/PageHeader';
|
|
import { DocumentDetailModal } from '@/components/approval/DocumentDetail';
|
|
import type { ProposalDocumentData, ExpenseReportDocumentData } from '@/components/approval/DocumentDetail/types';
|
|
import type { PurchaseRecord, PurchaseItem, PurchaseType } from './types';
|
|
import { PURCHASE_TYPE_LABELS } from './types';
|
|
import {
|
|
getPurchaseById,
|
|
createPurchase,
|
|
updatePurchase,
|
|
deletePurchase,
|
|
} from './actions';
|
|
import { getClients } from '../VendorManagement/actions';
|
|
import { toast } from 'sonner';
|
|
|
|
interface PurchaseDetailProps {
|
|
purchaseId: string;
|
|
mode: 'view' | 'edit' | 'new';
|
|
}
|
|
|
|
// ===== 거래처 타입 =====
|
|
interface ClientOption {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
// ===== 초기 품목 데이터 =====
|
|
const createEmptyItem = (): PurchaseItem => ({
|
|
id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
itemName: '',
|
|
quantity: 1,
|
|
unitPrice: 0,
|
|
supplyPrice: 0,
|
|
vat: 0,
|
|
note: '',
|
|
});
|
|
|
|
export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
|
const router = useRouter();
|
|
const isViewMode = mode === 'view';
|
|
const isNewMode = mode === 'new';
|
|
|
|
// ===== 로딩 상태 =====
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
// ===== 거래처 목록 =====
|
|
const [clients, setClients] = useState<ClientOption[]>([]);
|
|
|
|
// ===== 폼 상태 =====
|
|
const [purchaseNo, setPurchaseNo] = useState('');
|
|
const [purchaseDate, setPurchaseDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
|
const [vendorId, setVendorId] = useState('');
|
|
const [vendorName, setVendorName] = useState('');
|
|
const [purchaseType, setPurchaseType] = useState<PurchaseType>('unset');
|
|
const [items, setItems] = useState<PurchaseItem[]>([createEmptyItem()]);
|
|
const [taxInvoiceReceived, setTaxInvoiceReceived] = useState(false);
|
|
|
|
// ===== 문서 관련 상태 (sourceDocument는 API에서 아직 미지원) =====
|
|
const [sourceDocument, setSourceDocument] = useState<PurchaseRecord['sourceDocument']>(undefined);
|
|
const [withdrawalAccount, setWithdrawalAccount] = useState<PurchaseRecord['withdrawalAccount']>(undefined);
|
|
const [createdAt, setCreatedAt] = useState('');
|
|
|
|
// ===== 다이얼로그 상태 =====
|
|
const [documentModalOpen, setDocumentModalOpen] = useState(false);
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
|
|
// ===== 거래처 목록 로드 =====
|
|
useEffect(() => {
|
|
async function loadClients() {
|
|
const result = await getClients({ size: 1000, only_active: true });
|
|
if (result.success) {
|
|
setClients(result.data.map(v => ({
|
|
id: v.id,
|
|
name: v.vendorName,
|
|
})));
|
|
}
|
|
}
|
|
loadClients();
|
|
}, []);
|
|
|
|
// ===== 매입 상세 데이터 로드 =====
|
|
useEffect(() => {
|
|
async function loadPurchaseDetail() {
|
|
if (purchaseId && mode !== 'new') {
|
|
setIsLoading(true);
|
|
const result = await getPurchaseById(purchaseId);
|
|
if (result.success && result.data) {
|
|
const data = result.data;
|
|
setPurchaseNo(data.purchaseNo);
|
|
setPurchaseDate(data.purchaseDate);
|
|
setVendorId(data.vendorId);
|
|
setVendorName(data.vendorName);
|
|
setPurchaseType(data.purchaseType);
|
|
setItems(data.items.length > 0 ? data.items : [createEmptyItem()]);
|
|
setTaxInvoiceReceived(data.taxInvoiceReceived);
|
|
setSourceDocument(data.sourceDocument);
|
|
setWithdrawalAccount(data.withdrawalAccount);
|
|
setCreatedAt(data.createdAt);
|
|
}
|
|
setIsLoading(false);
|
|
} else if (isNewMode) {
|
|
// 신규: 매입번호는 서버에서 자동 생성
|
|
setPurchaseNo('(자동생성)');
|
|
setIsLoading(false);
|
|
} else {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
loadPurchaseDetail();
|
|
}, [purchaseId, mode, isNewMode]);
|
|
|
|
// ===== 합계 계산 =====
|
|
const totals = useMemo(() => {
|
|
const totalSupply = items.reduce((sum, item) => sum + item.supplyPrice, 0);
|
|
const totalVat = items.reduce((sum, item) => sum + item.vat, 0);
|
|
return {
|
|
supplyAmount: totalSupply,
|
|
vat: totalVat,
|
|
total: totalSupply + totalVat,
|
|
};
|
|
}, [items]);
|
|
|
|
// ===== 금액 포맷 =====
|
|
const formatAmount = (amount: number): string => {
|
|
return amount.toLocaleString();
|
|
};
|
|
|
|
// ===== 핸들러 =====
|
|
const handleVendorChange = useCallback((clientId: string) => {
|
|
const client = clients.find(c => c.id === clientId);
|
|
if (client) {
|
|
setVendorId(client.id);
|
|
setVendorName(client.name);
|
|
}
|
|
}, [clients]);
|
|
|
|
const handleItemChange = useCallback((index: number, field: keyof PurchaseItem, value: string | number) => {
|
|
setItems(prev => {
|
|
const newItems = [...prev];
|
|
const item = { ...newItems[index] };
|
|
|
|
if (field === 'quantity' || field === 'unitPrice') {
|
|
const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value;
|
|
item[field] = numValue;
|
|
// 자동 계산: 공급가액 = 수량 * 단가
|
|
item.supplyPrice = item.quantity * item.unitPrice;
|
|
item.vat = Math.floor(item.supplyPrice * 0.1);
|
|
} else {
|
|
(item as any)[field] = value;
|
|
}
|
|
|
|
newItems[index] = item;
|
|
return newItems;
|
|
});
|
|
}, []);
|
|
|
|
const handleAddItem = useCallback(() => {
|
|
setItems(prev => [...prev, createEmptyItem()]);
|
|
}, []);
|
|
|
|
const handleRemoveItem = useCallback((index: number) => {
|
|
setItems(prev => {
|
|
if (prev.length <= 1) return prev;
|
|
return prev.filter((_, i) => i !== index);
|
|
});
|
|
}, []);
|
|
|
|
// ===== 저장 =====
|
|
const handleSave = useCallback(async () => {
|
|
if (!vendorId) {
|
|
toast.warning('거래처를 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
|
|
const purchaseData: Partial<PurchaseRecord> = {
|
|
purchaseDate,
|
|
vendorId,
|
|
supplyAmount: totals.supplyAmount,
|
|
vat: totals.vat,
|
|
totalAmount: totals.total,
|
|
purchaseType,
|
|
taxInvoiceReceived,
|
|
};
|
|
|
|
try {
|
|
let result;
|
|
if (isNewMode) {
|
|
result = await createPurchase(purchaseData);
|
|
} else if (purchaseId) {
|
|
result = await updatePurchase(purchaseId, purchaseData);
|
|
}
|
|
|
|
if (result?.success) {
|
|
toast.success(isNewMode ? '매입이 등록되었습니다.' : '매입이 수정되었습니다.');
|
|
router.push('/ko/accounting/purchase');
|
|
} else {
|
|
toast.error(result?.error || '저장에 실패했습니다.');
|
|
}
|
|
} catch {
|
|
toast.error('저장 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}, [purchaseDate, vendorId, totals, purchaseType, taxInvoiceReceived, isNewMode, purchaseId, router]);
|
|
|
|
const handleBack = useCallback(() => {
|
|
router.push('/ko/accounting/purchase');
|
|
}, [router]);
|
|
|
|
const handleEdit = useCallback(() => {
|
|
router.push(`/ko/accounting/purchase/${purchaseId}?mode=edit`);
|
|
}, [router, purchaseId]);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
if (isNewMode) {
|
|
router.push('/ko/accounting/purchase');
|
|
} else {
|
|
router.push(`/ko/accounting/purchase/${purchaseId}`);
|
|
}
|
|
}, [router, purchaseId, isNewMode]);
|
|
|
|
// ===== 삭제 =====
|
|
const handleDelete = useCallback(async () => {
|
|
if (!purchaseId) return;
|
|
|
|
try {
|
|
const result = await deletePurchase(purchaseId);
|
|
setShowDeleteDialog(false);
|
|
|
|
if (result.success) {
|
|
toast.success('매입이 삭제되었습니다.');
|
|
router.push('/ko/accounting/purchase');
|
|
} else {
|
|
toast.error(result.error || '삭제에 실패했습니다.');
|
|
}
|
|
} catch {
|
|
toast.error('삭제 중 오류가 발생했습니다.');
|
|
}
|
|
}, [purchaseId, router]);
|
|
|
|
return (
|
|
<PageLayout>
|
|
{/* 페이지 헤더 */}
|
|
<PageHeader
|
|
title={isNewMode ? '매입 등록' : '매입 상세'}
|
|
description="매입 상세를 등록하고 관리합니다"
|
|
icon={Receipt}
|
|
/>
|
|
|
|
{/* 헤더 액션 버튼 */}
|
|
<div className="flex items-center justify-end gap-2 mb-6">
|
|
{/* view 모드: [목록] [삭제] [수정] */}
|
|
{isViewMode ? (
|
|
<>
|
|
<Button variant="outline" onClick={handleBack}>
|
|
<List className="h-4 w-4 mr-2" />
|
|
목록
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
className="text-red-500 border-red-200 hover:bg-red-50"
|
|
onClick={() => setShowDeleteDialog(true)}
|
|
>
|
|
삭제
|
|
</Button>
|
|
<Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600">
|
|
수정
|
|
</Button>
|
|
</>
|
|
) : (
|
|
/* edit/new 모드: [취소] [저장/등록] */
|
|
<>
|
|
<Button variant="outline" onClick={handleCancel}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleSave} className="bg-blue-500 hover:bg-blue-600" disabled={isSaving}>
|
|
{isSaving ? '저장 중...' : isNewMode ? '등록' : '저장'}
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
{/* ===== 기본 정보 섹션 ===== */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-lg">기본 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* 품의서/지출결의서인 경우 전용 레이아웃 */}
|
|
{sourceDocument ? (
|
|
<>
|
|
{/* 문서 타입 및 열람 버튼 */}
|
|
<div className="flex items-center gap-4 p-3 bg-orange-50 border border-orange-200 rounded-lg">
|
|
<Badge variant="outline" className="bg-orange-100 text-orange-800 border-orange-300">
|
|
{sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'}
|
|
</Badge>
|
|
<span className="text-sm text-muted-foreground">연결된 문서가 있습니다</span>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="ml-auto border-orange-300 text-orange-700 hover:bg-orange-100"
|
|
onClick={() => setDocumentModalOpen(true)}
|
|
>
|
|
<Eye className="h-4 w-4 mr-1" />
|
|
문서 열람
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 품의서/지출결의서용 필드 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* 품의서/지출결의서 제목 */}
|
|
<div className="space-y-2 col-span-2">
|
|
<Label>{sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} 제목</Label>
|
|
<Input
|
|
value={sourceDocument.title}
|
|
readOnly
|
|
disabled
|
|
className="bg-gray-50"
|
|
/>
|
|
</div>
|
|
|
|
{/* 예상비용 */}
|
|
<div className="space-y-2">
|
|
<Label>예상비용</Label>
|
|
<Input
|
|
value={`${sourceDocument.expectedCost.toLocaleString()}원`}
|
|
readOnly
|
|
disabled
|
|
className="bg-gray-50"
|
|
/>
|
|
</div>
|
|
|
|
{/* 매입번호 */}
|
|
<div className="space-y-2">
|
|
<Label>매입번호</Label>
|
|
<Input
|
|
value={purchaseNo}
|
|
readOnly
|
|
disabled
|
|
className="bg-gray-50"
|
|
/>
|
|
</div>
|
|
|
|
{/* 거래처명 */}
|
|
<div className="space-y-2">
|
|
<Label>거래처명</Label>
|
|
<Select
|
|
value={vendorId}
|
|
onValueChange={handleVendorChange}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="거래처 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{clients.map((client) => (
|
|
<SelectItem key={client.id} value={client.id}>
|
|
{client.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 매입 유형 */}
|
|
<div className="space-y-2 col-span-2">
|
|
<Label>매입 유형</Label>
|
|
<Select
|
|
value={purchaseType}
|
|
onValueChange={(value) => setPurchaseType(value as PurchaseType)}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="매입 유형 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.entries(PURCHASE_TYPE_LABELS).map(([key, label]) => (
|
|
<SelectItem key={key} value={key}>
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
/* 일반 매입 (품의서/지출결의서 없는 경우) */
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* 매입번호 */}
|
|
<div className="space-y-2">
|
|
<Label>매입번호</Label>
|
|
<Input
|
|
value={purchaseNo}
|
|
readOnly
|
|
disabled
|
|
className="bg-gray-50"
|
|
/>
|
|
</div>
|
|
|
|
{/* 매입일 */}
|
|
<div className="space-y-2">
|
|
<Label>매입일</Label>
|
|
<Input
|
|
type="date"
|
|
value={purchaseDate}
|
|
onChange={(e) => setPurchaseDate(e.target.value)}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* 거래처명 */}
|
|
<div className="space-y-2">
|
|
<Label>거래처명</Label>
|
|
<Select
|
|
value={vendorId}
|
|
onValueChange={handleVendorChange}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="거래처 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{clients.map((client) => (
|
|
<SelectItem key={client.id} value={client.id}>
|
|
{client.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 매입 유형 */}
|
|
<div className="space-y-2">
|
|
<Label>매입 유형</Label>
|
|
<Select
|
|
value={purchaseType}
|
|
onValueChange={(value) => setPurchaseType(value as PurchaseType)}
|
|
disabled={isViewMode}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="매입 유형 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.entries(PURCHASE_TYPE_LABELS).map(([key, label]) => (
|
|
<SelectItem key={key} value={key}>
|
|
{label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* ===== 품목 정보 섹션 ===== */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-lg">품목 정보</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[50px] text-center">#</TableHead>
|
|
<TableHead>품목명</TableHead>
|
|
<TableHead className="w-[100px] text-right">수량</TableHead>
|
|
<TableHead className="w-[120px] text-right">단가</TableHead>
|
|
<TableHead className="w-[120px] text-right">공급가액</TableHead>
|
|
<TableHead className="w-[100px] text-right">부가세</TableHead>
|
|
<TableHead>적요</TableHead>
|
|
<TableHead className="w-[50px]"></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{items.map((item, index) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="text-center">{index + 1}</TableCell>
|
|
<TableCell>
|
|
<Input
|
|
value={item.itemName}
|
|
onChange={(e) => handleItemChange(index, 'itemName', e.target.value)}
|
|
placeholder="품목명"
|
|
disabled={isViewMode}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Input
|
|
type="number"
|
|
value={item.quantity || ''}
|
|
onChange={(e) => handleItemChange(index, 'quantity', e.target.value)}
|
|
className="text-right"
|
|
disabled={isViewMode}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Input
|
|
type="number"
|
|
value={item.unitPrice || ''}
|
|
onChange={(e) => handleItemChange(index, 'unitPrice', e.target.value)}
|
|
className="text-right"
|
|
disabled={isViewMode}
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="text-right font-medium">
|
|
{formatAmount(item.supplyPrice)}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{formatAmount(item.vat)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Input
|
|
value={item.note || ''}
|
|
onChange={(e) => handleItemChange(index, 'note', e.target.value)}
|
|
placeholder="적요"
|
|
disabled={isViewMode}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
{!isViewMode && items.length > 1 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-red-600 hover:text-red-700"
|
|
onClick={() => handleRemoveItem(index)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{/* 합계 행 */}
|
|
<TableRow className="bg-gray-50 font-medium">
|
|
<TableCell colSpan={4} className="text-right">
|
|
합계
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{formatAmount(totals.supplyAmount)}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{formatAmount(totals.vat)}
|
|
</TableCell>
|
|
<TableCell colSpan={2}></TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* 품목 추가 버튼 */}
|
|
{!isViewMode && (
|
|
<div className="mt-4">
|
|
<Button variant="outline" onClick={handleAddItem}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* ===== 세금계산서 섹션 ===== */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-lg">세금계산서</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Label htmlFor="taxInvoice">세금계산서 수취</Label>
|
|
<Switch
|
|
id="taxInvoice"
|
|
checked={taxInvoiceReceived}
|
|
onCheckedChange={setTaxInvoiceReceived}
|
|
disabled={isViewMode}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{taxInvoiceReceived ? (
|
|
<span className="text-sm text-green-600 flex items-center gap-1">
|
|
<FileText className="h-4 w-4" />
|
|
수취완료
|
|
</span>
|
|
) : (
|
|
<span className="text-sm text-gray-500 flex items-center gap-1">
|
|
<FileText className="h-4 w-4" />
|
|
미수취
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* ===== 품의서/지출결의서 문서 열람 모달 ===== */}
|
|
{sourceDocument && (
|
|
<DocumentDetailModal
|
|
open={documentModalOpen}
|
|
onOpenChange={setDocumentModalOpen}
|
|
documentType={sourceDocument.type === 'proposal' ? 'proposal' : 'expenseReport'}
|
|
data={
|
|
sourceDocument.type === 'proposal'
|
|
? {
|
|
documentNo: sourceDocument.documentNo,
|
|
createdAt: createdAt.split('T')[0],
|
|
vendor: vendorName,
|
|
vendorPaymentDate: purchaseDate,
|
|
title: sourceDocument.title,
|
|
description: '품의 내역 상세',
|
|
reason: '업무 수행을 위한 필수 구매',
|
|
estimatedCost: sourceDocument.expectedCost,
|
|
attachments: [],
|
|
approvers: [
|
|
{
|
|
id: 'approver-1',
|
|
name: '김팀장',
|
|
position: '팀장',
|
|
department: '경영지원팀',
|
|
status: 'approved',
|
|
approvedAt: createdAt.split('T')[0],
|
|
},
|
|
{
|
|
id: 'approver-2',
|
|
name: '이부장',
|
|
position: '부장',
|
|
department: '경영지원팀',
|
|
status: 'approved',
|
|
approvedAt: createdAt.split('T')[0],
|
|
},
|
|
],
|
|
drafter: {
|
|
id: 'drafter-1',
|
|
name: '홍길동',
|
|
position: '대리',
|
|
department: '경영지원팀',
|
|
status: 'none',
|
|
},
|
|
} as ProposalDocumentData
|
|
: {
|
|
documentNo: sourceDocument.documentNo,
|
|
createdAt: createdAt.split('T')[0],
|
|
requestDate: purchaseDate,
|
|
paymentDate: purchaseDate,
|
|
items: items.map((item, idx) => ({
|
|
id: item.id,
|
|
no: idx + 1,
|
|
description: item.itemName,
|
|
amount: item.supplyPrice + item.vat,
|
|
note: item.note || '',
|
|
})),
|
|
cardInfo: withdrawalAccount
|
|
? `${withdrawalAccount.bankName} ${withdrawalAccount.accountNo}`
|
|
: '',
|
|
totalAmount: totals.total,
|
|
attachments: [],
|
|
approvers: [
|
|
{
|
|
id: 'approver-1',
|
|
name: '김팀장',
|
|
position: '팀장',
|
|
department: '경영지원팀',
|
|
status: 'approved',
|
|
approvedAt: createdAt.split('T')[0],
|
|
},
|
|
{
|
|
id: 'approver-2',
|
|
name: '이부장',
|
|
position: '부장',
|
|
department: '경영지원팀',
|
|
status: 'approved',
|
|
approvedAt: createdAt.split('T')[0],
|
|
},
|
|
],
|
|
drafter: {
|
|
id: 'drafter-1',
|
|
name: '홍길동',
|
|
position: '대리',
|
|
department: '경영지원팀',
|
|
status: 'none',
|
|
},
|
|
} as ExpenseReportDocumentData
|
|
}
|
|
onEdit={() => toast.info('문서 수정 기능 준비 중입니다.')}
|
|
onCopy={() => toast.info('문서 복제 기능 준비 중입니다.')}
|
|
onApprove={() => toast.info('승인 기능 준비 중입니다.')}
|
|
onReject={() => toast.info('반려 기능 준비 중입니다.')}
|
|
/>
|
|
)}
|
|
|
|
{/* ===== 삭제 확인 다이얼로그 ===== */}
|
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>매입 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
이 매입 내역을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleDelete}
|
|
className="bg-red-600 hover:bg-red-700"
|
|
>
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</PageLayout>
|
|
);
|
|
} |