@7365admin1/layer-common 3.2.7 → 3.2.8-staging.201

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 (283) hide show
  1. package/assets/css/primitives.css +1113 -0
  2. package/assets/css/screens.css +1097 -0
  3. package/assets/css/shell.css +258 -0
  4. package/assets/css/tokens.css +236 -0
  5. package/components/AcceptDialog.vue +17 -18
  6. package/components/AccessCard/AvailableStats.vue +24 -16
  7. package/components/AccessCardAddForm.vue +42 -42
  8. package/components/AccessCardAssignToUnitForm.vue +13 -21
  9. package/components/AccessCardDeleteDialog.vue +41 -39
  10. package/components/AccessCardDetailsDialog.vue +125 -79
  11. package/components/AccessCardPreviewDialog.vue +58 -84
  12. package/components/AccessCardQrTagging.vue +60 -63
  13. package/components/AccessCardReplaceForm.vue +27 -23
  14. package/components/AccessManagement.vue +86 -85
  15. package/components/AddEqupmentForm.vue +18 -22
  16. package/components/AddPassKeyToVisitor.vue +95 -73
  17. package/components/AppButton.vue +62 -0
  18. package/components/AppCard.vue +20 -0
  19. package/components/AppDateField.vue +49 -0
  20. package/components/AppField.vue +76 -0
  21. package/components/AppKpiTile.vue +65 -0
  22. package/components/AppSegmented.vue +38 -0
  23. package/components/AppSelect.vue +247 -0
  24. package/components/AreaChecklistHistoryLogs.vue +51 -44
  25. package/components/AreaChecklistHistoryMain.vue +14 -7
  26. package/components/AreaFormDialog.vue +71 -59
  27. package/components/AreaMain.vue +73 -107
  28. package/components/AttachmentsViewer.vue +84 -22
  29. package/components/AttendanceCheckInOutDialog.vue +85 -88
  30. package/components/AttendanceDetailsDialog.vue +57 -63
  31. package/components/AttendanceMain.vue +4 -3
  32. package/components/AttendanceMapSearchDialog.vue +42 -44
  33. package/components/AttendanceSettingsDialog.vue +79 -63
  34. package/components/Avatar/Main.vue +54 -2
  35. package/components/BillingMain.vue +26 -30
  36. package/components/BtnUploadFile.vue +53 -11
  37. package/components/BuildingForm.vue +34 -27
  38. package/components/BuildingManagement/buildings.vue +39 -39
  39. package/components/BuildingManagement/units.vue +26 -42
  40. package/components/BuildingUnitFormAdd.vue +20 -23
  41. package/components/BuildingUnitFormEdit.vue +8 -4
  42. package/components/BulletinBoardForm.vue +19 -24
  43. package/components/BulletinBoardManagement.vue +49 -57
  44. package/components/BulletinBoardView.vue +9 -6
  45. package/components/BulletinExpirationChip.vue +13 -4
  46. package/components/Button/Close.vue +2 -2
  47. package/components/CameraForm.vue +21 -22
  48. package/components/CameraMain.vue +50 -49
  49. package/components/Card/DeleteConfirmation.vue +24 -22
  50. package/components/Card/MemberInfoSummary.vue +28 -3
  51. package/components/Card/Toggle.vue +15 -3
  52. package/components/CardToolbar.vue +37 -0
  53. package/components/Carousel.vue +56 -36
  54. package/components/Chat/Bubbles.vue +43 -14
  55. package/components/Chat/Information.vue +49 -32
  56. package/components/Chat/ListCard.vue +38 -9
  57. package/components/Chat/Message.vue +41 -15
  58. package/components/Chat/Navigation.vue +27 -15
  59. package/components/Chat/SkeletonLoader.vue +9 -2
  60. package/components/ChecklistItemRow.vue +25 -1
  61. package/components/CleaningScheduleMain.vue +44 -49
  62. package/components/ClientDetailForm.vue +81 -56
  63. package/components/ClientMain.vue +92 -82
  64. package/components/ConfirmDialog.vue +14 -8
  65. package/components/Container/Standard.vue +9 -7
  66. package/components/CreateSubsciptionPlan.vue +148 -89
  67. package/components/DashboardEmptyState.vue +48 -3
  68. package/components/DashboardMain.vue +924 -676
  69. package/components/DashboardPanel.vue +84 -21
  70. package/components/DeliveryCompany.vue +34 -20
  71. package/components/Dialog/DeleteConfirmation.vue +20 -21
  72. package/components/Dialog/ReplaceAutofillPrompt.vue +19 -26
  73. package/components/Dialog/ReusablePrompt.vue +30 -29
  74. package/components/Dialog/UpdateMoreAction.vue +62 -59
  75. package/components/DocumentForm.vue +14 -21
  76. package/components/DocumentManagement.vue +162 -108
  77. package/components/DocumentViewer.vue +29 -12
  78. package/components/DrawImage.vue +29 -21
  79. package/components/EntryPass/QrTemplatePreview.vue +27 -16
  80. package/components/EntryPassInformation.vue +40 -10
  81. package/components/EntryPassMain.vue +49 -27
  82. package/components/EquipmentItemMain.vue +84 -97
  83. package/components/EquipmentManagementMain.vue +81 -103
  84. package/components/Facility/BookingSetup.vue +43 -52
  85. package/components/Feedback/Form.vue +26 -12
  86. package/components/FeedbackMain.vue +35 -49
  87. package/components/FileInput.vue +75 -58
  88. package/components/FileInputWithList.vue +59 -12
  89. package/components/FormDialog.vue +60 -21
  90. package/components/HidAccessLogDashboard.vue +512 -490
  91. package/components/HidCameraCaptureDialog.vue +216 -0
  92. package/components/HidEnabledGate.vue +67 -9
  93. package/components/HidIntercomManagement.vue +1322 -653
  94. package/components/HidQrCodeConfiguration.vue +1030 -258
  95. package/components/HidReaderForm.vue +142 -59
  96. package/components/HidReaderManagement.vue +211 -323
  97. package/components/HidServiceSettingsPanel.vue +44 -13
  98. package/components/HidUserEnrollment.vue +1185 -412
  99. package/components/HygieneUpdateMoreAction.vue +44 -44
  100. package/components/IncidentReport/Authorities.vue +30 -18
  101. package/components/IncidentReport/IncidentInformation.vue +10 -10
  102. package/components/IncidentReport/IncidentInformationDownload.vue +3 -4
  103. package/components/IncidentReport/affectedEntities.vue +41 -20
  104. package/components/Input/Date.vue +3 -0
  105. package/components/Input/DatePicker.vue +1 -2
  106. package/components/Input/DateTimePicker.vue +1 -1
  107. package/components/Input/File.vue +55 -10
  108. package/components/Input/FileV2.vue +39 -6
  109. package/components/Input/InputPhoneNumberV2.vue +2 -1
  110. package/components/Input/ListGroupSelection.vue +56 -8
  111. package/components/Input/NRICNumber.vue +1 -0
  112. package/components/Input/NewDate.vue +1 -0
  113. package/components/Input/Number.vue +1 -0
  114. package/components/Input/Password.vue +1 -0
  115. package/components/Input/PhoneNumber.vue +21 -1
  116. package/components/Input/VehicleNumber.vue +1 -0
  117. package/components/InputLabel.vue +2 -2
  118. package/components/InvitationClientForm.vue +21 -19
  119. package/components/InvitationForm.vue +15 -21
  120. package/components/InvitationMain.vue +46 -61
  121. package/components/Layout/Header.vue +263 -36
  122. package/components/Layout/NavigationDrawer.vue +300 -30
  123. package/components/ListItem.vue +33 -3
  124. package/components/LocalPagination.vue +27 -3
  125. package/components/ManageChecklistMain.vue +136 -86
  126. package/components/MemberInformation.vue +32 -16
  127. package/components/MemberMain.vue +113 -93
  128. package/components/MyAttendanceMain.vue +3 -5
  129. package/components/NavigationItem.vue +83 -3
  130. package/components/Nfc/NFCPatrolReportMain.vue +47 -33
  131. package/components/Nfc/NFCPatrolRouteForm.vue +18 -20
  132. package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
  133. package/components/Nfc/NFCPatrolSettings.vue +14 -4
  134. package/components/Nfc/NFCTagForm.vue +22 -8
  135. package/components/Nfc/NFCTagMain.vue +41 -43
  136. package/components/Nfc/PatrolReport/DailyReportTab.vue +8 -1
  137. package/components/Nfc/PatrolReport/MonthlyReportTab.vue +8 -1
  138. package/components/Nfc/PatrolReport/MonthlyReportTable.vue +5 -3
  139. package/components/Nfc/PatrolReport/PatrolActivityTable.vue +26 -6
  140. package/components/Nfc/PatrolReport/PatrolReportTab.vue +8 -0
  141. package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +41 -9
  142. package/components/Nfc/PatrolReport/RemarksDialog.vue +39 -9
  143. package/components/Nfc/PatrolReport/ReportEmptyState.vue +29 -9
  144. package/components/Nfc/PatrolReport/ReportFilters.vue +19 -10
  145. package/components/Nfc/PatrolReport/ReportLocationHeader.vue +32 -6
  146. package/components/Nfc/PatrolReport/SummaryReportTable.vue +28 -6
  147. package/components/NotificationSettings.vue +373 -0
  148. package/components/NumberSettingField.vue +9 -2
  149. package/components/OnlineFormConfigurationForm.vue +725 -237
  150. package/components/OnlineFormFill.vue +213 -106
  151. package/components/OnlineFormRenderPreview.vue +25 -30
  152. package/components/OnlineFormsConfiguration.vue +113 -109
  153. package/components/OrgViewDialog.vue +34 -16
  154. package/components/OvernightParkingAvailability.vue +49 -26
  155. package/components/OvernightParkingManagement.vue +78 -18
  156. package/components/PageHeader.vue +43 -0
  157. package/components/PassInformation.vue +109 -53
  158. package/components/PasswordConfirmation.vue +44 -26
  159. package/components/PeopleForm.vue +41 -20
  160. package/components/PhotoUpload.vue +110 -48
  161. package/components/PlaceholderComponent.vue +35 -19
  162. package/components/PlanActionsMenu.vue +12 -2
  163. package/components/PlanStatusBadge.vue +7 -8
  164. package/components/PlateNumberDisplay.vue +9 -6
  165. package/components/QrTemplate/PrintDialog.vue +66 -43
  166. package/components/RolePermissionFormCreate.vue +104 -79
  167. package/components/RolePermissionFormPreviewUpdate.vue +111 -109
  168. package/components/RolePermissionMain.vue +183 -81
  169. package/components/ScanVisitorQRCode.vue +99 -35
  170. package/components/ScheduleAreaMain.vue +34 -59
  171. package/components/ScheduleTaskForm.vue +61 -42
  172. package/components/ScheduleTaskMain.vue +56 -69
  173. package/components/SearchVehicleNumberUser.vue +39 -37
  174. package/components/ServiceProviderFormCreate.vue +29 -20
  175. package/components/ServiceProviderInvitationPrompt.vue +155 -0
  176. package/components/ServiceProviderMain.vue +601 -432
  177. package/components/Signature.vue +40 -34
  178. package/components/SignaturePad.vue +5 -29
  179. package/components/SiteSettings.vue +41 -11
  180. package/components/SiteSettingsWorkOrder.vue +61 -30
  181. package/components/SlideCardGroup.vue +35 -11
  182. package/components/Snackbar.vue +36 -3
  183. package/components/SpecificAttr.vue +18 -8
  184. package/components/StatusChip.vue +156 -0
  185. package/components/StockCard.vue +17 -12
  186. package/components/SubscriptionPlanTable.vue +207 -140
  187. package/components/SwitchContext.vue +201 -9
  188. package/components/TableHygiene.vue +36 -46
  189. package/components/TableList.vue +10 -10
  190. package/components/TableListSecondary.vue +105 -54
  191. package/components/TableMain.vue +251 -78
  192. package/components/TableV3.vue +61 -70
  193. package/components/TiptapEditor.vue +33 -40
  194. package/components/TiptapEditorQr.vue +5 -2
  195. package/components/Tooltip/Info.vue +69 -24
  196. package/components/UnitMain.vue +40 -60
  197. package/components/UnitPersonCard.vue +42 -13
  198. package/components/VehicleAddSelection.vue +48 -32
  199. package/components/VehicleForm.vue +112 -129
  200. package/components/VehicleManagement.vue +26 -12
  201. package/components/VehicleUpdateMoreAction.vue +59 -46
  202. package/components/VisitorDataFromScannedQRCodeForm.vue +90 -65
  203. package/components/VisitorForm.vue +774 -210
  204. package/components/VisitorFormSelection.vue +40 -37
  205. package/components/VisitorManagement.vue +239 -82
  206. package/components/VisitorPassKeyQRScanner.vue +65 -18
  207. package/components/VisitorSearchResultSummary.vue +60 -16
  208. package/components/VisitorSocketPopUp.vue +22 -14
  209. package/components/VisitorsReportPreview.vue +44 -29
  210. package/components/WorkOrder/Create.vue +26 -17
  211. package/components/WorkOrder/ListView.vue +38 -7
  212. package/components/WorkOrder/Main.vue +62 -59
  213. package/composables/useFeedback.ts +3 -3
  214. package/composables/useHidAmico.ts +269 -0
  215. package/composables/useHidNavigation.ts +2 -9
  216. package/composables/useMember.ts +3 -3
  217. package/composables/useNFCPatrolDailyReport.ts +13 -0
  218. package/composables/useNFCPatrolMonthlyReport.ts +22 -56
  219. package/composables/useNFCPatrolReport.ts +13 -0
  220. package/composables/useNotificationPreference.ts +87 -0
  221. package/composables/useOnlineFormPages.ts +2 -0
  222. package/composables/useRole.ts +22 -0
  223. package/composables/useServiceProvider.ts +3 -3
  224. package/composables/useServiceProviderInvitation.ts +145 -0
  225. package/composables/useSipWebPhone.ts +336 -0
  226. package/composables/useSubscriptionPlan.ts +62 -0
  227. package/composables/useThemePreference.ts +63 -0
  228. package/composables/useVisitor.ts +6 -1
  229. package/composables/useWebUsb.ts +127 -37
  230. package/composables/useWorkOrder.ts +17 -3
  231. package/error.vue +30 -6
  232. package/nuxt.config.ts +36 -0
  233. package/package.json +25 -4
  234. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +2 -2
  235. package/pages/[org]/[site]/access-mgmt/{identity-mapping → hid-cards}/index.vue +2 -2
  236. package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
  237. package/pages/notification-settings.vue +19 -0
  238. package/pages/payment-method-linked.vue +34 -10
  239. package/pages/require-customer.vue +48 -10
  240. package/pages/require-organization-membership.vue +32 -8
  241. package/pages/unauthorized.vue +29 -4
  242. package/plugins/secure-member.client.ts +21 -56
  243. package/plugins/vuetify.ts +69 -2
  244. package/types/nfc-patrol-report.ts +3 -2
  245. package/types/site.d.ts +71 -0
  246. package/types/subscription.ts +18 -11
  247. package/utils/breadcrumb.test.ts +108 -0
  248. package/utils/breadcrumb.ts +82 -0
  249. package/utils/dashboard.test.ts +118 -0
  250. package/utils/dashboard.ts +53 -0
  251. package/utils/data.test.ts +95 -0
  252. package/utils/data.ts +40 -2
  253. package/utils/hid-discover.test.ts +102 -0
  254. package/utils/list-page.test.ts +60 -0
  255. package/utils/list-page.ts +38 -0
  256. package/utils/materialSymbols.ts +363 -0
  257. package/utils/nav-sections.test.ts +190 -0
  258. package/utils/nav-sections.ts +151 -0
  259. package/utils/nfc-patrol-report.test.ts +130 -0
  260. package/utils/nfc-patrol-report.ts +101 -0
  261. package/utils/role.test.ts +69 -0
  262. package/utils/role.ts +10 -0
  263. package/utils/route-name.test.ts +85 -0
  264. package/utils/route-name.ts +24 -0
  265. package/utils/status.test.ts +110 -0
  266. package/utils/status.ts +135 -0
  267. package/utils/table-card.test.ts +84 -0
  268. package/utils/theme-aa-ledger.ts +148 -0
  269. package/utils/theme.test.ts +485 -7
  270. package/utils/theme.ts +295 -110
  271. package/.changeset/README.md +0 -8
  272. package/.changeset/config.json +0 -11
  273. package/.editorconfig +0 -12
  274. package/.github/workflows/main.yml +0 -17
  275. package/.github/workflows/publish-staging.yml +0 -47
  276. package/.github/workflows/publish.yml +0 -39
  277. package/PUBLISHING.md +0 -269
  278. package/components/HidIdentityMapping.vue +0 -975
  279. package/components/ScheduleTaskAreaFormDialog.vue +0 -144
  280. package/components/ScheduleTaskAreaUpdateMoreAction.vue +0 -109
  281. package/components/ScheduleTastTicketMain.vue +0 -182
  282. package/components/TableWithButton.vue +0 -94
  283. package/test/visitor-socket.test.mjs +0 -36
