@7365admin1/layer-common 3.0.18 → 3.0.20

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.
@@ -1,164 +1,234 @@
1
1
  <template>
2
- <v-row no-gutters class="w-100">
3
- <v-card class="w-100">
4
- <v-card-text>
5
- <v-btn block color="primary-button" :height="40" text="Scan QR Code" class="text-capitalize"
6
- prepend-icon="mdi-qrcode" @click="handleScanQRPass" />
7
- <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorScanPassMessage = ''">
8
- <v-row no-gutters class="pt-5 ga-2">
9
- <v-col cols="12">
10
- <InputLabel class="text-capitalize" title="Full Name" required />
11
- <v-text-field v-model.trim="memberForm.name" density="compact" hide-details
12
- :rules="[requiredRule]" />
13
- </v-col>
14
-
15
- <v-col cols="12">
16
- <InputLabel class="text-capitalize" title="NRIC" required />
17
- <InputNRICNumber v-model.trim="memberForm.nric" density="compact" hide-details
18
- :rules="[requiredRule]" />
19
- </v-col>
20
-
21
- <v-col cols="12">
22
- <v-autocomplete v-model="selectedPass" v-model:search="passInput" :hide-no-data="false"
23
- class="mt-3" :items="passItemsFilteredFinal" item-title="prefixAndName" item-value="_id"
24
- label="Pass (optional)" variant="outlined" density="compact" :error-messages="errorScanPassMessage"
25
- persistent-hint small-chips @click="fetchPasses">
26
- <template v-slot:no-data>
27
- <v-list-item density="compact">
28
- <v-list-item-title v-if="passInput">
29
- No results matching "<strong>{{ passInput }}</strong>". This value will be
30
- added as new
31
- option.
32
- </v-list-item-title>
33
- <v-list-item v-else>No data available</v-list-item>
34
- </v-list-item>
35
- </template>
36
-
37
- <template v-slot:chip="{ props, item }">
38
- <v-chip v-if="selectedPass" v-bind="props" prepend-icon="mdi-card-bulleted-outline"
39
- :text="item.raw?.prefixAndName"></v-chip>
40
- </template>
41
- </v-autocomplete>
42
- </v-col>
43
-
44
- <v-col cols="12">
45
- <InputLabel class="text-capitalize" title="Phone Number" required />
46
- <InputPhoneNumberV2 v-model="memberForm.contact" density="compact" default-country="SG"
47
- hide-details />
48
- </v-col>
49
-
50
- <v-row class="pt-3" justify="space-between">
51
- <v-col cols="6">
52
- <v-btn text="Add Member" :disabled="!validForm" prepend-icon="mdi-plus"
53
- color="primary-button" class="text-capitalize" @click="handleAddMember" />
54
- </v-col>
55
- <v-col cols="6" align="end">
56
- <v-btn text="Clear Form" prepend-icon="mdi-refresh" color="primary-button"
57
- class="text-capitalize" @click.stop="handleClearForm" />
58
- </v-col>
59
- </v-row>
60
- </v-row>
61
- </v-form>
62
- </v-card-text>
63
- </v-card>
64
- <visitor-pass-key-q-r-scanner :dialog="dialog.scanQRCode" v-model:scannedValue="scannedPassValue"
65
- @close-dialog="dialog.scanQRCode = false" />
66
- <v-divider class="my-3" />
67
- <v-row v-if="committedMembers.length > 0" no-gutters class="w-100 mt-5 ga-3">
68
- <template v-for="member, index in committedMembers" :key="member.nric">
69
- <CardMemberInfoSummary :member="membersDisplayed[index]" @remove="handleRemoveMember(index)" />
70
- </template>
71
- </v-row>
72
- <!-- <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" /> -->
2
+ <v-row no-gutters class="w-100">
3
+ <v-card class="w-100">
4
+ <v-card-text>
5
+ <v-btn
6
+ block
7
+ color="primary-button"
8
+ :height="40"
9
+ text="Scan QR Code"
10
+ class="text-capitalize"
11
+ prepend-icon="mdi-qrcode"
12
+ @click="handleScanQRPass"
13
+ />
14
+ <v-form
15
+ ref="formRef"
16
+ v-model="validForm"
17
+ :disabled="processing"
18
+ @click="errorScanPassMessage = ''"
19
+ >
20
+ <v-row no-gutters class="pt-5 ga-2">
21
+ <v-col cols="12">
22
+ <InputLabel class="text-capitalize" title="Full Name" required />
23
+ <v-text-field
24
+ v-model.trim="memberForm.name"
25
+ density="compact"
26
+ hide-details
27
+ :rules="[requiredRule]"
28
+ />
29
+ </v-col>
30
+
31
+ <v-col cols="12">
32
+ <InputLabel class="text-capitalize" title="NRIC" required />
33
+ <InputNRICNumber
34
+ v-model.trim="memberForm.nric"
35
+ density="compact"
36
+ hide-details
37
+ :rules="[requiredRule]"
38
+ />
39
+ </v-col>
40
+
41
+ <v-col cols="12">
42
+ <v-autocomplete
43
+ v-model="selectedPass"
44
+ v-model:search="passInput"
45
+ :hide-no-data="false"
46
+ class="mt-3"
47
+ :items="passItemsFilteredFinal"
48
+ item-title="prefixAndName"
49
+ item-value="_id"
50
+ label="Pass (optional)"
51
+ variant="outlined"
52
+ density="compact"
53
+ :error-messages="errorScanPassMessage"
54
+ persistent-hint
55
+ small-chips
56
+ :clearable="clearable"
57
+ @click="fetchPasses"
58
+ >
59
+ <template v-slot:no-data>
60
+ <v-list-item density="compact">
61
+ <v-list-item-title v-if="passInput">
62
+ No results matching "<strong>{{ passInput }}</strong
63
+ >". This value will be added as new option.
64
+ </v-list-item-title>
65
+ <v-list-item v-else>No data available</v-list-item>
66
+ </v-list-item>
67
+ </template>
68
+
69
+ <template v-slot:chip="{ props, item }">
70
+ <v-chip
71
+ v-if="selectedPass"
72
+ v-bind="props"
73
+ prepend-icon="mdi-card-bulleted-outline"
74
+ :text="item.raw?.prefixAndName"
75
+ ></v-chip>
76
+ </template>
77
+ </v-autocomplete>
78
+ </v-col>
79
+
80
+ <v-col cols="12">
81
+ <InputLabel
82
+ class="text-capitalize"
83
+ title="Phone Number"
84
+ required
85
+ />
86
+ <InputPhoneNumberV2
87
+ v-model="memberForm.contact"
88
+ density="compact"
89
+ default-country="SG"
90
+ hide-details
91
+ />
92
+ </v-col>
93
+
94
+ <v-row class="pt-3" justify="space-between">
95
+ <v-col cols="6">
96
+ <v-btn
97
+ text="Add Member"
98
+ :disabled="!validForm"
99
+ prepend-icon="mdi-plus"
100
+ color="primary-button"
101
+ class="text-capitalize"
102
+ @click="handleAddMember"
103
+ />
104
+ </v-col>
105
+ <v-col cols="6" align="end">
106
+ <v-btn
107
+ text="Clear Form"
108
+ prepend-icon="mdi-refresh"
109
+ color="primary-button"
110
+ class="text-capitalize"
111
+ @click.stop="handleClearForm"
112
+ />
113
+ </v-col>
114
+ </v-row>
115
+ </v-row>
116
+ </v-form>
117
+ </v-card-text>
118
+ </v-card>
119
+ <visitor-pass-key-q-r-scanner
120
+ :dialog="dialog.scanQRCode"
121
+ v-model:scannedValue="scannedPassValue"
122
+ @close-dialog="dialog.scanQRCode = false"
123
+ />
124
+ <v-divider class="my-3" />
125
+ <v-row
126
+ v-if="committedMembers.length > 0"
127
+ no-gutters
128
+ class="w-100 mt-5 ga-3"
129
+ >
130
+ <template v-for="(member, index) in committedMembers" :key="member.nric">
131
+ <CardMemberInfoSummary
132
+ :member="membersDisplayed[index]"
133
+ @remove="handleRemoveMember(index)"
134
+ />
135
+ </template>
73
136
  </v-row>
