@7365admin1/layer-common 4.90.2 → 4.90.3

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.90.3
4
+
5
+ ### Patch Changes
6
+
7
+ - d7c20a9: Visitor form: required and optional fields now match the security mobile app, and every field says "(Required)" or "(Optional)". Phone is optional for Drive-in and Walk-in; NRIC is optional for Delivery; Vehicle Number is required for Drive-in, Pick-Up and Drop-Off; Company Name is required for Contractor; Delivery Company is required. Walk-in no longer shows Company Name, and only the fields on the form are sent when a visitor is saved (no hidden company on Walk-in, no purpose on Drive-in). Pass, Keys and contractor member fields are labelled too.
8
+
3
9
  ## 4.90.2
4
10
 
5
11
  ### Patch Changes
@@ -2022,11 +2028,11 @@
2022
2028
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
2023
2029
  own rule from two endpoints the console already calls, unprojected:
2024
2030
 
2025
- GET /api/members/user/:user/app/admin the Seven365 staff membership
2026
- GET /api/roles/id/:role that membership's role document
2031
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
2032
+ GET /api/roles/id/:role that membership's role document
2027
2033
 
2028
- owner = member.type === "admin" && role.type === "admin" && role.default === true
2029
- staff = member.type === "admin" && role.type === "admin"
2034
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
2035
+ staff = member.type === "admin" && role.type === "admin"
2030
2036
 
2031
2037
  `role.default` is the marker because it is the only property of a platform
2032
2038
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -10,12 +10,12 @@
10
10
  <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorScanPassMessage = ''">
11
11
  <v-row no-gutters class="pt-5 ga-2">
12
12
  <v-col cols="12">
13
- <InputLabel class="text-capitalize" title="Full Name" required />
13
+ <InputLabel class="text-capitalize" title="Full Name (Required)" required />
14
14
  <v-text-field v-model.trim="memberForm.name" density="compact" hide-details :rules="[requiredRule]" />
15
15
  </v-col>
16
16
 
17
17
  <v-col cols="12">
18
- <InputLabel class="text-capitalize" title="NRIC" required />
18
+ <InputLabel class="text-capitalize" title="NRIC (Required)" required />
19
19
  <v-menu v-model="memberNRICSearchMenu" location="bottom" offset-y :close-on-content-click="false">
20
20
  <template #activator="{ props: menuProps }">
21
21
  <InputNRICNumber v-bind="menuProps" v-model.trim="memberForm.nric" density="compact" hide-details
@@ -34,7 +34,7 @@
34
34
  </v-col>
35
35
 
36
36
  <v-col cols="12">
37
- <InputLabel class="text-capitalize" title="Phone Number" required />
37
+ <InputLabel class="text-capitalize" title="Phone Number (Required)" required />
38
38
  <v-menu v-model="memberPhoneSearchMenu" location="bottom" offset-y :close-on-content-click="false">
39
39
  <template #activator="{ props: menuProps }">
40
40
  <InputPhoneNumberV2 v-bind="menuProps" v-model="memberForm.contact" density="compact"
@@ -54,7 +54,7 @@
54
54
 
55
55
  <v-col cols="12">
56
56
  <v-autocomplete v-model="selectedPass" v-model:search="passInput" :hide-no-data="false" class="mt-3"
57
- :items="passItemsFilteredFinal" item-title="prefixAndName" item-value="_id" label="Pass (optional)"
57
+ :items="passItemsFilteredFinal" item-title="prefixAndName" item-value="_id" label="Pass (Optional)"
58
58
  variant="outlined" density="compact" :error-messages="errorScanPassMessage" persistent-hint small-chips
59
59
  :clearable="clearable" @click="fetchPasses">
60
60
  <template v-slot:no-data>
@@ -19,7 +19,7 @@
19
19
  :rules="props.passRules"
20
20
  item-title="prefixAndName"
21
21
  item-value="_id"
22
- label="Pass"
22
+ label="Pass (Optional)"
23
23
  variant="outlined"
24
24
  hide-details
25
25
  persistent-hint
@@ -48,7 +48,7 @@
48
48
  :rules="props.passRules"
49
49
  item-title="prefixAndName"
50
50
  item-value="_id"
51
- label="Keys"
51
+ label="Keys (Optional)"
52
52
  multiple
53
53
  variant="outlined"
