@iservice365/layer-common 1.4.1 → 1.5.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,26 +1,52 @@
1
1
  <template>
2
- <v-row no-gutters class="w-100 pb-5" @click="resetErrorMessage">
3
- <v-file-upload v-model="uploadFiles" density="compact" @update:model-value="handleUpdateValue"
4
- :loading="processing" :disabled="processing" :height="height" :title="title" :accept="accept"
5
- name="upload_images" class="text-caption w-100" clearable :multiple="multiple">
6
- <template v-slot:item="{ props: itemProps, file }">
7
- <v-file-upload-item v-bind="itemProps" lines="one" nav>
8
- <template v-slot:prepend>
9
- <v-avatar size="32" rounded></v-avatar>
10
- </template>
11
-
12
- <template v-slot:clear="{ props: clearProps }">
13
- <v-btn color="primary" @click="handleRemove(file)"></v-btn>
14
- </template>
15
- </v-file-upload-item>
2
+ <v-row no-gutters class="w-100 pb-5" @click="resetErrorMessage">
3
+
4
+ <!-- VIEW MODE -->
5
+ <template v-if="viewMode">
6
+ <div class="w-100">
7
+ <v-file-upload-item v-for="({ file }, idx) in filesCollection" :key="fileKey(file)" :file="file" lines="one" nav
8
+ @click="handleClickItem(file)">
9
+ <template #prepend>
10
+ <v-avatar size="32" rounded></v-avatar>
11
+ </template>
12
+
13
+ <!-- delete hidden in view mode -->
14
+ <template #clear>
15
+ <!-- empty on purpose -->
16
+ </template>
17
+ </v-file-upload-item>
18
+ </div>
19
+ </template>
20
+
21
+ <!-- NORMAL MODE -->
22
+ <template v-else>
23
+ <v-file-upload v-model="uploadFiles" density="compact" @update:model-value="handleUpdateValue"
24
+ :loading="processing" :disabled="processing" :height="height" :title="title" :accept="accept"
25
+ :name="`upload_images`" class="text-caption w-100" clearable :multiple="multiple">
26
+ <template #item="{ props: itemProps, file }">
27
+ <v-file-upload-item v-bind="itemProps" lines="one" nav @click="handleClickItem(file)">
28
+ <template #prepend>
29
+ <v-avatar size="32" rounded></v-avatar>
16
30
  </template>
17
- </v-file-upload>
18
- <v-row no-gutters class="w-100" v-if="errorMessage">
31
+
32
+ <!-- delete button NOT shown in view mode -->
33
+ <template #clear="{ props: clearProps }">
34
+ <v-btn v-if="!viewMode" color="primary" @click.stop="handleRemove(file)"></v-btn>
35
+ </template>
36
+ </v-file-upload-item>
37
+ </template>
38
+ </v-file-upload>
39
+ </template>
40
+
41
+ <v-row no-gutters class="w-100" v-if="errorMessage">
19
42
  <p class="text-error w-100 text-center text-subtitle-2">{{ errorMessage }}</p>
20
43
  </v-row>
21
- </v-row>
44
+
45
+ <ImageCarousel v-model="showImageCarousel" :images="idsArray" :active-image-id="activeImageId" />
46
+ </v-row>
22
47
  </template>
23
48
 
49
+
24
50
  <script setup lang="ts">
25
51
  import { nextTick, ref, onMounted, watch } from 'vue'
26
52
 
@@ -44,11 +70,18 @@ const props = defineProps({
44
70
  accept: {
45
71
  type: String,
46
72
  default: "image/*"
73
+ },
74
+ viewMode: {
75
+ type: Boolean,
76
+ default: false
47
77
  }
48
78
  })
49
79
 
50
80
  const { addFile, deleteFile, getFileUrl, urlToFile } = useFile()
51
81
 
82
+ const showImageCarousel = ref(false)
83
+ const activeImageId = ref("")
84
+
52
85
  // The parent v-model binding
53
86
  const idsArray = defineModel<string[]>({ default: [] })
54
87
 
@@ -76,6 +109,21 @@ async function handleRemove(removedFile: File) {
76
109
 
77
110
  }
78
111
 