137
+ <!-- <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" /> -->
138
+ </v-row>
74
139
  </template>
75
140
 
76
141
  <script setup lang="ts">
77
- import type { PropType } from 'vue'
78
- import usePassKey from '../composables/usePassKey'
79
- import useUtils from '../composables/useUtils'
80
-
142
+ import type { PropType } from "vue";
143
+ import usePassKey from "../composables/usePassKey";
144
+ import useUtils from "../composables/useUtils";
81
145
 
82
146
  const props = defineProps({
83
- type: {
84
- type: String,
85
- required: true
86
- },
87
- contractorType: {
88
- type: String,
89
- required: false
90
- },
91
- site: {
92
- type: String,
93
- required: true
94
- },
95
- selectedVisitorPass: {
96
- type: Array as PropType<TPassKeyPayload[]>,
97
- default: () => []
98
- }
99
- })
100
-
101
- const { requiredRule } = useUtils()
102
- const { getPassKeysByPageSearch } = usePassKey()
103
-
147
+ type: {
148
+ type: String,
149
+ required: true,
150
+ },
151
+ contractorType: {
152
+ type: String,
153
+ required: false,
154
+ },
155
+ site: {
156
+ type: String,
157
+ required: true,
158
+ },
159
+ selectedVisitorPass: {
160
+ type: Array as PropType<TPassKeyPayload[]>,
161
+ default: () => [],
162
+ },
163
+ clearable: {
164
+ type: Boolean,
165
+ default: false,
166
+ },
167
+ });
168
+
169
+ const { requiredRule } = useUtils();
170
+ const { getPassKeysByPageSearch } = usePassKey();
104
171
 
