@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,216 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
5
+ import { Input } from '@/components/ui/input';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Badge } from '@/components/ui/badge';
8
+ import { Users, Calendar as CalendarIcon, Building2, Search, Filter } from 'lucide-react';
9
+ import { TeamHeaderStats } from './team-header-stats';
10
+ import { EngineerCard } from './engineer-card';
11
+ import { ScheduleCalendar } from './schedule-calendar';
12
+ import { DepartmentCoverage } from './department-coverage';
13
+ import { EditEngineerDialog } from './edit-engineer-dialog';
14
+ import { EngineerDetailsDialog } from './engineer-details-dialog';
15
+ import { EngineerTeamMember } from '@/lib/actions/engineers';
16
+ import { useRouter } from 'next/navigation';
17
+
18
+ interface TeamPageClientProps {
19
+ initialMembers: EngineerTeamMember[];
20
+ stats: {
21
+ totalEngineers: number;
22
+ onDutyToday: number;
23
+ onCallToday: number;
24
+ inMaintenanceToday: number;
25
+ offDutyToday: number;
26
+ totalActiveWorkOrders: number;
27
+ };
28
+ coverage: {
29
+ id: string;
30
+ name: string;
31
+ code: string;
32
+ deviceCount: number;
33
+ primaryEngineer: any;
34
+ }[];
35
+ }
36
+
37
+ export function TeamPageClient({ initialMembers, stats, coverage }: TeamPageClientProps) {
38
+ const router = useRouter();
39
+ const [search, setSearch] = useState('');
40
+ const [statusFilter, setStatusFilter] = useState<string>('all');
41
+ const [selectedEngineer, setSelectedEngineer] = useState<EngineerTeamMember | null>(null);
42
+ const [viewEngineer, setViewEngineer] = useState<EngineerTeamMember | null>(null);
43
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
44
+ const [isDetailsOpen, setIsDetailsOpen] = useState(false);
45
+
46
+ const filteredMembers = initialMembers.filter((m) => {
47
+ const matchesSearch =
48
+ m.fullName.toLowerCase().includes(search.toLowerCase()) ||
49
+ m.jobTitle.toLowerCase().includes(search.toLowerCase()) ||
50
+ (m.specialization && m.specialization.toLowerCase().includes(search.toLowerCase())) ||
51
+ (m.extension && m.extension.includes(search));
52
+
53
+ const matchesStatus =
54
+ statusFilter === 'all' ||
55
+ m.dutyStatus === statusFilter;
56
+
57
+ return matchesSearch && matchesStatus;
58
+ });
59
+
60
+ const handleEdit = (engineer: EngineerTeamMember) => {
61
+ setSelectedEngineer(engineer);
62
+ setIsEditDialogOpen(true);
63
+ };
64
+
65
+ const handleCardClick = (engineer: EngineerTeamMember) => {
66
+ setViewEngineer(engineer);
67
+ setIsDetailsOpen(true);
68
+ };
69
+
70
+ return (
71
+ <div className="space-y-6">
72
+ {/* Top Header */}
73
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
74
+ <div>
75
+ <h1 className="text-2xl font-bold tracking-tight">Biomedical Engineering Team & Roster</h1>
76
+ <p className="text-sm text-muted-foreground mt-0.5">
77
+ Clinical engineering staff directory, real-time hospital duty status, and department coverage roster.
78
+ </p>
79
+ </div>
80
+ </div>
81
+
82
+ {/* Top Stats Cards */}
83
+ <TeamHeaderStats stats={stats} />
84
+
85
+ {/* Main Tabs */}
86
+ <Tabs defaultValue="directory" className="space-y-5">
87
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
88
+ <TabsList className="grid grid-cols-3 w-full sm:w-auto h-9">
89
+ <TabsTrigger value="directory" className="text-xs gap-1.5 px-3">
90
+ <Users className="w-3.5 h-3.5" />
91
+ Team Directory
92
+ </TabsTrigger>
93
+ <TabsTrigger value="schedule" className="text-xs gap-1.5 px-3">
94
+ <CalendarIcon className="w-3.5 h-3.5" />
95
+ Duty Schedule
96
+ </TabsTrigger>
97
+ <TabsTrigger value="coverage" className="text-xs gap-1.5 px-3">
98
+ <Building2 className="w-3.5 h-3.5" />
99
+ Dept. Coverage
100
+ </TabsTrigger>
101
+ </TabsList>
102
+ </div>
103
+
104
+ {/* Tab 1: Directory */}
105
+ <TabsContent value="directory" className="space-y-4 m-0">
106
+ {/* Filters Bar */}
107
+ <div className="flex flex-col md:flex-row items-stretch md:items-center justify-between gap-3">
108
+ <div className="relative w-full md:max-w-md">
109
+ <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
110
+ <Input
111
+ placeholder="Search engineer by name, title, specialization, or extension..."
112
+ value={search}
113
+ onChange={(e) => setSearch(e.target.value)}
114
+ className="pl-9 h-9 text-xs"
115
+ />
116
+ </div>
117
+
118
+ <div className="flex items-center gap-1.5 overflow-x-auto pb-1 md:pb-0 shrink-0">
119
+ <Button
120
+ variant={statusFilter === 'all' ? 'secondary' : 'ghost'}
121
+ size="sm"
122
+ onClick={() => setStatusFilter('all')}
123
+ className="text-xs h-8"
124
+ >
125
+ All ({initialMembers.length})
126
+ </Button>
127
+ <Button
128
+ variant={statusFilter === 'on_duty' ? 'secondary' : 'ghost'}
129
+ size="sm"
130
+ onClick={() => setStatusFilter('on_duty')}
131
+ className="text-xs h-8 text-emerald-700 dark:text-emerald-400 gap-1.5"
132
+ >
133
+ <span className="w-2 h-2 rounded-full bg-emerald-500" />
134
+ On Duty ({stats.onDutyToday})
135
+ </Button>
136
+ <Button
137
+ variant={statusFilter === 'on_call' ? 'secondary' : 'ghost'}
138
+ size="sm"
139
+ onClick={() => setStatusFilter('on_call')}
140
+ className="text-xs h-8 text-amber-700 dark:text-amber-400 gap-1.5"
141
+ >
142
+ <span className="w-2 h-2 rounded-full bg-amber-500" />
143
+ On Call ({stats.onCallToday})
144
+ </Button>
145
+ <Button
146
+ variant={statusFilter === 'off_duty' ? 'secondary' : 'ghost'}
147
+ size="sm"
148
+ onClick={() => setStatusFilter('off_duty')}
149
+ className="text-xs h-8 text-muted-foreground gap-1.5"
150
+ >
151
+ <span className="w-2 h-2 rounded-full bg-slate-400" />
152
+ Off Duty ({stats.offDutyToday})
153
+ </Button>
154
+ </div>
155
+ </div>
156
+
157
+ {/* Engineers Cards Grid */}
158
+ {filteredMembers.length > 0 ? (
159
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
160
+ {filteredMembers.map((member) => (
161
+ <EngineerCard
162
+ key={member.id}
163
+ engineer={member}
164
+ onClick={handleCardClick}
165
+ onEdit={handleEdit}
166
+ />
167
+ ))}
168
+ </div>
169
+ ) : (
170
+ <div className="text-center py-12 border border-dashed rounded-xl space-y-2 bg-muted/10">
171
+ <Users className="w-10 h-10 text-muted-foreground mx-auto" />
172
+ <p className="font-semibold text-sm">No engineers match your filter</p>
173
+ <p className="text-xs text-muted-foreground">Try clearing your search query or status filter.</p>
174
+ <Button variant="outline" size="sm" onClick={() => { setSearch(''); setStatusFilter('all'); }}>
175
+ Reset Filters
176
+ </Button>
177
+ </div>
178
+ )}
179
+ </TabsContent>
180
+
181
+ {/* Tab 2: Duty Calendar */}
182
+ <TabsContent value="schedule" className="space-y-4 m-0">
183
+ <ScheduleCalendar engineers={initialMembers} />
184
+ </TabsContent>
185
+
186
+ {/* Tab 3: Department Coverage */}
187
+ <TabsContent value="coverage" className="space-y-4 m-0">
188
+ <DepartmentCoverage coverage={coverage} />
189
+ </TabsContent>
190
+ </Tabs>
191
+
192
+ {/* Full Engineer Details Dialog */}
193
+ {viewEngineer && (
194
+ <EngineerDetailsDialog
195
+ engineer={viewEngineer}
196
+ open={isDetailsOpen}
197
+ onOpenChange={setIsDetailsOpen}
198
+ onEdit={handleEdit}
199
+ onStatusUpdated={() => router.refresh()}
200
+ />
201
+ )}
202
+
203
+ {/* Edit Engineer Dialog */}
204
+ {selectedEngineer && (
205
+ <EditEngineerDialog
206
+ key={selectedEngineer.id}
207
+ engineer={selectedEngineer}
208
+ open={isEditDialogOpen}
209
+ onOpenChange={setIsEditDialogOpen}
210
+ onSuccess={() => router.refresh()}
211
+ />
212
+ )}
213
+ </div>
214
+ );
215
+ }
216
+
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useTheme, type Theme } from '@/components/providers/theme-provider'
5
+ import { Sun, Moon, Monitor } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ export function ThemeSelectorCard() {
9
+ const { theme, setTheme } = useTheme()
10
+ const [mounted, setMounted] = React.useState(false)
11
+
12
+ React.useEffect(() => {
13
+ setMounted(true)
14
+ }, [])
15
+
16
+ const options: { value: Theme; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
17
+ { value: 'light', label: 'Light', icon: Sun },
18
+ { value: 'dark', label: 'Dark', icon: Moon },
19
+ { value: 'system', label: 'System', icon: Monitor },
20
+ ]
21
+
22
+ return (
23
+ <div className="space-y-2">
24
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground px-1">
25
+ Appearance & Theme
26
+ </h3>
27
+ <div className="bg-card rounded-xl border p-3 sm:p-4 shadow-sm">
28
+ <div className="flex items-center justify-between gap-2">
29
+ <div className="flex flex-col">
30
+ <span className="text-sm font-medium leading-snug">Interface Theme</span>
31
+ <span className="text-xs text-muted-foreground">
32
+ Select your preferred visual style or sync with device settings
33
+ </span>
34
+ </div>
35
+ </div>
36
+
37
+ <div className="grid grid-cols-3 gap-2 mt-3.5">
38
+ {options.map((opt) => {
39
+ const Icon = opt.icon
40
+ const isSelected = mounted && theme === opt.value
41
+
42
+ return (
43
+ <button
44
+ key={opt.value}
45
+ type="button"
46
+ onClick={() => setTheme(opt.value)}
47
+ className={cn(
48
+ "flex items-center justify-center gap-2 p-2.5 rounded-lg border text-xs font-medium transition-all min-h-[44px]",
49
+ isSelected
50
+ ? "bg-primary text-primary-foreground border-primary shadow-sm"
51
+ : "bg-muted/40 hover:bg-muted text-muted-foreground hover:text-foreground border-transparent"
52
+ )}
53
+ >
54
+ <Icon className={cn("w-4 h-4", isSelected ? "text-primary-foreground" : "")} />
55
+ <span>{opt.label}</span>
56
+ </button>
57
+ )
58
+ })}
59
+ </div>
60
+ </div>
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,65 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useTheme } from '@/components/providers/theme-provider'
5
+ import { Moon, Sun, Monitor, Check } from 'lucide-react'
6
+ import { Button } from '@/components/ui/button'
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ } from '@/components/ui/dropdown-menu'
13
+
14
+ export function ThemeToggle() {
15
+ const { setTheme, theme } = useTheme()
16
+ const [mounted, setMounted] = React.useState(false)
17
+
18
+ React.useEffect(() => {
19
+ setMounted(true)
20
+ }, [])
21
+
22
+ if (!mounted) {
23
+ return (
24
+ <Button variant="ghost" size="icon" className="h-9 w-9">
25
+ <Sun className="h-4 w-4" />
26
+ <span className="sr-only">Toggle theme</span>
27
+ </Button>
28
+ )
29
+ }
30
+
31
+ return (
32
+ <DropdownMenu>
33
+ <DropdownMenuTrigger asChild>
34
+ <Button variant="ghost" size="icon" className="h-9 w-9">
35
+ <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
36
+ <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
37
+ <span className="sr-only">Toggle theme</span>
38
+ </Button>
39
+ </DropdownMenuTrigger>
40
+ <DropdownMenuContent align="end">
41
+ <DropdownMenuItem onClick={() => setTheme('light')} className="cursor-pointer justify-between">
42
+ <div className="flex items-center">
43
+ <Sun className="mr-2 h-4 w-4" />
44
+ <span>Light</span>
45
+ </div>
46
+ {theme === 'light' && <Check className="h-4 w-4 text-primary ml-2" />}
47
+ </DropdownMenuItem>
48
+ <DropdownMenuItem onClick={() => setTheme('dark')} className="cursor-pointer justify-between">
49
+ <div className="flex items-center">
50
+ <Moon className="mr-2 h-4 w-4" />
51
+ <span>Dark</span>
52
+ </div>
53
+ {theme === 'dark' && <Check className="h-4 w-4 text-primary ml-2" />}
54
+ </DropdownMenuItem>
55
+ <DropdownMenuItem onClick={() => setTheme('system')} className="cursor-pointer justify-between">
56
+ <div className="flex items-center">
57
+ <Monitor className="mr-2 h-4 w-4" />
58
+ <span>System</span>
59
+ </div>
60
+ {theme === 'system' && <Check className="h-4 w-4 text-primary ml-2" />}
61
+ </DropdownMenuItem>
62
+ </DropdownMenuContent>
63
+ </DropdownMenu>
64
+ )
65
+ }
@@ -0,0 +1,58 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { Badge } from '@/components/ui/badge';
3
+ import { getStaffStatus, getStaffStatusStyle } from '@/lib/utils/staff-status';
4
+
5
+ interface StaffStatusLabelProps {
6
+ statusCode: string;
7
+ showIcon?: boolean;
8
+ size?: 'sm' | 'default';
9
+ }
10
+
11
+ /**
12
+ * Staff-facing ticket status badge.
13
+ * Shows plain-language labels like "Being repaired" instead of "in_progress".
14
+ * Used in staff dashboards, ticket lists, and ticket detail pages.
15
+ */
16
+ export function StaffStatusLabel({ statusCode, showIcon = true, size = 'default' }: StaffStatusLabelProps) {
17
+ const info = getStaffStatus(statusCode);
18
+ const style = getStaffStatusStyle(statusCode);
19
+
20
+ return (
21
+ <Badge
22
+ variant="outline"
23
+ className={cn(
24
+ 'font-medium whitespace-nowrap',
25
+ style.bg,
26
+ style.text,
27
+ style.border,
28
+ size === 'sm' && 'text-xs px-1.5 py-0',
29
+ )}
30
+ >
31
+ {showIcon && <span className="mr-1">{info.icon}</span>}
32
+ {info.label}
33
+ </Badge>
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Larger staff status display with description.
39
+ * Used on ticket detail pages for staff.
40
+ */
41
+ export function StaffStatusBanner({ statusCode }: { statusCode: string }) {
42
+ const info = getStaffStatus(statusCode);
43
+ const style = getStaffStatusStyle(statusCode);
44
+
45
+ return (
46
+ <div className={cn('rounded-lg border px-4 py-3', style.bg, style.border)}>
47
+ <div className="flex items-center gap-2">
48
+ <span className="text-lg">{info.icon}</span>
49
+ <div>
50
+ <p className={cn('font-semibold text-sm', style.text)}>{info.label}</p>
51
+ {info.description && (
52
+ <p className="text-xs text-muted-foreground mt-0.5">{info.description}</p>
53
+ )}
54
+ </div>
55
+ </div>
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { cn } from '@/lib/utils';
5
+ import { getStaffStatus, getStaffStatusStyle } from '@/lib/utils/staff-status';
6
+ import { Monitor, User, MessageCircle } from 'lucide-react';
7
+
8
+ export interface StaffTicketItem {
9
+ id: string;
10
+ ticketNumber: string;
11
+ title: string;
12
+ statusCode: string;
13
+ reportedAt?: Date | string | null;
14
+ updatedAt?: Date | string | null;
15
+ deviceName?: string | null;
16
+ deviceAssetNumber?: string | null;
17
+ assignedEngineerName?: string | null;
18
+ }
19
+
20
+ export function StaffTicketCard({ ticket }: { ticket: StaffTicketItem }) {
21
+ const status = getStaffStatus(ticket.statusCode);
22
+ const style = getStaffStatusStyle(ticket.statusCode);
23
+ const needsResponse = ticket.statusCode === 'waiting_requester';
24
+ const isResolved = ['resolved', 'closed'].includes(ticket.statusCode);
25
+
26
+ const updatedText = ticket.updatedAt
27
+ ? formatRelativeTime(new Date(ticket.updatedAt))
28
+ : ticket.reportedAt
29
+ ? formatRelativeTime(new Date(ticket.reportedAt))
30
+ : '';
31
+
32
+ return (
33
+ <Link
34
+ href={`/tickets/${ticket.id}`}
35
+ className={cn(
36
+ 'block bg-card rounded-xl border shadow-sm overflow-hidden transition-all hover:shadow-md',
37
+ needsResponse && 'border-amber-300 dark:border-amber-800',
38
+ isResolved && 'opacity-75',
39
+ !needsResponse && !isResolved && 'border-border hover:border-primary/50',
40
+ )}
41
+ >
42
+ {/* Response requested banner */}
43
+ {needsResponse && (
44
+ <div className="bg-amber-50 dark:bg-amber-950/30 px-4 py-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300 flex items-center gap-1.5 border-b border-amber-200 dark:border-amber-800">
45
+ <MessageCircle className="w-3.5 h-3.5" />
46
+ Response requested — tap to reply
47
+ </div>
48
+ )}
49
+
50
+ <div className="p-4">
51
+ {/* Title and status */}
52
+ <div className="flex items-start justify-between gap-2">
53
+ <div className="min-w-0">
54
+ <p className="font-semibold text-sm line-clamp-1">{ticket.title}</p>
55
+ <p className="text-xs text-muted-foreground mt-0.5">
56
+ {ticket.ticketNumber}
57
+ {ticket.deviceName && (
58
+ <> · {ticket.deviceName}</>
59
+ )}
60
+ </p>
61
+ </div>
62
+ <span
63
+ className={cn(
64
+ 'text-xs px-2 py-0.5 rounded-full font-medium whitespace-nowrap border',
65
+ style.bg,
66
+ style.text,
67
+ style.border
68
+ )}
69
+ >
70
+ {status.icon} {status.label}
71
+ </span>
72
+ </div>
73
+
74
+ {/* Engineer and time */}
75
+ <div className="mt-2 flex items-center gap-4 text-xs text-muted-foreground">
76
+ {ticket.assignedEngineerName ? (
77
+ <span className="flex items-center gap-1">
78
+ <User className="w-3 h-3" />
79
+ {ticket.assignedEngineerName}
80
+ </span>
81
+ ) : (
82
+ <span className="flex items-center gap-1 text-muted-foreground/70">
83
+ <User className="w-3 h-3" />
84
+ Awaiting assignment
85
+ </span>
86
+ )}
87
+ {updatedText && (
88
+ <span>Updated {updatedText}</span>
89
+ )}
90
+ </div>
91
+ </div>
92
+ </Link>
93
+ );
94
+ }
95
+
96
+ /** Simple relative time formatter */
97
+ function formatRelativeTime(date: Date): string {
98
+ const now = new Date();
99
+ const diffMs = now.getTime() - date.getTime();
100
+ const diffMins = Math.floor(diffMs / 60000);
101
+ const diffHours = Math.floor(diffMs / 3600000);
102
+ const diffDays = Math.floor(diffMs / 86400000);
103
+
104
+ if (diffMins < 1) return 'just now';
105
+ if (diffMins < 60) return `${diffMins}m ago`;
106
+ if (diffHours < 24) return `${diffHours}h ago`;
107
+ if (diffDays < 7) return `${diffDays}d ago`;
108
+ return date.toLocaleDateString();
109
+ }