112
+ function handleClickItem(file: File) {
113
+ const isImage =
114
+ file.type.startsWith('image/') ||
115
+ /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(file.name)
116
+
117
+ if (!isImage) return
118
+
119
+ const found = filesCollection.value.find((item) => fileKey(item.file) === fileKey(file))
120
+ if (!found) return
121
+
122
+ activeImageId.value = found.id
123
+ showImageCarousel.value = true
124
+ }
125
+
126
+
79
127
  async function handleUpdateValue(value: File[]) {
80
128
  await nextTick()
81
129
  const max = props.maxLength
@@ -95,12 +143,12 @@ async function handleUpdateValue(value: File[]) {
95
143
  for (const file of addedFiles) {
96
144
  const res = await addFile(file) // should return { id, url }
97
145
  if (res?.id) {
98
- filesCollection.value.push({ file, id: res.id })
146
+ filesCollection.value = [...filesCollection.value, { file, id: res.id }]
99
147
  }
100
148
  }
101
149
 
102
150
  uploadFiles.value = filesCollection.value.map((x) => x.file)
103
- idsArray.value = filesCollection.value.map((x) => x.id)
151
+
104
152
  } catch (err) {
105
153
  console.error('Upload failed', err)
106
154
  errorMessage.value = 'Failed to upload some files.'
@@ -132,33 +180,30 @@ function resetErrorMessage() {
132
180
 
133
181
 
134
182
  onMounted(async () => {
135
- if (idsArray.value.length > 0) {
136
- processing.value = true
137
- const loaded = await loadFilesFromIds(idsArray.value)
138
- filesCollection.value = loaded
139
- uploadFiles.value = loaded.map((x) => x.file)
140
- processing.value = false
141
- }
183
+ setTimeout(async () => {
184
+ if (idsArray.value.length > 0) {
185
+ const loadedFiles = await loadFilesFromIds(idsArray.value)
186
+ filesCollection.value = loadedFiles
187
+ uploadFiles.value = loadedFiles.map((x) => x.file)
188
+ }
189
+ }, 1000)
142
190
  })
143
191
 
192
+ watch(filesCollection, () => {
193
+ idsArray.value = [...filesCollection.value.map(x => x.id)]
194
+ }, { deep: true })
195
+
144
196
 
145
- watch(
146
- filesCollection,
147
- (newVal) => {
148
- idsArray.value = newVal.map((x) => x.id)
149
- },
150
- { deep: true }
151
- )
152
197
  </script>
153
198
 
154
199
 
155
200
  <style scoped>
156
201
  * :deep(.v-file-upload-title) {
157
- font-size: 1rem;
158
- font-weight: 500;
202
+ font-size: 1rem;
203
+ font-weight: 500;
159
204
  }
160
205
 
161
206
  * :deep(.v-file-upload-items) {
162
- min-width: 100%;
207
+ min-width: 100%;
163
208
  }
164
209
  </style>
@@ -235,6 +235,33 @@ const apps = computed(() => {
235
235
  });
236
236
  }
237
237
 
238
+ const _pestControl = "pest_control_services";
239
+
240
+ if (props.app === _pestControl || orgNature.value === _pestControl) {
241
+ items.push({
242
+ title: "Pest Control Services",
243
+ value: _pestControl,
244
+ });
245
+ }
246
+
247
+ const _landscaping = "landscaping_services";
248
+
249
+ if (props.app === _landscaping || orgNature.value === _landscaping) {
250
+ items.push({
251
+ title: "Landscaping Services",
252
+ value: _landscaping,
253
+ });
254
+ }
255
+
256
+ const _poolMaintenance = "pool_maintenance_services";
257
+
258
+ if (props.app === _poolMaintenance || orgNature.value === _poolMaintenance) {
259
+ items.push({
260
+ title: "Pool Maintenance Services",
261
+ value: _poolMaintenance,
262
+ });
263
+ }
264
+
238
265
  return items;
239
266
  });
240
267
 
@@ -1,16 +1,49 @@
1
1
  <template>
2
2
  <v-row no-gutters>
3
3
  <v-col cols="12" class="mb-2">
4
- <v-row no-gutters>
5
- <v-btn
6
- class="text-none mr-2"
7
- rounded="pill"
8
- variant="tonal"
9
- @click="createDialog = true"
10
- size="large"
11
- >
12
- Add Service Provider
13
- </v-btn>
4
+ <v-row no-gutters align="center" justify="space-between">
5
+ <div>
6
+ <v-btn
7
+ class="text-none mr-2"
8
+ rounded="pill"
9
+ variant="tonal"
10
+ @click="(createDialog = true), setServiceProvider({ mode: 'add' })"
11
+ size="large"
12
+ >
13
+ Add
14
+ </v-btn>
15
+ <v-btn
16
+ class="text-none mr-2"
17
+ rounded="pill"
18
+ variant="tonal"
19
+ size="large"
20
+ >
21
+ Invite
22
+ </v-btn>
23
+ <v-btn
24
+ class="text-none mr-2"
25
+ rounded="pill"
26
+ variant="tonal"
27
+ size="large"
28
+ @click="
29
+ useRouter().push({
30
+ name: 'org-site-service-provider-mgmt-billing',
31
+ })
32
+ "
33
+ >
34
+ Billing
35
+ </v-btn>
36
+ </div>
37
+
38
+ <v-text-field
39
+ placeholder="Search..."
40
+ variant="outlined"
41
+ density="comfortable"
42
+ clearable
43
+ hide-details
44
+ class="ml-2"
45
+ style="max-width: 250px"
46
+ />
14
47
  </v-row>