54
54
  hide-details
@@ -16,14 +16,14 @@
16
16
  <v-form ref="formRef" v-model="validForm" :disabled="processing" @click="errorMessage = ''">
17
17
  <v-row no-gutters class="pt-4">
18
18
  <v-col v-if="shouldShowField('attachments')" class="w-100">
19
- <InputLabel class="text-capitalize" title="Attach Image" />
19
+ <InputLabel class="text-capitalize" title="Attach Image (Optional)" />
20
20
  <InputFileV2 v-model="visitor.attachments" multiple :max-length="3" />
21
21
  </v-col>
22
22
 
23
23
  <v-col v-if="shouldShowField('nric')" cols="12">
24
24
  <v-row>
25
25
  <v-col cols="12">
26
- <InputLabel class="text-capitalize" title="NRIC" :required="requireNRIC" />
26
+ <InputLabel class="text-capitalize" :title="requireNRIC ? 'NRIC (Required)' : 'NRIC (Optional)'" :required="requireNRIC" />
27
27
  <v-menu v-model="nricSearchMenu" location="bottom" offset-y :close-on-content-click="false"
28
28
  content-class="visitor-form__menu">
29
29
  <template #activator="{ props }">
@@ -55,11 +55,11 @@
55
55
  <v-col v-if="shouldShowField('contact')" cols="12">
56
56
  <v-row>
57
57
  <v-col cols="12">
58
- <InputLabel class="text-capitalize" title="Phone Number" required />
58
+ <InputLabel class="text-capitalize" :title="requireContact ? 'Phone Number (Required)' : 'Phone Number (Optional)'" :required="requireContact" />
59
59
  <v-menu v-model="phoneSearchMenu" location="bottom" offset-y :close-on-content-click="false"
60
60
  content-class="visitor-form__menu">
61
61
  <template #activator="{ props }">
62
- <InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requiredRule]"
62
+ <InputPhoneNumberV2 v-bind="props" v-model="visitor.contact" :rules="[requireContact ? requiredRule : () => true]"
63
63
  @focus="isPhoneFieldFocused = true" @blur="isPhoneFieldFocused = false" density="comfortable"
64
64
  :key="currentAutofillSource + 'phone-key'" @update:model-value="handleUpdatePhoneNumber" />
65
65
  </template>
@@ -87,12 +87,13 @@
87
87
  <v-col v-if="shouldShowField('plateNumber')" cols="12">
88
88
  <v-row>
89
89
  <v-col cols="12">
90
- <InputLabel class="text-capitalize" title="Vehicle Number" :required="isVehicleRequired" />
90
+ <InputLabel class="text-capitalize" :title="isVehicleRequired ? 'Vehicle Number (Required)' : 'Vehicle Number (Optional)'" :required="isVehicleRequired" />
91
91
  <!-- <v-text-field v-model.trim.uppercase="visitor.plateNumber" density="comfortable" /> -->
92
92
  <v-menu v-model="vehicleSearchMenu" location="bottom" offset-y :close-on-content-click="false"
93
93
  content-class="visitor-form__menu">
94
94
  <template #activator="{ props }">
95
95
  <InputVehicleNumber v-bind="props" v-model="visitor.plateNumber" density="comfortable"
96
+ :rules="[isVehicleRequired ? requiredRule : () => true]"
96
97
  @update:model-value="handleUpdateVehicleNumber" @focus="handleFocusPlateNumber"
97
98
  @blur="isPlateNumberFieldFocused = false" />
98
99
  </template>
@@ -117,7 +118,7 @@
117
118
  </v-col>
118
119
 
119
120
  <v-col v-if="shouldShowField('contractorType')" cols="12">
120
- <InputLabel class="text-capitalize" title="Contractor Type" required />
121
+ <InputLabel class="text-capitalize" title="Contractor Type (Required)" required />
121
122
  <v-combobox v-model="contractorTypeObj" v-model:search="contractorTypeInput" ref="contractorTypeCombo"
122
123
  autocomplete="off" :hide-no-data="false" @update:focused="handleFocusedContractorType"
123
124
  :items="contractorTypes" :rules="[requiredRule]" item-value="value"
@@ -139,7 +140,7 @@
139
140
  <v-col v-if="shouldShowField('name')" cols="12">
140
141
  <v-row>
