@7365admin1/layer-common 1.8.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 (198) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.editorconfig +12 -0
  4. package/.github/workflows/main.yml +17 -0
  5. package/.github/workflows/publish.yml +39 -0
  6. package/.nuxtrc +1 -0
  7. package/.playground/app.vue +41 -0
  8. package/.playground/eslint.config.mjs +6 -0
  9. package/.playground/nuxt.config.ts +22 -0
  10. package/.playground/pages/feedback.vue +30 -0
  11. package/CHANGELOG.md +263 -0
  12. package/README.md +73 -0
  13. package/app.vue +3 -0
  14. package/components/AccessCardAddForm.vue +363 -0
  15. package/components/AccessManagement.vue +420 -0
  16. package/components/Avatar/Main.vue +68 -0
  17. package/components/BillingMain.vue +66 -0
  18. package/components/BtnUploadFile.vue +139 -0
  19. package/components/BuildingForm.vue +303 -0
  20. package/components/BuildingManagement/buildings.vue +335 -0
  21. package/components/BuildingManagement/units.vue +350 -0
  22. package/components/BuildingUnitFormAdd.vue +441 -0
  23. package/components/BuildingUnitFormEdit.vue +429 -0
  24. package/components/CameraForm.vue +264 -0
  25. package/components/CameraMain.vue +352 -0
  26. package/components/Card/DeleteConfirmation.vue +51 -0
  27. package/components/Card/MemberInfoSummary.vue +44 -0
  28. package/components/Card/Toggle.vue +25 -0
  29. package/components/Chat/Bubbles.vue +53 -0
  30. package/components/Chat/Information.vue +416 -0
  31. package/components/Chat/ListCard.vue +62 -0
  32. package/components/Chat/Message.vue +158 -0
  33. package/components/Chat/Navigation.vue +150 -0
  34. package/components/ConfirmDialog.vue +66 -0
  35. package/components/Container/Standard.vue +33 -0
  36. package/components/DashboardPlaceholder.vue +1524 -0
  37. package/components/Dialog/DeleteConfirmation.vue +51 -0
  38. package/components/Dialog/ReplaceAutofillPrompt.vue +49 -0
  39. package/components/Dialog/UpdateMoreAction.vue +103 -0
  40. package/components/DocumentForm.vue +187 -0
  41. package/components/DocumentManagement.vue +376 -0
  42. package/components/Editor.vue +95 -0
  43. package/components/EntryPassMain.vue +518 -0
  44. package/components/Feedback/Form.vue +173 -0
  45. package/components/FeedbackDetail.vue +599 -0
  46. package/components/FeedbackMain.vue +588 -0
  47. package/components/FormDialog.vue +65 -0
  48. package/components/ImageCarousel.vue +138 -0
  49. package/components/Input/Date.vue +177 -0
  50. package/components/Input/DateTimePicker.vue +131 -0
  51. package/components/Input/File.vue +236 -0
  52. package/components/Input/FileV2.vue +234 -0
  53. package/components/Input/InputPhoneNumberV2.vue +164 -0
  54. package/components/Input/ListGroupSelection.vue +96 -0
  55. package/components/Input/NRICNumber.vue +53 -0
  56. package/components/Input/NewDate.vue +123 -0
  57. package/components/Input/Number.vue +124 -0
  58. package/components/Input/Password.vue +22 -0
  59. package/components/Input/PhoneNumber.vue +188 -0
  60. package/components/Input/VehicleNumber.vue +49 -0
  61. package/components/InputLabel.vue +22 -0
  62. package/components/InvitationForm.vue +359 -0
  63. package/components/InvitationMain.vue +310 -0
  64. package/components/Layout/Header.vue +129 -0
  65. package/components/Layout/NavigationDrawer.vue +44 -0
  66. package/components/ListItem.vue +35 -0
  67. package/components/ListView.vue +87 -0
  68. package/components/LocalPagination.vue +31 -0
  69. package/components/MemberMain.vue +459 -0
  70. package/components/NFC/NFCPatrolReportMain.vue +591 -0
  71. package/components/NFC/NFCPatrolRouteForm.vue +596 -0
  72. package/components/NFC/NFCPatrolRouteMain.vue +539 -0
  73. package/components/NFC/NFCTagForm.vue +236 -0
  74. package/components/NFC/NFCTagMain.vue +337 -0
  75. package/components/NFC/PatrolSettings.vue +130 -0
  76. package/components/NavigationItem.vue +83 -0
  77. package/components/NumberSettingField.vue +107 -0
  78. package/components/OnlineFormConfigurationForm.vue +290 -0
  79. package/components/OnlineFormsConfiguration.vue +429 -0
  80. package/components/PeopleForm.vue +452 -0
  81. package/components/PlaceholderComponent.vue +34 -0
  82. package/components/RolePermissionFormCreate.vue +161 -0
  83. package/components/RolePermissionFormPreviewUpdate.vue +183 -0
  84. package/components/RolePermissionMain.vue +361 -0
  85. package/components/SearchVehicleNumberUser.vue +91 -0
  86. package/components/ServiceProviderFormCreate.vue +154 -0
  87. package/components/ServiceProviderMain.vue +547 -0
  88. package/components/SignaturePad.vue +73 -0
  89. package/components/Snackbar.vue +23 -0
  90. package/components/SpecificAttr.vue +53 -0
  91. package/components/SupplyManagement.vue +292 -0
  92. package/components/SwitchContext.vue +108 -0
  93. package/components/TableList.vue +150 -0
  94. package/components/TableListSecondary.vue +164 -0
  95. package/components/TableMain.vue +142 -0
  96. package/components/TableWithButton.vue +94 -0
  97. package/components/VehicleUpdateMoreAction.vue +84 -0
  98. package/components/VideoPlayer.vue +125 -0
  99. package/components/VisitorForm.vue +659 -0
  100. package/components/VisitorFormSelection.vue +53 -0
  101. package/components/VisitorManagement.vue +490 -0
  102. package/components/WorkOrder/Create.vue +284 -0
  103. package/components/WorkOrder/Detail.vue +71 -0
  104. package/components/WorkOrder/ListView.vue +96 -0
  105. package/components/WorkOrder/Main.vue +489 -0
  106. package/components/Workorder.vue +1 -0
  107. package/composables/useAddress.ts +107 -0
  108. package/composables/useBuilding.ts +250 -0
  109. package/composables/useBuildingUnit.ts +116 -0
  110. package/composables/useCard.ts +46 -0
  111. package/composables/useCommonPermission.ts +207 -0
  112. package/composables/useCustomer.ts +113 -0
  113. package/composables/useCustomerSite.ts +56 -0
  114. package/composables/useDashboard.ts +31 -0
  115. package/composables/useDashboardData.ts +425 -0
  116. package/composables/useDocument.ts +57 -0
  117. package/composables/useFacility.ts +246 -0
  118. package/composables/useFeedback.ts +119 -0
  119. package/composables/useFile.ts +55 -0
  120. package/composables/useInvoice.ts +18 -0
  121. package/composables/useLocal.ts +131 -0
  122. package/composables/useLocalAuth.ts +137 -0
  123. package/composables/useLocalSetup.ts +13 -0
  124. package/composables/useMember.ts +111 -0
  125. package/composables/useNFCPatrolRoute.ts +77 -0
  126. package/composables/useNFCPatrolSettings.ts +19 -0
  127. package/composables/useNFCPatrolTag.ts +53 -0
  128. package/composables/useOnlineForm.ts +67 -0
  129. package/composables/useOrg.ts +129 -0
  130. package/composables/usePDFDownload.ts +25 -0
  131. package/composables/usePaymentMethod.ts +101 -0
  132. package/composables/usePeople.ts +81 -0
  133. package/composables/usePermission.ts +54 -0
  134. package/composables/usePhoneCountries.ts +561 -0
  135. package/composables/usePrice.ts +15 -0
  136. package/composables/usePromoCode.ts +36 -0
  137. package/composables/useRecapPermission.ts +26 -0
  138. package/composables/useRole.ts +104 -0
  139. package/composables/useSecurityUtils.ts +18 -0
  140. package/composables/useServiceProvider.ts +224 -0
  141. package/composables/useSite.ts +109 -0
  142. package/composables/useSiteEntryPassSettings.ts +46 -0
  143. package/composables/useSiteSettings.ts +123 -0
  144. package/composables/useSubscription.ts +150 -0
  145. package/composables/useUser.ts +132 -0
  146. package/composables/useUtils.ts +445 -0
  147. package/composables/useVerification.ts +34 -0
  148. package/composables/useVisitor.ts +120 -0
  149. package/composables/useWorkOrder.ts +85 -0
  150. package/error.vue +41 -0
  151. package/layouts/plain.vue +7 -0
  152. package/middleware/01.auth.ts +20 -0
  153. package/middleware/02.org.ts +21 -0
  154. package/middleware/03.customer.ts +13 -0
  155. package/middleware/member.ts +4 -0
  156. package/nuxt.config.ts +54 -0
  157. package/package.json +39 -0
  158. package/pages/index.vue +3 -0
  159. package/pages/payment-method-linked.vue +31 -0
  160. package/pages/require-customer.vue +56 -0
  161. package/pages/require-organization-membership.vue +47 -0
  162. package/pages/unauthorized.vue +29 -0
  163. package/plugins/API.ts +21 -0
  164. package/plugins/iconify.client.ts +5 -0
  165. package/plugins/secure-member.client.ts +86 -0
  166. package/plugins/vuetify.ts +62 -0
  167. package/public/bg-camera.jpg +0 -0
  168. package/public/bg-city.jpg +0 -0
  169. package/public/bg-condo.jpg +0 -0
  170. package/public/images/icons/delete-icon.png +0 -0
  171. package/public/sprite.svg +1 -0
  172. package/tsconfig.json +3 -0
  173. package/types/address.d.ts +13 -0
  174. package/types/building.d.ts +27 -0
  175. package/types/camera.d.ts +31 -0
  176. package/types/card.d.ts +22 -0
  177. package/types/customer.d.ts +27 -0
  178. package/types/document.d.ts +6 -0
  179. package/types/feedback.d.ts +68 -0
  180. package/types/local.d.ts +74 -0
  181. package/types/member.d.ts +21 -0
  182. package/types/online-form.d.ts +15 -0
  183. package/types/org.d.ts +13 -0
  184. package/types/people.d.ts +24 -0
  185. package/types/permission.d.ts +25 -0
  186. package/types/phone-number.d.ts +10 -0
  187. package/types/price.d.ts +17 -0
  188. package/types/promo-code.d.ts +19 -0
  189. package/types/role.d.ts +11 -0
  190. package/types/select.d.ts +4 -0
  191. package/types/service-provider.d.ts +15 -0
  192. package/types/site.d.ts +20 -0
  193. package/types/subscription.d.ts +23 -0
  194. package/types/user.d.ts +19 -0
  195. package/types/verification.d.ts +20 -0
  196. package/types/visitor.d.ts +42 -0
  197. package/types/work-order.d.ts +42 -0
  198. package/utils/phoneMasks.ts +1703 -0