105
172
  const memberForm = reactive<TMemberInfo>({
106
- name: "",
107
- nric: "",
108
- visitorPass: [] as { keyId: string }[],
109
- contact: ""
110
- })
111
-
112
- const validForm = ref(false)
113
- const formRef = ref<HTMLFormElement>()
173
+ name: "",
174
+ nric: "",
175
+ visitorPass: [] as { keyId: string }[],
176
+ contact: "",
177
+ });
178
+
179
+ const validForm = ref(false);
180
+ const formRef = ref<HTMLFormElement>();
114
181
  const processing = ref(false);
115
- const errorScanPassMessage = ref('')
182
+ const errorScanPassMessage = ref("");
116
183
 
117
184
  const message = ref("");
118
185
  const messageColor = ref("");
119
186
  const messageSnackbar = ref(false);
120
187
 
121
188
  function showMessage(msg: string, color: string) {
122
- message.value = msg;
123
- messageColor.value = color;
124
- messageSnackbar.value = true;
189
+ message.value = msg;
190
+ messageColor.value = color;
191
+ messageSnackbar.value = true;
125
192
  }
126
193
 
127
194
  // pass
128
- const passInput = ref('')
129
- const passItems = ref<TPassKey[]>([])
130
- const scannedPassValue = ref<string>("")
195
+ const passInput = ref("");
196
+ const passItems = ref<TPassKey[]>([]);
197
+ const scannedPassValue = ref<string>("");
131
198
 
132
199
  const dialog = reactive({
133
- scanQRCode: false,
134
- })
200
+ scanQRCode: false,
201
+ });
135
202
 
136
- const selectedPass = ref<string>('')
203
+ const selectedPass = ref<string>("");
137
204
 
138
- const members = defineModel<TMemberInfo[]>({ required: true, default: [] })
139
- const committedMembers = ref<TMemberInfo[]>([])
205
+ const members = defineModel<TMemberInfo[]>({ required: true, default: [] });
206
+ const committedMembers = ref<TMemberInfo[]>([]);
140
207
 
