@iservice365/layer-common 1.5.7 → 1.7.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.
@@ -1,19 +1,43 @@
1
1
  <template>
2
2
  <v-dialog v-model="overlay" v-if="overlay" width="100%" height="100%" opacity="50">
3
3
  <v-row align="center" justify="center" class="fill-height" style="position: relative;">
4
- <v-carousel hide-delimiters width="100%" height="100%" :show-arrows="images.length > 1 ? 'hover' : false"
4
+ <v-carousel hide-delimiters width="100%" height="100%" :show-arrows="files.length > 1 ? 'hover' : false"
5
5
  v-model="activeIndex">
6
- <template v-for="x, index in images" :key="x._id">
7
- <v-carousel-item height="100%" width="100%" rounded="lg">
8
- <v-row no-gutters class="w-100 h-100" align="center">
9
- <v-img :lazy-src="getFileUrl(x)" :src="getFileUrl(x)" width="70%" height="70%" :alt="'Image Viewer Card -' + index"></v-img>
10
- </v-row>
11
- <template v-slot:placeholder>
12
- <div class="d-flex align-center justify-center fill-height">
13
- <v-progress-circular color="grey-lighten-4" indeterminate></v-progress-circular>
14
- </div>
15
- </template>
16
- </v-carousel-item>
6
+ <template v-for="x, index in files" :key="x || index">
7
+ <template v-if="fileTypes?.[x] === 'image'">
8
+ <v-carousel-item height="100%" width="100%" rounded="lg">
9
+ <v-row no-gutters class="w-100 h-100" align="center">
10
+ <v-img :lazy-src="getFileUrl(x)" :src="getFileUrl(x)" width="70%" height="70%"
11
+ :alt="'Image Viewer Card -' + index"></v-img>
12
+ </v-row>
13
+ <template v-slot:placeholder>
14
+ <div class="d-flex align-center justify-center fill-height">
15
+ <v-progress-circular color="grey-lighten-4" indeterminate></v-progress-circular>
16
+ </div>
17
+ </template>
18
+ </v-carousel-item>
19
+ </template>
20
+ <template v-else-if="fileTypes?.[x] === 'video'">
21
+ <v-carousel-item>
22
+ <v-row no-gutters class="h-100 w-100" align="center" justify="center">
23
+ <video width="80%" height="80%" controls>
24
+ <source :src="getFileUrl(x)" />
25
+ </video>
26
+ </v-row>
27
+ <template v-slot:placeholder>
28
+ <div class="d-flex align-center justify-center fill-height">
29
+ <v-progress-circular color="grey-lighten-4" indeterminate></v-progress-circular>
30
+ </div>
31
+ </template>
32
+ </v-carousel-item>
33
+ </template>
34
+ <template v-else>
35
+ <v-carousel-item>
36
+ <v-row no-gutters class="h-100 w-100" align="center" justify="center">
37
+ <v-icon size="100" color="white">mdi-file</v-icon>
38
+ </v-row>
39
+ </v-carousel-item>
40
+ </template>
17
41
  </template>
18
42
 
19
43
  <template v-slot:prev="{ props }">
@@ -29,15 +53,17 @@
29
53
  class="cursor-pointer text-white text-h6 custom-shadow d-flex justify-space-between" @click="overlay = false">
30
54
  <v-row no-gutters style="position: relative">
31
55
  <v-col cols="8" xs="6" sm="2" md="2" lg="2">
32
- <v-btn prepend-icon="mdi-close" text="Close" rounded="lg" color="secondary" class="ml-2"
33
- style="position: absolute;"></v-btn>
34
- </v-col>
56
+ <v-btn prepend-icon="mdi-close" text="Close" rounded="lg" color="secondary" class="ml-2"
57
+ style="position: absolute;"></v-btn>
58
+ </v-col>
35
59
 
36
60
  </v-row>
37
61
 
38
62
  </div>
39
- <span class="text-white text-16px d-flex w-100 mt-2 justify-center" style="position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);">{{ `${activeIndex + 1}/${images.length}`
40
- }}</span>
63
+ <span class="text-white text-16px d-flex w-100 mt-2 justify-center"
64
+ style="position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);">{{ `${activeIndex +
65
+ 1}/${files.length}`
66
+ }}</span>
41
67
 
42
68
  </v-row>
43
69
  </v-dialog>
@@ -45,33 +71,29 @@
45
71
 
46
72
  <script setup lang="ts">
