feat: P11(#1) 시상/신인시상 환수 — award_ledger 환수 처리 (PL 자율진행)
incar 유지율시상환수/신인도입유치자시상환수 대응. award_ledger(V92)는 지급만 있고
환수가 없던 갭. 정착지원금/생보운영지원/지점장수당의 clawback_amount+net_amount 패턴으로
통일(신인시상=award_type NEW_RECRUIT로 기존재 → 별도 테이블 불필요).
- DB(V114): award_ledger에 clawback_amount/net_amount 컬럼 추가 + net=award 백필 +
CHECK(clawback>=0) + status CHECK에 CLAWED_BACK 추가(pg_constraint 동적 탐색 DROP/재생성).
- core: AwardLedgerVO/Resp/SaveReq에 clawbackAmount/netAmount. AwardLedgerMapper
aggregateByAgent SUM(award_amount)→SUM(net_amount), insert에 두 컬럼, applyClawback UPDATE.
- api: AwardService.createLedger가 net=award−clawback 계산(clawback≤award 검증). 신규
applyClawback(ledgerId, {clawbackAmount,reason}) — 초과/취소상태 E411, status=CLAWED_BACK.
PUT /api/awards/ledgers/{id}/clawback.
- frontend: Award 원장 그리드에 환수액/실지급 컬럼 + 합계 + 환수처리 모달(PermissionButton UPDATE).
- AggregateStep 무변경: aggregateByAgent가 net 반환 → 환수가 other_commission 자동 차감,
net<0이면 P9 환수채권 이월에 자연 연계.
검증: build+test SUCCESSFUL, Flyway V114(schema v114). 라이브: award 100000 원장 →
clawback 30000 → net 70000·CLAWED_BACK, 초과환수 200000 → E411, aggregate net=70000 반영
확인. 테스트 데이터 정리. 스펙 docs/DOMAIN_GAP_P11.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# P11 — 시상/신인시상 환수 (#1)
|
||||
|
||||
작성: PL, 2026-06-04. 후속 갭 3종 중 두 번째.
|
||||
근거: incar `유지율시상환수`/`신인도입유치자시상환수`/`시상내역 조회`. GA award_ledger(V92)는
|
||||
지급만 있고 환수(clawback) 처리가 없음. 정착지원금/생보운영지원/지점장수당은 이미
|
||||
`clawback_amount`+`net_amount` 패턴 보유 → **award_ledger도 동일 패턴으로 통일**.
|
||||
|
||||
신인시상은 award_type='NEW_RECRUIT'로 이미 존재 → 별도 테이블 불필요. 모든 award_type에 환수 적용.
|
||||
Flyway: **V113 최신 → V114**. 금액 NUMERIC(15,2), HALF_UP.
|
||||
|
||||
## 1. 현재 award_ledger (V92)
|
||||
ledger_id / agent_id / settle_month / award_type(NEW_RECRUIT/CORPORATE/RETENTION/PERFORMANCE) /
|
||||
base_amount / award_amount / status(CALCULATED/CONFIRMED/PAID/CANCELLED) / created_at / updated_at.
|
||||
aggregateByAgent = SUM(award_amount) WHERE status!=CANCELLED.
|
||||
|
||||
**문제**: clawback_amount/net_amount 없음. settling/life/branch는 net=support−clawback, aggregate=SUM(net).
|
||||
|
||||
## 2. 스키마 변경 (V114) — settling_support_ledger 패턴 미러
|
||||
```sql
|
||||
ALTER TABLE award_ledger
|
||||
ADD COLUMN clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN net_amount NUMERIC(15,2) NOT NULL DEFAULT 0;
|
||||
-- 기존 행 백필: 환수 없던 과거 = net = award
|
||||
UPDATE award_ledger SET net_amount = award_amount WHERE net_amount = 0 AND clawback_amount = 0;
|
||||
-- clawback 음수 방지 + 상태에 CLAWED_BACK 추가(settling 패턴과 통일)
|
||||
ALTER TABLE award_ledger ADD CONSTRAINT chk_award_clawback CHECK (clawback_amount >= 0);
|
||||
ALTER TABLE award_ledger DROP CONSTRAINT <기존 status CHECK 이름>; -- \d 로 확인 (예: award_ledger_status_check)
|
||||
ALTER TABLE award_ledger ADD CONSTRAINT chk_award_status
|
||||
CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED','CLAWED_BACK'));
|
||||
```
|
||||
> 기존 status CHECK 제약명은 `\d award_ledger` 또는 information_schema 로 정확히 확인 후 DROP. 추정명 사용 금지.
|
||||
> net_amount는 음수 허용(환수>시상 시 음수 가능)이나, **§4 검증에서 clawback ≤ award_amount 강제**하므로 실제 net≥0. (지급성이므로 음수 차단 — settling 동일)
|
||||
|
||||
메뉴 변경 없음(award 화면 기존). 공통코드 AWARD_STATUS에 CLAWED_BACK 있으면 추가(없으면 생략).
|
||||
|
||||
## 3. Core
|
||||
- AwardLedgerVO/Resp/SaveReq에 `clawbackAmount`, `netAmount` 추가.
|
||||
- `AwardLedgerMapper.aggregateByAgent` XML: `SUM(award_amount)` → **`SUM(net_amount)`** (net=award−clawback). status!=CANCELLED 유지.
|
||||
- 신규 Mapper 메서드 `applyClawback(@Param ledgerId, @Param clawbackAmount, @Param netAmount, @Param status)` (+XML): clawback_amount/net_amount/status='CLAWED_BACK'/updated_at 갱신.
|
||||
- createLedger 시 net_amount = award_amount − clawback_amount 저장(insert XML에 두 컬럼 포함).
|
||||
|
||||
## 4. API (AwardService 확장)
|
||||
- `createLedger`: clawback_amount(SaveReq, 기본0) 받아 **clawback ≤ award_amount 검증(초과 시 E411)**, net = award − clawback 계산해 저장.
|
||||
- **신규 액션** `applyClawback(ledgerId, AwardClawbackReq{clawbackAmount, reason})` @Transactional:
|
||||
- 원장 조회(없으면 NOT_FOUND). 이미 CANCELLED면 E411.
|
||||
- clawbackAmount > award_amount 면 E411("환수액이 시상액 초과").
|
||||
- net = award_amount − clawbackAmount. mapper.applyClawback(ledgerId, clawback, net, 'CLAWED_BACK').
|
||||
- @DataChangeLog.
|
||||
- Controller: `PUT /api/awards/ledgers/{id}/clawback` (@RequirePermission(menu=AWARD, action=UPDATE)). 기존 award 컨트롤러 경로 패턴 확인.
|
||||
|
||||
> **AggregateStep 무변경**: awardMapper.aggregateByAgent가 SUM(net_amount) 반환하므로 환수가 자동으로 other_commission 차감에 반영. net<0이면 P9 환수채권 이월에 자연 연계.
|
||||
|
||||
## 5. Frontend (기존 Award 화면 확장)
|
||||
- award 원장 그리드에 환수액/실지급(net) 컬럼 추가.
|
||||
- 행 액션 "환수처리"(PermissionButton UPDATE) → 모달(환수액 입력 + 사유) → PUT /clawback. 성공 시 invalidate.
|
||||
- api 모듈에 applyClawback 추가. 기존 award api/페이지 파일명 확인 후 확장.
|
||||
|
||||
## 6. 검증(PL)
|
||||
build+test, Flyway V114(award_ledger 컬럼/제약/백필), 라이브: award 원장 1건 생성(net=award) →
|
||||
applyClawback(부분환수) → net 감소·status CLAWED_BACK 확인 → clawback>award E411 → aggregateByAgent
|
||||
가 net 반영하는지 SQL 확인. 테스트 데이터 정리.
|
||||
</content>
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ga.api.controller.commission;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 시상 원장 환수 처리 요청 (PUT /api/awards/ledgers/{id}/clawback).
|
||||
*/
|
||||
@Data
|
||||
public class AwardClawbackReq {
|
||||
|
||||
@NotNull
|
||||
@DecimalMin("0")
|
||||
private BigDecimal clawbackAmount;
|
||||
|
||||
private String reason;
|
||||
}
|
||||
@@ -79,4 +79,13 @@ public class AwardController {
|
||||
public ApiResponse<Long> createLedger(@Valid @RequestBody AwardLedgerSaveReq req) {
|
||||
return ApiResponse.ok(service.createLedger(req));
|
||||
}
|
||||
|
||||
@PutMapping("/ledgers/{ledgerId}/clawback")
|
||||
@RequirePermission(menu = "AWARD", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "AWARD", table = "award_ledger")
|
||||
public ApiResponse<Void> applyClawback(@PathVariable Long ledgerId,
|
||||
@Valid @RequestBody AwardClawbackReq req) {
|
||||
service.applyClawback(ledgerId, req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,14 +112,38 @@ public class AwardService {
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
BigDecimal clawback = req.getClawbackAmount() != null ? req.getClawbackAmount() : BigDecimal.ZERO;
|
||||
if (clawback.compareTo(awardAmount) > 0) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "환수액이 시상액을 초과합니다");
|
||||
}
|
||||
BigDecimal netAmount = awardAmount.subtract(clawback).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
AwardLedgerVO vo = new AwardLedgerVO();
|
||||
vo.setAgentId(req.getAgentId());
|
||||
vo.setSettleMonth(req.getSettleMonth());
|
||||
vo.setAwardType(req.getAwardType());
|
||||
vo.setBaseAmount(req.getBaseAmount());
|
||||
vo.setAwardAmount(awardAmount);
|
||||
vo.setClawbackAmount(clawback);
|
||||
vo.setNetAmount(netAmount);
|
||||
vo.setStatus("CALCULATED");
|
||||
ledgerMapper.insert(vo);
|
||||
return vo.getLedgerId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void applyClawback(Long ledgerId, com.ga.api.controller.commission.AwardClawbackReq req) {
|
||||
AwardLedgerResp ledger = ledgerMapper.selectById(ledgerId);
|
||||
if (ledger == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
if ("CANCELLED".equals(ledger.getStatus())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "취소된 원장에는 환수를 적용할 수 없습니다");
|
||||
}
|
||||
if (req.getClawbackAmount().compareTo(ledger.getAwardAmount()) > 0) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "환수액이 시상액을 초과합니다");
|
||||
}
|
||||
BigDecimal netAmount = ledger.getAwardAmount()
|
||||
.subtract(req.getClawbackAmount())
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
ledgerMapper.applyClawback(ledgerId, req.getClawbackAmount(), netAmount, "CLAWED_BACK");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
-- V114: 시상 환수 (award_ledger 컬럼 추가 + status 확장)
|
||||
-- settling_support_ledger(V90) 패턴 미러:
|
||||
-- clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0
|
||||
-- net_amount NUMERIC(15,2) NOT NULL DEFAULT 0
|
||||
-- 기존 행 백필: 환수 없던 과거 데이터는 net = award
|
||||
-- status CHECK에 'CLAWED_BACK' 추가 (기존 제약은 DO 블록으로 동적 탐색 후 DROP)
|
||||
-- 공통코드: AWARD_STATUS 그룹이 V92/V96/V105 어디에도 없으므로 INSERT 생략
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 컬럼 추가
|
||||
-- ============================================================
|
||||
ALTER TABLE award_ledger
|
||||
ADD COLUMN clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN net_amount NUMERIC(15,2) NOT NULL DEFAULT 0;
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 기존 행 백필: 환수 없던 과거 = net = award
|
||||
-- ============================================================
|
||||
UPDATE award_ledger
|
||||
SET net_amount = award_amount
|
||||
WHERE net_amount = 0
|
||||
AND clawback_amount = 0;
|
||||
|
||||
-- ============================================================
|
||||
-- 3. clawback 음수 방지 제약
|
||||
-- ============================================================
|
||||
ALTER TABLE award_ledger
|
||||
ADD CONSTRAINT chk_award_clawback CHECK (clawback_amount >= 0);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. status CHECK 재생성 — CLAWED_BACK 추가
|
||||
-- 기존 인라인 CHECK 이름을 추정하지 않고 pg_constraint에서 동적 조회 후 DROP
|
||||
-- ============================================================
|
||||
DO $$
|
||||
DECLARE
|
||||
c text;
|
||||
BEGIN
|
||||
SELECT conname INTO c
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'award_ledger'::regclass
|
||||
AND contype = 'c'
|
||||
AND pg_get_constraintdef(oid) ILIKE '%status%';
|
||||
IF c IS NOT NULL THEN
|
||||
EXECUTE 'ALTER TABLE award_ledger DROP CONSTRAINT ' || quote_ident(c);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE award_ledger
|
||||
ADD CONSTRAINT chk_award_status
|
||||
CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED','CLAWED_BACK'));
|
||||
|
||||
-- ============================================================
|
||||
-- 5. 컬럼 코멘트
|
||||
-- ============================================================
|
||||
COMMENT ON COLUMN award_ledger.clawback_amount IS '시상 환수액';
|
||||
COMMENT ON COLUMN award_ledger.net_amount IS '실지급 시상액 = award_amount − clawback_amount';
|
||||
@@ -31,5 +31,14 @@ public interface AwardLedgerMapper {
|
||||
|
||||
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
|
||||
|
||||
/**
|
||||
* 환수 처리 — clawback_amount/net_amount/status/updated_at 갱신.
|
||||
* status 는 통상 'CLAWED_BACK'. Service에서 clawback ≤ award_amount 검증 후 호출.
|
||||
*/
|
||||
int applyClawback(@Param("ledgerId") Long ledgerId,
|
||||
@Param("clawbackAmount") BigDecimal clawbackAmount,
|
||||
@Param("netAmount") BigDecimal netAmount,
|
||||
@Param("status") String status);
|
||||
|
||||
int delete(@Param("ledgerId") Long ledgerId);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ public class AwardLedgerResp {
|
||||
private String awardTypeName;
|
||||
private BigDecimal baseAmount;
|
||||
private BigDecimal awardAmount;
|
||||
/** 환수액 (V114) */
|
||||
private BigDecimal clawbackAmount;
|
||||
/** 실지급액 = award_amount - clawback_amount (V114) */
|
||||
private BigDecimal netAmount;
|
||||
private String status;
|
||||
/** 공통코드 INCOME_STATUS */
|
||||
private String statusName;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ga.core.vo.commission;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
@@ -10,6 +11,7 @@ import java.math.BigDecimal;
|
||||
/**
|
||||
* award_ledger 등록 요청.
|
||||
* award_amount 는 Service가 계산 후 VO에 set.
|
||||
* net_amount 는 Service가 award_amount - clawbackAmount 계산 후 VO에 set.
|
||||
*/
|
||||
@Data
|
||||
public class AwardLedgerSaveReq {
|
||||
@@ -26,4 +28,8 @@ public class AwardLedgerSaveReq {
|
||||
@NotNull
|
||||
@Positive
|
||||
private BigDecimal baseAmount;
|
||||
|
||||
/** 등록 시 환수액 (선택, 기본 0). clawback ≤ award_amount 검증은 Service에서 수행. */
|
||||
@DecimalMin("0")
|
||||
private BigDecimal clawbackAmount;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* award_ledger — 시상 원장 (V92)
|
||||
* award_ledger — 시상 원장 (V92, clawback 컬럼 V114 추가)
|
||||
* award_amount = base_amount × award_rate (RATE) or award_fixed (FIXED)
|
||||
* net_amount = award_amount - clawback_amount
|
||||
*/
|
||||
@Data
|
||||
public class AwardLedgerVO {
|
||||
@@ -21,7 +22,11 @@ public class AwardLedgerVO {
|
||||
private BigDecimal baseAmount;
|
||||
/** 시상금 */
|
||||
private BigDecimal awardAmount;
|
||||
/** CALCULATED / CONFIRMED / PAID / CANCELLED */
|
||||
/** 환수액 (V114) */
|
||||
private BigDecimal clawbackAmount;
|
||||
/** 실지급액 = award_amount - clawback_amount (V114) */
|
||||
private BigDecimal netAmount;
|
||||
/** CALCULATED / CONFIRMED / PAID / CANCELLED / CLAWED_BACK */
|
||||
private String status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<resultMap id="VOMap" type="com.ga.core.vo.commission.AwardLedgerVO"/>
|
||||
<resultMap id="RespMap" type="com.ga.core.vo.commission.AwardLedgerResp"/>
|
||||
|
||||
<!-- 목록/단건 조회 컬럼 — 설계사명·조직명·공통코드명 조인 포함 -->
|
||||
<!-- 목록/단건 조회 컬럼 — 설계사명·조직명·공통코드명 조인 포함 (V114: clawback_amount, net_amount 추가) -->
|
||||
<sql id="ledgerJoinCols">
|
||||
l.ledger_id,
|
||||
l.agent_id, a.agent_name, o.org_name,
|
||||
@@ -15,6 +15,7 @@
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'AWARD_TYPE' AND cc.code = l.award_type) AS award_type_name,
|
||||
l.base_amount, l.award_amount,
|
||||
l.clawback_amount, l.net_amount,
|
||||
l.status,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
WHERE cc.group_code = 'INCOME_STATUS' AND cc.code = l.status) AS status_name
|
||||
@@ -54,12 +55,14 @@
|
||||
AND status != 'CANCELLED'
|
||||
</select>
|
||||
|
||||
<!-- award_amount 는 Service가 계산 후 VO에 set, status 기본 CALCULATED -->
|
||||
<!-- award_amount/net_amount 는 Service가 계산 후 VO에 set, status 기본 CALCULATED (V114: clawback_amount, net_amount 포함) -->
|
||||
<insert id="insert" useGeneratedKeys="true" keyProperty="ledgerId">
|
||||
INSERT INTO award_ledger
|
||||
(agent_id, settle_month, award_type, base_amount, award_amount, status)
|
||||
(agent_id, settle_month, award_type, base_amount, award_amount,
|
||||
clawback_amount, net_amount, status)
|
||||
VALUES
|
||||
(#{agentId}, #{settleMonth}, #{awardType}, #{baseAmount}, #{awardAmount},
|
||||
COALESCE(#{clawbackAmount}, 0), #{netAmount},
|
||||
COALESCE(#{status}, 'CALCULATED'))
|
||||
</insert>
|
||||
|
||||
@@ -73,14 +76,24 @@
|
||||
DELETE FROM award_ledger WHERE ledger_id = #{ledgerId}
|
||||
</delete>
|
||||
|
||||
<!-- 정산월 기준 설계사별 시상금 합계 -->
|
||||
<!-- 정산월 기준 설계사별 시상금 합계 — net_amount 기준(V114: 환수 자동 차감). CANCELLED 제외. -->
|
||||
<select id="aggregateByAgent" resultType="map">
|
||||
SELECT agent_id AS "agentId",
|
||||
SUM(award_amount) AS "total"
|
||||
SELECT agent_id AS "agentId",
|
||||
SUM(net_amount) AS "total"
|
||||
FROM award_ledger
|
||||
WHERE settle_month = #{settleMonth}
|
||||
AND status != 'CANCELLED'
|
||||
GROUP BY agent_id
|
||||
</select>
|
||||
|
||||
<!-- 환수 처리 — clawback_amount/net_amount/status/updated_at 갱신 -->
|
||||
<update id="applyClawback">
|
||||
UPDATE award_ledger
|
||||
SET clawback_amount = #{clawbackAmount},
|
||||
net_amount = #{netAmount},
|
||||
status = #{status},
|
||||
updated_at = NOW()
|
||||
WHERE ledger_id = #{ledgerId}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -262,9 +262,16 @@ export interface AwardLedgerRow extends Record<string, unknown> {
|
||||
awardType: string;
|
||||
baseAmount: number;
|
||||
awardAmount: number;
|
||||
clawbackAmount: number;
|
||||
netAmount: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface AwardClawbackReq {
|
||||
clawbackAmount: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
// ── P7: LifeSupport ───────────────────────────────
|
||||
export interface LifeSupportRuleRow extends Record<string, unknown> {
|
||||
ruleId: number;
|
||||
@@ -472,6 +479,8 @@ export const commissionApi = {
|
||||
unwrap<PageResponse<AwardLedgerRow>>(
|
||||
api.get('/api/awards/ledgers', { params: p }),
|
||||
),
|
||||
awardLedgerClawback: (ledgerId: number, body: AwardClawbackReq) =>
|
||||
unwrap<void>(api.put(`/api/awards/ledgers/${ledgerId}/clawback`, body)),
|
||||
|
||||
// ── P7: LifeSupport ───────────────────────────────
|
||||
lifeSupportRules: (p?: Partial<CommissionSearchParam>) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Alert, Form, Input, InputNumber, Modal, Select, Space, Tabs, message } from 'antd';
|
||||
import { ProCard } from '@ant-design/pro-components';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ColDef } from '@ag-grid-community/core';
|
||||
import dayjs from 'dayjs';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
commissionApi,
|
||||
AwardRuleRow,
|
||||
AwardLedgerRow,
|
||||
AwardClawbackReq,
|
||||
CommissionSearchParam,
|
||||
} from '@/api/commission';
|
||||
import { GRAY, RADIUS, SHADOW } from '@/theme/tokens';
|
||||
@@ -36,12 +37,14 @@ const RULE_COLS: ColDef<AwardRuleRow>[] = [
|
||||
];
|
||||
|
||||
const LEDGER_COLS: ColDef<AwardLedgerRow>[] = [
|
||||
{ field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' },
|
||||
{ field: 'agentName', headerName: '설계사', width: 130 },
|
||||
{ field: 'awardType', headerName: '시상유형', width: 120 },
|
||||
{ field: 'baseAmount', headerName: '기준실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'awardAmount', headerName: '시상금', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'status', headerName: '상태', width: 100 },
|
||||
{ field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' },
|
||||
{ field: 'agentName', headerName: '설계사', width: 130 },
|
||||
{ field: 'awardType', headerName: '시상유형', width: 120 },
|
||||
{ field: 'baseAmount', headerName: '기준실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'awardAmount', headerName: '시상금', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'clawbackAmount', headerName: '환수액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'netAmount', headerName: '실지급', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) },
|
||||
{ field: 'status', headerName: '상태', width: 110 },
|
||||
];
|
||||
|
||||
export default function Award() {
|
||||
@@ -53,6 +56,25 @@ export default function Award() {
|
||||
const [editingRule, setEditingRule] = useState<AwardRuleRow | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [clawbackTarget, setClawbackTarget] = useState<AwardLedgerRow | null>(null);
|
||||
const [clawbackForm] = Form.useForm();
|
||||
|
||||
const clawbackMutation = useMutation({
|
||||
mutationFn: ({ ledgerId, body }: { ledgerId: number; body: AwardClawbackReq }) =>
|
||||
commissionApi.awardLedgerClawback(ledgerId, body),
|
||||
onSuccess: () => {
|
||||
message.success('환수처리 완료');
|
||||
qc.invalidateQueries({ queryKey: ['award', 'ledgers'] });
|
||||
setClawbackTarget(null);
|
||||
clawbackForm.resetFields();
|
||||
},
|
||||
});
|
||||
|
||||
const handleClawbackOk = async () => {
|
||||
const values = await clawbackForm.validateFields();
|
||||
clawbackMutation.mutate({ ledgerId: clawbackTarget!.ledgerId, body: values });
|
||||
};
|
||||
|
||||
const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({
|
||||
queryKey: ['award', 'rules'],
|
||||
queryFn: () => commissionApi.awardRules(),
|
||||
@@ -97,8 +119,10 @@ export default function Award() {
|
||||
|
||||
const pinnedLedger = ledgers.length > 0 ? {
|
||||
agentName: '합계',
|
||||
baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0),
|
||||
awardAmount: ledgers.reduce((s, r) => s + (r.awardAmount ?? 0), 0),
|
||||
baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0),
|
||||
awardAmount: ledgers.reduce((s, r) => s + (r.awardAmount ?? 0), 0),
|
||||
clawbackAmount: ledgers.reduce((s, r) => s + (r.clawbackAmount ?? 0), 0),
|
||||
netAmount: ledgers.reduce((s, r) => s + (r.netAmount ?? 0), 0),
|
||||
} as Partial<AwardLedgerRow> : undefined;
|
||||
|
||||
const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm };
|
||||
@@ -159,7 +183,23 @@ export default function Award() {
|
||||
<ProCard style={cardStyle}>
|
||||
<DataGrid<AwardLedgerRow>
|
||||
rows={ledgers}
|
||||
columns={LEDGER_COLS}
|
||||
columns={[
|
||||
...LEDGER_COLS,
|
||||
{
|
||||
headerName: '액션', width: 110, pinned: 'right',
|
||||
cellRenderer: (p: { data: AwardLedgerRow }) => (
|
||||
<PermissionButton
|
||||
menuCode="AWARD"
|
||||
permCode="UPDATE"
|
||||
size="small"
|
||||
disabled={p.data?.status === 'CANCELLED'}
|
||||
onClick={() => { setClawbackTarget(p.data); clawbackForm.resetFields(); }}
|
||||
>
|
||||
환수처리
|
||||
</PermissionButton>
|
||||
),
|
||||
},
|
||||
]}
|
||||
loading={ledgersLoading}
|
||||
height={520}
|
||||
rowKey="ledgerId"
|
||||
@@ -204,6 +244,38 @@ export default function Award() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="환수처리"
|
||||
open={!!clawbackTarget}
|
||||
onOk={handleClawbackOk}
|
||||
onCancel={() => { setClawbackTarget(null); clawbackForm.resetFields(); }}
|
||||
okText="처리"
|
||||
cancelText="취소"
|
||||
confirmLoading={clawbackMutation.isPending}
|
||||
>
|
||||
{clawbackTarget && (
|
||||
<div style={{ marginBottom: 12, color: '#666', fontSize: 13 }}>
|
||||
{clawbackTarget.agentName} / {clawbackTarget.settleMonth} / 시상금 {fmt(clawbackTarget.awardAmount)}
|
||||
</div>
|
||||
)}
|
||||
<Form form={clawbackForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="clawbackAmount"
|
||||
label="환수액"
|
||||
rules={[{ required: true, message: '환수액을 입력하세요' }]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} min={0} placeholder="환수할 금액" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="reason"
|
||||
label="사유"
|
||||
rules={[{ required: true, message: '사유를 입력하세요' }]}
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder="환수 사유를 입력하세요" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user