141
208
  // Always keep the model in sync: committed members + current form draft
142
- watch(() => memberForm, (newForm) => {
209
+ watch(
210
+ () => memberForm,
211
+ (newForm) => {
143
212
  if (memberForm.name && memberForm.nric && memberForm.contact) {
144
- members.value = [...committedMembers.value, { ...newForm }]
213
+ members.value = [...committedMembers.value, { ...newForm }];
145
214
  } else {
146
- members.value = [...committedMembers.value]
215
+ members.value = [...committedMembers.value];
147
216
  }
148
- }, { deep: true })
149
-
217
+ },
218
+ { deep: true }
219
+ );
150
220
 
151
221
  const passTypesComputed = computed(() => {
152
- if (props.type === 'contractor') {
153
- if (props.contractorType === 'property-agent') {
154
- return ["agent-pass"]
155
- } else {
156
- return ["contractor-pass"]
157
- }
222
+ if (props.type === "contractor") {
223
+ if (props.contractorType === "property-agent") {
224
+ return ["agent-pass"];
158
225
  } else {
159
- return ["visitor-pass"]
226
+ return ["contractor-pass"];
160
227
  }
161
- })
228
+ } else {
229
+ return ["visitor-pass"];
230
+ }
231
+ });
162
232
 
163
233
  const isPassListLoading = ref(false);
164
234
 
@@ -184,60 +254,67 @@ async function fetchPasses() {
184
254
  }
185
255
 
186
256
  const passItemsFilteredFinal = computed(() => {
187
- return passItems.value.filter((item: TPassKey) => {
188
- return !props.selectedVisitorPass.some((pass: TPassKeyPayload) => pass.keyId === item._id) && !committedMembers.value.some((member: TMemberInfo) => member.visitorPass?.some((pass) => pass.keyId === item._id))
189
- })
190
- })
257
+ return passItems.value.filter((item: TPassKey) => {
258
+ return (
259
+ !props.selectedVisitorPass.some(
260
+ (pass: TPassKeyPayload) => pass.keyId === item._id
261
+ ) &&
262
+ !committedMembers.value.some((member: TMemberInfo) =>
263
+ member.visitorPass?.some((pass) => pass.keyId === item._id)
264
+ )
265
+ );
266
+ });
267
+ });
191
268
 
192
269
  const membersDisplayed = computed(() => {
193
- return committedMembers.value.map((member: TMemberInfo) => {
194
- return {
195
- ...member,
196
- visitorPass: passItems.value.find((item: TPassKey) => item._id === member.visitorPass?.[0]?.keyId)?.prefixAndName || ""
197
- }
198
- })
199
- })
270
+ return committedMembers.value.map((member: TMemberInfo) => {
271
+ return {
272
+ ...member,
273
+ visitorPass:
274
+ passItems.value.find(
275
+ (item: TPassKey) => item._id === member.visitorPass?.[0]?.keyId
276
+ )?.prefixAndName || "",
277
+ };
278
+ });
279
+ });
200
280
 
201
281
  function handleScanQRPass() {
202
- errorScanPassMessage.value = ""
203
- dialog.scanQRCode = true
282
+ errorScanPassMessage.value = "";
283
+ dialog.scanQRCode = true;
204
284
  }
205
285
 
206
286
  watch(selectedPass, (newVal) => {
207
- memberForm.visitorPass = [...(newVal ? [{ keyId: newVal }] : [])]
208
- })
287
+ memberForm.visitorPass = [...(newVal ? [{ keyId: newVal }] : [])];
288
+ });
209
289
 
210
290
  watch(scannedPassValue, (newVal) => {
211
- if (!newVal) return
291
+ if (!newVal) return;
212
292
 
213
- const matchedPass = passItemsFilteredFinal.value.find((p) => p.prefixAndName === newVal)
214
-
215
- if (matchedPass) {
216
- selectedPass.value = matchedPass._id ?? ""
217
- return
218
- }
219
-
220
- errorScanPassMessage.value = "Scanned pass not available"
221
- showMessage("Scanned pass not available", "error")
222
- })
293
+ const matchedPass = passItemsFilteredFinal.value.find(
294
+ (p) => p.prefixAndName === newVal
295
+ );
223
296
 
