feat: [회계] 매출/매입/부실채권/일일보고 UI 개선
- 부실채권 상세/목록/타입 개선 - 매출관리 SalesDetail 개선 - 매입관리 PurchaseDetail 개선 - 일일보고 UI 리팩토링 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,7 @@ export default function BadDebtCollectionPage() {
|
||||
return (
|
||||
<BadDebtCollection
|
||||
initialData={data}
|
||||
initialSummary={summary as { total_amount: number; collecting_amount: number; legal_action_amount: number; recovered_amount: number; bad_debt_amount: number; } | null}
|
||||
initialSummary={summary as { total_amount: number; collecting_amount: number; legal_action_amount: number; collection_end_amount: number; } | null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,8 +39,10 @@ import type {
|
||||
} from './types';
|
||||
import {
|
||||
STATUS_SELECT_OPTIONS,
|
||||
COLLECTION_END_REASON_OPTIONS,
|
||||
VENDOR_TYPE_LABELS,
|
||||
} from './types';
|
||||
import type { CollectionEndReason } from './types';
|
||||
import { createBadDebt, updateBadDebt, deleteBadDebt, addBadDebtMemo, deleteBadDebtMemo } from './actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
@@ -87,6 +89,7 @@ const getEmptyRecord = (): Omit<BadDebtRecord, 'id' | 'createdAt' | 'updatedAt'>
|
||||
assignedManagerId: null,
|
||||
assignedManager: null,
|
||||
settingToggle: true,
|
||||
collectionEndReason: undefined,
|
||||
badDebtCount: 0,
|
||||
badDebts: [],
|
||||
files: [],
|
||||
@@ -778,22 +781,47 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
{/* 상태 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-gray-700">상태</Label>
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(val) => handleChange('status', val as CollectionStatus)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_SELECT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(val) => {
|
||||
handleChange('status', val as CollectionStatus);
|
||||
if (val !== 'collectionEnd') {
|
||||
handleChange('collectionEndReason', null);
|
||||
}
|
||||
}}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_SELECT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{formData.status === 'collectionEnd' && (
|
||||
<Select
|
||||
value={formData.collectionEndReason || ''}
|
||||
onValueChange={(val) => handleChange('collectionEndReason', val as CollectionEndReason)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectValue placeholder="종료사유 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COLLECTION_END_REASON_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 연체일수 */}
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -72,8 +72,9 @@ function mapApiStatusToFrontend(apiStatus: string): CollectionStatus {
|
||||
switch (apiStatus) {
|
||||
case 'collecting': return 'collecting';
|
||||
case 'legal_action': return 'legalAction';
|
||||
case 'recovered': return 'recovered';
|
||||
case 'bad_debt': return 'badDebt';
|
||||
case 'recovered':
|
||||
case 'bad_debt':
|
||||
case 'collection_end': return 'collectionEnd';
|
||||
default: return 'collecting';
|
||||
}
|
||||
}
|
||||
@@ -82,8 +83,7 @@ function mapFrontendStatusToApi(status: CollectionStatus): string {
|
||||
switch (status) {
|
||||
case 'collecting': return 'collecting';
|
||||
case 'legalAction': return 'legal_action';
|
||||
case 'recovered': return 'recovered';
|
||||
case 'badDebt': return 'bad_debt';
|
||||
case 'collectionEnd': return 'collection_end';
|
||||
default: return 'collecting';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2';
|
||||
|
||||
import { useState, useMemo, useCallback, useTransition } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { AlertTriangle, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -56,6 +57,7 @@ const tableColumns = [
|
||||
{ key: 'managerName', label: '담당자', className: 'w-[100px]', sortable: true },
|
||||
{ key: 'status', label: '상태', className: 'text-center w-[100px]', sortable: true },
|
||||
{ key: 'setting', label: '설정', className: 'text-center w-[80px]' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center w-[80px]' },
|
||||
];
|
||||
|
||||
// ===== Props 타입 정의 =====
|
||||
@@ -65,8 +67,7 @@ interface BadDebtCollectionProps {
|
||||
total_amount: number;
|
||||
collecting_amount: number;
|
||||
legal_action_amount: number;
|
||||
recovered_amount: number;
|
||||
bad_debt_amount: number;
|
||||
collection_end_amount: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -132,7 +133,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
totalAmount: initialSummary.total_amount,
|
||||
collectingAmount: initialSummary.collecting_amount,
|
||||
legalActionAmount: initialSummary.legal_action_amount,
|
||||
recoveredAmount: initialSummary.recovered_amount,
|
||||
collectionEndAmount: initialSummary.collection_end_amount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,11 +145,11 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
const legalActionAmount = data
|
||||
.filter((d) => d.status === 'legalAction')
|
||||
.reduce((sum, d) => sum + d.debtAmount, 0);
|
||||
const recoveredAmount = data
|
||||
.filter((d) => d.status === 'recovered')
|
||||
const collectionEndAmount = data
|
||||
.filter((d) => d.status === 'collectionEnd')
|
||||
.reduce((sum, d) => sum + d.debtAmount, 0);
|
||||
|
||||
return { totalAmount, collectingAmount, legalActionAmount, recoveredAmount };
|
||||
return { totalAmount, collectingAmount, legalActionAmount, collectionEndAmount };
|
||||
}, [data, initialSummary]);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
@@ -335,7 +336,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
},
|
||||
{
|
||||
label: '회수완료',
|
||||
value: `${formatNumber(statsData.recoveredAmount)}원`,
|
||||
value: `${formatNumber(statsData.collectionEndAmount)}원`,
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-green-500',
|
||||
},
|
||||
@@ -390,6 +391,27 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
disabled={isPending}
|
||||
/>
|
||||
</TableCell>
|
||||
{/* 작업 */}
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => router.push(`/ko/accounting/bad-debt-collection/${item.id}?mode=edit`)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-red-500 hover:text-red-700"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
// ===== 악성채권 추심관리 타입 정의 =====
|
||||
|
||||
// 추심 상태
|
||||
export type CollectionStatus = 'collecting' | 'legalAction' | 'recovered' | 'badDebt';
|
||||
export type CollectionStatus = 'collecting' | 'legalAction' | 'collectionEnd';
|
||||
|
||||
// 추심종료 사유
|
||||
export type CollectionEndReason = 'recovered' | 'badDebt';
|
||||
|
||||
export const COLLECTION_END_REASON_OPTIONS: { value: CollectionEndReason; label: string }[] = [
|
||||
{ value: 'recovered', label: '회수완료' },
|
||||
{ value: 'badDebt', label: '대손처리' },
|
||||
];
|
||||
|
||||
// 정렬 옵션
|
||||
export type SortOption = 'latest' | 'oldest';
|
||||
@@ -70,6 +78,7 @@ export interface BadDebtRecord {
|
||||
debtAmount: number; // 총 미수금액
|
||||
badDebtCount: number; // 악성채권 건수
|
||||
status: CollectionStatus; // 대표 상태 (가장 최근)
|
||||
collectionEndReason?: CollectionEndReason; // 추심종료 사유 (status === 'collectionEnd'일 때)
|
||||
overdueDays: number; // 최대 연체일수
|
||||
overdueToggle: boolean;
|
||||
occurrenceDate: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { format, parseISO, subMonths, startOfMonth, endOfMonth } from 'date-fns';
|
||||
import { ko } from 'date-fns/locale';
|
||||
import { Download, FileText, Loader2, RefreshCw, Calendar } from 'lucide-react';
|
||||
import { Download, FileText, Loader2, Printer, Search } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
@@ -15,18 +15,27 @@ import {
|
||||
TableRow,
|
||||
TableFooter,
|
||||
} from '@/components/ui/table';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { DateRangePicker } from '@/components/ui/date-range-picker';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { NoteReceivableItem, DailyAccountItem } from './types';
|
||||
import { MATCH_STATUS_LABELS, MATCH_STATUS_COLORS } from './types';
|
||||
import { getNoteReceivables, getDailyAccounts, getDailyReportSummary } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
// ===== 빠른 월 선택 버튼 정의 =====
|
||||
const QUICK_MONTH_BUTTONS = [
|
||||
{ label: '이번달', months: 0 },
|
||||
{ label: '지난달', months: 1 },
|
||||
{ label: 'D-2월', months: 2 },
|
||||
{ label: 'D-3월', months: 3 },
|
||||
{ label: 'D-4월', months: 4 },
|
||||
{ label: 'D-5월', months: 5 },
|
||||
] as const;
|
||||
|
||||
// ===== Props 인터페이스 =====
|
||||
interface DailyReportProps {
|
||||
initialNoteReceivables?: NoteReceivableItem[];
|
||||
@@ -36,7 +45,9 @@ interface DailyReportProps {
|
||||
export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts = [] }: DailyReportProps) {
|
||||
const { canExport } = usePermission();
|
||||
// ===== 상태 관리 =====
|
||||
const [selectedDate, setSelectedDate] = useState(() => format(new Date(), 'yyyy-MM-dd'));
|
||||
const [startDate, setStartDate] = useState(() => format(startOfMonth(new Date()), 'yyyy-MM-dd'));
|
||||
const [endDate, setEndDate] = useState(() => format(new Date(), 'yyyy-MM-dd'));
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [noteReceivables, setNoteReceivables] = useState<NoteReceivableItem[]>(initialNoteReceivables);
|
||||
const [dailyAccounts, setDailyAccounts] = useState<DailyAccountItem[]>(initialDailyAccounts);
|
||||
const [summary, setSummary] = useState<{
|
||||
@@ -53,9 +64,9 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [noteResult, accountResult, summaryResult] = await Promise.all([
|
||||
getNoteReceivables({ date: selectedDate }),
|
||||
getDailyAccounts({ date: selectedDate }),
|
||||
getDailyReportSummary({ date: selectedDate }),
|
||||
getNoteReceivables({ date: startDate }),
|
||||
getDailyAccounts({ date: startDate }),
|
||||
getDailyReportSummary({ date: startDate }),
|
||||
]);
|
||||
|
||||
if (noteResult.success) {
|
||||
@@ -81,20 +92,20 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [selectedDate]);
|
||||
}, [startDate]);
|
||||
|
||||
// ===== 초기 로드 및 날짜 변경시 재로드 =====
|
||||
const isInitialMount = useRef(true);
|
||||
const prevDateRef = useRef(selectedDate);
|
||||
const prevDateRef = useRef(startDate);
|
||||
|
||||
useEffect(() => {
|
||||
// 초기 마운트 또는 날짜가 실제로 변경된 경우에만 로드
|
||||
if (isInitialMount.current || prevDateRef.current !== selectedDate) {
|
||||
if (isInitialMount.current || prevDateRef.current !== startDate) {
|
||||
isInitialMount.current = false;
|
||||
prevDateRef.current = selectedDate;
|
||||
prevDateRef.current = startDate;
|
||||
loadData();
|
||||
}
|
||||
}, [selectedDate, loadData]);
|
||||
}, [startDate, loadData]);
|
||||
|
||||
// ===== 어음 합계 (API 요약 사용) =====
|
||||
const noteReceivableTotal = useMemo(() => {
|
||||
@@ -144,9 +155,9 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
}, [accountTotals]);
|
||||
|
||||
// ===== 선택된 날짜 정보 =====
|
||||
const selectedDateInfo = useMemo(() => {
|
||||
const startDateInfo = useMemo(() => {
|
||||
try {
|
||||
const date = parseISO(selectedDate);
|
||||
const date = parseISO(startDate);
|
||||
return {
|
||||
formatted: format(date, 'yyyy년 M월 d일', { locale: ko }),
|
||||
dayOfWeek: format(date, 'EEEE', { locale: ko }),
|
||||
@@ -154,12 +165,12 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
} catch {
|
||||
return { formatted: '', dayOfWeek: '' };
|
||||
}
|
||||
}, [selectedDate]);
|
||||
}, [startDate]);
|
||||
|
||||
// ===== 엑셀 다운로드 (프록시 API 직접 호출) =====
|
||||
const handleExcelDownload = useCallback(async () => {
|
||||
try {
|
||||
const url = `/api/proxy/daily-report/export?date=${selectedDate}`;
|
||||
const url = `/api/proxy/daily-report/export?date=${startDate}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -169,7 +180,7 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
|
||||
const blob = await response.blob();
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
const filename = contentDisposition?.match(/filename="?(.+)"?/)?.[1] || `일일일보_${selectedDate}.xlsx`;
|
||||
const filename = contentDisposition?.match(/filename="?(.+)"?/)?.[1] || `일일일보_${startDate}.xlsx`;
|
||||
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -183,7 +194,36 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
} catch {
|
||||
toast.error('엑셀 다운로드 중 오류가 발생했습니다.');
|
||||
}
|
||||
}, [selectedDate]);
|
||||
}, [startDate]);
|
||||
|
||||
// ===== 빠른 월 선택 =====
|
||||
const handleQuickMonth = useCallback((monthsAgo: number) => {
|
||||
const target = monthsAgo === 0 ? new Date() : subMonths(new Date(), monthsAgo);
|
||||
setStartDate(format(startOfMonth(target), 'yyyy-MM-dd'));
|
||||
setEndDate(format(endOfMonth(target), 'yyyy-MM-dd'));
|
||||
}, []);
|
||||
|
||||
// ===== 인쇄 =====
|
||||
const handlePrint = useCallback(() => {
|
||||
window.print();
|
||||
}, []);
|
||||
|
||||
// ===== 검색 필터링 =====
|
||||
const filteredNoteReceivables = useMemo(() => {
|
||||
if (!searchTerm) return noteReceivables;
|
||||
const term = searchTerm.toLowerCase();
|
||||
return noteReceivables.filter(item =>
|
||||
item.content.toLowerCase().includes(term)
|
||||
);
|
||||
}, [noteReceivables, searchTerm]);
|
||||
|
||||
const filteredDailyAccounts = useMemo(() => {
|
||||
if (!searchTerm) return dailyAccounts;
|
||||
const term = searchTerm.toLowerCase();
|
||||
return dailyAccounts.filter(item =>
|
||||
item.category.toLowerCase().includes(term)
|
||||
);
|
||||
}, [dailyAccounts, searchTerm]);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
@@ -194,42 +234,57 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
icon={FileText}
|
||||
/>
|
||||
|
||||
{/* 헤더 액션 (날짜 선택, 버튼 등) */}
|
||||
{/* 헤더 액션 (날짜 선택, 빠른 월 선택, 검색, 인쇄) */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Calendar className="h-4 w-4 text-gray-500 shrink-0" />
|
||||
<span className="text-sm font-medium text-gray-700 shrink-0">조회 일자</span>
|
||||
<DatePicker
|
||||
value={selectedDate}
|
||||
onChange={setSelectedDate}
|
||||
className="w-auto min-w-[140px]"
|
||||
size="sm"
|
||||
align="start"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadData}
|
||||
disabled={isLoading}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
새로고침
|
||||
</Button>
|
||||
{canExport && (
|
||||
<Button variant="outline" size="sm" onClick={handleExcelDownload} className="h-8 px-2 text-xs">
|
||||
<Download className="mr-1 h-3.5 w-3.5" />
|
||||
엑셀
|
||||
<CardContent className="p-3 md:p-4">
|
||||
<div className="flex flex-col gap-2 md:gap-3">
|
||||
{/* DateRange */}
|
||||
<DateRangePicker
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
size="sm"
|
||||
className="w-full md:w-auto md:min-w-[280px]"
|
||||
displayFormat="yyyy-MM-dd"
|
||||
/>
|
||||
{/* 빠른 월 선택 버튼 - 모바일: 가로 스크롤 */}
|
||||
<div className="flex items-center gap-1.5 md:gap-2 overflow-x-auto pb-1 -mb-1">
|
||||
{QUICK_MONTH_BUTTONS.map((btn) => (
|
||||
<Button
|
||||
key={btn.label}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 md:h-8 px-2 md:px-2.5 text-xs shrink-0"
|
||||
onClick={() => handleQuickMonth(btn.months)}
|
||||
>
|
||||
{btn.label}
|
||||
</Button>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
{/* 검색 + 인쇄/엑셀 - 모바일: 세로 배치 */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:justify-between">
|
||||
<div className="relative flex-1 sm:max-w-[300px]">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="검색..."
|
||||
className="pl-8 h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handlePrint} className="h-7 md:h-8 px-2 md:px-3 text-xs">
|
||||
<Printer className="mr-1 h-3.5 w-3.5" />
|
||||
인쇄
|
||||
</Button>
|
||||
{canExport && (
|
||||
<Button variant="outline" size="sm" onClick={handleExcelDownload} className="h-7 md:h-8 px-2 md:px-3 text-xs">
|
||||
<Download className="mr-1 h-3.5 w-3.5" />
|
||||
엑셀
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -237,19 +292,19 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
|
||||
{/* 어음 및 외상매출채권현황 */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">어음 및 외상매출채권현황</h3>
|
||||
<CardContent className="px-3 pt-4 pb-3 md:px-6 md:pt-6 md:pb-6">
|
||||
<div className="flex items-center justify-between mb-3 md:mb-4">
|
||||
<h3 className="text-base md:text-lg font-semibold">어음 및 외상매출채권현황</h3>
|
||||
</div>
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<div className="min-w-[550px]">
|
||||
<div className="rounded-md border overflow-x-auto max-h-[40vh] md:max-h-[50vh] overflow-y-auto">
|
||||
<div className="min-w-[480px] md:min-w-[550px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableHeader className="sticky top-0 z-10 bg-background">
|
||||
<TableRow>
|
||||
<TableHead className="font-semibold max-w-[200px]">내용</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap">현재 잔액</TableHead>
|
||||
<TableHead className="font-semibold text-center whitespace-nowrap">발행일</TableHead>
|
||||
<TableHead className="font-semibold text-center whitespace-nowrap">만기일</TableHead>
|
||||
<TableHead className="font-semibold text-xs md:text-sm">내용</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap text-xs md:text-sm">현재 잔액</TableHead>
|
||||
<TableHead className="font-semibold text-center whitespace-nowrap text-xs md:text-sm">발행일</TableHead>
|
||||
<TableHead className="font-semibold text-center whitespace-nowrap text-xs md:text-sm">만기일</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -258,32 +313,32 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
<TableCell colSpan={4} className="text-center py-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
|
||||
<span className="text-gray-500">데이터를 불러오는 중...</span>
|
||||
<span className="text-gray-500 text-sm">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : noteReceivables.length === 0 ? (
|
||||
) : filteredNoteReceivables.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">
|
||||
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">
|
||||
데이터가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
noteReceivables.map((item) => (
|
||||
filteredNoteReceivables.map((item) => (
|
||||
<TableRow key={item.id} className="hover:bg-muted/50">
|
||||
<TableCell className="max-w-[200px] truncate">{item.content}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">{formatAmount(item.currentBalance)}</TableCell>
|
||||
<TableCell className="text-center whitespace-nowrap">{item.issueDate}</TableCell>
|
||||
<TableCell className="text-center whitespace-nowrap">{item.dueDate}</TableCell>
|
||||
<TableCell className="max-w-[160px] md:max-w-[200px] truncate text-xs md:text-sm">{item.content}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap text-xs md:text-sm">{formatAmount(item.currentBalance)}</TableCell>
|
||||
<TableCell className="text-center whitespace-nowrap text-xs md:text-sm">{item.issueDate}</TableCell>
|
||||
<TableCell className="text-center whitespace-nowrap text-xs md:text-sm">{item.dueDate}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
{noteReceivables.length > 0 && (
|
||||
<TableFooter>
|
||||
{filteredNoteReceivables.length > 0 && (
|
||||
<TableFooter className="sticky bottom-0 z-10 bg-background">
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
<TableCell className="font-bold">합계</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap">{formatAmount(noteReceivableTotal)}</TableCell>
|
||||
<TableCell className="font-bold text-xs md:text-sm">합계</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap text-xs md:text-sm">{formatAmount(noteReceivableTotal)}</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
@@ -297,82 +352,63 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
|
||||
{/* 일자별 상세 */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">
|
||||
일자: {selectedDateInfo.formatted} {selectedDateInfo.dayOfWeek}
|
||||
<CardContent className="px-3 pt-4 pb-3 md:px-6 md:pt-6 md:pb-6">
|
||||
<div className="flex items-center justify-between mb-3 md:mb-4">
|
||||
<h3 className="text-base md:text-lg font-semibold">
|
||||
일자: {startDateInfo.formatted} {startDateInfo.dayOfWeek}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<div className="min-w-[650px]">
|
||||
<div className="min-w-[420px] md:min-w-[650px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="font-semibold max-w-[180px]">구분</TableHead>
|
||||
<TableHead className="font-semibold text-center whitespace-nowrap">상태</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap">전월 이월</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap">수입</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap">지출</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap">잔액</TableHead>
|
||||
<TableHead className="font-semibold text-xs md:text-sm">구분</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap text-xs md:text-sm">입금</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap text-xs md:text-sm">출금</TableHead>
|
||||
<TableHead className="font-semibold text-right whitespace-nowrap text-xs md:text-sm">잔액</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8">
|
||||
<TableCell colSpan={4} className="text-center py-8">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
|
||||
<span className="text-gray-500">데이터를 불러오는 중...</span>
|
||||
<span className="text-gray-500 text-sm">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : dailyAccounts.length === 0 ? (
|
||||
) : filteredDailyAccounts.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">
|
||||
데이터가 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<>
|
||||
{/* KRW 계좌들 */}
|
||||
{dailyAccounts
|
||||
{filteredDailyAccounts
|
||||
.filter(item => item.currency === 'KRW')
|
||||
.map((item) => (
|
||||
<TableRow key={item.id} className="hover:bg-muted/50">
|
||||
<TableCell className="max-w-[180px] truncate">{item.category}</TableCell>
|
||||
<TableCell className="text-center whitespace-nowrap">
|
||||
<Badge className={MATCH_STATUS_COLORS[item.matchStatus]}>
|
||||
{MATCH_STATUS_LABELS[item.matchStatus]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">{formatAmount(item.carryover)}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">{formatAmount(item.income)}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">{formatAmount(item.expense)}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">{formatAmount(item.balance)}</TableCell>
|
||||
<TableCell className="max-w-[140px] md:max-w-[180px] truncate text-xs md:text-sm">{item.category}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap text-xs md:text-sm">{formatAmount(item.income)}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap text-xs md:text-sm">{formatAmount(item.expense)}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap text-xs md:text-sm">{formatAmount(item.balance)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
{dailyAccounts.length > 0 && (
|
||||
{filteredDailyAccounts.length > 0 && (
|
||||
<TableFooter>
|
||||
{/* 외화원 (USD) 합계 */}
|
||||
<TableRow className="bg-blue-50/50">
|
||||
<TableCell className="font-semibold whitespace-nowrap">외화원 (USD) 합계</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">${formatAmount(accountTotals.usd.carryover)}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">${formatAmount(accountTotals.usd.income)}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">${formatAmount(accountTotals.usd.expense)}</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap">${formatAmount(accountTotals.usd.balance)}</TableCell>
|
||||
</TableRow>
|
||||
{/* 현금성 자산 합계 */}
|
||||
{/* 합계 */}
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
<TableCell className="font-bold whitespace-nowrap">현금성 자산 합계</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap">{formatAmount(cashAssetTotal.carryover)}</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap">{formatAmount(cashAssetTotal.income)}</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap">{formatAmount(cashAssetTotal.expense)}</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap">{formatAmount(cashAssetTotal.balance)}</TableCell>
|
||||
<TableCell className="font-bold whitespace-nowrap text-xs md:text-sm">합계</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap text-xs md:text-sm">{formatAmount(cashAssetTotal.income)}</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap text-xs md:text-sm">{formatAmount(cashAssetTotal.expense)}</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap text-xs md:text-sm">{formatAmount(cashAssetTotal.balance)}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
)}
|
||||
@@ -381,6 +417,114 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 예금 입출금 내역 */}
|
||||
<Card>
|
||||
<CardContent className="px-3 pt-4 pb-3 md:px-6 md:pt-6 md:pb-6">
|
||||
<div className="flex items-center justify-center mb-3 md:mb-4 py-1.5 md:py-2 bg-gray-100 rounded-md">
|
||||
<h3 className="text-base md:text-lg font-semibold">예금 입출금 내역</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
|
||||
{/* 입금 */}
|
||||
<div>
|
||||
<div className="text-center py-1 md:py-1.5 bg-blue-50 rounded-t-md border border-b-0">
|
||||
<span className="font-semibold text-blue-700 text-sm md:text-base">입금</span>
|
||||
</div>
|
||||
<div className="rounded-b-md border overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="font-semibold text-xs md:text-sm">입금처/적요</TableHead>
|
||||
<TableHead className="font-semibold text-right text-xs md:text-sm">금액</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="text-center py-4 md:py-6">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-gray-400 mx-auto" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.income > 0).length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="text-center py-4 md:py-6 text-muted-foreground text-xs md:text-sm">
|
||||
입금 내역이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredDailyAccounts
|
||||
.filter(item => item.currency === 'KRW' && item.income > 0)
|
||||
.map((item) => (
|
||||
<TableRow key={`deposit-${item.id}`} className="hover:bg-muted/50">
|
||||
<TableCell>
|
||||
<div className="text-xs md:text-sm">{item.category}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap text-xs md:text-sm">{formatAmount(item.income)}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow className="bg-blue-50/50">
|
||||
<TableCell className="font-bold text-xs md:text-sm">입금 합계</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap text-xs md:text-sm">{formatAmount(cashAssetTotal.income)}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 출금 */}
|
||||
<div>
|
||||
<div className="text-center py-1 md:py-1.5 bg-red-50 rounded-t-md border border-b-0">
|
||||
<span className="font-semibold text-red-700 text-sm md:text-base">출금</span>
|
||||
</div>
|
||||
<div className="rounded-b-md border overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="font-semibold text-xs md:text-sm">출금처/적요</TableHead>
|
||||
<TableHead className="font-semibold text-right text-xs md:text-sm">금액</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="text-center py-4 md:py-6">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-gray-400 mx-auto" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredDailyAccounts.filter(item => item.currency === 'KRW' && item.expense > 0).length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="text-center py-4 md:py-6 text-muted-foreground text-xs md:text-sm">
|
||||
출금 내역이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredDailyAccounts
|
||||
.filter(item => item.currency === 'KRW' && item.expense > 0)
|
||||
.map((item) => (
|
||||
<TableRow key={`withdrawal-${item.id}`} className="hover:bg-muted/50">
|
||||
<TableCell>
|
||||
<div className="text-xs md:text-sm">{item.category}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right whitespace-nowrap text-xs md:text-sm">{formatAmount(item.expense)}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow className="bg-red-50/50">
|
||||
<TableCell className="font-bold text-xs md:text-sm">출금 합계</TableCell>
|
||||
<TableCell className="text-right font-bold whitespace-nowrap text-xs md:text-sm">{formatAmount(cashAssetTotal.expense)}</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ import { purchaseConfig } from './purchaseConfig';
|
||||
import { DocumentDetailModalV2 as DocumentDetailModal } from '@/components/approval/DocumentDetail';
|
||||
import type { ProposalDocumentData, ExpenseReportDocumentData, ExpenseEstimateDocumentData } from '@/components/approval/DocumentDetail/types';
|
||||
import { getApprovalById } from '@/components/approval/DocumentCreate/actions';
|
||||
import type { PurchaseRecord, PurchaseItem, PurchaseType } from './types';
|
||||
import { PURCHASE_TYPE_LABELS } from './types';
|
||||
import type { PurchaseRecord, PurchaseItem } from './types';
|
||||
import {
|
||||
getPurchaseById,
|
||||
createPurchase,
|
||||
@@ -74,7 +73,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
const [purchaseDate, setPurchaseDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
||||
const [vendorId, setVendorId] = useState('');
|
||||
const [vendorName, setVendorName] = useState('');
|
||||
const [purchaseType, setPurchaseType] = useState<PurchaseType>('unset');
|
||||
// purchaseType 삭제됨 (기획서 P.109)
|
||||
const [items, setItems] = useState<PurchaseItem[]>([createEmptyItem()]);
|
||||
const [taxInvoiceReceived, setTaxInvoiceReceived] = useState(false);
|
||||
|
||||
@@ -126,7 +125,6 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
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);
|
||||
@@ -250,7 +248,6 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
supplyAmount: totals.supplyAmount,
|
||||
vat: totals.vat,
|
||||
totalAmount: totals.total,
|
||||
purchaseType,
|
||||
taxInvoiceReceived,
|
||||
};
|
||||
|
||||
@@ -275,7 +272,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [purchaseDate, vendorId, totals, purchaseType, taxInvoiceReceived, isNewMode, purchaseId]);
|
||||
}, [purchaseDate, vendorId, totals, taxInvoiceReceived, isNewMode, purchaseId]);
|
||||
|
||||
// ===== 삭제 (IntegratedDetailTemplate 호환) =====
|
||||
const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
@@ -301,179 +298,101 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) {
|
||||
const renderFormContent = () => (
|
||||
<>
|
||||
<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 flex-col sm:flex-row sm:items-center gap-3 p-3 bg-orange-50 border border-orange-200 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className={getPresetStyle('orange')}>
|
||||
{sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">연결된 문서가 있습니다</span>
|
||||
</div>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* 품의서/지출결의서 */}
|
||||
<div className="space-y-2">
|
||||
<Label>품의서</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={sourceDocument ? `${sourceDocument.documentNo} ${sourceDocument.title}` : ''}
|
||||
readOnly
|
||||
disabled
|
||||
placeholder="연결된 품의서 없음"
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="sm:ml-auto border-orange-300 text-orange-700 hover:bg-orange-100 w-full sm:w-auto"
|
||||
className="shrink-0"
|
||||
onClick={handleOpenDocument}
|
||||
disabled={!sourceDocument}
|
||||
>
|
||||
<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 md: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={`${formatAmount(sourceDocument.expectedCost)}원`}
|
||||
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">
|
||||
<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>
|
||||
<DatePicker
|
||||
value={purchaseDate}
|
||||
onChange={setPurchaseDate}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 예상비용 */}
|
||||
<div className="space-y-2">
|
||||
<Label>예상비용</Label>
|
||||
<Input
|
||||
value={sourceDocument ? `${formatAmount(sourceDocument.expectedCost)}원` : ''}
|
||||
readOnly
|
||||
disabled
|
||||
placeholder="-"
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ===== 매입 정보 섹션 ===== */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">매입 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 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>
|
||||
<DatePicker
|
||||
value={purchaseDate}
|
||||
onChange={setPurchaseDate}
|
||||
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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -26,8 +26,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { getPresetStyle } from '@/lib/utils/status-config';
|
||||
// Badge, getPresetStyle removed (매입유형/연결문서 컬럼 삭제)
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -57,9 +56,7 @@ import { MobileCard } from '@/components/organisms/MobileCard';
|
||||
import type { PurchaseRecord } from './types';
|
||||
import {
|
||||
SORT_OPTIONS,
|
||||
PURCHASE_TYPE_LABELS,
|
||||
PURCHASE_TYPE_FILTER_OPTIONS,
|
||||
ISSUANCE_FILTER_OPTIONS,
|
||||
TAX_INVOICE_RECEIVED_FILTER_OPTIONS,
|
||||
ACCOUNT_SUBJECT_SELECTOR_OPTIONS,
|
||||
} from './types';
|
||||
import { getPurchases, togglePurchaseTaxInvoice, deletePurchase } from './actions';
|
||||
@@ -71,11 +68,9 @@ const tableColumns = [
|
||||
{ key: 'purchaseNo', label: '매입번호', sortable: true },
|
||||
{ key: 'purchaseDate', label: '매입일', sortable: true },
|
||||
{ key: 'vendorName', label: '거래처', sortable: true },
|
||||
{ key: 'sourceDocument', label: '연결문서', className: 'text-center', sortable: true },
|
||||
{ key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true },
|
||||
{ key: 'vat', label: '부가세', className: 'text-right', sortable: true },
|
||||
{ key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true },
|
||||
{ key: 'purchaseType', label: '매입유형', className: 'text-center', sortable: true },
|
||||
{ key: 'taxInvoice', label: '세금계산서 수취 확인', className: 'text-center' },
|
||||
];
|
||||
|
||||
@@ -92,8 +87,7 @@ export function PurchaseManagement() {
|
||||
// 통합 필터 상태 (filterConfig 기반)
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string | string[]>>({
|
||||
vendor: 'all',
|
||||
purchaseType: 'all',
|
||||
issuance: 'all',
|
||||
taxInvoiceReceived: 'all',
|
||||
sort: 'latest',
|
||||
});
|
||||
|
||||
@@ -142,9 +136,8 @@ export function PurchaseManagement() {
|
||||
return date.getMonth() === currentMonth && date.getFullYear() === currentYear;
|
||||
})
|
||||
.reduce((sum, d) => sum + d.totalAmount, 0);
|
||||
const unsetTypeCount = purchaseData.filter(d => d.purchaseType === 'unset').length;
|
||||
const taxInvoicePendingCount = purchaseData.filter(d => !d.taxInvoiceReceived).length;
|
||||
return { totalPurchaseAmount, monthlyAmount, unsetTypeCount, taxInvoicePendingCount };
|
||||
return { totalPurchaseAmount, monthlyAmount, taxInvoicePendingCount };
|
||||
}, [purchaseData]);
|
||||
|
||||
// ===== 거래처 목록 (필터용) =====
|
||||
@@ -163,17 +156,10 @@ export function PurchaseManagement() {
|
||||
allOptionLabel: '거래처 전체',
|
||||
},
|
||||
{
|
||||
key: 'purchaseType',
|
||||
label: '매입유형',
|
||||
key: 'taxInvoiceReceived',
|
||||
label: '세금계산서 수취여부',
|
||||
type: 'single',
|
||||
options: PURCHASE_TYPE_FILTER_OPTIONS.filter(o => o.value !== 'all'),
|
||||
allOptionLabel: '전체',
|
||||
},
|
||||
{
|
||||
key: 'issuance',
|
||||
label: '발행여부',
|
||||
type: 'single',
|
||||
options: ISSUANCE_FILTER_OPTIONS.filter(o => o.value !== 'all'),
|
||||
options: TAX_INVOICE_RECEIVED_FILTER_OPTIONS.filter(o => o.value !== 'all'),
|
||||
allOptionLabel: '전체',
|
||||
},
|
||||
{
|
||||
@@ -194,8 +180,7 @@ export function PurchaseManagement() {
|
||||
const handleFilterReset = useCallback(() => {
|
||||
setFilterValues({
|
||||
vendor: 'all',
|
||||
purchaseType: 'all',
|
||||
issuance: 'all',
|
||||
taxInvoiceReceived: 'all',
|
||||
sort: 'latest',
|
||||
});
|
||||
}, []);
|
||||
@@ -309,18 +294,16 @@ export function PurchaseManagement() {
|
||||
}
|
||||
|
||||
const vendorVal = fv.vendor as string;
|
||||
const purchaseTypeVal = fv.purchaseType as string;
|
||||
const issuanceVal = fv.issuance as string;
|
||||
const taxInvoiceReceivedVal = fv.taxInvoiceReceived as string;
|
||||
// 거래처 필터
|
||||
if (vendorVal !== 'all' && item.vendorName !== vendorVal) {
|
||||
return false;
|
||||
}
|
||||
// 매입유형 필터
|
||||
if (purchaseTypeVal !== 'all' && item.purchaseType !== purchaseTypeVal) {
|
||||
// 세금계산서 수취여부 필터
|
||||
if (taxInvoiceReceivedVal === 'received' && !item.taxInvoiceReceived) {
|
||||
return false;
|
||||
}
|
||||
// 발행여부 필터
|
||||
if (issuanceVal === 'taxInvoicePending' && item.taxInvoiceReceived) {
|
||||
if (taxInvoiceReceivedVal === 'notReceived' && item.taxInvoiceReceived) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -393,9 +376,8 @@ export function PurchaseManagement() {
|
||||
|
||||
// Stats 카드
|
||||
computeStats: (): StatCard[] => [
|
||||
{ label: '총 매입', value: `${formatNumber(stats.totalPurchaseAmount)}원`, icon: Receipt, iconColor: 'text-blue-500' },
|
||||
{ label: '총매입', value: `${formatNumber(stats.totalPurchaseAmount)}원`, icon: Receipt, iconColor: 'text-blue-500' },
|
||||
{ label: '당월 매입', value: `${formatNumber(stats.monthlyAmount)}원`, icon: Receipt, iconColor: 'text-green-500' },
|
||||
{ label: '매입유형 미설정', value: `${stats.unsetTypeCount}건`, icon: Receipt, iconColor: 'text-orange-500' },
|
||||
{ label: '세금계산서 수취 미확인', value: `${stats.taxInvoicePendingCount}건`, icon: Receipt, iconColor: 'text-red-500' },
|
||||
],
|
||||
|
||||
@@ -406,13 +388,10 @@ export function PurchaseManagement() {
|
||||
<TableCell className="text-center"></TableCell>
|
||||
<TableCell className="font-bold">합계</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalSupplyAmount)}</TableCell>
|
||||
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalVat)}</TableCell>
|
||||
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalAmount)}</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -428,9 +407,7 @@ export function PurchaseManagement() {
|
||||
index: number,
|
||||
globalIndex: number,
|
||||
handlers: SelectionHandlers & RowClickHandlers<PurchaseRecord>
|
||||
) => {
|
||||
const isUnsetType = item.purchaseType === 'unset';
|
||||
return (
|
||||
) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="hover:bg-muted/50 cursor-pointer"
|
||||
@@ -443,26 +420,9 @@ export function PurchaseManagement() {
|
||||
<TableCell className="text-sm font-medium">{item.purchaseNo}</TableCell>
|
||||
<TableCell>{item.purchaseDate}</TableCell>
|
||||
<TableCell>{item.vendorName}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{item.sourceDocument ? (
|
||||
<Badge variant="outline" className={`text-xs ${getPresetStyle('info')}`}>
|
||||
{item.sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-gray-400 text-xs">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.supplyAmount)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.vat)}</TableCell>
|
||||
<TableCell className="text-right font-medium">{formatNumber(item.totalAmount)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={isUnsetType ? 'border-red-500 text-red-500 bg-red-50' : ''}
|
||||
>
|
||||
{PURCHASE_TYPE_LABELS[item.purchaseType]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Switch
|
||||
@@ -474,8 +434,7 @@ export function PurchaseManagement() {
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 모바일 카드 렌더링
|
||||
renderMobileCard: (
|
||||
@@ -488,14 +447,11 @@ export function PurchaseManagement() {
|
||||
key={item.id}
|
||||
title={item.vendorName}
|
||||
subtitle={item.purchaseNo}
|
||||
badge={PURCHASE_TYPE_LABELS[item.purchaseType]}
|
||||
badgeVariant="outline"
|
||||
isSelected={handlers.isSelected}
|
||||
onToggle={handlers.onToggle}
|
||||
onClick={() => handleRowClick(item)}
|
||||
details={[
|
||||
{ label: '매입일', value: item.purchaseDate },
|
||||
{ label: '연결문서', value: item.sourceDocument ? (item.sourceDocument.type === 'proposal' ? '품의서' : '지출결의서') : '-' },
|
||||
{ label: '공급가액', value: `${formatNumber(item.supplyAmount)}원` },
|
||||
{ label: '합계금액', value: `${formatNumber(item.totalAmount)}원` },
|
||||
]}
|
||||
|
||||
@@ -81,8 +81,8 @@ export interface PurchaseRecord {
|
||||
// 정렬 옵션
|
||||
export type SortOption = 'latest' | 'oldest' | 'amountHigh' | 'amountLow';
|
||||
|
||||
// 발행여부 필터
|
||||
export type IssuanceFilter = 'all' | 'taxInvoicePending';
|
||||
// 세금계산서 수취여부 필터
|
||||
export type TaxInvoiceReceivedFilter = 'all' | 'received' | 'notReceived';
|
||||
|
||||
// ===== 상수 정의 =====
|
||||
|
||||
@@ -154,10 +154,11 @@ export const PURCHASE_TYPE_FILTER_OPTIONS: { value: string; label: string }[] =
|
||||
{ value: 'unset', label: '미설정' },
|
||||
];
|
||||
|
||||
// 발행여부 필터 옵션
|
||||
export const ISSUANCE_FILTER_OPTIONS: { value: IssuanceFilter; label: string }[] = [
|
||||
// 세금계산서 수취여부 필터 옵션
|
||||
export const TAX_INVOICE_RECEIVED_FILTER_OPTIONS: { value: TaxInvoiceReceivedFilter; label: string }[] = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: 'taxInvoicePending', label: '세금계산서 미수취' },
|
||||
{ value: 'received', label: '수취 확인' },
|
||||
{ value: 'notReceived', label: '수취 미확인' },
|
||||
];
|
||||
|
||||
// 계정과목명 셀렉터 옵션 (상단 일괄 변경용)
|
||||
|
||||
@@ -32,8 +32,7 @@ import {
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { LineItemsTable, useLineItems } from '@/components/organisms/LineItemsTable';
|
||||
import { salesConfig } from './salesConfig';
|
||||
import type { SalesRecord, SalesItem, SalesType } from './types';
|
||||
import { SALES_TYPE_OPTIONS } from './types';
|
||||
import type { SalesRecord, SalesItem } from './types';
|
||||
import { getSaleById, createSale, updateSale, deleteSale } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
import { getClients } from '../VendorManagement/actions';
|
||||
@@ -78,7 +77,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
const [salesDate, setSalesDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
||||
const [vendorId, setVendorId] = useState('');
|
||||
const [vendorName, setVendorName] = useState('');
|
||||
const [salesType, setSalesType] = useState<SalesType>('product');
|
||||
const [items, setItems] = useState<SalesItem[]>([createEmptyItem()]);
|
||||
const [taxInvoiceIssued, setTaxInvoiceIssued] = useState(false);
|
||||
const [transactionStatementIssued, setTransactionStatementIssued] = useState(false);
|
||||
@@ -126,7 +124,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
setSalesDate(data.salesDate);
|
||||
setVendorId(data.vendorId);
|
||||
setVendorName(data.vendorName);
|
||||
setSalesType(data.salesType);
|
||||
setItems(data.items.length > 0 ? data.items : [createEmptyItem()]);
|
||||
setTaxInvoiceIssued(data.taxInvoiceIssued);
|
||||
setTransactionStatementIssued(data.transactionStatementIssued);
|
||||
@@ -158,7 +155,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
const saleData: Partial<SalesRecord> = {
|
||||
salesDate,
|
||||
vendorId,
|
||||
salesType,
|
||||
items,
|
||||
totalSupplyAmount: totals.supplyAmount,
|
||||
totalVat: totals.vat,
|
||||
@@ -189,7 +185,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [salesDate, vendorId, salesType, items, totals, taxInvoiceIssued, transactionStatementIssued, note, isNewMode, salesId]);
|
||||
}, [salesDate, vendorId, items, totals, taxInvoiceIssued, transactionStatementIssued, note, isNewMode, salesId]);
|
||||
|
||||
// ===== 삭제 (IntegratedDetailTemplate 호환) =====
|
||||
const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
@@ -268,23 +264,6 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 매출 유형 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="salesType">매출 유형</Label>
|
||||
<Select value={salesType} onValueChange={(v) => setSalesType(v as SalesType)} disabled={isViewMode}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SALES_TYPE_OPTIONS.filter(o => o.value !== 'all').map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -318,28 +297,42 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
||||
<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={taxInvoiceIssued}
|
||||
onCheckedChange={setTaxInvoiceIssued}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="taxInvoice">세금계산서 발행</Label>
|
||||
<Switch
|
||||
id="taxInvoice"
|
||||
checked={taxInvoiceIssued}
|
||||
onCheckedChange={setTaxInvoiceIssued}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{taxInvoiceIssued ? (
|
||||
<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>
|
||||
<div className="flex items-center gap-2">
|
||||
{taxInvoiceIssued ? (
|
||||
<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 className="flex justify-end">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-gray-900 hover:bg-gray-800 text-white"
|
||||
onClick={() => {
|
||||
toast.info('세금계산서 발행 기능 준비 중입니다.');
|
||||
}}
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
세금계산서 발행하기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -55,11 +54,8 @@ import { MobileCard } from '@/components/organisms/MobileCard';
|
||||
import type { SalesRecord } from './types';
|
||||
import {
|
||||
SORT_OPTIONS,
|
||||
SALES_STATUS_LABELS,
|
||||
SALES_STATUS_COLORS,
|
||||
SALES_TYPE_LABELS,
|
||||
SALES_TYPE_FILTER_OPTIONS,
|
||||
ISSUANCE_FILTER_OPTIONS,
|
||||
TAX_INVOICE_FILTER_OPTIONS,
|
||||
TRANSACTION_STATEMENT_FILTER_OPTIONS,
|
||||
ACCOUNT_SUBJECT_SELECTOR_OPTIONS,
|
||||
} from './types';
|
||||
import { getSales, deleteSale, toggleSaleIssuance } from './actions';
|
||||
@@ -83,7 +79,6 @@ const tableColumns = [
|
||||
{ key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true },
|
||||
{ key: 'vat', label: '부가세', className: 'text-right', sortable: true },
|
||||
{ key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true },
|
||||
{ key: 'salesType', label: '매출유형', className: 'text-center', sortable: true },
|
||||
{ key: 'taxInvoice', label: '세금계산서 발행완료', className: 'text-center' },
|
||||
{ key: 'transactionStatement', label: '거래명세서 발행완료', className: 'text-center' },
|
||||
];
|
||||
@@ -113,8 +108,8 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
// 필터 초기값 (filterConfig 기반 - ULP가 내부 state로 관리)
|
||||
const initialFilterValues: Record<string, string | string[]> = {
|
||||
vendor: 'all',
|
||||
salesType: 'all',
|
||||
issuance: 'all',
|
||||
taxInvoice: 'all',
|
||||
transactionStatement: 'all',
|
||||
sort: 'latest',
|
||||
};
|
||||
|
||||
@@ -148,17 +143,17 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
allOptionLabel: '거래처 전체',
|
||||
},
|
||||
{
|
||||
key: 'salesType',
|
||||
label: '매출유형',
|
||||
key: 'taxInvoice',
|
||||
label: '세금계산서 발행여부',
|
||||
type: 'single',
|
||||
options: SALES_TYPE_FILTER_OPTIONS.filter(o => o.value !== 'all'),
|
||||
options: TAX_INVOICE_FILTER_OPTIONS.filter(o => o.value !== 'all'),
|
||||
allOptionLabel: '전체',
|
||||
},
|
||||
{
|
||||
key: 'issuance',
|
||||
label: '발행여부',
|
||||
key: 'transactionStatement',
|
||||
label: '거래명세서 발행여부',
|
||||
type: 'single',
|
||||
options: ISSUANCE_FILTER_OPTIONS.filter(o => o.value !== 'all'),
|
||||
options: TRANSACTION_STATEMENT_FILTER_OPTIONS.filter(o => o.value !== 'all'),
|
||||
allOptionLabel: '전체',
|
||||
},
|
||||
{
|
||||
@@ -322,18 +317,24 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
// 검색은 searchFilter에서 처리하므로 여기서는 필터만 처리
|
||||
customFilterFn: (items, fv) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
const issuanceVal = fv.issuance as string;
|
||||
const taxInvoiceVal = fv.taxInvoice as string;
|
||||
const transactionStatementVal = fv.transactionStatement as string;
|
||||
|
||||
let result = applyFilters(items, [
|
||||
enumFilter('vendorName', fv.vendor as string),
|
||||
enumFilter('salesType', fv.salesType as string),
|
||||
]);
|
||||
|
||||
// 발행여부 필터 (특수 로직 - enumFilter로 대체 불가)
|
||||
if (issuanceVal === 'taxInvoicePending') {
|
||||
// 세금계산서 발행여부 필터
|
||||
if (taxInvoiceVal === 'issued') {
|
||||
result = result.filter(item => item.taxInvoiceIssued);
|
||||
} else if (taxInvoiceVal === 'notIssued') {
|
||||
result = result.filter(item => !item.taxInvoiceIssued);
|
||||
}
|
||||
if (issuanceVal === 'transactionStatementPending') {
|
||||
|
||||
// 거래명세서 발행여부 필터
|
||||
if (transactionStatementVal === 'issued') {
|
||||
result = result.filter(item => item.transactionStatementIssued);
|
||||
} else if (transactionStatementVal === 'notIssued') {
|
||||
result = result.filter(item => !item.transactionStatementIssued);
|
||||
}
|
||||
|
||||
@@ -411,7 +412,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
<TableCell className="text-right font-bold">{formatNumber(tableTotals.totalAmount)}</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -443,9 +443,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
<TableCell className="text-right">{formatNumber(item.totalSupplyAmount)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(item.totalVat)}</TableCell>
|
||||
<TableCell className="text-right font-medium">{formatNumber(item.totalAmount)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant="outline">{SALES_TYPE_LABELS[item.salesType]}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Switch
|
||||
@@ -480,8 +477,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
key={item.id}
|
||||
title={item.vendorName}
|
||||
subtitle={item.salesNo}
|
||||
badge={SALES_TYPE_LABELS[item.salesType]}
|
||||
badgeVariant="outline"
|
||||
isSelected={handlers.isSelected}
|
||||
onToggle={handlers.onToggle}
|
||||
onClick={() => handleRowClick(item)}
|
||||
|
||||
@@ -133,13 +133,22 @@ export const ACCOUNT_SUBJECT_SELECTOR_OPTIONS = [
|
||||
{ value: 'other', label: '기타매출' },
|
||||
];
|
||||
|
||||
// ===== 발행여부 필터 =====
|
||||
export type IssuanceFilter = 'all' | 'taxInvoicePending' | 'transactionStatementPending';
|
||||
// ===== 세금계산서 발행여부 필터 =====
|
||||
export type TaxInvoiceFilter = 'all' | 'issued' | 'notIssued';
|
||||
|
||||
export const ISSUANCE_FILTER_OPTIONS: { value: IssuanceFilter; label: string }[] = [
|
||||
export const TAX_INVOICE_FILTER_OPTIONS: { value: TaxInvoiceFilter; label: string }[] = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: 'taxInvoicePending', label: '세금계산서 미발행' },
|
||||
{ value: 'transactionStatementPending', label: '거래명세서 미발행' },
|
||||
{ value: 'issued', label: '발행완료' },
|
||||
{ value: 'notIssued', label: '미발행' },
|
||||
];
|
||||
|
||||
// ===== 거래명세서 발행여부 필터 =====
|
||||
export type TransactionStatementFilter = 'all' | 'issued' | 'notIssued';
|
||||
|
||||
export const TRANSACTION_STATEMENT_FILTER_OPTIONS: { value: TransactionStatementFilter; label: string }[] = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: 'issued', label: '발행완료' },
|
||||
{ value: 'notIssued', label: '미발행' },
|
||||
];
|
||||
|
||||
// ===== 매출유형 필터 옵션 (스크린샷 기준) =====
|
||||
|
||||
Reference in New Issue
Block a user