@@ -1,87 +1,86 @@
1
1
  <template>
2
2
  <v-container fluid class="pa-6">
3
- <!-- Header: Title & Create Button -->
4
- <div class="d-flex align-center justify-space-between mb-6">
5
- <h1 class="text-h5 font-weight-bold text-primary">
6
- Subscription Plans
7
- </h1>
8
-
9
- <v-btn
10
- color="secondary"
11
- rounded="pill"
12
- elevation="0"
13
- @click="openCreateModal"
14
- >
15
- Create Plan
16
- </v-btn>
17
- </div>
3
+ <!--
4
+ THE DESIGN'S TITLE ROW. Drawn by the shared `PageHeader` at 22px/800 so
5
+ this screen's heading matches every other screen's.
6
+ -->
7
+ <PageHeader title="Subscription Plans">
8
+ <template #actions>
9
+ <v-btn
10
+ class="text-none screen-btn-primary"
11
+ variant="flat"
12
+ @click="openCreateModal"
13
+ >
14
+ Create Plan
15
+ </v-btn>
16
+ </template>
17
+ </PageHeader>
18
18
 
19
19
  <!-- Search & Filters Bar -->
20
20
  <v-row class="mb-2" density="compact">
21
21
  <!-- Search Input -->
22
22
  <v-col cols="12" sm="4" md="3">