297
+ if (matchedPass) {
298
+ selectedPass.value = matchedPass._id ?? "";
299
+ return;
300
+ }
224
301
 
302
+ errorScanPassMessage.value = "Scanned pass not available";
303
+ showMessage("Scanned pass not available", "error");
304
+ });
225
305
 
226
306
  function handleClearForm() {
227
- formRef.value?.reset()
307
+ formRef.value?.reset();
228
308
  }
229
309
 
230
310
  async function handleAddMember() {
231
- committedMembers.value.push({ ...memberForm })
232
- handleClearForm()
311
+ committedMembers.value.push({ ...memberForm });
312
+ handleClearForm();
233
313
  }
234
314
 
235
315
  function handleRemoveMember(index: number) {
236
- committedMembers.value = committedMembers.value.filter((_, i) => i !== index)
316
+ committedMembers.value = committedMembers.value.filter((_, i) => i !== index);
237
317
  }
238
-
239
-
240
-
241
318
  </script>
242
319
 
243
- <style scoped></style>
320
+ <style scoped></style>
@@ -57,6 +57,7 @@
57
57
  density="compact"
58
58
  persistent-hint
59
59
  small-chips
60
+ closable-chips
60
61
  :clearable="props.clearable"
61
62
  @click="fetchKeys"
62
63
  :loading="isKeyListLoading"
@@ -182,7 +183,7 @@ const isPassListLoading = ref(false);
182
183
 
