@7365admin1/layer-common 1.8.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.
Files changed (198) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.editorconfig +12 -0
  4. package/.github/workflows/main.yml +17 -0
  5. package/.github/workflows/publish.yml +39 -0
  6. package/.nuxtrc +1 -0
  7. package/.playground/app.vue +41 -0
  8. package/.playground/eslint.config.mjs +6 -0
  9. package/.playground/nuxt.config.ts +22 -0
  10. package/.playground/pages/feedback.vue +30 -0
  11. package/CHANGELOG.md +263 -0
  12. package/README.md +73 -0
  13. package/app.vue +3 -0
  14. package/components/AccessCardAddForm.vue +363 -0
  15. package/components/AccessManagement.vue +420 -0
  16. package/components/Avatar/Main.vue +68 -0
  17. package/components/BillingMain.vue +66 -0
  18. package/components/BtnUploadFile.vue +139 -0
  19. package/components/BuildingForm.vue +303 -0
  20. package/components/BuildingManagement/buildings.vue +335 -0
  21. package/components/BuildingManagement/units.vue +350 -0
  22. package/components/BuildingUnitFormAdd.vue +441 -0
  23. package/components/BuildingUnitFormEdit.vue +429 -0
  24. package/components/CameraForm.vue +264 -0
  25. package/components/CameraMain.vue +352 -0
  26. package/components/Card/DeleteConfirmation.vue +51 -0
  27. package/components/Card/MemberInfoSummary.vue +44 -0
  28. package/components/Card/Toggle.vue +25 -0
  29. package/components/Chat/Bubbles.vue +53 -0
  30. package/components/Chat/Information.vue +416 -0
  31. package/components/Chat/ListCard.vue +62 -0
  32. package/components/Chat/Message.vue +158 -0
  33. package/components/Chat/Navigation.vue +150 -0
  34. package/components/ConfirmDialog.vue +66 -0
  35. package/components/Container/Standard.vue +33 -0
  36. package/components/DashboardPlaceholder.vue +1524 -0
  37. package/components/Dialog/DeleteConfirmation.vue +51 -0
  38. package/components/Dialog/ReplaceAutofillPrompt.vue +49 -0
  39. package/components/Dialog/UpdateMoreAction.vue +103 -0
  40. package/components/DocumentForm.vue +187 -0
  41. package/components/DocumentManagement.vue +376 -0
  42. package/components/Editor.vue +95 -0
  43. package/components/EntryPassMain.vue +518 -0
  44. package/components/Feedback/Form.vue +173 -0
  45. package/components/FeedbackDetail.vue +599 -0
  46. package/components/FeedbackMain.vue +588 -0
  47. package/components/FormDialog.vue +65 -0
  48. package/components/ImageCarousel.vue +138 -0
  49. package/components/Input/Date.vue +177 -0
  50. package/components/Input/DateTimePicker.vue +131 -0
  51. package/components/Input/File.vue +236 -0
  52. package/components/Input/FileV2.vue +234 -0
  53. package/components/Input/InputPhoneNumberV2.vue +164 -0
  54. package/components/Input/ListGroupSelection.vue +96 -0
  55. package/components/Input/NRICNumber.vue +53 -0
  56. package/components/Input/NewDate.vue +123 -0
  57. package/components/Input/Number.vue +124 -0
  58. package/components/Input/Password.vue +22 -0
  59. package/components/Input/PhoneNumber.vue +188 -0
  60. package/components/Input/VehicleNumber.vue +49 -0
  61. package/components/InputLabel.vue +22 -0
  62. package/components/InvitationForm.vue +359 -0
  63. package/components/InvitationMain.vue +310 -0
  64. package/components/Layout/Header.vue +129 -0
  65. package/components/Layout/NavigationDrawer.vue +44 -0
  66. package/components/ListItem.vue +35 -0
  67. package/components/ListView.vue +87 -0
  68. package/components/LocalPagination.vue +31 -0
  69. package/components/MemberMain.vue +459 -0
  70. package/components/NFC/NFCPatrolReportMain.vue +591 -0
  71. package/components/NFC/NFCPatrolRouteForm.vue +596 -0
  72. package/components/NFC/NFCPatrolRouteMain.vue +539 -0
  73. package/components/NFC/NFCTagForm.vue +236 -0
  74. package/components/NFC/NFCTagMain.vue +337 -0
  75. package/components/NFC/PatrolSettings.vue +130 -0
  76. package/components/NavigationItem.vue +83 -0
  77. package/components/NumberSettingField.vue +107 -0
  78. package/components/OnlineFormConfigurationForm.vue +290 -0
  79. package/components/OnlineFormsConfiguration.vue +429 -0
  80. package/components/PeopleForm.vue +452 -0
  81. package/components/PlaceholderComponent.vue +34 -0
  82. package/components/RolePermissionFormCreate.vue +161 -0
  83. package/components/RolePermissionFormPreviewUpdate.vue +183 -0
  84. package/components/RolePermissionMain.vue +361 -0
  85. package/components/SearchVehicleNumberUser.vue +91 -0
  86. package/components/ServiceProviderFormCreate.vue +154 -0
  87. package/components/ServiceProviderMain.vue +547 -0
  88. package/components/SignaturePad.vue +73 -0
  89. package/components/Snackbar.vue +23 -0
  90. package/components/SpecificAttr.vue +53 -0
  91. package/components/SupplyManagement.vue +292 -0
  92. package/components/SwitchContext.vue +108 -0
  93. package/components/TableList.vue +150 -0
  94. package/components/TableListSecondary.vue +164 -0
  95. package/components/TableMain.vue +142 -0
  96. package/components/TableWithButton.vue +94 -0
  97. package/components/VehicleUpdateMoreAction.vue +84 -0
  98. package/components/VideoPlayer.vue +125 -0
  99. package/components/VisitorForm.vue +659 -0
  100. package/components/VisitorFormSelection.vue +53 -0
  101. package/components/VisitorManagement.vue +490 -0
  102. package/components/WorkOrder/Create.vue +284 -0
  103. package/components/WorkOrder/Detail.vue +71 -0
  104. package/components/WorkOrder/ListView.vue +96 -0
  105. package/components/WorkOrder/Main.vue +489 -0
  106. package/components/Workorder.vue +1 -0
  107. package/composables/useAddress.ts +107 -0
  108. package/composables/useBuilding.ts +250 -0
  109. package/composables/useBuildingUnit.ts +116 -0
  110. package/composables/useCard.ts +46 -0
  111. package/composables/useCommonPermission.ts +207 -0
  112. package/composables/useCustomer.ts +113 -0
  113. package/composables/useCustomerSite.ts +56 -0
  114. package/composables/useDashboard.ts +31 -0
  115. package/composables/useDashboardData.ts +425 -0
  116. package/composables/useDocument.ts +57 -0
  117. package/composables/useFacility.ts +246 -0
  118. package/composables/useFeedback.ts +119 -0
  119. package/composables/useFile.ts +55 -0
  120. package/composables/useInvoice.ts +18 -0
  121. package/composables/useLocal.ts +131 -0
  122. package/composables/useLocalAuth.ts +137 -0
  123. package/composables/useLocalSetup.ts +13 -0
  124. package/composables/useMember.ts +111 -0
  125. package/composables/useNFCPatrolRoute.ts +77 -0
  126. package/composables/useNFCPatrolSettings.ts +19 -0
  127. package/composables/useNFCPatrolTag.ts +53 -0
  128. package/composables/useOnlineForm.ts +67 -0
  129. package/composables/useOrg.ts +129 -0
  130. package/composables/usePDFDownload.ts +25 -0
  131. package/composables/usePaymentMethod.ts +101 -0
  132. package/composables/usePeople.ts +81 -0
  133. package/composables/usePermission.ts +54 -0
  134. package/composables/usePhoneCountries.ts +561 -0
  135. package/composables/usePrice.ts +15 -0
  136. package/composables/usePromoCode.ts +36 -0
  137. package/composables/useRecapPermission.ts +26 -0
  138. package/composables/useRole.ts +104 -0
  139. package/composables/useSecurityUtils.ts +18 -0
  140. package/composables/useServiceProvider.ts +224 -0
  141. package/composables/useSite.ts +109 -0
  142. package/composables/useSiteEntryPassSettings.ts +46 -0
  143. package/composables/useSiteSettings.ts +123 -0
  144. package/composables/useSubscription.ts +150 -0
  145. package/composables/useUser.ts +132 -0
  146. package/composables/useUtils.ts +445 -0
  147. package/composables/useVerification.ts +34 -0
  148. package/composables/useVisitor.ts +120 -0
  149. package/composables/useWorkOrder.ts +85 -0
  150. package/error.vue +41 -0
  151. package/layouts/plain.vue +7 -0
  152. package/middleware/01.auth.ts +20 -0
  153. package/middleware/02.org.ts +21 -0
  154. package/middleware/03.customer.ts +13 -0
  155. package/middleware/member.ts +4 -0
  156. package/nuxt.config.ts +54 -0
  157. package/package.json +39 -0
  158. package/pages/index.vue +3 -0
  159. package/pages/payment-method-linked.vue +31 -0
  160. package/pages/require-customer.vue +56 -0
  161. package/pages/require-organization-membership.vue +47 -0
  162. package/pages/unauthorized.vue +29 -0
  163. package/plugins/API.ts +21 -0
  164. package/plugins/iconify.client.ts +5 -0
  165. package/plugins/secure-member.client.ts +86 -0
  166. package/plugins/vuetify.ts +62 -0
  167. package/public/bg-camera.jpg +0 -0
  168. package/public/bg-city.jpg +0 -0
  169. package/public/bg-condo.jpg +0 -0
  170. package/public/images/icons/delete-icon.png +0 -0
  171. package/public/sprite.svg +1 -0
  172. package/tsconfig.json +3 -0
  173. package/types/address.d.ts +13 -0
  174. package/types/building.d.ts +27 -0
  175. package/types/camera.d.ts +31 -0
  176. package/types/card.d.ts +22 -0
  177. package/types/customer.d.ts +27 -0
  178. package/types/document.d.ts +6 -0
  179. package/types/feedback.d.ts +68 -0
  180. package/types/local.d.ts +74 -0
  181. package/types/member.d.ts +21 -0
  182. package/types/online-form.d.ts +15 -0
  183. package/types/org.d.ts +13 -0
  184. package/types/people.d.ts +24 -0
  185. package/types/permission.d.ts +25 -0
  186. package/types/phone-number.d.ts +10 -0
  187. package/types/price.d.ts +17 -0
  188. package/types/promo-code.d.ts +19 -0
  189. package/types/role.d.ts +11 -0
  190. package/types/select.d.ts +4 -0
  191. package/types/service-provider.d.ts +15 -0
  192. package/types/site.d.ts +20 -0
  193. package/types/subscription.d.ts +23 -0
  194. package/types/user.d.ts +19 -0
  195. package/types/verification.d.ts +20 -0
  196. package/types/visitor.d.ts +42 -0
  197. package/types/work-order.d.ts +42 -0
  198. package/utils/phoneMasks.ts +1703 -0