15
48
  </v-col>
16
49
  <v-col cols="12">
@@ -23,7 +56,12 @@
23
56
  >
24
57
  <v-toolbar density="compact" color="grey-lighten-4">
25
58
  <template #prepend>
26
- <v-btn fab icon density="comfortable" @click="getServiceProvider()">
59
+ <v-btn
60
+ fab
61
+ icon
62
+ density="comfortable"
63
+ @click="_getAllServiceProvider()"
64
+ >
27
65
  <v-icon>mdi-refresh</v-icon>
28
66
  </v-btn>
29
67
  </template>
@@ -36,7 +74,7 @@
36
74
  <local-pagination
37
75
  v-model="page"
38
76
  :length="pages"
39
- @update:value="_getServiceProvider()"
77
+ @update:value="_getAllServiceProvider()"
40
78
  />
41
79
  </v-row>
42
80
  </template>
@@ -52,28 +90,8 @@
52
90
  hide-default-header
53
91
  style="max-height: calc(100vh - (180px))"
54
92
  >
55
- <template #item.permissions="{ value }">
56
- <span class="text-caption font-weight-bold text-capitalize">
57
- permissions
58
- </span>
59
- <v-chip>{{ value.length }}</v-chip>
60
- </template>
61
-
62
- <template #item.nature="{ item }">
63
- <span class="text-capitalize">
64
- {{ item.nature }}
65
- </span>
66
- </template>
67
-
68
- <template #item.action-table="{ item }">
69
- <v-menu :close-on-content-click="false" offset-y width="150">
70
- <template v-slot:activator="{ props }">
71
- <v-icon v-bind="props">mdi-dots-horizontal</v-icon>
72
- </template>
73
- <v-list>
74
- <v-list-item @click=""> Delete Service Provider </v-list-item>
75
- </v-list>
76
- </v-menu>
93
+ <template #item.category="{ item }">
94
+ <span class="text-subtitle-2">{{ item.category }}</span>
77
95
  </template>
78
96
  </v-data-table>
79
97
  </v-card>
@@ -81,16 +99,143 @@
81
99
  </v-row>
82
100
 
83
101
  <v-dialog v-model="createDialog" width="500" persistent>
84
- <ServiceProviderFormCreate
102
+ <!-- <ServiceProviderFormCreate
85
103
  @cancel="createDialog = false"
86
104
  :org="props.orgId"
87
105
  :type="props.type"
88
106
  :site-id="props.siteId"
89
107
  :service-provider-org-id="props.orgId"
90
108
  @success="success()"
91
- @success:create-more="getServiceProvider()"
109
+ @success:create-more="_getAllServiceProvider()"
92
110
  @notify="onNotify"
