@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,120 @@
1
+ import Link from 'next/link';
2
+ import { auth } from '@/lib/auth';
3
+ import { redirect } from 'next/navigation';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
6
+ import {
7
+ Monitor, TicketCheck, Wrench, DollarSign, Users,
8
+ History, ArrowRight,
9
+ } from 'lucide-react';
10
+
11
+ const reportTypes = [
12
+ {
13
+ title: 'Inventory Report',
14
+ description: 'Devices by status, department, category, and manufacturer',
15
+ href: '/reports/inventory',
16
+ icon: Monitor,
17
+ color: 'text-blue-500',
18
+ },
19
+ {
20
+ title: 'Ticket Performance',
21
+ description: 'Response times, priorities, backlog, and resolution rates',
22
+ href: '/reports/tickets',
23
+ icon: TicketCheck,
24
+ color: 'text-orange-500',
25
+ },
26
+ {
27
+ title: 'PM Compliance',
28
+ description: 'Preventive maintenance completion rates and overdue items',
29
+ href: '/reports/maintenance',
30
+ icon: Wrench,
31
+ color: 'text-green-500',
32
+ },
33
+ {
34
+ title: 'Maintenance Costs',
35
+ description: 'Parts, vendor services, and labor costs by device and department',
36
+ href: '/reports/costs',
37
+ icon: DollarSign,
38
+ color: 'text-purple-500',
39
+ },
40
+ {
41
+ title: 'Engineer Workload',
42
+ description: 'Task distribution, resolution times, and PM completion',
43
+ href: '/reports/workload',
44
+ icon: Users,
45
+ color: 'text-teal-500',
46
+ },
47
+ ];
48
+
49
+ export default async function ReportsPage() {
50
+ const session = await auth();
51
+ if (!session?.user) redirect('/login');
52
+
53
+ return (
54
+ <div className="space-y-6 max-w-5xl mx-auto">
55
+ <div>
56
+ <h1 className="text-2xl md:text-3xl font-bold tracking-tight">Reports</h1>
57
+ <p className="text-muted-foreground mt-1">
58
+ Operational statistics and compliance reports
59
+ </p>
60
+ </div>
61
+
62
+ <Tabs defaultValue="live" className="w-full">
63
+ <TabsList className="w-full md:w-auto">
64
+ <TabsTrigger value="live" className="flex-1 md:flex-none min-h-[44px]">
65
+ Live Statistics
66
+ </TabsTrigger>
67
+ <TabsTrigger value="history" className="flex-1 md:flex-none min-h-[44px]">
68
+ Report History
69
+ </TabsTrigger>
70
+ </TabsList>
71
+
72
+ <TabsContent value="live" className="mt-6">
73
+ <div className="grid gap-4 md:grid-cols-2">
74
+ {reportTypes.map((report) => (
75
+ <Link key={report.href} href={report.href} className="block">
76
+ <Card className="hover:bg-muted/50 transition-colors cursor-pointer h-full">
77
+ <CardContent className="p-6">
78
+ <div className="flex items-start justify-between">
79
+ <div className="flex items-start gap-3">
80
+ <div className={`mt-0.5 ${report.color}`}>
81
+ <report.icon className="h-5 w-5" />
82
+ </div>
83
+ <div>
84
+ <h3 className="font-semibold">{report.title}</h3>
85
+ <p className="text-sm text-muted-foreground mt-1">
86
+ {report.description}
87
+ </p>
88
+ </div>
89
+ </div>
90
+ <ArrowRight className="h-4 w-4 text-muted-foreground mt-1 shrink-0" />
91
+ </div>
92
+ </CardContent>
93
+ </Card>
94
+ </Link>
95
+ ))}
96
+ </div>
97
+ </TabsContent>
98
+
99
+ <TabsContent value="history" className="mt-6">
100
+ <Card>
101
+ <CardContent className="py-12 text-center">
102
+ <History className="h-10 w-10 mx-auto text-muted-foreground mb-4" />
103
+ <h3 className="font-semibold text-lg">Report History</h3>
104
+ <p className="text-sm text-muted-foreground mt-2 max-w-sm mx-auto">
105
+ Generated report snapshots will appear here after you export a report.
106
+ </p>
107
+ <Link
108
+ href="/reports/history"
109
+ className="text-primary text-sm hover:underline mt-4 inline-block"
110
+ >
111
+ View all report history →
112
+ </Link>
113
+ </CardContent>
114
+ </Card>
115
+ </TabsContent>
116
+ </Tabs>
117
+ </div>
118
+ );
119
+ }
120
+
@@ -0,0 +1,139 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { redirect } from 'next/navigation';
3
+ import { getTicketPerformanceReport } from '@/lib/actions/reports';
4
+ import { ReportTransparencyHeader } from '@/components/reports/transparency-header';
5
+ import { ReportSummaryCards } from '@/components/reports/summary-cards';
6
+ import { ReportExportButton } from '@/components/reports/export-button';
7
+ import { Card, CardContent } from '@/components/ui/card';
8
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
9
+ import { ArrowLeft, Ticket, Clock, CheckCircle, AlertCircle } from 'lucide-react';
10
+ import Link from 'next/link';
11
+
12
+ function formatHours(hours: number | null | undefined): string {
13
+ if (hours == null) return 'N/A';
14
+ const h = Math.floor(hours);
15
+ const m = Math.round((hours - h) * 60);
16
+ if (h === 0) return `${m}m`;
17
+ return `${h}h ${m}m`;
18
+ }
19
+
20
+ export default async function TicketPerformanceReportPage({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
21
+ const session = await auth();
22
+ if (!session) redirect('/login');
23
+
24
+ const resolvedParams = await searchParams;
25
+
26
+ const defaultStartDate = new Date();
27
+ defaultStartDate.setDate(defaultStartDate.getDate() - 30);
28
+
29
+ const filters = {
30
+ hospitalId: typeof resolvedParams.hospitalId === 'string' ? resolvedParams.hospitalId : undefined,
31
+ departmentId: typeof resolvedParams.departmentId === 'string' ? resolvedParams.departmentId : undefined,
32
+ startDate: typeof resolvedParams.startDate === 'string' ? resolvedParams.startDate : defaultStartDate.toISOString(),
33
+ endDate: typeof resolvedParams.endDate === 'string' ? resolvedParams.endDate : new Date().toISOString(),
34
+ };
35
+
36
+ const data = await getTicketPerformanceReport(filters as any);
37
+
38
+ const summaryItems = [
39
+ { label: 'Total Tickets', value: data?.total || 0, icon: Ticket },
40
+ { label: 'Open', value: data?.openCount || 0, icon: AlertCircle, color: 'text-yellow-600' },
41
+ { label: 'Resolved', value: data?.closedCount || 0, icon: CheckCircle, color: 'text-green-600' },
42
+ { label: 'Avg Response', value: formatHours(data?.avgResponseTime), icon: Clock },
43
+ ];
44
+
45
+ return (
46
+ <div className="space-y-6">
47
+ <div className="flex items-center gap-4">
48
+ <Link href="/reports" className="text-muted-foreground hover:text-foreground inline-flex items-center gap-2">
49
+ <ArrowLeft className="h-4 w-4" />
50
+ <span className="hidden sm:inline">Back to Reports</span>
51
+ </Link>
52
+ <h1 className="text-2xl font-bold tracking-tight">Ticket Performance Report</h1>
53
+ </div>
54
+
55
+ <ReportTransparencyHeader
56
+ reportType="tickets"
57
+ timezone="UTC"
58
+ dateRange={{ start: filters.startDate, end: filters.endDate }}
59
+ filters={filters as Record<string, string>}
60
+ generatedAt={new Date()}
61
+ />
62
+
63
+ <ReportSummaryCards items={summaryItems} />
64
+
65
+ {!data || data.total === 0 ? (
66
+ <Card>
67
+ <CardContent className="p-6 text-center text-muted-foreground">
68
+ No ticket data found matching the selected filters.
69
+ </CardContent>
70
+ </Card>
71
+ ) : (
72
+ <>
73
+ <div className="grid gap-4 md:hidden">
74
+ <Card>
75
+ <CardContent className="p-4 space-y-2">
76
+ <div className="font-semibold mb-2">Priority Breakdown</div>
77
+ <div className="text-sm grid grid-cols-2 gap-2">
78
+ <span className="text-red-600 font-medium">P1 Critical: {data.priorityP1 || 0}</span>
79
+ <span className="text-orange-600 font-medium">P2 High: {data.priorityP2 || 0}</span>
80
+ <span className="text-yellow-600 font-medium">P3 Normal: {data.priorityP3 || 0}</span>
81
+ <span className="text-green-600 font-medium">P4 Low: {data.priorityP4 || 0}</span>
82
+ </div>
83
+ </CardContent>
84
+ </Card>
85
+ <Card>
86
+ <CardContent className="p-4 space-y-2">
87
+ <div className="font-semibold mb-2">Resolution Metrics</div>
88
+ <div className="text-sm grid grid-cols-2 gap-2">
89
+ <span>Avg Response:</span>
90
+ <span className="font-medium">{formatHours(data.avgResponseTime)}</span>
91
+ <span>Avg Resolution:</span>
92
+ <span className="font-medium">{formatHours(data.avgResolutionTime)}</span>
93
+ </div>
94
+ </CardContent>
95
+ </Card>
96
+ </div>
97
+
98
+ <div className="hidden md:block rounded-md border">
99
+ <Table>
100
+ <TableHeader>
101
+ <TableRow>
102
+ <TableHead>Metric</TableHead>
103
+ <TableHead className="text-right">Value</TableHead>
104
+ </TableRow>
105
+ </TableHeader>
106
+ <TableBody>
107
+ <TableRow>
108
+ <TableCell className="font-medium">Priority 1 (Critical)</TableCell>
109
+ <TableCell className="text-right text-red-600 font-medium">{data.priorityP1 || 0}</TableCell>
110
+ </TableRow>
111
+ <TableRow>
112
+ <TableCell className="font-medium">Priority 2 (High)</TableCell>
113
+ <TableCell className="text-right text-orange-600 font-medium">{data.priorityP2 || 0}</TableCell>
114
+ </TableRow>
115
+ <TableRow>
116
+ <TableCell className="font-medium">Priority 3 (Normal)</TableCell>
117
+ <TableCell className="text-right text-yellow-600 font-medium">{data.priorityP3 || 0}</TableCell>
118
+ </TableRow>
119
+ <TableRow>
120
+ <TableCell className="font-medium">Priority 4 (Low)</TableCell>
121
+ <TableCell className="text-right text-green-600 font-medium">{data.priorityP4 || 0}</TableCell>
122
+ </TableRow>
123
+ <TableRow>
124
+ <TableCell className="font-medium">Avg Resolution Time</TableCell>
125
+ <TableCell className="text-right font-medium">{formatHours(data.avgResolutionTime)}</TableCell>
126
+ </TableRow>
127
+ </TableBody>
128
+ </Table>
129
+ </div>
130
+ </>
131
+ )}
132
+
133
+ <div className="flex justify-end">
134
+ <ReportExportButton reportType="tickets" filters={filters as Record<string, string>} />
135
+ </div>
136
+ </div>
137
+ );
138
+ }
139
+
@@ -0,0 +1,135 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { redirect } from 'next/navigation';
3
+ import { db } from '@/lib/db';
4
+ import { users } from '@/lib/db/schema';
5
+ import { inArray } from 'drizzle-orm';
6
+ import { getWorkloadReport } from '@/lib/actions/reports';
7
+ import { ReportTransparencyHeader } from '@/components/reports/transparency-header';
8
+ import { ReportSummaryCards } from '@/components/reports/summary-cards';
9
+ import { ReportExportButton } from '@/components/reports/export-button';
10
+ import { Card, CardContent } from '@/components/ui/card';
11
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
12
+ import { ArrowLeft, Users, Briefcase, Clock, Activity } from 'lucide-react';
13
+ import Link from 'next/link';
14
+
15
+ export default async function WorkloadReportPage({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
16
+ const session = await auth();
17
+ if (!session) redirect('/login');
18
+
19
+ const resolvedParams = await searchParams;
20
+
21
+ const filters = {
22
+ hospitalId: typeof resolvedParams.hospitalId === 'string' ? resolvedParams.hospitalId : undefined,
23
+ departmentId: typeof resolvedParams.departmentId === 'string' ? resolvedParams.departmentId : undefined,
24
+ startDate: typeof resolvedParams.startDate === 'string' ? resolvedParams.startDate : undefined,
25
+ endDate: typeof resolvedParams.endDate === 'string' ? resolvedParams.endDate : undefined,
26
+ };
27
+
28
+ const rawData = await getWorkloadReport(filters as any);
29
+
30
+ // Fetch engineer names
31
+ const engineerIds = rawData.map(r => r.engineerId).filter((id): id is string => id !== null);
32
+ let engineerMap = new Map<string, string>();
33
+
34
+ if (engineerIds.length > 0) {
35
+ const engineers = await db.select({ id: users.id, fullName: users.fullName })
36
+ .from(users)
37
+ .where(inArray(users.id, engineerIds));
38
+
39
+ engineerMap = new Map(engineers.map(e => [e.id, e.fullName || 'Unknown Engineer']));
40
+ }
41
+
42
+ const data = rawData.map(r => ({
43
+ ...r,
44
+ name: r.engineerId ? engineerMap.get(r.engineerId) || r.engineerId : 'Unassigned',
45
+ }));
46
+
47
+ const totalEngineers = data.length;
48
+ const totalAssigned = data.reduce((acc, curr) => acc + curr.assignedCount, 0);
49
+ const totalCompleted = data.reduce((acc, curr) => acc + curr.completedCount, 0);
50
+
51
+ const summaryItems = [
52
+ { label: 'Total Engineers', value: totalEngineers, icon: Users },
53
+ { label: 'Total Tasks Assigned', value: totalAssigned, icon: Briefcase },
54
+ { label: 'Total Completed', value: totalCompleted, icon: Activity, color: 'text-green-600' },
55
+ ];
56
+
57
+ return (
58
+ <div className="space-y-6">
59
+ <div className="flex items-center gap-4">
60
+ <Link href="/reports" className="text-muted-foreground hover:text-foreground inline-flex items-center gap-2">
61
+ <ArrowLeft className="h-4 w-4" />
62
+ <span className="hidden sm:inline">Back to Reports</span>
63
+ </Link>
64
+ <h1 className="text-2xl font-bold tracking-tight">Engineer Workload Report</h1>
65
+ </div>
66
+
67
+ <ReportTransparencyHeader
68
+ reportType="workload"
69
+ timezone="UTC"
70
+ dateRange={{ start: filters.startDate, end: filters.endDate }}
71
+ filters={filters as Record<string, string>}
72
+ generatedAt={new Date()}
73
+ />
74
+
75
+ <ReportSummaryCards items={summaryItems} />
76
+
77
+ {!data || data.length === 0 ? (
78
+ <Card>
79
+ <CardContent className="p-6 text-center text-muted-foreground">
80
+ No workload data found matching the selected filters.
81
+ </CardContent>
82
+ </Card>
83
+ ) : (
84
+ <>
85
+ <div className="grid gap-4 md:hidden">
86
+ {data.map((item) => (
87
+ <Card key={item.engineerId || 'unassigned'}>
88
+ <CardContent className="p-4 space-y-2">
89
+ <div className="font-semibold">{item.name}</div>
90
+ <div className="text-sm grid grid-cols-2 gap-2">
91
+ <span className="text-muted-foreground">Assigned: {item.assignedCount}</span>
92
+ <span className="text-green-600 font-medium">Completed: {item.completedCount}</span>
93
+ <span className="text-red-600 font-medium col-span-2">P1 Critical: {item.priorityP1Count}</span>
94
+ </div>
95
+ </CardContent>
96
+ </Card>
97
+ ))}
98
+ </div>
99
+
100
+ <div className="hidden md:block rounded-md border">
101
+ <Table>
102
+ <TableHeader>
103
+ <TableRow>
104
+ <TableHead>Engineer</TableHead>
105
+ <TableHead className="text-right">Assigned</TableHead>
106
+ <TableHead className="text-right text-green-600">Completed</TableHead>
107
+ <TableHead className="text-right text-red-600">P1 Critical</TableHead>
108
+ <TableHead className="text-right">PM Rate</TableHead>
109
+ </TableRow>
110
+ </TableHeader>
111
+ <TableBody>
112
+ {data.map((item) => (
113
+ <TableRow key={item.engineerId || 'unassigned'}>
114
+ <TableCell className="font-medium">{item.name}</TableCell>
115
+ <TableCell className="text-right">{item.assignedCount}</TableCell>
116
+ <TableCell className="text-right font-medium text-green-600">{item.completedCount}</TableCell>
117
+ <TableCell className="text-right font-medium text-red-600">{item.priorityP1Count}</TableCell>
118
+ <TableCell className="text-right">
119
+ {item.assignedCount > 0 ? ((item.completedCount / item.assignedCount) * 100).toFixed(1) : 0}%
120
+ </TableCell>
121
+ </TableRow>
122
+ ))}
123
+ </TableBody>
124
+ </Table>
125
+ </div>
126
+ </>
127
+ )}
128
+
129
+ <div className="flex justify-end">
130
+ <ReportExportButton reportType="workload" filters={filters as Record<string, string>} />
131
+ </div>
132
+ </div>
133
+ );
134
+ }
135
+
@@ -0,0 +1,197 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { redirect } from 'next/navigation';
3
+ import { getRoundDetail } from '@/lib/actions/engineer-rounds';
4
+ import { PageHeader } from '@/components/shared/page-header';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
6
+ import { Button } from '@/components/ui/button';
7
+ import {
8
+ CheckCircle2, Circle, SkipForward, AlertTriangle, FileText,
9
+ Clock, Building2, ShieldCheck, ArrowLeft, Calendar,
10
+ } from 'lucide-react';
11
+ import { cn } from '@/lib/utils';
12
+ import Link from 'next/link';
13
+
14
+ export const dynamic = 'force-dynamic';
15
+
16
+ export default async function RoundDetailPage({ params }: { params: Promise<{ id: string }> }) {
17
+ const session = await auth();
18
+ if (!session?.user) redirect('/login');
19
+
20
+ const { id } = await params;
21
+ const result = await getRoundDetail(id);
22
+
23
+ if (!result.success || !result.data) {
24
+ return (
25
+ <div className="max-w-2xl mx-auto space-y-6">
26
+ <PageHeader title="Round Detail" />
27
+ <Card>
28
+ <CardContent className="pt-6 text-center">
29
+ <p className="text-muted-foreground">{result.error || 'Round not found'}</p>
30
+ <Link href="/rounds/history">
31
+ <Button variant="outline" className="mt-4">Back to History</Button>
32
+ </Link>
33
+ </CardContent>
34
+ </Card>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ const round = result.data;
40
+ const formatTime = (ts: Date | string | null | undefined) => {
41
+ if (!ts) return '—';
42
+ return new Date(ts).toLocaleTimeString('en-US', {
43
+ hour: '2-digit', minute: '2-digit', hour12: true,
44
+ });
45
+ };
46
+
47
+ const formatDate = (d: string) => {
48
+ return new Date(d + 'T00:00:00').toLocaleDateString('en-US', {
49
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
50
+ });
51
+ };
52
+
53
+ const completedVisits = round.departmentVisits.filter((v: any) => v.status === 'completed');
54
+ const skippedVisits = round.departmentVisits.filter((v: any) => v.status === 'skipped');
55
+
56
+ const statusColor = {
57
+ completed: 'text-green-600 dark:text-green-400',
58
+ in_progress: 'text-blue-600 dark:text-blue-400',
59
+ missed: 'text-red-600 dark:text-red-400',
60
+ scheduled: 'text-muted-foreground',
61
+ cancelled: 'text-muted-foreground',
62
+ };
63
+
64
+ return (
65
+ <div className="max-w-2xl mx-auto space-y-6">
66
+ <PageHeader title="Round Detail">
67
+ <Link href="/rounds/history">
68
+ <Button variant="outline" size="sm" className="gap-1.5">
69
+ <ArrowLeft className="h-4 w-4" />
70
+ Back
71
+ </Button>
72
+ </Link>
73
+ </PageHeader>
74
+
75
+ {/* Round Info Card */}
76
+ <Card>
77
+ <CardContent className="pt-6 space-y-3">
78
+ <div className="flex items-center justify-between">
79
+ <h2 className="text-lg font-semibold">{round.engineer?.fullName}</h2>
80
+ <span className={cn(
81
+ 'text-sm font-medium capitalize',
82
+ statusColor[round.status as keyof typeof statusColor] || 'text-muted-foreground',
83
+ )}>
84
+ {round.status?.replace(/_/g, ' ')}
85
+ </span>
86
+ </div>
87
+ <div className="grid gap-2 text-sm text-muted-foreground">
88
+ <div className="flex items-center gap-2">
89
+ <Calendar className="h-4 w-4" />
90
+ {formatDate(round.roundDate)} · <span className="capitalize">{round.shiftType}</span> shift
91
+ </div>
92
+ <div className="flex items-center gap-2">
93
+ <Building2 className="h-4 w-4" />
94
+ {round.hospital?.name}
95
+ </div>
96
+ <div className="flex items-center gap-2">
97
+ <Clock className="h-4 w-4" />
98
+ {formatTime(round.startedAt)} — {formatTime(round.completedAt)}
99
+ </div>
100
+ {round.startSignature && (
101
+ <div className="flex items-center gap-2 text-green-600 dark:text-green-400">
102
+ <ShieldCheck className="h-4 w-4" />
103
+ Signed at {formatTime(round.startSignature.signedAt as any)}
104
+ </div>
105
+ )}
106
+ </div>
107
+
108
+ {/* Stats */}
109
+ <div className="flex flex-wrap gap-4 pt-2 border-t text-sm">
110
+ <div>
111
+ <span className="text-muted-foreground">Departments: </span>
112
+ <span className="font-medium">{completedVisits.length}/{round.departmentVisits.length}</span>
113
+ </div>
114
+ <div>
115
+ <span className="text-muted-foreground">Issues: </span>
116
+ <span className="font-medium">{round.totalIssuesReported}</span>
117
+ </div>
118
+ <div>
119
+ <span className="text-muted-foreground">Notes: </span>
120
+ <span className="font-medium">{round.totalNotes}</span>
121
+ </div>
122
+ </div>
123
+ </CardContent>
124
+ </Card>
125
+
126
+ {/* Department Visits */}
127
+ <div className="space-y-2">
128
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider px-1">
129
+ Department Visits
130
+ </h3>
131
+ {round.departmentVisits.map((visit: any) => (
132
+ <Card key={visit.id} className={cn(
133
+ visit.status === 'completed' && 'border-green-200 dark:border-green-900/40',
134
+ visit.status === 'skipped' && 'opacity-60',
135
+ )}>
136
+ <CardContent className="pt-4 pb-4 space-y-2">
137
+ <div className="flex items-center gap-2">
138
+ {visit.status === 'completed' ? (
139
+ <CheckCircle2 className="h-5 w-5 text-green-500 shrink-0" />
140
+ ) : visit.status === 'skipped' ? (
141
+ <SkipForward className="h-5 w-5 text-muted-foreground shrink-0" />
142
+ ) : (
143
+ <Circle className="h-5 w-5 text-muted-foreground/40 shrink-0" />
144
+ )}
145
+ <span className="font-medium">{visit.departmentName}</span>
146
+ {visit.completedAt && (
147
+ <span className="ml-auto text-xs text-muted-foreground">
148
+ {formatTime(visit.startedAt)} — {formatTime(visit.completedAt)}
149
+ </span>
150
+ )}
151
+ </div>
152
+
153
+ {/* Notes */}
154
+ {visit.notes?.length > 0 && (
155
+ <div className="ml-7 space-y-1.5">
156
+ {visit.notes.map((note: any) => (
157
+ <div key={note.id} className="text-sm bg-muted/50 rounded-md p-2.5">
158
+ <p className="text-foreground">{note.noteText}</p>
159
+ {note.deviceMentions?.length > 0 && (
160
+ <div className="flex flex-wrap gap-1 mt-1">
161
+ {note.deviceMentions.map((m: any, i: number) => (
162
+ <span
163
+ key={`${m.deviceId}-${i}`}
164
+ className="inline-flex items-center text-xs bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 px-1.5 py-0.5 rounded"
165
+ >
166
+ @{m.deviceName}
167
+ </span>
168
+ ))}
169
+ </div>
170
+ )}
171
+ </div>
172
+ ))}
173
+ </div>
174
+ )}
175
+
176
+ {/* Tickets */}
177
+ {visit.tickets?.length > 0 && (
178
+ <div className="ml-7 space-y-1">
179
+ {visit.tickets.map((ticket: any) => (
180
+ <Link
181
+ key={ticket.ticketNumber}
182
+ href={`/tickets/${ticket.ticketNumber}`}
183
+ className="flex items-center gap-2 text-sm text-yellow-600 dark:text-yellow-400 hover:underline"
184
+ >
185
+ <AlertTriangle className="h-3.5 w-3.5" />
186
+ {ticket.ticketNumber} — {ticket.title}
187
+ </Link>
188
+ ))}
189
+ </div>
190
+ )}
191
+ </CardContent>
192
+ </Card>
193
+ ))}
194
+ </div>
195
+ </div>
196
+ );
197
+ }
@@ -0,0 +1,82 @@
1
+ import { auth } from '@/lib/auth';
2
+ import { redirect } from 'next/navigation';
3
+ import {
4
+ getRoundHistory, getRoundCalendarData, getTeamRoundCompliance, isEngineerRoundEnabled,
5
+ } from '@/lib/actions/engineer-rounds';
6
+ import { hasRole } from '@/lib/auth/rbac';
7
+ import { PageHeader } from '@/components/shared/page-header';
8
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
9
+ import { Button } from '@/components/ui/button';
10
+ import { RoundHistoryClient } from '@/components/rounds/round-history-client';
11
+ import { ArrowLeft, AlertCircle, Download } from 'lucide-react';
12
+ import Link from 'next/link';
13
+
14
+ export const dynamic = 'force-dynamic';
15
+
16
+ export default async function RoundHistoryPage() {
17
+ const session = await auth();
18
+ if (!session?.user) redirect('/login');
19
+
20
+ const user = session.user as any;
21
+ const enabled = await isEngineerRoundEnabled();
22
+
23
+ if (!enabled) {
24
+ return (
25
+ <div className="max-w-3xl mx-auto space-y-6">
26
+ <PageHeader title="Round History" />
27
+ <Card>
28
+ <CardContent className="pt-6 text-center">
29
+ <AlertCircle className="h-10 w-10 mx-auto text-muted-foreground mb-3" />
30
+ <p className="text-muted-foreground">Feature not enabled</p>
31
+ </CardContent>
32
+ </Card>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ const isManager = hasRole(user.roles || [], 'BIOMED_MGR', 'HOSP_ADMIN', 'ORG_ADMIN', 'SYS_ADMIN');
38
+ const now = new Date();
39
+ const currentMonth = now.getMonth() + 1;
40
+ const currentYear = now.getFullYear();
41
+
42
+ // Fetch initial data
43
+ const [historyResult, calendarResult, complianceResult] = await Promise.all([
44
+ getRoundHistory({ pageSize: 20 }),
45
+ getRoundCalendarData({ engineerId: user.id, month: currentMonth, year: currentYear }),
46
+ isManager ? getTeamRoundCompliance() : Promise.resolve(null),
47
+ ]);
48
+
49
+ return (
50
+ <div className="max-w-3xl mx-auto space-y-6">
51
+ <PageHeader title="Round History" description="View past rounds and compliance">
52
+ <div className="flex items-center gap-2">
53
+ <a
54
+ href="/api/admin/data/export?entities=rounds&format=xlsx"
55
+ target="_blank"
56
+ rel="noopener noreferrer"
57
+ >
58
+ <Button variant="outline" size="sm" className="gap-1.5">
59
+ <Download className="h-4 w-4" />
60
+ Export
61
+ </Button>
62
+ </a>
63
+ <Link href="/rounds">
64
+ <Button variant="outline" size="sm" className="gap-1.5">
65
+ <ArrowLeft className="h-4 w-4" />
66
+ Today
67
+ </Button>
68
+ </Link>
69
+ </div>
70
+ </PageHeader>
71
+
72
+ <RoundHistoryClient
73
+ initialHistory={historyResult.success ? historyResult.data || [] : []}
74
+ initialCalendar={calendarResult.success ? calendarResult.data : null}
75
+ initialCompliance={complianceResult?.success ? complianceResult.data : null}
76
+ isManager={isManager}
77
+ currentUserId={user.id}
78
+ pagination={historyResult.success ? (historyResult as any).pagination : null}
79
+ />
80
+ </div>
81
+ );
82
+ }