@azlib/cms 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -42
- package/dist/index.cjs +1649 -47
- package/dist/index.d.cts +447 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +447 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1639 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -2857,7 +2857,7 @@ var EcommerceService = class {
|
|
|
2857
2857
|
};
|
|
2858
2858
|
//#endregion
|
|
2859
2859
|
//#region src/plugins/ecommerce/routes.ts
|
|
2860
|
-
function json(data, status = 200) {
|
|
2860
|
+
function json$1(data, status = 200) {
|
|
2861
2861
|
return new Response(JSON.stringify(data), {
|
|
2862
2862
|
status,
|
|
2863
2863
|
headers: {
|
|
@@ -2866,11 +2866,11 @@ function json(data, status = 200) {
|
|
|
2866
2866
|
}
|
|
2867
2867
|
});
|
|
2868
2868
|
}
|
|
2869
|
-
function badRequest(message) {
|
|
2870
|
-
return json({ error: message }, 400);
|
|
2869
|
+
function badRequest$1(message) {
|
|
2870
|
+
return json$1({ error: message }, 400);
|
|
2871
2871
|
}
|
|
2872
|
-
function notFound(message) {
|
|
2873
|
-
return json({ error: message }, 404);
|
|
2872
|
+
function notFound$1(message) {
|
|
2873
|
+
return json$1({ error: message }, 404);
|
|
2874
2874
|
}
|
|
2875
2875
|
function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
2876
2876
|
const prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
|
|
@@ -2893,7 +2893,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2893
2893
|
const offsetStr = url.searchParams.get("offset");
|
|
2894
2894
|
const limit = limitStr ? parseInt(limitStr, 10) : 20;
|
|
2895
2895
|
const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
|
|
2896
|
-
return json(await service.listProducts({
|
|
2896
|
+
return json$1(await service.listProducts({
|
|
2897
2897
|
categorySlug,
|
|
2898
2898
|
categoryId,
|
|
2899
2899
|
tagSlug,
|
|
@@ -2909,7 +2909,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2909
2909
|
offset
|
|
2910
2910
|
}));
|
|
2911
2911
|
} catch (err) {
|
|
2912
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
2912
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2913
2913
|
}
|
|
2914
2914
|
});
|
|
2915
2915
|
ctx.registerRoute("GET", `${prefix}/products/:id`, async (_req, { params, url }) => {
|
|
@@ -2921,32 +2921,32 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2921
2921
|
product = await service.getProduct(id);
|
|
2922
2922
|
if (!product) product = await service.getProductBySlug(id);
|
|
2923
2923
|
}
|
|
2924
|
-
if (!product) return notFound(`Product '${id}' not found`);
|
|
2925
|
-
return json(product);
|
|
2924
|
+
if (!product) return notFound$1(`Product '${id}' not found`);
|
|
2925
|
+
return json$1(product);
|
|
2926
2926
|
});
|
|
2927
2927
|
ctx.registerRoute("POST", `${prefix}/products`, async (req) => {
|
|
2928
2928
|
try {
|
|
2929
2929
|
const body = await req.json();
|
|
2930
|
-
if (!body.title) return badRequest("Product 'title' is required.");
|
|
2931
|
-
if (body.price === void 0 || body.price < 0) return badRequest("Valid product 'price' is required.");
|
|
2932
|
-
return json(await service.createProduct(body), 201);
|
|
2930
|
+
if (!body.title) return badRequest$1("Product 'title' is required.");
|
|
2931
|
+
if (body.price === void 0 || body.price < 0) return badRequest$1("Valid product 'price' is required.");
|
|
2932
|
+
return json$1(await service.createProduct(body), 201);
|
|
2933
2933
|
} catch (err) {
|
|
2934
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
2934
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2935
2935
|
}
|
|
2936
2936
|
});
|
|
2937
2937
|
ctx.registerRoute("PUT", `${prefix}/products/:id`, async (req, { params }) => {
|
|
2938
2938
|
try {
|
|
2939
2939
|
const body = await req.json();
|
|
2940
2940
|
const updated = await service.updateProduct(params.id, body);
|
|
2941
|
-
if (!updated) return notFound(`Product '${params.id}' not found.`);
|
|
2942
|
-
return json(updated);
|
|
2941
|
+
if (!updated) return notFound$1(`Product '${params.id}' not found.`);
|
|
2942
|
+
return json$1(updated);
|
|
2943
2943
|
} catch (err) {
|
|
2944
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
2944
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2945
2945
|
}
|
|
2946
2946
|
});
|
|
2947
2947
|
ctx.registerRoute("DELETE", `${prefix}/products/:id`, async (_req, { params }) => {
|
|
2948
|
-
if (!await service.deleteProduct(params.id)) return notFound(`Product '${params.id}' not found.`);
|
|
2949
|
-
return json({
|
|
2948
|
+
if (!await service.deleteProduct(params.id)) return notFound$1(`Product '${params.id}' not found.`);
|
|
2949
|
+
return json$1({
|
|
2950
2950
|
success: true,
|
|
2951
2951
|
id: params.id
|
|
2952
2952
|
});
|
|
@@ -2954,8 +2954,8 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2954
2954
|
ctx.registerRoute("POST", `${prefix}/products/:id/images`, async (req, { params }) => {
|
|
2955
2955
|
try {
|
|
2956
2956
|
const body = await req.json();
|
|
2957
|
-
if (!body.filename || !body.mimeType) return badRequest("'filename' and 'mimeType' are required.");
|
|
2958
|
-
return json(await service.uploadProductImage(params.id, {
|
|
2957
|
+
if (!body.filename || !body.mimeType) return badRequest$1("'filename' and 'mimeType' are required.");
|
|
2958
|
+
return json$1(await service.uploadProductImage(params.id, {
|
|
2959
2959
|
filename: body.filename,
|
|
2960
2960
|
mimeType: body.mimeType,
|
|
2961
2961
|
sizeBytes: body.sizeBytes ?? 0,
|
|
@@ -2967,68 +2967,68 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
2967
2967
|
isFeatured: body.isFeatured
|
|
2968
2968
|
}), 201);
|
|
2969
2969
|
} catch (err) {
|
|
2970
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
2970
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2971
2971
|
}
|
|
2972
2972
|
});
|
|
2973
2973
|
ctx.registerRoute("GET", `${prefix}/categories`, async (_req, { url }) => {
|
|
2974
2974
|
try {
|
|
2975
|
-
if (url.searchParams.get("tree") === "true") return json(await service.getCategoryTree());
|
|
2975
|
+
if (url.searchParams.get("tree") === "true") return json$1(await service.getCategoryTree());
|
|
2976
2976
|
const parentId = url.searchParams.get("parentId");
|
|
2977
|
-
return json(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
|
|
2977
|
+
return json$1(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
|
|
2978
2978
|
} catch (err) {
|
|
2979
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
2979
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2980
2980
|
}
|
|
2981
2981
|
});
|
|
2982
2982
|
ctx.registerRoute("POST", `${prefix}/categories`, async (req) => {
|
|
2983
2983
|
try {
|
|
2984
2984
|
const body = await req.json();
|
|
2985
|
-
if (!body.name) return badRequest("Category 'name' is required.");
|
|
2986
|
-
return json(await service.createCategory(body), 201);
|
|
2985
|
+
if (!body.name) return badRequest$1("Category 'name' is required.");
|
|
2986
|
+
return json$1(await service.createCategory(body), 201);
|
|
2987
2987
|
} catch (err) {
|
|
2988
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
2988
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
2989
2989
|
}
|
|
2990
2990
|
});
|
|
2991
2991
|
if (options.enableDiscounts !== false) {
|
|
2992
2992
|
ctx.registerRoute("POST", `${prefix}/discounts`, async (req) => {
|
|
2993
2993
|
try {
|
|
2994
2994
|
const body = await req.json();
|
|
2995
|
-
if (!body.title || !body.code) return badRequest("'title' and 'code' are required.");
|
|
2996
|
-
if (body.value === void 0 || body.value < 0) return badRequest("Valid discount 'value' is required.");
|
|
2997
|
-
return json(await service.createDiscount(body), 201);
|
|
2995
|
+
if (!body.title || !body.code) return badRequest$1("'title' and 'code' are required.");
|
|
2996
|
+
if (body.value === void 0 || body.value < 0) return badRequest$1("Valid discount 'value' is required.");
|
|
2997
|
+
return json$1(await service.createDiscount(body), 201);
|
|
2998
2998
|
} catch (err) {
|
|
2999
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
2999
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3000
3000
|
}
|
|
3001
3001
|
});
|
|
3002
3002
|
ctx.registerRoute("POST", `${prefix}/discounts/validate`, async (req) => {
|
|
3003
3003
|
try {
|
|
3004
3004
|
const body = await req.json();
|
|
3005
|
-
if (!body.code) return badRequest("Discount 'code' is required.");
|
|
3005
|
+
if (!body.code) return badRequest$1("Discount 'code' is required.");
|
|
3006
3006
|
const subtotal = Number(body.subtotal ?? 0);
|
|
3007
3007
|
const productIds = Array.isArray(body.productIds) ? body.productIds : [];
|
|
3008
|
-
return json(await service.validateDiscount(body.code, subtotal, productIds));
|
|
3008
|
+
return json$1(await service.validateDiscount(body.code, subtotal, productIds));
|
|
3009
3009
|
} catch (err) {
|
|
3010
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
3010
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3011
3011
|
}
|
|
3012
3012
|
});
|
|
3013
3013
|
}
|
|
3014
3014
|
ctx.registerRoute("POST", `${prefix}/cart/calculate`, async (req) => {
|
|
3015
3015
|
try {
|
|
3016
3016
|
const body = await req.json();
|
|
3017
|
-
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest("'items' array is required and must not be empty.");
|
|
3018
|
-
return json(await service.calculateCart(body));
|
|
3017
|
+
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$1("'items' array is required and must not be empty.");
|
|
3018
|
+
return json$1(await service.calculateCart(body));
|
|
3019
3019
|
} catch (err) {
|
|
3020
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
3020
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3021
3021
|
}
|
|
3022
3022
|
});
|
|
3023
3023
|
if (options.enableOrders !== false) {
|
|
3024
3024
|
ctx.registerRoute("POST", `${prefix}/orders`, async (req) => {
|
|
3025
3025
|
try {
|
|
3026
3026
|
const body = await req.json();
|
|
3027
|
-
if (!body.customerEmail) return badRequest("'customerEmail' is required.");
|
|
3028
|
-
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest("'items' array is required.");
|
|
3029
|
-
return json(await service.createOrder(body), 201);
|
|
3027
|
+
if (!body.customerEmail) return badRequest$1("'customerEmail' is required.");
|
|
3028
|
+
if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$1("'items' array is required.");
|
|
3029
|
+
return json$1(await service.createOrder(body), 201);
|
|
3030
3030
|
} catch (err) {
|
|
3031
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
3031
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3032
3032
|
}
|
|
3033
3033
|
});
|
|
3034
3034
|
ctx.registerRoute("GET", `${prefix}/orders/:id`, async (_req, { params, url }) => {
|
|
@@ -3040,18 +3040,18 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
|
|
|
3040
3040
|
order = await service.getOrder(id);
|
|
3041
3041
|
if (!order) order = await service.getOrderByNumber(id);
|
|
3042
3042
|
}
|
|
3043
|
-
if (!order) return notFound(`Order '${id}' not found.`);
|
|
3044
|
-
return json(order);
|
|
3043
|
+
if (!order) return notFound$1(`Order '${id}' not found.`);
|
|
3044
|
+
return json$1(order);
|
|
3045
3045
|
});
|
|
3046
3046
|
ctx.registerRoute("PATCH", `${prefix}/orders/:id/status`, async (req, { params }) => {
|
|
3047
3047
|
try {
|
|
3048
3048
|
const body = await req.json();
|
|
3049
|
-
if (!body.status) return badRequest("New 'status' is required.");
|
|
3049
|
+
if (!body.status) return badRequest$1("New 'status' is required.");
|
|
3050
3050
|
const updated = await service.updateOrderStatus(params.id, body.status, body.note);
|
|
3051
|
-
if (!updated) return notFound(`Order '${params.id}' not found.`);
|
|
3052
|
-
return json(updated);
|
|
3051
|
+
if (!updated) return notFound$1(`Order '${params.id}' not found.`);
|
|
3052
|
+
return json$1(updated);
|
|
3053
3053
|
} catch (err) {
|
|
3054
|
-
return badRequest(err instanceof Error ? err.message : String(err));
|
|
3054
|
+
return badRequest$1(err instanceof Error ? err.message : String(err));
|
|
3055
3055
|
}
|
|
3056
3056
|
});
|
|
3057
3057
|
}
|
|
@@ -3235,6 +3235,1597 @@ function getEcommerceService(engine, options) {
|
|
|
3235
3235
|
return service;
|
|
3236
3236
|
}
|
|
3237
3237
|
//#endregion
|
|
3238
|
+
//#region src/plugins/hrms/schemas.ts
|
|
3239
|
+
/**
|
|
3240
|
+
* Creates the collection configuration for Employers (Organizations / Companies).
|
|
3241
|
+
*/
|
|
3242
|
+
function createEmployerCollection(options = {}) {
|
|
3243
|
+
return collection({
|
|
3244
|
+
slug: options.employerCollectionSlug ?? "employers",
|
|
3245
|
+
label: "Employers",
|
|
3246
|
+
singularLabel: "Employer",
|
|
3247
|
+
description: "Companies and organizational entities managing employees, shifts, and leave policies",
|
|
3248
|
+
timestamps: true,
|
|
3249
|
+
revisions: true,
|
|
3250
|
+
draftable: true,
|
|
3251
|
+
defaultSort: {
|
|
3252
|
+
field: "createdAt",
|
|
3253
|
+
direction: "desc"
|
|
3254
|
+
},
|
|
3255
|
+
fields: [
|
|
3256
|
+
fields.text({
|
|
3257
|
+
name: "companyName",
|
|
3258
|
+
label: "Company Name",
|
|
3259
|
+
required: true
|
|
3260
|
+
}),
|
|
3261
|
+
fields.slug({
|
|
3262
|
+
from: "companyName",
|
|
3263
|
+
unique: true
|
|
3264
|
+
}),
|
|
3265
|
+
fields.text({
|
|
3266
|
+
name: "legalName",
|
|
3267
|
+
label: "Legal Entity Name"
|
|
3268
|
+
}),
|
|
3269
|
+
fields.text({
|
|
3270
|
+
name: "taxId",
|
|
3271
|
+
label: "Tax / EIN Number"
|
|
3272
|
+
}),
|
|
3273
|
+
fields.text({
|
|
3274
|
+
name: "email",
|
|
3275
|
+
label: "Primary Email"
|
|
3276
|
+
}),
|
|
3277
|
+
fields.text({
|
|
3278
|
+
name: "phone",
|
|
3279
|
+
label: "Phone Number"
|
|
3280
|
+
}),
|
|
3281
|
+
fields.text({
|
|
3282
|
+
name: "website",
|
|
3283
|
+
label: "Website URL"
|
|
3284
|
+
}),
|
|
3285
|
+
fields.image({
|
|
3286
|
+
name: "logo",
|
|
3287
|
+
label: "Company Logo"
|
|
3288
|
+
}),
|
|
3289
|
+
fields.json({
|
|
3290
|
+
name: "address",
|
|
3291
|
+
label: "Company Address"
|
|
3292
|
+
}),
|
|
3293
|
+
fields.text({
|
|
3294
|
+
name: "timezone",
|
|
3295
|
+
label: "Primary Timezone",
|
|
3296
|
+
defaultValue: "UTC"
|
|
3297
|
+
}),
|
|
3298
|
+
fields.json({
|
|
3299
|
+
name: "workSchedule",
|
|
3300
|
+
label: "Standard Work Schedule",
|
|
3301
|
+
defaultValue: {
|
|
3302
|
+
startTime: options.workScheduleStart ?? "09:00",
|
|
3303
|
+
endTime: options.workScheduleEnd ?? "17:00",
|
|
3304
|
+
standardHoursPerDay: options.standardWorkDayHours ?? 8,
|
|
3305
|
+
gracePeriodMinutes: options.gracePeriodMinutes ?? 15,
|
|
3306
|
+
workDays: [
|
|
3307
|
+
1,
|
|
3308
|
+
2,
|
|
3309
|
+
3,
|
|
3310
|
+
4,
|
|
3311
|
+
5
|
|
3312
|
+
]
|
|
3313
|
+
}
|
|
3314
|
+
}),
|
|
3315
|
+
fields.select({
|
|
3316
|
+
name: "status",
|
|
3317
|
+
label: "Status",
|
|
3318
|
+
options: ["active", "inactive"],
|
|
3319
|
+
defaultValue: "active"
|
|
3320
|
+
})
|
|
3321
|
+
]
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Creates the collection configuration for Employees.
|
|
3326
|
+
*/
|
|
3327
|
+
function createEmployeeCollection(options = {}) {
|
|
3328
|
+
const slug = options.employeeCollectionSlug ?? "employees";
|
|
3329
|
+
const employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3330
|
+
return collection({
|
|
3331
|
+
slug,
|
|
3332
|
+
label: "Employees",
|
|
3333
|
+
singularLabel: "Employee",
|
|
3334
|
+
description: "Employee profiles, job assignments, emergency contacts, and attached records",
|
|
3335
|
+
timestamps: true,
|
|
3336
|
+
revisions: true,
|
|
3337
|
+
draftable: true,
|
|
3338
|
+
taxonomies: [options.departmentsTaxonomySlug ?? "hrms_departments", options.designationsTaxonomySlug ?? "hrms_designations"],
|
|
3339
|
+
defaultSort: {
|
|
3340
|
+
field: "createdAt",
|
|
3341
|
+
direction: "desc"
|
|
3342
|
+
},
|
|
3343
|
+
fields: [
|
|
3344
|
+
fields.relationship({
|
|
3345
|
+
name: "employerId",
|
|
3346
|
+
label: "Employer",
|
|
3347
|
+
targetCollection: employerSlug,
|
|
3348
|
+
required: true
|
|
3349
|
+
}),
|
|
3350
|
+
fields.text({
|
|
3351
|
+
name: "userId",
|
|
3352
|
+
label: "Associated User ID",
|
|
3353
|
+
description: "Optional reference to an authenticated user account"
|
|
3354
|
+
}),
|
|
3355
|
+
fields.text({
|
|
3356
|
+
name: "employeeNumber",
|
|
3357
|
+
label: "Employee Number",
|
|
3358
|
+
required: true,
|
|
3359
|
+
unique: true
|
|
3360
|
+
}),
|
|
3361
|
+
fields.text({
|
|
3362
|
+
name: "firstName",
|
|
3363
|
+
label: "First Name",
|
|
3364
|
+
required: true
|
|
3365
|
+
}),
|
|
3366
|
+
fields.text({
|
|
3367
|
+
name: "lastName",
|
|
3368
|
+
label: "Last Name",
|
|
3369
|
+
required: true
|
|
3370
|
+
}),
|
|
3371
|
+
fields.text({
|
|
3372
|
+
name: "email",
|
|
3373
|
+
label: "Work Email",
|
|
3374
|
+
required: true
|
|
3375
|
+
}),
|
|
3376
|
+
fields.text({
|
|
3377
|
+
name: "phone",
|
|
3378
|
+
label: "Contact Phone"
|
|
3379
|
+
}),
|
|
3380
|
+
fields.image({
|
|
3381
|
+
name: "avatar",
|
|
3382
|
+
label: "Profile Picture"
|
|
3383
|
+
}),
|
|
3384
|
+
fields.text({
|
|
3385
|
+
name: "jobTitle",
|
|
3386
|
+
label: "Job Title"
|
|
3387
|
+
}),
|
|
3388
|
+
fields.select({
|
|
3389
|
+
name: "employmentType",
|
|
3390
|
+
label: "Employment Type",
|
|
3391
|
+
options: [
|
|
3392
|
+
"full_time",
|
|
3393
|
+
"part_time",
|
|
3394
|
+
"contractor",
|
|
3395
|
+
"intern"
|
|
3396
|
+
],
|
|
3397
|
+
defaultValue: "full_time"
|
|
3398
|
+
}),
|
|
3399
|
+
fields.select({
|
|
3400
|
+
name: "status",
|
|
3401
|
+
label: "Status",
|
|
3402
|
+
options: [
|
|
3403
|
+
"active",
|
|
3404
|
+
"on_leave",
|
|
3405
|
+
"terminated",
|
|
3406
|
+
"suspended"
|
|
3407
|
+
],
|
|
3408
|
+
defaultValue: "active"
|
|
3409
|
+
}),
|
|
3410
|
+
fields.date({
|
|
3411
|
+
name: "hireDate",
|
|
3412
|
+
label: "Hire Date",
|
|
3413
|
+
required: true
|
|
3414
|
+
}),
|
|
3415
|
+
fields.date({
|
|
3416
|
+
name: "terminationDate",
|
|
3417
|
+
label: "Termination Date"
|
|
3418
|
+
}),
|
|
3419
|
+
fields.relationship({
|
|
3420
|
+
name: "managerId",
|
|
3421
|
+
label: "Reporting Manager",
|
|
3422
|
+
targetCollection: slug
|
|
3423
|
+
}),
|
|
3424
|
+
fields.json({
|
|
3425
|
+
name: "emergencyContact",
|
|
3426
|
+
label: "Emergency Contact"
|
|
3427
|
+
}),
|
|
3428
|
+
fields.repeater({
|
|
3429
|
+
name: "documents",
|
|
3430
|
+
label: "Attached Documents & Contracts",
|
|
3431
|
+
fields: [
|
|
3432
|
+
fields.text({
|
|
3433
|
+
name: "title",
|
|
3434
|
+
label: "Document Title",
|
|
3435
|
+
required: true
|
|
3436
|
+
}),
|
|
3437
|
+
fields.text({
|
|
3438
|
+
name: "fileUrl",
|
|
3439
|
+
label: "File URL",
|
|
3440
|
+
required: true
|
|
3441
|
+
}),
|
|
3442
|
+
fields.text({
|
|
3443
|
+
name: "category",
|
|
3444
|
+
label: "Category"
|
|
3445
|
+
}),
|
|
3446
|
+
fields.text({
|
|
3447
|
+
name: "uploadedAt",
|
|
3448
|
+
label: "Uploaded Date"
|
|
3449
|
+
})
|
|
3450
|
+
]
|
|
3451
|
+
}),
|
|
3452
|
+
fields.number({
|
|
3453
|
+
name: "salary",
|
|
3454
|
+
label: "Base Salary"
|
|
3455
|
+
}),
|
|
3456
|
+
fields.text({
|
|
3457
|
+
name: "notes",
|
|
3458
|
+
label: "Internal Notes"
|
|
3459
|
+
})
|
|
3460
|
+
]
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Creates the collection configuration for Daily Attendance.
|
|
3465
|
+
*/
|
|
3466
|
+
function createAttendanceCollection(options = {}) {
|
|
3467
|
+
const slug = options.attendanceCollectionSlug ?? "attendance";
|
|
3468
|
+
const employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3469
|
+
const employeeSlug = options.employeeCollectionSlug ?? "employees";
|
|
3470
|
+
return collection({
|
|
3471
|
+
slug,
|
|
3472
|
+
label: "Attendance",
|
|
3473
|
+
singularLabel: "Attendance Record",
|
|
3474
|
+
description: "Daily check-in and check-out logs, hours worked, overtime, and punctuality status",
|
|
3475
|
+
timestamps: true,
|
|
3476
|
+
revisions: false,
|
|
3477
|
+
draftable: false,
|
|
3478
|
+
defaultSort: {
|
|
3479
|
+
field: "date",
|
|
3480
|
+
direction: "desc"
|
|
3481
|
+
},
|
|
3482
|
+
fields: [
|
|
3483
|
+
fields.relationship({
|
|
3484
|
+
name: "employerId",
|
|
3485
|
+
label: "Employer",
|
|
3486
|
+
targetCollection: employerSlug,
|
|
3487
|
+
required: true
|
|
3488
|
+
}),
|
|
3489
|
+
fields.relationship({
|
|
3490
|
+
name: "employeeId",
|
|
3491
|
+
label: "Employee",
|
|
3492
|
+
targetCollection: employeeSlug,
|
|
3493
|
+
required: true
|
|
3494
|
+
}),
|
|
3495
|
+
fields.date({
|
|
3496
|
+
name: "date",
|
|
3497
|
+
label: "Date",
|
|
3498
|
+
required: true
|
|
3499
|
+
}),
|
|
3500
|
+
fields.text({
|
|
3501
|
+
name: "checkInAt",
|
|
3502
|
+
label: "Check-In Timestamp",
|
|
3503
|
+
required: true
|
|
3504
|
+
}),
|
|
3505
|
+
fields.text({
|
|
3506
|
+
name: "checkOutAt",
|
|
3507
|
+
label: "Check-Out Timestamp"
|
|
3508
|
+
}),
|
|
3509
|
+
fields.number({
|
|
3510
|
+
name: "totalHours",
|
|
3511
|
+
label: "Total Hours",
|
|
3512
|
+
defaultValue: 0
|
|
3513
|
+
}),
|
|
3514
|
+
fields.number({
|
|
3515
|
+
name: "overtimeHours",
|
|
3516
|
+
label: "Overtime Hours",
|
|
3517
|
+
defaultValue: 0
|
|
3518
|
+
}),
|
|
3519
|
+
fields.select({
|
|
3520
|
+
name: "status",
|
|
3521
|
+
label: "Status",
|
|
3522
|
+
options: [
|
|
3523
|
+
"present",
|
|
3524
|
+
"late",
|
|
3525
|
+
"half_day",
|
|
3526
|
+
"absent",
|
|
3527
|
+
"on_leave"
|
|
3528
|
+
],
|
|
3529
|
+
defaultValue: "present"
|
|
3530
|
+
}),
|
|
3531
|
+
fields.text({
|
|
3532
|
+
name: "location",
|
|
3533
|
+
label: "Location / IP / Geofence"
|
|
3534
|
+
}),
|
|
3535
|
+
fields.text({
|
|
3536
|
+
name: "notes",
|
|
3537
|
+
label: "Notes"
|
|
3538
|
+
})
|
|
3539
|
+
]
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* Creates the collection configuration for Leave Types.
|
|
3544
|
+
*/
|
|
3545
|
+
function createLeaveTypeCollection(options = {}) {
|
|
3546
|
+
const slug = options.leaveTypeCollectionSlug ?? "leave_types";
|
|
3547
|
+
const employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3548
|
+
return collection({
|
|
3549
|
+
slug,
|
|
3550
|
+
label: "Leave Types",
|
|
3551
|
+
singularLabel: "Leave Type",
|
|
3552
|
+
description: "Available leave categories (Annual, Sick, Unpaid) and yearly quotas",
|
|
3553
|
+
timestamps: true,
|
|
3554
|
+
revisions: true,
|
|
3555
|
+
draftable: false,
|
|
3556
|
+
fields: [
|
|
3557
|
+
fields.relationship({
|
|
3558
|
+
name: "employerId",
|
|
3559
|
+
label: "Employer",
|
|
3560
|
+
targetCollection: employerSlug
|
|
3561
|
+
}),
|
|
3562
|
+
fields.text({
|
|
3563
|
+
name: "name",
|
|
3564
|
+
label: "Leave Name",
|
|
3565
|
+
required: true
|
|
3566
|
+
}),
|
|
3567
|
+
fields.text({
|
|
3568
|
+
name: "code",
|
|
3569
|
+
label: "Leave Code",
|
|
3570
|
+
required: true
|
|
3571
|
+
}),
|
|
3572
|
+
fields.number({
|
|
3573
|
+
name: "daysAllowedPerYear",
|
|
3574
|
+
label: "Days Allowed Per Year",
|
|
3575
|
+
required: true,
|
|
3576
|
+
defaultValue: 15,
|
|
3577
|
+
min: 0
|
|
3578
|
+
}),
|
|
3579
|
+
fields.boolean({
|
|
3580
|
+
name: "paid",
|
|
3581
|
+
label: "Paid Leave",
|
|
3582
|
+
defaultValue: true
|
|
3583
|
+
}),
|
|
3584
|
+
fields.boolean({
|
|
3585
|
+
name: "requiresApproval",
|
|
3586
|
+
label: "Requires Manager Approval",
|
|
3587
|
+
defaultValue: true
|
|
3588
|
+
}),
|
|
3589
|
+
fields.text({
|
|
3590
|
+
name: "color",
|
|
3591
|
+
label: "Display Color Code"
|
|
3592
|
+
}),
|
|
3593
|
+
fields.text({
|
|
3594
|
+
name: "description",
|
|
3595
|
+
label: "Description / Policy"
|
|
3596
|
+
})
|
|
3597
|
+
]
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
/**
|
|
3601
|
+
* Creates the collection configuration for Leave Requests.
|
|
3602
|
+
*/
|
|
3603
|
+
function createLeaveRequestCollection(options = {}) {
|
|
3604
|
+
const slug = options.leaveRequestCollectionSlug ?? "leave_requests";
|
|
3605
|
+
const employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3606
|
+
const employeeSlug = options.employeeCollectionSlug ?? "employees";
|
|
3607
|
+
const leaveTypeSlug = options.leaveTypeCollectionSlug ?? "leave_types";
|
|
3608
|
+
return collection({
|
|
3609
|
+
slug,
|
|
3610
|
+
label: "Leave Requests",
|
|
3611
|
+
singularLabel: "Leave Request",
|
|
3612
|
+
description: "Employee time-off requests, approval tracking, and deducted balances",
|
|
3613
|
+
timestamps: true,
|
|
3614
|
+
revisions: true,
|
|
3615
|
+
draftable: false,
|
|
3616
|
+
defaultSort: {
|
|
3617
|
+
field: "createdAt",
|
|
3618
|
+
direction: "desc"
|
|
3619
|
+
},
|
|
3620
|
+
fields: [
|
|
3621
|
+
fields.relationship({
|
|
3622
|
+
name: "employerId",
|
|
3623
|
+
label: "Employer",
|
|
3624
|
+
targetCollection: employerSlug,
|
|
3625
|
+
required: true
|
|
3626
|
+
}),
|
|
3627
|
+
fields.relationship({
|
|
3628
|
+
name: "employeeId",
|
|
3629
|
+
label: "Employee",
|
|
3630
|
+
targetCollection: employeeSlug,
|
|
3631
|
+
required: true
|
|
3632
|
+
}),
|
|
3633
|
+
fields.relationship({
|
|
3634
|
+
name: "leaveTypeId",
|
|
3635
|
+
label: "Leave Type",
|
|
3636
|
+
targetCollection: leaveTypeSlug,
|
|
3637
|
+
required: true
|
|
3638
|
+
}),
|
|
3639
|
+
fields.date({
|
|
3640
|
+
name: "startDate",
|
|
3641
|
+
label: "Start Date",
|
|
3642
|
+
required: true
|
|
3643
|
+
}),
|
|
3644
|
+
fields.date({
|
|
3645
|
+
name: "endDate",
|
|
3646
|
+
label: "End Date",
|
|
3647
|
+
required: true
|
|
3648
|
+
}),
|
|
3649
|
+
fields.number({
|
|
3650
|
+
name: "daysCount",
|
|
3651
|
+
label: "Days Count",
|
|
3652
|
+
required: true,
|
|
3653
|
+
min: .5
|
|
3654
|
+
}),
|
|
3655
|
+
fields.text({
|
|
3656
|
+
name: "reason",
|
|
3657
|
+
label: "Reason"
|
|
3658
|
+
}),
|
|
3659
|
+
fields.select({
|
|
3660
|
+
name: "status",
|
|
3661
|
+
label: "Request Status",
|
|
3662
|
+
options: [
|
|
3663
|
+
"pending",
|
|
3664
|
+
"approved",
|
|
3665
|
+
"rejected",
|
|
3666
|
+
"cancelled"
|
|
3667
|
+
],
|
|
3668
|
+
defaultValue: "pending"
|
|
3669
|
+
}),
|
|
3670
|
+
fields.relationship({
|
|
3671
|
+
name: "approvedBy",
|
|
3672
|
+
label: "Approved / Rejected By",
|
|
3673
|
+
targetCollection: employeeSlug
|
|
3674
|
+
}),
|
|
3675
|
+
fields.text({
|
|
3676
|
+
name: "approvedAt",
|
|
3677
|
+
label: "Decision Timestamp"
|
|
3678
|
+
}),
|
|
3679
|
+
fields.text({
|
|
3680
|
+
name: "rejectionReason",
|
|
3681
|
+
label: "Rejection Reason"
|
|
3682
|
+
})
|
|
3683
|
+
]
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
/**
|
|
3687
|
+
* Creates the standard HRMS taxonomies: departments and designations.
|
|
3688
|
+
*/
|
|
3689
|
+
function createHRMSTaxonomies(options = {}) {
|
|
3690
|
+
const employeeSlug = options.employeeCollectionSlug ?? "employees";
|
|
3691
|
+
const deptSlug = options.departmentsTaxonomySlug ?? "hrms_departments";
|
|
3692
|
+
const desigSlug = options.designationsTaxonomySlug ?? "hrms_designations";
|
|
3693
|
+
return [{
|
|
3694
|
+
slug: deptSlug,
|
|
3695
|
+
label: "Departments",
|
|
3696
|
+
singularLabel: "Department",
|
|
3697
|
+
hierarchical: true,
|
|
3698
|
+
postTypes: [employeeSlug],
|
|
3699
|
+
description: "Hierarchical departmental tree (e.g. Engineering, Sales, HR)"
|
|
3700
|
+
}, {
|
|
3701
|
+
slug: desigSlug,
|
|
3702
|
+
label: "Designations",
|
|
3703
|
+
singularLabel: "Designation",
|
|
3704
|
+
hierarchical: false,
|
|
3705
|
+
postTypes: [employeeSlug],
|
|
3706
|
+
description: "Job titles and designations across the workforce"
|
|
3707
|
+
}];
|
|
3708
|
+
}
|
|
3709
|
+
//#endregion
|
|
3710
|
+
//#region src/plugins/hrms/service.ts
|
|
3711
|
+
var HRMSService = class {
|
|
3712
|
+
engine;
|
|
3713
|
+
options;
|
|
3714
|
+
employerSlug;
|
|
3715
|
+
employeeSlug;
|
|
3716
|
+
attendanceSlug;
|
|
3717
|
+
leaveTypeSlug;
|
|
3718
|
+
leaveRequestSlug;
|
|
3719
|
+
departmentsTaxonomy;
|
|
3720
|
+
designationsTaxonomy;
|
|
3721
|
+
standardWorkDayHours;
|
|
3722
|
+
workScheduleStart;
|
|
3723
|
+
workScheduleEnd;
|
|
3724
|
+
gracePeriodMinutes;
|
|
3725
|
+
constructor(engine, options = {}) {
|
|
3726
|
+
this.engine = engine;
|
|
3727
|
+
this.options = options;
|
|
3728
|
+
this.employerSlug = options.employerCollectionSlug ?? "employers";
|
|
3729
|
+
this.employeeSlug = options.employeeCollectionSlug ?? "employees";
|
|
3730
|
+
this.attendanceSlug = options.attendanceCollectionSlug ?? "attendance";
|
|
3731
|
+
this.leaveTypeSlug = options.leaveTypeCollectionSlug ?? "leave_types";
|
|
3732
|
+
this.leaveRequestSlug = options.leaveRequestCollectionSlug ?? "leave_requests";
|
|
3733
|
+
this.departmentsTaxonomy = options.departmentsTaxonomySlug ?? "hrms_departments";
|
|
3734
|
+
this.designationsTaxonomy = options.designationsTaxonomySlug ?? "hrms_designations";
|
|
3735
|
+
this.standardWorkDayHours = options.standardWorkDayHours ?? 8;
|
|
3736
|
+
this.workScheduleStart = options.workScheduleStart ?? "09:00";
|
|
3737
|
+
this.workScheduleEnd = options.workScheduleEnd ?? "17:00";
|
|
3738
|
+
this.gracePeriodMinutes = options.gracePeriodMinutes ?? 15;
|
|
3739
|
+
}
|
|
3740
|
+
get employersCollection() {
|
|
3741
|
+
return this.engine.collection(this.employerSlug);
|
|
3742
|
+
}
|
|
3743
|
+
get employeesCollection() {
|
|
3744
|
+
return this.engine.collection(this.employeeSlug);
|
|
3745
|
+
}
|
|
3746
|
+
get attendanceCollection() {
|
|
3747
|
+
return this.engine.collection(this.attendanceSlug);
|
|
3748
|
+
}
|
|
3749
|
+
get leaveTypesCollection() {
|
|
3750
|
+
return this.engine.collection(this.leaveTypeSlug);
|
|
3751
|
+
}
|
|
3752
|
+
get leaveRequestsCollection() {
|
|
3753
|
+
return this.engine.collection(this.leaveRequestSlug);
|
|
3754
|
+
}
|
|
3755
|
+
async createEmployer(input, authorId) {
|
|
3756
|
+
const employerData = {
|
|
3757
|
+
companyName: input.companyName,
|
|
3758
|
+
legalName: input.legalName,
|
|
3759
|
+
taxId: input.taxId,
|
|
3760
|
+
email: input.email,
|
|
3761
|
+
phone: input.phone,
|
|
3762
|
+
website: input.website,
|
|
3763
|
+
logo: input.logo,
|
|
3764
|
+
address: input.address,
|
|
3765
|
+
timezone: input.timezone ?? "UTC",
|
|
3766
|
+
workSchedule: input.workSchedule ?? {
|
|
3767
|
+
startTime: this.workScheduleStart,
|
|
3768
|
+
endTime: this.workScheduleEnd,
|
|
3769
|
+
standardHoursPerDay: this.standardWorkDayHours,
|
|
3770
|
+
gracePeriodMinutes: this.gracePeriodMinutes,
|
|
3771
|
+
workDays: [
|
|
3772
|
+
1,
|
|
3773
|
+
2,
|
|
3774
|
+
3,
|
|
3775
|
+
4,
|
|
3776
|
+
5
|
|
3777
|
+
]
|
|
3778
|
+
},
|
|
3779
|
+
status: input.status ?? "active"
|
|
3780
|
+
};
|
|
3781
|
+
const employer = await this.employersCollection.create({
|
|
3782
|
+
title: input.companyName,
|
|
3783
|
+
status: input.status === "inactive" ? "draft" : "published",
|
|
3784
|
+
data: employerData
|
|
3785
|
+
}, authorId);
|
|
3786
|
+
await this.engine.hooks.doAction("hrms.employer_created", employer);
|
|
3787
|
+
return employer;
|
|
3788
|
+
}
|
|
3789
|
+
async getEmployer(id) {
|
|
3790
|
+
return this.employersCollection.findById(id);
|
|
3791
|
+
}
|
|
3792
|
+
async getEmployerBySlug(slug) {
|
|
3793
|
+
return this.employersCollection.findBySlug(slug);
|
|
3794
|
+
}
|
|
3795
|
+
async updateEmployer(id, input, authorId) {
|
|
3796
|
+
const existing = await this.getEmployer(id);
|
|
3797
|
+
if (!existing) return null;
|
|
3798
|
+
const updatedData = {
|
|
3799
|
+
...existing.data,
|
|
3800
|
+
...input.companyName !== void 0 ? { companyName: input.companyName } : {},
|
|
3801
|
+
...input.legalName !== void 0 ? { legalName: input.legalName } : {},
|
|
3802
|
+
...input.taxId !== void 0 ? { taxId: input.taxId } : {},
|
|
3803
|
+
...input.email !== void 0 ? { email: input.email } : {},
|
|
3804
|
+
...input.phone !== void 0 ? { phone: input.phone } : {},
|
|
3805
|
+
...input.website !== void 0 ? { website: input.website } : {},
|
|
3806
|
+
...input.logo !== void 0 ? { logo: input.logo } : {},
|
|
3807
|
+
...input.address !== void 0 ? { address: input.address } : {},
|
|
3808
|
+
...input.timezone !== void 0 ? { timezone: input.timezone } : {},
|
|
3809
|
+
...input.workSchedule !== void 0 ? { workSchedule: input.workSchedule } : {},
|
|
3810
|
+
...input.status !== void 0 ? { status: input.status } : {}
|
|
3811
|
+
};
|
|
3812
|
+
const updated = await this.employersCollection.update(id, {
|
|
3813
|
+
title: input.companyName ?? existing.title,
|
|
3814
|
+
data: updatedData
|
|
3815
|
+
}, authorId);
|
|
3816
|
+
if (updated) await this.engine.hooks.doAction("hrms.employer_updated", updated);
|
|
3817
|
+
return updated;
|
|
3818
|
+
}
|
|
3819
|
+
async listEmployers(query = {}) {
|
|
3820
|
+
const limit = query.limit ?? 20;
|
|
3821
|
+
const offset = query.page ? (query.page - 1) * limit : 0;
|
|
3822
|
+
let items = (await this.employersCollection.find({ limit: 500 })).items;
|
|
3823
|
+
if (query.status) items = items.filter((item) => item.data.status === query.status);
|
|
3824
|
+
const total = items.length;
|
|
3825
|
+
return {
|
|
3826
|
+
items: items.slice(offset, offset + limit),
|
|
3827
|
+
total,
|
|
3828
|
+
limit,
|
|
3829
|
+
offset,
|
|
3830
|
+
hasMore: offset + limit < total
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
async createEmployee(input, authorId) {
|
|
3834
|
+
if (!await this.getEmployer(input.employerId)) throw new Error(`[HRMSService] Employer with ID '${input.employerId}' not found.`);
|
|
3835
|
+
if (await this.getEmployeeByNumber(input.employerId, input.employeeNumber)) throw new Error(`[HRMSService] Employee number '${input.employeeNumber}' is already registered for this employer.`);
|
|
3836
|
+
const employeeData = {
|
|
3837
|
+
employerId: input.employerId,
|
|
3838
|
+
userId: input.userId,
|
|
3839
|
+
employeeNumber: input.employeeNumber,
|
|
3840
|
+
firstName: input.firstName,
|
|
3841
|
+
lastName: input.lastName,
|
|
3842
|
+
email: input.email,
|
|
3843
|
+
phone: input.phone,
|
|
3844
|
+
avatar: input.avatar,
|
|
3845
|
+
jobTitle: input.jobTitle,
|
|
3846
|
+
employmentType: input.employmentType ?? "full_time",
|
|
3847
|
+
status: input.status ?? "active",
|
|
3848
|
+
hireDate: input.hireDate,
|
|
3849
|
+
managerId: input.managerId,
|
|
3850
|
+
emergencyContact: input.emergencyContact,
|
|
3851
|
+
documents: input.documents ?? [],
|
|
3852
|
+
salary: input.salary,
|
|
3853
|
+
notes: input.notes
|
|
3854
|
+
};
|
|
3855
|
+
const title = `${input.firstName} ${input.lastName}`;
|
|
3856
|
+
const employee = await this.employeesCollection.create({
|
|
3857
|
+
title,
|
|
3858
|
+
status: input.status === "terminated" ? "draft" : "published",
|
|
3859
|
+
data: employeeData
|
|
3860
|
+
}, authorId);
|
|
3861
|
+
if (input.departmentSlug) {
|
|
3862
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, input.departmentSlug);
|
|
3863
|
+
if (term) await this.engine.taxonomies.assignTerms(employee.id, [term.id]);
|
|
3864
|
+
}
|
|
3865
|
+
await this.engine.hooks.doAction("hrms.employee_created", employee);
|
|
3866
|
+
return employee;
|
|
3867
|
+
}
|
|
3868
|
+
async getEmployee(id) {
|
|
3869
|
+
return this.employeesCollection.findById(id);
|
|
3870
|
+
}
|
|
3871
|
+
async getEmployeeByNumber(employerId, employeeNumber) {
|
|
3872
|
+
return (await this.employeesCollection.find({ limit: 500 })).items.find((e) => e.data.employerId === employerId && e.data.employeeNumber === employeeNumber) ?? null;
|
|
3873
|
+
}
|
|
3874
|
+
async updateEmployee(id, input, authorId) {
|
|
3875
|
+
const existing = await this.getEmployee(id);
|
|
3876
|
+
if (!existing) return null;
|
|
3877
|
+
const updatedData = {
|
|
3878
|
+
...existing.data,
|
|
3879
|
+
...input.employerId !== void 0 ? { employerId: input.employerId } : {},
|
|
3880
|
+
...input.userId !== void 0 ? { userId: input.userId } : {},
|
|
3881
|
+
...input.employeeNumber !== void 0 ? { employeeNumber: input.employeeNumber } : {},
|
|
3882
|
+
...input.firstName !== void 0 ? { firstName: input.firstName } : {},
|
|
3883
|
+
...input.lastName !== void 0 ? { lastName: input.lastName } : {},
|
|
3884
|
+
...input.email !== void 0 ? { email: input.email } : {},
|
|
3885
|
+
...input.phone !== void 0 ? { phone: input.phone } : {},
|
|
3886
|
+
...input.avatar !== void 0 ? { avatar: input.avatar } : {},
|
|
3887
|
+
...input.jobTitle !== void 0 ? { jobTitle: input.jobTitle } : {},
|
|
3888
|
+
...input.employmentType !== void 0 ? { employmentType: input.employmentType } : {},
|
|
3889
|
+
...input.status !== void 0 ? { status: input.status } : {},
|
|
3890
|
+
...input.hireDate !== void 0 ? { hireDate: input.hireDate } : {},
|
|
3891
|
+
...input.terminationDate !== void 0 ? { terminationDate: input.terminationDate } : {},
|
|
3892
|
+
...input.managerId !== void 0 ? { managerId: input.managerId } : {},
|
|
3893
|
+
...input.emergencyContact !== void 0 ? { emergencyContact: input.emergencyContact } : {},
|
|
3894
|
+
...input.documents !== void 0 ? { documents: input.documents } : {},
|
|
3895
|
+
...input.salary !== void 0 ? { salary: input.salary } : {},
|
|
3896
|
+
...input.notes !== void 0 ? { notes: input.notes } : {}
|
|
3897
|
+
};
|
|
3898
|
+
const title = input.firstName || input.lastName ? `${updatedData.firstName} ${updatedData.lastName}` : existing.title;
|
|
3899
|
+
const updated = await this.employeesCollection.update(id, {
|
|
3900
|
+
title,
|
|
3901
|
+
data: updatedData
|
|
3902
|
+
}, authorId);
|
|
3903
|
+
if (updated) {
|
|
3904
|
+
if (input.departmentSlug) {
|
|
3905
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, input.departmentSlug);
|
|
3906
|
+
if (term) await this.engine.taxonomies.assignTerms(id, [term.id]);
|
|
3907
|
+
}
|
|
3908
|
+
await this.engine.hooks.doAction("hrms.employee_updated", updated);
|
|
3909
|
+
}
|
|
3910
|
+
return updated;
|
|
3911
|
+
}
|
|
3912
|
+
async deleteEmployee(id) {
|
|
3913
|
+
const deleted = await this.employeesCollection.delete(id);
|
|
3914
|
+
if (deleted) await this.engine.hooks.doAction("hrms.employee_deleted", id);
|
|
3915
|
+
return deleted;
|
|
3916
|
+
}
|
|
3917
|
+
async listEmployees(query = {}) {
|
|
3918
|
+
const limit = query.limit ?? 20;
|
|
3919
|
+
const offset = query.page ? (query.page - 1) * limit : 0;
|
|
3920
|
+
let termIds;
|
|
3921
|
+
if (query.department) {
|
|
3922
|
+
const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, query.department);
|
|
3923
|
+
if (term) termIds = [term.id];
|
|
3924
|
+
else return {
|
|
3925
|
+
items: [],
|
|
3926
|
+
total: 0,
|
|
3927
|
+
limit,
|
|
3928
|
+
offset,
|
|
3929
|
+
hasMore: false
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3932
|
+
let items = (await this.employeesCollection.find({
|
|
3933
|
+
termIds,
|
|
3934
|
+
limit: 1e3
|
|
3935
|
+
})).items;
|
|
3936
|
+
if (query.employerId) items = items.filter((e) => e.data.employerId === query.employerId);
|
|
3937
|
+
if (query.employmentType) items = items.filter((e) => e.data.employmentType === query.employmentType);
|
|
3938
|
+
if (query.status) items = items.filter((e) => e.data.status === query.status);
|
|
3939
|
+
if (query.search) {
|
|
3940
|
+
const s = query.search.toLowerCase();
|
|
3941
|
+
items = items.filter((e) => e.data.firstName.toLowerCase().includes(s) || e.data.lastName.toLowerCase().includes(s) || e.data.email.toLowerCase().includes(s) || e.data.employeeNumber.toLowerCase().includes(s) || e.data.jobTitle && e.data.jobTitle.toLowerCase().includes(s));
|
|
3942
|
+
}
|
|
3943
|
+
const total = items.length;
|
|
3944
|
+
return {
|
|
3945
|
+
items: items.slice(offset, offset + limit),
|
|
3946
|
+
total,
|
|
3947
|
+
limit,
|
|
3948
|
+
offset,
|
|
3949
|
+
hasMore: offset + limit < total
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
async getDirectReports(managerId) {
|
|
3953
|
+
return (await this.employeesCollection.find({ limit: 1e3 })).items.filter((e) => e.data.managerId === managerId);
|
|
3954
|
+
}
|
|
3955
|
+
parseTimeToMinutes(timeStr) {
|
|
3956
|
+
const [hours, minutes] = timeStr.split(":").map((v) => parseInt(v, 10));
|
|
3957
|
+
return (hours || 0) * 60 + (minutes || 0);
|
|
3958
|
+
}
|
|
3959
|
+
formatDateString(date, timezone) {
|
|
3960
|
+
if (!timezone || timezone === "UTC") return date.toISOString().slice(0, 10);
|
|
3961
|
+
try {
|
|
3962
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
3963
|
+
timeZone: timezone,
|
|
3964
|
+
year: "numeric",
|
|
3965
|
+
month: "2-digit",
|
|
3966
|
+
day: "2-digit"
|
|
3967
|
+
}).format(date);
|
|
3968
|
+
} catch {
|
|
3969
|
+
return date.toISOString().slice(0, 10);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
getHoursAndMinutes(date, timezone) {
|
|
3973
|
+
if (!timezone || timezone === "UTC") return {
|
|
3974
|
+
hours: date.getUTCHours(),
|
|
3975
|
+
minutes: date.getUTCMinutes()
|
|
3976
|
+
};
|
|
3977
|
+
try {
|
|
3978
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
3979
|
+
timeZone: timezone,
|
|
3980
|
+
hour: "numeric",
|
|
3981
|
+
minute: "numeric",
|
|
3982
|
+
hour12: false
|
|
3983
|
+
}).formatToParts(date);
|
|
3984
|
+
const hoursPart = parts.find((p) => p.type === "hour");
|
|
3985
|
+
const minutesPart = parts.find((p) => p.type === "minute");
|
|
3986
|
+
return {
|
|
3987
|
+
hours: hoursPart ? parseInt(hoursPart.value, 10) : date.getUTCHours(),
|
|
3988
|
+
minutes: minutesPart ? parseInt(minutesPart.value, 10) : date.getUTCMinutes()
|
|
3989
|
+
};
|
|
3990
|
+
} catch {
|
|
3991
|
+
return {
|
|
3992
|
+
hours: date.getUTCHours(),
|
|
3993
|
+
minutes: date.getUTCMinutes()
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
/**
|
|
3998
|
+
* Check in an employee for today (or specified timestamp).
|
|
3999
|
+
*/
|
|
4000
|
+
async checkIn(input) {
|
|
4001
|
+
const employee = await this.getEmployee(input.employeeId);
|
|
4002
|
+
if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
|
|
4003
|
+
const employer = await this.getEmployer(employee.data.employerId);
|
|
4004
|
+
const timezone = employer?.data?.timezone ?? "UTC";
|
|
4005
|
+
const checkInDate = input.timestamp ? new Date(input.timestamp) : /* @__PURE__ */ new Date();
|
|
4006
|
+
const dateStr = this.formatDateString(checkInDate, timezone);
|
|
4007
|
+
const existing = await this.getDailyAttendance(employee.id, dateStr);
|
|
4008
|
+
if (existing && existing.data.checkInAt) throw new Error(`[HRMSService] Employee '${employee.id}' is already checked in for date ${dateStr}.`);
|
|
4009
|
+
const schedule = employer?.data?.workSchedule;
|
|
4010
|
+
const schedStart = schedule?.startTime ?? this.workScheduleStart;
|
|
4011
|
+
const grace = schedule?.gracePeriodMinutes ?? this.gracePeriodMinutes;
|
|
4012
|
+
const schedMinutes = this.parseTimeToMinutes(schedStart);
|
|
4013
|
+
const { hours: punchHours, minutes: punchMins } = this.getHoursAndMinutes(checkInDate, timezone);
|
|
4014
|
+
const status = punchHours * 60 + punchMins > schedMinutes + grace ? "late" : "present";
|
|
4015
|
+
const attendanceData = {
|
|
4016
|
+
employerId: employee.data.employerId,
|
|
4017
|
+
employeeId: employee.id,
|
|
4018
|
+
date: dateStr,
|
|
4019
|
+
checkInAt: checkInDate.toISOString(),
|
|
4020
|
+
totalHours: 0,
|
|
4021
|
+
overtimeHours: 0,
|
|
4022
|
+
status,
|
|
4023
|
+
location: input.location,
|
|
4024
|
+
notes: input.notes
|
|
4025
|
+
};
|
|
4026
|
+
const attendance = await this.attendanceCollection.create({
|
|
4027
|
+
title: `${employee.title} - ${dateStr}`,
|
|
4028
|
+
status: "published",
|
|
4029
|
+
data: attendanceData
|
|
4030
|
+
});
|
|
4031
|
+
await this.engine.hooks.doAction("hrms.checked_in", attendance, employee);
|
|
4032
|
+
return attendance;
|
|
4033
|
+
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Check out an employee for today (or specified timestamp).
|
|
4036
|
+
*/
|
|
4037
|
+
async checkOut(input) {
|
|
4038
|
+
const employee = await this.getEmployee(input.employeeId);
|
|
4039
|
+
if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
|
|
4040
|
+
const employer = await this.getEmployer(employee.data.employerId);
|
|
4041
|
+
const timezone = employer?.data?.timezone ?? "UTC";
|
|
4042
|
+
const checkOutDate = input.timestamp ? new Date(input.timestamp) : /* @__PURE__ */ new Date();
|
|
4043
|
+
const dateStr = this.formatDateString(checkOutDate, timezone);
|
|
4044
|
+
const record = await this.getDailyAttendance(employee.id, dateStr);
|
|
4045
|
+
if (!record || !record.data.checkInAt) throw new Error(`[HRMSService] No active check-in record found for employee '${employee.id}' on ${dateStr}.`);
|
|
4046
|
+
if (record.data.checkOutAt) throw new Error(`[HRMSService] Employee '${employee.id}' has already checked out for date ${dateStr}.`);
|
|
4047
|
+
const checkInDate = new Date(record.data.checkInAt);
|
|
4048
|
+
const durationMs = Math.max(0, checkOutDate.getTime() - checkInDate.getTime());
|
|
4049
|
+
const totalHours = Math.round(durationMs / (1e3 * 60 * 60) * 100) / 100;
|
|
4050
|
+
const standardHours = employer?.data?.workSchedule?.standardHoursPerDay ?? this.standardWorkDayHours;
|
|
4051
|
+
const overtimeHours = Math.max(0, Math.round((totalHours - standardHours) * 100) / 100);
|
|
4052
|
+
let status = record.data.status;
|
|
4053
|
+
if (totalHours < standardHours / 2 && status === "present") status = "half_day";
|
|
4054
|
+
const updatedData = {
|
|
4055
|
+
...record.data,
|
|
4056
|
+
checkOutAt: checkOutDate.toISOString(),
|
|
4057
|
+
totalHours,
|
|
4058
|
+
overtimeHours,
|
|
4059
|
+
status,
|
|
4060
|
+
...input.location ? { location: input.location } : {},
|
|
4061
|
+
...input.notes ? { notes: input.notes } : {}
|
|
4062
|
+
};
|
|
4063
|
+
const updated = await this.attendanceCollection.update(record.id, { data: updatedData });
|
|
4064
|
+
if (!updated) throw new Error(`[HRMSService] Failed to update attendance record for '${employee.id}'.`);
|
|
4065
|
+
await this.engine.hooks.doAction("hrms.checked_out", updated, employee);
|
|
4066
|
+
return updated;
|
|
4067
|
+
}
|
|
4068
|
+
async getDailyAttendance(employeeId, date) {
|
|
4069
|
+
return (await this.attendanceCollection.find({ limit: 1e3 })).items.find((a) => a.data.employeeId === employeeId && a.data.date === date) ?? null;
|
|
4070
|
+
}
|
|
4071
|
+
async recordAttendanceManual(input) {
|
|
4072
|
+
const existing = await this.getDailyAttendance(input.employeeId, input.date);
|
|
4073
|
+
const attendanceData = {
|
|
4074
|
+
employerId: input.employerId,
|
|
4075
|
+
employeeId: input.employeeId,
|
|
4076
|
+
date: input.date,
|
|
4077
|
+
checkInAt: input.checkInAt,
|
|
4078
|
+
checkOutAt: input.checkOutAt,
|
|
4079
|
+
totalHours: input.totalHours ?? 0,
|
|
4080
|
+
overtimeHours: input.overtimeHours ?? 0,
|
|
4081
|
+
status: input.status ?? "present",
|
|
4082
|
+
location: input.location,
|
|
4083
|
+
notes: input.notes
|
|
4084
|
+
};
|
|
4085
|
+
if (existing) {
|
|
4086
|
+
const updated = await this.attendanceCollection.update(existing.id, { data: attendanceData });
|
|
4087
|
+
if (!updated) throw new Error(`[HRMSService] Failed to update attendance for '${input.employeeId}'.`);
|
|
4088
|
+
return updated;
|
|
4089
|
+
}
|
|
4090
|
+
return this.attendanceCollection.create({
|
|
4091
|
+
title: `${input.employeeId} - ${input.date}`,
|
|
4092
|
+
status: "published",
|
|
4093
|
+
data: attendanceData
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4096
|
+
async listAttendance(query = {}) {
|
|
4097
|
+
const limit = query.limit ?? 30;
|
|
4098
|
+
const offset = query.page ? (query.page - 1) * limit : 0;
|
|
4099
|
+
let items = (await this.attendanceCollection.find({ limit: 2e3 })).items;
|
|
4100
|
+
if (query.employerId) items = items.filter((a) => a.data.employerId === query.employerId);
|
|
4101
|
+
if (query.employeeId) items = items.filter((a) => a.data.employeeId === query.employeeId);
|
|
4102
|
+
if (query.date) items = items.filter((a) => a.data.date === query.date);
|
|
4103
|
+
if (query.startDate) items = items.filter((a) => a.data.date >= query.startDate);
|
|
4104
|
+
if (query.endDate) items = items.filter((a) => a.data.date <= query.endDate);
|
|
4105
|
+
if (query.status) items = items.filter((a) => a.data.status === query.status);
|
|
4106
|
+
const total = items.length;
|
|
4107
|
+
return {
|
|
4108
|
+
items: items.slice(offset, offset + limit),
|
|
4109
|
+
total,
|
|
4110
|
+
limit,
|
|
4111
|
+
offset,
|
|
4112
|
+
hasMore: offset + limit < total
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
async createLeaveType(input, authorId) {
|
|
4116
|
+
const leaveTypeData = {
|
|
4117
|
+
employerId: input.employerId,
|
|
4118
|
+
name: input.name,
|
|
4119
|
+
code: input.code.toUpperCase(),
|
|
4120
|
+
daysAllowedPerYear: input.daysAllowedPerYear,
|
|
4121
|
+
paid: input.paid ?? true,
|
|
4122
|
+
requiresApproval: input.requiresApproval ?? true,
|
|
4123
|
+
color: input.color,
|
|
4124
|
+
description: input.description
|
|
4125
|
+
};
|
|
4126
|
+
const item = await this.leaveTypesCollection.create({
|
|
4127
|
+
title: input.name,
|
|
4128
|
+
status: "published",
|
|
4129
|
+
data: leaveTypeData
|
|
4130
|
+
}, authorId);
|
|
4131
|
+
await this.engine.hooks.doAction("hrms.leave_type_created", item);
|
|
4132
|
+
return item;
|
|
4133
|
+
}
|
|
4134
|
+
async getLeaveType(id) {
|
|
4135
|
+
return this.leaveTypesCollection.findById(id);
|
|
4136
|
+
}
|
|
4137
|
+
async listLeaveTypes(employerId) {
|
|
4138
|
+
const result = await this.leaveTypesCollection.find({ limit: 100 });
|
|
4139
|
+
if (!employerId) return result.items;
|
|
4140
|
+
return result.items.filter((lt) => !lt.data.employerId || lt.data.employerId === employerId);
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* Calculate leave balance report for an employee for a given year.
|
|
4144
|
+
*/
|
|
4145
|
+
async calculateLeaveBalance(employeeId, year) {
|
|
4146
|
+
const targetYear = year ?? (/* @__PURE__ */ new Date()).getFullYear();
|
|
4147
|
+
const employee = await this.getEmployee(employeeId);
|
|
4148
|
+
if (!employee) throw new Error(`[HRMSService] Employee with ID '${employeeId}' not found.`);
|
|
4149
|
+
const leaveTypes = await this.listLeaveTypes(employee.data.employerId);
|
|
4150
|
+
const allRequests = await this.leaveRequestsCollection.find({ limit: 1e3 });
|
|
4151
|
+
const yearStr = String(targetYear);
|
|
4152
|
+
const employeeRequests = allRequests.items.filter((r) => r.data.employeeId === employeeId && (r.data.startDate.startsWith(yearStr) || r.data.endDate.startsWith(yearStr)));
|
|
4153
|
+
const balances = leaveTypes.map((lt) => {
|
|
4154
|
+
const approved = employeeRequests.filter((r) => r.data.leaveTypeId === lt.id && r.data.status === "approved");
|
|
4155
|
+
const pending = employeeRequests.filter((r) => r.data.leaveTypeId === lt.id && r.data.status === "pending");
|
|
4156
|
+
const usedDays = approved.reduce((sum, r) => sum + (r.data.daysCount || 0), 0);
|
|
4157
|
+
const pendingDays = pending.reduce((sum, r) => sum + (r.data.daysCount || 0), 0);
|
|
4158
|
+
const allocatedDays = lt.data.daysAllowedPerYear ?? 0;
|
|
4159
|
+
const remainingDays = Math.max(0, allocatedDays - usedDays);
|
|
4160
|
+
return {
|
|
4161
|
+
leaveTypeId: lt.id,
|
|
4162
|
+
leaveTypeName: lt.data.name,
|
|
4163
|
+
leaveTypeCode: lt.data.code,
|
|
4164
|
+
allocatedDays,
|
|
4165
|
+
usedDays,
|
|
4166
|
+
pendingDays,
|
|
4167
|
+
remainingDays
|
|
4168
|
+
};
|
|
4169
|
+
});
|
|
4170
|
+
const report = {
|
|
4171
|
+
employeeId,
|
|
4172
|
+
year: targetYear,
|
|
4173
|
+
balances,
|
|
4174
|
+
totalAllocated: balances.reduce((sum, b) => sum + b.allocatedDays, 0),
|
|
4175
|
+
totalUsed: balances.reduce((sum, b) => sum + b.usedDays, 0),
|
|
4176
|
+
totalRemaining: balances.reduce((sum, b) => sum + b.remainingDays, 0)
|
|
4177
|
+
};
|
|
4178
|
+
return this.engine.hooks.applyFilters("hrms.calculate_leave_balance", report, employeeId, targetYear);
|
|
4179
|
+
}
|
|
4180
|
+
/**
|
|
4181
|
+
* Submit a new leave request.
|
|
4182
|
+
*/
|
|
4183
|
+
async requestLeave(input, authorId) {
|
|
4184
|
+
const employee = await this.getEmployee(input.employeeId);
|
|
4185
|
+
if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
|
|
4186
|
+
const leaveType = await this.getLeaveType(input.leaveTypeId);
|
|
4187
|
+
if (!leaveType) throw new Error(`[HRMSService] Leave type with ID '${input.leaveTypeId}' not found.`);
|
|
4188
|
+
if (input.startDate > input.endDate) throw new Error(`[HRMSService] Start date '${input.startDate}' cannot be after end date '${input.endDate}'.`);
|
|
4189
|
+
const start = new Date(input.startDate);
|
|
4190
|
+
const end = new Date(input.endDate);
|
|
4191
|
+
const calculatedDays = Math.max(1, Math.round((end.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24)) + 1);
|
|
4192
|
+
const daysCount = input.daysCount ?? calculatedDays;
|
|
4193
|
+
const startYear = start.getFullYear();
|
|
4194
|
+
const balance = (await this.calculateLeaveBalance(employee.id, startYear)).balances.find((b) => b.leaveTypeId === leaveType.id);
|
|
4195
|
+
if (balance && leaveType.data.requiresApproval && daysCount > balance.remainingDays) throw new Error(`[HRMSService] Insufficient leave balance for ${leaveType.data.name}. Requested: ${daysCount}, Remaining: ${balance.remainingDays}.`);
|
|
4196
|
+
const leaveRequestData = {
|
|
4197
|
+
employerId: input.employerId ?? employee.data.employerId,
|
|
4198
|
+
employeeId: employee.id,
|
|
4199
|
+
leaveTypeId: leaveType.id,
|
|
4200
|
+
startDate: input.startDate,
|
|
4201
|
+
endDate: input.endDate,
|
|
4202
|
+
daysCount,
|
|
4203
|
+
reason: input.reason,
|
|
4204
|
+
status: "pending"
|
|
4205
|
+
};
|
|
4206
|
+
const item = await this.leaveRequestsCollection.create({
|
|
4207
|
+
title: `${employee.title} - ${leaveType.data.name} (${input.startDate})`,
|
|
4208
|
+
status: "published",
|
|
4209
|
+
data: leaveRequestData
|
|
4210
|
+
}, authorId);
|
|
4211
|
+
await this.engine.hooks.doAction("hrms.leave_requested", item, employee);
|
|
4212
|
+
return item;
|
|
4213
|
+
}
|
|
4214
|
+
/**
|
|
4215
|
+
* Approve a pending leave request.
|
|
4216
|
+
*/
|
|
4217
|
+
async approveLeave(input) {
|
|
4218
|
+
const request = await this.leaveRequestsCollection.findById(input.requestId);
|
|
4219
|
+
if (!request) throw new Error(`[HRMSService] Leave request with ID '${input.requestId}' not found.`);
|
|
4220
|
+
if (request.data.status !== "pending") throw new Error(`[HRMSService] Cannot approve leave request with status '${request.data.status}'.`);
|
|
4221
|
+
const updated = await this.leaveRequestsCollection.update(request.id, { data: {
|
|
4222
|
+
...request.data,
|
|
4223
|
+
status: "approved",
|
|
4224
|
+
approvedBy: input.approverId,
|
|
4225
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4226
|
+
} });
|
|
4227
|
+
if (!updated) throw new Error(`[HRMSService] Failed to update leave request.`);
|
|
4228
|
+
await this.engine.hooks.doAction("hrms.leave_approved", updated);
|
|
4229
|
+
return updated;
|
|
4230
|
+
}
|
|
4231
|
+
/**
|
|
4232
|
+
* Reject a pending leave request.
|
|
4233
|
+
*/
|
|
4234
|
+
async rejectLeave(input) {
|
|
4235
|
+
const request = await this.leaveRequestsCollection.findById(input.requestId);
|
|
4236
|
+
if (!request) throw new Error(`[HRMSService] Leave request with ID '${input.requestId}' not found.`);
|
|
4237
|
+
if (request.data.status !== "pending") throw new Error(`[HRMSService] Cannot reject leave request with status '${request.data.status}'.`);
|
|
4238
|
+
const updated = await this.leaveRequestsCollection.update(request.id, { data: {
|
|
4239
|
+
...request.data,
|
|
4240
|
+
status: "rejected",
|
|
4241
|
+
approvedBy: input.approverId,
|
|
4242
|
+
rejectionReason: input.reason,
|
|
4243
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4244
|
+
} });
|
|
4245
|
+
if (!updated) throw new Error(`[HRMSService] Failed to update leave request.`);
|
|
4246
|
+
await this.engine.hooks.doAction("hrms.leave_rejected", updated);
|
|
4247
|
+
return updated;
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Cancel a leave request.
|
|
4251
|
+
*/
|
|
4252
|
+
async cancelLeave(requestId) {
|
|
4253
|
+
const request = await this.leaveRequestsCollection.findById(requestId);
|
|
4254
|
+
if (!request) throw new Error(`[HRMSService] Leave request with ID '${requestId}' not found.`);
|
|
4255
|
+
if (request.data.status === "cancelled") return request;
|
|
4256
|
+
const updated = await this.leaveRequestsCollection.update(request.id, { data: {
|
|
4257
|
+
...request.data,
|
|
4258
|
+
status: "cancelled"
|
|
4259
|
+
} });
|
|
4260
|
+
if (!updated) throw new Error(`[HRMSService] Failed to cancel leave request.`);
|
|
4261
|
+
await this.engine.hooks.doAction("hrms.leave_cancelled", updated);
|
|
4262
|
+
return updated;
|
|
4263
|
+
}
|
|
4264
|
+
async listLeaveRequests(query = {}) {
|
|
4265
|
+
const limit = query.limit ?? 20;
|
|
4266
|
+
const offset = query.page ? (query.page - 1) * limit : 0;
|
|
4267
|
+
let items = (await this.leaveRequestsCollection.find({ limit: 1e3 })).items;
|
|
4268
|
+
if (query.employerId) items = items.filter((r) => r.data.employerId === query.employerId);
|
|
4269
|
+
if (query.employeeId) items = items.filter((r) => r.data.employeeId === query.employeeId);
|
|
4270
|
+
if (query.leaveTypeId) items = items.filter((r) => r.data.leaveTypeId === query.leaveTypeId);
|
|
4271
|
+
if (query.status) items = items.filter((r) => r.data.status === query.status);
|
|
4272
|
+
if (query.year) {
|
|
4273
|
+
const yearStr = String(query.year);
|
|
4274
|
+
items = items.filter((r) => r.data.startDate.startsWith(yearStr) || r.data.endDate.startsWith(yearStr));
|
|
4275
|
+
}
|
|
4276
|
+
const total = items.length;
|
|
4277
|
+
return {
|
|
4278
|
+
items: items.slice(offset, offset + limit),
|
|
4279
|
+
total,
|
|
4280
|
+
limit,
|
|
4281
|
+
offset,
|
|
4282
|
+
hasMore: offset + limit < total
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
4285
|
+
};
|
|
4286
|
+
//#endregion
|
|
4287
|
+
//#region src/plugins/hrms/routes.ts
|
|
4288
|
+
function json(data, status = 200) {
|
|
4289
|
+
return new Response(JSON.stringify(data), {
|
|
4290
|
+
status,
|
|
4291
|
+
headers: {
|
|
4292
|
+
"Content-Type": "application/json",
|
|
4293
|
+
"Access-Control-Allow-Origin": "*"
|
|
4294
|
+
}
|
|
4295
|
+
});
|
|
4296
|
+
}
|
|
4297
|
+
function badRequest(message) {
|
|
4298
|
+
return json({ error: message }, 400);
|
|
4299
|
+
}
|
|
4300
|
+
function notFound(message) {
|
|
4301
|
+
return json({ error: message }, 404);
|
|
4302
|
+
}
|
|
4303
|
+
function registerHRMSRoutes(ctx, service, options = {}) {
|
|
4304
|
+
const prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
|
|
4305
|
+
ctx.registerRoute("GET", `${prefix}/employers`, async (_req, { url }) => {
|
|
4306
|
+
try {
|
|
4307
|
+
const status = url.searchParams.get("status");
|
|
4308
|
+
const pageStr = url.searchParams.get("page");
|
|
4309
|
+
const limitStr = url.searchParams.get("limit");
|
|
4310
|
+
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4311
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4312
|
+
return json(await service.listEmployers({
|
|
4313
|
+
status,
|
|
4314
|
+
page,
|
|
4315
|
+
limit
|
|
4316
|
+
}));
|
|
4317
|
+
} catch (err) {
|
|
4318
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4319
|
+
}
|
|
4320
|
+
});
|
|
4321
|
+
ctx.registerRoute("POST", `${prefix}/employers`, async (req) => {
|
|
4322
|
+
try {
|
|
4323
|
+
const body = await req.json();
|
|
4324
|
+
if (!body.companyName) return badRequest("companyName is required.");
|
|
4325
|
+
return json(await service.createEmployer(body), 201);
|
|
4326
|
+
} catch (err) {
|
|
4327
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4328
|
+
}
|
|
4329
|
+
});
|
|
4330
|
+
ctx.registerRoute("GET", `${prefix}/employers/:id`, async (_req, { params }) => {
|
|
4331
|
+
try {
|
|
4332
|
+
const employer = await service.getEmployer(params.id);
|
|
4333
|
+
if (!employer) return notFound(`Employer '${params.id}' not found.`);
|
|
4334
|
+
return json(employer);
|
|
4335
|
+
} catch (err) {
|
|
4336
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4337
|
+
}
|
|
4338
|
+
});
|
|
4339
|
+
ctx.registerRoute("PUT", `${prefix}/employers/:id`, async (req, { params }) => {
|
|
4340
|
+
try {
|
|
4341
|
+
const body = await req.json();
|
|
4342
|
+
const updated = await service.updateEmployer(params.id, body);
|
|
4343
|
+
if (!updated) return notFound(`Employer '${params.id}' not found.`);
|
|
4344
|
+
return json(updated);
|
|
4345
|
+
} catch (err) {
|
|
4346
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4347
|
+
}
|
|
4348
|
+
});
|
|
4349
|
+
ctx.registerRoute("GET", `${prefix}/employees`, async (_req, { url }) => {
|
|
4350
|
+
try {
|
|
4351
|
+
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4352
|
+
const department = url.searchParams.get("department") ?? void 0;
|
|
4353
|
+
const employmentType = url.searchParams.get("employmentType");
|
|
4354
|
+
const status = url.searchParams.get("status");
|
|
4355
|
+
const search = url.searchParams.get("search") ?? void 0;
|
|
4356
|
+
const pageStr = url.searchParams.get("page");
|
|
4357
|
+
const limitStr = url.searchParams.get("limit");
|
|
4358
|
+
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4359
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4360
|
+
return json(await service.listEmployees({
|
|
4361
|
+
employerId,
|
|
4362
|
+
department,
|
|
4363
|
+
employmentType,
|
|
4364
|
+
status,
|
|
4365
|
+
search,
|
|
4366
|
+
page,
|
|
4367
|
+
limit
|
|
4368
|
+
}));
|
|
4369
|
+
} catch (err) {
|
|
4370
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4371
|
+
}
|
|
4372
|
+
});
|
|
4373
|
+
ctx.registerRoute("POST", `${prefix}/employees`, async (req) => {
|
|
4374
|
+
try {
|
|
4375
|
+
const body = await req.json();
|
|
4376
|
+
if (!body.employerId) return badRequest("employerId is required.");
|
|
4377
|
+
if (!body.employeeNumber) return badRequest("employeeNumber is required.");
|
|
4378
|
+
if (!body.firstName || !body.lastName) return badRequest("firstName and lastName are required.");
|
|
4379
|
+
if (!body.email) return badRequest("email is required.");
|
|
4380
|
+
return json(await service.createEmployee(body), 201);
|
|
4381
|
+
} catch (err) {
|
|
4382
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4383
|
+
}
|
|
4384
|
+
});
|
|
4385
|
+
ctx.registerRoute("GET", `${prefix}/employees/:id`, async (_req, { params }) => {
|
|
4386
|
+
try {
|
|
4387
|
+
const employee = await service.getEmployee(params.id);
|
|
4388
|
+
if (!employee) return notFound(`Employee '${params.id}' not found.`);
|
|
4389
|
+
return json(employee);
|
|
4390
|
+
} catch (err) {
|
|
4391
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4392
|
+
}
|
|
4393
|
+
});
|
|
4394
|
+
ctx.registerRoute("PUT", `${prefix}/employees/:id`, async (req, { params }) => {
|
|
4395
|
+
try {
|
|
4396
|
+
const body = await req.json();
|
|
4397
|
+
const updated = await service.updateEmployee(params.id, body);
|
|
4398
|
+
if (!updated) return notFound(`Employee '${params.id}' not found.`);
|
|
4399
|
+
return json(updated);
|
|
4400
|
+
} catch (err) {
|
|
4401
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4402
|
+
}
|
|
4403
|
+
});
|
|
4404
|
+
ctx.registerRoute("DELETE", `${prefix}/employees/:id`, async (_req, { params }) => {
|
|
4405
|
+
try {
|
|
4406
|
+
if (!await service.deleteEmployee(params.id)) return notFound(`Employee '${params.id}' not found.`);
|
|
4407
|
+
return json({ success: true });
|
|
4408
|
+
} catch (err) {
|
|
4409
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
ctx.registerRoute("GET", `${prefix}/employees/:id/leave-balance`, async (_req, { params, url }) => {
|
|
4413
|
+
try {
|
|
4414
|
+
const yearStr = url.searchParams.get("year");
|
|
4415
|
+
const year = yearStr ? parseInt(yearStr, 10) : void 0;
|
|
4416
|
+
return json(await service.calculateLeaveBalance(params.id, year));
|
|
4417
|
+
} catch (err) {
|
|
4418
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4419
|
+
}
|
|
4420
|
+
});
|
|
4421
|
+
ctx.registerRoute("GET", `${prefix}/employees/:id/direct-reports`, async (_req, { params }) => {
|
|
4422
|
+
try {
|
|
4423
|
+
const reports = await service.getDirectReports(params.id);
|
|
4424
|
+
return json({
|
|
4425
|
+
items: reports,
|
|
4426
|
+
total: reports.length
|
|
4427
|
+
});
|
|
4428
|
+
} catch (err) {
|
|
4429
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4430
|
+
}
|
|
4431
|
+
});
|
|
4432
|
+
ctx.registerRoute("POST", `${prefix}/attendance/check-in`, async (req) => {
|
|
4433
|
+
try {
|
|
4434
|
+
const body = await req.json();
|
|
4435
|
+
if (!body.employeeId) return badRequest("employeeId is required.");
|
|
4436
|
+
return json(await service.checkIn(body), 201);
|
|
4437
|
+
} catch (err) {
|
|
4438
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4439
|
+
}
|
|
4440
|
+
});
|
|
4441
|
+
ctx.registerRoute("POST", `${prefix}/attendance/check-out`, async (req) => {
|
|
4442
|
+
try {
|
|
4443
|
+
const body = await req.json();
|
|
4444
|
+
if (!body.employeeId) return badRequest("employeeId is required.");
|
|
4445
|
+
return json(await service.checkOut(body));
|
|
4446
|
+
} catch (err) {
|
|
4447
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
ctx.registerRoute("GET", `${prefix}/attendance`, async (_req, { url }) => {
|
|
4451
|
+
try {
|
|
4452
|
+
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4453
|
+
const employeeId = url.searchParams.get("employeeId") ?? void 0;
|
|
4454
|
+
const date = url.searchParams.get("date") ?? void 0;
|
|
4455
|
+
const startDate = url.searchParams.get("startDate") ?? void 0;
|
|
4456
|
+
const endDate = url.searchParams.get("endDate") ?? void 0;
|
|
4457
|
+
const status = url.searchParams.get("status");
|
|
4458
|
+
const pageStr = url.searchParams.get("page");
|
|
4459
|
+
const limitStr = url.searchParams.get("limit");
|
|
4460
|
+
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4461
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4462
|
+
return json(await service.listAttendance({
|
|
4463
|
+
employerId,
|
|
4464
|
+
employeeId,
|
|
4465
|
+
date,
|
|
4466
|
+
startDate,
|
|
4467
|
+
endDate,
|
|
4468
|
+
status,
|
|
4469
|
+
page,
|
|
4470
|
+
limit
|
|
4471
|
+
}));
|
|
4472
|
+
} catch (err) {
|
|
4473
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4474
|
+
}
|
|
4475
|
+
});
|
|
4476
|
+
ctx.registerRoute("POST", `${prefix}/attendance/manual`, async (req) => {
|
|
4477
|
+
try {
|
|
4478
|
+
const body = await req.json();
|
|
4479
|
+
if (!body.employerId || !body.employeeId || !body.date || !body.checkInAt) return badRequest("employerId, employeeId, date, and checkInAt are required.");
|
|
4480
|
+
return json(await service.recordAttendanceManual(body), 201);
|
|
4481
|
+
} catch (err) {
|
|
4482
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4483
|
+
}
|
|
4484
|
+
});
|
|
4485
|
+
ctx.registerRoute("GET", `${prefix}/leave-types`, async (_req, { url }) => {
|
|
4486
|
+
try {
|
|
4487
|
+
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4488
|
+
const types = await service.listLeaveTypes(employerId);
|
|
4489
|
+
return json({
|
|
4490
|
+
items: types,
|
|
4491
|
+
total: types.length
|
|
4492
|
+
});
|
|
4493
|
+
} catch (err) {
|
|
4494
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4495
|
+
}
|
|
4496
|
+
});
|
|
4497
|
+
ctx.registerRoute("POST", `${prefix}/leave-types`, async (req) => {
|
|
4498
|
+
try {
|
|
4499
|
+
const body = await req.json();
|
|
4500
|
+
if (!body.name || !body.code || body.daysAllowedPerYear === void 0) return badRequest("name, code, and daysAllowedPerYear are required.");
|
|
4501
|
+
return json(await service.createLeaveType(body), 201);
|
|
4502
|
+
} catch (err) {
|
|
4503
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4504
|
+
}
|
|
4505
|
+
});
|
|
4506
|
+
ctx.registerRoute("GET", `${prefix}/leave-types/:id`, async (_req, { params }) => {
|
|
4507
|
+
try {
|
|
4508
|
+
const leaveType = await service.getLeaveType(params.id);
|
|
4509
|
+
if (!leaveType) return notFound(`Leave type '${params.id}' not found.`);
|
|
4510
|
+
return json(leaveType);
|
|
4511
|
+
} catch (err) {
|
|
4512
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4513
|
+
}
|
|
4514
|
+
});
|
|
4515
|
+
ctx.registerRoute("GET", `${prefix}/leave-requests`, async (_req, { url }) => {
|
|
4516
|
+
try {
|
|
4517
|
+
const employerId = url.searchParams.get("employerId") ?? void 0;
|
|
4518
|
+
const employeeId = url.searchParams.get("employeeId") ?? void 0;
|
|
4519
|
+
const leaveTypeId = url.searchParams.get("leaveTypeId") ?? void 0;
|
|
4520
|
+
const status = url.searchParams.get("status");
|
|
4521
|
+
const yearStr = url.searchParams.get("year");
|
|
4522
|
+
const year = yearStr ? parseInt(yearStr, 10) : void 0;
|
|
4523
|
+
const pageStr = url.searchParams.get("page");
|
|
4524
|
+
const limitStr = url.searchParams.get("limit");
|
|
4525
|
+
const page = pageStr ? parseInt(pageStr, 10) : void 0;
|
|
4526
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4527
|
+
return json(await service.listLeaveRequests({
|
|
4528
|
+
employerId,
|
|
4529
|
+
employeeId,
|
|
4530
|
+
leaveTypeId,
|
|
4531
|
+
status,
|
|
4532
|
+
year,
|
|
4533
|
+
page,
|
|
4534
|
+
limit
|
|
4535
|
+
}));
|
|
4536
|
+
} catch (err) {
|
|
4537
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4538
|
+
}
|
|
4539
|
+
});
|
|
4540
|
+
ctx.registerRoute("POST", `${prefix}/leave-requests`, async (req) => {
|
|
4541
|
+
try {
|
|
4542
|
+
const body = await req.json();
|
|
4543
|
+
if (!body.employeeId || !body.leaveTypeId || !body.startDate || !body.endDate) return badRequest("employeeId, leaveTypeId, startDate, and endDate are required.");
|
|
4544
|
+
return json(await service.requestLeave(body), 201);
|
|
4545
|
+
} catch (err) {
|
|
4546
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4547
|
+
}
|
|
4548
|
+
});
|
|
4549
|
+
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/approve`, async (req, { params }) => {
|
|
4550
|
+
try {
|
|
4551
|
+
const approverId = (await req.json().catch(() => ({}))).approverId ?? "admin";
|
|
4552
|
+
return json(await service.approveLeave({
|
|
4553
|
+
requestId: params.id,
|
|
4554
|
+
approverId
|
|
4555
|
+
}));
|
|
4556
|
+
} catch (err) {
|
|
4557
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4558
|
+
}
|
|
4559
|
+
});
|
|
4560
|
+
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/reject`, async (req, { params }) => {
|
|
4561
|
+
try {
|
|
4562
|
+
const body = await req.json().catch(() => ({}));
|
|
4563
|
+
const approverId = body.approverId ?? "admin";
|
|
4564
|
+
return json(await service.rejectLeave({
|
|
4565
|
+
requestId: params.id,
|
|
4566
|
+
approverId,
|
|
4567
|
+
reason: body.reason
|
|
4568
|
+
}));
|
|
4569
|
+
} catch (err) {
|
|
4570
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4571
|
+
}
|
|
4572
|
+
});
|
|
4573
|
+
ctx.registerRoute("POST", `${prefix}/leave-requests/:id/cancel`, async (_req, { params }) => {
|
|
4574
|
+
try {
|
|
4575
|
+
return json(await service.cancelLeave(params.id));
|
|
4576
|
+
} catch (err) {
|
|
4577
|
+
return badRequest(err instanceof Error ? err.message : String(err));
|
|
4578
|
+
}
|
|
4579
|
+
});
|
|
4580
|
+
}
|
|
4581
|
+
//#endregion
|
|
4582
|
+
//#region src/plugins/hrms/client.ts
|
|
4583
|
+
var HRMSClient = class {
|
|
4584
|
+
client;
|
|
4585
|
+
options;
|
|
4586
|
+
service;
|
|
4587
|
+
prefix;
|
|
4588
|
+
constructor(client, options = {}) {
|
|
4589
|
+
this.client = client;
|
|
4590
|
+
this.options = options;
|
|
4591
|
+
this.prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
|
|
4592
|
+
const engine = client.getEngine();
|
|
4593
|
+
if (engine) this.service = new HRMSService(engine, options);
|
|
4594
|
+
}
|
|
4595
|
+
employers = {
|
|
4596
|
+
find: async (query = {}) => {
|
|
4597
|
+
if (this.service) return this.service.listEmployers(query);
|
|
4598
|
+
const params = new URLSearchParams();
|
|
4599
|
+
if (query.status) params.set("status", query.status);
|
|
4600
|
+
if (query.page) params.set("page", String(query.page));
|
|
4601
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
4602
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
4603
|
+
return this.client.request(`${this.prefix}/employers${q}`);
|
|
4604
|
+
},
|
|
4605
|
+
get: async (id) => {
|
|
4606
|
+
if (this.service) return this.service.getEmployer(id);
|
|
4607
|
+
return this.client.request(`${this.prefix}/employers/${encodeURIComponent(id)}`);
|
|
4608
|
+
},
|
|
4609
|
+
create: async (input) => {
|
|
4610
|
+
if (this.service) return this.service.createEmployer(input);
|
|
4611
|
+
return this.client.request(`${this.prefix}/employers`, {
|
|
4612
|
+
method: "POST",
|
|
4613
|
+
body: JSON.stringify(input)
|
|
4614
|
+
});
|
|
4615
|
+
},
|
|
4616
|
+
update: async (id, input) => {
|
|
4617
|
+
if (this.service) return this.service.updateEmployer(id, input);
|
|
4618
|
+
return this.client.request(`${this.prefix}/employers/${encodeURIComponent(id)}`, {
|
|
4619
|
+
method: "PUT",
|
|
4620
|
+
body: JSON.stringify(input)
|
|
4621
|
+
});
|
|
4622
|
+
}
|
|
4623
|
+
};
|
|
4624
|
+
employees = {
|
|
4625
|
+
find: async (query = {}) => {
|
|
4626
|
+
if (this.service) return this.service.listEmployees(query);
|
|
4627
|
+
const params = new URLSearchParams();
|
|
4628
|
+
if (query.employerId) params.set("employerId", query.employerId);
|
|
4629
|
+
if (query.department) params.set("department", query.department);
|
|
4630
|
+
if (query.employmentType) params.set("employmentType", query.employmentType);
|
|
4631
|
+
if (query.status) params.set("status", query.status);
|
|
4632
|
+
if (query.search) params.set("search", query.search);
|
|
4633
|
+
if (query.page) params.set("page", String(query.page));
|
|
4634
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
4635
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
4636
|
+
return this.client.request(`${this.prefix}/employees${q}`);
|
|
4637
|
+
},
|
|
4638
|
+
get: async (id) => {
|
|
4639
|
+
if (this.service) return this.service.getEmployee(id);
|
|
4640
|
+
return this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`);
|
|
4641
|
+
},
|
|
4642
|
+
getByNumber: async (employerId, employeeNumber) => {
|
|
4643
|
+
if (this.service) return this.service.getEmployeeByNumber(employerId, employeeNumber);
|
|
4644
|
+
return (await this.employees.find({
|
|
4645
|
+
employerId,
|
|
4646
|
+
search: employeeNumber
|
|
4647
|
+
})).items.find((e) => e.data.employeeNumber === employeeNumber) ?? null;
|
|
4648
|
+
},
|
|
4649
|
+
create: async (input) => {
|
|
4650
|
+
if (this.service) return this.service.createEmployee(input);
|
|
4651
|
+
return this.client.request(`${this.prefix}/employees`, {
|
|
4652
|
+
method: "POST",
|
|
4653
|
+
body: JSON.stringify(input)
|
|
4654
|
+
});
|
|
4655
|
+
},
|
|
4656
|
+
update: async (id, input) => {
|
|
4657
|
+
if (this.service) return this.service.updateEmployee(id, input);
|
|
4658
|
+
return this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`, {
|
|
4659
|
+
method: "PUT",
|
|
4660
|
+
body: JSON.stringify(input)
|
|
4661
|
+
});
|
|
4662
|
+
},
|
|
4663
|
+
delete: async (id) => {
|
|
4664
|
+
if (this.service) return this.service.deleteEmployee(id);
|
|
4665
|
+
return (await this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`, { method: "DELETE" })).success;
|
|
4666
|
+
},
|
|
4667
|
+
getLeaveBalance: async (employeeId, year) => {
|
|
4668
|
+
if (this.service) return this.service.calculateLeaveBalance(employeeId, year);
|
|
4669
|
+
const q = year ? `?year=${year}` : "";
|
|
4670
|
+
return this.client.request(`${this.prefix}/employees/${encodeURIComponent(employeeId)}/leave-balance${q}`);
|
|
4671
|
+
},
|
|
4672
|
+
getDirectReports: async (managerId) => {
|
|
4673
|
+
if (this.service) return this.service.getDirectReports(managerId);
|
|
4674
|
+
return (await this.client.request(`${this.prefix}/employees/${encodeURIComponent(managerId)}/direct-reports`)).items;
|
|
4675
|
+
}
|
|
4676
|
+
};
|
|
4677
|
+
attendance = {
|
|
4678
|
+
checkIn: async (input) => {
|
|
4679
|
+
if (this.service) return this.service.checkIn(input);
|
|
4680
|
+
return this.client.request(`${this.prefix}/attendance/check-in`, {
|
|
4681
|
+
method: "POST",
|
|
4682
|
+
body: JSON.stringify(input)
|
|
4683
|
+
});
|
|
4684
|
+
},
|
|
4685
|
+
checkOut: async (input) => {
|
|
4686
|
+
if (this.service) return this.service.checkOut(input);
|
|
4687
|
+
return this.client.request(`${this.prefix}/attendance/check-out`, {
|
|
4688
|
+
method: "POST",
|
|
4689
|
+
body: JSON.stringify(input)
|
|
4690
|
+
});
|
|
4691
|
+
},
|
|
4692
|
+
getDaily: async (employeeId, date) => {
|
|
4693
|
+
if (this.service) return this.service.getDailyAttendance(employeeId, date);
|
|
4694
|
+
return (await this.attendance.find({
|
|
4695
|
+
employeeId,
|
|
4696
|
+
date
|
|
4697
|
+
})).items[0] ?? null;
|
|
4698
|
+
},
|
|
4699
|
+
find: async (query = {}) => {
|
|
4700
|
+
if (this.service) return this.service.listAttendance(query);
|
|
4701
|
+
const params = new URLSearchParams();
|
|
4702
|
+
if (query.employerId) params.set("employerId", query.employerId);
|
|
4703
|
+
if (query.employeeId) params.set("employeeId", query.employeeId);
|
|
4704
|
+
if (query.date) params.set("date", query.date);
|
|
4705
|
+
if (query.startDate) params.set("startDate", query.startDate);
|
|
4706
|
+
if (query.endDate) params.set("endDate", query.endDate);
|
|
4707
|
+
if (query.status) params.set("status", query.status);
|
|
4708
|
+
if (query.page) params.set("page", String(query.page));
|
|
4709
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
4710
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
4711
|
+
return this.client.request(`${this.prefix}/attendance${q}`);
|
|
4712
|
+
},
|
|
4713
|
+
recordManual: async (input) => {
|
|
4714
|
+
if (this.service) return this.service.recordAttendanceManual(input);
|
|
4715
|
+
return this.client.request(`${this.prefix}/attendance/manual`, {
|
|
4716
|
+
method: "POST",
|
|
4717
|
+
body: JSON.stringify(input)
|
|
4718
|
+
});
|
|
4719
|
+
}
|
|
4720
|
+
};
|
|
4721
|
+
leaves = {
|
|
4722
|
+
listTypes: async (employerId) => {
|
|
4723
|
+
if (this.service) return this.service.listLeaveTypes(employerId);
|
|
4724
|
+
const q = employerId ? `?employerId=${encodeURIComponent(employerId)}` : "";
|
|
4725
|
+
return (await this.client.request(`${this.prefix}/leave-types${q}`)).items;
|
|
4726
|
+
},
|
|
4727
|
+
createType: async (input) => {
|
|
4728
|
+
if (this.service) return this.service.createLeaveType(input);
|
|
4729
|
+
return this.client.request(`${this.prefix}/leave-types`, {
|
|
4730
|
+
method: "POST",
|
|
4731
|
+
body: JSON.stringify(input)
|
|
4732
|
+
});
|
|
4733
|
+
},
|
|
4734
|
+
getType: async (id) => {
|
|
4735
|
+
if (this.service) return this.service.getLeaveType(id);
|
|
4736
|
+
return this.client.request(`${this.prefix}/leave-types/${encodeURIComponent(id)}`);
|
|
4737
|
+
},
|
|
4738
|
+
findRequests: async (query = {}) => {
|
|
4739
|
+
if (this.service) return this.service.listLeaveRequests(query);
|
|
4740
|
+
const params = new URLSearchParams();
|
|
4741
|
+
if (query.employerId) params.set("employerId", query.employerId);
|
|
4742
|
+
if (query.employeeId) params.set("employeeId", query.employeeId);
|
|
4743
|
+
if (query.leaveTypeId) params.set("leaveTypeId", query.leaveTypeId);
|
|
4744
|
+
if (query.status) params.set("status", query.status);
|
|
4745
|
+
if (query.year) params.set("year", String(query.year));
|
|
4746
|
+
if (query.page) params.set("page", String(query.page));
|
|
4747
|
+
if (query.limit) params.set("limit", String(query.limit));
|
|
4748
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
4749
|
+
return this.client.request(`${this.prefix}/leave-requests${q}`);
|
|
4750
|
+
},
|
|
4751
|
+
request: async (input) => {
|
|
4752
|
+
if (this.service) return this.service.requestLeave(input);
|
|
4753
|
+
return this.client.request(`${this.prefix}/leave-requests`, {
|
|
4754
|
+
method: "POST",
|
|
4755
|
+
body: JSON.stringify(input)
|
|
4756
|
+
});
|
|
4757
|
+
},
|
|
4758
|
+
approve: async (input) => {
|
|
4759
|
+
if (this.service) return this.service.approveLeave(input);
|
|
4760
|
+
return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(input.requestId)}/approve`, {
|
|
4761
|
+
method: "POST",
|
|
4762
|
+
body: JSON.stringify({ approverId: input.approverId })
|
|
4763
|
+
});
|
|
4764
|
+
},
|
|
4765
|
+
reject: async (input) => {
|
|
4766
|
+
if (this.service) return this.service.rejectLeave(input);
|
|
4767
|
+
return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(input.requestId)}/reject`, {
|
|
4768
|
+
method: "POST",
|
|
4769
|
+
body: JSON.stringify({
|
|
4770
|
+
approverId: input.approverId,
|
|
4771
|
+
reason: input.reason
|
|
4772
|
+
})
|
|
4773
|
+
});
|
|
4774
|
+
},
|
|
4775
|
+
cancel: async (requestId) => {
|
|
4776
|
+
if (this.service) return this.service.cancelLeave(requestId);
|
|
4777
|
+
return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(requestId)}/cancel`, { method: "POST" });
|
|
4778
|
+
}
|
|
4779
|
+
};
|
|
4780
|
+
};
|
|
4781
|
+
/**
|
|
4782
|
+
* Get or create an HRMSClient adapter for a CMSClient.
|
|
4783
|
+
*/
|
|
4784
|
+
function getHRMSClient(client, options) {
|
|
4785
|
+
return new HRMSClient(client, options);
|
|
4786
|
+
}
|
|
4787
|
+
//#endregion
|
|
4788
|
+
//#region src/plugins/hrms/index.ts
|
|
4789
|
+
/**
|
|
4790
|
+
* @azlib/cms - Built-in HRMS Plugin
|
|
4791
|
+
*/
|
|
4792
|
+
/**
|
|
4793
|
+
* Built-in HRMS plugin factory for @azlib/cms.
|
|
4794
|
+
* Equips the CMS engine with multi-tenant employers, employee profiles,
|
|
4795
|
+
* daily attendance check-in/out tracking, and leave quota approval workflows.
|
|
4796
|
+
*/
|
|
4797
|
+
const hrmsPlugin = definePlugin((options) => {
|
|
4798
|
+
const opts = options || {};
|
|
4799
|
+
const collections = [createEmployeeCollection(opts)];
|
|
4800
|
+
if (opts.enableEmployers !== false) collections.unshift(createEmployerCollection(opts));
|
|
4801
|
+
if (opts.enableAttendance !== false) collections.push(createAttendanceCollection(opts));
|
|
4802
|
+
if (opts.enableLeaves !== false) {
|
|
4803
|
+
collections.push(createLeaveTypeCollection(opts));
|
|
4804
|
+
collections.push(createLeaveRequestCollection(opts));
|
|
4805
|
+
}
|
|
4806
|
+
return {
|
|
4807
|
+
name: "hrms",
|
|
4808
|
+
version: "1.0.0",
|
|
4809
|
+
description: "Built-in Human Resource Management System (HRMS) plugin for employers, employees, attendance, and leave management",
|
|
4810
|
+
collections,
|
|
4811
|
+
taxonomies: createHRMSTaxonomies(opts),
|
|
4812
|
+
setup(ctx) {
|
|
4813
|
+
const service = new HRMSService(ctx.engine, opts);
|
|
4814
|
+
ctx.engine.__hrmsService = service;
|
|
4815
|
+
registerHRMSRoutes(ctx, service, opts);
|
|
4816
|
+
}
|
|
4817
|
+
};
|
|
4818
|
+
});
|
|
4819
|
+
/**
|
|
4820
|
+
* Retrieve the active HRMSService instance associated with a CMSEngine.
|
|
4821
|
+
*/
|
|
4822
|
+
function getHRMSService(engine, options) {
|
|
4823
|
+
if (engine.__hrmsService) return engine.__hrmsService;
|
|
4824
|
+
const service = new HRMSService(engine, options);
|
|
4825
|
+
engine.__hrmsService = service;
|
|
4826
|
+
return service;
|
|
4827
|
+
}
|
|
4828
|
+
//#endregion
|
|
3238
4829
|
exports.CMSClient = CMSClient;
|
|
3239
4830
|
exports.CMSEngine = CMSEngine;
|
|
3240
4831
|
exports.CMSRouter = CMSRouter;
|
|
@@ -3243,6 +4834,8 @@ exports.DEFAULT_COLLECTIONS = DEFAULT_COLLECTIONS;
|
|
|
3243
4834
|
exports.DEFAULT_ROLE_CAPABILITIES = DEFAULT_ROLE_CAPABILITIES;
|
|
3244
4835
|
exports.EcommerceClient = EcommerceClient;
|
|
3245
4836
|
exports.EcommerceService = EcommerceService;
|
|
4837
|
+
exports.HRMSClient = HRMSClient;
|
|
4838
|
+
exports.HRMSService = HRMSService;
|
|
3246
4839
|
exports.HooksManager = HooksManager;
|
|
3247
4840
|
exports.MediaManager = MediaManager;
|
|
3248
4841
|
exports.MemoryStorageAdapter = MemoryStorageAdapter;
|
|
@@ -3252,11 +4845,17 @@ exports.RevisionManager = RevisionManager;
|
|
|
3252
4845
|
exports.TaxonomyManager = TaxonomyManager;
|
|
3253
4846
|
exports.VALID_STATUS_TRANSITIONS = VALID_STATUS_TRANSITIONS;
|
|
3254
4847
|
exports.collection = collection;
|
|
4848
|
+
exports.createAttendanceCollection = createAttendanceCollection;
|
|
3255
4849
|
exports.createCMSEngine = createCMSEngine;
|
|
3256
4850
|
exports.createCMSRouter = createCMSRouter;
|
|
3257
4851
|
exports.createCmsClient = createCmsClient;
|
|
3258
4852
|
exports.createDiscountCollection = createDiscountCollection;
|
|
3259
4853
|
exports.createEcommerceTaxonomies = createEcommerceTaxonomies;
|
|
4854
|
+
exports.createEmployeeCollection = createEmployeeCollection;
|
|
4855
|
+
exports.createEmployerCollection = createEmployerCollection;
|
|
4856
|
+
exports.createHRMSTaxonomies = createHRMSTaxonomies;
|
|
4857
|
+
exports.createLeaveRequestCollection = createLeaveRequestCollection;
|
|
4858
|
+
exports.createLeaveTypeCollection = createLeaveTypeCollection;
|
|
3260
4859
|
exports.createOrderCollection = createOrderCollection;
|
|
3261
4860
|
exports.createProductCollection = createProductCollection;
|
|
3262
4861
|
exports.defaultHooks = defaultHooks;
|
|
@@ -3266,6 +4865,9 @@ exports.ecommercePlugin = ecommercePlugin;
|
|
|
3266
4865
|
exports.fields = fields;
|
|
3267
4866
|
exports.getEcommerceClient = getEcommerceClient;
|
|
3268
4867
|
exports.getEcommerceService = getEcommerceService;
|
|
4868
|
+
exports.getHRMSClient = getHRMSClient;
|
|
4869
|
+
exports.getHRMSService = getHRMSService;
|
|
4870
|
+
exports.hrmsPlugin = hrmsPlugin;
|
|
3269
4871
|
exports.normalizeConfig = normalizeConfig;
|
|
3270
4872
|
exports.resolveUniqueSlug = resolveUniqueSlug;
|
|
3271
4873
|
exports.slugify = slugify;
|