47
73
  const props = defineProps({
48
- activeImageId: {
74
+ activeFileId: {
49
75
  type: String,
50
76
  required: false,
51
77
  },
52
- isFavorite: {
53
- type: Boolean,
54
- default: false,
55
- required: false
56
- },
57
- images: {
78
+ files: {
58
79
  type: Array as PropType<string[]>,
59
80
  default: []
60
81
  }
61
82
  });
62
83
 
63
84
 
64
- const { getFileUrl } = useFile()
85
+ const { getFileUrl, urlToFile } = useFile()
65
86
  const overlay = defineModel({ required: true, default: false });
66
87
  const activeIndex = ref(0);
67
- const { currentUser} = useLocalAuth();
88
+ const fileTypes = ref<Record<string, "image" | "video" | "other">>({});
89
+
68
90
 
69
91
  const emit = defineEmits(['share', 'like'])
70
92
 
71
93
  watchEffect(() => {
72
- if (props.activeImageId && props.images.length > 0) {
94
+ if (props.activeFileId && props.files.length > 0) {
73
95
  const index =
74
- props.images?.findIndex((x) => x == props.activeImageId) || 0;
96
+ props.files?.findIndex((x) => x == props.activeFileId) || 0;
75
97
  if (index !== -1) {
76
98
  activeIndex.value = index;
77
99
  } else activeIndex.value = 0;
@@ -80,15 +102,33 @@ watchEffect(() => {
80
102
  }
81
103
  });
82
104
 
105
+ async function resolveFileTypes() {
106
+ fileTypes.value = {}; // reset
83
107
 
84
- const handleShare = () => {
85
- overlay.value = false;
86
- emit('share')
87
- }
108
+ for (const x of props.files) {
109
+ try {
110
+ const url = getFileUrl(x);
111
+ const file = await urlToFile(url, x);
112
+ const type = file?.type;
113
+
114
+
115
+ if (type?.startsWith("video")) fileTypes.value[x] = "video";
116
+ else if (type?.startsWith("image")) fileTypes.value[x] = "image";
117
+ else fileTypes.value[x] = "other";
88
118
 
89
- const handleLike = () => {
90
- emit('like')
119
+ } catch (err) {
120
+ fileTypes.value[x] = "other";
121
+ }
122
+ }
91
123
  }
124
+
125
+ watch(
126
+ () => props.files,
127
+ () => resolveFileTypes(),
128
+ { immediate: true }
129
+ );
130
+
131
+
92
132
  </script>
93
133
 
94
134
  <style scoped>
@@ -42,7 +42,7 @@
42
42
  <p class="text-error w-100 text-center text-subtitle-2">{{ errorMessage }}</p>
43
43
  </v-row>
44
44
 
45
- <ImageCarousel v-model="showImageCarousel" :images="idsArray" :active-image-id="activeImageId" />
45
+ <ImageCarousel v-model="showImageCarousel" :files="idsArray" :active-file-id="activeImageId" />
46
46
  </v-row>
47
47
  </template>
48
48
 
@@ -21,6 +21,7 @@
21
21
  <script setup lang="ts">
22
22
  import { ref, computed, type PropType } from 'vue'
23
23
  import type { ValidationRule } from 'vuetify/lib/types.mjs'
24
+ //@ts-ignore
24
25
  import phoneMasks from '~/utils/phoneMasks'
25
26
 
26
27
  const props = defineProps({
@@ -35,7 +35,7 @@ const props = defineProps({
35
35
 
36
36
  const emit = defineEmits(['update:modelValue'])
37
37
 
38
- const model = defineModel({required: true, default: ""})
38
+ const model = defineModel({required: true})
39
39
 
40
40
  function onInput(event) {
41
41
  const value = typeof event === 'string' ? event : event?.target?.value || ''
@@ -43,7 +43,7 @@ function onInput(event) {
43
43
  let formatted = value.replace(/[^A-Za-z0-9]/g, '')
44
44
  formatted = formatted.toUpperCase()
45
45
 
46
- model.value = formatted
46
+ model.value = formatted || ""
47
47
  }
48
48
 
49
49
 
@@ -36,7 +36,7 @@ const props = defineProps({
36
36
  }
37
37
  })
38
38
 
39
- const model = defineModel({required: true, default: ""})
39
+ const model = defineModel({required: true })
40
40
 
41
41
  function onInput(event) {
42
42
  const value = typeof event === 'string' ? event : event?.target?.value || ''
@@ -0,0 +1,210 @@
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
12
+ style="max-height: 100vh; overflow-y: auto"
13
+ class="pa-5 my-5 px-7"
14
+ >
15
+ <v-form ref="formRef" v-model="valid" @submit.prevent="submit">
16
+ <v-row no-gutters>
17
+ <v-col cols="12" class="mb-4">
18
+ <label class="text-subtitle-2 font-weight-medium mb-2">
19
+ Name <span class="text-red">*</span>
20
+ </label>
21
+ <v-text-field
22
+ v-model="form.name"
23
+ placeholder="Enter name"
24
+ variant="outlined"
25
+ density="comfortable"
26
+ :rules="[rules.required]"
27
+ hide-details="auto"
28
+ />
29
+ </v-col>
30
+
31
+ <v-col cols="12" class="mb-4">
32
+ <label class="text-subtitle-2 font-weight-medium mb-2">
33
+ Tag ID <span class="text-red">*</span>
34
+ </label>
35
+ <v-text-field
36
+ v-model="form.tagID"
37
+ placeholder="Enter tag id"
38
+ variant="outlined"
39
+ density="comfortable"
40
+ :rules="[rules.required, rules.noSpaces]"
41
+ hide-details="auto"
42
+ @input="handleTagIDInput"
43
+ />
44
+ </v-col>
45
+
46
+ <v-col cols="12" class="mb-4" v-if="mode === 'edit'">
47
+ <label class="text-subtitle-2 font-weight-medium mb-2">
48
+ Status
49
+ </label>
50
+ <v-select
51
+ v-model="form.status"
52
+ :items="statusOptions"
53
+ variant="outlined"
54
+ density="comfortable"
55
+ hide-details="auto"
56
+ />
57
+ </v-col>
58
+ </v-row>
59
+ </v-form>
60
+ </v-card-text>
61
+
62
+ <v-toolbar class="pa-0" density="compact">
63
+ <v-row no-gutters>
64
+ <v-col cols="6" class="pa-0">
65
+ <v-btn
66
+ block
67
+ variant="text"
68
+ class="text-none"
69
+ size="large"
70
+ @click="$emit('cancel')"
71
+ height="48"
72
+ :disabled="loading"
73
+ >
74
+ Cancel
75
+ </v-btn>
76
+ </v-col>
77
+
78
+ <v-col cols="6" class="pa-0">
79
+ <v-btn
80
+ block
81
+ tile
82
+ variant="flat"
83
+ class="text-none"
84
+ size="large"
85
+ height="48"
86
+ color="black"
87
+ @click="submit"
88
+ :loading="loading"
89
+ :disabled="!valid || loading"
90
+ >
91
+ Submit
92
+ </v-btn>
93
+ </v-col>
94
+ </v-row>
95
+ </v-toolbar>
96
+ </v-card>
97
+ </template>
98
+
99
+ <script setup lang="ts">
100
+
101
+ import useNFCPatrolTag from "../../composables/useNFCPatrolTag";
102
+
103
+ const props = defineProps({
104
+ title: {
105
+ type: String,
106
+ default: "Add NFC Tag",
107
+ },
108
+ site: {
109
+ type: String,
110
+ required: true,
111
+ },
112
+ orgId: {
113
+ type: String,
114
+ required: true,
115
+ },
116
+ mode: {
117
+ type: String as PropType<"create" | "edit">,
118
+ default: "create",
119
+ },
120
+ nfcTag: {
121
+ type: Object as PropType<any>,
122
+ default: () => ({
123
+ name: "",
124
+ tagID: "",
125
+ status: "active",
126
+ }),
127
+ },
128
+ });
129
+
130
+ const emit = defineEmits(["cancel", "success", "error"]);
131
+
132
+ const formRef = ref();
133
+ const valid = ref(false);
134
+ const loading = ref(false);
135
+
136
+ const form = ref({
137
+ name: "",
138
+ tagID: "",
139
+ status: "active",
140
+ });
141
+
142
+ const statusOptions = [
143
+ { title: "Active", value: "active" },
144
+ { title: "Inactive", value: "inactive" },
145
+ ];
146
+
147
+ const rules = {
148
+ required: (v: any) => !!v || "This field is required",
149
+ noSpaces: (v: any) => !/\s/.test(v) || "Spaces are not allowed",
150
+ };
151
+
152
+ // Initialize form with existing data in edit mode
153
+ watchEffect(() => {
154
+ if (props.mode === "edit" && props.nfcTag) {
155
+ form.value = {
156
+ name: props.nfcTag.name || "",
157
+ tagID: props.nfcTag.tagID || "",
158
+ status: props.nfcTag.status || "active",
159
+ };
160
+ }
161
+ });
162
+
163
+ const { add: addNFCTag, updateById: updateNFCTag } = useNFCPatrolTag();
164
+
165
+ function handleTagIDInput(event: any) {
166
+ // Convert to uppercase and remove spaces
167
+ const value = event.target.value;
168
+ form.value.tagID = value.toUpperCase().replace(/\s/g, "");
169
+ }
170
+
171
+ async function submit() {
172
+ const { valid: isValid } = await formRef.value.validate();
173
+
174
+ if (!isValid) return;
175
+
176
+ loading.value = true;
177
+
178
+ try {
179
+
180
+ const payload: { site: string; tagID: string; name: string } = {
181
+ site: props.site,
182
+ tagID: form.value.tagID,
183
+ name: form.value.name,
184
+ };
185
+
186
+ if (props.mode === "create") {
187
+ await addNFCTag(payload);
188
+ } else {
189
+ await updateNFCTag(props.nfcTag._id, payload);
190
+ }
191
+
192
+ emit("success");
193
+
194
+
195
+ if (props.mode === "create") {
196
+ form.value = {
197
+ name: "",
198
+ tagID: "",
199
+ status: "active",
200
+ };
201
+ formRef.value.reset();
202
+ }
203
+ } catch (error: any) {
204
+ console.error("Error submitting NFC tag:", error);
205
+ emit("error", error);
206
+ } finally {
207
+ loading.value = false;
208
+ }
209
+ }
210
+ </script>