@iservice365/layer-common 1.0.9 → 1.0.11

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,71 @@
1
+ <template>
2
+ <div class="feedback-detail-wrapper">
3
+ <v-row no-gutters class="fill-height">
4
+ <v-col cols="12" xl="7" lg="7" md="7" class="fill-height">
5
+ <div class="panel-container border-e">
6
+ <ChatMessage :type="'workOrder'" />
7
+ </div>
8
+ </v-col>
9
+ <v-col cols="12" xl="5" lg="5" md="5" class="fill-height">
10
+ <div class="panel-container">
11
+ <ChatInformation
12
+ :item="workOrder"
13
+ :service-providers="_serviceProviders"
14
+ :type="'workOrder'"
15
+ @edit="openEditDialog"
16
+ @mark-complete-request="showCompleteDialog = true"
17
+ @delete="showDeleteDialog = true"
18
+ />
19
+ </div>
20
+ </v-col>
21
+ </v-row>
22
+ </div>
23
+ </template>
24
+ <script lang="ts" setup>
25
+ const route = useRoute();
26
+ const id = route.params.id;
27
+
28
+ const {
29
+ workOrder,
30
+ workOrders,
31
+ getWorkOrderById,
32
+ getWorkOrders: _getWorkOrders,
33
+ } = useWorkOrder();
34
+
35
+ const { getServiceProviderNames } = useServiceProvider();
36
+
37
+ const _getWorkOrderById = async () => {
38
+ try {
39
+ const data = await getWorkOrderById(id as string);
40
+ workOrder.value = data;
41
+ } catch (error) {
42
+ console.error("Error fetching feedback:", error);
43
+ }
44
+ };
45
+
46
+ _getWorkOrderById();
47
+
48
+ const _serviceProviders = ref<TServiceProviderName[]>([]);
49
+
50
+ const _getServiceProviderNames = async () => {
51
+ try {
52
+ const response = await getServiceProviderNames();
53
+ if (!response) return;
54
+
55
+ _serviceProviders.value = response.items.map((provider) => ({
56
+ _id: provider._id as string,
57
+ name: provider.name,
58
+ }));
59
+ } catch (error) {
60
+ console.error("Error fetching service providers:", error);
61
+ }
62
+ };
63
+
64
+ _getServiceProviderNames();
65
+
66
+ const showCreateDialog = ref(false);
67
+ const showCompleteDialog = ref(false);
68
+ const showDeleteDialog = ref(false);
69
+
70
+ function openEditDialog() {}
71
+ </script>
@@ -1,12 +1,12 @@
1
1
  <template>
2
2
  <v-row align="start" justify="center" class="fill-height">
3
- <v-col cols="12" lg="11" md="10">
3
+ <v-col cols="12" lg="12" md="12">
4
4
  <ListView
5
5
  :headers="headers"
6
6
  :items="items"
7
7
  :pages="pages"
8
8
  :page-range="pageRange"
9
- :loading="loading"
9
+ :loading="loading || onNextPrevPageLoading"
10
10
  :height="'calc(100vh - 175px)'"
11
11
  v-model:page="page"
12
12
  :selected="selected"
@@ -14,6 +14,8 @@
14
14
  @update:selected="onSelectedUpdate"
15
15
  @click:create="showCreateDialog = true"
16
16
  :length="pages"
17
+ :clickable-rows="true"
18
+ @update:pagination="updatePage"
17
19
  >
18
20
  <template #title>
19
21
  <span class="text-h6 font-weight-regular">Work Orders</span>
@@ -47,12 +49,34 @@
47
49
  {{ item.status || "No Status" }}
48
50
  </v-chip>
49
51
  </template>
52
+ <template #action-table="{ item }">
53
+ <v-menu
54
+ v-model="item.menuOpen"
55
+ :close-on-content-click="false"
56
+ offset-y
57
+ width="150"
58
+ >
59
+ <template v-slot:activator="{ props }">
60
+ <v-icon v-bind="props">mdi-dots-horizontal-circle-outline</v-icon>
61
+ </template>
62
+ <v-list>
63
+ <v-list-item @click="onViewWorkOrder(item)">View</v-list-item>
64
+ <v-list-item @click="editWorkOrder(item)">Edit</v-list-item>
65
+ <v-list-item
66
+ @click="confirmDeleteWorkOrder(item)"
67
+ v-if="canDeleteWorkOrder"
68
+ >Delete</v-list-item
69
+ >
70
+ </v-list>
71
+ </v-menu>
72
+ </template>
50
73
  </ListView>
51
74
  </v-col>
52
75
  </v-row>
53
76
 
