@7365admin1/layer-common 1.10.10 → 1.11.1

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.
@@ -0,0 +1,304 @@
1
+ <template>
2
+ <div>
3
+ <v-file-input
4
+ v-model="files"
5
+ :label="label"
6
+ accept="image/*"
7
+ :prepend-icon="prependIcon"
8
+ hide-details
9
+ show-size
10
+ chips
11
+ multiple
12
+ clearable
13
+ @update:modelValue="handleFileSelect"
14
+ @click:clear="handleClear"
15
+ :hide-input="hasHideInput"
16
+ >
17
+ <template v-slot:append v-if="hasLabel">
18
+ <slot name="append">
19
+ <v-btn color="primary" height="50px" @click="openCameraDialog">
20
+ <v-icon>mdi-camera</v-icon>
21
+ </v-btn>
22
+ </slot>
23
+ </template>
24
+ </v-file-input>
25
+
26
+ <v-row no-gutters v-if="hasLabel">
27
+ <v-col cols="12" class="mt-1">
28
+ <v-chip-group column>
29
+ <template v-for="(file, index) in files" :key="file.name">
30
+ <v-chip
31
+ closable
32
+ class="text-wrap text-caption custom-chip"
33
+ @click:close="removeFile(index)"
34
+ >
35
+ <span class="chip-text">{{ file.name }}</span>
36
+ </v-chip>
37
+ </template>
38
+ </v-chip-group>
39
+ </v-col>
40
+ </v-row>
41
+
42
+ <!-- Camera Dialog -->
43
+ <v-dialog
44
+ v-model="showCameraDialog"
45
+ transition="dialog-bottom-transition"
46
+ width="800"
47
+ max-width="800"
48
+ persistent
49
+ @after-enter="startCamera"
50
+ >
51
+ <v-container
52
+ class="d-flex justify-center"
53
+ max-height="90vh"
54
+ width="800"
55
+ max-width="800"
56
+ >
57
+ <v-card elevation="2" class="d-flex flex-column align-center pa-2">
58
+ <v-toolbar>
59
+ <v-card-title class="text-h5">Take a Picture</v-card-title>
60
+ <v-spacer></v-spacer>
61
+ <v-btn
62
+ color="grey-darken-1"
63
+ icon="mdi-close"
64
+ @click="closeCameraDialog"
65
+ ></v-btn>
66
+ </v-toolbar>
67
+
68
+ <div
69
+ id="reader"
70
+ class="d-flex justify-center align-center"
71
+ style="
72
+ position: relative;
73
+ width: 500px;
74
+ min-width: 400px;
75
+ height: 400px;
76
+ "
77
+ >
78
+ <video
79
+ ref="video"
80
+ style="flex: 1; height: 400px; min-width: 300px"
81
+ class="video-shutter"
82
+ autoplay
83
+ ></video>
84
+ <canvas
85
+ ref="canvas"
86
+ style="flex: 1; height: 400px; min-width: 300px; display: none"
87
+ ></canvas>
88
+ </div>
89
+
90
+ <v-row align="center" justify="center">
91
+ <v-col cols="6">
92
+ <v-btn color="primary" icon class="mt-4" @click="switchCamera">
93
+ <v-icon>mdi-camera-switch</v-icon>
94
+ </v-btn>
95
+ </v-col>
96
+ <v-col cols="6">
97
+ <v-btn
98
+ color="secondary"
99
+ icon
100
+ class="mt-4"
101
+ @click="captureImageFromCamera"
102
+ >
103
+ <v-icon large>mdi-camera-outline</v-icon>
104
+ </v-btn>
105
+ </v-col>
106
+ </v-row>
107
+ </v-card>
108
+ </v-container>
109
+ </v-dialog>
110
+ </div>
111
+ </template>
112
+
113
+ <script setup lang="ts">
114
+ interface FileWithPreview {
115
+ name: string;
116
+ data: File;
117
+ progress: number;
118
+ url: string;
119
+ }
120
+
121
+ const props = defineProps({
122
+ label: {
123
+ type: String,
124
+ default: "Select File",
125
+ },
126
+ prependIcon: {
127
+ type: String,
128
+ default: "mdi-paperclip",
129
+ },
130
+ required: {
131
+ type: Boolean,
132
+ default: true,
133
+ },
134
+ initFiles: {
135
+ type: Array,
136
+ },
137
+ hasLabel: {
138
+ type: Boolean,
139
+ default: true,
140
+ },
141
+ hasHideInput: {
142
+ type: Boolean,
143
+ default: false,
144
+ },
145
+ });
146
+
147
+ const emit = defineEmits<{
148
+ (event: "onFileAttach", payload: Array<{ data: File }>): void;
149
+ (event: "update:files", files: FileWithPreview[]): void;
150
+ (event: "onFileRemoved", payload: { index: number; file: File }): void;
151
+ (event: "onClear"): void;
152
+ }>();
153
+
154
+ const { showUploadedFiles } = useUploadFiles();
155
+
156
+ const files = ref<File[]>([]);
157
+ const attachedFiles = ref<FileWithPreview[]>([]);
158
+ const showCameraDialog = ref(false);
159
+ const video = ref<HTMLVideoElement | null>(null);
160
+ const canvas = ref<HTMLCanvasElement | null>(null);
161
+ const cameraFacingMode = ref<"environment" | "user">("environment");
162
+
163
+ const message = ref("");
164
+ const messageColor = ref("");
165
+ const messageSnackbar = ref(false);
166
+
167
+ function showMessage(msg: string, color: string) {
168
+ message.value = msg;
169
+ messageColor.value = color;
170
+ messageSnackbar.value = true;
171
+ }
172
+
173
+ watchEffect(() => {
174
+ if (Array.isArray(props.initFiles) && props.initFiles.length > 0) {
175
+ files.value = props.initFiles.filter((file) => file && file.name); // Ensure valid files
176
+ }
177
+ });
178
+ const handleFileSelect = async () => {
179
+ if (files.value && files.value.length > 0) {
180
+ const newFiles = files.value.map((file: File) => ({
181
+ name: file.name,
182
+ data: file,
183
+ progress: 0,
184
+ url: URL.createObjectURL(file),
185
+ }));
186
+
187
+ // attachedFiles.value = [...newFiles];
188
+ showUploadedFiles(newFiles);
189
+
190
+ emit("update:files", newFiles);
191
+ } else {
192
+ files.value = [...(props.initFiles as typeof files.value)];
193
+ }
194
+ };
195
+
196
+ const handleClear = () => {
197
+ files.value = [];
198
+ emit("onClear");
199
+ };
200
+
201
+ const openCameraDialog = () => {
202
+ showCameraDialog.value = true;
203
+ };
204
+
205
+ const closeCameraDialog = () => {
206
+ showCameraDialog.value = false;
207
+ stopCamera();
208
+ };
209
+
210
+ const startCamera = async () => {
211
+ try {
212
+ const constraints = {
213
+ video: {
214
+ facingMode: cameraFacingMode.value,
215
+ },
216
+ };
217
+
218
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
219
+ if (video.value) {
220
+ video.value.srcObject = stream;
221
+ video.value.play();
222
+ }
223
+ } catch (error: any) {
224
+ showMessage(`Error accessing camera: ${error.message}`, "error");
225
+ closeCameraDialog();
226
+ }
227
+ };
228
+
229
+ const stopCamera = () => {
230
+ if (video.value) {
231
+ const stream = video.value.srcObject as MediaStream;
232
+ if (stream) {
233
+ stream.getTracks().forEach((track) => track.stop());
234
+ }
235
+ }
236
+ };
237
+
238
+ const switchCamera = async () => {
239
+ await stopCamera();
240
+ cameraFacingMode.value =
241
+ cameraFacingMode.value === "environment" ? "user" : "environment";
242
+ showMessage(
243
+ `Switched to ${
244
+ cameraFacingMode.value === "environment" ? "Back Camera" : "Front Camera"
245
+ }`,
246
+ "error"
247
+ );
248
+ startCamera();
249
+ };
250
+
251
+ const captureImageFromCamera = () => {
252
+ if (!video.value || !canvas.value) return;
253
+
254
+ const context = canvas.value.getContext("2d");
255
+ if (!context) return;
256
+
257
+ // Set canvas dimensions to match video
258
+ canvas.value.width = video.value.videoWidth;
259
+ canvas.value.height = video.value.videoHeight;
260
+
261
+ // Capture the frame
262
+ context.drawImage(video.value, 0, 0, canvas.value.width, canvas.value.height);
263
+
264
+ // Convert to file
265
+ canvas.value.toBlob((blob) => {
266
+ if (!blob) return;
267
+
268
+ const file = new File([blob], `camera-capture-${Date.now()}.png`, {
269
+ type: "image/png",
270
+ });
271
+
272
+ files.value = [file];
273
+ handleFileSelect();
274
+ closeCameraDialog();
275
+ }, "image/png");
276
+ };
277
+
278
+ const removeFile = (index) => {
279
+ const removedFile = files.value[index];
280
+ files.value = files.value.filter((_, i) => i !== index);
281
+ emit("onFileRemoved", { index, file: removedFile }); // Emit when a file is removed
282
+ // emit("onFilesUpdated", files.value); // Emit updated files list
283
+ };
284
+
285
+ // Cleanup
286
+ onUnmounted(() => {
287
+ stopCamera();
288
+ });
289
+ </script>
290
+
291
+ <style scoped>
292
+ .custom-chip {
293
+ max-width: 100%;
294
+ height: auto !important;
295
+ white-space: normal;
296
+ padding: 3px 20px;
297
+ }
298
+
299
+ .chip-text {
300
+ word-break: break-word;
301
+ white-space: normal;
302
+ line-height: 1.2;
303
+ }
304
+ </style>
@@ -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>