141
142
  <v-col cols="12">
142
- <InputLabel class="text-capitalize" title="Full Name" required />
143
+ <InputLabel class="text-capitalize" title="Full Name (Required)" required />
143
144
  <v-text-field v-model.trim="visitor.name" density="comfortable" :rules="[requiredRule]" />
144
145
  </v-col>
145
146
  </v-row>
@@ -159,7 +160,7 @@
159
160
  place of `primary`, which is the darker link tone. Without
160
161
  the label this was the only required field in the form with
161
162
  nothing naming it: just a bare Food/Parcel pair. -->
162
- <InputLabel class="text-capitalize" title="Delivery Type" required />
163
+ <InputLabel class="text-capitalize" title="Delivery Type (Required)" required />
163
164
  <v-radio-group v-model="visitor.deliveryType" inline color="accent" :rules="[requiredRule]">
164
165
  <v-radio label="Food" value="Food"></v-radio>
165
166
  <v-radio label="Parcel" value="Parcel"></v-radio>
@@ -170,8 +171,9 @@
170
171
 
171
172
  <template v-if="shouldShowField('company')">
172
173
  <v-col cols="12">
173
- <InputLabel class="text-capitalize" title="Company Name" />
174
+ <InputLabel class="text-capitalize" :title="requireCompany ? 'Company Name (Required)' : 'Company Name (Optional)'" :required="requireCompany" />
174
175
  <v-combobox v-model="visitor.company" v-model:search="companyNameInput" ref="companyCombo"
176
+ :rules="[requireCompany ? requiredRule : () => true]"
175
177
  autocomplete="off" :hide-no-data="true" :items="companyNames" item-value="value"
176
178
  @update:model-value="handleSelectCompanyName" variant="outlined" density="comfortable" persistent-hint
177
179
  small-chips>
@@ -182,8 +184,9 @@
182
184
 
183
185
  <template v-if="shouldShowField('delivery-company')">
184
186
  <v-col cols="12">
185
- <InputLabel class="text-capitalize" title="Delivery Company" />
187
+ <InputLabel class="text-capitalize" title="Delivery Company (Required)" required />
186
188
  <v-combobox v-model="deliveryCompany" v-model:search="deliveryCompanyInput" ref="companyCombo"
189
+ :rules="[requiredRule]"
187
190
  autocomplete="off" :hide-no-data="false" :items="deliveryCompanyList" item-value="value"
188
191
  :loading="siteDataPending" variant="outlined" density="comfortable" persistent-hint small-chips>
189
192
  <template v-slot:no-data>
@@ -205,7 +208,7 @@
205
208
 
206
209
 
207
210
  <v-col v-if="shouldShowField('block')" cols="12">
208
- <InputLabel class="text-capitalize" title="Block" required />
211
+ <InputLabel class="text-capitalize" title="Block (Required)" required />
209
212
  <v-autocomplete v-model="visitor.block" :items="blocksArray" item-value="value" autocomplete="off"
210
213
  item-title="title" :key="blockListDataPending ? 'block-list-pending' : 'block-list-ready'
211
214
  " :loading="blockListDataPending" @update:model-value="handleChangeBlock" density="comfortable"
@@ -213,13 +216,13 @@
213
216
  </v-col>
214
217
 
215
218
  <v-col v-if="shouldShowField('level')" cols="12">
216
- <InputLabel class="text-capitalize" title="Level" required />
219
+ <InputLabel class="text-capitalize" title="Level (Required)" required />
217
220
  <v-autocomplete v-model="visitor.level" :items="levelsArray" density="comfortable" autocomplete="off"
218
221
  :disabled="!visitor.block" :key="levelsDataPending ? 'level-list-pending' : 'level-list-ready'
219
222
  " :loading="levelsDataPending" @update:model-value="handleChangeLevel" :rules="[requiredRule]" />
220
223
  </v-col>
221
224
  <v-col v-if="shouldShowField('unit')" cols="12">
222
- <InputLabel class="text-capitalize" title="Unit" required />
225
+ <InputLabel class="text-capitalize" title="Unit (Required)" required />
223
226
  <v-autocomplete v-model="visitor.unit" :items="unitsArray" density="comfortable" autocomplete="off"
224
227
  item-title="title" item-value="value" :disabled="!visitor.level"
