| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980 |
- import request from '@/utils/request';
- function unwrap(response) {
- const result = response?.data || {};
- if (Number(result.code) === 0) return result.data;
- const error = new Error(result.message || '服务调用失败');
- if (response?.__toastShown) error.__toastShown = true;
- return Promise.reject(error);
- }
- function compact(object = {}) {
- return Object.keys(object).reduce((result, key) => {
- const value = object[key];
- if (value !== '' && value !== null && value !== undefined) result[key] = value;
- return result;
- }, {});
- }
- /** 大整数主键保持字符串,避免 Number 精度丢失(雪花 ID) */
- function keepBigIntId(value) {
- if (value === undefined || value === null || value === '') return undefined;
- const text = String(value).trim();
- if (!/^-?\d+$/.test(text)) return value;
- if (!Number.isSafeInteger(Number(text))) return text;
- return Number(text);
- }
- function keepBigIntIdList(list = []) {
- return (Array.isArray(list) ? list : [])
- .map((item) => keepBigIntId(item))
- .filter((item) => item !== undefined && item !== null && item !== '');
- }
- function flag(value) {
- return value ? 1 : 0;
- }
- function on(value) {
- return Number(value) === 1;
- }
- function toHms(value, fallback = '00:00:00') {
- const text = String(value || '').trim();
- if (!text) return fallback;
- if (text.startsWith('24:00')) return '23:59:59';
- if (/^\d{2}:\d{2}:\d{2}$/.test(text)) return text;
- if (/^\d{2}:\d{2}$/.test(text)) return `${text}:00`;
- return fallback;
- }
- function toHm(value) {
- const text = String(value || '');
- return text.length >= 5 ? text.slice(0, 5) : text;
- }
- function today() {
- const now = new Date();
- const month = String(now.getMonth() + 1).padStart(2, '0');
- const day = String(now.getDate()).padStart(2, '0');
- return `${now.getFullYear()}-${month}-${day}`;
- }
- function genCode(prefix) {
- return `${prefix}${Date.now().toString().slice(-10)}`;
- }
- const RULE_TYPE_TO_API = {
- fixed: 'FIXED',
- scheduled: 'SCHEDULED',
- flexible: 'FLEXIBLE',
- comprehensive: 'COMPREHENSIVE',
- };
- const RULE_TYPE_FROM_API = {
- FIXED: 'fixed',
- SCHEDULED: 'scheduled',
- FLEXIBLE: 'flexible',
- COMPREHENSIVE: 'comprehensive',
- };
- const PUNCH_METHOD_FROM_API = {
- GPS: 'gps',
- WIFI: 'wifi',
- BEACON: 'beacon',
- ATTENDANCE_MACHINE: 'device',
- FACE: 'face',
- PC: 'ding',
- FIELD: 'outside',
- };
- const WEEK_TO_API = { 周一: 1, 周二: 2, 周三: 3, 周四: 4, 周五: 5, 周六: 6, 周日: 7 };
- const WEEK_FROM_API = { 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六', 7: '周日' };
- const REST_MODE_TO_API = {
- double: 'DOUBLE',
- single: 'SINGLE',
- alternate: 'BIG_SMALL',
- none: 'NONE',
- custom: 'CUSTOM',
- };
- const REST_MODE_FROM_API = {
- DOUBLE: 'double',
- SINGLE: 'single',
- BIG_SMALL: 'alternate',
- NONE: 'none',
- CUSTOM: 'custom',
- };
- const CALENDAR_TYPE_TO_API = { 1: 'WORKDAY', 2: 'HOLIDAY', 3: 'CUSTOM' };
- const CALENDAR_TYPE_FROM_API = { WORKDAY: 1, HOLIDAY: 2, CUSTOM: 3 };
- const DATE_TYPE_TO_API = { 1: 'WORKDAY', 2: 'RESTDAY', 3: 'LEGAL_HOLIDAY' };
- const DATE_TYPE_FROM_API = {
- WORKDAY: 1,
- COMPENSATORY_WORKDAY: 1,
- SPECIAL_WORKDAY: 1,
- RESTDAY: 2,
- COMPENSATORY_RESTDAY: 2,
- LEGAL_HOLIDAY: 3,
- };
- const DEVICE_TYPE_TO_API = {
- 考勤机: 'OTHER',
- 考勤门禁一体机: 'OTHER',
- 人脸识别考勤机: 'FACE',
- 指纹考勤机: 'FINGERPRINT',
- 刷卡考勤机: 'CARD',
- 混合识别考勤机: 'FACE_FINGERPRINT',
- };
- const DEVICE_TYPE_FROM_API = {
- FACE: '人脸识别考勤机',
- FINGERPRINT: '指纹考勤机',
- CARD: '刷卡考勤机',
- FACE_FINGERPRINT: '混合识别考勤机',
- OTHER: '考勤机',
- };
- const OUTSIDE_POLICY = {
- 异常: 'ALLOW_WITH_EXCEPTION',
- 记录为地点异常: 'ALLOW_WITH_EXCEPTION',
- 允许: 'ALLOW',
- 允许打卡: 'ALLOW',
- 不允许: 'REJECT',
- 不允许打卡: 'REJECT',
- };
- const OUTSIDE_POLICY_LABEL = {
- ALLOW_WITH_EXCEPTION: '异常',
- ALLOW: '允许',
- REJECT: '不允许',
- };
- const TIMEZONE_TO_API = {
- '(GMT+08:00)中国标准时间-北京': 'Asia/Shanghai',
- '(GMT+00:00)世界标准时间 UTC': 'UTC',
- '(GMT-05:00)美国东部时间-纽约': 'America/New_York',
- '(GMT+09:00)日本标准时间-东京': 'Asia/Tokyo',
- '(GMT+05:30)印度标准时间-新德里': 'Asia/Kolkata',
- };
- const TIMEZONE_FROM_API = Object.keys(TIMEZONE_TO_API).reduce((map, label) => {
- map[TIMEZONE_TO_API[label]] = label;
- return map;
- }, {});
- const ROUND_MODE_TO_API = {
- 四舍五入: 'ROUND',
- 向上取整: 'CEIL',
- 向下取整: 'FLOOR',
- 不取整: 'ROUND',
- };
- const ROUND_MODE_FROM_API = { ROUND: '四舍五入', CEIL: '向上取整', FLOOR: '向下取整' };
- const MAKEUP_TYPE_TO_API = {
- '缺卡/旷工': 'MISSING,ABSENT',
- '迟到/早退': 'LATE,EARLY',
- 其他异常: 'OTHER',
- 正常: 'NORMAL',
- };
- const MAKEUP_TYPE_FROM_API = {
- MISSING: '缺卡/旷工',
- ABSENT: '缺卡/旷工',
- LATE: '迟到/早退',
- EARLY: '迟到/早退',
- OTHER: '其他异常',
- NORMAL: '正常',
- };
- const LEAVE_TYPE_LABEL = {
- ANNUAL: '年假',
- SICK: '病假',
- PERSONAL: '事假',
- MARRIAGE: '婚假',
- MATERNITY: '产假',
- PATERNITY: '陪产假',
- BEREAVEMENT: '丧假',
- WORK_INJURY: '工伤假',
- INJURY: '工伤假',
- COMPENSATORY: '调休',
- OTHER: '其他',
- };
- const EXCEPTION_LABEL = {
- LATE: '迟到',
- EARLY: '早退',
- ABSENT: '旷工',
- MISSING: '缺卡',
- MISSING_IN: '缺上班卡',
- MISSING_OUT: '缺下班卡',
- LOCATION: '地点异常',
- OTHER: '其他',
- };
- function parseRadius(range) {
- const number = Number(String(range || '').replace(/[^\d.]/g, ''));
- return number || 300;
- }
- function parseLimitDays(label) {
- if (!label || label === '不限制') return undefined;
- if (label === '当月内') return 31;
- const matched = String(label).match(/\d+/);
- return matched ? Number(matched[0]) : undefined;
- }
- function parseCount(label) {
- if (!label || label === '不限') return undefined;
- const matched = String(label).match(/\d+/);
- return matched ? Number(matched[0]) : undefined;
- }
- function parseDeadlineDay(label) {
- if (!label || label === '不设置') return undefined;
- if (label === '当月最后一天') return 31;
- const matched = String(label).match(/\d+/);
- return matched ? Number(matched[0]) : undefined;
- }
- function reminderMinutes(label) {
- if (!label || label === '不提醒') return 0;
- if (String(label).includes('5 分钟')) return 5;
- if (String(label).includes('10 分钟')) return 10;
- return 10;
- }
- function overtimePeriodTimes(period, fallbackStart, fallbackEnd) {
- if (period === '全天' || period === '所有时段' || period === '所有非工作时间') {
- return { start: '00:00:00', end: '23:59:59' };
- }
- return { start: toHms(fallbackStart, '18:00:00'), end: toHms(fallbackEnd, '09:00:00') };
- }
- function pageResult(data) {
- return {
- list: Array.isArray(data?.list) ? data.list : [],
- count: Number(data?.count ?? 0),
- };
- }
- /** 考勤规则状态 */
- const ATTENDANCE_RULE_STATUS_LABEL = {
- DRAFT: '草稿',
- EXPIRED: '自然到期',
- ACTIVE: '开启/生效',
- DISABLED: '停用',
- };
- const ATTENDANCE_RULE_STATUS_TAG = {
- DRAFT: 'tag-gray',
- EXPIRED: 'tag-amber',
- ACTIVE: 'tag-green',
- DISABLED: 'tag-red',
- };
- export function attendanceRuleStatusLabel(status) {
- return ATTENDANCE_RULE_STATUS_LABEL[status] || status || '-';
- }
- export function attendanceRuleStatusTag(status) {
- return ATTENDANCE_RULE_STATUS_TAG[status] || 'tag-gray';
- }
- /** 开启/生效时可停用,其余状态可启用 */
- export function isAttendanceRuleActive(status) {
- return status === 'ACTIVE';
- }
- /** 考勤规则适用范围类型 */
- export const ATTENDANCE_RULE_SCOPE_TYPE_OPTIONS = [
- { value: 'EMPLOYEE', label: '员工', priority: 1, tone: 'tag-purple' },
- { value: 'POSITION', label: '岗位', priority: 2, tone: 'tag-blue' },
- { value: 'DEPT', label: '部门', priority: 3, tone: 'tag-cyan' },
- { value: 'ORG', label: '机构', priority: 4, tone: 'tag-green' },
- { value: 'FACTORY', label: '工厂', priority: 5, tone: 'tag-amber' },
- ];
- const ATTENDANCE_RULE_SCOPE_TYPE_LABEL = ATTENDANCE_RULE_SCOPE_TYPE_OPTIONS.reduce(
- (map, item) => {
- map[item.value] = item.label;
- return map;
- },
- {},
- );
- export function attendanceRuleScopeTypeLabel(type) {
- return ATTENDANCE_RULE_SCOPE_TYPE_LABEL[type] || type || '-';
- }
- export function attendanceRuleScopeTypeTone(type) {
- const found = ATTENDANCE_RULE_SCOPE_TYPE_OPTIONS.find((item) => item.value === type);
- return found?.tone || 'tag-gray';
- }
- export function defaultAttendanceRuleScopePriority(type) {
- const found = ATTENDANCE_RULE_SCOPE_TYPE_OPTIONS.find((item) => item.value === type);
- return found?.priority || 1;
- }
- export function normalizeAttendanceRuleScopes(scopes = []) {
- return (Array.isArray(scopes) ? scopes : [])
- .filter((item) => item && item.scopeType && item.scopeId != null && item.scopeId !== '')
- .map((item, index) => ({
- scopeType: item.scopeType,
- scopeId: keepBigIntId(item.scopeId),
- priority:
- Number(item.priority) > 0
- ? Number(item.priority)
- : defaultAttendanceRuleScopePriority(item.scopeType) || index + 1,
- scopeName: item.scopeName || '',
- }));
- }
- /** 提交给后端的 scopes:scopeType + scopeId(可带 priority,服务端会重算) */
- function toAttendanceRuleScopesPayload(scopes = []) {
- return normalizeAttendanceRuleScopes(scopes).map((item) =>
- compact({
- scopeType: item.scopeType,
- scopeId: item.scopeId,
- priority: item.priority,
- }),
- );
- }
- /**
- * 分页查询考勤规则
- * GET /hr/attendance/rules/pageRules
- * query: dateEnd, dateStart, deptId, keyword, pageNum, periodId, punchMethod, size, status, userId
- */
- export const getAttendanceRulePage = async (params = {}) => {
- const query = compact({
- dateEnd: params.dateEnd || undefined,
- dateStart: params.dateStart || undefined,
- deptId: keepBigIntId(params.deptId),
- keyword: params.keyword || undefined,
- pageNum: params.pageNum != null ? Number(params.pageNum) : 1,
- periodId: keepBigIntId(params.periodId),
- punchMethod: params.punchMethod || undefined,
- size: params.size != null ? Number(params.size) : 20,
- status: params.status || undefined,
- userId: keepBigIntId(params.userId),
- });
- const data = await unwrap(
- await request.get('/hr/attendance/rules/pageRules', { params: query }),
- );
- return pageResult(data);
- };
- /** 列表筛选用的打卡方式(与 pageRules.punchMethod 枚举一致) */
- export const ATTENDANCE_RULE_PUNCH_METHOD_FILTER_OPTIONS = [
- { value: 'GPS', label: 'GPS' },
- { value: 'WIFI', label: 'WiFi' },
- { value: 'BEACON', label: '蓝牙' },
- { value: 'ATTENDANCE_MACHINE', label: '考勤机' },
- { value: 'FACE', label: '人脸' },
- { value: 'PC', label: 'PC' },
- { value: 'FIELD', label: '外勤' },
- ];
- /**
- * 查询考勤规则
- * GET /hr/attendance/rules/getRule/{id}
- * 路径参数:id(规则主键)
- */
- export const getAttendanceRuleById = async (id) => {
- const ruleId = keepBigIntId(id);
- if (ruleId == null || ruleId === '') {
- return Promise.reject(new Error('规则主键不能为空'));
- }
- return unwrap(
- await request.get(
- `/hr/attendance/rules/getRule/${encodeURIComponent(String(ruleId))}`,
- ),
- );
- };
- /** 新增考勤规则 POST /hr/attendance/rules/createRule */
- export const createAttendanceRule = async (data) =>
- unwrap(await request.post('/hr/attendance/rules/createRule', data));
- /** @deprecated 使用 createAttendanceRule */
- /**
- * 修改考勤规则
- * PUT /hr/attendance/rules/updateRule/{id}
- * body 与 createRule 同为「考勤规则保存参数」,修改时 id 必填
- */
- export const updateAttendanceRule = async (id, data = {}) => {
- const ruleId = keepBigIntId(id);
- if (ruleId == null || ruleId === '') {
- return Promise.reject(new Error('规则主键不能为空'));
- }
- const body = {
- ...data,
- id: keepBigIntId(data.id) ?? ruleId,
- };
- return unwrap(
- await request.put(
- `/hr/attendance/rules/updateRule/${encodeURIComponent(String(ruleId))}`,
- body,
- ),
- );
- };
- /**
- * 删除考勤规则草稿
- * DELETE /hr/attendance/rules/deleteRuleDraft/{id}
- * 路径参数:id(规则主键)
- */
- export const deleteAttendanceRule = async (id) => {
- const ruleId = keepBigIntId(id);
- if (ruleId == null || ruleId === '') {
- return Promise.reject(new Error('规则主键不能为空'));
- }
- return unwrap(
- await request.delete(
- `/hr/attendance/rules/deleteRuleDraft/${encodeURIComponent(String(ruleId))}`,
- ),
- );
- };
- export const copyAttendanceRule = async (id) =>
- unwrap(
- await request.post(`/hr/attendance/rules/copyRule/${encodeURIComponent(String(id))}`),
- );
- /**
- * 停用考勤规则
- * POST /hr/attendance/rules/disableRule/{id}
- * 路径参数:id(规则主键)
- */
- export const disableAttendanceRule = async (id) => {
- const ruleId = keepBigIntId(id);
- if (ruleId == null || ruleId === '') {
- return Promise.reject(new Error('规则主键不能为空'));
- }
- return unwrap(
- await request.post(
- `/hr/attendance/rules/disableRule/${encodeURIComponent(String(ruleId))}`,
- ),
- );
- };
- /**
- * 提交/启用考勤规则
- * POST /hr/attendance/rules/submitRule/{id}
- * 路径参数:id(规则主键)
- */
- export const submitAttendanceRule = async (id) => {
- const ruleId = keepBigIntId(id);
- if (ruleId == null || ruleId === '') {
- return Promise.reject(new Error('规则主键不能为空'));
- }
- return unwrap(
- await request.post(
- `/hr/attendance/rules/submitRule/${encodeURIComponent(String(ruleId))}`,
- ),
- );
- };
- export const getAttendanceCalendarById = async (id) =>
- unwrap(
- await request.get(
- `/hr/attendance/calendars/getCalendar/${encodeURIComponent(String(id))}`,
- ),
- );
- export const saveAttendanceCalendar = async (data) =>
- unwrap(await request.post('/hr/attendance/calendars/saveCalendar', data));
- export const getAttendanceShiftById = async (id) =>
- unwrap(
- await request.get(`/hr/attendance/shifts/getShift/${encodeURIComponent(String(id))}`),
- );
- export const saveAttendanceShift = async (data) =>
- unwrap(await request.post('/hr/attendance/shifts/saveShift', data));
- export const updateAttendanceShift = async (id, data) =>
- unwrap(
- await request.put(
- `/hr/attendance/shifts/updateShift/${encodeURIComponent(String(id))}`,
- data,
- ),
- );
- export const getAttendanceDevicePage = async (params = {}) =>
- pageResult(await unwrap(await request.get('/hr/attendance/devices/pageDevices', { params: compact(params) })));
- export const saveAttendanceDevice = async (data) =>
- unwrap(await request.post('/hr/attendance/devices/saveDevice', data));
- export const updateAttendanceDevice = async (id, data) =>
- unwrap(await request.put(`/hr/attendance/devices/updateDevice/${id}`, data));
- export const getLeavePolicyPage = async (params = {}) =>
- pageResult(await unwrap(await request.get('/hr/attendance/leave-policies/pageLeavePolicies', { params: compact(params) })));
- /** 假期制度详情(编辑抽屉回显用;返回结构化字段 + 后端算好的适用范围/薪资口径文案) */
- export const getLeavePolicyDetail = async (id) => {
- if (!id) throw new Error('假期制度主键不能为空');
- return adaptLeavePolicy(unwrap(await request.get(`/hr/attendance/leave-policies/getLeavePolicy/${id}`)) || {});
- };
- /** 假期制度统计(列表顶部胶囊:制度数 / 扣薪数 / 额度待核定数 / 动态额度数) */
- export const getLeavePolicySummary = async (params = {}) =>
- unwrap(await request.get('/hr/attendance/leave-policies/summary', { params: compact(params) })) || {};
- /** 停用影响(本月审批中的请假单数量,用于停用二次确认) */
- export const getLeavePolicyDisableImpact = async (id) => {
- if (!id) throw new Error('假期制度主键不能为空');
- return unwrap(await request.get(`/hr/attendance/leave-policies/${id}/disable-impact`)) || {};
- };
- export const saveLeavePolicy = async (data) =>
- unwrap(await request.post('/hr/attendance/leave-policies/saveLeavePolicy', data));
- export const getDepartmentAttendanceReport = async (data = {}) =>
- unwrap(await request.post('/hr/attendance/reports/department', compact(data)));
- export const getPersonalAttendanceReport = async (data = {}) =>
- pageResult(await unwrap(await request.post('/hr/attendance/reports/personal', compact(data))));
- export const getAttendanceExceptionReport = async (data = {}) =>
- pageResult(await unwrap(await request.post('/hr/attendance/reports/exceptions', compact(data))));
- export const getDailyAttendancePage = async (data = {}) =>
- pageResult(await unwrap(await request.post('/hr/attendance/daily/page', compact(data))));
- function formatYmd(value) {
- const date = value instanceof Date ? value : new Date(value);
- if (Number.isNaN(date.getTime())) return '';
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${date.getFullYear()}-${month}-${day}`;
- }
- function addDaysYmd(value, days) {
- const date = new Date(`${String(value).slice(0, 10)}T00:00:00`);
- if (Number.isNaN(date.getTime())) return '';
- date.setDate(date.getDate() + Number(days || 0));
- return formatYmd(date);
- }
- function eachYmdInRange(start, end, callback) {
- const begin = String(start || '').slice(0, 10);
- const stop = String(end || '').slice(0, 10);
- if (!begin || !stop || begin > stop) return;
- let cursor = begin;
- while (cursor && cursor <= stop) {
- callback(cursor);
- cursor = addDaysYmd(cursor, 1);
- }
- }
- export const LEGAL_HOLIDAY_NAMES = [
- '元旦',
- '春节',
- '清明节',
- '劳动节',
- '端午节',
- '中秋节',
- '国庆节',
- ];
- export function createLegalHolidayRangeForm(seed = {}) {
- const start = seed.start || (Array.isArray(seed.dateRange) ? seed.dateRange[0] : '') || '';
- const end = seed.end || (Array.isArray(seed.dateRange) ? seed.dateRange[1] : '') || '';
- return {
- id: seed.id || `LH-${seed.name || Date.now()}-${Math.random().toString(16).slice(2)}`,
- name: seed.name || '',
- start,
- end,
- dateRange: start && end ? [start, end] : null,
- preset: seed.preset !== false && LEGAL_HOLIDAY_NAMES.includes(seed.name),
- };
- }
- export function createDefaultLegalHolidayRanges() {
- return LEGAL_HOLIDAY_NAMES.map((name) =>
- createLegalHolidayRangeForm({ name, preset: true, id: `LH-PRESET-${name}` }),
- );
- }
- export function mergeLegalHolidayRangeForm(list = []) {
- const saved = (Array.isArray(list) ? list : []).map((item) =>
- createLegalHolidayRangeForm(item),
- );
- const byName = {};
- saved.forEach((item) => {
- if (item.name) byName[item.name] = item;
- });
- const presets = LEGAL_HOLIDAY_NAMES.map((name) =>
- createLegalHolidayRangeForm({
- ...(byName[name] || {}),
- name,
- preset: true,
- id: (byName[name] && byName[name].id) || `LH-PRESET-${name}`,
- }),
- );
- const extras = saved.filter(
- (item) => item.name && !LEGAL_HOLIDAY_NAMES.includes(item.name),
- );
- return [...presets, ...extras];
- }
- function mergeLegalHolidayDatesToRanges(list = []) {
- const ranges = [];
- [...list]
- .filter((item) => item && item.calendarDate)
- .sort((a, b) => String(a.calendarDate).localeCompare(String(b.calendarDate)))
- .forEach((item) => {
- const calendarDate = String(item.calendarDate).slice(0, 10);
- const name = item.name || item.changeReason || '法定节假日';
- const last = ranges[ranges.length - 1];
- if (last && last.name === name && addDaysYmd(last.end, 1) === calendarDate) {
- last.end = calendarDate;
- last.dateRange = [last.start, last.end];
- return;
- }
- ranges.push({
- id: item.id || `LH${ranges.length}-${calendarDate}`,
- name,
- start: calendarDate,
- end: calendarDate,
- dateRange: [calendarDate, calendarDate],
- });
- });
- return ranges;
- }
- function expandLegalHolidayRanges(ranges = [], occupiedDates = []) {
- const occupied = new Set((occupiedDates || []).map((date) => String(date).slice(0, 10)));
- const dates = [];
- (ranges || []).forEach((range) => {
- const name = String(range.name || '法定节假日').trim() || '法定节假日';
- const start = range.start || (Array.isArray(range.dateRange) ? range.dateRange[0] : '');
- const end = range.end || (Array.isArray(range.dateRange) ? range.dateRange[1] : '');
- eachYmdInRange(start, end, (calendarDate) => {
- if (occupied.has(calendarDate)) return;
- occupied.add(calendarDate);
- dates.push({
- calendarDate,
- dayType: 'LEGAL_HOLIDAY',
- sourceType: 'MANUAL',
- changeReason: name,
- });
- });
- });
- return dates;
- }
- const CALENDAR_RULES_BLOCK = /\[calendarRules\]([\s\S]*?)\[\/calendarRules\]/;
- const BIG_SMALL_REASON_PREFIX = '大小周';
- export function stripCalendarRuleConfig(remark) {
- return String(remark || '')
- .replace(/\s*\[calendarRules\][\s\S]*?\[\/calendarRules\]\s*/g, '')
- .trim();
- }
- function parseCalendarRuleConfig(remark) {
- const match = String(remark || '').match(CALENDAR_RULES_BLOCK);
- if (!match) return {};
- try {
- const parsed = JSON.parse(match[1]);
- return parsed && typeof parsed === 'object' ? parsed : {};
- } catch (error) {
- return {};
- }
- }
- function withCalendarRuleConfig(remark, config) {
- const stripped = stripCalendarRuleConfig(remark);
- if (!config || !Object.keys(config).length) return stripped;
- const block = `[calendarRules]${JSON.stringify(config)}[/calendarRules]`;
- return stripped ? `${stripped}\n${block}` : block;
- }
- function parseYmdDate(value) {
- const text = String(value || '').slice(0, 10);
- const match = text.match(/^(\d{4})-(\d{2})-(\d{2})$/);
- if (!match) return null;
- const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
- return Number.isNaN(date.getTime()) ? null : date;
- }
- function mondayOf(date) {
- const next = new Date(date.getFullYear(), date.getMonth(), date.getDate());
- const day = next.getDay() || 7;
- next.setDate(next.getDate() - (day - 1));
- return next;
- }
- function toYmd(date) {
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${date.getFullYear()}-${month}-${day}`;
- }
- function isBigSmallGeneratedDate(item = {}) {
- return String(item.changeReason || item.remark || '').startsWith(BIG_SMALL_REASON_PREFIX);
- }
- export function resolveBigSmallConfig(form = {}, fallbackYear) {
- const year = Number(
- (Array.isArray(form.applyYear) ? form.applyYear[0] : form.applyYear) ||
- fallbackYear ||
- new Date().getFullYear(),
- );
- return {
- startDate: form.alternateStartDate || `${year}-01-01`,
- startType: form.alternateStartType === 'big' ? 'big' : 'small',
- };
- }
- /** 大小周:周日固定休息;小周周六上班,大周周六休息。周从周一起算。 */
- export function isBigSmallRestDate(value, config = {}) {
- const date = parseYmdDate(value);
- if (!date) return false;
- const weekDay = date.getDay();
- if (weekDay === 0) return true;
- if (weekDay !== 6) return false;
- const start = parseYmdDate(config.startDate) || new Date(date.getFullYear(), 0, 1);
- const weeks = Math.round((mondayOf(date) - mondayOf(start)) / (7 * 24 * 60 * 60 * 1000));
- const startIsSmall = config.startType !== 'big';
- const isSmall = weeks % 2 === 0 ? startIsSmall : !startIsSmall;
- // 小周:周六上班;大周:周六休息
- return !isSmall;
- }
- function expandBigSmallDates(form = {}, occupiedDates = []) {
- const restMode = form.restMode || REST_MODE_FROM_API[form.restPattern] || '';
- if (restMode !== 'alternate') return [];
- const occupied = new Set((occupiedDates || []).map((date) => String(date).slice(0, 10)));
- const years = Array.isArray(form.applyYear) ? form.applyYear : [form.applyYear].filter(Boolean);
- const months = (form.applyMonth || []).map(Number).filter(Boolean);
- const monthList = months.length ? months : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
- const config = resolveBigSmallConfig(form, years[0]);
- const dates = [];
- years.forEach((year) => {
- monthList.forEach((month) => {
- const cursor = new Date(Number(year), month - 1, 1);
- const end = new Date(Number(year), month, 0);
- while (cursor <= end) {
- const weekDay = cursor.getDay();
- if (weekDay === 0 || weekDay === 6) {
- const calendarDate = toYmd(cursor);
- if (!occupied.has(calendarDate)) {
- occupied.add(calendarDate);
- const isRest = isBigSmallRestDate(calendarDate, config);
- dates.push({
- calendarDate,
- dayType: isRest ? 'RESTDAY' : 'WORKDAY',
- sourceType: 'MANUAL',
- changeReason:
- weekDay === 0
- ? `${BIG_SMALL_REASON_PREFIX}-周日休息`
- : isRest
- ? `${BIG_SMALL_REASON_PREFIX}-大周周六休息`
- : `${BIG_SMALL_REASON_PREFIX}-小周周六上班`,
- });
- }
- }
- cursor.setDate(cursor.getDate() + 1);
- }
- });
- });
- return dates;
- }
- function inferBigSmallConfig(raw = {}) {
- const fromRemark = parseCalendarRuleConfig(raw.remark);
- if (fromRemark.alternateStartDate) {
- return {
- alternateStartDate: String(fromRemark.alternateStartDate).slice(0, 10),
- alternateStartType: fromRemark.alternateStartType === 'big' ? 'big' : 'small',
- };
- }
- const saturdays = (raw.dates || [])
- .filter((item) => isBigSmallGeneratedDate(item))
- .map((item) => ({
- calendarDate: String(item.calendarDate || '').slice(0, 10),
- dayType: item.dayType,
- }))
- .filter((item) => {
- const date = parseYmdDate(item.calendarDate);
- return date && date.getDay() === 6;
- })
- .sort((a, b) => a.calendarDate.localeCompare(b.calendarDate));
- const year = raw.applicableYear || new Date().getFullYear();
- if (!saturdays[0]) {
- return { alternateStartDate: `${year}-01-01`, alternateStartType: 'small' };
- }
- const firstDate = parseYmdDate(saturdays[0].calendarDate);
- return {
- alternateStartDate: toYmd(mondayOf(firstDate)),
- alternateStartType:
- saturdays[0].dayType === 'RESTDAY' || saturdays[0].dayType === 'COMPENSATORY_RESTDAY'
- ? 'big'
- : 'small',
- };
- }
- function serializeRestWeekdays(form = {}) {
- const mode = form.restMode || REST_MODE_FROM_API[form.restPattern] || 'double';
- if (mode === 'single' || mode === 'alternate') return '7';
- if (mode === 'none') return '';
- if (mode === 'custom') {
- return (form.restWeekdays || [])
- .map((day) => (Number(day) === 0 ? 7 : Number(day)))
- .filter((day) => day >= 1 && day <= 7)
- .join(',');
- }
- return '6,7';
- }
- function workDaysForShiftPlans(form = {}) {
- const days = form.workDays?.length
- ? [...form.workDays]
- : ['周一', '周二', '周三', '周四', '周五'];
- if (form.restMode !== 'alternate') return days;
- if (!days.includes('周六')) days.push('周六');
- return days.filter((day) => day !== '周日');
- }
- function normalizeLegalHolidayRangesForSave(ranges = []) {
- return (Array.isArray(ranges) ? ranges : []).map((item) => {
- const start =
- String(item?.start || (Array.isArray(item?.dateRange) ? item.dateRange[0] : '') || '').slice(
- 0,
- 10,
- );
- const end =
- String(item?.end || (Array.isArray(item?.dateRange) ? item.dateRange[1] : '') || start || '').slice(
- 0,
- 10,
- );
- return {
- ...item,
- name: String(item?.name || '').trim(),
- start,
- end,
- dateRange: start && end ? [start, end] : null,
- };
- });
- }
- export function serializeAttendanceCalendar(form = {}) {
- const years = Array.isArray(form.applyYear) ? form.applyYear : [form.applyYear].filter(Boolean);
- const months = Array.isArray(form.applyMonth) ? form.applyMonth : [];
- const legalHolidayRanges = normalizeLegalHolidayRangesForSave(form.legalHolidayRanges);
- const dates = [];
- const occupied = [];
- (form.holidayRules || []).forEach((rule) => {
- if (!rule.calendarDate) return;
- if (isBigSmallGeneratedDate(rule)) return;
- const calendarDate = String(rule.calendarDate).slice(0, 10);
- occupied.push(calendarDate);
- dates.push({
- calendarDate,
- dayType: DATE_TYPE_TO_API[Number(rule.dateType)] || 'SPECIAL_WORKDAY',
- sourceType: 'MANUAL',
- changeReason: rule.remark || '',
- });
- });
- (form.legalHolidayWorkDates || []).forEach((date) => {
- const calendarDate = String(date).slice(0, 10);
- occupied.push(calendarDate);
- dates.push({
- calendarDate,
- dayType: 'COMPENSATORY_WORKDAY',
- sourceType: 'MANUAL',
- changeReason: '法定节假日调为上班',
- });
- });
- dates.push(...expandLegalHolidayRanges(legalHolidayRanges, occupied));
- occupied.push(...dates.map((item) => item.calendarDate));
- dates.push(...expandBigSmallDates(form, occupied));
- const restPattern = REST_MODE_TO_API[form.restMode] || form.restPattern || 'DOUBLE';
- const bigSmall = restPattern === 'BIG_SMALL' ? resolveBigSmallConfig(form, years[0]) : null;
- return compact({
- id: keepBigIntId(form.calendarId || form.id) || undefined,
- calendarCode: form.calendarCode || genCode('CLD'),
- calendarName: form.calendarName,
- calendarType: CALENDAR_TYPE_TO_API[form.calendarType] || form.calendarType || 'WORKDAY',
- applicableYear: Number(years[0] || new Date().getFullYear()),
- applicableMonths: months.join(','),
- restPattern,
- restWeekdays: serializeRestWeekdays(form),
- status: Number(form.calendarStatus ?? form.status) === 1 ? 'ENABLED' : 'DISABLED',
- remark: withCalendarRuleConfig(
- form.remark,
- bigSmall
- ? {
- alternateStartDate: bigSmall.startDate,
- alternateStartType: bigSmall.startType,
- }
- : {},
- ),
- dates,
- });
- }
- export function adaptAttendanceCalendar(raw = {}) {
- const restMode = REST_MODE_FROM_API[raw.restPattern] || 'double';
- const restWeekdays = String(raw.restWeekdays || '')
- .split(',')
- .map((item) => Number(item))
- .filter((item) => item >= 0)
- .map((item) => (item === 7 ? 0 : item));
- const holidayRules = (raw.dates || [])
- .filter((item) => item.dayType !== 'COMPENSATORY_WORKDAY' && item.dayType !== 'LEGAL_HOLIDAY')
- .filter((item) => !isBigSmallGeneratedDate(item))
- .map((item, index) => ({
- id: item.id || `HR${index}`,
- calendarDate: String(item.calendarDate || '').slice(0, 10),
- dateType: DATE_TYPE_FROM_API[item.dayType] || 1,
- remark: item.changeReason || '',
- }));
- const legalHolidayRanges = mergeLegalHolidayDatesToRanges(
- (raw.dates || [])
- .filter((item) => item.dayType === 'LEGAL_HOLIDAY')
- .map((item) => ({
- id: item.id,
- calendarDate: String(item.calendarDate || '').slice(0, 10),
- name: item.changeReason || '法定节假日',
- })),
- );
- const bigSmall = restMode === 'alternate' ? inferBigSmallConfig(raw) : {};
- return {
- calendarId: raw.id,
- calendarCode: raw.calendarCode || '',
- calendarName: raw.calendarName || '',
- calendarType: CALENDAR_TYPE_FROM_API[raw.calendarType] || 1,
- applyYear: raw.applicableYear ? [Number(raw.applicableYear)] : [new Date().getFullYear()],
- applyMonth: String(raw.applicableMonths || '')
- .split(',')
- .map(Number)
- .filter(Boolean),
- calendarStatus: raw.status === 'ENABLED' ? 1 : 0,
- restMode,
- restWeekdays: restWeekdays.length ? restWeekdays : restMode === 'alternate' ? [0] : [6, 0],
- alternateStartDate: bigSmall.alternateStartDate || '',
- alternateStartType: bigSmall.alternateStartType || 'small',
- legalHolidayWorkDates: (raw.dates || [])
- .filter((item) => item.dayType === 'COMPENSATORY_WORKDAY')
- .map((item) => String(item.calendarDate || '').slice(0, 10)),
- legalHolidayRanges: mergeLegalHolidayRangeForm(legalHolidayRanges),
- holidayRules,
- remark: stripCalendarRuleConfig(raw.remark),
- restDays:
- restMode === 'alternate'
- ? ['周日', '隔周周六']
- : restWeekdays.map((day) => WEEK_FROM_API[day === 0 ? 7 : day]).filter(Boolean),
- };
- }
- export function serializeAttendanceShift(form = {}, index = 0) {
- const shift = (form.workShifts || [])[index] || {};
- const start = toHms(shift.start || form.workStart, '09:00:00');
- const end = toHms(shift.end || form.workEnd, '18:00:00');
- const lunch = shift.lunchBreaks?.[0] || {};
- const startMinutes = Number(start.slice(0, 2)) * 60 + Number(start.slice(3, 5));
- const endMinutes = Number(end.slice(0, 2)) * 60 + Number(end.slice(3, 5));
- const crossDay = endMinutes <= startMinutes ? 1 : 0;
- const lunchMinutes =
- lunch.start && lunch.end
- ? Math.max(
- 0,
- Number(toHms(lunch.end).slice(0, 2)) * 60 +
- Number(toHms(lunch.end).slice(3, 5)) -
- (Number(toHms(lunch.start).slice(0, 2)) * 60 + Number(toHms(lunch.start).slice(3, 5))),
- )
- : 0;
- const requiredMinutes = Math.max(1, (crossDay ? endMinutes + 24 * 60 : endMinutes) - startMinutes - lunchMinutes);
- const punchMethodMode =
- form.method === 'device' ? 'DEVICE' : form.method === 'phone-device' ? 'MIXED' : 'MOBILE';
- return compact({
- id: keepBigIntId(shift.shiftId) || undefined,
- shiftCode: shift.shiftCode || form.shiftCode || genCode(`SH${index + 1}`),
- shiftName: shift.name || `${form.name || '考勤'}班次${index + 1}`,
- shiftType: crossDay ? 'CROSS_DAY' : form.type === 'scheduled' ? 'FIXED' : RULE_TYPE_TO_API[form.type] || 'FIXED',
- startTime: start,
- endTime: end,
- crossDay,
- requiredMinutes,
- punchMethodMode,
- punchWindowStartTime: toHms(form.punchStart, '05:00:00'),
- punchWindowEndTime: toHms(form.punchEnd, '24:00:00'),
- punchWindowEndDayOffset: form.punchEnd === '24:00' || form.crossDay ? 1 : 0,
- flexibleEnabled: flag(form.flexEnabled),
- halfDayStartTime: toHms(form.halfStart, '08:00:00'),
- halfDayEndTime: toHms(form.halfEnd, '12:00:00'),
- nextDayLateArrivalEnabled: flag(Boolean(form.flexEnabled && form.lateComp)),
- breaks:
- lunch.start && lunch.end
- ? [
- {
- breakType: 'LUNCH',
- countAsWork: 0,
- startTime: toHms(lunch.start),
- endTime: toHms(lunch.end),
- },
- ]
- : [],
- segments: [
- {
- segmentNo: 1,
- startTime: start,
- endTime: end,
- needInPunch: shift.startOpen === '否' ? 0 : 1,
- needOutPunch: shift.endOpen === '否' ? 0 : 1,
- crossDayOffset: crossDay,
- },
- ],
- status: 'ENABLED',
- });
- }
- export function serializeAttendanceDevice(device = {}) {
- return compact({
- id: keepBigIntId(device.id) || undefined,
- deviceCode: device.deviceCode || device.sn || genCode('DEV'),
- deviceName: device.name || device.deviceName,
- deviceType: DEVICE_TYPE_TO_API[device.deviceType] || device.deviceType || 'OTHER',
- serialNo: device.sn || device.serialNo || '',
- ip: device.ip || '',
- location: device.location || '',
- manufacturer: device.manufacturer || '',
- model: device.model || '',
- status: device.online === false ? 'DISABLED' : 'ENABLED',
- });
- }
- export function adaptAttendanceDevice(raw = {}) {
- return {
- id: raw.id,
- name: raw.deviceName || '',
- deviceName: raw.deviceName || '',
- deviceCode: raw.deviceCode || '',
- sn: raw.serialNo || raw.deviceCode || '',
- serialNo: raw.serialNo || '',
- ip: raw.ip || '',
- port: raw.port || 4370,
- deviceType: DEVICE_TYPE_FROM_API[raw.deviceType] || raw.deviceType || '考勤机',
- location: raw.location || '',
- manufacturer: raw.manufacturer || '',
- model: raw.model || '',
- online: raw.connectionStatus === 'ONLINE' || raw.status === 'ENABLED',
- lastSync: raw.lastSyncTime ? String(raw.lastSyncTime).replace('T', ' ').slice(0, 19) : '',
- department: raw.departmentName || '',
- onlineDuration: '-',
- remark: raw.remark || '',
- };
- }
- function buildPunchMethods(form) {
- const methods = [];
- const combineMode = form.method === 'phone-device' ? 'ANY' : 'ALL';
- const push = (punchMethod) => {
- if (!methods.some((item) => item.punchMethod === punchMethod)) {
- methods.push({ punchMethod, combineMode, requiredFlag: 1 });
- }
- };
- if (form.method === 'phone' || form.method === 'phone-device') {
- if ((form.locations || []).length) push('GPS');
- if ((form.wifis || []).length) push('WIFI');
- if (!methods.length) push('GPS');
- }
- if (form.method === 'device' || form.method === 'phone-device') {
- push('ATTENDANCE_MACHINE');
- /* 人脸打卡未上线(助理管理里的人脸配置已隐藏),这里不再因 faceRecognition 追加 FACE;
- 恢复人脸功能时改回:if (form.faceRecognition === '启用' || form.faceEachPunch) push('FACE'); */
- }
- if (form.outsideEnabled) push('FIELD');
- if (form.relatedTarget === 'ding') push('PC');
- return methods;
- }
- function buildOvertimeRules(form) {
- const roundMode = ROUND_MODE_TO_API[form.overtimeRound] || 'ROUND';
- const minimumMinutes = Number(form.overtimeStart || 30);
- const makeRule = (dayType, enabled, period, calcMethod, deductRest, compOrPay) => {
- const times = overtimePeriodTimes(period, form.workEnd, form.workStart);
- return compact({
- dayType,
- enabledFlag: flag(enabled),
- allowedStartTime: times.start,
- allowedEndTime: times.end,
- approvalRequiredFlag: flag(calcMethod === '按审批时长计算'),
- deductBreakFlag: flag(deductRest),
- compensationMode: compOrPay ? 'PAY' : 'COMP_LEAVE',
- compLeaveRatio: compOrPay ? 0 : 1,
- minimumMinutes,
- roundMode,
- roundUnitMinutes: form.overtimeRound === '不取整' ? 1 : 30,
- });
- };
- return [
- makeRule('WORKDAY', form.workOvertimeEnabled, form.workOvertimePeriod, form.workCalcMethod, form.workDeductRest, form.workCompOrPay),
- makeRule('RESTDAY', form.restOvertimeEnabled, form.restOvertimePeriod, form.restCalcMethod, form.restDeductRest, form.restCompOrPay),
- makeRule('LEGAL_HOLIDAY', form.holidayOvertimeEnabled, form.holidayOvertimePeriod, form.holidayCalcMethod, form.holidayDeductRest, form.holidayCompOrPay),
- ];
- }
- export function emptyAttendanceSettlement() {
- return {
- autoCloseEnabled: false,
- closeDay: 1,
- closeTime: '00:00:00',
- cycleType: 'NATURAL_MONTH',
- cycleStartDay: 1,
- cycleEndDay: 31,
- };
- }
- function adaptAttendanceSettlement(raw = {}) {
- const source = raw && typeof raw === 'object' ? raw : {};
- const cycleType = source.cycleType === 'CUSTOM_DAY' ? 'CUSTOM_DAY' : 'NATURAL_MONTH';
- const startDay = Number(source.cycleStartDay);
- const endDay = Number(source.cycleEndDay);
- const closeDay = Number(source.closeDay);
- return {
- autoCloseEnabled: on(source.autoCloseEnabled) || source.autoCloseEnabled === true,
- closeDay: closeDay >= 1 && closeDay <= 31 ? closeDay : 1,
- closeTime: toHms(source.closeTime, '00:00:00'),
- cycleType,
- cycleStartDay: startDay >= 1 && startDay <= 31 ? startDay : 1,
- cycleEndDay: endDay >= 1 && endDay <= 31 ? endDay : 31,
- };
- }
- function buildSettlement(form = {}) {
- const raw = adaptAttendanceSettlement(form.settlement || form.raw?.settlement || {});
- const payload = {
- cycleType: raw.cycleType || 'NATURAL_MONTH',
- autoCloseEnabled: flag(raw.autoCloseEnabled),
- };
- if (payload.cycleType === 'CUSTOM_DAY') {
- payload.cycleStartDay = Number(raw.cycleStartDay || 1);
- payload.cycleEndDay = Number(raw.cycleEndDay || 31);
- }
- if (raw.autoCloseEnabled) {
- payload.closeDay = Number(raw.closeDay || 1);
- payload.closeTime = toHms(raw.closeTime, '00:00:00');
- }
- return compact(payload);
- }
- /**
- * 序列化考勤规则保存参数(createRule / updateRule 共用)
- * 对应文档「考勤规则保存参数」
- */
- export function serializeAttendanceRule(form = {}, extras = {}) {
- const locationOutsidePolicy =
- OUTSIDE_POLICY[form.outOfRange] ||
- OUTSIDE_POLICY[(form.locations || [])[0]?.outsidePolicy] ||
- 'ALLOW_WITH_EXCEPTION';
- const locations = (form.locations || [])
- .filter((item) => item.name || item.locationName)
- .map((item) => ({
- locationName: item.name || item.locationName || '',
- latitude: Number(item.latitude ?? 0),
- longitude: Number(item.longitude ?? 0),
- radius: parseRadius(item.range || item.radius),
- outsidePolicy:
- OUTSIDE_POLICY[item.outsidePolicy] ||
- item.outsidePolicy ||
- locationOutsidePolicy,
- }));
- const wifiList = (form.wifis || [])
- .filter((item) => item.bssid)
- .map((item) => ({
- ssid: item.ssid || '',
- bssid: item.bssid || '',
- locationName: item.locationName || item.ssid || '',
- status: item.status || 'ENABLED',
- }));
- const workDays = workDaysForShiftPlans(form);
- const shiftIds = keepBigIntIdList(
- extras.shiftIds || (form.workShifts || []).map((item) => item.shiftId).filter(Boolean),
- );
- const shiftPlans = shiftIds.flatMap((shiftId) =>
- workDays.map((day) => ({
- planType: 'WEEKLY',
- shiftId,
- weekDay: WEEK_TO_API[day],
- needInPunch: 1,
- needOutPunch: 1,
- halfDayFlag: 0,
- cycleDayNo: undefined,
- })).map((item) => compact(item)),
- );
- const scopes = toAttendanceRuleScopesPayload(
- form.scopes?.length
- ? form.scopes
- : form.scopeId
- ? [{ scopeType: form.scopeType || 'ORG', scopeId: form.scopeId }]
- : [],
- );
- const reportTime = toHms(form.reportStart, '09:00:00');
- const calendarId = keepBigIntId(extras.calendarId || form.calendarId);
- const deviceIds = keepBigIntIdList(
- extras.deviceIds || (form.devices || []).map((item) => item.id).filter(Boolean),
- );
- const payload = {
- ruleCode: form.ruleCode || form.code || genCode('AR'),
- ruleName: form.name || '',
- ruleType: RULE_TYPE_TO_API[form.type] || 'FIXED',
- effectiveStartDate: form.effectiveStartDate || today(),
- effectiveEndDate: form.effectiveEndDate || undefined,
- timezone: TIMEZONE_TO_API[form.timezone] || form.timezone || 'Asia/Shanghai',
- calendarId: calendarId || undefined,
- absentThresholdMinutes:
- form.absentThresholdMinutes != null
- ? Number(form.absentThresholdMinutes)
- : form.raw?.absentThresholdMinutes != null
- ? Number(form.raw.absentThresholdMinutes)
- : undefined,
- lateGraceMinutes: Number(form.lateGraceMinutes || 0) || undefined,
- earlyGraceMinutes: Number(form.earlyGraceMinutes || 0) || undefined,
- punchMethods: buildPunchMethods(form),
- locations,
- wifiList,
- deviceIds,
- overtimeRules: buildOvertimeRules(form),
- overtimeRequireApproval: flag(
- form.workCalcMethod === '按审批时长计算' ||
- form.restCalcMethod === '按审批时长计算' ||
- form.holidayCalcMethod === '按审批时长计算',
- ),
- compLeavePreferred: flag(!form.workCompOrPay),
- cardRepair: {
- repairEnabled: flag(form.makeupEnabled),
- allowedTypes: MAKEUP_TYPE_TO_API[form.makeupType] || 'MISSING,ABSENT',
- timeLimitDays: parseLimitDays(form.makeupTimeLimit),
- reminderEnabled: flag(form.makeupReminder === '启用'),
- reminderTime: form.makeupReminder === '启用' ? toHm(form.makeupReminderTime) : undefined,
- monthlyLimitCount: parseCount(form.makeupMaxPerMonth),
- monthlyDeadlineDay: parseDeadlineDay(form.makeupDeadline),
- approvalPunchEnabled: flag(form.approvalPunchEnabled),
- },
- assistance: {
- enabledFlag: flag(form.assistantEnabled),
- timezone: TIMEZONE_TO_API[form.timezone] || 'Asia/Shanghai',
- punchReminderMinutes: reminderMinutes(form.reminderOffset),
- outsidePolicy: OUTSIDE_POLICY[form.asstOutOfRange] || 'ALLOW_WITH_EXCEPTION',
- photoRequiredFlag: flag(form.photoEachPunch),
- /* 人脸打卡未上线:助理管理里的人脸配置已隐藏,接口恒不启用;
- 恢复人脸时改回 flag(form.faceEachPunch || form.faceRecognition === '启用') */
- faceRequiredFlag: 0,
- fieldSyncEnabled: flag(form.outsourceSync),
- },
- field: {
- enabledFlag: flag(form.outsideEnabled),
- fieldMode: form.outsideMode === 'location' ? 'LOCATION' : 'TIME',
- punchStartTime: toHms(form.outsideStart, '09:00:00'),
- punchEndTime: toHms(form.outsideEnd, '18:00:00'),
- locationRestrictedFlag: flag(form.outsideAddress && form.outsideAddress !== '不限制'),
- photoRequiredFlag: flag(form.outsidePhoto),
- summaryHour: Number(reportTime.slice(0, 2)),
- summaryMinute: Number(reportTime.slice(3, 5)),
- },
- scopes,
- settlement: buildSettlement(form),
- /* 请假离岗/返岗打卡设置:后端参数 leavePunch(两个字段都必填)
- leavePunchRequired:0否、1是;punchTimeLimitType:见 LEAVE_PUNCH_TIME_OPTIONS 的枚举 */
- leavePunch: {
- leavePunchRequired: flag(form.leaveNeedPunch),
- punchTimeLimitType: form.leavePunchWindow || 'UNLIMITED',
- },
- /* 该规则关联的启用假期制度主键(同一规则内请假类型不能重复,由页面勾选维护) */
- leavePolicyIds: keepBigIntIdList(
- (form.leavePolicyIds || []).map((item) =>
- item && typeof item === 'object' ? item.id ?? item.policyId : item,
- ),
- ),
- exemptUserIds: keepBigIntIdList(
- form.exemptUserIds?.length
- ? form.exemptUserIds
- : (form.assistantExemptUsers || [])
- .map((item) => (item && typeof item === 'object' ? item.id : item))
- .filter(Boolean),
- ),
- shiftPlans,
- remark: form.remark || '',
- };
- // 修改 updateRule 时 body.id 必填;新增 createRule 不传 id
- if (extras.forUpdate || extras.includeId) {
- const id = keepBigIntId(form.id || extras.id);
- if (id != null) payload.id = id;
- }
- return compact(payload);
- }
- function inferMethod(punchMethods = []) {
- const codes = punchMethods.map((item) => item.punchMethod || item);
- const hasMobile = codes.some((item) => ['GPS', 'WIFI', 'BEACON', 'PC', 'FIELD'].includes(item));
- const hasDevice = codes.some((item) => ['ATTENDANCE_MACHINE', 'FACE'].includes(item));
- if (hasMobile && hasDevice) return 'phone-device';
- if (hasDevice) return 'device';
- return 'phone';
- }
- function overtimeRule(list, dayType) {
- return (list || []).find((item) => item.dayType === dayType) || {};
- }
- /** 审计时间(接口 createTime 为 ISO,updateTime 为 yyyy-MM-dd HH:mm:ss)统一成 yyyy-MM-dd HH:mm */
- function formatAuditTime(value) {
- if (!value) return '';
- return String(value).replace('T', ' ').slice(0, 16);
- }
- export function adaptAttendanceRule(raw = {}) {
- const punchMethods = raw.punchMethods || [];
- const methods = [...new Set(
- punchMethods
- .map((item) => PUNCH_METHOD_FROM_API[item.punchMethod] || String(item.punchMethod || '').toLowerCase())
- .filter(Boolean),
- )];
- const locations = (raw.locations || []).map((item) => ({
- name: item.locationName,
- range: `${item.radius || 300}米`,
- latitude: item.latitude,
- longitude: item.longitude,
- outsidePolicy: item.outsidePolicy || '',
- }));
- const firstLocationPolicy = locations.find((item) => item.outsidePolicy)?.outsidePolicy;
- const wifis = (raw.wifiList || []).map((item) => ({
- ssid: item.ssid,
- bssid: item.bssid,
- locationName: item.locationName,
- }));
- const work = overtimeRule(raw.overtimeRules, 'WORKDAY');
- const rest = overtimeRule(raw.overtimeRules, 'RESTDAY');
- const holiday = overtimeRule(raw.overtimeRules, 'LEGAL_HOLIDAY');
- const card = raw.cardRepair || {};
- const assist = raw.assistance || {};
- const field = raw.field || {};
- const firstScope = (raw.scopes || [])[0] || {};
- const scopes = (raw.scopes || []).map((item, index) => ({
- scopeType: item.scopeType || 'ORG',
- scopeId: keepBigIntId(item.scopeId),
- priority: Number(item.priority) > 0 ? Number(item.priority) : index + 1,
- scopeName: item.scopeName || item.name || '',
- }));
- const firstPlan = (raw.shiftPlans || [])[0] || {};
- const workDays = [...new Set((raw.shiftPlans || []).map((item) => WEEK_FROM_API[item.weekDay]).filter(Boolean))];
- const firstType = String(card.allowedTypes || '').split(',')[0];
- return {
- id: keepBigIntId(raw.id),
- ruleCode: raw.ruleCode,
- name: raw.ruleName || '',
- type: RULE_TYPE_FROM_API[raw.ruleType] || 'fixed',
- status: raw.status,
- enabled: raw.status === 'ACTIVE',
- effectiveStartDate: raw.effectiveStartDate ? String(raw.effectiveStartDate).slice(0, 10) : undefined,
- effectiveEndDate: raw.effectiveEndDate ? String(raw.effectiveEndDate).slice(0, 10) : undefined,
- settlement: adaptAttendanceSettlement(raw.settlement),
- absentThresholdMinutes: raw.absentThresholdMinutes,
- scopes,
- scopeType: firstScope.scopeType || 'ORG',
- scopeId: keepBigIntId(firstScope.scopeId),
- scope: keepBigIntId(firstScope.scopeId),
- timezone: TIMEZONE_FROM_API[raw.timezone || assist.timezone] || '(GMT+08:00)中国标准时间-北京',
- calendarId: keepBigIntId(raw.calendarId),
- holiday: raw.calendarSummary || '跟随国家法定节假日',
- workTime: raw.shiftTimeSummary || '',
- restDays: workDays.length ? ['周六', '周日'].filter((day) => !workDays.includes(day)) : ['周六', '周日'],
- methods: methods.length ? methods : ['gps'],
- location: locations.map((item) => `${item.name}(${item.range})`).join('、') || '-',
- wifi: wifis.map((item) => item.ssid).filter(Boolean).join('、') || '-',
- deviceSummary: raw.deviceIds?.length ? `${raw.deviceIds.length} 台` : '-',
- overtime: (raw.overtimeRules || []).some((item) => on(item.enabledFlag)) ? '开启' : '关闭',
- overtimeDuration: raw.overtimeSummary || '',
- makeup: on(card.repairEnabled) ? '允许' : '不允许',
- leaveMakeup: '请假当日无需补卡',
- /* 审计字段:接口返回 createUserId / createUserName / createTime / updateUserName / updateTime,
- 列表「创建人 / 创建时间 / 修改人 / 修改时间」列直接取这几个,不映射就会显示 "-" */
- createUserId: keepBigIntId(raw.createUserId),
- createUserName: raw.createUserName || '',
- createTime: formatAuditTime(raw.createTime),
- updateUserName: raw.updateUserName || '',
- updateTime: formatAuditTime(raw.updateTime),
- updatedAt: formatAuditTime(raw.updateTime || raw.createTime),
- method: inferMethod(punchMethods),
- locations,
- wifis,
- deviceIds: keepBigIntIdList(raw.deviceIds || []),
- devices: [],
- devicesList: [],
- outOfRange: OUTSIDE_POLICY_LABEL[firstLocationPolicy] || '异常',
- workDays: workDays.length ? workDays : ['周一', '周二', '周三', '周四', '周五'],
- workShifts: [
- {
- key: 'WS1',
- shiftId: keepBigIntId(firstPlan.shiftId),
- start: '09:00',
- end: '18:00',
- startOpen: on(firstPlan.needInPunch) || firstPlan.needInPunch == null ? '是' : '否',
- endOpen: on(firstPlan.needOutPunch) || firstPlan.needOutPunch == null ? '是' : '否',
- lunchBreaks: [{ key: 'L1', start: '12:00', end: '13:00' }],
- },
- ],
- flexEnabled: false,
- lateComp: false,
- halfDayRange: 'morning',
- halfStart: '08:00',
- halfEnd: '12:00',
- punchStart: '05:00',
- punchEnd: '24:00',
- relatedTarget: methods.includes('ding') ? 'ding' : methods.includes('device') ? 'device' : 'app',
- overtimeOn: (raw.overtimeRules || []).some((item) => on(item.enabledFlag)),
- overtimeUnit: '小时',
- overtimeRound: ROUND_MODE_FROM_API[work.roundMode] || '四舍五入',
- overtimeDecimal: '1位小数',
- overtimeDayHours: 9,
- overtimeStart: Number(work.minimumMinutes || 30),
- workOvertimeEnabled: on(work.enabledFlag),
- workOvertimePeriod: '下班后到次日上班前',
- workCalcMethod: on(work.approvalRequiredFlag) ? '按审批时长计算' : '按打卡时长计算',
- workDeductRest: on(work.deductBreakFlag),
- workCompOrPay: work.compensationMode !== 'COMP_LEAVE',
- restOvertimeEnabled: on(rest.enabledFlag),
- restOvertimePeriod: '全天',
- restCalcMethod: on(rest.approvalRequiredFlag) ? '按审批时长计算' : '按打卡时长计算',
- restDeductRest: on(rest.deductBreakFlag),
- restCompOrPay: rest.compensationMode !== 'COMP_LEAVE',
- holidayOvertimeEnabled: on(holiday.enabledFlag),
- holidayOvertimePeriod: '全天',
- holidayCalcMethod: on(holiday.approvalRequiredFlag) ? '按审批时长计算' : '按打卡时长计算',
- holidayDeductRest: on(holiday.deductBreakFlag),
- holidayCompOrPay: holiday.compensationMode !== 'COMP_LEAVE',
- makeupEnabled: on(card.repairEnabled),
- makeupType: MAKEUP_TYPE_FROM_API[firstType] || '缺卡/旷工',
- makeupTimeLimit: card.timeLimitDays ? `异常后 ${card.timeLimitDays} 天内` : '不限制',
- makeupReminder: on(card.reminderEnabled) ? '启用' : '禁用',
- makeupReminderTime: toHm(card.reminderTime) || '09:00',
- makeupMaxPerMonth: card.monthlyLimitCount ? `${card.monthlyLimitCount}次` : '不限',
- makeupDeadline: card.monthlyDeadlineDay === 31 ? '当月最后一天' : card.monthlyDeadlineDay ? `次月 ${card.monthlyDeadlineDay} 日` : '不设置',
- approvalPunchEnabled: on(card.approvalPunchEnabled),
- assistantEnabled: on(assist.enabledFlag),
- assistantExemptUsers: keepBigIntIdList(raw.exemptUserIds || []),
- exemptUserIds: keepBigIntIdList(raw.exemptUserIds || []),
- reminderOffset: assist.punchReminderMinutes === 5 ? '上班前 5 分钟、下班延后 5 分钟' : '上班前 10 分钟、下班准点',
- asstOutOfRange: assist.outsidePolicy === 'ALLOW' ? '允许打卡' : assist.outsidePolicy === 'REJECT' ? '不允许打卡' : '记录为地点异常',
- faceRecognition: on(assist.faceRequiredFlag) ? '启用' : '禁用',
- photoEachPunch: on(assist.photoRequiredFlag),
- faceEachPunch: on(assist.faceRequiredFlag),
- outsourceSync: on(assist.fieldSyncEnabled),
- outsideEnabled: on(field.enabledFlag),
- outsideMode: field.fieldMode === 'LOCATION' ? 'location' : 'time',
- outsideStart: toHm(field.punchStartTime) || '09:00',
- outsideEnd: toHm(field.punchEndTime) || '18:00',
- outsideAddress: on(field.locationRestrictedFlag) ? '限制定' : '不限制',
- outsidePhoto: on(field.photoRequiredFlag),
- reportStart: `${String(field.summaryHour ?? 9).padStart(2, '0')}:${String(field.summaryMinute ?? 0).padStart(2, '0')}`,
- reportEnd: '10:00',
- /* 请假离岗/返岗打卡设置:读后端 leavePunch(回填真实值,不再硬编码)。
- 历史规则(后端字段上线前创建)没有该对象时才用后端默认:需打卡 + 不限制。 */
- leaveNeedPunch: raw.leavePunch ? on(raw.leavePunch.leavePunchRequired) : true,
- leavePunchWindow: raw.leavePunch?.punchTimeLimitType || 'UNLIMITED',
- /* 该规则已关联的假期制度主键(页面「关联」勾选据此回显) */
- leavePolicyIds: keepBigIntIdList(raw.leavePolicyIds || []),
- leaveRules: [],
- raw,
- };
- }
- /**
- * 请假离岗/返岗「打卡时间」选项(value 就是后端 leavePunch.punchTimeLimitType 枚举)。
- * 页面下拉必须用这套码,别再写中文字符串当 value(否则保存时对不上后端枚举)。
- */
- export const LEAVE_PUNCH_TIME_OPTIONS = [
- { value: 'UNLIMITED', label: '不限制' },
- { value: 'BOTH_30_MINUTES', label: '上下班前后 30 分钟' },
- { value: 'BOTH_60_MINUTES', label: '上下班前后 1 小时' },
- { value: 'LEAVE_START_ONLY', label: '仅请假开始时间' },
- { value: 'LEAVE_END_ONLY', label: '仅请假结束时间' },
- ];
- /* ==================== 假期制度(打卡规则 → 请假规则 Tab) ====================
- * ★ 后端已结构化(2026-10-17 起,见 tmp/swagger-hr-new.json):
- * POST /hr/attendance/leave-policies/saveLeavePolicy 直接收
- * leaveCategory / scopeType+scopeIds / conditions[{conditionType,conditionValue}] /
- * quotaMode+quotaDays+quotaRule / salaryMode+salaryRate / status / 生效区间,
- * grantRuleJson 降级为「兼容旧调用方的扩展项载体」,只用来存 typeText、scopeNames 这类展示扩展。
- * 枚举码以下面常量为唯一口径,页面下拉、表格展示、提交全用码,不再写中文字符串当 value。
- */
- const LEAVE_TYPE_TO_API = {
- 年假: 'ANNUAL',
- 事假: 'PERSONAL',
- 病假: 'SICK',
- 婚假: 'MARRIAGE',
- 产假: 'MATERNITY',
- 陪产假: 'PATERNITY',
- 丧假: 'BEREAVEMENT',
- 调休: 'COMPENSATORY',
- 工伤假: 'INJURY',
- 带薪假: 'ANNUAL',
- 扣除薪资: 'PERSONAL',
- 特殊假期: 'OTHER',
- 加班调休: 'COMPENSATORY',
- };
- /** leaveType 枚举(后端 required,未知假期名称统一落 OTHER) */
- const LEAVE_TYPE_CODES = [
- 'ANNUAL',
- 'SICK',
- 'PERSONAL',
- 'MARRIAGE',
- 'MATERNITY',
- 'PATERNITY',
- 'BEREAVEMENT',
- 'INJURY',
- 'COMPENSATORY',
- 'OTHER',
- ];
- /** 假期分类(保存参数 leaveCategory:LEAVE 请假 / PUBLIC 公假 / WELFARE 福利假) */
- export const LEAVE_CATEGORY_OPTIONS = [
- { value: 'LEAVE', label: '请假' },
- { value: 'PUBLIC', label: '公假' },
- { value: 'WELFARE', label: '福利假' },
- ];
- /** 适用人员(保存参数 scopeType:ALL / DEPARTMENT / POSITION / EMPLOYEE) */
- export const LEAVE_SCOPE_OPTIONS = [
- { value: 'ALL', label: '全体员工' },
- { value: 'DEPARTMENT', label: '指定部门' },
- { value: 'POSITION', label: '指定岗位' },
- { value: 'EMPLOYEE', label: '指定员工' },
- ];
- /** 满足条件类型(conditions[].conditionType);quotaRule 是动态额度时的折算口径文案 */
- export const LEAVE_CONDITION_OPTIONS = [
- {
- value: 'EMPLOYMENT_STATUS',
- label: '在职状态',
- placeholder: '如:员工状态为在职',
- quotaRule: '按在职状态核定',
- },
- {
- value: 'TENURE',
- label: '在职年限',
- placeholder: '如:大于 1 年',
- quotaRule: '按在职年限折算',
- },
- {
- value: 'OVERTIME_CONVERSION',
- label: '加班时长折算',
- placeholder: '如:8 小时 = 1 天',
- quotaRule: '按加班余额折算',
- },
- {
- value: 'INJURY_ASSESSMENT',
- label: '工伤情况核定',
- placeholder: '由工伤核定单认定天数',
- quotaRule: '按工伤核定天数',
- },
- { value: 'NONE', label: '无条件', placeholder: '无需填写参数', quotaRule: '' },
- ];
- /** 年度额度模式(quotaMode) */
- export const LEAVE_QUOTA_MODE_OPTIONS = [
- { value: 'FIXED', label: '固定额度(天)' },
- { value: 'DYNAMIC', label: '按条件动态' },
- ];
- /** 薪资规则(salaryMode:KEEP / BASE / DAILY) */
- export const LEAVE_SALARY_MODE_OPTIONS = [
- { value: 'KEEP', label: '不扣薪资' },
- { value: 'BASE', label: '按基本薪资比例扣除' },
- { value: 'DAILY', label: '按日薪资比例扣除' },
- ];
- /**
- * 历史码表兼容:旧版前端/文档里用过下面这套码,读老数据时归一成后端现行码。
- * 写库一律用现行码,不再产出旧码。
- */
- const LEAVE_LEGACY_CODE_MAP = {
- DEPT: 'DEPARTMENT',
- BENEFIT: 'WELFARE',
- SERVICE_YEARS: 'TENURE',
- OVERTIME_CONVERT: 'OVERTIME_CONVERSION',
- WORK_INJURY: 'INJURY_ASSESSMENT',
- BASE_RATIO: 'BASE',
- DAILY_RATIO: 'DAILY',
- };
- /** 假期制度状态(后端 status) */
- export const LEAVE_POLICY_STATUS_OPTIONS = [
- { value: 'ACTIVE', label: '启用' },
- { value: 'DISABLED', label: '停用' },
- { value: 'DRAFT', label: '草稿' },
- ];
- function leaveOptionLabel(options, value) {
- const found = options.find((item) => item.value === value);
- return found ? found.label : '';
- }
- /**
- * 归一:正常存的是枚举码,历史 grantRuleJson 里可能是旧码或中文文案,三者都认;
- * 认不出来的返回空串(页面按「未返回」展示,前端不猜)。
- */
- function leaveOptionValue(options, raw) {
- const text = String(raw === undefined || raw === null ? '' : raw).trim();
- if (!text) return '';
- const normalized = LEAVE_LEGACY_CODE_MAP[text] || text;
- const found = options.find((item) => item.value === normalized || item.label === text);
- return found ? found.value : '';
- }
- export function leaveConditionLabel(type) {
- return leaveOptionLabel(LEAVE_CONDITION_OPTIONS, leaveOptionValue(LEAVE_CONDITION_OPTIONS, type));
- }
- export function leaveConditionPlaceholder(type) {
- const value = leaveOptionValue(LEAVE_CONDITION_OPTIONS, type);
- const found = LEAVE_CONDITION_OPTIONS.find((item) => item.value === value);
- return found ? found.placeholder : '';
- }
- export function leaveCategoryLabel(category) {
- return leaveOptionLabel(LEAVE_CATEGORY_OPTIONS, leaveOptionValue(LEAVE_CATEGORY_OPTIONS, category));
- }
- export function leaveScopeLabel(scope) {
- return leaveOptionLabel(LEAVE_SCOPE_OPTIONS, leaveOptionValue(LEAVE_SCOPE_OPTIONS, scope));
- }
- export function leavePolicyStatusLabel(statusCode) {
- return leaveOptionLabel(LEAVE_POLICY_STATUS_OPTIONS, statusCode);
- }
- function formatLeaveRatio(value) {
- if (value === null || value === undefined || value === '') return '';
- const number = Number(value);
- if (!Number.isFinite(number)) return '';
- return String(Number(number.toFixed(2)));
- }
- /**
- * 薪资规则一句话文案(表格 tag / 抽屉说明共用)。
- * 优先用后端 salaryRuleDescription(后端口径说明),没有再按 salaryMode + salaryRate 拼;
- * 两者都取不到返回空串,由页面显示「—」。
- */
- export function leaveSalaryRuleText(row = {}) {
- const mode = leaveOptionValue(LEAVE_SALARY_MODE_OPTIONS, row.salaryMode);
- if (mode === 'KEEP') return '不扣薪资';
- if (!mode) return '';
- const rate = row.salaryRate === null || row.salaryRate === undefined ? row.salaryRatio : row.salaryRate;
- const base = mode === 'BASE' ? '基本薪资' : '日薪资';
- const ratioText = formatLeaveRatio(rate);
- /* 比率没落库时不编造数字,只说明扣薪基数 */
- return ratioText ? `扣薪 · ${base}×${ratioText}%` : `扣薪 · 按${base}比例`;
- }
- /** 动态额度折算口径:按满足条件生成,如「按在职年限折算(大于 1 年)」 */
- export function leaveDynamicQuotaText(conditions = []) {
- return (Array.isArray(conditions) ? conditions : [])
- .filter((item) => item && item.conditionType && item.conditionType !== 'NONE')
- .map((item) => {
- const rule = LEAVE_CONDITION_OPTIONS.find((option) => option.value === item.conditionType);
- const base = rule ? rule.quotaRule : '';
- const param = String(item.conditionValue || '').trim();
- if (!base) return param;
- return param ? `${base}(${param})` : base;
- })
- .filter(Boolean)
- .join(' + ');
- }
- function resolveLeaveType(row = {}) {
- const raw = String(row.leaveType || '').trim();
- if (LEAVE_TYPE_CODES.includes(raw)) return raw;
- return (
- LEAVE_TYPE_TO_API[row.leaveType] ||
- LEAVE_TYPE_TO_API[row.type] ||
- LEAVE_TYPE_TO_API[row.name] ||
- 'OTHER'
- );
- }
- /** 归一条件数组:新版 {conditionType,conditionValue} 与旧版 {type,param} 都认 */
- function normalizeLeaveConditions(list = []) {
- return (Array.isArray(list) ? list : [])
- .map((item) => {
- const type = leaveOptionValue(LEAVE_CONDITION_OPTIONS, item && (item.conditionType || item.type));
- const value = String((item && (item.conditionValue ?? item.param)) || '').trim();
- return type ? { conditionType: type, conditionValue: type === 'NONE' ? '' : value } : null;
- })
- .filter(Boolean);
- }
- function toDateOnly(value) {
- return value ? String(value).slice(0, 10) : '';
- }
- /**
- * 保存参数(POST /hr/attendance/leave-policies/saveLeavePolicy)。
- * 结构化字段是主通道;grantRuleJson 只保留后端没有对应字段的展示扩展(typeText、scopeNames、approval),
- * 供编辑抽屉回显中文名称,不参与后端校验。
- * @param {Object} row 页面表单 / 列表行
- * @param {Object} extra { zeroQuotaConfirmed, disableConfirmed }
- */
- export function serializeLeavePolicy(row = {}, extra = {}) {
- const conditions = normalizeLeaveConditions(row.conditions);
- const quotaMode = row.quotaMode === 'DYNAMIC' ? 'DYNAMIC' : 'FIXED';
- const salaryMode = leaveOptionValue(LEAVE_SALARY_MODE_OPTIONS, row.salaryMode) || 'KEEP';
- const scopeType = leaveOptionValue(LEAVE_SCOPE_OPTIONS, row.scopeType || row.scope) || 'ALL';
- const category = leaveOptionValue(LEAVE_CATEGORY_OPTIONS, row.leaveCategory || row.category) || 'LEAVE';
- const status = leaveOptionValue(LEAVE_POLICY_STATUS_OPTIONS, row.statusCode) || 'DRAFT';
- const scopeIds = scopeType === 'ALL' ? [] : keepBigIntIdList(row.scopeIds || []);
- const scopeNames = scopeType === 'ALL' ? [] : (row.scopeNames || []).map((item) => String(item));
- const quotaDays = quotaMode === 'DYNAMIC' ? undefined : Number(row.quotaDays);
- const salaryRate =
- salaryMode === 'KEEP'
- ? undefined
- : Number(row.salaryRate === null || row.salaryRate === undefined ? row.salaryRatio : row.salaryRate);
- const grantRuleJson = JSON.stringify({
- typeText: String(row.type || '').trim(),
- scopeNames,
- approval: row.approval || '',
- });
- return compact({
- id: keepBigIntId(row.id) || undefined,
- /* 新增不传 policyCode,由后端按租户唯一生成;修改沿用原编码 */
- policyCode: row.policyCode || undefined,
- policyName: String(row.name || row.policyName || '').trim(),
- leaveCategory: category,
- leaveType: resolveLeaveType(row),
- scopeType,
- scopeIds,
- conditions,
- quotaMode,
- quotaDays,
- quotaRule: quotaMode === 'DYNAMIC' ? leaveDynamicQuotaText(conditions) || undefined : undefined,
- salaryMode,
- salaryRate,
- status,
- effectiveStartDate: toDateOnly(row.effectiveStartDate) || today(),
- effectiveEndDate: toDateOnly(row.effectiveEndDate) || undefined,
- remark: row.remark || '',
- grantRuleJson,
- zeroQuotaConfirmed: extra.zeroQuotaConfirmed === true ? true : undefined,
- disableConfirmed: extra.disableConfirmed === true ? true : undefined,
- });
- }
- /**
- * 列表 / 详情返回(假期政策返回模型)→ 页面行模型。
- * 结构化字段优先读后端字段;后端还没回填结构化字段的历史数据,退回读 grantRuleJson 里旧版结构,
- * 并把旧文本字段(conditionText/quotaText/salaryText)带出来,页面上打「旧数据」标记,
- * 编辑保存一次即升级为结构化数据。
- */
- export function adaptLeavePolicy(raw = {}) {
- let extra = {};
- try {
- extra = raw.grantRuleJson ? JSON.parse(raw.grantRuleJson) : {};
- } catch (error) {
- extra = {};
- }
- const structuredConditions = Array.isArray(raw.conditions) && raw.conditions.length
- ? raw.conditions
- : extra.conditions;
- const conditions = normalizeLeaveConditions(structuredConditions);
- const quotaDaysRaw =
- raw.quotaDays === null || raw.quotaDays === undefined || raw.quotaDays === ''
- ? extra.quotaDays
- : raw.quotaDays;
- const quotaDays =
- quotaDaysRaw === null || quotaDaysRaw === undefined || quotaDaysRaw === '' ? null : Number(quotaDaysRaw);
- /* 额度模式:后端结构化字段优先;历史数据按额度是否落库推断,都取不到就留空(页面显示旧文本/—) */
- const quotaMode =
- leaveOptionValue(LEAVE_QUOTA_MODE_OPTIONS, raw.quotaMode || extra.quotaMode)
- || (Number.isFinite(quotaDays) ? 'FIXED' : '');
- const salaryMode = leaveOptionValue(LEAVE_SALARY_MODE_OPTIONS, raw.salaryMode || extra.salaryMode);
- const salaryRateRaw =
- raw.salaryRate === null || raw.salaryRate === undefined ? extra.salaryRatio : raw.salaryRate;
- const salaryRate =
- salaryRateRaw === null || salaryRateRaw === undefined || salaryRateRaw === '' ? null : Number(salaryRateRaw);
- const statusCode = leaveOptionValue(LEAVE_POLICY_STATUS_OPTIONS, raw.status);
- const scopeType = leaveOptionValue(LEAVE_SCOPE_OPTIONS, raw.scopeType || extra.scope || extra.applyScope);
- return {
- id: raw.id,
- policyCode: raw.policyCode || '',
- leaveType: raw.leaveType || '',
- type: extra.typeText || LEAVE_TYPE_LABEL[raw.leaveType] || raw.leaveType || '',
- name: raw.policyName || '',
- leaveCategory: leaveOptionValue(LEAVE_CATEGORY_OPTIONS, raw.leaveCategory || extra.category),
- scopeType,
- /* 兼容页面里旧的 scope 命名,值已是后端现行码 */
- scope: scopeType,
- scopeIds: keepBigIntIdList(raw.scopeIds || extra.scopeIds || []),
- scopeNames: (extra.scopeNames || []).map((item) => String(item)),
- /* 后端算好的适用范围文案(如「研发部、人事部」),表格直接展示 */
- scopeDescription: raw.scopeDescription || '',
- conditions,
- /* 历史数据里的自由文本条件 / 额度 / 薪资:结构化字段缺失时原样展示 */
- conditionText: extra.condition || extra.require || '',
- quotaText: extra.quota || extra.annualQuota || '',
- salaryText: extra.salary || extra.salaryRule || '',
- quotaMode,
- quotaDays,
- quotaRule: raw.quotaRule || extra.dynamicBy || '',
- /* 兼容页面里旧的 dynamicBy 命名 */
- dynamicBy: raw.quotaRule || extra.dynamicBy || '',
- salaryMode,
- salaryRate,
- /* 兼容页面里旧的 salaryRatio 命名 */
- salaryRatio: salaryRate,
- salaryRuleDescription: raw.salaryRuleDescription || '',
- zeroQuotaWarning: raw.zeroQuotaWarning === true,
- zeroSalaryRateWarning: raw.zeroSalaryRateWarning === true,
- currentVersion: Number(raw.currentVersion || 0),
- approval: extra.approval || '',
- statusCode,
- status: leavePolicyStatusLabel(statusCode),
- action: statusCode === 'ACTIVE' ? '停用' : '启用',
- effectiveStartDate: toDateOnly(raw.effectiveStartDate),
- effectiveEndDate: toDateOnly(raw.effectiveEndDate),
- remark: raw.remark || '',
- };
- }
- export function adaptPersonalReport(raw = {}, users = {}) {
- const user = users[String(raw.userId)] || {};
- const lateCount = Number(raw.lateCount || 0);
- const absenceDays = Number(raw.absenceDays || 0);
- const status = absenceDays > 0 ? 'danger' : lateCount > 0 ? 'warning' : 'normal';
- return {
- id: raw.id || `${raw.userId}-${raw.periodId}`,
- userId: raw.userId,
- name: user.name || raw.userName || raw.userCode || String(raw.userId || '-'),
- department: user.deptName || user.groupName || '-',
- subDept: user.postName || '',
- position: user.postName || '-',
- attendance: Number(raw.actualDays || 0),
- expected: Number(raw.requiredDays || 0),
- late: lateCount || '正常',
- absent: absenceDays || '正常',
- status,
- statusLabel: status === 'danger' ? '旷工' : status === 'warning' ? '迟到' : '正常',
- raw,
- };
- }
- export function exceptionLabel(type) {
- return EXCEPTION_LABEL[type] || type || '其他';
- }
- /* ==================== 考勤明细(每日考勤 GET /hr/attendance/daily) ==================== */
- /** 打卡方式展示文案(punchRecords.punchMethod) */
- export const DAILY_PUNCH_METHOD_LABEL = {
- GPS: 'GPS打卡',
- WIFI: 'WiFi打卡',
- BEACON: '蓝牙打卡',
- ATTENDANCE_MACHINE: '考勤机打卡',
- FACE: '人脸打卡',
- PC: 'PC打卡',
- FIELD: '外勤打卡',
- };
- export function dailyPunchMethodLabel(method) {
- return DAILY_PUNCH_METHOD_LABEL[method] || method || '';
- }
- /** ISO 时间 → yyyy-MM-dd HH:mm:ss(无时间部分只留日期) */
- function formatDailyDateTime(value) {
- const text = String(value || '');
- if (!text) return '';
- const date = text.slice(0, 10);
- const time = text.length >= 19 ? text.slice(11, 19) : '';
- return time ? `${date} ${time}` : date;
- }
- /** 打卡记录「打卡情况」:补卡来源优先标补卡,其余按 status 出文案 */
- export function dailyPunchStatusText(record = {}) {
- if (record.sourceType === 'REPAIR') return '补卡';
- return { VALID: '正常', INVALID: '无效', PENDING_APPROVAL: '待审批' }[record.status] || record.status || '';
- }
- /**
- * 分页查询每日考勤(夜班归属班次开始日)
- * GET /hr/attendance/daily
- * query: dateEnd, dateStart, deptId, employeeKeyword, keyword, pageNum, periodId, punchMethod, size, status, userId
- */
- export const getDailyAttendanceDetailPage = async (params = {}) => {
- const query = compact({
- dateEnd: params.dateEnd || undefined,
- dateStart: params.dateStart || undefined,
- deptId: keepBigIntId(params.deptId),
- employeeKeyword: params.employeeKeyword || undefined,
- keyword: params.keyword || undefined,
- pageNum: params.pageNum != null ? Number(params.pageNum) : 1,
- periodId: keepBigIntId(params.periodId),
- punchMethod: params.punchMethod || undefined,
- size: params.size != null ? Number(params.size) : 20,
- status: params.status || undefined,
- userId: keepBigIntId(params.userId),
- });
- return pageResult(await unwrap(await request.get('/hr/attendance/daily', { params: query })));
- };
- /**
- * 每日考勤结果返回模型 → 页面行模型。
- * ★ 接口不返回姓名/部门(只回 userId):姓名、工号、部门走 loadEmployeeBriefs 主数据批量接口补齐,
- * 查不到就留空;打卡地址、工作地址接口完全没有对应字段,恒留空,缺口转后端改造清单。
- */
- export function adaptDailyAttendance(raw = {}, brief = null) {
- const records = Array.isArray(raw.punchRecords) ? raw.punchRecords : [];
- const statusList = Array.isArray(raw.statusList) ? raw.statusList : [];
- return {
- id: keepBigIntId(raw.id),
- userId: keepBigIntId(raw.userId),
- name: brief?.name || '',
- employeeNo: brief?.employeeNo || '',
- department: brief?.department || '',
- attendanceDate: String(raw.attendanceDate || '').slice(0, 10),
- statusList,
- statusText: statusList.map((item) => exceptionLabel(item)).join('、'),
- punchTypes: [
- ...new Set(records.map((item) => dailyPunchMethodLabel(item.punchMethod)).filter(Boolean)),
- ].join('、'),
- /* 接口未返回打卡地址(文档明确 punchRecords 不含定位),留空待后端补充 */
- punchAddress: '',
- punchStatuses: records.map((item) => ({
- punchRecordId: keepBigIntId(item.punchRecordId),
- punchTime: formatDailyDateTime(item.punchTime),
- punchMethod: dailyPunchMethodLabel(item.punchMethod),
- punchType: item.punchType || '',
- sourceType: item.sourceType || '',
- status: item.status || '',
- statusText: dailyPunchStatusText(item),
- })),
- actualIn: formatDailyDateTime(raw.actualIn),
- actualOut: formatDailyDateTime(raw.actualOut),
- /* 接口未返回工作地址,留空待后端补充 */
- workAddress: '',
- scheduledStart: formatDailyDateTime(raw.scheduledStart),
- scheduledEnd: formatDailyDateTime(raw.scheduledEnd),
- requiredMinutes: raw.requiredMinutes ?? null,
- actualWorkMinutes: raw.actualWorkMinutes ?? null,
- effectiveWorkMinutes: raw.effectiveWorkMinutes ?? null,
- lateMinutes: raw.lateMinutes ?? null,
- earlyMinutes: raw.earlyMinutes ?? null,
- absenceMinutes: raw.absenceMinutes ?? null,
- leaveMinutes: raw.leaveMinutes ?? null,
- outingMinutes: raw.outingMinutes ?? null,
- tripMinutes: raw.tripMinutes ?? null,
- overtimeMinutes: raw.overtimeMinutes ?? null,
- nightMinutes: raw.nightMinutes ?? null,
- exceptionCount: raw.exceptionCount ?? null,
- calculationStatus: raw.calculationStatus || '',
- lastCalcTime: formatDailyDateTime(raw.lastCalcTime),
- raw,
- };
- }
|