Files
ga-commission-system/ga-frontend/src/pages/settle/DeductionList.tsx
T
ysukkyu 88e14edffd 1
2026-05-14 18:14:49 +09:00

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>
);
}