@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
@@ -0,0 +1,373 @@
1
+ <template>
2
+ <v-row no-gutters class="pa-8" justify="center">
3
+ <v-col cols="12" md="10">
4
+ <v-row no-gutters>
5
+ <v-col cols="12">
6
+ <PageHeader title="Notifications" />
7
+ </v-col>
8
+
9
+ <v-col cols="12" md="8" class="screen-hint intro-copy">
10
+ Choose what you want to hear about, and how. These settings are yours
11
+ alone and apply everywhere you use iService — on this site and on your
12
+ phone.
13
+ </v-col>
14
+
15
+ <!-- loading -->
16
+ <v-col v-if="loading" cols="12" class="mt-8">
17
+ <v-skeleton-loader type="list-item-two-line@4" />
18
+ </v-col>
19
+
20
+ <!-- could not load -->
21
+ <v-col v-else-if="loadError" cols="12" class="mt-8">
22
+ <v-alert type="error" variant="tonal" rounded="lg">
23
+ {{ loadError }}
24
+ </v-alert>
25
+ </v-col>
26
+
27
+ <!-- nothing to offer -->
28
+ <v-col v-else-if="!categories.length" cols="12" class="mt-8">
29
+ <v-alert type="info" variant="tonal" rounded="lg">
30
+ You are not set up to receive any notifications yet. Once you are
31
+ given access to a part of the system, its settings will appear here.
32
+ </v-alert>
33
+ </v-col>
34
+
35
+ <template v-else>
36
+ <!-- everything, at once -->
37
+ <v-col cols="12" class="mt-8">
38
+ <AppCard>
39
+ <v-row no-gutters align="center" class="pa-6">
40
+ <v-col cols="12" sm="8">
41
+ <div class="screen-card-title">Everything</div>
42
+ <div class="screen-hint summary-line mt-1">
43
+ {{ everythingSummary }}
44
+ </div>
45
+ </v-col>
46
+ <v-col cols="12" sm="4" class="d-flex justify-sm-end mt-3 mt-sm-0">
47
+ <AppButton
48
+ variant="ghost"
49
+ :disabled="saving"
50
+ @click="setAll(!allOn)"
51
+ >
52
+ {{ allOn ? "Turn everything off" : "Turn everything on" }}
53
+ </AppButton>
54
+ </v-col>
55
+ </v-row>
56
+ </AppCard>
57
+ </v-col>
58
+
59
+ <!-- one panel per module, collapsed by default so a person with many
60
+ modules gets a short list rather than a wall of switches -->
61
+ <v-col cols="12" class="mt-4">
62
+ <v-expansion-panels variant="accordion" multiple class="screen-accordion">
63
+ <v-expansion-panel
64
+ v-for="category in categories"
65
+ :key="category.key"
66
+ :value="category.key"
67
+ elevation="0"
68
+ >
69
+ <v-expansion-panel-title>
70
+ <v-row no-gutters align="center">
71
+ <v-col cols="12" sm="6">
72
+ <span>{{ category.label }}</span>
73
+ <!-- "Safety" is a TAG, not a status word, so it takes
74
+ the handoff's tag chip - "small 7px-radius label" -
75
+ which is the dotless shape, on the `info` tone the
76
+ handoff assigns to "Role & info tags". A leading dot
77
+ here would draw it as a status the screen does not
78
+ have. -->
79
+ <StatusChip
80
+ v-if="category.safety"
81
+ tone="info"
82
+ label="Safety"
83
+ :dot="false"
84
+ class="ml-2"
85
+ />
86
+ </v-col>
87
+ <v-col cols="12" sm="6" class="screen-hint summary-line">
88
+ {{ summaryFor(category) }}
89
+ </v-col>
90
+ </v-row>
91
+ </v-expansion-panel-title>
92
+
93
+ <v-expansion-panel-text>
94
+ <div class="screen-hint summary-line mb-4">
95
+ {{ category.description }}
96
+ </div>
97
+
98
+ <AppButton
99
+ variant="ghost"
100
+ class="mb-4"
101
+ :disabled="saving || !switchableIn(category).length"
102
+ @click="setCategory(category, !categoryAllOn(category))"
103
+ >
104
+ {{
105
+ categoryAllOn(category)
106
+ ? "Turn all off for " + category.label
107
+ : "Turn all on for " + category.label
108
+ }}
109
+ </AppButton>
110
+
111
+ <!-- one channel per row: a switch must never sit beside the
112
+ NEXT channel's label, which is what a three-up grid does -->
113
+ <v-row no-gutters>
114
+ <v-col
115
+ v-for="(channel, index) in category.channels"
116
+ :key="channel.key"
117
+ cols="12"
118
+ :class="index < category.channels.length - 1 ? 'border-b-thin' : ''"
119
+ >
120
+ <div
121
+ class="d-flex align-center justify-space-between py-2"
122
+ :class="{ 'channel-unavailable': !channel.supported }"
123
+ >
124
+ <div class="pr-4">
125
+ <div class="channel-label">{{ channel.label }}</div>
126
+ <div v-if="!channel.supported" class="channel-note mt-1">
127
+ {{ channel.note }}
128
+ </div>
129
+ </div>
130
+
131
+ <v-switch
132
+ :model-value="channel.enabled"
133
+ :disabled="!channel.supported || saving"
134
+ color="primary"
135
+ hide-details
136
+ density="compact"
137
+ :aria-label="`${channel.label} for ${category.label}`"
138
+ @update:model-value="
139
+ (value) => onToggle(category, channel, !!value)
140
+ "
141
+ />
142
+ </div>
143
+ </v-col>
144
+ </v-row>
145
+ </v-expansion-panel-text>
146
+ </v-expansion-panel>
147
+ </v-expansion-panels>
148
+ </v-col>
149
+ </template>
150
+ </v-row>
151
+ </v-col>
152
+
153
+ <!-- switching a safety alert off: say plainly what stops arriving -->
154
+ <v-dialog v-model="confirmOpen" max-width="460" persistent>
155
+ <v-card class="screen-modal pa-2">
156
+ <v-card-title>{{ confirmText.title }}</v-card-title>
157
+ <v-card-text class="screen-modal__body">
158
+ {{ confirmText.body }}
159
+ </v-card-text>
160
+ <v-card-actions class="justify-end">
161
+ <AppButton variant="ghost" @click="cancelConfirm">
162
+ {{ confirmText.cancel }}
163
+ </AppButton>
164
+ <AppButton @click="acceptConfirm">
165
+ {{ confirmText.confirm }}
166
+ </AppButton>
167
+ </v-card-actions>
168
+ </v-card>
169
+ </v-dialog>
170
+
171
+ <v-snackbar v-model="noticeOpen" :timeout="4000" location="bottom">
172
+ {{ notice }}
173
+ </v-snackbar>
174
+ </v-row>
175
+ </template>
176
+
177
+ <script setup lang="ts">
178
+ import type {
179
+ TNotificationCategorySetting,
180
+ TNotificationChannelSetting,
181
+ } from "../composables/useNotificationPreference";
182
+
183
+ const { getPreferences, savePreferences } = useNotificationPreference();
184
+
185
+ const categories = ref<TNotificationCategorySetting[]>([]);
186
+ const loading = ref(true);
187
+ const saving = ref(false);
188
+ const loadError = ref("");
189
+ const notice = ref("");
190
+ const noticeOpen = ref(false);
191
+
192
+ const confirmOpen = ref(false);
193
+ const confirmText = ref({ title: "", body: "", confirm: "", cancel: "" });
194
+ let confirmAccept: (() => void) | null = null;
195
+
196
+ /** the channels a person can actually move on this category */
197
+ const switchableIn = (category: TNotificationCategorySetting) =>
198
+ category.channels.filter((c) => c.supported);
199
+
200
+ const categoryAllOn = (category: TNotificationCategorySetting) =>
201
+ switchableIn(category).every((c) => c.enabled);
202
+
203
+ const allOn = computed(
204
+ () =>
205
+ categories.value.length > 0 &&
206
+ categories.value.every((category) => categoryAllOn(category)),
207
+ );
208
+
209
+ /** what is on, in words — so a collapsed panel still says what it is doing */
210
+ function summaryFor(category: TNotificationCategorySetting) {
211
+ const on = switchableIn(category).filter((c) => c.enabled);
212
+ if (!on.length) return "Off";
213
+ if (on.length === switchableIn(category).length) return "On everywhere";
214
+ return `On: ${on.map((c) => c.label).join(", ")}`;
215
+ }
216
+
217
+ const everythingSummary = computed(() => {
218
+ const total = categories.value.length;
219
+ const on = categories.value.filter((c) => categoryAllOn(c)).length;
220
+ if (on === total) return "Everything is on.";
221
+ if (on === 0) return "Everything is off.";
222
+ return `${on} of ${total} fully on.`;
223
+ });
224
+
225
+ async function load() {
226
+ loading.value = true;
227
+ loadError.value = "";
228
+ try {
229
+ const res = await getPreferences();
230
+ categories.value = res.data.categories;
231
+ } catch {
232
+ loadError.value =
233
+ "We could not load your notification settings. Please refresh the page and try again.";
234
+ } finally {
235
+ loading.value = false;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Save the whole screen after any change, and put it back the way it was if the
241
+ * save fails — so what is on screen is never a setting the server did not take.
242
+ */
243
+ async function persist(previous: TNotificationCategorySetting[]) {
244
+ saving.value = true;
245
+ try {
246
+ const res = await savePreferences({ channels: [], categories: categories.value });
247
+ categories.value = res.data.categories;
248
+ notice.value = "Saved.";
249
+ } catch {
250
+ categories.value = previous;
251
+ notice.value = "We could not save that change. Please try again.";
252
+ } finally {
253
+ noticeOpen.value = true;
254
+ saving.value = false;
255
+ }
256
+ }
257
+
258
+ const snapshot = () =>
259
+ JSON.parse(JSON.stringify(categories.value)) as TNotificationCategorySetting[];
260
+
261
+ function apply(mutate: () => void) {
262
+ const previous = snapshot();
263
+ mutate();
264
+ void persist(previous);
265
+ }
266
+
267
+ function onToggle(
268
+ category: TNotificationCategorySetting,
269
+ channel: TNotificationChannelSetting,
270
+ value: boolean,
271
+ ) {
272
+ if (!channel.supported) return;
273
+
274
+ const commit = () => apply(() => (channel.enabled = value));
275
+
276
+ // switching a safety alert OFF is the only thing worth stopping to confirm
277
+ if (category.safety && !value) {
278
+ confirmText.value = safetyOffConfirmation(category.label, channel.label);
279
+ confirmAccept = commit;
280
+ confirmOpen.value = true;
281
+ return;
282
+ }
283
+
284
+ commit();
285
+ }
286
+
287
+ function setCategory(category: TNotificationCategorySetting, value: boolean) {
288
+ const commit = () =>
289
+ apply(() => {
290
+ for (const channel of switchableIn(category)) channel.enabled = value;
291
+ });
292
+
293
+ if (category.safety && !value) {
294
+ confirmText.value = safetyOffConfirmation(category.label, "every channel");
295
+ confirmAccept = commit;
296
+ confirmOpen.value = true;
297
+ return;
298
+ }
299
+
300
+ commit();
301
+ }
302
+
303
+ function setAll(value: boolean) {
304
+ const commit = () =>
305
+ apply(() => {
306
+ for (const category of categories.value) {
307
+ for (const channel of switchableIn(category)) channel.enabled = value;
308
+ }
309
+ });
310
+
311
+ if (!value && categories.value.some((c) => c.safety)) {
312
+ confirmText.value = safetyOffConfirmation("safety and security", "every channel");
313
+ confirmAccept = commit;
314
+ confirmOpen.value = true;
315
+ return;
316
+ }
317
+
318
+ commit();
319
+ }
320
+
321
+ function acceptConfirm() {
322
+ confirmOpen.value = false;
323
+ confirmAccept?.();
324
+ confirmAccept = null;
325
+ }
326
+
327
+ function cancelConfirm() {
328
+ confirmOpen.value = false;
329
+ confirmAccept = null;
330
+ }
331
+
332
+ onMounted(load);
333
+ </script>
334
+
335
+ <style scoped>
336
+ /* The handoff names no type for long-form intro copy or a summary line, so
337
+ both take the design's own secondary sizes rather than Vuetify's `text-h6`
338
+ / `text-body-2` scale. */
339
+ .intro-copy {
340
+ font-size: var(--fs-cell);
341
+ margin-top: 6px;
342
+ }
343
+
344
+ .summary-line {
345
+ font-size: var(--fs-breadcrumb);
346
+ }
347
+
348
+ .channel-label {
349
+ font-family: var(--font-sans);
350
+ font-size: var(--fs-cell);
351
+ font-weight: var(--fw-cell);
352
+ color: var(--text);
353
+ }
354
+
355
+ .channel-note {
356
+ font-family: var(--font-sans);
357
+ font-size: var(--fs-breadcrumb);
358
+ color: var(--text2);
359
+ }
360
+
361
+ /* Unavailable channels are shown and explained, never hidden.
362
+ *
363
+ * They are NOT faded. The disabled switch already says the control cannot be
364
+ * moved, and the sentence beside it is the only place a person is told why —
365
+ * dimming that sentence is dimming the explanation. A blanket opacity here
366
+ * measured 2.3:1 against the card, which is unreadable; the row is now marked
367
+ * by a rule down its edge instead, and every word stays at full strength. */
368
+ .channel-unavailable {
369
+ border-left: 2px solid rgb(var(--v-border-color));
370
+ padding-left: 12px;
371
+ opacity: 1;
372
+ }
373
+ </style>
@@ -4,11 +4,18 @@
4
4
  <v-row>
5
5
  <v-col cols="6">
6
6
  <InputLabel class="text-capitalize font-weight-bold" :title="title" :required="required" />
7
- <v-text-field v-model.number="count" type="number" density="comfortable" :rules="rules" :readonly="readOnly" />
7
+ <v-text-field v-model.number="count" type="number" class="filter-field" density="comfortable" :rules="rules" :readonly="readOnly" />
8
8
  </v-col>
9
9
 
10
10
  <v-col cols="6">
11
- <v-btn v-if="!readOnly" color="primary" class="text-none mt-6" size="large" variant="flat"
11
+ <!-- Was `color="primary" size="large"`. On a `v-btn`,
12
+ `primary` is this theme's FOREGROUND token (links,
13
+ icons, text buttons) - the fill is `primary-button`, so
14
+ a flat button asking for `primary` painted itself with
15
+ an ink colour. `.screen-btn-primary` is the design's
16
+ 40px primary and carries the correct fill, so the
17
+ colour prop and the Vuetify size both go. -->
18
+ <v-btn v-if="!readOnly" class="text-none mt-6 screen-btn-primary" variant="flat"
12
19
  :disabled="!valid || disabled" :loading="loading || updating" text="Save" @click="handleSave" />
13
20
  </v-col>
14
21
  </v-row>