23
- <v-text-field
24
- v-model="search"
25
- placeholder="Search Plan"
26
- prepend-inner-icon="mdi-magnify"
27
- variant="outlined"
28
- density="compact"
29
- hide-details
30
- clearable
31
- />
23
+ <AppField v-model="search" search placeholder="Search Plan" />
32
24
  </v-col>
33
25
 
34
26
  <!-- Filter by Category -->
35
27
  <v-col cols="12" sm="4" md="3">
36
- <v-select
28
+ <AppSelect
37
29
  v-model="categoryFilter"
38
30
  :items="categoryOptions"
39
31
  label="Filter by category"
40
- variant="outlined"
41
- density="compact"
42
- hide-details
43
32
  />
44
33
  </v-col>
45
34
 
46
35
  <!-- Filter by Billing Cycle -->
47
36
  <v-col cols="12" sm="4" md="3">
48
- <v-select
37
+ <AppSelect
49
38
  v-model="billingFilter"
50
39
  :items="billingOptions"
51
40
  label="Filter by billing cycle"
52
- variant="outlined"
53
- density="compact"
54
- hide-details
55
41
  />
56
42
  </v-col>
57
43
  </v-row>
58
44
 
59
45
  <!-- Main Table Card -->
60
- <v-card variant="outlined" class="mt-4">
61
- <!-- Status Tabs & Actions Bar -->
62
- <div class="d-flex align-center justify-space-between bg-grey-lighten-4 border-b">
63
- <v-tabs v-model="activeTab" color="primary" density="compact">
64
- <v-tab value="active" class="text-none font-weight-bold">Active</v-tab>
65
- <v-tab value="deactive" class="text-none font-weight-bold">Deactive</v-tab>
66
- </v-tabs>
67
-
68
- <div class="pa-2">
46
+ <AppCard class="table-card mt-4">
47
+ <!--
48
+ The tabs and the refresh/pagination are the card's own toolbar row,
49
+ which is where the design draws tabs - it follows the theme because
50
+ it is tokenised, unlike the old hard-coded `bg-grey-lighten-4` strip.
51
+ -->
52
+ <CardToolbar :count="pageRange">
53
+ <template #tabs>
54
+ <v-tabs v-model="activeTab" color="primary" density="compact">
55
+ <v-tab value="active" class="text-none font-weight-bold">Active</v-tab>
56
+ <v-tab value="deactive" class="text-none font-weight-bold">Deactivated</v-tab>
57
+ </v-tabs>
58
+ </template>
59
+
60
+ <template #right>
69
61
  <v-btn