@@ -0,0 +1,1524 @@
1
+ <template>
2
+ <v-container fluid class="">
3
+ <!-- Header -->
4
+ <v-row no-gutters class="mb-6">
5
+ <v-col cols="12">
6
+ <h1 class="text-h4 text-md-h3 font-weight-bold">
7
+ Dashboard{{ currentSiteName ? ` - ${currentSiteName}` : "" }}
8
+ </h1>
9
+ </v-col>
10
+ </v-row>
11
+
12
+ <!-- Count Cards -->
13
+ <v-row class="mb-4">
14
+ <v-col
15
+ v-for="card in countCardList"
16
+ :key="card.id"
17
+ cols="12"
18
+ sm="6"
19
+ md="6"
20
+ lg="4"
21
+ xl="2.4"
22
+ >
23
+ <v-card flat border class="h-100 card-hover" elevation="0">
24
+ <v-card-text class="pa-4 pa-md-6">
25
+ <v-row no-gutters class="align-start">
26
+ <v-col cols="8">
27
+ <p class="text-caption text-grey-darken-1 mb-2 text-uppercase">
28
+ {{ card.label }}
29
+ </p>
30
+ <h2 class="text-h4 text-md-h3 font-weight-bold mb-3">
31
+ {{ card.value }}
32
+ </h2>
33
+ <v-chip :color="card.chipColor" size="small" variant="flat">
34
+ <v-icon
35
+ size="14"
36
+ icon="mdi-trending-up"
37
+ class="mr-1"
38
+ ></v-icon>
39
+ {{ card.percentage }}
40
+ </v-chip>
41
+ </v-col>
42
+ <v-col cols="4" class="text-right">
43
+ <div class="d-flex justify-end">
44
+ <div
45
+ class="avatar-custom"
46
+ :style="{ backgroundColor: card.color }"
47
+ >
48
+ <v-icon :icon="card.icon" color="white" size="28"></v-icon>
49
+ </div>
50
+ </div>
51
+ </v-col>
52
+ </v-row>
53
+ <v-row no-gutters class="mt-4">
54
+ <v-col cols="12">
55
+ <v-select
56
+ :model-value="card.period"
57
+ :items="['This Week', 'This Month', 'This Year']"
58
+ density="compact"
59
+ hide-details
60
+ variant="outlined"
61
+ @update:model-value="
62
+ updateCardPeriodDynamic(card.label, $event)
63
+ "
64
+ ></v-select>
65
+ </v-col>
66
+ </v-row>
67
+ </v-card-text>
68
+ </v-card>
69
+ </v-col>
70
+ </v-row>
71
+
72
+ <!-- Line Chart -->
73
+ <!-- <v-row class="mb-4">
74
+ <v-col cols="12">
75
+ <v-card flat border elevation="0">
76
+ <v-card-title class="pa-4 pa-md-6">
77
+ <v-row no-gutters align="center">
78
+ <v-col cols="12" md="6" class="mb-4 mb-md-0">
79
+ <h3 class="text-h6 text-md-h5 font-weight-medium">
80
+ No. of Visitors
81
+ </h3>
82
+ </v-col>
83
+ <v-col cols="12" md="6" class="d-flex justify-md-end">
84
+ <v-select
85
+ v-model="visitorPeriod"
86
+ :items="['This Week', 'This Month', 'This Year']"
87
+ density="compact"
88
+ hide-details
89
+ variant="outlined"
90
+ style="max-width: 200px"
91
+ ></v-select>
92
+ </v-col>
93
+ </v-row>
94
+ </v-card-title>
95
+ <v-card-text class="pa-4 pa-md-6 pt-0">
96
+ <div
97
+ class="chart-container"
98
+ style="min-height: 280px; overflow-x: auto"
99
+ >
100
+ <svg
101
+ viewBox="0 0 1200 300"
102
+ style="min-width: 800px; width: 100%; height: 100%"
103
+ preserveAspectRatio="xMidYMid meet"
104
+ >
105
+
106
+ <text x="20" y="30" font-size="12" fill="#666">6</text>
107
+ <text x="20" y="80" font-size="12" fill="#666">5</text>
108
+ <text x="20" y="130" font-size="12" fill="#666">4</text>
109
+ <text x="20" y="180" font-size="12" fill="#666">3</text>
110
+ <text x="20" y="230" font-size="12" fill="#666">2</text>
111
+ <text x="20" y="280" font-size="12" fill="#666">1</text>
112
+ <text x="20" y="310" font-size="12" fill="#666">0</text>
113
+
114
+
115
+ <line
116
+ x1="60"
117
+ y1="20"
118
+ x2="1180"
119
+ y2="20"
120
+ stroke="#E0E0E0"
121
+ stroke-width="1"
122
+ />
123
+ <line
124
+ x1="60"
125
+ y1="70"
126
+ x2="1180"
127
+ y2="70"
128
+ stroke="#E0E0E0"
129
+ stroke-width="1"
130
+ />
131
+ <line
132
+ x1="60"
133
+ y1="120"
134
+ x2="1180"
135
+ y2="120"
136
+ stroke="#E0E0E0"
137
+ stroke-width="1"
138
+ />
139
+ <line
140
+ x1="60"
141
+ y1="170"
142
+ x2="1180"
143
+ y2="170"
144
+ stroke="#E0E0E0"
145
+ stroke-width="1"
146
+ />
147
+ <line
148
+ x1="60"
149
+ y1="220"
150
+ x2="1180"
151
+ y2="220"
152
+ stroke="#E0E0E0"
153
+ stroke-width="1"
154
+ />
155
+ <line
156
+ x1="60"
157
+ y1="270"
158
+ x2="1180"
159
+ y2="270"
160
+ stroke="#E0E0E0"
161
+ stroke-width="1"
162
+ />
163
+ <line
164
+ x1="60"
165
+ y1="300"
166
+ x2="1180"
167
+ y2="300"
168
+ stroke="#E0E0E0"
169
+ stroke-width="1"
170
+ />
171
+
172
+
173
+ <polyline
174
+ :points="visitorPoints.map((p) => `${p.x},${p.y}`).join(' ')"
175
+ fill="none"
176
+ stroke="#2196F3"
177
+ stroke-width="3"
178
+ />
179
+
180
+ <circle
181
+ v-for="point in visitorPoints"
182
+ :key="point.x"
183
+ :cx="point.x"
184
+ :cy="point.y"
185
+ r="5"
186
+ fill="#2196F3"
187
+ />
188
+
189
+ <text
190
+ v-for="(label, idx) in visitorLabels"
191
+ :key="idx"
192
+ :x="label.x"
193
+ y="320"
194
+ font-size="11"
195
+ fill="#666"
196
+ text-anchor="middle"
197
+ >
198
+ {{ label.text }}
199
+ </text>
200
+ </svg>
201
+ </div>
202
+ </v-card-text>
203
+ </v-card>
204
+ </v-col>
205
+ </v-row> -->
206
+
207
+ <!-- Bar Chart -->
208
+ <!-- <v-row class="mb-4">
209
+ <v-col cols="12">
210
+ <v-card flat border elevation="0">
211
+ <v-card-title class="pa-4 pa-md-6">
212
+ <v-row no-gutters align="center">
213
+ <v-col cols="12" md="6" class="mb-4 mb-md-0">
214
+ <h3 class="text-h6 text-md-h5 font-weight-medium">
215
+ Feedback Tickets
216
+ </h3>
217
+ </v-col>
218
+ <v-col
219
+ cols="12"
220
+ md="6"
221
+ class="d-flex flex-column flex-md-row justify-md-end gap-2"
222
+ >
223
+ <v-select
224
+ v-model="selectedService"
225
+ :items="serviceList"
226
+ item-title="name"
227
+ item-value="_id"
228
+ label="Select Service"
229
+ density="compact"
230
+ hide-details
231
+ variant="outlined"
232
+ style="max-width: 200px"
233
+ clearable
234
+ class="pr-4"
235
+ @update:model-value="selectedService = $event"
236
+ ></v-select>
237
+ <v-select
238
+ v-model="feedbackPeriod"
239
+ :items="['This Week', 'This Month', 'This Year']"
240
+ density="compact"
241
+ hide-details
242
+ variant="outlined"
243
+ style="max-width: 200px"
244
+ ></v-select>
245
+ </v-col>
246
+ </v-row>
247
+ </v-card-title>
248
+ <v-card-text class="pa-4 pa-md-6 pt-2">
249
+ <div class="d-flex justify-center gap-4 mb-4">
250
+ <div class="d-flex align-center">
251
+ <div class="legend-box mr-2" style="background: #2196f3"></div>
252
+ <span class="text-caption">Resolved</span>
253
+ </div>
254
+ <div class="d-flex align-center">
255
+ <div class="legend-box mr-2" style="background: #4caf50"></div>
256
+ <span class="text-caption">Unresolved</span>
257
+ </div>
258
+ </div>
259
+
260
+ <div
261
+ class="chart-container"
262
+ style="min-height: 280px; overflow-x: auto"
263
+ >
264
+ <svg
265
+ viewBox="0 0 1200 300"
266
+ style="min-width: 800px; width: 100%; height: 100%"
267
+ preserveAspectRatio="xMidYMid meet"
268
+ >
269
+ <text x="20" y="20" font-size="12" fill="#999">100</text>
270
+ <text x="20" y="80" font-size="12" fill="#999">80</text>
271
+ <text x="20" y="140" font-size="12" fill="#999">60</text>
272
+ <text x="20" y="200" font-size="12" fill="#999">40</text>
273
+ <text x="20" y="260" font-size="12" fill="#999">20</text>
274
+ <text x="30" y="295" font-size="12" fill="#999">0</text>
275
+
276
+ <line
277
+ x1="60"
278
+ y1="20"
279
+ x2="1180"
280
+ y2="20"
281
+ stroke="#E0E0E0"
282
+ stroke-width="1"
283
+ />
284
+ <line
285
+ x1="60"
286
+ y1="80"
287
+ x2="1180"
288
+ y2="80"
289
+ stroke="#E0E0E0"
290
+ stroke-width="1"
291
+ />
292
+ <line
293
+ x1="60"
294
+ y1="140"
295
+ x2="1180"
296
+ y2="140"
297
+ stroke="#E0E0E0"
298
+ stroke-width="1"
299
+ />
300
+ <line
301
+ x1="60"
302
+ y1="200"
303
+ x2="1180"
304
+ y2="200"
305
+ stroke="#E0E0E0"
306
+ stroke-width="1"
307
+ />
308
+ <line
309
+ x1="60"
310
+ y1="260"
311
+ x2="1180"
312
+ y2="260"
313
+ stroke="#E0E0E0"
314
+ stroke-width="1"
315
+ />
316
+ <line
317
+ x1="60"
318
+ y1="290"
319
+ x2="1180"
320
+ y2="290"
321
+ stroke="#E0E0E0"
322
+ stroke-width="1"
323
+ />
324
+
325
+ <g v-for="(bar, idx) in feedbackBars" :key="idx">
326
+ <rect
327
+ :x="bar.x"
328
+ :y="bar.y"
329
+ :width="bar.width"
330
+ :height="bar.height"
331
+ :fill="bar.color"
332
+ rx="4"
333
+ />
334
+
335
+ <text
336
+ :x="bar.x + bar.width / 2"
337
+ :y="bar.y - 8"
338
+ font-size="14"
339
+ font-weight="bold"
340
+ fill="#333"
341
+ text-anchor="middle"
342
+ >
343
+ {{ bar.value }}
344
+ </text>
345
+
346
+ <text
347
+ :x="bar.x + bar.width / 2"
348
+ y="310"
349
+ font-size="12"
350
+ fill="#666"
351
+ text-anchor="middle"
352
+ >
353
+ {{ bar.label }}
354
+ </text>
355
+ </g>
356
+ </svg>
357
+ </div>
358
+ </v-card-text>
359
+ </v-card>
360
+ </v-col>
361
+ </v-row> -->
362
+
363
+ <!-- Pie Charts -->
364
+ <!-- <v-row class="mb-4">
365
+ <v-col v-for="pie in pieChartList" :key="pie.id" cols="12" md="6" lg="4">
366
+ <v-card flat border elevation="0" class="h-100">
367
+ <v-card-text class="pa-4 pa-md-6">
368
+
369
+ <div class="mb-3">
370
+ <h3 class="text-h6 font-weight-bold mb-1">{{ pie.title }}</h3>
371
+ <p class="text-body-2 text-grey-darken-1">
372
+ Total: {{ pie.total }}
373
+ </p>
374
+ </div>
375
+
376
+
377
+ <div class="mb-4 d-flex flex-column gap-2">
378
+
379
+ <v-select
380
+ v-if="pie.id === 1"
381
+ v-model="selectedBooking"
382
+ :items="bookingsList"
383
+ item-title="name"
384
+ item-value="_id"
385
+ label="Select Facility"
386
+ density="compact"
387
+ hide-details
388
+ variant="outlined"
389
+ clearable
390
+ class="pb-4"
391
+ ></v-select>
392
+
393
+ <v-select
394
+ :model-value="pie.period"
395
+ :items="['This Week', 'This Month', 'This Year']"
396
+ density="compact"
397
+ hide-details
398
+ variant="outlined"
399
+ @update:model-value="updatePiePeriod(pie.id, $event)"
400
+ ></v-select>
401
+ </div>
402
+
403
+
404
+ <div class="d-flex flex-wrap gap-3 mb-4">
405
+ <div
406
+ v-for="(segment, idx) in pie.segments"
407
+ :key="idx"
408
+ class="d-flex align-center mr-4 mb-2"
409
+ >
410
+ <div
411
+ class="legend-dot mr-2"
412
+ :style="{ backgroundColor: segment.color }"
413
+ ></div>
414
+ <span class="text-caption">{{ segment.label }}</span>
415
+ </div>
416
+ </div>
417
+
418
+
419
+ <div class="d-flex justify-center align-center">
420
+ <div class="position-relative">
421
+ <svg viewBox="0 0 200 200" width="220" height="220">
422
+ <circle
423
+ v-for="(segment, idx) in pie.segments"
424
+ :key="idx"
425
+ cx="100"
426
+ cy="100"
427
+ r="70"
428
+ fill="none"
429
+ :stroke="segment.color"
430
+ stroke-width="45"
431
+ :stroke-dasharray="`${segment.percentage * 4.398} 439.8`"
432
+ :transform="`rotate(${segment.rotation} 100 100)`"
433
+ class="donut-segment"
434
+ />
435
+ </svg>
436
+
437
+ <div class="donut-center">
438
+ <p class="text-h5 font-weight-bold">{{ pie.centerValue }}</p>
439
+ </div>
440
+ </div>
441
+ </div>
442
+ </v-card-text>
443
+ </v-card>
444
+ </v-col>
445
+ </v-row> -->
446
+
447
+ <!-- Facility Section -->
448
+ <!-- <v-row v-if="facilityBookings.length" class="mt-4">
449
+ <v-col cols="12" class="mb-4">
450
+ <h2 class="text-h5 font-weight-bold">Facility</h2>
451
+ </v-col>
452
+ <v-col
453
+ v-for="booking in facilityBookings"
454
+ :key="booking.id"
455
+ cols="12"
456
+ sm="6"
457
+ md="6"
458
+ lg="3"
459
+ >
460
+ <v-card flat border elevation="0" class="h-100 card-hover">
461
+ <v-card-text class="pa-6">
462
+ <v-row no-gutters align="start" class="mb-3">
463
+ <v-col>
464
+ <p class="text-body-2 text-grey-darken-1 mb-1">
465
+ {{ booking.label }}
466
+ </p>
467
+ </v-col>
468
+ <v-col cols="auto">
469
+ <v-icon
470
+ :icon="booking.icon"
471
+ :color="booking.iconColor"
472
+ size="28"
473
+ ></v-icon>
474
+ </v-col>
475
+ </v-row>
476
+ <h3 class="text-h3 font-weight-bold">
477
+ {{ booking.value }}
478
+ </h3>
479
+ </v-card-text>
480
+ </v-card>
481
+ </v-col>
482
+ </v-row> -->
483
+ </v-container>
484
+ </template>
485
+
486
+ <script setup lang="ts">
487
+ import useDashboard from '../composables/useDashboard';
488
+
489
+ const { getServiceProviderNames } = useServiceProvider();
490
+ const { getAll: getAllBuildings } = useBuilding();
491
+ const { getSiteById } = useSite();
492
+ const route = useRoute();
493
+ const { APP_NAME } = useRuntimeConfig().public;
494
+
495
+ // Get siteId from route params
496
+ const siteId = computed(() => (route.params.site as string) || "");
497
+
498
+ // Reactive site data that updates when siteId changes
499
+ const siteData = ref<any>(null);
500
+
501
+ // Fetch site data whenever siteId changes
502
+ watchEffect(async () => {
503
+ if (siteId.value) {
504
+ try {
505
+ const site = await getSiteById(siteId.value);
506
+ siteData.value = site;
507
+ } catch (error) {
508
+ console.error("Error fetching site:", error);
509
+ siteData.value = null;
510
+ }
511
+ } else {
512
+ siteData.value = null;
513
+ }
514
+ });
515
+
516
+ // Get current site name from fetched site data
517
+ const currentSiteName = computed(() => siteData.value?.name || "");
518
+
519
+ // Type definitions
520
+ type Period = "This Week" | "This Month" | "This Year";
521
+ type CardType = "guest" | "pickup" | "dropoff" | "contractor" | "delivery";
522
+
523
+ interface CardPeriods {
524
+ guest: Period;
525
+ pickup: Period;
526
+ dropoff: Period;
527
+ contractor: Period;
528
+ delivery: Period;
529
+ }
530
+
531
+ // Loading states
532
+ const isDashboardLoading = ref(false);
533
+ const isFeedbackTicketLoading = ref(false);
534
+
535
+ // Get dashboard data based on APP_NAME
536
+ const {
537
+ cards: dashboardCards,
538
+ feedbackItems,
539
+ feedbackChartDataMap,
540
+ periodMultipliers,
541
+ } = useDashboardData(APP_NAME);
542
+
543
+ // Period states for each section
544
+ const visitorPeriod = ref<Period>("This Week");
545
+ const feedbackPeriod = ref<Period>("This Week");
546
+ const bookingsPeriod = ref<Period>("This Week");
547
+ const buildingPeriod = ref<Period>("This Week");
548
+ const workOrdersPeriod = ref<Period>("This Week");
549
+
550
+ // Service provider states - now using feedback items from composable
551
+ const selectedService = ref<any>(null);
552
+ const serviceList = ref<Array<{ _id: string; name: string }>>(feedbackItems);
553
+
554
+ // Per-card periods keyed by label
555
+ const dynamicCardPeriods = ref<Record<string, Period>>({});
556
+ function updateCardPeriodDynamic(label: string, period: string) {
557
+ dynamicCardPeriods.value[label] = period as Period;
558
+ }
559
+
560
+ // Booking filter - list of facilities/buildings
561
+ const selectedBooking = ref<any>(null);
562
+ const bookingsList = ref<Array<{ _id: string; name: string }>>([]);
563
+
564
+ // Individual card periods
565
+ const cardPeriods = ref<CardPeriods>({
566
+ guest: "This Week",
567
+ pickup: "This Week",
568
+ dropoff: "This Week",
569
+ contractor: "This Week",
570
+ delivery: "This Week",
571
+ });
572
+
573
+ // Data sets for different periods
574
+ const dataByPeriod: Record<string, any> = {
575
+ "This Week": {
576
+ guest: { value: "156", percentage: "12.5 %", chipColor: "success" },
577
+ pickup: { value: "42", percentage: "8.3 %", chipColor: "success" },
578
+ dropoff: { value: "38", percentage: "5.2 %", chipColor: "success" },
579
+ contractor: { value: "23", percentage: "-15.4 %", chipColor: "error" },
580
+ delivery: { value: "67", percentage: "18.9 %", chipColor: "success" },
581
+ visitors: [
582
+ { x: 100, y: 220 },
583
+ { x: 200, y: 180 },
584
+ { x: 300, y: 150 },
585
+ { x: 400, y: 200 },
586
+ { x: 500, y: 140 },
587
+ { x: 600, y: 160 },
588
+ { x: 700, y: 100 },
589
+ { x: 800, y: 130 },
590
+ { x: 900, y: 90 },
591
+ { x: 1000, y: 120 },
592
+ { x: 1100, y: 80 },
593
+ ],
594
+ feedback: [
595
+ {
596
+ x: 100,
597
+ y: 272,
598
+ width: 60,
599
+ height: 18,
600
+ value: 1,
601
+ label: "1",
602
+ color: "#4CAF50",
603
+ },
604
+ {
605
+ x: 200,
606
+ y: 176,
607
+ width: 60,
608
+ height: 114,
609
+ value: 19,
610
+ label: "2",
611
+ color: "#4CAF50",
612
+ },
613
+ {
614
+ x: 300,
615
+ y: 230,
616
+ width: 60,
617
+ height: 60,
618
+ value: 10,
619
+ label: "3",
620
+ color: "#4CAF50",
621
+ },
622
+ {
623
+ x: 400,
624
+ y: 248,
625
+ width: 60,
626
+ height: 42,
627
+ value: 7,
628
+ label: "4",
629
+ color: "#4CAF50",
630
+ },
631
+ {
632
+ x: 500,
633
+ y: 260,
634
+ width: 60,
635
+ height: 30,
636
+ value: 5,
637
+ label: "5",
638
+ color: "#4CAF50",
639
+ },
640
+ {
641
+ x: 600,
642
+ y: 272,
643
+ width: 60,
644
+ height: 18,
645
+ value: 3,
646
+ label: "6",
647
+ color: "#4CAF50",
648
+ },
649
+ {
650
+ x: 700,
651
+ y: 194,
652
+ width: 60,
653
+ height: 96,
654
+ value: 16,
655
+ label: "7",
656
+ color: "#4CAF50",
657
+ },
658
+ {
659
+ x: 800,
660
+ y: 188,
661
+ width: 60,
662
+ height: 102,
663
+ value: 17,
664
+ label: "8",
665
+ color: "#4CAF50",
666
+ },
667
+ {
668
+ x: 900,
669
+ y: 32,
670
+ width: 60,
671
+ height: 258,
672
+ value: 86,
673
+ label: "9",
674
+ color: "#4CAF50",
675
+ },
676
+ ],
677
+ },
678
+ "This Month": {
679
+ guest: { value: "624", percentage: "22.3 %", chipColor: "success" },
680
+ pickup: { value: "178", percentage: "15.7 %", chipColor: "success" },
681
+ dropoff: { value: "156", percentage: "12.1 %", chipColor: "success" },
682
+ contractor: { value: "89", percentage: "-8.2 %", chipColor: "error" },
683
+ delivery: { value: "245", percentage: "28.5 %", chipColor: "success" },
684
+ visitors: [
685
+ { x: 100, y: 180 },
686
+ { x: 200, y: 150 },
687
+ { x: 300, y: 120 },
688
+ { x: 400, y: 160 },
689
+ { x: 500, y: 100 },
690
+ { x: 600, y: 130 },
691
+ { x: 700, y: 80 },
692
+ { x: 800, y: 110 },
693
+ { x: 900, y: 70 },
694
+ { x: 1000, y: 90 },
695
+ { x: 1100, y: 60 },
696
+ ],
697
+ feedback: [
698
+ {
699
+ x: 100,
700
+ y: 254,
701
+ width: 60,
702
+ height: 36,
703
+ value: 6,
704
+ label: "1",
705
+ color: "#4CAF50",
706
+ },
707
+ {
708
+ x: 200,
709
+ y: 140,
710
+ width: 60,
711
+ height: 150,
712
+ value: 25,
713
+ label: "2",
714
+ color: "#4CAF50",
715
+ },
716
+ {
717
+ x: 300,
718
+ y: 194,
719
+ width: 60,
720
+ height: 96,
721
+ value: 16,
722
+ label: "3",
723
+ color: "#4CAF50",
724
+ },
725
+ {
726
+ x: 400,
727
+ y: 212,
728
+ width: 60,
729
+ height: 78,
730
+ value: 13,
731
+ label: "4",
732
+ color: "#4CAF50",
733
+ },
734
+ {
735
+ x: 500,
736
+ y: 230,
737
+ width: 60,
738
+ height: 60,
739
+ value: 10,
740
+ label: "5",
741
+ color: "#4CAF50",
742
+ },
743
+ {
744
+ x: 600,
745
+ y: 248,
746
+ width: 60,
747
+ height: 42,
748
+ value: 7,
749
+ label: "6",
750
+ color: "#4CAF50",
751
+ },
752
+ {
753
+ x: 700,
754
+ y: 158,
755
+ width: 60,
756
+ height: 132,
757
+ value: 22,
758
+ label: "7",
759
+ color: "#4CAF50",
760
+ },
761
+ {
762
+ x: 800,
763
+ y: 176,
764
+ width: 60,
765
+ height: 114,
766
+ value: 19,
767
+ label: "8",
768
+ color: "#4CAF50",
769
+ },
770
+ {
771
+ x: 900,
772
+ y: 50,
773
+ width: 60,
774
+ height: 240,
775
+ value: 80,
776
+ label: "9",
777
+ color: "#4CAF50",
778
+ },
779
+ ],
780
+ },
781
+ "This Year": {
782
+ guest: { value: "7,842", percentage: "35.6 %", chipColor: "success" },
783
+ pickup: { value: "2,134", percentage: "28.4 %", chipColor: "success" },
784
+ dropoff: { value: "1,987", percentage: "24.9 %", chipColor: "success" },
785
+ contractor: { value: "1,045", percentage: "5.3 %", chipColor: "success" },
786
+ delivery: { value: "3,256", percentage: "42.8 %", chipColor: "success" },
787
+ visitors: [
788
+ { x: 100, y: 240 },
789
+ { x: 200, y: 210 },
790
+ { x: 300, y: 190 },
791
+ { x: 400, y: 220 },
792
+ { x: 500, y: 170 },
793
+ { x: 600, y: 200 },
794
+ { x: 700, y: 140 },
795
+ { x: 800, y: 180 },
796
+ { x: 900, y: 130 },
797
+ { x: 1000, y: 160 },
798
+ { x: 1100, y: 110 },
799
+ ],
800
+ feedback: [
801
+ {
802
+ x: 100,
803
+ y: 236,
804
+ width: 60,
805
+ height: 54,
806
+ value: 9,
807
+ label: "1",
808
+ color: "#4CAF50",
809
+ },
810
+ {
811
+ x: 200,
812
+ y: 104,
813
+ width: 60,
814
+ height: 186,
815
+ value: 31,
816
+ label: "2",
817
+ color: "#4CAF50",
818
+ },
819
+ {
820
+ x: 300,
821
+ y: 176,
822
+ width: 60,
823
+ height: 114,
824
+ value: 19,
825
+ label: "3",
826
+ color: "#4CAF50",
827
+ },
828
+ {
829
+ x: 400,
830
+ y: 194,
831
+ width: 60,
832
+ height: 96,
833
+ value: 16,
834
+ label: "4",
835
+ color: "#4CAF50",
836
+ },
837
+ {
838
+ x: 500,
839
+ y: 212,
840
+ width: 60,
841
+ height: 78,
842
+ value: 13,
843
+ label: "5",
844
+ color: "#4CAF50",
845
+ },
846
+ {
847
+ x: 600,
848
+ y: 230,
849
+ width: 60,
850
+ height: 60,
851
+ value: 10,
852
+ label: "6",
853
+ color: "#4CAF50",
854
+ },
855
+ {
856
+ x: 700,
857
+ y: 122,
858
+ width: 60,
859
+ height: 168,
860
+ value: 28,
861
+ label: "7",
862
+ color: "#4CAF50",
863
+ },
864
+ {
865
+ x: 800,
866
+ y: 158,
867
+ width: 60,
868
+ height: 132,
869
+ value: 22,
870
+ label: "8",
871
+ color: "#4CAF50",
872
+ },
873
+ {
874
+ x: 900,
875
+ y: 20,
876
+ width: 60,
877
+ height: 270,
878
+ value: 90,
879
+ label: "9",
880
+ color: "#4CAF50",
881
+ },
882
+ ],
883
+ },
884
+ };
885
+
886
+
887
+ const { getDashboardData } = useDashboard();
888
+
889
+
890
+ const {
891
+ data: getDashboardDataReq,
892
+ refresh: getDashboardDataRefresh,
893
+ pending: getDashboardDataPending,
894
+ } = await useLazyAsyncData(
895
+ `get-dashboard-data-${siteId}`,
896
+ () => getDashboardData({}),
897
+ {
898
+ watch: [ () => route.query],
899
+ }
900
+ );
901
+
902
+ watch(getDashboardDataReq, (newVal) => {
903
+ // items.value = newVal?.items || [];
904
+ // pages.value = newVal?.pages || 0;
905
+ // pageRange.value = newVal?.pageRange || "-- - -- of --";
906
+ });
907
+
908
+ // Computed properties for reactive data with site-based multiplier and APP_NAME-based cards
909
+ const countCardList = computed(() => {
910
+ // Calculate multiplier based on siteId (different sites get different multipliers)
911
+ const siteHash = siteId.value
912
+ ? siteId.value.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
913
+ : 0;
914
+ const siteMultiplier = siteId.value ? 0.6 + (siteHash % 40) / 100 : 1; // Between 0.6 and 1.0
915
+
916
+ const applyMultiplier = (value: number) => {
917
+ const newValue = Math.round(value * siteMultiplier);
918
+ return newValue >= 1000 ? newValue.toLocaleString() : newValue.toString();
919
+ };
920
+
921
+ // Map dashboard cards to card list format
922
+ return dashboardCards.map((card, idx) => {
923
+ const label = card.title;
924
+ const period =
925
+ dynamicCardPeriods.value[label] ||
926
+ (dynamicCardPeriods.value[label] = "This Week");
927
+ const periodFactor = periodMultipliers[period] ?? 1;
928
+ const multipliedValue = Math.round(card.value * periodFactor);
929
+
930
+ return {
931
+ id: idx + 1,
932
+ label,
933
+ value: applyMultiplier(multipliedValue),
934
+ icon: card.icon,
935
+ color: card.color,
936
+ percentage: `${card.percentage > 0 ? "+" : ""}${card.percentage} %`,
937
+ chipColor: card.isPositive ? "success" : "error",
938
+ period,
939
+ };
940
+ });
941
+ });
942
+
943
+ // Computed reactive data
944
+ const visitorPoints = computed(
945
+ () => dataByPeriod[visitorPeriod.value].visitors
946
+ );
947
+
948
+ const visitorLabels = [
949
+ { x: 100, text: "Mon" },
950
+ { x: 300, text: "Tue" },
951
+ { x: 500, text: "Wed" },
952
+ { x: 700, text: "Thu" },
953
+ { x: 900, text: "Fri" },
954
+ { x: 1100, text: "Sat" },
955
+ ];
956
+
957
+ const feedbackBars = computed(() => {
958
+ // If no service is selected, return the base data
959
+ if (!selectedService.value) {
960
+ return dataByPeriod[feedbackPeriod.value].feedback;
961
+ }
962
+
963
+ // If a service is selected, use the feedback chart data from composable
964
+ const chartData = feedbackChartDataMap[selectedService.value];
965
+ if (chartData && chartData[feedbackPeriod.value]) {
966
+ return chartData[feedbackPeriod.value];
967
+ }
968
+
969
+ // Fallback to base data if service data not found
970
+ return dataByPeriod[feedbackPeriod.value].feedback;
971
+ });
972
+
973
+ // Function to update individual card periods
974
+ const updateCardPeriod = (cardType: string, period: string) => {
975
+ const validCardType = cardType as CardType;
976
+ cardPeriods.value[validCardType] = period as Period;
977
+ };
978
+
979
+ // Function to update pie chart periods
980
+ const updatePiePeriod = (pieId: number, period: string) => {
981
+ const validPeriod = period as Period;
982
+ if (pieId === 1) bookingsPeriod.value = validPeriod;
983
+ else if (pieId === 2) buildingPeriod.value = validPeriod;
984
+ else if (pieId === 3) workOrdersPeriod.value = validPeriod;
985
+ };
986
+
987
+ // Function to fetch bookings data based on selected facility
988
+ const fetchBookingsData = async () => {
989
+ if (!selectedBooking.value) {
990
+ // Reset to base data when no booking selected
991
+ return;
992
+ }
993
+ };
994
+
995
+ // Watch for booking selection changes
996
+ watch(selectedBooking, () => {
997
+ fetchBookingsData();
998
+ });
999
+
1000
+ // Pie chart data by period
1001
+ const pieChartDataByPeriod = {
1002
+ "This Week": {
1003
+ bookings: {
1004
+ total: 186,
1005
+ centerValue: "75.4%",
1006
+ segments: [
1007
+ {
1008
+ label: "approved",
1009
+ color: "#2196F3",
1010
+ percentage: 14.0,
1011
+ rotation: -90,
1012
+ },
1013
+ {
1014
+ label: "rejected",
1015
+ color: "#4CAF50",
1016
+ percentage: 10.6,
1017
+ rotation: -39.6,
1018
+ },
1019
+ {
1020
+ label: "pending",
1021
+ color: "#FFC107",
1022
+ percentage: 75.4,
1023
+ rotation: -1.44,
1024
+ },
1025
+ ],
1026
+ },
1027
+ building: {
1028
+ total: 278,
1029
+ centerValue: "98.2%",
1030
+ segments: [
1031
+ { label: "occupied", color: "#2196F3", percentage: 1.8, rotation: -90 },
1032
+ {
1033
+ label: "unoccupied",
1034
+ color: "#4CAF50",
1035
+ percentage: 98.2,
1036
+ rotation: -83.5,
1037
+ },
1038
+ ],
1039
+ },
1040
+ workOrders: {
1041
+ total: 127,
1042
+ centerValue: "33.3%",
1043
+ segments: [
1044
+ { label: "to-do", color: "#2196F3", percentage: 33.3, rotation: -90 },
1045
+ {
1046
+ label: "in-progress",
1047
+ color: "#4CAF50",
1048
+ percentage: 33.3,
1049
+ rotation: 29.9,
1050
+ },
1051
+ {
1052
+ label: "completed",
1053
+ color: "#FFC107",
1054
+ percentage: 33.4,
1055
+ rotation: 149.8,
1056
+ },
1057
+ ],
1058
+ },
1059
+ },
1060
+ "This Month": {
1061
+ bookings: {
1062
+ total: 742,
1063
+ centerValue: "68.2%",
1064
+ segments: [
1065
+ {
1066
+ label: "approved",
1067
+ color: "#2196F3",
1068
+ percentage: 22.5,
1069
+ rotation: -90,
1070
+ },
1071
+ {
1072
+ label: "rejected",
1073
+ color: "#4CAF50",
1074
+ percentage: 9.3,
1075
+ rotation: -9.0,
1076
+ },
1077
+ {
1078
+ label: "pending",
1079
+ color: "#FFC107",
1080
+ percentage: 68.2,
1081
+ rotation: 24.48,
1082
+ },
1083
+ ],
1084
+ },
1085
+ building: {
1086
+ total: 278,
1087
+ centerValue: "95.7%",
1088
+ segments: [
1089
+ { label: "occupied", color: "#2196F3", percentage: 4.3, rotation: -90 },
1090
+ {
1091
+ label: "unoccupied",
1092
+ color: "#4CAF50",
1093
+ percentage: 95.7,
1094
+ rotation: -74.5,
1095
+ },
1096
+ ],
1097
+ },
1098
+ workOrders: {
1099
+ total: 456,
1100
+ centerValue: "42.1%",
1101
+ segments: [
1102
+ { label: "to-do", color: "#2196F3", percentage: 28.5, rotation: -90 },
1103
+ {
1104
+ label: "in-progress",
1105
+ color: "#4CAF50",
1106
+ percentage: 29.4,
1107
+ rotation: 12.6,
1108
+ },
1109
+ {
1110
+ label: "completed",
1111
+ color: "#FFC107",
1112
+ percentage: 42.1,
1113
+ rotation: 118.4,
1114
+ },
1115
+ ],
1116
+ },
1117
+ },
1118
+ "This Year": {
1119
+ bookings: {
1120
+ total: 8924,
1121
+ centerValue: "58.9%",
1122
+ segments: [
1123
+ {
1124
+ label: "approved",
1125
+ color: "#2196F3",
1126
+ percentage: 35.6,
1127
+ rotation: -90,
1128
+ },
1129
+ {
1130
+ label: "rejected",
1131
+ color: "#4CAF50",
1132
+ percentage: 5.5,
1133
+ rotation: 38.16,
1134
+ },
1135
+ {
1136
+ label: "pending",
1137
+ color: "#FFC107",
1138
+ percentage: 58.9,
1139
+ rotation: 57.96,
1140
+ },
1141
+ ],
1142
+ },
1143
+ building: {
1144
+ total: 278,
1145
+ centerValue: "89.2%",
1146
+ segments: [
1147
+ {
1148
+ label: "occupied",
1149
+ color: "#2196F3",
1150
+ percentage: 10.8,
1151
+ rotation: -90,
1152
+ },
1153
+ {
1154
+ label: "unoccupied",
1155
+ color: "#4CAF50",
1156
+ percentage: 89.2,
1157
+ rotation: -51.1,
1158
+ },
1159
+ ],
1160
+ },
1161
+ workOrders: {
1162
+ total: 5234,
1163
+ centerValue: "62.8%",
1164
+ segments: [
1165
+ { label: "to-do", color: "#2196F3", percentage: 18.4, rotation: -90 },
1166
+ {
1167
+ label: "in-progress",
1168
+ color: "#4CAF50",
1169
+ percentage: 18.8,
1170
+ rotation: -23.8,
1171
+ },
1172
+ {
1173
+ label: "completed",
1174
+ color: "#FFC107",
1175
+ percentage: 62.8,
1176
+ rotation: 43.8,
1177
+ },
1178
+ ],
1179
+ },
1180
+ },
1181
+ };
1182
+
1183
+ // Computed Pie Charts Data
1184
+ const pieChartList = computed(() => {
1185
+ // Get base bookings data
1186
+ const baseBookingsData = pieChartDataByPeriod[bookingsPeriod.value].bookings;
1187
+ let bookingsData = { ...baseBookingsData };
1188
+
1189
+ if (selectedBooking.value) {
1190
+ const bookingHash = selectedBooking.value
1191
+ .split("")
1192
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0);
1193
+ const multiplier = 0.35 + (bookingHash % 45) / 100; // Between 0.35 and 0.8
1194
+ const newTotal = Math.round(baseBookingsData.total * multiplier);
1195
+
1196
+ const segmentMultipliers = [
1197
+ 0.8 + (bookingHash % 15) / 100, // approved: 0.8-0.95
1198
+ 0.4 + (bookingHash % 30) / 100, // rejected: 0.4-0.7
1199
+ 0.5 + (bookingHash % 25) / 100, // pending: 0.5-0.75
1200
+ ];
1201
+
1202
+ const newSegments = baseBookingsData.segments.map(
1203
+ (seg: any, idx: number) => {
1204
+ const segMultiplier = segmentMultipliers[idx] || multiplier;
1205
+ const newPercentage = seg.percentage * segMultiplier;
1206
+ return {
1207
+ ...seg,
1208
+ percentage: newPercentage,
1209
+ };
1210
+ }
1211
+ );
1212
+
1213
+ // Normalize percentages to total 100% (make full circle)
1214
+ const totalPercentage = newSegments.reduce(
1215
+ (sum: number, seg: any) => sum + seg.percentage,
1216
+ 0
1217
+ );
1218
+ const normalizedSegments = newSegments.map((seg: any) => ({
1219
+ ...seg,
1220
+ percentage: (seg.percentage / totalPercentage) * 100,
1221
+ }));
1222
+
1223
+ // Recalculate rotations based on normalized percentages
1224
+ let currentRotation = -90;
1225
+ const adjustedSegments = normalizedSegments.map((seg: any) => {
1226
+ const segmentWithRotation = {
1227
+ ...seg,
1228
+ rotation: currentRotation,
1229
+ };
1230
+ currentRotation += seg.percentage * 3.6; // 360 degrees / 100 percentage
1231
+ return segmentWithRotation;
1232
+ });
1233
+
1234
+ // Calculate new center value based on the largest segment
1235
+ const maxSegment = adjustedSegments.reduce((max: any, seg: any) =>
1236
+ seg.percentage > max.percentage ? seg : max
1237
+ );
1238
+ const newCenterValue = `${Math.round(maxSegment.percentage)}%`;
1239
+
1240
+ bookingsData = {
1241
+ total: newTotal,
1242
+ centerValue: newCenterValue,
1243
+ segments: adjustedSegments,
1244
+ };
1245
+ }
1246
+
1247
+ return [
1248
+ {
1249
+ id: 1,
1250
+ title: "Bookings",
1251
+ total: bookingsData.total,
1252
+ centerValue: bookingsData.centerValue,
1253
+ segments: bookingsData.segments,
1254
+ period: bookingsPeriod.value,
1255
+ },
1256
+ {
1257
+ id: 2,
1258
+ title: "Building Mngm.",
1259
+ total: pieChartDataByPeriod[buildingPeriod.value].building.total,
1260
+ centerValue:
1261
+ pieChartDataByPeriod[buildingPeriod.value].building.centerValue,
1262
+ segments: pieChartDataByPeriod[buildingPeriod.value].building.segments,
1263
+ period: buildingPeriod.value,
1264
+ },
1265
+ {
1266
+ id: 3,
1267
+ title: "Work Orders",
1268
+ total: pieChartDataByPeriod[workOrdersPeriod.value].workOrders.total,
1269
+ centerValue:
1270
+ pieChartDataByPeriod[workOrdersPeriod.value].workOrders.centerValue,
1271
+ segments:
1272
+ pieChartDataByPeriod[workOrdersPeriod.value].workOrders.segments,
1273
+ period: workOrdersPeriod.value,
1274
+ },
1275
+ ];
1276
+ });
1277
+
1278
+ // Function to fetch feedback data based on selected service
1279
+ const fetchFeedbackData = async () => {
1280
+ if (!selectedService.value) {
1281
+ // Reset to base data when no service selected
1282
+ return;
1283
+ }
1284
+ };
1285
+
1286
+ // Watch for service selection changes
1287
+ watch(selectedService, () => {
1288
+ fetchFeedbackData();
1289
+ });
1290
+
1291
+ // Fetch buildings/facilities on mount
1292
+ onMounted(async () => {
1293
+ try {
1294
+ // Fetch buildings/facilities for bookings dropdown
1295
+ // Using buildings as facilities - you can adjust to use actual facilities API
1296
+ const buildingsResponse = await getAllBuildings({
1297
+ limit: 100,
1298
+ status: "active", // Only show active buildings
1299
+ });
1300
+ if (buildingsResponse && buildingsResponse.items) {
1301
+ bookingsList.value = buildingsResponse.items.map((building: any) => ({
1302
+ _id: building._id,
1303
+ name: building.name || building.block || `Building ${building._id}`,
1304
+ }));
1305
+ }
1306
+ } catch (error) {
1307
+ console.error("Error fetching data:", error);
1308
+ }
1309
+ });
1310
+
1311
+ // Base Facility Bookings Data
1312
+ const baseFacilityBookings = [
1313
+ {
1314
+ id: 1,
1315
+ label: "Approved Bookings",
1316
+ value: 34,
1317
+ icon: "mdi-calendar-check",
1318
+ iconColor: "success",
1319
+ },
1320
+ {
1321
+ id: 2,
1322
+ label: "Pending Bookings",
1323
+ value: 97,
1324
+ icon: "mdi-calendar-clock",
1325
+ iconColor: "warning",
1326
+ },
1327
+ {
1328
+ id: 3,
1329
+ label: "Cancelled Bookings",
1330
+ value: 28,
1331
+ icon: "mdi-calendar-remove",
1332
+ iconColor: "error",
1333
+ },
1334
+ {
1335
+ id: 4,
1336
+ label: "Rejected Bookings",
1337
+ value: 27,
1338
+ icon: "mdi-calendar-remove",
1339
+ iconColor: "error",
1340
+ },
1341
+ ];
1342
+
1343
+ // Computed Facility Bookings - changes based on selected facility
1344
+ const facilityBookings = computed(() => {
1345
+ // If no facility is selected, return base data
1346
+ if (!selectedBooking.value) {
1347
+ return baseFacilityBookings.map((booking) => ({
1348
+ ...booking,
1349
+ value: booking.value.toString(),
1350
+ }));
1351
+ }
1352
+
1353
+ // If a facility is selected, multiply values
1354
+ const bookingHash = selectedBooking.value
1355
+ .split("")
1356
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0);
1357
+ const multiplier = 0.35 + (bookingHash % 45) / 100; // Between 0.35 and 0.8
1358
+
1359
+ return baseFacilityBookings.map((booking) => ({
1360
+ ...booking,
1361
+ value: Math.round(booking.value * multiplier).toString(),
1362
+ }));
1363
+ });
1364
+ </script>
1365
+
1366
+ <style scoped>
1367
+ .dashboard-container {
1368
+ max-width: 100%;
1369
+ background-color: #f5f5f5;
1370
+ }
1371
+
1372
+ .card-hover {
1373
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1374
+ cursor: pointer;
1375
+ }
1376
+
1377
+ .card-hover:hover {
1378
+ transform: translateY(-4px);
1379
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1) !important;
1380
+ }
1381
+
1382
+ .chart-container {
1383
+ min-height: 250px;
1384
+ display: flex;
1385
+ align-items: center;
1386
+ justify-content: center;
1387
+ overflow-x: auto;
1388
+ -webkit-overflow-scrolling: touch;
1389
+ }
1390
+
1391
+ .h-100 {
1392
+ height: 100%;
1393
+ }
1394
+
1395
+ .w-100 {
1396
+ width: 100%;
1397
+ }
1398
+
1399
+ .position-relative {
1400
+ position: relative;
1401
+ }
1402
+
1403
+ .position-absolute {
1404
+ position: absolute;
1405
+ }
1406
+
1407
+ /* Responsive text sizing */
1408
+ @media (max-width: 600px) {
1409
+ .dashboard-container {
1410
+ padding: 12px !important;
1411
+ }
1412
+
1413
+ .chart-container {
1414
+ min-height: 250px;
1415
+ padding-bottom: 10px;
1416
+ }
1417
+
1418
+ .chart-container svg {
1419
+ min-width: 700px !important;
1420
+ }
1421
+ }
1422
+
1423
+ @media (min-width: 600px) and (max-width: 960px) {
1424
+ .chart-container {
1425
+ min-height: 260px;
1426
+ }
1427
+
1428
+ .chart-container svg {
1429
+ min-width: 800px !important;
1430
+ }
1431
+ }
1432
+
1433
+ @media (min-width: 960px) {
1434
+ .chart-container {
1435
+ min-height: 300px;
1436
+ }
1437
+
1438
+ .chart-container svg {
1439
+ min-width: auto !important;
1440
+ }
1441
+ }
1442
+
1443
+ /* SVG Animations */
1444
+ svg {
1445
+ max-width: 100%;
1446
+ height: auto;
1447
+ }
1448
+
1449
+ svg circle {
1450
+ transition: all 0.5s ease;
1451
+ }
1452
+
1453
+ svg polyline,
1454
+ svg rect {
1455
+ animation: fadeIn 1s ease-in-out;
1456
+ }
1457
+
1458
+ @keyframes fadeIn {
1459
+ from {
1460
+ opacity: 0;
1461
+ }
1462
+ to {
1463
+ opacity: 0.3;
1464
+ }
1465
+ }
1466
+
1467
+ /* Card styling improvements */
1468
+ .v-card {
1469
+ border-radius: 12px !important;
1470
+ overflow: hidden;
1471
+ }
1472
+
1473
+ .v-avatar {
1474
+ border-radius: 12px !important;
1475
+ }
1476
+
1477
+ .avatar-custom {
1478
+ width: 56px;
1479
+ height: 56px;
1480
+ border-radius: 12px;
1481
+ display: flex;
1482
+ align-items: center;
1483
+ justify-content: center;
1484
+ flex-shrink: 0;
1485
+ }
1486
+
1487
+ /* Better chip styling */
1488
+ .v-chip {
1489
+ font-weight: 600;
1490
+ }
1491
+
1492
+ /* Select dropdown styling */
1493
+ .v-select {
1494
+ border-radius: 8px;
1495
+ }
1496
+
1497
+ .legend-dot {
1498
+ width: 10px;
1499
+ height: 10px;
1500
+ border-radius: 50%;
1501
+ }
1502
+
1503
+ .legend-box {
1504
+ width: 12px;
1505
+ height: 12px;
1506
+ border-radius: 2px;
1507
+ }
1508
+
1509
+ .donut-center {
1510
+ position: absolute;
1511
+ top: 50%;
1512
+ left: 50%;
1513
+ transform: translate(-50%, -50%);
1514
+ text-align: center;
1515
+ }
1516
+
1517
+ .donut-segment {
1518
+ transition: opacity 0.2s ease;
1519
+ }
1520
+
1521
+ .donut-segment:hover {
1522
+ opacity: 0.8;
1523
+ }
1524
+ </style>