@7365admin1/module-hygiene 4.10.0 → 4.12.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.mjs CHANGED
@@ -2486,12 +2486,30 @@ function useParentChecklistRepo() {
2486
2486
  throw error;
2487
2487
  }
2488
2488
  }
2489
+ async function getTodayParentChecklists() {
2490
+ const now = /* @__PURE__ */ new Date();
2491
+ const start = new Date(now);
2492
+ start.setUTCHours(0, 0, 0, 0);
2493
+ const end = new Date(now);
2494
+ end.setUTCHours(23, 59, 59, 999);
2495
+ try {
2496
+ const items = await collection.find(
2497
+ { createdAt: { $gte: start, $lte: end } },
2498
+ { projection: { _id: 1, site: 1 } }
2499
+ ).toArray();
2500
+ return items;
2501
+ } catch (error) {
2502
+ logger14.error("Failed to get today's parent checklists", error);
2503
+ throw error;
2504
+ }
2505
+ }
2489
2506
  return {
2490
2507
  createIndex,
2491
2508
  createParentChecklist,
2492
2509
  getAllParentChecklist,
2493
2510
  updateParentChecklistStatuses,
2494
- closeExpiredParentChecklists
2511
+ closeExpiredParentChecklists,
2512
+ getTodayParentChecklists
2495
2513
  };
2496
2514
  }
2497
2515
 
@@ -2587,7 +2605,9 @@ var areaChecklistSchema = Joi8.object({
2587
2605
  unit: Joi8.string().hex().required(),
2588
2606
  name: Joi8.string().required()
2589
2607
  }).required()
2590
- ).min(1).required()
2608
+ ).min(1).required(),
2609
+ isScheduleTask: Joi8.boolean().optional().default(false),
2610
+ scheduleTaskId: Joi8.string().hex().optional().allow(null)
2591
2611
  }).required()
2592
2612
  ).optional().default([]),
2593
2613
  createdBy: Joi8.string().hex().required()