70
62
  icon="mdi-refresh"
71
63
  variant="text"
72
64
  density="comfortable"
73
- title="Refresh"
65
+ aria-label="Refresh"
74
66
  @click="refresh"
75
67
  />
76
- </div>
77
- </div>
68
+ <local-pagination
69
+ v-model="page"
70
+ :length="pages"
71
+ @update:value="getSubscriptionPlans"
72
+ />
73
+ </template>
74
+ </CardToolbar>
78
75
 
79
76
  <!-- Table View -->
80
77
  <v-data-table
81
78
  :headers="headers"
82
- :items="filteredPlans"
83
- :page="page"
84
- :items-per-page="pageSize"
79
+ :items="items"
80
+ :loading="loading"
81
+ item-value="id"
82
+ items-per-page="20"
83
+ hide-default-footer
85
84
  class="elevation-0"
86
85
  >
87
86
  <!-- Plan Name -->
@@ -91,7 +90,7 @@
91
90
 
92
91
  <!-- Plan Type -->
93
92
  <template #item.planType="{ item }">
94
- {{ item.planType === 'free_bundle' ? 'Free' : 'Paid' }}
93
+ {{ item.planType === "free_bundle" ? "Free" : "Paid" }}
95
94
  </template>
96
95
 
97
96
  <!-- Billing Cycle -->
