@7365admin1/layer-common 1.9.0 → 1.10.1

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AcceptDialog.vue +44 -0
  3. package/components/AccessCardAddForm.vue +101 -13
  4. package/components/AccessManagement.vue +130 -47
  5. package/components/AddSupplyForm.vue +165 -0
  6. package/components/AreaChecklistHistoryLogs.vue +235 -0
  7. package/components/AreaChecklistHistoryMain.vue +176 -0
  8. package/components/AreaFormDialog.vue +266 -0
  9. package/components/AreaMain.vue +841 -0
  10. package/components/AttendanceCheckInOutDialog.vue +416 -0
  11. package/components/AttendanceDetailsDialog.vue +184 -0
  12. package/components/AttendanceMain.vue +155 -0
  13. package/components/AttendanceMapSearchDialog.vue +393 -0
  14. package/components/AttendanceSettingsDialog.vue +398 -0
  15. package/components/BuildingManagement/buildings.vue +5 -5
  16. package/components/BuildingManagement/units.vue +5 -5
  17. package/components/ChecklistItemRow.vue +54 -0
  18. package/components/CheckoutItemMain.vue +705 -0
  19. package/components/CleaningScheduleMain.vue +271 -0
  20. package/components/DocumentManagement.vue +8 -9
  21. package/components/EntryPass/QrTemplatePreview.vue +104 -0
  22. package/components/EntryPassMain.vue +252 -200
  23. package/components/HygieneUpdateMoreAction.vue +238 -0
  24. package/components/IncidentReport/Authorities.vue +226 -0
  25. package/components/IncidentReport/IncidentInformation.vue +258 -0
  26. package/components/IncidentReport/affectedEntities.vue +167 -0
  27. package/components/InvitationMain.vue +19 -17
  28. package/components/ManageChecklistMain.vue +384 -0
  29. package/components/MemberMain.vue +48 -20
  30. package/components/MyAttendanceMain.vue +224 -0
  31. package/components/OnlineFormsConfiguration.vue +9 -2
  32. package/components/PasswordConfirmation.vue +95 -0
  33. package/components/PhotoUpload.vue +410 -0
  34. package/components/RolePermissionMain.vue +17 -15
  35. package/components/ScheduleAreaMain.vue +313 -0
  36. package/components/ScheduleTaskAreaFormDialog.vue +144 -0
  37. package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
  38. package/components/ScheduleTaskForm.vue +471 -0
  39. package/components/ScheduleTaskMain.vue +345 -0
  40. package/components/ScheduleTastTicketMain.vue +182 -0
  41. package/components/ServiceProviderMain.vue +27 -7
  42. package/components/StockCard.vue +191 -0
  43. package/components/SupplyManagementMain.vue +557 -0
  44. package/components/TableHygiene.vue +617 -0
  45. package/components/UnitMain.vue +451 -0
  46. package/components/VisitorManagement.vue +28 -15
  47. package/composables/useAccessManagement.ts +90 -0
  48. package/composables/useAreaPermission.ts +51 -0
  49. package/composables/useAreas.ts +99 -0
  50. package/composables/useAttendance.ts +89 -0
  51. package/composables/useAttendancePermission.ts +68 -0
  52. package/composables/useBuilding.ts +2 -2
  53. package/composables/useBuildingUnit.ts +2 -2
  54. package/composables/useCard.ts +2 -0
  55. package/composables/useCheckout.ts +61 -0
  56. package/composables/useCheckoutPermission.ts +80 -0
  57. package/composables/useCleaningPermission.ts +229 -0
  58. package/composables/useCleaningSchedulePermission.ts +58 -0
  59. package/composables/useCleaningSchedules.ts +233 -0
  60. package/composables/useCountry.ts +8 -0
  61. package/composables/useDOBEntries.ts +13 -0
  62. package/composables/useDashboardData.ts +2 -2
  63. package/composables/useDocument.ts +3 -2
  64. package/composables/useFeedback.ts +1 -1
  65. package/composables/useFile.ts +4 -6
  66. package/composables/useLocation.ts +78 -0
  67. package/composables/useOnlineForm.ts +16 -9
  68. package/composables/usePeople.ts +87 -72
  69. package/composables/useQR.ts +29 -0
  70. package/composables/useRole.ts +3 -2
  71. package/composables/useScheduleTask.ts +89 -0
  72. package/composables/useScheduleTaskArea.ts +85 -0
  73. package/composables/useScheduleTaskPermission.ts +68 -0
  74. package/composables/useSiteEntryPassSettings.ts +4 -15
  75. package/composables/useStock.ts +45 -0
  76. package/composables/useSupply.ts +63 -0
  77. package/composables/useSupplyPermission.ts +92 -0
  78. package/composables/useUnitPermission.ts +51 -0
  79. package/composables/useUnits.ts +82 -0
  80. package/composables/useWebUsb.ts +389 -0
  81. package/composables/useWorkOrder.ts +1 -1
  82. package/nuxt.config.ts +3 -0
  83. package/package.json +4 -1
  84. package/types/area.d.ts +22 -0
  85. package/types/attendance.d.ts +38 -0
  86. package/types/checkout-item.d.ts +27 -0
  87. package/types/cleaner-schedule.d.ts +54 -0
  88. package/types/location.d.ts +42 -0
  89. package/types/schedule-task.d.ts +18 -0
  90. package/types/stock.d.ts +16 -0
  91. package/types/supply.d.ts +11 -0
  92. package/types/verification.d.ts +1 -1
  93. package/utils/acm-crypto.ts +30 -0
