@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,415 @@
1
+ 'use server';
2
+
3
+ import { db } from '@/lib/db';
4
+ import {
5
+ maintenanceTasks,
6
+ serviceTickets,
7
+ devices,
8
+ checklistTemplateVersions,
9
+ maintenanceChecklistResults,
10
+ users
11
+ } from '@/lib/db/schema';
12
+ import { requireAuth, requireRole, requireScope } from '@/lib/auth/rbac';
13
+ import { createAuditLog } from '@/lib/audit';
14
+ import { eq, and, desc, inArray, ilike, or, count, getTableColumns, sql } from 'drizzle-orm';
15
+ import {
16
+ createMaintenanceTaskSchema,
17
+ assignMaintenanceTaskSchema,
18
+ updateMaintenanceTaskStageSchema,
19
+ completeMaintenanceTaskSchema,
20
+ cancelMaintenanceTaskSchema,
21
+ maintenanceTaskFilterSchema
22
+ } from '@/lib/validators/maintenance';
23
+ import { z } from 'zod';
24
+ import crypto from 'crypto';
25
+
26
+ function generateTaskNumber() {
27
+ const date = new Date();
28
+ const yearMonth = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}`;
29
+ const randomStr = crypto.randomBytes(3).toString('hex').toUpperCase();
30
+ return `TSK-${yearMonth}-${randomStr}`;
31
+ }
32
+
33
+ export async function createMaintenanceTask(input: z.infer<typeof createMaintenanceTaskSchema>) {
34
+ const user = await requireAuth();
35
+
36
+ try {
37
+ const data = createMaintenanceTaskSchema.parse(input);
38
+ await requireScope({ organizationId: data.organizationId, hospitalId: data.hospitalId });
39
+
40
+ const taskNumber = generateTaskNumber();
41
+
42
+ const [newTask] = await db.insert(maintenanceTasks).values({
43
+ taskNumber,
44
+ organizationId: data.organizationId,
45
+ hospitalId: data.hospitalId,
46
+ departmentId: data.departmentId,
47
+ locationId: data.locationId,
48
+ deviceId: data.deviceId,
49
+ serviceTicketId: data.serviceTicketId,
50
+ maintenanceScheduleOccurrenceId: data.maintenanceScheduleOccurrenceId,
51
+ maintenanceType: data.maintenanceType,
52
+ technicalProblemCategory: data.technicalProblemCategory,
53
+ title: data.title,
54
+ description: data.description,
55
+ assignedEngineerUserId: data.assignedEngineerUserId,
56
+ priorityCode: data.priorityCode,
57
+ dueDate: data.dueDate ? new Date(data.dueDate) : undefined,
58
+ checklistTemplateVersionId: data.checklistTemplateVersionId,
59
+ statusCode: data.assignedEngineerUserId ? 'assigned' : 'draft',
60
+ createdByUserId: user.id,
61
+ updatedByUserId: user.id,
62
+ }).returning();
63
+
64
+ await createAuditLog({
65
+ actionType: 'CREATE',
66
+ entityType: 'MAINTENANCE_TASK',
67
+ entityId: newTask.id,
68
+ organizationId: newTask.organizationId,
69
+ hospitalId: newTask.hospitalId,
70
+ departmentId: newTask.departmentId,
71
+ userId: user.id,
72
+ details: { taskNumber, maintenanceType: data.maintenanceType }
73
+ });
74
+
75
+ return { success: true, data: newTask };
76
+ } catch (error: any) {
77
+ console.error('Failed to create maintenance task:', error);
78
+ return { success: false, error: error.message };
79
+ }
80
+ }
81
+
82
+ export async function getMaintenanceTaskById(id: string) {
83
+ const user = await requireAuth();
84
+
85
+ try {
86
+ const task = await db.query.maintenanceTasks.findFirst({
87
+ where: eq(maintenanceTasks.id, id),
88
+ with: {
89
+ device: true,
90
+ hospital: true,
91
+ department: true,
92
+ assignedEngineer: true,
93
+ serviceTicket: true,
94
+ checklistTemplateVersion: true,
95
+ checklistResults: true,
96
+ createdByUser: true,
97
+ }
98
+ });
99
+
100
+ if (!task) return { success: false, error: 'Maintenance task not found' };
101
+
102
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
103
+
104
+ return { success: true, data: task };
105
+ } catch (error: any) {
106
+ return { success: false, error: error.message };
107
+ }
108
+ }
109
+
110
+ export async function listMaintenanceTasks(input?: z.infer<typeof maintenanceTaskFilterSchema>) {
111
+ const user = await requireAuth();
112
+
113
+ try {
114
+ const filters = maintenanceTaskFilterSchema.parse(input || { page: 1, pageSize: 20 });
115
+ const conditions = [];
116
+
117
+ // Basic role scoping - users should only see tasks in their org/hospital
118
+ if (!user.roles.includes('SYS_ADMIN')) {
119
+ if (user.organizationId) {
120
+ conditions.push(eq(maintenanceTasks.organizationId, user.organizationId));
121
+ }
122
+ }
123
+
124
+ if (filters.hospitalId) conditions.push(eq(maintenanceTasks.hospitalId, filters.hospitalId));
125
+ if (filters.departmentId) conditions.push(eq(maintenanceTasks.departmentId, filters.departmentId));
126
+ if (filters.deviceId) conditions.push(eq(maintenanceTasks.deviceId, filters.deviceId));
127
+ if (filters.statusCode) conditions.push(eq(maintenanceTasks.statusCode, filters.statusCode as any));
128
+ if (filters.maintenanceType) conditions.push(eq(maintenanceTasks.maintenanceType, filters.maintenanceType as any));
129
+ if (filters.assignedEngineerUserId) conditions.push(eq(maintenanceTasks.assignedEngineerUserId, filters.assignedEngineerUserId));
130
+
131
+ if (filters.search) {
132
+ conditions.push(
133
+ or(
134
+ ilike(maintenanceTasks.taskNumber, `%${filters.search}%`),
135
+ ilike(maintenanceTasks.title, `%${filters.search}%`)
136
+ )
137
+ );
138
+ }
139
+
140
+ const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
141
+
142
+ const offset = (filters.page - 1) * filters.pageSize;
143
+
144
+ const [totalCount, data] = await Promise.all([
145
+ db.select({ count: count() }).from(maintenanceTasks).where(whereClause),
146
+ db.query.maintenanceTasks.findMany({
147
+ where: whereClause,
148
+ orderBy: [desc(maintenanceTasks.createdAt)],
149
+ limit: filters.pageSize,
150
+ offset: offset,
151
+ with: {
152
+ device: { columns: { name: true, assetNumber: true } },
153
+ assignedEngineer: { columns: { fullName: true } }
154
+ }
155
+ })
156
+ ]);
157
+
158
+ return {
159
+ success: true,
160
+ data,
161
+ meta: {
162
+ total: totalCount[0].count,
163
+ page: filters.page,
164
+ pageSize: filters.pageSize,
165
+ totalPages: Math.ceil(totalCount[0].count / filters.pageSize)
166
+ }
167
+ };
168
+ } catch (error: any) {
169
+ return { success: false, error: error.message };
170
+ }
171
+ }
172
+
173
+ export async function assignMaintenanceTask(id: string, input: z.infer<typeof assignMaintenanceTaskSchema>) {
174
+ const user = await requireAuth();
175
+
176
+ try {
177
+ const data = assignMaintenanceTaskSchema.parse(input);
178
+
179
+ const task = await db.query.maintenanceTasks.findFirst({
180
+ where: eq(maintenanceTasks.id, id),
181
+ });
182
+
183
+ if (!task) return { success: false, error: 'Task not found' };
184
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
185
+
186
+ const validStates = ['draft', 'assigned'];
187
+ if (!validStates.includes(task.statusCode)) {
188
+ return { success: false, error: `Cannot assign task in ${task.statusCode} state` };
189
+ }
190
+
191
+ const [updated] = await db.update(maintenanceTasks).set({
192
+ assignedEngineerUserId: data.assignedEngineerUserId,
193
+ statusCode: 'assigned',
194
+ updatedAt: new Date(),
195
+ updatedByUserId: user.id
196
+ }).where(eq(maintenanceTasks.id, id)).returning();
197
+
198
+ await createAuditLog({
199
+ actionType: 'ASSIGN',
200
+ entityType: 'MAINTENANCE_TASK',
201
+ entityId: id,
202
+ organizationId: task.organizationId,
203
+ userId: user.id,
204
+ details: { assignedTo: data.assignedEngineerUserId, handoffNote: data.handoffNote }
205
+ });
206
+
207
+ return { success: true, data: updated };
208
+ } catch (error: any) {
209
+ return { success: false, error: error.message };
210
+ }
211
+ }
212
+
213
+ export async function startMaintenanceTask(id: string) {
214
+ const user = await requireAuth();
215
+
216
+ try {
217
+ const task = await db.query.maintenanceTasks.findFirst({
218
+ where: eq(maintenanceTasks.id, id),
219
+ });
220
+
221
+ if (!task) return { success: false, error: 'Task not found' };
222
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
223
+
224
+ const validStates = ['assigned', 'waiting_dependency'];
225
+ if (!validStates.includes(task.statusCode)) {
226
+ return { success: false, error: `Cannot start task from ${task.statusCode} state` };
227
+ }
228
+
229
+ const updates: any = {
230
+ statusCode: 'in_progress',
231
+ updatedAt: new Date(),
232
+ updatedByUserId: user.id
233
+ };
234
+
235
+ if (!task.startedAt) {
236
+ updates.startedAt = new Date();
237
+ }
238
+
239
+ const [updated] = await db.update(maintenanceTasks)
240
+ .set(updates)
241
+ .where(eq(maintenanceTasks.id, id))
242
+ .returning();
243
+
244
+ await createAuditLog({
245
+ actionType: 'UPDATE',
246
+ entityType: 'MAINTENANCE_TASK',
247
+ entityId: id,
248
+ organizationId: task.organizationId,
249
+ userId: user.id,
250
+ details: { action: 'start_work', previousState: task.statusCode }
251
+ });
252
+
253
+ return { success: true, data: updated };
254
+ } catch (error: any) {
255
+ return { success: false, error: error.message };
256
+ }
257
+ }
258
+
259
+ export async function markTaskWaiting(id: string, input: z.infer<typeof updateMaintenanceTaskStageSchema>) {
260
+ const user = await requireAuth();
261
+
262
+ try {
263
+ const data = updateMaintenanceTaskStageSchema.parse(input);
264
+ const task = await db.query.maintenanceTasks.findFirst({
265
+ where: eq(maintenanceTasks.id, id),
266
+ });
267
+
268
+ if (!task) return { success: false, error: 'Task not found' };
269
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
270
+
271
+ if (task.statusCode !== 'in_progress') {
272
+ return { success: false, error: `Cannot pause task from ${task.statusCode} state` };
273
+ }
274
+
275
+ const [updated] = await db.update(maintenanceTasks).set({
276
+ statusCode: 'waiting_dependency',
277
+ waitingDependencyType: data.waitingDependencyType,
278
+ waitingReason: data.waitingReason,
279
+ updatedAt: new Date(),
280
+ updatedByUserId: user.id
281
+ }).where(eq(maintenanceTasks.id, id)).returning();
282
+
283
+ await createAuditLog({
284
+ actionType: 'UPDATE',
285
+ entityType: 'MAINTENANCE_TASK',
286
+ entityId: id,
287
+ organizationId: task.organizationId,
288
+ userId: user.id,
289
+ details: { action: 'mark_waiting', reason: data.waitingReason, type: data.waitingDependencyType }
290
+ });
291
+
292
+ return { success: true, data: updated };
293
+ } catch (error: any) {
294
+ return { success: false, error: error.message };
295
+ }
296
+ }
297
+
298
+ export async function completeTaskWork(id: string, input: z.infer<typeof completeMaintenanceTaskSchema>) {
299
+ const user = await requireAuth();
300
+
301
+ try {
302
+ const data = completeMaintenanceTaskSchema.parse(input);
303
+ const task = await db.query.maintenanceTasks.findFirst({
304
+ where: eq(maintenanceTasks.id, id),
305
+ with: { checklistResults: true }
306
+ });
307
+
308
+ if (!task) return { success: false, error: 'Task not found' };
309
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
310
+
311
+ const validStates = ['in_progress', 'returned_for_rework'];
312
+ if (!validStates.includes(task.statusCode)) {
313
+ return { success: false, error: `Cannot complete work from ${task.statusCode} state` };
314
+ }
315
+
316
+ // TODO: Verify checklist completion logic here (if checklist is attached, verify all required are answered)
317
+
318
+ const [updated] = await db.update(maintenanceTasks).set({
319
+ statusCode: 'work_complete',
320
+ completedAt: new Date(),
321
+ updatedAt: new Date(),
322
+ updatedByUserId: user.id
323
+ }).where(eq(maintenanceTasks.id, id)).returning();
324
+
325
+ await createAuditLog({
326
+ actionType: 'UPDATE',
327
+ entityType: 'MAINTENANCE_TASK',
328
+ entityId: id,
329
+ organizationId: task.organizationId,
330
+ userId: user.id,
331
+ details: { action: 'work_complete', finalResultCode: data.finalResultCode }
332
+ });
333
+
334
+ return { success: true, data: updated };
335
+ } catch (error: any) {
336
+ return { success: false, error: error.message };
337
+ }
338
+ }
339
+
340
+ export async function cancelMaintenanceTask(id: string, input: z.infer<typeof cancelMaintenanceTaskSchema>) {
341
+ const user = await requireAuth();
342
+
343
+ try {
344
+ const data = cancelMaintenanceTaskSchema.parse(input);
345
+ const task = await db.query.maintenanceTasks.findFirst({
346
+ where: eq(maintenanceTasks.id, id),
347
+ });
348
+
349
+ if (!task) return { success: false, error: 'Task not found' };
350
+ await requireScope({ organizationId: task.organizationId, hospitalId: task.hospitalId });
351
+
352
+ const activeStates = ['draft', 'assigned', 'in_progress', 'waiting_dependency'];
353
+ if (!activeStates.includes(task.statusCode)) {
354
+ return { success: false, error: `Cannot cancel task in ${task.statusCode} state` };
355
+ }
356
+
357
+ const [updated] = await db.update(maintenanceTasks).set({
358
+ statusCode: 'cancelled',
359
+ updatedAt: new Date(),
360
+ updatedByUserId: user.id
361
+ }).where(eq(maintenanceTasks.id, id)).returning();
362
+
363
+ await createAuditLog({
364
+ actionType: 'UPDATE',
365
+ entityType: 'MAINTENANCE_TASK',
366
+ entityId: id,
367
+ organizationId: task.organizationId,
368
+ userId: user.id,
369
+ details: { action: 'cancel', reason: data.cancellationReason }
370
+ });
371
+
372
+ return { success: true, data: updated };
373
+ } catch (error: any) {
374
+ return { success: false, error: error.message };
375
+ }
376
+ }
377
+
378
+ export async function createMaintenanceTaskFromTicket(ticketId: string, input: z.infer<typeof createMaintenanceTaskSchema>) {
379
+ const user = await requireAuth();
380
+
381
+ try {
382
+ const ticket = await db.query.serviceTickets.findFirst({
383
+ where: eq(serviceTickets.id, ticketId)
384
+ });
385
+
386
+ if (!ticket) return { success: false, error: 'Service ticket not found' };
387
+ await requireScope({ organizationId: ticket.organizationId, hospitalId: ticket.hospitalId });
388
+
389
+ // Override some fields with ticket context
390
+ const dataToCreate = {
391
+ ...input,
392
+ organizationId: ticket.organizationId,
393
+ hospitalId: ticket.hospitalId,
394
+ departmentId: ticket.departmentId || undefined,
395
+ deviceId: ticket.deviceId!,
396
+ serviceTicketId: ticket.id,
397
+ };
398
+
399
+ const result = await createMaintenanceTask(dataToCreate);
400
+
401
+ if (result.success) {
402
+ // Transition ticket to in_progress if it isn't already closed/resolved
403
+ if (!['resolved', 'closed', 'cancelled'].includes(ticket.statusCode)) {
404
+ await db.update(serviceTickets)
405
+ .set({ statusCode: 'in_progress', updatedAt: new Date() })
406
+ .where(eq(serviceTickets.id, ticket.id));
407
+ }
408
+ }
409
+
410
+ return result;
411
+ } catch (error: any) {
412
+ return { success: false, error: error.message };
413
+ }
414
+ }
415
+
@@ -0,0 +1,132 @@
1
+ 'use server';
2
+
3
+ import { db } from '@/lib/db';
4
+ import { manufacturers } from '@/lib/db/schema';
5
+ import { requireAuth, requireRole } from '@/lib/auth/rbac';
6
+ import { createAuditLog } from '@/lib/audit';
7
+ import { createManufacturerSchema, updateManufacturerSchema } from '@/lib/validators/devices';
8
+ import { DEFAULT_MANUFACTURERS } from '@/lib/constants/manufacturers';
9
+ import { eq, desc, asc } from 'drizzle-orm';
10
+
11
+ export async function getManufacturers(organizationId?: string) {
12
+ const session = await requireAuth();
13
+ try {
14
+ const orgId = organizationId || session.organizationId;
15
+ let data = await db.query.manufacturers.findMany({
16
+ where: orgId ? eq(manufacturers.organizationId, orgId) : undefined,
17
+ orderBy: [asc(manufacturers.name)],
18
+ });
19
+
20
+ // Auto-seed default global manufacturers if empty for this organization
21
+ if (data.length === 0 && orgId) {
22
+ for (const mfr of DEFAULT_MANUFACTURERS) {
23
+ await db.insert(manufacturers).values({
24
+ organizationId: orgId,
25
+ name: mfr.name,
26
+ code: mfr.code,
27
+ country: mfr.country,
28
+ website: mfr.website,
29
+ createdByUserId: session.id,
30
+ });
31
+ }
32
+ data = await db.query.manufacturers.findMany({
33
+ where: eq(manufacturers.organizationId, orgId),
34
+ orderBy: [asc(manufacturers.name)],
35
+ });
36
+ }
37
+
38
+ return { success: true, data };
39
+ } catch (error: any) {
40
+ return { success: false, error: error.message };
41
+ }
42
+ }
43
+
44
+ export async function getManufacturerById(id: string) {
45
+ await requireAuth();
46
+ try {
47
+ const data = await db.query.manufacturers.findFirst({
48
+ where: eq(manufacturers.id, id),
49
+ });
50
+ return { success: true, data };
51
+ } catch (error: any) {
52
+ return { success: false, error: error.message };
53
+ }
54
+ }
55
+
56
+ export async function createManufacturer(input: unknown) {
57
+ const session = await requireAuth();
58
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN', 'BIOMED_MGR', 'BIOMED_ENG');
59
+
60
+ let orgId = (input as any)?.organizationId || session.organizationId;
61
+ if (!orgId) {
62
+ const firstOrg = await db.query.organizations.findFirst();
63
+ orgId = firstOrg?.id;
64
+ }
65
+
66
+ const payload = typeof input === 'object' && input !== null
67
+ ? { organizationId: orgId, ...input }
68
+ : input;
69
+
70
+ const validated = createManufacturerSchema.safeParse(payload);
71
+ if (!validated.success) return { success: false, error: validated.error.message };
72
+
73
+ try {
74
+ return await db.transaction(async (tx) => {
75
+ const [newMfr] = await tx.insert(manufacturers).values({
76
+ ...validated.data,
77
+ createdByUserId: session.id,
78
+ }).returning();
79
+ await createAuditLog(tx, {
80
+ action: 'CREATE', entityType: 'manufacturer', entityId: newMfr.id,
81
+ actorId: session.id, details: validated.data,
82
+ });
83
+ return { success: true, data: newMfr };
84
+ });
85
+ } catch (error: any) {
86
+ return { success: false, error: error.message };
87
+ }
88
+ }
89
+
90
+ export async function updateManufacturer(id: string, input: unknown) {
91
+ const session = await requireAuth();
92
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN', 'BIOMED_MGR');
93
+ const validated = updateManufacturerSchema.safeParse(input);
94
+ if (!validated.success) return { success: false, error: validated.error.message };
95
+
96
+ try {
97
+ return await db.transaction(async (tx) => {
98
+ const [updated] = await tx.update(manufacturers)
99
+ .set(validated.data)
100
+ .where(eq(manufacturers.id, id))
101
+ .returning();
102
+ await createAuditLog(tx, {
103
+ action: 'UPDATE', entityType: 'manufacturer', entityId: id,
104
+ actorId: session.id, details: validated.data,
105
+ });
106
+ return { success: true, data: updated };
107
+ });
108
+ } catch (error: any) {
109
+ return { success: false, error: error.message };
110
+ }
111
+ }
112
+
113
+ export async function archiveManufacturer(id: string) {
114
+ const session = await requireAuth();
115
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN', 'BIOMED_MGR');
116
+ try {
117
+ return await db.transaction(async (tx) => {
118
+ const [archived] = await tx.update(manufacturers)
119
+ .set({ status: 'archived', archivedAt: new Date() })
120
+ .where(eq(manufacturers.id, id))
121
+ .returning();
122
+ await createAuditLog(tx, {
123
+ action: 'ARCHIVE', entityType: 'manufacturer', entityId: id,
124
+ actorId: session.id, details: { status: 'archived' },
125
+ });
126
+ return { success: true, data: archived };
127
+ });
128
+ } catch (error: any) {
129
+ return { success: false, error: error.message };
130
+ }
131
+ }
132
+
@@ -0,0 +1,144 @@
1
+ 'use server';
2
+
3
+ import { db } from '@/lib/db';
4
+ import { requireAuth } from '@/lib/auth/rbac';
5
+ import {
6
+ notifications,
7
+ serviceTickets,
8
+ maintenanceTasks,
9
+ maintenanceScheduleOccurrences,
10
+ maintenancePlans,
11
+ } from '@/lib/db/schema';
12
+ import { eq, and, isNull, inArray, notInArray, sql } from 'drizzle-orm';
13
+
14
+ export interface NavBadgeCounts {
15
+ notifications: number;
16
+ triage: number;
17
+ tickets: number;
18
+ maintenance: number;
19
+ workOrders: number;
20
+ pmPlans: number;
21
+ hasMaintenanceDue: boolean;
22
+ }
23
+
24
+ const DEFAULT_COUNTS: NavBadgeCounts = {
25
+ notifications: 0,
26
+ triage: 0,
27
+ tickets: 0,
28
+ maintenance: 0,
29
+ workOrders: 0,
30
+ pmPlans: 0,
31
+ hasMaintenanceDue: false,
32
+ };
33
+
34
+ /**
35
+ * Fetch dynamic navigation badge counts for the current user and their scope.
36
+ */
37
+ export async function getNavBadgeCounts(): Promise<NavBadgeCounts> {
38
+ try {
39
+ const user = await requireAuth();
40
+ const userId = user.id;
41
+ const orgId = user.organizationId;
42
+ const roles = user.roles || [];
43
+
44
+ const isStaffOnly =
45
+ roles.some((r) => ['STAFF', 'DEPT_MGR'].includes(r)) &&
46
+ !roles.some((r) =>
47
+ ['BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR', 'SYS_ADMIN', 'ORG_ADMIN', 'HOSP_ADMIN'].includes(r)
48
+ );
49
+
50
+ const isBmeOrAdmin = roles.some((r) =>
51
+ ['BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR', 'SYS_ADMIN', 'ORG_ADMIN', 'HOSP_ADMIN'].includes(r)
52
+ );
53
+
54
+ // 1. Unread notifications for this user
55
+ const [unreadNotifResult] = await db
56
+ .select({ count: sql<number>`count(*)::int` })
57
+ .from(notifications)
58
+ .where(and(eq(notifications.recipientUserId, userId), isNull(notifications.readAt)));
59
+
60
+ const notificationsCount = unreadNotifResult?.count || 0;
61
+
62
+ let triageCount = 0;
63
+ let ticketsCount = 0;
64
+ let workOrdersCount = 0;
65
+ let pmDueCount = 0;
66
+
67
+ if (isStaffOnly) {
68
+ // Staff: open tickets reported by this user
69
+ const [staffTicketsResult] = await db
70
+ .select({ count: sql<number>`count(*)::int` })
71
+ .from(serviceTickets)
72
+ .where(
73
+ and(
74
+ eq(serviceTickets.reportedByUserId, userId),
75
+ notInArray(serviceTickets.statusCode, ['resolved', 'closed', 'cancelled'])
76
+ )
77
+ );
78
+ ticketsCount = staffTicketsResult?.count || 0;
79
+ } else if (isBmeOrAdmin && orgId) {
80
+ // BME / Admin: Triage queue tickets (new, acknowledged, in_triage)
81
+ const [triageResult] = await db
82
+ .select({ count: sql<number>`count(*)::int` })
83
+ .from(serviceTickets)
84
+ .where(
85
+ and(
86
+ eq(serviceTickets.organizationId, orgId),
87
+ inArray(serviceTickets.statusCode, ['new', 'acknowledged', 'in_triage'])
88
+ )
89
+ );
90
+ triageCount = triageResult?.count || 0;
91
+
92
+ // Active helpdesk tickets in the organization
93
+ const [orgTicketsResult] = await db
94
+ .select({ count: sql<number>`count(*)::int` })
95
+ .from(serviceTickets)
96
+ .where(
97
+ and(
98
+ eq(serviceTickets.organizationId, orgId),
99
+ notInArray(serviceTickets.statusCode, ['resolved', 'closed', 'cancelled'])
100
+ )
101
+ );
102
+ ticketsCount = orgTicketsResult?.count || 0;
103
+
104
+ // Maintenance Work orders active / pending
105
+ const [tasksResult] = await db
106
+ .select({ count: sql<number>`count(*)::int` })
107
+ .from(maintenanceTasks)
108
+ .where(
109
+ and(
110
+ eq(maintenanceTasks.organizationId, orgId),
111
+ inArray(maintenanceTasks.statusCode, [
112
+ 'assigned',
113
+ 'in_progress',
114
+ 'waiting_dependency',
115
+ ])
116
+ )
117
+ );
118
+ workOrdersCount = tasksResult?.count || 0;
119
+
120
+ // PM schedule occurrences overdue or due today
121
+ const pmResult = await db.execute(sql`
122
+ SELECT COUNT(*)::int as count
123
+ FROM maintenance_schedule_occurrences mso
124
+ JOIN maintenance_plans mp ON mso.maintenance_plan_id = mp.id
125
+ WHERE mp.organization_id = ${orgId}
126
+ AND mso.due_state IN ('due_today', 'overdue')
127
+ `);
128
+ pmDueCount = Number((pmResult.rows[0] as any)?.count) || 0;
129
+ }
130
+
131
+ return {
132
+ notifications: notificationsCount,
133
+ triage: triageCount,
134
+ tickets: ticketsCount,
135
+ maintenance: pmDueCount + workOrdersCount,
136
+ workOrders: workOrdersCount,
137
+ pmPlans: pmDueCount,
138
+ hasMaintenanceDue: pmDueCount > 0 || workOrdersCount > 0,
139
+ };
140
+ } catch (error) {
141
+ console.error('[getNavBadgeCounts] Failed to fetch badge counts:', error);
142
+ return DEFAULT_COUNTS;
143
+ }
144
+ }