@7365admin1/layer-common 1.10.10 → 1.11.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.
@@ -148,7 +148,7 @@
148
148
  size="small"
149
149
  color="blue"
150
150
  class="cursor-pointer"
151
- @click="showNRICComplainant = !showNRICComplainant"
151
+ @click="toggleNRICComplainant"
152
152
  >
153
153
  {{ showNRICComplainant ? "mdi-eye-off" : "mdi-eye" }}
154
154
  </v-icon>
@@ -190,7 +190,7 @@
190
190
  size="small"
191
191
  color="blue"
192
192
  class="cursor-pointer"
193
- @click="showNRICRecipient = !showNRICRecipient"
193
+ @click="toggleNRICRecipient"
194
194
  >
195
195
  {{ showNRICRecipient ? "mdi-eye-off" : "mdi-eye" }}
196
196
  </v-icon>
@@ -262,6 +262,8 @@ const props = defineProps({
262
262
  },
263
263
  });
264
264
 
265
+ const emit = defineEmits(["showNRICComplainant", "showNRICRecipient"]);
266
+
265
267
  // utilities
266
268
  const { getSiteById } = useSiteSettings();
267
269
 
@@ -293,6 +295,16 @@ function toLocalDate(utcString: string) {
293
295
  });
294
296
  }
295
297
 