@@ -0,0 +1,384 @@
1
+ <template>
2
+ <v-row no-gutters align="center" justify="center">
3
+ <v-col cols="12" lg="12">
4
+ <v-row no-gutters class="mb-4">
5
+ <v-col cols="auto">
6
+ <v-btn variant="text" color="primary" class="text-none" @click="back">
7
+ <v-icon left>mdi-arrow-left</v-icon>
8
+ Back
9
+ </v-btn>
10
+ </v-col>
11
+ </v-row>
12
+ <TableHygiene
13
+ ref="tableHygieneRef"
14
+ :title="'Cleaner Checklist'"
15
+ :headers="headers"
16
+ :items="items"
17
+ :selected="selectedItems"
18
+ :item-value="'unit'"
19
+ v-model:page="page"
20
+ :pages="pages"
21
+ :pageRange="pageRange"
22
+ :loading="loading"
23
+ :no-data-text="`No checklist found`"
24
+ :show-header="true"
25
+ :can-manage-schedule-tasks="canManageScheduleTasks"
26
+ :can-add-remarks="canAddRemarks"
27
+ @refresh="getUnitCleanerChecklistRefresh"
28
+ @update:selected="selectedItems = $event"
29
+ @action-click="handleActionClick"
30
+ @request-completion-dialog="openCompletionDialog"
31
+ />
32
+ </v-col>
33
+ </v-row>
34
+
35
+ <v-dialog v-model="showCompletionModal" max-width="600" persistent>
36
+ <v-card>
37
+ <v-card-title class="text-h5 pa-4">
38
+ {{
39
+ modalData.currentSet !== undefined
40
+ ? `Set ${modalData.currentSet} Completed`
41
+ : "All Items Checked"
42
+ }}
43
+ </v-card-title>
44
+
45
+ <v-divider />
46
+
47
+ <v-card-text class="pa-4">
48
+ <v-row>
49
+ <v-col cols="12">
50
+ <InputLabel
51
+ title="Remarks (Optional)"
52
+ :required="false"
53
+ class="mb-2"
54
+ />
55
+ <v-textarea
56
+ v-model="modalData.remark"
57
+ placeholder="Enter your remarks here"
58
+ rows="4"
59
+ variant="outlined"
60
+ density="comfortable"
61
+ hide-details
62
+ />
63
+ </v-col>
64
+
65
+ <v-col cols="12">
66
+ <v-divider class="mb-4" />
67
+ <PhotoUpload
68
+ title="Take/Upload Photo"
69
+ :required="false"
70
+ v-model="modalData.photos"
71
+ v-model:photo-ids="modalData.photoIds"
72
+ @error="handlePhotoError"
73
+ />
74
+ </v-col>
75
+ </v-row>
76
+ </v-card-text>
77
+
78
+ <v-divider />
79
+
80
+ <v-card-actions class="pa-4">
81
+ <v-spacer />
82
+ <v-btn color="grey" variant="text" @click="closeModal"> Cancel </v-btn>
83
+ <v-btn
84
+ color="primary"
85
+ variant="flat"
86
+ @click="submitModal"
87
+ :disabled="!isModalValid"
88
+ >
89
+ Submit
90
+ </v-btn>
91
+ </v-card-actions>
92
+ </v-card>
93
+ </v-dialog>
94
+
95
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
96
+ </template>
97
+
98
+ <script setup lang="ts">
99
+ import { useCleaningSchedulePermission } from "../composables/useCleaningSchedulePermission";
100
+ import useCleaningSchedules from "../composables/useCleaningSchedules";
101
+
102
+ const props = defineProps({
103
+ orgId: { type: String, default: "" },
104
+ site: { type: String, default: "" },
105
+ cleanerChecklistId: { type: String, default: "" },
106
+ scheduleAreaId: { type: String, default: "" },
107
+ type: { type: String, default: "cleaner" },
108
+ });
109
+
110
+ const { getUnitCleanerChecklist, updateUnitChecklist } = useCleaningSchedules();
111
+ const { getBySiteAsServiceProvider } = useCustomerSite();
112
+ const { back } = useUtils();
113
+ const { canAddRemarks, canManageScheduleTasks } =
114
+ useCleaningSchedulePermission();
115
+
116
+ const page = ref<number>(1);
117
+ const pages = ref<number>(0);
118
+ const pageRange = ref<string>("-- - -- of --");
119
+ const items = ref<TChecklistSet[]>([]);
120
+ const selectedItems = ref<Array<{ unit: string; set: number }>>([]);
121
+ const submitting = ref<boolean>(false);
122
+ const message = ref<string>("");
123
+ const messageSnackbar = ref<boolean>(false);
124
+ const messageColor = ref<string>("");
125
+ const categories = ref<
126
+ Array<{ title: string; value: string; subtitle: string }>
127
+ >([]);
128
+ const tableHygieneRef = ref<any>(null);
129
+
130
+ const headers = [{ title: "Name", value: "name" }];
131
+
132
+ const { data: getCategoriesReq } = await useLazyAsyncData(
133
+ "get-categories-for-work-order",
134
+ () => getBySiteAsServiceProvider(props.site)
135
+ );
136
+
137
+ // Modal state management
138
+ const showCompletionModal = ref<boolean>(false);
139
+ const modalData = reactive({
140
+ remark: "",
141
+ photos: [] as string[],
142
+ photoIds: [] as string[],
143
+ currentSet: undefined as number | undefined,
144
+ approvedItems: [] as Array<{ key: string; item: any; action: "approve" }>,
145
+ lastApprovedKey: null as string | null,
146
+ });
147
+
148
+ const isModalValid = computed(() => {
149
+ return modalData.remark?.trim().length > 0;
150
+ });
151
+
152
+ const {
153
+ data: getUnitCleanerChecklistReq,
154
+ refresh: getUnitCleanerChecklistRefresh,
155
+ pending: loading,
156
+ } = await useLazyAsyncData(
157
+ `get-all-unit-cleaner-checklist`,
158
+ () =>
159
+ getUnitCleanerChecklist({
160
+ page: page.value,
161
+ scheduleAreaId: props.scheduleAreaId,
162
+ }),
163
+ {
164
+ watch: [() => page.value, () => props.scheduleAreaId],
165
+ }
166
+ );
167
+
168
+ watchEffect(() => {
169
+ if (getCategoriesReq.value) {
170
+ categories.value = getCategoriesReq.value.map((i: any) => ({
171
+ title: i.nature.replace(/_/g, " "),
172
+ subtitle: i.title,
173
+ value: i._id.org,
174
+ }));
175
+ }
176
+ });
177
+
178
+ watchEffect(() => {
179
+ if (getUnitCleanerChecklistReq.value) {
180
+ items.value = getUnitCleanerChecklistReq.value.items;
181
+ page.value = getUnitCleanerChecklistReq.value.page;
182
+ pages.value = getUnitCleanerChecklistReq.value.pages;
183
+ pageRange.value = getUnitCleanerChecklistReq.value.pageRange;
184
+ }
185
+ });
186
+
187
+ watchEffect(() => {
188
+ const completed: Array<{ unit: string; set: number }> = [];
189
+
190
+ items.value.forEach((checklistSet: TChecklistSet) => {
191
+ checklistSet.units?.forEach((unit: TUnitChecklistItem) => {
192
+ if (
193
+ unit.status?.toLowerCase() === "completed" ||
194
+ (unit as any).approve === true
195
+ ) {
196
+ completed.push({ unit: unit.unit, set: checklistSet.set });
197
+ }
198
+ });
199
+ });
200
+
201
+ selectedItems.value = completed;
202
+ });
203
+
204
+ function showMessage(msg: string, color: string = "error"): void {
205
+ message.value = msg;
206
+ messageColor.value = color;
207
+ messageSnackbar.value = true;
208
+ }
209
+
210
+ function resetModalData(): void {
211
+ modalData.remark = "";
212
+ modalData.photos = [];
213
+ modalData.photoIds = [];
214
+ modalData.currentSet = undefined;
215
+ modalData.approvedItems = [];
216
+ modalData.lastApprovedKey = null;
217
+ }
218
+
219
+ function closeModal(): void {
220
+ const setNumber = modalData.currentSet;
221
+ showCompletionModal.value = false;
222
+ resetModalData();
223
+
224
+ // Notify TableHygiene to revert approval states
225
+ if (setNumber !== undefined && tableHygieneRef.value) {
226
+ tableHygieneRef.value.revertSetApprovals(setNumber);
227
+ }
228
+ }
229
+
230
+ function handlePhotoError(errorMessage: string): void {
231
+ console.error("Photo upload error:", errorMessage);
232
+ showMessage(errorMessage, "error");
233
+ }
234
+
235
+ function openCompletionDialog({
236
+ setNumber,
237
+ approvedItems,
238
+ lastApprovedKey,
239
+ }: {
240
+ setNumber: number | undefined;
241
+ approvedItems: Array<{ key: string; item: any; action: "approve" }>;
242
+ lastApprovedKey: string | null;
243
+ }): void {
244
+ // If user does not need to add remarks, perform approvals immediately
245
+ if (!canAddRemarks.value) {
246
+ // call update directly without opening modal
247
+ _updateUnitChecklist({
248
+ remark: "",
249
+ photos: [],
250
+ photoIds: [],
251
+ approvedItems,
252
+ lastApprovedKey,
253
+ setNumber,
254
+ }).catch((err) => {
255
+ console.error("Error auto-approving set:", err);
256
+ showMessage(err?.message || "Failed to approve set", "error");
257
+ });
258
+ return;
259
+ }
260
+
261
+ modalData.currentSet = setNumber;
262
+ modalData.approvedItems = approvedItems;
263
+ modalData.lastApprovedKey = lastApprovedKey;
264
+ showCompletionModal.value = true;
265
+ }
266
+
267
+ async function submitModal(): Promise<void> {
268
+ if (!isModalValid.value) return;
269
+
270
+ await _updateUnitChecklist({
271
+ remark: modalData.remark,
272
+ photos: modalData.photos,
273
+ photoIds: modalData.photoIds,
274
+ approvedItems: modalData.approvedItems,
275
+ lastApprovedKey: modalData.lastApprovedKey,
276
+ setNumber: modalData.currentSet,
277
+ });
278
+
279
+ closeModal();
280
+ }
281
+
282
+ async function handleActionClick(data: {
283
+ item: TFlattenedUnitItem;
284
+ action: "approve" | "reject";
285
+ }): Promise<void> {
286
+ const { item, action } = data;
287
+
288
+ if (!item?.unit || item?.set === undefined) {
289
+ showMessage("Invalid unit or set", "error");
290
+ return;
291
+ }
292
+
293
+ const isApproved = selectedItems.value.some(
294
+ (s) => s.unit === item.unit && s.set === item.set
295
+ );
296
+
297
+ if (isApproved && action === "approve") {
298
+ showMessage("Unit already approved", "info");
299
+ return;
300
+ }
301
+
302
+ submitting.value = true;
303
+
304
+ try {
305
+ const checklistId = props.scheduleAreaId;
306
+ const unitId = item.unit;
307
+ const setNumber = item.set;
308
+
309
+ const response = await updateUnitChecklist(
310
+ checklistId,
311
+ unitId,
312
+ setNumber,
313
+ action
314
+ );
315
+
316
+ showMessage(
317
+ response?.message ||
318
+ `Unit ${action === "approve" ? "approved" : "rejected"} successfully`,
319
+ "success"
320
+ );
321
+
322
+ await getUnitCleanerChecklistRefresh();
323
+ } catch (error: any) {
324
+ console.error("Error updating unit checklist:", error);
325
+ showMessage(error?.message || "Failed to update checklist", "error");
326
+ } finally {
327
+ submitting.value = false;
328
+ }
329
+ }
330
+
331
+ async function _updateUnitChecklist({
332
+ remark,
333
+ photoIds,
334
+ approvedItems,
335
+ setNumber,
336
+ }: {
337
+ remark: string;
338
+ photos: string[];
339
+ photoIds: string[];
340
+ approvedItems: Array<{ key: string; item: any; action: "approve" }>;
341
+ lastApprovedKey: string | null;
342
+ setNumber: number | undefined;
343
+ }) {
344
+ submitting.value = true;
345
+
346
+ try {
347
+ if (!approvedItems || approvedItems.length === 0) {
348
+ throw new Error("No items to approve");
349
+ }
350
+
351
+ if (!remark?.trim()) {
352
+ throw new Error("Remark is required");
353
+ }
354
+
355
+ const checklistId = props.scheduleAreaId;
356
+
357
+ for (let i = 0; i < approvedItems.length; i++) {
358
+ const { item } = approvedItems[i];
359
+ const unitId = item.unit;
360
+ const setNum = item.set;
361
+ const isLastItem = i === approvedItems.length - 1;
362
+
363
+ await updateUnitChecklist(
364
+ checklistId,
365
+ unitId,
366
+ setNum,
367
+ "approve",
368
+ isLastItem ? remark : undefined,
369
+ isLastItem ? photoIds : undefined
370
+ );
371
+ }
372
+
373
+ showMessage(`Set ${setNumber} approved successfully`, "success");
374
+
375
+ // Refresh data
376
+ await getUnitCleanerChecklistRefresh();
377
+ } catch (error: any) {
378
+ console.error("Error approving set:", error);
379
+ showMessage(error?.message || "Failed to approve set", "error");
380
+ } finally {
381
+ submitting.value = false;
382
+ }
383
+ }
384
+ </script>
@@ -41,20 +41,18 @@
41
41
  </template>
