@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,155 @@
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
+ @refresh="getAttendancesRefresh"
15
+ @row-click="handleRowClick"
16
+ >
17
+ <template #actions>
18
+ <v-btn
19
+ v-if="canManageAttendanceSettings"
20
+ variant="flat"
21
+ color="black"
22
+ class="text-none"
23
+ @click="dialogShowSettings = true"
24
+ >
25
+ <v-icon class="mr-2">mdi-cog</v-icon>
26
+ Settings
27
+ </v-btn>
28
+ </template>
29
+ <template #item.checkInTimestamp="{ item }">
30
+ <span>{{ formatDate(item.checkInTimestamp) }}</span>
31
+ </template>
32
+
33
+ <template #item.checkOutTimestamp="{ item }">
34
+ <span>{{
35
+ item.checkOutTimestamp ? formatDate(item.checkOutTimestamp) : "N/A"
36
+ }}</span>
37
+ </template>
38
+
39
+ <template #item.totalHours="{ item }">
40
+ <span>{{ calculateTotalHours(item) }}</span>
41
+ </template>
42
+ </TableMain>
43
+ </v-col>
44
+ </v-row>
45
+
46
+ <AttendanceDetailsDialog
47
+ v-model="dialogShowMoreActions"
48
+ :attendance-id="selectedAttendanceId"
49
+ @close="dialogShowMoreActions = false"
50
+ />
51
+
52
+ <AttendanceSettingsDialog
53
+ v-model="dialogShowSettings"
54
+ :site="props.site"
55
+ @saved="onSettingsSaved"
56
+ @close="dialogShowSettings = false"
57
+ />
58
+ </template>
59
+
60
+ <script setup lang="ts">
61
+ import useAttendance from "../composables/useAttendance";
62
+ import { useAttendancePermission } from "../composables/useAttendancePermission";
63
+
64
+ const props = defineProps({
65
+ orgId: { type: String, default: "" },
66
+ site: { type: String, default: "" },
67
+ });
68
+
69
+ const {
70
+ canViewAllAttendance,
71
+ canViewAttendanceDetails,
72
+ canManageAttendanceSettings,
73
+ } = useAttendancePermission();
74
+
75
+ const submitting = ref(false);
76
+ const page = ref(1);
77
+ const pages = ref(0);
78
+ const pageRange = ref("-- - -- of --");
79
+ const searchInput = ref("");
80
+
81
+ const items = ref<Array<Record<string, any>>>([]);
82
+
83
+ const headers = [
84
+ { title: "Cleaner", value: "userName" },
85
+ { title: "Check In", value: "checkInTimestamp" },
86
+ { title: "Check Out", value: "checkOutTimestamp" },
87
+ { title: "Total Hours", value: "totalHours" },
88
+ ];
89
+
90
+ const { getAttendances } = useAttendance();
91
+ const { formatDate } = useUtils();
92
+
93
+ function calculateTotalHours(attendance: any): string {
94
+ const checkInTime =
95
+ attendance.checkInTimestamp || attendance.checkIn?.timestamp;
96
+ const checkOutTime =
97
+ attendance.checkOutTimestamp || attendance.checkOut?.timestamp;
98
+
99
+ if (!checkInTime) return "N/A";
100
+ if (!checkOutTime) return "In Progress";
101
+
102
+ const checkIn = new Date(checkInTime);
103
+ const checkOut = new Date(checkOutTime);
104
+ const diffMs = checkOut.getTime() - checkIn.getTime();
105
+ const diffHours = diffMs / (1000 * 60 * 60);
106
+
107
+ return `${diffHours.toFixed(2)} hrs`;
108
+ }
109
+
110
+ const {
111
+ data: getAttendancesReq,
112
+ refresh: getAttendancesRefresh,
113
+ pending: loading,
114
+ } = await useLazyAsyncData(
115
+ "get-my-attendances",
116
+ () =>
117
+ getAttendances({
118
+ page: page.value,
119
+ search: searchInput.value,
120
+ site: props.site,
121
+ }),
122
+ {
123
+ watch: [page, searchInput, () => props.site],
124
+ }
125
+ );
126
+
127
+ watchEffect(() => {
128
+ if (getAttendancesReq.value) {
129
+ items.value = getAttendancesReq.value.items.map((item: any) => ({
130
+ ...item,
131
+ totalHours: calculateTotalHours(item),
132
+ }));
133
+ pages.value = getAttendancesReq.value.pages;
134
+ pageRange.value = getAttendancesReq.value.pageRange;
135
+ }
136
+ });
137
+
138
+ const dialogShowMoreActions = ref(false);
139
+ const dialogShowSettings = ref(false);
140
+ const selectedAttendanceId = ref("");
141
+
142
+ async function handleRowClick(data: any) {
143
+ const id = (data?.item as any)?._id;
144
+ if (!id) return;
145
+
146
+ selectedAttendanceId.value = id;
147
+ dialogShowMoreActions.value = true;
148
+ }
149
+
150
+ const onSettingsSaved = async (payload: any) => {
151
+ console.log("Settings saved:", payload);
152
+ // TODO: Implement API call to save settings
153
+ await getAttendancesRefresh();
154
+ };
155
+ </script>
@@ -0,0 +1,393 @@
1
+ <template>
2
+ <v-dialog v-model="showDialog" max-width="500" persistent>
3
+ <v-card>
4
+ <v-toolbar>
5
+ <v-row no-gutters class="fill-height px-6" align="center">
6
+ <span class="font-weight-bold text-h6">Search Location</span>
7
+ </v-row>
8
+ </v-toolbar>
9
+
10
+ <v-card-text class="pa-6">
11
+ <v-form ref="formRef" @submit.prevent="handleSearch">
12
+ <v-row dense>
13
+ <v-col cols="12" md="6">
14
+ <InputLabel title="Country (Optional)" />
15
+ <v-autocomplete
16
+ v-model="formData.country"
17
+ :items="countryList"
18
+ :loading="loadingCountries"
19
+ density="comfortable"
20
+ placeholder="Select country"
21
+ clearable
22
+ hide-details="auto"
23
+ @update:model-value="handleCountryChange"
24
+ />
25
+ </v-col>
26
+
27
+ <v-col cols="12" md="6">
28
+ <InputLabel title="Postal Code" required />
29
+ <v-text-field
30
+ v-model="formData.postalCode"
31
+ clearable
32
+ :loading="postalLoading"
33
+ density="comfortable"
34
+ placeholder="Enter postal code"
35
+ :rules="postalCodeRules"
36
+ hide-details="auto"
37
+ @update:model-value="handlePostalCodeInput"
38
+ @click:clear="handlePostalCodeClear"
39
+ />
40
+ </v-col>
41
+
42
+ <v-col cols="12" md="6">
43
+ <InputLabel title="City (Optional)" />
44
+ <v-text-field
45
+ v-model="formData.city"
46
+ :loading="postalLoading"
47
+ density="comfortable"
48
+ placeholder="Enter city"
49
+ hide-details="auto"
50
+ @update:model-value="handleCityChange"
51
+ />
52
+ </v-col>
53
+
54
+ <v-col cols="12" md="6">
55
+ <InputLabel title="Address (Optional)" />
56
+ <v-text-field
57
+ v-model="formData.address"
58
+ :loading="postalLoading"
59
+ density="comfortable"
60
+ placeholder="Enter address"
61
+ hide-details="auto"
62
+ @update:model-value="handleAddressChange"
63
+ />
64
+ </v-col>
65
+ </v-row>
66
+ </v-form>
67
+ </v-card-text>
68
+
69
+ <v-toolbar class="pa-0" density="compact">
70
+ <v-row no-gutters>
71
+ <v-col cols="6" class="pa-0">
72
+ <v-btn
73
+ block
74
+ variant="text"
75
+ class="text-none"
76
+ size="large"
77
+ height="56"
78
+ :disabled="searching"
79
+ @click="handleClose"
80
+ >
81
+ Cancel
82
+ </v-btn>
83
+ </v-col>
84
+ <v-col cols="6" class="pa-0">
85
+ <v-btn
86
+ block
87
+ variant="flat"
88
+ color="black"
89
+ class="text-none font-weight-bold rounded-0"
90
+ height="56"
91
+ size="large"
92
+ :loading="searching"
93
+ :disabled="!formData.postalCode"
94
+ @click="handleSearch"
95
+ >
96
+ <v-icon class="mr-2">mdi-magnify</v-icon>
97
+ Search
98
+ </v-btn>
99
+ </v-col>
100
+ </v-row>
101
+ </v-toolbar>
102
+ </v-card>
103
+ </v-dialog>
104
+
105
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
106
+ </template>
107
+ That is
108
+
109
+ <script setup lang="ts">
110
+ const showDialog = defineModel({ type: Boolean, default: false });
111
+
112
+ const props = defineProps<{
113
+ initialCountry?: string;
114
+ initialPostalCode?: string;
115
+ initialCity?: string;
116
+ initialAddress?: string;
117
+ }>();
118
+
119
+ const emit = defineEmits<{
120
+ locationSelected: [location: TLocationData];
121
+ }>();
122
+
123
+ const { getCountries, getAddressByPostalCode, searchLocationByAddress } =
124
+ useLocation();
125
+
126
+ const formRef = ref<any>(null);
127
+ const searching = ref(false);
128
+ const loadingCountries = ref(false);
129
+ const postalLoading = ref(false);
130
+
131
+ const formData = ref<TLocationFormData>({
132
+ country: props.initialCountry || "",
133
+ postalCode: props.initialPostalCode || "",
134
+ city: props.initialCity || "",
135
+ address: props.initialAddress || "",
136
+ });
137
+
138
+ const manuallyEdited = ref<TLocationFieldState>({
139
+ country: false,
140
+ city: false,
141
+ address: false,
142
+ });
143
+
144
+ const autoFilled = ref<TLocationFieldState>({
145
+ country: !!props.initialCountry,
146
+ city: !!props.initialCity,
147
+ address: !!props.initialAddress,
148
+ });
149
+
150
+ const message = ref("");
151
+ const messageSnackbar = ref(false);
152
+ const messageColor = ref("");
153
+
154
+ const countryList = ref<string[]>([]);
155
+
156
+ let postalCodeTimeout: ReturnType<typeof setTimeout> | null = null;
157
+
158
+ const postalCodeRules = [(v: string) => !!v || "Postal code is required"];
159
+
160
+ onMounted(async () => {
161
+ await loadCountriesList();
162
+ });
163
+
164
+ onUnmounted(() => {
165
+ if (postalCodeTimeout) {
166
+ clearTimeout(postalCodeTimeout);
167
+ }
168
+ });
169
+
170
+ const loadCountriesList = async () => {
171
+ try {
172
+ loadingCountries.value = true;
173
+ countryList.value = await getCountries();
174
+ } catch (err) {
175
+ console.error("Failed to load countries:", err);
176
+ showMessage("Failed to load countries list", "error");
177
+ } finally {
178
+ loadingCountries.value = false;
179
+ }
180
+ };
181
+
182
+ const handleCountryChange = () => {
183
+ manuallyEdited.value.country = true;
184
+ autoFilled.value.country = false;
185
+ };
186
+
187
+ const handleCityChange = () => {
188
+ manuallyEdited.value.city = true;
189
+ autoFilled.value.city = false;
190
+ };
191
+
192
+ const handleAddressChange = () => {
193
+ manuallyEdited.value.address = true;
194
+ autoFilled.value.address = false;
195
+ };
196
+
197
+ const handlePostalCodeInput = () => {
198
+ postalLoading.value = true;
199
+
200
+ if (postalCodeTimeout) {
201
+ clearTimeout(postalCodeTimeout);
202
+ }
203
+
204
+ postalCodeTimeout = setTimeout(() => {
205
+ fetchAddressFromPostal();
206
+ }, 800);
207
+ };
208
+
209
+ const handlePostalCodeClear = () => {
210
+ if (postalCodeTimeout) {
211
+ clearTimeout(postalCodeTimeout);
212
+ postalCodeTimeout = null;
213
+ }
214
+
215
+ postalLoading.value = false;
216
+ formData.value.postalCode = "";
217
+
218
+ if (autoFilled.value.country) {
219
+ formData.value.country = "";
220
+ autoFilled.value.country = false;
221
+ }
222
+
223
+ if (autoFilled.value.city) {
224
+ formData.value.city = "";
225
+ autoFilled.value.city = false;
226
+ }
227
+
228
+ if (autoFilled.value.address) {
229
+ formData.value.address = "";
230
+ autoFilled.value.address = false;
231
+ }
232
+ };
233
+
234
+ const fetchAddressFromPostal = async () => {
235
+ if (!formData.value.postalCode) {
236
+ postalLoading.value = false;
237
+ return;
238
+ }
239
+
240
+ try {
241
+ postalLoading.value = true;
242
+
243
+ const result = await getAddressByPostalCode(
244
+ formData.value.postalCode,
245
+ formData.value.country,
246
+ );
247
+
248
+ if (result) {
249
+ const addressData = result.address || {};
250
+ const displayParts =
251
+ result.display_name?.split(",").map((s: string) => s.trim()) || [];
252
+
253
+ const countryName = displayParts[displayParts.length - 1] || "";
254
+ const derivedCity =
255
+ addressData.city ||
256
+ addressData.town ||
257
+ addressData.village ||
258
+ addressData.state ||
259
+ displayParts[displayParts.length - 2] ||
260
+ "";
261
+
262
+ if (!manuallyEdited.value.country && countryName) {
263
+ formData.value.country = countryName;
264
+ autoFilled.value.country = true;
265
+ }
266
+
267
+ if (!manuallyEdited.value.city && derivedCity) {
268
+ formData.value.city = derivedCity;
269
+ autoFilled.value.city = true;
270
+ }
271
+ if (!manuallyEdited.value.address && result.display_name) {
272
+ formData.value.address = result.display_name;
273
+ autoFilled.value.address = true;
274
+ }
275
+ } else {
276
+ if (autoFilled.value.country && !manuallyEdited.value.country) {
277
+ formData.value.country = "";
278
+ autoFilled.value.country = false;
279
+ }
280
+
281
+ if (autoFilled.value.city && !manuallyEdited.value.city) {
282
+ formData.value.city = "";
283
+ autoFilled.value.city = false;
284
+ }
285
+
286
+ if (autoFilled.value.address && !manuallyEdited.value.address) {
287
+ formData.value.address = "";
288
+ autoFilled.value.address = false;
289
+ }
290
+ }
291
+ } catch (err) {
292
+ console.error("Error fetching address from postal code:", err);
293
+ } finally {
294
+ postalLoading.value = false;
295
+ }
296
+ };
297
+
298
+ const handleSearch = async () => {
299
+ const { valid } = await formRef.value.validate();
300
+
301
+ if (!valid) {
302
+ return;
303
+ }
304
+
305
+ searching.value = true;
306
+
307
+ try {
308
+ const item = await searchLocationByAddress({
309
+ postalCode: formData.value.postalCode,
310
+ city: manuallyEdited.value.city ? formData.value.city : undefined,
311
+ address: manuallyEdited.value.address
312
+ ? formData.value.address
313
+ : undefined,
314
+ country: formData.value.country,
315
+ });
316
+
317
+ if (item) {
318
+ const location: TLocationData = {
319
+ latitude: parseFloat(item.lat),
320
+ longitude: parseFloat(item.lon),
321
+ address: formData.value.address || item.display_name || "",
322
+ city:
323
+ formData.value.city ||
324
+ item.address?.city ||
325
+ item.address?.town ||
326
+ item.address?.village ||
327
+ "",
328
+ country: formData.value.country || item.address?.country || "",
329
+ postalCode: formData.value.postalCode || "",
330
+ };
331
+
332
+ emit("locationSelected", location);
333
+ showMessage("Location found successfully", "success");
334
+ handleClose();
335
+ } else {
336
+ showMessage(
337
+ "Location not found. Please try different search criteria.",
338
+ "error",
339
+ );
340
+ }
341
+ } catch (err) {
342
+ console.error("Error searching location:", err);
343
+ showMessage("Failed to search location. Please try again.", "error");
344
+ } finally {
345
+ searching.value = false;
346
+ }
347
+ };
348
+
349
+ const handleClose = () => {
350
+ resetForm();
351
+ showDialog.value = false;
352
+ };
353
+
354
+ const resetForm = () => {
355
+ formData.value = {
356
+ country: props.initialCountry || "",
357
+ postalCode: props.initialPostalCode || "",
358
+ city: props.initialCity || "",
359
+ address: props.initialAddress || "",
360
+ };
361
+
362
+ manuallyEdited.value = {
363
+ country: false,
364
+ city: false,
365
+ address: false,
366
+ };
367
+
368
+ autoFilled.value = {
369
+ country: !!props.initialCountry,
370
+ city: !!props.initialCity,
371
+ address: !!props.initialAddress,
372
+ };
373
+
374
+ if (postalCodeTimeout) {
375
+ clearTimeout(postalCodeTimeout);
376
+ postalCodeTimeout = null;
377
+ }
378
+
379
+ postalLoading.value = false;
380
+ };
381
+
382
+ const showMessage = (msg: string, color: string = "error") => {
383
+ message.value = msg;
384
+ messageColor.value = color;
385
+ messageSnackbar.value = true;
386
+ };
387
+
388
+ watch(showDialog, (newVal) => {
389
+ if (newVal) {
390
+ resetForm();
391
+ }
392
+ });
393
+ </script>