@@ -0,0 +1,659 @@
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 }} {{ formatVisitorType(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 class="text-subtitle-1 w-100 font-weight-bold mb-3">{{
13
+ formTitle
14
+ }}</span>
15
+ <span v-if="prop.type === 'contractor'" class="text-subtitle-2 font-weight-bold" style="text-wrap: nowrap">Step
16
+ <span class="text-primary-button">{{ contractorStep }}</span>/3</span>
17
+ </div>
18
+ <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''">
19
+ <v-row no-gutters class="pt-4">
20
+ <v-col v-if="shouldShowField('contractorType')" cols="12">
21
+ <InputLabel class="text-capitalize" title="Contractor Type" required />
22
+ <v-combobox v-model="contractorTypeObj" v-model:search="contractorTypeInput" ref="contractorTypeCombo"
23
+ autocomplete="off" :hide-no-data="false" @update:focused="handleFocusedContractorType"
24
+ :items="contractorTypes" :rules="[requiredRule]" item-value="value"
25
+ @update:model-value="handleSelectContractorType" variant="outlined" density="comfortable" persistent-hint
26
+ small-chips>
27
+ <template v-slot:no-data>
28
+ <v-list-item>
29
+ <v-list-item-title @click.stop="handleAddNewContractorType" class="d-flex align-center ga-1">
30
+ <span><v-icon icon="mdi-plus" /></span> Add "<strong>{{
31
+ contractorTypeInput
32
+ }}</strong>" as custom contractor type.
33
+ </v-list-item-title>
34
+ </v-list-item>
35
+ </template>
36
+ </v-combobox>
37
+ </v-col>
38
+
39
+ <v-col v-if="shouldShowField('attachments')" class="w-100">
40
+ <InputLabel class="text-capitalize" title="Attach Image" />
41
+ <InputFileV2 v-model="visitor.attachments" multiple :max-length="3" />
42
+ </v-col>
43
+
44
+ <v-col v-if="shouldShowField('name')" cols="12">
45
+ <v-row>
46
+ <v-col cols="12">
47
+ <InputLabel class="text-capitalize" title="Full Name" required />
48
+ <v-text-field v-model.trim="visitor.name" density="comfortable" :rules="[requiredRule]" />
49
+ </v-col>
50
+ </v-row>
51
+ </v-col>
52
+
53
+ <v-col v-if="shouldShowField('nric')" cols="12">
54
+ <v-row>
55
+ <v-col cols="12">
56
+ <InputLabel class="text-capitalize" title="NRIC/Passport/ID No." required />
57
+ <InputNRICNumber v-model="visitor.nric" density="comfortable" :rules="[requiredRule]"
58
+ @update:model-value="handleUpdateNRIC" :loading="fetchPersonByNRICPending" />
59
+ </v-col>
60
+ </v-row>
61
+ </v-col>
62
+
63
+ <v-col v-if="shouldShowField('contact')" cols="12">
64
+ <InputLabel class="text-capitalize" title="Phone Number" required />
65
+ <InputPhoneNumberV2 v-model="visitor.contact" :rules="[requiredRule]" density="comfortable"
66
+ :loading="fetchPersonByContactPending" @update:model-value="handleUpdateContact" />
67
+ </v-col>
68
+
69
+ <v-col v-if="shouldShowField('deliveryType')" cols="12">
70
+ <v-row>
71
+ <v-col cols="12">
72
+ <v-radio-group v-model="visitor.deliveryType" inline color="primary" :rules="[requiredRule]">
73
+ <v-radio label="Food" value="Food"></v-radio>
74
+ <v-radio label="Parcel" value="Parcel"></v-radio>
75
+ </v-radio-group>
76
+ </v-col>
77
+ </v-row>
78
+ </v-col>
79
+
80
+ <template v-if="shouldShowField('company')">
81
+ <v-col cols="12">
82
+ <InputLabel class="text-capitalize" title="Company Name" />
83
+ <v-combobox v-model="visitor.company" v-model:search="companyNameInput" ref="companyCombo"
84
+ autocomplete="off" :hide-no-data="false" :items="companyNames" item-value="value"
85
+ :loading="fetchCompanyListPending" @update:model-value="handleSelectCompanyName" variant="outlined"
86
+ density="comfortable" persistent-hint small-chips>
87
+ <template v-slot:no-data>
88
+ <v-list-item>
89
+ <v-list-item-title v-if="fetchCompanyListPending">
90
+ <v-progress-circular indeterminate size="20" class="mr-3" />
91
+ Searching companies…
92
+ </v-list-item-title>
93
+ <v-list-item-title v-else-if="companyNameInput" @click.stop="handleAddNewCompany"
94
+ class="d-flex align-center ga-1">
95
+ <span><v-icon icon="mdi-plus" /></span>Add "<strong>{{ companyNameInput }}</strong>" as new
96
+ company.
97
+ </v-list-item-title>
98
+ <v-list-item-title v-else-if="!companyNameInput && companyNames.length === 0">
99
+ Start typing to search for companies.
100
+ </v-list-item-title>
101
+ <v-list-item-title v-else>
102
+ No companies available. Start typing to add a new one.
103
+ </v-list-item-title>
104
+ </v-list-item>
105
+ </template>
106
+ </v-combobox>
107
+ </v-col>
108
+ </template>
109
+
110
+
111
+ <v-col v-if="shouldShowField('plateNumber')" cols="12">
112
+ <v-row>
113
+ <v-col cols="12">
114
+ <InputLabel class="text-capitalize" title="Vehicle Number" required />
115
+ <!-- <v-text-field v-model.trim.uppercase="visitor.plateNumber" density="comfortable" /> -->
116
+ <InputVehicleNumber v-model="visitor.plateNumber" density="comfortable" />
117
+ </v-col>
118
+ </v-row>
119
+ </v-col>
120
+
121
+ <v-col v-if="shouldShowField('block')" cols="12">
122
+ <InputLabel class="text-capitalize" title="Block" required />
123
+ <v-select v-model="visitor.block" :items="blocksArray" item-value="value" item-title="title"
124
+ :loading="blockStatus === 'pending'" @update:model-value="handleChangeBlock" density="comfortable"
125
+ :rules="[requiredRule]" />
126
+ </v-col>
127
+
128
+ <v-col v-if="shouldShowField('level')" cols="12">
129
+ <InputLabel class="text-capitalize" title="Level" required />
130
+ <v-select v-model="visitor.level" :items="levelsArray" density="comfortable" :disabled="!visitor.block"
131
+ :loading="levelsStatus === 'pending'" @update:model-value="handleChangeLevel" :rules="[requiredRule]" />
132
+ </v-col>
133
+
134
+ <v-col v-if="shouldShowField('unit')" cols="12">
135
+ <InputLabel class="text-capitalize" title="Unit" required />
136
+ <v-select v-model="visitor.unit" :items="unitsArray" density="comfortable" item-title="title"
137
+ item-value="value" :disabled="!visitor.level"
138
+ :loading="unitsStatus === 'pending'" :rules="[requiredRule]" @update:model-value="handleUpdateUnit" />
139
+ </v-col>
140
+
141
+ <v-col v-if="shouldShowField('remarks')" cols="12">
142
+ <InputLabel class="text-capitalize" title="Remarks" />
143
+ <v-textarea v-model="visitor.remarks" density="comfortable" :rows="3" no-resize />
144
+ </v-col>
145
+
146
+ <v-col v-if="prop.type === 'contractor' && contractorStep === 2" cols="12">
147
+ <PassInformation />
148
+ </v-col>
149
+
150
+ <v-col v-if="prop.type === 'contractor' && contractorStep === 3" cols="12">
151
+ <MemberInformation v-model="visitor.members" />
152
+ </v-col>
153
+
154
+ <v-col v-if="prop.mode === 'add'" cols="12" class="mt-2">
155
+ <v-checkbox v-model="createMore" density="comfortable" hide-details>
156
+ <template #label>
157
+ <span class="text-subtitle-2 font-weight-bold">
158
+ Create more
159
+ </span>
160
+ </template>
161
+ </v-checkbox>
162
+ </v-col>
163
+ </v-row>
164
+ </v-form>
165
+ </v-card-text>
166
+
167
+ <v-row no-gutters class="w-100" v-if="errorMessage">
168
+ <p class="text-error w-100 text-center text-subtitle-2">
169
+ {{ errorMessage }}
170
+ </p>
171
+ </v-row>
172
+ <v-toolbar density="compact">
173
+ <v-row no-gutters>
174
+ <v-col cols="6">
175
+ <v-btn v-if="
176
+ prop.mode === 'add' &&
177
+ prop.type === 'contractor' &&
178
+ contractorStep > 1
179
+ " tile block variant="text" class="text-none" size="48" @click="handleGoToPreviousPage" text="Back" />
180
+ <v-btn v-else-if="prop.mode === 'add'" tile block variant="text" class="text-none" size="48"
181
+ @click="backToSelection" text="Back to Selection" />
182
+ <v-btn v-else tile block variant="text" class="text-none" size="48" @click="close" text="Close" />
183
+ </v-col>
184
+ <v-col cols="6">
185
+ <v-btn v-if="prop.type === 'contractor'" tile block variant="flat" color="primary-button" class="text-none"
186
+ size="48" :disabled="processing" @click="handleNextPage" :text="contractorStep === 3 ? 'Submit' : 'Next'" />
187
+ <v-btn v-else tile block variant="flat" color="black" class="text-none" size="48"
188
+ :disabled="!validForm || processing" @click="submit" :text="prop.mode == 'add' ? 'Submit' : 'Update'" />
189
+ </v-col>
190
+ </v-row>
191
+ </v-toolbar>
192
+ </v-card>
193
+ </template>
194
+
195
+ <script setup lang="ts">
196
+
197
+ const prop = defineProps({
198
+ type: {
199
+ type: String as PropType<TVisitorType>,
200
+ required: true,
201
+ },
202
+ org: {
203
+ type: String,
204
+ required: true,
205
+ },
206
+ site: {
207
+ type: String,
208
+ required: true,
209
+ },
210
+ mode: {
211
+ type: String as PropType<"add" | "edit">,
212
+ default: "add",
213
+ },
214
+ visitorData: {
215
+ type: Object as PropType<Partial<TVisitor> | null>,
216
+ default: null,
217
+ },
218
+ });
219
+
220
+ const { requiredRule, debounce } = useUtils();
221
+ const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
222
+ const { createVisitor, typeFieldMap, contractorTypes } = useVisitor();
223
+ const { findPersonByNRIC, findPersonByContact, searchCompanyList } = usePeople()
224
+
225
+ const emit = defineEmits([
226
+ "back",
227
+ "select",
228
+ "done",
229
+ "done:more",
230
+ "error",
231
+ "close",
232
+ ]);
233
+
234
+ const visitor = reactive<Partial<TVisitorPayload>>({
235
+ contractorType: "",
236
+ name: "",
237
+ deliveryType: "",
238
+ company: "",
239
+ nric: "",
240
+ contact: "",
241
+ plateNumber: "",
242
+ block: "",
243
+ level: "",
244
+ unit: "",
245
+ unitName: "",
246
+ remarks: "",
247
+ attachments: [],
248
+ members: [],
249
+ });
250
+
251
+ const validForm = ref(false);
252
+ const formRef = ref<HTMLFormElement | null>(null);
253
+ const processing = ref(false);
254
+ const message = ref("");
255
+ const errorMessage = ref("");
256
+ const createMore = ref(false);
257
+
258
+ const contractorTypeInput = ref("");
259
+ const contractorTypeObj = ref<TDefaultOptionObj | null>(null);
260
+ const contractorStep = ref(1);
261
+
262
+ const companyNameInput = ref("");
263
+ const companyNameObj = ref<string | null>(null);
264
+
265
+
266
+ const blocksArray = ref<TDefaultOptionObj[]>([]);
267
+ const levelsArray = ref<TDefaultOptionObj[]>([]);
268
+ const unitsArray = ref<TDefaultOptionObj[]>([]);
269
+
270
+ const shouldShowField = (fieldKey: keyof TVisitorPayload) => {
271
+ if (prop.type !== "contractor" || contractorStep.value === 1) {
272
+ const visibleFields = typeFieldMap[prop.type];
273
+ return visibleFields?.includes(fieldKey);
274
+ }
275
+ };
276
+
277
+ const companyNames = ref<string[]>([])
278
+ const companyAutofillDataArray = ref<{ companyName: string[], unit: string, block: number, level: string, unitName: string }[]>([])
279
+
280
+ const formTitle = computed(() => {
281
+ const isContractorForm = prop.type === "contractor";
282
+ const step = contractorStep.value;
283
+ if (isContractorForm && step === 2) {
284
+ return "Pass & Keys Information";
285
+ } else if (isContractorForm && step === 3) {
286
+ return "Members Information";
287
+ } else return "General Information";
288
+ });
289
+
290
+ function handleSelectContractorType(obj: { title: string; value: string }) {
291
+ visitor.contractorType = obj.value ?? "";
292
+ }
293
+
294
+ function handleFocusedContractorType() {
295
+ // const obj = contractorTypeObj.value;
296
+ // const matched = contractorTypes.some(
297
+ // (x) => x.value === obj?.value && x.title === obj?.title
298
+ // );
299
+ // if (!matched) {
300
+ // contractorTypeObj.value = null;
301
+ // contractorTypeInput.value = ""
302
+ // visitor.contractorType = "";
303
+ // }
304
+ }
305
+
306
+ const contractorTypeCombo = ref(null)
307
+
308
+ async function handleAddNewContractorType() {
309
+ visitor.contractorType = contractorTypeInput.value
310
+
311
+ const combo = contractorTypeCombo.value as any
312
+ await nextTick();
313
+
314
+ combo.isMenuActive = false;
315
+ combo.$el.querySelector("input")?.blur(); ``
316
+
317
+ }
318
+
319
+ async function handleSelectCompanyName(company: string) {
320
+ visitor.company = company
321
+
322
+ const selected = companyAutofillDataArray.value.find(x => x.companyName?.includes(company))
323
+ if (!selected) return
324
+
325
+ visitor.block = selected.block || ""
326
+ visitor.level = selected.level || ""
327
+ visitor.unit = selected.unit || ""
328
+ visitor.unitName = selected.unitName || ""
329
+
330
+ }
331
+
332
+ const companyCombo = ref(null)
333
+
334
+ async function handleAddNewCompany() {
335
+ visitor.company = companyNameInput.value
336
+
337
+ const combo = companyCombo.value as any
338
+ await nextTick();
339
+
340
+ combo.isMenuActive = false;
341
+ combo.$el.querySelector("input")?.blur();
342
+ }
343
+
344
+
345
+ const {
346
+ data: fetchPersonByNRICReq,
347
+ refresh: fetchPersonByNRICRefresh,
348
+ pending: fetchPersonByNRICPending,
349
+ } = useLazyAsyncData(`fetch-person`, () => {
350
+ const NRIC = visitor.nric;
351
+ if (!NRIC) return Promise.resolve(null)
352
+ return findPersonByNRIC(NRIC)
353
+ }
354
+ );
355
+
356
+ watch(fetchPersonByNRICReq, (obj) => {
357
+ if (obj) {
358
+ companyNames.value = obj.companyName ?? []
359
+ visitor.name = obj.name
360
+ visitor.contact = obj.contact
361
+ if (!visitor.company) {
362
+ visitor.company = companyNames.value?.[0]
363
+ }
364
+ visitor.plateNumber = obj.plateNumber ?? ""
365
+ visitor.block = obj.block ?? ""
366
+ visitor.level = obj.level ?? ""
367
+ visitor.unit = obj.unit ?? ""
368
+ }
369
+ })
370
+
371
+ const {
372
+ data: fetchPersonByContactReq,
373
+ refresh: fetchPersonByContactRefresh,
374
+ pending: fetchPersonByContactPending,
375
+ } = useLazyAsyncData(`fetch-contact`, () => {
376
+ const contact = visitor.contact;
377
+ if (!contact) return Promise.resolve(null)
378
+ return findPersonByContact(contact)
379
+ }
380
+ );
381
+
382
+ watch(fetchPersonByContactReq, (obj) => {
383
+ if (obj) {
384
+ companyNames.value = obj.companyName ?? []
385
+ visitor.name = obj.name
386
+ if (!visitor.company) {
387
+ visitor.company = companyNames.value?.[0]
388
+ }
389
+ visitor.plateNumber = obj.plateNumber ?? ""
390
+ visitor.block = obj.block ?? ""
391
+ visitor.level = obj.level ?? ""
392
+ visitor.unit = obj.unit ?? ""
393
+ visitor.nric = obj.nric ?? ""
394
+ }
395
+ })
396
+
397
+ const {
398
+ data: fetchCompanyListReq,
399
+ refresh: fetchCompanyListRefresh,
400
+ pending: fetchCompanyListPending,
401
+
402
+ } = useLazyAsyncData(`fetch-company-list`, () => {
403
+ if (!companyNameInput.value) return Promise.resolve(null)
404
+ return searchCompanyList(companyNameInput.value)
405
+ })
406
+
407
+ watch(fetchCompanyListReq, (arr) => {
408
+ if (Array.isArray(arr)) {
409
+ companyAutofillDataArray.value = arr;
410
+ companyNames.value = arr?.flatMap(x => x?.companyName)
411
+ }
412
+ })
413
+
414
+ const debounceFetchCompany = debounce(async () => fetchCompanyListRefresh(), 200)
415
+
416
+ watch(companyNameInput, async (val) => {
417
+ if (!val) {
418
+ companyNames.value = []
419
+ return
420
+ }
421
+ await debounceFetchCompany()
422
+ })
423
+
424
+
425
+
426
+ const {
427
+ data: siteData,
428
+ refresh: refreshSiteData,
429
+ status: blockStatus,
430
+ } = useLazyAsyncData(`fetch-site-data-${prop.site}`, () =>
431
+ getSiteById(prop.site)
432
+ );
433
+
434
+ const {
435
+ data: levelsData,
436
+ refresh: refreshLevelsData,
437
+ status: levelsStatus,
438
+ } = useLazyAsyncData(
439
+ `fetch-levels-data-${prop.site}-${visitor.block}`,
440
+ async () => {
441
+ if (!visitor.block) return Promise.resolve(null);
442
+ return await getSiteLevels(prop.site, { block: Number(visitor.block) });
443
+ }, { watch: [() => visitor.block] }
444
+ );
445
+
446
+ const {
447
+ data: unitsData,
448
+ refresh: refreshUnitsData,
449
+ status: unitsStatus,
450
+ } = useLazyAsyncData(
451
+ `fetch-units-data-${prop.site}-${visitor.level}`,
452
+ async () => {
453
+ if (!visitor.level) return Promise.resolve(null);
454
+ return await getSiteUnits(prop.site, Number(visitor.block), visitor.level);
455
+ }, { watch: [() => visitor.level] }
456
+ );
457
+
458
+ watch(
459
+ siteData,
460
+ (newVal) => {
461
+ const siteDataValue = newVal as any;
462
+ if (siteDataValue) {
463
+ const numberOfBlocks = siteDataValue.metadata?.block || 0;
464
+ for (let i = 1; i <= numberOfBlocks; i++) {
465
+ blocksArray.value.push({
466
+ title: `Block ${i}`,
467
+ value: i,
468
+ });
469
+ }
470
+ } else {
471
+ blocksArray.value = [];
472
+ }
473
+ },
474
+ { immediate: true }
475
+ );
476
+
477
+ watch(
478
+ levelsData,
479
+ (newVal: any) => {
480
+ if (newVal) {
481
+ const arr = newVal.levels || [];
482
+ levelsArray.value = arr?.map((level: any) => ({
483
+ title: level,
484
+ value: level,
485
+ }));
486
+ } else {
487
+ levelsArray.value = [];
488
+ }
489
+ },
490
+ { immediate: true }
491
+ );
492
+
493
+ watch(
494
+ unitsData,
495
+ (newVal: any) => {
496
+ if (!newVal) {
497
+ unitsArray.value = []
498
+ return
499
+ }
500
+
501
+ const mapped = newVal.map((unit: any) => ({
502
+ title: unit.name,
503
+ value: unit._id,
504
+ }))
505
+
506
+ // keep custom unit pushed from autofill
507
+ const selected = visitor.unit
508
+ const exists = mapped.some((u: TDefaultOptionObj) => u.value === selected)
509
+
510
+ if (!exists && visitor.unitName) {
511
+ mapped.push({
512
+ title: visitor.unitName,
513
+ value: selected,
514
+ })
515
+ }
516
+
517
+ unitsArray.value = mapped
518
+ },
519
+ { immediate: true }
520
+ )
521
+
522
+
523
+ function handleChangeBlock(value: any) {
524
+ visitor.level = "";
525
+ visitor.unit = "";
526
+ }
527
+
528
+ function handleChangeLevel(value: any) {
529
+ visitor.unit = "";
530
+ }
531
+
532
+ function handleUpdateUnit(value: any) {
533
+ const selectedUnit = unitsArray.value?.find((x: any) => x.value === value);
534
+ visitor.unitName = selectedUnit?.title || "";
535
+ }
536
+
537
+ const debounceFetchNRIC = debounce(fetchPersonByNRICRefresh, 500)
538
+ const debounceFetchContact = debounce(fetchPersonByContactRefresh, 500)
539
+
540
+ function handleUpdateNRIC() {
541
+ debounceFetchNRIC()
542
+ }
543
+
544
+ function handleUpdateContact() {
545
+ debounceFetchContact()
546
+ }
547
+
548
+ function backToSelection() {
549
+ emit("back");
550
+ message.value = "";
551
+ }
552
+
553
+ function handleGoToPreviousPage() {
554
+ if (contractorStep.value > 1) {
555
+ contractorStep.value--;
556
+ }
557
+ }
558
+
559
+ function close() {
560
+ emit("close");
561
+ message.value = "";
562
+ }
563
+
564
+ function formatVisitorType(type: TVisitorType): string {
565
+ switch (type) {
566
+ case "guest":
567
+ return "Guest";
568
+ case "contractor":
569
+ return "Contractor";
570
+ case "delivery":
571
+ return "Delivery";
572
+ case "walk-in":
573
+ return "Walk-In";
574
+ case "pick-up":
575
+ return "Pick-Up";
576
+ case "drop-off":
577
+ return "Drop-Off";
578
+ default:
579
+ return "";
580
+ }
581
+ }
582
+
583
+ async function handleNextPage() {
584
+ errorMessage.value = "";
585
+ formRef.value!.validate();
586
+ if (!validForm.value) {
587
+ errorMessage.value = "Please complete all required fields *";
588
+ return;
589
+ }
590
+ const step = contractorStep.value;
591
+ if (step < 3) contractorStep.value++;
592
+ else await submit();
593
+ }
594
+
595
+ async function submit() {
596
+ formRef.value!.validate();
597
+ errorMessage.value = "";
598
+ processing.value = true;
599
+
600
+ let payload: Partial<TVisitorPayload> = {
601
+ type: prop.type,
602
+ org: prop.org,
603
+ site: prop.site,
604
+ };
605
+
606
+ if (prop.mode === "add") {
607
+ const allowedFields = typeFieldMap[prop.type];
608
+ for (const key of allowedFields as (keyof TVisitorPayload)[]) {
609
+ if (visitor[key] !== undefined) {
610
+ payload = {
611
+ ...payload,
612
+ [key]: visitor[key],
613
+ };
614
+ }
615
+ }
616
+
617
+ if (prop.type === "contractor") {
618
+ // contractor type logic payload
619
+ payload = {
620
+ ...payload,
621
+ members: visitor.members,
622
+ };
623
+ }
624
+ } else if (prop.mode === "edit") {
625
+ }
626
+ try {
627
+ const res = await createVisitor(payload);
628
+ if (res) {
629
+ if (createMore.value) {
630
+ emit("done:more");
631
+ } else emit("done");
632
+ }
633
+ } catch (error: any) {
634
+ const err = error?.data?.message;
635
+ errorMessage.value =
636
+ err ||
637
+ `Failed to ${prop.mode === "add" ? "add" : "update"
638
+ } visitor. Please try again.`;
639
+ } finally {
640
+ processing.value = false;
641
+ }
642
+ }
643
+
644
+ watch(
645
+ () => visitor.plateNumber,
646
+ (newVal) => {
647
+ visitor.plateNumber = newVal?.toUpperCase();
648
+ }
649
+ );
650
+
651
+ onMounted(() => {
652
+ contractorStep.value = 1;
653
+ });
654
+ </script>
655
+ <style scoped>
656
+ .button-outline-class {
657
+ border: 1px solid rgba(var(--v-theme-primary));
658
+ }
659
+ </style>