42
42
 
43
43
  <template #extension>
44
- <v-tabs>
44
+ <v-tabs
45
+ v-model="selectedStatus"
46
+ color="primary"
47
+ :height="40"
48
+ @update:model-value="toRoute"
49
+ class="w-100"
50
+ >
45
51
  <v-tab
46
- v-for="tab in [
47
- { name: 'Active', status: 'active' },
48
- { name: 'Suspended', status: 'suspended' },
49
- ]"
52
+ v-for="tab in tabOptions"
53
+ :value="tab.status"
50
54
  :key="tab.status"
51
- :to="{
52
- name: props.route,
53
- params: setRouteParams({
54
- status: tab.status,
55
- orgId: props.orgId,
56
- }),
57
- }"
55
+ class="text-capitalize"
58
56
  >
59
57
  {{ tab.name }}
60
58
  </v-tab>
@@ -331,18 +329,48 @@ const {
331
329
  const { headerSearch } = useLocal();
332
330
  const { replaceMatch, setRouteParams, requiredRule } = useUtils();
333
331
 
332
+ const selectedStatus = ref(props.status);
333
+ const route = useRoute();
334
+ const orgId = route.params.org as string;
335
+ const routeName = (useRoute().name as string) ?? "";
336
+
337
+ const tabOptions = [
338
+ { name: "Active", status: "active" },
339
+ { name: "Suspended", status: "suspended" },
340
+ ];
341
+
342
+ function toRoute(status: any) {
343
+ const obj = tabOptions.find((x) => x.status === status);
344
+ if (!obj) return;
345
+ page.value = 1;
346
+ navigateTo({
347
+ name: routeName,
348
+ params: {
349
+ org: orgId,
350
+ },
351
+ query: {
352
+ status: obj.status,
353
+ },
354
+ });
355
+ }
356
+
334
357
  const {
335
358
  data: getAllReq,
336
359
  refresh: getAll,
337
360
  status: getAllReqStatus,
338
- } = useLazyAsyncData("get-all-members", () =>
339
- _getAll({
340
- status: props.status,
341
- org: props.orgId,
342
- search: headerSearch.value,
343
- page: page.value,
344
- type: props.type,
345
- })
361
+ } = useLazyAsyncData(
362
+ "get-all-members",
363
+ () =>
364
+ _getAll({
365
+ status: selectedStatus.value,
366
+ org: orgId,
367
+ search: headerSearch.value,
368
+ page: page.value,
369
+ type: props.type,
370
+ }),
371
+ {
372
+ watch: [page, () => route.query],
373
+ }
346
374
  );
347
375
 
348
376
  const loading = computed(() => getAllReqStatus.value === "pending");
@@ -0,0 +1,224 @@
1
+ <template>
2
+ <v-row no-gutters align="center" justify="center">
3
+ <v-col cols="12" lg="12">
4
+ <TableMain
5
+ title="Attendance Records"
6
+ :items="items"
7
+ :headers="headers"
8
+ :loading="loading"
9
+ :show-header="true"
10
+ v-model:page="page"
11
+ :pages="pages"
12
+ :pageRange="pageRange"
13
+ no-data-text="No attendance records found."
14
+ @row-click="handleRowClick"
15
+ >
16
+ <template #actions>
17
+ <v-row no-gutters align="center" class="w-100">
18
+ <v-col cols="auto" v-if="canCheckInOut">
19
+ <v-btn
20
+ class="text-none"
21
+ rounded="pill"
22
+ variant="tonal"
23
+ size="large"
24
+ @click="onCreateItem"
25
+ >
26
+ {{ hasCheckedInToday ? "Check Out" : "Check In" }}
27
+ </v-btn>
28
+ </v-col>
29
+ <v-spacer />
30
+
31
+ <v-col cols="auto">
32
+ <v-text-field
33
+ v-model="searchInput"
34
+ density="compact"
35
+ placeholder="Search"
36
+ clearable
37
+ width="300"
38
+ append-inner-icon="mdi-magnify"
39
+ hide-details
40
+ />
41
+ </v-col>
42
+ </v-row>
43
+ </template>
44
+
45
+ <template #item.checkInTimestamp="{ item }">
46
+ <span>{{ formatDate(item.checkInTimestamp) }}</span>
47
+ </template>
48
+
49
+ <template #item.checkOutTimestamp="{ item }">
50
+ <span>{{
51
+ item.checkOutTimestamp ? formatDate(item.checkOutTimestamp) : "N/A"
52
+ }}</span>
53
+ </template>
54
+
55
+ <template #item.totalHours="{ item }">
56
+ <span>{{ calculateTotalHours(item) }}</span>
57
+ </template>
58
+ </TableMain>
59
+ </v-col>
60
+ </v-row>
61
+
62
+ <AttendanceCheckInOutDialog
63
+ v-model="dialogShowForm"
64
+ :action="currentAction"
65
+ @saved="onAttendanceSaved"
66
+ @close="dialogShowForm = false"
67
+ />
68
+
69
+ <AttendanceDetailsDialog
70
+ v-model="dialogShowMoreActions"
71
+ :attendance-id="selectedAttendanceId"
72
+ @close="dialogShowMoreActions = false"
73
+ />
74
+
75
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
76
+ </template>
77
+
78
+ <script setup lang="ts">
79
+ import useAttendance from "../composables/useAttendance";
80
+ import { useAttendancePermission } from "../composables/useAttendancePermission";
81
+
82
+ const props = defineProps({
83
+ orgId: { type: String, default: "" },
84
+ site: { type: String, default: "" },
85
+ });
86
+
87
+ const { canViewOwnAttendance, canCheckInOut, canViewAttendanceDetails } =
88
+ useAttendancePermission();
89
+
90
+ const searchInput = ref("");
91
+ const dialogShowForm = ref(false);
92
+ const currentAction = ref<"checkIn" | "checkOut">("checkIn");
93
+ const messageSnackbar = ref(false);
94
+ const message = ref("");
95
+ const messageColor = ref("success");
96
+
97
+ const items = ref<Array<Record<string, any>>>([]);
98
+ const submitting = ref(false);
99
+
100
+ const headers = [
101
+ { title: "Team Member", value: "userName" },
102
+ { title: "Check In", value: "checkInTimestamp" },
103
+ { title: "Check Out", value: "checkOutTimestamp" },
104
+ { title: "Total Hours", value: "totalHours" },
105
+ ];
106
+
107
+ const { getMyAttendances, checkIn, checkOut } = useAttendance();
108
+ const { formatDate } = useUtils();
109
+
110
+ const dialogShowMoreActions = ref(false);
111
+ const selectedAttendanceId = ref("");
112
+ const todayAttendance = ref<Record<string, any> | null>(null);
113
+
114
+ const hasCheckedInToday = computed(() => {
115
+ return (
116
+ todayAttendance.value &&
117
+ todayAttendance.value.checkInTimestamp &&
118
+ !todayAttendance.value.checkOutTimestamp
119
+ );
120
+ });
121
+
122
+ function calculateTotalHours(attendance: any): string {
123
+ const checkInTime =
124
+ attendance.checkInTimestamp || attendance.checkIn?.timestamp;
125
+ const checkOutTime =
126
+ attendance.checkOutTimestamp || attendance.checkOut?.timestamp;
127
+
128
+ if (!checkInTime) return "N/A";
129
+ if (!checkOutTime) return "In Progress";
130
+
131
+ const checkIn = new Date(checkInTime);
132
+ const checkOut = new Date(checkOutTime);
133
+ const diffMs = checkOut.getTime() - checkIn.getTime();
134
+ const diffHours = diffMs / (1000 * 60 * 60);
135
+
136
+ return `${diffHours.toFixed(2)} hrs`;
137
+ }
138
+
139
+ const page = ref(1);
140
+ const pages = ref(0);
141
+ const pageRange = ref("-- - -- of --");
142
+
143
+ const {
144
+ data: getMyAttendancesReq,
145
+ refresh: getMyAttendancesRefresh,
146
+ pending: loading,
147
+ } = await useLazyAsyncData(
148
+ "get-my-attendances",
149
+ () =>
150
+ getMyAttendances({
151
+ page: page.value,
152
+ search: searchInput.value,
153
+ site: props.site,
154
+ }),
155
+ {
156
+ watch: [page, searchInput, () => props.site],
157
+ }
158
+ );
159
+
160
+ watchEffect(() => {
161
+ if (getMyAttendancesReq.value) {
162
+ items.value = getMyAttendancesReq.value.items;
163
+ pages.value = getMyAttendancesReq.value.pages;
164
+ pageRange.value = getMyAttendancesReq.value.pageRange;
165
+
166
+ const today = new Date().toISOString().split("T")[0];
167
+ todayAttendance.value =
168
+ getMyAttendancesReq.value.items.find((item: any) => {
169
+ if (!item.checkInTimestamp) return false;
170
+ const itemDate = new Date(item.checkInTimestamp)
171
+ .toISOString()
172
+ .split("T")[0];
173
+ return itemDate === today && !item.checkOutTimestamp;
174
+ }) || null;
175
+ }
176
+ });
177
+
178
+ const onCreateItem = () => {
179
+ currentAction.value = hasCheckedInToday.value ? "checkOut" : "checkIn";
180
+ dialogShowForm.value = true;
181
+ };
182
+
183
+ async function handleRowClick(data: any) {
184
+ const id = (data?.item as any)?._id;
185
+ if (!id) return;
186
+
187
+ selectedAttendanceId.value = id;
188
+ dialogShowMoreActions.value = true;
189
+ }
190
+
191
+ const onAttendanceSaved = async (
192
+ payload: TAttendanceCheckIn | TAttendanceCheckOut
193
+ ) => {
194
+ submitting.value = true;
195
+
196
+ try {
197
+ let response;
198
+ if (currentAction.value === "checkIn") {
199
+ response = await checkIn(props.site, payload as TAttendanceCheckIn);
200
+ } else {
201
+ if (!todayAttendance.value?._id) {
202
+ throw new Error("Attendance ID not found");
203
+ }
204
+ response = await checkOut(
205
+ todayAttendance.value._id,
206
+ payload as TAttendanceCheckOut
207
+ );
208
+ }
209
+
210
+ message.value = response.message;
211
+ messageColor.value = "success";
212
+ messageSnackbar.value = true;
213
+ dialogShowForm.value = false;
214
+
215
+ await getMyAttendancesRefresh();
216
+ } catch (err: any) {
217
+ message.value = err.data?.message;
218
+ messageColor.value = "error";
219
+ messageSnackbar.value = true;
220
+ } finally {
221
+ submitting.value = false;
222
+ }
223
+ };
224
+ </script>