@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.
- package/CHANGELOG.md +12 -0
- package/components/AcceptDialog.vue +44 -0
- package/components/AccessCardAddForm.vue +101 -13
- package/components/AccessManagement.vue +130 -47
- package/components/AddSupplyForm.vue +165 -0
- package/components/AreaChecklistHistoryLogs.vue +235 -0
- package/components/AreaChecklistHistoryMain.vue +176 -0
- package/components/AreaFormDialog.vue +266 -0
- package/components/AreaMain.vue +841 -0
- package/components/AttendanceCheckInOutDialog.vue +416 -0
- package/components/AttendanceDetailsDialog.vue +184 -0
- package/components/AttendanceMain.vue +155 -0
- package/components/AttendanceMapSearchDialog.vue +393 -0
- package/components/AttendanceSettingsDialog.vue +398 -0
- package/components/BuildingManagement/buildings.vue +5 -5
- package/components/BuildingManagement/units.vue +5 -5
- package/components/ChecklistItemRow.vue +54 -0
- package/components/CheckoutItemMain.vue +705 -0
- package/components/CleaningScheduleMain.vue +271 -0
- package/components/DocumentManagement.vue +8 -9
- package/components/EntryPass/QrTemplatePreview.vue +104 -0
- package/components/EntryPassMain.vue +252 -200
- package/components/HygieneUpdateMoreAction.vue +238 -0
- package/components/IncidentReport/Authorities.vue +226 -0
- package/components/IncidentReport/IncidentInformation.vue +258 -0
- package/components/IncidentReport/affectedEntities.vue +167 -0
- package/components/InvitationMain.vue +19 -17
- package/components/ManageChecklistMain.vue +384 -0
- package/components/MemberMain.vue +48 -20
- package/components/MyAttendanceMain.vue +224 -0
- package/components/OnlineFormsConfiguration.vue +9 -2
- package/components/PasswordConfirmation.vue +95 -0
- package/components/PhotoUpload.vue +410 -0
- package/components/RolePermissionMain.vue +17 -15
- package/components/ScheduleAreaMain.vue +313 -0
- package/components/ScheduleTaskAreaFormDialog.vue +144 -0
- package/components/ScheduleTaskAreaUpdateMoreAction.vue +109 -0
- package/components/ScheduleTaskForm.vue +471 -0
- package/components/ScheduleTaskMain.vue +345 -0
- package/components/ScheduleTastTicketMain.vue +182 -0
- package/components/ServiceProviderMain.vue +27 -7
- package/components/StockCard.vue +191 -0
- package/components/SupplyManagementMain.vue +557 -0
- package/components/TableHygiene.vue +617 -0
- package/components/UnitMain.vue +451 -0
- package/components/VisitorManagement.vue +28 -15
- package/composables/useAccessManagement.ts +90 -0
- package/composables/useAreaPermission.ts +51 -0
- package/composables/useAreas.ts +99 -0
- package/composables/useAttendance.ts +89 -0
- package/composables/useAttendancePermission.ts +68 -0
- package/composables/useBuilding.ts +2 -2
- package/composables/useBuildingUnit.ts +2 -2
- package/composables/useCard.ts +2 -0
- package/composables/useCheckout.ts +61 -0
- package/composables/useCheckoutPermission.ts +80 -0
- package/composables/useCleaningPermission.ts +229 -0
- package/composables/useCleaningSchedulePermission.ts +58 -0
- package/composables/useCleaningSchedules.ts +233 -0
- package/composables/useCountry.ts +8 -0
- package/composables/useDOBEntries.ts +13 -0
- package/composables/useDashboardData.ts +2 -2
- package/composables/useDocument.ts +3 -2
- package/composables/useFeedback.ts +1 -1
- package/composables/useFile.ts +4 -6
- package/composables/useLocation.ts +78 -0
- package/composables/useOnlineForm.ts +16 -9
- package/composables/usePeople.ts +87 -72
- package/composables/useQR.ts +29 -0
- package/composables/useRole.ts +3 -2
- package/composables/useScheduleTask.ts +89 -0
- package/composables/useScheduleTaskArea.ts +85 -0
- package/composables/useScheduleTaskPermission.ts +68 -0
- package/composables/useSiteEntryPassSettings.ts +4 -15
- package/composables/useStock.ts +45 -0
- package/composables/useSupply.ts +63 -0
- package/composables/useSupplyPermission.ts +92 -0
- package/composables/useUnitPermission.ts +51 -0
- package/composables/useUnits.ts +82 -0
- package/composables/useWebUsb.ts +389 -0
- package/composables/useWorkOrder.ts +1 -1
- package/nuxt.config.ts +3 -0
- package/package.json +4 -1
- package/types/area.d.ts +22 -0
- package/types/attendance.d.ts +38 -0
- package/types/checkout-item.d.ts +27 -0
- package/types/cleaner-schedule.d.ts +54 -0
- package/types/location.d.ts +42 -0
- package/types/schedule-task.d.ts +18 -0
- package/types/stock.d.ts +16 -0
- package/types/supply.d.ts +11 -0
- package/types/verification.d.ts +1 -1
- 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
|
-
|
|
48
|
-
{ name: 'Suspended', status: 'suspended' },
|
|
49
|
-
]"
|
|
52
|
+
v-for="tab in tabOptions"
|
|
53
|
+
:value="tab.status"
|
|
50
54
|
:key="tab.status"
|
|
51
|
-
|
|
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(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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>
|