@@ -107,22 +106,22 @@
107
106
  </template>
108
107
 
109
108
  <!-- Applications Badge -->
109
+ <!-- A count, not a status word, so it takes the design's neutral tag
110
+ shape (no leading dot) rather than a colour it has not earned. -->
110
111
  <template #item.applicationIds="{ item }">
111
- <v-chip size="small" variant="tonal" color="grey-darken-2">
112
- {{ item.applicationIds.length }} Application{{ item.applicationIds.length > 1 ? 's' : '' }}
113
- </v-chip>
112
+ <StatusChip tone="neutral" :dot="false">
113
+ {{ item.applicationIds.length }} Application{{
114
+ item.applicationIds.length > 1 ? "s" : ""
115
+ }}
116
+ </StatusChip>
114
117
  </template>
115
118
 
116
119
  <!-- Status Badge -->
120
+ <!-- The shared word -> tone map: "Active" is `ok` (was Vuetify
121
+ `success`) and "Deactivated" is unmapped, so it comes out
122
+ neutral - which is the grey it already was. -->
117
123
  <template #item.status="{ item }">
118
- <v-chip
119
- size="small"
120
- :color="item.status === 'active' ? 'success' : 'grey'"
121
- variant="flat"
122
- class="text-caption font-weight-bold"
123
- >
124
- {{ item.status === 'active' ? 'Active' : 'Deactive' }}
125
- </v-chip>
124
+ <StatusChip :status="item.status === 'active' ? 'Active' : 'Deactivated'" />
126
125
  </template>
