332 lines
11 KiB
TypeScript
332 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|