183
184
  async function fetchPasses() {
184
185
  // load pass list
185
- passItems.value = [];
186
+
186
187
  isPassListLoading.value = true;
187
188
  try {
188
189
  const result = await getPassKeysByPageSearch({
@@ -204,7 +205,6 @@ async function fetchPasses() {
204
205
  const isKeyListLoading = ref(false);
205
206
 
206
207
  async function fetchKeys() {
207
- keyItems.value = [];
208
208
  isKeyListLoading.value = true;
209
209
  try {
210
210
  const keys: any = await getPassKeysByPageSearch({
@@ -215,19 +215,21 @@ async function fetchKeys() {
215
215
  statuses: ["Available"],
216
216
  sortBy: { column: "prefixAndName", order: "1" },
217
217
  });
218
- const selectedKeys = keyItems.value.filter((item: any) => {
218
+
219
+ const previouslySelectedKeys = keyItems.value.filter((item: any) => {
219
220
  if (Array.isArray(props?.passKeys) && props.passKeys.length) {
220
221
  return props.passKeys.includes(item._id);
221
222
  }
222
223
  return false;
223
224
  });
225
+
224
226
  const filteredArray = keys.items.filter((item: any) => {
225
227
  if (Array.isArray(props?.passKeys) && props.passKeys.length) {
226
228
  return !props.passKeys.includes(item._id);
227
229
  }
228
230
  return true;
229
231
  });
230
- keyItems.value = [...selectedKeys, ...filteredArray];
232
+ keyItems.value = [...previouslySelectedKeys, ...filteredArray];
231
233
  } catch (error) {
232
234
  showMessage(errorConverter(error as string), "error");
233
235
  } finally {
@@ -732,6 +732,7 @@ const {
732
732
  add: addServiceProvider,
733
733
  createServiceProviderInvite,
734
734
  updateStatus: updateServiceProviderStatus,
735
+ invite
735
736
  } = useServiceProvider();
736
737
  const { getAll: getAllMembers } = useMember();
737
738
 
@@ -759,9 +760,10 @@ const createInviteDialog = ref(false);
759
760
  // const serviceProviderInviteRoles = ref<Array<Record<string, any>>>([]);
760
761
 
761
762
  const { getByUserType } = useMember();
763
+ const { getOrgs } = useOrg();
762
764
 
763
765
  async function checkExistingAccount(email: string) {
764
- // existingAccount.value = null;
766
+ existingAccount.value = null;
765
767
 
766
768
  if (!email.trim() || emailRule(email) !== true) return;
767
769
 
@@ -772,19 +774,23 @@ async function checkExistingAccount(email: string) {
772
774
 
773
775
  if (!user?._id) return;
774
776
 
775
- const member = await getByUserType(user._id, "organization");
776
- console.log("member raw:", member);
777
- console.log("onboardingCompleted:", member?.onboardingCompleted);
777
+ const orgs = await getOrgs({
778
+ user: user._id,
779
+ });
780
+
781
+ const matchedOrg = orgs.items?.find(
782
+ (org: any) => org.type === serviceProviderInvite.value.app
783
+ );
784
+
785
+ if (matchedOrg) {
786
+ existingAccount.value = {
787
+ _id: matchedOrg.value,
788
+ text: matchedOrg.text,
789
+ ...matchedOrg,
790
+ };
791
+ }
778
792
 
779
- if (member?.onboardingCompleted) {
780
- existingAccount.value = {
781
- _id: member.org,
782
- text: member.orgName,
783
- ...member,
784
- };
785
- }
786
793
  } catch (error) {
787
- console.error(error);
788
794
  existingAccount.value = null;
789
795
  } finally {
790
796
  checkingExistingAccount.value = false;
@@ -799,6 +805,14 @@ watch(
799
805
  await checkExistingAccount(value);
800
806
  }
801
807
  );
808
+ watch(
809
+ () => serviceProviderInvite.value.app,
810
+ () => {
811
+ if (serviceProviderInvite.value.email) {
812
+ checkExistingAccount(serviceProviderInvite.value.email);
813
+ }
814
+ }
815
+ );
802
816
 
803
817
  const serviceProviderInviteApps = computed(() =>
804
818
  natureOfBusinessV3.map((item) => ({
@@ -1183,33 +1197,42 @@ async function submitServiceProviderAdd() {
1183
1197
  disableServiceProvider.value = false;
1184
1198
  }
1185
1199
  }
1186
-
1187
1200
  async function submitServiceProviderInvite() {
1188
1201
  disableServiceProvider.value = true;
1189
1202
  messageServiceProvider.value = "";
1203
+
1190
1204
  try {
1191
- const payload = {
1205
+ await invite({
1192
1206
  email: serviceProviderInvite.value.email.trim(),
1193
- name: "",
1194
- app: serviceProviderInvite.value.app,
1195
1207
  orgId: props.orgId,
1196
1208
  siteId: serviceProviderInvite.value.siteId,
1197
1209
  siteName:
1198
- serviceProviderInvite.value.siteName || site.value?.name || props.siteName,
1199
- };
1210
+ serviceProviderInvite.value.siteName ||
1211
+ site.value?.name ||
1212
+ props.siteName,
1213
+
1214
+ inviteType: existingAccount.value
1215
+ ? "create-org"
1216
+ : "organization-invite",
1217
+ });
1200
1218
 
1201
- await createServiceProviderInvite(payload);
1202
1219
  showMessage("Invitation sent.", "success");
1220
+
1203
1221
  if (createMoreServiceProvider.value) {
1204
1222
  serviceProviderInvite.value.email = "";
1205
1223
  } else {
1206
- await setServiceProvider({ mode: "invite", dialog: false });
1224
+ await setServiceProvider({
1225
+ mode: "invite",
1226
+ dialog: false,
1227
+ });
1207
1228
  }
1229
+
1208
1230
  await loadList();
1209
1231
  } catch (error: any) {
1210
1232
  messageServiceProvider.value =
1211
1233
  error?.response?._data?.message ??
1212
1234
  "An error occurred while inviting the service provider.";
1235
+
1213
1236
  showMessage(errorConverter(error), "error");
1214
1237
  } finally {
1215
1238
  disableServiceProvider.value = false;