- MobileCard 접기/펼치기(collapsible) 기능 추가 및 반응형 레이아웃 개선 - DatePicker 공휴일/세무일정 색상 코딩 통합, DateTimePicker 신규 추가 - useCalendarScheduleInit 훅으로 전역 공휴일/일정 데이터 캐싱 - 전 도메인 날짜 필드 DatePicker 표준화 - 생산대시보드/작업지시/견적서/주문관리 모바일 호환성 강화 - 회계 모듈 기능 개선 (매입상세 결재연동, 미수금현황 조회조건 등) - 달력 일정 관리 API 연동 및 대량 등록 다이얼로그 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
559 lines
20 KiB
TypeScript
559 lines
20 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 세금계산서 관리 - 메인 리스트 페이지
|
|
*
|
|
* - 매출/매입 탭 (카운트 표시)
|
|
* - 일자유형 Select + 날짜범위 + 분기버튼 + 거래처 검색
|
|
* - 요약 카드 (매출/매입 공급가액/세액/합계)
|
|
* - 테이블 + 범례 + 기간 요약
|
|
* - 분개 버튼 → JournalEntryModal
|
|
* - 수기 입력 버튼 → ManualEntryModal
|
|
*/
|
|
|
|
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
|
import dynamic from 'next/dynamic';
|
|
import { toast } from 'sonner';
|
|
import {
|
|
FileText,
|
|
Download,
|
|
PenLine,
|
|
Search,
|
|
} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { StatCards } from '@/components/organisms/StatCards';
|
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
|
import { TableRow, TableCell } from '@/components/ui/table';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
UniversalListPage,
|
|
type UniversalListConfig,
|
|
type SelectionHandlers,
|
|
type RowClickHandlers,
|
|
} from '@/components/templates/UniversalListPage';
|
|
import { MobileCard } from '@/components/organisms/MobileCard';
|
|
import {
|
|
getTaxInvoices,
|
|
getTaxInvoiceSummary,
|
|
downloadTaxInvoiceExcel,
|
|
} from './actions';
|
|
|
|
const ManualEntryModal = dynamic(
|
|
() => import('./ManualEntryModal').then(mod => ({ default: mod.ManualEntryModal })),
|
|
);
|
|
const JournalEntryModal = dynamic(
|
|
() => import('./JournalEntryModal').then(mod => ({ default: mod.JournalEntryModal })),
|
|
);
|
|
import type {
|
|
TaxInvoiceMgmtRecord,
|
|
InvoiceTab,
|
|
TaxInvoiceSummary,
|
|
} from './types';
|
|
import {
|
|
TAB_OPTIONS,
|
|
DATE_TYPE_OPTIONS,
|
|
TAX_TYPE_LABELS,
|
|
RECEIPT_TYPE_LABELS,
|
|
INVOICE_STATUS_MAP,
|
|
INVOICE_SOURCE_LABELS,
|
|
} from './types';
|
|
import { formatNumber } from '@/lib/utils/amount';
|
|
|
|
// ===== 분기 옵션 =====
|
|
const QUARTER_BUTTONS = [
|
|
{ value: 'Q1', label: '1분기', startMonth: 1, endMonth: 3 },
|
|
{ value: 'Q2', label: '2분기', startMonth: 4, endMonth: 6 },
|
|
{ value: 'Q3', label: '3분기', startMonth: 7, endMonth: 9 },
|
|
{ value: 'Q4', label: '4분기', startMonth: 10, endMonth: 12 },
|
|
];
|
|
|
|
// ===== 테이블 컬럼 =====
|
|
const tableColumns = [
|
|
{ key: 'writeDate', label: '작성일자', className: 'text-center', sortable: true },
|
|
{ key: 'issueDate', label: '발급일자', className: 'text-center', sortable: true },
|
|
{ key: 'vendorName', label: '거래처', sortable: true },
|
|
{ key: 'vendorBusinessNumber', label: '사업자번호\n(주민번호)', className: 'text-center', sortable: true },
|
|
{ key: 'taxType', label: '과세형태', className: 'text-center', sortable: true },
|
|
{ key: 'itemName', label: '품목', sortable: true },
|
|
{ key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true },
|
|
{ key: 'taxAmount', label: '세액', className: 'text-right', sortable: true },
|
|
{ key: 'totalAmount', label: '합계', className: 'text-right', sortable: true },
|
|
{ key: 'receiptType', label: '영수청구', className: 'text-center', sortable: true },
|
|
{ key: 'documentType', label: '문서형태', className: 'text-center', sortable: true },
|
|
{ key: 'issueType', label: '발급형태', className: 'text-center', sortable: true },
|
|
{ key: 'status', label: '상태', className: 'text-center', sortable: true },
|
|
{ key: 'journal', label: '분개', className: 'text-center w-[80px]' },
|
|
];
|
|
|
|
// ===== 날짜 헬퍼 =====
|
|
function getQuarterDates(year: number, quarter: string) {
|
|
const q = QUARTER_BUTTONS.find((b) => b.value === quarter);
|
|
if (!q) return { start: '', end: '' };
|
|
const start = `${year}-${String(q.startMonth).padStart(2, '0')}-01`;
|
|
const lastDay = new Date(year, q.endMonth, 0).getDate();
|
|
const end = `${year}-${String(q.endMonth).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
|
return { start, end };
|
|
}
|
|
|
|
function getCurrentQuarter(): string {
|
|
const month = new Date().getMonth() + 1;
|
|
if (month <= 3) return 'Q1';
|
|
if (month <= 6) return 'Q2';
|
|
if (month <= 9) return 'Q3';
|
|
return 'Q4';
|
|
}
|
|
|
|
export function TaxInvoiceManagement() {
|
|
// ===== 필터 상태 =====
|
|
const currentYear = new Date().getFullYear();
|
|
const [activeTab, setActiveTab] = useState<InvoiceTab>('sales');
|
|
const [dateType, setDateType] = useState('write_date');
|
|
const [selectedQuarter, setSelectedQuarter] = useState(getCurrentQuarter());
|
|
const [startDate, setStartDate] = useState(() => {
|
|
const q = getQuarterDates(currentYear, getCurrentQuarter());
|
|
return q.start;
|
|
});
|
|
const [endDate, setEndDate] = useState(() => {
|
|
const q = getQuarterDates(currentYear, getCurrentQuarter());
|
|
return q.end;
|
|
});
|
|
const [vendorSearch, setVendorSearch] = useState('');
|
|
|
|
// ===== 데이터 상태 =====
|
|
const [invoiceData, setInvoiceData] = useState<TaxInvoiceMgmtRecord[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const isInitialLoadDone = useRef(false);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [pagination, setPagination] = useState({
|
|
currentPage: 1,
|
|
lastPage: 1,
|
|
perPage: 20,
|
|
total: 0,
|
|
});
|
|
|
|
// ===== 요약 상태 =====
|
|
const [summary, setSummary] = useState<TaxInvoiceSummary>({
|
|
salesSupplyAmount: 0,
|
|
salesTaxAmount: 0,
|
|
salesTotalAmount: 0,
|
|
salesCount: 0,
|
|
purchaseSupplyAmount: 0,
|
|
purchaseTaxAmount: 0,
|
|
purchaseTotalAmount: 0,
|
|
purchaseCount: 0,
|
|
});
|
|
|
|
// ===== 모달 상태 =====
|
|
const [showManualEntry, setShowManualEntry] = useState(false);
|
|
const [journalTarget, setJournalTarget] = useState<TaxInvoiceMgmtRecord | null>(null);
|
|
|
|
// ===== 분기 버튼 클릭 =====
|
|
const handleQuarterClick = useCallback((quarter: string) => {
|
|
setSelectedQuarter(quarter);
|
|
const dates = getQuarterDates(currentYear, quarter);
|
|
setStartDate(dates.start);
|
|
setEndDate(dates.end);
|
|
setCurrentPage(1);
|
|
}, [currentYear]);
|
|
|
|
// ===== 데이터 로드 =====
|
|
const loadData = useCallback(async () => {
|
|
if (!isInitialLoadDone.current) {
|
|
setIsLoading(true);
|
|
}
|
|
try {
|
|
const [listResult, summaryResult] = await Promise.all([
|
|
getTaxInvoices({
|
|
division: activeTab,
|
|
dateType,
|
|
startDate,
|
|
endDate,
|
|
vendorSearch,
|
|
page: currentPage,
|
|
perPage: 20,
|
|
}),
|
|
getTaxInvoiceSummary({
|
|
dateType,
|
|
startDate,
|
|
endDate,
|
|
vendorSearch,
|
|
}),
|
|
]);
|
|
|
|
if (listResult.success) {
|
|
setInvoiceData(listResult.data);
|
|
setPagination(listResult.pagination);
|
|
} else {
|
|
toast.error(listResult.error || '목록 조회에 실패했습니다.');
|
|
}
|
|
|
|
if (summaryResult.success && summaryResult.data) {
|
|
setSummary(summaryResult.data);
|
|
}
|
|
} catch {
|
|
toast.error('서버 오류가 발생했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
isInitialLoadDone.current = true;
|
|
}
|
|
}, [activeTab, dateType, startDate, endDate, vendorSearch, currentPage]);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
// ===== 탭 변경 =====
|
|
const handleTabChange = useCallback((tab: InvoiceTab) => {
|
|
setActiveTab(tab);
|
|
setCurrentPage(1);
|
|
}, []);
|
|
|
|
// ===== 조회 버튼 =====
|
|
const handleSearch = useCallback(() => {
|
|
setCurrentPage(1);
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
// ===== 엑셀 다운로드 =====
|
|
const handleExcelDownload = useCallback(async () => {
|
|
const result = await downloadTaxInvoiceExcel({
|
|
division: activeTab,
|
|
dateType,
|
|
startDate,
|
|
endDate,
|
|
vendorSearch,
|
|
});
|
|
if (result.success && result.data) {
|
|
window.open(result.data.url, '_blank');
|
|
} else {
|
|
toast.error(result.error || '엑셀 다운로드에 실패했습니다.');
|
|
}
|
|
}, [activeTab, dateType, startDate, endDate, vendorSearch]);
|
|
|
|
// ===== 수기 등록 완료 =====
|
|
const handleManualEntrySuccess = useCallback(() => {
|
|
setShowManualEntry(false);
|
|
loadData();
|
|
toast.success('세금계산서가 등록되었습니다.');
|
|
}, [loadData]);
|
|
|
|
// ===== 분개 완료 =====
|
|
const handleJournalSuccess = useCallback(() => {
|
|
setJournalTarget(null);
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
// ===== 기간 요약 계산 =====
|
|
const periodDifference = useMemo(() => {
|
|
return summary.salesTotalAmount - summary.purchaseTotalAmount;
|
|
}, [summary]);
|
|
|
|
// ===== UniversalListPage Config =====
|
|
const config: UniversalListConfig<TaxInvoiceMgmtRecord> = useMemo(
|
|
() => ({
|
|
title: '세금계산서 관리',
|
|
description: '홈택스에 신고된 세금계산서 매입/매출 내역을 조회하고 관리합니다',
|
|
icon: FileText,
|
|
basePath: '/accounting/tax-invoices',
|
|
|
|
idField: 'id',
|
|
|
|
actions: {
|
|
getList: async () => ({
|
|
success: true,
|
|
data: invoiceData,
|
|
totalCount: pagination.total,
|
|
}),
|
|
},
|
|
|
|
columns: tableColumns,
|
|
clientSideFiltering: false,
|
|
itemsPerPage: 20,
|
|
hideSearch: true,
|
|
showCheckbox: false,
|
|
|
|
// ===== 검색 영역 (beforeTableContent) =====
|
|
beforeTableContent: (
|
|
<div className="space-y-3">
|
|
{/* 검색 필터 카드 */}
|
|
<Card>
|
|
<CardContent className="p-4 space-y-3">
|
|
{/* Row1: 일자타입 + 날짜범위 + 분기 버튼 + 조회 */}
|
|
<div className="flex flex-col lg:flex-row lg:items-center gap-2">
|
|
<Select value={dateType} onValueChange={setDateType}>
|
|
<SelectTrigger className="w-full lg:min-w-[120px] lg:w-auto h-9 text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DATE_TYPE_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<DateRangeSelector
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
onStartDateChange={setStartDate}
|
|
onEndDateChange={setEndDate}
|
|
hidePresets
|
|
/>
|
|
|
|
<div className="flex gap-1">
|
|
{QUARTER_BUTTONS.map((q) => (
|
|
<Button
|
|
key={q.value}
|
|
size="sm"
|
|
variant={selectedQuarter === q.value ? 'default' : 'outline'}
|
|
onClick={() => handleQuarterClick(q.value)}
|
|
>
|
|
{q.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
<Button size="sm" onClick={handleSearch}>
|
|
조회
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Row2: 거래처 검색 (아이콘 포함) */}
|
|
<div className="relative max-w-md">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
value={vendorSearch}
|
|
onChange={(e) => setVendorSearch(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
placeholder="사업자 번호 또는 사업자명"
|
|
className="pl-9 h-9"
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 요약 카드 5개 */}
|
|
<StatCards
|
|
stats={[
|
|
{ label: '매출 공급가액', value: `${formatNumber(summary.salesSupplyAmount)}원` },
|
|
{ label: '매출 세액', value: `${formatNumber(summary.salesTaxAmount)}원` },
|
|
{ label: '매입 과세 공급가액', value: `${formatNumber(summary.purchaseSupplyAmount)}원` },
|
|
{ label: '매입 면세 공급가액', value: '0원' },
|
|
{ label: '매입 세액', value: `${formatNumber(summary.purchaseTaxAmount)}원` },
|
|
]}
|
|
/>
|
|
|
|
{/* 매출/매입 탭 + 액션 버튼 */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
|
<Tabs value={activeTab} onValueChange={(v) => handleTabChange(v as InvoiceTab)}>
|
|
<TabsList>
|
|
{TAB_OPTIONS.map((t) => (
|
|
<TabsTrigger key={t.value} value={t.value}>
|
|
{t.label} {t.value === 'sales' ? summary.salesCount : summary.purchaseCount}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
</Tabs>
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" size="sm" onClick={handleExcelDownload}>
|
|
<Download className="h-4 w-4 mr-1" />
|
|
엑셀 다운로드
|
|
</Button>
|
|
<Button size="sm" onClick={() => setShowManualEntry(true)}>
|
|
<PenLine className="h-4 w-4 mr-1" />
|
|
수기 입력
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
|
|
// 탭은 beforeTableContent에서 수동 렌더링 (카드와 테이블 사이)
|
|
|
|
// ===== 테이블 행 렌더링 =====
|
|
renderTableRow: (
|
|
item: TaxInvoiceMgmtRecord,
|
|
_index: number,
|
|
_globalIndex: number,
|
|
_handlers: SelectionHandlers & RowClickHandlers<TaxInvoiceMgmtRecord>
|
|
) => (
|
|
<TableRow
|
|
key={item.id}
|
|
className={`hover:bg-muted/50 ${item.source === 'manual' ? 'bg-yellow-50/50' : ''}`}
|
|
>
|
|
<TableCell className="text-center text-sm">{item.writeDate}</TableCell>
|
|
<TableCell className="text-center text-sm">{item.issueDate || '-'}</TableCell>
|
|
<TableCell className="text-sm">{item.vendorName}</TableCell>
|
|
<TableCell className="text-center text-sm">{item.vendorBusinessNumber}</TableCell>
|
|
<TableCell className="text-center">
|
|
<Badge variant="outline" className="text-xs">
|
|
{TAX_TYPE_LABELS[item.taxType]}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-sm">{item.itemName || '-'}</TableCell>
|
|
<TableCell className="text-right text-sm">{formatNumber(item.supplyAmount)}</TableCell>
|
|
<TableCell className="text-right text-sm">{formatNumber(item.taxAmount)}</TableCell>
|
|
<TableCell className="text-right text-sm font-medium">{formatNumber(item.totalAmount)}</TableCell>
|
|
<TableCell className="text-center text-sm">
|
|
<span className="inline-flex items-center gap-1">
|
|
<span className={`inline-block w-2 h-2 rounded-full ${item.source === 'manual' ? 'bg-purple-500' : 'bg-blue-500'}`} />
|
|
{RECEIPT_TYPE_LABELS[item.receiptType]}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-center text-sm">{item.documentNumber || '-'}</TableCell>
|
|
<TableCell className="text-center text-sm">{INVOICE_SOURCE_LABELS[item.source]}</TableCell>
|
|
<TableCell className="text-center">
|
|
<Badge className={`text-xs ${INVOICE_STATUS_MAP[item.status].color}`}>
|
|
{INVOICE_STATUS_MAP[item.status].label}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 px-2 text-xs"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setJournalTarget(item);
|
|
}}
|
|
>
|
|
분개
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
),
|
|
|
|
// ===== 모바일 카드 렌더링 =====
|
|
renderMobileCard: (
|
|
item: TaxInvoiceMgmtRecord,
|
|
_index: number,
|
|
_globalIndex: number,
|
|
_handlers: SelectionHandlers & RowClickHandlers<TaxInvoiceMgmtRecord>
|
|
) => (
|
|
<MobileCard
|
|
key={item.id}
|
|
title={item.vendorName}
|
|
subtitle={item.documentNumber || item.writeDate}
|
|
badge={INVOICE_STATUS_MAP[item.status].label}
|
|
badgeVariant="outline"
|
|
onClick={() => setJournalTarget(item)}
|
|
details={[
|
|
{ label: '작성일자', value: item.writeDate },
|
|
{ label: '공급가액', value: `${formatNumber(item.supplyAmount)}원` },
|
|
{ label: '세액', value: `${formatNumber(item.taxAmount)}원` },
|
|
{ label: '합계', value: `${formatNumber(item.totalAmount)}원` },
|
|
{ label: '과세여부', value: TAX_TYPE_LABELS[item.taxType] },
|
|
{ label: '소스', value: INVOICE_SOURCE_LABELS[item.source] },
|
|
]}
|
|
/>
|
|
),
|
|
|
|
// ===== 범례 (테이블 안) =====
|
|
tableFooter: (
|
|
<TableRow>
|
|
<TableCell colSpan={14} className="py-2">
|
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="w-2.5 h-2.5 bg-purple-500 rounded-full" />
|
|
<span>수기 세금계산서</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="w-2.5 h-2.5 bg-blue-500 rounded-full" />
|
|
<span>홈택스 연동 세금계산서</span>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
),
|
|
|
|
// ===== 기간 요약 (테이블 뒤) =====
|
|
afterTableContent: () => (
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="text-sm font-semibold mb-3">기간 요약</div>
|
|
{/* 모바일: 세로 스택, sm 이상: 가로 배치 */}
|
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-2 sm:gap-4 text-center">
|
|
<div className="w-full sm:w-auto">
|
|
<div className="text-xs text-muted-foreground mb-1">매출 합계 (공급가액 + 세액)</div>
|
|
<div className="text-lg sm:text-xl font-bold">{formatNumber(summary.salesTotalAmount)}원</div>
|
|
</div>
|
|
<div className="text-xl sm:text-2xl font-bold text-muted-foreground">⊖</div>
|
|
<div className="w-full sm:w-auto">
|
|
<div className="text-xs text-muted-foreground mb-1">매입 합계 (공급가액 + 세액)</div>
|
|
<div className="text-lg sm:text-xl font-bold">{formatNumber(summary.purchaseTotalAmount)}원</div>
|
|
</div>
|
|
<div className="text-xl sm:text-2xl font-bold text-muted-foreground">=</div>
|
|
<div className="w-full sm:w-auto">
|
|
<div className="text-xs text-muted-foreground mb-1">예상 부가세</div>
|
|
<div className={`text-lg sm:text-xl font-bold ${periodDifference >= 0 ? 'text-blue-700' : 'text-red-700'}`}>
|
|
{formatNumber(periodDifference)}원
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
),
|
|
}),
|
|
[
|
|
invoiceData,
|
|
pagination,
|
|
summary,
|
|
dateType,
|
|
startDate,
|
|
endDate,
|
|
selectedQuarter,
|
|
vendorSearch,
|
|
periodDifference,
|
|
handleQuarterClick,
|
|
handleSearch,
|
|
handleExcelDownload,
|
|
]
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<UniversalListPage
|
|
config={config}
|
|
initialData={invoiceData}
|
|
externalPagination={{
|
|
currentPage: pagination.currentPage,
|
|
totalPages: pagination.lastPage,
|
|
totalItems: pagination.total,
|
|
itemsPerPage: pagination.perPage,
|
|
onPageChange: setCurrentPage,
|
|
}}
|
|
externalIsLoading={isLoading}
|
|
/>
|
|
|
|
{/* 수기 입력 팝업 */}
|
|
<ManualEntryModal
|
|
open={showManualEntry}
|
|
onOpenChange={setShowManualEntry}
|
|
onSuccess={handleManualEntrySuccess}
|
|
defaultDivision={activeTab}
|
|
/>
|
|
|
|
{/* 분개 수정 팝업 */}
|
|
{journalTarget && (
|
|
<JournalEntryModal
|
|
open={!!journalTarget}
|
|
onOpenChange={(open: boolean) => !open && setJournalTarget(null)}
|
|
invoice={journalTarget}
|
|
onSuccess={handleJournalSuccess}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|