@iservice365/layer-common 1.1.0 → 1.2.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/components/CameraForm.vue +264 -0
  3. package/components/CameraMain.vue +352 -0
  4. package/components/Card/DeleteConfirmation.vue +51 -0
  5. package/components/Card/MemberInfoSummary.vue +44 -0
  6. package/components/Chat/Information.vue +28 -11
  7. package/components/Dialog/DeleteConfirmation.vue +51 -0
  8. package/components/Dialog/UpdateMoreAction.vue +99 -0
  9. package/components/Feedback/Form.vue +17 -3
  10. package/components/FeedbackDetail.vue +0 -11
  11. package/components/FeedbackMain.vue +21 -11
  12. package/components/Input/DateTimePicker.vue +5 -1
  13. package/components/Input/File.vue +1 -1
  14. package/components/Input/FileV2.vue +111 -63
  15. package/components/Input/InputPhoneNumberV2.vue +115 -0
  16. package/components/Input/NRICNumber.vue +41 -0
  17. package/components/Input/PhoneNumber.vue +1 -0
  18. package/components/Input/VehicleNumber.vue +41 -0
  19. package/components/NumberSettingField.vue +107 -0
  20. package/components/PeopleForm.vue +420 -0
  21. package/components/TableMain.vue +2 -1
  22. package/components/VehicleUpdateMoreAction.vue +84 -0
  23. package/components/VisitorForm.vue +712 -0
  24. package/components/VisitorFormSelection.vue +53 -0
  25. package/components/VisitorManagement.vue +568 -0
  26. package/components/WorkOrder/Create.vue +70 -46
  27. package/components/WorkOrder/Main.vue +11 -10
  28. package/composables/useBuilding.ts +250 -0
  29. package/composables/useBuildingUnit.ts +116 -0
  30. package/composables/useFeedback.ts +3 -3
  31. package/composables/useFile.ts +7 -9
  32. package/composables/useLocal.ts +67 -0
  33. package/composables/usePeople.ts +48 -0
  34. package/composables/useSecurityUtils.ts +18 -0
  35. package/composables/useSiteSettings.ts +111 -0
  36. package/composables/useUtils.ts +30 -1
  37. package/composables/useVisitor.ts +79 -0
  38. package/package.json +1 -1
  39. package/plugins/vuetify.ts +6 -1
  40. package/types/building.d.ts +19 -0
  41. package/types/camera.d.ts +31 -0
  42. package/types/people.d.ts +22 -0
  43. package/types/select.d.ts +4 -0
  44. package/types/site.d.ts +10 -7
  45. package/types/visitor.d.ts +42 -0
  46. package/utils/phoneMasks.ts +1703 -0