@@ -2616,6 +2636,8 @@ function MAreaChecklist(value) {
2616
2636
  value.checklist = value.checklist.map((checklistItem) => {
2617
2637
  return {
2618
2638
  set: checklistItem.set,
2639
+ isScheduleTask: checklistItem.isScheduleTask ?? false,
2640
+ scheduleTaskId: checklistItem.scheduleTaskId ?? void 0,
2619
2641
  units: checklistItem.units.map((unit) => ({
2620
2642
  unit: new ObjectId8(unit.unit),
2621
2643
  name: unit.name,
@@ -2651,7 +2673,13 @@ function MAreaChecklist(value) {
2651
2673
  }
2652
2674
 
2653
2675
  // src/services/hygiene-area-checklist.service.ts
2654
- import { logger as logger18, useAtlas as useAtlas7 } from "@7365admin1/node-server-utils";
2676
+ import { ObjectId as ObjectId10 } from "mongodb";
2677
+ import {
2678
+ logger as logger18,
2679
+ UnauthorizedError,
2680
+ useAtlas as useAtlas7
2681
+ } from "@7365admin1/node-server-utils";
2682
+ import { useUserRepo } from "@7365admin1/core";
2655
2683
 
2656
2684
  // src/repositories/hygiene-area-checklist.repository.ts
2657
2685
  import { ObjectId as ObjectId9 } from "mongodb";
@@ -2701,26 +2729,26 @@ function useAreaChecklistRepo() {
2701
2729
  );
2702
2730
  }
2703
2731
  }
2732
+ async function createUniqueIndex() {
2733
+ try {
2734
+ await collection.createIndex({ schedule: 1, area: 1 }, { unique: true });
2735
+ } catch (error) {
2736
+ throw new InternalServerError5(
2737
+ "Failed to create unique index on hygiene checklist area."
2738
+ );
2739
+ }
2740
+ }
2704
2741
  async function createAreaChecklist(value, session) {
2705
2742
  try {
2706
2743
  const parentChecklistId = new ObjectId9(value.schedule);
2707
- const currentDate = /* @__PURE__ */ new Date();
2708
- const startOfDay = new Date(currentDate);
2709
- startOfDay.setUTCHours(0, 0, 0, 0);
2710
- const endOfDay = new Date(currentDate);
2711
- endOfDay.setUTCHours(23, 59, 59, 999);
2712
- const existingChecklist = await collection.findOne({
2713
- type: value.type,
2714
- schedule: parentChecklistId,
2715
- name: value.name,
2716
- createdAt: {
2717
- $gte: startOfDay,
2718
- $lte: endOfDay
2719
- }
2720
- });
2744
+ const areaId = new ObjectId9(value.area);
2745
+ const existingChecklist = await collection.findOne(
2746
+ { schedule: parentChecklistId, area: areaId },
2747
+ { session }
2748
+ );
2721
2749
  if (existingChecklist) {
2722
2750
  logger17.info(
2723
- `Area checklist already exists for area ${value.name} on ${currentDate.toISOString().split("T")[0]}`
2751
+ `Area checklist already exists for area ${value.name} (schedule: ${parentChecklistId}, area: ${areaId})`
2724
2752
  );
2725
2753
  return existingChecklist._id;
2726
2754
  }
@@ -2736,6 +2764,14 @@ function useAreaChecklistRepo() {
2736
2764
  });
2737
2765
  return result.insertedId;
2738
2766
  } catch (error) {
2767
+ if (error?.code === 11e3) {
2768
+ logger17.info(`Duplicate area checklist skipped for area ${value.name}`);
2769
+ const existing = await collection.findOne({
2770
+ schedule: new ObjectId9(value.schedule),
2771
+ area: new ObjectId9(value.area)
2772
+ });
2773
+ return existing._id;
2774
+ }
2739
2775
  throw error;
2740
2776
  }
2741
2777
  }
@@ -3073,6 +3109,9 @@ function useAreaChecklistRepo() {
3073
3109
  $project: {
3074
3110
  _id: 0,
3075
3111
  set: "$checklist.set",
3112
+ isScheduleTask: {
3113
+ $ifNull: ["$checklist.isScheduleTask", false]
3114
+ },
3076
3115
  unit: "$checklist.units.unit",
3077
3116
  name: "$checklist.units.name",
3078
3117
  remarks: "$checklist.units.remarks",
@@ -3103,6 +3142,7 @@ function useAreaChecklistRepo() {
3103
3142
  {
3104
3143
  $group: {
3105
3144
  _id: "$set",
3145
+ isScheduleTask: { $first: "$isScheduleTask" },
3106
3146
  units: {
3107
3147
  $push: {
3108
3148
  unit: "$unit",
@@ -3119,6 +3159,7 @@ function useAreaChecklistRepo() {
3119
3159
  $project: {
3120
3160
  _id: 0,
3121
3161
  set: "$_id",
3162
+ isScheduleTask: 1,
3122
3163
  units: 1
3123
3164
  }
3124
3165
  },
@@ -3192,7 +3233,19 @@ function useAreaChecklistRepo() {
3192
3233
  },
3193
3234
  {
3194
3235
  $addFields: {
3195
- isCompleted: { $ne: ["$checklist.units.completedBy", ""] }
3236
+ isCompleted: { $ne: ["$checklist.units.completedBy", ""] },
3237
+ isActioned: {
3238
+ $cond: {
3239
+ if: {
3240
+ $or: [
3241
+ { $eq: ["$checklist.units.approve", true] },
3242
+ { $eq: ["$checklist.units.reject", true] }
3243
+ ]
3244
+ },
3245
+ then: 1,
3246
+ else: 0
3247
+ }
3248
+ }
3196
3249
  }
3197
3250
  },
3198
3251
  {
@@ -3221,11 +3274,24 @@ function useAreaChecklistRepo() {
3221
3274
  preserveNullAndEmptyArrays: true
3222
3275
  }
3223
3276
  },
3224
- { $sort: { set: 1, isCompleted: -1, "checklist.units.timestamp": 1 } },
3277
+ {
3278
+ $sort: {
3279
+ "checklist.set": 1,
3280
+ isActioned: 1,
3281
+ "checklist.units.timestamp": 1,
3282
+ isCompleted: -1
3283
+ }
3284
+ },
3225
3285
  {
3226
3286
  $project: {
3227
3287
  _id: 0,
3228
3288
  set: "$checklist.set",
3289
+ isScheduleTask: {
3290
+ $ifNull: ["$checklist.isScheduleTask", false]
3291
+ },
3292
+ scheduleTaskId: {
3293
+ $ifNull: ["$checklist.scheduleTaskId", null]
3294
+ },
3229
3295
  remarks: "$checklist.remarks",
3230
3296
  attachment: "$checklist.attachment",
3231
3297
  unit: "$checklist.units.unit",
@@ -3258,6 +3324,8 @@ function useAreaChecklistRepo() {
3258
3324
  {
3259
3325
  $group: {
3260
3326
  _id: "$set",
3327
+ isScheduleTask: { $first: "$isScheduleTask" },
3328
+ scheduleTaskId: { $first: "$scheduleTaskId" },
3261
3329
  remarks: { $first: "$remarks" },
3262
3330
  attachment: { $first: "$attachment" },
3263
3331
  completedByName: { $first: "$completedByName" },
@@ -3279,13 +3347,33 @@ function useAreaChecklistRepo() {
3279
3347
  $project: {
3280
3348
  _id: 0,
3281
3349
  set: "$_id",
3350
+ isScheduleTask: 1,
3351
+ scheduleTaskId: 1,
3282
3352
  remarks: "$remarks",
3283
3353
  attachment: "$attachment",
3284
3354
  completedByName: 1,
3285
3355
  units: 1
3286
3356
  }
3287
3357
  },
3288
- { $sort: { set: 1 } },
3358
+ {
3359
+ $addFields: {
3360
+ isFullyActioned: {
3361
+ $allElementsTrue: {
3362
+ $map: {
3363
+ input: "$units",
3364
+ as: "u",
3365
+ in: {
3366
+ $or: [
3367
+ { $eq: ["$$u.approve", true] },
3368
+ { $eq: ["$$u.reject", true] }
3369
+ ]
3370
+ }
3371
+ }
3372
+ }
3373
+ }
3374
+ }
3375
+ },
3376
+ { $sort: { isFullyActioned: 1, set: 1 } },
3289
3377
  { $skip: page * limit },
3290
3378
  { $limit: limit }
3291
3379
  ];
@@ -3489,12 +3577,26 @@ function useAreaChecklistRepo() {
3489
3577
  throw error;
3490
3578
  }
3491
3579
  }
3580
+ async function getAreaChecklistSetOwner(_id, set, session) {
3581
+ try {
3582
+ _id = new ObjectId9(_id);
3583
+ } catch (error) {
3584
+ throw new BadRequestError15("Invalid area checklist ID format.");
3585
+ }
3586
+ const existing = await collection.findOne(
3587
+ { _id, "checklist.set": set },
3588
+ { projection: { "checklist.$": 1 }, session }
3589
+ );
3590
+ if (!existing?.checklist?.length)
3591
+ return null;
3592
+ return existing.checklist[0].units.map((u) => u.completedBy).find((cb) => cb && cb.toString() !== "") ?? null;
3593
+ }
3492
3594
  async function getMaxSetNumberForArea(areaId, session) {
3493
3595
  try {
3494
3596
  const _id = new ObjectId9(areaId);
3495
3597
  const result = await collection.aggregate(
3496
3598
  [
3497
- { $match: { area: _id.toString() } },
3599
+ { $match: { area: _id } },
3498
3600
  { $unwind: "$checklist" },
3499
3601
  { $group: { _id: null, maxSet: { $max: "$checklist.set" } } }
3500
3602
  ],
@@ -3542,9 +3644,155 @@ function useAreaChecklistRepo() {
3542
3644
  throw error;
3543
3645
  }
3544
3646
  }
3647
+ async function pushScheduleTaskSets(scheduleId, areaId, scheduleTaskId, newSets) {
3648
+ try {
3649
+ const schedule = new ObjectId9(scheduleId);
3650
+ const area = new ObjectId9(areaId);
3651
+ const taskId = new ObjectId9(scheduleTaskId);
3652
+ const result = await collection.updateOne(
3653
+ {
3654
+ schedule,
3655
+ area,
3656
+ checklist: { $not: { $elemMatch: { scheduleTaskId: taskId } } }
3657
+ },
3658
+ {
3659
+ $push: { checklist: { $each: newSets } },
3660
+ $set: { updatedAt: /* @__PURE__ */ new Date() }
3661
+ }
3662
+ );
3663
+ if (result.modifiedCount > 0) {
3664
+ delNamespace().catch((err) => {
3665
+ logger17.error(
3666
+ `Failed to clear cache for namespace: ${namespace_collection}`,
3667
+ err
3668
+ );
3669
+ });
3670
+ }
3671
+ return result.modifiedCount;
3672
+ } catch (error) {
3673
+ logger17.error("Error in pushScheduleTaskSets:", error);
3674
+ throw error;
3675
+ }
3676
+ }
3677
+ async function insertAutoGenSets(scheduleId, areaId, newSets) {
3678
+ try {
3679
+ const schedule = new ObjectId9(scheduleId);
3680
+ const area = new ObjectId9(areaId);
3681
+ const result = await collection.updateOne(
3682
+ {
3683
+ schedule,
3684
+ area,
3685
+ checklist: {
3686
+ $not: { $elemMatch: { isScheduleTask: { $ne: true } } }
3687
+ }
3688
+ },
3689
+ {
3690
+ $push: { checklist: { $each: newSets } },
3691
+ $set: { updatedAt: /* @__PURE__ */ new Date() }
3692
+ }
3693
+ );
3694
+ if (result.modifiedCount > 0) {
3695
+ delNamespace().catch((err) => {
3696
+ logger17.error(
3697
+ `Failed to clear cache for namespace: ${namespace_collection}`,
3698
+ err
3699
+ );
3700
+ });
3701
+ }
3702
+ return result.modifiedCount;
3703
+ } catch (error) {
3704
+ logger17.error("Error in insertAutoGenSets:", error);
3705
+ throw error;
3706
+ }
3707
+ }
3708
+ async function trimOverflowSets() {
3709
+ try {
3710
+ const db2 = useAtlas6.getDb();
3711
+ if (!db2)
3712
+ return 0;
3713
+ const bloated = await collection.aggregate([
3714
+ {
3715
+ $lookup: {
3716
+ from: "site.cleaning.areas",
3717
+ localField: "area",
3718
+ foreignField: "_id",
3719
+ as: "areaDoc"
3720
+ }
3721
+ },
3722
+ {
3723
+ $unwind: { path: "$areaDoc", preserveNullAndEmptyArrays: false }
3724
+ },
3725
+ {
3726
+ $addFields: {
3727
+ configuredSet: { $max: [1, { $ifNull: ["$areaDoc.set", 1] }] },
3728
+ autoGenSets: {
3729
+ $filter: {
3730
+ input: { $ifNull: ["$checklist", []] },
3731
+ as: "item",
3732
+ cond: { $ne: ["$$item.isScheduleTask", true] }
3733
+ }
3734
+ },
3735
+ scheduledSets: {
3736
+ $filter: {
3737
+ input: { $ifNull: ["$checklist", []] },
3738
+ as: "item",
3739
+ cond: { $eq: ["$$item.isScheduleTask", true] }
3740
+ }
3741
+ }
3742
+ }
3743
+ },
3744
+ {
3745
+ $match: {
3746
+ $expr: {
3747
+ $gt: [{ $size: "$autoGenSets" }, "$configuredSet"]
3748
+ }
3749
+ }
3750
+ },
3751
+ {
3752
+ $project: {
3753
+ _id: 1,
3754
+ checklist: 1,
3755
+ configuredSet: 1,
3756
+ autoGenSets: 1,
3757
+ scheduledSets: 1
3758
+ }
3759
+ }
3760
+ ]).toArray();
3761
+ if (bloated.length === 0) {
3762
+ logger17.info(
3763
+ "trimOverflowSets: no bloated area-checklist documents found"
3764
+ );
3765
+ return 0;
3766
+ }
3767
+ let trimmed = 0;
3768
+ for (const doc of bloated) {
3769
+ const limit = Number(doc.configuredSet) || 1;
3770
+ const trimmedAutoGen = [...doc.autoGenSets ?? []].sort((a, b) => (a.set ?? 0) - (b.set ?? 0)).slice(0, limit);
3771
+ const keep = [...trimmedAutoGen, ...doc.scheduledSets ?? []];
3772
+ const res = await collection.updateOne(
3773
+ { _id: doc._id },
3774
+ { $set: { checklist: keep, updatedAt: /* @__PURE__ */ new Date() } }
3775
+ );
3776
+ if (res.modifiedCount > 0) {
3777
+ trimmed++;
3778
+ logger17.info(
3779
+ `trimOverflowSets: trimmed doc ${doc._id} \u2014 kept ${keep.length} sets (${trimmedAutoGen.length} auto-gen, ${(doc.scheduledSets ?? []).length} scheduled), removed ${doc.checklist.length - keep.length} excess auto-gen`
3780
+ );
3781
+ }
3782
+ }
3783
+ delNamespace().catch(() => {
3784
+ });
3785
+ logger17.info(`trimOverflowSets: trimmed ${trimmed} document(s)`);
3786
+ return trimmed;
3787
+ } catch (error) {
3788
+ logger17.error("Error in trimOverflowSets:", error);
3789
+ return 0;
3790
+ }
3791
+ }
3545
3792
  return {
3546
3793
  createIndex,
3547
3794
  createTextIndex,
3795
+ createUniqueIndex,
3548
3796
  createAreaChecklist,
3549
3797
  getAllAreaChecklist,
3550
3798
  getAreaChecklistHistory,
@@ -3552,11 +3800,15 @@ function useAreaChecklistRepo() {
3552
3800
  getAreaChecklistUnits,
3553
3801
  getAreaChecklistById,
3554
3802
  getAreaChecklistByAreaAndSchedule,
3803
+ getAreaChecklistSetOwner,
3555
3804
  updateAreaChecklist,
3556
3805
  updateAreaChecklistStatus,
3557
3806
  updateAreaChecklistUnits,
3558
3807
  getMaxSetNumberForArea,
3559
- closeExpiredAreaChecklists
3808
+ closeExpiredAreaChecklists,
3809
+ pushScheduleTaskSets,
3810
+ insertAutoGenSets,
3811
+ trimOverflowSets
3560
3812
  };
3561
3813
  }
3562
3814
 
@@ -3568,17 +3820,18 @@ function useAreaChecklistService() {
3568
3820
  getAllAreaChecklist,
3569
3821
  getAreaChecklistUnits,
3570
3822
  getAreaChecklistById,
3823
+ getAreaChecklistByAreaAndSchedule,
3824
+ getAreaChecklistSetOwner,
3571
3825
  updateAreaChecklistUnits: _updateAreaChecklistUnits,
3572
3826
  updateAreaChecklistStatus,
3573
- getMaxSetNumberForArea
3827
+ insertAutoGenSets
3574
3828
  } = useAreaChecklistRepo();
3575
3829
  const { updateParentChecklistStatuses } = useParentChecklistRepo();
3830
+ const { getUserById } = useUserRepo();
3576
3831
  async function createAreaChecklist(value) {
3577
- const session = useAtlas7.getClient()?.startSession();
3832
+ const results = [];
3833
+ let totalChecklistsCreated = 0;
3578
3834
  try {
3579
- session?.startTransaction();
3580
- const results = [];
3581
- let totalChecklistsCreated = 0;
3582
3835
  const BATCH_SIZE = 10;
3583
3836
  const areasResult = await getAreasForChecklist(value.site);
3584
3837
  const areas = areasResult || [];
@@ -3592,15 +3845,72 @@ function useAreaChecklistService() {
3592
3845
  );
3593
3846
  return null;
3594
3847
  }
3595
- const maxSet = await getMaxSetNumberForArea(area._id, session);
3596
- const setCount = area.set || 1;
3848
+ let existing = null;
3849
+ try {
3850
+ existing = await getAreaChecklistByAreaAndSchedule(
3851
+ value.schedule.toString(),
3852
+ area._id.toString()
3853
+ );
3854
+ } catch (_) {
3855
+ }
3856
+ if (existing) {
3857
+ const hasAutoGenSets = existing.checklist?.some(
3858
+ (c) => c.isScheduleTask !== true
3859
+ );
3860
+ if (hasAutoGenSets) {
3861
+ logger18.info(
3862
+ `Area checklist already exists with auto-gen sets for area ${area.name}, skipping`
3863
+ );
3864
+ return existing._id;
3865
+ }
3866
+ logger18.info(
3867
+ `Area checklist for area ${area.name} exists but has no auto-gen sets. Inserting auto-gen sets.`
3868
+ );
3869
+ const setCount2 = Number(area.set) || 1;
3870
+ const totalExistingSets = existing.checklist?.length ?? 0;
3871
+ if (totalExistingSets >= setCount2) {
3872
+ logger18.info(
3873
+ `Area checklist for area ${area.name} already has ${totalExistingSets} set(s) (configured: ${setCount2}). Skipping auto-gen sets.`
3874
+ );
3875
+ return existing._id;
3876
+ }
3877
+ const maxExistingSet = existing.checklist?.reduce(
3878
+ (max, c) => Math.max(max, c.set ?? 0),
3879
+ 0
3880
+ ) ?? 0;
3881
+ const setsToAdd = setCount2 - totalExistingSets;
3882
+ const autoGenSets = Array.from(
3883
+ { length: setsToAdd },
3884
+ (_, index) => ({
3885
+ set: maxExistingSet + index + 1,
3886
+ isScheduleTask: false,
3887
+ units: area.units.map((unit) => ({
3888
+ unit: new ObjectId10(unit.unit),
3889
+ name: unit.name,
3890
+ approve: false,
3891
+ reject: false,
3892
+ status: "open",
3893
+ remarks: "",
3894
+ completedBy: "",
3895
+ timestamp: ""
3896
+ }))
3897
+ })
3898
+ );
3899
+ await insertAutoGenSets(
3900
+ value.schedule.toString(),
3901
+ area._id.toString(),
3902
+ autoGenSets
3903
+ );
3904
+ return existing._id;
3905
+ }
3906
+ const setCount = Number(area.set) || 1;
3597
3907
  const checklistData = {
3598
3908
  schedule: value.schedule,
3599
3909
  area: area._id.toString(),
3600
3910
  name: area.name,
3601
3911
  type: area.type,
3602
3912
  checklist: Array.from({ length: setCount }, (_, index) => ({
3603
- set: maxSet + index + 1,
3913
+ set: index + 1,
3604
3914
  units: area.units.map((unit) => ({
3605
3915
  unit: unit.unit.toString(),
3606
3916
  name: unit.name
@@ -3608,10 +3918,7 @@ function useAreaChecklistService() {
3608
3918
  })),
3609
3919
  createdBy: value.createdBy
3610
3920
  };
3611
- const insertedId = await _createAreaChecklist(
3612
- checklistData,
3613
- session
3614
- );
3921
+ const insertedId = await _createAreaChecklist(checklistData);
3615
3922
  totalChecklistsCreated++;
3616
3923
  return insertedId;
3617
3924
  });
@@ -3624,25 +3931,26 @@ function useAreaChecklistService() {
3624
3931
  } else {
3625
3932
  logger18.warn(`No common areas found for site: ${value.site}`);
3626
3933
  }
3627
- await session?.commitTransaction();
3628
3934
  logger18.info(
3629
3935
  `Successfully created ${totalChecklistsCreated} area checklists for site: ${value.site}`
3630
3936
  );
3631
3937
  return results;
3632
3938
  } catch (error) {
3633
3939
  logger18.error(`Error generating area checklists:`, error);
3634
- if (session?.inTransaction()) {
3635
- await session?.abortTransaction();
3636
- }
3637
3940
  throw error;
3638
- } finally {
3639
- session?.endSession();
3640
3941
  }
3641
3942
  }
3642
3943
  async function updateAreaChecklistUnits(_id, set, unitId, value) {
3643
3944
  const session = useAtlas7.getClient()?.startSession();
3644
3945
  try {
3645
3946
  session?.startTransaction();
3947
+ const setOwner = await getAreaChecklistSetOwner(_id, set, session);
3948
+ if (setOwner && value.completedBy && setOwner.toString() !== value.completedBy.toString()) {
3949
+ const acceptedBy = await getUserById(setOwner.toString());
3950
+ throw new UnauthorizedError(
3951
+ `${acceptedBy.name} has already taken this set.`
3952
+ );
3953
+ }
3646
3954
  await _updateAreaChecklistUnits(_id, set, unitId, value, session);
3647
3955
  const allUnitsResult = await getAreaChecklistUnits(
3648
3956
  {
@@ -3733,7 +4041,7 @@ import { BadRequestError as BadRequestError16, InternalServerError as InternalSe
3733
4041
  // src/services/hygiene-checklist-pdf.service.ts
3734
4042
  import { launch } from "puppeteer";
3735
4043
  import { InternalServerError as InternalServerError6, useAtlas as useAtlas8 } from "@7365admin1/node-server-utils";
3736
- import { ObjectId as ObjectId10 } from "mongodb";
4044
+ import { ObjectId as ObjectId11 } from "mongodb";
3737
4045
  function escapeHtml(text) {
3738
4046
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3739
4047
  }
@@ -3907,7 +4215,7 @@ function useChecklistPdfService() {
3907
4215
  const collection = db.collection("site.cleaning.schedule.areas");
3908
4216
  let scheduleObjectId;
3909
4217
  try {
3910
- scheduleObjectId = new ObjectId10(scheduleId);
4218
+ scheduleObjectId = new ObjectId11(scheduleId);
3911
4219
  } catch {
3912
4220
  throw new InternalServerError6("Invalid schedule ID format.");
3913
4221
  }
@@ -4216,7 +4524,7 @@ function useAreaChecklistController() {
4216
4524
 
4217
4525
  // src/models/hygiene-supply.model.ts
4218
4526
  import Joi10 from "joi";
4219
- import { ObjectId as ObjectId11 } from "mongodb";
4527
+ import { ObjectId as ObjectId12 } from "mongodb";
4220
4528
  import { BadRequestError as BadRequestError17, logger as logger21 } from "@7365admin1/node-server-utils";
4221
4529
  var supplySchema = Joi10.object({
4222
4530
  site: Joi10.string().hex().required(),
@@ -4231,7 +4539,7 @@ function MSupply(value) {
4231
4539
  }
4232
4540
  if (value.site) {
4233
4541
  try {
4234
- value.site = new ObjectId11(value.site);
4542
+ value.site = new ObjectId12(value.site);
4235
4543
  } catch (error2) {
4236
4544
  throw new BadRequestError17("Invalid site ID format.");
4237
4545
  }
@@ -4249,7 +4557,7 @@ function MSupply(value) {
4249
4557
  }
4250
4558
 
4251
4559
  // src/repositories/hygiene-supply.repository.ts
4252
- import { ObjectId as ObjectId12 } from "mongodb";
4560
+ import { ObjectId as ObjectId13 } from "mongodb";
4253
4561
  import {
4254
4562
  useAtlas as useAtlas9,
4255
4563
  InternalServerError as InternalServerError8,
@@ -4334,7 +4642,7 @@ function useSupplyRepository() {
4334
4642
  limit
4335
4643
  };
4336
4644
  try {
4337
- site = new ObjectId12(site);
4645
+ site = new ObjectId13(site);
4338
4646
  query.site = site;
4339
4647
  cacheOptions.site = site.toString();
4340
4648
  } catch (error) {
@@ -4378,7 +4686,7 @@ function useSupplyRepository() {
4378
4686
  }
4379
4687
  async function getSupplyById(_id, session) {
4380
4688
  try {
4381
- _id = new ObjectId12(_id);
4689
+ _id = new ObjectId13(_id);
4382
4690
  } catch (error) {
4383
4691
  throw new BadRequestError18("Invalid supply ID format.");
4384
4692
  }
@@ -4424,7 +4732,7 @@ function useSupplyRepository() {
4424
4732
  }
4425
4733
  async function updateSupply(_id, value, session) {
4426
4734
  try {
4427
- _id = new ObjectId12(_id);
4735
+ _id = new ObjectId13(_id);
4428
4736
  } catch (error) {
4429
4737
  throw new BadRequestError18("Invalid supply ID format.");
4430
4738
  }
@@ -4457,7 +4765,7 @@ function useSupplyRepository() {
4457
4765
  }
4458
4766
  async function deleteSupply(_id, session) {
4459
4767
  try {
4460
- _id = new ObjectId12(_id);
4768
+ _id = new ObjectId13(_id);
4461
4769
  } catch (error) {
4462
4770
  throw new BadRequestError18("Invalid supply ID format.");
4463
4771
  }
@@ -4638,7 +4946,7 @@ function useSupplyController() {
4638
4946
 
4639
4947
  // src/models/hygiene-stock.model.ts
4640
4948
  import Joi12 from "joi";
4641
- import { ObjectId as ObjectId13 } from "mongodb";
4949
+ import { ObjectId as ObjectId14 } from "mongodb";
4642
4950
  import { BadRequestError as BadRequestError20, logger as logger24 } from "@7365admin1/node-server-utils";
4643
4951
  var stockSchema = Joi12.object({
4644
4952
  site: Joi12.string().hex().required(),
@@ -4656,14 +4964,14 @@ function MStock(value) {
4656
4964
  }
4657
4965
  if (value.site) {
4658
4966
  try {
4659
- value.site = new ObjectId13(value.site);
4967
+ value.site = new ObjectId14(value.site);
4660
4968
  } catch (error2) {
4661
4969
  throw new BadRequestError20("Invalid site ID format.");
4662
4970
  }
4663
4971
  }
4664
4972
  if (value.supply) {
4665
4973
  try {
4666
- value.supply = new ObjectId13(value.supply);
4974
+ value.supply = new ObjectId14(value.supply);
4667
4975
  } catch (error2) {
4668
4976
  throw new BadRequestError20("Invalid supply ID format.");
4669
4977
  }
@@ -4683,7 +4991,7 @@ function MStock(value) {
4683
4991
  }
4684
4992
 
4685
4993
  // src/repositories/hygiene-stock.repository.ts
4686
- import { ObjectId as ObjectId14 } from "mongodb";
4994
+ import { ObjectId as ObjectId15 } from "mongodb";
4687
4995
  import {
4688
4996
  useAtlas as useAtlas10,
4689
4997
  InternalServerError as InternalServerError9,
@@ -4756,14 +5064,14 @@ function useStockRepository() {
4756
5064
  limit
4757
5065
  };
4758
5066
  try {
4759
- site = new ObjectId14(site);
5067
+ site = new ObjectId15(site);
4760
5068
  query.site = site;
4761
5069
  cacheOptions.site = site.toString();
4762
5070
  } catch (error) {
4763
5071
  throw new BadRequestError21("Invalid site ID format.");
4764
5072
  }
4765
5073
  try {
4766
- supply = new ObjectId14(supply);
5074
+ supply = new ObjectId15(supply);
4767
5075
  query.supply = supply;
4768
5076
  cacheOptions.supply = supply.toString();
4769
5077
  } catch (error) {
@@ -4941,7 +5249,7 @@ function useStockController() {
4941
5249
 
4942
5250
  // src/models/hygiene-checkout-item.model.ts
4943
5251
  import Joi14 from "joi";
4944
- import { ObjectId as ObjectId15 } from "mongodb";
5252
+ import { ObjectId as ObjectId16 } from "mongodb";
4945
5253
  import { BadRequestError as BadRequestError24, logger as logger27 } from "@7365admin1/node-server-utils";
4946
5254
  var allowedCheckOutItemStatus = ["pending", "completed"];
4947
5255
  var checkOutItemSchema = Joi14.object({
@@ -4961,14 +5269,14 @@ function MCheckOutItem(value) {
4961
5269
  }
4962
5270
  if (value.site) {
4963
5271
  try {
4964
- value.site = new ObjectId15(value.site);
5272
+ value.site = new ObjectId16(value.site);
4965
5273
  } catch (error2) {
4966
5274
  throw new BadRequestError24("Invalid site ID format.");
4967
5275
  }
4968
5276
  }
4969
5277
  if (value.supply) {
4970
5278
  try {
4971
- value.supply = new ObjectId15(value.supply);
5279
+ value.supply = new ObjectId16(value.supply);
4972
5280
  } catch (error2) {
4973
5281
  throw new BadRequestError24("Invalid supply ID format.");
4974
5282
  }
@@ -4989,7 +5297,7 @@ function MCheckOutItem(value) {
4989
5297
  }
4990
5298
 
4991
5299
  // src/repositories/hygiene-checkout-item.repository.ts
4992
- import { ObjectId as ObjectId16 } from "mongodb";
5300
+ import { ObjectId as ObjectId17 } from "mongodb";
4993
5301
  import {
4994
5302
  useAtlas as useAtlas12,
4995
5303
  InternalServerError as InternalServerError10,
@@ -5062,7 +5370,7 @@ function useCheckOutItemRepository() {
5062
5370
  limit
5063
5371
  };
5064
5372
  try {
5065
- site = new ObjectId16(site);
5373
+ site = new ObjectId17(site);
5066
5374
  query.site = site;
5067
5375
  cacheOptions.site = site.toString();
5068
5376
  } catch (error) {
@@ -5135,7 +5443,7 @@ function useCheckOutItemRepository() {
5135
5443
  }
5136
5444
  async function getCheckOutItemById(_id, session) {
5137
5445
  try {
5138
- _id = new ObjectId16(_id);
5446
+ _id = new ObjectId17(_id);
5139
5447
  } catch (error) {
5140
5448
  throw new BadRequestError25("Invalid check out item ID format.");
5141
5449
  }
@@ -5199,7 +5507,7 @@ function useCheckOutItemRepository() {
5199
5507
  }
5200
5508
  async function completeCheckOutItem(_id, session) {
5201
5509
  try {
5202
- _id = new ObjectId16(_id);
5510
+ _id = new ObjectId17(_id);
5203
5511
  } catch (error) {
5204
5512
  throw new BadRequestError25("Invalid check out item ID format.");
5205
5513
  }
@@ -5240,7 +5548,7 @@ function useCheckOutItemRepository() {
5240
5548
  }
5241
5549
 
5242
5550
  // src/services/hygiene-checkout-item.service.ts
5243
- import { useUserRepo } from "@7365admin1/core";
5551
+ import { useUserRepo as useUserRepo2 } from "@7365admin1/core";
5244
5552
  import { BadRequestError as BadRequestError26, useAtlas as useAtlas13 } from "@7365admin1/node-server-utils";
5245
5553
  function useCheckOutItemService() {
5246
5554
  const {
@@ -5249,7 +5557,7 @@ function useCheckOutItemService() {
5249
5557
  completeCheckOutItem
5250
5558
  } = useCheckOutItemRepository();
5251
5559
  const { getSupplyById } = useSupplyRepository();
5252
- const { getUserById } = useUserRepo();
5560
+ const { getUserById } = useUserRepo2();
5253
5561
  const { createStock } = useStockService();
5254
5562
  async function createCheckOutItem(value) {
5255
5563
  const session = useAtlas13.getClient()?.startSession();
@@ -5485,7 +5793,7 @@ function useCheckOutItemController() {
5485
5793
  // src/models/hygiene-schedule-task.model.ts
5486
5794
  import { BadRequestError as BadRequestError28, logger as logger30 } from "@7365admin1/node-server-utils";
5487
5795
  import Joi16 from "joi";
5488
- import { ObjectId as ObjectId17 } from "mongodb";
5796
+ import { ObjectId as ObjectId18 } from "mongodb";
5489
5797
  var scheduleTaskSchema = Joi16.object({
5490
5798
  site: Joi16.string().hex().required(),
5491
5799
  title: Joi16.string().required(),
@@ -5510,7 +5818,7 @@ function MScheduleTask(value) {
5510
5818
  }
5511
5819
  if (value.site) {
5512
5820
  try {
5513
- value.site = new ObjectId17(value.site);
5821
+ value.site = new ObjectId18(value.site);
5514
5822
  } catch (error2) {
5515
5823
  throw new BadRequestError28("Invalid site ID format.");
5516
5824
  }
@@ -5520,7 +5828,7 @@ function MScheduleTask(value) {
5520
5828
  try {
5521
5829
  return {
5522
5830
  name: area.name,
5523
- value: new ObjectId17(area.value.toString())
5831
+ value: new ObjectId18(area.value.toString())
5524
5832
  };
5525
5833
  } catch (error2) {
5526
5834
  throw new BadRequestError28(`Invalid area value format: ${area.name}`);
@@ -5529,7 +5837,7 @@ function MScheduleTask(value) {
5529
5837
  }
5530
5838
  if (value.createdBy) {
5531
5839
  try {
5532
- value.createdBy = new ObjectId17(value.createdBy);
5840
+ value.createdBy = new ObjectId18(value.createdBy);
5533
5841
  } catch (error2) {
5534
5842
  throw new BadRequestError28("Invalid createdBy ID format.");
5535
5843
  }
@@ -5550,7 +5858,7 @@ function MScheduleTask(value) {
5550
5858
  }
5551
5859
 
5552
5860
  // src/repositories/hygiene-schedule-task.repository.ts
5553
- import { ObjectId as ObjectId18 } from "mongodb";
5861
+ import { ObjectId as ObjectId19 } from "mongodb";
5554
5862
  import {
5555
5863
  useAtlas as useAtlas14,
5556
5864
  InternalServerError as InternalServerError11,
@@ -5622,7 +5930,7 @@ function useScheduleTaskRepository() {
5622
5930
  limit
5623
5931
  };
5624
5932
  try {
5625
- site = new ObjectId18(site);
5933
+ site = new ObjectId19(site);
5626
5934
  query.site = site;
5627
5935
  cacheOptions.site = site.toString();
5628
5936
  } catch (error) {
@@ -5690,7 +5998,7 @@ function useScheduleTaskRepository() {
5690
5998
  limit
5691
5999
  };
5692
6000
  try {
5693
- site = new ObjectId18(site);
6001
+ site = new ObjectId19(site);
5694
6002
  query.site = site;
5695
6003
  cacheOptions.site = site.toString();
5696
6004
  } catch (error) {
@@ -5733,7 +6041,7 @@ function useScheduleTaskRepository() {
5733
6041
  }
5734
6042
  async function getScheduleTaskById(_id, session) {
5735
6043
  try {
5736
- _id = new ObjectId18(_id);
6044
+ _id = new ObjectId19(_id);
5737
6045
  } catch (error) {
5738
6046
  throw new BadRequestError29("Invalid schedule task ID format.");
5739
6047
  }
@@ -5783,7 +6091,7 @@ function useScheduleTaskRepository() {
5783
6091
  }
5784
6092
  async function updateScheduleTask(_id, value, session) {
5785
6093
  try {
5786
- _id = new ObjectId18(_id);
6094
+ _id = new ObjectId19(_id);
5787
6095
  } catch (error) {
5788
6096
  throw new BadRequestError29("Invalid schedule task ID format.");
5789
6097
  }
@@ -5792,7 +6100,7 @@ function useScheduleTaskRepository() {
5792
6100
  try {
5793
6101
  return {
5794
6102
  name: area.name,
5795
- value: new ObjectId18(area.value.toString())
6103
+ value: new ObjectId19(area.value.toString())
5796
6104
  };
5797
6105
  } catch (error) {
5798
6106
  throw new BadRequestError29(`Invalid area value format: ${area.name}`);
@@ -5837,6 +6145,7 @@ function useScheduleTaskRepository() {
5837
6145
  }
5838
6146
 
5839
6147
  // src/services/hygiene-schedule-task.service.ts
6148
+ import { ObjectId as ObjectId20 } from "mongodb";
5840
6149
  import { logger as logger32 } from "@7365admin1/node-server-utils";
5841
6150
  function useScheduleTaskService() {
5842
6151
  const { createParentChecklist } = useParentChecklistRepo();
@@ -5844,7 +6153,7 @@ function useScheduleTaskService() {
5844
6153
  const {
5845
6154
  createAreaChecklist,
5846
6155
  getAreaChecklistByAreaAndSchedule,
5847
- updateAreaChecklist
6156
+ pushScheduleTaskSets
5848
6157
  } = useAreaChecklistRepo();
5849
6158
  const { getAreaById } = useAreaRepo();
5850
6159
  function checkScheduleConditions(schedule, currentDate = /* @__PURE__ */ new Date()) {
@@ -5883,6 +6192,35 @@ function useScheduleTaskService() {
5883
6192
  );
5884
6193
  return false;
5885
6194
  }
6195
+ const relevantTimestamp = schedule.updatedAt || schedule.createdAt;
6196
+ if (relevantTimestamp) {
6197
+ const savedAt = new Date(relevantTimestamp);
6198
+ const savedHour = parseInt(
6199
+ savedAt.toLocaleTimeString("en-US", {
6200
+ hour: "2-digit",
6201
+ hour12: false,
6202
+ timeZone: "Asia/Singapore"
6203
+ }),
6204
+ 10
6205
+ );
6206
+ const savedMinute = savedAt.toLocaleString("en-US", {
6207
+ minute: "2-digit",
6208
+ timeZone: "Asia/Singapore"
6209
+ });
6210
+ const savedDateFormatted = savedAt.toLocaleDateString("en-CA", {
6211
+ timeZone: "Asia/Singapore"
6212
+ });
6213
+ const savedMinNum = parseInt(savedMinute, 10);
6214
+ const wasSavedThisMinute = savedDateFormatted === currentDateFormatted && savedHour === currentHour && savedMinNum === currentMinute;
6215
+ if (wasSavedThisMinute) {
6216
+ logger32.info(
6217
+ `Schedule ${schedule._id}: Skipping \u2014 task was saved this minute (${savedHour}:${String(
6218
+ savedMinNum
6219
+ ).padStart(2, "0")}). Will fire on next occurrence.`
6220
+ );
6221
+ return false;
6222
+ }
6223
+ }
5886
6224
  logger32.info(
5887
6225
  `Schedule ${schedule._id}: All conditions matched - Date is within range and time matches`
5888
6226
  );
@@ -5950,8 +6288,14 @@ function useScheduleTaskService() {
5950
6288
  let units = [];
5951
6289
  if (areaDetails.units && areaDetails.units.length > 0) {
5952
6290
  units = areaDetails.units.map((unit) => ({
5953
- unit: unit.unit.toString(),
5954
- name: unit.name
6291
+ unit: new ObjectId20(unit.unit),
6292
+ name: unit.name,
6293
+ approve: false,
6294
+ reject: false,
6295
+ status: "open",
6296
+ remarks: "",
6297
+ completedBy: "",
6298
+ timestamp: ""
5955
6299
  }));
5956
6300
  logger32.info(
5957
6301
  `Area ${area.name} (${areaId}): Using units from area details: ${JSON.stringify(
@@ -5988,79 +6332,62 @@ function useScheduleTaskService() {
5988
6332
  );
5989
6333
  }
5990
6334
  if (existingAreaChecklist) {
5991
- const currentSetNumber = existingAreaChecklist.checklist?.length || 0;
5992
- const newSet = {
5993
- set: currentSetNumber + 1,
5994
- units
5995
- };
5996
- const updatedChecklist = [
5997
- ...existingAreaChecklist.checklist || [],
5998
- newSet
5999
- ];
6000
- logger32.info(
6001
- `Area ${area.name} (${areaId}): Appending new set ${newSet.set} to checklist. Updated checklist: ${JSON.stringify(
6002
- updatedChecklist
6003
- )}`
6335
+ const areaSetConfig = Number(areaDetails.set) || 1;
6336
+ const maxSetInChecklist = existingAreaChecklist.checklist?.length ? Math.max(
6337
+ ...existingAreaChecklist.checklist.map(
6338
+ (c) => c.set
6339
+ )
6340
+ ) : 0;
6341
+ const startSetNumber = maxSetInChecklist + 1;
6342
+ const newSets = Array.from(
6343
+ { length: areaSetConfig },
6344
+ (_, index) => ({
6345
+ set: startSetNumber + index,
6346
+ units,
6347
+ isScheduleTask: true,
6348
+ scheduleTaskId: scheduleTask._id
6349
+ })
6004
6350
  );
6005
- await updateAreaChecklist(existingAreaChecklist._id, {
6006
- checklist: updatedChecklist
6007
- });
6008
- logger32.info(
6009
- `Appended set ${newSet.set} to area checklist for area ${area.name}`
6351
+ const modified = await pushScheduleTaskSets(
6352
+ parentChecklistId.toString(),
6353
+ areaId,
6354
+ scheduleTask._id.toString(),
6355
+ newSets
6010
6356
  );
6011
- try {
6012
- const verifyChecklist = await getAreaChecklistByAreaAndSchedule(
6013
- parentChecklistId.toString(),
6014
- areaId
6015
- );
6357
+ if (modified > 0) {
6016
6358
  logger32.info(
6017
- `Area ${area.name} (${areaId}): Checklist after update: ${JSON.stringify(
6018
- verifyChecklist.checklist
6019
- )}`
6359
+ `Appended ${areaSetConfig} set(s) starting from ${startSetNumber} to area checklist for area ${area.name}`
6020
6360
  );
6021
- } catch (verifyError) {
6022
- logger32.warn(
6023
- `Area ${area.name} (${areaId}): Error verifying checklist after update:`,
6024
- verifyError
6361
+ } else {
6362
+ logger32.info(
6363
+ `Area ${area.name} (${areaId}): Schedule task ${scheduleTask._id} already processed (atomic check), skipping.`
6025
6364
  );
6026
6365
  }
6027
6366
  } else {
6367
+ const areaSetConfig = Number(areaDetails.set) || 1;
6368
+ const schedTaskId = scheduleTask._id;
6369
+ const schedTaskSets = Array.from(
6370
+ { length: areaSetConfig },
6371
+ (_, index) => ({
6372
+ set: index + 1,
6373
+ units,
6374
+ isScheduleTask: true,
6375
+ scheduleTaskId: schedTaskId
6376
+ })
6377
+ );
6028
6378
  const checklistData = {
6029
6379
  schedule: parentChecklistId.toString(),
6030
6380
  area: areaId,
6031
6381
  name: area.name,
6032
6382
  type: areaDetails.type || "common",
6033
- checklist: [
6034
- {
6035
- set: 1,
6036
- units
6037
- }
6038
- ],
6383
+ checklist: schedTaskSets,
6039
6384
  createdBy: scheduleTask.createdBy
6040
6385
  };
6041
6386
  logger32.info(
6042
- `Area ${area.name} (${areaId}): Creating new area checklist with data: ${JSON.stringify(
6043
- checklistData
6044
- )}`
6387
+ `Area ${area.name} (${areaId}): Creating new area checklist with schedule-task sets only`
6045
6388
  );
6046
6389
  await createAreaChecklist(checklistData);
6047
6390
  logger32.info(`Created new area checklist for area ${area.name}`);
6048
- try {
6049
- const verifyChecklist = await getAreaChecklistByAreaAndSchedule(
6050
- parentChecklistId.toString(),
6051
- areaId
6052
- );
6053
- logger32.info(
6054
- `Area ${area.name} (${areaId}): Checklist after creation: ${JSON.stringify(
6055
- verifyChecklist.checklist
6056
- )}`
6057
- );
6058
- } catch (verifyError) {
6059
- logger32.warn(
6060
- `Area ${area.name} (${areaId}): Error verifying checklist after creation:`,
6061
- verifyError
6062
- );
6063
- }
6064
6391
  }
6065
6392
  } catch (error) {
6066
6393
  logger32.error(`Error processing area ${area.name}:`, error);