A2/A4/A6 实现员工头像后端、组织架构接口与合同模型
这个提交包含在:
+536
@@ -37,6 +37,7 @@
|
|||||||
"pg-boss": "^10.3.0",
|
"pg-boss": "^10.3.0",
|
||||||
"prisma": "^7.3.0",
|
"prisma": "^7.3.0",
|
||||||
"prom-client": "^15.1.0",
|
"prom-client": "^15.1.0",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"tencentcloud-sdk-nodejs": "^4.1.14",
|
"tencentcloud-sdk-nodejs": "^4.1.14",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
@@ -1316,6 +1317,23 @@
|
|||||||
"@electric-sql/pglite": "0.3.15"
|
"@electric-sql/pglite": "0.3.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
@@ -2160,6 +2178,471 @@
|
|||||||
"hono": "^4"
|
"hono": "^4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/colour": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-ppc64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-riscv64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/balanced-match": {
|
"node_modules/@isaacs/balanced-match": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||||
@@ -6448,6 +6931,15 @@
|
|||||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
@@ -9181,6 +9673,50 @@
|
|||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sharp": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@img/colour": "^1.0.0",
|
||||||
|
"detect-libc": "^2.1.2",
|
||||||
|
"semver": "^7.7.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-darwin-arm64": "0.34.5",
|
||||||
|
"@img/sharp-darwin-x64": "0.34.5",
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||||
|
"@img/sharp-linux-arm": "0.34.5",
|
||||||
|
"@img/sharp-linux-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linux-ppc64": "0.34.5",
|
||||||
|
"@img/sharp-linux-riscv64": "0.34.5",
|
||||||
|
"@img/sharp-linux-s390x": "0.34.5",
|
||||||
|
"@img/sharp-linux-x64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||||
|
"@img/sharp-wasm32": "0.34.5",
|
||||||
|
"@img/sharp-win32-arm64": "0.34.5",
|
||||||
|
"@img/sharp-win32-ia32": "0.34.5",
|
||||||
|
"@img/sharp-win32-x64": "0.34.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"pg-boss": "^10.3.0",
|
"pg-boss": "^10.3.0",
|
||||||
"prisma": "^7.3.0",
|
"prisma": "^7.3.0",
|
||||||
"prom-client": "^15.1.0",
|
"prom-client": "^15.1.0",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"tencentcloud-sdk-nodejs": "^4.1.14",
|
"tencentcloud-sdk-nodejs": "^4.1.14",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "employees" ADD COLUMN "avatar_path" TEXT;
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "EmployeeContractType" AS ENUM ('labor', 'nda', 'non_compete', 'internship');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "employee_contracts" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"employee_id" UUID NOT NULL,
|
||||||
|
"contract_no" TEXT NOT NULL,
|
||||||
|
"contract_type" "EmployeeContractType" NOT NULL,
|
||||||
|
"start_date" DATE NOT NULL,
|
||||||
|
"end_date" DATE NOT NULL,
|
||||||
|
"terminated_date" DATE,
|
||||||
|
"attachment_path" TEXT,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"created_by" UUID,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_by" UUID,
|
||||||
|
|
||||||
|
CONSTRAINT "employee_contracts_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "employee_contracts_employee_contract_no_ux" ON "employee_contracts"("employee_id", "contract_no");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "idx_employee_contracts_employee_start" ON "employee_contracts"("employee_id", "start_date");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "idx_employee_contracts_employee_end" ON "employee_contracts"("employee_id", "end_date");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "employee_contracts" ADD CONSTRAINT "employee_contracts_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employees"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
+91
-59
@@ -343,6 +343,13 @@ enum EmployeePresenceSource {
|
|||||||
admin_override
|
admin_override
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum EmployeeContractType {
|
||||||
|
labor
|
||||||
|
nda
|
||||||
|
non_compete
|
||||||
|
internship
|
||||||
|
}
|
||||||
|
|
||||||
enum WeeklyReportStatus {
|
enum WeeklyReportStatus {
|
||||||
draft
|
draft
|
||||||
submitted
|
submitted
|
||||||
@@ -497,6 +504,7 @@ model Employee {
|
|||||||
emergency_contact_name String?
|
emergency_contact_name String?
|
||||||
emergency_contact_phone String?
|
emergency_contact_phone String?
|
||||||
notes String?
|
notes String?
|
||||||
|
avatar_path String?
|
||||||
|
|
||||||
bank_account_encrypted String
|
bank_account_encrypted String
|
||||||
bank_name String
|
bank_name String
|
||||||
@@ -563,28 +571,29 @@ model Employee {
|
|||||||
workflow_tasks_assigned WorkflowTask[] @relation("WorkflowTaskAssignee")
|
workflow_tasks_assigned WorkflowTask[] @relation("WorkflowTaskAssignee")
|
||||||
workflow_tasks_completed WorkflowTask[] @relation("WorkflowTaskCompleter")
|
workflow_tasks_completed WorkflowTask[] @relation("WorkflowTaskCompleter")
|
||||||
|
|
||||||
approval_transfers_from ApprovalTransfer[] @relation("TransferFrom")
|
approval_transfers_from ApprovalTransfer[] @relation("TransferFrom")
|
||||||
approval_transfers_to ApprovalTransfer[] @relation("TransferTo")
|
approval_transfers_to ApprovalTransfer[] @relation("TransferTo")
|
||||||
password_reset_codes PasswordResetCode[]
|
password_reset_codes PasswordResetCode[]
|
||||||
lifecycle_events EmployeeLifecycleEvent[]
|
lifecycle_events EmployeeLifecycleEvent[]
|
||||||
checklist_tasks EmployeeChecklistTask[]
|
checklist_tasks EmployeeChecklistTask[]
|
||||||
presence_records EmployeePresence[]
|
presence_records EmployeePresence[]
|
||||||
weekly_reports WeeklyReport[]
|
weekly_reports WeeklyReport[]
|
||||||
todo_items_assigned TodoItem[] @relation("TodoAssignee")
|
todo_items_assigned TodoItem[] @relation("TodoAssignee")
|
||||||
todo_items_created TodoItem[] @relation("TodoCreator")
|
todo_items_created TodoItem[] @relation("TodoCreator")
|
||||||
wecom_identity_binding WecomIdentityBinding?
|
wecom_identity_binding WecomIdentityBinding?
|
||||||
owned_projects Project[] @relation("ProjectOwner")
|
owned_projects Project[] @relation("ProjectOwner")
|
||||||
managed_projects Project[] @relation("ProjectManager")
|
managed_projects Project[] @relation("ProjectManager")
|
||||||
project_memberships ProjectMember[]
|
project_memberships ProjectMember[]
|
||||||
owned_project_milestones ProjectMilestone[] @relation("ProjectMilestoneOwner")
|
owned_project_milestones ProjectMilestone[] @relation("ProjectMilestoneOwner")
|
||||||
assigned_project_tasks ProjectTask[] @relation("ProjectTaskAssignee")
|
assigned_project_tasks ProjectTask[] @relation("ProjectTaskAssignee")
|
||||||
created_project_tasks ProjectTask[] @relation("ProjectTaskCreator")
|
created_project_tasks ProjectTask[] @relation("ProjectTaskCreator")
|
||||||
owned_project_risks ProjectRisk[] @relation("ProjectRiskOwner")
|
owned_project_risks ProjectRisk[] @relation("ProjectRiskOwner")
|
||||||
owned_project_delivery_records ProjectDeliveryRecord[] @relation("ProjectDeliveryRecordOwner")
|
owned_project_delivery_records ProjectDeliveryRecord[] @relation("ProjectDeliveryRecordOwner")
|
||||||
created_project_communication_logs ProjectCommunicationLog[] @relation("ProjectCommunicationLogCreator")
|
created_project_communication_logs ProjectCommunicationLog[] @relation("ProjectCommunicationLogCreator")
|
||||||
completed_project_closure_checklists ProjectClosureChecklistItem[] @relation("ProjectClosureChecklistItemCompleter")
|
completed_project_closure_checklists ProjectClosureChecklistItem[] @relation("ProjectClosureChecklistItemCompleter")
|
||||||
owned_contract_registries ContractRegistry[] @relation("ContractRegistryOwner")
|
owned_contract_registries ContractRegistry[] @relation("ContractRegistryOwner")
|
||||||
owned_confidential_document_refs ConfidentialDocumentRef[] @relation("ConfidentialDocumentRefOwner")
|
owned_confidential_document_refs ConfidentialDocumentRef[] @relation("ConfidentialDocumentRefOwner")
|
||||||
|
contracts EmployeeContract[]
|
||||||
|
|
||||||
@@index([department_id])
|
@@index([department_id])
|
||||||
@@index([legal_entity_id], map: "idx_employees_legal_entity")
|
@@index([legal_entity_id], map: "idx_employees_legal_entity")
|
||||||
@@ -1549,30 +1558,30 @@ model EntityRiskFlag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model PaymentBatch {
|
model PaymentBatch {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
batch_no String @unique @db.VarChar(64)
|
batch_no String @unique @db.VarChar(64)
|
||||||
entity_type PaymentBatchEntityType
|
entity_type PaymentBatchEntityType
|
||||||
name String
|
name String
|
||||||
status PaymentBatchStatus @default(draft)
|
status PaymentBatchStatus @default(draft)
|
||||||
default_payment_date DateTime? @db.Date
|
default_payment_date DateTime? @db.Date
|
||||||
default_payment_channel String? @db.VarChar(20)
|
default_payment_channel String? @db.VarChar(20)
|
||||||
voucher_prefix String? @db.VarChar(32)
|
voucher_prefix String? @db.VarChar(32)
|
||||||
review_comment String?
|
review_comment String?
|
||||||
secondary_review_comment String?
|
secondary_review_comment String?
|
||||||
execution_summary_json Json?
|
execution_summary_json Json?
|
||||||
|
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
created_by String? @db.Uuid
|
created_by String? @db.Uuid
|
||||||
reviewed_at DateTime?
|
reviewed_at DateTime?
|
||||||
reviewed_by String? @db.Uuid
|
reviewed_by String? @db.Uuid
|
||||||
secondary_reviewed_at DateTime?
|
secondary_reviewed_at DateTime?
|
||||||
secondary_reviewed_by String? @db.Uuid
|
secondary_reviewed_by String? @db.Uuid
|
||||||
executed_at DateTime?
|
executed_at DateTime?
|
||||||
executed_by String? @db.Uuid
|
executed_by String? @db.Uuid
|
||||||
cancelled_at DateTime?
|
cancelled_at DateTime?
|
||||||
cancelled_by String? @db.Uuid
|
cancelled_by String? @db.Uuid
|
||||||
updated_at DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
updated_by String? @db.Uuid
|
updated_by String? @db.Uuid
|
||||||
|
|
||||||
items PaymentBatchItem[]
|
items PaymentBatchItem[]
|
||||||
|
|
||||||
@@ -1780,15 +1789,15 @@ model Project {
|
|||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @default(now()) @updatedAt
|
updated_at DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
owner Employee @relation("ProjectOwner", fields: [owner_employee_id], references: [id], onDelete: Restrict)
|
owner Employee @relation("ProjectOwner", fields: [owner_employee_id], references: [id], onDelete: Restrict)
|
||||||
manager Employee? @relation("ProjectManager", fields: [manager_employee_id], references: [id], onDelete: SetNull)
|
manager Employee? @relation("ProjectManager", fields: [manager_employee_id], references: [id], onDelete: SetNull)
|
||||||
members ProjectMember[]
|
members ProjectMember[]
|
||||||
milestones ProjectMilestone[]
|
milestones ProjectMilestone[]
|
||||||
tasks ProjectTask[]
|
tasks ProjectTask[]
|
||||||
risks ProjectRisk[]
|
risks ProjectRisk[]
|
||||||
reimbursements ReimbursementForm[]
|
reimbursements ReimbursementForm[]
|
||||||
travel_requests TravelRequest[]
|
travel_requests TravelRequest[]
|
||||||
todo_items TodoItem[]
|
todo_items TodoItem[]
|
||||||
weekly_reports ProjectLinkOnWeeklyReport[]
|
weekly_reports ProjectLinkOnWeeklyReport[]
|
||||||
communication_logs ProjectCommunicationLog[]
|
communication_logs ProjectCommunicationLog[]
|
||||||
closure_checklist_items ProjectClosureChecklistItem[]
|
closure_checklist_items ProjectClosureChecklistItem[]
|
||||||
@@ -1905,16 +1914,16 @@ model ProjectLinkOnWeeklyReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model ProjectCommunicationLog {
|
model ProjectCommunicationLog {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
project_id String @db.Uuid
|
project_id String @db.Uuid
|
||||||
happened_at DateTime
|
happened_at DateTime
|
||||||
channel ProjectCommunicationChannel
|
channel ProjectCommunicationChannel
|
||||||
external_contact_name String?
|
external_contact_name String?
|
||||||
external_org_name String?
|
external_org_name String?
|
||||||
summary String
|
summary String
|
||||||
next_action String?
|
next_action String?
|
||||||
next_follow_up_date DateTime? @db.Date
|
next_follow_up_date DateTime? @db.Date
|
||||||
created_by String @db.Uuid
|
created_by String @db.Uuid
|
||||||
|
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @default(now()) @updatedAt
|
updated_at DateTime @default(now()) @updatedAt
|
||||||
@@ -1929,15 +1938,15 @@ model ProjectCommunicationLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model ProjectClosureChecklistItem {
|
model ProjectClosureChecklistItem {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
project_id String @db.Uuid
|
project_id String @db.Uuid
|
||||||
item_key String @db.VarChar(64)
|
item_key String @db.VarChar(64)
|
||||||
title String
|
title String
|
||||||
status ProjectClosureChecklistStatus @default(pending)
|
status ProjectClosureChecklistStatus @default(pending)
|
||||||
note String?
|
note String?
|
||||||
sort_order Int @default(100)
|
sort_order Int @default(100)
|
||||||
completed_at DateTime?
|
completed_at DateTime?
|
||||||
completed_by String? @db.Uuid
|
completed_by String? @db.Uuid
|
||||||
|
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @default(now()) @updatedAt
|
updated_at DateTime @default(now()) @updatedAt
|
||||||
@@ -1975,6 +1984,29 @@ model ContractRegistry {
|
|||||||
@@map("contract_registry")
|
@@map("contract_registry")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model EmployeeContract {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
employee_id String @db.Uuid
|
||||||
|
contract_no String
|
||||||
|
contract_type EmployeeContractType
|
||||||
|
start_date DateTime @db.Date
|
||||||
|
end_date DateTime @db.Date
|
||||||
|
terminated_date DateTime? @db.Date
|
||||||
|
attachment_path String?
|
||||||
|
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
created_by String? @db.Uuid
|
||||||
|
updated_at DateTime @default(now()) @updatedAt
|
||||||
|
updated_by String? @db.Uuid
|
||||||
|
|
||||||
|
employee Employee @relation(fields: [employee_id], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([employee_id, contract_no], map: "employee_contracts_employee_contract_no_ux")
|
||||||
|
@@index([employee_id, start_date], map: "idx_employee_contracts_employee_start")
|
||||||
|
@@index([employee_id, end_date], map: "idx_employee_contracts_employee_end")
|
||||||
|
@@map("employee_contracts")
|
||||||
|
}
|
||||||
|
|
||||||
model ProjectDeliveryRecord {
|
model ProjectDeliveryRecord {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
project_id String @db.Uuid
|
project_id String @db.Uuid
|
||||||
|
|||||||
+299
-2
@@ -1,14 +1,21 @@
|
|||||||
|
const sharp = require("sharp");
|
||||||
const { z } = require("zod");
|
const { z } = require("zod");
|
||||||
|
|
||||||
const usersRoutes = require("./users");
|
const usersRoutes = require("./users");
|
||||||
|
const { currentDateString } = require("../utils/date");
|
||||||
const { AppError } = require("../utils/AppError");
|
const { AppError } = require("../utils/AppError");
|
||||||
const { sendData } = require("../utils/reply");
|
const { sendData } = require("../utils/reply");
|
||||||
const { buildAuditLogData } = require("../utils/audit");
|
const { buildAuditLogData } = require("../utils/audit");
|
||||||
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
|
||||||
|
const { listOrganizationDepartments } = require("../services/organization");
|
||||||
|
const { StorageNotFoundError, openReadStream, writeBuffer } = require("../services/storage");
|
||||||
|
|
||||||
const UUID_SCHEMA = z.string().uuid();
|
const UUID_SCHEMA = z.string().uuid();
|
||||||
const DATE_SCHEMA = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
const DATE_SCHEMA = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
const REPORTING_CHAIN_MAX_DEPTH = 12;
|
const REPORTING_CHAIN_MAX_DEPTH = 12;
|
||||||
|
const AVATAR_ALLOWED_MIME_TYPES = new Set(["image/jpeg", "image/jpg", "image/png", "image/webp"]);
|
||||||
|
const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
|
||||||
|
const CONTRACT_TYPE_SCHEMA = z.enum(["labor", "nda", "non_compete", "internship"]);
|
||||||
const PROFILE_PATCH_SCHEMA = z
|
const PROFILE_PATCH_SCHEMA = z
|
||||||
.object({
|
.object({
|
||||||
departmentId: UUID_SCHEMA.optional(),
|
departmentId: UUID_SCHEMA.optional(),
|
||||||
@@ -50,6 +57,19 @@ const OFFBOARDING_SCHEMA = z.object({
|
|||||||
notes: z.string().trim().max(500).nullable().optional(),
|
notes: z.string().trim().max(500).nullable().optional(),
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
|
const CONTRACT_CREATE_SCHEMA = z.object({
|
||||||
|
contractNo: z.string().trim().min(1).max(100),
|
||||||
|
contractType: CONTRACT_TYPE_SCHEMA,
|
||||||
|
startDate: DATE_SCHEMA,
|
||||||
|
endDate: DATE_SCHEMA,
|
||||||
|
terminatedDate: DATE_SCHEMA.nullable().optional(),
|
||||||
|
attachmentPath: z.string().trim().max(500).nullable().optional(),
|
||||||
|
}).strict();
|
||||||
|
|
||||||
|
const CONTRACT_PATCH_SCHEMA = CONTRACT_CREATE_SCHEMA.partial()
|
||||||
|
.strict()
|
||||||
|
.refine((value) => Object.keys(value).length > 0, { message: "empty_patch" });
|
||||||
|
|
||||||
function buildValidationError(details, statusCode = 400) {
|
function buildValidationError(details, statusCode = 400) {
|
||||||
return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details });
|
return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details });
|
||||||
}
|
}
|
||||||
@@ -91,10 +111,67 @@ function canManageProfile(fastify, request, employeeId) {
|
|||||||
return request.employee.id === employeeId || fastify.hasPermission(request.employee, "system.users") || fastify.hasPermission(request.employee, "report.dashboard.view");
|
return request.employee.id === employeeId || fastify.hasPermission(request.employee, "system.users") || fastify.hasPermission(request.employee, "report.dashboard.view");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canViewPeopleDirectory(fastify, request) {
|
||||||
|
return fastify.hasPermission(request.employee, "system.users") || fastify.hasPermission(request.employee, "report.dashboard.view");
|
||||||
|
}
|
||||||
|
|
||||||
function canManageLifecycle(fastify, request) {
|
function canManageLifecycle(fastify, request) {
|
||||||
return fastify.hasPermission(request.employee, "system.users") || fastify.hasPermission(request.employee, "system.settings") || fastify.hasPermission(request.employee, "*");
|
return fastify.hasPermission(request.employee, "system.users") || fastify.hasPermission(request.employee, "system.settings") || fastify.hasPermission(request.employee, "*");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canUploadAvatar(fastify, request, employeeId) {
|
||||||
|
return request.employee.id === employeeId || fastify.hasPermission(request.employee, "system.users");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAvatarUrl(employeeId, avatarPath) {
|
||||||
|
return avatarPath ? `/api/avatars/${employeeId}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateOnly(value) {
|
||||||
|
return value ? new Date(`${value}T00:00:00.000Z`) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateContractDates(value) {
|
||||||
|
if (value.startDate && value.endDate && value.startDate > value.endDate) {
|
||||||
|
throw buildValidationError({ endDate: "must_be_after_or_equal_startDate" });
|
||||||
|
}
|
||||||
|
if (value.startDate && value.terminatedDate && value.terminatedDate < value.startDate) {
|
||||||
|
throw buildValidationError({ terminatedDate: "must_be_after_or_equal_startDate" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveEmployeeContractStatus(contract, timeZone) {
|
||||||
|
if (contract.terminated_date) {
|
||||||
|
return "terminated";
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = currentDateString(timeZone ?? "Asia/Shanghai");
|
||||||
|
const endDate = contract.end_date?.toISOString?.()?.slice(0, 10) ?? null;
|
||||||
|
if (endDate && endDate < today) {
|
||||||
|
return "expired";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "active";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapEmployeeContract(contract, timeZone) {
|
||||||
|
return {
|
||||||
|
id: contract.id,
|
||||||
|
employeeId: contract.employee_id,
|
||||||
|
contractNo: contract.contract_no,
|
||||||
|
contractType: contract.contract_type,
|
||||||
|
startDate: contract.start_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||||
|
endDate: contract.end_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||||
|
terminatedDate: contract.terminated_date?.toISOString?.()?.slice(0, 10) ?? null,
|
||||||
|
attachmentPath: contract.attachment_path ?? null,
|
||||||
|
status: deriveEmployeeContractStatus(contract, timeZone),
|
||||||
|
createdAt: contract.created_at?.toISOString?.() ?? null,
|
||||||
|
createdBy: contract.created_by ?? null,
|
||||||
|
updatedAt: contract.updated_at?.toISOString?.() ?? null,
|
||||||
|
updatedBy: contract.updated_by ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function mapChecklistTask(item) {
|
function mapChecklistTask(item) {
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -130,6 +207,7 @@ function mapOrganizationNode(item) {
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
|
avatarUrl: buildAvatarUrl(item.id, item.avatar_path),
|
||||||
department: item.department ? { id: item.department.id, name: item.department.name } : null,
|
department: item.department ? { id: item.department.id, name: item.department.name } : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -154,6 +232,16 @@ async function ensureEmployeeExists(prisma, employeeId) {
|
|||||||
return employee;
|
return employee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureEmployeeContractExists(prisma, employeeId, contractId) {
|
||||||
|
const contract = await prisma.employeeContract.findFirst({
|
||||||
|
where: { id: contractId, employee_id: employeeId },
|
||||||
|
});
|
||||||
|
if (!contract) {
|
||||||
|
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
||||||
|
}
|
||||||
|
return contract;
|
||||||
|
}
|
||||||
|
|
||||||
function buildDefaultChecklist(scene, employeeId, actorId) {
|
function buildDefaultChecklist(scene, employeeId, actorId) {
|
||||||
const common = { employee_id: employeeId };
|
const common = { employee_id: employeeId };
|
||||||
if (scene === "onboarding") {
|
if (scene === "onboarding") {
|
||||||
@@ -231,6 +319,7 @@ async function buildEmployeeOrganization(prisma, employee) {
|
|||||||
name: true,
|
name: true,
|
||||||
title: true,
|
title: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
avatar_path: true,
|
||||||
direct_manager_id: true,
|
direct_manager_id: true,
|
||||||
department: { select: { id: true, name: true } },
|
department: { select: { id: true, name: true } },
|
||||||
},
|
},
|
||||||
@@ -255,6 +344,7 @@ async function buildEmployeeOrganization(prisma, employee) {
|
|||||||
name: true,
|
name: true,
|
||||||
title: true,
|
title: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
avatar_path: true,
|
||||||
department: { select: { id: true, name: true } },
|
department: { select: { id: true, name: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -287,10 +377,11 @@ async function buildEmployeeProfile(prisma, employeeId) {
|
|||||||
emergency_contact_name: true,
|
emergency_contact_name: true,
|
||||||
emergency_contact_phone: true,
|
emergency_contact_phone: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
|
avatar_path: true,
|
||||||
archived_at: true,
|
archived_at: true,
|
||||||
roles: { select: { role_code: true } },
|
roles: { select: { role_code: true } },
|
||||||
department: { select: { id: true, name: true } },
|
department: { select: { id: true, name: true } },
|
||||||
direct_manager: { select: { id: true, employee_no: true, name: true } },
|
direct_manager: { select: { id: true, employee_no: true, name: true, avatar_path: true } },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.employeeChecklistTask.findMany({ where: { employee_id: employeeId }, orderBy: [{ created_at: "desc" }] }),
|
prisma.employeeChecklistTask.findMany({ where: { employee_id: employeeId }, orderBy: [{ created_at: "desc" }] }),
|
||||||
@@ -328,10 +419,18 @@ async function buildEmployeeProfile(prisma, employeeId) {
|
|||||||
emergencyContactName: employee.emergency_contact_name ?? null,
|
emergencyContactName: employee.emergency_contact_name ?? null,
|
||||||
emergencyContactPhone: employee.emergency_contact_phone ?? null,
|
emergencyContactPhone: employee.emergency_contact_phone ?? null,
|
||||||
notes: employee.notes ?? null,
|
notes: employee.notes ?? null,
|
||||||
|
avatarUrl: buildAvatarUrl(employee.id, employee.avatar_path),
|
||||||
archivedAt: employee.archived_at?.toISOString?.() ?? null,
|
archivedAt: employee.archived_at?.toISOString?.() ?? null,
|
||||||
roleCodes: employee.roles.map((item) => item.role_code),
|
roleCodes: employee.roles.map((item) => item.role_code),
|
||||||
department: employee.department ? { id: employee.department.id, name: employee.department.name } : null,
|
department: employee.department ? { id: employee.department.id, name: employee.department.name } : null,
|
||||||
directManager: employee.direct_manager ? { id: employee.direct_manager.id, employeeNo: employee.direct_manager.employee_no, name: employee.direct_manager.name } : null,
|
directManager: employee.direct_manager
|
||||||
|
? {
|
||||||
|
id: employee.direct_manager.id,
|
||||||
|
employeeNo: employee.direct_manager.employee_no,
|
||||||
|
name: employee.direct_manager.name,
|
||||||
|
avatarUrl: buildAvatarUrl(employee.direct_manager.id, employee.direct_manager.avatar_path),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
lifecycle: {
|
lifecycle: {
|
||||||
events: lifecycleEvents.map(mapLifecycleEvent),
|
events: lifecycleEvents.map(mapLifecycleEvent),
|
||||||
@@ -352,6 +451,104 @@ async function buildEmployeeProfile(prisma, employeeId) {
|
|||||||
async function employeesRoutes(fastify) {
|
async function employeesRoutes(fastify) {
|
||||||
await usersRoutes(fastify, { basePath: "/employees", allowDepartmentFilter: true });
|
await usersRoutes(fastify, { basePath: "/employees", allowDepartmentFilter: true });
|
||||||
|
|
||||||
|
fastify.get("/avatars/:employeeId", async (request, reply) => {
|
||||||
|
const employeeId = parseId(request.params?.employeeId);
|
||||||
|
const employee = await fastify.prisma.employee.findUnique({
|
||||||
|
where: { id: employeeId },
|
||||||
|
select: { avatar_path: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!employee?.avatar_path) {
|
||||||
|
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const opened = await openReadStream({
|
||||||
|
config: fastify.config,
|
||||||
|
storagePath: employee.avatar_path,
|
||||||
|
});
|
||||||
|
reply.header("cache-control", "no-store");
|
||||||
|
return reply.type("image/webp").send(opened.stream);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof StorageNotFoundError) {
|
||||||
|
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.put("/employees/:id/avatar", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||||
|
await ensureFeatureEnabled(fastify, request);
|
||||||
|
const employeeId = parseId(request.params?.id);
|
||||||
|
if (!canUploadAvatar(fastify, request, employeeId)) {
|
||||||
|
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "employee.avatar.write" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureEmployeeExists(fastify.prisma, employeeId);
|
||||||
|
if (!request.isMultipart()) {
|
||||||
|
throw buildValidationError({ file: "invalid_multipart" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePart = await request.file();
|
||||||
|
if (!filePart) {
|
||||||
|
throw buildValidationError({ file: "required" });
|
||||||
|
}
|
||||||
|
if (filePart.fieldname !== "file") {
|
||||||
|
throw buildValidationError({ file: "invalid_field" });
|
||||||
|
}
|
||||||
|
if (!AVATAR_ALLOWED_MIME_TYPES.has(filePart.mimetype)) {
|
||||||
|
throw buildValidationError({ file: "invalid_type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawBuffer = await filePart.toBuffer();
|
||||||
|
if (!rawBuffer.length) {
|
||||||
|
throw buildValidationError({ file: "empty" });
|
||||||
|
}
|
||||||
|
if (rawBuffer.length > AVATAR_MAX_BYTES) {
|
||||||
|
throw buildValidationError({ file: "file_too_large" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalizedBuffer;
|
||||||
|
try {
|
||||||
|
normalizedBuffer = await sharp(rawBuffer)
|
||||||
|
.resize(200, 200, { fit: "cover", position: "centre" })
|
||||||
|
.webp({ quality: 90 })
|
||||||
|
.toBuffer();
|
||||||
|
} catch {
|
||||||
|
throw buildValidationError({ file: "invalid_image" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarPath = await writeBuffer({
|
||||||
|
config: fastify.config,
|
||||||
|
key: `avatars/${employeeId}.webp`,
|
||||||
|
buffer: normalizedBuffer,
|
||||||
|
contentType: "image/webp",
|
||||||
|
});
|
||||||
|
|
||||||
|
await fastify.prisma.employee.update({
|
||||||
|
where: { id: employeeId },
|
||||||
|
data: {
|
||||||
|
avatar_path: avatarPath,
|
||||||
|
updated_by: request.employee.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return sendData(reply, request, {
|
||||||
|
employeeId,
|
||||||
|
avatarUrl: buildAvatarUrl(employeeId, avatarPath),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.get("/employees/organization", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||||
|
await ensureFeatureEnabled(fastify, request);
|
||||||
|
if (!canViewPeopleDirectory(fastify, request)) {
|
||||||
|
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "employee.organization.view" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = await listOrganizationDepartments(fastify.prisma);
|
||||||
|
return sendData(reply, request, { items });
|
||||||
|
});
|
||||||
|
|
||||||
fastify.get("/employees/:id/profile", { preHandler: fastify.authenticate }, async (request, reply) => {
|
fastify.get("/employees/:id/profile", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||||
await ensureFeatureEnabled(fastify, request);
|
await ensureFeatureEnabled(fastify, request);
|
||||||
const employeeId = parseId(request.params?.id);
|
const employeeId = parseId(request.params?.id);
|
||||||
@@ -362,6 +559,104 @@ async function employeesRoutes(fastify) {
|
|||||||
return sendData(reply, request, profile);
|
return sendData(reply, request, profile);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fastify.get("/employees/:id/contracts", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||||
|
await ensureFeatureEnabled(fastify, request);
|
||||||
|
if (!fastify.hasPermission(request.employee, "system.users")) {
|
||||||
|
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const employeeId = parseId(request.params?.id);
|
||||||
|
await ensureEmployeeExists(fastify.prisma, employeeId);
|
||||||
|
|
||||||
|
const items = await fastify.prisma.employeeContract.findMany({
|
||||||
|
where: { employee_id: employeeId },
|
||||||
|
orderBy: [{ start_date: "desc" }, { created_at: "desc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
return sendData(reply, request, {
|
||||||
|
items: items.map((item) => mapEmployeeContract(item, fastify.config.appTimezone)),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.post("/employees/:id/contracts", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||||
|
await ensureFeatureEnabled(fastify, request);
|
||||||
|
if (!fastify.hasPermission(request.employee, "system.users")) {
|
||||||
|
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const employeeId = parseId(request.params?.id);
|
||||||
|
const body = parseBody(CONTRACT_CREATE_SCHEMA, request.body);
|
||||||
|
validateContractDates(body);
|
||||||
|
await ensureEmployeeExists(fastify.prisma, employeeId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contract = await fastify.prisma.employeeContract.create({
|
||||||
|
data: {
|
||||||
|
employee_id: employeeId,
|
||||||
|
contract_no: body.contractNo,
|
||||||
|
contract_type: body.contractType,
|
||||||
|
start_date: parseDateOnly(body.startDate),
|
||||||
|
end_date: parseDateOnly(body.endDate),
|
||||||
|
terminated_date: parseDateOnly(body.terminatedDate ?? null),
|
||||||
|
attachment_path: body.attachmentPath ?? null,
|
||||||
|
created_by: request.employee.id,
|
||||||
|
updated_by: request.employee.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return sendData(reply, request, mapEmployeeContract(contract, fastify.config.appTimezone));
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === "P2002") {
|
||||||
|
throw buildValidationError({ contractNo: "duplicate" });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.patch("/employees/:id/contracts/:contractId", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||||
|
await ensureFeatureEnabled(fastify, request);
|
||||||
|
if (!fastify.hasPermission(request.employee, "system.users")) {
|
||||||
|
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const employeeId = parseId(request.params?.id);
|
||||||
|
const contractId = parseId(request.params?.contractId);
|
||||||
|
const body = parseBody(CONTRACT_PATCH_SCHEMA, request.body);
|
||||||
|
await ensureEmployeeExists(fastify.prisma, employeeId);
|
||||||
|
|
||||||
|
const current = await ensureEmployeeContractExists(fastify.prisma, employeeId, contractId);
|
||||||
|
validateContractDates({
|
||||||
|
startDate: body.startDate ?? current.start_date?.toISOString?.()?.slice(0, 10),
|
||||||
|
endDate: body.endDate ?? current.end_date?.toISOString?.()?.slice(0, 10),
|
||||||
|
terminatedDate:
|
||||||
|
body.terminatedDate !== undefined
|
||||||
|
? body.terminatedDate
|
||||||
|
: current.terminated_date?.toISOString?.()?.slice(0, 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contract = await fastify.prisma.employeeContract.update({
|
||||||
|
where: { id: contractId },
|
||||||
|
data: {
|
||||||
|
...(body.contractNo !== undefined ? { contract_no: body.contractNo } : {}),
|
||||||
|
...(body.contractType !== undefined ? { contract_type: body.contractType } : {}),
|
||||||
|
...(body.startDate !== undefined ? { start_date: parseDateOnly(body.startDate) } : {}),
|
||||||
|
...(body.endDate !== undefined ? { end_date: parseDateOnly(body.endDate) } : {}),
|
||||||
|
...(body.terminatedDate !== undefined ? { terminated_date: parseDateOnly(body.terminatedDate ?? null) } : {}),
|
||||||
|
...(body.attachmentPath !== undefined ? { attachment_path: body.attachmentPath ?? null } : {}),
|
||||||
|
updated_by: request.employee.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return sendData(reply, request, mapEmployeeContract(contract, fastify.config.appTimezone));
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === "P2002") {
|
||||||
|
throw buildValidationError({ contractNo: "duplicate" });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
fastify.patch("/employees/:id/profile", { preHandler: fastify.authenticate }, async (request, reply) => {
|
fastify.patch("/employees/:id/profile", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||||
await ensureFeatureEnabled(fastify, request);
|
await ensureFeatureEnabled(fastify, request);
|
||||||
const employeeId = parseId(request.params?.id);
|
const employeeId = parseId(request.params?.id);
|
||||||
@@ -599,5 +894,7 @@ module.exports = employeesRoutes;
|
|||||||
module.exports.__testables = {
|
module.exports.__testables = {
|
||||||
buildEmployeeOrganization,
|
buildEmployeeOrganization,
|
||||||
buildEmployeeProfile,
|
buildEmployeeProfile,
|
||||||
|
deriveEmployeeContractStatus,
|
||||||
|
mapEmployeeContract,
|
||||||
mapOrganizationNode,
|
mapOrganizationNode,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ function mapEmployeeSummary(row) {
|
|||||||
status: row.status,
|
status: row.status,
|
||||||
departmentId: row.department_id,
|
departmentId: row.department_id,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
|
avatarUrl: row.avatar_path ? `/api/avatars/${row.id}` : null,
|
||||||
roleCodes: row.roles.map((item) => item.role_code),
|
roleCodes: row.roles.map((item) => item.role_code),
|
||||||
createdAt: row.created_at?.toISOString?.() ?? null,
|
createdAt: row.created_at?.toISOString?.() ?? null,
|
||||||
updatedAt: row.updated_at?.toISOString?.() ?? null,
|
updatedAt: row.updated_at?.toISOString?.() ?? null,
|
||||||
@@ -785,6 +786,7 @@ async function usersRoutes(fastify, opts = {}) {
|
|||||||
status: true,
|
status: true,
|
||||||
department_id: true,
|
department_id: true,
|
||||||
title: true,
|
title: true,
|
||||||
|
avatar_path: true,
|
||||||
created_at: true,
|
created_at: true,
|
||||||
updated_at: true,
|
updated_at: true,
|
||||||
roles: { select: { role_code: true } },
|
roles: { select: { role_code: true } },
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
function buildAvatarUrl(employeeId, avatarPath) {
|
||||||
|
return avatarPath ? `/api/avatars/${employeeId}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareByEmployeeNo(left, right) {
|
||||||
|
return left.employee_no.localeCompare(right.employee_no, "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDepartmentSummary(department) {
|
||||||
|
if (!department) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: department.id,
|
||||||
|
name: department.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapExternalManager(employee, departmentId, activeEmployeeById) {
|
||||||
|
if (!employee.direct_manager_id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = activeEmployeeById.get(employee.direct_manager_id) ?? null;
|
||||||
|
if (!manager) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager.department_id === departmentId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: manager.id,
|
||||||
|
employeeNo: manager.employee_no,
|
||||||
|
name: manager.name,
|
||||||
|
department: mapDepartmentSummary(manager.department),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOrganizationNode(employee, departmentId, activeEmployeeById, childrenByManagerId, stack) {
|
||||||
|
const nextStack = new Set(stack);
|
||||||
|
nextStack.add(employee.id);
|
||||||
|
|
||||||
|
const children = (childrenByManagerId.get(employee.id) ?? [])
|
||||||
|
.slice()
|
||||||
|
.sort(compareByEmployeeNo)
|
||||||
|
.filter((item) => !nextStack.has(item.id))
|
||||||
|
.map((item) => mapOrganizationNode(item, departmentId, activeEmployeeById, childrenByManagerId, nextStack));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: employee.id,
|
||||||
|
employeeNo: employee.employee_no,
|
||||||
|
name: employee.name,
|
||||||
|
title: employee.title,
|
||||||
|
status: employee.status,
|
||||||
|
avatarUrl: buildAvatarUrl(employee.id, employee.avatar_path),
|
||||||
|
department: mapDepartmentSummary(employee.department),
|
||||||
|
externalManager: mapExternalManager(employee, departmentId, activeEmployeeById),
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDepartmentSection(department, employees, activeEmployeeById) {
|
||||||
|
const departmentId = department.id;
|
||||||
|
const departmentEmployeeById = new Map(employees.map((item) => [item.id, item]));
|
||||||
|
const childrenByManagerId = new Map();
|
||||||
|
|
||||||
|
for (const employee of employees) {
|
||||||
|
childrenByManagerId.set(employee.id, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const employee of employees) {
|
||||||
|
if (!employee.direct_manager_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const manager = activeEmployeeById.get(employee.direct_manager_id);
|
||||||
|
if (!manager || manager.department_id !== departmentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!departmentEmployeeById.has(manager.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
childrenByManagerId.get(manager.id).push(employee);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roots = employees
|
||||||
|
.filter((employee) => {
|
||||||
|
if (!employee.direct_manager_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const manager = activeEmployeeById.get(employee.direct_manager_id);
|
||||||
|
return !manager || manager.department_id !== departmentId;
|
||||||
|
})
|
||||||
|
.slice()
|
||||||
|
.sort(compareByEmployeeNo)
|
||||||
|
.map((employee) => mapOrganizationNode(employee, departmentId, activeEmployeeById, childrenByManagerId, new Set()));
|
||||||
|
|
||||||
|
return {
|
||||||
|
department: mapDepartmentSummary(department),
|
||||||
|
roots,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listOrganizationDepartments(prisma) {
|
||||||
|
const employees = await prisma.employee.findMany({
|
||||||
|
where: { status: "active" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
employee_no: true,
|
||||||
|
name: true,
|
||||||
|
title: true,
|
||||||
|
status: true,
|
||||||
|
avatar_path: true,
|
||||||
|
department_id: true,
|
||||||
|
direct_manager_id: true,
|
||||||
|
department: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeEmployeeById = new Map(employees.map((item) => [item.id, item]));
|
||||||
|
const departmentGroups = new Map();
|
||||||
|
|
||||||
|
for (const employee of employees) {
|
||||||
|
const departmentId = employee.department?.id ?? employee.department_id;
|
||||||
|
if (!departmentId || !employee.department) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!departmentGroups.has(departmentId)) {
|
||||||
|
departmentGroups.set(departmentId, {
|
||||||
|
department: employee.department,
|
||||||
|
employees: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
departmentGroups.get(departmentId).employees.push(employee);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...departmentGroups.values()]
|
||||||
|
.sort((left, right) => left.department.name.localeCompare(right.department.name, "zh-CN"))
|
||||||
|
.map((group) => buildDepartmentSection(group.department, group.employees, activeEmployeeById));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listOrganizationDepartments,
|
||||||
|
__testables: {
|
||||||
|
buildDepartmentSection,
|
||||||
|
mapOrganizationNode,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -108,6 +108,7 @@ function mapPresenceEmployee(row) {
|
|||||||
employeeNo: row.employee_no,
|
employeeNo: row.employee_no,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
|
avatarUrl: row.avatar_path ? `/api/avatars/${row.id}` : null,
|
||||||
employmentStatus: row.employment_status,
|
employmentStatus: row.employment_status,
|
||||||
department: row.department ? { id: row.department.id, name: row.department.name } : null,
|
department: row.department ? { id: row.department.id, name: row.department.name } : null,
|
||||||
};
|
};
|
||||||
@@ -309,6 +310,7 @@ async function listActivePresenceEmployees(prisma, filters = {}) {
|
|||||||
employee_no: true,
|
employee_no: true,
|
||||||
name: true,
|
name: true,
|
||||||
title: true,
|
title: true,
|
||||||
|
avatar_path: true,
|
||||||
employment_status: true,
|
employment_status: true,
|
||||||
department: { select: { id: true, name: true } },
|
department: { select: { id: true, name: true } },
|
||||||
},
|
},
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户