@omersx/bemms 0.0.0-stage → 0.1.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,740 @@
1
+ 'use server';
2
+
3
+ import { db } from '@/lib/db';
4
+ import { users, userRoleAssignments, userAccessScopes, serviceTickets, engineerProfiles, roles, hospitals, departments } from '@/lib/db/schema';
5
+ import { requireAuth, requireRole, requireScope } from '@/lib/auth/rbac';
6
+ import { createAuditLog } from '@/lib/audit';
7
+ import { hashPassword } from '@/lib/auth/password';
8
+ import { inviteUserSchema, updateUserProfileSchema, assignRoleSchema, assignScopeSchema } from '@/lib/validators/users';
9
+ import { eq, and, sql, not, inArray } from 'drizzle-orm';
10
+ import { revalidatePath } from 'next/cache';
11
+
12
+ export async function getUsers(filters?: { organizationId?: string }) {
13
+ await requireAuth();
14
+ try {
15
+ const whereClause = filters?.organizationId ? eq(users.organizationId, filters.organizationId) : undefined;
16
+ const usersList = await db.query.users.findMany({
17
+ where: whereClause,
18
+ orderBy: [users.createdAt],
19
+ });
20
+
21
+ // Query active role assignments for all users
22
+ const assignments = await db
23
+ .select({
24
+ userId: userRoleAssignments.userId,
25
+ roleId: roles.id,
26
+ roleCode: roles.code,
27
+ roleName: roles.name,
28
+ })
29
+ .from(userRoleAssignments)
30
+ .innerJoin(roles, eq(userRoleAssignments.roleId, roles.id))
31
+ .where(eq(userRoleAssignments.status, 'active'));
32
+
33
+ const rolesByUserId = new Map<string, { id: string; code: string; name: string }[]>();
34
+ for (const a of assignments) {
35
+ const existing = rolesByUserId.get(a.userId) || [];
36
+ existing.push({ id: a.roleId, code: a.roleCode, name: a.roleName });
37
+ rolesByUserId.set(a.userId, existing);
38
+ }
39
+
40
+ // Query access scopes for all users with hospital and department info
41
+ const scopes = await db
42
+ .select({
43
+ userId: userAccessScopes.userId,
44
+ scopeType: userAccessScopes.scopeType,
45
+ hospitalId: userAccessScopes.hospitalId,
46
+ hospitalName: hospitals.name,
47
+ departmentId: userAccessScopes.departmentId,
48
+ departmentName: departments.name,
49
+ })
50
+ .from(userAccessScopes)
51
+ .leftJoin(hospitals, eq(userAccessScopes.hospitalId, hospitals.id))
52
+ .leftJoin(departments, eq(userAccessScopes.departmentId, departments.id));
53
+
54
+ const scopesByUserId = new Map<string, typeof scopes>();
55
+ for (const s of scopes) {
56
+ const existing = scopesByUserId.get(s.userId) || [];
57
+ existing.push(s);
58
+ scopesByUserId.set(s.userId, existing);
59
+ }
60
+
61
+ const data = usersList.map((u) => {
62
+ const userRoles = rolesByUserId.get(u.id) || [];
63
+ const userScopes = scopesByUserId.get(u.id) || [];
64
+ const primaryScope = userScopes.find((s) => s.departmentName) || userScopes[0] || null;
65
+
66
+ const roleCodes = userRoles.map((r) => r.code);
67
+ const isEngineer = roleCodes.some((code) =>
68
+ ['BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR'].includes(code)
69
+ );
70
+ const isStaff = roleCodes.some((code) =>
71
+ ['STAFF', 'DEPT_MGR'].includes(code)
72
+ );
73
+ const isAdmin = roleCodes.some((code) =>
74
+ ['SYS_ADMIN', 'ORG_ADMIN', 'HOSP_ADMIN'].includes(code)
75
+ );
76
+
77
+ let userType: 'engineer' | 'department' | 'admin' | 'other' = 'other';
78
+ if (isEngineer) userType = 'engineer';
79
+ else if (isStaff) userType = 'department';
80
+ else if (isAdmin) userType = 'admin';
81
+
82
+ return {
83
+ ...u,
84
+ roles: userRoles,
85
+ scopes: userScopes,
86
+ departmentName: primaryScope?.departmentName || null,
87
+ hospitalName: primaryScope?.hospitalName || null,
88
+ userType,
89
+ };
90
+ });
91
+
92
+ return { success: true, data };
93
+ } catch (error: any) {
94
+ return { success: false, error: error.message };
95
+ }
96
+ }
97
+
98
+ export async function getUserById(id: string) {
99
+ await requireAuth();
100
+ try {
101
+ const data = await db.query.users.findFirst({
102
+ where: eq(users.id, id),
103
+ with: {
104
+ organization: true,
105
+ },
106
+ });
107
+ if (!data) return { success: false, error: "User not found" };
108
+
109
+ const roleAssignments = await db.query.userRoleAssignments.findMany({
110
+ where: eq(userRoleAssignments.userId, id),
111
+ with: {
112
+ role: true,
113
+ },
114
+ });
115
+
116
+ const accessScopes = await db.query.userAccessScopes.findMany({
117
+ where: eq(userAccessScopes.userId, id),
118
+ with: {
119
+ hospital: true,
120
+ department: true,
121
+ location: true,
122
+ },
123
+ });
124
+
125
+ const engineerProfile = await db.query.engineerProfiles.findFirst({
126
+ where: eq(engineerProfiles.userId, id),
127
+ });
128
+
129
+ return {
130
+ success: true,
131
+ data: {
132
+ ...data,
133
+ roleAssignments,
134
+ roles: roleAssignments.map((ra) => ({
135
+ ...ra.role,
136
+ assignmentId: ra.id,
137
+ effectiveFrom: ra.effectiveFrom,
138
+ effectiveTo: ra.effectiveTo,
139
+ })),
140
+ accessScopes,
141
+ engineerProfile: engineerProfile || null,
142
+ },
143
+ };
144
+ } catch (error: any) {
145
+ return { success: false, error: error.message };
146
+ }
147
+ }
148
+
149
+ export async function inviteUser(input: unknown) {
150
+ const session = await requireAuth();
151
+ const validated = inviteUserSchema.safeParse(input);
152
+ if (!validated.success) return { success: false, error: validated.error.message };
153
+
154
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
155
+ await requireScope({ organizationId: validated.data.organizationId });
156
+
157
+ try {
158
+ return await db.transaction(async (tx) => {
159
+ // Check if email already exists
160
+ const normalizedEmail = validated.data.email.toLowerCase().trim();
161
+ const existingEmail = await tx.query.users.findFirst({
162
+ where: eq(users.email, normalizedEmail),
163
+ });
164
+ if (existingEmail) {
165
+ return { success: false, error: `A user with email "${validated.data.email}" already exists.` };
166
+ }
167
+
168
+ // Check if username already exists if provided
169
+ const finalUsername = (validated.data.username || validated.data.employeeIdentifier)?.trim() || null;
170
+ if (finalUsername) {
171
+ const existingUsername = await tx.query.users.findFirst({
172
+ where: eq(users.employeeIdentifier, finalUsername),
173
+ });
174
+ if (existingUsername) {
175
+ return { success: false, error: `Username "${finalUsername}" is already in use.` };
176
+ }
177
+ }
178
+
179
+ const passwordHash = validated.data.password
180
+ ? await hashPassword(validated.data.password)
181
+ : null;
182
+ const accountStatus = passwordHash ? 'active' : 'invited';
183
+
184
+ const [newUser] = await tx.insert(users).values({
185
+ email: normalizedEmail,
186
+ fullName: validated.data.fullName.trim(),
187
+ organizationId: validated.data.organizationId,
188
+ jobTitle: validated.data.jobTitle?.trim() || null,
189
+ employeeIdentifier: finalUsername,
190
+ phone: validated.data.phone?.trim() || null,
191
+ passwordHash,
192
+ accountStatus,
193
+ }).returning();
194
+
195
+ for (const roleId of validated.data.roleIds) {
196
+ await tx.insert(userRoleAssignments).values({
197
+ userId: newUser.id,
198
+ roleId,
199
+ assignedByUserId: session.id,
200
+ });
201
+ }
202
+
203
+ // 1. Assign Hospital and Department Access Scopes if provided
204
+ if (validated.data.hospitalId || validated.data.departmentId) {
205
+ await tx.insert(userAccessScopes).values({
206
+ userId: newUser.id,
207
+ organizationId: validated.data.organizationId,
208
+ scopeType: validated.data.departmentId ? 'department' : 'hospital',
209
+ hospitalId: validated.data.hospitalId || null,
210
+ departmentId: validated.data.departmentId || null,
211
+ createdByUserId: session.id,
212
+ });
213
+ }
214
+
215
+ // 2. Automatically create Engineer Profile if an engineering role is assigned
216
+ const assignedRoles = await tx.query.roles.findMany({
217
+ where: inArray(roles.id, validated.data.roleIds),
218
+ });
219
+ const roleCodes = assignedRoles.map((r) => r.code);
220
+ const isEngineer = roleCodes.some((code) =>
221
+ ['BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR'].includes(code)
222
+ );
223
+
224
+ if (isEngineer) {
225
+ await tx.insert(engineerProfiles).values({
226
+ userId: newUser.id,
227
+ organizationId: validated.data.organizationId,
228
+ hospitalId: validated.data.hospitalId || null,
229
+ jobTitle: validated.data.jobTitle || 'Biomedical Engineer',
230
+ specialization: validated.data.specialization || 'General Biomedical Engineering',
231
+ extension: validated.data.employeeIdentifier || null,
232
+ mobileNumber: validated.data.phone || null,
233
+ dutyStatus: validated.data.dutyStatus || 'on_duty',
234
+ coveredDepartmentIds: validated.data.departmentId ? [validated.data.departmentId] : [],
235
+ });
236
+ }
237
+
238
+ // 3. Link Department Manager if DEPT_MGR role and departmentId provided
239
+ if (roleCodes.includes('DEPT_MGR') && validated.data.departmentId) {
240
+ await tx
241
+ .update(departments)
242
+ .set({ managerUserId: newUser.id, updatedAt: new Date() })
243
+ .where(eq(departments.id, validated.data.departmentId));
244
+ }
245
+
246
+ await createAuditLog(tx, {
247
+ action: 'INVITE',
248
+ entityType: 'user',
249
+ entityId: newUser.id,
250
+ actorId: session.id,
251
+ details: validated.data,
252
+ });
253
+
254
+ revalidatePath('/admin/users');
255
+ revalidatePath('/team');
256
+ revalidatePath('/departments');
257
+ if (validated.data.departmentId) {
258
+ revalidatePath(`/departments/${validated.data.departmentId}`);
259
+ }
260
+ return { success: true, data: newUser };
261
+ });
262
+ } catch (error: any) {
263
+ return { success: false, error: error.message };
264
+ }
265
+ }
266
+
267
+ export const addUser = inviteUser;
268
+
269
+ export async function updateUserProfile(id: string, input: unknown) {
270
+ const session = await requireAuth();
271
+ const validated = updateUserProfileSchema.safeParse(input);
272
+ if (!validated.success) return { success: false, error: validated.error.message };
273
+
274
+ if (session.id !== id) {
275
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
276
+ }
277
+
278
+ try {
279
+ return await db.transaction(async (tx) => {
280
+ const [updated] = await tx.update(users).set(validated.data).where(eq(users.id, id)).returning();
281
+ await createAuditLog(tx, {
282
+ action: 'UPDATE',
283
+ entityType: 'user',
284
+ entityId: id,
285
+ actorId: session.id,
286
+ details: validated.data,
287
+ });
288
+ return { success: true, data: updated };
289
+ });
290
+ } catch (error: any) {
291
+ return { success: false, error: error.message };
292
+ }
293
+ }
294
+
295
+ export async function assignUserRole(input: unknown) {
296
+ const session = await requireAuth();
297
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
298
+ const validated = assignRoleSchema.safeParse(input);
299
+ if (!validated.success) return { success: false, error: validated.error.message };
300
+
301
+ try {
302
+ const result = await db.transaction(async (tx) => {
303
+ // Check if assignment already exists
304
+ const existing = await tx.query.userRoleAssignments.findFirst({
305
+ where: and(
306
+ eq(userRoleAssignments.userId, validated.data.userId),
307
+ eq(userRoleAssignments.roleId, validated.data.roleId),
308
+ ),
309
+ });
310
+
311
+ let assigned;
312
+ if (existing) {
313
+ if (existing.status !== 'active') {
314
+ const [updated] = await tx
315
+ .update(userRoleAssignments)
316
+ .set({
317
+ status: 'active',
318
+ effectiveFrom: validated.data.effectiveFrom || new Date(),
319
+ effectiveTo: validated.data.effectiveTo,
320
+ })
321
+ .where(eq(userRoleAssignments.id, existing.id))
322
+ .returning();
323
+ assigned = updated;
324
+ } else {
325
+ assigned = existing;
326
+ }
327
+ } else {
328
+ const [inserted] = await tx.insert(userRoleAssignments).values({
329
+ userId: validated.data.userId,
330
+ roleId: validated.data.roleId,
331
+ assignedByUserId: session.id,
332
+ effectiveFrom: validated.data.effectiveFrom,
333
+ effectiveTo: validated.data.effectiveTo,
334
+ status: 'active',
335
+ }).returning();
336
+ assigned = inserted;
337
+ }
338
+
339
+ // If this role is an engineering role, ensure an engineer profile exists
340
+ const roleRecord = await tx.query.roles.findFirst({
341
+ where: eq(roles.id, validated.data.roleId),
342
+ });
343
+
344
+ if (roleRecord && ['BIOMED_ENG', 'BIOMED_TECH', 'BIOMED_MGR'].includes(roleRecord.code)) {
345
+ const existingProfile = await tx.query.engineerProfiles.findFirst({
346
+ where: eq(engineerProfiles.userId, validated.data.userId),
347
+ });
348
+
349
+ if (!existingProfile) {
350
+ const userRecord = await tx.query.users.findFirst({
351
+ where: eq(users.id, validated.data.userId),
352
+ });
353
+
354
+ await tx.insert(engineerProfiles).values({
355
+ userId: validated.data.userId,
356
+ organizationId: userRecord?.organizationId || null,
357
+ jobTitle: userRecord?.jobTitle || roleRecord.name || 'Biomedical Engineer',
358
+ specialization: 'General Biomedical Engineering',
359
+ extension: userRecord?.employeeIdentifier || null,
360
+ mobileNumber: userRecord?.phone || null,
361
+ dutyStatus: 'on_duty',
362
+ coveredDepartmentIds: [],
363
+ });
364
+ }
365
+ }
366
+
367
+ await createAuditLog(tx, {
368
+ action: 'ASSIGN_ROLE',
369
+ entityType: 'user',
370
+ entityId: validated.data.userId,
371
+ actorId: session.id,
372
+ details: validated.data,
373
+ });
374
+
375
+ return { success: true, data: assigned };
376
+ });
377
+
378
+ revalidatePath('/admin/users');
379
+ revalidatePath(`/admin/users/${validated.data.userId}`);
380
+ revalidatePath('/team');
381
+ revalidatePath('/engineering-team');
382
+
383
+ return result;
384
+ } catch (error: any) {
385
+ return { success: false, error: error.message };
386
+ }
387
+ }
388
+
389
+ export async function revokeUserRole(assignmentId: string) {
390
+ const session = await requireAuth();
391
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
392
+
393
+ try {
394
+ const result = await db.transaction(async (tx) => {
395
+ const assignment = await tx.query.userRoleAssignments.findFirst({ where: eq(userRoleAssignments.id, assignmentId) });
396
+ if (!assignment) throw new Error("Assignment not found");
397
+
398
+ await tx.delete(userRoleAssignments).where(eq(userRoleAssignments.id, assignmentId));
399
+
400
+ await createAuditLog(tx, {
401
+ action: 'REVOKE_ROLE',
402
+ entityType: 'user',
403
+ entityId: assignment.userId,
404
+ actorId: session.id,
405
+ details: { assignmentId },
406
+ });
407
+
408
+ return { success: true, userId: assignment.userId };
409
+ });
410
+
411
+ if (result.userId) {
412
+ revalidatePath('/admin/users');
413
+ revalidatePath(`/admin/users/${result.userId}`);
414
+ revalidatePath('/team');
415
+ revalidatePath('/engineering-team');
416
+ }
417
+
418
+ return { success: true };
419
+ } catch (error: any) {
420
+ return { success: false, error: error.message };
421
+ }
422
+ }
423
+
424
+ export async function assignUserScope(input: unknown) {
425
+ const session = await requireAuth();
426
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
427
+ const validated = assignScopeSchema.safeParse(input);
428
+ if (!validated.success) return { success: false, error: validated.error.message };
429
+
430
+ try {
431
+ return await db.transaction(async (tx) => {
432
+ const [assigned] = await tx.insert(userAccessScopes).values({
433
+ userId: validated.data.userId,
434
+ organizationId: validated.data.organizationId,
435
+ scopeType: validated.data.scopeType,
436
+ hospitalId: validated.data.hospitalId,
437
+ departmentId: validated.data.departmentId,
438
+ locationId: validated.data.locationId,
439
+ createdByUserId: session.id,
440
+ }).returning();
441
+
442
+ await createAuditLog(tx, {
443
+ action: 'ASSIGN_SCOPE',
444
+ entityType: 'user',
445
+ entityId: validated.data.userId,
446
+ actorId: session.id,
447
+ details: validated.data,
448
+ });
449
+
450
+ return { success: true, data: assigned };
451
+ });
452
+ } catch (error: any) {
453
+ return { success: false, error: error.message };
454
+ }
455
+ }
456
+
457
+ export async function revokeUserScope(scopeId: string) {
458
+ const session = await requireAuth();
459
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
460
+
461
+ try {
462
+ return await db.transaction(async (tx) => {
463
+ const scope = await tx.query.userAccessScopes.findFirst({ where: eq(userAccessScopes.id, scopeId) });
464
+ if (!scope) throw new Error("Scope not found");
465
+
466
+ await tx.delete(userAccessScopes).where(eq(userAccessScopes.id, scopeId));
467
+
468
+ await createAuditLog(tx, {
469
+ action: 'REVOKE_SCOPE',
470
+ entityType: 'user',
471
+ entityId: scope.userId,
472
+ actorId: session.id,
473
+ details: { scopeId },
474
+ });
475
+
476
+ return { success: true };
477
+ });
478
+ } catch (error: any) {
479
+ return { success: false, error: error.message };
480
+ }
481
+ }
482
+
483
+ export async function deactivateUser(id: string, reason?: string) {
484
+ const session = await requireAuth();
485
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
486
+ try {
487
+ return await db.transaction(async (tx) => {
488
+ const [updated] = await tx.update(users).set({ accountStatus: 'deactivated' }).where(eq(users.id, id)).returning();
489
+ await createAuditLog(tx, {
490
+ action: 'DEACTIVATE',
491
+ entityType: 'user',
492
+ entityId: id,
493
+ actorId: session.id,
494
+ changeReason: reason || 'Administrative deactivation',
495
+ details: { reason },
496
+ });
497
+ return { success: true, data: updated };
498
+ });
499
+ } catch (error: any) {
500
+ return { success: false, error: error.message };
501
+ }
502
+ }
503
+
504
+ export async function reactivateUser(id: string, reason?: string) {
505
+ const session = await requireAuth();
506
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
507
+ try {
508
+ return await db.transaction(async (tx) => {
509
+ const [updated] = await tx.update(users).set({ accountStatus: 'active' }).where(eq(users.id, id)).returning();
510
+ await createAuditLog(tx, {
511
+ action: 'REACTIVATE',
512
+ entityType: 'user',
513
+ entityId: id,
514
+ actorId: session.id,
515
+ changeReason: reason || 'Administrative reactivation',
516
+ details: { reason },
517
+ });
518
+ return { success: true, data: updated };
519
+ });
520
+ } catch (error: any) {
521
+ return { success: false, error: error.message };
522
+ }
523
+ }
524
+
525
+ export async function archiveUser(id: string) {
526
+ const session = await requireAuth();
527
+ await requireRole('SYS_ADMIN', 'ORG_ADMIN');
528
+ try {
529
+ return await db.transaction(async (tx) => {
530
+ const [updated] = await tx.update(users).set({ accountStatus: 'archived' }).where(eq(users.id, id)).returning();
531
+ await createAuditLog(tx, { action: 'ARCHIVE', entityType: 'user', entityId: id, actorId: session.id, details: {} });
532
+ return { success: true, data: updated };
533
+ });
534
+ } catch (error: any) {
535
+ return { success: false, error: error.message };
536
+ }
537
+ }
538
+
539
+ // ── Department Staff Profile Actions ──────────────────────────────────────────
540
+
541
+ export interface DepartmentStaffProfile {
542
+ id: string;
543
+ fullName: string;
544
+ email: string;
545
+ phone: string | null;
546
+ jobTitle: string | null;
547
+ employeeIdentifier: string | null;
548
+ avatarUrl: string | null;
549
+ hospitalName: string | null;
550
+ departmentName: string | null;
551
+ departmentId: string | null;
552
+ hospitalId: string | null;
553
+ roles: string[];
554
+ activeTicketsCount: number;
555
+ engineerProfile?: {
556
+ id: string;
557
+ jobTitle: string;
558
+ specialization: string | null;
559
+ extension: string | null;
560
+ mobileNumber: string | null;
561
+ dutyStatus: string;
562
+ shiftHours: string | null;
563
+ workingDays: string[];
564
+ onCallDays: string[];
565
+ coveredDepartmentIds: string[];
566
+ notes: string | null;
567
+ } | null;
568
+ }
569
+
570
+ export async function getCurrentUserProfile(): Promise<{
571
+ success: boolean;
572
+ data?: DepartmentStaffProfile;
573
+ error?: string;
574
+ }> {
575
+ const session = await requireAuth();
576
+ try {
577
+ const user = await db.query.users.findFirst({
578
+ where: eq(users.id, session.id),
579
+ });
580
+ if (!user) return { success: false, error: 'User not found' };
581
+
582
+ const scopes = await db.query.userAccessScopes.findMany({
583
+ where: eq(userAccessScopes.userId, session.id),
584
+ with: {
585
+ hospital: true,
586
+ department: true,
587
+ },
588
+ });
589
+
590
+ const roleAssignments = await db.query.userRoleAssignments.findMany({
591
+ where: eq(userRoleAssignments.userId, session.id),
592
+ with: { role: true },
593
+ });
594
+
595
+ const primaryScope = scopes.find((s) => s.departmentId) || scopes[0] || null;
596
+
597
+ const engineerProfile = await db.query.engineerProfiles.findFirst({
598
+ where: eq(engineerProfiles.userId, session.id),
599
+ });
600
+
601
+ const [ticketCount] = await db
602
+ .select({ count: sql<number>`count(*)::int` })
603
+ .from(serviceTickets)
604
+ .where(
605
+ and(
606
+ eq(serviceTickets.reportedByUserId, session.id),
607
+ not(inArray(serviceTickets.statusCode, ['resolved', 'closed', 'cancelled']))
608
+ )
609
+ );
610
+
611
+ return {
612
+ success: true,
613
+ data: {
614
+ id: user.id,
615
+ fullName: user.fullName,
616
+ email: user.email,
617
+ phone: user.phone,
618
+ jobTitle: user.jobTitle,
619
+ employeeIdentifier: user.employeeIdentifier,
620
+ avatarUrl: user.avatarUrl,
621
+ hospitalName: (primaryScope as any)?.hospital?.name || null,
622
+ departmentName: (primaryScope as any)?.department?.name || null,
623
+ departmentId: primaryScope?.departmentId || null,
624
+ hospitalId: primaryScope?.hospitalId || null,
625
+ roles: roleAssignments.map((ra) => ra.role.code),
626
+ activeTicketsCount: ticketCount?.count || 0,
627
+ engineerProfile: engineerProfile
628
+ ? {
629
+ id: engineerProfile.id,
630
+ jobTitle: engineerProfile.jobTitle,
631
+ specialization: engineerProfile.specialization,
632
+ extension: engineerProfile.extension,
633
+ mobileNumber: engineerProfile.mobileNumber,
634
+ dutyStatus: engineerProfile.dutyStatus,
635
+ shiftHours: engineerProfile.shiftHours,
636
+ workingDays: (engineerProfile.workingDays as string[]) || [],
637
+ onCallDays: (engineerProfile.onCallDays as string[]) || [],
638
+ coveredDepartmentIds: (engineerProfile.coveredDepartmentIds as string[]) || [],
639
+ notes: engineerProfile.notes,
640
+ }
641
+ : null,
642
+ },
643
+ };
644
+ } catch (error: any) {
645
+ return { success: false, error: error.message };
646
+ }
647
+ }
648
+
649
+ export async function updateCurrentUserProfile(input: {
650
+ fullName?: string;
651
+ phone?: string;
652
+ jobTitle?: string;
653
+ employeeIdentifier?: string;
654
+ }): Promise<{
655
+ success: boolean;
656
+ data?: any;
657
+ error?: string;
658
+ }> {
659
+ const session = await requireAuth();
660
+ const validated = updateUserProfileSchema.safeParse(input);
661
+ if (!validated.success) return { success: false, error: validated.error.message };
662
+
663
+ try {
664
+ const [updated] = await db
665
+ .update(users)
666
+ .set({
667
+ ...validated.data,
668
+ updatedAt: new Date(),
669
+ })
670
+ .where(eq(users.id, session.id))
671
+ .returning();
672
+
673
+ revalidatePath('/profile');
674
+ revalidatePath('/dashboard');
675
+ return { success: true, data: updated };
676
+ } catch (error: any) {
677
+ return { success: false, error: error.message };
678
+ }
679
+ }
680
+
681
+ export async function getDepartmentStaffProfile(userId: string): Promise<{
682
+ success: boolean;
683
+ data?: DepartmentStaffProfile;
684
+ error?: string;
685
+ }> {
686
+ await requireAuth();
687
+ try {
688
+ const user = await db.query.users.findFirst({
689
+ where: eq(users.id, userId),
690
+ });
691
+ if (!user) return { success: false, error: 'User not found' };
692
+
693
+ const scopes = await db.query.userAccessScopes.findMany({
694
+ where: eq(userAccessScopes.userId, userId),
695
+ with: {
696
+ hospital: true,
697
+ department: true,
698
+ },
699
+ });
700
+
701
+ const roleAssignments = await db.query.userRoleAssignments.findMany({
702
+ where: eq(userRoleAssignments.userId, userId),
703
+ with: { role: true },
704
+ });
705
+
706
+ const primaryScope = scopes.find((s) => s.departmentId) || scopes[0] || null;
707
+
708
+ const [ticketCount] = await db
709
+ .select({ count: sql<number>`count(*)::int` })
710
+ .from(serviceTickets)
711
+ .where(
712
+ and(
713
+ eq(serviceTickets.reportedByUserId, userId),
714
+ not(inArray(serviceTickets.statusCode, ['resolved', 'closed', 'cancelled']))
715
+ )
716
+ );
717
+
718
+ return {
719
+ success: true,
720
+ data: {
721
+ id: user.id,
722
+ fullName: user.fullName,
723
+ email: user.email,
724
+ phone: user.phone,
725
+ jobTitle: user.jobTitle,
726
+ employeeIdentifier: user.employeeIdentifier,
727
+ avatarUrl: user.avatarUrl,
728
+ hospitalName: (primaryScope as any)?.hospital?.name || null,
729
+ departmentName: (primaryScope as any)?.department?.name || null,
730
+ departmentId: primaryScope?.departmentId || null,
731
+ hospitalId: primaryScope?.hospitalId || null,
732
+ roles: roleAssignments.map((ra) => ra.role.code),
733
+ activeTicketsCount: ticketCount?.count || 0,
734
+ },
735
+ };
736
+ } catch (error: any) {
737
+ return { success: false, error: error.message };
738
+ }
739
+ }
740
+