| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- import request from '@/utils/request';
- /**
- * 五险一金方案(参保方案)—— 对接后端 hr 分组真实接口 /hr/social/config/scheme
- *
- * 设计定位:方案是**公司级(法人级)**的,不是模板级。
- * 缴费比例与缴费基数上下限按参保城市走、随社保年度调整,一家公司配一次,
- * 所有薪资模板通过 socialPlanId 引用同一个方案(见「薪资模板 → 五险一金」页签)。
- *
- * 后端接口:
- * POST /hr/social/config/scheme 保存(新增或修改草稿;带 id 即修改)
- * POST /hr/social/config/scheme/page 分页查询
- * GET /hr/social/config/scheme/{id} 详情
- * PUT /hr/social/config/scheme/{id}/enable 启用
- * PUT /hr/social/config/scheme/{id}/disable 停用
- *
- * 注意:后端**没有删除接口**,也没有 PUT /{id}/ 直接改主档——
- * 修改一律走 POST 带 id(且已启用方案不可直接覆盖,避免影响历史台账)。
- */
- function unwrap(res) {
- if (res.data.code == 0) {
- return res.data.data;
- }
- return Promise.reject(new Error(res.data.message || '接口调用失败'));
- }
- /** 险种字典:与后端「社保公积金方案险种参数」itemType 枚举一致(文档原文列举) */
- export const SOCIAL_ITEM_TYPE_OPTIONS = [
- { value: 'PENSION', label: '养老保险' },
- { value: 'MEDICAL', label: '医疗保险' },
- { value: 'UNEMPLOYMENT', label: '失业保险' },
- { value: 'WORK_INJURY', label: '工伤保险' },
- { value: 'MATERNITY', label: '生育保险' },
- { value: 'CRITICAL_ILLNESS', label: '大病医疗' },
- { value: 'FUND', label: '住房公积金' },
- { value: 'OTHER', label: '其他' }
- ];
- /** 公积金的险种编码(唯一对「公积金」的险种) */
- export const FUND_ITEM_TYPE = 'FUND';
- /** 工伤险:独有「基准比例 / 浮动系数 / 行业风险等级」三个字段 */
- export const WORK_INJURY_ITEM_TYPE = 'WORK_INJURY';
- /**
- * 缴费基数来源。社保与公积金各自独立配置一个来源,但字典相同(文档原文列举)。
- */
- export const SOCIAL_BASE_SOURCE_OPTIONS = [
- { value: 'LAST_YEAR_AVG', label: '上年度月平均工资' },
- { value: 'CURRENT_SALARY', label: '本人当月工资' },
- { value: 'FIXED', label: '统一固定基数' },
- { value: 'CONTRACT', label: '劳动合同约定工资' },
- { value: 'MANUAL', label: '手工录入' }
- ];
- /** 金额处理方式:HALF_UP四舍五入、UP向上、DOWN向下,默认HALF_UP */
- export const ROUNDING_MODE_OPTIONS = [
- { value: 'HALF_UP', label: '四舍五入' },
- { value: 'UP', label: '向上进位' },
- { value: 'DOWN', label: '向下截断' }
- ];
- /** 金额小数精度:0元、1角、2分,默认2 */
- export const ROUNDING_SCALE_OPTIONS = [
- { value: 0, label: '元(0 位小数)' },
- { value: 1, label: '角(1 位小数)' },
- { value: 2, label: '分(2 位小数)' }
- ];
- /** 是否允许补缴 */
- export const ALLOW_SUPPLEMENT_OPTIONS = [
- { value: 1, label: '允许补缴' },
- { value: 0, label: '不允许补缴' }
- ];
- /**
- * 方案状态展示映射。
- *
- * 证据:Swagger 未给 scheme 的 status 枚举(「社保公积金方案返回模型」中 status 仅注释「方案状态」)。
- * 可确定的取值来自接口语义——保存接口描述为「新增或修改**草稿**方案」,
- * 且存在独立的 enable / disable 接口,故 DRAFT / ENABLED / DISABLED 三个值有依据。
- * 其余取值(如审批中)文档无据,这里不做臆测,落到「原样灰字显示」兜底。
- */
- export const SOCIAL_SCHEME_STATUS_META = {
- DRAFT: { label: '草稿', type: 'info' },
- ENABLED: { label: '已启用', type: 'success' },
- DISABLED: { label: '已停用', type: 'info' }
- };
- /** 状态展示:未识别取值原样显示,避免误标 */
- export function schemeStatusMeta(status) {
- return (
- SOCIAL_SCHEME_STATUS_META[status] || {
- label: status || '—',
- type: 'info'
- }
- );
- }
- /** 险种标签 */
- export function socialItemTypeLabel(itemType) {
- const hit = SOCIAL_ITEM_TYPE_OPTIONS.find((i) => i.value === itemType);
- return hit ? hit.label : itemType || '—';
- }
- /** 从返回模型转成前端行(字段名与后端保持一致,只补展示用字段) */
- function adaptScheme(d) {
- return {
- ...d,
- /* 参保地:后端模型没有 location 字段,区级名称统一用 region 字段。
- 把 province / city / region 拼成名称数组,供列表展示与编辑弹框的省市区级联回填;
- 相邻重名(如市与区同名)去掉 */
- location: (() => {
- if (Array.isArray(d.location) && d.location.length) {
- return d.location.filter(Boolean);
- }
- const parts = [d.province, d.city, d.region].filter(Boolean);
- return parts.filter((part, idx) => part !== parts[idx - 1]);
- })(),
- items: Array.isArray(d.items) ? d.items.map((item) => ({ ...item })) : [],
- statusLabel: schemeStatusMeta(d.status).label,
- statusType: schemeStatusMeta(d.status).type
- };
- }
- /** 单行险种:默认值,新增时用 */
- export function createEmptySchemeItem(sortNo = 0) {
- return {
- id: null,
- itemType: 'PENSION',
- personalRate: undefined,
- companyRate: undefined,
- personalFixedAmount: undefined,
- companyFixedAmount: undefined,
- baseRate: undefined,
- floatingCoefficient: undefined,
- industryRiskLevel: '',
- applicableFactoryId: null,
- applicablePositionId: null,
- sortNo
- };
- }
- /** 空方案表单:新增时用,险种默认列出「其他」以外的全部常用险种(比例为空待填) */
- export function createEmptyScheme() {
- return {
- id: null,
- schemeCode: '',
- schemeName: '',
- legalEntityId: '',
- legalEntityName: '',
- addressId: null,
- addressName: '',
- /* 参保地:接口字段 location 是地点名称数组(省 / 市 / 区),对应表单里的省市区级联 */
- location: [],
- schemeYear: new Date().getFullYear(),
- effectiveDate: '',
- expiryDate: '',
- socialBaseSource: 'LAST_YEAR_AVG',
- socialBaseMin: undefined,
- socialBaseMax: undefined,
- fundBaseSource: 'LAST_YEAR_AVG',
- fundBaseMin: undefined,
- fundBaseMax: undefined,
- roundingMode: 'HALF_UP',
- roundingScale: 2,
- allowSupplement: 0,
- supplementMonthLimit: undefined,
- policyId: null,
- policyVersion: '',
- remark: '',
- /* 除「其他」外的险种默认各预置一行,比例 / 固定金额留空待 HR 按当地政策填写 */
- items: SOCIAL_ITEM_TYPE_OPTIONS
- .filter((opt) => opt.value !== 'OTHER')
- .map((opt, index) => ({ ...createEmptySchemeItem(index), itemType: opt.value }))
- };
- }
- /**
- * 组装保存参数:只提交后端定义过的字段,避免未知字段被拒绝。
- * items 里同样只保留文档列出的键。
- */
- function toSchemeBody(data = {}) {
- const num = (v) => (v === '' || v == null || isNaN(Number(v)) ? undefined : Number(v));
- /* 参保地:按原提交方式——后端通过 location(省 / 市 / 区名称数组)、addressId、addressName 接收,
- 不单独拆 province / city / region;location 空数组视为未填。
- 注:前端回显仍统一读 region(见 adaptScheme),此处只管提交结构不变 */
- const locationArr = Array.isArray(data.location) && data.location.length
- ? data.location.map((item) => String(item))
- : undefined;
- return {
- id: data.id || undefined,
- schemeCode: String(data.schemeCode || '').trim(),
- schemeName: String(data.schemeName || '').trim(),
- legalEntityId: num(data.legalEntityId),
- legalEntityName: data.legalEntityName || undefined,
- addressId: num(data.addressId),
- addressName: data.addressName || undefined,
- location: locationArr,
- schemeYear: num(data.schemeYear),
- effectiveDate: data.effectiveDate || undefined,
- expiryDate: data.expiryDate || undefined,
- socialBaseSource: data.socialBaseSource || undefined,
- socialBaseMin: num(data.socialBaseMin),
- socialBaseMax: num(data.socialBaseMax),
- fundBaseSource: data.fundBaseSource || undefined,
- fundBaseMin: num(data.fundBaseMin),
- fundBaseMax: num(data.fundBaseMax),
- roundingMode: data.roundingMode || undefined,
- roundingScale: num(data.roundingScale),
- allowSupplement: num(data.allowSupplement),
- supplementMonthLimit: num(data.supplementMonthLimit),
- policyId: num(data.policyId),
- policyVersion: data.policyVersion || undefined,
- remark: String(data.remark || '').trim() || undefined,
- items: (data.items || []).map((item, index) => ({
- id: item.id || undefined,
- itemType: item.itemType,
- personalRate: num(item.personalRate),
- companyRate: num(item.companyRate),
- personalFixedAmount: num(item.personalFixedAmount),
- companyFixedAmount: num(item.companyFixedAmount),
- baseRate: num(item.baseRate),
- floatingCoefficient: num(item.floatingCoefficient),
- industryRiskLevel: item.industryRiskLevel || undefined,
- applicableFactoryId: num(item.applicableFactoryId),
- applicablePositionId: num(item.applicablePositionId),
- sortNo: item.sortNo != null ? Number(item.sortNo) : index
- }))
- };
- }
- /** 分页响应统一成 { list, count, total }(ele-pro-table datasource 约定) */
- function toPage(data) {
- const list = (data?.list || data?.records || []).map(adaptScheme);
- const count = Number(data?.count ?? data?.total ?? list.length);
- return { list, count, total: count };
- }
- /**
- * 五险一金方案 - 分页查询
- * 后端:POST /hr/social/config/scheme/page
- * 支持条件:legalEntityId(法人)、addressId(参保地区)、status、pageNum、size。
- * 注意:后端**没有方案名称查询参数**,名称筛选在本函数内对已返回结果做本地过滤
- * (因此带名称条件时会按法人拉取全量再过滤,避免只搜当前页造成漏项)。
- */
- export async function getSocialSchemePageList(params = {}) {
- const pageNum = Number(params.pageNum) || 1;
- const size = Number(params.size) || 20;
- const schemeName = String(params.schemeName || '').trim();
- const body = { pageNum, size };
- if (params.legalEntityId != null && params.legalEntityId !== '') {
- body.legalEntityId = params.legalEntityId;
- }
- if (params.addressId != null && params.addressId !== '') {
- body.addressId = params.addressId;
- }
- if (params.status) {
- body.status = params.status;
- }
- if (schemeName) {
- body.pageNum = 1;
- body.size = 200; // 最大 200
- }
- const res = await request.post('/hr/social/config/scheme/page', body);
- const page = toPage(await unwrap(res));
- if (!schemeName) return page;
- const keyword = schemeName.toLowerCase();
- const list = page.list.filter(
- (i) =>
- String(i.schemeName || '').toLowerCase().includes(keyword) ||
- String(i.schemeCode || '').toLowerCase().includes(keyword)
- );
- const start = (pageNum - 1) * size;
- return {
- list: list.slice(start, start + size),
- count: list.length,
- total: list.length
- };
- }
- /**
- * 五险一金方案 - 查询全部(用于下拉引用)
- * 薪资模板只存 socialPlanId,故下拉需要拿到可引用的方案;默认取启用态、最多 200 条。
- */
- export async function getSocialSchemeOptions(legalEntityId, onlyEnabled = true) {
- const res = await request.post('/hr/social/config/scheme/page', {
- legalEntityId: legalEntityId != null && legalEntityId !== '' ? legalEntityId : undefined,
- status: onlyEnabled ? 'ENABLED' : undefined,
- pageNum: 1,
- size: 200
- });
- const page = toPage(await unwrap(res));
- return onlyEnabled ? page.list.filter((i) => i.status === 'ENABLED') : page.list;
- }
- /**
- * 五险一金方案 - 详情
- * 后端:GET /hr/social/config/scheme/{id}
- */
- export async function getSocialSchemeDetail(id) {
- const res = await request.get(`/hr/social/config/scheme/${id}`);
- const data = await unwrap(res);
- return adaptScheme(data || {});
- }
- /**
- * 五险一金方案 - 保存(新增 / 修改)
- * 后端:POST /hr/social/config/scheme
- * 带 id 即修改;后端要求 items 至少包含一个险种。
- * @returns {Promise<number|string>} 方案主键
- */
- export async function saveSocialScheme(data = {}) {
- const res = await request.post('/hr/social/config/scheme', toSchemeBody(data));
- return unwrap(res);
- }
- /**
- * 五险一金方案 - 启用
- * 后端:PUT /hr/social/config/scheme/{id}/enable
- * 启用后方案可用于新参保登记;已有关系的历史比例仍以月度台账快照为准。
- */
- export async function enableSocialScheme(id) {
- const res = await request.put(`/hr/social/config/scheme/${id}/enable`, null);
- return unwrap(res);
- }
- /**
- * 五险一金方案 - 停用
- * 后端:PUT /hr/social/config/scheme/{id}/disable
- * 停用仅阻止新参保登记,不删除方案、关系或任何月度缴费历史。
- */
- export async function disableSocialScheme(id) {
- const res = await request.put(`/hr/social/config/scheme/${id}/disable`, null);
- return unwrap(res);
- }
- /**
- * 按给定基数试算方案的月缴额(个人 / 企业),用于列表展示。
- * 规则:缴费基数先按方案上下限截断;按比例的取 基数 × 比例,
- * 有固定金额的按固定金额累加(两者都有时相加,与表单「填比例或填固定金额」的口径一致)。
- * @param {object} scheme 方案返回模型
- * @param {number} base 试算基数(元)
- */
- export function calcSchemeMonthlyAmount(scheme, base = 8000) {
- const items = Array.isArray(scheme?.items) ? scheme.items : [];
- let socialBase = Number(base) || 0;
- let fundBase = Number(base) || 0;
- const min = Number(scheme?.socialBaseMin);
- const max = Number(scheme?.socialBaseMax);
- if (Number.isFinite(min) && min > 0) socialBase = Math.max(socialBase, min);
- if (Number.isFinite(max) && max > 0) socialBase = Math.min(socialBase, max);
- const fMin = Number(scheme?.fundBaseMin);
- const fMax = Number(scheme?.fundBaseMax);
- if (Number.isFinite(fMin) && fMin > 0) fundBase = Math.max(fundBase, fMin);
- if (Number.isFinite(fMax) && fMax > 0) fundBase = Math.min(fundBase, fMax);
- let personal = 0;
- let company = 0;
- items.forEach((item) => {
- const itemBase = item.itemType === FUND_ITEM_TYPE ? fundBase : socialBase;
- const pRate = Number(item.personalRate);
- const cRate = Number(item.companyRate);
- const pFixed = Number(item.personalFixedAmount);
- const cFixed = Number(item.companyFixedAmount);
- personal += (Number.isFinite(pRate) ? (itemBase * pRate) / 100 : 0) +
- (Number.isFinite(pFixed) ? pFixed : 0);
- company += (Number.isFinite(cRate) ? (itemBase * cRate) / 100 : 0) +
- (Number.isFinite(cFixed) ? cFixed : 0);
- });
- return {
- socialBase,
- fundBase,
- personal: Math.round(personal * 100) / 100,
- company: Math.round(company * 100) / 100
- };
- }
|