1
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { Modal, Space, message } from 'antd';
|
||||
import {
|
||||
ModalForm,
|
||||
ProForm,
|
||||
ProFormDigit,
|
||||
ProFormSelect,
|
||||
ProFormText,
|
||||
ProTable,
|
||||
type ActionType,
|
||||
type ProColumns,
|
||||
} from '@ant-design/pro-components';
|
||||
import dayjs from 'dayjs';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import CodeBadge from '@/components/common/CodeBadge';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import {
|
||||
deductionApi,
|
||||
DeductionResp,
|
||||
DeductionSaveReq,
|
||||
DeductionSearchParam,
|
||||
} from '@/api/deduction';
|
||||
|
||||
// ── 상수 ─────────────────────────────────────────────────────────────────
|
||||
const MENU_CODE = 'DEDUCTION';
|
||||
|
||||
// YYYYMM 유효성 검사
|
||||
function isValidYYYYMM(v: string): boolean {
|
||||
if (!/^\d{6}$/.test(v)) return false;
|
||||
const year = parseInt(v.slice(0, 4), 10);
|
||||
const month = parseInt(v.slice(4, 6), 10);
|
||||
return year >= 2000 && year <= 2099 && month >= 1 && month <= 12;
|
||||
}
|
||||
|
||||
// ── 컴포넌트 ──────────────────────────────────────────────────────────────
|
||||
export default function DeductionList() {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modal, setModal] = useState<{ open: boolean; editRecord?: DeductionResp }>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
const { data: statusCodes = [] } = useCommonCodes('DEDUCTION_STATUS');
|
||||
const { data: typeCodes = [] } = useCommonCodes('DEDUCTION_TYPE');
|
||||
|
||||
// ── 열기 ───────────────────────────────────────────────────────────────
|
||||
const openCreate = () => setModal({ open: true });
|
||||
const openEdit = (record: DeductionResp) => setModal({ open: true, editRecord: record });
|
||||
|
||||
// ── 취소 ───────────────────────────────────────────────────────────────
|
||||
const onCancel = (record: DeductionResp) => {
|
||||
Modal.confirm({
|
||||
title: '공제 취소',
|
||||
content: `[${record.agentName}] ${record.settleMonth} 공제를 취소하시겠습니까?`,
|
||||
okType: 'danger',
|
||||
okText: '취소 처리',
|
||||
cancelText: '닫기',
|
||||
onOk: async () => {
|
||||
await deductionApi.cancel(record.deductionId);
|
||||
message.success('취소 완료');
|
||||
actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ── 저장 ───────────────────────────────────────────────────────────────
|
||||
const onSubmit = async (values: Record<string, unknown>) => {
|
||||
const req: DeductionSaveReq = {
|
||||
agentId: values.agentId as number,
|
||||
settleMonth: values.settleMonth as string,
|
||||
deductionType: values.deductionType as string,
|
||||
amount: values.amount as number,
|
||||
description: values.description as string | undefined,
|
||||
};
|
||||
|
||||
if (modal.editRecord) {
|
||||
await deductionApi.update(modal.editRecord.deductionId, req);
|
||||
message.success('수정 완료');
|
||||
} else {
|
||||
await deductionApi.create(req);
|
||||
message.success('등록 완료');
|
||||
}
|
||||
setModal({ open: false });
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
};
|
||||
|
||||
// ── 컬럼 ───────────────────────────────────────────────────────────────
|
||||
const columns: ProColumns<DeductionResp>[] = [
|
||||
{
|
||||
title: '설계사명',
|
||||
dataIndex: 'agentName',
|
||||
width: 110,
|
||||
fixed: 'left',
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
title: '설계사ID',
|
||||
dataIndex: 'agentId',
|
||||
width: 90,
|
||||
hideInTable: true,
|
||||
valueType: 'digit',
|
||||
},
|
||||
{
|
||||
title: '정산월',
|
||||
dataIndex: 'settleMonth',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '차감유형',
|
||||
dataIndex: 'deductionType',
|
||||
width: 120,
|
||||
valueType: 'select',
|
||||
valueEnum: Object.fromEntries(
|
||||
typeCodes.map((c) => [c.code, { text: c.codeName }]),
|
||||
),
|
||||
render: (_, r) => (
|
||||
<CodeBadge groupCode="DEDUCTION_TYPE" value={r.deductionType} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '금액',
|
||||
dataIndex: 'amount',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
search: false,
|
||||
render: (_, r) => <MoneyText value={r.amount} />,
|
||||
},
|
||||
{
|
||||
title: '설명',
|
||||
dataIndex: 'description',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
search: false,
|
||||
},
|
||||
{
|
||||
title: '상태',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
valueType: 'select',
|
||||
valueEnum: Object.fromEntries(
|
||||
statusCodes.map((c) => [c.code, { text: c.codeName }]),
|
||||
),
|
||||
render: (_, r) => (
|
||||
<CodeBadge groupCode="DEDUCTION_STATUS" value={r.status} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '등록일',
|
||||
dataIndex: 'createdAt',
|
||||
width: 160,
|
||||
search: false,
|
||||
render: (_, r) => dayjs(r.createdAt).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: '액션',
|
||||
valueType: 'option',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
render: (_, r) =>
|
||||
r.status !== 'CANCELLED' ? (
|
||||
<Space>
|
||||
<PermissionButton
|
||||
key="edit"
|
||||
menuCode={MENU_CODE}
|
||||
permCode="UPDATE"
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => openEdit(r)}
|
||||
>
|
||||
수정
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
key="cancel"
|
||||
menuCode={MENU_CODE}
|
||||
permCode="UPDATE"
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
onClick={() => onCancel(r)}
|
||||
>
|
||||
취소
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
// ── 렌더링 ─────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<PageContainer title="공제차감 관리" description="설계사별 정산월 공제 항목 등록 및 관리">
|
||||
<ProTable<DeductionResp, DeductionSearchParam>
|
||||
actionRef={actionRef}
|
||||
rowKey="deductionId"
|
||||
columns={columns}
|
||||
scroll={{ x: 1100 }}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, agentId, settleMonth, deductionType, status } =
|
||||
params as DeductionSearchParam & { current?: number; pageSize?: number };
|
||||
const res = await deductionApi.list({
|
||||
agentId,
|
||||
settleMonth,
|
||||
deductionType,
|
||||
status,
|
||||
pageNum: current,
|
||||
pageSize,
|
||||
});
|
||||
return { data: res.list, total: res.total, success: true };
|
||||
}}
|
||||
pagination={{
|
||||
pageSize: 30,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['20', '50', '100'],
|
||||
showTotal: (total) => `총 ${total.toLocaleString()}건`,
|
||||
}}
|
||||
search={{
|
||||
labelWidth: 'auto',
|
||||
searchText: '검색',
|
||||
resetText: '초기화',
|
||||
collapsed: false,
|
||||
collapseRender: false,
|
||||
}}
|
||||
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||
toolBarRender={() => [
|
||||
<PermissionButton
|
||||
key="create"
|
||||
menuCode={MENU_CODE}
|
||||
permCode="CREATE"
|
||||
type="primary"
|
||||
onClick={openCreate}
|
||||
>
|
||||
+ 공제 등록
|
||||
</PermissionButton>,
|
||||
]}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
|
||||
{/* ── 등록/수정 모달 ─────────────────────────────────────────────── */}
|
||||
<ModalForm
|
||||
title={modal.editRecord ? '공제 수정' : '공제 등록'}
|
||||
open={modal.open}
|
||||
onOpenChange={(o) => !o && setModal({ open: false })}
|
||||
width={520}
|
||||
layout="vertical"
|
||||
key={modal.editRecord?.deductionId ?? 'new'}
|
||||
initialValues={
|
||||
modal.editRecord
|
||||
? {
|
||||
agentId: modal.editRecord.agentId,
|
||||
settleMonth: modal.editRecord.settleMonth,
|
||||
deductionType: modal.editRecord.deductionType,
|
||||
amount: modal.editRecord.amount,
|
||||
description: modal.editRecord.description,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onFinish={onSubmit}
|
||||
submitter={{
|
||||
searchConfig: {
|
||||
submitText: modal.editRecord ? '수정' : '등록',
|
||||
resetText: '취소',
|
||||
},
|
||||
}}
|
||||
modalProps={{ destroyOnClose: true, maskClosable: false }}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormDigit
|
||||
name="agentId"
|
||||
label="설계사 ID"
|
||||
rules={[{ required: true, message: '설계사 ID를 입력하세요' }]}
|
||||
width="sm"
|
||||
min={1}
|
||||
fieldProps={{ precision: 0 }}
|
||||
/>
|
||||
<ProFormText
|
||||
name="settleMonth"
|
||||
label="정산월"
|
||||
rules={[
|
||||
{ required: true, message: '정산월을 입력하세요' },
|
||||
{
|
||||
validator: (_, v) =>
|
||||
!v || isValidYYYYMM(v)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('YYYYMM 6자리로 입력하세요 (예: 202501)')),
|
||||
},
|
||||
]}
|
||||
width="sm"
|
||||
placeholder="예: 202501"
|
||||
fieldProps={{ maxLength: 6 }}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
<ProForm.Group>
|
||||
<ProFormSelect
|
||||
name="deductionType"
|
||||
label="차감유형"
|
||||
rules={[{ required: true, message: '차감유형을 선택하세요' }]}
|
||||
width="md"
|
||||
options={typeCodes.map((c) => ({ value: c.code, label: c.codeName }))}
|
||||
/>
|
||||
<ProFormDigit
|
||||
name="amount"
|
||||
label="금액"
|
||||
rules={[
|
||||
{ required: true, message: '금액을 입력하세요' },
|
||||
{
|
||||
validator: (_, v) =>
|
||||
v === undefined || v === null || v > 0
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('금액은 양수여야 합니다')),
|
||||
},
|
||||
]}
|
||||
width="md"
|
||||
min={1}
|
||||
fieldProps={{
|
||||
precision: 0,
|
||||
formatter: (v) => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ','),
|
||||
}}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
<ProFormText
|
||||
name="description"
|
||||
label="설명"
|
||||
width="xl"
|
||||
placeholder="공제 사유를 입력하세요"
|
||||
/>
|
||||
</ModalForm>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user