@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 770867c: People mgmt - initial release
8
+
3
9
  ## 1.1.0
4
10
 
5
11
  ### Minor Changes
@@ -0,0 +1,264 @@
1
+ <template>
2
+ <v-card width="100%">
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
+ {{ title }}
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+
11
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5">
12
+ <v-form ref="formRef" v-model="validForm" :disabled="disable">
13
+ <v-row no-gutters class="pt-4">
14
+ <v-col cols="12">
15
+ <InputLabel class="text-capitalize" title="URL" required />
16
+ <v-text-field
17
+ v-model.trim="camera.host"
18
+ density="comfortable"
19
+ :rules="[requiredRule, isValidBaseURL]"
20
+ placeholder="Enter camera URL"
21
+ />
22
+ </v-col>
23
+
24
+ <v-col v-if="isANPR" cols="12">
25
+ <InputLabel class="text-capitalize" title="Category" required />
26
+ <v-select
27
+ v-model="camera.category"
28
+ :items="anprCategoryOptions"
29
+ density="comfortable"
30
+ :rules="[requiredRule]"
31
+ />
32
+ </v-col>
33
+
34
+ <v-col v-if="isANPR" cols="12">
35
+ <InputLabel class="text-capitalize" title="Direction" required />
36
+ <v-select
37
+ v-model="camera.direction"
38
+ :items="anprDirectionOptions"
39
+ density="comfortable"
40
+ :rules="[requiredRule]"
41
+ />
42
+ </v-col>
43
+
44
+ <v-col v-if="isANPR" cols="12">
45
+ <InputLabel class="text-capitalize" title="Guard House" />
46
+ <v-select
47
+ v-model="camera.guardPost"
48
+ :items="guardPostOptions"
49
+ density="comfortable"
50
+ />
51
+ </v-col>
52
+
53
+ <v-col cols="12">
54
+ <InputLabel class="text-capitalize" title="User" required />
55
+ <v-text-field
56
+ v-model.trim="camera.username"
57
+ density="comfortable"
58
+ :rules="[requiredRule]"
59
+ placeholder="Enter username"
60
+ />
61
+ </v-col>
62
+
63
+ <v-col cols="12">
64
+ <InputLabel class="text-capitalize" title="Password" required />
65
+ <v-text-field
66
+ v-model.trim="camera.password"
67
+ type="password"
68
+ density="comfortable"
69
+ :rules="[requiredRule]"
70
+ placeholder="Enter password"
71
+ />
72
+ </v-col>
73
+ </v-row>
74
+ </v-form>
75
+ </v-card-text>
76
+
77
+ <v-toolbar density="compact">
78
+ <v-row no-gutters>
79
+ <v-col cols="6">
80
+ <v-btn
81
+ tile
82
+ block
83
+ variant="text"
84
+ class="text-none"
85
+ size="48"
86
+ @click="back"
87
+ text="Cancel"
88
+ />
89
+ </v-col>
90
+ <v-col cols="6">
91
+ <v-btn
92
+ tile
93
+ block
94
+ variant="flat"
95
+ color="black"
96
+ class="text-none"
97
+ size="48"
98
+ :disabled="!validForm || disable || !hasChanges"
99
+ @click="submit"
100
+ :loading="disable"
101
+ text="Submit"
102
+ />
103
+ </v-col>
104
+ </v-row>
105
+ </v-toolbar>
106
+ </v-card>
107
+ </template>
108
+
109
+ <script setup lang="ts">
110
+ import useSiteSettings from '../composables/useSiteSettings';
111
+
112
+ const prop = defineProps({
113
+ title: {
114
+ type: String,
115
+ default: "Site Camera Form",
116
+ },
117
+ site: {
118
+ type: String,
119
+ required: true,
120
+ },
121
+ guardPosts: {
122
+ type: Number,
123
+ default: 0,
124
+ },
125
+ type: {
126
+ type: String as PropType<TSiteCamera["type"]>,
127
+ default: "anpr",
128
+ },
129
+ mode: {
130
+ type: String,
131
+ default: "add",
132
+ },
133
+ camera: {
134
+ type: Object as PropType<TSiteCamera>,
135
+ default: () => ({
136
+ site: "",
137
+ host: "",
138
+ username: "",
139
+ password: "",
140
+ type: "ip",
141
+ category: "standard",
142
+ direction: "none",
143
+ guardPost: null,
144
+ status: "active",
145
+ }),
146
+ },
147
+ });
148
+
149
+ const { requiredRule, isValidBaseURL } = useUtils();
150
+ const emit = defineEmits(["cancel", "select", "success", "error"]);
151
+
152
+ const camera = ref<TSiteCamera>({
153
+ site: "",
154
+ host: "",
155
+ username: "",
156
+ password: "",
157
+ type: "ip",
158
+ category: "standard",
159
+ direction: "none",
160
+ guardPost: null,
161
+ status: "active",
162
+ });
163
+
164
+ camera.value.site = prop.site;
165
+
166
+ if (prop.mode === "edit") {
167
+ camera.value = JSON.parse(JSON.stringify(prop.camera));
168
+ }
169
+
170
+ camera.value.site = prop.site;
171
+ camera.value.type = prop.type;
172
+ camera.value.category = prop.type === "anpr" ? "resident" : "standard";
173
+ camera.value.direction = prop.type === "anpr" ? "both" : "none";
174
+
175
+ const validForm = ref(false);
176
+ const formRef = ref<HTMLFormElement | null>(null);
177
+ const disable = ref(false);
178
+ const message = ref("");
179
+
180
+ const isANPR = computed(() => prop.type === "anpr");
181
+
182
+ const anprCategoryOptions = computed(() => [
183
+ { title: "Resident", value: "resident" },
184
+ { title: "Visitor", value: "visitor" },
185
+ ]);
186
+
187
+ const anprDirectionOptions = computed(() => [
188
+ { title: "Entry", value: "entry" },
189
+ { title: "Exit", value: "exit" },
190
+ { title: "Both", value: "both" },
191
+ { title: "None", value: "none" },
192
+ ]);
193
+
194
+ function back() {
195
+ message.value = "";
196
+
197
+ if (formRef.value) {
198
+ formRef.value.reset();
199
+ }
200
+
201
+ emit("cancel");
202
+ }
203
+
204
+ const hasChanges = computed(() => {
205
+ // Only check for changes in edit mode
206
+ if (prop.mode === "edit") {
207
+ return (
208
+ camera.value.host !== prop.camera.host ||
209
+ camera.value.username !== prop.camera.username ||
210
+ camera.value.password !== prop.camera.password ||
211
+ camera.value.category !== prop.camera.category ||
212
+ camera.value.guardPost !== prop.camera.guardPost ||
213
+ camera.value.direction !== prop.camera.direction
214
+ );
215
+ }
216
+
217
+ // For add mode, always return true (allow submission)
218
+ return true;
219
+ });
220
+
221
+ const { addCamera, updateSiteCamera } = useSiteSettings();
222
+
223
+ async function submit() {
224
+ disable.value = true;
225
+ camera.value.guardPost = camera.value.guardPost ?? 0;
226
+
227
+ try {
228
+ if (prop.mode === "add") {
229
+ await addCamera(camera.value);
230
+ }
231
+
232
+ if (prop.mode === "edit") {
233
+ await updateSiteCamera(camera.value._id ?? "", {
234
+ host: camera.value.host,
235
+ username: camera.value.username,
236
+ password: camera.value.password,
237
+ category: camera.value.category,
238
+ guardPost: camera.value.guardPost,
239
+ direction: camera.value.direction,
240
+ });
241
+ }
242
+
243
+ emit("success", `${prop.title} added successfully!`);
244
+ } catch (error: any) {
245
+ emit("error", error.response?._data?.message || "Failed to add camera");
246
+ } finally {
247
+ disable.value = false;
248
+ }
249
+ }
250
+
251
+ const guardPostOptions = computed(() => {
252
+ const items = [];
253
+
254
+ for (let index = 0; index < prop.guardPosts; index++) {
255
+ items.push({
256
+ title: `Guard House ${index + 1}`,
257
+ value: index + 1,
258
+ });
259
+ }
260
+
261
+ return items;
262
+ });
263
+ </script>
264
+ <style scoped></style>
@@ -0,0 +1,352 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12">
4
+ <v-card width="100%" variant="outlined" border="thin" rounded="lg">
5
+ <v-toolbar density="compact" color="grey-lighten-4 pl-2 pr-4">
6
+ <template #prepend>
7
+ <v-btn fab icon density="comfortable" @click="getCameraRefresh()">
8
+ <v-icon>mdi-refresh</v-icon>
9
+ </v-btn>
10
+ </template>
11
+
12
+ <template #append>
13
+ <v-btn
14
+ variant="flat"
15
+ color="primary"
16
+ class="text-none"
17
+ @click="dialogAdd = true"
18
+ >
19
+ Add
20
+ </v-btn>
21
+ </template>
22
+ </v-toolbar>
23
+
24
+ <v-card-text class="pa-0 bg-red">
25
+ <v-data-table
26
+ :headers="prop.headers"
27
+ :items="items"
28
+ fixed-header
29
+ hide-default-footer
30
+ hide-default-header
31
+ @click:row="handleRowClick"
32
+ style="max-height: 300px"
33
+ >
34
+ </v-data-table>
35
+ </v-card-text>
36
+
37
+ <v-toolbar density="compact" color="grey-lighten-4">
38
+ <template #append>
39
+ <v-row no-gutters justify="end" align="center">
40
+ <span class="mr-2 text-caption text-font gray">
41
+ {{ pageRange }}
42
+ </span>
43
+ <local-pagination
44
+ v-model="page"
45
+ :length="pages"
46
+ @update:value="$emit('update:value', $event)"
47
+ />
48
+ </v-row>
49
+ </template>
50
+ </v-toolbar>
51
+ </v-card>
52
+ </v-col>
53
+
54
+ <!-- dialog -->
55
+ <v-dialog v-model="dialogAdd" persistent width="450">
56
+ <CameraForm
57
+ title="Add Camera"
58
+ :site="prop.site"
59
+ :guard-posts="prop.guardPosts"
60
+ :type="prop.type"
61
+ @cancel="dialogAdd = false"
62
+ @success="handleSuccess"
63
+ />
64
+ </v-dialog>
65
+
66
+ <v-dialog v-model="dialogEdit" persistent width="450">
67
+ <CameraForm
68
+ title="Edit Camera"
69
+ :site="prop.site"
70
+ :guard-posts="prop.guardPosts"
71
+ :type="prop.type"
72
+ @cancel="dialogEdit = false"
73
+ @success="handleSuccess"
74
+ mode="edit"
75
+ :camera="camera"
76
+ />
77
+ </v-dialog>
78
+
79
+ <v-dialog v-model="dialogPreview" persistent width="540">
80
+ <v-card width="100%">
81
+ <v-toolbar>
82
+ <v-row no-gutters class="fill-height px-6" align="center">
83
+ <span class="font-weight-bold text-h5 text-capitalize">
84
+ Preview
85
+ </span>
86
+ </v-row>
87
+ </v-toolbar>
88
+
89
+ <v-card-text
90
+ style="max-height: 100vh; overflow-y: auto"
91
+ class="pa-5 my-5 px-7"
92
+ >
93
+ <v-row no-gutters>
94
+ <v-col cols="12" class="mb-2">
95
+ Host:
96
+ <span class="font-weight-bold">
97
+ {{ camera.host }}
98
+ </span>
99
+ </v-col>
100
+
101
+ <v-col cols="12" class="mb-2">
102
+ Username:
103
+ <span class="font-weight-bold">
104
+ {{ camera.username }}
105
+ </span>
106
+ </v-col>
107
+
108
+ <v-col cols="12" class="mb-2">
109
+ Password:
110
+ <span class="font-weight-bold">
111
+ {{ camera.password }}
112
+ </span>
113
+ </v-col>
114
+
115
+ <v-col cols="12" class="mb-2">
116
+ Type:
117
+ <span class="font-weight-bold">
118
+ {{ camera.type }}
119
+ </span>
120
+ </v-col>
121
+
122
+ <v-col cols="12" class="mb-2">
123
+ Category:
124
+ <span class="font-weight-bold">
125
+ {{ camera.category }}
126
+ </span>
127
+ </v-col>
128
+
129
+ <v-col v-if="camera.type === 'anpr'" cols="12" class="mb-2">
130
+ Direction:
131
+ <span class="font-weight-bold">
132
+ {{ camera.direction }}
133
+ </span>
134
+ </v-col>
135
+
136
+ <v-col v-if="camera.type === 'anpr'" cols="12">
137
+ Guard House:
138
+ <span class="font-weight-bold">
139
+ {{ camera.guardPost }}
140
+ </span>
141
+ </v-col>
142
+ </v-row>
143
+ </v-card-text>
144
+
145
+ <v-toolbar class="pa-0" density="compact">
146
+ <v-row no-gutters>
147
+ <v-col cols="6" class="pa-0">
148
+ <v-btn
149
+ block
150
+ variant="text"
151
+ class="text-none"
152
+ size="large"
153
+ @click="dialogPreview = false"
154
+ height="48"
155
+ >
156
+ Close
157
+ </v-btn>
158
+ </v-col>
159
+
160
+ <v-col cols="6" class="pa-0">
161
+ <v-menu>
162
+ <template #activator="{ props }">
163
+ <v-btn
164
+ block
165
+ variant="flat"
166
+ color="black"
167
+ class="text-none"
168
+ height="48"
169
+ v-bind="props"
170
+ tile
171
+ >
172
+ More actions
173
+ </v-btn>
174
+ </template>
175
+
176
+ <v-list class="pa-0">
177
+ <v-list-item @click="openDialogEdit()">
178
+ <v-list-item-title class="text-subtitle-2">
179
+ Edit Building
180
+ </v-list-item-title>
181
+ </v-list-item>
182
+
183
+ <v-list-item @click="openDialogDelete()" class="text-red">
184
+ <v-list-item-title class="text-subtitle-2">
185
+ Delete Building
186
+ </v-list-item-title>
187
+ </v-list-item>
188
+ </v-list>
189
+ </v-menu>
190
+ </v-col>
191
+ </v-row>
192
+ </v-toolbar>
193
+ </v-card>
194
+ </v-dialog>
195
+
196
+ <v-dialog v-model="dialogDelete" persistent width="540">
197
+ <v-card width="100%">
198
+ <v-card-text
199
+ style="max-height: 100vh; overflow-y: auto"
200
+ class="pa-5 my-5 px-7 text-center"
201
+ >
202
+ Are you sure want to delete this {{ camera.type }} camera?
203
+ </v-card-text>
204
+
205
+ <v-toolbar class="pa-0" density="compact">
206
+ <v-row no-gutters>
207
+ <v-col cols="6" class="pa-0">
208
+ <v-btn
209
+ block
210
+ variant="text"
211
+ class="text-none"
212
+ size="large"
213
+ tile
214
+ @click="dialogDelete = false"
215
+ height="48"
216
+ >
217
+ Cancel
218
+ </v-btn>
219
+ </v-col>
220
+
221
+ <v-col cols="6" class="pa-0">
222
+ <v-btn
223
+ block
224
+ tile
225
+ variant="flat"
226
+ class="text-none"
227
+ size="large"
228
+ height="48"
229
+ color="black"
230
+ @click="submitDelete()"
231
+ >
232
+ Delete
233
+ </v-btn>
234
+ </v-col>
235
+ </v-row>
236
+ </v-toolbar>
237
+ </v-card>
238
+ </v-dialog>
239
+ </v-row>
240
+ </template>
241
+
242
+ <script setup lang="ts">
243
+ import type { PropType } from "vue";
244
+ import useSiteSettings from "../composables/useSiteSettings";
245
+
246
+ const events = defineEmits(["update:value", "row-click"]);
247
+
248
+ const prop = defineProps({
249
+ site: {
250
+ type: String,
251
+ required: true,
252
+ default: "site",
253
+ },
254
+ type: {
255
+ type: String as PropType<TSiteCamera["type"]>,
256
+ default: "anpr",
257
+ },
258
+ guardPosts: {
259
+ type: Number,
260
+ default: 0,
261
+ },
262
+ headers: {
263
+ type: Array as PropType<Array<any>>,
264
+ default: () => [
265
+ { text: "Host", value: "host" },
266
+ { text: "Category", value: "category" },
267
+ { text: "Guard House", value: "guardPost" },
268
+ { text: "Actions", value: "action", sortable: false },
269
+ ],
270
+ },
271
+ });
272
+
273
+ const items = ref<Array<TCamera>>([]);
274
+ const page = ref(1);
275
+ const pages = ref(0);
276
+ const pageRange = ref("-- - -- of --");
277
+
278
+ const { getAllSiteCameras, deleteSiteCameraById } = useSiteSettings();
279
+ const { data: getCameraReq, refresh: getCameraRefresh } =
280
+ await useLazyAsyncData(
281
+ `get-site-${prop.site}-${prop.type}-cameras`,
282
+ () =>
283
+ getAllSiteCameras({ site: prop.site, type: prop.type, page: page.value }),
284
+ {
285
+ watch: [page],
286
+ }
287
+ );
288
+
289
+ watchEffect(() => {
290
+ if (getCameraReq.value) {
291
+ items.value = getCameraReq.value.items;
292
+ pageRange.value = getCameraReq.value.pageRange;
293
+ pages.value = getCameraReq.value.pages;
294
+ }
295
+ });
296
+
297
+ const dialogAdd = ref(false);
298
+ const dialogEdit = ref(false);
299
+ const dialogPreview = ref(false);
300
+ const dialogDelete = ref(false);
301
+
302
+ function openDialogEdit() {
303
+ dialogEdit.value = true;
304
+
305
+ if (dialogPreview.value) dialogPreview.value = false;
306
+ }
307
+
308
+ function openDialogDelete() {
309
+ dialogDelete.value = true;
310
+
311
+ if (dialogPreview.value) dialogPreview.value = false;
312
+ }
313
+
314
+ function handleSuccess() {
315
+ if (dialogAdd.value) dialogAdd.value = false;
316
+
317
+ if (dialogEdit.value) dialogEdit.value = false;
318
+
319
+ if (dialogPreview.value) dialogPreview.value = false;
320
+
321
+ if (dialogDelete.value) dialogDelete.value = false;
322
+
323
+ getCameraRefresh();
324
+ }
325
+
326
+ const camera = ref<TSiteCamera>({
327
+ site: "",
328
+ host: "",
329
+ username: "",
330
+ password: "",
331
+ type: "ip",
332
+ category: "standard",
333
+ direction: "none",
334
+ guardPost: null,
335
+ status: "active",
336
+ });
337
+
338
+ function handleRowClick(_: any, data: any) {
339
+ dialogPreview.value = true;
340
+ camera.value = JSON.parse(JSON.stringify(data.item));
341
+ }
342
+
343
+ async function submitDelete() {
344
+ try {
345
+ await deleteSiteCameraById(camera.value._id ?? "");
346
+ await getCameraRefresh();
347
+ dialogDelete.value = false;
348
+ } catch (error) {
349
+ console.error("Error deleting camera:", error);
350
+ }
351
+ }
352
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <v-card width="100%" :disabled="loading" :loading="loading">
3
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5 px-7 text-center">
4
+ <span> {{ promptTitle }}</span>
5
+
6
+ <span v-if="message" class="text-error mt-2">
7
+ {{ message }} Do you want to delete anyway?
8
+ </span>
9
+ </v-card-text>
10
+
11
+ <v-toolbar class="pa-0" density="compact">
12
+ <v-row no-gutters>
13
+ <v-col cols="6" class="pa-0">
14
+ <v-btn block variant="text" class="text-none" size="large" tile @click="emit('close')"
15
+ height="48">
16
+ Cancel
17
+ </v-btn>
18
+ </v-col>
19
+
20
+ <v-col cols="6" class="pa-0">
21
+ <v-btn block tile variant="flat" class="text-none" size="large" height="48" color="black"
22
+ @click="emit('delete')">
23
+ Delete
24
+ </v-btn>
25
+ </v-col>
26
+ </v-row>
27
+ </v-toolbar>
28
+ </v-card>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ const props = defineProps({
33
+ message: {
34
+ type: String,
35
+ default: ""
36
+ },
37
+ promptTitle: {
38
+ type: String,
39
+ default: "Are you sure want to delete this? "
40
+ },
41
+ loading: {
42
+ type: Boolean,
43
+ default: false
44
+ }
45
+ })
46
+
47
+ const emit = defineEmits(["close", "delete"])
48
+
49
+ </script>
50
+
51
+ <style scoped></style>