@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
@@ -68,6 +68,26 @@ export default function useRole() {
68
68
  });
69
69
  }
70
70
 
71
+ function getDeletionPreview(_id: string) {
72
+ return useNuxtApp().$api<Record<string, any>>(
73
+ `/api/roles/${_id}/deletion-preview`,
74
+ { method: "GET" }
75
+ );
76
+ }
77
+
78
+ function deleteWithReassignments(
79
+ _id: string,
80
+ reassignments: Array<{ memberId: string; roleId: string }>
81
+ ) {
82
+ return useNuxtApp().$api<Record<string, any>>(
83
+ `/api/roles/${_id}/delete-with-reassignments`,
84
+ {
85
+ method: "PUT",
86
+ body: { reassignments },
87
+ }
88
+ );
89
+ }
90
+
71
91
  const role = useState<TRole>("userRole", () => {
72
92
  return {
73
93
  _id: "",
@@ -103,6 +123,8 @@ export default function useRole() {
103
123
  updateRoleById,
104
124
  updateRoleFieldById,
105
125
  deleteRole,
126
+ getDeletionPreview,
127
+ deleteWithReassignments,
106
128
  updatePermissionById,
107
129
  getRoleByUserId,
108
130
  };
@@ -8,9 +8,9 @@ export default function useServiceProvider() {
8
8
  () => []
9
9
  );
10
10
 
11
- const page = useState("page", () => 1);
12
- const pages = useState("pages", () => 0);
13
- const pageRange = useState("pageRange", () => "-- - -- of --");
11
+ const page = useState("serviceProviderPage", () => 1);
12
+ const pages = useState("serviceProviderPages", () => 0);
13
+ const pageRange = useState("serviceProviderPageRange", () => "-- - -- of --");
14
14
 
15
15
  const serviceProvider = useState(
16
16
  "serviceProvider",
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Service-provider invitations — the list a property manager sees and the
3
+ * things they can do to a row.
4
+ *
5
+ * Separate from `useVerification` because these rows are not generic
6
+ * verifications any more: they have their own states (waiting for Seven365,
7
+ * waiting for the provider, not approved, declined) and their own actions,
8
+ * and the server scopes them to the caller's own organisation.
9
+ */
10
+ export type TServiceProviderInvitation = {
11
+ _id: string;
12
+ email: string;
13
+ type: string;
14
+ status: string;
15
+ statusLabel: string;
16
+ rejectionReason?: string | null;
17
+ createdAt?: string;
18
+ expireAt?: string;
19
+ deletedAt?: string | null;
20
+ metadata?: Record<string, any>;
21
+ history?: Array<{
22
+ action: string;
23
+ at: string;
24
+ byName?: string;
25
+ reason?: string;
26
+ }>;
27
+ };
28
+
29
+ /** The words on screen. Kept beside the server's own list on purpose — a
30
+ * status the server invents must never render as a raw stored value. */
31
+ export const SERVICE_PROVIDER_INVITE_LABELS: Record<string, string> = {
32
+ "awaiting-approval": "Waiting for Seven365",
33
+ pending: "Waiting for the provider",
34
+ rejected: "Not approved",
35
+ complete: "Accepted",
36
+ declined: "Declined",
37
+ cancelled: "Cancelled",
38
+ expired: "Expired",
39
+ };
40
+
41
+ export function serviceProviderInviteLabel(status?: string) {
42
+ return SERVICE_PROVIDER_INVITE_LABELS[String(status ?? "")] ?? "Unknown";
43
+ }
44
+
45
+ export default function useServiceProviderInvitation() {
46
+ const base = "/api/service-provider-invitations";
47
+
48
+ function getInvitations({
49
+ org = "",
50
+ status = "",
51
+ page = 1,
52
+ limit = 20,
53
+ } = {}): Promise<{
54
+ items: TServiceProviderInvitation[];
55
+ pages: number;
56
+ pageRange: string;
57
+ }> {
58
+ return useNuxtApp().$api(base, {
59
+ method: "GET",
60
+ query: { org, status, page, limit },
61
+ });
62
+ }
63
+
64
+ function cancelInvitation(id: string) {
65
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/cancel`, {
66
+ method: "PUT",
67
+ });
68
+ }
69
+
70
+ /** Sends the same invitation again. Never creates a second row. */
71
+ function resendInvitation(id: string) {
72
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/resend`, {
73
+ method: "PUT",
74
+ });
75
+ }
76
+
77
+ /** Soft delete — the row leaves this list, Seven365 keeps the record. */
78
+ function deleteInvitation(id: string) {
79
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}`, {
80
+ method: "DELETE",
81
+ });
82
+ }
83
+
84
+ // ---- Seven365 super admin ------------------------------------------------
85
+
86
+ function getApprovals({
87
+ status = "awaiting-approval",
88
+ page = 1,
89
+ limit = 20,
90
+ includeDeleted = false,
91
+ } = {}): Promise<{
92
+ items: TServiceProviderInvitation[];
93
+ pages: number;
94
+ pageRange: string;
95
+ awaitingApprovalCount: number;
96
+ }> {
97
+ return useNuxtApp().$api(`${base}/approvals`, {
98
+ method: "GET",
99
+ query: { status, page, limit, includeDeleted },
100
+ });
101
+ }
102
+
103
+ function getApprovalCount(): Promise<{ count: number }> {
104
+ return useNuxtApp().$api(`${base}/approvals/count`, { method: "GET" });
105
+ }
106
+
107
+ function approveInvitation(id: string) {
108
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/approve`, {
109
+ method: "PUT",
110
+ });
111
+ }
112
+
113
+ /** The reason is required — it is what the property manager is shown. */
114
+ function rejectInvitation(id: string, reason: string) {
115
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/reject`, {
116
+ method: "PUT",
117
+ body: { reason },
118
+ });
119
+ }
120
+
121
+ // ---- the invited service provider ----------------------------------------
122
+
123
+ function getInvitation(id: string): Promise<TServiceProviderInvitation> {
124
+ return useNuxtApp().$api(`${base}/${id}`, { method: "GET" });
125
+ }
126
+
127
+ function declineInvitation(id: string) {
128
+ return useNuxtApp().$api<{ message: string }>(`${base}/${id}/decline`, {
129
+ method: "PUT",
130
+ });
131
+ }
132
+
133
+ return {
134
+ getInvitations,
135
+ cancelInvitation,
136
+ resendInvitation,
137
+ deleteInvitation,
138
+ getApprovals,
139
+ getApprovalCount,
140
+ approveInvitation,
141
+ rejectInvitation,
142
+ getInvitation,
143
+ declineInvitation,
144
+ };
145
+ }
@@ -0,0 +1,336 @@
1
+ import { computed, onScopeDispose, readonly, ref, shallowRef } from "vue";
2
+ import type { SimpleUser, SimpleUserDelegate, SimpleUserOptions } from "sip.js/lib/platform/web";
3
+ import type { SessionDescriptionHandlerOptions } from "sip.js/lib/platform/web/session-description-handler/session-description-handler-options";
4
+
5
+ export type SipWebPhoneConfig = {
6
+ webSocketServer: string;
7
+ aor: string;
8
+ authorizationUsername: string;
9
+ authorizationPassword: string;
10
+ displayName?: string;
11
+ video?: boolean;
12
+ };
13
+
14
+ export type SipWebPhoneMedia = {
15
+ remoteAudio?: HTMLAudioElement | null;
16
+ remoteVideo?: HTMLVideoElement | null;
17
+ };
18
+
19
+ type ConnectionState = "disconnected" | "connecting" | "registered" | "error";
20
+ type CallState = "idle" | "calling" | "incoming" | "active" | "ending";
21
+
22
+ export default function useSipWebPhone() {
23
+ const simpleUser = shallowRef<SimpleUser>();
24
+ const connectionState = ref<ConnectionState>("disconnected");
25
+ const callState = ref<CallState>("idle");
26
+ const errorMessage = ref("");
27
+ const muted = ref(false);
28
+ const remoteVideoAvailable = ref(false);
29
+ const currentTarget = ref("");
30
+ const activeConfig = shallowRef<SipWebPhoneConfig>();
31
+
32
+ const isRegistered = computed(() => connectionState.value === "registered");
33
+ const hasCall = computed(() => callState.value !== "idle");
34
+ const audioConstraints: MediaTrackConstraints = {
35
+ echoCancellation: true,
36
+ noiseSuppression: true,
37
+ autoGainControl: true,
38
+ channelCount: 1,
39
+ };
40
+
41
+ async function connect(config: SipWebPhoneConfig, media: SipWebPhoneMedia) {
42
+ validateConfig(config);
43
+ await disconnect();
44
+
45
+ connectionState.value = "connecting";
46
+ errorMessage.value = "";
47
+ activeConfig.value = { ...config };
48
+ let clearRegistrationTimer = () => {};
49
+
50
+ try {
51
+ const { SimpleUser: SimpleUserConstructor } = await import("sip.js/lib/platform/web");
52
+ let registrationSettled = false;
53
+ let resolveRegistration: (() => void) | undefined;
54
+ let rejectRegistration: ((error: Error) => void) | undefined;
55
+ const registrationPromise = new Promise<void>((resolve, reject) => {
56
+ resolveRegistration = resolve;
57
+ rejectRegistration = reject;
58
+ });
59
+ const registrationTimeout = globalThis.setTimeout(() => {
60
+ if (registrationSettled) return;
61
+ registrationSettled = true;
62
+ rejectRegistration?.(new Error("SIP registration timed out. Check the WebSocket server and account credentials."));
63
+ }, 15000);
64
+ clearRegistrationTimer = () => globalThis.clearTimeout(registrationTimeout);
65
+ const settleRegistration = (error?: Error) => {
66
+ if (registrationSettled) return;
67
+ registrationSettled = true;
68
+ clearRegistrationTimer();
69
+ if (error) rejectRegistration?.(error);
70
+ else resolveRegistration?.();
71
+ };
72
+ const delegate: SimpleUserDelegate = {
73
+ onCallCreated: () => {
74
+ if (callState.value !== "incoming") callState.value = "calling";
75
+ },
76
+ onCallReceived: () => {
77
+ callState.value = "incoming";
78
+ currentTarget.value = "Incoming call";
79
+ },
80
+ onCallAnswered: () => {
81
+ callState.value = "active";
82
+ ensureMicrophoneSending();
83
+ syncVideoState();
84
+ },
85
+ onCallHangup: () => {
86
+ const previousState = callState.value;
87
+ resetCall();
88
+ if (previousState === "calling") {
89
+ errorMessage.value = "The call was not answered or was rejected by the destination.";
90
+ }
91
+ },
92
+ onRegistered: () => {
93
+ connectionState.value = "registered";
94
+ settleRegistration();
95
+ },
96
+ onUnregistered: () => {
97
+ if (connectionState.value === "connecting") {
98
+ settleRegistration(new Error("SIP registration was rejected. Check the username and password."));
99
+ }
100
+ connectionState.value = "disconnected";
101
+ },
102
+ onServerDisconnect: () => {
103
+ const error = new Error("The SIP WebSocket server disconnected during registration.");
104
+ connectionState.value = "error";
105
+ errorMessage.value = error.message;
106
+ settleRegistration(error);
107
+ resetCall();
108
+ },
109
+ };
110
+ const options: SimpleUserOptions = {
111
+ aor: normalizeSipUri(config.aor),
112
+ delegate,
113
+ media: {
114
+ // The browser sends microphone audio and receives HID audio/video.
115
+ // Do not request a local camera merely because the HID video leg is enabled.
116
+ constraints: { audio: audioConstraints, video: false },
117
+ remote: {
118
+ audio: media.remoteAudio || undefined,
119
+ video: media.remoteVideo || undefined,
120
+ },
121
+ },
122
+ userAgentOptions: {
123
+ authorizationUsername: config.authorizationUsername,
124
+ authorizationPassword: config.authorizationPassword,
125
+ displayName: config.displayName || config.authorizationUsername,
126
+ },
127
+ };
128
+
129
+ const user = new SimpleUserConstructor(config.webSocketServer.trim(), options);
130
+ simpleUser.value = user;
131
+ await user.connect();
132
+ await user.register();
133
+ await registrationPromise;
134
+ } catch (error) {
135
+ clearRegistrationTimer();
136
+ await disconnect(false);
137
+ connectionState.value = "error";
138
+ errorMessage.value = getErrorMessage(error);
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ async function disconnect(clearError = true) {
144
+ const user = simpleUser.value;
145
+ simpleUser.value = undefined;
146
+
147
+ if (user) {
148
+ try {
149
+ if (hasCall.value) await user.hangup();
150
+ } catch {
151
+ // The session may already have ended remotely.
152
+ }
153
+ try {
154
+ await user.unregister();
155
+ } catch {
156
+ // Registration may not have completed.
157
+ }
158
+ try {
159
+ await user.disconnect();
160
+ } catch {
161
+ // The WebSocket may already be closed.
162
+ }
163
+ }
164
+
165
+ connectionState.value = "disconnected";
166
+ activeConfig.value = undefined;
167
+ resetCall();
168
+ if (clearError) errorMessage.value = "";
169
+ }
170
+
171
+ async function call(target: string) {
172
+ const user = requireRegisteredUser();
173
+ const destination = normalizeDestination(target, activeConfig.value?.aor || "");
174
+ errorMessage.value = "";
175
+ currentTarget.value = destination;
176
+ callState.value = "calling";
177
+ try {
178
+ const sessionDescriptionHandlerOptions: SessionDescriptionHandlerOptions = {
179
+ constraints: { audio: audioConstraints, video: false },
180
+ offerOptions: {
181
+ offerToReceiveAudio: true,
182
+ offerToReceiveVideo: Boolean(activeConfig.value?.video),
183
+ },
184
+ };
185
+ await user.call(destination, { sessionDescriptionHandlerOptions });
186
+ } catch (error) {
187
+ resetCall();
188
+ errorMessage.value = getErrorMessage(error);
189
+ throw error;
190
+ }
191
+ }
192
+
193
+ async function answer() {
194
+ const user = requireRegisteredUser();
195
+ const sessionDescriptionHandlerOptions: SessionDescriptionHandlerOptions = {
196
+ constraints: { audio: audioConstraints, video: false },
197
+ offerOptions: {
198
+ offerToReceiveAudio: true,
199
+ offerToReceiveVideo: Boolean(activeConfig.value?.video),
200
+ },
201
+ };
202
+ await user.answer({ sessionDescriptionHandlerOptions });
203
+ }
204
+
205
+ async function decline() {
206
+ const user = requireUser();
207
+ callState.value = "ending";
208
+ await user.decline();
209
+ resetCall();
210
+ }
211
+
212
+ async function hangup() {
213
+ const user = requireUser();
214
+ callState.value = "ending";
215
+ await user.hangup();
216
+ resetCall();
217
+ }
218
+
219
+ function toggleMute() {
220
+ const user = requireUser();
221
+ const tracks = user.localMediaStream?.getAudioTracks() || [];
222
+ if (!tracks.length) throw new Error("No microphone track is available for this call.");
223
+ const enable = muted.value;
224
+ tracks.forEach((track) => {
225
+ track.enabled = enable;
226
+ });
227
+ muted.value = !enable;
228
+ }
229
+
230
+ function ensureMicrophoneSending() {
231
+ const tracks = simpleUser.value?.localMediaStream?.getAudioTracks() || [];
232
+ if (!tracks.length) {
233
+ errorMessage.value = "The call connected without a microphone track. Reconnect and allow microphone access.";
234
+ return;
235
+ }
236
+ tracks.forEach((track) => {
237
+ track.enabled = true;
238
+ });
239
+ muted.value = false;
240
+ }
241
+
242
+ function syncVideoState() {
243
+ const user = simpleUser.value;
244
+ const remoteStream = user?.remoteMediaStream;
245
+ const remoteTrack = remoteStream?.getVideoTracks()[0];
246
+
247
+ const updateState = () => {
248
+ const currentRemoteTrack = remoteStream?.getVideoTracks()[0];
249
+ remoteVideoAvailable.value = Boolean(currentRemoteTrack && currentRemoteTrack.readyState === "live");
250
+ };
251
+
252
+ updateState();
253
+ if (remoteStream) remoteStream.onaddtrack = updateState;
254
+
255
+ if (remoteTrack) {
256
+ remoteTrack.onended = () => {
257
+ remoteVideoAvailable.value = false;
258
+ };
259
+ }
260
+ }
261
+
262
+ function resetCall() {
263
+ callState.value = "idle";
264
+ currentTarget.value = "";
265
+ muted.value = false;
266
+ remoteVideoAvailable.value = false;
267
+ }
268
+
269
+ function requireRegisteredUser() {
270
+ const user = requireUser();
271
+ if (!isRegistered.value) throw new Error("Connect the web phone before making a call.");
272
+ return user;
273
+ }
274
+
275
+ function requireUser() {
276
+ if (!simpleUser.value) throw new Error("Web phone is not connected.");
277
+ return simpleUser.value;
278
+ }
279
+
280
+ onScopeDispose(() => {
281
+ void disconnect();
282
+ });
283
+
284
+ return {
285
+ connectionState: readonly(connectionState),
286
+ callState: readonly(callState),
287
+ errorMessage: readonly(errorMessage),
288
+ muted: readonly(muted),
289
+ remoteVideoAvailable: readonly(remoteVideoAvailable),
290
+ currentTarget: readonly(currentTarget),
291
+ isRegistered,
292
+ hasCall,
293
+ connect,
294
+ disconnect,
295
+ call,
296
+ answer,
297
+ decline,
298
+ hangup,
299
+ toggleMute,
300
+ };
301
+ }
302
+
303
+ function validateConfig(config: SipWebPhoneConfig) {
304
+ if (!/^wss:\/\//i.test(config.webSocketServer.trim())) {
305
+ throw new Error("SIP WebSocket URL must use WSS.");
306
+ }
307
+ if (!config.aor.trim() || !config.authorizationUsername.trim() || !config.authorizationPassword) {
308
+ throw new Error("SIP address, username, and password are required.");
309
+ }
310
+ }
311
+
312
+ function normalizeSipUri(value: string) {
313
+ const trimmed = value.trim();
314
+ return trimmed.toLowerCase().startsWith("sip:") ? trimmed : `sip:${trimmed}`;
315
+ }
316
+
317
+ function normalizeDestination(target: string, aor: string) {
318
+ const trimmed = target.trim();
319
+ if (!trimmed) throw new Error("Enter an extension or SIP address to call.");
320
+ if (trimmed.toLowerCase().startsWith("sip:")) return trimmed;
321
+ if (trimmed.includes("@")) return `sip:${trimmed}`;
322
+
323
+ const domain = normalizeSipUri(aor).split("@")[1];
324
+ if (!domain) throw new Error("Enter a full SIP address for the destination.");
325
+ return `sip:${trimmed}@${domain}`;
326
+ }
327
+
328
+ function getErrorMessage(error: unknown) {
329
+ if (error instanceof DOMException && error.name === "NotAllowedError") {
330
+ return "Microphone permission was denied. Allow microphone access in the browser and reconnect the web phone.";
331
+ }
332
+ if (error instanceof DOMException && error.name === "NotFoundError") {
333
+ return "No microphone was found for this call.";
334
+ }
335
+ return error instanceof Error ? error.message : "SIP connection failed.";
336
+ }
@@ -0,0 +1,62 @@
1
+ export default function useSubscriptionPlan() {
2
+ function getAll({
3
+ page = 1,
4
+ search = "",
5
+ limit = 10,
6
+ status = "active",
7
+ planType = "",
8
+ billingCycle = "",
9
+ } = {}) {
10
+ return useNuxtApp().$api<Record<string, any>>(`/api/subscription-plans`, {
11
+ method: "GET",
12
+ query: {
13
+ page,
14
+ search,
15
+ limit,
16
+ status,
17
+ planType,
18
+ billingCycle,
19
+ },
20
+ });
21
+ }
22
+ function add(value: any) {
23
+ return useNuxtApp().$api<Record<string, any>>("/api/subscription-plans", {
24
+ method: "POST",
25
+ body: value,
26
+ });
27
+ }
28
+ function getById(id: string) {
29
+ return useNuxtApp().$api<Record<string, any>>(
30
+ `/api/subscription-plans/id/${id}`,
31
+ {
32
+ method: "GET",
33
+ }
34
+ );
35
+ }
36
+ function updateById(id: string, value: any) {
37
+ return useNuxtApp().$api<Record<string, any>>(
38
+ `/api/subscription-plans/${id}`,
39
+ {
40
+ method: "PUT",
41
+ body: value,
42
+ }
43
+ );
44
+ }
45
+ function updateStatusById(id: string, status: "active" | "deactive") {
46
+ return useNuxtApp().$api<Record<string, any>>(
47
+ `/api/subscription-plans/${id}/status`,
48
+ {
49
+ method: "PATCH",
50
+ body: { status },
51
+ }
52
+ );
53
+ }
54
+
55
+ return {
56
+ getAll,
57
+ add,
58
+ getById,
59
+ updateById,
60
+ updateStatusById,
61
+ };
62
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * THE USER'S LIGHT/DARK CHOICE, REMEMBERED.
3
+ *
4
+ * Until now `Layout/Header.vue` flipped `theme.global.name` and wrote nothing,
5
+ * so the choice was gone on the next reload.
6
+ *
7
+ * A COOKIE, NOT localStorage. The handoff says "persists in localStorage", and
8
+ * for a single-page prototype that is right - but this product is ELEVEN
9
+ * applications on eleven hostnames. localStorage is per-origin, so a user who
10
+ * picked dark mode in Security would land on a white screen in Property
11
+ * Management, and again in Account, and would have to set it eleven times.
12
+ *
13
+ * A cookie written with the layer's own `cookieConfig` carries the parent
14
+ * domain, which is exactly how `sid`, `user` and `landing-page` already travel
15
+ * between these applications. So this is the estate's existing preference
16
+ * mechanism rather than a new one, and it is also the only one that makes the
17
+ * setting mean what a user expects it to mean.
18
+ *
19
+ * NO FLASH OF THE WRONG THEME. Nothing here corrects the theme after mount -
20
+ * `plugins/vuetify.ts` reads the same cookie BEFORE `createVuetify` and boots
21
+ * on it, so the first paint is already right. The apps are `ssr: false`, so
22
+ * there is no server render to disagree with.
23
+ */
24
+ import { useTheme } from "vuetify";
25
+ import type { CookieOptions } from "#app";
26
+
27
+ export type TThemeName = "light" | "dark";
28
+
29
+ /** The one place the cookie is named. `plugins/vuetify.ts` reads the same one. */
30
+ export const THEME_COOKIE = "theme";
31
+
32
+ export default function useThemePreference() {
33
+ // `runtimeConfig.public.cookieConfig` is typed `unknown`, so it is narrowed
34
+ // here rather than at every call site. Same object `sid`, `user` and
35
+ // `landing-page` are written with - it carries the shared parent domain.
36
+ const cookieConfig = useRuntimeConfig().public.cookieConfig as CookieOptions<
37
+ TThemeName | null
38
+ > & { readonly?: false };
39
+
40
+ const vuetifyTheme = useTheme();
41
+
42
+ const stored = useCookie<TThemeName | null>(THEME_COOKIE, cookieConfig);
43
+
44
+ const current = computed<TThemeName>(() =>
45
+ vuetifyTheme.global.current.value.dark ? "dark" : "light"
46
+ );
47
+
48
+ /**
49
+ * Sets the theme AND remembers it. The write is the whole point - a caller
50
+ * that only wants a temporary theme for one subtree should use
51
+ * `<v-theme-provider>` instead, which is what `plain-dark` does.
52
+ */
53
+ function setTheme(name: TThemeName) {
54
+ vuetifyTheme.global.name.value = name;
55
+ stored.value = name;
56
+ }
57
+
58
+ function toggleTheme() {
59
+ setTheme(current.value === "dark" ? "light" : "dark");
60
+ }
61
+
62
+ return { current, setTheme, toggleTheme, stored };
63
+ }
@@ -92,6 +92,7 @@ export default function () {
92
92
  checkedOut?: boolean;
93
93
  plateNumber?: string;
94
94
  tab?: string;
95
+ passOrKey?: string;
95
96
  };
96
97
 
97
98
  type SearchUncheckedOutUnregisteredVisitorParams = {
@@ -130,6 +131,7 @@ export default function () {
130
131
  plateNumber = "",
131
132
  checkedOut,
132
133
  tab,
134
+ passOrKey,
133
135
  }: GetVisitorsParams = {}) {
134
136
  const query = Object.fromEntries(
135
137
  Object.entries({
@@ -146,7 +148,10 @@ export default function () {
146
148
  plateNumber,
147
149
  order,
148
150
  tab,
149
- }).filter(([, value]) => value !== undefined && value !== null && value !== "")
151
+ passOrKey,
152
+ }).filter(
153
+ ([, value]) => value !== undefined && value !== null && value !== ""
154
+ )
150
155
  );
151
156
 
152
157
  return await useNuxtApp().$api<Record<string, any>>(