feat: CEO 대시보드 리팩토링 및 회계 관리 개선
- CEO 대시보드: 컴포넌트 분리(DashboardSettingsSections, DetailModalSections), 모달/섹션 개선, useCEODashboard 최적화 - 회계: 부실채권/매출/매입/일일보고 UI 및 타입 개선 - 공통: Sidebar, useDashboardFetch 훅 추가, amount/status-config 유틸 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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