@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,54 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { savePushSubscription } from '@/lib/actions/notifications';
3
+ import { getLogger } from '@/lib/logger';
4
+
5
+ const log = getLogger('push-subscribe');
6
+
7
+ const MAX_ENDPOINT_LENGTH = 2048;
8
+ const MAX_KEY_LENGTH = 512;
9
+
10
+ export async function POST(req: NextRequest) {
11
+ try {
12
+ const body = await req.json();
13
+
14
+ if (!body.endpoint || !body.keys?.p256dh || !body.keys?.auth) {
15
+ return NextResponse.json(
16
+ { error: 'Invalid push subscription' },
17
+ { status: 400 }
18
+ );
19
+ }
20
+
21
+ // Validate input lengths to prevent abuse
22
+ if (
23
+ typeof body.endpoint !== 'string' ||
24
+ body.endpoint.length > MAX_ENDPOINT_LENGTH ||
25
+ typeof body.keys.p256dh !== 'string' ||
26
+ body.keys.p256dh.length > MAX_KEY_LENGTH ||
27
+ typeof body.keys.auth !== 'string' ||
28
+ body.keys.auth.length > MAX_KEY_LENGTH
29
+ ) {
30
+ return NextResponse.json(
31
+ { error: 'Invalid push subscription data' },
32
+ { status: 400 }
33
+ );
34
+ }
35
+
36
+ const subscription = await savePushSubscription({
37
+ endpoint: body.endpoint,
38
+ keys: {
39
+ p256dh: body.keys.p256dh,
40
+ auth: body.keys.auth,
41
+ },
42
+ userAgent: req.headers.get('user-agent') || undefined,
43
+ });
44
+
45
+ return NextResponse.json({ success: true, id: subscription.id });
46
+ } catch (error: unknown) {
47
+ // Log the real error server-side; return a generic message to the client
48
+ log.error('Push subscription error', error instanceof Error ? error : { message: String(error) });
49
+ return NextResponse.json(
50
+ { error: 'Failed to save push subscription' },
51
+ { status: 500 }
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,7 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function GET() {
4
+ const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || '';
5
+ return NextResponse.json({ publicKey });
6
+ }
7
+
@@ -0,0 +1,102 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { requireAuth, requirePermission } from '@/lib/auth/rbac';
3
+ import { getInventoryReport, getTicketPerformanceReport, getCostReport, getWorkloadReport } from '@/lib/actions/reports';
4
+ import { generateCSV, formatReportMetadata } from '@/lib/utils/export';
5
+ import { saveReportToHistory } from '@/lib/actions/report-history';
6
+
7
+ export async function GET(req: NextRequest) {
8
+ try {
9
+ const user = await requireAuth();
10
+ await requirePermission('REPORTS', 'VIEW');
11
+
12
+ const searchParams = req.nextUrl.searchParams;
13
+ const reportType = searchParams.get('type') || searchParams.get('reportType');
14
+
15
+ // Parse filters
16
+ const filters = Object.fromEntries(searchParams.entries());
17
+
18
+ let data: any[] = [];
19
+ let columns: { key: string; header: string }[] = [];
20
+
21
+ if (reportType === 'inventory') {
22
+ const results = await getInventoryReport(filters as any);
23
+ data = results;
24
+ columns = [
25
+ { key: 'groupId', header: 'Group' },
26
+ { key: 'totalCount', header: 'Total Devices' },
27
+ { key: 'operationalCount', header: 'Operational' },
28
+ { key: 'maintenanceCount', header: 'Under Maintenance' },
29
+ { key: 'outOfServiceCount', header: 'Out of Service' },
30
+ { key: 'decommissionedCount', header: 'Decommissioned' }
31
+ ];
32
+ } else if (reportType === 'tickets') {
33
+ const results = await getTicketPerformanceReport(filters as any);
34
+ data = [results];
35
+ columns = [
36
+ { key: 'total', header: 'Total Tickets' },
37
+ { key: 'openCount', header: 'Open Tickets' },
38
+ { key: 'closedCount', header: 'Closed Tickets' },
39
+ { key: 'avgResponseTime', header: 'Avg Response Time (hrs)' },
40
+ { key: 'avgResolutionTime', header: 'Avg Resolution Time (hrs)' },
41
+ { key: 'priorityP1', header: 'P1 Critical' },
42
+ { key: 'priorityP2', header: 'P2 High' },
43
+ { key: 'priorityP3', header: 'P3 Normal' },
44
+ { key: 'priorityP4', header: 'P4 Low' }
45
+ ];
46
+ } else if (reportType === 'costs') {
47
+ const results = await getCostReport(filters as any);
48
+ data = results.byCostType;
49
+ columns = [
50
+ { key: 'costType', header: 'Cost Type' },
51
+ { key: 'totalAmount', header: 'Total Amount' },
52
+ { key: 'count', header: 'Record Count' }
53
+ ];
54
+ } else if (reportType === 'workload') {
55
+ const results = await getWorkloadReport(filters as any);
56
+ data = results;
57
+ columns = [
58
+ { key: 'engineerId', header: 'Engineer ID' },
59
+ { key: 'assignedCount', header: 'Total Tasks' },
60
+ { key: 'completedCount', header: 'Completed Tasks' },
61
+ { key: 'inProgressCount', header: 'In Progress Tasks' },
62
+ { key: 'priorityP1Count', header: 'P1 Critical Tasks' },
63
+ { key: 'pmCount', header: 'Preventive Tasks' }
64
+ ];
65
+ } else {
66
+ return NextResponse.json({ error: 'Unsupported report type' }, { status: 400 });
67
+ }
68
+
69
+ const csvData = generateCSV(data, columns);
70
+ const metadata = formatReportMetadata({
71
+ reportType,
72
+ filters,
73
+ timezone: filters.timezone || 'UTC',
74
+ generatedAt: new Date(),
75
+ generatedBy: user.fullName || user.email,
76
+ });
77
+
78
+ const finalCsv = metadata + csvData;
79
+ const fileName = `${reportType}_report_${new Date().toISOString().split('T')[0]}.csv`;
80
+
81
+ await saveReportToHistory({
82
+ reportType,
83
+ title: `${reportType} Report`,
84
+ filters,
85
+ rowCount: data.length,
86
+ fileFormat: 'csv',
87
+ fileName
88
+ });
89
+
90
+ return new NextResponse(finalCsv, {
91
+ headers: {
92
+ 'Content-Type': 'text/csv; charset=utf-8',
93
+ 'Content-Disposition': `attachment; filename="${fileName}"`,
94
+ },
95
+ });
96
+
97
+ } catch (error: any) {
98
+ console.error('Export error:', error);
99
+ return NextResponse.json({ error: error.message || 'Internal Server Error' }, { status: 500 });
100
+ }
101
+ }
102
+
@@ -0,0 +1,129 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { db } from '@/lib/db';
4
+ import { serviceTicketAttachments, serviceTickets } from '@/lib/db/schema';
5
+ import { eq } from 'drizzle-orm';
6
+ import { writeFile, mkdir } from 'fs/promises';
7
+ import path from 'path';
8
+
9
+ export const dynamic = 'force-dynamic';
10
+
11
+ const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB
12
+
13
+ export async function POST(
14
+ req: NextRequest,
15
+ { params }: { params: Promise<{ id: string }> }
16
+ ) {
17
+ const session = await auth();
18
+ if (!session?.user?.id) {
19
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
20
+ }
21
+
22
+ const { id: ticketId } = await params;
23
+
24
+ try {
25
+ const ticket = await db.query.serviceTickets.findFirst({
26
+ where: eq(serviceTickets.id, ticketId),
27
+ });
28
+
29
+ if (!ticket) {
30
+ return NextResponse.json({ error: 'Ticket not found' }, { status: 404 });
31
+ }
32
+
33
+ const formData = await req.formData();
34
+ const file = formData.get('file') as File | null;
35
+ const visibility = (formData.get('visibility') as string) || 'public';
36
+
37
+ if (!file) {
38
+ return NextResponse.json({ error: 'No file provided' }, { status: 400 });
39
+ }
40
+
41
+ if (file.size > MAX_FILE_SIZE) {
42
+ return NextResponse.json(
43
+ { error: 'File exceeds maximum size of 20MB' },
44
+ { status: 400 }
45
+ );
46
+ }
47
+
48
+ // Sanitize filename and create unique storage name
49
+ const originalName = file.name || 'attachment';
50
+ const ext = path.extname(originalName) || '';
51
+ const baseName = path.basename(originalName, ext).replace(/[^a-zA-Z0-9_-]/g, '_');
52
+ const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
53
+ const savedFileName = `${baseName}-${uniqueSuffix}${ext}`;
54
+
55
+ const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'tickets');
56
+ await mkdir(uploadDir, { recursive: true });
57
+
58
+ const filePath = path.join(uploadDir, savedFileName);
59
+ const buffer = Buffer.from(await file.arrayBuffer());
60
+ await writeFile(filePath, buffer);
61
+
62
+ const storageKey = `/uploads/tickets/${savedFileName}`;
63
+
64
+ const [attachment] = await db
65
+ .insert(serviceTicketAttachments)
66
+ .values({
67
+ serviceTicketId: ticketId,
68
+ fileName: originalName,
69
+ fileSize: file.size,
70
+ mimeType: file.type || 'application/octet-stream',
71
+ storageKey,
72
+ visibility: visibility as any,
73
+ uploadedByUserId: (session.user as any).id,
74
+ })
75
+ .returning();
76
+
77
+ return NextResponse.json({
78
+ success: true,
79
+ attachment: {
80
+ id: attachment.id,
81
+ fileName: attachment.fileName,
82
+ fileSize: attachment.fileSize,
83
+ mimeType: attachment.mimeType,
84
+ url: attachment.storageKey,
85
+ createdAt: attachment.createdAt,
86
+ },
87
+ });
88
+ } catch (error: any) {
89
+ console.error('[Attachment Upload] Error:', error);
90
+ return NextResponse.json(
91
+ { error: error.message || 'File upload failed' },
92
+ { status: 500 }
93
+ );
94
+ }
95
+ }
96
+
97
+ export async function GET(
98
+ req: NextRequest,
99
+ { params }: { params: Promise<{ id: string }> }
100
+ ) {
101
+ const session = await auth();
102
+ if (!session?.user?.id) {
103
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
104
+ }
105
+
106
+ const { id: ticketId } = await params;
107
+
108
+ try {
109
+ const attachments = await db.query.serviceTicketAttachments.findMany({
110
+ where: eq(serviceTicketAttachments.serviceTicketId, ticketId),
111
+ with: {
112
+ uploadedByUser: {
113
+ columns: {
114
+ id: true,
115
+ fullName: true,
116
+ },
117
+ },
118
+ },
119
+ orderBy: (t, { desc }) => [desc(t.createdAt)],
120
+ });
121
+
122
+ return NextResponse.json({ success: true, attachments });
123
+ } catch (error: any) {
124
+ return NextResponse.json(
125
+ { error: error.message || 'Failed to fetch attachments' },
126
+ { status: 500 }
127
+ );
128
+ }
129
+ }
@@ -0,0 +1,52 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { db } from '@/lib/db';
4
+ import { serviceTickets, serviceTicketComments } from '@/lib/db/schema';
5
+ import { eq, sql } from 'drizzle-orm';
6
+
7
+ export const dynamic = 'force-dynamic';
8
+
9
+ export async function GET(
10
+ req: NextRequest,
11
+ { params }: { params: Promise<{ id: string }> }
12
+ ) {
13
+ const session = await auth();
14
+ if (!session?.user?.id) {
15
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
16
+ }
17
+
18
+ const { id: ticketId } = await params;
19
+
20
+ try {
21
+ const [ticket, commentResult] = await Promise.all([
22
+ db.query.serviceTickets.findFirst({
23
+ where: eq(serviceTickets.id, ticketId),
24
+ columns: {
25
+ id: true,
26
+ statusCode: true,
27
+ assignedEngineerUserId: true,
28
+ updatedAt: true,
29
+ },
30
+ }),
31
+ db.select({
32
+ count: sql<number>`count(*)::int`,
33
+ })
34
+ .from(serviceTicketComments)
35
+ .where(eq(serviceTicketComments.serviceTicketId, ticketId)),
36
+ ]);
37
+
38
+ if (!ticket) {
39
+ return NextResponse.json({ error: 'Not found' }, { status: 404 });
40
+ }
41
+
42
+ return NextResponse.json({
43
+ id: ticket.id,
44
+ statusCode: ticket.statusCode,
45
+ assignedEngineerUserId: ticket.assignedEngineerUserId,
46
+ updatedAt: ticket.updatedAt ? new Date(ticket.updatedAt).toISOString() : null,
47
+ commentCount: commentResult[0]?.count || 0,
48
+ });
49
+ } catch (error: any) {
50
+ return NextResponse.json({ error: error.message }, { status: 500 });
51
+ }
52
+ }
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Card, CardContent } from '@/components/ui/card';
6
+ import { AlertTriangle, RotateCcw } from 'lucide-react';
7
+ import Link from 'next/link';
8
+
9
+ export default function GlobalError({
10
+ error,
11
+ reset,
12
+ }: {
13
+ error: Error & { digest?: string };
14
+ reset: () => void;
15
+ }) {
16
+ useEffect(() => {
17
+ // Only log full error details in development; production errors are
18
+ // captured server-side and identified by their digest hash.
19
+ if (process.env.NODE_ENV !== 'production') {
20
+ console.error('Unhandled application error:', error);
21
+ }
22
+ }, [error]);
23
+
24
+ return (
25
+ <div className="min-h-screen flex items-center justify-center p-4 bg-background">
26
+ <Card className="max-w-md w-full text-center">
27
+ <CardContent className="pt-8 pb-8 space-y-4">
28
+ <div className="h-12 w-12 rounded-full bg-destructive/10 text-destructive flex items-center justify-center mx-auto">
29
+ <AlertTriangle className="h-6 w-6" />
30
+ </div>
31
+ <h1 className="text-2xl font-bold tracking-tight">Something went wrong</h1>
32
+ <p className="text-sm text-muted-foreground">
33
+ An unexpected error occurred. Our engineering team has been notified.
34
+ </p>
35
+ {error.digest && (
36
+ <p className="text-xs font-mono text-muted-foreground bg-muted p-2 rounded">
37
+ Error ID: {error.digest}
38
+ </p>
39
+ )}
40
+ <div className="flex items-center justify-center gap-2 pt-2">
41
+ <Button onClick={reset} variant="default" className="min-h-[44px] gap-2">
42
+ <RotateCcw className="h-4 w-4" />
43
+ Try Again
44
+ </Button>
45
+ <Button asChild variant="outline" className="min-h-[44px]">
46
+ <Link href="/dashboard">Dashboard</Link>
47
+ </Button>
48
+ </div>
49
+ </CardContent>
50
+ </Card>
51
+ </div>
52
+ );
53
+ }
54
+
@@ -0,0 +1,89 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 221.2 83.2% 53.3%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --secondary: 210 40% 96.1%;
16
+ --secondary-foreground: 222.2 47.4% 11.2%;
17
+ --muted: 210 40% 96.1%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 210 40% 96.1%;
20
+ --accent-foreground: 222.2 47.4% 11.2%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 210 40% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 221.2 83.2% 53.3%;
26
+ --radius: 0.5rem;
27
+ --warning: 38 92% 50%;
28
+ --warning-foreground: 48 96% 89%;
29
+ --success: 142 76% 36%;
30
+ --success-foreground: 355.7 100% 97.3%;
31
+ --chart-1: 221 83% 53%;
32
+ --chart-2: 160 60% 45%;
33
+ --chart-3: 30 80% 55%;
34
+ --chart-4: 280 65% 60%;
35
+ --chart-5: 340 75% 55%;
36
+ --sidebar-background: 0 0% 100%;
37
+ --sidebar-foreground: 222.2 84% 4.9%;
38
+ --sidebar-primary: 221.2 83.2% 53.3%;
39
+ --sidebar-primary-foreground: 210 40% 98%;
40
+ --sidebar-accent: 210 40% 96.1%;
41
+ --sidebar-accent-foreground: 222.2 47.4% 11.2%;
42
+ --sidebar-border: 214.3 31.8% 91.4%;
43
+ --sidebar-ring: 221.2 83.2% 53.3%;
44
+ }
45
+
46
+ .dark {
47
+ --background: 224 71.4% 4.1%;
48
+ --foreground: 210 20% 98%;
49
+ --card: 224 71.4% 4.1%;
50
+ --card-foreground: 210 20% 98%;
51
+ --popover: 224 71.4% 4.1%;
52
+ --popover-foreground: 210 20% 98%;
53
+ --primary: 217.2 91.2% 59.8%;
54
+ --primary-foreground: 222.2 47.4% 11.2%;
55
+ --secondary: 215 27.9% 16.9%;
56
+ --secondary-foreground: 210 20% 98%;
57
+ --muted: 215 27.9% 16.9%;
58
+ --muted-foreground: 217.9 10.6% 64.9%;
59
+ --accent: 215 27.9% 16.9%;
60
+ --accent-foreground: 210 20% 98%;
61
+ --destructive: 0 62.8% 30.6%;
62
+ --destructive-foreground: 210 20% 98%;
63
+ --border: 215 27.9% 16.9%;
64
+ --input: 215 27.9% 16.9%;
65
+ --ring: 216 12.2% 83.9%;
66
+ --warning: 38 92% 50%;
67
+ --warning-foreground: 48 96% 89%;
68
+ --success: 142 71% 45%;
69
+ --success-foreground: 144 80% 95%;
70
+ --chart-1: 217 91% 60%;
71
+ --chart-2: 160 60% 45%;
72
+ --chart-3: 30 80% 55%;
73
+ --chart-4: 280 65% 60%;
74
+ --chart-5: 340 75% 55%;
75
+ --sidebar-background: 224 71.4% 4.1%;
76
+ --sidebar-foreground: 210 20% 98%;
77
+ --sidebar-primary: 217.2 91.2% 59.8%;
78
+ --sidebar-primary-foreground: 222.2 47.4% 11.2%;
79
+ --sidebar-accent: 215 27.9% 16.9%;
80
+ --sidebar-accent-foreground: 210 20% 98%;
81
+ --sidebar-border: 215 27.9% 16.9%;
82
+ --sidebar-ring: 216 12.2% 83.9%;
83
+ }
84
+ }
85
+
86
+ @layer base {
87
+ * { @apply border-border; }
88
+ body { @apply bg-background text-foreground; }
89
+ }
@@ -0,0 +1,59 @@
1
+ import type { Metadata, Viewport } from 'next'
2
+ import { Inter } from 'next/font/google'
3
+ import './globals.css'
4
+ import { Toaster } from 'sonner'
5
+ import AuthProvider from '@/components/providers/session-provider'
6
+ import { ThemeProvider } from '@/components/providers/theme-provider'
7
+ import { ServiceWorkerRegister } from '@/components/pwa/sw-register'
8
+ import { InstallPrompt } from '@/components/pwa/install-prompt'
9
+
10
+ const inter = Inter({ subsets: ['latin'] })
11
+
12
+ export const metadata: Metadata = {
13
+ title: 'BEMMS',
14
+ description: 'Biomedical Equipment Management and Maintenance System',
15
+ manifest: '/manifest.webmanifest',
16
+ appleWebApp: {
17
+ capable: true,
18
+ statusBarStyle: 'default',
19
+ title: 'BEMMS',
20
+ },
21
+ icons: {
22
+ icon: '/icons/icon-192.png',
23
+ apple: '/icons/icon-192.png',
24
+ },
25
+ }
26
+
27
+ export const viewport: Viewport = {
28
+ width: 'device-width',
29
+ initialScale: 1,
30
+ maximumScale: 1,
31
+ viewportFit: 'cover',
32
+ themeColor: '#2563eb',
33
+ }
34
+
35
+ export default function RootLayout({
36
+ children,
37
+ }: {
38
+ children: React.ReactNode
39
+ }) {
40
+ return (
41
+ <html lang="en" suppressHydrationWarning>
42
+ <body className={inter.className} suppressHydrationWarning>
43
+ <ThemeProvider
44
+ attribute="class"
45
+ defaultTheme="system"
46
+ enableSystem
47
+ disableTransitionOnChange
48
+ >
49
+ <AuthProvider>
50
+ {children}
51
+ <Toaster richColors closeButton />
52
+ <ServiceWorkerRegister />
53
+ <InstallPrompt />
54
+ </AuthProvider>
55
+ </ThemeProvider>
56
+ </body>
57
+ </html>
58
+ )
59
+ }
@@ -0,0 +1,32 @@
1
+ import Link from 'next/link';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Card, CardContent } from '@/components/ui/card';
4
+ import { Activity, ArrowLeft } from 'lucide-react';
5
+
6
+ export default function NotFound() {
7
+ return (
8
+ <div className="min-h-screen flex items-center justify-center p-4 bg-background">
9
+ <Card className="max-w-md w-full text-center">
10
+ <CardContent className="pt-8 pb-8 space-y-4">
11
+ <div className="h-12 w-12 rounded-full bg-primary/10 text-primary flex items-center justify-center mx-auto">
12
+ <Activity className="h-6 w-6" />
13
+ </div>
14
+ <h1 className="text-3xl font-bold tracking-tight">404</h1>
15
+ <h2 className="text-lg font-semibold">Page Not Found</h2>
16
+ <p className="text-sm text-muted-foreground">
17
+ The requested page does not exist or has been moved.
18
+ </p>
19
+ <div className="pt-2">
20
+ <Button asChild className="min-h-[44px]">
21
+ <Link href="/dashboard" className="gap-2">
22
+ <ArrowLeft className="h-4 w-4" />
23
+ Back to Dashboard
24
+ </Link>
25
+ </Button>
26
+ </div>
27
+ </CardContent>
28
+ </Card>
29
+ </div>
30
+ );
31
+ }
32
+
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation'
2
+
3
+ export default function Home() {
4
+ redirect('/dashboard')
5
+ }