@azlib/cms 0.4.0 → 0.6.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/dist/index.cjs CHANGED
@@ -1,4 +1,28 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let exceljs = require("exceljs");
25
+ exceljs = __toESM(exceljs, 1);
2
26
  //#region src/content/schema.ts
3
27
  const fields = {
4
28
  text(options) {
@@ -2857,7 +2881,7 @@ var EcommerceService = class {
2857
2881
  };
2858
2882
  //#endregion
2859
2883
  //#region src/plugins/ecommerce/routes.ts
2860
- function json(data, status = 200) {
2884
+ function json$2(data, status = 200) {
2861
2885
  return new Response(JSON.stringify(data), {
2862
2886
  status,
2863
2887
  headers: {
@@ -2866,11 +2890,11 @@ function json(data, status = 200) {
2866
2890
  }
2867
2891
  });
2868
2892
  }
2869
- function badRequest(message) {
2870
- return json({ error: message }, 400);
2893
+ function badRequest$2(message) {
2894
+ return json$2({ error: message }, 400);
2871
2895
  }
2872
- function notFound(message) {
2873
- return json({ error: message }, 404);
2896
+ function notFound$2(message) {
2897
+ return json$2({ error: message }, 404);
2874
2898
  }
2875
2899
  function registerEcommerceRoutes(ctx, service, options = {}) {
2876
2900
  const prefix = (options.apiPrefix ?? "/api/ecommerce").replace(/\/+$/, "");
@@ -2893,7 +2917,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
2893
2917
  const offsetStr = url.searchParams.get("offset");
2894
2918
  const limit = limitStr ? parseInt(limitStr, 10) : 20;
2895
2919
  const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
2896
- return json(await service.listProducts({
2920
+ return json$2(await service.listProducts({
2897
2921
  categorySlug,
2898
2922
  categoryId,
2899
2923
  tagSlug,
@@ -2909,7 +2933,7 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
2909
2933
  offset
2910
2934
  }));
2911
2935
  } catch (err) {
2912
- return badRequest(err instanceof Error ? err.message : String(err));
2936
+ return badRequest$2(err instanceof Error ? err.message : String(err));
2913
2937
  }
2914
2938
  });
2915
2939
  ctx.registerRoute("GET", `${prefix}/products/:id`, async (_req, { params, url }) => {
@@ -2921,32 +2945,32 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
2921
2945
  product = await service.getProduct(id);
2922
2946
  if (!product) product = await service.getProductBySlug(id);
2923
2947
  }
2924
- if (!product) return notFound(`Product '${id}' not found`);
2925
- return json(product);
2948
+ if (!product) return notFound$2(`Product '${id}' not found`);
2949
+ return json$2(product);
2926
2950
  });
2927
2951
  ctx.registerRoute("POST", `${prefix}/products`, async (req) => {
2928
2952
  try {
2929
2953
  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);
2954
+ if (!body.title) return badRequest$2("Product 'title' is required.");
2955
+ if (body.price === void 0 || body.price < 0) return badRequest$2("Valid product 'price' is required.");
2956
+ return json$2(await service.createProduct(body), 201);
2933
2957
  } catch (err) {
2934
- return badRequest(err instanceof Error ? err.message : String(err));
2958
+ return badRequest$2(err instanceof Error ? err.message : String(err));
2935
2959
  }
2936
2960
  });
2937
2961
  ctx.registerRoute("PUT", `${prefix}/products/:id`, async (req, { params }) => {
2938
2962
  try {
2939
2963
  const body = await req.json();
2940
2964
  const updated = await service.updateProduct(params.id, body);
2941
- if (!updated) return notFound(`Product '${params.id}' not found.`);
2942
- return json(updated);
2965
+ if (!updated) return notFound$2(`Product '${params.id}' not found.`);
2966
+ return json$2(updated);
2943
2967
  } catch (err) {
2944
- return badRequest(err instanceof Error ? err.message : String(err));
2968
+ return badRequest$2(err instanceof Error ? err.message : String(err));
2945
2969
  }
2946
2970
  });
2947
2971
  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({
2972
+ if (!await service.deleteProduct(params.id)) return notFound$2(`Product '${params.id}' not found.`);
2973
+ return json$2({
2950
2974
  success: true,
2951
2975
  id: params.id
2952
2976
  });
@@ -2954,8 +2978,8 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
2954
2978
  ctx.registerRoute("POST", `${prefix}/products/:id/images`, async (req, { params }) => {
2955
2979
  try {
2956
2980
  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, {
2981
+ if (!body.filename || !body.mimeType) return badRequest$2("'filename' and 'mimeType' are required.");
2982
+ return json$2(await service.uploadProductImage(params.id, {
2959
2983
  filename: body.filename,
2960
2984
  mimeType: body.mimeType,
2961
2985
  sizeBytes: body.sizeBytes ?? 0,
@@ -2967,68 +2991,68 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
2967
2991
  isFeatured: body.isFeatured
2968
2992
  }), 201);
2969
2993
  } catch (err) {
2970
- return badRequest(err instanceof Error ? err.message : String(err));
2994
+ return badRequest$2(err instanceof Error ? err.message : String(err));
2971
2995
  }
2972
2996
  });
2973
2997
  ctx.registerRoute("GET", `${prefix}/categories`, async (_req, { url }) => {
2974
2998
  try {
2975
- if (url.searchParams.get("tree") === "true") return json(await service.getCategoryTree());
2999
+ if (url.searchParams.get("tree") === "true") return json$2(await service.getCategoryTree());
2976
3000
  const parentId = url.searchParams.get("parentId");
2977
- return json(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
3001
+ return json$2(await service.getCategories({ parentId: parentId === "null" ? null : parentId ?? void 0 }));
2978
3002
  } catch (err) {
2979
- return badRequest(err instanceof Error ? err.message : String(err));
3003
+ return badRequest$2(err instanceof Error ? err.message : String(err));
2980
3004
  }
2981
3005
  });
2982
3006
  ctx.registerRoute("POST", `${prefix}/categories`, async (req) => {
2983
3007
  try {
2984
3008
  const body = await req.json();
2985
- if (!body.name) return badRequest("Category 'name' is required.");
2986
- return json(await service.createCategory(body), 201);
3009
+ if (!body.name) return badRequest$2("Category 'name' is required.");
3010
+ return json$2(await service.createCategory(body), 201);
2987
3011
  } catch (err) {
2988
- return badRequest(err instanceof Error ? err.message : String(err));
3012
+ return badRequest$2(err instanceof Error ? err.message : String(err));
2989
3013
  }
2990
3014
  });
2991
3015
  if (options.enableDiscounts !== false) {
2992
3016
  ctx.registerRoute("POST", `${prefix}/discounts`, async (req) => {
2993
3017
  try {
2994
3018
  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);
3019
+ if (!body.title || !body.code) return badRequest$2("'title' and 'code' are required.");
3020
+ if (body.value === void 0 || body.value < 0) return badRequest$2("Valid discount 'value' is required.");
3021
+ return json$2(await service.createDiscount(body), 201);
2998
3022
  } catch (err) {
2999
- return badRequest(err instanceof Error ? err.message : String(err));
3023
+ return badRequest$2(err instanceof Error ? err.message : String(err));
3000
3024
  }
3001
3025
  });
3002
3026
  ctx.registerRoute("POST", `${prefix}/discounts/validate`, async (req) => {
3003
3027
  try {
3004
3028
  const body = await req.json();
3005
- if (!body.code) return badRequest("Discount 'code' is required.");
3029
+ if (!body.code) return badRequest$2("Discount 'code' is required.");
3006
3030
  const subtotal = Number(body.subtotal ?? 0);
3007
3031
  const productIds = Array.isArray(body.productIds) ? body.productIds : [];
3008
- return json(await service.validateDiscount(body.code, subtotal, productIds));
3032
+ return json$2(await service.validateDiscount(body.code, subtotal, productIds));
3009
3033
  } catch (err) {
3010
- return badRequest(err instanceof Error ? err.message : String(err));
3034
+ return badRequest$2(err instanceof Error ? err.message : String(err));
3011
3035
  }
3012
3036
  });
3013
3037
  }
3014
3038
  ctx.registerRoute("POST", `${prefix}/cart/calculate`, async (req) => {
3015
3039
  try {
3016
3040
  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));
3041
+ if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$2("'items' array is required and must not be empty.");
3042
+ return json$2(await service.calculateCart(body));
3019
3043
  } catch (err) {
3020
- return badRequest(err instanceof Error ? err.message : String(err));
3044
+ return badRequest$2(err instanceof Error ? err.message : String(err));
3021
3045
  }
3022
3046
  });
3023
3047
  if (options.enableOrders !== false) {
3024
3048
  ctx.registerRoute("POST", `${prefix}/orders`, async (req) => {
3025
3049
  try {
3026
3050
  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);
3051
+ if (!body.customerEmail) return badRequest$2("'customerEmail' is required.");
3052
+ if (!body.items || !Array.isArray(body.items) || body.items.length === 0) return badRequest$2("'items' array is required.");
3053
+ return json$2(await service.createOrder(body), 201);
3030
3054
  } catch (err) {
3031
- return badRequest(err instanceof Error ? err.message : String(err));
3055
+ return badRequest$2(err instanceof Error ? err.message : String(err));
3032
3056
  }
3033
3057
  });
3034
3058
  ctx.registerRoute("GET", `${prefix}/orders/:id`, async (_req, { params, url }) => {
@@ -3040,18 +3064,18 @@ function registerEcommerceRoutes(ctx, service, options = {}) {
3040
3064
  order = await service.getOrder(id);
3041
3065
  if (!order) order = await service.getOrderByNumber(id);
3042
3066
  }
3043
- if (!order) return notFound(`Order '${id}' not found.`);
3044
- return json(order);
3067
+ if (!order) return notFound$2(`Order '${id}' not found.`);
3068
+ return json$2(order);
3045
3069
  });
3046
3070
  ctx.registerRoute("PATCH", `${prefix}/orders/:id/status`, async (req, { params }) => {
3047
3071
  try {
3048
3072
  const body = await req.json();
3049
- if (!body.status) return badRequest("New 'status' is required.");
3073
+ if (!body.status) return badRequest$2("New 'status' is required.");
3050
3074
  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);
3075
+ if (!updated) return notFound$2(`Order '${params.id}' not found.`);
3076
+ return json$2(updated);
3053
3077
  } catch (err) {
3054
- return badRequest(err instanceof Error ? err.message : String(err));
3078
+ return badRequest$2(err instanceof Error ? err.message : String(err));
3055
3079
  }
3056
3080
  });
3057
3081
  }
@@ -3235,6 +3259,3401 @@ function getEcommerceService(engine, options) {
3235
3259
  return service;
3236
3260
  }
3237
3261
  //#endregion
3262
+ //#region src/plugins/hrms/schemas.ts
3263
+ /**
3264
+ * Creates the collection configuration for Employers (Organizations / Companies).
3265
+ */
3266
+ function createEmployerCollection(options = {}) {
3267
+ return collection({
3268
+ slug: options.employerCollectionSlug ?? "employers",
3269
+ label: "Employers",
3270
+ singularLabel: "Employer",
3271
+ description: "Companies and organizational entities managing employees, shifts, and leave policies",
3272
+ timestamps: true,
3273
+ revisions: true,
3274
+ draftable: true,
3275
+ defaultSort: {
3276
+ field: "createdAt",
3277
+ direction: "desc"
3278
+ },
3279
+ fields: [
3280
+ fields.text({
3281
+ name: "companyName",
3282
+ label: "Company Name",
3283
+ required: true
3284
+ }),
3285
+ fields.slug({
3286
+ from: "companyName",
3287
+ unique: true
3288
+ }),
3289
+ fields.text({
3290
+ name: "legalName",
3291
+ label: "Legal Entity Name"
3292
+ }),
3293
+ fields.text({
3294
+ name: "taxId",
3295
+ label: "Tax / EIN Number"
3296
+ }),
3297
+ fields.text({
3298
+ name: "email",
3299
+ label: "Primary Email"
3300
+ }),
3301
+ fields.text({
3302
+ name: "phone",
3303
+ label: "Phone Number"
3304
+ }),
3305
+ fields.text({
3306
+ name: "website",
3307
+ label: "Website URL"
3308
+ }),
3309
+ fields.image({
3310
+ name: "logo",
3311
+ label: "Company Logo"
3312
+ }),
3313
+ fields.json({
3314
+ name: "address",
3315
+ label: "Company Address"
3316
+ }),
3317
+ fields.text({
3318
+ name: "timezone",
3319
+ label: "Primary Timezone",
3320
+ defaultValue: "UTC"
3321
+ }),
3322
+ fields.json({
3323
+ name: "workSchedule",
3324
+ label: "Standard Work Schedule",
3325
+ defaultValue: {
3326
+ startTime: options.workScheduleStart ?? "09:00",
3327
+ endTime: options.workScheduleEnd ?? "17:00",
3328
+ standardHoursPerDay: options.standardWorkDayHours ?? 8,
3329
+ gracePeriodMinutes: options.gracePeriodMinutes ?? 15,
3330
+ workDays: [
3331
+ 1,
3332
+ 2,
3333
+ 3,
3334
+ 4,
3335
+ 5
3336
+ ]
3337
+ }
3338
+ }),
3339
+ fields.select({
3340
+ name: "status",
3341
+ label: "Status",
3342
+ options: ["active", "inactive"],
3343
+ defaultValue: "active"
3344
+ })
3345
+ ]
3346
+ });
3347
+ }
3348
+ /**
3349
+ * Creates the collection configuration for Employees.
3350
+ */
3351
+ function createEmployeeCollection(options = {}) {
3352
+ const slug = options.employeeCollectionSlug ?? "employees";
3353
+ const employerSlug = options.employerCollectionSlug ?? "employers";
3354
+ return collection({
3355
+ slug,
3356
+ label: "Employees",
3357
+ singularLabel: "Employee",
3358
+ description: "Employee profiles, job assignments, emergency contacts, and attached records",
3359
+ timestamps: true,
3360
+ revisions: true,
3361
+ draftable: true,
3362
+ taxonomies: [options.departmentsTaxonomySlug ?? "hrms_departments", options.designationsTaxonomySlug ?? "hrms_designations"],
3363
+ defaultSort: {
3364
+ field: "createdAt",
3365
+ direction: "desc"
3366
+ },
3367
+ fields: [
3368
+ fields.relationship({
3369
+ name: "employerId",
3370
+ label: "Employer",
3371
+ targetCollection: employerSlug,
3372
+ required: true
3373
+ }),
3374
+ fields.text({
3375
+ name: "userId",
3376
+ label: "Associated User ID",
3377
+ description: "Optional reference to an authenticated user account"
3378
+ }),
3379
+ fields.text({
3380
+ name: "employeeNumber",
3381
+ label: "Employee Number",
3382
+ required: true,
3383
+ unique: true
3384
+ }),
3385
+ fields.text({
3386
+ name: "firstName",
3387
+ label: "First Name",
3388
+ required: true
3389
+ }),
3390
+ fields.text({
3391
+ name: "lastName",
3392
+ label: "Last Name",
3393
+ required: true
3394
+ }),
3395
+ fields.text({
3396
+ name: "email",
3397
+ label: "Work Email",
3398
+ required: true
3399
+ }),
3400
+ fields.text({
3401
+ name: "phone",
3402
+ label: "Contact Phone"
3403
+ }),
3404
+ fields.image({
3405
+ name: "avatar",
3406
+ label: "Profile Picture"
3407
+ }),
3408
+ fields.text({
3409
+ name: "jobTitle",
3410
+ label: "Job Title"
3411
+ }),
3412
+ fields.select({
3413
+ name: "employmentType",
3414
+ label: "Employment Type",
3415
+ options: [
3416
+ "full_time",
3417
+ "part_time",
3418
+ "contractor",
3419
+ "intern"
3420
+ ],
3421
+ defaultValue: "full_time"
3422
+ }),
3423
+ fields.select({
3424
+ name: "status",
3425
+ label: "Status",
3426
+ options: [
3427
+ "active",
3428
+ "on_leave",
3429
+ "terminated",
3430
+ "suspended"
3431
+ ],
3432
+ defaultValue: "active"
3433
+ }),
3434
+ fields.date({
3435
+ name: "hireDate",
3436
+ label: "Hire Date",
3437
+ required: true
3438
+ }),
3439
+ fields.date({
3440
+ name: "terminationDate",
3441
+ label: "Termination Date"
3442
+ }),
3443
+ fields.relationship({
3444
+ name: "managerId",
3445
+ label: "Reporting Manager",
3446
+ targetCollection: slug
3447
+ }),
3448
+ fields.json({
3449
+ name: "emergencyContact",
3450
+ label: "Emergency Contact"
3451
+ }),
3452
+ fields.repeater({
3453
+ name: "documents",
3454
+ label: "Attached Documents & Contracts",
3455
+ fields: [
3456
+ fields.text({
3457
+ name: "title",
3458
+ label: "Document Title",
3459
+ required: true
3460
+ }),
3461
+ fields.text({
3462
+ name: "fileUrl",
3463
+ label: "File URL",
3464
+ required: true
3465
+ }),
3466
+ fields.text({
3467
+ name: "category",
3468
+ label: "Category"
3469
+ }),
3470
+ fields.text({
3471
+ name: "uploadedAt",
3472
+ label: "Uploaded Date"
3473
+ })
3474
+ ]
3475
+ }),
3476
+ fields.number({
3477
+ name: "salary",
3478
+ label: "Base Salary"
3479
+ }),
3480
+ fields.text({
3481
+ name: "notes",
3482
+ label: "Internal Notes"
3483
+ })
3484
+ ]
3485
+ });
3486
+ }
3487
+ /**
3488
+ * Creates the collection configuration for Daily Attendance.
3489
+ */
3490
+ function createAttendanceCollection(options = {}) {
3491
+ const slug = options.attendanceCollectionSlug ?? "attendance";
3492
+ const employerSlug = options.employerCollectionSlug ?? "employers";
3493
+ const employeeSlug = options.employeeCollectionSlug ?? "employees";
3494
+ return collection({
3495
+ slug,
3496
+ label: "Attendance",
3497
+ singularLabel: "Attendance Record",
3498
+ description: "Daily check-in and check-out logs, hours worked, overtime, and punctuality status",
3499
+ timestamps: true,
3500
+ revisions: false,
3501
+ draftable: false,
3502
+ defaultSort: {
3503
+ field: "date",
3504
+ direction: "desc"
3505
+ },
3506
+ fields: [
3507
+ fields.relationship({
3508
+ name: "employerId",
3509
+ label: "Employer",
3510
+ targetCollection: employerSlug,
3511
+ required: true
3512
+ }),
3513
+ fields.relationship({
3514
+ name: "employeeId",
3515
+ label: "Employee",
3516
+ targetCollection: employeeSlug,
3517
+ required: true
3518
+ }),
3519
+ fields.date({
3520
+ name: "date",
3521
+ label: "Date",
3522
+ required: true
3523
+ }),
3524
+ fields.text({
3525
+ name: "checkInAt",
3526
+ label: "Check-In Timestamp",
3527
+ required: true
3528
+ }),
3529
+ fields.text({
3530
+ name: "checkOutAt",
3531
+ label: "Check-Out Timestamp"
3532
+ }),
3533
+ fields.number({
3534
+ name: "totalHours",
3535
+ label: "Total Hours",
3536
+ defaultValue: 0
3537
+ }),
3538
+ fields.number({
3539
+ name: "overtimeHours",
3540
+ label: "Overtime Hours",
3541
+ defaultValue: 0
3542
+ }),
3543
+ fields.select({
3544
+ name: "status",
3545
+ label: "Status",
3546
+ options: [
3547
+ "present",
3548
+ "late",
3549
+ "half_day",
3550
+ "absent",
3551
+ "on_leave"
3552
+ ],
3553
+ defaultValue: "present"
3554
+ }),
3555
+ fields.text({
3556
+ name: "location",
3557
+ label: "Location / IP / Geofence"
3558
+ }),
3559
+ fields.text({
3560
+ name: "notes",
3561
+ label: "Notes"
3562
+ })
3563
+ ]
3564
+ });
3565
+ }
3566
+ /**
3567
+ * Creates the collection configuration for Leave Types.
3568
+ */
3569
+ function createLeaveTypeCollection(options = {}) {
3570
+ const slug = options.leaveTypeCollectionSlug ?? "leave_types";
3571
+ const employerSlug = options.employerCollectionSlug ?? "employers";
3572
+ return collection({
3573
+ slug,
3574
+ label: "Leave Types",
3575
+ singularLabel: "Leave Type",
3576
+ description: "Available leave categories (Annual, Sick, Unpaid) and yearly quotas",
3577
+ timestamps: true,
3578
+ revisions: true,
3579
+ draftable: false,
3580
+ fields: [
3581
+ fields.relationship({
3582
+ name: "employerId",
3583
+ label: "Employer",
3584
+ targetCollection: employerSlug
3585
+ }),
3586
+ fields.text({
3587
+ name: "name",
3588
+ label: "Leave Name",
3589
+ required: true
3590
+ }),
3591
+ fields.text({
3592
+ name: "code",
3593
+ label: "Leave Code",
3594
+ required: true
3595
+ }),
3596
+ fields.number({
3597
+ name: "daysAllowedPerYear",
3598
+ label: "Days Allowed Per Year",
3599
+ required: true,
3600
+ defaultValue: 15,
3601
+ min: 0
3602
+ }),
3603
+ fields.boolean({
3604
+ name: "paid",
3605
+ label: "Paid Leave",
3606
+ defaultValue: true
3607
+ }),
3608
+ fields.boolean({
3609
+ name: "requiresApproval",
3610
+ label: "Requires Manager Approval",
3611
+ defaultValue: true
3612
+ }),
3613
+ fields.text({
3614
+ name: "color",
3615
+ label: "Display Color Code"
3616
+ }),
3617
+ fields.text({
3618
+ name: "description",
3619
+ label: "Description / Policy"
3620
+ })
3621
+ ]
3622
+ });
3623
+ }
3624
+ /**
3625
+ * Creates the collection configuration for Leave Requests.
3626
+ */
3627
+ function createLeaveRequestCollection(options = {}) {
3628
+ const slug = options.leaveRequestCollectionSlug ?? "leave_requests";
3629
+ const employerSlug = options.employerCollectionSlug ?? "employers";
3630
+ const employeeSlug = options.employeeCollectionSlug ?? "employees";
3631
+ const leaveTypeSlug = options.leaveTypeCollectionSlug ?? "leave_types";
3632
+ return collection({
3633
+ slug,
3634
+ label: "Leave Requests",
3635
+ singularLabel: "Leave Request",
3636
+ description: "Employee time-off requests, approval tracking, and deducted balances",
3637
+ timestamps: true,
3638
+ revisions: true,
3639
+ draftable: false,
3640
+ defaultSort: {
3641
+ field: "createdAt",
3642
+ direction: "desc"
3643
+ },
3644
+ fields: [
3645
+ fields.relationship({
3646
+ name: "employerId",
3647
+ label: "Employer",
3648
+ targetCollection: employerSlug,
3649
+ required: true
3650
+ }),
3651
+ fields.relationship({
3652
+ name: "employeeId",
3653
+ label: "Employee",
3654
+ targetCollection: employeeSlug,
3655
+ required: true
3656
+ }),
3657
+ fields.relationship({
3658
+ name: "leaveTypeId",
3659
+ label: "Leave Type",
3660
+ targetCollection: leaveTypeSlug,
3661
+ required: true
3662
+ }),
3663
+ fields.date({
3664
+ name: "startDate",
3665
+ label: "Start Date",
3666
+ required: true
3667
+ }),
3668
+ fields.date({
3669
+ name: "endDate",
3670
+ label: "End Date",
3671
+ required: true
3672
+ }),
3673
+ fields.number({
3674
+ name: "daysCount",
3675
+ label: "Days Count",
3676
+ required: true,
3677
+ min: .5
3678
+ }),
3679
+ fields.text({
3680
+ name: "reason",
3681
+ label: "Reason"
3682
+ }),
3683
+ fields.select({
3684
+ name: "status",
3685
+ label: "Request Status",
3686
+ options: [
3687
+ "pending",
3688
+ "approved",
3689
+ "rejected",
3690
+ "cancelled"
3691
+ ],
3692
+ defaultValue: "pending"
3693
+ }),
3694
+ fields.relationship({
3695
+ name: "approvedBy",
3696
+ label: "Approved / Rejected By",
3697
+ targetCollection: employeeSlug
3698
+ }),
3699
+ fields.text({
3700
+ name: "approvedAt",
3701
+ label: "Decision Timestamp"
3702
+ }),
3703
+ fields.text({
3704
+ name: "rejectionReason",
3705
+ label: "Rejection Reason"
3706
+ })
3707
+ ]
3708
+ });
3709
+ }
3710
+ /**
3711
+ * Creates the standard HRMS taxonomies: departments and designations.
3712
+ */
3713
+ function createHRMSTaxonomies(options = {}) {
3714
+ const employeeSlug = options.employeeCollectionSlug ?? "employees";
3715
+ const deptSlug = options.departmentsTaxonomySlug ?? "hrms_departments";
3716
+ const desigSlug = options.designationsTaxonomySlug ?? "hrms_designations";
3717
+ return [{
3718
+ slug: deptSlug,
3719
+ label: "Departments",
3720
+ singularLabel: "Department",
3721
+ hierarchical: true,
3722
+ postTypes: [employeeSlug],
3723
+ description: "Hierarchical departmental tree (e.g. Engineering, Sales, HR)"
3724
+ }, {
3725
+ slug: desigSlug,
3726
+ label: "Designations",
3727
+ singularLabel: "Designation",
3728
+ hierarchical: false,
3729
+ postTypes: [employeeSlug],
3730
+ description: "Job titles and designations across the workforce"
3731
+ }];
3732
+ }
3733
+ //#endregion
3734
+ //#region src/plugins/hrms/service.ts
3735
+ var HRMSService = class {
3736
+ engine;
3737
+ options;
3738
+ employerSlug;
3739
+ employeeSlug;
3740
+ attendanceSlug;
3741
+ leaveTypeSlug;
3742
+ leaveRequestSlug;
3743
+ departmentsTaxonomy;
3744
+ designationsTaxonomy;
3745
+ standardWorkDayHours;
3746
+ workScheduleStart;
3747
+ workScheduleEnd;
3748
+ gracePeriodMinutes;
3749
+ constructor(engine, options = {}) {
3750
+ this.engine = engine;
3751
+ this.options = options;
3752
+ this.employerSlug = options.employerCollectionSlug ?? "employers";
3753
+ this.employeeSlug = options.employeeCollectionSlug ?? "employees";
3754
+ this.attendanceSlug = options.attendanceCollectionSlug ?? "attendance";
3755
+ this.leaveTypeSlug = options.leaveTypeCollectionSlug ?? "leave_types";
3756
+ this.leaveRequestSlug = options.leaveRequestCollectionSlug ?? "leave_requests";
3757
+ this.departmentsTaxonomy = options.departmentsTaxonomySlug ?? "hrms_departments";
3758
+ this.designationsTaxonomy = options.designationsTaxonomySlug ?? "hrms_designations";
3759
+ this.standardWorkDayHours = options.standardWorkDayHours ?? 8;
3760
+ this.workScheduleStart = options.workScheduleStart ?? "09:00";
3761
+ this.workScheduleEnd = options.workScheduleEnd ?? "17:00";
3762
+ this.gracePeriodMinutes = options.gracePeriodMinutes ?? 15;
3763
+ }
3764
+ get employersCollection() {
3765
+ return this.engine.collection(this.employerSlug);
3766
+ }
3767
+ get employeesCollection() {
3768
+ return this.engine.collection(this.employeeSlug);
3769
+ }
3770
+ get attendanceCollection() {
3771
+ return this.engine.collection(this.attendanceSlug);
3772
+ }
3773
+ get leaveTypesCollection() {
3774
+ return this.engine.collection(this.leaveTypeSlug);
3775
+ }
3776
+ get leaveRequestsCollection() {
3777
+ return this.engine.collection(this.leaveRequestSlug);
3778
+ }
3779
+ async createEmployer(input, authorId) {
3780
+ const employerData = {
3781
+ companyName: input.companyName,
3782
+ legalName: input.legalName,
3783
+ taxId: input.taxId,
3784
+ email: input.email,
3785
+ phone: input.phone,
3786
+ website: input.website,
3787
+ logo: input.logo,
3788
+ address: input.address,
3789
+ timezone: input.timezone ?? "UTC",
3790
+ workSchedule: input.workSchedule ?? {
3791
+ startTime: this.workScheduleStart,
3792
+ endTime: this.workScheduleEnd,
3793
+ standardHoursPerDay: this.standardWorkDayHours,
3794
+ gracePeriodMinutes: this.gracePeriodMinutes,
3795
+ workDays: [
3796
+ 1,
3797
+ 2,
3798
+ 3,
3799
+ 4,
3800
+ 5
3801
+ ]
3802
+ },
3803
+ status: input.status ?? "active"
3804
+ };
3805
+ const employer = await this.employersCollection.create({
3806
+ title: input.companyName,
3807
+ status: input.status === "inactive" ? "draft" : "published",
3808
+ data: employerData
3809
+ }, authorId);
3810
+ await this.engine.hooks.doAction("hrms.employer_created", employer);
3811
+ return employer;
3812
+ }
3813
+ async getEmployer(id) {
3814
+ return this.employersCollection.findById(id);
3815
+ }
3816
+ async getEmployerBySlug(slug) {
3817
+ return this.employersCollection.findBySlug(slug);
3818
+ }
3819
+ async updateEmployer(id, input, authorId) {
3820
+ const existing = await this.getEmployer(id);
3821
+ if (!existing) return null;
3822
+ const updatedData = {
3823
+ ...existing.data,
3824
+ ...input.companyName !== void 0 ? { companyName: input.companyName } : {},
3825
+ ...input.legalName !== void 0 ? { legalName: input.legalName } : {},
3826
+ ...input.taxId !== void 0 ? { taxId: input.taxId } : {},
3827
+ ...input.email !== void 0 ? { email: input.email } : {},
3828
+ ...input.phone !== void 0 ? { phone: input.phone } : {},
3829
+ ...input.website !== void 0 ? { website: input.website } : {},
3830
+ ...input.logo !== void 0 ? { logo: input.logo } : {},
3831
+ ...input.address !== void 0 ? { address: input.address } : {},
3832
+ ...input.timezone !== void 0 ? { timezone: input.timezone } : {},
3833
+ ...input.workSchedule !== void 0 ? { workSchedule: input.workSchedule } : {},
3834
+ ...input.status !== void 0 ? { status: input.status } : {}
3835
+ };
3836
+ const updated = await this.employersCollection.update(id, {
3837
+ title: input.companyName ?? existing.title,
3838
+ data: updatedData
3839
+ }, authorId);
3840
+ if (updated) await this.engine.hooks.doAction("hrms.employer_updated", updated);
3841
+ return updated;
3842
+ }
3843
+ async listEmployers(query = {}) {
3844
+ const limit = query.limit ?? 20;
3845
+ const offset = query.page ? (query.page - 1) * limit : 0;
3846
+ let items = (await this.employersCollection.find({ limit: 500 })).items;
3847
+ if (query.status) items = items.filter((item) => item.data.status === query.status);
3848
+ const total = items.length;
3849
+ return {
3850
+ items: items.slice(offset, offset + limit),
3851
+ total,
3852
+ limit,
3853
+ offset,
3854
+ hasMore: offset + limit < total
3855
+ };
3856
+ }
3857
+ async createEmployee(input, authorId) {
3858
+ if (!await this.getEmployer(input.employerId)) throw new Error(`[HRMSService] Employer with ID '${input.employerId}' not found.`);
3859
+ if (await this.getEmployeeByNumber(input.employerId, input.employeeNumber)) throw new Error(`[HRMSService] Employee number '${input.employeeNumber}' is already registered for this employer.`);
3860
+ const employeeData = {
3861
+ employerId: input.employerId,
3862
+ userId: input.userId,
3863
+ employeeNumber: input.employeeNumber,
3864
+ firstName: input.firstName,
3865
+ lastName: input.lastName,
3866
+ email: input.email,
3867
+ phone: input.phone,
3868
+ avatar: input.avatar,
3869
+ jobTitle: input.jobTitle,
3870
+ employmentType: input.employmentType ?? "full_time",
3871
+ status: input.status ?? "active",
3872
+ hireDate: input.hireDate,
3873
+ managerId: input.managerId,
3874
+ emergencyContact: input.emergencyContact,
3875
+ documents: input.documents ?? [],
3876
+ salary: input.salary,
3877
+ notes: input.notes
3878
+ };
3879
+ const title = `${input.firstName} ${input.lastName}`;
3880
+ const employee = await this.employeesCollection.create({
3881
+ title,
3882
+ status: input.status === "terminated" ? "draft" : "published",
3883
+ data: employeeData
3884
+ }, authorId);
3885
+ if (input.departmentSlug) {
3886
+ const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, input.departmentSlug);
3887
+ if (term) await this.engine.taxonomies.assignTerms(employee.id, [term.id]);
3888
+ }
3889
+ await this.engine.hooks.doAction("hrms.employee_created", employee);
3890
+ return employee;
3891
+ }
3892
+ async getEmployee(id) {
3893
+ return this.employeesCollection.findById(id);
3894
+ }
3895
+ async getEmployeeByNumber(employerId, employeeNumber) {
3896
+ return (await this.employeesCollection.find({ limit: 500 })).items.find((e) => e.data.employerId === employerId && e.data.employeeNumber === employeeNumber) ?? null;
3897
+ }
3898
+ async updateEmployee(id, input, authorId) {
3899
+ const existing = await this.getEmployee(id);
3900
+ if (!existing) return null;
3901
+ const updatedData = {
3902
+ ...existing.data,
3903
+ ...input.employerId !== void 0 ? { employerId: input.employerId } : {},
3904
+ ...input.userId !== void 0 ? { userId: input.userId } : {},
3905
+ ...input.employeeNumber !== void 0 ? { employeeNumber: input.employeeNumber } : {},
3906
+ ...input.firstName !== void 0 ? { firstName: input.firstName } : {},
3907
+ ...input.lastName !== void 0 ? { lastName: input.lastName } : {},
3908
+ ...input.email !== void 0 ? { email: input.email } : {},
3909
+ ...input.phone !== void 0 ? { phone: input.phone } : {},
3910
+ ...input.avatar !== void 0 ? { avatar: input.avatar } : {},
3911
+ ...input.jobTitle !== void 0 ? { jobTitle: input.jobTitle } : {},
3912
+ ...input.employmentType !== void 0 ? { employmentType: input.employmentType } : {},
3913
+ ...input.status !== void 0 ? { status: input.status } : {},
3914
+ ...input.hireDate !== void 0 ? { hireDate: input.hireDate } : {},
3915
+ ...input.terminationDate !== void 0 ? { terminationDate: input.terminationDate } : {},
3916
+ ...input.managerId !== void 0 ? { managerId: input.managerId } : {},
3917
+ ...input.emergencyContact !== void 0 ? { emergencyContact: input.emergencyContact } : {},
3918
+ ...input.documents !== void 0 ? { documents: input.documents } : {},
3919
+ ...input.salary !== void 0 ? { salary: input.salary } : {},
3920
+ ...input.notes !== void 0 ? { notes: input.notes } : {}
3921
+ };
3922
+ const title = input.firstName || input.lastName ? `${updatedData.firstName} ${updatedData.lastName}` : existing.title;
3923
+ const updated = await this.employeesCollection.update(id, {
3924
+ title,
3925
+ data: updatedData
3926
+ }, authorId);
3927
+ if (updated) {
3928
+ if (input.departmentSlug) {
3929
+ const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, input.departmentSlug);
3930
+ if (term) await this.engine.taxonomies.assignTerms(id, [term.id]);
3931
+ }
3932
+ await this.engine.hooks.doAction("hrms.employee_updated", updated);
3933
+ }
3934
+ return updated;
3935
+ }
3936
+ async deleteEmployee(id) {
3937
+ const deleted = await this.employeesCollection.delete(id);
3938
+ if (deleted) await this.engine.hooks.doAction("hrms.employee_deleted", id);
3939
+ return deleted;
3940
+ }
3941
+ async listEmployees(query = {}) {
3942
+ const limit = query.limit ?? 20;
3943
+ const offset = query.page ? (query.page - 1) * limit : 0;
3944
+ let termIds;
3945
+ if (query.department) {
3946
+ const term = await this.engine.taxonomies.getTermBySlug(this.departmentsTaxonomy, query.department);
3947
+ if (term) termIds = [term.id];
3948
+ else return {
3949
+ items: [],
3950
+ total: 0,
3951
+ limit,
3952
+ offset,
3953
+ hasMore: false
3954
+ };
3955
+ }
3956
+ let items = (await this.employeesCollection.find({
3957
+ termIds,
3958
+ limit: 1e3
3959
+ })).items;
3960
+ if (query.employerId) items = items.filter((e) => e.data.employerId === query.employerId);
3961
+ if (query.employmentType) items = items.filter((e) => e.data.employmentType === query.employmentType);
3962
+ if (query.status) items = items.filter((e) => e.data.status === query.status);
3963
+ if (query.search) {
3964
+ const s = query.search.toLowerCase();
3965
+ 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));
3966
+ }
3967
+ const total = items.length;
3968
+ return {
3969
+ items: items.slice(offset, offset + limit),
3970
+ total,
3971
+ limit,
3972
+ offset,
3973
+ hasMore: offset + limit < total
3974
+ };
3975
+ }
3976
+ async getDirectReports(managerId) {
3977
+ return (await this.employeesCollection.find({ limit: 1e3 })).items.filter((e) => e.data.managerId === managerId);
3978
+ }
3979
+ parseTimeToMinutes(timeStr) {
3980
+ const [hours, minutes] = timeStr.split(":").map((v) => parseInt(v, 10));
3981
+ return (hours || 0) * 60 + (minutes || 0);
3982
+ }
3983
+ formatDateString(date, timezone) {
3984
+ if (!timezone || timezone === "UTC") return date.toISOString().slice(0, 10);
3985
+ try {
3986
+ return new Intl.DateTimeFormat("en-CA", {
3987
+ timeZone: timezone,
3988
+ year: "numeric",
3989
+ month: "2-digit",
3990
+ day: "2-digit"
3991
+ }).format(date);
3992
+ } catch {
3993
+ return date.toISOString().slice(0, 10);
3994
+ }
3995
+ }
3996
+ getHoursAndMinutes(date, timezone) {
3997
+ if (!timezone || timezone === "UTC") return {
3998
+ hours: date.getUTCHours(),
3999
+ minutes: date.getUTCMinutes()
4000
+ };
4001
+ try {
4002
+ const parts = new Intl.DateTimeFormat("en-US", {
4003
+ timeZone: timezone,
4004
+ hour: "numeric",
4005
+ minute: "numeric",
4006
+ hour12: false
4007
+ }).formatToParts(date);
4008
+ const hoursPart = parts.find((p) => p.type === "hour");
4009
+ const minutesPart = parts.find((p) => p.type === "minute");
4010
+ return {
4011
+ hours: hoursPart ? parseInt(hoursPart.value, 10) : date.getUTCHours(),
4012
+ minutes: minutesPart ? parseInt(minutesPart.value, 10) : date.getUTCMinutes()
4013
+ };
4014
+ } catch {
4015
+ return {
4016
+ hours: date.getUTCHours(),
4017
+ minutes: date.getUTCMinutes()
4018
+ };
4019
+ }
4020
+ }
4021
+ /**
4022
+ * Check in an employee for today (or specified timestamp).
4023
+ */
4024
+ async checkIn(input) {
4025
+ const employee = await this.getEmployee(input.employeeId);
4026
+ if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
4027
+ const employer = await this.getEmployer(employee.data.employerId);
4028
+ const timezone = employer?.data?.timezone ?? "UTC";
4029
+ const checkInDate = input.timestamp ? new Date(input.timestamp) : /* @__PURE__ */ new Date();
4030
+ const dateStr = this.formatDateString(checkInDate, timezone);
4031
+ const existing = await this.getDailyAttendance(employee.id, dateStr);
4032
+ if (existing && existing.data.checkInAt) throw new Error(`[HRMSService] Employee '${employee.id}' is already checked in for date ${dateStr}.`);
4033
+ const schedule = employer?.data?.workSchedule;
4034
+ const schedStart = schedule?.startTime ?? this.workScheduleStart;
4035
+ const grace = schedule?.gracePeriodMinutes ?? this.gracePeriodMinutes;
4036
+ const schedMinutes = this.parseTimeToMinutes(schedStart);
4037
+ const { hours: punchHours, minutes: punchMins } = this.getHoursAndMinutes(checkInDate, timezone);
4038
+ const status = punchHours * 60 + punchMins > schedMinutes + grace ? "late" : "present";
4039
+ const attendanceData = {
4040
+ employerId: employee.data.employerId,
4041
+ employeeId: employee.id,
4042
+ date: dateStr,
4043
+ checkInAt: checkInDate.toISOString(),
4044
+ totalHours: 0,
4045
+ overtimeHours: 0,
4046
+ status,
4047
+ location: input.location,
4048
+ notes: input.notes
4049
+ };
4050
+ const attendance = await this.attendanceCollection.create({
4051
+ title: `${employee.title} - ${dateStr}`,
4052
+ status: "published",
4053
+ data: attendanceData
4054
+ });
4055
+ await this.engine.hooks.doAction("hrms.checked_in", attendance, employee);
4056
+ return attendance;
4057
+ }
4058
+ /**
4059
+ * Check out an employee for today (or specified timestamp).
4060
+ */
4061
+ async checkOut(input) {
4062
+ const employee = await this.getEmployee(input.employeeId);
4063
+ if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
4064
+ const employer = await this.getEmployer(employee.data.employerId);
4065
+ const timezone = employer?.data?.timezone ?? "UTC";
4066
+ const checkOutDate = input.timestamp ? new Date(input.timestamp) : /* @__PURE__ */ new Date();
4067
+ const dateStr = this.formatDateString(checkOutDate, timezone);
4068
+ const record = await this.getDailyAttendance(employee.id, dateStr);
4069
+ if (!record || !record.data.checkInAt) throw new Error(`[HRMSService] No active check-in record found for employee '${employee.id}' on ${dateStr}.`);
4070
+ if (record.data.checkOutAt) throw new Error(`[HRMSService] Employee '${employee.id}' has already checked out for date ${dateStr}.`);
4071
+ const checkInDate = new Date(record.data.checkInAt);
4072
+ const durationMs = Math.max(0, checkOutDate.getTime() - checkInDate.getTime());
4073
+ const totalHours = Math.round(durationMs / (1e3 * 60 * 60) * 100) / 100;
4074
+ const standardHours = employer?.data?.workSchedule?.standardHoursPerDay ?? this.standardWorkDayHours;
4075
+ const overtimeHours = Math.max(0, Math.round((totalHours - standardHours) * 100) / 100);
4076
+ let status = record.data.status;
4077
+ if (totalHours < standardHours / 2 && status === "present") status = "half_day";
4078
+ const updatedData = {
4079
+ ...record.data,
4080
+ checkOutAt: checkOutDate.toISOString(),
4081
+ totalHours,
4082
+ overtimeHours,
4083
+ status,
4084
+ ...input.location ? { location: input.location } : {},
4085
+ ...input.notes ? { notes: input.notes } : {}
4086
+ };
4087
+ const updated = await this.attendanceCollection.update(record.id, { data: updatedData });
4088
+ if (!updated) throw new Error(`[HRMSService] Failed to update attendance record for '${employee.id}'.`);
4089
+ await this.engine.hooks.doAction("hrms.checked_out", updated, employee);
4090
+ return updated;
4091
+ }
4092
+ async getDailyAttendance(employeeId, date) {
4093
+ return (await this.attendanceCollection.find({ limit: 1e3 })).items.find((a) => a.data.employeeId === employeeId && a.data.date === date) ?? null;
4094
+ }
4095
+ async recordAttendanceManual(input) {
4096
+ const existing = await this.getDailyAttendance(input.employeeId, input.date);
4097
+ const attendanceData = {
4098
+ employerId: input.employerId,
4099
+ employeeId: input.employeeId,
4100
+ date: input.date,
4101
+ checkInAt: input.checkInAt,
4102
+ checkOutAt: input.checkOutAt,
4103
+ totalHours: input.totalHours ?? 0,
4104
+ overtimeHours: input.overtimeHours ?? 0,
4105
+ status: input.status ?? "present",
4106
+ location: input.location,
4107
+ notes: input.notes
4108
+ };
4109
+ if (existing) {
4110
+ const updated = await this.attendanceCollection.update(existing.id, { data: attendanceData });
4111
+ if (!updated) throw new Error(`[HRMSService] Failed to update attendance for '${input.employeeId}'.`);
4112
+ return updated;
4113
+ }
4114
+ return this.attendanceCollection.create({
4115
+ title: `${input.employeeId} - ${input.date}`,
4116
+ status: "published",
4117
+ data: attendanceData
4118
+ });
4119
+ }
4120
+ async listAttendance(query = {}) {
4121
+ const limit = query.limit ?? 30;
4122
+ const offset = query.page ? (query.page - 1) * limit : 0;
4123
+ let items = (await this.attendanceCollection.find({ limit: 2e3 })).items;
4124
+ if (query.employerId) items = items.filter((a) => a.data.employerId === query.employerId);
4125
+ if (query.employeeId) items = items.filter((a) => a.data.employeeId === query.employeeId);
4126
+ if (query.date) items = items.filter((a) => a.data.date === query.date);
4127
+ if (query.startDate) items = items.filter((a) => a.data.date >= query.startDate);
4128
+ if (query.endDate) items = items.filter((a) => a.data.date <= query.endDate);
4129
+ if (query.status) items = items.filter((a) => a.data.status === query.status);
4130
+ const total = items.length;
4131
+ return {
4132
+ items: items.slice(offset, offset + limit),
4133
+ total,
4134
+ limit,
4135
+ offset,
4136
+ hasMore: offset + limit < total
4137
+ };
4138
+ }
4139
+ async createLeaveType(input, authorId) {
4140
+ const leaveTypeData = {
4141
+ employerId: input.employerId,
4142
+ name: input.name,
4143
+ code: input.code.toUpperCase(),
4144
+ daysAllowedPerYear: input.daysAllowedPerYear,
4145
+ paid: input.paid ?? true,
4146
+ requiresApproval: input.requiresApproval ?? true,
4147
+ color: input.color,
4148
+ description: input.description
4149
+ };
4150
+ const item = await this.leaveTypesCollection.create({
4151
+ title: input.name,
4152
+ status: "published",
4153
+ data: leaveTypeData
4154
+ }, authorId);
4155
+ await this.engine.hooks.doAction("hrms.leave_type_created", item);
4156
+ return item;
4157
+ }
4158
+ async getLeaveType(id) {
4159
+ return this.leaveTypesCollection.findById(id);
4160
+ }
4161
+ async listLeaveTypes(employerId) {
4162
+ const result = await this.leaveTypesCollection.find({ limit: 100 });
4163
+ if (!employerId) return result.items;
4164
+ return result.items.filter((lt) => !lt.data.employerId || lt.data.employerId === employerId);
4165
+ }
4166
+ /**
4167
+ * Calculate leave balance report for an employee for a given year.
4168
+ */
4169
+ async calculateLeaveBalance(employeeId, year) {
4170
+ const targetYear = year ?? (/* @__PURE__ */ new Date()).getFullYear();
4171
+ const employee = await this.getEmployee(employeeId);
4172
+ if (!employee) throw new Error(`[HRMSService] Employee with ID '${employeeId}' not found.`);
4173
+ const leaveTypes = await this.listLeaveTypes(employee.data.employerId);
4174
+ const allRequests = await this.leaveRequestsCollection.find({ limit: 1e3 });
4175
+ const yearStr = String(targetYear);
4176
+ const employeeRequests = allRequests.items.filter((r) => r.data.employeeId === employeeId && (r.data.startDate.startsWith(yearStr) || r.data.endDate.startsWith(yearStr)));
4177
+ const balances = leaveTypes.map((lt) => {
4178
+ const approved = employeeRequests.filter((r) => r.data.leaveTypeId === lt.id && r.data.status === "approved");
4179
+ const pending = employeeRequests.filter((r) => r.data.leaveTypeId === lt.id && r.data.status === "pending");
4180
+ const usedDays = approved.reduce((sum, r) => sum + (r.data.daysCount || 0), 0);
4181
+ const pendingDays = pending.reduce((sum, r) => sum + (r.data.daysCount || 0), 0);
4182
+ const allocatedDays = lt.data.daysAllowedPerYear ?? 0;
4183
+ const remainingDays = Math.max(0, allocatedDays - usedDays);
4184
+ return {
4185
+ leaveTypeId: lt.id,
4186
+ leaveTypeName: lt.data.name,
4187
+ leaveTypeCode: lt.data.code,
4188
+ allocatedDays,
4189
+ usedDays,
4190
+ pendingDays,
4191
+ remainingDays
4192
+ };
4193
+ });
4194
+ const report = {
4195
+ employeeId,
4196
+ year: targetYear,
4197
+ balances,
4198
+ totalAllocated: balances.reduce((sum, b) => sum + b.allocatedDays, 0),
4199
+ totalUsed: balances.reduce((sum, b) => sum + b.usedDays, 0),
4200
+ totalRemaining: balances.reduce((sum, b) => sum + b.remainingDays, 0)
4201
+ };
4202
+ return this.engine.hooks.applyFilters("hrms.calculate_leave_balance", report, employeeId, targetYear);
4203
+ }
4204
+ /**
4205
+ * Submit a new leave request.
4206
+ */
4207
+ async requestLeave(input, authorId) {
4208
+ const employee = await this.getEmployee(input.employeeId);
4209
+ if (!employee) throw new Error(`[HRMSService] Employee with ID '${input.employeeId}' not found.`);
4210
+ const leaveType = await this.getLeaveType(input.leaveTypeId);
4211
+ if (!leaveType) throw new Error(`[HRMSService] Leave type with ID '${input.leaveTypeId}' not found.`);
4212
+ if (input.startDate > input.endDate) throw new Error(`[HRMSService] Start date '${input.startDate}' cannot be after end date '${input.endDate}'.`);
4213
+ const start = new Date(input.startDate);
4214
+ const end = new Date(input.endDate);
4215
+ const calculatedDays = Math.max(1, Math.round((end.getTime() - start.getTime()) / (1e3 * 60 * 60 * 24)) + 1);
4216
+ const daysCount = input.daysCount ?? calculatedDays;
4217
+ const startYear = start.getFullYear();
4218
+ const balance = (await this.calculateLeaveBalance(employee.id, startYear)).balances.find((b) => b.leaveTypeId === leaveType.id);
4219
+ if (balance && leaveType.data.requiresApproval && daysCount > balance.remainingDays) throw new Error(`[HRMSService] Insufficient leave balance for ${leaveType.data.name}. Requested: ${daysCount}, Remaining: ${balance.remainingDays}.`);
4220
+ const leaveRequestData = {
4221
+ employerId: input.employerId ?? employee.data.employerId,
4222
+ employeeId: employee.id,
4223
+ leaveTypeId: leaveType.id,
4224
+ startDate: input.startDate,
4225
+ endDate: input.endDate,
4226
+ daysCount,
4227
+ reason: input.reason,
4228
+ status: "pending"
4229
+ };
4230
+ const item = await this.leaveRequestsCollection.create({
4231
+ title: `${employee.title} - ${leaveType.data.name} (${input.startDate})`,
4232
+ status: "published",
4233
+ data: leaveRequestData
4234
+ }, authorId);
4235
+ await this.engine.hooks.doAction("hrms.leave_requested", item, employee);
4236
+ return item;
4237
+ }
4238
+ /**
4239
+ * Approve a pending leave request.
4240
+ */
4241
+ async approveLeave(input) {
4242
+ const request = await this.leaveRequestsCollection.findById(input.requestId);
4243
+ if (!request) throw new Error(`[HRMSService] Leave request with ID '${input.requestId}' not found.`);
4244
+ if (request.data.status !== "pending") throw new Error(`[HRMSService] Cannot approve leave request with status '${request.data.status}'.`);
4245
+ const updated = await this.leaveRequestsCollection.update(request.id, { data: {
4246
+ ...request.data,
4247
+ status: "approved",
4248
+ approvedBy: input.approverId,
4249
+ approvedAt: (/* @__PURE__ */ new Date()).toISOString()
4250
+ } });
4251
+ if (!updated) throw new Error(`[HRMSService] Failed to update leave request.`);
4252
+ await this.engine.hooks.doAction("hrms.leave_approved", updated);
4253
+ return updated;
4254
+ }
4255
+ /**
4256
+ * Reject a pending leave request.
4257
+ */
4258
+ async rejectLeave(input) {
4259
+ const request = await this.leaveRequestsCollection.findById(input.requestId);
4260
+ if (!request) throw new Error(`[HRMSService] Leave request with ID '${input.requestId}' not found.`);
4261
+ if (request.data.status !== "pending") throw new Error(`[HRMSService] Cannot reject leave request with status '${request.data.status}'.`);
4262
+ const updated = await this.leaveRequestsCollection.update(request.id, { data: {
4263
+ ...request.data,
4264
+ status: "rejected",
4265
+ approvedBy: input.approverId,
4266
+ rejectionReason: input.reason,
4267
+ approvedAt: (/* @__PURE__ */ new Date()).toISOString()
4268
+ } });
4269
+ if (!updated) throw new Error(`[HRMSService] Failed to update leave request.`);
4270
+ await this.engine.hooks.doAction("hrms.leave_rejected", updated);
4271
+ return updated;
4272
+ }
4273
+ /**
4274
+ * Cancel a leave request.
4275
+ */
4276
+ async cancelLeave(requestId) {
4277
+ const request = await this.leaveRequestsCollection.findById(requestId);
4278
+ if (!request) throw new Error(`[HRMSService] Leave request with ID '${requestId}' not found.`);
4279
+ if (request.data.status === "cancelled") return request;
4280
+ const updated = await this.leaveRequestsCollection.update(request.id, { data: {
4281
+ ...request.data,
4282
+ status: "cancelled"
4283
+ } });
4284
+ if (!updated) throw new Error(`[HRMSService] Failed to cancel leave request.`);
4285
+ await this.engine.hooks.doAction("hrms.leave_cancelled", updated);
4286
+ return updated;
4287
+ }
4288
+ async listLeaveRequests(query = {}) {
4289
+ const limit = query.limit ?? 20;
4290
+ const offset = query.page ? (query.page - 1) * limit : 0;
4291
+ let items = (await this.leaveRequestsCollection.find({ limit: 1e3 })).items;
4292
+ if (query.employerId) items = items.filter((r) => r.data.employerId === query.employerId);
4293
+ if (query.employeeId) items = items.filter((r) => r.data.employeeId === query.employeeId);
4294
+ if (query.leaveTypeId) items = items.filter((r) => r.data.leaveTypeId === query.leaveTypeId);
4295
+ if (query.status) items = items.filter((r) => r.data.status === query.status);
4296
+ if (query.year) {
4297
+ const yearStr = String(query.year);
4298
+ items = items.filter((r) => r.data.startDate.startsWith(yearStr) || r.data.endDate.startsWith(yearStr));
4299
+ }
4300
+ const total = items.length;
4301
+ return {
4302
+ items: items.slice(offset, offset + limit),
4303
+ total,
4304
+ limit,
4305
+ offset,
4306
+ hasMore: offset + limit < total
4307
+ };
4308
+ }
4309
+ };
4310
+ //#endregion
4311
+ //#region src/plugins/hrms/routes.ts
4312
+ function json$1(data, status = 200) {
4313
+ return new Response(JSON.stringify(data), {
4314
+ status,
4315
+ headers: {
4316
+ "Content-Type": "application/json",
4317
+ "Access-Control-Allow-Origin": "*"
4318
+ }
4319
+ });
4320
+ }
4321
+ function badRequest$1(message) {
4322
+ return json$1({ error: message }, 400);
4323
+ }
4324
+ function notFound$1(message) {
4325
+ return json$1({ error: message }, 404);
4326
+ }
4327
+ function registerHRMSRoutes(ctx, service, options = {}) {
4328
+ const prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
4329
+ ctx.registerRoute("GET", `${prefix}/employers`, async (_req, { url }) => {
4330
+ try {
4331
+ const status = url.searchParams.get("status");
4332
+ const pageStr = url.searchParams.get("page");
4333
+ const limitStr = url.searchParams.get("limit");
4334
+ const page = pageStr ? parseInt(pageStr, 10) : void 0;
4335
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
4336
+ return json$1(await service.listEmployers({
4337
+ status,
4338
+ page,
4339
+ limit
4340
+ }));
4341
+ } catch (err) {
4342
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4343
+ }
4344
+ });
4345
+ ctx.registerRoute("POST", `${prefix}/employers`, async (req) => {
4346
+ try {
4347
+ const body = await req.json();
4348
+ if (!body.companyName) return badRequest$1("companyName is required.");
4349
+ return json$1(await service.createEmployer(body), 201);
4350
+ } catch (err) {
4351
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4352
+ }
4353
+ });
4354
+ ctx.registerRoute("GET", `${prefix}/employers/:id`, async (_req, { params }) => {
4355
+ try {
4356
+ const employer = await service.getEmployer(params.id);
4357
+ if (!employer) return notFound$1(`Employer '${params.id}' not found.`);
4358
+ return json$1(employer);
4359
+ } catch (err) {
4360
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4361
+ }
4362
+ });
4363
+ ctx.registerRoute("PUT", `${prefix}/employers/:id`, async (req, { params }) => {
4364
+ try {
4365
+ const body = await req.json();
4366
+ const updated = await service.updateEmployer(params.id, body);
4367
+ if (!updated) return notFound$1(`Employer '${params.id}' not found.`);
4368
+ return json$1(updated);
4369
+ } catch (err) {
4370
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4371
+ }
4372
+ });
4373
+ ctx.registerRoute("GET", `${prefix}/employees`, async (_req, { url }) => {
4374
+ try {
4375
+ const employerId = url.searchParams.get("employerId") ?? void 0;
4376
+ const department = url.searchParams.get("department") ?? void 0;
4377
+ const employmentType = url.searchParams.get("employmentType");
4378
+ const status = url.searchParams.get("status");
4379
+ const search = url.searchParams.get("search") ?? void 0;
4380
+ const pageStr = url.searchParams.get("page");
4381
+ const limitStr = url.searchParams.get("limit");
4382
+ const page = pageStr ? parseInt(pageStr, 10) : void 0;
4383
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
4384
+ return json$1(await service.listEmployees({
4385
+ employerId,
4386
+ department,
4387
+ employmentType,
4388
+ status,
4389
+ search,
4390
+ page,
4391
+ limit
4392
+ }));
4393
+ } catch (err) {
4394
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4395
+ }
4396
+ });
4397
+ ctx.registerRoute("POST", `${prefix}/employees`, async (req) => {
4398
+ try {
4399
+ const body = await req.json();
4400
+ if (!body.employerId) return badRequest$1("employerId is required.");
4401
+ if (!body.employeeNumber) return badRequest$1("employeeNumber is required.");
4402
+ if (!body.firstName || !body.lastName) return badRequest$1("firstName and lastName are required.");
4403
+ if (!body.email) return badRequest$1("email is required.");
4404
+ return json$1(await service.createEmployee(body), 201);
4405
+ } catch (err) {
4406
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4407
+ }
4408
+ });
4409
+ ctx.registerRoute("GET", `${prefix}/employees/:id`, async (_req, { params }) => {
4410
+ try {
4411
+ const employee = await service.getEmployee(params.id);
4412
+ if (!employee) return notFound$1(`Employee '${params.id}' not found.`);
4413
+ return json$1(employee);
4414
+ } catch (err) {
4415
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4416
+ }
4417
+ });
4418
+ ctx.registerRoute("PUT", `${prefix}/employees/:id`, async (req, { params }) => {
4419
+ try {
4420
+ const body = await req.json();
4421
+ const updated = await service.updateEmployee(params.id, body);
4422
+ if (!updated) return notFound$1(`Employee '${params.id}' not found.`);
4423
+ return json$1(updated);
4424
+ } catch (err) {
4425
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4426
+ }
4427
+ });
4428
+ ctx.registerRoute("DELETE", `${prefix}/employees/:id`, async (_req, { params }) => {
4429
+ try {
4430
+ if (!await service.deleteEmployee(params.id)) return notFound$1(`Employee '${params.id}' not found.`);
4431
+ return json$1({ success: true });
4432
+ } catch (err) {
4433
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4434
+ }
4435
+ });
4436
+ ctx.registerRoute("GET", `${prefix}/employees/:id/leave-balance`, async (_req, { params, url }) => {
4437
+ try {
4438
+ const yearStr = url.searchParams.get("year");
4439
+ const year = yearStr ? parseInt(yearStr, 10) : void 0;
4440
+ return json$1(await service.calculateLeaveBalance(params.id, year));
4441
+ } catch (err) {
4442
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4443
+ }
4444
+ });
4445
+ ctx.registerRoute("GET", `${prefix}/employees/:id/direct-reports`, async (_req, { params }) => {
4446
+ try {
4447
+ const reports = await service.getDirectReports(params.id);
4448
+ return json$1({
4449
+ items: reports,
4450
+ total: reports.length
4451
+ });
4452
+ } catch (err) {
4453
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4454
+ }
4455
+ });
4456
+ ctx.registerRoute("POST", `${prefix}/attendance/check-in`, async (req) => {
4457
+ try {
4458
+ const body = await req.json();
4459
+ if (!body.employeeId) return badRequest$1("employeeId is required.");
4460
+ return json$1(await service.checkIn(body), 201);
4461
+ } catch (err) {
4462
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4463
+ }
4464
+ });
4465
+ ctx.registerRoute("POST", `${prefix}/attendance/check-out`, async (req) => {
4466
+ try {
4467
+ const body = await req.json();
4468
+ if (!body.employeeId) return badRequest$1("employeeId is required.");
4469
+ return json$1(await service.checkOut(body));
4470
+ } catch (err) {
4471
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4472
+ }
4473
+ });
4474
+ ctx.registerRoute("GET", `${prefix}/attendance`, async (_req, { url }) => {
4475
+ try {
4476
+ const employerId = url.searchParams.get("employerId") ?? void 0;
4477
+ const employeeId = url.searchParams.get("employeeId") ?? void 0;
4478
+ const date = url.searchParams.get("date") ?? void 0;
4479
+ const startDate = url.searchParams.get("startDate") ?? void 0;
4480
+ const endDate = url.searchParams.get("endDate") ?? void 0;
4481
+ const status = url.searchParams.get("status");
4482
+ const pageStr = url.searchParams.get("page");
4483
+ const limitStr = url.searchParams.get("limit");
4484
+ const page = pageStr ? parseInt(pageStr, 10) : void 0;
4485
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
4486
+ return json$1(await service.listAttendance({
4487
+ employerId,
4488
+ employeeId,
4489
+ date,
4490
+ startDate,
4491
+ endDate,
4492
+ status,
4493
+ page,
4494
+ limit
4495
+ }));
4496
+ } catch (err) {
4497
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4498
+ }
4499
+ });
4500
+ ctx.registerRoute("POST", `${prefix}/attendance/manual`, async (req) => {
4501
+ try {
4502
+ const body = await req.json();
4503
+ if (!body.employerId || !body.employeeId || !body.date || !body.checkInAt) return badRequest$1("employerId, employeeId, date, and checkInAt are required.");
4504
+ return json$1(await service.recordAttendanceManual(body), 201);
4505
+ } catch (err) {
4506
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4507
+ }
4508
+ });
4509
+ ctx.registerRoute("GET", `${prefix}/leave-types`, async (_req, { url }) => {
4510
+ try {
4511
+ const employerId = url.searchParams.get("employerId") ?? void 0;
4512
+ const types = await service.listLeaveTypes(employerId);
4513
+ return json$1({
4514
+ items: types,
4515
+ total: types.length
4516
+ });
4517
+ } catch (err) {
4518
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4519
+ }
4520
+ });
4521
+ ctx.registerRoute("POST", `${prefix}/leave-types`, async (req) => {
4522
+ try {
4523
+ const body = await req.json();
4524
+ if (!body.name || !body.code || body.daysAllowedPerYear === void 0) return badRequest$1("name, code, and daysAllowedPerYear are required.");
4525
+ return json$1(await service.createLeaveType(body), 201);
4526
+ } catch (err) {
4527
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4528
+ }
4529
+ });
4530
+ ctx.registerRoute("GET", `${prefix}/leave-types/:id`, async (_req, { params }) => {
4531
+ try {
4532
+ const leaveType = await service.getLeaveType(params.id);
4533
+ if (!leaveType) return notFound$1(`Leave type '${params.id}' not found.`);
4534
+ return json$1(leaveType);
4535
+ } catch (err) {
4536
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4537
+ }
4538
+ });
4539
+ ctx.registerRoute("GET", `${prefix}/leave-requests`, async (_req, { url }) => {
4540
+ try {
4541
+ const employerId = url.searchParams.get("employerId") ?? void 0;
4542
+ const employeeId = url.searchParams.get("employeeId") ?? void 0;
4543
+ const leaveTypeId = url.searchParams.get("leaveTypeId") ?? void 0;
4544
+ const status = url.searchParams.get("status");
4545
+ const yearStr = url.searchParams.get("year");
4546
+ const year = yearStr ? parseInt(yearStr, 10) : void 0;
4547
+ const pageStr = url.searchParams.get("page");
4548
+ const limitStr = url.searchParams.get("limit");
4549
+ const page = pageStr ? parseInt(pageStr, 10) : void 0;
4550
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
4551
+ return json$1(await service.listLeaveRequests({
4552
+ employerId,
4553
+ employeeId,
4554
+ leaveTypeId,
4555
+ status,
4556
+ year,
4557
+ page,
4558
+ limit
4559
+ }));
4560
+ } catch (err) {
4561
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4562
+ }
4563
+ });
4564
+ ctx.registerRoute("POST", `${prefix}/leave-requests`, async (req) => {
4565
+ try {
4566
+ const body = await req.json();
4567
+ if (!body.employeeId || !body.leaveTypeId || !body.startDate || !body.endDate) return badRequest$1("employeeId, leaveTypeId, startDate, and endDate are required.");
4568
+ return json$1(await service.requestLeave(body), 201);
4569
+ } catch (err) {
4570
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4571
+ }
4572
+ });
4573
+ ctx.registerRoute("POST", `${prefix}/leave-requests/:id/approve`, async (req, { params }) => {
4574
+ try {
4575
+ const approverId = (await req.json().catch(() => ({}))).approverId ?? "admin";
4576
+ return json$1(await service.approveLeave({
4577
+ requestId: params.id,
4578
+ approverId
4579
+ }));
4580
+ } catch (err) {
4581
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4582
+ }
4583
+ });
4584
+ ctx.registerRoute("POST", `${prefix}/leave-requests/:id/reject`, async (req, { params }) => {
4585
+ try {
4586
+ const body = await req.json().catch(() => ({}));
4587
+ const approverId = body.approverId ?? "admin";
4588
+ return json$1(await service.rejectLeave({
4589
+ requestId: params.id,
4590
+ approverId,
4591
+ reason: body.reason
4592
+ }));
4593
+ } catch (err) {
4594
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4595
+ }
4596
+ });
4597
+ ctx.registerRoute("POST", `${prefix}/leave-requests/:id/cancel`, async (_req, { params }) => {
4598
+ try {
4599
+ return json$1(await service.cancelLeave(params.id));
4600
+ } catch (err) {
4601
+ return badRequest$1(err instanceof Error ? err.message : String(err));
4602
+ }
4603
+ });
4604
+ }
4605
+ //#endregion
4606
+ //#region src/plugins/hrms/client.ts
4607
+ var HRMSClient = class {
4608
+ client;
4609
+ options;
4610
+ service;
4611
+ prefix;
4612
+ constructor(client, options = {}) {
4613
+ this.client = client;
4614
+ this.options = options;
4615
+ this.prefix = (options.apiPrefix ?? "/api/hrms").replace(/\/+$/, "");
4616
+ const engine = client.getEngine();
4617
+ if (engine) this.service = new HRMSService(engine, options);
4618
+ }
4619
+ employers = {
4620
+ find: async (query = {}) => {
4621
+ if (this.service) return this.service.listEmployers(query);
4622
+ const params = new URLSearchParams();
4623
+ if (query.status) params.set("status", query.status);
4624
+ if (query.page) params.set("page", String(query.page));
4625
+ if (query.limit) params.set("limit", String(query.limit));
4626
+ const q = params.toString() ? `?${params.toString()}` : "";
4627
+ return this.client.request(`${this.prefix}/employers${q}`);
4628
+ },
4629
+ get: async (id) => {
4630
+ if (this.service) return this.service.getEmployer(id);
4631
+ return this.client.request(`${this.prefix}/employers/${encodeURIComponent(id)}`);
4632
+ },
4633
+ create: async (input) => {
4634
+ if (this.service) return this.service.createEmployer(input);
4635
+ return this.client.request(`${this.prefix}/employers`, {
4636
+ method: "POST",
4637
+ body: JSON.stringify(input)
4638
+ });
4639
+ },
4640
+ update: async (id, input) => {
4641
+ if (this.service) return this.service.updateEmployer(id, input);
4642
+ return this.client.request(`${this.prefix}/employers/${encodeURIComponent(id)}`, {
4643
+ method: "PUT",
4644
+ body: JSON.stringify(input)
4645
+ });
4646
+ }
4647
+ };
4648
+ employees = {
4649
+ find: async (query = {}) => {
4650
+ if (this.service) return this.service.listEmployees(query);
4651
+ const params = new URLSearchParams();
4652
+ if (query.employerId) params.set("employerId", query.employerId);
4653
+ if (query.department) params.set("department", query.department);
4654
+ if (query.employmentType) params.set("employmentType", query.employmentType);
4655
+ if (query.status) params.set("status", query.status);
4656
+ if (query.search) params.set("search", query.search);
4657
+ if (query.page) params.set("page", String(query.page));
4658
+ if (query.limit) params.set("limit", String(query.limit));
4659
+ const q = params.toString() ? `?${params.toString()}` : "";
4660
+ return this.client.request(`${this.prefix}/employees${q}`);
4661
+ },
4662
+ get: async (id) => {
4663
+ if (this.service) return this.service.getEmployee(id);
4664
+ return this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`);
4665
+ },
4666
+ getByNumber: async (employerId, employeeNumber) => {
4667
+ if (this.service) return this.service.getEmployeeByNumber(employerId, employeeNumber);
4668
+ return (await this.employees.find({
4669
+ employerId,
4670
+ search: employeeNumber
4671
+ })).items.find((e) => e.data.employeeNumber === employeeNumber) ?? null;
4672
+ },
4673
+ create: async (input) => {
4674
+ if (this.service) return this.service.createEmployee(input);
4675
+ return this.client.request(`${this.prefix}/employees`, {
4676
+ method: "POST",
4677
+ body: JSON.stringify(input)
4678
+ });
4679
+ },
4680
+ update: async (id, input) => {
4681
+ if (this.service) return this.service.updateEmployee(id, input);
4682
+ return this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`, {
4683
+ method: "PUT",
4684
+ body: JSON.stringify(input)
4685
+ });
4686
+ },
4687
+ delete: async (id) => {
4688
+ if (this.service) return this.service.deleteEmployee(id);
4689
+ return (await this.client.request(`${this.prefix}/employees/${encodeURIComponent(id)}`, { method: "DELETE" })).success;
4690
+ },
4691
+ getLeaveBalance: async (employeeId, year) => {
4692
+ if (this.service) return this.service.calculateLeaveBalance(employeeId, year);
4693
+ const q = year ? `?year=${year}` : "";
4694
+ return this.client.request(`${this.prefix}/employees/${encodeURIComponent(employeeId)}/leave-balance${q}`);
4695
+ },
4696
+ getDirectReports: async (managerId) => {
4697
+ if (this.service) return this.service.getDirectReports(managerId);
4698
+ return (await this.client.request(`${this.prefix}/employees/${encodeURIComponent(managerId)}/direct-reports`)).items;
4699
+ }
4700
+ };
4701
+ attendance = {
4702
+ checkIn: async (input) => {
4703
+ if (this.service) return this.service.checkIn(input);
4704
+ return this.client.request(`${this.prefix}/attendance/check-in`, {
4705
+ method: "POST",
4706
+ body: JSON.stringify(input)
4707
+ });
4708
+ },
4709
+ checkOut: async (input) => {
4710
+ if (this.service) return this.service.checkOut(input);
4711
+ return this.client.request(`${this.prefix}/attendance/check-out`, {
4712
+ method: "POST",
4713
+ body: JSON.stringify(input)
4714
+ });
4715
+ },
4716
+ getDaily: async (employeeId, date) => {
4717
+ if (this.service) return this.service.getDailyAttendance(employeeId, date);
4718
+ return (await this.attendance.find({
4719
+ employeeId,
4720
+ date
4721
+ })).items[0] ?? null;
4722
+ },
4723
+ find: async (query = {}) => {
4724
+ if (this.service) return this.service.listAttendance(query);
4725
+ const params = new URLSearchParams();
4726
+ if (query.employerId) params.set("employerId", query.employerId);
4727
+ if (query.employeeId) params.set("employeeId", query.employeeId);
4728
+ if (query.date) params.set("date", query.date);
4729
+ if (query.startDate) params.set("startDate", query.startDate);
4730
+ if (query.endDate) params.set("endDate", query.endDate);
4731
+ if (query.status) params.set("status", query.status);
4732
+ if (query.page) params.set("page", String(query.page));
4733
+ if (query.limit) params.set("limit", String(query.limit));
4734
+ const q = params.toString() ? `?${params.toString()}` : "";
4735
+ return this.client.request(`${this.prefix}/attendance${q}`);
4736
+ },
4737
+ recordManual: async (input) => {
4738
+ if (this.service) return this.service.recordAttendanceManual(input);
4739
+ return this.client.request(`${this.prefix}/attendance/manual`, {
4740
+ method: "POST",
4741
+ body: JSON.stringify(input)
4742
+ });
4743
+ }
4744
+ };
4745
+ leaves = {
4746
+ listTypes: async (employerId) => {
4747
+ if (this.service) return this.service.listLeaveTypes(employerId);
4748
+ const q = employerId ? `?employerId=${encodeURIComponent(employerId)}` : "";
4749
+ return (await this.client.request(`${this.prefix}/leave-types${q}`)).items;
4750
+ },
4751
+ createType: async (input) => {
4752
+ if (this.service) return this.service.createLeaveType(input);
4753
+ return this.client.request(`${this.prefix}/leave-types`, {
4754
+ method: "POST",
4755
+ body: JSON.stringify(input)
4756
+ });
4757
+ },
4758
+ getType: async (id) => {
4759
+ if (this.service) return this.service.getLeaveType(id);
4760
+ return this.client.request(`${this.prefix}/leave-types/${encodeURIComponent(id)}`);
4761
+ },
4762
+ findRequests: async (query = {}) => {
4763
+ if (this.service) return this.service.listLeaveRequests(query);
4764
+ const params = new URLSearchParams();
4765
+ if (query.employerId) params.set("employerId", query.employerId);
4766
+ if (query.employeeId) params.set("employeeId", query.employeeId);
4767
+ if (query.leaveTypeId) params.set("leaveTypeId", query.leaveTypeId);
4768
+ if (query.status) params.set("status", query.status);
4769
+ if (query.year) params.set("year", String(query.year));
4770
+ if (query.page) params.set("page", String(query.page));
4771
+ if (query.limit) params.set("limit", String(query.limit));
4772
+ const q = params.toString() ? `?${params.toString()}` : "";
4773
+ return this.client.request(`${this.prefix}/leave-requests${q}`);
4774
+ },
4775
+ request: async (input) => {
4776
+ if (this.service) return this.service.requestLeave(input);
4777
+ return this.client.request(`${this.prefix}/leave-requests`, {
4778
+ method: "POST",
4779
+ body: JSON.stringify(input)
4780
+ });
4781
+ },
4782
+ approve: async (input) => {
4783
+ if (this.service) return this.service.approveLeave(input);
4784
+ return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(input.requestId)}/approve`, {
4785
+ method: "POST",
4786
+ body: JSON.stringify({ approverId: input.approverId })
4787
+ });
4788
+ },
4789
+ reject: async (input) => {
4790
+ if (this.service) return this.service.rejectLeave(input);
4791
+ return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(input.requestId)}/reject`, {
4792
+ method: "POST",
4793
+ body: JSON.stringify({
4794
+ approverId: input.approverId,
4795
+ reason: input.reason
4796
+ })
4797
+ });
4798
+ },
4799
+ cancel: async (requestId) => {
4800
+ if (this.service) return this.service.cancelLeave(requestId);
4801
+ return this.client.request(`${this.prefix}/leave-requests/${encodeURIComponent(requestId)}/cancel`, { method: "POST" });
4802
+ }
4803
+ };
4804
+ };
4805
+ /**
4806
+ * Get or create an HRMSClient adapter for a CMSClient.
4807
+ */
4808
+ function getHRMSClient(client, options) {
4809
+ return new HRMSClient(client, options);
4810
+ }
4811
+ //#endregion
4812
+ //#region src/plugins/hrms/index.ts
4813
+ /**
4814
+ * @azlib/cms - Built-in HRMS Plugin
4815
+ */
4816
+ /**
4817
+ * Built-in HRMS plugin factory for @azlib/cms.
4818
+ * Equips the CMS engine with multi-tenant employers, employee profiles,
4819
+ * daily attendance check-in/out tracking, and leave quota approval workflows.
4820
+ */
4821
+ const hrmsPlugin = definePlugin((options) => {
4822
+ const opts = options || {};
4823
+ const collections = [createEmployeeCollection(opts)];
4824
+ if (opts.enableEmployers !== false) collections.unshift(createEmployerCollection(opts));
4825
+ if (opts.enableAttendance !== false) collections.push(createAttendanceCollection(opts));
4826
+ if (opts.enableLeaves !== false) {
4827
+ collections.push(createLeaveTypeCollection(opts));
4828
+ collections.push(createLeaveRequestCollection(opts));
4829
+ }
4830
+ return {
4831
+ name: "hrms",
4832
+ version: "1.0.0",
4833
+ description: "Built-in Human Resource Management System (HRMS) plugin for employers, employees, attendance, and leave management",
4834
+ collections,
4835
+ taxonomies: createHRMSTaxonomies(opts),
4836
+ setup(ctx) {
4837
+ const service = new HRMSService(ctx.engine, opts);
4838
+ ctx.engine.__hrmsService = service;
4839
+ registerHRMSRoutes(ctx, service, opts);
4840
+ }
4841
+ };
4842
+ });
4843
+ /**
4844
+ * Retrieve the active HRMSService instance associated with a CMSEngine.
4845
+ */
4846
+ function getHRMSService(engine, options) {
4847
+ if (engine.__hrmsService) return engine.__hrmsService;
4848
+ const service = new HRMSService(engine, options);
4849
+ engine.__hrmsService = service;
4850
+ return service;
4851
+ }
4852
+ //#endregion
4853
+ //#region src/plugins/transfer/parsers/json.ts
4854
+ /**
4855
+ * @azlib/cms - Universal JSON Data Parser
4856
+ */
4857
+ function parseJsonSource(input) {
4858
+ if (Array.isArray(input)) return input.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item)));
4859
+ let text;
4860
+ if (typeof input === "string") text = input.trim();
4861
+ else if (input instanceof Uint8Array || Buffer.isBuffer(input)) text = new TextDecoder("utf-8").decode(input).trim();
4862
+ else if (input && typeof input === "object") {
4863
+ const obj = input;
4864
+ if (Array.isArray(obj.data)) return parseJsonSource(obj.data);
4865
+ if (Array.isArray(obj.items)) return parseJsonSource(obj.items);
4866
+ return [obj];
4867
+ } else throw new Error("[TransferParser] Invalid JSON input: expected string, buffer, or array.");
4868
+ if (text.charCodeAt(0) === 65279) text = text.slice(1);
4869
+ if (!text) return [];
4870
+ try {
4871
+ const parsed = JSON.parse(text);
4872
+ if (Array.isArray(parsed)) return parsed.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item)));
4873
+ if (parsed && typeof parsed === "object") {
4874
+ if (Array.isArray(parsed.data)) return parseJsonSource(parsed.data);
4875
+ if (Array.isArray(parsed.items)) return parseJsonSource(parsed.items);
4876
+ return [parsed];
4877
+ }
4878
+ return [];
4879
+ } catch {
4880
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
4881
+ const records = [];
4882
+ for (const line of lines) try {
4883
+ const item = JSON.parse(line);
4884
+ if (item && typeof item === "object" && !Array.isArray(item)) records.push(item);
4885
+ } catch (err) {
4886
+ throw new Error(`[TransferParser] Failed to parse JSON source: ${err instanceof Error ? err.message : String(err)}`);
4887
+ }
4888
+ return records;
4889
+ }
4890
+ }
4891
+ //#endregion
4892
+ //#region src/plugins/transfer/parsers/csv.ts
4893
+ /**
4894
+ * @azlib/cms - Universal RFC 4180 CSV Data Parser
4895
+ */
4896
+ function parseCsvSource(input) {
4897
+ let text;
4898
+ if (typeof input === "string") text = input;
4899
+ else if (input instanceof Uint8Array || Buffer.isBuffer(input)) text = new TextDecoder("utf-8").decode(input);
4900
+ else throw new Error("[TransferParser] Invalid CSV input: expected string or buffer.");
4901
+ if (text.charCodeAt(0) === 65279) text = text.slice(1);
4902
+ const rows = parseCsvRows(text);
4903
+ if (rows.length === 0) return [];
4904
+ const headers = rows[0].map((h, i) => {
4905
+ return h.trim() || `column_${i + 1}`;
4906
+ });
4907
+ const records = [];
4908
+ for (let r = 1; r < rows.length; r++) {
4909
+ const row = rows[r];
4910
+ if (row.length === 0 || row.length === 1 && row[0].trim() === "") continue;
4911
+ const record = {};
4912
+ for (let c = 0; c < headers.length; c++) {
4913
+ const header = headers[c];
4914
+ record[header] = coerceCsvValue(c < row.length ? row[c].trim() : "");
4915
+ }
4916
+ records.push(record);
4917
+ }
4918
+ return records;
4919
+ }
4920
+ /**
4921
+ * Tokenize CSV into 2D array of string cells following RFC 4180.
4922
+ */
4923
+ function parseCsvRows(text) {
4924
+ const rows = [];
4925
+ let currentRow = [];
4926
+ let currentCell = "";
4927
+ let insideQuotes = false;
4928
+ let i = 0;
4929
+ const len = text.length;
4930
+ while (i < len) {
4931
+ const char = text[i];
4932
+ if (insideQuotes) if (char === "\"") if (i + 1 < len && text[i + 1] === "\"") {
4933
+ currentCell += "\"";
4934
+ i += 2;
4935
+ continue;
4936
+ } else {
4937
+ insideQuotes = false;
4938
+ i++;
4939
+ continue;
4940
+ }
4941
+ else {
4942
+ currentCell += char;
4943
+ i++;
4944
+ continue;
4945
+ }
4946
+ if (char === "\"") {
4947
+ insideQuotes = true;
4948
+ i++;
4949
+ continue;
4950
+ }
4951
+ if (char === ",") {
4952
+ currentRow.push(currentCell);
4953
+ currentCell = "";
4954
+ i++;
4955
+ continue;
4956
+ }
4957
+ if (char === "\r") {
4958
+ if (i + 1 < len && text[i + 1] === "\n") i++;
4959
+ currentRow.push(currentCell);
4960
+ rows.push(currentRow);
4961
+ currentRow = [];
4962
+ currentCell = "";
4963
+ i++;
4964
+ continue;
4965
+ }
4966
+ if (char === "\n") {
4967
+ currentRow.push(currentCell);
4968
+ rows.push(currentRow);
4969
+ currentRow = [];
4970
+ currentCell = "";
4971
+ i++;
4972
+ continue;
4973
+ }
4974
+ currentCell += char;
4975
+ i++;
4976
+ }
4977
+ if (currentCell.length > 0 || currentRow.length > 0) {
4978
+ currentRow.push(currentCell);
4979
+ rows.push(currentRow);
4980
+ }
4981
+ return rows;
4982
+ }
4983
+ /**
4984
+ * Coerce obvious numeric / boolean CSV strings to primitives when lossless.
4985
+ */
4986
+ function coerceCsvValue(val) {
4987
+ if (val === "") return "";
4988
+ if (val.toLowerCase() === "true") return true;
4989
+ if (val.toLowerCase() === "false") return false;
4990
+ if (val.toLowerCase() === "null") return null;
4991
+ if (/^-?\d+(\.\d+)?$/.test(val)) {
4992
+ if (val.length > 1 && val.startsWith("0") && !val.startsWith("0.")) return val;
4993
+ const num = Number(val);
4994
+ if (!Number.isNaN(num)) return num;
4995
+ }
4996
+ return val;
4997
+ }
4998
+ //#endregion
4999
+ //#region src/plugins/transfer/parsers/excel.ts
5000
+ /**
5001
+ * @azlib/cms - Universal Excel (.xlsx) Data Parser using ExcelJS
5002
+ */
5003
+ async function parseExcelSource(input, options = {}) {
5004
+ const workbook = new exceljs.default.Workbook();
5005
+ let buffer;
5006
+ if (Buffer.isBuffer(input)) buffer = input;
5007
+ else if (input instanceof Uint8Array) buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
5008
+ else if (input instanceof ArrayBuffer) buffer = Buffer.from(input);
5009
+ else throw new Error("[TransferParser] Invalid Excel input: expected Buffer, Uint8Array, or ArrayBuffer.");
5010
+ await workbook.xlsx.load(buffer);
5011
+ const sheetNames = workbook.worksheets.map((s) => s.name);
5012
+ if (sheetNames.length === 0) return {
5013
+ sheets: [],
5014
+ selectedSheet: "",
5015
+ records: []
5016
+ };
5017
+ let worksheet = options.sheetName ? workbook.getWorksheet(options.sheetName) : void 0;
5018
+ if (!worksheet) worksheet = workbook.worksheets[0];
5019
+ const selectedSheet = worksheet.name;
5020
+ const records = [];
5021
+ const headerRow = worksheet.getRow(1);
5022
+ const headers = [];
5023
+ headerRow.eachCell({ includeEmpty: false }, (cell, colNumber) => {
5024
+ let headerText = String(cell.value ?? "").trim();
5025
+ if (!headerText) headerText = `column_${colNumber}`;
5026
+ headers[colNumber] = headerText;
5027
+ });
5028
+ if (headers.length === 0) return {
5029
+ sheets: sheetNames,
5030
+ selectedSheet,
5031
+ records: []
5032
+ };
5033
+ const rowCount = worksheet.rowCount;
5034
+ for (let r = 2; r <= rowCount; r++) {
5035
+ const row = worksheet.getRow(r);
5036
+ let hasValues = false;
5037
+ const record = {};
5038
+ for (let c = 1; c < headers.length; c++) {
5039
+ const header = headers[c];
5040
+ if (!header) continue;
5041
+ const val = extractCellValue(row.getCell(c).value);
5042
+ if (val !== void 0 && val !== null && val !== "") hasValues = true;
5043
+ record[header] = val;
5044
+ }
5045
+ if (hasValues) records.push(record);
5046
+ }
5047
+ return {
5048
+ sheets: sheetNames,
5049
+ selectedSheet,
5050
+ records
5051
+ };
5052
+ }
5053
+ /**
5054
+ * Normalizes ExcelJS cell values (handles formulas, dates, rich text, hyperlinks).
5055
+ */
5056
+ function extractCellValue(val) {
5057
+ if (val === void 0 || val === null) return null;
5058
+ if (val instanceof Date) return val.toISOString();
5059
+ if (typeof val === "object") {
5060
+ if ("formula" in val) return extractCellValue(val.result);
5061
+ if ("text" in val && "hyperlink" in val) return val.text;
5062
+ if ("richText" in val && Array.isArray(val.richText)) return val.richText.map((t) => t.text).join("");
5063
+ if ("error" in val) return null;
5064
+ }
5065
+ return val;
5066
+ }
5067
+ //#endregion
5068
+ //#region src/plugins/transfer/parsers/index.ts
5069
+ /**
5070
+ * Detect the format of an input payload if not explicitly provided.
5071
+ */
5072
+ function detectFormat(input, fileName) {
5073
+ if (Array.isArray(input)) return "json";
5074
+ if (fileName) {
5075
+ const lower = fileName.toLowerCase();
5076
+ if (lower.endsWith(".xlsx") || lower.endsWith(".xls")) return "excel";
5077
+ if (lower.endsWith(".csv")) return "csv";
5078
+ if (lower.endsWith(".json") || lower.endsWith(".ndjson")) return "json";
5079
+ }
5080
+ if (input instanceof Uint8Array || Buffer.isBuffer(input)) {
5081
+ if (input.length >= 4 && input[0] === 80 && input[1] === 75 && input[2] === 3 && input[3] === 4) return "excel";
5082
+ try {
5083
+ const head = new TextDecoder("utf-8").decode(input.slice(0, 100)).trim();
5084
+ if (head.startsWith("{") || head.startsWith("[")) return "json";
5085
+ if (head.includes(",") && head.includes("\n")) return "csv";
5086
+ } catch {}
5087
+ }
5088
+ if (typeof input === "string") {
5089
+ const trimmed = input.trim();
5090
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
5091
+ if (trimmed.includes(",") || trimmed.includes("\n")) return "csv";
5092
+ }
5093
+ return "json";
5094
+ }
5095
+ /**
5096
+ * Universal source parser that handles JSON, CSV, and Excel (.xlsx).
5097
+ */
5098
+ async function parseSource(input, options = {}) {
5099
+ switch (options.format || detectFormat(input, options.fileName)) {
5100
+ case "excel": {
5101
+ const excelResult = await parseExcelSource(input, { sheetName: options.sheetName });
5102
+ return {
5103
+ format: "excel",
5104
+ sheets: excelResult.sheets,
5105
+ selectedSheet: excelResult.selectedSheet,
5106
+ records: excelResult.records
5107
+ };
5108
+ }
5109
+ case "csv": return {
5110
+ format: "csv",
5111
+ records: parseCsvSource(input)
5112
+ };
5113
+ default: return {
5114
+ format: "json",
5115
+ records: parseJsonSource(input)
5116
+ };
5117
+ }
5118
+ }
5119
+ //#endregion
5120
+ //#region src/plugins/transfer/serializers/json.ts
5121
+ function serializeJson(records, options = {}) {
5122
+ const indent = options.pretty !== false ? 2 : void 0;
5123
+ return JSON.stringify(records, null, indent);
5124
+ }
5125
+ //#endregion
5126
+ //#region src/plugins/transfer/serializers/csv.ts
5127
+ function serializeCsv(records, options = {}) {
5128
+ if (records.length === 0) return "";
5129
+ let columns = options.columns;
5130
+ if (!columns || columns.length === 0) {
5131
+ const keys = /* @__PURE__ */ new Set();
5132
+ for (const record of records) for (const key of Object.keys(record)) keys.add(key);
5133
+ columns = Array.from(keys).map((k) => ({
5134
+ key: k,
5135
+ header: k
5136
+ }));
5137
+ }
5138
+ const lines = [];
5139
+ lines.push(columns.map((c) => escapeCsvCell(c.header)).join(","));
5140
+ for (const record of records) {
5141
+ const rowCells = columns.map((col) => {
5142
+ const val = record[col.key];
5143
+ return escapeCsvCell(formatCsvValue(val));
5144
+ });
5145
+ lines.push(rowCells.join(","));
5146
+ }
5147
+ return lines.join("\r\n");
5148
+ }
5149
+ function formatCsvValue(val) {
5150
+ if (val === void 0 || val === null) return "";
5151
+ if (typeof val === "object") {
5152
+ if (val instanceof Date) return val.toISOString();
5153
+ return JSON.stringify(val);
5154
+ }
5155
+ return String(val);
5156
+ }
5157
+ function escapeCsvCell(cell) {
5158
+ if (cell.includes(",") || cell.includes("\"") || cell.includes("\n") || cell.includes("\r")) return `"${cell.replace(/"/g, "\"\"")}"`;
5159
+ return cell;
5160
+ }
5161
+ //#endregion
5162
+ //#region src/plugins/transfer/serializers/excel.ts
5163
+ /**
5164
+ * @azlib/cms - Universal Excel (.xlsx) Serializer using ExcelJS
5165
+ */
5166
+ async function serializeExcel(records, options = {}) {
5167
+ const workbook = new exceljs.default.Workbook();
5168
+ const sheetName = (options.sheetName || "Export").replace(/[:\\/?*\[\]]/g, "_");
5169
+ const worksheet = workbook.addWorksheet(sheetName.slice(0, 31));
5170
+ let columns = options.columns;
5171
+ if (!columns || columns.length === 0) {
5172
+ const keys = /* @__PURE__ */ new Set();
5173
+ for (const record of records) for (const key of Object.keys(record)) keys.add(key);
5174
+ columns = Array.from(keys).map((k) => ({
5175
+ key: k,
5176
+ header: formatHeaderLabel(k)
5177
+ }));
5178
+ }
5179
+ worksheet.columns = columns.map((col) => {
5180
+ let maxLen = col.header.length;
5181
+ for (let i = 0; i < Math.min(records.length, 100); i++) {
5182
+ const val = records[i][col.key];
5183
+ if (val !== void 0 && val !== null) {
5184
+ const str = typeof val === "object" ? JSON.stringify(val) : String(val);
5185
+ if (str.length > maxLen) maxLen = Math.min(str.length, 50);
5186
+ }
5187
+ }
5188
+ return {
5189
+ header: col.header,
5190
+ key: col.key,
5191
+ width: Math.max(maxLen + 4, col.width ?? 12)
5192
+ };
5193
+ });
5194
+ const headerRow = worksheet.getRow(1);
5195
+ headerRow.height = 28;
5196
+ headerRow.eachCell((cell) => {
5197
+ cell.font = {
5198
+ bold: true,
5199
+ color: { argb: "FF1E293B" },
5200
+ size: 11
5201
+ };
5202
+ cell.fill = {
5203
+ type: "pattern",
5204
+ pattern: "solid",
5205
+ fgColor: { argb: "FFF1F5F9" }
5206
+ };
5207
+ cell.alignment = {
5208
+ vertical: "middle",
5209
+ horizontal: "center",
5210
+ wrapText: true
5211
+ };
5212
+ cell.border = {
5213
+ top: {
5214
+ style: "thin",
5215
+ color: { argb: "FFE2E8F0" }
5216
+ },
5217
+ bottom: {
5218
+ style: "medium",
5219
+ color: { argb: "FFCBD5E1" }
5220
+ },
5221
+ left: {
5222
+ style: "thin",
5223
+ color: { argb: "FFE2E8F0" }
5224
+ },
5225
+ right: {
5226
+ style: "thin",
5227
+ color: { argb: "FFE2E8F0" }
5228
+ }
5229
+ };
5230
+ });
5231
+ for (const record of records) {
5232
+ const rowValues = {};
5233
+ for (const col of columns) {
5234
+ const val = record[col.key];
5235
+ rowValues[col.key] = formatExcelCellValue(val);
5236
+ }
5237
+ const row = worksheet.addRow(rowValues);
5238
+ row.height = 20;
5239
+ row.eachCell((cell) => {
5240
+ cell.alignment = { vertical: "middle" };
5241
+ cell.border = {
5242
+ top: {
5243
+ style: "thin",
5244
+ color: { argb: "FFF1F5F9" }
5245
+ },
5246
+ bottom: {
5247
+ style: "thin",
5248
+ color: { argb: "FFF1F5F9" }
5249
+ },
5250
+ left: {
5251
+ style: "thin",
5252
+ color: { argb: "FFF1F5F9" }
5253
+ },
5254
+ right: {
5255
+ style: "thin",
5256
+ color: { argb: "FFF1F5F9" }
5257
+ }
5258
+ };
5259
+ });
5260
+ }
5261
+ const arrayBuffer = await workbook.xlsx.writeBuffer();
5262
+ return new Uint8Array(arrayBuffer);
5263
+ }
5264
+ function formatHeaderLabel(key) {
5265
+ return key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
5266
+ }
5267
+ function formatExcelCellValue(val) {
5268
+ if (val === void 0 || val === null) return "";
5269
+ if (typeof val === "number" || typeof val === "boolean") return val;
5270
+ if (val instanceof Date) return val;
5271
+ if (typeof val === "object") return JSON.stringify(val);
5272
+ return String(val);
5273
+ }
5274
+ //#endregion
5275
+ //#region src/plugins/transfer/serializers/index.ts
5276
+ async function serializeSource(records, options = {}) {
5277
+ const format = options.format || "json";
5278
+ const baseName = options.fileName || "export";
5279
+ switch (format) {
5280
+ case "excel": return {
5281
+ format: "excel",
5282
+ data: await serializeExcel(records, {
5283
+ sheetName: options.sheetName,
5284
+ columns: options.columns
5285
+ }),
5286
+ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
5287
+ fileName: baseName.endsWith(".xlsx") ? baseName : `${baseName}.xlsx`,
5288
+ totalRecords: records.length
5289
+ };
5290
+ case "csv": return {
5291
+ format: "csv",
5292
+ data: serializeCsv(records, { columns: options.columns }),
5293
+ mimeType: "text/csv; charset=utf-8",
5294
+ fileName: baseName.endsWith(".csv") ? baseName : `${baseName}.csv`,
5295
+ totalRecords: records.length
5296
+ };
5297
+ default: return {
5298
+ format: "json",
5299
+ data: serializeJson(records, { pretty: true }),
5300
+ mimeType: "application/json; charset=utf-8",
5301
+ fileName: baseName.endsWith(".json") ? baseName : `${baseName}.json`,
5302
+ totalRecords: records.length
5303
+ };
5304
+ }
5305
+ }
5306
+ //#endregion
5307
+ //#region src/plugins/transfer/inspect.ts
5308
+ /**
5309
+ * Inspect an uploaded source file or payload to discover its schema,
5310
+ * infer column types, and generate auto-mapping suggestions against a target collection.
5311
+ */
5312
+ async function inspectSource(input, options = {}) {
5313
+ const parsed = await parseSource(input, {
5314
+ format: options.format,
5315
+ fileName: options.fileName,
5316
+ sheetName: options.sheetName
5317
+ });
5318
+ const records = parsed.records;
5319
+ const totalRows = records.length;
5320
+ const previewLimit = options.previewLimit ?? 5;
5321
+ const previewRows = records.slice(0, previewLimit);
5322
+ const fieldKeys = /* @__PURE__ */ new Set();
5323
+ for (const row of records) for (const key of Object.keys(row)) fieldKeys.add(key);
5324
+ const sourceFields = [];
5325
+ for (const key of fieldKeys) {
5326
+ let nullCount = 0;
5327
+ const samples = [];
5328
+ const detectedTypes = [];
5329
+ for (const row of records) {
5330
+ const val = row[key];
5331
+ if (val === void 0 || val === null || val === "") nullCount++;
5332
+ else {
5333
+ if (samples.length < 5) samples.push(val);
5334
+ detectedTypes.push(inferValueType(val));
5335
+ }
5336
+ }
5337
+ sourceFields.push({
5338
+ name: key,
5339
+ inferredType: resolveDominantType(detectedTypes),
5340
+ sampleValues: samples,
5341
+ nullCount,
5342
+ totalCount: totalRows
5343
+ });
5344
+ }
5345
+ let targetSchema;
5346
+ let suggestedMapping;
5347
+ if (options.collectionConfig) {
5348
+ targetSchema = summarizeCollection(options.collectionConfig);
5349
+ suggestedMapping = generateSuggestedMapping(sourceFields.map((f) => f.name), targetSchema);
5350
+ }
5351
+ return {
5352
+ format: parsed.format,
5353
+ sheets: parsed.sheets,
5354
+ selectedSheet: parsed.selectedSheet,
5355
+ totalRows,
5356
+ sourceFields,
5357
+ targetSchema,
5358
+ suggestedMapping,
5359
+ previewRows
5360
+ };
5361
+ }
5362
+ /**
5363
+ * Summarize a CMS CollectionConfig into consumer-friendly schema details.
5364
+ */
5365
+ function summarizeCollection(config) {
5366
+ return {
5367
+ slug: config.slug,
5368
+ label: config.label,
5369
+ singularLabel: config.singularLabel || config.label,
5370
+ description: config.description,
5371
+ taxonomies: config.taxonomies || [],
5372
+ fields: config.fields.map((f) => ({
5373
+ name: f.name,
5374
+ label: f.label || f.name,
5375
+ type: f.type,
5376
+ required: Boolean(f.required),
5377
+ unique: Boolean(f.unique),
5378
+ description: f.description,
5379
+ defaultValue: f.defaultValue,
5380
+ targetCollection: f.targetCollection,
5381
+ options: f.options
5382
+ }))
5383
+ };
5384
+ }
5385
+ /**
5386
+ * Generates intelligent auto-mapping suggestions matching source headers to CMS fields.
5387
+ */
5388
+ function generateSuggestedMapping(sourceHeaders, targetSchema) {
5389
+ const suggestions = [];
5390
+ const matchedTargets = /* @__PURE__ */ new Set();
5391
+ for (const sourceHeader of sourceHeaders) {
5392
+ let bestMatch = null;
5393
+ for (const targetField of targetSchema.fields) {
5394
+ if (matchedTargets.has(targetField.name)) continue;
5395
+ const score = calculateMatchConfidence(sourceHeader, targetField);
5396
+ if (score >= .6 && (!bestMatch || score > bestMatch.confidence)) bestMatch = {
5397
+ field: targetField.name,
5398
+ confidence: score
5399
+ };
5400
+ }
5401
+ if (bestMatch) {
5402
+ matchedTargets.add(bestMatch.field);
5403
+ suggestions.push({
5404
+ sourceField: sourceHeader,
5405
+ targetField: bestMatch.field,
5406
+ confidence: Number(bestMatch.confidence.toFixed(2))
5407
+ });
5408
+ }
5409
+ }
5410
+ return suggestions;
5411
+ }
5412
+ function normalize(str) {
5413
+ return str.toLowerCase().replace(/[^a-z0-9]/g, "");
5414
+ }
5415
+ /**
5416
+ * Common field synonyms dictionary for intuitive fuzzy matching.
5417
+ */
5418
+ const SYNONYMS = {
5419
+ employeeNumber: [
5420
+ "empid",
5421
+ "employeeid",
5422
+ "empnumber",
5423
+ "staffid",
5424
+ "badgenumber",
5425
+ "staffno",
5426
+ "empno"
5427
+ ],
5428
+ companyName: [
5429
+ "company",
5430
+ "organization",
5431
+ "employer",
5432
+ "businessname",
5433
+ "employername",
5434
+ "firm"
5435
+ ],
5436
+ firstName: [
5437
+ "fname",
5438
+ "givenname",
5439
+ "first"
5440
+ ],
5441
+ lastName: [
5442
+ "lname",
5443
+ "surname",
5444
+ "familyname",
5445
+ "last"
5446
+ ],
5447
+ email: [
5448
+ "mail",
5449
+ "workemail",
5450
+ "primaryemail",
5451
+ "emailaddress"
5452
+ ],
5453
+ phone: [
5454
+ "mobile",
5455
+ "cell",
5456
+ "telephone",
5457
+ "phonenumber",
5458
+ "contactnumber",
5459
+ "tel"
5460
+ ],
5461
+ employerId: [
5462
+ "employer",
5463
+ "companyid",
5464
+ "company",
5465
+ "organizationid",
5466
+ "orgid"
5467
+ ],
5468
+ hireDate: [
5469
+ "startdate",
5470
+ "joiningdate",
5471
+ "hired",
5472
+ "dateofjoining"
5473
+ ],
5474
+ jobTitle: [
5475
+ "title",
5476
+ "position",
5477
+ "role",
5478
+ "designation"
5479
+ ],
5480
+ employmentType: [
5481
+ "contracttype",
5482
+ "worktype",
5483
+ "jobtype"
5484
+ ],
5485
+ salary: [
5486
+ "compensation",
5487
+ "wage",
5488
+ "pay",
5489
+ "rate",
5490
+ "annualsalary"
5491
+ ],
5492
+ taxId: [
5493
+ "ein",
5494
+ "vat",
5495
+ "taxnumber",
5496
+ "tin"
5497
+ ],
5498
+ status: ["state", "active"]
5499
+ };
5500
+ function calculateMatchConfidence(sourceHeader, targetField) {
5501
+ const normSource = normalize(sourceHeader);
5502
+ const normTargetName = normalize(targetField.name);
5503
+ const normTargetLabel = normalize(targetField.label);
5504
+ if (sourceHeader === targetField.name) return 1;
5505
+ if (normSource === normTargetName) return .95;
5506
+ if (sourceHeader.toLowerCase() === targetField.label.toLowerCase()) return .92;
5507
+ if (normSource === normTargetLabel) return .9;
5508
+ const synonyms = SYNONYMS[targetField.name];
5509
+ if (synonyms && synonyms.includes(normSource)) return .85;
5510
+ if (normSource.includes(normTargetName) || normTargetName.includes(normSource)) return .7;
5511
+ if (normSource.includes(normTargetLabel) || normTargetLabel.includes(normSource)) return .65;
5512
+ return 0;
5513
+ }
5514
+ function inferValueType(val) {
5515
+ if (typeof val === "boolean") return "boolean";
5516
+ if (typeof val === "number") return "number";
5517
+ if (Array.isArray(val)) return "array";
5518
+ if (val && typeof val === "object") return "object";
5519
+ if (typeof val === "string") {
5520
+ const s = val.trim();
5521
+ if (s.toLowerCase() === "true" || s.toLowerCase() === "false") return "boolean";
5522
+ if (/^-?\d+(\.\d+)?$/.test(s) && !/^0\d+/.test(s)) return "number";
5523
+ if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(s)) {
5524
+ const parsed = Date.parse(s);
5525
+ if (!Number.isNaN(parsed)) return "date";
5526
+ }
5527
+ }
5528
+ return "string";
5529
+ }
5530
+ function resolveDominantType(types) {
5531
+ if (types.length === 0) return "string";
5532
+ const counts = /* @__PURE__ */ new Map();
5533
+ for (const t of types) counts.set(t, (counts.get(t) || 0) + 1);
5534
+ let dominant = "string";
5535
+ let maxCount = 0;
5536
+ for (const [t, count] of counts) if (count > maxCount) {
5537
+ maxCount = count;
5538
+ dominant = t;
5539
+ }
5540
+ return dominant;
5541
+ }
5542
+ //#endregion
5543
+ //#region src/plugins/transfer/mapping.ts
5544
+ /**
5545
+ * Apply a mapping definition to transform an external raw record into a CMS content payload.
5546
+ */
5547
+ function mapSourceRecord(rawRecord, mapping) {
5548
+ const mappedData = {};
5549
+ const mappedTargetFields = /* @__PURE__ */ new Set();
5550
+ for (const rule of mapping.fields) {
5551
+ mappedTargetFields.add(rule.targetField);
5552
+ const sourceVal = rawRecord[rule.sourceField];
5553
+ const transformed = applyFieldTransform(rule, sourceVal, rawRecord);
5554
+ if (transformed !== void 0) mappedData[rule.targetField] = transformed;
5555
+ }
5556
+ if (mapping.options?.ignoreUnmappedFields === false) {
5557
+ for (const [key, val] of Object.entries(rawRecord)) if (!mapping.fields.some((r) => r.sourceField === key)) mappedData[key] = val;
5558
+ }
5559
+ let title = typeof mappedData.title === "string" ? mappedData.title : void 0;
5560
+ if (!title) {
5561
+ if (typeof mappedData.companyName === "string") title = mappedData.companyName;
5562
+ else if (typeof mappedData.name === "string") title = mappedData.name;
5563
+ else if (typeof mappedData.firstName === "string" || typeof mappedData.lastName === "string") {
5564
+ const parts = [mappedData.firstName, mappedData.lastName].filter(Boolean);
5565
+ if (parts.length > 0) title = parts.join(" ");
5566
+ }
5567
+ }
5568
+ let slug = typeof mappedData.slug === "string" ? mappedData.slug : void 0;
5569
+ if (!slug && mapping.options?.autoGenerateSlug !== false && title) slug = slugify(title);
5570
+ let status = mapping.options?.defaultStatus;
5571
+ if (mapping.options?.statusField && mappedData[mapping.options.statusField]) status = String(mappedData[mapping.options.statusField]);
5572
+ return {
5573
+ data: mappedData,
5574
+ title,
5575
+ slug,
5576
+ status
5577
+ };
5578
+ }
5579
+ /**
5580
+ * Applies transform preset or custom function to a field value.
5581
+ */
5582
+ function applyFieldTransform(rule, value, rawRecord) {
5583
+ let val = value;
5584
+ if (typeof rule.transform === "function") try {
5585
+ val = rule.transform(val, rawRecord);
5586
+ } catch (err) {
5587
+ throw new Error(`Transform error on field '${rule.sourceField}': ${err instanceof Error ? err.message : String(err)}`);
5588
+ }
5589
+ else if (rule.transform && typeof rule.transform === "string") val = applyPresetTransform(rule.transform, val);
5590
+ if (val === void 0 || val === null || val === "") {
5591
+ if (rule.defaultValue !== void 0) val = rule.defaultValue;
5592
+ }
5593
+ return val;
5594
+ }
5595
+ function applyPresetTransform(preset, val) {
5596
+ if (val === void 0 || val === null) return val;
5597
+ switch (preset) {
5598
+ case "trim": return typeof val === "string" ? val.trim() : val;
5599
+ case "lowercase": return typeof val === "string" ? val.toLowerCase() : val;
5600
+ case "uppercase": return typeof val === "string" ? val.toUpperCase() : val;
5601
+ case "number": {
5602
+ if (typeof val === "number") return val;
5603
+ const parsed = Number(val);
5604
+ return Number.isNaN(parsed) ? val : parsed;
5605
+ }
5606
+ case "boolean": {
5607
+ if (typeof val === "boolean") return val;
5608
+ const s = String(val).toLowerCase().trim();
5609
+ if (s === "true" || s === "1" || s === "yes") return true;
5610
+ if (s === "false" || s === "0" || s === "no") return false;
5611
+ return Boolean(val);
5612
+ }
5613
+ case "date": {
5614
+ if (val instanceof Date) return val.toISOString();
5615
+ const s = String(val).trim();
5616
+ const parsed = Date.parse(s);
5617
+ return Number.isNaN(parsed) ? s : new Date(parsed).toISOString();
5618
+ }
5619
+ case "json":
5620
+ if (typeof val === "object") return val;
5621
+ try {
5622
+ return JSON.parse(String(val));
5623
+ } catch {
5624
+ return val;
5625
+ }
5626
+ case "slug": return slugify(String(val));
5627
+ case "split_comma":
5628
+ if (Array.isArray(val)) return val;
5629
+ return String(val).split(",").map((s) => s.trim()).filter(Boolean);
5630
+ default: return val;
5631
+ }
5632
+ }
5633
+ /**
5634
+ * Reverse mapping for export: transforms a CMS ContentItem into an external export record.
5635
+ */
5636
+ function mapCmsItemForExport(item, mapping, options = {}) {
5637
+ const exportRecord = {};
5638
+ if (mapping && mapping.fields.length > 0) {
5639
+ for (const rule of mapping.fields) {
5640
+ const val = item.data[rule.targetField] !== void 0 ? item.data[rule.targetField] : item[rule.targetField];
5641
+ exportRecord[rule.sourceField] = val !== void 0 ? val : rule.defaultValue ?? "";
5642
+ }
5643
+ return exportRecord;
5644
+ }
5645
+ if (options.includeId !== false) exportRecord.id = item.id;
5646
+ if (item.title) exportRecord.title = item.title;
5647
+ if (item.slug) exportRecord.slug = item.slug;
5648
+ if (item.status) exportRecord.status = item.status;
5649
+ for (const [k, v] of Object.entries(item.data)) exportRecord[k] = v;
5650
+ if (options.includeTimestamps) {
5651
+ exportRecord.createdAt = item.createdAt;
5652
+ exportRecord.updatedAt = item.updatedAt;
5653
+ }
5654
+ return exportRecord;
5655
+ }
5656
+ //#endregion
5657
+ //#region src/plugins/transfer/validator.ts
5658
+ /**
5659
+ * Validates a batch of source records against a target CMS collection configuration
5660
+ * and live database constraints.
5661
+ */
5662
+ async function validateTransferBatch(rawRecords, mapping, collectionConfig, options = {}) {
5663
+ const errors = [];
5664
+ const previewRows = [];
5665
+ const previewLimit = options.previewLimit ?? 10;
5666
+ const onDuplicate = options.onDuplicate ?? mapping.options?.onDuplicate ?? "error";
5667
+ const uniqueFields = collectionConfig.fields.filter((f) => Boolean(f.unique) || f.name === "slug");
5668
+ const relationshipFields = collectionConfig.fields.filter((f) => f.type === "relationship" && Boolean(f.targetCollection));
5669
+ const seenUniqueValues = /* @__PURE__ */ new Map();
5670
+ for (const uf of uniqueFields) seenUniqueValues.set(uf.name, /* @__PURE__ */ new Map());
5671
+ let validCount = 0;
5672
+ for (let i = 0; i < rawRecords.length; i++) {
5673
+ const rowNumber = i + 1;
5674
+ const raw = rawRecords[i];
5675
+ const rowErrors = [];
5676
+ let mappedResult;
5677
+ try {
5678
+ mappedResult = mapSourceRecord(raw, mapping);
5679
+ } catch (err) {
5680
+ const errDetail = {
5681
+ rowNumber,
5682
+ code: "TRANSFORM_ERROR",
5683
+ reason: err instanceof Error ? err.message : String(err)
5684
+ };
5685
+ rowErrors.push(errDetail);
5686
+ errors.push(errDetail);
5687
+ if (previewRows.length < previewLimit) previewRows.push({
5688
+ rowNumber,
5689
+ raw,
5690
+ mapped: {},
5691
+ valid: false,
5692
+ errors: rowErrors
5693
+ });
5694
+ continue;
5695
+ }
5696
+ const data = mappedResult.data;
5697
+ for (const field of collectionConfig.fields) validateField(field, data, rowNumber, rowErrors);
5698
+ for (const rule of mapping.fields) if (rule.required && !collectionConfig.fields.some((f) => f.name === rule.targetField)) {
5699
+ const val = data[rule.targetField];
5700
+ if (val === void 0 || val === null || val === "") rowErrors.push({
5701
+ rowNumber,
5702
+ field: rule.targetField,
5703
+ value: val,
5704
+ code: "REQUIRED_FIELD_MISSING",
5705
+ reason: `Field '${rule.targetField}' is required by mapping definition.`
5706
+ });
5707
+ }
5708
+ for (const uf of uniqueFields) {
5709
+ const val = data[uf.name] ?? (uf.name === "slug" ? mappedResult.slug : void 0);
5710
+ if (val !== void 0 && val !== null && val !== "") {
5711
+ const seenMap = seenUniqueValues.get(uf.name);
5712
+ if (seenMap.has(val)) {
5713
+ const prevRow = seenMap.get(val);
5714
+ rowErrors.push({
5715
+ rowNumber,
5716
+ field: uf.name,
5717
+ value: val,
5718
+ code: "CONSTRAINT_UNIQUE_VIOLATION",
5719
+ reason: `${uf.label || uf.name} value '${val}' is duplicated in row ${prevRow} and row ${rowNumber}.`
5720
+ });
5721
+ } else seenMap.set(val, rowNumber);
5722
+ if (options.storage && onDuplicate === "error") {
5723
+ if (await checkExistingUnique(options.storage, collectionConfig.slug, uf.name, val)) rowErrors.push({
5724
+ rowNumber,
5725
+ field: uf.name,
5726
+ value: val,
5727
+ code: "CONSTRAINT_UNIQUE_VIOLATION",
5728
+ reason: `${uf.label || uf.name} value '${val}' already exists in database.`
5729
+ });
5730
+ }
5731
+ }
5732
+ }
5733
+ if (options.storage) for (const rf of relationshipFields) {
5734
+ const targetColl = rf.targetCollection;
5735
+ const refVal = data[rf.name];
5736
+ if (refVal !== void 0 && refVal !== null && refVal !== "") {
5737
+ if (!await checkRelationshipExists(options.storage, targetColl, refVal)) rowErrors.push({
5738
+ rowNumber,
5739
+ field: rf.name,
5740
+ value: refVal,
5741
+ code: "CONSTRAINT_FOREIGN_KEY_VIOLATION",
5742
+ reason: `Referenced ${rf.label || rf.name} with identifier '${refVal}' does not exist in collection '${targetColl}'.`
5743
+ });
5744
+ }
5745
+ }
5746
+ if (rowErrors.length === 0) validCount++;
5747
+ else errors.push(...rowErrors);
5748
+ if (previewRows.length < previewLimit) previewRows.push({
5749
+ rowNumber,
5750
+ raw,
5751
+ mapped: data,
5752
+ valid: rowErrors.length === 0,
5753
+ errors: rowErrors.length > 0 ? rowErrors : void 0
5754
+ });
5755
+ }
5756
+ return {
5757
+ valid: errors.length === 0,
5758
+ totalRows: rawRecords.length,
5759
+ validCount,
5760
+ errorCount: errors.length,
5761
+ errors,
5762
+ previewRows
5763
+ };
5764
+ }
5765
+ /**
5766
+ * Validates a single field against its FieldDefinition.
5767
+ */
5768
+ function validateField(field, data, rowNumber, errors) {
5769
+ const val = data[field.name];
5770
+ const label = field.label || field.name;
5771
+ if (field.required && (val === void 0 || val === null || val === "")) {
5772
+ errors.push({
5773
+ rowNumber,
5774
+ field: field.name,
5775
+ value: val,
5776
+ code: "REQUIRED_FIELD_MISSING",
5777
+ reason: `${label} is required.`
5778
+ });
5779
+ return;
5780
+ }
5781
+ if (val === void 0 || val === null || val === "") return;
5782
+ switch (field.type) {
5783
+ case "number": {
5784
+ const num = typeof val === "number" ? val : Number(val);
5785
+ if (Number.isNaN(num)) errors.push({
5786
+ rowNumber,
5787
+ field: field.name,
5788
+ value: val,
5789
+ code: "INVALID_DATA_TYPE",
5790
+ reason: `${label} must be a valid number, received '${val}'.`
5791
+ });
5792
+ else {
5793
+ data[field.name] = num;
5794
+ const min = field.min;
5795
+ const max = field.max;
5796
+ if (min !== void 0 && num < min) errors.push({
5797
+ rowNumber,
5798
+ field: field.name,
5799
+ value: val,
5800
+ code: "VALIDATION_RULE_FAILED",
5801
+ reason: `${label} must be greater than or equal to ${min}.`
5802
+ });
5803
+ if (max !== void 0 && num > max) errors.push({
5804
+ rowNumber,
5805
+ field: field.name,
5806
+ value: val,
5807
+ code: "VALIDATION_RULE_FAILED",
5808
+ reason: `${label} must be less than or equal to ${max}.`
5809
+ });
5810
+ }
5811
+ break;
5812
+ }
5813
+ case "boolean":
5814
+ if (typeof val !== "boolean") {
5815
+ const s = String(val).toLowerCase().trim();
5816
+ if (s === "true" || s === "1" || s === "yes") data[field.name] = true;
5817
+ else if (s === "false" || s === "0" || s === "no") data[field.name] = false;
5818
+ else errors.push({
5819
+ rowNumber,
5820
+ field: field.name,
5821
+ value: val,
5822
+ code: "INVALID_DATA_TYPE",
5823
+ reason: `${label} must be a boolean (true/false), received '${val}'.`
5824
+ });
5825
+ }
5826
+ break;
5827
+ case "date": {
5828
+ const parsed = Date.parse(String(val));
5829
+ if (Number.isNaN(parsed)) errors.push({
5830
+ rowNumber,
5831
+ field: field.name,
5832
+ value: val,
5833
+ code: "INVALID_DATA_TYPE",
5834
+ reason: `${label} must be a valid date, received '${val}'.`
5835
+ });
5836
+ break;
5837
+ }
5838
+ case "select": {
5839
+ const options = field.options;
5840
+ if (options && options.length > 0) {
5841
+ const allowedValues = options.map((opt) => typeof opt === "object" ? String(opt.value) : String(opt));
5842
+ if (!allowedValues.includes(String(val))) errors.push({
5843
+ rowNumber,
5844
+ field: field.name,
5845
+ value: val,
5846
+ code: "INVALID_SELECT_OPTION",
5847
+ reason: `${label} value '${val}' is not a valid option (allowed: ${allowedValues.join(", ")}).`
5848
+ });
5849
+ }
5850
+ break;
5851
+ }
5852
+ case "json":
5853
+ if (typeof val === "string") try {
5854
+ data[field.name] = JSON.parse(val);
5855
+ } catch {
5856
+ errors.push({
5857
+ rowNumber,
5858
+ field: field.name,
5859
+ value: val,
5860
+ code: "INVALID_DATA_TYPE",
5861
+ reason: `${label} must be a valid JSON structure.`
5862
+ });
5863
+ }
5864
+ break;
5865
+ }
5866
+ if (typeof field.validate === "function") try {
5867
+ const res = field.validate(val, data);
5868
+ if (typeof res === "string") errors.push({
5869
+ rowNumber,
5870
+ field: field.name,
5871
+ value: val,
5872
+ code: "VALIDATION_RULE_FAILED",
5873
+ reason: res
5874
+ });
5875
+ else if (res === false) errors.push({
5876
+ rowNumber,
5877
+ field: field.name,
5878
+ value: val,
5879
+ code: "VALIDATION_RULE_FAILED",
5880
+ reason: `${label} failed custom validation constraint.`
5881
+ });
5882
+ } catch (err) {
5883
+ errors.push({
5884
+ rowNumber,
5885
+ field: field.name,
5886
+ value: val,
5887
+ code: "VALIDATION_RULE_FAILED",
5888
+ reason: err instanceof Error ? err.message : String(err)
5889
+ });
5890
+ }
5891
+ }
5892
+ async function checkExistingUnique(storage, collectionSlug, fieldName, value) {
5893
+ if (fieldName === "slug") return await storage.getContentBySlug(collectionSlug, String(value)) !== null;
5894
+ return (await storage.findContent(collectionSlug, {
5895
+ where: { [fieldName]: value },
5896
+ limit: 1
5897
+ })).items.length > 0;
5898
+ }
5899
+ async function checkRelationshipExists(storage, targetCollection, refId) {
5900
+ const idStr = String(refId);
5901
+ if (await storage.getContent(targetCollection, idStr) !== null) return true;
5902
+ return await storage.getContentBySlug(targetCollection, idStr) !== null;
5903
+ }
5904
+ //#endregion
5905
+ //#region src/plugins/transfer/presets/hrms.ts
5906
+ /**
5907
+ * Preconfigured mapping definition for importing and exporting HRMS Employers.
5908
+ */
5909
+ const HRMS_EMPLOYER_TRANSFER_PRESET = {
5910
+ id: "hrms_employers_preset",
5911
+ name: "HRMS Employers Standard Mapping",
5912
+ collectionSlug: "employers",
5913
+ fields: [
5914
+ {
5915
+ sourceField: "Company Name",
5916
+ targetField: "companyName",
5917
+ required: true
5918
+ },
5919
+ {
5920
+ sourceField: "Legal Name",
5921
+ targetField: "legalName"
5922
+ },
5923
+ {
5924
+ sourceField: "Tax ID",
5925
+ targetField: "taxId"
5926
+ },
5927
+ {
5928
+ sourceField: "Primary Email",
5929
+ targetField: "email",
5930
+ transform: "trim"
5931
+ },
5932
+ {
5933
+ sourceField: "Phone",
5934
+ targetField: "phone",
5935
+ transform: "trim"
5936
+ },
5937
+ {
5938
+ sourceField: "Website",
5939
+ targetField: "website"
5940
+ },
5941
+ {
5942
+ sourceField: "Timezone",
5943
+ targetField: "timezone",
5944
+ defaultValue: "UTC"
5945
+ },
5946
+ {
5947
+ sourceField: "Status",
5948
+ targetField: "status",
5949
+ defaultValue: "active"
5950
+ }
5951
+ ],
5952
+ options: {
5953
+ autoGenerateSlug: true,
5954
+ defaultStatus: "published",
5955
+ onDuplicate: "update",
5956
+ uniqueIdentifierField: "taxId"
5957
+ }
5958
+ };
5959
+ /**
5960
+ * Preconfigured mapping definition for importing and exporting HRMS Employees.
5961
+ */
5962
+ const HRMS_EMPLOYEE_TRANSFER_PRESET = {
5963
+ id: "hrms_employees_preset",
5964
+ name: "HRMS Employees Standard Mapping",
5965
+ collectionSlug: "employees",
5966
+ fields: [
5967
+ {
5968
+ sourceField: "Employer ID",
5969
+ targetField: "employerId",
5970
+ required: true
5971
+ },
5972
+ {
5973
+ sourceField: "Employee Number",
5974
+ targetField: "employeeNumber",
5975
+ required: true,
5976
+ transform: "trim"
5977
+ },
5978
+ {
5979
+ sourceField: "First Name",
5980
+ targetField: "firstName",
5981
+ required: true,
5982
+ transform: "trim"
5983
+ },
5984
+ {
5985
+ sourceField: "Last Name",
5986
+ targetField: "lastName",
5987
+ required: true,
5988
+ transform: "trim"
5989
+ },
5990
+ {
5991
+ sourceField: "Work Email",
5992
+ targetField: "email",
5993
+ required: true,
5994
+ transform: "lowercase"
5995
+ },
5996
+ {
5997
+ sourceField: "Phone",
5998
+ targetField: "phone",
5999
+ transform: "trim"
6000
+ },
6001
+ {
6002
+ sourceField: "Job Title",
6003
+ targetField: "jobTitle"
6004
+ },
6005
+ {
6006
+ sourceField: "Employment Type",
6007
+ targetField: "employmentType",
6008
+ defaultValue: "full_time"
6009
+ },
6010
+ {
6011
+ sourceField: "Hire Date",
6012
+ targetField: "hireDate",
6013
+ required: true,
6014
+ transform: "date"
6015
+ },
6016
+ {
6017
+ sourceField: "Status",
6018
+ targetField: "status",
6019
+ defaultValue: "active"
6020
+ }
6021
+ ],
6022
+ options: {
6023
+ autoGenerateSlug: true,
6024
+ defaultStatus: "published",
6025
+ onDuplicate: "error",
6026
+ uniqueIdentifierField: "employeeNumber"
6027
+ }
6028
+ };
6029
+ //#endregion
6030
+ //#region src/plugins/transfer/service.ts
6031
+ var TransferService = class {
6032
+ engine;
6033
+ options;
6034
+ presets = /* @__PURE__ */ new Map();
6035
+ constructor(engine, options = {}) {
6036
+ this.engine = engine;
6037
+ this.options = options;
6038
+ this.registerPreset(HRMS_EMPLOYER_TRANSFER_PRESET);
6039
+ this.registerPreset(HRMS_EMPLOYEE_TRANSFER_PRESET);
6040
+ }
6041
+ /**
6042
+ * List all registered collections across all active plugins in the engine.
6043
+ */
6044
+ listCollections() {
6045
+ return this.engine.getCollections().map(summarizeCollection);
6046
+ }
6047
+ /**
6048
+ * Get the collection schema summary for a specific collection slug.
6049
+ */
6050
+ getCollectionSchema(collectionSlug) {
6051
+ return summarizeCollection(this.getCollectionConfigOrThrow(collectionSlug));
6052
+ }
6053
+ /**
6054
+ * Register a reusable data mapping preset.
6055
+ */
6056
+ registerPreset(preset) {
6057
+ const id = preset.id || `${preset.collectionSlug}_${preset.name || "preset"}`;
6058
+ this.presets.set(id, {
6059
+ ...preset,
6060
+ id
6061
+ });
6062
+ }
6063
+ /**
6064
+ * Get a registered preset by ID.
6065
+ */
6066
+ getPreset(id) {
6067
+ return this.presets.get(id);
6068
+ }
6069
+ /**
6070
+ * Get all registered presets, optionally filtered by target collection.
6071
+ */
6072
+ getPresets(collectionSlug) {
6073
+ const all = Array.from(this.presets.values());
6074
+ if (collectionSlug) return all.filter((p) => p.collectionSlug === collectionSlug);
6075
+ return all;
6076
+ }
6077
+ /**
6078
+ * Generate a 1:1 default mapping template for any collection.
6079
+ */
6080
+ getMappingTemplate(collectionSlug) {
6081
+ const config = this.getCollectionConfigOrThrow(collectionSlug);
6082
+ return {
6083
+ id: `${collectionSlug}_default_template`,
6084
+ name: `${config.label} Default Template`,
6085
+ collectionSlug,
6086
+ fields: config.fields.map((f) => ({
6087
+ sourceField: f.name,
6088
+ targetField: f.name,
6089
+ required: Boolean(f.required),
6090
+ defaultValue: f.defaultValue
6091
+ })),
6092
+ options: {
6093
+ autoGenerateSlug: true,
6094
+ defaultStatus: config.draftable ? "draft" : "published",
6095
+ onDuplicate: "error"
6096
+ }
6097
+ };
6098
+ }
6099
+ /**
6100
+ * Inspect any source payload or file (JSON, Excel, CSV) to extract headers,
6101
+ * infer data types, and generate auto-mapping suggestions against a target collection.
6102
+ */
6103
+ async inspectSource(input, options = {}) {
6104
+ const config = options.collectionSlug ? this.engine.getCollectionConfig(options.collectionSlug) : void 0;
6105
+ return inspectSource(input, {
6106
+ ...options,
6107
+ collectionConfig: config
6108
+ });
6109
+ }
6110
+ /**
6111
+ * Dry-run validation of a source batch against a target collection's schema
6112
+ * and live database constraints without committing any changes.
6113
+ */
6114
+ async validateImport(collectionSlug, input, mapping, options = {}) {
6115
+ const config = this.getCollectionConfigOrThrow(collectionSlug);
6116
+ const parsed = await parseSource(input, {
6117
+ format: options.format,
6118
+ fileName: options.fileName,
6119
+ sheetName: options.sheetName
6120
+ });
6121
+ const activeMapping = mapping || this.getMappingTemplate(collectionSlug);
6122
+ return validateTransferBatch(parsed.records, activeMapping, config, {
6123
+ storage: this.engine.storage,
6124
+ onDuplicate: options.onDuplicate,
6125
+ previewLimit: options.previewLimit
6126
+ });
6127
+ }
6128
+ /**
6129
+ * Execute an import batch from raw data or an external file (JSON, Excel, CSV)
6130
+ * into a target CMS collection.
6131
+ */
6132
+ async importData(collectionSlug, input, mapping, options = {}) {
6133
+ const config = this.getCollectionConfigOrThrow(collectionSlug);
6134
+ const parsed = await parseSource(input, {
6135
+ format: options.format,
6136
+ fileName: options.fileName,
6137
+ sheetName: options.sheetName
6138
+ });
6139
+ const activeMapping = mapping || this.getMappingTemplate(collectionSlug);
6140
+ const rawRecords = parsed.records;
6141
+ const onDuplicate = options.onDuplicate ?? activeMapping.options?.onDuplicate ?? "error";
6142
+ const abortOnError = options.abortOnError ?? activeMapping.options?.abortOnError ?? false;
6143
+ const validation = await validateTransferBatch(rawRecords, activeMapping, config, {
6144
+ storage: this.engine.storage,
6145
+ onDuplicate
6146
+ });
6147
+ if (options.dryRun) return {
6148
+ success: validation.valid,
6149
+ collectionSlug,
6150
+ totalRows: rawRecords.length,
6151
+ importedCount: validation.validCount,
6152
+ updatedCount: 0,
6153
+ skippedCount: 0,
6154
+ failedCount: validation.errorCount,
6155
+ createdIds: [],
6156
+ updatedIds: [],
6157
+ errors: validation.errors
6158
+ };
6159
+ if (abortOnError && !validation.valid) return {
6160
+ success: false,
6161
+ collectionSlug,
6162
+ totalRows: rawRecords.length,
6163
+ importedCount: 0,
6164
+ updatedCount: 0,
6165
+ skippedCount: 0,
6166
+ failedCount: validation.errorCount,
6167
+ createdIds: [],
6168
+ updatedIds: [],
6169
+ errors: validation.errors
6170
+ };
6171
+ await this.engine.hooks.doAction("cms.transfer_import_started", {
6172
+ collectionSlug,
6173
+ totalRows: rawRecords.length
6174
+ });
6175
+ const createdIds = [];
6176
+ const updatedIds = [];
6177
+ const errors = [...validation.errors];
6178
+ let importedCount = 0;
6179
+ let updatedCount = 0;
6180
+ let skippedCount = 0;
6181
+ const rowErrorsMap = /* @__PURE__ */ new Map();
6182
+ for (const err of validation.errors) {
6183
+ const list = rowErrorsMap.get(err.rowNumber) || [];
6184
+ list.push(err);
6185
+ rowErrorsMap.set(err.rowNumber, list);
6186
+ }
6187
+ const collService = this.engine.collection(collectionSlug);
6188
+ for (let i = 0; i < rawRecords.length; i++) {
6189
+ const rowNumber = i + 1;
6190
+ const rowErrors = rowErrorsMap.get(rowNumber);
6191
+ if (rowErrors && rowErrors.length > 0) {
6192
+ if (rowErrors.some((e) => e.code !== "CONSTRAINT_UNIQUE_VIOLATION" || onDuplicate === "error")) continue;
6193
+ }
6194
+ const raw = rawRecords[i];
6195
+ let mapped;
6196
+ try {
6197
+ mapped = mapSourceRecord(raw, activeMapping);
6198
+ } catch (err) {
6199
+ errors.push({
6200
+ rowNumber,
6201
+ code: "TRANSFORM_ERROR",
6202
+ reason: err instanceof Error ? err.message : String(err)
6203
+ });
6204
+ continue;
6205
+ }
6206
+ mapped = await this.engine.hooks.applyFilters("cms.transfer_before_import_row", mapped, {
6207
+ collectionSlug,
6208
+ rowNumber,
6209
+ raw
6210
+ });
6211
+ const identifierField = options.uniqueIdentifierField || activeMapping.options?.uniqueIdentifierField || this.detectUniqueField(config);
6212
+ let existingItem = null;
6213
+ if (identifierField && mapped.data[identifierField] !== void 0) {
6214
+ const val = mapped.data[identifierField];
6215
+ if (identifierField === "slug") existingItem = await this.engine.storage.getContentBySlug(collectionSlug, String(val));
6216
+ else existingItem = (await this.engine.storage.findContent(collectionSlug, {
6217
+ where: { [identifierField]: val },
6218
+ limit: 1
6219
+ })).items[0] || null;
6220
+ }
6221
+ if (existingItem) {
6222
+ if (onDuplicate === "skip") {
6223
+ skippedCount++;
6224
+ continue;
6225
+ }
6226
+ if (onDuplicate === "update") {
6227
+ try {
6228
+ const updated = await collService.update(existingItem.id, {
6229
+ title: mapped.title ?? existingItem.title,
6230
+ slug: mapped.slug ?? existingItem.slug,
6231
+ status: mapped.status ?? existingItem.status,
6232
+ data: {
6233
+ ...existingItem.data,
6234
+ ...mapped.data
6235
+ }
6236
+ }, options.authorId, options.revisionNote || "Import transfer update");
6237
+ if (updated) {
6238
+ updatedIds.push(updated.id);
6239
+ updatedCount++;
6240
+ await this.engine.hooks.doAction("cms.transfer_row_imported", {
6241
+ collectionSlug,
6242
+ action: "update",
6243
+ item: updated
6244
+ });
6245
+ }
6246
+ } catch (err) {
6247
+ errors.push({
6248
+ rowNumber,
6249
+ code: "VALIDATION_RULE_FAILED",
6250
+ reason: err instanceof Error ? err.message : String(err)
6251
+ });
6252
+ }
6253
+ continue;
6254
+ }
6255
+ }
6256
+ try {
6257
+ const created = await collService.create({
6258
+ title: mapped.title,
6259
+ slug: mapped.slug,
6260
+ status: mapped.status,
6261
+ data: mapped.data
6262
+ }, options.authorId);
6263
+ createdIds.push(created.id);
6264
+ importedCount++;
6265
+ await this.engine.hooks.doAction("cms.transfer_row_imported", {
6266
+ collectionSlug,
6267
+ action: "create",
6268
+ item: created
6269
+ });
6270
+ } catch (err) {
6271
+ errors.push({
6272
+ rowNumber,
6273
+ code: "VALIDATION_RULE_FAILED",
6274
+ reason: err instanceof Error ? err.message : String(err)
6275
+ });
6276
+ }
6277
+ }
6278
+ const result = {
6279
+ success: errors.length === 0,
6280
+ collectionSlug,
6281
+ totalRows: rawRecords.length,
6282
+ importedCount,
6283
+ updatedCount,
6284
+ skippedCount,
6285
+ failedCount: rawRecords.length - (importedCount + updatedCount + skippedCount),
6286
+ createdIds,
6287
+ updatedIds,
6288
+ errors
6289
+ };
6290
+ await this.engine.hooks.doAction("cms.transfer_import_completed", result);
6291
+ await this.engine.hooks.doAction(`cms.${collectionSlug}_transferred`, result);
6292
+ return result;
6293
+ }
6294
+ /**
6295
+ * Export CMS collection records to JSON, Excel (.xlsx), or CSV.
6296
+ */
6297
+ async exportData(collectionSlug, options = {}) {
6298
+ const config = this.getCollectionConfigOrThrow(collectionSlug);
6299
+ const collService = this.engine.collection(collectionSlug);
6300
+ const query = options.query || { limit: 1e4 };
6301
+ const items = (await collService.find(query)).items;
6302
+ const exportRecords = [];
6303
+ for (const item of items) {
6304
+ let mappedRow = mapCmsItemForExport(item, options.mapping, {
6305
+ includeId: options.includeId,
6306
+ includeTimestamps: options.includeTimestamps
6307
+ });
6308
+ mappedRow = await this.engine.hooks.applyFilters("cms.transfer_before_export_row", mappedRow, {
6309
+ collectionSlug,
6310
+ item
6311
+ });
6312
+ exportRecords.push(mappedRow);
6313
+ }
6314
+ return serializeSource(exportRecords, {
6315
+ format: options.format || "json",
6316
+ fileName: options.fileName || `${collectionSlug}_export_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
6317
+ sheetName: options.sheetName || config.label
6318
+ });
6319
+ }
6320
+ getCollectionConfigOrThrow(collectionSlug) {
6321
+ const config = this.engine.getCollectionConfig(collectionSlug);
6322
+ if (!config) throw new Error(`[TransferService] Collection '${collectionSlug}' is not registered in the CMS engine.`);
6323
+ return config;
6324
+ }
6325
+ detectUniqueField(config) {
6326
+ const unique = config.fields.find((f) => Boolean(f.unique));
6327
+ if (unique) return unique.name;
6328
+ const slugField = config.fields.find((f) => f.type === "slug" || f.name === "slug");
6329
+ if (slugField) return slugField.name;
6330
+ }
6331
+ };
6332
+ //#endregion
6333
+ //#region src/plugins/transfer/routes.ts
6334
+ function json(data, status = 200, headers = {}) {
6335
+ return new Response(JSON.stringify(data), {
6336
+ status,
6337
+ headers: {
6338
+ "Content-Type": "application/json",
6339
+ "Access-Control-Allow-Origin": "*",
6340
+ ...headers
6341
+ }
6342
+ });
6343
+ }
6344
+ function badRequest(message) {
6345
+ return json({ error: message }, 400);
6346
+ }
6347
+ function notFound(message) {
6348
+ return json({ error: message }, 404);
6349
+ }
6350
+ function registerTransferRoutes(ctx, service, options = {}) {
6351
+ const prefix = (options.apiPrefix ?? "/api/transfer").replace(/\/+$/, "");
6352
+ ctx.registerRoute("GET", `${prefix}/collections`, async () => {
6353
+ try {
6354
+ return json({ collections: service.listCollections() });
6355
+ } catch (err) {
6356
+ return badRequest(err instanceof Error ? err.message : String(err));
6357
+ }
6358
+ });
6359
+ ctx.registerRoute("GET", `${prefix}/presets`, async (_req, { url }) => {
6360
+ try {
6361
+ const collectionSlug = url.searchParams.get("collection") || void 0;
6362
+ return json({ presets: service.getPresets(collectionSlug) });
6363
+ } catch (err) {
6364
+ return badRequest(err instanceof Error ? err.message : String(err));
6365
+ }
6366
+ });
6367
+ ctx.registerRoute("POST", `${prefix}/presets`, async (req) => {
6368
+ try {
6369
+ const body = await req.json();
6370
+ if (!body.collectionSlug || !Array.isArray(body.fields)) return badRequest("collectionSlug and fields array are required.");
6371
+ service.registerPreset(body);
6372
+ return json({
6373
+ success: true,
6374
+ preset: body
6375
+ }, 201);
6376
+ } catch (err) {
6377
+ return badRequest(err instanceof Error ? err.message : String(err));
6378
+ }
6379
+ });
6380
+ ctx.registerRoute("GET", `${prefix}/:collection/schema`, async (_req, { params }) => {
6381
+ try {
6382
+ return json({
6383
+ schema: service.getCollectionSchema(params.collection),
6384
+ template: service.getMappingTemplate(params.collection)
6385
+ });
6386
+ } catch (err) {
6387
+ return notFound(err instanceof Error ? err.message : String(err));
6388
+ }
6389
+ });
6390
+ ctx.registerRoute("POST", `${prefix}/inspect`, async (req) => {
6391
+ try {
6392
+ const payload = await extractPayload(req);
6393
+ return json(await service.inspectSource(payload.data, {
6394
+ collectionSlug: payload.collectionSlug,
6395
+ format: payload.format,
6396
+ fileName: payload.fileName,
6397
+ sheetName: payload.sheetName
6398
+ }));
6399
+ } catch (err) {
6400
+ return badRequest(err instanceof Error ? err.message : String(err));
6401
+ }
6402
+ });
6403
+ ctx.registerRoute("POST", `${prefix}/:collection/preview`, async (req, { params }) => {
6404
+ try {
6405
+ const payload = await extractPayload(req);
6406
+ return json(await service.validateImport(params.collection, payload.data, payload.mapping, {
6407
+ format: payload.format,
6408
+ fileName: payload.fileName,
6409
+ sheetName: payload.sheetName,
6410
+ onDuplicate: payload.onDuplicate
6411
+ }));
6412
+ } catch (err) {
6413
+ return badRequest(err instanceof Error ? err.message : String(err));
6414
+ }
6415
+ });
6416
+ ctx.registerRoute("POST", `${prefix}/:collection/import`, async (req, { params }) => {
6417
+ try {
6418
+ const payload = await extractPayload(req);
6419
+ const result = await service.importData(params.collection, payload.data, payload.mapping, {
6420
+ format: payload.format,
6421
+ fileName: payload.fileName,
6422
+ sheetName: payload.sheetName,
6423
+ onDuplicate: payload.onDuplicate,
6424
+ uniqueIdentifierField: payload.uniqueIdentifierField,
6425
+ abortOnError: payload.abortOnError,
6426
+ authorId: payload.authorId,
6427
+ revisionNote: payload.revisionNote
6428
+ });
6429
+ return json(result, result.success ? 200 : 207);
6430
+ } catch (err) {
6431
+ return badRequest(err instanceof Error ? err.message : String(err));
6432
+ }
6433
+ });
6434
+ ctx.registerRoute("POST", `${prefix}/:collection/export`, async (req, { params }) => {
6435
+ try {
6436
+ const body = await req.json();
6437
+ const result = await service.exportData(params.collection, {
6438
+ format: body.format,
6439
+ mapping: body.mapping,
6440
+ query: body.query,
6441
+ fileName: body.fileName,
6442
+ sheetName: body.sheetName,
6443
+ includeId: body.includeId,
6444
+ includeTimestamps: body.includeTimestamps
6445
+ });
6446
+ return new Response(result.data, {
6447
+ status: 200,
6448
+ headers: {
6449
+ "Content-Type": result.mimeType,
6450
+ "Content-Disposition": `attachment; filename="${result.fileName}"`,
6451
+ "Access-Control-Allow-Origin": "*"
6452
+ }
6453
+ });
6454
+ } catch (err) {
6455
+ return badRequest(err instanceof Error ? err.message : String(err));
6456
+ }
6457
+ });
6458
+ ctx.registerRoute("GET", `${prefix}/:collection/export`, async (_req, { params, url }) => {
6459
+ try {
6460
+ const format = url.searchParams.get("format") || "json";
6461
+ const fileName = url.searchParams.get("fileName") || void 0;
6462
+ const status = url.searchParams.get("status") || void 0;
6463
+ const search = url.searchParams.get("search") || void 0;
6464
+ const result = await service.exportData(params.collection, {
6465
+ format,
6466
+ fileName,
6467
+ query: {
6468
+ status,
6469
+ search
6470
+ }
6471
+ });
6472
+ return new Response(result.data, {
6473
+ status: 200,
6474
+ headers: {
6475
+ "Content-Type": result.mimeType,
6476
+ "Content-Disposition": `attachment; filename="${result.fileName}"`,
6477
+ "Access-Control-Allow-Origin": "*"
6478
+ }
6479
+ });
6480
+ } catch (err) {
6481
+ return badRequest(err instanceof Error ? err.message : String(err));
6482
+ }
6483
+ });
6484
+ }
6485
+ /**
6486
+ * Helper to extract payload and options from JSON or Multipart requests.
6487
+ */
6488
+ async function extractPayload(req) {
6489
+ if ((req.headers.get("content-type") || "").includes("multipart/form-data")) {
6490
+ const formData = await req.formData();
6491
+ const file = formData.get("file");
6492
+ let data = null;
6493
+ let fileName;
6494
+ if (file && typeof file === "object" && "arrayBuffer" in file) {
6495
+ data = new Uint8Array(await file.arrayBuffer());
6496
+ fileName = file.name;
6497
+ }
6498
+ const mappingRaw = formData.get("mapping");
6499
+ const mapping = mappingRaw ? JSON.parse(String(mappingRaw)) : void 0;
6500
+ return {
6501
+ data,
6502
+ collectionSlug: formData.get("collectionSlug") || void 0,
6503
+ format: formData.get("format") || void 0,
6504
+ fileName: formData.get("fileName") || fileName,
6505
+ sheetName: formData.get("sheetName") || void 0,
6506
+ mapping,
6507
+ onDuplicate: formData.get("onDuplicate") || void 0,
6508
+ uniqueIdentifierField: formData.get("uniqueIdentifierField") || void 0,
6509
+ abortOnError: formData.get("abortOnError") === "true",
6510
+ authorId: formData.get("authorId") || void 0,
6511
+ revisionNote: formData.get("revisionNote") || void 0
6512
+ };
6513
+ }
6514
+ const body = await req.json();
6515
+ let data = body.data;
6516
+ if (typeof body.base64 === "string") data = Buffer.from(body.base64, "base64");
6517
+ return {
6518
+ data: data !== void 0 ? data : body,
6519
+ collectionSlug: body.collectionSlug,
6520
+ format: body.format,
6521
+ fileName: body.fileName,
6522
+ sheetName: body.sheetName,
6523
+ mapping: body.mapping,
6524
+ onDuplicate: body.onDuplicate,
6525
+ uniqueIdentifierField: body.uniqueIdentifierField,
6526
+ abortOnError: body.abortOnError,
6527
+ authorId: body.authorId,
6528
+ revisionNote: body.revisionNote
6529
+ };
6530
+ }
6531
+ //#endregion
6532
+ //#region src/plugins/transfer/client.ts
6533
+ var TransferClient = class {
6534
+ client;
6535
+ options;
6536
+ service;
6537
+ prefix;
6538
+ constructor(client, options = {}) {
6539
+ this.client = client;
6540
+ this.options = options;
6541
+ this.prefix = (options.apiPrefix ?? "/api/transfer").replace(/\/+$/, "");
6542
+ const engine = client.getEngine();
6543
+ if (engine) this.service = new TransferService(engine, options);
6544
+ }
6545
+ /**
6546
+ * List all registered collections across all active plugins.
6547
+ */
6548
+ async listCollections() {
6549
+ if (this.service) return this.service.listCollections();
6550
+ return (await this.client.request(`${this.prefix}/collections`)).collections;
6551
+ }
6552
+ /**
6553
+ * Get the collection schema and default mapping template.
6554
+ */
6555
+ async getSchema(collectionSlug) {
6556
+ if (this.service) return {
6557
+ schema: this.service.getCollectionSchema(collectionSlug),
6558
+ template: this.service.getMappingTemplate(collectionSlug)
6559
+ };
6560
+ return this.client.request(`${this.prefix}/${collectionSlug}/schema`);
6561
+ }
6562
+ /**
6563
+ * Inspect a source file or payload.
6564
+ */
6565
+ async inspect(data, options = {}) {
6566
+ if (this.service) return this.service.inspectSource(data, options);
6567
+ return this.client.request(`${this.prefix}/inspect`, {
6568
+ method: "POST",
6569
+ body: JSON.stringify({
6570
+ data,
6571
+ ...options
6572
+ })
6573
+ });
6574
+ }
6575
+ /**
6576
+ * Dry-run validation of a source batch against a target collection and database constraints.
6577
+ */
6578
+ async preview(collectionSlug, data, mapping, options = {}) {
6579
+ if (this.service) return this.service.validateImport(collectionSlug, data, mapping, options);
6580
+ return this.client.request(`${this.prefix}/${collectionSlug}/preview`, {
6581
+ method: "POST",
6582
+ body: JSON.stringify({
6583
+ data,
6584
+ mapping,
6585
+ ...options
6586
+ })
6587
+ });
6588
+ }
6589
+ /**
6590
+ * Execute an import batch into a target collection.
6591
+ */
6592
+ async import(collectionSlug, data, mapping, options = {}) {
6593
+ if (this.service) return this.service.importData(collectionSlug, data, mapping, options);
6594
+ return this.client.request(`${this.prefix}/${collectionSlug}/import`, {
6595
+ method: "POST",
6596
+ body: JSON.stringify({
6597
+ data,
6598
+ mapping,
6599
+ ...options
6600
+ })
6601
+ });
6602
+ }
6603
+ /**
6604
+ * Export collection records to JSON, Excel, or CSV.
6605
+ */
6606
+ async export(collectionSlug, options = {}) {
6607
+ if (this.service) return this.service.exportData(collectionSlug, options);
6608
+ return await this.client.request(`${this.prefix}/${collectionSlug}/export`, {
6609
+ method: "POST",
6610
+ body: JSON.stringify(options)
6611
+ });
6612
+ }
6613
+ };
6614
+ /**
6615
+ * Access or instantiate the TransferClient associated with a CMSClient instance.
6616
+ */
6617
+ function getTransferClient(client, options) {
6618
+ if (client.__transferClient) return client.__transferClient;
6619
+ const tc = new TransferClient(client, options);
6620
+ client.__transferClient = tc;
6621
+ return tc;
6622
+ }
6623
+ //#endregion
6624
+ //#region src/plugins/transfer/index.ts
6625
+ /**
6626
+ * @azlib/cms - Built-in Universal Transfer Plugin
6627
+ */
6628
+ /**
6629
+ * Built-in Universal Data Transfer plugin factory for @azlib/cms.
6630
+ * Equips the CMS engine with dynamic, schema-driven import and export capabilities
6631
+ * across all collections, featuring file schema discovery, definition mapping,
6632
+ * multi-level data validation, database constraint verification, and multi-format support.
6633
+ */
6634
+ const transferPlugin = definePlugin((options) => {
6635
+ const opts = options || {};
6636
+ return {
6637
+ name: "transfer",
6638
+ version: "1.0.0",
6639
+ description: "Universal schema-driven data transfer plugin supporting import/export, definition mapping, and constraint validation",
6640
+ setup(ctx) {
6641
+ const service = new TransferService(ctx.engine, opts);
6642
+ ctx.engine.__transferService = service;
6643
+ if (opts.enableRoutes !== false) registerTransferRoutes(ctx, service, opts);
6644
+ }
6645
+ };
6646
+ });
6647
+ /**
6648
+ * Retrieve the active TransferService instance associated with a CMSEngine.
6649
+ */
6650
+ function getTransferService(engine, options) {
6651
+ if (engine.__transferService) return engine.__transferService;
6652
+ const service = new TransferService(engine, options);
6653
+ engine.__transferService = service;
6654
+ return service;
6655
+ }
6656
+ //#endregion
3238
6657
  exports.CMSClient = CMSClient;
3239
6658
  exports.CMSEngine = CMSEngine;
3240
6659
  exports.CMSRouter = CMSRouter;
@@ -3243,6 +6662,10 @@ exports.DEFAULT_COLLECTIONS = DEFAULT_COLLECTIONS;
3243
6662
  exports.DEFAULT_ROLE_CAPABILITIES = DEFAULT_ROLE_CAPABILITIES;
3244
6663
  exports.EcommerceClient = EcommerceClient;
3245
6664
  exports.EcommerceService = EcommerceService;
6665
+ exports.HRMSClient = HRMSClient;
6666
+ exports.HRMSService = HRMSService;
6667
+ exports.HRMS_EMPLOYEE_TRANSFER_PRESET = HRMS_EMPLOYEE_TRANSFER_PRESET;
6668
+ exports.HRMS_EMPLOYER_TRANSFER_PRESET = HRMS_EMPLOYER_TRANSFER_PRESET;
3246
6669
  exports.HooksManager = HooksManager;
3247
6670
  exports.MediaManager = MediaManager;
3248
6671
  exports.MemoryStorageAdapter = MemoryStorageAdapter;
@@ -3250,23 +6673,53 @@ exports.OptionsManager = OptionsManager;
3250
6673
  exports.RBACManager = RBACManager;
3251
6674
  exports.RevisionManager = RevisionManager;
3252
6675
  exports.TaxonomyManager = TaxonomyManager;
6676
+ exports.TransferClient = TransferClient;
6677
+ exports.TransferService = TransferService;
3253
6678
  exports.VALID_STATUS_TRANSITIONS = VALID_STATUS_TRANSITIONS;
6679
+ exports.applyFieldTransform = applyFieldTransform;
3254
6680
  exports.collection = collection;
6681
+ exports.createAttendanceCollection = createAttendanceCollection;
3255
6682
  exports.createCMSEngine = createCMSEngine;
3256
6683
  exports.createCMSRouter = createCMSRouter;
3257
6684
  exports.createCmsClient = createCmsClient;
3258
6685
  exports.createDiscountCollection = createDiscountCollection;
3259
6686
  exports.createEcommerceTaxonomies = createEcommerceTaxonomies;
6687
+ exports.createEmployeeCollection = createEmployeeCollection;
6688
+ exports.createEmployerCollection = createEmployerCollection;
6689
+ exports.createHRMSTaxonomies = createHRMSTaxonomies;
6690
+ exports.createLeaveRequestCollection = createLeaveRequestCollection;
6691
+ exports.createLeaveTypeCollection = createLeaveTypeCollection;
3260
6692
  exports.createOrderCollection = createOrderCollection;
3261
6693
  exports.createProductCollection = createProductCollection;
3262
6694
  exports.defaultHooks = defaultHooks;
3263
6695
  exports.defineConfig = defineConfig;
3264
6696
  exports.definePlugin = definePlugin;
6697
+ exports.detectFormat = detectFormat;
3265
6698
  exports.ecommercePlugin = ecommercePlugin;
3266
6699
  exports.fields = fields;
6700
+ exports.generateSuggestedMapping = generateSuggestedMapping;
3267
6701
  exports.getEcommerceClient = getEcommerceClient;
3268
6702
  exports.getEcommerceService = getEcommerceService;
6703
+ exports.getHRMSClient = getHRMSClient;
6704
+ exports.getHRMSService = getHRMSService;
6705
+ exports.getTransferClient = getTransferClient;
6706
+ exports.getTransferService = getTransferService;
6707
+ exports.hrmsPlugin = hrmsPlugin;
6708
+ exports.inspectSource = inspectSource;
6709
+ exports.mapCmsItemForExport = mapCmsItemForExport;
6710
+ exports.mapSourceRecord = mapSourceRecord;
3269
6711
  exports.normalizeConfig = normalizeConfig;
6712
+ exports.parseCsvSource = parseCsvSource;
6713
+ exports.parseExcelSource = parseExcelSource;
6714
+ exports.parseJsonSource = parseJsonSource;
6715
+ exports.parseSource = parseSource;
3270
6716
  exports.resolveUniqueSlug = resolveUniqueSlug;
6717
+ exports.serializeCsv = serializeCsv;
6718
+ exports.serializeExcel = serializeExcel;
6719
+ exports.serializeJson = serializeJson;
6720
+ exports.serializeSource = serializeSource;
3271
6721
  exports.slugify = slugify;
6722
+ exports.summarizeCollection = summarizeCollection;
6723
+ exports.transferPlugin = transferPlugin;
3272
6724
  exports.validateAndNormalizeData = validateAndNormalizeData;
6725
+ exports.validateTransferBatch = validateTransferBatch;