93
- />
111
+ /> -->
112
+ <v-card width="100%">
113
+ <v-toolbar>
114
+ <v-row no-gutters class="fill-height px-6" align="center">
115
+ <span class="font-weight-bold text-h5"> Add Service Provider </span>
116
+ </v-row>
117
+ </v-toolbar>
118
+ <v-card-text style="max-height: 100vh; overflow-y: auto">
119
+ <v-form
120
+ v-model="validServiceProvider"
121
+ :disabled="disableServiceProvider"
122
+ >
123
+ <v-row no-gutters class="px-4">
124
+ <v-col cols="12" class="mt-2">
125
+ <v-row no-gutters>
126
+ <InputLabel class="text-capitalize" title="Name" required />
127
+ <v-col cols="12">
128
+ <v-text-field
129
+ v-model="serviceProvider.name"
130
+ density="comfortable"
131
+ :rules="[requiredRule]"
132
+ ></v-text-field>
133
+ </v-col>
134
+ </v-row>
135
+ </v-col>
136
+
137
+ <v-col cols="12" class="mt-2">
138
+ <v-row no-gutters>
139
+ <InputLabel class="text-capitalize" title="Type" required />
140
+ <v-col cols="12">
141
+ <v-select
142
+ v-model="serviceProvider.type"
143
+ item-title="title"
144
+ item-value="value"
145
+ :items="natureOfBusiness"
146
+ density="comfortable"
147
+ :rules="[requiredRule]"
148
+ ></v-select>
149
+ </v-col>
150
+ </v-row>
151
+ </v-col>
152
+
153
+ <v-col cols="12" class="mt-2">
154
+ <v-row no-gutters>
155
+ <InputLabel class="text-capitalize" title="Email" required />
156
+ <v-col cols="12">
157
+ <v-text-field
158
+ v-model="serviceProvider.email"
159
+ density="comfortable"
160
+ :rules="[emailRule, requiredRule]"
161
+ ></v-text-field>
162
+ </v-col>
163
+ </v-row>
164
+ </v-col>
165
+
166
+ <v-col cols="12" class="mt-2">
167
+ <v-row no-gutters>
168
+ <InputLabel class="text-capitalize" title="Description" />
169
+ <v-col cols="12">
170
+ <v-textarea
171
+ v-model="serviceProvider.description"
172
+ density="comfortable"
173
+ no-resize
174
+ rows="2"
175
+ ></v-textarea>
176
+ </v-col>
177
+ </v-row>
178
+ </v-col>
179
+
180
+ <v-col cols="12">
181
+ <v-checkbox
182
+ v-model="createMoreServiceProvider"
183
+ density="comfortable"
184
+ hide-details
185
+ >
186
+ <template #label>
187
+ <span class="text-subtitle-2 font-weight-bold">
188
+ Create more
189
+ </span>
190
+ </template>
191
+ </v-checkbox>
192
+ </v-col>
193
+
194
+ <v-col cols="12" class="my-2">
195
+ <v-row no-gutters>
196
+ <v-col cols="12" class="text-center">
197
+ <span
198
+ class="text-none text-subtitle-2 font-weight-medium text-error"
199
+ >
200
+ {{ messageServiceProvider }}
201
+ </span>
202
+ </v-col>
203
+ </v-row>
204
+ </v-col>
205
+ </v-row>
206
+ </v-form>
207
+ </v-card-text>
208
+ <v-toolbar>
209
+ <v-row class="px-6">
210
+ <v-col cols="6">
211
+ <v-btn
212
+ block
213
+ variant="text"
214
+ class="text-none"
215
+ size="large"
216
+ @click="setServiceProvider({ mode: 'add', dialog: false })"
217
+ >
218
+ Cancel
219
+ </v-btn>
220
+ </v-col>
221
+
222
+ <v-col cols="6">
223
+ <v-btn
224
+ block
225
+ variant="flat"
226
+ color="black"
227
+ class="text-none"
228
+ size="large"
229
+ :disabled="!validServiceProvider"
230
+ :loading="disableServiceProvider"
231
+ @click="submitServiceProviderAdd"
232
+ >
233
+ Submit
234
+ </v-btn>
235
+ </v-col>
236
+ </v-row>
237
+ </v-toolbar>
238
+ </v-card>
94
239
  </v-dialog>
95
240
 
96
241
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
@@ -130,10 +275,23 @@ const props = defineProps({
130
275
  });
131
276
 
132
277
  const headers = [
133
- { title: "Name", value: "name" },
134
- { title: "Nature", value: "nature" },
278
+ {
279
+ title: "Name",
280
+ value: "name",
281
+ },
282
+ {
283
+ title: "Status",
284
+ value: "status",
285
+ },
286
+ {
287
+ title: "Category",
288
+ value: "category",
289
+ },
135
290
  ];
136
291
 
292
+ const { natureOfBusiness } = useLocal();
293
+ const { requiredRule, emailRule } = useUtils();
294
+
137
295
  const page = ref(1);
138
296
  const pages = ref(0);
139
297
  const pageRange = ref("-- - -- of --");
@@ -144,43 +302,58 @@ const messageColor = ref("");
144
302
 
145
303
  const items = ref<Array<Record<string, any>>>([]);
146
304
 
147
- const { getAll: _getServiceProvider } = useServiceProvider();
305
+ const validServiceProvider = ref(false);
306
+ const disableServiceProvider = ref(false);
307
+ const createMoreServiceProvider = ref(false);
308
+ const messageServiceProvider = ref("");
309
+ const serviceProvider = ref({
310
+ name: "",
311
+ type: "",
312
+ email: "",
313
+ description: "",
314
+ orgId: "",
315
+ siteId: "",
316
+ siteName: "",
317
+ orgName: "",
318
+ category: "",
319
+ });
148
320
 