225
228
  :key="unitsDataPending ? 'unit-list-pending' : 'unit-list-ready'" :loading="unitsDataPending"
@@ -227,14 +230,14 @@
227
230
  </v-col>
228
231
 
229
232
  <v-col v-if="shouldShowField('unit')" cols="12">
230
- <InputLabel class="text-capitalize" title="Registered Unit Company Name" required
233
+ <InputLabel class="text-capitalize" title="Registered Unit Company Name"
231
234
  :key="'unit-key' + visitor.level" />
232
235
  <v-text-field v-model.trim="registeredUnitCompanyName" density="comfortable"
233
236
  :loading="buildingUnitDataPending" readonly class="no-pointer" />
234
237
  </v-col>
235
238
 
236
239
  <v-col v-if="shouldShowField('remarks')" cols="12">
237
- <InputLabel class="text-capitalize" title="Remarks" />
240
+ <InputLabel class="text-capitalize" title="Remarks (Optional)" />
238
241
  <v-textarea v-model="visitor.remarks" density="comfortable" :rows="3" no-resize />
239
242
  </v-col>
240
243
 
@@ -831,12 +834,29 @@ const formMode = computed<"add" | "edit" | "register">(() => {
831
834
  return selectedUnregisteredVisitor.value ? "register" : prop.mode;
832
835
  });
833
836
 
837
+ // Fields this form actually has for the visitor type - drives both what is
838
+ // shown and what is sent on submit, so nothing outside the form is saved.
839
+ // typeFieldMap also feeds the details dialog, so it can list more than this:
840
+ // - walk-in "company": not on the security mobile app's walk-in, hidden here
841
+ // but still shown on older walk-in records in the details dialog;
842
+ // - guest "purpose": never drawn in this form.
843
+ const formFieldsExcluded: Partial<Record<TVisitorType, string[]>> = {
844
+ "walk-in": ["company"],
845
+ guest: ["purpose"],
846
+ };
847
+
848
+ const isFormField = (
849
+ fieldKey: keyof TVisitorPayload | "delivery-company"
850
+ ) => {
851
+ if (formFieldsExcluded[prop.type]?.includes(fieldKey)) return false;
852
+ return !!typeFieldMap[prop.type]?.includes(fieldKey);
853
+ };
854
+
834
855
  const shouldShowField = (
835
856
  fieldKey: keyof TVisitorPayload | "delivery-company"
836
857
  ) => {
837
858
  if ((!withStep2.value && !withStep3.value) || contractorStep.value === 1) {
838
- const visibleFields = typeFieldMap[prop.type];
839
- return visibleFields?.includes(fieldKey);
859
+ return isFormField(fieldKey);
840
860
  }
841
861
  };
842
862
 
@@ -866,8 +886,21 @@ const showShowKeysField = computed(() => {
866
886
  return ["guest", "contractor", "walk-in"].includes(prop.type);
867
887
  });
868
888
 
889
+ // Required / optional per type - kept in line with the security mobile app.
869
890
  const requireNRIC = computed(() => {
870
- return prop.type !== "walk-in" && prop.type !== "guest";
891
+ return prop.type === "contractor";
892
+ });
893
+
894
+ const requireContact = computed(() => {
895
+ return ["contractor", "delivery"].includes(prop.type);
896
+ });
897
+
898
+ const isVehicleRequired = computed(() => {
899
+ return ["guest", "pick-up", "drop-off"].includes(prop.type);
900
+ });
901
+
902
+ const requireCompany = computed(() => {
903
+ return prop.type === "contractor";
871
904
  });
872
905
 
873
906
  const hidQrCodePassConfig = computed(() => {
@@ -1942,7 +1975,8 @@ async function submit(options: { skipNonCheckedOutCheck?: boolean } = {}) {
1942
1975
  if (formMode.value === "add" || formMode.value === "register") {
1943
1976
  const allowedFields = typeFieldMap[prop.type];
1944
1977
  for (const key of allowedFields as (keyof TVisitorPayload)[]) {
1945
- if (visitor[key] !== undefined) {
1978
+ // only what the form has (see isFormField)
1979
+ if (isFormField(key) && visitor[key] !== undefined) {
1946
1980
  payload = {
1947
1981
  ...payload,
1948
1982
  [key]: visitor[key],
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "4.90.2",
5
+ "version": "4.90.3",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",