동기화
This commit is contained in:
@@ -3,6 +3,14 @@ import LoginPage from '@/pages/LoginPage';
|
||||
import MainLayout from '@/layouts/MainLayout';
|
||||
import Dashboard from '@/pages/Dashboard';
|
||||
|
||||
// ── P5: 연말명세서 / 분개장 ──────────────────────────
|
||||
import AgentAnnualIncomeList from '@/pages/income/AgentAnnualIncomeList';
|
||||
import AccountingJournalList from '@/pages/accounting/AccountingJournalList';
|
||||
|
||||
// ── P5-W2: 원천세 / 부가세 신고 ──────────────────────
|
||||
import WithholdingTaxReportList from '@/pages/tax/WithholdingTaxReportList';
|
||||
import VatReportList from '@/pages/tax/VatReportList';
|
||||
|
||||
// ── P4: 수수료 종류 ──────────────────────────────────
|
||||
import CommissionMaster from '@/pages/commission/CommissionMaster';
|
||||
import CollectionCommission from '@/pages/commission/CollectionCommission';
|
||||
@@ -146,6 +154,14 @@ export default function App() {
|
||||
<Route path="kpi/retention" element={<KpiRetention />} />
|
||||
<Route path="kpi/cumulative" element={<KpiCumulative />} />
|
||||
|
||||
{/* P5: 연말명세서 / 분개장 */}
|
||||
<Route path="commission/annual-income" element={<AgentAnnualIncomeList />} />
|
||||
<Route path="commission/accounting-journal" element={<AccountingJournalList />} />
|
||||
|
||||
{/* P5-W2: 원천세 / 부가세 신고 */}
|
||||
<Route path="commission/withholding-tax" element={<WithholdingTaxReportList />} />
|
||||
<Route path="commission/vat-report" element={<VatReportList />} />
|
||||
|
||||
{/* P4: 수수료 종류 */}
|
||||
<Route path="commission/type" element={<CommissionMaster />} />
|
||||
<Route path="commission/master" element={<CommissionMaster />} />
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import api, { unwrap } from './request';
|
||||
|
||||
export interface AccountingJournalResp extends Record<string, unknown> {
|
||||
entryId: number;
|
||||
journalNo?: string;
|
||||
agentId?: number;
|
||||
agentName?: string;
|
||||
contractNo?: string;
|
||||
settleMonth?: string;
|
||||
accountCode: string;
|
||||
accountName?: string;
|
||||
debitAmount: number;
|
||||
creditAmount: number;
|
||||
description?: string;
|
||||
status?: string;
|
||||
statusName?: string;
|
||||
closedAt?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface AccountingGenerateReq {
|
||||
settleMonth: string;
|
||||
}
|
||||
|
||||
export interface AccountingCloseReq {
|
||||
journalNo: string;
|
||||
entryIds: number[];
|
||||
}
|
||||
|
||||
export interface AccountingSearchParam {
|
||||
settleMonth?: string;
|
||||
journalNo?: string;
|
||||
agentName?: string;
|
||||
status?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export const accountingApi = {
|
||||
list: (params?: AccountingSearchParam) =>
|
||||
unwrap<{ list: AccountingJournalResp[]; total: number }>(
|
||||
api.get('/api/accounting-entries', { params })
|
||||
),
|
||||
generate: (req: AccountingGenerateReq) =>
|
||||
unwrap<void>(api.post('/api/accounting-entries/generate', req)),
|
||||
close: (req: AccountingCloseReq) =>
|
||||
unwrap<void>(api.put('/api/accounting-entries/close', req)),
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import api, { unwrap } from './request';
|
||||
|
||||
export interface AgentAnnualIncomeResp extends Record<string, unknown> {
|
||||
incomeId: number;
|
||||
agentId: number;
|
||||
agentName?: string;
|
||||
agentCode?: string;
|
||||
orgName?: string;
|
||||
settleYear: number;
|
||||
totalCommission: number;
|
||||
totalTaxWithheld: number;
|
||||
businessIncome: number;
|
||||
status?: string;
|
||||
statusName?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface AgentAnnualIncomeSaveReq {
|
||||
agentId: number;
|
||||
settleYear: number;
|
||||
totalCommission: number;
|
||||
totalTaxWithheld: number;
|
||||
businessIncome: number;
|
||||
}
|
||||
|
||||
export interface AgentAnnualIncomeSearchParam {
|
||||
settleYear?: number;
|
||||
agentId?: number;
|
||||
agentName?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export const incomeApi = {
|
||||
list: (params?: AgentAnnualIncomeSearchParam) =>
|
||||
unwrap<{ list: AgentAnnualIncomeResp[]; total: number }>(
|
||||
api.get('/api/agent-annual-incomes', { params })
|
||||
),
|
||||
detail: (id: number) =>
|
||||
unwrap<AgentAnnualIncomeResp>(api.get(`/api/agent-annual-incomes/${id}`)),
|
||||
regenerate: (year: number) =>
|
||||
unwrap<void>(api.post(`/api/agent-annual-incomes/regenerate`, null, { params: { year } })),
|
||||
};
|
||||
@@ -40,4 +40,6 @@ export const regulatoryApi = {
|
||||
unwrap<void>(api.put(`/api/regulatory-limits/${id}`, req)),
|
||||
deactivate: (id: number) =>
|
||||
unwrap<void>(api.put(`/api/regulatory-limits/${id}/deactivate`)),
|
||||
activate: (id: number) =>
|
||||
unwrap<void>(api.put(`/api/regulatory-limits/${id}/activate`)),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import api, { unwrap } from './request';
|
||||
|
||||
export interface VatReportResp extends Record<string, unknown> {
|
||||
reportId: number;
|
||||
reportYear: number;
|
||||
reportQuarter: number;
|
||||
salesAmount: number;
|
||||
salesVat: number;
|
||||
purchaseAmount: number;
|
||||
purchaseVat: number;
|
||||
payableVat: number;
|
||||
status?: string;
|
||||
statusName?: string;
|
||||
reportedAt?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface VatSearchParam {
|
||||
reportYear?: number;
|
||||
reportQuarter?: number;
|
||||
status?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface VatRegenerateReq {
|
||||
reportYear: number;
|
||||
reportQuarter: number;
|
||||
}
|
||||
|
||||
export interface VatReportCompleteReq {
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
export const vatApi = {
|
||||
list: (params?: VatSearchParam) =>
|
||||
unwrap<{ list: VatReportResp[]; total: number }>(
|
||||
api.get('/api/vat-reports', { params })
|
||||
),
|
||||
detail: (id: number) =>
|
||||
unwrap<VatReportResp>(api.get(`/api/vat-reports/${id}`)),
|
||||
regenerate: (req: VatRegenerateReq) =>
|
||||
unwrap<void>(api.post('/api/vat-reports/regenerate', req)),
|
||||
complete: (req: VatReportCompleteReq) =>
|
||||
unwrap<void>(api.put('/api/vat-reports/complete', req)),
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import api, { unwrap } from './request';
|
||||
|
||||
export interface WithholdingTaxReportResp extends Record<string, unknown> {
|
||||
reportId: number;
|
||||
agentId: number;
|
||||
agentName?: string;
|
||||
agentCode?: string;
|
||||
orgName?: string;
|
||||
reportYear: number;
|
||||
reportQuarter: number;
|
||||
totalIncome: number;
|
||||
withholdingTax: number;
|
||||
status?: string;
|
||||
statusName?: string;
|
||||
reportedAt?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface WithholdingTaxSearchParam {
|
||||
reportYear?: number;
|
||||
reportQuarter?: number;
|
||||
agentId?: number;
|
||||
agentName?: string;
|
||||
status?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface WithholdingTaxRegenerateReq {
|
||||
reportYear: number;
|
||||
reportQuarter: number;
|
||||
}
|
||||
|
||||
export interface WithholdingTaxReportCompleteReq {
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
export const withholdingTaxApi = {
|
||||
list: (params?: WithholdingTaxSearchParam) =>
|
||||
unwrap<{ list: WithholdingTaxReportResp[]; total: number }>(
|
||||
api.get('/api/withholding-tax-reports', { params })
|
||||
),
|
||||
detail: (id: number) =>
|
||||
unwrap<WithholdingTaxReportResp>(api.get(`/api/withholding-tax-reports/${id}`)),
|
||||
regenerate: (req: WithholdingTaxRegenerateReq) =>
|
||||
unwrap<void>(api.post('/api/withholding-tax-reports/regenerate', req)),
|
||||
complete: (req: WithholdingTaxReportCompleteReq) =>
|
||||
unwrap<void>(api.put('/api/withholding-tax-reports/complete', req)),
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { Skeleton } from 'antd';
|
||||
import { AgGridReact } from '@ag-grid-community/react';
|
||||
import { ModuleRegistry } from '@ag-grid-community/core';
|
||||
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
||||
import type { ColDef, GridReadyEvent, GridApi, RowClassParams, RowStyle } from '@ag-grid-community/core';
|
||||
import type { ColDef, GridReadyEvent, GridApi, RowClassParams, RowStyle, SelectionChangedEvent } from '@ag-grid-community/core';
|
||||
import '@ag-grid-community/styles/ag-grid.css';
|
||||
import '@ag-grid-community/styles/ag-theme-quartz.css';
|
||||
import { GRAY } from '@/theme/tokens';
|
||||
@@ -33,6 +33,8 @@ interface Props<T> {
|
||||
rowKey?: keyof T;
|
||||
/** 행별 스타일 콜백 (만료 임박 강조 등) */
|
||||
getRowStyle?: (params: RowClassParams<T>) => RowStyle | undefined;
|
||||
rowSelection?: 'single' | 'multiple';
|
||||
onSelectionChanged?: (e: SelectionChangedEvent<T>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,6 +52,8 @@ export default function DataGrid<T extends Record<string, unknown>>({
|
||||
onCellChanged,
|
||||
rowKey,
|
||||
getRowStyle: getRowStyleProp,
|
||||
rowSelection: rowSelectionProp = 'single',
|
||||
onSelectionChanged,
|
||||
}: Props<T>) {
|
||||
const apiRef = useRef<GridApi<T> | null>(null);
|
||||
|
||||
@@ -93,7 +97,8 @@ export default function DataGrid<T extends Record<string, unknown>>({
|
||||
rowHeight={56}
|
||||
headerHeight={44}
|
||||
localeText={AG_GRID_LOCALE_KO}
|
||||
rowSelection="single"
|
||||
rowSelection={rowSelectionProp}
|
||||
onSelectionChanged={onSelectionChanged}
|
||||
getRowId={rowKey ? (p) => String(p.data[rowKey]) : undefined}
|
||||
pinnedBottomRowData={pinnedBottomRow ? [pinnedBottomRow as T] : undefined}
|
||||
getRowStyle={(params) => {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Alert, Form, Input, Modal, Space, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ColDef, SelectionChangedEvent } from '@ag-grid-community/core';
|
||||
import dayjs from 'dayjs';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import DataGrid from '@/components/common/DataGrid';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { accountingApi, AccountingJournalResp, AccountingSearchParam } from '@/api/accounting';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
|
||||
const COLUMNS: ColDef<AccountingJournalResp>[] = [
|
||||
{ field: 'journalNo', headerName: '분개번호', width: 140, pinned: 'left', rowGroup: false },
|
||||
{ field: 'settleMonth', headerName: '정산월', width: 100 },
|
||||
{ field: 'agentName', headerName: '설계사', width: 120 },
|
||||
{ field: 'contractNo', headerName: '계약번호', width: 140 },
|
||||
{ field: 'accountCode', headerName: '계정코드', width: 110 },
|
||||
{ field: 'accountName', headerName: '계정명', width: 150 },
|
||||
{ field: 'debitAmount', headerName: '차변', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'creditAmount', headerName: '대변', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'description', headerName: '적요', flex: 1 },
|
||||
{ field: 'statusName', headerName: '상태', width: 90 },
|
||||
{ field: 'closedAt', headerName: '마감일시', width: 140 },
|
||||
];
|
||||
|
||||
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||
|
||||
export default function AccountingJournalList() {
|
||||
const qc = useQueryClient();
|
||||
const currentMonth = dayjs().format('YYYYMM');
|
||||
const [params, setParams] = useState<AccountingSearchParam>({
|
||||
settleMonth: currentMonth,
|
||||
pageSize: 500,
|
||||
});
|
||||
const [selectedRows, setSelectedRows] = useState<AccountingJournalResp[]>([]);
|
||||
const [closeModalOpen, setCloseModalOpen] = useState(false);
|
||||
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
||||
const [closeForm] = Form.useForm();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['accounting-journals', params],
|
||||
queryFn: () => accountingApi.list(params),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const rows = data?.list ?? [];
|
||||
|
||||
const handleSelectionChanged = useCallback((e: SelectionChangedEvent<AccountingJournalResp>) => {
|
||||
setSelectedRows(e.api.getSelectedRows());
|
||||
}, []);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
try {
|
||||
await accountingApi.generate({ settleMonth: params.settleMonth ?? currentMonth });
|
||||
message.success('분개 생성 완료');
|
||||
qc.invalidateQueries({ queryKey: ['accounting-journals'] });
|
||||
setGenerateModalOpen(false);
|
||||
} catch { /* request.ts 처리 */ }
|
||||
};
|
||||
|
||||
const handleClose = async () => {
|
||||
const values = await closeForm.validateFields();
|
||||
const ids = selectedRows.map((r) => r.entryId);
|
||||
try {
|
||||
await accountingApi.close({ journalNo: values.journalNo, entryIds: ids });
|
||||
message.success('마감 완료');
|
||||
qc.invalidateQueries({ queryKey: ['accounting-journals'] });
|
||||
setCloseModalOpen(false);
|
||||
closeForm.resetFields();
|
||||
setSelectedRows([]);
|
||||
} catch { /* request.ts 처리 */ }
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="회계 분개장"
|
||||
description="정산 회계 분개 엔트리 관리"
|
||||
extra={
|
||||
<Space>
|
||||
<PermissionButton
|
||||
menuCode="ACCOUNTING_JOURNAL"
|
||||
permCode="CREATE"
|
||||
onClick={() => setGenerateModalOpen(true)}
|
||||
>
|
||||
분개 생성
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
menuCode="ACCOUNTING_JOURNAL"
|
||||
permCode="APPROVE"
|
||||
type="primary"
|
||||
disabled={selectedRows.length === 0}
|
||||
onClick={() => setCloseModalOpen(true)}
|
||||
>
|
||||
마감 ({selectedRows.length}건)
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{isError && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="API 미응답"
|
||||
description="/api/accounting-journals 를 확인하세요."
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'month', name: 'settleMonth', label: '정산월', span: 6 },
|
||||
{ type: 'text', name: 'journalNo', label: '분개번호', span: 6 },
|
||||
{ type: 'text', name: 'agentName', label: '설계사명', span: 6 },
|
||||
]}
|
||||
onSearch={(v) => setParams({ ...v as AccountingSearchParam, pageSize: 500 })}
|
||||
onReset={() => setParams({ settleMonth: currentMonth, pageSize: 500 })}
|
||||
/>
|
||||
|
||||
<ProCard style={cardStyle}>
|
||||
<DataGrid<AccountingJournalResp>
|
||||
rows={rows}
|
||||
columns={COLUMNS}
|
||||
loading={isLoading}
|
||||
height={620}
|
||||
rowKey="entryId"
|
||||
rowSelection="multiple"
|
||||
onSelectionChanged={handleSelectionChanged}
|
||||
/>
|
||||
</ProCard>
|
||||
|
||||
{/* 분개 생성 확인 모달 */}
|
||||
<Modal
|
||||
title="분개 생성"
|
||||
open={generateModalOpen}
|
||||
onOk={handleGenerate}
|
||||
onCancel={() => setGenerateModalOpen(false)}
|
||||
okText="생성"
|
||||
cancelText="취소"
|
||||
>
|
||||
<p>{params.settleMonth} 정산월의 회계 분개를 생성하시겠습니까?</p>
|
||||
</Modal>
|
||||
|
||||
{/* 마감 처리 모달 */}
|
||||
<Modal
|
||||
title="분개 마감"
|
||||
open={closeModalOpen}
|
||||
onOk={handleClose}
|
||||
onCancel={() => { setCloseModalOpen(false); closeForm.resetFields(); }}
|
||||
okText="마감"
|
||||
cancelText="취소"
|
||||
>
|
||||
<p>선택한 {selectedRows.length}건의 분개를 마감합니다.</p>
|
||||
<Form form={closeForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item
|
||||
name="journalNo"
|
||||
label="분개번호 (journal_no)"
|
||||
rules={[{ required: true, message: '분개번호를 입력하세요' }]}
|
||||
>
|
||||
<Input placeholder="예: JNL-2024-001" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useState } from 'react';
|
||||
import { Alert, Modal, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ColDef } from '@ag-grid-community/core';
|
||||
import dayjs from 'dayjs';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import DataGrid from '@/components/common/DataGrid';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { incomeApi, AgentAnnualIncomeResp, AgentAnnualIncomeSearchParam } from '@/api/income';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
|
||||
const COLUMNS: ColDef<AgentAnnualIncomeResp>[] = [
|
||||
{ field: 'settleYear', headerName: '귀속연도', width: 100, pinned: 'left' },
|
||||
{ field: 'agentCode', headerName: '설계사코드', width: 120 },
|
||||
{ field: 'agentName', headerName: '설계사명', width: 120 },
|
||||
{ field: 'orgName', headerName: '소속조직', width: 160 },
|
||||
{ field: 'totalCommission', headerName: '총수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'totalTaxWithheld', headerName: '원천징수세액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'businessIncome', headerName: '사업소득', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'statusName', headerName: '상태', width: 90 },
|
||||
{ field: 'updatedAt', headerName: '최종수정', width: 140 },
|
||||
];
|
||||
|
||||
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||
|
||||
export default function AgentAnnualIncomeList() {
|
||||
const qc = useQueryClient();
|
||||
const currentYear = dayjs().year();
|
||||
const [params, setParams] = useState<AgentAnnualIncomeSearchParam>({
|
||||
settleYear: currentYear,
|
||||
pageSize: 500,
|
||||
});
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['annual-income', params],
|
||||
queryFn: () => incomeApi.list(params),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const rows = data?.list ?? [];
|
||||
|
||||
const pinnedRow = rows.length > 0 ? {
|
||||
agentName: '합계',
|
||||
totalCommission: rows.reduce((s, r) => s + (r.totalCommission ?? 0), 0),
|
||||
totalTaxWithheld: rows.reduce((s, r) => s + (r.totalTaxWithheld ?? 0), 0),
|
||||
businessIncome: rows.reduce((s, r) => s + (r.businessIncome ?? 0), 0),
|
||||
} as Partial<AgentAnnualIncomeResp> : undefined;
|
||||
|
||||
const handleRegenerate = () => {
|
||||
Modal.confirm({
|
||||
title: '연말 사업소득 재집계',
|
||||
content: `${params.settleYear ?? currentYear}년 데이터를 재집계하시겠습니까? 기존 데이터가 갱신됩니다.`,
|
||||
okText: '재집계',
|
||||
cancelText: '취소',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await incomeApi.regenerate(params.settleYear ?? currentYear);
|
||||
message.success('재집계 완료');
|
||||
qc.invalidateQueries({ queryKey: ['annual-income'] });
|
||||
} catch { /* request.ts 처리 */ }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="연말 사업소득 명세서"
|
||||
description="설계사별 귀속연도 사업소득 집계 현황"
|
||||
extra={
|
||||
<PermissionButton
|
||||
menuCode="ANNUAL_INCOME"
|
||||
permCode="APPROVE"
|
||||
type="primary"
|
||||
onClick={handleRegenerate}
|
||||
>
|
||||
재집계
|
||||
</PermissionButton>
|
||||
}
|
||||
>
|
||||
{isError && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="API 미응답"
|
||||
description="/api/agent-annual-incomes 를 확인하세요."
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'text', name: 'settleYear', label: '귀속연도', span: 6 },
|
||||
{ type: 'text', name: 'agentName', label: '설계사명', span: 6 },
|
||||
]}
|
||||
onSearch={(v) => setParams({ ...v as AgentAnnualIncomeSearchParam, pageSize: 500 })}
|
||||
onReset={() => setParams({ settleYear: currentYear, pageSize: 500 })}
|
||||
/>
|
||||
|
||||
<ProCard style={cardStyle}>
|
||||
<DataGrid<AgentAnnualIncomeResp>
|
||||
rows={rows}
|
||||
columns={COLUMNS}
|
||||
loading={isLoading}
|
||||
height={620}
|
||||
rowKey="incomeId"
|
||||
pinnedBottomRow={pinnedRow}
|
||||
/>
|
||||
</ProCard>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -59,6 +59,20 @@ export default function RegulatoryLimitList() {
|
||||
});
|
||||
};
|
||||
|
||||
const onActivate = (record: RegulatoryLimitResp) => {
|
||||
Modal.confirm({
|
||||
title: '규제 한도 활성화',
|
||||
content: `[${record.limitTypeName ?? record.limitType}] 한도를 활성화하시겠습니까?`,
|
||||
okText: '활성화',
|
||||
cancelText: '취소',
|
||||
onOk: async () => {
|
||||
await regulatoryApi.activate(record.limitId);
|
||||
message.success('활성화 완료');
|
||||
actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (v: any) => {
|
||||
const req: RegulatoryLimitSaveReq = {
|
||||
...v,
|
||||
@@ -108,9 +122,12 @@ export default function RegulatoryLimitList() {
|
||||
render: (_, r) => [
|
||||
<PermissionButton key="edit" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link"
|
||||
onClick={() => openEdit(r)}>수정</PermissionButton>,
|
||||
r.isActive && (
|
||||
r.isActive ? (
|
||||
<PermissionButton key="deact" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link" danger
|
||||
onClick={() => onDeactivate(r)}>비활성화</PermissionButton>
|
||||
) : (
|
||||
<PermissionButton key="act" menuCode="REGULATORY_LIMIT" permCode="UPDATE" size="small" type="link"
|
||||
onClick={() => onActivate(r)}>활성화</PermissionButton>
|
||||
),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Alert, Modal, Space, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ColDef, SelectionChangedEvent } from '@ag-grid-community/core';
|
||||
import dayjs from 'dayjs';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import DataGrid from '@/components/common/DataGrid';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { vatApi, VatReportResp, VatSearchParam } from '@/api/vat';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
|
||||
const COLUMNS: ColDef<VatReportResp>[] = [
|
||||
{ field: 'reportYear', headerName: '신고연도', width: 100, pinned: 'left' },
|
||||
{ field: 'reportQuarter', headerName: '분기', width: 80, pinned: 'left', valueFormatter: (p) => `${p.value}Q` },
|
||||
{ field: 'salesAmount', headerName: '매출액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'salesVat', headerName: '매출세액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'purchaseAmount', headerName: '매입액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'purchaseVat', headerName: '매입세액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'payableVat', headerName: '납부세액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'statusName', headerName: '상태', width: 90 },
|
||||
{ field: 'reportedAt', headerName: '신고일시', width: 140 },
|
||||
];
|
||||
|
||||
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||
|
||||
export default function VatReportList() {
|
||||
const qc = useQueryClient();
|
||||
const currentYear = dayjs().year();
|
||||
const currentQuarter = Math.ceil((dayjs().month() + 1) / 3);
|
||||
|
||||
const [params, setParams] = useState<VatSearchParam>({
|
||||
reportYear: currentYear,
|
||||
reportQuarter: currentQuarter,
|
||||
pageSize: 200,
|
||||
});
|
||||
const [selectedRows, setSelectedRows] = useState<VatReportResp[]>([]);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['vat-reports', params],
|
||||
queryFn: () => vatApi.list(params),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const rows = data?.list ?? [];
|
||||
|
||||
const pinnedRow = rows.length > 0 ? {
|
||||
reportYear: '합계' as unknown as number,
|
||||
salesAmount: rows.reduce((s, r) => s + (r.salesAmount ?? 0), 0),
|
||||
salesVat: rows.reduce((s, r) => s + (r.salesVat ?? 0), 0),
|
||||
purchaseAmount: rows.reduce((s, r) => s + (r.purchaseAmount ?? 0), 0),
|
||||
purchaseVat: rows.reduce((s, r) => s + (r.purchaseVat ?? 0), 0),
|
||||
payableVat: rows.reduce((s, r) => s + (r.payableVat ?? 0), 0),
|
||||
} as Partial<VatReportResp> : undefined;
|
||||
|
||||
const handleSelectionChanged = useCallback((e: SelectionChangedEvent<VatReportResp>) => {
|
||||
setSelectedRows(e.api.getSelectedRows());
|
||||
}, []);
|
||||
|
||||
const handleRegenerate = () => {
|
||||
Modal.confirm({
|
||||
title: '부가세 재집계',
|
||||
content: `${params.reportYear}년 ${params.reportQuarter}Q 데이터를 재집계하시겠습니까?`,
|
||||
okText: '재집계',
|
||||
cancelText: '취소',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await vatApi.regenerate({
|
||||
reportYear: params.reportYear ?? currentYear,
|
||||
reportQuarter: params.reportQuarter ?? currentQuarter,
|
||||
});
|
||||
message.success('재집계 완료');
|
||||
qc.invalidateQueries({ queryKey: ['vat-reports'] });
|
||||
} catch { /* request.ts 처리 */ }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
Modal.confirm({
|
||||
title: '신고완료 처리',
|
||||
content: `선택한 ${selectedRows.length}건을 신고완료 처리하시겠습니까?`,
|
||||
okText: '신고완료',
|
||||
cancelText: '취소',
|
||||
onOk: async () => {
|
||||
try {
|
||||
// NOTE: ids 필드명은 T19 컨트롤러 확인 후 수정 필요
|
||||
await vatApi.complete({ ids: selectedRows.map((r) => r.reportId) });
|
||||
message.success('신고완료 처리됨');
|
||||
qc.invalidateQueries({ queryKey: ['vat-reports'] });
|
||||
setSelectedRows([]);
|
||||
} catch { /* request.ts 처리 */ }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="부가세 신고 현황"
|
||||
description="분기별 매출/매입/납부 부가세 집계 및 관리"
|
||||
extra={
|
||||
<Space>
|
||||
<PermissionButton menuCode="VAT_REPORT" permCode="APPROVE" onClick={handleRegenerate}>
|
||||
재집계
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
menuCode="VAT_REPORT"
|
||||
permCode="APPROVE"
|
||||
type="primary"
|
||||
disabled={selectedRows.length === 0}
|
||||
onClick={handleComplete}
|
||||
>
|
||||
신고완료 ({selectedRows.length}건)
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{isError && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="API 미응답"
|
||||
description="/api/vat-reports 를 확인하세요."
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'text', name: 'reportYear', label: '신고연도', span: 6 },
|
||||
{ type: 'text', name: 'reportQuarter', label: '분기(1~4)', span: 6 },
|
||||
]}
|
||||
onSearch={(v) => setParams({ ...v as VatSearchParam, pageSize: 200 })}
|
||||
onReset={() => setParams({ reportYear: currentYear, reportQuarter: currentQuarter, pageSize: 200 })}
|
||||
/>
|
||||
|
||||
<ProCard style={cardStyle}>
|
||||
<DataGrid<VatReportResp>
|
||||
rows={rows}
|
||||
columns={COLUMNS}
|
||||
loading={isLoading}
|
||||
height={520}
|
||||
rowKey="reportId"
|
||||
rowSelection="multiple"
|
||||
onSelectionChanged={handleSelectionChanged}
|
||||
pinnedBottomRow={pinnedRow}
|
||||
/>
|
||||
</ProCard>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Alert, Modal, Space, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ColDef, SelectionChangedEvent } from '@ag-grid-community/core';
|
||||
import dayjs from 'dayjs';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import DataGrid from '@/components/common/DataGrid';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import {
|
||||
withholdingTaxApi,
|
||||
WithholdingTaxReportResp,
|
||||
WithholdingTaxSearchParam,
|
||||
} from '@/api/withholdingTax';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
|
||||
const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-';
|
||||
|
||||
const COLUMNS: ColDef<WithholdingTaxReportResp>[] = [
|
||||
{ field: 'reportYear', headerName: '신고연도', width: 100, pinned: 'left' },
|
||||
{ field: 'reportQuarter', headerName: '분기', width: 80, pinned: 'left', valueFormatter: (p) => `${p.value}Q` },
|
||||
{ field: 'agentCode', headerName: '설계사코드', width: 120 },
|
||||
{ field: 'agentName', headerName: '설계사명', width: 120 },
|
||||
{ field: 'orgName', headerName: '소속조직', width: 160 },
|
||||
{ field: 'totalIncome', headerName: '지급총액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'withholdingTax', headerName: '원천징수세', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'statusName', headerName: '상태', width: 90 },
|
||||
{ field: 'reportedAt', headerName: '신고일시', width: 140 },
|
||||
];
|
||||
|
||||
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||
|
||||
export default function WithholdingTaxReportList() {
|
||||
const qc = useQueryClient();
|
||||
const currentYear = dayjs().year();
|
||||
const currentQuarter = Math.ceil((dayjs().month() + 1) / 3);
|
||||
|
||||
const [params, setParams] = useState<WithholdingTaxSearchParam>({
|
||||
reportYear: currentYear,
|
||||
reportQuarter: currentQuarter,
|
||||
pageSize: 500,
|
||||
});
|
||||
const [selectedRows, setSelectedRows] = useState<WithholdingTaxReportResp[]>([]);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['withholding-tax', params],
|
||||
queryFn: () => withholdingTaxApi.list(params),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const rows = data?.list ?? [];
|
||||
|
||||
const pinnedRow = rows.length > 0 ? {
|
||||
agentName: '합계',
|
||||
totalIncome: rows.reduce((s, r) => s + (r.totalIncome ?? 0), 0),
|
||||
withholdingTax: rows.reduce((s, r) => s + (r.withholdingTax ?? 0), 0),
|
||||
} as Partial<WithholdingTaxReportResp> : undefined;
|
||||
|
||||
const handleSelectionChanged = useCallback((e: SelectionChangedEvent<WithholdingTaxReportResp>) => {
|
||||
setSelectedRows(e.api.getSelectedRows());
|
||||
}, []);
|
||||
|
||||
const handleRegenerate = () => {
|
||||
Modal.confirm({
|
||||
title: '원천세 재집계',
|
||||
content: `${params.reportYear}년 ${params.reportQuarter}Q 데이터를 재집계하시겠습니까?`,
|
||||
okText: '재집계',
|
||||
cancelText: '취소',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await withholdingTaxApi.regenerate({
|
||||
reportYear: params.reportYear ?? currentYear,
|
||||
reportQuarter: params.reportQuarter ?? currentQuarter,
|
||||
});
|
||||
message.success('재집계 완료');
|
||||
qc.invalidateQueries({ queryKey: ['withholding-tax'] });
|
||||
} catch { /* request.ts 처리 */ }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
Modal.confirm({
|
||||
title: '신고완료 처리',
|
||||
content: `선택한 ${selectedRows.length}건을 신고완료 처리하시겠습니까?`,
|
||||
okText: '신고완료',
|
||||
cancelText: '취소',
|
||||
onOk: async () => {
|
||||
try {
|
||||
// NOTE: ids 필드명은 T18 컨트롤러 확인 후 수정 필요
|
||||
await withholdingTaxApi.complete({ ids: selectedRows.map((r) => r.reportId) });
|
||||
message.success('신고완료 처리됨');
|
||||
qc.invalidateQueries({ queryKey: ['withholding-tax'] });
|
||||
setSelectedRows([]);
|
||||
} catch { /* request.ts 처리 */ }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="원천세 신고 현황"
|
||||
description="설계사별 분기 원천세 신고 집계 및 관리"
|
||||
extra={
|
||||
<Space>
|
||||
<PermissionButton menuCode="WITHHOLDING_TAX" permCode="APPROVE" onClick={handleRegenerate}>
|
||||
재집계
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
menuCode="WITHHOLDING_TAX"
|
||||
permCode="APPROVE"
|
||||
type="primary"
|
||||
disabled={selectedRows.length === 0}
|
||||
onClick={handleComplete}
|
||||
>
|
||||
신고완료 ({selectedRows.length}건)
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{isError && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="API 미응답"
|
||||
description="/api/withholding-tax-reports 를 확인하세요."
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'text', name: 'reportYear', label: '신고연도', span: 6 },
|
||||
{ type: 'text', name: 'reportQuarter', label: '분기(1~4)', span: 6 },
|
||||
{ type: 'text', name: 'agentName', label: '설계사명', span: 6 },
|
||||
]}
|
||||
onSearch={(v) => setParams({ ...v as WithholdingTaxSearchParam, pageSize: 500 })}
|
||||
onReset={() => setParams({ reportYear: currentYear, reportQuarter: currentQuarter, pageSize: 500 })}
|
||||
/>
|
||||
|
||||
<ProCard style={cardStyle}>
|
||||
<DataGrid<WithholdingTaxReportResp>
|
||||
rows={rows}
|
||||
columns={COLUMNS}
|
||||
loading={isLoading}
|
||||
height={620}
|
||||
rowKey="reportId"
|
||||
rowSelection="multiple"
|
||||
onSelectionChanged={handleSelectionChanged}
|
||||
pinnedBottomRow={pinnedRow}
|
||||
/>
|
||||
</ProCard>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user