年假计算公式

parent 8f7cb21f
......@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"bcryptjs": "^2.4.3",
"chinese-holidays": "^1.8.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"exceljs": "^4.4.0",
......@@ -362,6 +363,22 @@
"node": ">=0.4.0"
}
},
"node_modules/ajv": {
"version": "6.15.0",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz",
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
......@@ -448,12 +465,45 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
......@@ -463,6 +513,12 @@
"node": ">= 6.0.0"
}
},
"node_modules/aws4": {
"version": "1.13.2",
"resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.13.2.tgz",
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
......@@ -489,6 +545,15 @@
],
"license": "MIT"
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz",
......@@ -719,6 +784,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"license": "Apache-2.0"
},
"node_modules/chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/chainsaw/-/chainsaw-0.1.0.tgz",
......@@ -731,6 +802,18 @@
"node": "*"
}
},
"node_modules/chinese-holidays": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/chinese-holidays/-/chinese-holidays-1.8.0.tgz",
"integrity": "sha512-l+KcCqY7+ob1sR+4EM6EiV1oZVJxQ4pHhhiTzgRUqBlmKX+81pxuzEBnlGYkVqVgMVlkHuZSnutv0hf2p17WwA==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.13",
"moment": "^2.19.3",
"request": "^2.88.0",
"request-promise-native": "^1.0.5"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
......@@ -756,6 +839,18 @@
"fsevents": "~2.3.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/compress-commons": {
"version": "4.1.2",
"resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-4.1.2.tgz",
......@@ -911,6 +1006,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/dayjs": {
"version": "1.11.21",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz",
......@@ -926,6 +1033,15 @@
"ms": "2.0.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz",
......@@ -1009,6 +1125,16 @@
"xtend": "^4.0.0"
}
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"license": "MIT",
"dependencies": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
......@@ -1167,6 +1293,21 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
"engines": [
"node >=0.6.0"
],
"license": "MIT"
},
"node_modules/fast-csv": {
"version": "4.3.6",
"resolved": "https://registry.npmmirror.com/fast-csv/-/fast-csv-4.3.6.tgz",
......@@ -1180,6 +1321,18 @@
"node": ">=10.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
......@@ -1211,6 +1364,29 @@
"node": ">= 0.8"
}
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
......@@ -1327,6 +1503,15 @@
"node": ">= 0.4"
}
},
"node_modules/getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmmirror.com/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
......@@ -1379,6 +1564,29 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/har-validator": {
"version": "5.1.5",
"resolved": "https://registry.npmmirror.com/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"deprecated": "this library is no longer supported",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
......@@ -1423,6 +1631,21 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
},
"engines": {
"node": ">=0.8",
"npm": ">=1.3.7"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
......@@ -1555,12 +1778,48 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"license": "MIT"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"license": "MIT"
},
"node_modules/jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"license": "MIT"
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC"
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
......@@ -1589,6 +1848,21 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"license": "MIT",
"dependencies": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.4.0",
"verror": "1.10.0"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
......@@ -1649,6 +1923,12 @@
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==",
"license": "ISC"
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
......@@ -1888,6 +2168,15 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
......@@ -1990,6 +2279,15 @@
"node": ">=0.10.0"
}
},
"node_modules/oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmmirror.com/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
......@@ -2069,6 +2367,12 @@
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT"
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz",
......@@ -2101,6 +2405,27 @@
"node": ">= 0.10"
}
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.15.2",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz",
......@@ -2204,6 +2529,90 @@
"node": ">=8.10.0"
}
},
"node_modules/request": {
"version": "2.88.2",
"resolved": "https://registry.npmmirror.com/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
"license": "Apache-2.0",
"dependencies": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/request-promise-core": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/request-promise-core/-/request-promise-core-1.1.4.tgz",
"integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.19"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"request": "^2.34"
}
},
"node_modules/request-promise-native": {
"version": "1.0.9",
"resolved": "https://registry.npmmirror.com/request-promise-native/-/request-promise-native-1.0.9.tgz",
"integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==",
"deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142",
"license": "ISC",
"dependencies": {
"request-promise-core": "1.1.4",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
},
"engines": {
"node": ">=0.12.0"
},
"peerDependencies": {
"request": "^2.34"
}
},
"node_modules/request/node_modules/qs": {
"version": "6.5.5",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.5.5.tgz",
"integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.6"
}
},
"node_modules/request/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"license": "MIT",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz",
......@@ -2454,6 +2863,31 @@
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
}
},
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmmirror.com/sshpk/-/sshpk-1.18.0.tgz",
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"license": "MIT",
"dependencies": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
},
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
......@@ -2463,6 +2897,15 @@
"node": ">= 0.8"
}
},
"node_modules/stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==",
"license": "ISC",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz",
......@@ -2580,6 +3023,19 @@
"node": ">=0.6"
}
},
"node_modules/tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmmirror.com/traverse/-/traverse-0.3.9.tgz",
......@@ -2704,6 +3160,24 @@
"strip-json-comments": "^2.0.0"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense"
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
......@@ -2770,6 +3244,15 @@
"setimmediate": "~1.0.4"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
......@@ -2810,6 +3293,26 @@
"node": ">= 0.8"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz",
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"engines": [
"node >=0.6.0"
],
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
},
"node_modules/verror/node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
......
......@@ -10,6 +10,7 @@
},
"dependencies": {
"bcryptjs": "^2.4.3",
"chinese-holidays": "^1.8.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"exceljs": "^4.4.0",
......
......@@ -25,8 +25,8 @@ CREATE TABLE IF NOT EXISTS employees (
department VARCHAR(50) COMMENT '部门',
entry_date DATE COMMENT '入职时间',
status ENUM('在职','离职') NOT NULL DEFAULT '在职' COMMENT '状态',
annual_leave_days DECIMAL(5,1) DEFAULT 3.0 COMMENT '年假天数',
used_leave_days DECIMAL(5,1) DEFAULT 0.0 COMMENT '已用年假天数',
annual_leave_days DECIMAL(5,1) DEFAULT 0.0 COMMENT '年假天数(快照,实时计算以annual_leave_yearly为准)',
used_leave_days DECIMAL(5,1) DEFAULT 0.0 COMMENT '已用年假天数(快照,实时计算以annual_leave_yearly为准)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB COMMENT='员工表';
......@@ -108,17 +108,44 @@ CREATE TABLE IF NOT EXISTS compensatory_leaves (
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
) ENGINE=InnoDB COMMENT='调休表';
-- 法定假日表
CREATE TABLE IF NOT EXISTS holidays (
id INT PRIMARY KEY AUTO_INCREMENT,
holiday_date DATE NOT NULL COMMENT '日期',
is_holiday TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=假日, 0=调休工作日',
description VARCHAR(100) COMMENT '说明(如春节、国庆)',
UNIQUE KEY uk_date (holiday_date)
) ENGINE=InnoDB COMMENT='法定假日及调休表';
-- 年度年假额度表
CREATE TABLE IF NOT EXISTS annual_leave_yearly (
id INT PRIMARY KEY AUTO_INCREMENT,
employee_id INT NOT NULL COMMENT '员工ID',
year_start DATE NOT NULL COMMENT '年假周期起始日(满周年日)',
year_end DATE NOT NULL COMMENT '年假周期截止日(次年5月31日)',
total_days DECIMAL(5,1) NOT NULL COMMENT '该周期总年假天数',
used_days DECIMAL(5,1) DEFAULT 0.0 COMMENT '该周期已用天数',
status ENUM('active','expired') DEFAULT 'active' COMMENT 'active=使用中, expired=已过期',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
INDEX idx_emp_period (employee_id, year_start)
) ENGINE=InnoDB COMMENT='年度年假额度表';
-- 年假记录表
CREATE TABLE IF NOT EXISTS annual_leave_records (
id INT PRIMARY KEY AUTO_INCREMENT,
employee_id INT NOT NULL COMMENT '员工ID',
leave_date DATE NOT NULL COMMENT '请假日期',
days DECIMAL(4,1) NOT NULL COMMENT '请假天数',
leave_date DATE NOT NULL COMMENT '请假开始日期',
end_date DATE COMMENT '请假结束日期(日历日)',
days DECIMAL(4,1) NOT NULL COMMENT '请假天数(工作日)',
leave_year_id INT COMMENT '关联 annual_leave_yearly.id,标识属于哪个年假周期',
description TEXT COMMENT '说明',
status ENUM('待审批','已批准','已拒绝') DEFAULT '待审批' COMMENT '状态',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (leave_year_id) REFERENCES annual_leave_yearly(id) ON DELETE SET NULL
) ENGINE=InnoDB COMMENT='年假记录表';
-- 操作日志表
......
......@@ -2,14 +2,11 @@ import { Request, Response } from 'express';
import pool from '../config/database';
import { AuthRequest } from '../middleware/auth';
import { logOperation } from '../utils/logger';
// 计算年假天数
const calcAnnualLeaveDays = (entryDate: string): number => {
const entry = new Date(entryDate);
const now = new Date();
const yearsWorked = now.getFullYear() - entry.getFullYear();
return Math.min(3 + yearsWorked, 15); // 最多15天
};
import {
getAnnualLeaveBalance as calcBalance,
getPendingLeaveDays,
calcWorkingDaysSpan,
} from '../services/annualleave.service';
// 获取年假列表
export const getAnnualLeaves = async (req: AuthRequest, res: Response): Promise<void> => {
......@@ -35,7 +32,7 @@ export const getAnnualLeaves = async (req: AuthRequest, res: Response): Promise<
) as any[];
const [rows] = await pool.execute(
`SELECT a.*, e.name as employee_name, e.annual_leave_days, e.used_leave_days
`SELECT a.*, e.name as employee_name
FROM annual_leave_records a
LEFT JOIN employees e ON a.employee_id = e.id
${where} ORDER BY a.leave_date DESC LIMIT ${Number(limit)} OFFSET ${offset}`,
......@@ -63,14 +60,32 @@ export const createAnnualLeave = async (req: AuthRequest, res: Response): Promis
const { leave_date, days, description } = req.body;
const employee_id = req.user?.id;
if (!employee_id) {
res.status(401).json({ code: 401, message: '未认证' });
return;
}
if (!leave_date || !days) {
res.status(400).json({ code: 400, message: '日期和天数不能为空' });
return;
}
// 获取员工年假信息
// 校验每次不少于0.5天
const leaveDays = Number(days);
if (leaveDays < 0.5) {
res.status(400).json({ code: 400, message: '每次请假不得少于0.5天' });
return;
}
// 校验0.5天的倍数
if (leaveDays % 0.5 !== 0) {
res.status(400).json({ code: 400, message: '请假天数必须为0.5的倍数' });
return;
}
// 获取员工入职日期
const [empRows] = await pool.execute(
'SELECT annual_leave_days, used_leave_days FROM employees WHERE id = ?',
'SELECT entry_date FROM employees WHERE id = ?',
[employee_id]
) as any[];
......@@ -79,20 +94,20 @@ export const createAnnualLeave = async (req: AuthRequest, res: Response): Promis
return;
}
const emp = (empRows as any[])[0];
const remaining = Number(emp.annual_leave_days) - Number(emp.used_leave_days);
const entryDate = (empRows as any[])[0].entry_date;
// 也统计待审批的申请
const [pendingRows] = await pool.execute(
`SELECT COALESCE(SUM(days), 0) as pending FROM annual_leave_records
WHERE employee_id = ? AND status = '待审批'`,
[employee_id]
) as any[];
const pendingDays = Number((pendingRows as any[])[0].pending);
const available = remaining - pendingDays;
// 计算年假余额
const balance = await calcBalance(employee_id as number, entryDate);
if (!balance.hasRight) {
res.status(400).json({ code: 400, message: '入职未满一年,暂无年假资格' });
return;
}
// 待审批天数
const pendingDays = await getPendingLeaveDays(employee_id as number);
const available = balance.remainingDays - pendingDays;
if (Number(days) > available) {
if (leaveDays > available) {
res.status(400).json({
code: 400,
message: `年假天数不足,当前可用年假:${available}天(含待审批)`
......@@ -100,16 +115,41 @@ export const createAnnualLeave = async (req: AuthRequest, res: Response): Promis
return;
}
// 计算工作日结束日期(排除周末+法定假日)
const { endDate, calendarDays } = await calcWorkingDaysSpan(leave_date, leaveDays);
// 确定该请假属于哪个年假周期
// 优先使用当前周期,其次使用结转周期
let leaveYearId = balance.currentPeriod?.id || null;
// 如果当前周期剩余不足,需要从结转周期扣
if (balance.currentPeriod && leaveDays > balance.currentPeriod.remaining) {
// 部分使用当前周期,部分使用结转周期
// 先全部记录在当前周期,审批时再细算
leaveYearId = balance.currentPeriod.id!;
} else if (!balance.currentPeriod || balance.currentPeriod.remaining <= 0) {
// 当前周期没有剩余,使用结转周期
leaveYearId = balance.carriedPeriod?.id || null;
}
const [result] = await pool.execute(
`INSERT INTO annual_leave_records (employee_id, leave_date, days, description)
VALUES (?, ?, ?, ?)`,
[employee_id, leave_date, days, description]
`INSERT INTO annual_leave_records (employee_id, leave_date, end_date, days, leave_year_id, description)
VALUES (?, ?, ?, ?, ?, ?)`,
[employee_id, leave_date, endDate, leaveDays, leaveYearId, description || null]
) as any[];
// 记录日志
logOperation('年假管理', `提交了${days}天年假申请`, req.user?.id || null, req.user?.name || '系统', req.user?.role || '');
logOperation('年假管理', `提交了${leaveDays}天年假申请(${leave_date}~${endDate},占${calendarDays}个日历日)`, req.user?.id || null, req.user?.name || '系统', req.user?.role || '');
res.json({ code: 200, message: '年假申请已提交', data: { id: (result as any).insertId } });
res.json({
code: 200,
message: '年假申请已提交',
data: {
id: (result as any).insertId,
end_date: endDate,
calendar_days: calendarDays,
}
});
} catch (error) {
console.error(error);
res.status(500).json({ code: 500, message: '服务器错误' });
......@@ -128,7 +168,7 @@ export const approveAnnualLeave = async (req: AuthRequest, res: Response): Promi
}
const [rows] = await pool.execute(
'SELECT employee_id, days, status as cur_status FROM annual_leave_records WHERE id = ?',
'SELECT employee_id, days, leave_year_id, status as cur_status FROM annual_leave_records WHERE id = ?',
[id]
) as any[];
......@@ -139,11 +179,23 @@ export const approveAnnualLeave = async (req: AuthRequest, res: Response): Promi
const record = (rows as any[])[0];
// 如果批准,更新员工已用年假
// 如果批准,更新年假周期的used_days
if (status === '已批准' && record.cur_status === '待审批') {
const leaveDays = Number(record.days);
const yearId = record.leave_year_id;
if (yearId) {
// 更新对应年假周期
await pool.execute(
'UPDATE annual_leave_yearly SET used_days = used_days + ? WHERE id = ?',
[leaveDays, yearId]
);
}
// 同步更新员工表快照
await pool.execute(
'UPDATE employees SET used_leave_days = used_leave_days + ? WHERE id = ?',
[record.days, record.employee_id]
[leaveDays, record.employee_id]
);
}
......@@ -161,12 +213,16 @@ export const approveAnnualLeave = async (req: AuthRequest, res: Response): Promi
};
// 获取年假余额
export const getAnnualLeaveBalance = async (req: AuthRequest, res: Response): Promise<void> => {
export const getBalance = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const employee_id = req.user?.id;
if (!employee_id) {
res.status(401).json({ code: 401, message: '未认证' });
return;
}
const [rows] = await pool.execute(
'SELECT annual_leave_days, used_leave_days, entry_date FROM employees WHERE id = ?',
'SELECT entry_date FROM employees WHERE id = ?',
[employee_id]
) as any[];
......@@ -175,17 +231,48 @@ export const getAnnualLeaveBalance = async (req: AuthRequest, res: Response): Pr
return;
}
const emp = (rows as any[])[0];
// 重新计算(可能跨年)
const currentEntitlement = calcAnnualLeaveDays(emp.entry_date);
const entryDate = (rows as any[])[0].entry_date;
const balance = await calcBalance(employee_id, entryDate);
if (!balance.hasRight) {
res.json({
code: 200,
data: {
total_days: 0,
used_days: 0,
remaining_days: 0,
current_period: null,
carried_days: 0,
has_right: false,
message: '入职未满一年,暂无年假资格'
}
});
return;
}
res.json({
code: 200,
data: {
total_days: currentEntitlement,
used_days: emp.used_leave_days,
remaining_days: currentEntitlement - Number(emp.used_leave_days)
total_days: balance.totalDays,
used_days: balance.usedDays,
remaining_days: balance.remainingDays,
current_period: balance.currentPeriod ? {
start: balance.currentPeriod.periodStart.toISOString().slice(0, 10),
end: balance.currentPeriod.periodEnd.toISOString().slice(0, 10),
total: balance.currentPeriod.totalDays,
used: balance.currentPeriod.usedDays,
remaining: balance.currentPeriod.remaining,
is_carryover: balance.currentPeriod.isCarryover,
} : null,
carried_period: balance.carriedPeriod ? {
start: balance.carriedPeriod.periodStart.toISOString().slice(0, 10),
end: balance.carriedPeriod.periodEnd.toISOString().slice(0, 10),
total: balance.carriedPeriod.totalDays,
used: balance.carriedPeriod.usedDays,
remaining: balance.carriedPeriod.remaining,
} : null,
carried_days: balance.carriedPeriod?.remaining || 0,
has_right: true,
}
});
} catch (error) {
......
......@@ -61,19 +61,11 @@ export const createEmployee = async (req: Request, res: Response): Promise<void>
const hashedPassword = await bcrypt.hash(password, 10);
// 计算初始年假天数
let annualLeaveDays = 3;
if (entryDate) {
const entryYear = new Date(entryDate).getFullYear();
const currentYear = new Date().getFullYear();
const yearsWorked = currentYear - entryYear;
annualLeaveDays = Math.min(3 + yearsWorked, 15);
}
// 年假天数初始为0,实际额度由annual_leave_yearly表实时计算
const [result] = await pool.execute(
`INSERT INTO employees (name, password, email, phone, role, department, entry_date, status, annual_leave_days)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[name, hashedPassword, email, phone, role || '开发', department, entryDate, status || '在职', annualLeaveDays]
[name, hashedPassword, email, phone, role || '开发', department, entryDate, status || '在职', 0]
) as any[];
res.json({ code: 200, message: '员工创建成功', data: { id: result.insertId } });
......@@ -100,10 +92,6 @@ export const updateEmployee = async (req: Request, res: Response): Promise<void>
if (department !== undefined) { updates.push('department = ?'); params.push(department); }
if (entryDate) {
updates.push('entry_date = ?'); params.push(entryDate);
const entryYear = new Date(entryDate).getFullYear();
const currentYear = new Date().getFullYear();
const annualLeaveDays = Math.min(3 + (currentYear - entryYear), 15);
updates.push('annual_leave_days = ?'); params.push(annualLeaveDays);
}
if (status) { updates.push('status = ?'); params.push(status); }
if (password) {
......
......@@ -2,7 +2,9 @@ import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import path from 'path';
import cron from 'node-cron';
import pool from './config/database';
import authRoutes from './routes/auth.routes';
import employeeRoutes from './routes/employee.routes';
import projectRoutes from './routes/project.routes';
......@@ -14,9 +16,56 @@ import annualLeaveRoutes from './routes/annualleave.routes';
import roleRoutes from './routes/role.routes';
import logRoutes from './routes/log.routes';
import { startLogCleanupScheduler } from './utils/scheduler';
import { initHolidayData, startHolidayScheduler } from './services/holiday.service';
import { expireOldLeavePeriods, syncEmployeeLeaveSnapshot } from './services/annualleave.service';
dotenv.config();
/** 确保数据库表存在(针对新增的表) */
const ensureTables = async (): Promise<void> => {
try {
// holidays 表
await pool.execute(`
CREATE TABLE IF NOT EXISTS holidays (
id INT PRIMARY KEY AUTO_INCREMENT,
holiday_date DATE NOT NULL COMMENT '日期',
is_holiday TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=假日, 0=调休工作日',
description VARCHAR(100) COMMENT '说明',
UNIQUE KEY uk_date (holiday_date)
) ENGINE=InnoDB COMMENT='法定假日及调休表'
`);
// annual_leave_yearly 表
await pool.execute(`
CREATE TABLE IF NOT EXISTS annual_leave_yearly (
id INT PRIMARY KEY AUTO_INCREMENT,
employee_id INT NOT NULL COMMENT '员工ID',
year_start DATE NOT NULL COMMENT '年假周期起始日',
year_end DATE NOT NULL COMMENT '年假周期截止日',
total_days DECIMAL(5,1) NOT NULL COMMENT '该周期总年假天数',
used_days DECIMAL(5,1) DEFAULT 0.0 COMMENT '该周期已用天数',
status ENUM('active','expired') DEFAULT 'active' COMMENT 'active=使用中, expired=已过期',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
INDEX idx_emp_period (employee_id, year_start)
) ENGINE=InnoDB COMMENT='年度年假额度表'
`);
// annual_leave_records 表增加 end_date 和 leave_year_id 列(如果表已存在但缺少这些列)
try {
await pool.execute(`ALTER TABLE annual_leave_records ADD COLUMN end_date DATE COMMENT '请假结束日期'`);
} catch (_) { /* 列已存在则忽略 */ }
try {
await pool.execute(`ALTER TABLE annual_leave_records ADD COLUMN leave_year_id INT COMMENT '关联 annual_leave_yearly.id'`);
} catch (_) { /* 列已存在则忽略 */ }
console.log('[初始化] 数据库表检查完成');
} catch (error) {
console.error('[初始化] 数据库表检查失败:', error);
}
};
const app = express();
const PORT = process.env.PORT || 3000;
......@@ -49,10 +98,34 @@ app.get('/api/health', (req, res) => {
res.json({ code: 200, message: 'Server is running', timestamp: new Date() });
});
app.listen(PORT, () => {
// 年假周期过期检查定时器
const startLeaveExpireScheduler = () => {
// 每天凌晨3点检查过期周期,每年6月1日会真正触发过期
cron.schedule('0 3 * * *', async () => {
const now = new Date();
// 在6月1日或之后检查
if (now.getMonth() === 5 && now.getDate() >= 1) {
await expireOldLeavePeriods();
await syncEmployeeLeaveSnapshot();
}
});
console.log('[年假服务] 年假周期过期检查定时器已启动(每天3:00,6月1日起生效)');
};
app.listen(PORT, async () => {
console.log(`🚀 Server is running on http://localhost:${PORT}`);
// 确保数据库表存在
await ensureTables();
// 启动日志自动清理定时器
startLogCleanupScheduler();
// 初始化法定假日数据
await initHolidayData();
// 启动法定假日定时更新
startHolidayScheduler();
// 启动年假周期过期检查(每天凌晨3点执行)
startLeaveExpireScheduler();
// 同步年假快照
await syncEmployeeLeaveSnapshot();
});
export default app;
import { Router } from 'express';
import {
getAnnualLeaves, createAnnualLeave,
approveAnnualLeave, getAnnualLeaveBalance, deleteAnnualLeave
approveAnnualLeave, getBalance, deleteAnnualLeave
} from '../controllers/annualleave.controller';
import { authMiddleware } from '../middleware/auth';
const router = Router();
router.get('/balance', authMiddleware, getAnnualLeaveBalance);
router.get('/balance', authMiddleware, getBalance);
router.get('/', authMiddleware, getAnnualLeaves);
router.post('/', authMiddleware, createAnnualLeave);
router.put('/:id/approve', authMiddleware, approveAnnualLeave);
......
import pool from '../config/database';
import { getHolidaySet } from './holiday.service';
// ==================== 类型定义 ====================
export interface LeavePeriod {
id?: number; // annual_leave_yearly.id
periodStart: Date; // 满周年日
periodEnd: Date; // 次年5月31日
totalDays: number; // 该周期总年假天数
usedDays: number; // 该周期已用天数
remaining: number; // 该周期剩余
isCarryover: boolean; // 是否处于结转期(已跨年)
status: 'active' | 'expired';
}
export interface AnnualLeaveBalance {
currentPeriod: LeavePeriod | null; // 当前周期
carriedPeriod: LeavePeriod | null; // 上一周期结转
totalDays: number; // 总可用天数
usedDays: number; // 总已用天数
remainingDays: number; // 总剩余天数
hasRight: boolean; // 是否有年假资格
}
// ==================== 年假天数折算 ====================
/**
* 计算满N周年后的年假天数
* 规则:满1年5天,每多满1年+1天,上限10天
* 即:min(5 + (N - 1), 10),N为已满周年数
*/
const calcAnnualLeaveDays = (yearsWorked: number): number => {
return Math.min(5 + (yearsWorked - 1), 10);
};
// ==================== 年假周期计算 ====================
/**
* 获取员工的所有满周年日列表(已到达的)
*/
const getAnniversaries = (entryDate: string): Date[] => {
const entry = new Date(entryDate);
const now = new Date();
const anniversaries: Date[] = [];
for (let y = entry.getFullYear() + 1; ; y++) {
const anniv = new Date(y, entry.getMonth(), entry.getDate());
// 使用当天结束时间比较,确保满周年日当天即可享受年假
const annivEnd = new Date(y, entry.getMonth(), entry.getDate(), 23, 59, 59, 999);
if (annivEnd > now) break;
anniversaries.push(anniv);
}
return anniversaries;
};
/**
* 根据满周年日计算周期结束日(次年5月31日)
*/
const getPeriodEnd = (anniversary: Date): Date => {
return new Date(anniversary.getFullYear() + 1, 4, 31); // 次年5月31日
};
/**
* 计算指定满周年日的年假总天数
*/
const calcTotalDaysForAnniversary = (
_anniversary: Date,
anniversaryIndex: number // 第几个满周年日(1-based)
): number => {
return calcAnnualLeaveDays(anniversaryIndex);
};
/**
* 获取或创建员工的年假周期记录
* 返回当前有效周期
*/
const ensureLeavePeriod = async (
employeeId: number,
entryDate: string
): Promise<LeavePeriod | null> => {
const anniversaries = getAnniversaries(entryDate);
if (anniversaries.length === 0) return null;
const now = new Date();
const latestAnniv = anniversaries[anniversaries.length - 1];
const periodEnd = getPeriodEnd(latestAnniv);
// 如果当前时间已经超过最新周期的截止日,但下一个周年日还没到(空窗期)
// 此时最新周期虽已过期,但数据库中可能仍有该周期的 active 记录和剩余天数
// 应返回该周期信息以允许使用结转余额
if (now > periodEnd) {
// 查询该周期在数据库中的记录(可能已被标记为 expired 或仍为 active)
const yearStartStr = latestAnniv.toISOString().slice(0, 10);
const yearEndStr = periodEnd.toISOString().slice(0, 10);
const totalDays = calcTotalDaysForAnniversary(latestAnniv, anniversaries.length);
let [existing] = await pool.execute(
`SELECT id, total_days, used_days, status FROM annual_leave_yearly
WHERE employee_id = ? AND year_start = ?`,
[employeeId, yearStartStr]
) as any[];
let periodId: number;
let usedDays: number;
let status: string;
if ((existing as any[]).length > 0) {
const row = (existing as any[])[0];
periodId = row.id;
usedDays = Number(row.used_days);
status = row.status;
} else {
// 数据库中没有记录,创建一条(以便后续使用结转余额)
const [result] = await pool.execute(
`INSERT INTO annual_leave_yearly (employee_id, year_start, year_end, total_days, status)
VALUES (?, ?, ?, ?, 'active')`,
[employeeId, yearStartStr, yearEndStr, totalDays]
) as any[];
periodId = (result as any).insertId;
usedDays = 0;
status = 'active';
}
const remaining = Math.max(0, totalDays - usedDays);
if (remaining <= 0) {
return null;
}
return {
id: periodId,
periodStart: latestAnniv,
periodEnd,
totalDays,
usedDays,
remaining,
isCarryover: true,
status,
};
}
// 查询或创建当前周期记录
const yearStartStr = latestAnniv.toISOString().slice(0, 10);
const yearEndStr = periodEnd.toISOString().slice(0, 10);
const totalDays = calcTotalDaysForAnniversary(latestAnniv, anniversaries.length);
const [existing] = await pool.execute(
`SELECT id, total_days, used_days, status FROM annual_leave_yearly
WHERE employee_id = ? AND year_start = ?`,
[employeeId, yearStartStr]
) as any[];
let periodId: number;
let usedDays: number;
if ((existing as any[]).length > 0) {
const row = (existing as any[])[0];
periodId = row.id;
usedDays = Number(row.used_days);
} else {
// 创建新周期记录
const [result] = await pool.execute(
`INSERT INTO annual_leave_yearly (employee_id, year_start, year_end, total_days, status)
VALUES (?, ?, ?, ?, 'active')`,
[employeeId, yearStartStr, yearEndStr, totalDays]
) as any[];
periodId = (result as any).insertId;
usedDays = 0;
}
return {
id: periodId,
periodStart: latestAnniv,
periodEnd,
totalDays,
usedDays,
remaining: Math.max(0, totalDays - usedDays),
isCarryover: now.getFullYear() > latestAnniv.getFullYear(),
status: 'active',
};
};
// ==================== 年假余额查询 ====================
/**
* 获取员工年假余额
* 返回:当前周期剩余 + 上一周期结转剩余
*/
export const getAnnualLeaveBalance = async (
employeeId: number,
entryDate: string
): Promise<AnnualLeaveBalance> => {
const anniversaries = getAnniversaries(entryDate);
const now = new Date();
// 不满1年,无年假
if (anniversaries.length === 0) {
return {
currentPeriod: null,
carriedPeriod: null,
totalDays: 0,
usedDays: 0,
remainingDays: 0,
hasRight: false,
};
}
// 当前周期(最新的满周年日),可能因过期且无剩余返回 null
const currentPeriod = await ensureLeavePeriod(employeeId, entryDate);
// 上一周期结转(倒数第二个满周年日)
let carriedPeriod: LeavePeriod | null = null;
if (anniversaries.length >= 2) {
const prevIdx = currentPeriod && currentPeriod.isCarryover
? anniversaries.length - 3 // currentPeriod 已经是 carryover,上一周期再往前一个
: anniversaries.length - 2;
if (prevIdx >= 0) {
const prevAnniv = anniversaries[prevIdx];
const prevYearStart = prevAnniv.toISOString().slice(0, 10);
const prevPeriodEnd = getPeriodEnd(prevAnniv);
// 上一周期未过期(在次年5月31日之前)才有结转
if (now <= prevPeriodEnd) {
const [prevRows] = await pool.execute(
`SELECT id, total_days, used_days FROM annual_leave_yearly
WHERE employee_id = ? AND year_start = ? AND status = 'active'`,
[employeeId, prevYearStart]
) as any[];
if ((prevRows as any[]).length > 0) {
const prev = (prevRows as any[])[0];
const prevRemaining = Math.max(0, Number(prev.total_days) - Number(prev.used_days));
if (prevRemaining > 0) {
carriedPeriod = {
id: prev.id,
periodStart: prevAnniv,
periodEnd: prevPeriodEnd,
totalDays: Number(prev.total_days),
usedDays: Number(prev.used_days),
remaining: prevRemaining,
isCarryover: true,
status: 'active',
};
}
}
}
}
}
// 如果当前周期为 null,且没有结转周期,则无年假资格
if (!currentPeriod && !carriedPeriod) {
return {
currentPeriod: null,
carriedPeriod: null,
totalDays: 0,
usedDays: 0,
remainingDays: 0,
hasRight: false,
};
}
// 如果当前周期为 null 但有结转,把结转当作 currentPeriod 使用
const effectivePeriod = currentPeriod || carriedPeriod;
const effectiveCarried = currentPeriod ? carriedPeriod : null;
const carriedDays = effectiveCarried?.remaining || 0;
const totalDays = (effectivePeriod?.remaining || 0) + carriedDays;
const usedDays = (effectivePeriod?.usedDays || 0) + (effectiveCarried?.usedDays || 0);
return {
currentPeriod: effectivePeriod,
carriedPeriod: effectiveCarried,
totalDays,
usedDays,
remainingDays: totalDays,
hasRight: true,
};
};
// ==================== 待审批天数查询 ====================
/**
* 获取员工待审批的年假天数
*/
export const getPendingLeaveDays = async (employeeId: number): Promise<number> => {
const [rows] = await pool.execute(
`SELECT COALESCE(SUM(days), 0) as pending FROM annual_leave_records
WHERE employee_id = ? AND status = '待审批'`,
[employeeId]
) as any[];
return Number((rows as any[])[0].pending);
};
// ==================== 工作日计算 ====================
/**
* 根据起始日期和工作日天数,计算实际占用的日历天数
* 排除周末和法定假日
*/
export const calcWorkingDaysSpan = async (
startDate: string,
workDays: number
): Promise<{ endDate: string; calendarDays: number }> => {
const holidaySet = await getHolidaySet();
let accumulated = 0;
const current = new Date(startDate);
while (accumulated < workDays) {
const dateStr = current.toISOString().slice(0, 10);
const dayOfWeek = current.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const isHoliday = holidaySet.has(dateStr);
if (!isWeekend && !isHoliday) {
accumulated++;
}
current.setDate(current.getDate() + 1);
}
// current 已经多加了1天,回退
current.setDate(current.getDate() - 1);
const endDate = current.toISOString().slice(0, 10);
// 计算日历天数
const start = new Date(startDate);
const calendarDays = Math.ceil(
(current.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)
) + 1;
return { endDate, calendarDays };
};
// ==================== 年假周期管理 ====================
/**
* 检查并标记已过期的年假周期
* 在每年6月1日执行:将year_end < now的周期标记为expired
*/
export const expireOldLeavePeriods = async (): Promise<void> => {
try {
const [result] = await pool.execute(
`UPDATE annual_leave_yearly SET status = 'expired'
WHERE year_end < CURDATE() AND status = 'active'`
) as any[];
const count = (result as any).affectedRows || 0;
if (count > 0) {
console.log(`[年假服务] 已标记 ${count} 条过期年假周期`);
}
} catch (error) {
console.error('[年假服务] 标记过期周期失败:', error);
}
};
/**
* 同步员工表年假快照
* 更新 employees 表的 annual_leave_days 和 used_leave_days 快照字段
*/
export const syncEmployeeLeaveSnapshot = async (employeeId?: number): Promise<void> => {
try {
let whereClause = '';
const params: any[] = [];
if (employeeId) {
whereClause = 'WHERE e.id = ?';
params.push(employeeId);
}
const [rows] = await pool.execute(
`SELECT e.id, e.entry_date,
COALESCE(SUM(CASE WHEN aly.status = 'active' THEN aly.total_days ELSE 0 END), 0) as total_days,
COALESCE(SUM(CASE WHEN aly.status = 'active' THEN aly.used_days ELSE 0 END), 0) as used_days
FROM employees e
LEFT JOIN annual_leave_yearly aly ON e.id = aly.employee_id
${whereClause}
GROUP BY e.id, e.entry_date`,
params
) as any[];
for (const row of rows as any[]) {
await pool.execute(
'UPDATE employees SET annual_leave_days = ?, used_leave_days = ? WHERE id = ?',
[row.total_days, row.used_days, row.id]
);
}
if ((rows as any[]).length > 0) {
console.log(`[年假服务] 已同步 ${(rows as any[]).length} 名员工的年假快照`);
}
} catch (error) {
console.error('[年假服务] 同步快照失败:', error);
}
};
import pool from '../config/database';
import ChineseHolidays from 'chinese-holidays';
/**
* 法定假日服务
* - 项目启动时:同步当年+次年假日数据
* - 每年12月31日23:00:自动更新次年假日数据
*/
interface HolidayItem {
date: string;
isHoliday: boolean;
description: string;
}
/**
* 调用 chinese-holidays 库获取指定年份的假日数据
* chinese-holidays 使用方式:
* const book = await ChineseHolidays.ready();
* book.all() 获取所有假日事件
* 每个事件有 days() 返回日期数组,isHoliday() 判断是否休息日
*/
const fetchYearHolidays = async (year: number): Promise<HolidayItem[]> => {
const holidays: HolidayItem[] = [];
try {
const book = await ChineseHolidays.ready();
const allEvents = book.all(); // 获取所有假日事件(不包含调休工作日)
for (const event of allEvents) {
const days: Date[] = event.days();
for (const date of days) {
// days() 返回 Date 对象数组
const dateStr = date.toISOString().slice(0, 10);
// 只取目标年份的数据
if (!dateStr.startsWith(String(year))) continue;
holidays.push({
date: dateStr,
isHoliday: true, // all() 返回的都是休息日
description: event.name || '法定假日',
});
}
}
} catch (error) {
console.error(`[假日服务] 获取${year}年假日数据失败:`, error);
}
return holidays;
};
/**
* 将假日数据写入数据库
*/
const saveHolidays = async (holidays: HolidayItem[]): Promise<void> => {
if (holidays.length === 0) return;
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
for (const h of holidays) {
await conn.execute(
`INSERT INTO holidays (holiday_date, is_holiday, description)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE is_holiday = VALUES(is_holiday), description = VALUES(description)`,
[h.date, h.isHoliday ? 1 : 0, h.description]
);
}
await conn.commit();
console.log(`[假日服务] 已写入 ${holidays.length} 条假日数据`);
} catch (error) {
await conn.rollback();
console.error('[假日服务] 写入假日数据失败:', error);
throw error;
} finally {
conn.release();
}
};
/**
* 更新指定年份的假日数据
*/
const updateYearHolidays = async (year: number): Promise<void> => {
// 先检查该年份是否已有数据
const [rows] = await pool.execute(
'SELECT COUNT(*) as cnt FROM holidays WHERE YEAR(holiday_date) = ?',
[year]
) as any[];
if ((rows as any[])[0].cnt > 0) {
console.log(`[假日服务] ${year}年假日数据已存在,跳过更新`);
return;
}
const holidays = await fetchYearHolidays(year);
if (holidays.length > 0) {
await saveHolidays(holidays);
}
};
/**
* 项目启动时初始化假日数据
*/
export const initHolidayData = async (): Promise<void> => {
console.log('[假日服务] 开始初始化法定假日数据...');
const currentYear = new Date().getFullYear();
await updateYearHolidays(currentYear);
await updateYearHolidays(currentYear + 1);
console.log('[假日服务] 法定假日数据初始化完成');
};
/**
* 获取假日日期集合(用于工作日计算)
* 返回一个 Set,包含所有休息日(法定假日+周末调休休息日)
*/
export const getHolidaySet = async (): Promise<Set<string>> => {
const [rows] = await pool.execute(
'SELECT holiday_date, is_holiday FROM holidays'
) as any[];
const holidaySet = new Set<string>();
for (const row of rows as any[]) {
// is_holiday=1 表示休息日,需要排除
// is_holiday=0 表示调休工作日,是正常上班日
if (row.is_holiday === 1) {
holidaySet.add(row.holiday_date);
}
}
return holidaySet;
};
/**
* 启动假日定时更新任务
* 每年12月31日23:00更新次年假日数据
* 使用 node-cron 调度,避免 setTimeout 32位溢出问题
*/
export const startHolidayScheduler = (): void => {
// 每天检查一次,如果到了12月31日就更新
const checkAndUpdate = async () => {
const now = new Date();
// 12月31日执行更新
if (now.getMonth() === 11 && now.getDate() === 31 && now.getHours() >= 23) {
const nextYear = now.getFullYear() + 1;
console.log(`[假日服务] 开始更新${nextYear}年法定假日数据...`);
await updateYearHolidays(nextYear);
}
};
// 每小时检查一次
setInterval(checkAndUpdate, 60 * 60 * 1000);
// 启动时也检查一次(防止启动时正好是12月31日)
checkAndUpdate();
console.log('[假日服务] 假日定时更新任务已启动(每小时检查一次,12月31日23:00触发更新)');
};
declare module 'chinese-holidays' {
interface DaysEvent {
name: string;
days(): Date[];
isHoliday(): boolean;
isWorkingday(): boolean;
}
class Book {
all(): DaysEvent[];
isHoliday(date: Date | string): boolean;
isWorkingday(date: Date | string): boolean;
events(): DaysEvent[];
}
class ChineseHolidays {
static ready(options?: { offline?: boolean }): Promise<Book>;
}
export default ChineseHolidays;
}
......@@ -28,7 +28,7 @@
<div class="balance-item">
<div class="balance-label">年假剩余</div>
<div class="balance-value annual">{{ balance.remaining_days ?? '-' }} <span></span></div>
<div class="balance-sub">{{ balance.total_days ?? '-' }} 天 · 已用 {{ balance.used_days ?? '-' }} </div>
<div class="balance-sub">当前周期{{ balance.current_period?.remaining ?? '-' }}天 · 结转{{ balance.carried_days ?? 0 }}天 · 已用{{ balance.used_days ?? '-' }}</div>
</div>
<el-divider direction="vertical" style="height: 80px" />
<div class="balance-item">
......
......@@ -9,22 +9,27 @@
<el-card shadow="never" style="margin-bottom:16px">
<div class="annual-balance">
<div class="ab-item">
<div class="ab-val" style="color:#409eff">{{ balance.total_days ?? '-' }}</div>
<div class="ab-label">年假总额(天)</div>
<div class="ab-val" style="color:#409eff">{{ balance.remaining_days ?? '-' }}</div>
<div class="ab-label">可用年假(天)</div>
</div>
<el-divider direction="vertical" style="height:50px" />
<div class="ab-item">
<div class="ab-val" style="color:#f56c6c">{{ balance.used_days ?? '-' }}</div>
<div class="ab-label">已用(天)</div>
<div class="ab-val" style="color:#67c23a">{{ balance.current_period?.remaining ?? '-' }}</div>
<div class="ab-label">当前周期剩余(天)</div>
</div>
<el-divider direction="vertical" style="height:50px" />
<div class="ab-item">
<div class="ab-val" style="color:#67c23a">{{ balance.remaining_days ?? '-' }}</div>
<div class="ab-label">可用余额(天)</div>
<div class="ab-val" style="color:#e6a23c">{{ balance.carried_days ?? '-' }}</div>
<div class="ab-label">结转可用(天)</div>
</div>
<el-divider direction="vertical" style="height:50px" />
<div class="ab-item">
<div class="ab-val" style="color:#f56c6c">{{ balance.used_days ?? '-' }}</div>
<div class="ab-label">已用(天)</div>
</div>
<div class="ab-rule">
<el-icon><InfoFilled /></el-icon>
年假规则:入职满1年起计算,初始3天,每满1年+1天,最多15天
年假规则:满1年享5天,每满1年+1天(上限10天);周末法定假日不计入;未休完可结转至次年5月31日
</div>
</div>
</el-card>
......@@ -52,9 +57,12 @@
<el-card shadow="never">
<el-table :data="list" stripe v-loading="loading">
<el-table-column prop="employee_name" label="员工" width="90" v-if="isProjecter" />
<el-table-column label="请假日期" width="110">
<el-table-column label="开始日期" width="110">
<template #default="{ row }">{{ dayjs(row.leave_date).format('YYYY-MM-DD') }}</template>
</el-table-column>
<el-table-column label="结束日期" width="110">
<template #default="{ row }">{{ row.end_date ? dayjs(row.end_date).format('YYYY-MM-DD') : '-' }}</template>
</el-table-column>
<el-table-column prop="days" label="天数" width="80" />
<el-table-column prop="description" label="说明" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="90">
......@@ -83,18 +91,26 @@
</div>
</el-card>
<el-dialog v-model="dialogVisible" title="申请年假" width="480px" destroy-on-close>
<el-dialog v-model="dialogVisible" title="申请年假" width="500px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="请假日期" prop="leave_date">
<el-form-item label="开始日期" prop="leave_date">
<el-date-picker v-model="form.leave_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="请假天数" prop="days">
<el-input-number v-model="form.days" :min="0.5" :max="balance.remaining_days || 0" :step="0.5" style="width:100%" />
<div style="font-size:12px;color:#909399;margin-top:4px">可用余额:{{ balance.remaining_days ?? '-' }} 天</div>
<el-input-number v-model="form.days" :min="0.5" :max="Math.max(0.5, balance.remaining_days || 0)" :step="0.5" style="width:100%" />
<div style="font-size:12px;color:#909399;margin-top:4px">
可用余额:{{ balance.remaining_days ?? '-' }} 天(当前周期{{ balance.current_period?.remaining ?? 0 }}天 + 结转{{ balance.carried_days ?? 0 }}天)
</div>
</el-form-item>
<el-form-item label="说明">
<el-input v-model="form.description" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="提示" v-if="form.leave_date && form.days >= 0.5">
<div style="font-size:12px;color:#e6a23c">
<el-icon style="vertical-align:middle"><Warning /></el-icon>
请假日期为工作日计算,实际结束日期将自动排除周末和法定假日
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
......@@ -130,7 +146,10 @@ const form = reactive<any>({ leave_date: dayjs().format('YYYY-MM-DD'), days: 1,
const rules = {
leave_date: [{ required: true, message: '请选择日期', trigger: 'change' }],
days: [{ required: true, message: '请输入天数', trigger: 'blur' }]
days: [
{ required: true, message: '请输入天数', trigger: 'blur' },
{ type: 'number', min: 0.5, message: '每次不得少于0.5天', trigger: 'blur' }
]
}
const statusType = (s: string) => ({ '待审批': 'warning', '已批准': 'success', '已拒绝': 'danger' }[s] || '')
......@@ -157,19 +176,24 @@ const openDialog = () => {
}
const handleSave = async () => {
await formRef.value?.validate(async (valid: boolean) => {
if (!valid) return
saving.value = true
try {
await createAnnualLeave(form)
ElMessage.success('申请已提交')
dialogVisible.value = false
loadData()
loadBalance()
} finally {
saving.value = false
}
})
if (!formRef.value) return
try {
await formRef.value.validate()
} catch {
return // 验证不通过
}
saving.value = true
try {
const res: any = await createAnnualLeave(form)
const endDate = res.data?.end_date || ''
const calendarDays = res.data?.calendar_days || 0
ElMessage.success(`申请已提交,预计结束日期:${endDate}(占用${calendarDays}个日历日)`)
dialogVisible.value = false
loadData()
loadBalance()
} finally {
saving.value = false
}
}
const handleApprove = async (row: any, status: string) => {
......@@ -201,8 +225,9 @@ onMounted(async () => {
.annual-balance {
display: flex;
align-items: center;
gap: 32px;
gap: 24px;
padding: 8px 16px;
flex-wrap: wrap;
}
.ab-item {
text-align: center;
......@@ -223,6 +248,8 @@ onMounted(async () => {
font-size: 12px;
color: #909399;
margin-left: auto;
max-width: 320px;
line-height: 1.4;
}
.action-btns { display: flex; align-items: center; gap: 4px; flex-wrap: nowrap; }
</style>
......@@ -42,9 +42,9 @@
<el-table-column label="入职时间" width="110">
<template #default="{ row }">{{ dayjs(row.entry_date).format('YYYY-MM-DD') }}</template>
</el-table-column>
<el-table-column label="年假" width="80">
<el-table-column label="年假(剩余/总额)" width="110">
<template #default="{ row }">
{{ row.annual_leave_days - row.used_leave_days }}/{{ row.annual_leave_days }}
{{ Number(row.annual_leave_days) - Number(row.used_leave_days) }}/{{ row.annual_leave_days }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
......@@ -108,7 +108,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入职时间">
<el-form-item label="入职时间" prop="entry_date">
<el-date-picker v-model="form.entry_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
</el-col>
......@@ -155,7 +155,8 @@ const rules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
email: [{ required: true, type: 'email', message: '请输入有效邮箱', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
entry_date: [{ required: true, message: '请选择入职时间(用于计算年假)', trigger: 'change' }]
}
const roleType = (r: string) => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment