feat(frontend): 헤더 단순화 + 등록/수정 모달 도입
헤더 깨짐 수정 (MainLayout): - logo 를 div 박스 대신 단순 img(/vite.svg) 로 (ProLayout 기본 슬롯 호환) - avatarProps.render 의 chevron 제거 → ProLayout 기본 dom 만 Dropdown 트리거 - token.header 의 잘못된 키(colorBgRightActionsItemHover) 제거, heightLayoutHeader 56 으로 통일 - actionsRender 의 정산월 영역을 Space + 라벨 조합으로 정리 등록/수정 모달 (DrawerForm / ModalForm): - AgentList: DrawerForm (680px) — 설계사명 / 사번 / 소속(Select) / 직급 / 전화 / 이메일 / 주민번호(암호화) / 은행 / 계좌(암호화) / 입사일, 수정 시 detail API 자동 fetch + initialValues 주입, 액션 컬럼: 수정/퇴사 (Modal.confirm) - ContractList: ModalForm (680px) — 증권번호 / 설계사(Select) / 상품(Select) / 계약자 / 피보험자 / 보험료 / 납입주기 / 계약일 / 효력발생일 toolBarRender 등록 버튼 추가 - ExceptionLedger: ModalForm (620px) — 설계사 / 정산월 / 예외코드 / 금액 / 증권번호 / 보험사 / 내용 toolBarRender 등록 버튼 추가, 예외코드 옵션은 ruleApi 에서 동적 로드 api/contract.ts: ContractSaveReq + create/update 추가 검증: tsc -b 통과 / dev 서버 HMR 정상 반영 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,8 +33,24 @@ export interface ContractSearchParam {
|
|||||||
endDate?: string;
|
endDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContractSaveReq {
|
||||||
|
agentId: number;
|
||||||
|
productId: number;
|
||||||
|
policyNo: string;
|
||||||
|
contractorName?: string;
|
||||||
|
insuredName?: string;
|
||||||
|
premium?: number;
|
||||||
|
payCycle?: string;
|
||||||
|
payPeriod?: number;
|
||||||
|
coveragePeriod?: number;
|
||||||
|
contractDate?: string;
|
||||||
|
effectiveDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const contractApi = {
|
export const contractApi = {
|
||||||
list: (p: ContractSearchParam) =>
|
list: (p: ContractSearchParam) =>
|
||||||
unwrap<PageResponse<ContractResp>>(api.get('/api/contracts', { params: p })),
|
unwrap<PageResponse<ContractResp>>(api.get('/api/contracts', { params: p })),
|
||||||
detail: (id: number) => unwrap<ContractResp>(api.get(`/api/contracts/${id}`)),
|
detail: (id: number) => unwrap<ContractResp>(api.get(`/api/contracts/${id}`)),
|
||||||
|
create: (req: ContractSaveReq) => unwrap<number>(api.post('/api/contracts', req)),
|
||||||
|
update: (id: number, req: ContractSaveReq) => unwrap<void>(api.put(`/api/contracts/${id}`, req)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ProLayout, type MenuDataItem } from '@ant-design/pro-components';
|
import { ProLayout, type MenuDataItem } from '@ant-design/pro-components';
|
||||||
import { Avatar, Badge, Button, DatePicker, Dropdown, Space } from 'antd';
|
import { Badge, Button, DatePicker, Dropdown, Space } from 'antd';
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { IconBell, IconLogout, IconUser, IconChevronDown } from '@tabler/icons-react';
|
import { IconBell, IconLogout, IconUser } from '@tabler/icons-react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { menuApi, type MenuResp } from '@/api/menu';
|
import { menuApi, type MenuResp } from '@/api/menu';
|
||||||
import { authApi } from '@/api/auth';
|
import { authApi } from '@/api/auth';
|
||||||
@@ -46,16 +46,7 @@ export default function MainLayout() {
|
|||||||
return (
|
return (
|
||||||
<ProLayout
|
<ProLayout
|
||||||
title="GA 정산시스템"
|
title="GA 정산시스템"
|
||||||
logo={
|
logo="/vite.svg"
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 28, height: 28, borderRadius: 6,
|
|
||||||
background: 'linear-gradient(135deg, #1677FF 0%, #0C447C 100%)',
|
|
||||||
color: '#fff', display: 'flex', alignItems: 'center',
|
|
||||||
justifyContent: 'center', fontWeight: 800, fontSize: 13,
|
|
||||||
}}
|
|
||||||
>GA</div>
|
|
||||||
}
|
|
||||||
layout="mix"
|
layout="mix"
|
||||||
navTheme="light"
|
navTheme="light"
|
||||||
contentWidth="Fluid"
|
contentWidth="Fluid"
|
||||||
@@ -73,29 +64,22 @@ export default function MainLayout() {
|
|||||||
breadcrumbRender={(routers = []) => routers}
|
breadcrumbRender={(routers = []) => routers}
|
||||||
onMenuHeaderClick={() => navigate('/')}
|
onMenuHeaderClick={() => navigate('/')}
|
||||||
token={{
|
token={{
|
||||||
// ProLayout 디자인 토큰 — antd pro 컬러 시스템
|
|
||||||
sider: {
|
sider: {
|
||||||
colorMenuBackground: '#ffffff',
|
colorMenuBackground: '#ffffff',
|
||||||
colorTextMenuSelected: '#185FA5',
|
colorTextMenuSelected: '#185FA5',
|
||||||
colorBgMenuItemSelected: '#E6F1FB',
|
colorBgMenuItemSelected: '#E6F1FB',
|
||||||
colorTextMenuItemHover: '#1677FF',
|
colorTextMenuItemHover: '#1677FF',
|
||||||
colorMenuItemDivider: '#f0f0f0',
|
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
colorBgHeader: '#ffffff',
|
colorBgHeader: '#ffffff',
|
||||||
colorHeaderTitle: '#185FA5',
|
colorHeaderTitle: '#185FA5',
|
||||||
colorBgRightActionsItemHover: '#f5f7fa',
|
heightLayoutHeader: 56,
|
||||||
heightLayoutHeader: 48,
|
|
||||||
},
|
},
|
||||||
bgLayout: '#f5f7fa',
|
bgLayout: '#f5f7fa',
|
||||||
pageContainer: {
|
|
||||||
paddingBlockPageContainerContent: 16,
|
|
||||||
paddingInlinePageContainerContent: 20,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
actionsRender={() => [
|
actionsRender={() => [
|
||||||
<Space key="month" size={6}>
|
<Space key="month" size={4} align="center">
|
||||||
<span style={{ fontSize: 12, color: '#999' }}>정산월</span>
|
<span style={{ fontSize: 12, color: '#666' }}>정산월</span>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
picker="month"
|
picker="month"
|
||||||
value={settleMonth}
|
value={settleMonth}
|
||||||
@@ -103,7 +87,7 @@ export default function MainLayout() {
|
|||||||
allowClear={false}
|
allowClear={false}
|
||||||
format="YYYY-MM"
|
format="YYYY-MM"
|
||||||
size="small"
|
size="small"
|
||||||
style={{ width: 110 }}
|
style={{ width: 100 }}
|
||||||
/>
|
/>
|
||||||
</Space>,
|
</Space>,
|
||||||
<Badge key="bell" count={3} size="small" offset={[-2, 4]}>
|
<Badge key="bell" count={3} size="small" offset={[-2, 4]}>
|
||||||
@@ -111,11 +95,9 @@ export default function MainLayout() {
|
|||||||
</Badge>,
|
</Badge>,
|
||||||
]}
|
]}
|
||||||
avatarProps={{
|
avatarProps={{
|
||||||
src: undefined,
|
|
||||||
size: 'small',
|
size: 'small',
|
||||||
title: auth.userName ?? auth.loginId ?? '관리자',
|
title: auth.userName ?? auth.loginId ?? '관리자',
|
||||||
icon: <IconUser size={14} />,
|
icon: <IconUser size={14} />,
|
||||||
style: { background: 'linear-gradient(135deg, #1677FF 0%, #185FA5 100%)' },
|
|
||||||
render: (_props, dom) => (
|
render: (_props, dom) => (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
@@ -124,10 +106,7 @@ export default function MainLayout() {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Space style={{ cursor: 'pointer' }}>
|
{dom}
|
||||||
{dom}
|
|
||||||
<IconChevronDown size={12} color="#999" />
|
|
||||||
</Space>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { Modal, Space, message } from 'antd';
|
import { Modal, Space, message } from 'antd';
|
||||||
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
import {
|
||||||
|
ModalForm, ProForm, ProFormDigit, ProFormSelect, ProFormText, ProFormTextArea,
|
||||||
|
ProTable, type ActionType, type ProColumns,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import PageContainer from '@/components/common/PageContainer';
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
import CodeBadge from '@/components/common/CodeBadge';
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
import MoneyText from '@/components/common/MoneyText';
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
import PermissionButton from '@/components/common/PermissionButton';
|
import PermissionButton from '@/components/common/PermissionButton';
|
||||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||||
import { exceptionApi, ExceptionLedgerResp } from '@/api/exceptionLedger';
|
import { exceptionApi, ExceptionLedgerResp, ExceptionLedgerSaveReq } from '@/api/exceptionLedger';
|
||||||
|
import { agentApi } from '@/api/agent';
|
||||||
|
import { ruleApi } from '@/api/rule';
|
||||||
|
|
||||||
export default function ExceptionLedger() {
|
export default function ExceptionLedger() {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const { data: approveCodes = [] } = useCommonCodes('APPROVE_STATUS');
|
const { data: approveCodes = [] } = useCommonCodes('APPROVE_STATUS');
|
||||||
|
|
||||||
|
const { data: agents } = useQuery({
|
||||||
|
queryKey: ['agents', 'all'], queryFn: () => agentApi.list({ pageSize: 999 }), enabled: open,
|
||||||
|
});
|
||||||
|
const { data: excCodes = [] } = useQuery({
|
||||||
|
queryKey: ['rules', 'exceptions'], queryFn: ruleApi.exceptionCodes, enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
const onApprove = (id: number, status: 'APPROVED' | 'REJECTED') =>
|
const onApprove = (id: number, status: 'APPROVED' | 'REJECTED') =>
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: status === 'APPROVED' ? '승인하시겠습니까?' : '반려하시겠습니까?',
|
title: status === 'APPROVED' ? '승인하시겠습니까?' : '반려하시겠습니까?',
|
||||||
@@ -22,6 +36,15 @@ export default function ExceptionLedger() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (v: any) => {
|
||||||
|
const req: ExceptionLedgerSaveReq = { ...v };
|
||||||
|
await exceptionApi.create(req);
|
||||||
|
message.success('등록 완료. 승인 대기 상태입니다.');
|
||||||
|
setOpen(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const columns: ProColumns<ExceptionLedgerResp>[] = [
|
const columns: ProColumns<ExceptionLedgerResp>[] = [
|
||||||
{ title: '정산월', dataIndex: 'settleMonth', width: 90, valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' } },
|
{ title: '정산월', dataIndex: 'settleMonth', width: 90, valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' } },
|
||||||
{
|
{
|
||||||
@@ -78,8 +101,49 @@ export default function ExceptionLedger() {
|
|||||||
}}
|
}}
|
||||||
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<PermissionButton key="create" menuCode="LEDGER_EXCEPTION" permCode="CREATE" type="primary"
|
||||||
|
onClick={() => setOpen(true)}>+ 예외금액 등록</PermissionButton>,
|
||||||
|
]}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ModalForm
|
||||||
|
title="예외금액 등록"
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
width={620}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onSubmit}
|
||||||
|
submitter={{ searchConfig: { submitText: '등록', resetText: '취소' } }}
|
||||||
|
modalProps={{ destroyOnClose: true, maskClosable: false }}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
name="agentId" label="설계사" rules={[{ required: true }]} width="md" showSearch
|
||||||
|
options={agents?.list.map((a) => ({ value: a.agentId, label: `${a.agentName} (${a.licenseNo ?? '-'})` })) ?? []}
|
||||||
|
/>
|
||||||
|
<ProFormText
|
||||||
|
name="settleMonth" label="정산월 (YYYYMM)" rules={[{ required: true, pattern: /^\d{6}$/ }]}
|
||||||
|
width="md" placeholder="202605"
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
name="exceptionCode" label="예외코드" rules={[{ required: true }]} width="md"
|
||||||
|
options={excCodes.map((c) => ({ value: c.exceptionCode, label: `${c.exceptionName} (${c.direction})` }))}
|
||||||
|
/>
|
||||||
|
<ProFormDigit
|
||||||
|
name="amount" label="금액" rules={[{ required: true }]} width="md" min={1}
|
||||||
|
fieldProps={{ formatter: (v) => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',') }}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText name="policyNo" label="증권번호 (선택)" width="md" />
|
||||||
|
<ProFormText name="companyCode" label="보험사코드 (선택)" width="md" />
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProFormTextArea name="description" label="내용" placeholder="예외 사유 / 참조 문서" />
|
||||||
|
</ModalForm>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,72 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { Space } from 'antd';
|
import { Modal, Space, message } from 'antd';
|
||||||
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
import {
|
||||||
|
DrawerForm, ProForm, ProFormDatePicker, ProFormSelect, ProFormText,
|
||||||
|
ProTable, type ActionType, type ProColumns,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import CodeBadge from '@/components/common/CodeBadge';
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
import MoneyText from '@/components/common/MoneyText';
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
import PermissionButton from '@/components/common/PermissionButton';
|
import PermissionButton from '@/components/common/PermissionButton';
|
||||||
import ExcelExportButton from '@/components/common/ExcelExportButton';
|
import ExcelExportButton from '@/components/common/ExcelExportButton';
|
||||||
import PageContainer from '@/components/common/PageContainer';
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||||
import { agentApi, AgentResp, AgentSearchParam } from '@/api/agent';
|
import { agentApi, AgentResp, AgentSaveReq, AgentSearchParam } from '@/api/agent';
|
||||||
|
import { orgApi } from '@/api/org';
|
||||||
|
|
||||||
export default function AgentList() {
|
export default function AgentList() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const { data: statusCodes = [] } = useCommonCodes('AGENT_STATUS');
|
const { data: statusCodes = [] } = useCommonCodes('AGENT_STATUS');
|
||||||
|
const { data: bankCodes = [] } = useCommonCodes('BANK_CODE');
|
||||||
|
|
||||||
|
const [drawer, setDrawer] = useState<{ open: boolean; editId?: number }>({ open: false });
|
||||||
|
|
||||||
|
const { data: orgList = [] } = useQuery({
|
||||||
|
queryKey: ['orgs', 'list'],
|
||||||
|
queryFn: orgApi.list,
|
||||||
|
});
|
||||||
|
const orgOptions = orgList.map((o) => ({ value: o.orgId, label: `${o.orgName}` }));
|
||||||
|
const gradeOptions = [1, 2, 3, 4, 5, 6].map((id) => ({ value: id, label: `직급 #${id}` }));
|
||||||
|
|
||||||
|
// 수정 시 상세 조회
|
||||||
|
const { data: editing } = useQuery({
|
||||||
|
queryKey: ['agent', 'detail', drawer.editId],
|
||||||
|
queryFn: () => agentApi.detail(drawer.editId!),
|
||||||
|
enabled: !!drawer.editId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (values: any) => {
|
||||||
|
const req: AgentSaveReq = {
|
||||||
|
...values,
|
||||||
|
joinDate: values.joinDate ? dayjs(values.joinDate).format('YYYY-MM-DD') : undefined,
|
||||||
|
};
|
||||||
|
if (drawer.editId) {
|
||||||
|
await agentApi.update(drawer.editId, req);
|
||||||
|
message.success('수정 완료');
|
||||||
|
} else {
|
||||||
|
await agentApi.create(req);
|
||||||
|
message.success('등록 완료');
|
||||||
|
}
|
||||||
|
setDrawer({ open: false });
|
||||||
|
actionRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (id: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '설계사 삭제',
|
||||||
|
content: '관련 계약/원장이 있으면 실패합니다. 진행하시겠습니까?',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: async () => {
|
||||||
|
await agentApi.changeStatus(id, 'LEAVE');
|
||||||
|
message.success('퇴사 처리 완료');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const columns: ProColumns<AgentResp>[] = [
|
const columns: ProColumns<AgentResp>[] = [
|
||||||
{
|
{
|
||||||
@@ -42,6 +95,15 @@ export default function AgentList() {
|
|||||||
render: (_, r) => <MoneyText value={r.totalCommission} />,
|
render: (_, r) => <MoneyText value={r.totalCommission} />,
|
||||||
},
|
},
|
||||||
{ title: '입사일', dataIndex: 'joinDate', width: 110, search: false },
|
{ title: '입사일', dataIndex: 'joinDate', width: 110, search: false },
|
||||||
|
{
|
||||||
|
title: '액션', valueType: 'option', width: 140, fixed: 'right',
|
||||||
|
render: (_, r) => [
|
||||||
|
<PermissionButton key="edit" menuCode="ORG_AGENT" permCode="UPDATE" size="small" type="link"
|
||||||
|
onClick={() => setDrawer({ open: true, editId: r.agentId })}>수정</PermissionButton>,
|
||||||
|
<PermissionButton key="del" menuCode="ORG_AGENT" permCode="DELETE" size="small" type="link" danger
|
||||||
|
onClick={() => onDelete(r.agentId)}>퇴사</PermissionButton>,
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||||
fieldProps: { placeholder: '설계사명' },
|
fieldProps: { placeholder: '설계사명' },
|
||||||
@@ -57,37 +119,78 @@ export default function AgentList() {
|
|||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey="agentId"
|
rowKey="agentId"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1300 }}
|
||||||
request={async (params) => {
|
request={async (params) => {
|
||||||
const { current, pageSize, ...rest } = params as any;
|
const { current, pageSize, ...rest } = params as any;
|
||||||
const res = await agentApi.list({ ...rest, pageNum: current, pageSize });
|
const res = await agentApi.list({ ...rest, pageNum: current, pageSize });
|
||||||
return { data: res.list, total: res.total, success: true };
|
return { data: res.list, total: res.total, success: true };
|
||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
pageSize: 20,
|
pageSize: 20, showSizeChanger: true, pageSizeOptions: ['20', '50', '100'],
|
||||||
showSizeChanger: true,
|
|
||||||
pageSizeOptions: ['20', '50', '100'],
|
|
||||||
showTotal: (total) => `총 ${total.toLocaleString()}건`,
|
showTotal: (total) => `총 ${total.toLocaleString()}건`,
|
||||||
}}
|
}}
|
||||||
search={{
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
labelWidth: 'auto',
|
|
||||||
searchText: '검색',
|
|
||||||
resetText: '초기화',
|
|
||||||
collapsed: false,
|
|
||||||
collapseRender: false,
|
|
||||||
}}
|
|
||||||
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
toolBarRender={() => [
|
toolBarRender={() => [
|
||||||
<ExcelExportButton key="export" url="/api/agents/export" fileName="설계사목록" />,
|
<ExcelExportButton key="export" url="/api/agents/export" fileName="설계사목록" />,
|
||||||
<PermissionButton
|
<PermissionButton key="create" menuCode="ORG_AGENT" permCode="CREATE" type="primary"
|
||||||
key="create"
|
onClick={() => setDrawer({ open: true })}>+ 설계사 등록</PermissionButton>,
|
||||||
menuCode="ORG_AGENT" permCode="CREATE" type="primary"
|
|
||||||
onClick={() => navigate('/org/agents/new')}
|
|
||||||
>+ 설계사 등록</PermissionButton>,
|
|
||||||
]}
|
]}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
headerTitle={<Space size={6}><span style={{ fontWeight: 600 }}>설계사 목록</span></Space>}
|
headerTitle={<Space size={6}><span style={{ fontWeight: 600 }}>설계사 목록</span></Space>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 등록/수정 Drawer */}
|
||||||
|
<DrawerForm
|
||||||
|
title={drawer.editId ? '설계사 수정' : '설계사 등록'}
|
||||||
|
open={drawer.open}
|
||||||
|
onOpenChange={(o) => !o && setDrawer({ open: false })}
|
||||||
|
width={680}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={drawer.editId && editing ? {
|
||||||
|
...editing,
|
||||||
|
joinDate: editing.joinDate ? dayjs(editing.joinDate) : null,
|
||||||
|
} : undefined}
|
||||||
|
// 수정으로 전환 시 form 강제 갱신
|
||||||
|
key={drawer.editId ?? 'new'}
|
||||||
|
onFinish={onSubmit}
|
||||||
|
submitter={{ searchConfig: { submitText: drawer.editId ? '수정' : '등록', resetText: '취소' } }}
|
||||||
|
drawerProps={{ destroyOnClose: true, maskClosable: false }}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText
|
||||||
|
name="agentName" label="설계사명" rules={[{ required: true, message: '필수입니다' }]}
|
||||||
|
width="md" placeholder="홍길동"
|
||||||
|
/>
|
||||||
|
<ProFormText name="licenseNo" label="사번" width="md" placeholder="L00123" />
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
name="orgId" label="소속" rules={[{ required: true }]}
|
||||||
|
width="md" options={orgOptions} showSearch
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
name="gradeId" label="직급" rules={[{ required: true }]}
|
||||||
|
width="md" options={gradeOptions}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText name="phone" label="전화번호" width="md" placeholder="010-1234-5678" />
|
||||||
|
<ProFormText
|
||||||
|
name="email" label="이메일" width="md" placeholder="x@ga.com"
|
||||||
|
rules={[{ type: 'email', message: '이메일 형식이 아닙니다' }]}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProFormText name="residentNo" label="주민번호 (저장 시 자동 암호화)" placeholder="900101-1234567" />
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
name="bankCode" label="은행" width="md"
|
||||||
|
options={bankCodes.map((c) => ({ value: c.code, label: c.codeName }))}
|
||||||
|
/>
|
||||||
|
<ProFormText name="accountNo" label="계좌번호 (저장 시 자동 암호화)" width="md" />
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProFormDatePicker name="joinDate" label="입사일" />
|
||||||
|
</DrawerForm>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,55 @@
|
|||||||
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
import { useRef, useState } from 'react';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import {
|
||||||
|
ModalForm, ProForm, ProFormDatePicker, ProFormDigit, ProFormSelect, ProFormText,
|
||||||
|
ProTable, type ActionType, type ProColumns,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import PageContainer from '@/components/common/PageContainer';
|
import PageContainer from '@/components/common/PageContainer';
|
||||||
import CodeBadge from '@/components/common/CodeBadge';
|
import CodeBadge from '@/components/common/CodeBadge';
|
||||||
import MoneyText from '@/components/common/MoneyText';
|
import MoneyText from '@/components/common/MoneyText';
|
||||||
|
import PermissionButton from '@/components/common/PermissionButton';
|
||||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||||
import { contractApi, ContractResp, ContractSearchParam } from '@/api/contract';
|
import { contractApi, ContractResp, ContractSaveReq, ContractSearchParam } from '@/api/contract';
|
||||||
|
import { productApi } from '@/api/product';
|
||||||
|
import { agentApi } from '@/api/agent';
|
||||||
|
|
||||||
export default function ContractList() {
|
export default function ContractList() {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
||||||
const { data: statusCodes = [] } = useCommonCodes('CONTRACT_STATUS');
|
const { data: statusCodes = [] } = useCommonCodes('CONTRACT_STATUS');
|
||||||
|
const { data: payCycleCodes = [] } = useCommonCodes('PAY_CYCLE');
|
||||||
|
|
||||||
|
// 등록 모달용 - 상품/설계사 옵션
|
||||||
|
const { data: products } = useQuery({
|
||||||
|
queryKey: ['products', 'all'],
|
||||||
|
queryFn: () => productApi.list({ pageSize: 999 }),
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
const { data: agents } = useQuery({
|
||||||
|
queryKey: ['agents', 'all'],
|
||||||
|
queryFn: () => agentApi.list({ pageSize: 999 }),
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (v: any) => {
|
||||||
|
const req: ContractSaveReq = {
|
||||||
|
...v,
|
||||||
|
contractDate: v.contractDate ? dayjs(v.contractDate).format('YYYY-MM-DD') : undefined,
|
||||||
|
effectiveDate: v.effectiveDate ? dayjs(v.effectiveDate).format('YYYY-MM-DD') : undefined,
|
||||||
|
};
|
||||||
|
await contractApi.create(req);
|
||||||
|
message.success('등록 완료');
|
||||||
|
setOpen(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const columns: ProColumns<ContractResp>[] = [
|
const columns: ProColumns<ContractResp>[] = [
|
||||||
{
|
{ title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||||
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
fieldProps: { placeholder: '계약자/증권번호' } },
|
||||||
fieldProps: { placeholder: '계약자/증권번호' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '보험종류', dataIndex: 'insuranceType', width: 100, valueType: 'select',
|
title: '보험종류', dataIndex: 'insuranceType', width: 100, valueType: 'select',
|
||||||
valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
|
valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||||
@@ -43,6 +79,7 @@ export default function ContractList() {
|
|||||||
return (
|
return (
|
||||||
<PageContainer title="계약 관리" description="보험사·상품·계약 통합 관리">
|
<PageContainer title="계약 관리" description="보험사·상품·계약 통합 관리">
|
||||||
<ProTable<ContractResp, ContractSearchParam>
|
<ProTable<ContractResp, ContractSearchParam>
|
||||||
|
actionRef={actionRef}
|
||||||
rowKey="contractId"
|
rowKey="contractId"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
scroll={{ x: 1300 }}
|
scroll={{ x: 1300 }}
|
||||||
@@ -52,15 +89,56 @@ export default function ContractList() {
|
|||||||
return { data: res.list, total: res.total, success: true };
|
return { data: res.list, total: res.total, success: true };
|
||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
pageSize: 20,
|
pageSize: 20, showSizeChanger: true, pageSizeOptions: ['20', '50', '100'],
|
||||||
showSizeChanger: true,
|
|
||||||
pageSizeOptions: ['20', '50', '100'],
|
|
||||||
showTotal: (total) => `총 ${total.toLocaleString()}건`,
|
showTotal: (total) => `총 ${total.toLocaleString()}건`,
|
||||||
}}
|
}}
|
||||||
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||||
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<PermissionButton key="create" menuCode="CONTRACT_LIST" permCode="CREATE" type="primary"
|
||||||
|
onClick={() => setOpen(true)}>+ 계약 등록</PermissionButton>,
|
||||||
|
]}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ModalForm
|
||||||
|
title="계약 등록"
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
width={680}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onSubmit}
|
||||||
|
submitter={{ searchConfig: { submitText: '등록', resetText: '취소' } }}
|
||||||
|
modalProps={{ destroyOnClose: true, maskClosable: false }}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText name="policyNo" label="증권번호" rules={[{ required: true }]} width="md" />
|
||||||
|
<ProFormSelect
|
||||||
|
name="agentId" label="설계사" rules={[{ required: true }]} width="md" showSearch
|
||||||
|
options={agents?.list.map((a) => ({ value: a.agentId, label: `${a.agentName} (${a.licenseNo ?? '-'})` })) ?? []}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProFormSelect
|
||||||
|
name="productId" label="상품" rules={[{ required: true }]} showSearch
|
||||||
|
options={products?.list.map((p) => ({ value: p.productId, label: `${p.productName} — ${p.companyName}` })) ?? []}
|
||||||
|
/>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText name="contractorName" label="계약자" width="md" />
|
||||||
|
<ProFormText name="insuredName" label="피보험자" width="md" />
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormDigit name="premium" label="보험료" width="md" min={0}
|
||||||
|
fieldProps={{ formatter: (v) => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',') }} />
|
||||||
|
<ProFormSelect
|
||||||
|
name="payCycle" label="납입주기" width="md"
|
||||||
|
options={payCycleCodes.map((c) => ({ value: c.code, label: c.codeName }))}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormDatePicker name="contractDate" label="계약일" width="md" />
|
||||||
|
<ProFormDatePicker name="effectiveDate" label="효력발생일" width="md" />
|
||||||
|
</ProForm.Group>
|
||||||
|
</ModalForm>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user