298
+ const toggleNRICComplainant = () => {
299
+ showNRICComplainant.value = !showNRICComplainant.value;
300
+ emit("showNRICComplainant", showNRICComplainant.value);
301
+ };
302
+
303
+ const toggleNRICRecipient = () => {
304
+ showNRICRecipient.value = !showNRICRecipient.value;
305
+ emit("showNRICRecipient", showNRICRecipient.value);
306
+ };
307
+
296
308
  const maskNRIC = (value: any) => {
297
309
  if (!value) return "NA";
298
310
 
@@ -98,15 +98,11 @@
98
98
  <v-col cols="4" class="pa-3 border-b">
99
99
  <p class="font-weight-bold">NRIC/WP No.</p>
100
100
  <p>
101
- <v-icon
102
- v-if="incidentInformation?.complaintInfo?.nric"
103
- size="small"
104
- color="blue"
105
- class="cursor-pointer"
106
- @click="showNRICComplainant = !showNRICComplainant"
107
- >
108
- {{ showNRICComplainant ? "mdi-eye-off" : "mdi-eye" }}
109
- </v-icon>
101
+ {{
102
+ showNRICComplainant
103
+ ? incidentInformation?.complaintInfo?.nric || "-"
104
+ : maskNRIC(incidentInformation?.complaintInfo?.nric)
105
+ }}
110
106
  </p>
111
107
  </v-col>
112
108
 
@@ -175,6 +171,7 @@ const props = defineProps({
175
171
 
176
172
  // utilities
177
173
  const { getSiteById } = useSiteSettings();
174
+ const route = useRoute();
178
175
 
179
176
  // state
180
177
  const siteName = ref("");
@@ -184,6 +181,22 @@ const siteId = computed(() => props.incidentInformation?.siteInfo?.site);
184
181
  const showNRICComplainant = ref(false);
185
182
  const showNRICRecipient = ref(false);
186
183
 
184
+ watch(
185
+ () => route.query.nricComp,
186
+ (val) => {
187
+ showNRICComplainant.value = val === "true";
188
+ },
189
+ { immediate: true }
190
+ );
191
+
192
+ watch(
193
+ () => route.query.nricRec,
194
+ (val) => {
195
+ showNRICRecipient.value = val === "true";
196
+ },
197
+ { immediate: true }
198
+ );
199
+
187
200
  watch(
188
201
  siteId,
189
202
  async (newSiteId) => {
@@ -27,6 +27,7 @@
27
27
  <div class="d-flex align-center">
28
28
  NRIC
29
29
  <v-icon
30
+ v-if="isShowEyeIcon"
30
31
  class="cursor-pointer ml-1"
31
32
  size="18"
32
33
  color="blue"
@@ -90,6 +91,10 @@ const props = defineProps({
90
91
  type: Object as PropType<Record<string, any> | null>,
91
92
  required: true,
92
93
  },
94
+ isShowEyeIcon: {
95
+ type: Boolean,
96
+ required: true,
97
+ },
93
98
  });
94
99
 
95
100
  // emits
@@ -0,0 +1,133 @@
1
+ <template>
2
+ <v-dialog max-width="700" v-model="isDialogVisible" persistent>
3
+ <v-card>
4
+ <v-toolbar>
5
+ <v-toolbar-title>Signature </v-toolbar-title>
6
+ <v-spacer />
7
+ <v-btn icon="mdi-close" @click="hideModal"></v-btn>
8
+ </v-toolbar>
9
+ <v-card-text>
10
+ <v-row no-gutters>
11
+ <v-col cols="12">
12
+ <div class="text-subtitle-1 text-medium-emphasis ml-2">
13
+ Your signature here
14
+ </div>
15
+
16
+ <NuxtSignaturePad
17
+ ref="signature"
18
+ :options="state.option"
19
+ :width="'100%'"
20
+ :height="'400px'"
21
+ :disabled="state.disabled"
22
+ class="border"
23
+ />
24
+ </v-col>
25
+
26
+ <v-col cols="12" class="text-center">
27
+ <v-row class="d-flex">
28
+ <v-col class="w-50 px-3">
29
+ <v-btn
30
+ text="clear"
31
+ color="warning"
32
+ type="submit"
33
+ class="my-4 w-100 rounded-lg"
34
+ height="40px"
35
+ @click="clear()"
36
+ />
37
+ </v-col>
38
+
39
+ <v-col class="w-50 px-3">
40
+ <v-btn
41
+ text="submit"
42
+ color="#1867C0"
43
+ type="submit"
44
+ class="my-4 w-100 rounded-lg"
45
+ height="40px"
46
+ :loading="loading"
47
+ @click="submit"
48
+ :disabled="loading"
49
+ />
50
+ </v-col>
51
+ </v-row>
52
+ </v-col>
53
+ </v-row>
54
+ </v-card-text>
55
+ </v-card>
56
+ </v-dialog>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ const loading = ref(false);
61
+ // const { isValid } = useAudit();
62
+ // const { uiRequiredInput, uiSetSnackbar } = useUtils();
63
+
64
+ const message = ref("");
65
+ const messageColor = ref("");
66
+ const messageSnackbar = ref(false);
67
+
68
+ function showMessage(msg: string, color: string) {
69
+ message.value = msg;
70
+ messageColor.value = color;
71
+ messageSnackbar.value = true;
72
+ }
73
+
74
+ const signature = ref(null);
75
+ const state = ref({
76
+ count: 0,
77
+ option: {
78
+ penColor: "rgb(0, 0, 0)",
79
+ backgroundColor: "rgb(255,255,255)",
80
+ },
81
+ disabled: false,
82
+ });
83
+
84
+ const emit = defineEmits<{
85
+ (event: "onSubmit", payload: string): void;
86
+ (event: "onCloseDialog"): void;
87
+ }>();
88
+ let props = defineProps({
89
+ isShowDialog: {
90
+ type: Boolean,
91
+ default: false,
92
+ },
93
+ });
94
+
95
+ const isDialogVisible = computed(() => props.isShowDialog);
96
+
97
+ const hideModal = () => {
98
+ emit("onCloseDialog");
99
+ };
100
+ const file = ref<File | null>(null);
101
+ const { addFile } = useFile();
102
+ const submit = async () => {
103
+ try {
104
+ loading.value = true;
105
+ const base64 = signature.value.saveSignature();
106
+ const blob = await (await fetch(base64)).blob();
107
+
108
+ file.value = new File([blob], "signature.jpg", { type: "image/jpeg" });
109
+
110
+ const uploadItem = {
111
+ data: file.value,
112
+ name: file.value.name,
113
+ url: URL.createObjectURL(file.value),
114
+ progress: 0,
115
+ type: file.value.type,
116
+ };
117
+
118
+ const response = await addFile(uploadItem.data);
119
+
120
+ if (response && response.length > 0) {
121
+ emit("onSubmit", response[0]._id);
122
+ }
123
+ } catch (error) {
124
+ showMessage("Error uploading signature. Please try again.", "error");
125
+ } finally {
126
+ loading.value = false;
127
+ }
128
+ };
129
+
130
+ const clear = () => {
131
+ signature.value.clearCanvas();
132
+ };
133
+ </script>
@@ -0,0 +1,194 @@
1
+ <template>
2
+ <v-sheet class="mx-auto">
3
+ <v-slide-group
4
+ class="pa-1"
5
+ show-arrows
6
+ :style="{ backgroundColor: '#FFFF' }"
7
+ >
8
+ <v-slide-group-item
9
+ v-for="(data, idx) in localDataFiles"
10
+ :key="idx"
11
+ v-slot="{ isSelected }"
12
+ >
13
+ <v-card
14
+ :class="['ma-1', isSelected && 'selected']"
15
+ width="150"
16
+ height="100"
17
+ @click="clickable && emit('onClickCarousel', data.path)"
18
+ class="rounded-lg border-thin"
19
+ border=" opacity-50 thin "
20
+ >
21
+ <v-btn
22
+ icon
23
+ variant="text"
24
+ size="x-small"
25
+ class="remove-btn pa-0 ma-0"
26
+ @click.stop="removeFile(data)"
27
+ >
28
+ <v-icon color="red" class="pa-0 ma-0" size="x-large"
29
+ >mdi-close-circle</v-icon
30
+ >
31
+ </v-btn>
32
+ <!-- File Icons -->
33
+
34
+ <v-img :src="data.url" cover height="200">
35
+ <template #placeholder>
36
+ <v-skeleton-loader height="100%" width="100%" />
37
+ </template>
38
+ </v-img>
39
+ <!-- <template v-if="url.type !== 'image'">
40
+ <div
41
+ class="d-flex fill-height align-center justify-center flex-column"
42
+ >
43
+ <v-icon
44
+ :icon="getFileIcon(url.type)"
45
+ size="64"
46
+ :color="getFileColor(url.type)"
47
+ ></v-icon>
48
+ </div>
49
+ </template>
50
+
51
+
52
+
53
+ <template v-else>
54
+ <v-img :src="url.path" cover height="200">
55
+ <template #placeholder>
56
+ <v-skeleton-loader height="100%" width="100%" />
57
+ </template>
58
+ </v-img>
59
+ </template> -->
60
+
61
+ <!-- Edit Button -->
62
+ <!-- <div v-if="imgEditable" class="edit-btn">
63
+ <v-btn
64
+ color="primary"
65
+ size="small"
66
+ @click.stop="onImageEdit(url.path, idx)"
67
+ >
68
+ <v-icon start>mdi-pencil</v-icon>
69
+ Edit
70
+ </v-btn>
71
+ </div> -->
72
+ </v-card>
73
+ </v-slide-group-item>
74
+ </v-slide-group>
75
+ </v-sheet>
76
+
77
+ <DrawImage
78
+ v-if="isShowImageEdit"
79
+ :is-show-dialog="isShowImageEdit"
80
+ :image-url="imageUrl"
81
+ :image-idx="imageIdx"
82
+ @on-submit="onImageSubmitEdit"
83
+ @on-close-dialog="isShowImageEdit = false"
84
+ />
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ const { getImage } = useUtils();
89
+ const { attachedFiles } = useUploadFiles();
90
+
91
+ const emit = defineEmits<{
92
+ (event: "onClickCarousel", url: string): void;
93
+ (event: "onImageEdit", url: string, idx: number): void;
94
+ (event: "onFileRemove"): void;
95
+ }>();
96
+
97
+ const props = defineProps<{
98
+ clickable?: boolean;
99
+ imgEditable?: boolean;
100
+ dataFiles?: File[];
101
+ }>();
102
+ const localDataFiles = ref([...props.dataFiles]);
103
+ const isShowImageEdit = ref(false);
104
+ const imageUrl = ref("");
105
+ const imageIdx = ref(0);
106
+
107
+ const getFileType = (url: string) => {
108
+ const extension = url.split(".").pop()?.toLowerCase() || "";
109
+ if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) return "image";
110
+ if (["pdf"].includes(extension)) return "pdf";
111
+ if (["doc", "docx"].includes(extension)) return "word";
112
+ return "other";
113
+ };
114
+
115
+ const getFileIcon = (type: string) => {
116
+ switch (type) {
117
+ case "pdf":
118
+ return "mdi-file-pdf-box";
119
+ case "word":
120
+ return "mdi-file-word-box";
121
+ default:
122
+ return "mdi-file-outline";
123
+ }
124
+ };
125
+
126
+ const getFileColor = (type: string) => {
127
+ switch (type) {
128
+ case "pdf":
129
+ return "red";
130
+ case "word":
131
+ return "blue";
132
+ default:
133
+ return "grey";
134
+ }
135
+ };
136
+
137
+ const onImageEdit = (url: string, idx: number) => {
138
+ isShowImageEdit.value = true;
139
+ imageUrl.value = url;
140
+ imageIdx.value = idx;
141
+ };
142
+
143
+ const onImageSubmitEdit = async (url: string, idx: number) => {
144
+ const response = await getImage(url);
145
+ if (!response) return;
146
+ attachedFiles.value[idx].data = new File(
147
+ [response],
148
+ attachedFiles.value[idx].data?.name
149
+ );
150
+ attachedFiles.value[idx].url = url;
151
+ isShowImageEdit.value = false;
152
+ };
153
+
154
+ const isImageFile = (type: string) => {
155
+ return type?.startsWith("image/");
156
+ };
157
+
158
+ watch(
159
+ () => props.dataFiles,
160
+ (newDataFiles) => {
161
+ localDataFiles.value = [...newDataFiles];
162
+ }
163
+ );
164
+
165
+ const removeFile = (file) => {
166
+ const index = localDataFiles.value.findIndex(
167
+ (dataFile) => dataFile.id === file.id
168
+ );
169
+
170
+ if (index !== -1) {
171
+ localDataFiles.value.splice(index, 1);
172
+ emit("onFileRemove", file);
173
+ }
174
+ };
175
+ </script>
176
+
177
+ <style scoped>
178
+ .edit-btn {
179
+ position: absolute;
180
+ bottom: 8px;
181
+ right: 8px;
182
+ }
183
+
184
+ .selected {
185
+ border: 2px solid var(--v-theme-primary);
186
+ }
187
+
188
+ .remove-btn {
189
+ position: absolute;
190
+ top: 0px;
191
+ right: 0px;
192
+ z-index: 2;
193
+ }
194
+ </style>
@@ -322,6 +322,7 @@ const { requiredRule, debounce, UTCToLocalTIme } = useUtils();
322
322
  const { getSiteById, getSiteLevels, getSiteUnits } = useSiteSettings();
323
323
  const { createVisitor, typeFieldMap, contractorTypes, getVisitors, updateVisitor } = useVisitor();
324
324
  const { getBySiteId: getEntryPassSettingsBySiteId } = useSiteEntryPassSettings();
325
+ const { createVisitorPass } = useAccessManagement();
325
326
  const { findPersonByNRIC, findPersonByContact, searchCompanyList, findUsersByPlateNumber } = usePeople()
326
327
  const { getById: getUnitDataById } = useBuildingUnit()
327
328
 
@@ -354,7 +355,7 @@ const visitor = reactive<Partial<TVisitorPayload>>({
354
355
 
355
356
  const passType = ref("");
356
357
  const passQuantity = ref<number | null>(1);
357
- const passCards = ref<string[]>([]);
358
+ const passCards = ref<{ _id: string; cardNo: string }[]>([]);
358
359
 
359
360
  const registeredUnitCompanyName = ref('N/A')
360
361
 
@@ -906,6 +907,21 @@ async function submit() {
906
907
  try {
907
908
  const res = await createVisitor(payload);
908
909
  if (res) {
910
+ if (prop.type === "contractor" && passType.value) {
911
+ const visitorId = res?._id;
912
+ const acmUrl = entryPassSettings.value?.data?.settings?.acm_url;
913
+ if (visitorId && acmUrl) {
914
+ await createVisitorPass({
915
+ site: prop.site,
916
+ unitId: visitor.unit!,
917
+ quantity: passQuantity.value ?? 1,
918
+ type: passType.value === "QR" ? "QRCODE" : passType.value,
919
+ nfcCards: passCards.value,
920
+ acm_url: acmUrl,
921
+ visitorId,
922
+ });
923
+ }
924
+ }
909
925
  if (createMore.value) {
910
926
  emit("done:more");
911
927
  } else emit("done");
@@ -111,8 +111,8 @@ export default function useAccessManagement() {
111
111
  site: string;
112
112
  userType: string;
113
113
  type: string;
114
- accessLevel: string;
115
- liftAccessLevel: string;
114
+ accessLevel: string | null;
115
+ liftAccessLevel: string | null;
116
116
  }) {
117
117
  return useNuxtApp().$api<Record<string, any>>(
118
118
  `/api/access-management/access-and-lift-cards`,
@@ -122,8 +122,8 @@ export default function useAccessManagement() {
122
122
  site: params.site,
123
123
  userType: params.userType,
124
124
  type: params.type,
125
- accessLevel: params.accessLevel,
126
- liftAccessLevel: params.liftAccessLevel,
125
+ ...(params.accessLevel != null ? { accessLevel: params.accessLevel } : {}),
126
+ ...(params.liftAccessLevel != null ? { liftAccessLevel: params.liftAccessLevel } : {}),
127
127
  },
128
128
  }
129
129
  );
@@ -135,8 +135,8 @@ export default function useAccessManagement() {
135
135
  type: string;
136
136
  site: string;
137
137
  userType: string;
138
- accessLevel: string;
139
- liftAccessLevel: string;
138
+ accessLevel: string | null;
139
+ liftAccessLevel: string | null;
140
140
  }) {
141
141
  return useNuxtApp().$api<Record<string, any>>(
142
142
  `/api/access-management/assign-access-card`,
@@ -261,6 +261,24 @@ export default function useAccessManagement() {
261
261
  );
262
262
  }
263
263
 
264
+ function createVisitorPass(payload: {
265
+ site: string;
266
+ unitId: string;
267
+ quantity: number;
268
+ type: string;
269
+ nfcCards: { _id: string; cardNo: string }[];
270
+ acm_url: string;
271
+ visitorId: string;
272
+ }) {
273
+ return useNuxtApp().$api<Record<string, any>>(
274
+ `/api/access-management/visitor`,
275
+ {
276
+ method: "POST",
277
+ body: payload,
278
+ }
279
+ );
280
+ }
281
+
264
282
  return {
265
283
  getDoorAccessLevels,
266
284
  getLiftAccessLevels,
@@ -280,5 +298,6 @@ export default function useAccessManagement() {
280
298
  getAllVisitorAccessCardsQrTags,
281
299
  getAvailableContractorCards,
282
300
  generateQrVms,
301
+ createVisitorPass,
283
302
  };
284
303
  }
@@ -0,0 +1,147 @@
1
+ const { currentUser } = useLocalAuth();
2
+ export default function useComment() {
3
+ class MComment implements TCommentChat {
4
+ _id?: string;
5
+ comment: string;
6
+ attachments?: TAttachment[];
7
+ createdBy: string;
8
+ workOrder: string;
9
+ createdAt?: string;
10
+ updatedAt?: string;
11
+ createdByName?: string;
12
+ createdByType?: string;
13
+ feedback?: string;
14
+ justify?: string | undefined;
15
+ seenByNames: string;
16
+
17
+ constructor(comment: Partial<TCommentChat> = {}) {
18
+ this._id = comment._id || "";
19
+ this.comment = comment.comment || "";
20
+ this.attachments = comment.attachments || [];
21
+ this.workOrder = comment.workOrder || "";
22
+ this.createdAt = comment.createdAt
23
+ ? comment.createdAt.toString()
24
+ : undefined;
25
+ this.updatedAt = comment.updatedAt || "";
26
+ this.createdBy = comment.createdBy || "";
27
+ this.createdByName = comment.createdByName || "";
28
+ this.createdByType = comment.createdByType || "";
29
+ this.justify = comment.justify || "";
30
+ this.feedback = comment.feedback || "";
31
+ this.seenByNames = comment.seenByNames ?? "";
32
+ }
33
+ }
34
+
35
+ const comment = useState("comment", () => new MComment());
36
+ const comments = useState("comments", (): TCommentChat[] => []);
37
+ const page = useState("commentPage", () => 1);
38
+ const itemsPerPage = useState("itemsPerPage", () => 2);
39
+ const search = useState("commentSearch", () => "");
40
+ const pageRange = useState("commentPageRange", () => "-- - -- of --");
41
+ const isCommentsLoaded = useState("isCommentsLoaded", () => false);
42
+
43
+ function addComment(payload: TCommentChat) {
44
+ delete payload._id;
45
+ return useNuxtApp().$api<{ value?: { message: string } }>(
46
+ "/api/comments/v1",
47
+ {
48
+ method: "POST",
49
+ body: payload,
50
+ }
51
+ );
52
+ }
53
+
54
+ function getCommentById(id: string) {
55
+ return useNuxtApp().$api(`/api/auth/comments/id/${id}`, {
56
+ method: "GET",
57
+ });
58
+ }
59
+
60
+ function setComment(data?: TCommentChat) {
61
+ comment.value = new MComment(data);
62
+ }
63
+
64
+ const isInviteValid = useState("isInviteValid", () => false);
65
+
66
+ function getCommentsByWorkOrder(id: string) {
67
+ return useNuxtApp().$api(`/api/comments/v1/work-order/${id}`);
68
+ }
69
+
70
+ function getCommentsByWorkOrderId(id: string) {
71
+ return useNuxtApp().$api(`/api/comments/v1/work-order/${id}`);
72
+ }
73
+
74
+ function getCommentsByFeedBackId(id: string, type?: string) {
75
+ const commentFeedbackOrWOD =
76
+ type === "workOrder" ? "work-order" : "feedback";
77
+ return useNuxtApp().$api(`/api/comments/v1/${commentFeedbackOrWOD}/${id}`);
78
+ }
79
+
80
+ async function setComments(id: string, type?: string) {
81
+ try {
82
+ isCommentsLoaded.value = true;
83
+ const _comments = (await getCommentsByFeedBackId(
84
+ id,
85
+ type
86
+ )) as TCommentPaginated;
87
+
88
+ let updateSeenIds: any = [];
89
+
90
+ comments.value = _comments.items
91
+ .map((comment: any) => {
92
+ if (comment.createdBy === currentUser.value._id) {
93
+ comment.justify = "end";
94
+ } else {
95
+ comment.justify = "start";
96
+ if (
97
+ ((Array.isArray(comment.seenBy) &&
98
+ !comment.seenBy.includes(currentUser.value._id)) ||
99
+ !comment?.seenBy) &&
100
+ comment?._id
101
+ ) {
102
+ updateSeenIds.push(comment._id);
103
+ }
104
+ }
105
+ return comment;
106
+ })
107
+ .sort(
108
+ (firstComment: any, secondComment: any) =>
109
+ new Date(firstComment.createdAt).getTime() -
110
+ new Date(secondComment.createdAt).getTime()
111
+ );
112
+
113
+ if (Array.isArray(updateSeenIds) && updateSeenIds.length > 0) {
114
+ const seenBy = await updateSeenBy(updateSeenIds, currentUser.value._id);
115
+ }
116
+ } catch (error) {
117
+ console.log("error :", error);
118
+ } finally {
119
+ isCommentsLoaded.value = false;
120
+ }
121
+ }
122
+
123
+ function updateSeenBy(ids: string[], seenById: string) {
124
+ return useNuxtApp().$api(`/api/comments/v1/seen-by`, {
125
+ method: "PUT",
126
+ body: { seenById, ids },
127
+ });
128
+ }
129
+
130
+ return {
131
+ comment,
132
+ comments,
133
+ page,
134
+ itemsPerPage,
135
+ pageRange,
136
+ addComment,
137
+ setComment,
138
+ search,
139
+ isCommentsLoaded,
140
+ isInviteValid,
141
+ setComments,
142
+ getCommentById,
143
+ getCommentsByWorkOrderId,
144
+ updateSeenBy,
145
+ getCommentsByFeedBackId,
146
+ };
147
+ }