127
126
 
128
127
  <!-- Actions -->
@@ -153,9 +152,15 @@
153
152
 
154
153
  <!-- Toggle Status (Deactivate / Activate) -->
155
154
  <v-list-item
156
- :prepend-icon="item.status === 'active' ? 'mdi-trash-can-outline' : 'mdi-check-circle-outline'"
155
+ :prepend-icon="
156
+ item.status === 'active'
157
+ ? 'mdi-trash-can-outline'
158
+ : 'mdi-check-circle-outline'
159
+ "
157
160
  :title="item.status === 'active' ? 'Deactivate' : 'Activate'"
158
- :class="item.status === 'active' ? 'text-error' : 'text-success'"
161
+ :class="
162
+ item.status === 'active' ? 'text-error' : 'text-success'
163
+ "
159
164
  value="toggle-status"
160
165
  class="text-body-2"
161
166
  @click="togglePlanStatus(item)"
@@ -166,13 +171,15 @@
166
171
  </template>
167
172
 
168
173
  <!-- Empty State -->
174
+ <!-- Same sentence, in the shared empty block the design draws it in. -->
169
175
  <template #no-data>
170
- <div class="py-8 text-center text-grey-darken-1">
171
- No plans found.
176
+ <div class="table-card__empty">
177
+ <v-icon icon="mdi-magnify-close" size="32" />
178
+ <span>No plans found.</span>
172
179
  </div>
173
180
  </template>
174
181
  </v-data-table>
175
- </v-card>
182
+ </AppCard>
176
183
 
177
184
  <!-- Create / Edit Dialog -->
178
185
  <v-dialog v-model="showModal" max-width="600px" persistent>
@@ -184,12 +191,15 @@
184
191
  />
185
192
  </v-card>
186
193
  </v-dialog>
194
+
195
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
187
196
  </v-container>
188
197
  </template>
189
198
 
190
199
  <script setup lang="ts">
191
- import { ref, reactive, computed, watch } from "vue";
200
+ import { ref, computed, watch, watchEffect } from "vue";
192
201
  import CreateSubscriptionPlan from "./CreateSubsciptionPlan.vue";
202
+ import useSubscriptionPlan from "../composables/useSubscriptionPlan";
193
203
 