54
77
  <WorkOrderCreate
55
78
  v-model="showCreateDialog"
79
+ :created-from="'workOrder'"
56
80
  :work-order="_workOrder"
57
81
  @update:work-order="(val: TWorkOrderCreate) => (_workOrder = val)"
58
82
  :is-edit-mode="isEditMode"
@@ -75,7 +99,7 @@
75
99
  import { useTheme } from "vuetify";
76
100
  const emit = defineEmits(["click:create", "update:pagination"]);
77
101
 
78
- defineProps({
102
+ const props = defineProps({
79
103
  detailRoute: {
80
104
  type: String,
81
105
  default: "index",
@@ -149,6 +173,7 @@ const headers = [
149
173
  { title: "Category", value: "category", align: "start" },
150
174
  { title: "Date", value: "createdAt", align: "start" },
151
175
  { title: "Status", value: "status", align: "start" },
176
+ { title: "", value: "action-table", align: "end" },
152
177
  ];
153
178
 
154
179
  const submitting = ref(false);
@@ -176,6 +201,7 @@ const {
176
201
  );
177
202
 
178
203
  const loading = computed(() => getAllReqStatus.value === "pending");
204
+ const onNextPrevPageLoading = ref(false);
179
205
 
180
206
  watchEffect(() => {
181
207
  if (getAllWorkOrderReq.value) {
@@ -184,6 +210,23 @@ watchEffect(() => {
184
210
  pageRange.value = getAllWorkOrderReq.value.pageRange;
185
211
  }
186
212
  });
213
+
214
+ async function updatePage(pageVal: any) {
215
+ onNextPrevPageLoading.value = true;
216
+ page.value = pageVal;
217
+ const response = await _getWorkOrders({
218
+ page: page.value,
219
+ organization: route.params.org as string,
220
+ site: route.params.site as string,
221
+ });
222
+ if (response) {
223
+ items.value = response.items;
224
+ pages.value = response.pages;
225
+ pageRange.value = response.pageRange;
226
+ onNextPrevPageLoading.value = false;
227
+ }
228
+ }
229
+
187
230
  function onSelectedUpdate(newSelected: string[]) {
188
231
  selected.value = newSelected;
189
232
  }
@@ -305,4 +348,20 @@ async function submitWorkOrder() {
305
348
  isSubmitting.value = false;
306
349
  }
307
350
  }
351
+
352
+ function onViewWorkOrder(item: any) {
353
+ const route = useRoute();
354
+ const org = route.params.org;
355
+ const customer = route.params.customer;
356
+ const site = route.params.site;
357
+ const id = item._id;
358
+ useRouter().push({
359
+ name: props.detailRoute,
360
+ params: { org, site, id },
361
+ });
362
+ }
363
+
364
+ function editWorkOrder(item: any) {}
365
+
366
+ function confirmDeleteWorkOrder(item: any) {}
308
367
  </script>
@@ -1,11 +1,20 @@
1
1
  export default function useCustomerSite() {
2
2
  function add(payload: TCustomerSite) {
3
- return useNuxtApp().$api<Record<string, any>>(`/api/customer-sites`, {
3
+ return useNuxtApp().$api<Record<string, any>>("/api/customer-sites", {
4
4
  method: "POST",
5
5
  body: payload,
6
6
  });
7
7
  }
8
8
 
9
+ function addViaInvite(invite: string) {
10
+ return useNuxtApp().$api<Record<string, any>>(
11
+ `/api/customer-sites/invite/${invite}`,
12
+ {
13
+ method: "POST",
14
+ }
15
+ );
16
+ }
17
+
9
18
  async function getAll({
10
19
  page = 1,
11
20
  search = "",
@@ -32,5 +41,6 @@ export default function useCustomerSite() {
32
41
  return {
33
42
  add,
34
43
  getAll,
44
+ addViaInvite,
35
45
  };
36
46
  }
@@ -10,6 +10,7 @@ export default function useFeedback() {
10
10
  _id: "",
11
11
  attachments: [],
12
12
  category: "",
13
+ categoryInfo: "",
13
14
  subject: "",
14
15
  location: "",
15
16
  description: "",
@@ -27,6 +28,7 @@ export default function useFeedback() {
27
28
  attachments: "",
28
29
  completedAt: "",
29
30
  },
31
+ workOrderNo: "",
30
32
  })
31
33
  );
32
34
 
@@ -1,6 +1,6 @@
1
1
  export default function useFile() {
2
2
  const baseUrl =
3
- "https://seven365-storage.sgp1.cdn.digitaloceanspaces.com/dev";
3
+ "https://iservice365-dev.sgp1.cdn.digitaloceanspaces.com/default";
4
4
 
5
5
  function addFile(file: File | null) {
6
6
  if (!file) {
@@ -23,6 +23,16 @@ export default function useFile() {
23
23
  return `${baseUrl}/${id}`;
24
24
  }
25
25
 
26
+ async function urlToFile(url: string, filename: string): Promise<File> {
27
+ const response = await fetch(url)
28
+ const blob = await response.blob()
29
+
30
+ // Try to extract MIME type if possible (e.g. image/png)
31
+ const mimeType = blob.type || 'image/jpeg'
32
+
33
+ return new File([blob], filename, { type: mimeType })
34
+ }
35
+
26
36
  function deleteFile(attachmentId: string) {
27
37
  return useNuxtApp().$api<Record<string, any>>(
28
38
  `/api/files/${attachmentId}`,
@@ -35,6 +45,7 @@ export default function useFile() {
35
45
  return {
36
46
  addFile,
37
47
  deleteFile,
48
+ urlToFile,
38
49
  getFileUrl,
39
50
  };
40
51
  }
@@ -256,6 +256,7 @@ export default function useUtils() {
256
256
 
257
257
  const search = useState("search", () => "");
258
258
 
259
+ //returns Oct 16, 2025, 03:45 PM // N/A format
259
260
  function formatDate(dateString: string) {
260
261
  if (!dateString) return "N/A";
261
262
 
@@ -269,6 +270,23 @@ export default function useUtils() {
269
270
  });
270
271
  }
271
272
 
273
+ // returns 16/10/2025, 15:45 format
274
+ function UTCToLocalTIme(UTCDateTime: string) {
275
+ if (!UTCDateTime) return "";
276
+ const local = new Date(UTCDateTime);
277
+
278
+ const formatted = local.toLocaleString("en-GB", {
279
+ day: "2-digit",
280
+ month: "2-digit",
281
+ year: "numeric",
282
+ hour: "2-digit",
283
+ minute: "2-digit",
284
+ hour12: false, // 24-hour format
285
+ });
286
+
287
+ return formatted;
288
+ }
289
+
272
290
  function formatNature(value: string): string {
273
291
  if (!value) return "";
274
292
  return value
@@ -341,6 +359,7 @@ export default function useUtils() {
341
359
  }
342
360
 
343
361
  function formatCamelCaseToWords(key: string){
362
+ if(!key) return "";
344
363
  return key
345
364
  .replace(/([A-Z])/g, ' $1')
346
365
  .replace(/^./, str => str.toUpperCase());
@@ -372,6 +391,7 @@ export default function useUtils() {
372
391
  getOrigin,
373
392
  search,
374
393
  formatDate,
394
+ UTCToLocalTIme,
375
395
  formatNature,
376
396
  replaceMatch,
377
397
  setRouteParams,
@@ -1,10 +1,12 @@
1
- export default function useFeedback() {
1
+ export default function useWorkOrder() {
2
2
  const workOrders = useState<Array<TWorkOrder>>("workOrders", () => []);
3
3
  const page = useState("page", () => 1);
4
4
  const pages = useState("pages", () => 0);
5
5
  const pageRange = useState("pageRange", () => "-- - -- of --");
6
6
  const workOrder = useState<TWorkOrder>("workOrder", () => ({
7
7
  _id: "",
8
+ category: "",
9
+ categoryInfo: "",
8
10
  subject: "",
9
11
  description: "",
10
12
  createdBy: "",
@@ -49,6 +51,12 @@ export default function useFeedback() {
49
51
  }
50
52
  }
51
53
 
54
+ function getWorkOrderById(id: string) {
55
+ return useNuxtApp().$api<TWorkOrder>(`/api/work-orders/${id}`, {
56
+ method: "GET",
57
+ });
58
+ }
59
+
52
60
  function createWorkOrder(payload: TWorkOrderCreate) {
53
61
  return useNuxtApp().$api<Record<string, any>>("/api/work-orders", {
54
62
  method: "POST",
@@ -64,5 +72,6 @@ export default function useFeedback() {
64
72
  pageRange,
65
73
  createWorkOrder,
66
74
  getWorkOrders,
75
+ getWorkOrderById,
67
76
  };
68
77
  }
@@ -9,7 +9,10 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
9
9
 
10
10
  const { organization, org } = to.params;
11
11
 
12
+ // console.log('org-aut-middleware running', hexSchema.safeParse(organization).success)
13
+
12
14
  if (!hexSchema.safeParse(organization || org).success) {
15
+ console.log('[02.org] middleware run')
13
16
  return navigateTo(
14
17
  { name: "require-organization-membership" },
15
18
  { replace: true }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@iservice365/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "1.0.9",
5
+ "version": "1.0.11",
6
6
  "main": "./nuxt.config.ts",
7
7
  "scripts": {
8
8
  "dev": "nuxi dev .playground",
@@ -1,3 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ const hexSchema = z
4
+ .string()
5
+ .regex(/^[0-9a-fA-F]{24}$/, "Invalid organization ID");
6
+
1
7
  export default defineNuxtPlugin(() => {
2
8
  const router = useRouter();
3
9
  const { getByUserType } = useMember();
@@ -17,17 +23,24 @@ export default defineNuxtPlugin(() => {
17
23
  (to.params.org as string) || (to.params.organization as string) || ""
18
24
  );
19
25
 
26
+ console.log('[secure-member-plugin', 'org', org.value)
27
+
28
+ if (!hexSchema.safeParse(org.value).success) {
29
+ return router.replace({ name: "require-organization-membership" });
30
+ }
31
+
20
32
  const userId = computed(() => useCookie("user").value ?? "");
21
33
 
22
34
  const { data: userMemberData, error: userMemberError } =
23
35
  await useLazyAsyncData(
24
- "get-member-by-id",
36
+ "plugin-get-member-by-id-" + userId.value + "-" + APP + "-" + org.value,
25
37
  () => getByUserType(userId.value, APP, org.value),
26
38
  { watch: [userId] }
27
39
  );
28
40
 
29
41
  watchEffect(() => {
30
42
  if (userMemberError.value) {
43
+ console.log('running-secure-member-redirect-plugin')
31
44
  navigateTo(
32
45
  {
33
46
  name: "index",
@@ -37,14 +50,17 @@ export default defineNuxtPlugin(() => {
37
50
  }
38
51
  });
39
52
 
53
+ const roleId = ref("roleId");
54
+
40
55
  watchEffect(() => {
41
56
  if (userMemberData.value) {
42
57
  id.value = userMemberData.value.org ?? "";
58
+ roleId.value = userMemberData.value.role ?? "roleId";
43
59
  }
44
60
  });
45
61
 
46
62
  const { data: getOrgByIdReq } = await useLazyAsyncData(
47
- "get-org-by-id",
63
+ "plugin-get-org-by-id-" + org.value,
48
64
  () => getById(org.value),
49
65
  { watch: [org] }
50
66
  );
@@ -55,12 +71,10 @@ export default defineNuxtPlugin(() => {
55
71
  }
56
72
  });
57
73
 
58
- const roleId = computed(() => userMemberData.value?.role ?? "");
59
-
60
74
  const { data: getRoleByIdReq } = await useLazyAsyncData(
61
- "get-role-by-id",
75
+ "plugin-get-role-by-id-" + roleId.value,
62
76
  () => getRoleById(roleId.value),
63
- { watch: [roleId], immediate: false }
77
+ { watch: [roleId] }
64
78
  );
65
79
 
66
80
  watchEffect(() => {
@@ -1,5 +1,6 @@
1
1
  // import this after install `@mdi/font` package
2
2
  import "@mdi/font/css/materialdesignicons.css";
3
+ import { VFileUpload, VFileUploadItem } from 'vuetify/labs/VFileUpload'
3
4
 
4
5
  import "vuetify/styles";
5
6
  import { createVuetify } from "vuetify";
@@ -46,6 +47,10 @@ export default defineNuxtPlugin((app) => {
46
47
  },
47
48
  },
48
49
  },
50
+ components: {
51
+ VFileUpload,
52
+ VFileUploadItem
53
+ }
49
54
  });
50
55
 
51
56
  app.vueApp.use(vuetify);
@@ -2,6 +2,7 @@ declare type TFeedback = {
2
2
  _id: string;
3
3
  attachments?: string[];
4
4
  category: string;
5
+ categoryInfo?: string;
5
6
  subject: string;
6
7
  location: string;
7
8
  description: string;
@@ -19,6 +20,8 @@ declare type TFeedback = {
19
20
  serviceProvider?: string;
20
21
  assignee?: string;
21
22
  createdBy?: string;
23
+ workOrderNo?: string;
24
+ workOrderId?: string;
22
25
  };
23
26
 
24
27
  declare type TFeedbackMetadata = {
@@ -1,6 +1,8 @@
1
1
  declare type TWorkOrder = {
2
2
  _id: string;
3
3
  subject: string;
4
+ category: string;
5
+ categoryInfo?: string;
4
6
  description: string;
5
7
  createdBy: string;
6
8
  createdByName?: string;