feat(frontend): Ant Design Pro 패턴 전면 적용
@ant-design/pro-components ^2.8.10 + @ant-design/icons ^6.2 추가. ProLayout (MainLayout): - mix 레이아웃 (사이드바 + 헤더 통합), navTheme=light - 자체 메뉴 트리 렌더링 (children 트리), 동적 메뉴 매핑 - 헤더 actionsRender: 정산월 MonthPicker / 알림 뱃지 - avatarProps: 그라디언트 아바타 + Dropdown 로그아웃 - token: 사이드바/헤더 컬러, pageContainer padding - footerRender: 버전 표기 PageContainer (Pro): - @ant-design/pro-layout 의 PageContainer 사용 - 자동 Breadcrumb / Title / extra 슬롯 ProTable 마이그레이션 (6 페이지): - AgentList: search + valueEnum, request → PageHelper 변환 - ContractList, RecruitLedger, ExceptionLedger, SettleList, PaymentList, UserList - 검색 폼 자동 생성 (valueType: select/dateMonth 등) - toolBarRender: 등록/엑셀 버튼 - options: density/fullScreen/reload/setting Dashboard: - ProCard + StatisticCard 로 KPI 4종 그리드 - 차트 + 배치 진행 + 알림을 ProCard 로 묶음 검증: - tsc -b 통과 - vite build 성공 (2.6MB / gzip 811KB) - 백엔드 76건 단위테스트 영향 없음 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+707
-4
@@ -12,6 +12,8 @@
|
||||
"@ag-grid-community/core": "^31.3.2",
|
||||
"@ag-grid-community/react": "^31.3.2",
|
||||
"@ag-grid-community/styles": "^31.3.2",
|
||||
"@ant-design/icons": "^6.2.2",
|
||||
"@ant-design/pro-components": "^2.8.10",
|
||||
"@tabler/icons-react": "^3.5.0",
|
||||
"@tanstack/react-query": "^5.40.0",
|
||||
"antd": "^5.18.0",
|
||||
@@ -126,6 +128,69 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/icons": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.2.tgz",
|
||||
"integrity": "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^8.0.1",
|
||||
"@ant-design/icons-svg": "^4.4.2",
|
||||
"@rc-component/util": "^1.10.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/icons-svg": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
|
||||
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ant-design/icons/node_modules/@ant-design/colors": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz",
|
||||
"integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/fast-color": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/icons/node_modules/@ant-design/fast-color": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz",
|
||||
"integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-card": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-card/-/pro-card-2.10.0.tgz",
|
||||
"integrity": "sha512-sLONn1odmE0Wkbse8pol4WiaEzBV8JU5s3FAMflPpycfUcbSaa1ktXzQ7LCo2SAvOS7gkfmpFjBPtrfbigKh4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^1.21.1",
|
||||
"@ant-design/icons": "^5.0.0",
|
||||
"@ant-design/pro-provider": "2.16.2",
|
||||
"@ant-design/pro-utils": "2.18.0",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"classnames": "^2.3.2",
|
||||
"rc-resize-observer": "^1.0.0",
|
||||
"rc-util": "^5.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-card/node_modules/@ant-design/icons": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
|
||||
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
|
||||
@@ -145,12 +210,409 @@
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/icons-svg": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
|
||||
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
|
||||
"node_modules/@ant-design/pro-components": {
|
||||
"version": "2.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-components/-/pro-components-2.8.10.tgz",
|
||||
"integrity": "sha512-QHnnIXdmC5GTAtm6i8eeJy5yT9npPlFyxpDm+duiDrTRKRFaAQBduArxlH3DA/hoRCCypzPONxfK9BQNIhIyZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-card": "2.10.0",
|
||||
"@ant-design/pro-descriptions": "2.6.10",
|
||||
"@ant-design/pro-field": "3.1.0",
|
||||
"@ant-design/pro-form": "2.32.0",
|
||||
"@ant-design/pro-layout": "7.22.7",
|
||||
"@ant-design/pro-list": "2.6.10",
|
||||
"@ant-design/pro-provider": "2.16.2",
|
||||
"@ant-design/pro-skeleton": "2.2.1",
|
||||
"@ant-design/pro-table": "3.21.0",
|
||||
"@ant-design/pro-utils": "2.18.0",
|
||||
"@babel/runtime": "^7.16.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-descriptions": {
|
||||
"version": "2.6.10",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-descriptions/-/pro-descriptions-2.6.10.tgz",
|
||||
"integrity": "sha512-+4MbiOfumnWlW0Awm4m8JML5o3lR649FD24AaivCmr8BQvIAAXdTITnDMXEg8BqvdP4KOvNsStZrvYfqoev33A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-field": "3.1.0",
|
||||
"@ant-design/pro-form": "2.32.0",
|
||||
"@ant-design/pro-provider": "2.16.2",
|
||||
"@ant-design/pro-skeleton": "2.2.1",
|
||||
"@ant-design/pro-utils": "2.18.0",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"rc-resize-observer": "^0.2.3",
|
||||
"rc-util": "^5.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-descriptions/node_modules/rc-resize-observer": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-0.2.6.tgz",
|
||||
"integrity": "sha512-YX6nYnd6fk7zbuvT6oSDMKiZjyngjHoy+fz+vL3Tez38d/G5iGdaDJa2yE7345G6sc4Mm1IGRUIwclvltddhmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
"classnames": "^2.2.1",
|
||||
"rc-util": "^5.0.0",
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-field": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-field/-/pro-field-3.1.0.tgz",
|
||||
"integrity": "sha512-+Dgp31WjD+iwg9KIRAMgNkfQivkJKMcYBrIBmho1e8ep/O0HgWSp48g70tBIWi/Lfem/Ky2schF7O8XCFouczw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.0.0",
|
||||
"@ant-design/pro-provider": "2.16.2",
|
||||
"@ant-design/pro-utils": "2.18.0",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"@chenshuai2144/sketch-color": "^1.0.8",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rc-util": "^5.4.0",
|
||||
"swr": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-field/node_modules/@ant-design/icons": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
|
||||
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/icons-svg": "^4.4.0",
|
||||
"@babel/runtime": "^7.24.8",
|
||||
"classnames": "^2.2.6",
|
||||
"rc-util": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-form": {
|
||||
"version": "2.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-form/-/pro-form-2.32.0.tgz",
|
||||
"integrity": "sha512-GZnVAMeYv+YHJb17lJ7rX5PYuQPvEA6EotQnPbHi9tGLN3PfexcAd21rqzuO+OrulU2x7TEMDIxtY9MzvvOGbg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.0.0",
|
||||
"@ant-design/pro-field": "3.1.0",
|
||||
"@ant-design/pro-provider": "2.16.2",
|
||||
"@ant-design/pro-utils": "2.18.0",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"@chenshuai2144/sketch-color": "^1.0.7",
|
||||
"@umijs/use-params": "^1.0.9",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rc-resize-observer": "^1.1.0",
|
||||
"rc-util": "^5.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"rc-field-form": ">=1.22.0",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-form/node_modules/@ant-design/icons": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
|
||||
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/icons-svg": "^4.4.0",
|
||||
"@babel/runtime": "^7.24.8",
|
||||
"classnames": "^2.2.6",
|
||||
"rc-util": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-layout": {
|
||||
"version": "7.22.7",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.7.tgz",
|
||||
"integrity": "sha512-fvmtNA1r9SaasVIQIQt611VSlNxtVxDbQ3e+1GhYQza3tVJi/3gCZuDyfMfTnbLmf3PaW/YvLkn7MqDbzAzoLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^1.21.1",
|
||||
"@ant-design/icons": "^5.0.0",
|
||||
"@ant-design/pro-provider": "2.16.2",
|
||||
"@ant-design/pro-utils": "2.18.0",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"@umijs/route-utils": "^4.0.0",
|
||||
"@umijs/use-params": "^1.0.9",
|
||||
"classnames": "^2.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"path-to-regexp": "8.2.0",
|
||||
"rc-resize-observer": "^1.1.0",
|
||||
"rc-util": "^5.0.6",
|
||||
"swr": "^2.0.0",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-layout/node_modules/@ant-design/icons": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
|
||||
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/icons-svg": "^4.4.0",
|
||||
"@babel/runtime": "^7.24.8",
|
||||
"classnames": "^2.2.6",
|
||||
"rc-util": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-list": {
|
||||
"version": "2.6.10",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-list/-/pro-list-2.6.10.tgz",
|
||||
"integrity": "sha512-xSWwnqCr+hPEYR4qY7nFUaxO5RQBxNlFaPNmobP2i+Im31slk9JuAusgWeIYO0mNhLJuLbxd8CCma2AZij3fBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^1.21.1",
|
||||
"@ant-design/icons": "^5.0.0",
|
||||
"@ant-design/pro-card": "2.10.0",
|
||||
"@ant-design/pro-field": "3.1.0",
|
||||
"@ant-design/pro-table": "3.21.0",
|
||||
"@ant-design/pro-utils": "2.18.0",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"rc-resize-observer": "^1.0.0",
|
||||
"rc-util": "^4.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-list/node_modules/@ant-design/icons": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
|
||||
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/icons-svg": "^4.4.0",
|
||||
"@babel/runtime": "^7.24.8",
|
||||
"classnames": "^2.2.6",
|
||||
"rc-util": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-list/node_modules/@ant-design/icons/node_modules/rc-util": {
|
||||
"version": "5.44.4",
|
||||
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz",
|
||||
"integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"react-is": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-list/node_modules/@ant-design/icons/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ant-design/pro-list/node_modules/rc-util": {
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz",
|
||||
"integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"add-dom-event-listener": "^1.1.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react-is": "^16.12.0",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
"shallowequal": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-provider": {
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-provider/-/pro-provider-2.16.2.tgz",
|
||||
"integrity": "sha512-0KmCH1EaOND787Jz6VRMYtLNZmqfT0JPjdUfxhyOxFfnBRfrjyfZgIa6CQoAJLEUMWv57PccWS8wRHVUUk2Yiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^1.21.1",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"@ctrl/tinycolor": "^3.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"rc-util": "^5.0.1",
|
||||
"swr": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-skeleton": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-skeleton/-/pro-skeleton-2.2.1.tgz",
|
||||
"integrity": "sha512-3M2jNOZQZWEDR8pheY00OkHREfb0rquvFZLCa6DypGmiksiuuYuR9Y4iA82ZF+mva2FmpHekdwbje/GpbxqBeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-table": {
|
||||
"version": "3.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-table/-/pro-table-3.21.0.tgz",
|
||||
"integrity": "sha512-sI81d3FYRv5sXamUc+M5CsHZ9CchuUQgOAPzo5H4oPAVL5h+mkYGRsBzPsxQX7khTNpWjrAtPoRm5ipx3vvWog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/cssinjs": "^1.21.1",
|
||||
"@ant-design/icons": "^5.0.0",
|
||||
"@ant-design/pro-card": "2.10.0",
|
||||
"@ant-design/pro-field": "3.1.0",
|
||||
"@ant-design/pro-form": "2.32.0",
|
||||
"@ant-design/pro-provider": "2.16.2",
|
||||
"@ant-design/pro-utils": "2.18.0",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rc-resize-observer": "^1.0.0",
|
||||
"rc-util": "^5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"rc-field-form": ">=1.22.0",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-table/node_modules/@ant-design/icons": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
|
||||
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/icons-svg": "^4.4.0",
|
||||
"@babel/runtime": "^7.24.8",
|
||||
"classnames": "^2.2.6",
|
||||
"rc-util": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-utils": {
|
||||
"version": "2.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.18.0.tgz",
|
||||
"integrity": "sha512-8+ikyrN8L8a8Ph4oeHTOJEiranTj18+9+WHCHjKNdEfukI7Rjn8xpYdLJWb2AUJkb9d4eoAqjd5+k+7w81Df0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.0.0",
|
||||
"@ant-design/pro-provider": "2.16.2",
|
||||
"@babel/runtime": "^7.18.0",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rc-util": "^5.0.6",
|
||||
"safe-stable-stringify": "^2.4.3",
|
||||
"swr": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^4.24.15 || ^5.11.2",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/pro-utils/node_modules/@ant-design/icons": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
|
||||
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/icons-svg": "^4.4.0",
|
||||
"@babel/runtime": "^7.24.8",
|
||||
"classnames": "^2.2.6",
|
||||
"rc-util": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/react-slick": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
|
||||
@@ -458,6 +920,95 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@chenshuai2144/sketch-color": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@chenshuai2144/sketch-color/-/sketch-color-1.0.9.tgz",
|
||||
"integrity": "sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"reactcss": "^1.2.3",
|
||||
"tinycolor2": "^1.4.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/modifiers": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
|
||||
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.0.6",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
|
||||
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.0.7",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/hash": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
@@ -1058,6 +1609,26 @@
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/util": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.1.tgz",
|
||||
"integrity": "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-mobile": "^5.0.0",
|
||||
"react-is": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/util/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
@@ -1658,6 +2229,21 @@
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@umijs/route-utils": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.3.tgz",
|
||||
"integrity": "sha512-zPEcYhl1cSfkSRDzzGgoD1mDvGjxoOTJFvkn55srfgdQ3NZe2ZMCScCU6DEnOxuKP1XDVf8pqyqCDVd2+RCQIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@umijs/use-params": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@umijs/use-params/-/use-params-1.0.9.tgz",
|
||||
"integrity": "sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
@@ -1679,6 +2265,15 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/add-dom-event-listener": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz",
|
||||
"integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/antd": {
|
||||
"version": "5.29.3",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz",
|
||||
@@ -1744,6 +2339,26 @@
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/antd/node_modules/@ant-design/icons": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
|
||||
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@ant-design/icons-svg": "^4.4.0",
|
||||
"@babel/runtime": "^7.24.8",
|
||||
"classnames": "^2.2.6",
|
||||
"rc-util": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0.0",
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -2057,6 +2672,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
@@ -2364,6 +2988,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-mobile": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz",
|
||||
"integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -2411,6 +3041,12 @@
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -2505,6 +3141,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
||||
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -3204,6 +3849,12 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
@@ -3277,6 +3928,15 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reactcss": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||
"integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
|
||||
@@ -3366,6 +4026,15 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
@@ -3394,6 +4063,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -3416,6 +4091,19 @@
|
||||
"integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
|
||||
"integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
|
||||
@@ -3431,6 +4119,12 @@
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/toggle-selection": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||
@@ -3579,6 +4273,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"@ag-grid-community/core": "^31.3.2",
|
||||
"@ag-grid-community/react": "^31.3.2",
|
||||
"@ag-grid-community/styles": "^31.3.2",
|
||||
"@ant-design/icons": "^6.2.2",
|
||||
"@ant-design/pro-components": "^2.8.10",
|
||||
"@tabler/icons-react": "^3.5.0",
|
||||
"@tanstack/react-query": "^5.40.0",
|
||||
"antd": "^5.18.0",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
const { Title } = Typography;
|
||||
import { PageContainer as ProPageContainer } from '@ant-design/pro-components';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -11,52 +9,24 @@ interface Props {
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 표준 컨테이너.
|
||||
* - 상단: 제목 + 좌측 컬러 바 + 우측 액션
|
||||
* - 본문: 흰 카드 + 그림자
|
||||
* 09_UI_DESIGN.md §4 타입 A 의 시각적 보강 버전.
|
||||
* Ant Design Pro 의 PageContainer 래퍼.
|
||||
* - 자동 Breadcrumb / Title / Tabs / Footer 슬롯 제공
|
||||
* - extra 는 우측 상단 액션 영역
|
||||
*/
|
||||
export default function PageContainer({ title, description, extra, children }: Props) {
|
||||
return (
|
||||
<div>
|
||||
{/* 페이지 제목 영역 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '16px 20px',
|
||||
background: '#ffffff',
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
boxShadow: 'var(--ga-shadow-card)',
|
||||
borderLeft: '4px solid var(--ga-primary)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
|
||||
{title}
|
||||
</Title>
|
||||
{description && (
|
||||
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{extra && <div>{extra}</div>}
|
||||
</div>
|
||||
|
||||
{/* 본문 카드 */}
|
||||
<div
|
||||
style={{
|
||||
background: '#ffffff',
|
||||
borderRadius: 8,
|
||||
padding: 20,
|
||||
boxShadow: 'var(--ga-shadow-card)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<ProPageContainer
|
||||
header={{
|
||||
title,
|
||||
breadcrumb: {},
|
||||
ghost: false,
|
||||
extra,
|
||||
}}
|
||||
content={
|
||||
description ? <span style={{ color: '#666', fontSize: 13 }}>{description}</span> : undefined
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ProPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { useState } from 'react';
|
||||
import { Layout, Menu, Dropdown, Badge, Space, Avatar, Button, DatePicker, theme } from 'antd';
|
||||
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
|
||||
import { ProLayout, type MenuDataItem } from '@ant-design/pro-components';
|
||||
import { Avatar, Badge, Button, DatePicker, Dropdown, Space } from 'antd';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
IconMenu2, IconBell, IconUser, IconLogout, IconChevronDown,
|
||||
} from '@tabler/icons-react';
|
||||
import { IconBell, IconLogout, IconUser, IconChevronDown } from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
import { menuApi, MenuResp } from '@/api/menu';
|
||||
import { menuApi, type MenuResp } from '@/api/menu';
|
||||
import { authApi } from '@/api/auth';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { getMenuIcon } from '@/components/common/MenuIcon';
|
||||
|
||||
const { Header, Sider, Content, Footer } = Layout;
|
||||
|
||||
function toMenuItems(menus: MenuResp[]): any[] {
|
||||
return menus.map((m) => ({
|
||||
key: m.menuPath ?? m.menuCode,
|
||||
label: m.menuName,
|
||||
icon: getMenuIcon(m.menuCode, m.menuPath),
|
||||
children: m.children?.length ? toMenuItems(m.children) : undefined,
|
||||
}));
|
||||
/** API 메뉴 트리 → ProLayout MenuDataItem */
|
||||
function toProMenu(menus: MenuResp[]): MenuDataItem[] {
|
||||
return menus.map((m) => {
|
||||
const item: MenuDataItem = {
|
||||
name: m.menuName,
|
||||
path: m.menuPath ?? `/_grp/${m.menuCode}`,
|
||||
key: m.menuCode,
|
||||
icon: getMenuIcon(m.menuCode, m.menuPath),
|
||||
};
|
||||
if (m.children?.length) item.children = toProMenu(m.children);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
export default function MainLayout() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const auth = useAuthStore();
|
||||
const { token } = theme.useToken();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [settleMonth, setSettleMonth] = useState(dayjs());
|
||||
|
||||
const { data: menus = [] } = useQuery({
|
||||
@@ -44,181 +44,100 @@ export default function MainLayout() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider
|
||||
width={220}
|
||||
collapsedWidth={64}
|
||||
collapsed={collapsed}
|
||||
theme="light"
|
||||
style={{
|
||||
borderRight: '1px solid #f0f0f0',
|
||||
background: '#ffffff',
|
||||
boxShadow: '2px 0 8px rgba(0,0,0,0.04)',
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
>
|
||||
{/* 로고 영역 */}
|
||||
<ProLayout
|
||||
title="GA 정산시스템"
|
||||
logo={
|
||||
<div
|
||||
style={{
|
||||
height: 56,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
padding: collapsed ? 0 : '0 16px',
|
||||
width: 28, height: 28, borderRadius: 6,
|
||||
background: 'linear-gradient(135deg, #1677FF 0%, #0C447C 100%)',
|
||||
color: '#fff',
|
||||
gap: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.3,
|
||||
color: '#fff', display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'center', fontWeight: 800, fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 32, height: 32, borderRadius: 8,
|
||||
background: 'rgba(255,255,255,0.18)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 16, fontWeight: 800,
|
||||
}}
|
||||
>GA</div>
|
||||
{!collapsed && <span style={{ fontSize: 14 }}>정산 시스템</span>}
|
||||
>GA</div>
|
||||
}
|
||||
layout="mix"
|
||||
navTheme="light"
|
||||
contentWidth="Fluid"
|
||||
fixedHeader
|
||||
fixSiderbar
|
||||
siderWidth={220}
|
||||
menu={{ locale: false }}
|
||||
menuDataRender={() => toProMenu(menus)}
|
||||
location={{ pathname: location.pathname }}
|
||||
menuItemRender={(item, dom) => (
|
||||
<div onClick={() => item.path && !item.path.startsWith('/_grp/') && navigate(item.path)}>
|
||||
{dom}
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
mode="inline"
|
||||
items={toMenuItems(menus)}
|
||||
selectedKeys={[location.pathname]}
|
||||
onClick={({ key }) => navigate(String(key))}
|
||||
style={{ borderRight: 0, paddingTop: 8, paddingBottom: 8 }}
|
||||
/>
|
||||
|
||||
{!collapsed && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0,
|
||||
padding: '12px 16px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
background: '#fafafa',
|
||||
fontSize: 11, color: '#999',
|
||||
)}
|
||||
breadcrumbRender={(routers = []) => routers}
|
||||
onMenuHeaderClick={() => navigate('/')}
|
||||
token={{
|
||||
// ProLayout 디자인 토큰 — antd pro 컬러 시스템
|
||||
sider: {
|
||||
colorMenuBackground: '#ffffff',
|
||||
colorTextMenuSelected: '#185FA5',
|
||||
colorBgMenuItemSelected: '#E6F1FB',
|
||||
colorTextMenuItemHover: '#1677FF',
|
||||
colorMenuItemDivider: '#f0f0f0',
|
||||
},
|
||||
header: {
|
||||
colorBgHeader: '#ffffff',
|
||||
colorHeaderTitle: '#185FA5',
|
||||
colorBgRightActionsItemHover: '#f5f7fa',
|
||||
heightLayoutHeader: 48,
|
||||
},
|
||||
bgLayout: '#f5f7fa',
|
||||
pageContainer: {
|
||||
paddingBlockPageContainerContent: 16,
|
||||
paddingInlinePageContainerContent: 20,
|
||||
},
|
||||
}}
|
||||
actionsRender={() => [
|
||||
<Space key="month" size={6}>
|
||||
<span style={{ fontSize: 12, color: '#999' }}>정산월</span>
|
||||
<DatePicker
|
||||
picker="month"
|
||||
value={settleMonth}
|
||||
onChange={(d) => d && setSettleMonth(d)}
|
||||
allowClear={false}
|
||||
format="YYYY-MM"
|
||||
size="small"
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
</Space>,
|
||||
<Badge key="bell" count={3} size="small" offset={[-2, 4]}>
|
||||
<Button type="text" icon={<IconBell size={18} />} />
|
||||
</Badge>,
|
||||
]}
|
||||
avatarProps={{
|
||||
src: undefined,
|
||||
size: 'small',
|
||||
title: auth.userName ?? auth.loginId ?? '관리자',
|
||||
icon: <IconUser size={14} />,
|
||||
style: { background: 'linear-gradient(135deg, #1677FF 0%, #185FA5 100%)' },
|
||||
render: (_props, dom) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'logout', label: '로그아웃', icon: <IconLogout size={14} />, onClick: handleLogout },
|
||||
],
|
||||
}}
|
||||
>
|
||||
v1.0.0-alpha · trading_ai
|
||||
</div>
|
||||
)}
|
||||
</Sider>
|
||||
|
||||
<Layout>
|
||||
<Header
|
||||
style={{
|
||||
height: 48,
|
||||
lineHeight: '48px',
|
||||
background: '#ffffff',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '0 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<Space size={8}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<IconMenu2 size={18} />}
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13, fontWeight: 500, color: '#666',
|
||||
paddingLeft: 8, borderLeft: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
{breadcrumbOf(location.pathname)}
|
||||
</span>
|
||||
</Space>
|
||||
<Space size={12}>
|
||||
<span style={{ fontSize: 12, color: '#999' }}>정산월</span>
|
||||
<DatePicker
|
||||
picker="month"
|
||||
value={settleMonth}
|
||||
onChange={(d) => d && setSettleMonth(d)}
|
||||
allowClear={false}
|
||||
format="YYYY-MM"
|
||||
size="small"
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
<Badge count={3} size="small" offset={[-2, 4]}>
|
||||
<Button type="text" icon={<IconBell size={18} />} />
|
||||
</Badge>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
label: '로그아웃',
|
||||
icon: <IconLogout size={14} />,
|
||||
onClick: handleLogout,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Space
|
||||
style={{
|
||||
cursor: 'pointer', padding: '4px 8px', borderRadius: 6,
|
||||
background: token.colorBgLayout,
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size={26}
|
||||
style={{ background: 'linear-gradient(135deg, #1677FF 0%, #185FA5 100%)' }}
|
||||
icon={<IconUser size={14} />}
|
||||
/>
|
||||
<span style={{ fontSize: 13, fontWeight: 500 }}>
|
||||
{auth.userName ?? auth.loginId ?? '관리자'}
|
||||
</span>
|
||||
<IconChevronDown size={12} color="#999" />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Header>
|
||||
<Content style={{ padding: 'var(--ga-content-padding)', overflow: 'auto' }}>
|
||||
<div className="ga-page-container">
|
||||
<Outlet />
|
||||
</div>
|
||||
</Content>
|
||||
<Footer
|
||||
style={{
|
||||
textAlign: 'center', fontSize: 11, color: '#bbb',
|
||||
padding: '8px 0', background: 'transparent',
|
||||
}}
|
||||
>
|
||||
© GA Commission System · trading_ai · admin@local
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<Space style={{ cursor: 'pointer' }}>
|
||||
{dom}
|
||||
<IconChevronDown size={12} color="#999" />
|
||||
</Space>
|
||||
</Dropdown>
|
||||
),
|
||||
}}
|
||||
footerRender={() => (
|
||||
<div style={{ textAlign: 'center', padding: '8px 0', fontSize: 11, color: '#bbb' }}>
|
||||
© GA Commission System v1.0.0-alpha · trading_ai
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Outlet />
|
||||
</ProLayout>
|
||||
);
|
||||
}
|
||||
|
||||
/** 경로 → 한글 breadcrumb 라벨 */
|
||||
function breadcrumbOf(pathname: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'/': '대시보드',
|
||||
'/org/agents': '조직 / 설계사',
|
||||
'/contracts': '계약 관리',
|
||||
'/ledger/recruit': '원장 / 모집수수료',
|
||||
'/ledger/exception': '원장 / 예외금액',
|
||||
'/settle': '정산 / 결과',
|
||||
'/settle/batch': '정산 / 배치 실행',
|
||||
'/payments': '지급 관리',
|
||||
'/system/users': '시스템 / 사용자',
|
||||
'/system/roles': '시스템 / 역할 권한',
|
||||
'/system/codes': '시스템 / 공통 코드',
|
||||
};
|
||||
return map[pathname] ?? '';
|
||||
}
|
||||
|
||||
+122
-137
@@ -1,28 +1,27 @@
|
||||
import { Card, Col, Row, Typography } from 'antd';
|
||||
import {
|
||||
IconCash, IconCheck, IconClock, IconAlertTriangle,
|
||||
IconTrendingUp, IconTrendingDown,
|
||||
} from '@tabler/icons-react';
|
||||
import { ProCard, StatisticCard } from '@ant-design/pro-components';
|
||||
import { Tag } from 'antd';
|
||||
import {
|
||||
ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Legend,
|
||||
} from 'recharts';
|
||||
import {
|
||||
IconCash, IconCheck, IconClock, IconAlertTriangle, IconTrendingUp,
|
||||
} from '@tabler/icons-react';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Statistic } = StatisticCard;
|
||||
|
||||
const KPI_CARDS = [
|
||||
{ key: 'recruit', label: '모집수수료', value: 124_500_000, delta: +8.2,
|
||||
icon: <IconCash size={20} />, color: '#1677FF', bg: '#E6F1FB' },
|
||||
{ key: 'maintain', label: '유지수수료', value: 82_300_000, delta: +3.1,
|
||||
icon: <IconCheck size={20} />, color: '#0F6E56', bg: '#E1F5EE' },
|
||||
{ key: 'pending', label: '확정 대기', value: 12, delta: -2.0, suffix: '건',
|
||||
icon: <IconClock size={20} />, color: '#BA7517', bg: '#FAEEDA' },
|
||||
{ key: 'alert', label: '대사 차이', value: 3, delta: +50.0, suffix: '건',
|
||||
icon: <IconAlertTriangle size={20} />, color: '#E24B4A', bg: '#FCEBEB' },
|
||||
const KPI = [
|
||||
{ title: '모집수수료', value: 124_500_000, suffix: '원', precision: 0,
|
||||
color: '#1677FF', bg: '#E6F1FB', icon: <IconCash size={20} />, delta: '+8.2%' },
|
||||
{ title: '유지수수료', value: 82_300_000, suffix: '원', precision: 0,
|
||||
color: '#0F6E56', bg: '#E1F5EE', icon: <IconCheck size={20} />, delta: '+3.1%' },
|
||||
{ title: '확정 대기', value: 12, suffix: '건', precision: 0,
|
||||
color: '#BA7517', bg: '#FAEEDA', icon: <IconClock size={20} />, delta: '-2건' },
|
||||
{ title: '대사 차이', value: 3, suffix: '건', precision: 0,
|
||||
color: '#E24B4A', bg: '#FCEBEB', icon: <IconAlertTriangle size={20} />, delta: '+1건' },
|
||||
];
|
||||
|
||||
const MONTHLY_TREND = [
|
||||
const MONTHLY = [
|
||||
{ month: '12월', recruit: 95, maintain: 70, chargeback: 5 },
|
||||
{ month: '1월', recruit: 105, maintain: 72, chargeback: 8 },
|
||||
{ month: '2월', recruit: 110, maintain: 75, chargeback: 6 },
|
||||
@@ -31,7 +30,7 @@ const MONTHLY_TREND = [
|
||||
{ month: '5월', recruit: 125, maintain: 82, chargeback: 4 },
|
||||
];
|
||||
|
||||
const BATCH_STEPS = [
|
||||
const STEPS = [
|
||||
{ name: '데이터 수신', status: 'done', time: '02:14' },
|
||||
{ name: '데이터 변환', status: 'done', time: '00:52' },
|
||||
{ name: '대사 검증', status: 'done', time: '00:21' },
|
||||
@@ -46,143 +45,129 @@ export default function Dashboard() {
|
||||
return (
|
||||
<PageContainer
|
||||
title="대시보드"
|
||||
description="이번 달 (2026-05) 정산 요약 / 실시간 배치 진행 / 알림"
|
||||
description="2026-05 정산 요약 / 실시간 배치 진행 / 알림"
|
||||
>
|
||||
{/* KPI 카드 4개 */}
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
{KPI_CARDS.map((c) => {
|
||||
const isUp = c.delta >= 0;
|
||||
return (
|
||||
<Col span={6} key={c.key}>
|
||||
<Card
|
||||
bordered={false}
|
||||
{/* KPI */}
|
||||
<ProCard
|
||||
gutter={16}
|
||||
ghost
|
||||
style={{ marginBottom: 16 }}
|
||||
wrap
|
||||
>
|
||||
{KPI.map((k) => (
|
||||
<ProCard
|
||||
key={k.title}
|
||||
colSpan={6}
|
||||
bordered={false}
|
||||
style={{ borderTop: `3px solid ${k.color}` }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
boxShadow: 'var(--ga-shadow-card)',
|
||||
borderTop: `3px solid ${c.color}`,
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: k.bg, color: k.color,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{k.icon}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Statistic
|
||||
title={k.title}
|
||||
value={k.value}
|
||||
suffix={k.suffix}
|
||||
precision={k.precision}
|
||||
description={
|
||||
<Tag color={k.delta.startsWith('+') ? 'success' : 'error'} style={{ marginTop: 4 }}>
|
||||
<IconTrendingUp size={11} style={{ verticalAlign: -2 }} /> {k.delta}
|
||||
</Tag>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ProCard>
|
||||
))}
|
||||
</ProCard>
|
||||
|
||||
{/* 차트 + 배치 진행 */}
|
||||
<ProCard gutter={16} ghost wrap style={{ marginBottom: 16 }}>
|
||||
<ProCard
|
||||
colSpan={16}
|
||||
title="월별 추이 (최근 6개월)"
|
||||
headerBordered
|
||||
bordered={false}
|
||||
style={{ minHeight: 320 }}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<BarChart data={MONTHLY}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" vertical={false} />
|
||||
<XAxis dataKey="month" tick={{ fontSize: 12, fill: '#666' }} />
|
||||
<YAxis tick={{ fontSize: 12, fill: '#666' }} />
|
||||
<Tooltip />
|
||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||
<Bar dataKey="recruit" name="모집" fill="#1677FF" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="maintain" name="유지" fill="#0F6E56" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="chargeback" name="환수" fill="#E24B4A" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</ProCard>
|
||||
|
||||
<ProCard
|
||||
colSpan={8}
|
||||
title="배치 진행 (5월)"
|
||||
headerBordered
|
||||
bordered={false}
|
||||
>
|
||||
{STEPS.map((s, i) => {
|
||||
const dot = s.status === 'done' ? '#0F6E56'
|
||||
: s.status === 'progress' ? '#1677FF' : '#ddd';
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '6px 0',
|
||||
borderBottom: i < STEPS.length - 1 ? '1px dashed #f5f5f5' : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: c.bg, color: c.color,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{c.icon}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 12, color: '#888' }}>{c.label}</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600, color: '#222', marginTop: 2 }}>
|
||||
{c.suffix === '건' ? c.value.toLocaleString() : (
|
||||
<MoneyText value={c.value} />
|
||||
)}
|
||||
<span style={{ fontSize: 12, color: '#888', marginLeft: 4, fontWeight: 400 }}>
|
||||
{c.suffix ?? '원'}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11, marginTop: 4,
|
||||
color: isUp ? '#0F6E56' : '#E24B4A',
|
||||
display: 'flex', alignItems: 'center', gap: 2,
|
||||
}}
|
||||
>
|
||||
{isUp ? <IconTrendingUp size={12} /> : <IconTrendingDown size={12} />}
|
||||
{Math.abs(c.delta).toFixed(1)}% 전월 대비
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
{/* 차트 + 배치 현황 */}
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={16}>
|
||||
<Card
|
||||
title={<span style={{ fontWeight: 600 }}>월별 추이 (최근 6개월)</span>}
|
||||
bordered={false}
|
||||
style={{ boxShadow: 'var(--ga-shadow-card)' }}
|
||||
styles={{ body: { padding: 16, height: 280 } }}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={MONTHLY_TREND}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" vertical={false} />
|
||||
<XAxis dataKey="month" tick={{ fontSize: 12, fill: '#666' }} />
|
||||
<YAxis tick={{ fontSize: 12, fill: '#666' }}
|
||||
label={{ value: '백만원', angle: 0, position: 'top', fontSize: 11, fill: '#999' }} />
|
||||
<Tooltip />
|
||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||
<Bar dataKey="recruit" name="모집" fill="#1677FF" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="maintain" name="유지" fill="#0F6E56" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="chargeback" name="환수" fill="#E24B4A" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card
|
||||
title={<span style={{ fontWeight: 600 }}>배치 진행 (5월)</span>}
|
||||
bordered={false}
|
||||
style={{ boxShadow: 'var(--ga-shadow-card)' }}
|
||||
styles={{ body: { padding: 16 } }}
|
||||
>
|
||||
{BATCH_STEPS.map((s, i) => {
|
||||
const dotColor = s.status === 'done' ? '#0F6E56' :
|
||||
s.status === 'progress' ? '#1677FF' : '#ddd';
|
||||
return (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '6px 0', borderBottom: i < BATCH_STEPS.length - 1 ? '1px dashed #f5f5f5' : 'none',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{
|
||||
width: 8, height: 8, borderRadius: '50%', background: dotColor,
|
||||
width: 8, height: 8, borderRadius: '50%', background: dot,
|
||||
boxShadow: s.status === 'progress' ? '0 0 0 4px rgba(22,119,255,0.15)' : 'none',
|
||||
}} />
|
||||
<span style={{ fontSize: 13, color: s.status === 'pending' ? '#bbb' : '#333' }}>
|
||||
Step {i + 1}. {s.name}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 11, color: '#999' }}>{s.time ?? '-'}</span>
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 13, color: s.status === 'pending' ? '#bbb' : '#333' }}>
|
||||
Step {i + 1}. {s.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<span style={{ fontSize: 11, color: '#999' }}>{s.time ?? '-'}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
|
||||
{/* 알림 */}
|
||||
<Card
|
||||
title={<span style={{ fontWeight: 600 }}>최근 알림</span>}
|
||||
bordered={false}
|
||||
style={{ boxShadow: 'var(--ga-shadow-card)' }}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
<ProCard title="최근 알림" headerBordered bordered={false}>
|
||||
{[
|
||||
{ tag: '대사불일치', tagColor: '#E24B4A', tagBg: '#FCEBEB',
|
||||
text: '삼성생명 5월 통보액 vs 시스템 계산액 3건 차이', time: '10분 전' },
|
||||
{ tag: '파싱에러', tagColor: '#BA7517', tagBg: '#FAEEDA',
|
||||
{ tag: '대사불일치', color: 'error',
|
||||
text: '삼성생명 5월 통보액 vs 시스템 계산액 3건 차이', time: '10분 전' },
|
||||
{ tag: '파싱에러', color: 'warning',
|
||||
text: '한화생명 raw 파일 12행 날짜 형식 오류 (parse_error_log #4521)', time: '1시간 전' },
|
||||
{ tag: '승인대기', tagColor: '#1677FF', tagBg: '#E6F1FB',
|
||||
{ tag: '승인대기', color: 'processing',
|
||||
text: '예외금액 원장 승인 대기 5건 (정산월 202605)', time: '3시간 전' },
|
||||
].map((n, i) => (
|
||||
].map((n, i, arr) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12, padding: '12px 16px',
|
||||
borderBottom: i < 2 ? '1px solid #f5f5f5' : 'none',
|
||||
display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0',
|
||||
borderBottom: i < arr.length - 1 ? '1px solid #f5f5f5' : 'none',
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 11, fontWeight: 500, padding: '2px 8px', borderRadius: 4,
|
||||
color: n.tagColor, background: n.tagBg,
|
||||
}}>{n.tag}</span>
|
||||
<Text style={{ flex: 1, fontSize: 13 }}>{n.text}</Text>
|
||||
<Tag color={n.color}>{n.tag}</Tag>
|
||||
<span style={{ flex: 1, fontSize: 13 }}>{n.text}</span>
|
||||
<span style={{ fontSize: 11, color: '#999' }}>{n.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</ProCard>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import { Table, Space, message, Modal } from 'antd';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useRef } from 'react';
|
||||
import { Modal, Space, message } from 'antd';
|
||||
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import CodeBadge from '@/components/common/CodeBadge';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { defaultPagination } from '@/utils/tablePagination';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { exceptionApi, ExceptionLedgerResp } from '@/api/exceptionLedger';
|
||||
|
||||
export default function ExceptionLedger() {
|
||||
const qc = useQueryClient();
|
||||
const [param, setParam] = useState<{ settleMonth?: string; status?: string; pageNum: number; pageSize: number }>({
|
||||
pageNum: 1, pageSize: 30,
|
||||
});
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['ledger', 'exception', param],
|
||||
queryFn: () => exceptionApi.list(param),
|
||||
});
|
||||
|
||||
const refresh = () => qc.invalidateQueries({ queryKey: ['ledger', 'exception'] });
|
||||
const actionRef = useRef<ActionType>();
|
||||
const { data: approveCodes = [] } = useCommonCodes('APPROVE_STATUS');
|
||||
|
||||
const onApprove = (id: number, status: 'APPROVED' | 'REJECTED') =>
|
||||
Modal.confirm({
|
||||
@@ -28,71 +18,67 @@ export default function ExceptionLedger() {
|
||||
onOk: async () => {
|
||||
await exceptionApi.approve(id, status);
|
||||
message.success(status === 'APPROVED' ? '승인 완료' : '반려 완료');
|
||||
refresh();
|
||||
actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer title="예외금액 원장">
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'month', name: 'settleMonth', label: '정산월' },
|
||||
{ type: 'code', name: 'status', label: '승인상태', groupCode: 'APPROVE_STATUS' },
|
||||
]}
|
||||
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })}
|
||||
onReset={() => setParam({ pageNum: 1, pageSize: 30 })}
|
||||
/>
|
||||
const columns: ProColumns<ExceptionLedgerResp>[] = [
|
||||
{ title: '정산월', dataIndex: 'settleMonth', width: 90, valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' } },
|
||||
{
|
||||
title: '승인상태', dataIndex: 'approveStatus', width: 100, valueType: 'select',
|
||||
valueEnum: Object.fromEntries(approveCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="APPROVE_STATUS" value={r.approveStatus} />,
|
||||
},
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100, search: false },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160, search: false },
|
||||
{ title: '예외코드', dataIndex: 'exceptionName', width: 120, search: false },
|
||||
{
|
||||
title: '구분', dataIndex: 'direction', width: 80, search: false,
|
||||
render: (_, r) => <CodeBadge groupCode="EXCEPTION_DIRECTION" value={r.direction} />,
|
||||
},
|
||||
{
|
||||
title: '금액', dataIndex: 'amount', width: 130, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.direction === 'MINUS' ? -Math.abs(r.amount) : r.amount} />,
|
||||
},
|
||||
{ title: '내용', dataIndex: 'description', ellipsis: true, search: false },
|
||||
{
|
||||
title: '재반복', dataIndex: 'recurringLeft', width: 90, align: 'center', search: false,
|
||||
render: (_, r) => r.recurringYn === 'Y' ? `${r.recurringLeft}회 남음` : '-',
|
||||
},
|
||||
{ title: '승인자', dataIndex: 'approverName', width: 100, search: false },
|
||||
{ title: '승인일시', dataIndex: 'approveDate', width: 160, search: false },
|
||||
{
|
||||
title: '액션', dataIndex: 'ledgerId', width: 180, fixed: 'right', search: false,
|
||||
render: (_, r) => r.approveStatus === 'PENDING' ? (
|
||||
<Space>
|
||||
<PermissionButton menuCode="LEDGER_EXCEPTION" permCode="APPROVE" size="small"
|
||||
type="primary" onClick={() => onApprove(r.ledgerId, 'APPROVED')}>승인</PermissionButton>
|
||||
<PermissionButton menuCode="LEDGER_EXCEPTION" permCode="APPROVE" size="small"
|
||||
danger onClick={() => onApprove(r.ledgerId, 'REJECTED')}>반려</PermissionButton>
|
||||
</Space>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
<Table<ExceptionLedgerResp>
|
||||
return (
|
||||
<PageContainer title="예외금액 원장" description="수동 등록 예외 금액 / 승인 워크플로우">
|
||||
<ProTable<ExceptionLedgerResp>
|
||||
actionRef={actionRef}
|
||||
rowKey="ledgerId"
|
||||
loading={isLoading}
|
||||
dataSource={data?.list}
|
||||
size="small"
|
||||
scroll={{ x: 1400 }}
|
||||
pagination={{
|
||||
...defaultPagination,
|
||||
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
|
||||
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
|
||||
columns={columns}
|
||||
scroll={{ x: 1500 }}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, ...rest } = params as any;
|
||||
const res = await exceptionApi.list({ ...rest, pageNum: current, pageSize });
|
||||
return { data: res.list, total: res.total, success: true };
|
||||
}}
|
||||
columns={[
|
||||
{ title: '정산월', dataIndex: 'settleMonth', width: 90 },
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100 },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160 },
|
||||
{ title: '예외코드', dataIndex: 'exceptionName', width: 120 },
|
||||
{
|
||||
title: '구분', dataIndex: 'direction', width: 80,
|
||||
render: (v) => <CodeBadge groupCode="EXCEPTION_DIRECTION" value={v} />,
|
||||
},
|
||||
{
|
||||
title: '금액', dataIndex: 'amount', width: 130, align: 'right',
|
||||
render: (v, r) => <MoneyText value={r.direction === 'MINUS' ? -Math.abs(v) : v} />,
|
||||
},
|
||||
{ title: '내용', dataIndex: 'description', ellipsis: true },
|
||||
{ title: '재반복', dataIndex: 'recurringLeft', width: 90, align: 'center',
|
||||
render: (v, r) => r.recurringYn === 'Y' ? `${v}회 남음` : '-' },
|
||||
{
|
||||
title: '승인상태', dataIndex: 'approveStatus', width: 100,
|
||||
render: (v) => <CodeBadge groupCode="APPROVE_STATUS" value={v} />,
|
||||
},
|
||||
{ title: '승인자', dataIndex: 'approverName', width: 100 },
|
||||
{ title: '승인일시', dataIndex: 'approveDate', width: 160 },
|
||||
{
|
||||
title: '액션', dataIndex: 'ledgerId', width: 180, fixed: 'right',
|
||||
render: (id, r) =>
|
||||
r.approveStatus === 'PENDING' ? (
|
||||
<Space>
|
||||
<PermissionButton menuCode="LEDGER_EXCEPTION" permCode="APPROVE" size="small"
|
||||
type="primary" onClick={() => onApprove(id, 'APPROVED')}>
|
||||
승인
|
||||
</PermissionButton>
|
||||
<PermissionButton menuCode="LEDGER_EXCEPTION" permCode="APPROVE" size="small"
|
||||
danger onClick={() => onApprove(id, 'REJECTED')}>
|
||||
반려
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
) : null,
|
||||
},
|
||||
]}
|
||||
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 }}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -1,75 +1,83 @@
|
||||
import { useState } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import CodeBadge from '@/components/common/CodeBadge';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import ExcelExportButton from '@/components/common/ExcelExportButton';
|
||||
import { defaultPagination } from '@/utils/tablePagination';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { ledgerApi, LedgerResp, LedgerSearchParam } from '@/api/ledger';
|
||||
|
||||
export default function RecruitLedger() {
|
||||
const [param, setParam] = useState<LedgerSearchParam>({ pageNum: 1, pageSize: 50 });
|
||||
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
||||
const { data: reconCodes = [] } = useCommonCodes('RECON_STATUS');
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['ledger', 'recruit', param],
|
||||
queryFn: () => ledgerApi.recruit(param),
|
||||
});
|
||||
const columns: ProColumns<LedgerResp>[] = [
|
||||
{
|
||||
title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left',
|
||||
valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' },
|
||||
},
|
||||
{ title: '증권번호', dataIndex: 'policyNo', width: 130, fixed: 'left',
|
||||
fieldProps: { placeholder: '증권번호' } },
|
||||
{
|
||||
title: '보험종류', dataIndex: 'insuranceType', width: 100, valueType: 'select',
|
||||
valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
|
||||
},
|
||||
{
|
||||
title: '대사상태', dataIndex: 'reconcileStatus', width: 100, valueType: 'select',
|
||||
valueEnum: Object.fromEntries(reconCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="RECON_STATUS" value={r.reconcileStatus} />,
|
||||
},
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100, search: false },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160, search: false },
|
||||
{ title: '보험사', dataIndex: 'companyName', width: 120, search: false },
|
||||
{ title: '상품', dataIndex: 'productName', width: 200, search: false },
|
||||
{
|
||||
title: '보험료', dataIndex: 'premium', width: 110, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.premium} />,
|
||||
},
|
||||
{
|
||||
title: '회사율', dataIndex: 'companyRate', width: 80, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.companyRate} fraction={2} />,
|
||||
},
|
||||
{
|
||||
title: '회사수수료', dataIndex: 'companyAmount', width: 120, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.companyAmount} />,
|
||||
},
|
||||
{
|
||||
title: '지급율', dataIndex: 'payoutRate', width: 80, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.payoutRate} fraction={2} />,
|
||||
},
|
||||
{
|
||||
title: '설계사수수료', dataIndex: 'agentAmount', width: 120, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.agentAmount} />,
|
||||
},
|
||||
{
|
||||
title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.taxAmount} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="모집수수료 원장"
|
||||
extra={
|
||||
<ExcelExportButton url="/api/ledger/recruit/export" params={param} fileName="모집수수료원장" />
|
||||
}
|
||||
>
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'month', name: 'settleMonth', label: '정산월' },
|
||||
{ type: 'text', name: 'policyNo', label: '증권번호' },
|
||||
{ type: 'code', name: 'insuranceType', label: '보험종류', groupCode: 'INSURANCE_TYPE' },
|
||||
{ type: 'code', name: 'reconcileStatus', label: '대사상태', groupCode: 'RECON_STATUS' },
|
||||
]}
|
||||
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })}
|
||||
onReset={() => setParam({ pageNum: 1, pageSize: 50 })}
|
||||
/>
|
||||
|
||||
<Table<LedgerResp>
|
||||
<PageContainer title="모집수수료 원장" description="신규 계약 모집에 따른 수수료 원장">
|
||||
<ProTable<LedgerResp, LedgerSearchParam>
|
||||
rowKey="ledgerId"
|
||||
loading={isLoading}
|
||||
dataSource={data?.list}
|
||||
size="small"
|
||||
columns={columns}
|
||||
scroll={{ x: 1500 }}
|
||||
pagination={{
|
||||
...defaultPagination,
|
||||
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
|
||||
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
|
||||
request={async (params) => {
|
||||
const { current, pageSize, ...rest } = params as any;
|
||||
const res = await ledgerApi.recruit({ ...rest, pageNum: current, pageSize });
|
||||
return { data: res.list, total: res.total, success: true };
|
||||
}}
|
||||
columns={[
|
||||
{ title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left' },
|
||||
{ title: '증권번호', dataIndex: 'policyNo', width: 130, fixed: 'left' },
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100 },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160 },
|
||||
{ title: '보험사', dataIndex: 'companyName', width: 120 },
|
||||
{ title: '상품', dataIndex: 'productName', width: 200 },
|
||||
{ title: '보험종류', dataIndex: 'insuranceType', width: 100,
|
||||
render: (v) => <CodeBadge groupCode="INSURANCE_TYPE" value={v} /> },
|
||||
{ title: '보험료', dataIndex: 'premium', width: 110, align: 'right',
|
||||
render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '회사율', dataIndex: 'companyRate', width: 80, align: 'right',
|
||||
render: (v) => <MoneyText value={v} fraction={2} /> },
|
||||
{ title: '회사수수료', dataIndex: 'companyAmount', width: 120, align: 'right',
|
||||
render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '지급율', dataIndex: 'payoutRate', width: 80, align: 'right',
|
||||
render: (v) => <MoneyText value={v} fraction={2} /> },
|
||||
{ title: '설계사수수료', dataIndex: 'agentAmount', width: 120, align: 'right',
|
||||
render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right',
|
||||
render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '대사상태', dataIndex: 'reconcileStatus', width: 100,
|
||||
render: (v) => <CodeBadge groupCode="RECON_STATUS" value={v} /> },
|
||||
pagination={{
|
||||
pageSize: 50, showSizeChanger: true, pageSizeOptions: ['20', '50', '100', '200'],
|
||||
showTotal: (total) => `총 ${total.toLocaleString()}건`,
|
||||
}}
|
||||
search={{ labelWidth: 'auto', searchText: '검색', resetText: '초기화', collapsed: false, collapseRender: false }}
|
||||
options={{ density: false, fullScreen: true, reload: true, setting: true }}
|
||||
toolBarRender={() => [
|
||||
<ExcelExportButton key="export" url="/api/ledger/recruit/export" fileName="모집수수료원장" />,
|
||||
]}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -1,79 +1,92 @@
|
||||
import { useState } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRef } from 'react';
|
||||
import { Space } from 'antd';
|
||||
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import CodeBadge from '@/components/common/CodeBadge';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import ExcelExportButton from '@/components/common/ExcelExportButton';
|
||||
import { defaultPagination } from '@/utils/tablePagination';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { agentApi, AgentResp, AgentSearchParam } from '@/api/agent';
|
||||
|
||||
export default function AgentList() {
|
||||
const navigate = useNavigate();
|
||||
const [param, setParam] = useState<AgentSearchParam>({ pageNum: 1, pageSize: 20 });
|
||||
const actionRef = useRef<ActionType>();
|
||||
const { data: statusCodes = [] } = useCommonCodes('AGENT_STATUS');
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['agent', 'list', param],
|
||||
queryFn: () => agentApi.list(param),
|
||||
});
|
||||
const columns: ProColumns<AgentResp>[] = [
|
||||
{
|
||||
title: '설계사명', dataIndex: 'agentName', width: 120, fixed: 'left',
|
||||
render: (_, r) => (
|
||||
<a onClick={() => navigate(`/org/agents/${r.agentId}`)} style={{ fontWeight: 500 }}>
|
||||
{r.agentName}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{ title: '사번', dataIndex: 'licenseNo', width: 120, search: false },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 200, search: false },
|
||||
{ title: '직급', dataIndex: 'gradeName', width: 100, search: false },
|
||||
{ title: '전화번호', dataIndex: 'phone', width: 140, search: false },
|
||||
{
|
||||
title: '상태', dataIndex: 'status', width: 100, valueType: 'select',
|
||||
valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="AGENT_STATUS" value={r.status} />,
|
||||
},
|
||||
{
|
||||
title: '계약수', dataIndex: 'contractCount', width: 90, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.contractCount} />,
|
||||
},
|
||||
{
|
||||
title: '누적수수료', dataIndex: 'totalCommission', width: 130, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.totalCommission} />,
|
||||
},
|
||||
{ title: '입사일', dataIndex: 'joinDate', width: 110, search: false },
|
||||
{
|
||||
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||
fieldProps: { placeholder: '설계사명' },
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="설계사 관리"
|
||||
extra={
|
||||
<div>
|
||||
<PermissionButton menuCode="ORG_AGENT" permCode="CREATE" type="primary"
|
||||
onClick={() => navigate('/org/agents/new')} style={{ marginRight: 8 }}>
|
||||
등록
|
||||
</PermissionButton>
|
||||
<ExcelExportButton url="/api/agents/export" params={param} fileName="설계사목록" />
|
||||
</div>
|
||||
}
|
||||
description="조직별 설계사 등록·관리, 누적 수수료 추적"
|
||||
>
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'text', name: 'searchKeyword', label: '설계사명' },
|
||||
{ type: 'code', name: 'status', label: '상태', groupCode: 'AGENT_STATUS' },
|
||||
]}
|
||||
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })}
|
||||
onReset={() => setParam({ pageNum: 1, pageSize: 20 })}
|
||||
/>
|
||||
|
||||
<Table<AgentResp>
|
||||
<ProTable<AgentResp, AgentSearchParam>
|
||||
actionRef={actionRef}
|
||||
rowKey="agentId"
|
||||
loading={isLoading}
|
||||
dataSource={data?.list}
|
||||
pagination={{
|
||||
...defaultPagination,
|
||||
current: param.pageNum,
|
||||
pageSize: param.pageSize,
|
||||
total: data?.total,
|
||||
onChange: (page, size) => setParam({ ...param, pageNum: page, pageSize: size }),
|
||||
columns={columns}
|
||||
scroll={{ x: 1200 }}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, ...rest } = params as any;
|
||||
const res = await agentApi.list({ ...rest, pageNum: current, pageSize });
|
||||
return { data: res.list, total: res.total, success: true };
|
||||
}}
|
||||
onRow={(r) => ({ onClick: () => navigate(`/org/agents/${r.agentId}`) })}
|
||||
columns={[
|
||||
{ title: '설계사명', dataIndex: 'agentName', width: 120 },
|
||||
{ title: '사번', dataIndex: 'licenseNo', width: 120 },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 200 },
|
||||
{ title: '직급', dataIndex: 'gradeName', width: 100 },
|
||||
{ title: '전화번호', dataIndex: 'phone', width: 140 },
|
||||
{
|
||||
title: '상태', dataIndex: 'status', width: 100,
|
||||
render: (v) => <CodeBadge groupCode="AGENT_STATUS" value={v} />,
|
||||
},
|
||||
{
|
||||
title: '계약수', dataIndex: 'contractCount', align: 'right', width: 90,
|
||||
render: (v) => <MoneyText value={v} />,
|
||||
},
|
||||
{
|
||||
title: '누적수수료', dataIndex: 'totalCommission', align: 'right', width: 130,
|
||||
render: (v) => <MoneyText value={v} />,
|
||||
},
|
||||
{ title: '입사일', dataIndex: 'joinDate', width: 110 },
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
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={() => [
|
||||
<ExcelExportButton key="export" url="/api/agents/export" fileName="설계사목록" />,
|
||||
<PermissionButton
|
||||
key="create"
|
||||
menuCode="ORG_AGENT" permCode="CREATE" type="primary"
|
||||
onClick={() => navigate('/org/agents/new')}
|
||||
>+ 설계사 등록</PermissionButton>,
|
||||
]}
|
||||
dateFormatter="string"
|
||||
headerTitle={<Space size={6}><span style={{ fontWeight: 600 }}>설계사 목록</span></Space>}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -1,67 +1,65 @@
|
||||
import { useState } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import CodeBadge from '@/components/common/CodeBadge';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import { defaultPagination } from '@/utils/tablePagination';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { contractApi, ContractResp, ContractSearchParam } from '@/api/contract';
|
||||
|
||||
export default function ContractList() {
|
||||
const [param, setParam] = useState<ContractSearchParam>({ pageNum: 1, pageSize: 20 });
|
||||
const { data: insuranceCodes = [] } = useCommonCodes('INSURANCE_TYPE');
|
||||
const { data: statusCodes = [] } = useCommonCodes('CONTRACT_STATUS');
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['contract', 'list', param],
|
||||
queryFn: () => contractApi.list(param),
|
||||
});
|
||||
const columns: ProColumns<ContractResp>[] = [
|
||||
{
|
||||
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||
fieldProps: { placeholder: '계약자/증권번호' },
|
||||
},
|
||||
{
|
||||
title: '보험종류', dataIndex: 'insuranceType', width: 100, valueType: 'select',
|
||||
valueEnum: Object.fromEntries(insuranceCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="INSURANCE_TYPE" value={r.insuranceType} />,
|
||||
},
|
||||
{
|
||||
title: '상태', dataIndex: 'status', width: 90, valueType: 'select',
|
||||
valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="CONTRACT_STATUS" value={r.status} />,
|
||||
},
|
||||
{ title: '증권번호', dataIndex: 'policyNo', width: 140, search: false },
|
||||
{ title: '계약자', dataIndex: 'contractorName', width: 100, search: false },
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100, search: false },
|
||||
{ title: '보험사', dataIndex: 'companyName', width: 120, search: false },
|
||||
{ title: '상품', dataIndex: 'productName', width: 220, search: false },
|
||||
{
|
||||
title: '보험료', dataIndex: 'premium', width: 120, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.premium} />,
|
||||
},
|
||||
{
|
||||
title: '납입주기', dataIndex: 'payCycle', width: 90, search: false,
|
||||
render: (_, r) => <CodeBadge groupCode="PAY_CYCLE" value={r.payCycle} />,
|
||||
},
|
||||
{ title: '계약일', dataIndex: 'contractDate', width: 110, search: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer title="계약 관리">
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'text', name: 'searchKeyword', label: '계약자/증권번호' },
|
||||
{ type: 'code', name: 'insuranceType', label: '보험종류', groupCode: 'INSURANCE_TYPE' },
|
||||
{ type: 'code', name: 'status', label: '상태', groupCode: 'CONTRACT_STATUS' },
|
||||
{ type: 'dateRange', name: 'dateRange', label: '계약일' },
|
||||
]}
|
||||
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })}
|
||||
onReset={() => setParam({ pageNum: 1, pageSize: 20 })}
|
||||
/>
|
||||
|
||||
<Table<ContractResp>
|
||||
<PageContainer title="계약 관리" description="보험사·상품·계약 통합 관리">
|
||||
<ProTable<ContractResp, ContractSearchParam>
|
||||
rowKey="contractId"
|
||||
loading={isLoading}
|
||||
dataSource={data?.list}
|
||||
pagination={{
|
||||
...defaultPagination,
|
||||
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
|
||||
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
|
||||
columns={columns}
|
||||
scroll={{ x: 1300 }}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, ...rest } = params as any;
|
||||
const res = await contractApi.list({ ...rest, pageNum: current, pageSize });
|
||||
return { data: res.list, total: res.total, success: true };
|
||||
}}
|
||||
columns={[
|
||||
{ title: '증권번호', dataIndex: 'policyNo', width: 140 },
|
||||
{ title: '계약자', dataIndex: 'contractorName', width: 100 },
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100 },
|
||||
{ title: '보험사', dataIndex: 'companyName', width: 120 },
|
||||
{ title: '상품', dataIndex: 'productName', width: 220 },
|
||||
{
|
||||
title: '보험종류', dataIndex: 'insuranceType', width: 100,
|
||||
render: (v) => <CodeBadge groupCode="INSURANCE_TYPE" value={v} />,
|
||||
},
|
||||
{
|
||||
title: '보험료', dataIndex: 'premium', width: 120, align: 'right',
|
||||
render: (v) => <MoneyText value={v} />,
|
||||
},
|
||||
{
|
||||
title: '납입주기', dataIndex: 'payCycle', width: 90,
|
||||
render: (v) => <CodeBadge groupCode="PAY_CYCLE" value={v} />,
|
||||
},
|
||||
{
|
||||
title: '상태', dataIndex: 'status', width: 90,
|
||||
render: (v) => <CodeBadge groupCode="CONTRACT_STATUS" value={v} />,
|
||||
},
|
||||
{ title: '계약일', dataIndex: 'contractDate', width: 110 },
|
||||
]}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
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 }}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -1,60 +1,52 @@
|
||||
import { useState } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ProTable, type ProColumns } from '@ant-design/pro-components';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import CodeBadge from '@/components/common/CodeBadge';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import { defaultPagination } from '@/utils/tablePagination';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { paymentApi, PaymentResp } from '@/api/payment';
|
||||
|
||||
export default function PaymentList() {
|
||||
const [param, setParam] = useState<{ settleMonth?: string; status?: string; pageNum: number; pageSize: number }>({
|
||||
pageNum: 1, pageSize: 30,
|
||||
});
|
||||
const { data: statusCodes = [] } = useCommonCodes('PAYMENT_STATUS');
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['payment', 'list', param],
|
||||
queryFn: () => paymentApi.list(param),
|
||||
});
|
||||
const columns: ProColumns<PaymentResp>[] = [
|
||||
{
|
||||
title: '정산월', dataIndex: 'settleMonth', width: 90,
|
||||
valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' },
|
||||
},
|
||||
{
|
||||
title: '지급상태', dataIndex: 'payStatus', width: 100, valueType: 'select',
|
||||
valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="PAYMENT_STATUS" value={r.payStatus} />,
|
||||
},
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100, search: false },
|
||||
{ title: '은행', dataIndex: 'bankName', width: 100, search: false },
|
||||
{ title: '계좌', dataIndex: 'accountNo', width: 180, search: false },
|
||||
{
|
||||
title: '지급액', dataIndex: 'payAmount', width: 140, align: 'right', search: false,
|
||||
render: (_, r) => <strong><MoneyText value={r.payAmount} /></strong>,
|
||||
},
|
||||
{ title: '지급일', dataIndex: 'payDate', width: 110, search: false },
|
||||
{ title: '실패사유', dataIndex: 'failReason', ellipsis: true, search: false },
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer title="지급 관리">
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'month', name: 'settleMonth', label: '정산월' },
|
||||
{ type: 'code', name: 'status', label: '지급상태', groupCode: 'PAYMENT_STATUS' },
|
||||
]}
|
||||
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })}
|
||||
onReset={() => setParam({ pageNum: 1, pageSize: 30 })}
|
||||
/>
|
||||
|
||||
<Table<PaymentResp>
|
||||
<PageContainer title="지급 관리" description="확정 정산 → 이체 파일 생성 / 결과 추적">
|
||||
<ProTable<PaymentResp>
|
||||
rowKey="paymentId"
|
||||
loading={isLoading}
|
||||
dataSource={data?.list}
|
||||
size="small"
|
||||
pagination={{
|
||||
...defaultPagination,
|
||||
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
|
||||
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
|
||||
columns={columns}
|
||||
scroll={{ x: 1100 }}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, ...rest } = params as any;
|
||||
const res = await paymentApi.list({ ...rest, pageNum: current, pageSize });
|
||||
return { data: res.list, total: res.total, success: true };
|
||||
}}
|
||||
columns={[
|
||||
{ title: '정산월', dataIndex: 'settleMonth', width: 90 },
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100 },
|
||||
{ title: '은행', dataIndex: 'bankName', width: 100 },
|
||||
{ title: '계좌', dataIndex: 'accountNo', width: 180 },
|
||||
{
|
||||
title: '지급액', dataIndex: 'payAmount', width: 140, align: 'right',
|
||||
render: (v) => <strong><MoneyText value={v} /></strong>,
|
||||
},
|
||||
{ title: '지급일', dataIndex: 'payDate', width: 110 },
|
||||
{
|
||||
title: '상태', dataIndex: 'payStatus', width: 100,
|
||||
render: (v) => <CodeBadge groupCode="PAYMENT_STATUS" value={v} />,
|
||||
},
|
||||
{ title: '실패사유', dataIndex: 'failReason', ellipsis: 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 }}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, Col, Row, Statistic, Table, message, Modal, Input, Space } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Input, Modal, Space, message } from 'antd';
|
||||
import { ProCard, ProTable, StatisticCard, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import CodeBadge from '@/components/common/CodeBadge';
|
||||
import MoneyText from '@/components/common/MoneyText';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { defaultPagination } from '@/utils/tablePagination';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { settleApi, SettleResp, SettleSearchParam } from '@/api/settle';
|
||||
|
||||
const { Statistic } = StatisticCard;
|
||||
|
||||
export default function SettleList() {
|
||||
const qc = useQueryClient();
|
||||
const [param, setParam] = useState<SettleSearchParam>({ pageNum: 1, pageSize: 30 });
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['settle', 'list', param],
|
||||
queryFn: () => settleApi.list(param),
|
||||
});
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [settleMonth, setSettleMonth] = useState<string | undefined>(undefined);
|
||||
const { data: statusCodes = [] } = useCommonCodes('SETTLE_STATUS');
|
||||
|
||||
const { data: summary } = useQuery({
|
||||
queryKey: ['settle', 'summary', param.settleMonth],
|
||||
queryFn: () => settleApi.summary(param.settleMonth!),
|
||||
enabled: !!param.settleMonth,
|
||||
queryKey: ['settle', 'summary', settleMonth],
|
||||
queryFn: () => settleApi.summary(settleMonth!),
|
||||
enabled: !!settleMonth,
|
||||
});
|
||||
|
||||
const refresh = () => qc.invalidateQueries({ queryKey: ['settle'] });
|
||||
const refresh = () => {
|
||||
qc.invalidateQueries({ queryKey: ['settle'] });
|
||||
actionRef.current?.reload();
|
||||
};
|
||||
|
||||
const onConfirm = (id: number) =>
|
||||
Modal.confirm({
|
||||
@@ -41,73 +43,83 @@ export default function SettleList() {
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer title="정산 결과">
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'month', name: 'settleMonth', label: '정산월' },
|
||||
{ type: 'code', name: 'status', label: '상태', groupCode: 'SETTLE_STATUS' },
|
||||
]}
|
||||
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })}
|
||||
onReset={() => setParam({ pageNum: 1, pageSize: 30 })}
|
||||
/>
|
||||
const columns: ProColumns<SettleResp>[] = [
|
||||
{ title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left',
|
||||
valueType: 'dateMonth', fieldProps: { format: 'YYYYMM' } },
|
||||
{
|
||||
title: '상태', dataIndex: 'status', width: 100, valueType: 'select', fixed: 'right',
|
||||
valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="SETTLE_STATUS" value={r.status} />,
|
||||
},
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100, fixed: 'left', search: false },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160, search: false },
|
||||
{ title: '직급', dataIndex: 'gradeName', width: 80, search: false },
|
||||
{ title: '모집', dataIndex: 'recruitTotal', width: 110, align: 'right', search: false, render: (_, r) => <MoneyText value={r.recruitTotal} /> },
|
||||
{ title: '유지', dataIndex: 'maintainTotal', width: 110, align: 'right', search: false, render: (_, r) => <MoneyText value={r.maintainTotal} /> },
|
||||
{ title: '예외(+)', dataIndex: 'exceptionPlus', width: 100, align: 'right', search: false, render: (_, r) => <MoneyText value={r.exceptionPlus} /> },
|
||||
{ title: '예외(-)', dataIndex: 'exceptionMinus', width: 100, align: 'right', search: false, render: (_, r) => <MoneyText value={r.exceptionMinus} /> },
|
||||
{ title: '오버라이드', dataIndex: 'overrideTotal', width: 110, align: 'right', search: false, render: (_, r) => <MoneyText value={r.overrideTotal} /> },
|
||||
{
|
||||
title: '환수', dataIndex: 'chargebackTotal', width: 100, align: 'right', search: false,
|
||||
render: (_, r) => <MoneyText value={r.chargebackTotal ? -Math.abs(Number(r.chargebackTotal)) : 0} />,
|
||||
},
|
||||
{ title: '총액', dataIndex: 'grossAmount', width: 120, align: 'right', search: false, render: (_, r) => <MoneyText value={r.grossAmount} /> },
|
||||
{ title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', search: false, render: (_, r) => <MoneyText value={r.taxAmount} /> },
|
||||
{
|
||||
title: '실지급', dataIndex: 'netAmount', width: 130, align: 'right', search: false,
|
||||
render: (_, r) => <strong><MoneyText value={r.netAmount} /></strong>,
|
||||
},
|
||||
{
|
||||
title: '액션', dataIndex: 'settleId', width: 200, fixed: 'right', search: false,
|
||||
render: (_, r) => (
|
||||
<Space>
|
||||
<PermissionButton menuCode="SETTLE_LIST" permCode="APPROVE" size="small" type="primary"
|
||||
onClick={() => onConfirm(r.settleId)}
|
||||
disabled={r.status === 'CONFIRMED' || r.status === 'PAID'}>확정</PermissionButton>
|
||||
<PermissionButton menuCode="SETTLE_LIST" permCode="APPROVE" size="small" danger
|
||||
onClick={() => onHold(r.settleId)} disabled={r.status === 'PAID'}>보류</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer title="정산 결과" description="설계사별 월 정산 합산 / 확정·보류">
|
||||
{summary && (
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}><Card><Statistic title="대상 설계사" value={Number(summary.agentCount ?? 0)} suffix="명" /></Card></Col>
|
||||
<Col span={6}><Card><Statistic title="총 지급액(net)" value={Number(summary.netAmount ?? 0)} /></Card></Col>
|
||||
<Col span={6}><Card><Statistic title="총 세액" value={Number(summary.taxAmount ?? 0)} valueStyle={{ color: '#cf1322' }} /></Card></Col>
|
||||
<Col span={6}><Card><Statistic title="확정 건수" value={Number(summary.confirmedCount ?? 0)} suffix="건" /></Card></Col>
|
||||
</Row>
|
||||
<ProCard ghost gutter={16} style={{ marginBottom: 16 }} wrap>
|
||||
<ProCard colSpan={6} bordered={false}>
|
||||
<Statistic title="대상 설계사" value={Number(summary.agentCount ?? 0)} suffix="명" />
|
||||
</ProCard>
|
||||
<ProCard colSpan={6} bordered={false}>
|
||||
<Statistic title="총 지급액 (net)" value={Number(summary.netAmount ?? 0)} />
|
||||
</ProCard>
|
||||
<ProCard colSpan={6} bordered={false}>
|
||||
<Statistic title="총 세액" value={Number(summary.taxAmount ?? 0)} valueStyle={{ color: '#E24B4A' }} />
|
||||
</ProCard>
|
||||
<ProCard colSpan={6} bordered={false}>
|
||||
<Statistic title="확정 건수" value={Number(summary.confirmedCount ?? 0)} suffix="건" />
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
)}
|
||||
|
||||
<Table<SettleResp>
|
||||
<ProTable<SettleResp, SettleSearchParam>
|
||||
actionRef={actionRef}
|
||||
rowKey="settleId"
|
||||
loading={isLoading}
|
||||
dataSource={data?.list}
|
||||
size="small"
|
||||
scroll={{ x: 1600 }}
|
||||
pagination={{
|
||||
...defaultPagination,
|
||||
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
|
||||
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
|
||||
columns={columns}
|
||||
scroll={{ x: 1700 }}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, settleMonth: sm, ...rest } = params as any;
|
||||
setSettleMonth(sm);
|
||||
const res = await settleApi.list({ ...rest, settleMonth: sm, pageNum: current, pageSize });
|
||||
return { data: res.list, total: res.total, success: true };
|
||||
}}
|
||||
columns={[
|
||||
{ title: '정산월', dataIndex: 'settleMonth', width: 90, fixed: 'left' },
|
||||
{ title: '설계사', dataIndex: 'agentName', width: 100, fixed: 'left' },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160 },
|
||||
{ title: '직급', dataIndex: 'gradeName', width: 80 },
|
||||
{ title: '모집', dataIndex: 'recruitTotal', width: 110, align: 'right', render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '유지', dataIndex: 'maintainTotal', width: 110, align: 'right', render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '예외(+)', dataIndex: 'exceptionPlus', width: 100, align: 'right', render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '예외(-)', dataIndex: 'exceptionMinus', width: 100, align: 'right', render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '오버라이드', dataIndex: 'overrideTotal', width: 110, align: 'right', render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '환수', dataIndex: 'chargebackTotal', width: 100, align: 'right',
|
||||
render: (v) => <MoneyText value={v ? -Math.abs(Number(v)) : 0} /> },
|
||||
{ title: '총액', dataIndex: 'grossAmount', width: 120, align: 'right', render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '세액', dataIndex: 'taxAmount', width: 100, align: 'right', render: (v) => <MoneyText value={v} /> },
|
||||
{ title: '실지급', dataIndex: 'netAmount', width: 130, align: 'right',
|
||||
render: (v) => <strong><MoneyText value={v} /></strong> },
|
||||
{ title: '상태', dataIndex: 'status', width: 100, fixed: 'right',
|
||||
render: (v) => <CodeBadge groupCode="SETTLE_STATUS" value={v} /> },
|
||||
{
|
||||
title: '액션', dataIndex: 'settleId', width: 200, fixed: 'right',
|
||||
render: (id, r) => (
|
||||
<Space>
|
||||
<PermissionButton menuCode="SETTLE_LIST" permCode="APPROVE" size="small"
|
||||
type="primary" onClick={() => onConfirm(id)}
|
||||
disabled={r.status === 'CONFIRMED' || r.status === 'PAID'}>
|
||||
확정
|
||||
</PermissionButton>
|
||||
<PermissionButton menuCode="SETTLE_LIST" permCode="APPROVE" size="small"
|
||||
danger onClick={() => onHold(id)}
|
||||
disabled={r.status === 'PAID'}>
|
||||
보류
|
||||
</PermissionButton>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
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 }}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { useState } from 'react';
|
||||
import { Table, Space, message, Modal } from 'antd';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useRef } from 'react';
|
||||
import { Modal, Space, message } from 'antd';
|
||||
import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import PageContainer from '@/components/common/PageContainer';
|
||||
import SearchForm from '@/components/common/SearchForm';
|
||||
import CodeBadge from '@/components/common/CodeBadge';
|
||||
import PermissionButton from '@/components/common/PermissionButton';
|
||||
import { defaultPagination } from '@/utils/tablePagination';
|
||||
import { useCommonCodes } from '@/hooks/useCommonCodes';
|
||||
import { userApi, UserResp } from '@/api/user';
|
||||
|
||||
export default function UserList() {
|
||||
const qc = useQueryClient();
|
||||
const [param, setParam] = useState<{ searchKeyword?: string; status?: string; pageNum: number; pageSize: number }>({
|
||||
pageNum: 1, pageSize: 30,
|
||||
});
|
||||
const actionRef = useRef<ActionType>();
|
||||
const { data: statusCodes = [] } = useCommonCodes('AGENT_STATUS');
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['user', 'list', param],
|
||||
queryFn: () => userApi.list(param),
|
||||
});
|
||||
|
||||
const refresh = () => qc.invalidateQueries({ queryKey: ['user'] });
|
||||
const refresh = () => {
|
||||
qc.invalidateQueries({ queryKey: ['user'] });
|
||||
actionRef.current?.reload();
|
||||
};
|
||||
|
||||
const onReset = (id: number) =>
|
||||
Modal.confirm({
|
||||
@@ -34,52 +31,57 @@ export default function UserList() {
|
||||
onOk: async () => { await userApi.unlock(id); message.success('해제 완료'); refresh(); },
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer title="사용자 관리">
|
||||
<SearchForm
|
||||
conditions={[
|
||||
{ type: 'text', name: 'searchKeyword', label: '아이디/이름' },
|
||||
{ type: 'code', name: 'status', label: '상태', groupCode: 'AGENT_STATUS' },
|
||||
]}
|
||||
onSearch={(v) => setParam({ ...param, ...v, pageNum: 1 })}
|
||||
onReset={() => setParam({ pageNum: 1, pageSize: 30 })}
|
||||
/>
|
||||
const columns: ProColumns<UserResp>[] = [
|
||||
{
|
||||
title: '검색어', dataIndex: 'searchKeyword', hideInTable: true,
|
||||
fieldProps: { placeholder: '아이디 / 이름' },
|
||||
},
|
||||
{
|
||||
title: '상태', dataIndex: 'status', width: 100, valueType: 'select',
|
||||
valueEnum: Object.fromEntries(statusCodes.map((c) => [c.code, { text: c.codeName }])),
|
||||
render: (_, r) => <CodeBadge groupCode="AGENT_STATUS" value={r.status} />,
|
||||
},
|
||||
{ title: '로그인 ID', dataIndex: 'loginId', width: 120, search: false },
|
||||
{ title: '이름', dataIndex: 'userName', width: 120, search: false },
|
||||
{ title: '이메일', dataIndex: 'email', width: 200, search: false },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160, search: false },
|
||||
{ title: '연결 설계사', dataIndex: 'agentName', width: 100, search: false },
|
||||
{ title: '실패횟수', dataIndex: 'failCount', width: 90, align: 'right', search: false },
|
||||
{ title: '마지막 로그인', dataIndex: 'lastLoginAt', width: 160, search: false },
|
||||
{
|
||||
title: '액션', width: 220, fixed: 'right', search: false,
|
||||
render: (_, r) => (
|
||||
<Space>
|
||||
<PermissionButton menuCode="SYSTEM_USER" permCode="UPDATE" size="small"
|
||||
onClick={() => onReset(r.userId)}>비번초기화</PermissionButton>
|
||||
{r.status === 'LOCKED' && (
|
||||
<PermissionButton menuCode="SYSTEM_USER" permCode="UPDATE" size="small" type="primary"
|
||||
onClick={() => onUnlock(r.userId)}>잠금해제</PermissionButton>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
<Table<UserResp>
|
||||
return (
|
||||
<PageContainer title="사용자 관리" description="시스템 사용자 / 역할 매핑 / 비밀번호">
|
||||
<ProTable<UserResp>
|
||||
actionRef={actionRef}
|
||||
rowKey="userId"
|
||||
loading={isLoading}
|
||||
dataSource={data?.list}
|
||||
pagination={{
|
||||
...defaultPagination,
|
||||
current: param.pageNum, pageSize: param.pageSize, total: data?.total,
|
||||
onChange: (p, s) => setParam({ ...param, pageNum: p, pageSize: s }),
|
||||
columns={columns}
|
||||
scroll={{ x: 1300 }}
|
||||
request={async (params) => {
|
||||
const { current, pageSize, ...rest } = params as any;
|
||||
const res = await userApi.list({ ...rest, pageNum: current, pageSize });
|
||||
return { data: res.list, total: res.total, success: true };
|
||||
}}
|
||||
columns={[
|
||||
{ title: '로그인 ID', dataIndex: 'loginId', width: 120 },
|
||||
{ title: '이름', dataIndex: 'userName', width: 120 },
|
||||
{ title: '이메일', dataIndex: 'email', width: 200 },
|
||||
{ title: '소속', dataIndex: 'orgName', width: 160 },
|
||||
{ title: '연결 설계사', dataIndex: 'agentName', width: 100 },
|
||||
{
|
||||
title: '상태', dataIndex: 'status', width: 100,
|
||||
render: (v) => <CodeBadge groupCode="AGENT_STATUS" value={v} />,
|
||||
},
|
||||
{ title: '실패횟수', dataIndex: 'failCount', width: 90, align: 'right' },
|
||||
{ title: '마지막 로그인', dataIndex: 'lastLoginAt', width: 160 },
|
||||
{
|
||||
title: '액션', width: 220, fixed: 'right',
|
||||
render: (_, r) => (
|
||||
<Space>
|
||||
<PermissionButton menuCode="SYSTEM_USER" permCode="UPDATE" size="small"
|
||||
onClick={() => onReset(r.userId)}>비번초기화</PermissionButton>
|
||||
{r.status === 'LOCKED' && (
|
||||
<PermissionButton menuCode="SYSTEM_USER" permCode="UPDATE" size="small" type="primary"
|
||||
onClick={() => onUnlock(r.userId)}>잠금해제</PermissionButton>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
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 }}
|
||||
dateFormatter="string"
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user