@iservice365/layer-common 1.5.5 → 1.5.6

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.5.6
4
+
5
+ ### Patch Changes
6
+
7
+ - ec81e23: Dashboard static data initial release
8
+
3
9
  ## 1.5.5
4
10
 
5
11
  ### Patch Changes
@@ -3,7 +3,9 @@
3
3
  <!-- Header -->
4
4
  <v-row no-gutters class="mb-6">
5
5
  <v-col cols="12">
6
- <h1 class="text-h4 text-md-h3 font-weight-bold">Dashboard</h1>
6
+ <h1 class="text-h4 text-md-h3 font-weight-bold">
7
+ Dashboard{{ currentSiteName ? ` - ${currentSiteName}` : "" }}
8
+ </h1>
7
9
  </v-col>
8
10
  </v-row>
9
11
 
@@ -208,7 +210,24 @@
208
210
  Feedback Tickets
209
211
  </h3>
210
212
  </v-col>
211
- <v-col cols="12" md="6" class="d-flex justify-md-end">
213
+ <v-col
214
+ cols="12"
215
+ md="6"
216
+ class="d-flex flex-column flex-md-row justify-md-end gap-2"
217
+ >
218
+ <v-select
219
+ v-model="selectedService"
220
+ :items="serviceList"
221
+ item-title="name"
222
+ item-value="_id"
223
+ label="Select Service"
224
+ density="compact"
225
+ hide-details
226
+ variant="outlined"
227
+ style="max-width: 200px"
228
+ clearable
229
+ class="pr-4"
230
+ ></v-select>
212
231
  <v-select
213
232
  v-model="feedbackPeriod"
214
233
  :items="['This Week', 'This Month', 'This Year']"
@@ -353,8 +372,23 @@
353
372
  </p>
354
373
  </div>
355
374
 
356
- <!-- Dropdown -->
357
- <div class="mb-4">
375
+ <!-- Dropdowns -->
376
+ <div class="mb-4 d-flex flex-column gap-2">
377
+ <!-- Facility/Building selector for Bookings only -->
378
+ <v-select
379
+ v-if="pie.id === 1"
380
+ v-model="selectedBooking"
381
+ :items="bookingsList"
382
+ item-title="name"
383
+ item-value="_id"
384
+ label="Select Facility"
385
+ density="compact"
386
+ hide-details
387
+ variant="outlined"
388
+ clearable
389
+ class="pb-4"
390
+ ></v-select>
391
+ <!-- Period selector -->
358
392
  <v-select
359
393
  :model-value="pie.period"
360
394
  :items="['This Week', 'This Month', 'This Year']"
@@ -449,14 +483,41 @@
449
483
  </template>
450
484
 
451
485
  <script setup lang="ts">
452
- import { ref, computed } from "vue";
453
486
  import { useDisplay } from "vuetify";
454
487
 
455
488
  const display = useDisplay();
489
+ const { getServiceProviderNames } = useServiceProvider();
490
+ const { getAll: getAllBuildings } = useBuilding();
491
+ const { getSiteById } = useSite();
492
+ const route = useRoute();
493
+
494
+ // Get siteId from route params
495
+ const siteId = computed(() => (route.params.site as string) || "");
496
+
497
+ // Reactive site data that updates when siteId changes
498
+ const siteData = ref<any>(null);
499
+
500
+ // Fetch site data whenever siteId changes
501
+ watchEffect(async () => {
502
+ if (siteId.value) {
503
+ try {
504
+ const site = await getSiteById(siteId.value);
505
+ siteData.value = site;
506
+ } catch (error) {
507
+ console.error("Error fetching site:", error);
508
+ siteData.value = null;
509
+ }
510
+ } else {
511
+ siteData.value = null;
512
+ }
513
+ });
514
+
515
+ // Get current site name from fetched site data
516
+ const currentSiteName = computed(() => siteData.value?.name || "");
456
517
 
457
518
  // Type definitions
458
- type Period = 'This Week' | 'This Month' | 'This Year';
459
- type CardType = 'guest' | 'pickup' | 'dropoff' | 'contractor' | 'delivery';
519
+ type Period = "This Week" | "This Month" | "This Year";
520
+ type CardType = "guest" | "pickup" | "dropoff" | "contractor" | "delivery";
460
521
 
