@omersx/bemms 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +203 -2
  3. package/drizzle.config.ts +11 -0
  4. package/next.config.ts +51 -0
  5. package/package.json +99 -3
  6. package/postcss.config.mjs +7 -0
  7. package/public/hospital_demo_devices.csv +32 -0
  8. package/public/icons/icon-192.png +0 -0
  9. package/public/icons/icon-512.png +0 -0
  10. package/public/icons/icon-maskable-512.png +0 -0
  11. package/public/manifest.webmanifest +31 -0
  12. package/public/sw.js +177 -0
  13. package/src/app/(auth)/layout.tsx +20 -0
  14. package/src/app/(auth)/login/page.tsx +135 -0
  15. package/src/app/(dashboard)/admin/audit-logs/page.tsx +178 -0
  16. package/src/app/(dashboard)/admin/data/page.tsx +37 -0
  17. package/src/app/(dashboard)/admin/departments/[deptId]/edit/page.tsx +206 -0
  18. package/src/app/(dashboard)/admin/departments/[deptId]/page.tsx +71 -0
  19. package/src/app/(dashboard)/admin/departments/new/page.tsx +6 -0
  20. package/src/app/(dashboard)/admin/departments/page.tsx +6 -0
  21. package/src/app/(dashboard)/admin/general/page.tsx +374 -0
  22. package/src/app/(dashboard)/admin/hospitals/[hospitalId]/edit/page.tsx +304 -0
  23. package/src/app/(dashboard)/admin/hospitals/[hospitalId]/page.tsx +94 -0
  24. package/src/app/(dashboard)/admin/hospitals/new/page.tsx +302 -0
  25. package/src/app/(dashboard)/admin/hospitals/page.tsx +6 -0
  26. package/src/app/(dashboard)/admin/layout.tsx +49 -0
  27. package/src/app/(dashboard)/admin/organizations/[orgId]/edit/page.tsx +172 -0
  28. package/src/app/(dashboard)/admin/organizations/[orgId]/page.tsx +82 -0
  29. package/src/app/(dashboard)/admin/organizations/new/page.tsx +150 -0
  30. package/src/app/(dashboard)/admin/organizations/page.tsx +6 -0
  31. package/src/app/(dashboard)/admin/page.tsx +258 -0
  32. package/src/app/(dashboard)/admin/roles/page.tsx +153 -0
  33. package/src/app/(dashboard)/admin/settings/page.tsx +69 -0
  34. package/src/app/(dashboard)/admin/users/[userId]/edit/page.tsx +153 -0
  35. package/src/app/(dashboard)/admin/users/[userId]/page.tsx +317 -0
  36. package/src/app/(dashboard)/admin/users/invite/page.tsx +3 -0
  37. package/src/app/(dashboard)/admin/users/new/page.tsx +5 -0
  38. package/src/app/(dashboard)/admin/users/page.tsx +23 -0
  39. package/src/app/(dashboard)/dashboard/page.tsx +462 -0
  40. package/src/app/(dashboard)/departments/[deptId]/page.tsx +39 -0
  41. package/src/app/(dashboard)/departments/new/page.tsx +330 -0
  42. package/src/app/(dashboard)/departments/page.tsx +52 -0
  43. package/src/app/(dashboard)/devices/[id]/edit/page.tsx +131 -0
  44. package/src/app/(dashboard)/devices/[id]/page.tsx +408 -0
  45. package/src/app/(dashboard)/devices/[id]/qr/page.tsx +54 -0
  46. package/src/app/(dashboard)/devices/new/page.tsx +382 -0
  47. package/src/app/(dashboard)/devices/page.tsx +80 -0
  48. package/src/app/(dashboard)/engineering-team/page.tsx +41 -0
  49. package/src/app/(dashboard)/layout.tsx +43 -0
  50. package/src/app/(dashboard)/maintenance/checklists/page.tsx +57 -0
  51. package/src/app/(dashboard)/maintenance/page.tsx +231 -0
  52. package/src/app/(dashboard)/maintenance/plans/page.tsx +142 -0
  53. package/src/app/(dashboard)/maintenance/tasks/[id]/page.tsx +997 -0
  54. package/src/app/(dashboard)/maintenance/tasks/page.tsx +119 -0
  55. package/src/app/(dashboard)/more/page.tsx +310 -0
  56. package/src/app/(dashboard)/my-department/page.tsx +151 -0
  57. package/src/app/(dashboard)/notifications/notification-list.tsx +103 -0
  58. package/src/app/(dashboard)/notifications/page.tsx +59 -0
  59. package/src/app/(dashboard)/profile/page.tsx +27 -0
  60. package/src/app/(dashboard)/reports/costs/page.tsx +127 -0
  61. package/src/app/(dashboard)/reports/history/page.tsx +105 -0
  62. package/src/app/(dashboard)/reports/inventory/page.tsx +116 -0
  63. package/src/app/(dashboard)/reports/maintenance/page.tsx +136 -0
  64. package/src/app/(dashboard)/reports/page.tsx +120 -0
  65. package/src/app/(dashboard)/reports/tickets/page.tsx +139 -0
  66. package/src/app/(dashboard)/reports/workload/page.tsx +135 -0
  67. package/src/app/(dashboard)/rounds/[id]/page.tsx +197 -0
  68. package/src/app/(dashboard)/rounds/history/page.tsx +82 -0
  69. package/src/app/(dashboard)/rounds/page.tsx +107 -0
  70. package/src/app/(dashboard)/scan/[code]/page.tsx +154 -0
  71. package/src/app/(dashboard)/scan/page.tsx +73 -0
  72. package/src/app/(dashboard)/team/page.tsx +42 -0
  73. package/src/app/(dashboard)/tickets/[id]/page.tsx +465 -0
  74. package/src/app/(dashboard)/tickets/create/page.tsx +722 -0
  75. package/src/app/(dashboard)/tickets/page.tsx +55 -0
  76. package/src/app/(dashboard)/tickets/triage/page.tsx +161 -0
  77. package/src/app/api/admin/data/export/route.ts +316 -0
  78. package/src/app/api/auth/[...nextauth]/route.ts +3 -0
  79. package/src/app/api/health/route.ts +46 -0
  80. package/src/app/api/notifications/sse/route.ts +91 -0
  81. package/src/app/api/notifications/subscribe/route.ts +54 -0
  82. package/src/app/api/notifications/vapid-key/route.ts +7 -0
  83. package/src/app/api/reports/export/route.ts +102 -0
  84. package/src/app/api/tickets/[id]/attachments/route.ts +129 -0
  85. package/src/app/api/tickets/[id]/live-sync/route.ts +52 -0
  86. package/src/app/error.tsx +54 -0
  87. package/src/app/globals.css +89 -0
  88. package/src/app/layout.tsx +59 -0
  89. package/src/app/not-found.tsx +32 -0
  90. package/src/app/page.tsx +5 -0
  91. package/src/components/admin/add-department-user-dialog.tsx +759 -0
  92. package/src/components/admin/add-engineer-dialog.tsx +773 -0
  93. package/src/components/admin/add-user-type-dialog.tsx +89 -0
  94. package/src/components/admin/audit-log-export.tsx +113 -0
  95. package/src/components/admin/audit-log-filters.tsx +255 -0
  96. package/src/components/admin/audit-log-table.tsx +383 -0
  97. package/src/components/admin/data-management/danger-zone.tsx +301 -0
  98. package/src/components/admin/data-management/data-management-tabs.tsx +61 -0
  99. package/src/components/admin/data-management/export-center.tsx +301 -0
  100. package/src/components/admin/data-management/import-wizard.tsx +558 -0
  101. package/src/components/admin/settings-forms/features-form.tsx +115 -0
  102. package/src/components/admin/settings-forms/health-panel.tsx +182 -0
  103. package/src/components/admin/settings-forms/localization-form.tsx +203 -0
  104. package/src/components/admin/settings-forms/retention-form.tsx +164 -0
  105. package/src/components/admin/settings-forms/security-form.tsx +186 -0
  106. package/src/components/admin/system-settings-tabs.tsx +88 -0
  107. package/src/components/admin/user-role-dialog.tsx +293 -0
  108. package/src/components/admin/user-status-dialog.tsx +166 -0
  109. package/src/components/admin/users-page-client.tsx +514 -0
  110. package/src/components/dashboard/action-buttons.tsx +93 -0
  111. package/src/components/dashboard/admin-alerts.tsx +86 -0
  112. package/src/components/dashboard/availability-alerts.tsx +65 -0
  113. package/src/components/dashboard/maintenance-due-list.tsx +87 -0
  114. package/src/components/dashboard/round-widget.tsx +110 -0
  115. package/src/components/dashboard/stat-card.tsx +38 -0
  116. package/src/components/dashboard/ticket-list-compact.tsx +86 -0
  117. package/src/components/departments/department-cards-view.tsx +244 -0
  118. package/src/components/departments/department-detail-view.tsx +467 -0
  119. package/src/components/departments/department-visuals.tsx +190 -0
  120. package/src/components/departments/my-department-view.tsx +333 -0
  121. package/src/components/devices/change-status-dialog.tsx +146 -0
  122. package/src/components/devices/device-action-buttons.tsx +125 -0
  123. package/src/components/devices/device-availability-banner.tsx +59 -0
  124. package/src/components/devices/device-badges.tsx +55 -0
  125. package/src/components/devices/device-documents-tab.tsx +93 -0
  126. package/src/components/devices/device-maintenance-tab.tsx +113 -0
  127. package/src/components/devices/device-mini-qr.tsx +109 -0
  128. package/src/components/devices/device-qr-card.tsx +187 -0
  129. package/src/components/devices/device-signatures-tab.tsx +607 -0
  130. package/src/components/devices/device-tickets-tab.tsx +139 -0
  131. package/src/components/devices/device-timelines.tsx +124 -0
  132. package/src/components/devices/qr-camera-scanner.tsx +167 -0
  133. package/src/components/devices/qr-code-display.tsx +139 -0
  134. package/src/components/devices/quick-add-category-dialog.tsx +135 -0
  135. package/src/components/devices/quick-add-manufacturer-dialog.tsx +212 -0
  136. package/src/components/devices/transfer-device-dialog.tsx +190 -0
  137. package/src/components/layout/header.tsx +80 -0
  138. package/src/components/layout/mobile-nav.tsx +91 -0
  139. package/src/components/layout/more-item-badge.tsx +15 -0
  140. package/src/components/layout/nav-badge.tsx +72 -0
  141. package/src/components/layout/sidebar.tsx +297 -0
  142. package/src/components/maintenance/maintenance-badges.tsx +579 -0
  143. package/src/components/maintenance/maintenance-nav.tsx +107 -0
  144. package/src/components/maintenance/parts-costs-manager.tsx +909 -0
  145. package/src/components/maintenance/signature-card.tsx +413 -0
  146. package/src/components/maintenance/task-action-bar.tsx +955 -0
  147. package/src/components/notifications/notification-bell.tsx +367 -0
  148. package/src/components/notifications/notification-item.tsx +93 -0
  149. package/src/components/notifications/notification-preferences.tsx +182 -0
  150. package/src/components/profile/user-profile-view.tsx +626 -0
  151. package/src/components/providers/nav-counts-provider.tsx +235 -0
  152. package/src/components/providers/session-provider.tsx +7 -0
  153. package/src/components/providers/theme-provider.tsx +124 -0
  154. package/src/components/pwa/install-prompt.tsx +82 -0
  155. package/src/components/pwa/sw-register.tsx +43 -0
  156. package/src/components/reports/export-button.tsx +62 -0
  157. package/src/components/reports/filter-chips.tsx +47 -0
  158. package/src/components/reports/summary-cards.tsx +40 -0
  159. package/src/components/reports/transparency-header.tsx +54 -0
  160. package/src/components/rounds/department-visit-sheet.tsx +199 -0
  161. package/src/components/rounds/mention-textarea.tsx +187 -0
  162. package/src/components/rounds/round-calendar.tsx +233 -0
  163. package/src/components/rounds/round-department-list.tsx +281 -0
  164. package/src/components/rounds/round-history-client.tsx +218 -0
  165. package/src/components/rounds/start-round-card.tsx +184 -0
  166. package/src/components/shared/confirm-dialog.tsx +67 -0
  167. package/src/components/shared/data-table.tsx +104 -0
  168. package/src/components/shared/page-header.tsx +20 -0
  169. package/src/components/shared/sign-out-button.tsx +52 -0
  170. package/src/components/shared/status-badge.tsx +42 -0
  171. package/src/components/shared/table-search.tsx +55 -0
  172. package/src/components/signatures/sign-action-dialog.tsx +253 -0
  173. package/src/components/signatures/signature-history.tsx +121 -0
  174. package/src/components/signatures/signature-status-panel.tsx +164 -0
  175. package/src/components/team/department-coverage.tsx +141 -0
  176. package/src/components/team/edit-engineer-dialog.tsx +242 -0
  177. package/src/components/team/engineer-card.tsx +180 -0
  178. package/src/components/team/engineer-details-dialog.tsx +432 -0
  179. package/src/components/team/schedule-calendar.tsx +204 -0
  180. package/src/components/team/staff-engineer-team-view.tsx +368 -0
  181. package/src/components/team/team-header-stats.tsx +88 -0
  182. package/src/components/team/team-page-client.tsx +216 -0
  183. package/src/components/theme/theme-selector-card.tsx +63 -0
  184. package/src/components/theme/theme-toggle.tsx +65 -0
  185. package/src/components/tickets/staff-status-label.tsx +58 -0
  186. package/src/components/tickets/staff-ticket-card.tsx +109 -0
  187. package/src/components/tickets/staff-ticket-progress-tracker.tsx +405 -0
  188. package/src/components/tickets/staff-tickets-view.tsx +182 -0
  189. package/src/components/tickets/ticket-action-buttons.tsx +873 -0
  190. package/src/components/tickets/ticket-auto-refresh.tsx +197 -0
  191. package/src/components/tickets/ticket-badges.tsx +43 -0
  192. package/src/components/tickets/ticket-card.tsx +197 -0
  193. package/src/components/tickets/ticket-cards-view.tsx +395 -0
  194. package/src/components/tickets/ticket-comment-section.tsx +684 -0
  195. package/src/components/tickets/ticket-dispatch-status.tsx +184 -0
  196. package/src/components/tickets/ticket-list-auto-refresh.tsx +55 -0
  197. package/src/components/tickets/ticket-reporter-profile.tsx +235 -0
  198. package/src/components/tickets/ticket-signatures-panel.tsx +32 -0
  199. package/src/components/tickets/ticket-timeline.tsx +131 -0
  200. package/src/components/tickets/ticket-triage-panel.tsx +124 -0
  201. package/src/components/tickets/tickets-nav.tsx +107 -0
  202. package/src/components/ui/avatar.tsx +43 -0
  203. package/src/components/ui/badge.tsx +34 -0
  204. package/src/components/ui/button.tsx +58 -0
  205. package/src/components/ui/card.tsx +77 -0
  206. package/src/components/ui/checkbox.tsx +31 -0
  207. package/src/components/ui/dialog.tsx +123 -0
  208. package/src/components/ui/dropdown-menu.tsx +126 -0
  209. package/src/components/ui/form.tsx +179 -0
  210. package/src/components/ui/input.tsx +24 -0
  211. package/src/components/ui/label.tsx +27 -0
  212. package/src/components/ui/popover.tsx +34 -0
  213. package/src/components/ui/progress.tsx +36 -0
  214. package/src/components/ui/scroll-area.tsx +47 -0
  215. package/src/components/ui/select.tsx +160 -0
  216. package/src/components/ui/separator.tsx +25 -0
  217. package/src/components/ui/sheet.tsx +131 -0
  218. package/src/components/ui/switch.tsx +30 -0
  219. package/src/components/ui/table.tsx +121 -0
  220. package/src/components/ui/tabs.tsx +62 -0
  221. package/src/components/ui/textarea.tsx +23 -0
  222. package/src/components/ui/tooltip.tsx +27 -0
  223. package/src/lib/actions/__tests__/devices.test.ts +176 -0
  224. package/src/lib/actions/__tests__/signatures.test.ts +303 -0
  225. package/src/lib/actions/__tests__/tickets.test.ts +243 -0
  226. package/src/lib/actions/audit-logs.ts +298 -0
  227. package/src/lib/actions/checklists.ts +429 -0
  228. package/src/lib/actions/dashboard.ts +358 -0
  229. package/src/lib/actions/data-management.ts +723 -0
  230. package/src/lib/actions/departments.ts +369 -0
  231. package/src/lib/actions/device-categories.ts +117 -0
  232. package/src/lib/actions/devices.ts +748 -0
  233. package/src/lib/actions/engineer-rounds.ts +973 -0
  234. package/src/lib/actions/engineers.ts +285 -0
  235. package/src/lib/actions/hospitals.ts +148 -0
  236. package/src/lib/actions/locations.ts +115 -0
  237. package/src/lib/actions/maintenance-plans.ts +316 -0
  238. package/src/lib/actions/maintenance-tasks.ts +415 -0
  239. package/src/lib/actions/manufacturers.ts +132 -0
  240. package/src/lib/actions/nav-counts.ts +144 -0
  241. package/src/lib/actions/notifications.ts +136 -0
  242. package/src/lib/actions/organizations.ts +99 -0
  243. package/src/lib/actions/qr.ts +147 -0
  244. package/src/lib/actions/report-history.ts +77 -0
  245. package/src/lib/actions/reports.ts +192 -0
  246. package/src/lib/actions/roles.ts +32 -0
  247. package/src/lib/actions/signature-auth.ts +377 -0
  248. package/src/lib/actions/signatures.ts +532 -0
  249. package/src/lib/actions/system-settings.ts +274 -0
  250. package/src/lib/actions/tickets.ts +1445 -0
  251. package/src/lib/actions/users.ts +740 -0
  252. package/src/lib/audit/index.ts +103 -0
  253. package/src/lib/auth/__tests__/rbac.test.ts +33 -0
  254. package/src/lib/auth/index.ts +338 -0
  255. package/src/lib/auth/password.ts +10 -0
  256. package/src/lib/auth/rbac.ts +103 -0
  257. package/src/lib/auth/types.ts +57 -0
  258. package/src/lib/constants/departments.ts +16 -0
  259. package/src/lib/constants/manufacturers.ts +32 -0
  260. package/src/lib/db/index.ts +23 -0
  261. package/src/lib/db/schema/audit-logs.ts +42 -0
  262. package/src/lib/db/schema/departments.ts +39 -0
  263. package/src/lib/db/schema/device-categories.ts +38 -0
  264. package/src/lib/db/schema/devices.ts +195 -0
  265. package/src/lib/db/schema/engineer-profiles.ts +63 -0
  266. package/src/lib/db/schema/engineer-rounds.ts +115 -0
  267. package/src/lib/db/schema/enums.ts +148 -0
  268. package/src/lib/db/schema/hospitals.ts +34 -0
  269. package/src/lib/db/schema/index.ts +24 -0
  270. package/src/lib/db/schema/locations.ts +46 -0
  271. package/src/lib/db/schema/maintenance-plans.ts +99 -0
  272. package/src/lib/db/schema/maintenance-records.ts +161 -0
  273. package/src/lib/db/schema/maintenance-tasks.ts +196 -0
  274. package/src/lib/db/schema/manufacturers.ts +60 -0
  275. package/src/lib/db/schema/notifications.ts +48 -0
  276. package/src/lib/db/schema/organizations.ts +24 -0
  277. package/src/lib/db/schema/report-history.ts +28 -0
  278. package/src/lib/db/schema/roles.ts +59 -0
  279. package/src/lib/db/schema/sessions.ts +46 -0
  280. package/src/lib/db/schema/signature-events.ts +48 -0
  281. package/src/lib/db/schema/signature-policies.ts +60 -0
  282. package/src/lib/db/schema/signatures.ts +87 -0
  283. package/src/lib/db/schema/tickets.ts +169 -0
  284. package/src/lib/db/schema/user-assignments.ts +85 -0
  285. package/src/lib/db/schema/users.ts +39 -0
  286. package/src/lib/db/seed.ts +131 -0
  287. package/src/lib/logger/__tests__/logger.test.ts +154 -0
  288. package/src/lib/logger/formatters.ts +159 -0
  289. package/src/lib/logger/index.ts +184 -0
  290. package/src/lib/logger/types.ts +48 -0
  291. package/src/lib/notifications/create.ts +71 -0
  292. package/src/lib/notifications/dispatcher.ts +490 -0
  293. package/src/lib/notifications/index.ts +7 -0
  294. package/src/lib/notifications/push-sender.ts +128 -0
  295. package/src/lib/notifications/push.ts +64 -0
  296. package/src/lib/notifications/triggers.ts +166 -0
  297. package/src/lib/utils/__tests__/crypto.test.ts +30 -0
  298. package/src/lib/utils/__tests__/export.test.ts +55 -0
  299. package/src/lib/utils/__tests__/nav-badge.test.ts +147 -0
  300. package/src/lib/utils/__tests__/staff-status.test.ts +50 -0
  301. package/src/lib/utils/attestations.ts +19 -0
  302. package/src/lib/utils/crypto.ts +18 -0
  303. package/src/lib/utils/csv-parser.ts +181 -0
  304. package/src/lib/utils/export.ts +45 -0
  305. package/src/lib/utils/nav-badge.ts +225 -0
  306. package/src/lib/utils/sound.ts +152 -0
  307. package/src/lib/utils/staff-status.ts +115 -0
  308. package/src/lib/utils.ts +6 -0
  309. package/src/lib/validators/__tests__/auth.test.ts +127 -0
  310. package/src/lib/validators/__tests__/data-management.test.ts +182 -0
  311. package/src/lib/validators/__tests__/devices-profile.test.ts +129 -0
  312. package/src/lib/validators/__tests__/devices.test.ts +98 -0
  313. package/src/lib/validators/__tests__/engineer-rounds.test.ts +170 -0
  314. package/src/lib/validators/__tests__/maintenance.test.ts +178 -0
  315. package/src/lib/validators/__tests__/signatures.test.ts +154 -0
  316. package/src/lib/validators/__tests__/system-settings.test.ts +119 -0
  317. package/src/lib/validators/__tests__/tickets.test.ts +176 -0
  318. package/src/lib/validators/__tests__/users.test.ts +92 -0
  319. package/src/lib/validators/auth.ts +32 -0
  320. package/src/lib/validators/data-management.ts +67 -0
  321. package/src/lib/validators/departments.ts +14 -0
  322. package/src/lib/validators/devices.ts +94 -0
  323. package/src/lib/validators/engineer-rounds.ts +70 -0
  324. package/src/lib/validators/hospitals.ts +17 -0
  325. package/src/lib/validators/locations.ts +15 -0
  326. package/src/lib/validators/maintenance.ts +211 -0
  327. package/src/lib/validators/organizations.ts +11 -0
  328. package/src/lib/validators/reports.ts +31 -0
  329. package/src/lib/validators/signatures.ts +49 -0
  330. package/src/lib/validators/system-settings.ts +55 -0
  331. package/src/lib/validators/tickets.ts +125 -0
  332. package/src/lib/validators/users.ts +43 -0
  333. package/src/middleware.ts +45 -0
  334. package/tailwind.config.ts +76 -0
  335. package/tsconfig.json +28 -0