@@ -0,0 +1,420 @@
1
+ <template>
2
+ <v-card width="100%" :loading="processing">
3
+ <v-toolbar>
4
+ <v-row no-gutters class="fill-height px-6" align="center">
5
+ <span class="font-weight-bold text-h5 text-capitalize">
6
+ {{ prop.mode }} {{ prop.type === 'visitor' ? 'Guest' : prop.type }}
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-3">
11
+ <div class="w-100 d-flex justify-space-between ga-2">
12
+ <span v class="text-subtitle-1 w-100 font-weight-bold mb-3">
13
+ <span v-if="step === 1">General Information</span>
14
+ <span v-else-if="step === 2">Pass Information</span>
15
+ </span>
16
+ <span class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap;">Step
17
+ <span class="text-primary-button">{{ step }}</span>/2</span>
18
+ </div>
19
+ <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''">
20
+ <v-row no-gutters class="pt-4">
21
+
22
+ <template v-if="step === 1">
23
+ <v-col cols="12">
24
+ <v-row>
25
+ <v-col cols="12">
26
+ <InputLabel class="text-capitalize" title="Full Name" required />
27
+ <v-text-field v-model.trim="form.name" density="comfortable" :rules="[requiredRule]" />
28
+ </v-col>
29
+ </v-row>
30
+ </v-col>
31
+
32
+
33
+ <v-col cols="12">
34
+ <v-row>
35
+ <v-col cols="12">
36
+ <InputLabel class="text-capitalize" title="NRIC/Passport/ID No." />
37
+ <InputNRICNumber v-model.trim="form.nric" density="comfortable" />
38
+ </v-col>
39
+ </v-row>
40
+ </v-col>
41
+
42
+ <v-col cols="12">
43
+ <InputLabel class="text-capitalize" title="Phone Number" required />
44
+ <InputPhoneNumberV2 v-model="form.contact"
45
+ density="comfortable" :rules="[requiredRule]" />
46
+ </v-col>
47
+
48
+ <v-col cols="12">
49
+ <InputLabel class="text-capitalize" title="Vehicle Number" />
50
+ <InputVehicleNumber v-model.trim="form.plateNumber" density="comfortable" />
51
+ </v-col>
52
+
53
+ <v-col cols="12">
54
+ <InputLabel class="text-capitalize" title="Block" required />
55
+ <v-select v-model="form.block" :items="blocksArray" item-value="value" item-title="title" attached
56
+ :loading="blockStatus === 'pending'" @update:model-value="handleChangeBlock" density="comfortable"
57
+ :rules="[requiredRule]" />
58
+ </v-col>
59
+
60
+ <v-col cols="12">
61
+ <InputLabel class="text-capitalize" title="Level" required />
62
+ <v-select v-model="form.level" :items="levelsArray" density="comfortable" :disabled="!form.block"
63
+ :loading="levelsStatus === 'pending'" @update:model-value="handleChangeLevel" :rules="[requiredRule]" />
64
+ </v-col>
65
+
66
+ <v-col cols="12">
67
+ <InputLabel class="text-capitalize" title="Unit" required />
68
+ <v-select v-model="form.unit" :items="unitsArray" @update:model-value="handleUpdateUnit" density="comfortable" :disabled="!form.level"
69
+ :loading="unitsStatus === 'pending'" :rules="[requiredRule]" />
70
+ </v-col>
71
+
72
+ <v-col cols="12">
73
+ <InputLabel class="text-capitalize" title="Start Date" required />
74
+ <InputDateTimePicker v-model:utc="form.start" ref="startDateRef"
75
+ :rules="[validStartDateRule]" />
76
+ </v-col>
77
+ <v-col cols="12">
78
+ <InputLabel class="text-capitalize" title="End Date" required />
79
+ <InputDateTimePicker v-model:utc="form.end" ref="endDateRef"
80
+ :rules="[validExpiryDateRule]" />
81
+ </v-col>
82
+
83
+ <v-col cols="12">
84
+ <InputLabel class="text-capitalize" title="Remarks" />
85
+ <v-textarea v-model="form.remarks" density="comfortable" :rows="3" no-resize />
86
+ </v-col>
87
+ </template>
88
+
89
+
90
+ <template v-else-if="step === 2">
91
+ <v-col cols="12">
92
+ <PassInformation />
93
+ </v-col>
94
+
95
+ <v-col v-if="prop.mode === 'add'" cols="12" class="mt-2">
96
+ <v-checkbox v-model="createMore" density="comfortable" hide-details>
97
+ <template #label>
98
+ <span class="text-subtitle-2 font-weight-bold">
99
+ Create more
100
+ </span>
101
+ </template>
102
+ </v-checkbox>
103
+ </v-col>
104
+ </template>
105
+
106
+ </v-row>
107
+ </v-form>
108
+ </v-card-text>
109
+
110
+ <v-row no-gutters class="w-100" v-if="errorMessage">
111
+ <p class="text-error w-100 text-center text-subtitle-2">{{ errorMessage }}</p>
112
+ </v-row>
113
+ <v-toolbar density="compact">
114
+ <v-row no-gutters>
115
+ <v-col cols="6">
116
+ <v-btn v-if="step > 1" tile block variant="text" class="text-none" size="48" @click="back" text="Back" />
117
+ <v-btn v-else tile block variant="text" class="text-none" size="48" @click="close" text="Close" />
118
+ </v-col>
119
+ <v-col cols="6">
120
+ <v-btn tile block variant="flat" color="primary-button" class="text-none" size="48" :disabled="processing"
121
+ @click="handleNextPage" :text="step === 2 ? 'Submit' : 'Next'" />
122
+ </v-col>
123
+ </v-row>
124
+ </v-toolbar>
125
+ </v-card>
126
+ </template>
127
+
128
+ <script setup lang="ts">
129
+ import { property } from 'zod/v4';
130
+
131
+ const prop = defineProps({
132
+ org: {
133
+ type: String,
134
+ required: true
135
+ },
136
+ site: {
137
+ type: String,
138
+ required: true
139
+ },
140
+ mode: {
141
+ type: String as PropType<'add' | 'edit'>,
142
+ default: 'add'
143
+ },
144
+ type: {
145
+ type: String as PropType<TPeopleType>,
146
+ required: true
147
+ }
148
+ });
149
+
150
+ const { requiredRule, formatDateISO8601 } = useUtils();
151
+ const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
152
+ const { create } = usePeople();
153
+
154
+ const emit = defineEmits(['back', 'select', 'done', 'done:more', 'error', 'close']);
155
+
156
+
157
+ const form = reactive<Partial<TPeoplePayload>>({
158
+ name: "",
159
+ nric: "",
160
+ contact: "",
161
+ plateNumber: "",
162
+ block: "",
163
+ level: "",
164
+ unit: "",
165
+ unitName: "",
166
+ remarks: "",
167
+ start: dateToday() || "",
168
+ end: "",
169
+ type: prop.type
170
+ })
171
+
172
+
173
+ const validForm = ref(false);
174
+ const formRef = ref<HTMLFormElement | null>(null);
175
+ const processing = ref(false);
176
+ const message = ref('');
177
+ const errorMessage = ref('');
178
+ const createMore = ref(false)
179
+
180
+ const startDateRef = ref<HTMLElement>()
181
+ const endDateRef = ref<HTMLElement>()
182
+
183
+ const step = ref(1)
184
+
185
+ const blocksArray = ref<TDefaultOptionObj[]>([]);
186
+ const levelsArray = ref<TDefaultOptionObj[]>([]);
187
+ const unitsArray = ref<TDefaultOptionObj[]>([]);
188
+
189
+
190
+
191
+ const contractorTypes = [
192
+ { title: "Estate Contractor", value: "estate-contractor" },
193
+ { title: "Home Contractor", value: "home-contractor" },
194
+ { title: "Property Agent", value: "property-agent" },
195
+ { title: "House Mover", value: "house-mover" },
196
+ ]
197
+
198
+
199
+
200
+ const { data: siteData, refresh: refreshSiteData, status: blockStatus } = useLazyAsyncData(
201
+ `fetch-site-data-${prop.site}`,
202
+ () => getSiteById(prop.site));
203
+
204
+ const { data: levelsData, refresh: refreshLevelsData, status: levelsStatus } = useLazyAsyncData(
205
+ `fetch-levels-data-${prop.site}-${form.block}`,
206
+ async () => {
207
+ if (!form.block) return Promise.resolve(null);
208
+ return await getSiteLevels(prop.site, { block: Number(form.block) })
209
+ });
210
+
211
+ const { data: unitsData, refresh: refreshUnitsData, status: unitsStatus } = useLazyAsyncData(
212
+ `fetch-units-data-${prop.site}-${form.level}`,
213
+ async () => {
214
+ if (!form.level) return Promise.resolve(null);
215
+ return await getSiteUnits(prop.site, Number(form.block), form.level)
216
+ });
217
+
218
+ watch(
219
+ siteData,
220
+ (newVal) => {
221
+ const siteDataValue = newVal as any;
222
+ if (siteDataValue) {
223
+ const numberOfBlocks = siteDataValue.metadata?.block || 0;
224
+ for (let i = 1; i <= numberOfBlocks; i++) {
225
+ blocksArray.value.push({
226
+ title: `Block ${i}`,
227
+ value: i
228
+ });
229
+ }
230
+
231
+ } else {
232
+ blocksArray.value = [];
233
+ }
234
+ }, { immediate: true });
235
+
236
+
237
+ watch(
238
+ levelsData,
239
+ (newVal: any) => {
240
+ if (newVal) {
241
+ const arr = newVal.levels || [];
242
+ levelsArray.value = arr?.map((level: any) => ({
243
+ title: level,
244
+ value: level
245
+ }));
246
+ } else {
247
+ levelsArray.value = [];
248
+ }
249
+ }, { immediate: true });
250
+
251
+ watch(
252
+ unitsData,
253
+ (newVal: any) => {
254
+ if (newVal && Array.isArray(newVal)) {
255
+ const arr = newVal || [];
256
+ unitsArray.value = arr?.map((unit: any) => ({
257
+ title: unit?.name,
258
+ value: unit?._id
259
+ }));
260
+ } else {
261
+ unitsArray.value = [];
262
+ }
263
+ }, { immediate: true });
264
+
265
+
266
+
267
+ function handleChangeBlock(value: any) {
268
+ form.level = '';
269
+ form.unit = '';
270
+ refreshLevelsData();
271
+ }
272
+
273
+ function handleChangeLevel(value: any) {
274
+ form.unit = '';
275
+ refreshUnitsData();
276
+ }
277
+
278
+ function handleUpdateUnit(value: any) {
279
+ const selectedUnit = unitsArray.value?.find((x: any) => x.value === value)
280
+ form.unitName = selectedUnit?.title || ''
281
+ }
282
+
283
+ function dateToday(){
284
+ const today = new Date()
285
+ return today.toISOString()
286
+ }
287
+
288
+
289
+ function back() {
290
+ if (step.value > 1) {
291
+ step.value--
292
+ }
293
+ emit("back");
294
+ message.value = '';
295
+ }
296
+
297
+ function close() {
298
+ emit("close");
299
+ message.value = '';
300
+ }
301
+
302
+
303
+
304
+ async function handleNextPage() {
305
+ errorMessage.value = ''
306
+ formRef.value!.validate()
307
+ if (!validForm.value) {
308
+ errorMessage.value = "Please complete all required fields *"
309
+ return
310
+ }
311
+ const stepVal = step.value
312
+ if (stepVal < 2) step.value++
313
+ else if (stepVal === 2) {
314
+ await submit()
315
+ }
316
+
317
+ }
318
+
319
+ function resetForm() {
320
+ formRef.value?.resetValidation();
321
+
322
+ Object.assign(form, {
323
+ name: "",
324
+ nric: "",
325
+ contact: "",
326
+ plateNumber: "",
327
+ block: "",
328
+ level: "",
329
+ unit: "",
330
+ remarks: ""
331
+ });
332
+
333
+ validForm.value = false;
334
+ }
335
+
336
+ function validStartDateRule(value: string) {
337
+ if (!value) {
338
+ return 'Start Date is required';
339
+ }
340
+
341
+ return true;
342
+ }
343
+
344
+ function validExpiryDateRule(value: string) {
345
+ const startDateISO = form.start;
346
+ if (!value) {
347
+ return 'End Date is required';
348
+ }
349
+
350
+ if (value && startDateISO) {
351
+ const expiry = new Date(value);
352
+ const start = new Date(startDateISO as string);
353
+ return expiry > start || 'End date must be later than start date';
354
+ }
355
+ return true;
356
+ }
357
+
358
+
359
+
360
+ async function submit() {
361
+ formRef.value!.validate()
362
+ errorMessage.value = '';
363
+ processing.value = true;
364
+
365
+ let payload: Partial<TPeoplePayload> = {
366
+ org: prop.org,
367
+ site: prop.site
368
+ }
369
+
370
+ if (prop.mode === 'add') {
371
+ payload = {
372
+ ...payload,
373
+ ...form,
374
+ }
375
+
376
+ } else if (prop.mode === 'edit') {
377
+
378
+ }
379
+ try {
380
+
381
+ const res = await create(payload)
382
+ if (res) {
383
+ if (createMore.value) {
384
+ resetForm()
385
+ step.value = 1;
386
+ errorMessage.value = ""
387
+ emit("done:more")
388
+ createMore.value = false
389
+ } else emit("done")
390
+ }
391
+
392
+ } catch (error: any) {
393
+ const err = error?.data?.message
394
+ errorMessage.value = err || `Failed to ${prop.mode === 'add' ? 'add' : 'update'} ${prop.type}. Please try again.`;
395
+ } finally {
396
+ processing.value = false;
397
+ }
398
+ }
399
+
400
+ watch(() => form.plateNumber, (newVal) => {
401
+ form.plateNumber = newVal?.toUpperCase()
402
+ })
403
+
404
+ watch(() => [form.end, form.start], () => {
405
+ (endDateRef.value as any)?.validate();
406
+ (startDateRef.value as any)?.validate();
407
+ });
408
+
409
+ onMounted(() => {
410
+ step.value = 1;
411
+ createMore.value = false;
412
+ })
413
+
414
+ </script>
415
+ <style scoped>
416
+ .button-outline-class {
417
+ border: 1px solid rgba(var(--v-theme-primary));
418
+ }
419
+
420
+ </style>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
3
  <!-- Top Actions -->