461
522
  interface CardPeriods {
462
523
  guest: Period;
@@ -477,6 +538,14 @@ const bookingsPeriod = ref<Period>("This Week");
477
538
  const buildingPeriod = ref<Period>("This Week");
478
539
  const workOrdersPeriod = ref<Period>("This Week");
479
540
 
541
+ // Service provider states
542
+ const selectedService = ref<any>(null);
543
+ const serviceList = ref<Array<{ _id: string; name: string }>>([]);
544
+
545
+ // Booking filter - list of facilities/buildings
546
+ const selectedBooking = ref<any>(null);
547
+ const bookingsList = ref<Array<{ _id: string; name: string }>>([]);
548
+
480
549
  // Individual card periods
481
550
  const cardPeriods = ref<CardPeriods>({
482
551
  guest: "This Week",
@@ -487,7 +556,7 @@ const cardPeriods = ref<CardPeriods>({
487
556
  });
488
557
 
489
558
  // Data sets for different periods
490
- const dataByPeriod = {
559
+ const dataByPeriod: Record<string, any> = {
491
560
  "This Week": {
492
561
  guest: { value: "156", percentage: "12.5 %", chipColor: "success" },
493
562
  pickup: { value: "42", percentage: "8.3 %", chipColor: "success" },
@@ -799,60 +868,83 @@ const dataByPeriod = {
799
868
  },
800
869
  };
801
870
 
802
- // Computed properties for reactive data
803
- const countCardList = computed(() => [
804
- {
805
- id: 1,
806
- label: "Guest",
807
- value: dataByPeriod[cardPeriods.value.guest].guest.value,
808
- icon: "mdi-account-multiple",
809
- color: "primary",
810
- percentage: dataByPeriod[cardPeriods.value.guest].guest.percentage,
811
- chipColor: dataByPeriod[cardPeriods.value.guest].guest.chipColor,
812
- period: cardPeriods.value.guest,
813
- },
814
- {
815
- id: 2,
816
- label: "Pickup",
817
- value: dataByPeriod[cardPeriods.value.pickup].pickup.value,
818
- icon: "mdi-package",
819
- color: "error",
820
- percentage: dataByPeriod[cardPeriods.value.pickup].pickup.percentage,
821
- chipColor: dataByPeriod[cardPeriods.value.pickup].pickup.chipColor,
822
- period: cardPeriods.value.pickup,
823
- },
824
- {
825
- id: 3,
826
- label: "Drop-off",
827
- value: dataByPeriod[cardPeriods.value.dropoff].dropoff.value,
828
- icon: "mdi-package-down",
829
- color: "warning",
830
- percentage: dataByPeriod[cardPeriods.value.dropoff].dropoff.percentage,
831
- chipColor: dataByPeriod[cardPeriods.value.dropoff].dropoff.chipColor,
832
- period: cardPeriods.value.dropoff,
833
- },
834
- {
835
- id: 4,
836
- label: "Contractor",
837
- value: dataByPeriod[cardPeriods.value.contractor].contractor.value,
838
- icon: "mdi-hard-hat",
839
- color: "success",
840
- percentage:
841
- dataByPeriod[cardPeriods.value.contractor].contractor.percentage,
842
- chipColor: dataByPeriod[cardPeriods.value.contractor].contractor.chipColor,
843
- period: cardPeriods.value.contractor,
844
- },
845
- {
846
- id: 5,
847
- label: "Delivery",
848
- value: dataByPeriod[cardPeriods.value.delivery].delivery.value,
849
- icon: "mdi-truck",
850
- color: "grey",
851
- percentage: dataByPeriod[cardPeriods.value.delivery].delivery.percentage,
852
- chipColor: dataByPeriod[cardPeriods.value.delivery].delivery.chipColor,
853
- period: cardPeriods.value.delivery,
854
- },
855
- ]);
871
+ // Computed properties for reactive data with site-based multiplier
872
+ const countCardList = computed(() => {
873
+ // Calculate multiplier based on siteId (different sites get different multipliers)
874
+ const siteHash = siteId.value
875
+ ? siteId.value.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
876
+ : 0;
877
+ const siteMultiplier = siteId.value ? 0.6 + (siteHash % 40) / 100 : 1; // Between 0.6 and 1.0
878
+
879
+ const applyMultiplier = (value: string) => {
880
+ const numValue = parseInt(value.replace(/,/g, ""));
881
+ const newValue = Math.round(numValue * siteMultiplier);
882
+ return newValue >= 1000 ? newValue.toLocaleString() : newValue.toString();
883
+ };
884
+
885
+ return [
886
+ {
887
+ id: 1,
888
+ label: "Guest",
889
+ value: applyMultiplier(dataByPeriod[cardPeriods.value.guest].guest.value),
890
+ icon: "mdi-account-multiple",
891
+ color: "primary",
892
+ percentage: dataByPeriod[cardPeriods.value.guest].guest.percentage,
893
+ chipColor: dataByPeriod[cardPeriods.value.guest].guest.chipColor,
894
+ period: cardPeriods.value.guest,
895
+ },
896
+ {
897
+ id: 2,
898
+ label: "Pickup",
899
+ value: applyMultiplier(
900
+ dataByPeriod[cardPeriods.value.pickup].pickup.value
901
+ ),
902
+ icon: "mdi-package",
903
+ color: "error",
904
+ percentage: dataByPeriod[cardPeriods.value.pickup].pickup.percentage,
905
+ chipColor: dataByPeriod[cardPeriods.value.pickup].pickup.chipColor,
906
+ period: cardPeriods.value.pickup,
907
+ },
908
+ {
909
+ id: 3,
910
+ label: "Drop-off",
911
+ value: applyMultiplier(
912
+ dataByPeriod[cardPeriods.value.dropoff].dropoff.value
913
+ ),
914
+ icon: "mdi-package-down",
915
+ color: "warning",
916
+ percentage: dataByPeriod[cardPeriods.value.dropoff].dropoff.percentage,
917
+ chipColor: dataByPeriod[cardPeriods.value.dropoff].dropoff.chipColor,
918
+ period: cardPeriods.value.dropoff,
919
+ },
920
+ {
921
+ id: 4,
922
+ label: "Contractor",
923
+ value: applyMultiplier(
924
+ dataByPeriod[cardPeriods.value.contractor].contractor.value
925
+ ),
926
+ icon: "mdi-hard-hat",
927
+ color: "success",
928
+ percentage:
929
+ dataByPeriod[cardPeriods.value.contractor].contractor.percentage,
930
+ chipColor:
931
+ dataByPeriod[cardPeriods.value.contractor].contractor.chipColor,
932
+ period: cardPeriods.value.contractor,
933
+ },
934
+ {
935
+ id: 5,
936
+ label: "Delivery",
937
+ value: applyMultiplier(
938
+ dataByPeriod[cardPeriods.value.delivery].delivery.value
939
+ ),
940
+ icon: "mdi-truck",
941
+ color: "grey",
942
+ percentage: dataByPeriod[cardPeriods.value.delivery].delivery.percentage,
943
+ chipColor: dataByPeriod[cardPeriods.value.delivery].delivery.chipColor,
944
+ period: cardPeriods.value.delivery,
945
+ },
946
+ ];
947
+ });
856
948
 
857
949
  // Computed reactive data
858
950
  const visitorPoints = computed(
@@ -868,9 +960,34 @@ const visitorLabels = [
868
960
  { x: 1100, text: "Sat" },
869
961
  ];
870
962
 
871
- const feedbackBars = computed(
872
- () => dataByPeriod[feedbackPeriod.value].feedback
873
- );
963
+ const feedbackBars = computed(() => {
964
+ const baseFeedback = dataByPeriod[feedbackPeriod.value].feedback;
965
+
966
+ // If no service is selected, return the base data
967
+ if (!selectedService.value) {
968
+ return baseFeedback;
969
+ }
970
+
971
+ // If a service is selected, multiply values by a consistent factor
972
+ // Use the service ID to generate a consistent multiplier for that service
973
+ const serviceHash = selectedService.value
974
+ .split("")
975
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0);
976
+ const multiplier = 0.3 + (serviceHash % 50) / 100; // Between 0.3 and 0.8
977
+
978
+ return baseFeedback.map((bar: any) => {
979
+ const newValue = Math.round(bar.value * multiplier);
980
+ const newHeight = bar.height * multiplier;
981
+ const newY = 290 - newHeight;
982
+
983
+ return {
984
+ ...bar,
985
+ value: newValue,
986
+ height: newHeight,
987
+ y: newY,
988
+ };
989
+ });
990
+ });
874
991
 
875
992
  // Function to update individual card periods
876
993
  const updateCardPeriod = (cardType: string, period: string) => {
@@ -886,6 +1003,19 @@ const updatePiePeriod = (pieId: number, period: string) => {
886
1003
  else if (pieId === 3) workOrdersPeriod.value = validPeriod;
887
1004
  };
888
1005
 
1006
+ // Function to fetch bookings data based on selected facility
1007
+ const fetchBookingsData = async () => {
1008
+ if (!selectedBooking.value) {
1009
+ // Reset to base data when no booking selected
1010
+ return;
1011
+ }
1012
+ };
1013
+
1014
+ // Watch for booking selection changes
1015
+ watch(selectedBooking, () => {
1016
+ fetchBookingsData();
1017
+ });
1018
+
889
1019
  // Pie chart data by period
890
1020
  const pieChartDataByPeriod = {
891
1021
  "This Week": {
@@ -1070,67 +1200,192 @@ const pieChartDataByPeriod = {
1070
1200
  };
1071
1201
 
1072
1202
  // Computed Pie Charts Data
1073
- const pieChartList = computed(() => [
1074
- {
1075
- id: 1,
1076
- title: "Bookings",
1077
- total: pieChartDataByPeriod[bookingsPeriod.value].bookings.total,
1078
- centerValue:
1079
- pieChartDataByPeriod[bookingsPeriod.value].bookings.centerValue,
1080
- segments: pieChartDataByPeriod[bookingsPeriod.value].bookings.segments,
1081
- period: bookingsPeriod.value,
1082
- },
1083
- {
1084
- id: 2,
1085
- title: "Building Mngm.",
1086
- total: pieChartDataByPeriod[buildingPeriod.value].building.total,
1087
- centerValue:
1088
- pieChartDataByPeriod[buildingPeriod.value].building.centerValue,
1089
- segments: pieChartDataByPeriod[buildingPeriod.value].building.segments,
1090
- period: buildingPeriod.value,
1091
- },
1092
- {
1093
- id: 3,
1094
- title: "Work Orders",
1095
- total: pieChartDataByPeriod[workOrdersPeriod.value].workOrders.total,
1096
- centerValue:
1097
- pieChartDataByPeriod[workOrdersPeriod.value].workOrders.centerValue,
1098
- segments: pieChartDataByPeriod[workOrdersPeriod.value].workOrders.segments,
1099
- period: workOrdersPeriod.value,
1100
- },
1101
- ]);
1203
+ const pieChartList = computed(() => {
1204
+ // Get base bookings data
1205
+ const baseBookingsData = pieChartDataByPeriod[bookingsPeriod.value].bookings;
1206
+ let bookingsData = { ...baseBookingsData };
1207
+
1208
+ if (selectedBooking.value) {
1209
+ const bookingHash = selectedBooking.value
1210
+ .split("")
1211
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0);
1212
+ const multiplier = 0.35 + (bookingHash % 45) / 100; // Between 0.35 and 0.8
1213
+ const newTotal = Math.round(baseBookingsData.total * multiplier);
1214
+
1215
+ const segmentMultipliers = [
1216
+ 0.8 + (bookingHash % 15) / 100, // approved: 0.8-0.95
1217
+ 0.4 + (bookingHash % 30) / 100, // rejected: 0.4-0.7
1218
+ 0.5 + (bookingHash % 25) / 100, // pending: 0.5-0.75
1219
+ ];
1220
+
1221
+ const newSegments = baseBookingsData.segments.map(
1222
+ (seg: any, idx: number) => {
1223
+ const segMultiplier = segmentMultipliers[idx] || multiplier;
1224
+ const newPercentage = seg.percentage * segMultiplier;
1225
+ return {
1226
+ ...seg,
1227
+ percentage: newPercentage,
1228
+ };
1229
+ }
1230
+ );
1231
+
1232
+ // Normalize percentages to total 100% (make full circle)
1233
+ const totalPercentage = newSegments.reduce(
1234
+ (sum: number, seg: any) => sum + seg.percentage,
1235
+ 0
1236
+ );
1237
+ const normalizedSegments = newSegments.map((seg: any) => ({
1238
+ ...seg,
1239
+ percentage: (seg.percentage / totalPercentage) * 100,
1240
+ }));
1241
+
1242
+ // Recalculate rotations based on normalized percentages
1243
+ let currentRotation = -90;
1244
+ const adjustedSegments = normalizedSegments.map((seg: any) => {
1245
+ const segmentWithRotation = {
1246
+ ...seg,
1247
+ rotation: currentRotation,
1248
+ };
1249
+ currentRotation += seg.percentage * 3.6; // 360 degrees / 100 percentage
1250
+ return segmentWithRotation;
1251
+ });
1102
1252
 
1103
- // Facility Bookings Data
1104
- const facilityBookings = [
1253
+ // Calculate new center value based on the largest segment
1254
+ const maxSegment = adjustedSegments.reduce((max: any, seg: any) =>
1255
+ seg.percentage > max.percentage ? seg : max
1256
+ );
1257
+ const newCenterValue = `${Math.round(maxSegment.percentage)}%`;
1258
+
1259
+ bookingsData = {
1260
+ total: newTotal,
1261
+ centerValue: newCenterValue,
1262
+ segments: adjustedSegments,
1263
+ };
1264
+ }
1265
+
1266
+ return [
1267
+ {
1268
+ id: 1,
1269
+ title: "Bookings",
1270
+ total: bookingsData.total,
1271
+ centerValue: bookingsData.centerValue,
1272
+ segments: bookingsData.segments,
1273
+ period: bookingsPeriod.value,
1274
+ },
1275
+ {
1276
+ id: 2,
1277
+ title: "Building Mngm.",
1278
+ total: pieChartDataByPeriod[buildingPeriod.value].building.total,
1279
+ centerValue:
1280
+ pieChartDataByPeriod[buildingPeriod.value].building.centerValue,
1281
+ segments: pieChartDataByPeriod[buildingPeriod.value].building.segments,
1282
+ period: buildingPeriod.value,
1283
+ },
1284
+ {
1285
+ id: 3,
1286
+ title: "Work Orders",
1287
+ total: pieChartDataByPeriod[workOrdersPeriod.value].workOrders.total,
1288
+ centerValue:
1289
+ pieChartDataByPeriod[workOrdersPeriod.value].workOrders.centerValue,
1290
+ segments:
1291
+ pieChartDataByPeriod[workOrdersPeriod.value].workOrders.segments,
1292
+ period: workOrdersPeriod.value,
1293
+ },
1294
+ ];
1295
+ });
1296
+
1297
+ // Function to fetch feedback data based on selected service
1298
+ const fetchFeedbackData = async () => {
1299
+ if (!selectedService.value) {
1300
+ // Reset to base data when no service selected
1301
+ return;
1302
+ }
1303
+ };
1304
+
1305
+ // Watch for service selection changes
1306
+ watch(selectedService, () => {
1307
+ fetchFeedbackData();
1308
+ });
1309
+
1310
+ // Fetch service providers and buildings/facilities on mount
1311
+ onMounted(async () => {
1312
+ try {
1313
+ // Fetch service providers for feedback dropdown
1314
+ const serviceResponse = await getServiceProviderNames({ limit: 100 });
1315
+ if (serviceResponse && serviceResponse.items) {
1316
+ serviceList.value = serviceResponse.items;
1317
+ }
1318
+
1319
+ // Fetch buildings/facilities for bookings dropdown
1320
+ // Using buildings as facilities - you can adjust to use actual facilities API
1321
+ const buildingsResponse = await getAllBuildings({
1322
+ limit: 100,
1323
+ status: "active", // Only show active buildings
1324
+ });
1325
+ if (buildingsResponse && buildingsResponse.items) {
1326
+ bookingsList.value = buildingsResponse.items.map((building: any) => ({
1327
+ _id: building._id,
1328
+ name: building.name || building.block || `Building ${building._id}`,
1329
+ }));
1330
+ }
1331
+ } catch (error) {
1332
+ console.error("Error fetching data:", error);
1333
+ }
1334
+ });
1335
+
1336
+ // Base Facility Bookings Data
1337
+ const baseFacilityBookings = [
1105
1338
  {
1106
1339
  id: 1,
1107
1340
  label: "Approved Bookings",
1108
- value: "34",
1341
+ value: 34,
1109
1342
  icon: "mdi-calendar-check",
1110
1343
  iconColor: "success",
1111
1344
  },
1112
1345
  {
1113
1346
  id: 2,
1114
1347
  label: "Pending Bookings",
1115
- value: "97",
1348
+ value: 97,
1116
1349
  icon: "mdi-calendar-clock",
1117
1350
  iconColor: "warning",
1118
1351
  },
1119
1352
  {
1120
1353
  id: 3,
1121
1354
  label: "Cancelled Bookings",
1122
- value: "28",
1355
+ value: 28,
1123
1356
  icon: "mdi-calendar-remove",
1124
1357
  iconColor: "error",
1125
1358
  },
1126
1359
  {
1127
1360
  id: 4,
1128
1361
  label: "Rejected Bookings",
1129
- value: "27",
1362
+ value: 27,
1130
1363
  icon: "mdi-calendar-remove",
1131
1364
  iconColor: "error",
1132
1365
  },
1133
1366
  ];
1367
+
1368
+ // Computed Facility Bookings - changes based on selected facility
1369
+ const facilityBookings = computed(() => {
1370
+ // If no facility is selected, return base data
1371
+ if (!selectedBooking.value) {
1372
+ return baseFacilityBookings.map((booking) => ({
1373
+ ...booking,
1374
+ value: booking.value.toString(),
1375
+ }));
1376
+ }
1377
+
1378
+ // If a facility is selected, multiply values
1379
+ const bookingHash = selectedBooking.value
1380
+ .split("")
1381
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0);
1382
+ const multiplier = 0.35 + (bookingHash % 45) / 100; // Between 0.35 and 0.8
1383
+
1384
+ return baseFacilityBookings.map((booking) => ({
1385
+ ...booking,
1386
+ value: Math.round(booking.value * multiplier).toString(),
1387
+ }));
1388
+ });
1134
1389
  </script>
1135
1390
 
1136
1391
  <style scoped>
@@ -0,0 +1,101 @@
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
+ {{ prop.mode }} Document
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-0">
11
+ <v-form v-model="validForm" :disabled="disable">
12
+ <v-col cols="12" class="px-6">
13
+ <InputLabel class="text-capitalize" title="Document Attachment" />
14
+ <InputFileV2
15
+ v-model="document.attachment"
16
+ :multiple="false"
17
+ :max-length="10"
18
+ title="Upload Images"
19
+ />
20
+ </v-col>
21
+
22
+ <v-col cols="12">
23
+ <v-row no-gutters>
24
+ <v-col cols="12" class="text-center">
25
+ <span
26
+ class="text-none text-subtitle-2 font-weight-medium text-error"
27
+ >
28
+ {{ message }}
29
+ </span>
30
+ </v-col>
31
+ </v-row>
32
+ </v-col>
33
+ </v-form>
34
+ </v-card-text>
35
+
36
+ <v-toolbar density="compact">
37
+ <v-row no-gutters>
38
+ <v-col cols="6">
39
+ <v-btn
40
+ tile
41
+ block
42
+ variant="text"
43
+ class="text-none"
44
+ size="48"
45
+ @click="cancel"
46
+ :disabled="disable"
47
+ >
48
+ Cancel
49
+ </v-btn>
50
+ </v-col>
51
+
52
+ <v-col cols="6">
53
+ <v-btn
54
+ tile
55
+ block
56
+ variant="flat"
57
+ color="black"
58
+ class="text-none"
59
+ size="48"
60
+ :disabled="!validForm || disable"
61
+ @click="submit"
62
+ :loading="disable"
63
+ >
64
+ Submit
65
+ </v-btn>
66
+ </v-col>
67
+ </v-row>
68
+ </v-toolbar>
69
+ </v-card>
70
+ </template>
71
+ <script setup lang="ts">
72
+ const prop = defineProps({
73
+ mode: {
74
+ type: String,
75
+ default: "add",
76
+ },
77
+ document: {
78
+ type: Object as PropType<TDocument>,
79
+ default: () => ({
80
+ name: "",
81
+ attachment: "",
82
+ }),
83
+ },
84
+ });
85
+
86
+ const emit = defineEmits(["cancel", "success"]);
87
+
88
+ const validForm = ref(false);
89
+ const disable = ref(false);
90
+ const message = ref("");
91
+
92
+ function cancel() {
93
+ // createMore.value = false;
94
+ message.value = "";
95
+ emit("cancel");
96
+ }
97
+
98
+ async function submit() {
99
+ disable.value = true;
100
+ }
101
+ </script>
@@ -0,0 +1,187 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12" class="mb-2">
4
+ <v-row no-gutters>
5
+ <v-btn
6
+ class="text-none"
7
+ rounded="pill"
8
+ variant="tonal"
9
+ size="large"
10
+ @click="setDocument()"
11
+ v-if="canCreate && canCreateDocument"
12
+ >
13
+ Add Document
14
+ </v-btn>
15
+ </v-row>
16
+ </v-col>
17
+ <v-col cols="12">
18
+ <v-card
19
+ width="100%"
20
+ variant="outlined"
21
+ border="thin"
22
+ rounded="lg"
23
+ :loading="loading"
24
+ >
25
+ <v-toolbar density="compact" color="grey-lighten-4">
26
+ <template #prepend>
27
+ <v-btn fab icon density="comfortable" @click="">
28
+ <v-icon>mdi-refresh</v-icon>
29
+ </v-btn>
30
+ </template>
31
+
32
+ <template #append>
33
+ <v-row no-gutters justify="end" align="center">
34
+ <span class="mr-2 text-caption text-fontgray">
35
+ {{ pageRange }}
36
+ </span>
37
+ <local-pagination
38
+ v-model="page"
39
+ :length="pages"
40
+ @update:value=""
41
+ />
42
+ </v-row>
43
+ </template>
44
+ </v-toolbar>
45
+ <v-data-table
46
+ :headers="headers"
47
+ :items="items"
48
+ item-value="_id"
49
+ items-per-page="10"
50
+ fixed-header
51
+ hide-default-footer
52
+ hide-default-header
53
+ @click:row="tableRowClickHandler"
54
+ style="max-height: calc(100vh - (200px))"
55
+ ></v-data-table>
56
+ </v-card>
57
+ </v-col>
58
+
59
+ <!-- Create Dialog -->
60
+ <v-dialog v-model="createDialog" width="450" persistent>
61
+ <DocumentForm @cancel="createDialog = false" @success="successCreate()" />
62
+ </v-dialog>
63
+ </v-row>
64
+ </template>
65
+ <script setup lang="ts">
66
+ definePageMeta({
67
+ middleware: ["01-auth", "02-org"],
68
+ memberOnly: true,
69
+ });
70
+ const props = defineProps({
71
+ headers: {
72
+ type: Array as PropType<Array<Record<string, any>>>,
73
+ default: () => [
74
+ {
75
+ title: "Name",
76
+ value: "name",
77
+ },
78
+ { title: "Action", value: "action-table" },
79
+ ],
80
+ },
81
+ canCreate: {
82
+ type: Boolean,
83
+ default: true,
84
+ },
85
+ canUpdate: {
86
+ type: Boolean,
87
+ default: true,
88
+ },
89
+ canDelete: {
90
+ type: Boolean,
91
+ default: true,
92
+ },
93
+ canCreateDocument: {
94
+ type: Boolean,
95
+ default: true,
96
+ },
97
+ canUpdateDocument: {
98
+ type: Boolean,
99
+ default: true,
100
+ },
101
+ canDeleteDocument: {
102
+ type: Boolean,
103
+ default: true,
104
+ },
105
+ });
106
+
107
+ const { headerSearch } = useLocal();
108
+ const { getAll: _getAllDocuments } = useDocument();
109
+
110
+ const page = ref(1);
111
+ const pages = ref(0);
112
+ const pageRange = ref("-- - -- of --");
113
+
114
+ const message = ref("");
115
+ const messageSnackbar = ref(false);
116
+ const messageColor = ref("");
117
+
118
+ const items = ref<Array<Record<string, any>>>([]);
119
+
120
+ const {
121
+ data: getDocumentReq,
122
+ refresh: getDocuments,
123
+ status: getAllReqStatus,
124
+ } = useLazyAsyncData(
125
+ "get-all-documents",
126
+ () =>
127
+ _getAllDocuments({
128
+ page: page.value,
129
+ search: headerSearch.value,
130
+ }),
131
+ {
132
+ watch: [page, headerSearch],
133
+ }
134
+ );
135
+
136
+ const loading = computed(() => getAllReqStatus.value === "pending");
137
+
138
+ watchEffect(() => {
139
+ if (getDocumentReq.value) {
140
+ items.value = getDocumentReq.value.items;
141
+ pages.value = getDocumentReq.value.pages;
142
+ pageRange.value = getDocumentReq.value.pageRange;
143
+ }
144
+ });
145
+
146
+ const createDialog = ref(false);
147
+ const editDialog = ref(false);
148
+ const previewDialog = ref(false);
149
+ const selectedDocument = ref<TDocument>({
150
+ _id: "",
151
+ name: "",
152
+ attachment: [],
153
+ });
154
+
155
+ function tableRowClickHandler(_: any, data: any) {
156
+ selectedDocument.value = data.item as TDocument;
157
+ previewDialog.value = true;
158
+ }
159
+
160
+ function setDocument({
161
+ mode = "create",
162
+ dialog = true,
163
+ data = {} as TDocument,
164
+ } = {}) {
165
+ if (mode === "create") {
166
+ createDialog.value = dialog;
167
+ } else if (mode === "edit") {
168
+ editDialog.value = dialog;
169
+ selectedDocument.value = data;
170
+ } else if (mode === "preview") {
171
+ previewDialog.value = dialog;
172
+ selectedDocument.value = data;
173
+ }
174
+ }
175
+
176
+ function showMessage(msg: string, color: string) {
177
+ message.value = msg;
178
+ messageColor.value = color;
179
+ messageSnackbar.value = true;
180
+ }
181
+
182
+ function successCreate() {
183
+ createDialog.value = false;
184
+ getDocuments();
185
+ showMessage("Document created successfully!", "success");
186
+ }
187
+ </script>
@@ -36,7 +36,7 @@
36
36
  >
37
37
  <v-toolbar density="compact" color="grey-lighten-4">
38
38
  <template #prepend>
39
- <v-btn fab icon density="comfortable" @click="updatePage">
39
+ <v-btn fab icon density="comfortable" @click="updatePage(1)">
40
40
  <v-icon>mdi-refresh</v-icon>
41
41
  </v-btn>
42
42
  </template>
@@ -351,6 +351,7 @@ async function updatePage(pageVal: any) {
351
351
  const response = await _getFeedbacks({
352
352
  page: page.value,
353
353
  site: route.params.site as string,
354
+ category: props.category,
354
355
  });
355
356
  if (response) {
356
357
  items.value = response.items;
@@ -3,13 +3,14 @@
3
3
  <v-col cols="12" class="d-flex ga-2">
4
4
  <v-select v-model="selectedCode" :variant="variant" :items="countries" item-title="code" item-value="code"
5
5
  hide-details class="px-0" :density="density" style="max-width: 95px" :rules="[...props.rules]"
6
- @update:model-value="handleUpdateCountry">
6
+ :readonly="props.readOnly" @update:model-value="handleUpdateCountry">
7
7
  <template v-slot:item="{ props: itemProps, item }">
8
8
  <v-list-item v-bind="itemProps" :title="item.raw.name" :subtitle="item.raw.dial_code" width="300" />
9
9
  </template>
10
10
  </v-select>
11
- <v-mask-input v-model="phone" :mask="currentMask" :rules="[...props.rules, validatePhone]" :loading="loading"
12
- :variant="variant" hint="Enter a valid phone number" hide-details persistent-hint return-masked-value
11
+ <v-mask-input v-model="input" :mask="currentMask" :rules="[...props.rules, validatePhone]" ref="maskRef" :key="`mask-key-${maskKey}`"
12
+ :loading="loading" :readonly="props.readOnly" :variant="variant" hint="Enter a valid phone number"
13
+ hide-details persistent-hint return-masked-value :prefix="phonePrefix || '###'" persistent-placeholder
13
14
  :density="density" :placeholder="placeholder || currentMask"></v-mask-input>
14
15
  </v-col>
15
16
  <span class="text-error text-caption w-100" v-if="errorMessage && !hideDetails">{{ errorMessage }}</span>
@@ -45,6 +46,10 @@ const props = defineProps({
45
46
  loading: {
46
47
  type: Boolean,
47
48
  default: false
49
+ },
50
+ readOnly: {
51
+ type: Boolean,
52
+ default: false
48
53
  }
49
54
  })
50
55
 
@@ -60,9 +65,12 @@ type TPhoneMask =
60
65
  }
61
66
 
62
67
  const phone = defineModel({ default: '' })
68
+ const input = ref('')
63
69
  const selectedCode = ref('SG')
64
70
  const countries = phoneMasks
65
71
  const errorMessage = ref('')
72
+ const maskRef = ref()
73
+ const maskKey = ref(0)
66
74
 
67
75
  const currentMask = computed(() => {
68
76
  const country = phoneMasks.find((c: TPhoneMask) => c.code === selectedCode.value)
@@ -72,7 +80,9 @@ const currentMask = computed(() => {
72
80
 
73
81
 
74
82
 
75
- const validatePhone = (value: string): boolean | string => {
83
+ const validatePhone = (): boolean | string => {
84
+ if(props.readOnly) return true;
85
+ const value = phone.value
76
86
  if (!value) {
77
87
  errorMessage.value = ''
78
88
  return true;
@@ -92,23 +102,29 @@ const validatePhone = (value: string): boolean | string => {
92
102
 
93
103
 
94
104
 
95
-
96
105
  function generateMaskFromRegex(regex: string): string {
106
+ let pattern = regex.replace(/^\^|\$$/g, '');
97
107
 
98
- let pattern = regex.replace(/^\^|\$$/g, '')
99
- pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) => '#'.repeat(Number(count)))
100
- pattern = pattern.replace(/\\d/g, '#')
108
+ pattern = pattern.replace(/\(\?:\+?\d+\)\?/g, '');
109
+ pattern = pattern.replace(/\+?\d{1,4}/, '');
101
110
 
102
- pattern = pattern.replace(/\\/g, '')
111
+ pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) => '#'.repeat(Number(count)));
103
112
 
104
- pattern = pattern.replace(/\s\?/g, ' ')
105
- pattern = pattern.replace(/\s/g, ' ')
113
+ pattern = pattern.replace(/\\d/g, '#');
106
114
 
107
- pattern = pattern.replace(/\(\?:/g, '(')
115
+ pattern = pattern.replace(/\\/g, '');
116
+ pattern = pattern.replace(/\(\?:/g, '');
117
+ pattern = pattern.trim();
108
118
 
109
- return pattern.trim()
119
+ return pattern;
110
120
  }
111
121
 
122
+ const phonePrefix = computed(() => {
123
+ const country = phoneMasks.find((c: TPhoneMask) => c.code === selectedCode.value)
124
+ return country?.dial_code || ''
125
+ })
126
+
127
+
112
128
  function handleUpdateCountry() {
113
129
  phone.value = ''
114
130
  }
@@ -116,8 +132,32 @@ function handleUpdateCountry() {
116
132
  const emit = defineEmits(['update:modelValue'])
117
133
 
118
134
  watch(phone, (newVal) => {
119
- emit('update:modelValue', newVal)
135
+ emit('update:modelValue', newVal)
136
+ })
137
+
138
+ watch(input, (newInput) => {
139
+ const prefix = phonePrefix.value
140
+
141
+ if (!newInput) {
142
+ return
143
+ }
144
+
145
+ phone.value = prefix + newInput
146
+ })
147
+
148
+
149
+ onMounted(() => {
150
+ if (!phone.value) return
151
+
152
+ const found = phoneMasks.find((c: any) => phone.value?.startsWith(c?.dial_code))
153
+ if (found) {
154
+ selectedCode.value = found.code
155
+ }
156
+
157
+ input.value = phone.value.replace(found?.dial_code || '', '')
158
+ maskKey.value++
120
159
  })
121
160
 
122
161
 
162
+
123
163
  </script>
@@ -7,7 +7,7 @@
7
7
  rounded="pill"
8
8
  variant="tonal"
9
9
  size="large"
10
- @click="showCreateDialog = true"
10
+ @click="(showCreateDialog = true), (isEditMode = false)"
11
11
  v-if="canCreateWorkOrder"
12
12
  >
13
13
  Create Work Order
@@ -35,7 +35,7 @@
35
35
  >
36
36
  <v-toolbar density="compact" color="grey-lighten-4">
37
37
  <template #prepend>
38
- <v-btn fab icon density="comfortable" @click="updatePage">
38
+ <v-btn fab icon density="comfortable" @click="updatePage(1)">
39
39
  <v-icon>mdi-refresh</v-icon>
40
40
  </v-btn>
41
41
  </template>
@@ -258,7 +258,12 @@ const selected = ref<string[]>([]);
258
258
  const route = useRoute();
259
259
  const { customers } = useCustomer();
260
260
 
261
- const { getWorkOrders: _getWorkOrders, createWorkOrder } = useWorkOrder();
261
+ const {
262
+ getWorkOrders: _getWorkOrders,
263
+ createWorkOrder,
264
+ getWorkOrderById,
265
+ updateWorkOrder,
266
+ } = useWorkOrder();
262
267
 
263
268
  const page = ref(1);
264
269
  const pages = ref(0);
@@ -294,6 +299,7 @@ async function updatePage(pageVal: any) {
294
299
  const response = await _getWorkOrders({
295
300
  page: page.value,
296
301
  site: route.params.site as string,
302
+ category: props.category,
297
303
  });
298
304
  if (response) {
299
305
  items.value = response.items;
@@ -412,7 +418,13 @@ async function submitWorkOrder() {
412
418
  site: route.params.site as string,
413
419
  };
414
420
 
415
- const res = await createWorkOrder(payload);
421
+ let res: Record<string, any> = {};
422
+
423
+ if (isEditMode.value) {
424
+ res = await updateWorkOrder(_workOrderId.value as string, payload);
425
+ } else {
426
+ res = await createWorkOrder(payload);
427
+ }
416
428
 
417
429
  showMessage(res.message, "success");
418
430
  showCreateDialog.value = false;
@@ -437,12 +449,32 @@ function onViewWorkOrder(item: any) {
437
449
  });
438
450
  }
439
451
 
440
- function editWorkOrder(item: any) {}
452
+ async function editWorkOrder(item: any) {
453
+ try {
454
+ const _workOrders = await getWorkOrderById(item._id);
455
+ console.log(_workOrders);
456
+ _workOrder.value = {
457
+ attachments: (_workOrders.attachments || []) as string[],
458
+ category: _workOrders.category || "",
459
+ subject: _workOrders.subject || "",
460
+ description: _workOrders.description || "",
461
+ highPriority: _workOrders.highPriority || false,
462
+ unit: _workOrders.location || "",
463
+ };
464
+ _workOrderId.value = item._id;
465
+ isEditMode.value = true;
466
+ showCreateDialog.value = true;
467
+ dialogPreview.value = false;
468
+ } catch (error) {
469
+ showMessage("Failed to load full work order", "error");
470
+ }
471
+ }
472
+
473
+ async function _updateWorkOrder() {}
441
474
 
442
475
  function confirmDeleteWorkOrder(item: any) {}
443
476
 
444
477
  function tableRowClickHandler(_: any, data: any) {
445
- console.log(data.item);
446
478
  selectedWorkOrder.value = data.item;
447
479
  dialogPreview.value = true;
448
480
  }
@@ -0,0 +1,27 @@
1
+ export default function useDocument() {
2
+ function getAll({
3
+ page = 1,
4
+ search = "",
5
+ limit = 10,
6
+ status = "active",
7
+ site = "",
8
+ } = {}) {
9
+ return useNuxtApp().$api<Record<string, any>>(
10
+ `/api/documents`,
11
+ {
12
+ method: "GET",
13
+ query: {
14
+ page,
15
+ search,
16
+ limit,
17
+ status,
18
+ site,
19
+ },
20
+ }
21
+ );
22
+ }
23
+
24
+ return {
25
+ getAll,
26
+ };
27
+ }
@@ -36,7 +36,18 @@ export default function () {
36
36
  }
37
37
  );
38
38
  }
39
-
39
+ async function updateSitebyId(
40
+ siteId: string,
41
+ payload: { field: string; value: number }
42
+ ) {
43
+ return await useNuxtApp().$api<Record<string, any>>(
44
+ `/api/sites/id/${siteId}`,
45
+ {
46
+ method: "PATCH",
47
+ body: payload,
48
+ }
49
+ );
50
+ }
40
51
  async function addCamera(camera: TSiteCamera) {
41
52
  return await useNuxtApp().$api<Record<string, any>>(`/api/site-cameras`, {
42
53
  method: "POST",
@@ -107,5 +118,6 @@ export default function () {
107
118
  setSiteGuardPosts,
108
119
  updateSiteCamera,
109
120
  deleteSiteCameraById,
121
+ updateSitebyId,
110
122
  };
111
123
  }
@@ -64,6 +64,13 @@ export default function useWorkOrder() {
64
64
  });
65
65
  }
66
66
 
67
+ function updateWorkOrder(id: string, payload: object) {
68
+ return useNuxtApp().$api<Record<string, any>>(`/api/work-orders/${id}`, {
69
+ method: "PUT",
70
+ body: payload,
71
+ });
72
+ }
73
+
67
74
  return {
68
75
  workOrders,
69
76
  workOrder,
@@ -73,5 +80,6 @@ export default function useWorkOrder() {
73
80
  createWorkOrder,
74
81
  getWorkOrders,
75
82
  getWorkOrderById,
83
+ updateWorkOrder,
76
84
  };
77
85
  }
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.5.5",
5
+ "version": "1.5.6",
6
6
  "main": "./nuxt.config.ts",
7
7
  "scripts": {
8
8
  "dev": "nuxi dev .playground",
@@ -0,0 +1,5 @@
1
+ declare type TDocument = {
2
+ _id?: string;
3
+ name: string;
4
+ attachment: string[];
5
+ };