@@ -0,0 +1,235 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { getNavBadgeCounts, type NavBadgeCounts } from '@/lib/actions/nav-counts';
5
+ import { type NavBadgeConfig } from '@/components/layout/nav-badge';
6
+
7
+ export interface NavCountsContextValue {
8
+ counts: NavBadgeCounts;
9
+ isLoading: boolean;
10
+ refreshCounts: () => Promise<void>;
11
+ /**
12
+ * Helper to resolve a complete NavBadgeConfig from a key and/or custom config.
13
+ */
14
+ resolveBadge: (
15
+ badgeKey?: string,
16
+ customConfig?: Partial<NavBadgeConfig>,
17
+ options?: { isCollapsed?: boolean }
18
+ ) => NavBadgeConfig | null;
19
+ }
20
+
21
+ const defaultCounts: NavBadgeCounts = {
22
+ notifications: 0,
23
+ triage: 0,
24
+ tickets: 0,
25
+ maintenance: 0,
26
+ workOrders: 0,
27
+ pmPlans: 0,
28
+ hasMaintenanceDue: false,
29
+ };
30
+
31
+ const NavCountsContext = React.createContext<NavCountsContextValue>({
32
+ counts: defaultCounts,
33
+ isLoading: false,
34
+ refreshCounts: async () => {},
35
+ resolveBadge: () => null,
36
+ });
37
+
38
+ export function NavCountsProvider({
39
+ children,
40
+ initialCounts,
41
+ }: {
42
+ children: React.ReactNode;
43
+ initialCounts?: NavBadgeCounts;
44
+ }) {
45
+ const [counts, setCounts] = React.useState<NavBadgeCounts>(initialCounts || defaultCounts);
46
+ const [isLoading, setIsLoading] = React.useState(false);
47
+
48
+ const refreshCounts = React.useCallback(async () => {
49
+ try {
50
+ setIsLoading(true);
51
+ const newCounts = await getNavBadgeCounts();
52
+ setCounts(newCounts);
53
+ } catch (err) {
54
+ console.error('[NavCountsProvider] Failed to refresh badge counts:', err);
55
+ } finally {
56
+ setIsLoading(false);
57
+ }
58
+ }, []);
59
+
60
+ // Initial fetch and automatic refresh hooks
61
+ React.useEffect(() => {
62
+ refreshCounts();
63
+
64
+ // 1. Listen for global notification events from SSE or Service Worker
65
+ const handleNotificationEvent = () => {
66
+ refreshCounts();
67
+ };
68
+
69
+ window.addEventListener('bemms:notification', handleNotificationEvent);
70
+
71
+ // 2. Refresh on window focus / visibilitychange
72
+ const handleVisibility = () => {
73
+ if (!document.hidden) {
74
+ refreshCounts();
75
+ }
76
+ };
77
+ document.addEventListener('visibilitychange', handleVisibility);
78
+
79
+ // 3. Periodic polling every 30 seconds
80
+ const interval = setInterval(() => {
81
+ if (!document.hidden) {
82
+ refreshCounts();
83
+ }
84
+ }, 30000);
85
+
86
+ return () => {
87
+ window.removeEventListener('bemms:notification', handleNotificationEvent);
88
+ document.removeEventListener('visibilitychange', handleVisibility);
89
+ clearInterval(interval);
90
+ };
91
+ }, [refreshCounts]);
92
+
93
+ // Helper to resolve badge configuration for any navbar item
94
+ const resolveBadge = React.useCallback(
95
+ (
96
+ badgeKey?: string,
97
+ customConfig?: Partial<NavBadgeConfig>,
98
+ options?: { isCollapsed?: boolean }
99
+ ): NavBadgeConfig | null => {
100
+ const isCollapsed = options?.isCollapsed ?? false;
101
+
102
+ // If explicitly disabled, return null
103
+ if (customConfig && customConfig.enabled === false) {
104
+ return null;
105
+ }
106
+
107
+ let baseConfig: NavBadgeConfig | null = null;
108
+
109
+ switch (badgeKey) {
110
+ case 'notifications':
111
+ baseConfig = {
112
+ enabled: counts.notifications > 0,
113
+ type: 'count',
114
+ value: counts.notifications,
115
+ variant: 'primary',
116
+ position: isCollapsed ? 'icon-corner' : 'trailing',
117
+ hideWhenZero: true,
118
+ maxCount: 99,
119
+ ariaLabel: `${counts.notifications} unread notifications`,
120
+ };
121
+ break;
122
+
123
+ case 'triage':
124
+ baseConfig = {
125
+ enabled: counts.triage > 0,
126
+ type: 'count',
127
+ value: counts.triage,
128
+ variant: 'danger',
129
+ position: isCollapsed ? 'icon-corner' : 'trailing',
130
+ hideWhenZero: true,
131
+ maxCount: 99,
132
+ pulse: counts.triage > 0,
133
+ ariaLabel: `${counts.triage} tickets pending triage`,
134
+ };
135
+ break;
136
+
137
+ case 'tickets':
138
+ baseConfig = {
139
+ enabled: counts.tickets > 0,
140
+ type: 'count',
141
+ value: counts.tickets,
142
+ variant: 'secondary',
143
+ position: isCollapsed ? 'icon-corner' : 'trailing',
144
+ hideWhenZero: true,
145
+ maxCount: 99,
146
+ ariaLabel: `${counts.tickets} active tickets`,
147
+ };
148
+ break;
149
+
150
+ case 'maintenance':
151
+ baseConfig = {
152
+ enabled: counts.hasMaintenanceDue,
153
+ type: 'dot',
154
+ variant: counts.pmPlans > 0 ? 'warning' : 'default',
155
+ position: isCollapsed ? 'icon-corner' : 'trailing',
156
+ pulse: counts.pmPlans > 0,
157
+ ariaLabel: 'Maintenance items due or scheduled',
158
+ };
159
+ break;
160
+
161
+ case 'workOrders':
162
+ baseConfig = {
163
+ enabled: counts.workOrders > 0,
164
+ type: 'count',
165
+ value: counts.workOrders,
166
+ variant: 'secondary',
167
+ position: isCollapsed ? 'icon-corner' : 'trailing',
168
+ hideWhenZero: true,
169
+ maxCount: 99,
170
+ ariaLabel: `${counts.workOrders} active work orders`,
171
+ };
172
+ break;
173
+
174
+ case 'pmPlans':
175
+ baseConfig = {
176
+ enabled: counts.pmPlans > 0,
177
+ type: counts.pmPlans > 0 ? 'count' : 'dot',
178
+ value: counts.pmPlans,
179
+ variant: 'warning',
180
+ position: isCollapsed ? 'icon-corner' : 'trailing',
181
+ hideWhenZero: true,
182
+ maxCount: 99,
183
+ ariaLabel: `${counts.pmPlans} preventive maintenance items due`,
184
+ };
185
+ break;
186
+
187
+ default:
188
+ if (!customConfig) return null;
189
+ baseConfig = {
190
+ enabled: true,
191
+ type: customConfig.type || 'count',
192
+ value: customConfig.value,
193
+ variant: customConfig.variant || 'default',
194
+ position: isCollapsed ? 'icon-corner' : (customConfig.position || 'trailing'),
195
+ hideWhenZero: customConfig.hideWhenZero ?? true,
196
+ maxCount: customConfig.maxCount ?? 99,
197
+ };
198
+ break;
199
+ }
200
+
201
+ if (!baseConfig && !customConfig) return null;
202
+
203
+ // Merge customConfig over baseConfig
204
+ const finalConfig: NavBadgeConfig = {
205
+ ...(baseConfig || {}),
206
+ ...(customConfig || {}),
207
+ position: isCollapsed ? 'icon-corner' : (customConfig?.position || baseConfig?.position || 'trailing'),
208
+ };
209
+
210
+ return finalConfig;
211
+ },
212
+ [counts]
213
+ );
214
+
215
+ return (
216
+ <NavCountsContext.Provider
217
+ value={{
218
+ counts,
219
+ isLoading,
220
+ refreshCounts,
221
+ resolveBadge,
222
+ }}
223
+ >
224
+ {children}
225
+ </NavCountsContext.Provider>
226
+ );
227
+ }
228
+
229
+ export function useNavCounts() {
230
+ const context = React.useContext(NavCountsContext);
231
+ if (!context) {
232
+ throw new Error('useNavCounts must be used within a NavCountsProvider');
233
+ }
234
+ return context;
235
+ }
@@ -0,0 +1,7 @@
1
+ 'use client'
2
+
3
+ import { SessionProvider } from 'next-auth/react'
4
+
5
+ export default function AuthProvider({ children }: { children: React.ReactNode }) {
6
+ return <SessionProvider>{children}</SessionProvider>
7
+ }
@@ -0,0 +1,124 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ export type Theme = 'light' | 'dark' | 'system'
6
+
7
+ interface ThemeContextType {
8
+ theme: Theme
9
+ setTheme: (theme: Theme) => void
10
+ resolvedTheme: 'light' | 'dark'
11
+ systemTheme: 'light' | 'dark'
12
+ }
13
+
14
+ const ThemeContext = React.createContext<ThemeContextType | undefined>(undefined)
15
+
16
+ const STORAGE_KEY = 'bemms-theme'
17
+
18
+ function getSystemTheme(): 'light' | 'dark' {
19
+ if (typeof window === 'undefined') return 'light'
20
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
21
+ }
22
+
23
+ interface ThemeProviderProps {
24
+ children: React.ReactNode
25
+ defaultTheme?: Theme
26
+ storageKey?: string
27
+ attribute?: string
28
+ enableSystem?: boolean
29
+ disableTransitionOnChange?: boolean
30
+ }
31
+
32
+ export function ThemeProvider({
33
+ children,
34
+ defaultTheme = 'system',
35
+ storageKey = STORAGE_KEY,
36
+ }: ThemeProviderProps) {
37
+ const [theme, setThemeState] = React.useState<Theme>(defaultTheme)
38
+ const [systemTheme, setSystemTheme] = React.useState<'light' | 'dark'>('light')
39
+ const [mounted, setMounted] = React.useState(false)
40
+
41
+ // Initialize theme from storage
42
+ React.useEffect(() => {
43
+ try {
44
+ const stored = localStorage.getItem(storageKey) as Theme | null
45
+ if (stored === 'light' || stored === 'dark' || stored === 'system') {
46
+ setThemeState(stored)
47
+ }
48
+ } catch {
49
+ // Ignore localStorage read errors
50
+ }
51
+
52
+ const currentSystem = getSystemTheme()
53
+ setSystemTheme(currentSystem)
54
+ setMounted(true)
55
+ }, [storageKey])
56
+
57
+ // Listen to system theme changes
58
+ React.useEffect(() => {
59
+ if (typeof window === 'undefined') return
60
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
61
+
62
+ const handleChange = (e: MediaQueryListEvent) => {
63
+ setSystemTheme(e.matches ? 'dark' : 'light')
64
+ }
65
+
66
+ mediaQuery.addEventListener('change', handleChange)
67
+ return () => mediaQuery.removeEventListener('change', handleChange)
68
+ }, [])
69
+
70
+ const resolvedTheme: 'light' | 'dark' = theme === 'system' ? systemTheme : theme
71
+
72
+ // Apply class to documentElement
73
+ React.useEffect(() => {
74
+ if (!mounted) return
75
+ const root = document.documentElement
76
+
77
+ if (resolvedTheme === 'dark') {
78
+ root.classList.add('dark')
79
+ } else {
80
+ root.classList.remove('dark')
81
+ }
82
+ }, [resolvedTheme, mounted])
83
+
84
+ const setTheme = React.useCallback(
85
+ (newTheme: Theme) => {
86
+ setThemeState(newTheme)
87
+ try {
88
+ localStorage.setItem(storageKey, newTheme)
89
+ } catch {
90
+ // Ignore localStorage write errors
91
+ }
92
+ },
93
+ [storageKey]
94
+ )
95
+
96
+ const value = React.useMemo(
97
+ () => ({
98
+ theme,
99
+ setTheme,
100
+ resolvedTheme: mounted ? resolvedTheme : 'light',
101
+ systemTheme,
102
+ }),
103
+ [theme, setTheme, resolvedTheme, systemTheme, mounted]
104
+ )
105
+
106
+ return (
107
+ <ThemeContext.Provider value={value}>
108
+ <script
109
+ dangerouslySetInnerHTML={{
110
+ __html: `(function(){try{var t=localStorage.getItem('${storageKey}');var s=window.matchMedia('(prefers-color-scheme: dark)').matches;if(t==='dark'||(!t&&s)||(t==='system'&&s)){document.documentElement.classList.add('dark')}else{document.documentElement.classList.remove('dark')}}catch(e){}})()`,
111
+ }}
112
+ />
113
+ {children}
114
+ </ThemeContext.Provider>
115
+ )
116
+ }
117
+
118
+ export function useTheme(): ThemeContextType {
119
+ const context = React.useContext(ThemeContext)
120
+ if (!context) {
121
+ throw new Error('useTheme must be used within a ThemeProvider')
122
+ }
123
+ return context
124
+ }
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Download, X } from 'lucide-react';
6
+
7
+ interface BeforeInstallPromptEvent extends Event {
8
+ prompt: () => Promise<void>;
9
+ userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
10
+ }
11
+
12
+ export function InstallPrompt() {
13
+ const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
14
+ const [showBanner, setShowBanner] = useState(false);
15
+ const [isInstalled, setIsInstalled] = useState(false);
16
+
17
+ useEffect(() => {
18
+ // Check if already installed
19
+ if (window.matchMedia('(display-mode: standalone)').matches) {
20
+ setIsInstalled(true);
21
+ return;
22
+ }
23
+
24
+ const handler = (e: Event) => {
25
+ e.preventDefault();
26
+ setDeferredPrompt(e as BeforeInstallPromptEvent);
27
+ // Show banner after a short delay so it doesn't interrupt initial load
28
+ setTimeout(() => setShowBanner(true), 3000);
29
+ };
30
+
31
+ window.addEventListener('beforeinstallprompt', handler);
32
+
33
+ // Listen for successful install
34
+ window.addEventListener('appinstalled', () => {
35
+ setIsInstalled(true);
36
+ setShowBanner(false);
37
+ setDeferredPrompt(null);
38
+ });
39
+
40
+ return () => window.removeEventListener('beforeinstallprompt', handler);
41
+ }, []);
42
+
43
+ const handleInstall = async () => {
44
+ if (!deferredPrompt) return;
45
+ await deferredPrompt.prompt();
46
+ const { outcome } = await deferredPrompt.userChoice;
47
+ if (outcome === 'accepted') {
48
+ setShowBanner(false);
49
+ }
50
+ setDeferredPrompt(null);
51
+ };
52
+
53
+ if (isInstalled || !showBanner) return null;
54
+
55
+ return (
56
+ <div className="fixed bottom-20 md:bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-80 z-50 animate-in slide-in-from-bottom-4 duration-300">
57
+ <div className="bg-card border rounded-lg shadow-lg p-4 flex items-center gap-3">
58
+ <div className="bg-primary/10 p-2 rounded-lg shrink-0">
59
+ <Download className="h-5 w-5 text-primary" />
60
+ </div>
61
+ <div className="flex-1 min-w-0">
62
+ <p className="text-sm font-medium">Install BEMMS</p>
63
+ <p className="text-xs text-muted-foreground">Quick access from your home screen</p>
64
+ </div>
65
+ <div className="flex items-center gap-1 shrink-0">
66
+ <Button size="sm" onClick={handleInstall} className="min-h-[36px]">
67
+ Install
68
+ </Button>
69
+ <Button
70
+ variant="ghost"
71
+ size="icon"
72
+ onClick={() => setShowBanner(false)}
73
+ className="h-8 w-8"
74
+ >
75
+ <X className="h-4 w-4" />
76
+ </Button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
82
+
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ export function ServiceWorkerRegister() {
6
+ useEffect(() => {
7
+ if (typeof window === 'undefined' || !('serviceWorker' in navigator)) return;
8
+
9
+ // In development mode on localhost, unregister any service worker and clear caches
10
+ // to prevent stale chunk caching from breaking Webpack HMR
11
+ if (process.env.NODE_ENV === 'development' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
12
+ navigator.serviceWorker.getRegistrations().then((registrations) => {
13
+ for (const registration of registrations) {
14
+ registration.unregister();
15
+ }
16
+ });
17
+ if ('caches' in window) {
18
+ caches.keys().then((keys) => {
19
+ for (const key of keys) {
20
+ caches.delete(key);
21
+ }
22
+ });
23
+ }
24
+ return;
25
+ }
26
+
27
+ navigator.serviceWorker
28
+ .register('/sw.js')
29
+ .then((registration) => {
30
+ console.log('[PWA] Service worker registered:', registration.scope);
31
+
32
+ // Check for updates periodically
33
+ setInterval(() => {
34
+ registration.update();
35
+ }, 60 * 60 * 1000); // Every hour
36
+ })
37
+ .catch((error) => {
38
+ console.error('[PWA] Service worker registration failed:', error);
39
+ });
40
+ }, []);
41
+
42
+ return null;
43
+ }
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { Download } from 'lucide-react';
5
+ import { useState } from 'react';
6
+
7
+ interface ExportButtonProps {
8
+ reportType: string;
9
+ filters: Record<string, string>;
10
+ label?: string;
11
+ }
12
+
13
+ export function ReportExportButton({ reportType, filters, label = 'Export CSV' }: ExportButtonProps) {
14
+ const [loading, setLoading] = useState(false);
15
+
16
+ const handleExport = async () => {
17
+ setLoading(true);
18
+ try {
19
+ const params = new URLSearchParams({
20
+ type: reportType,
21
+ ...Object.fromEntries(
22
+ Object.entries(filters).filter(([, v]) => v && v !== 'all')
23
+ ),
24
+ });
25
+
26
+ const response = await fetch(`/api/reports/export?${params.toString()}`);
27
+
28
+ if (!response.ok) {
29
+ throw new Error('Export failed');
30
+ }
31
+
32
+ const blob = await response.blob();
33
+ const url = URL.createObjectURL(blob);
34
+ const a = document.createElement('a');
35
+ const disposition = response.headers.get('content-disposition');
36
+ const filenameMatch = disposition?.match(/filename="(.+)"/);
37
+ a.download = filenameMatch?.[1] || `${reportType}-report.csv`;
38
+ a.href = url;
39
+ document.body.appendChild(a);
40
+ a.click();
41
+ document.body.removeChild(a);
42
+ URL.revokeObjectURL(url);
43
+ } catch (error) {
44
+ console.error('Export error:', error);
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <Button
52
+ onClick={handleExport}
53
+ disabled={loading}
54
+ variant="outline"
55
+ className="min-h-[44px] w-full sm:w-auto"
56
+ >
57
+ <Download className="h-4 w-4 mr-2" />
58
+ {loading ? 'Exporting...' : label}
59
+ </Button>
60
+ );
61
+ }
62
+
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import { useRouter, useSearchParams } from 'next/navigation';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ interface FilterOption {
8
+ key: string;
9
+ label: string;
10
+ value: string;
11
+ onRemove: () => void;
12
+ }
13
+
14
+ interface ReportFilterChipsProps {
15
+ filters: FilterOption[];
16
+ onClear: () => void;
17
+ }
18
+
19
+ export function ReportFilterChips({ filters, onClear }: ReportFilterChipsProps) {
20
+ if (filters.length === 0) return null;
21
+
22
+ return (
23
+ <div className="flex items-center gap-2 overflow-x-auto pb-2 scrollbar-hide">
24
+ {filters.map((filter) => (
25
+ <Badge
26
+ key={filter.key}
27
+ variant="secondary"
28
+ className="shrink-0 gap-1 cursor-pointer hover:bg-destructive/10 min-h-[32px] px-3"
29
+ onClick={filter.onRemove}
30
+ >
31
+ <span className="text-xs text-muted-foreground">{filter.label}:</span>
32
+ <span className="text-xs font-medium">{filter.value}</span>
33
+ <span className="text-muted-foreground ml-1">×</span>
34
+ </Badge>
35
+ ))}
36
+ {filters.length > 1 && (
37
+ <button
38
+ onClick={onClear}
39
+ className="text-xs text-muted-foreground hover:text-foreground shrink-0 underline"
40
+ >
41
+ Clear all
42
+ </button>
43
+ )}
44
+ </div>
45
+ );
46
+ }
47
+
@@ -0,0 +1,40 @@
1
+ import { Card, CardContent } from '@/components/ui/card';
2
+ import { cn } from '@/lib/utils';
3
+ import { LucideIcon } from 'lucide-react';
4
+
5
+ interface SummaryItem {
6
+ label: string;
7
+ value: number | string;
8
+ icon?: LucideIcon;
9
+ color?: string;
10
+ }
11
+
12
+ interface ReportSummaryCardsProps {
13
+ items: SummaryItem[];
14
+ className?: string;
15
+ }
16
+
17
+ export function ReportSummaryCards({ items, className }: ReportSummaryCardsProps) {
18
+ return (
19
+ <div className={cn('grid gap-3 grid-cols-2 lg:grid-cols-4', className)}>
20
+ {items.map((item) => (
21
+ <Card key={item.label} className="bg-muted/20">
22
+ <CardContent className="p-4">
23
+ <div className="flex items-center justify-between">
24
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
25
+ {item.label}
26
+ </p>
27
+ {item.icon && (
28
+ <item.icon className={cn('h-4 w-4', item.color || 'text-muted-foreground')} />
29
+ )}
30
+ </div>
31
+ <p className={cn('text-2xl font-bold mt-1', item.color)}>
32
+ {item.value}
33
+ </p>
34
+ </CardContent>
35
+ </Card>
36
+ ))}
37
+ </div>
38
+ );
39
+ }
40
+
@@ -0,0 +1,54 @@
1
+ import { Calendar, Filter, Globe } from 'lucide-react';
2
+
3
+ interface TransparencyHeaderProps {
4
+ reportType: string;
5
+ dateRange?: { start?: string; end?: string };
6
+ timezone: string;
7
+ filters?: Record<string, string>;
8
+ generatedAt?: Date;
9
+ }
10
+
11
+ export function ReportTransparencyHeader({
12
+ reportType,
13
+ dateRange,
14
+ timezone,
15
+ filters,
16
+ generatedAt,
17
+ }: TransparencyHeaderProps) {
18
+ const dateStr = dateRange?.start && dateRange?.end
19
+ ? `${new Date(dateRange.start).toLocaleDateString()} – ${new Date(dateRange.end).toLocaleDateString()}`
20
+ : 'All time';
21
+
22
+ const activeFilters = filters
23
+ ? Object.entries(filters)
24
+ .filter(([, v]) => v && v !== 'all')
25
+ .map(([k, v]) => `${k}: ${v}`)
26
+ : [];
27
+
28
+ return (
29
+ <div className="bg-muted/30 border rounded-lg px-4 py-3 text-sm space-y-1">
30
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-muted-foreground">
31
+ <span className="flex items-center gap-1.5">
32
+ <Calendar className="h-3.5 w-3.5" />
33
+ {dateStr}
34
+ </span>
35
+ <span className="flex items-center gap-1.5">
36
+ <Globe className="h-3.5 w-3.5" />
37
+ {timezone}
38
+ </span>
39
+ {activeFilters.length > 0 && (
40
+ <span className="flex items-center gap-1.5">
41
+ <Filter className="h-3.5 w-3.5" />
42
+ {activeFilters.join(' · ')}
43
+ </span>
44
+ )}
45
+ </div>
46
+ {generatedAt && (
47
+ <p className="text-xs text-muted-foreground">
48
+ Generated: {generatedAt.toLocaleString()}
49
+ </p>
50
+ )}
51
+ </div>
52
+ );
53
+ }
54
+