149
321
  const {
150
- data: getAllReq,
151
- refresh: getServiceProvider,
152
- status: getServiceProviderReqStatus,
153
- } = useLazyAsyncData(
154
- "service-provider-get-all",
155
- () =>
156
- _getServiceProvider({
157
- page: page.value,
158
- orgId: props.orgId,
159
- }),
160
- {
161
- watch: [page],
162
- }
322
+ getAll: getAllServiceProvider,
323
+ add: addServiceProvider,
324
+ invite: inviteServiceProvider,
325
+ } = useServiceProvider();
326
+
327
+ const loading = ref(true);
328
+
329
+ const { getSiteById } = useSite();
330
+
331
+ const { data: site } = await useLazyAsyncData(
332
+ "get-site-by-id-" + props.siteId,
333
+ () => getSiteById(props.siteId)
163
334
  );
164
335
 
165
- const loading = computed(() => getServiceProviderReqStatus.value === "pending");
336
+ const { data: serviceProviderReq, refresh: _getAllServiceProvider } =
337
+ await useLazyAsyncData("get-all-service-providers", () =>
338
+ getAllServiceProvider({ siteId: props.siteId })
339
+ );
166
340
 
167
341
  watchEffect(() => {
168
- if (getAllReq.value) {
169
- items.value = getAllReq.value.items.map((i: any) => ({
170
- ...i,
171
- name: i.name,
172
- nature: i.nature.replace(/_/g, " "),
173
- }));
174
- pages.value = getAllReq.value.pages;
175
- pageRange.value = getAllReq.value.pageRange;
342
+ // console.log("serviceProviderReq", serviceProviderReq.value);
343
+ if (serviceProviderReq.value) {
344
+ items.value = serviceProviderReq.value.items;
345
+ pages.value = serviceProviderReq.value.pages;
346
+ pageRange.value = serviceProviderReq.value.pageRange;
176
347
  }
348
+ loading.value = false;
177
349
  });
178
350
 
179
351
  const createDialog = ref(false);
180
352
 
181
353
  const success = () => {
182
354
  createDialog.value = false;
183
- getServiceProvider();
355
+ // getServiceProvider();
356
+ _getAllServiceProvider();
184
357
  };
185
358
 
186
359
  function showMessage(msg: string, color: string) {
@@ -192,4 +365,61 @@ function showMessage(msg: string, color: string) {
192
365
  function onNotify(payload: { message: string; color: string }) {
193
366
  showMessage(payload.message, payload.color);
194
367
  }
368
+
369
+ const dialogServiceProviderInvite = ref(false);
370
+ const dialogServiceProviderAdd = ref(false);
371
+
372
+ function setServiceProvider({
373
+ mode = "invite",
374
+ dialog = true,
375
+ data = {} as Record<string, any>,
376
+ } = {}) {
377
+ if (mode === "invite") {
378
+ // dialogServiceProviderInvite.value = dialog;
379
+ }
380
+
381
+ if (mode === "add") {
382
+ // dialogServiceProviderAdd.value = dialog;
383
+ createDialog.value = dialog;
384
+ }
385
+
386
+ if (dialog) {
387
+ serviceProvider.value = {
388
+ name: "",
389
+ type: "",
390
+ email: "",
391
+ description: "",
392
+ orgId: props.orgId,
393
+ siteId: props.siteId,
394
+ siteName: site.value?.name ?? "",
395
+ orgName: "Org Name",
396
+ category: mode === "add" ? "internal" : "external",
397
+ };
398
+ return;
399
+ }
400
+
401
+ serviceProvider.value.description = data.description ?? "";
402
+ serviceProvider.value.email = data.email ?? "";
403
+ serviceProvider.value.name = data.name ?? "";
404
+ serviceProvider.value.type = data.type ?? "";
405
+ }
406
+
407
+ async function submitServiceProviderAdd() {
408
+ disableServiceProvider.value = true;
409
+ messageServiceProvider.value = "";
410
+
411
+ try {
412
+ console.log("serviceProvider.value");
413
+ console.log(serviceProvider.value);
414
+ await addServiceProvider(serviceProvider.value);
415
+ await setServiceProvider({ mode: "add", dialog: false });
416
+ await _getAllServiceProvider();
417
+ } catch (error: any) {
418
+ messageServiceProvider.value =
419
+ error?.response?._data?.message ??
420
+ "An error occurred while adding the service provider.";
421
+ } finally {
422
+ disableServiceProvider.value = false;
423
+ }
424
+ }
195
425
  </script>