4
- <v-col cols="12" class="mb-2" v-if="canCreate || $slots.actions">
4
+ <v-col cols="12" class="mb-2" v-if="(canCreate || $slots.actions)">
5
5
  <v-row no-gutters>
6
6
  <slot name="actions">
7
7
  <v-btn v-if="canCreate" class="text-none" rounded="pill" variant="tonal" size="large"
@@ -21,6 +21,7 @@
21
21
  <v-btn fab icon density="comfortable" @click="emits('refresh')">
22
22
  <v-icon>mdi-refresh</v-icon>
23
23
  </v-btn>
24
+ <slot name="prepend-additional" />
24
25
  </template>
25
26
 
26
27
  <template #append>
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <v-card width="100%">
3
+ <v-toolbar>
4
+ <template v-if="$slots.header">
5
+ <slot name="header" :title="title" :onClose="onClose" />
6
+ </template>
7
+ <template v-else>
8
+ <v-row no-gutters class="fill-height px-6" align="center">
9
+ <span class="font-weight-bold text-h5 text-capitalize">{{
10
+ title
11
+ }}</span>
12
+ </v-row>
13
+ </template>
14
+ </v-toolbar>
15
+
16
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
17
+ <slot name="content" />
18
+ </v-card-text>
19
+
20
+ <v-toolbar class="pa-0" density="compact">
21
+ <v-row no-gutters>
22
+ <v-col :cols="canDelete || canUpdate ? 6 : 12" class="pa-0">
23
+ <v-btn block variant="text" class="text-none" size="large" @click="emit('close')" height="48">
24
+ Close
25
+ </v-btn>
26
+ </v-col>
27
+
28
+ <v-col cols="6" class="pa-0" v-if="canDelete || canUpdate">
29
+ <v-menu>
30
+ <template #activator="{ props }">
31
+ <v-btn block variant="flat" color="black" class="text-none" height="48" v-bind="props" tile>
32
+ More actions
33
+ </v-btn>
34
+ </template>
35
+
36
+ <v-list class="pa-0">
37
+ <v-list-item v-if="canUpdate" @click="emit('edit')">
38
+ <v-list-item-title class="text-subtitle-2">
39
+ {{ editButtonLabel }}
40
+ </v-list-item-title>
41
+ </v-list-item>
42
+
43
+ <v-list-item v-if="canDelete" @click="emit('delete')" class="text-red">
44
+ <v-list-item-title class="text-subtitle-2">
45
+ {{ deleteButtonLabel }}
46
+ </v-list-item-title>
47
+ </v-list-item>
48
+ </v-list>
49
+ </v-menu>
50
+ </v-col>
51
+ </v-row>
52
+ </v-toolbar>
53
+ </v-card>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ const prop = defineProps({
58
+ canUpdate: {
59
+ type: Boolean,
60
+ default: true,
61
+ },
62
+ canDelete: {
63
+ type: Boolean,
64
+ default: true,
65
+ },
66
+ editButtonLabel: {
67
+ type: String,
68
+ default: "Edit",
69
+ },
70
+ deleteButtonLabel: {
71
+ type: String,
72
+ default: "Delete",
73
+ },
74
+ title: {
75
+ type: String,
76
+ default: "Details",
77
+ },
78
+ });
79
+
80
+ const emit = defineEmits(["close", "edit", "delete"]);
81
+ const { canUpdate, editButtonLabel, deleteButtonLabel, title } = prop;
82
+ </script>
83
+
84
+ <style scoped></style>