194
204
  import type {
195
205
  SubscriptionPlan,
@@ -205,7 +215,12 @@ const headers = [
205
215
  { title: "Price", key: "price", align: "start" as const },
206
216
  { title: "Applications", key: "applicationIds", align: "center" as const },
207
217
  { title: "Status", key: "status", align: "center" as const },
208
- { title: "Actions", key: "actions", align: "center" as const, sortable: false },
218
+ {
219
+ title: "Actions",
220
+ key: "actions",
221
+ align: "center" as const,
222
+ sortable: false,
223
+ },
209
224
  ];
210
225
 
211
226
  // Select Options
@@ -222,30 +237,12 @@ const billingOptions = [
222
237
  { title: "Yearly", value: "annually" },
223
238
  ];
224
239
 
225
- const plans = reactive<SubscriptionPlan[]>([
226
- {
227
- id: "1",
228
- name: "Standard",
229
- description: "",
230
- maxSeats: 10,
231
- planType: "paid_bundle",
232
- billingCycle: "monthly",
233
- price: 299,
234
- applicationIds: ["crm"],
235
- status: "active",
236
- },
237
- {
238
- id: "2",
239
- name: "Premium",
240
- description: "",
241
- maxSeats: 25,
242
- planType: "paid_bundle",
243
- billingCycle: "annually",
244
- price: 299,
245
- applicationIds: ["crm", "hrm"],
246
- status: "active",
247
- },
248
- ]);
240
+ const {
241
+ getAll: _getAll,
242
+ add: _add,
243
+ updateById: _updateById,
244
+ updateStatusById: _updateStatusById,
245
+ } = useSubscriptionPlan();
249
246
 
250
247
  const search = ref("");
251
248
  const categoryFilter = ref("all");
@@ -253,38 +250,79 @@ const billingFilter = ref<"all" | BillingCycle>("all");
253
250
  const activeTab = ref<"active" | "deactive">("active");
254
251
 
255
252
  const page = ref(1);
256
- const pageSize = 20;
253
+ const pages = ref(0);
254
+ const pageRange = ref("-- - -- of --");
255
+
256
+ const message = ref("");
257
+ const messageSnackbar = ref(false);
258
+ const messageColor = ref("");
257
259
 
258
260
  const showModal = ref(false);
259
261
  const editingPlan = ref<SubscriptionPlan | null>(null);
262
+ const items = ref<SubscriptionPlan[]>([]);
263
+
264
+ function normalizePlan(raw: Record<string, any>): SubscriptionPlan {
265
+ return {
266
+ id: String(raw._id ?? raw.id),
267
+ name: raw.name,
268
+ description: raw.description ?? "",
269
+ maxSeats: raw.maxSeats,
270
+ planType: raw.planType,
271
+ billingCycle: raw.billingCycle ?? null,
272
+ price: raw.price ?? null,
273
+ applicationIds: (raw.applications ?? []).map(
274
+ (app: Record<string, any>) => app.applicationId
275
+ ),
276
+ status: raw.status === "active" ? "active" : "deactive",
277
+ };
278
+ }
260
279
 
261
- const filteredPlans = computed(() =>
262
- plans.filter((plan) => {
263
- if (plan.status !== activeTab.value) return false;
280
+ const {
281
+ data: getSubscriptionPlansReq,
282
+ refresh: getSubscriptionPlans,
283
+ status: getAllReqStatus,
284
+ } = useLazyAsyncData("get-all-subscription-plans", () =>
285
+ _getAll({
286
+ page: page.value,
287
+ search: search.value,
288
+ status: activeTab.value,
289
+ planType: categoryFilter.value === "all" ? "" : categoryFilter.value,
290
+ billingCycle: billingFilter.value === "all" ? "" : billingFilter.value,
291
+ })
292
+ );
264
293
 
265
- if (
266
- search.value &&
267
- !plan.name.toLowerCase().includes(search.value.toLowerCase())
268
- ) {
269
- return false;
270
- }
294
+ const loading = computed(() => getAllReqStatus.value === "pending");
271
295
 
272
- if (categoryFilter.value !== "all" && plan.planType !== categoryFilter.value) {
273
- return false;
274
- }
275
-
276
- if (billingFilter.value !== "all" && plan.billingCycle !== billingFilter.value) {
277
- return false;
278
- }
296
+ watchEffect(() => {
297
+ if (getSubscriptionPlansReq.value) {
298
+ items.value = (getSubscriptionPlansReq.value.items ?? []).map(
299
+ normalizePlan
300
+ );
301
+ pages.value = getSubscriptionPlansReq.value.pages;
302
+ pageRange.value = getSubscriptionPlansReq.value.pageRange;
303
+ }
304
+ });
279
305
 
280
- return true;
281
- })
282
- );
306
+ let searchDebounce: ReturnType<typeof setTimeout> | null = null;
307
+ watch(search, () => {
308
+ if (searchDebounce) clearTimeout(searchDebounce);
309
+ searchDebounce = setTimeout(() => {
310
+ page.value = 1;
311
+ getSubscriptionPlans();
312
+ }, 400);
313
+ });
283
314
 
284
- watch([search, categoryFilter, billingFilter, activeTab], () => {
315
+ watch([categoryFilter, billingFilter, activeTab], () => {
285
316
  page.value = 1;
317
+ getSubscriptionPlans();
286
318
  });
287
319
 
320
+ function showMessage(msg: string, color: string) {
321
+ message.value = msg;
322
+ messageColor.value = color;
323
+ messageSnackbar.value = true;
324
+ }
325
+
288
326
  function formatBillingCycle(cycle?: string) {
289
327
  if (!cycle) return "—";
290
328
  if (cycle === "annually" || cycle === "yearly") return "Yearly";
@@ -303,6 +341,7 @@ function refresh() {
303
341
  search.value = "";
304
342
  categoryFilter.value = "all";
305
343
  billingFilter.value = "all";
344
+ getSubscriptionPlans();
306
345
  }
307
346
 
308
347
  function openCreateModal() {
@@ -320,43 +359,71 @@ function closeModal() {
320
359
  editingPlan.value = null;
321
360
  }
322
361
 
323
- function handleSubmit(payload: PlanFormState) {
324
- const applicationIds =
325
- payload.planType === "custom_apps"
326
- ? payload.customApps
327
- : payload.bundleApps;
362
+ function buildApplications(payload: PlanFormState) {
363
+ if (payload.planType === "custom_apps") {
364
+ return payload.customApps.map((applicationId) => ({
365
+ applicationId,
366
+ price: payload.customAppPrices[applicationId] ?? 0,
367
+ }));
368
+ }
369
+
370
+ return payload.bundleApps.map((applicationId) => ({ applicationId }));
371
+ }
372
+
373
+ async function handleSubmit(payload: PlanFormState) {
374
+ const applications = buildApplications(payload);
328
375
 
329
376
  const price =
330
377
  payload.planType === "custom_apps"
331
- ? null
378
+ ? payload.customApps.reduce(
379
+ (total, value) => total + (payload.customAppPrices[value] ?? 0),
380
+ 0
381
+ )
332
382
  : payload.planType === "paid_bundle"
333
383
  ? payload.bundlePrice
334
384
  : null;
335
385
 
336
- if (editingPlan.value) {
337
- Object.assign(editingPlan.value, {
338
- name: payload.name,
339
- description: payload.description,
340
- maxSeats: payload.maxSeats,
341
- planType: payload.planType,
342
- billingCycle: payload.billingCycle,
343
- price,
344
- applicationIds,
345
- });
346
- } else {
347
- plans.push({
348
- id: crypto.randomUUID(),
349
- name: payload.name,
350
- description: payload.description,
351
- maxSeats: payload.maxSeats,
352
- planType: payload.planType,
353
- billingCycle: payload.billingCycle,
354
- price,
355
- applicationIds,
356
- status: "active",
357
- });
386
+ const body = {
387
+ name: payload.name,
388
+ description: payload.description,
389
+ maxSeats: payload.maxSeats,
390
+ planType: payload.planType,
391
+ billingCycle: payload.billingCycle,
392
+ price,
393
+ applications,
394
+ };
395
+
396
+ try {
397
+ if (editingPlan.value) {
398
+ await _updateById(editingPlan.value.id, body);
399
+ showMessage("Subscription plan updated successfully!", "success");
400
+ } else {
401
+ await _add(body);
402
+ showMessage("Subscription plan created successfully!", "success");
403
+ }
404
+
405
+ await getSubscriptionPlans();
406
+ closeModal();
407
+ } catch (error: any) {
408
+ showMessage(
409
+ error?.response?._data?.message || "Failed to save subscription plan.",
410
+ "error"
411
+ );
358
412
  }
413
+ }
359
414
 
360
- closeModal();
415
+ async function togglePlanStatus(plan: SubscriptionPlan) {
416
+ const nextStatus = plan.status === "active" ? "deactive" : "active";
417
+
418
+ try {
419
+ await _updateStatusById(plan.id, nextStatus);
420
+ await getSubscriptionPlans();
421
+ showMessage("Subscription plan status updated successfully!", "success");
422
+ } catch (error: any) {
423
+ showMessage(
424
+ error?.response?._data?.message || "Failed to update plan status.",
425
+ "error"
426
+ );
427
+ }
361
428
  }
362
429
  </script>