@omersx/bemms 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +203 -2
  3. package/drizzle.config.ts +11 -0
  4. package/next.config.ts +51 -0
  5. package/package.json +99 -3
  6. package/postcss.config.mjs +7 -0
  7. package/public/hospital_demo_devices.csv +32 -0
  8. package/public/icons/icon-192.png +0 -0
  9. package/public/icons/icon-512.png +0 -0
  10. package/public/icons/icon-maskable-512.png +0 -0
  11. package/public/manifest.webmanifest +31 -0
  12. package/public/sw.js +177 -0
  13. package/src/app/(auth)/layout.tsx +20 -0
  14. package/src/app/(auth)/login/page.tsx +135 -0
  15. package/src/app/(dashboard)/admin/audit-logs/page.tsx +178 -0
  16. package/src/app/(dashboard)/admin/data/page.tsx +37 -0
  17. package/src/app/(dashboard)/admin/departments/[deptId]/edit/page.tsx +206 -0
  18. package/src/app/(dashboard)/admin/departments/[deptId]/page.tsx +71 -0
  19. package/src/app/(dashboard)/admin/departments/new/page.tsx +6 -0
  20. package/src/app/(dashboard)/admin/departments/page.tsx +6 -0
  21. package/src/app/(dashboard)/admin/general/page.tsx +374 -0
  22. package/src/app/(dashboard)/admin/hospitals/[hospitalId]/edit/page.tsx +304 -0
  23. package/src/app/(dashboard)/admin/hospitals/[hospitalId]/page.tsx +94 -0
  24. package/src/app/(dashboard)/admin/hospitals/new/page.tsx +302 -0
  25. package/src/app/(dashboard)/admin/hospitals/page.tsx +6 -0
  26. package/src/app/(dashboard)/admin/layout.tsx +49 -0
  27. package/src/app/(dashboard)/admin/organizations/[orgId]/edit/page.tsx +172 -0
  28. package/src/app/(dashboard)/admin/organizations/[orgId]/page.tsx +82 -0
  29. package/src/app/(dashboard)/admin/organizations/new/page.tsx +150 -0
  30. package/src/app/(dashboard)/admin/organizations/page.tsx +6 -0
  31. package/src/app/(dashboard)/admin/page.tsx +258 -0
  32. package/src/app/(dashboard)/admin/roles/page.tsx +153 -0
  33. package/src/app/(dashboard)/admin/settings/page.tsx +69 -0
  34. package/src/app/(dashboard)/admin/users/[userId]/edit/page.tsx +153 -0
  35. package/src/app/(dashboard)/admin/users/[userId]/page.tsx +317 -0
  36. package/src/app/(dashboard)/admin/users/invite/page.tsx +3 -0
  37. package/src/app/(dashboard)/admin/users/new/page.tsx +5 -0
  38. package/src/app/(dashboard)/admin/users/page.tsx +23 -0
  39. package/src/app/(dashboard)/dashboard/page.tsx +462 -0
  40. package/src/app/(dashboard)/departments/[deptId]/page.tsx +39 -0
  41. package/src/app/(dashboard)/departments/new/page.tsx +330 -0
  42. package/src/app/(dashboard)/departments/page.tsx +52 -0
  43. package/src/app/(dashboard)/devices/[id]/edit/page.tsx +131 -0
  44. package/src/app/(dashboard)/devices/[id]/page.tsx +408 -0
  45. package/src/app/(dashboard)/devices/[id]/qr/page.tsx +54 -0
  46. package/src/app/(dashboard)/devices/new/page.tsx +382 -0
  47. package/src/app/(dashboard)/devices/page.tsx +80 -0
  48. package/src/app/(dashboard)/engineering-team/page.tsx +41 -0
  49. package/src/app/(dashboard)/layout.tsx +43 -0
  50. package/src/app/(dashboard)/maintenance/checklists/page.tsx +57 -0
  51. package/src/app/(dashboard)/maintenance/page.tsx +231 -0
  52. package/src/app/(dashboard)/maintenance/plans/page.tsx +142 -0
  53. package/src/app/(dashboard)/maintenance/tasks/[id]/page.tsx +997 -0
  54. package/src/app/(dashboard)/maintenance/tasks/page.tsx +119 -0
  55. package/src/app/(dashboard)/more/page.tsx +310 -0
  56. package/src/app/(dashboard)/my-department/page.tsx +151 -0
  57. package/src/app/(dashboard)/notifications/notification-list.tsx +103 -0
  58. package/src/app/(dashboard)/notifications/page.tsx +59 -0
  59. package/src/app/(dashboard)/profile/page.tsx +27 -0
  60. package/src/app/(dashboard)/reports/costs/page.tsx +127 -0
  61. package/src/app/(dashboard)/reports/history/page.tsx +105 -0
  62. package/src/app/(dashboard)/reports/inventory/page.tsx +116 -0
  63. package/src/app/(dashboard)/reports/maintenance/page.tsx +136 -0
  64. package/src/app/(dashboard)/reports/page.tsx +120 -0
  65. package/src/app/(dashboard)/reports/tickets/page.tsx +139 -0
  66. package/src/app/(dashboard)/reports/workload/page.tsx +135 -0
  67. package/src/app/(dashboard)/rounds/[id]/page.tsx +197 -0
  68. package/src/app/(dashboard)/rounds/history/page.tsx +82 -0
  69. package/src/app/(dashboard)/rounds/page.tsx +107 -0
  70. package/src/app/(dashboard)/scan/[code]/page.tsx +154 -0
  71. package/src/app/(dashboard)/scan/page.tsx +73 -0
  72. package/src/app/(dashboard)/team/page.tsx +42 -0
  73. package/src/app/(dashboard)/tickets/[id]/page.tsx +465 -0
  74. package/src/app/(dashboard)/tickets/create/page.tsx +722 -0
  75. package/src/app/(dashboard)/tickets/page.tsx +55 -0
  76. package/src/app/(dashboard)/tickets/triage/page.tsx +161 -0
  77. package/src/app/api/admin/data/export/route.ts +316 -0
  78. package/src/app/api/auth/[...nextauth]/route.ts +3 -0
  79. package/src/app/api/health/route.ts +46 -0
  80. package/src/app/api/notifications/sse/route.ts +91 -0
  81. package/src/app/api/notifications/subscribe/route.ts +54 -0
  82. package/src/app/api/notifications/vapid-key/route.ts +7 -0
  83. package/src/app/api/reports/export/route.ts +102 -0
  84. package/src/app/api/tickets/[id]/attachments/route.ts +129 -0
  85. package/src/app/api/tickets/[id]/live-sync/route.ts +52 -0
  86. package/src/app/error.tsx +54 -0
  87. package/src/app/globals.css +89 -0
  88. package/src/app/layout.tsx +59 -0
  89. package/src/app/not-found.tsx +32 -0
  90. package/src/app/page.tsx +5 -0
  91. package/src/components/admin/add-department-user-dialog.tsx +759 -0
  92. package/src/components/admin/add-engineer-dialog.tsx +773 -0
  93. package/src/components/admin/add-user-type-dialog.tsx +89 -0
  94. package/src/components/admin/audit-log-export.tsx +113 -0
  95. package/src/components/admin/audit-log-filters.tsx +255 -0
  96. package/src/components/admin/audit-log-table.tsx +383 -0
  97. package/src/components/admin/data-management/danger-zone.tsx +301 -0
  98. package/src/components/admin/data-management/data-management-tabs.tsx +61 -0
  99. package/src/components/admin/data-management/export-center.tsx +301 -0
  100. package/src/components/admin/data-management/import-wizard.tsx +558 -0
  101. package/src/components/admin/settings-forms/features-form.tsx +115 -0
  102. package/src/components/admin/settings-forms/health-panel.tsx +182 -0
  103. package/src/components/admin/settings-forms/localization-form.tsx +203 -0
  104. package/src/components/admin/settings-forms/retention-form.tsx +164 -0
  105. package/src/components/admin/settings-forms/security-form.tsx +186 -0
  106. package/src/components/admin/system-settings-tabs.tsx +88 -0
  107. package/src/components/admin/user-role-dialog.tsx +293 -0
  108. package/src/components/admin/user-status-dialog.tsx +166 -0
  109. package/src/components/admin/users-page-client.tsx +514 -0
  110. package/src/components/dashboard/action-buttons.tsx +93 -0
  111. package/src/components/dashboard/admin-alerts.tsx +86 -0
  112. package/src/components/dashboard/availability-alerts.tsx +65 -0
  113. package/src/components/dashboard/maintenance-due-list.tsx +87 -0
  114. package/src/components/dashboard/round-widget.tsx +110 -0
  115. package/src/components/dashboard/stat-card.tsx +38 -0
  116. package/src/components/dashboard/ticket-list-compact.tsx +86 -0
  117. package/src/components/departments/department-cards-view.tsx +244 -0
  118. package/src/components/departments/department-detail-view.tsx +467 -0
  119. package/src/components/departments/department-visuals.tsx +190 -0
  120. package/src/components/departments/my-department-view.tsx +333 -0
  121. package/src/components/devices/change-status-dialog.tsx +146 -0
  122. package/src/components/devices/device-action-buttons.tsx +125 -0
  123. package/src/components/devices/device-availability-banner.tsx +59 -0
  124. package/src/components/devices/device-badges.tsx +55 -0
  125. package/src/components/devices/device-documents-tab.tsx +93 -0
  126. package/src/components/devices/device-maintenance-tab.tsx +113 -0
  127. package/src/components/devices/device-mini-qr.tsx +109 -0
  128. package/src/components/devices/device-qr-card.tsx +187 -0
  129. package/src/components/devices/device-signatures-tab.tsx +607 -0
  130. package/src/components/devices/device-tickets-tab.tsx +139 -0
  131. package/src/components/devices/device-timelines.tsx +124 -0
  132. package/src/components/devices/qr-camera-scanner.tsx +167 -0
  133. package/src/components/devices/qr-code-display.tsx +139 -0
  134. package/src/components/devices/quick-add-category-dialog.tsx +135 -0
  135. package/src/components/devices/quick-add-manufacturer-dialog.tsx +212 -0
  136. package/src/components/devices/transfer-device-dialog.tsx +190 -0
  137. package/src/components/layout/header.tsx +80 -0
  138. package/src/components/layout/mobile-nav.tsx +91 -0
  139. package/src/components/layout/more-item-badge.tsx +15 -0
  140. package/src/components/layout/nav-badge.tsx +72 -0
  141. package/src/components/layout/sidebar.tsx +297 -0
  142. package/src/components/maintenance/maintenance-badges.tsx +579 -0
  143. package/src/components/maintenance/maintenance-nav.tsx +107 -0
  144. package/src/components/maintenance/parts-costs-manager.tsx +909 -0
  145. package/src/components/maintenance/signature-card.tsx +413 -0
  146. package/src/components/maintenance/task-action-bar.tsx +955 -0
  147. package/src/components/notifications/notification-bell.tsx +367 -0
  148. package/src/components/notifications/notification-item.tsx +93 -0
  149. package/src/components/notifications/notification-preferences.tsx +182 -0
  150. package/src/components/profile/user-profile-view.tsx +626 -0
  151. package/src/components/providers/nav-counts-provider.tsx +235 -0
  152. package/src/components/providers/session-provider.tsx +7 -0
  153. package/src/components/providers/theme-provider.tsx +124 -0
  154. package/src/components/pwa/install-prompt.tsx +82 -0
  155. package/src/components/pwa/sw-register.tsx +43 -0
  156. package/src/components/reports/export-button.tsx +62 -0
  157. package/src/components/reports/filter-chips.tsx +47 -0
  158. package/src/components/reports/summary-cards.tsx +40 -0
  159. package/src/components/reports/transparency-header.tsx +54 -0
  160. package/src/components/rounds/department-visit-sheet.tsx +199 -0
  161. package/src/components/rounds/mention-textarea.tsx +187 -0
  162. package/src/components/rounds/round-calendar.tsx +233 -0
  163. package/src/components/rounds/round-department-list.tsx +281 -0
  164. package/src/components/rounds/round-history-client.tsx +218 -0
  165. package/src/components/rounds/start-round-card.tsx +184 -0
  166. package/src/components/shared/confirm-dialog.tsx +67 -0
  167. package/src/components/shared/data-table.tsx +104 -0
  168. package/src/components/shared/page-header.tsx +20 -0
  169. package/src/components/shared/sign-out-button.tsx +52 -0
  170. package/src/components/shared/status-badge.tsx +42 -0
  171. package/src/components/shared/table-search.tsx +55 -0
  172. package/src/components/signatures/sign-action-dialog.tsx +253 -0
  173. package/src/components/signatures/signature-history.tsx +121 -0
  174. package/src/components/signatures/signature-status-panel.tsx +164 -0
  175. package/src/components/team/department-coverage.tsx +141 -0
  176. package/src/components/team/edit-engineer-dialog.tsx +242 -0
  177. package/src/components/team/engineer-card.tsx +180 -0
  178. package/src/components/team/engineer-details-dialog.tsx +432 -0
  179. package/src/components/team/schedule-calendar.tsx +204 -0
  180. package/src/components/team/staff-engineer-team-view.tsx +368 -0
  181. package/src/components/team/team-header-stats.tsx +88 -0
  182. package/src/components/team/team-page-client.tsx +216 -0
  183. package/src/components/theme/theme-selector-card.tsx +63 -0
  184. package/src/components/theme/theme-toggle.tsx +65 -0
  185. package/src/components/tickets/staff-status-label.tsx +58 -0
  186. package/src/components/tickets/staff-ticket-card.tsx +109 -0
  187. package/src/components/tickets/staff-ticket-progress-tracker.tsx +405 -0
  188. package/src/components/tickets/staff-tickets-view.tsx +182 -0
  189. package/src/components/tickets/ticket-action-buttons.tsx +873 -0
  190. package/src/components/tickets/ticket-auto-refresh.tsx +197 -0
  191. package/src/components/tickets/ticket-badges.tsx +43 -0
  192. package/src/components/tickets/ticket-card.tsx +197 -0
  193. package/src/components/tickets/ticket-cards-view.tsx +395 -0
  194. package/src/components/tickets/ticket-comment-section.tsx +684 -0
  195. package/src/components/tickets/ticket-dispatch-status.tsx +184 -0
  196. package/src/components/tickets/ticket-list-auto-refresh.tsx +55 -0
  197. package/src/components/tickets/ticket-reporter-profile.tsx +235 -0
  198. package/src/components/tickets/ticket-signatures-panel.tsx +32 -0
  199. package/src/components/tickets/ticket-timeline.tsx +131 -0
  200. package/src/components/tickets/ticket-triage-panel.tsx +124 -0
  201. package/src/components/tickets/tickets-nav.tsx +107 -0
  202. package/src/components/ui/avatar.tsx +43 -0
  203. package/src/components/ui/badge.tsx +34 -0
  204. package/src/components/ui/button.tsx +58 -0
  205. package/src/components/ui/card.tsx +77 -0
  206. package/src/components/ui/checkbox.tsx +31 -0
  207. package/src/components/ui/dialog.tsx +123 -0
  208. package/src/components/ui/dropdown-menu.tsx +126 -0
  209. package/src/components/ui/form.tsx +179 -0
  210. package/src/components/ui/input.tsx +24 -0
  211. package/src/components/ui/label.tsx +27 -0
  212. package/src/components/ui/popover.tsx +34 -0
  213. package/src/components/ui/progress.tsx +36 -0
  214. package/src/components/ui/scroll-area.tsx +47 -0
  215. package/src/components/ui/select.tsx +160 -0
  216. package/src/components/ui/separator.tsx +25 -0
  217. package/src/components/ui/sheet.tsx +131 -0
  218. package/src/components/ui/switch.tsx +30 -0
  219. package/src/components/ui/table.tsx +121 -0
  220. package/src/components/ui/tabs.tsx +62 -0
  221. package/src/components/ui/textarea.tsx +23 -0
  222. package/src/components/ui/tooltip.tsx +27 -0
  223. package/src/lib/actions/__tests__/devices.test.ts +176 -0
  224. package/src/lib/actions/__tests__/signatures.test.ts +303 -0
  225. package/src/lib/actions/__tests__/tickets.test.ts +243 -0
  226. package/src/lib/actions/audit-logs.ts +298 -0
  227. package/src/lib/actions/checklists.ts +429 -0
  228. package/src/lib/actions/dashboard.ts +358 -0
  229. package/src/lib/actions/data-management.ts +723 -0
  230. package/src/lib/actions/departments.ts +369 -0
  231. package/src/lib/actions/device-categories.ts +117 -0
  232. package/src/lib/actions/devices.ts +748 -0
  233. package/src/lib/actions/engineer-rounds.ts +973 -0
  234. package/src/lib/actions/engineers.ts +285 -0
  235. package/src/lib/actions/hospitals.ts +148 -0
  236. package/src/lib/actions/locations.ts +115 -0
  237. package/src/lib/actions/maintenance-plans.ts +316 -0
  238. package/src/lib/actions/maintenance-tasks.ts +415 -0
  239. package/src/lib/actions/manufacturers.ts +132 -0
  240. package/src/lib/actions/nav-counts.ts +144 -0
  241. package/src/lib/actions/notifications.ts +136 -0
  242. package/src/lib/actions/organizations.ts +99 -0
  243. package/src/lib/actions/qr.ts +147 -0
  244. package/src/lib/actions/report-history.ts +77 -0
  245. package/src/lib/actions/reports.ts +192 -0
  246. package/src/lib/actions/roles.ts +32 -0
  247. package/src/lib/actions/signature-auth.ts +377 -0
  248. package/src/lib/actions/signatures.ts +532 -0
  249. package/src/lib/actions/system-settings.ts +274 -0
  250. package/src/lib/actions/tickets.ts +1445 -0
  251. package/src/lib/actions/users.ts +740 -0
  252. package/src/lib/audit/index.ts +103 -0
  253. package/src/lib/auth/__tests__/rbac.test.ts +33 -0
  254. package/src/lib/auth/index.ts +338 -0
  255. package/src/lib/auth/password.ts +10 -0
  256. package/src/lib/auth/rbac.ts +103 -0
  257. package/src/lib/auth/types.ts +57 -0
  258. package/src/lib/constants/departments.ts +16 -0
  259. package/src/lib/constants/manufacturers.ts +32 -0
  260. package/src/lib/db/index.ts +23 -0
  261. package/src/lib/db/schema/audit-logs.ts +42 -0
  262. package/src/lib/db/schema/departments.ts +39 -0
  263. package/src/lib/db/schema/device-categories.ts +38 -0
  264. package/src/lib/db/schema/devices.ts +195 -0
  265. package/src/lib/db/schema/engineer-profiles.ts +63 -0
  266. package/src/lib/db/schema/engineer-rounds.ts +115 -0
  267. package/src/lib/db/schema/enums.ts +148 -0
  268. package/src/lib/db/schema/hospitals.ts +34 -0
  269. package/src/lib/db/schema/index.ts +24 -0
  270. package/src/lib/db/schema/locations.ts +46 -0
  271. package/src/lib/db/schema/maintenance-plans.ts +99 -0
  272. package/src/lib/db/schema/maintenance-records.ts +161 -0
  273. package/src/lib/db/schema/maintenance-tasks.ts +196 -0
  274. package/src/lib/db/schema/manufacturers.ts +60 -0
  275. package/src/lib/db/schema/notifications.ts +48 -0
  276. package/src/lib/db/schema/organizations.ts +24 -0
  277. package/src/lib/db/schema/report-history.ts +28 -0
  278. package/src/lib/db/schema/roles.ts +59 -0
  279. package/src/lib/db/schema/sessions.ts +46 -0
  280. package/src/lib/db/schema/signature-events.ts +48 -0
  281. package/src/lib/db/schema/signature-policies.ts +60 -0
  282. package/src/lib/db/schema/signatures.ts +87 -0
  283. package/src/lib/db/schema/tickets.ts +169 -0
  284. package/src/lib/db/schema/user-assignments.ts +85 -0
  285. package/src/lib/db/schema/users.ts +39 -0
  286. package/src/lib/db/seed.ts +131 -0
  287. package/src/lib/logger/__tests__/logger.test.ts +154 -0
  288. package/src/lib/logger/formatters.ts +159 -0
  289. package/src/lib/logger/index.ts +184 -0
  290. package/src/lib/logger/types.ts +48 -0
  291. package/src/lib/notifications/create.ts +71 -0
  292. package/src/lib/notifications/dispatcher.ts +490 -0
  293. package/src/lib/notifications/index.ts +7 -0
  294. package/src/lib/notifications/push-sender.ts +128 -0
  295. package/src/lib/notifications/push.ts +64 -0
  296. package/src/lib/notifications/triggers.ts +166 -0
  297. package/src/lib/utils/__tests__/crypto.test.ts +30 -0
  298. package/src/lib/utils/__tests__/export.test.ts +55 -0
  299. package/src/lib/utils/__tests__/nav-badge.test.ts +147 -0
  300. package/src/lib/utils/__tests__/staff-status.test.ts +50 -0
  301. package/src/lib/utils/attestations.ts +19 -0
  302. package/src/lib/utils/crypto.ts +18 -0
  303. package/src/lib/utils/csv-parser.ts +181 -0
  304. package/src/lib/utils/export.ts +45 -0
  305. package/src/lib/utils/nav-badge.ts +225 -0
  306. package/src/lib/utils/sound.ts +152 -0
  307. package/src/lib/utils/staff-status.ts +115 -0
  308. package/src/lib/utils.ts +6 -0
  309. package/src/lib/validators/__tests__/auth.test.ts +127 -0
  310. package/src/lib/validators/__tests__/data-management.test.ts +182 -0
  311. package/src/lib/validators/__tests__/devices-profile.test.ts +129 -0
  312. package/src/lib/validators/__tests__/devices.test.ts +98 -0
  313. package/src/lib/validators/__tests__/engineer-rounds.test.ts +170 -0
  314. package/src/lib/validators/__tests__/maintenance.test.ts +178 -0
  315. package/src/lib/validators/__tests__/signatures.test.ts +154 -0
  316. package/src/lib/validators/__tests__/system-settings.test.ts +119 -0
  317. package/src/lib/validators/__tests__/tickets.test.ts +176 -0
  318. package/src/lib/validators/__tests__/users.test.ts +92 -0
  319. package/src/lib/validators/auth.ts +32 -0
  320. package/src/lib/validators/data-management.ts +67 -0
  321. package/src/lib/validators/departments.ts +14 -0
  322. package/src/lib/validators/devices.ts +94 -0
  323. package/src/lib/validators/engineer-rounds.ts +70 -0
  324. package/src/lib/validators/hospitals.ts +17 -0
  325. package/src/lib/validators/locations.ts +15 -0
  326. package/src/lib/validators/maintenance.ts +211 -0
  327. package/src/lib/validators/organizations.ts +11 -0
  328. package/src/lib/validators/reports.ts +31 -0
  329. package/src/lib/validators/signatures.ts +49 -0
  330. package/src/lib/validators/system-settings.ts +55 -0
  331. package/src/lib/validators/tickets.ts +125 -0
  332. package/src/lib/validators/users.ts +43 -0
  333. package/src/middleware.ts +45 -0
  334. package/tailwind.config.ts +76 -0
  335. package/tsconfig.json +28 -0
@@ -0,0 +1,199 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useTransition } from 'react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
6
+ import { MentionTextarea } from './mention-textarea';
7
+ import { completeDepartmentVisit, getDepartmentDevices } from '@/lib/actions/engineer-rounds';
8
+ import { toast } from 'sonner';
9
+ import { CheckCircle2, AlertTriangle, FileText, Loader2 } from 'lucide-react';
10
+ import { cn } from '@/lib/utils';
11
+ import { useRouter } from 'next/navigation';
12
+
13
+ interface DepartmentVisitSheetProps {
14
+ open: boolean;
15
+ onOpenChange: (open: boolean) => void;
16
+ visit: {
17
+ id: string;
18
+ departmentId: string;
19
+ departmentName: string;
20
+ status: string;
21
+ notes?: { id: string; noteText: string; deviceMentions: any[]; author?: { fullName: string } }[];
22
+ ticketCount?: number;
23
+ } | null;
24
+ roundId: string;
25
+ hospitalId: string;
26
+ onCompleted: () => void;
27
+ }
28
+
29
+ export function DepartmentVisitSheet({
30
+ open,
31
+ onOpenChange,
32
+ visit,
33
+ roundId,
34
+ hospitalId,
35
+ onCompleted,
36
+ }: DepartmentVisitSheetProps) {
37
+ const router = useRouter();
38
+ const [noteText, setNoteText] = useState('');
39
+ const [deviceMentions, setDeviceMentions] = useState<any[]>([]);
40
+ const [devices, setDevices] = useState<any[]>([]);
41
+ const [isPending, startTransition] = useTransition();
42
+ const [loadingDevices, setLoadingDevices] = useState(false);
43
+
44
+ // Load devices for this department
45
+ useEffect(() => {
46
+ if (visit && open) {
47
+ setLoadingDevices(true);
48
+ getDepartmentDevices(visit.departmentId).then(result => {
49
+ if (result.success) {
50
+ setDevices(result.data || []);
51
+ }
52
+ setLoadingDevices(false);
53
+ });
54
+ }
55
+ }, [visit?.departmentId, open]);
56
+
57
+ // Reset form when visit changes
58
+ useEffect(() => {
59
+ if (open) {
60
+ setNoteText('');
61
+ setDeviceMentions([]);
62
+ }
63
+ }, [visit?.id, open]);
64
+
65
+ if (!visit) return null;
66
+
67
+ const isCompleted = visit.status === 'completed';
68
+
69
+ const handleComplete = () => {
70
+ startTransition(async () => {
71
+ const result = await completeDepartmentVisit({
72
+ visitId: visit.id,
73
+ noteText: noteText.trim() || undefined,
74
+ deviceMentions,
75
+ });
76
+
77
+ if (result.success) {
78
+ toast.success(`${visit.departmentName} completed`);
79
+ onCompleted();
80
+ onOpenChange(false);
81
+ } else {
82
+ toast.error(result.error || 'Failed to complete visit');
83
+ }
84
+ });
85
+ };
86
+
87
+ const handleReportIssue = () => {
88
+ // Navigate to ticket creation with round context pre-filled
89
+ const params = new URLSearchParams({
90
+ departmentId: visit.departmentId,
91
+ hospitalId,
92
+ roundVisitId: visit.id,
93
+ source: 'department_device_selection',
94
+ });
95
+ router.push(`/tickets/create?${params.toString()}`);
96
+ onOpenChange(false);
97
+ };
98
+
99
+ return (
100
+ <Sheet open={open} onOpenChange={onOpenChange}>
101
+ <SheetContent side="bottom" className="h-[85vh] sm:h-auto sm:max-h-[80vh] rounded-t-xl">
102
+ <SheetHeader>
103
+ <SheetTitle className="flex items-center gap-2 text-lg">
104
+ {isCompleted ? (
105
+ <CheckCircle2 className="h-5 w-5 text-green-500" />
106
+ ) : (
107
+ <div className="h-5 w-5 rounded-full border-2 border-muted-foreground/40" />
108
+ )}
109
+ {visit.departmentName}
110
+ </SheetTitle>
111
+ <p className="text-sm text-muted-foreground">
112
+ {devices.length} device{devices.length !== 1 ? 's' : ''} in department
113
+ </p>
114
+ </SheetHeader>
115
+
116
+ <div className="mt-6 space-y-4">
117
+ {/* Existing notes */}
118
+ {visit.notes && visit.notes.length > 0 && (
119
+ <div className="space-y-2">
120
+ <h4 className="text-sm font-medium text-muted-foreground">Previous Notes</h4>
121
+ {visit.notes.map(note => (
122
+ <div key={note.id} className="rounded-md bg-muted/50 p-3 text-sm">
123
+ <p>{note.noteText}</p>
124
+ {note.deviceMentions?.length > 0 && (
125
+ <div className="flex flex-wrap gap-1 mt-1.5">
126
+ {note.deviceMentions.map((m: any, i: number) => (
127
+ <span
128
+ key={`${m.deviceId}-${i}`}
129
+ 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"
130
+ >
131
+ @{m.deviceName}
132
+ </span>
133
+ ))}
134
+ </div>
135
+ )}
136
+ </div>
137
+ ))}
138
+ </div>
139
+ )}
140
+
141
+ {/* Ticket count */}
142
+ {(visit.ticketCount ?? 0) > 0 && (
143
+ <div className="flex items-center gap-2 text-sm text-yellow-600 dark:text-yellow-400">
144
+ <AlertTriangle className="h-4 w-4" />
145
+ {visit.ticketCount} issue{visit.ticketCount !== 1 ? 's' : ''} reported
146
+ </div>
147
+ )}
148
+
149
+ {/* Note input (only if not completed) */}
150
+ {!isCompleted && (
151
+ <>
152
+ <div>
153
+ <label className="text-sm font-medium mb-1.5 block">
154
+ Notes <span className="text-muted-foreground font-normal">(optional)</span>
155
+ </label>
156
+ <MentionTextarea
157
+ value={noteText}
158
+ onChange={(val, mentions) => {
159
+ setNoteText(val);
160
+ setDeviceMentions(mentions);
161
+ }}
162
+ devices={devices}
163
+ placeholder='e.g. "All devices working well" or "Issue with @device..."'
164
+ disabled={isPending}
165
+ />
166
+ </div>
167
+
168
+ {/* Action Buttons */}
169
+ <div className="flex flex-col gap-2 pt-2">
170
+ <Button
171
+ variant="outline"
172
+ className="w-full justify-start gap-2 h-12 text-yellow-600 border-yellow-200 hover:bg-yellow-50 dark:text-yellow-400 dark:border-yellow-800 dark:hover:bg-yellow-950/30"
173
+ onClick={handleReportIssue}
174
+ disabled={isPending}
175
+ >
176
+ <AlertTriangle className="h-5 w-5" />
177
+ Report Issue
178
+ </Button>
179
+
180
+ <Button
181
+ className="w-full h-12 gap-2 text-base"
182
+ onClick={handleComplete}
183
+ disabled={isPending}
184
+ >
185
+ {isPending ? (
186
+ <Loader2 className="h-5 w-5 animate-spin" />
187
+ ) : (
188
+ <CheckCircle2 className="h-5 w-5" />
189
+ )}
190
+ Complete Department
191
+ </Button>
192
+ </div>
193
+ </>
194
+ )}
195
+ </div>
196
+ </SheetContent>
197
+ </Sheet>
198
+ );
199
+ }
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, useCallback } from 'react';
4
+ import { Textarea } from '@/components/ui/textarea';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ interface DeviceMention {
8
+ deviceId: string;
9
+ deviceName: string;
10
+ internalCode: string;
11
+ }
12
+
13
+ interface MentionTextareaProps {
14
+ value: string;
15
+ onChange: (value: string, mentions: DeviceMention[]) => void;
16
+ devices: { id: string; name: string; internalCode: string; assetNumber: string; currentStatusCode: string }[];
17
+ placeholder?: string;
18
+ className?: string;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ export function MentionTextarea({
23
+ value,
24
+ onChange,
25
+ devices,
26
+ placeholder = 'Add a note... Use @device to mention specific devices',
27
+ className,
28
+ disabled,
29
+ }: MentionTextareaProps) {
30
+ const [showDropdown, setShowDropdown] = useState(false);
31
+ const [searchTerm, setSearchTerm] = useState('');
32
+ const [mentions, setMentions] = useState<DeviceMention[]>([]);
33
+ const [selectedIndex, setSelectedIndex] = useState(0);
34
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
35
+
36
+ const filteredDevices = devices.filter(d =>
37
+ d.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
38
+ d.internalCode.toLowerCase().includes(searchTerm.toLowerCase())
39
+ ).slice(0, 8);
40
+
41
+ const handleTextChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
42
+ const newValue = e.target.value;
43
+ const cursorPos = e.target.selectionStart;
44
+
45
+ // Detect @ trigger
46
+ const textBeforeCursor = newValue.slice(0, cursorPos);
47
+ const atMatch = textBeforeCursor.match(/@([^\s@]*)$/);
48
+
49
+ if (atMatch) {
50
+ setSearchTerm(atMatch[1]);
51
+ setShowDropdown(true);
52
+ setSelectedIndex(0);
53
+ } else {
54
+ setShowDropdown(false);
55
+ setSearchTerm('');
56
+ }
57
+
58
+ onChange(newValue, mentions);
59
+ }, [mentions, onChange]);
60
+
61
+ const insertMention = useCallback((device: typeof devices[0]) => {
62
+ if (!textareaRef.current) return;
63
+
64
+ const textarea = textareaRef.current;
65
+ const cursorPos = textarea.selectionStart;
66
+ const textBeforeCursor = value.slice(0, cursorPos);
67
+ const textAfterCursor = value.slice(cursorPos);
68
+
69
+ // Find the @ position
70
+ const atMatch = textBeforeCursor.match(/@([^\s@]*)$/);
71
+ if (!atMatch) return;
72
+
73
+ const atStart = textBeforeCursor.lastIndexOf('@');
74
+ const mentionText = `@${device.name}`;
75
+ const newValue = value.slice(0, atStart) + mentionText + ' ' + textAfterCursor;
76
+
77
+ const newMention: DeviceMention = {
78
+ deviceId: device.id,
79
+ deviceName: device.name,
80
+ internalCode: device.internalCode,
81
+ };
82
+
83
+ const updatedMentions = [...mentions, newMention];
84
+ setMentions(updatedMentions);
85
+ setShowDropdown(false);
86
+ setSearchTerm('');
87
+ onChange(newValue, updatedMentions);
88
+
89
+ // Restore focus and cursor
90
+ setTimeout(() => {
91
+ textarea.focus();
92
+ const newCursorPos = atStart + mentionText.length + 1;
93
+ textarea.setSelectionRange(newCursorPos, newCursorPos);
94
+ }, 0);
95
+ }, [value, mentions, onChange]);
96
+
97
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
98
+ if (!showDropdown || filteredDevices.length === 0) return;
99
+
100
+ if (e.key === 'ArrowDown') {
101
+ e.preventDefault();
102
+ setSelectedIndex(i => Math.min(i + 1, filteredDevices.length - 1));
103
+ } else if (e.key === 'ArrowUp') {
104
+ e.preventDefault();
105
+ setSelectedIndex(i => Math.max(i - 1, 0));
106
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
107
+ e.preventDefault();
108
+ insertMention(filteredDevices[selectedIndex]);
109
+ } else if (e.key === 'Escape') {
110
+ setShowDropdown(false);
111
+ }
112
+ }, [showDropdown, filteredDevices, selectedIndex, insertMention]);
113
+
114
+ // Close dropdown on outside click
115
+ useEffect(() => {
116
+ const handleClick = () => setShowDropdown(false);
117
+ if (showDropdown) {
118
+ document.addEventListener('click', handleClick);
119
+ return () => document.removeEventListener('click', handleClick);
120
+ }
121
+ }, [showDropdown]);
122
+
123
+ return (
124
+ <div className="relative">
125
+ <Textarea
126
+ ref={textareaRef}
127
+ value={value}
128
+ onChange={handleTextChange}
129
+ onKeyDown={handleKeyDown}
130
+ placeholder={placeholder}
131
+ className={cn('min-h-[80px] resize-none', className)}
132
+ disabled={disabled}
133
+ />
134
+
135
+ {showDropdown && filteredDevices.length > 0 && (
136
+ <div className="absolute z-50 mt-1 w-full max-h-48 overflow-y-auto rounded-md border bg-popover p-1 shadow-md animate-in fade-in-0 zoom-in-95">
137
+ <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
138
+ Devices in this department
139
+ </div>
140
+ {filteredDevices.map((device, idx) => (
141
+ <button
142
+ key={device.id}
143
+ type="button"
144
+ className={cn(
145
+ 'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',
146
+ idx === selectedIndex
147
+ ? 'bg-accent text-accent-foreground'
148
+ : 'hover:bg-accent/50'
149
+ )}
150
+ onClick={(e) => {
151
+ e.stopPropagation();
152
+ insertMention(device);
153
+ }}
154
+ onMouseEnter={() => setSelectedIndex(idx)}
155
+ >
156
+ <div className="flex flex-col items-start">
157
+ <span className="font-medium">{device.name}</span>
158
+ <span className="text-xs text-muted-foreground">{device.internalCode}</span>
159
+ </div>
160
+ <span className={cn(
161
+ 'ml-auto text-[10px] px-1.5 py-0.5 rounded-full',
162
+ device.currentStatusCode === 'operational'
163
+ ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
164
+ : 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
165
+ )}>
166
+ {device.currentStatusCode?.replace(/_/g, ' ')}
167
+ </span>
168
+ </button>
169
+ ))}
170
+ </div>
171
+ )}
172
+
173
+ {mentions.length > 0 && (
174
+ <div className="flex flex-wrap gap-1 mt-1.5">
175
+ {mentions.map((m, i) => (
176
+ <span
177
+ key={`${m.deviceId}-${i}`}
178
+ className="inline-flex items-center gap-1 text-xs bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 px-1.5 py-0.5 rounded"
179
+ >
180
+ @{m.deviceName}
181
+ </span>
182
+ ))}
183
+ </div>
184
+ )}
185
+ </div>
186
+ );
187
+ }
@@ -0,0 +1,233 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipProvider,
8
+ TooltipTrigger,
9
+ } from '@/components/ui/tooltip';
10
+ import { useRouter } from 'next/navigation';
11
+
12
+ interface CalendarDay {
13
+ date: string;
14
+ day: number;
15
+ dayName: string;
16
+ isWorkingDay: boolean;
17
+ status: 'completed' | 'partial' | 'missed' | 'in_progress' | 'scheduled' | 'off_day';
18
+ round: {
19
+ id: string;
20
+ totalDepartments: number;
21
+ completedDepartments: number;
22
+ totalIssuesReported: number;
23
+ totalNotes: number;
24
+ } | null;
25
+ }
26
+
27
+ interface RoundCalendarProps {
28
+ month: number;
29
+ year: number;
30
+ calendarDays: CalendarDay[];
31
+ summary: {
32
+ totalWorkingDays: number;
33
+ completed: number;
34
+ missed: number;
35
+ complianceRate: number;
36
+ };
37
+ engineerName?: string;
38
+ onMonthChange?: (month: number, year: number) => void;
39
+ }
40
+
41
+ const STATUS_COLORS: Record<string, string> = {
42
+ completed: 'bg-green-500 dark:bg-green-600 hover:bg-green-600 dark:hover:bg-green-500',
43
+ partial: 'bg-yellow-400 dark:bg-yellow-600 hover:bg-yellow-500 dark:hover:bg-yellow-500',
44
+ missed: 'bg-red-400 dark:bg-red-600 hover:bg-red-500 dark:hover:bg-red-500',
45
+ in_progress: 'bg-blue-400 dark:bg-blue-600 hover:bg-blue-500 dark:hover:bg-blue-500 animate-pulse',
46
+ scheduled: 'bg-muted hover:bg-muted-foreground/20',
47
+ off_day: 'bg-transparent',
48
+ };
49
+
50
+ const STATUS_LABELS: Record<string, string> = {
51
+ completed: 'Completed',
52
+ partial: 'Partial',
53
+ missed: 'Missed',
54
+ in_progress: 'In Progress',
55
+ scheduled: 'Scheduled',
56
+ off_day: 'Off Day',
57
+ };
58
+
59
+ const MONTH_NAMES = [
60
+ 'January', 'February', 'March', 'April', 'May', 'June',
61
+ 'July', 'August', 'September', 'October', 'November', 'December'
62
+ ];
63
+
64
+ export function RoundCalendar({
65
+ month,
66
+ year,
67
+ calendarDays,
68
+ summary,
69
+ engineerName,
70
+ onMonthChange,
71
+ }: RoundCalendarProps) {
72
+ const router = useRouter();
73
+
74
+ // Build calendar grid (weeks × 7)
75
+ const firstDay = new Date(year, month - 1, 1).getDay(); // 0=Sun
76
+ const totalDays = calendarDays.length;
77
+ const weeks: (CalendarDay | null)[][] = [];
78
+
79
+ let currentWeek: (CalendarDay | null)[] = [];
80
+ // Pad start
81
+ for (let i = 0; i < firstDay; i++) {
82
+ currentWeek.push(null);
83
+ }
84
+ for (const day of calendarDays) {
85
+ currentWeek.push(day);
86
+ if (currentWeek.length === 7) {
87
+ weeks.push(currentWeek);
88
+ currentWeek = [];
89
+ }
90
+ }
91
+ // Pad end
92
+ if (currentWeek.length > 0) {
93
+ while (currentWeek.length < 7) {
94
+ currentWeek.push(null);
95
+ }
96
+ weeks.push(currentWeek);
97
+ }
98
+
99
+ const handlePrev = () => {
100
+ const newMonth = month === 1 ? 12 : month - 1;
101
+ const newYear = month === 1 ? year - 1 : year;
102
+ onMonthChange?.(newMonth, newYear);
103
+ };
104
+
105
+ const handleNext = () => {
106
+ const newMonth = month === 12 ? 1 : month + 1;
107
+ const newYear = month === 12 ? year + 1 : year;
108
+ onMonthChange?.(newMonth, newYear);
109
+ };
110
+
111
+ return (
112
+ <div className="space-y-4">
113
+ {/* Header */}
114
+ <div className="flex items-center justify-between">
115
+ <button
116
+ onClick={handlePrev}
117
+ className="p-2 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
118
+ >
119
+ ←
120
+ </button>
121
+ <div className="text-center">
122
+ <h3 className="font-semibold text-lg">
123
+ {MONTH_NAMES[month - 1]} {year}
124
+ </h3>
125
+ {engineerName && (
126
+ <p className="text-sm text-muted-foreground">{engineerName}</p>
127
+ )}
128
+ </div>
129
+ <button
130
+ onClick={handleNext}
131
+ className="p-2 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
132
+ >
133
+ →
134
+ </button>
135
+ </div>
136
+
137
+ {/* Calendar Grid */}
138
+ <div className="rounded-lg border bg-card p-4">
139
+ {/* Day labels */}
140
+ <div className="grid grid-cols-7 gap-1 mb-2">
141
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(d => (
142
+ <div key={d} className="text-center text-xs font-medium text-muted-foreground py-1">
143
+ {d}
144
+ </div>
145
+ ))}
146
+ </div>
147
+
148
+ {/* Weeks */}
149
+ <TooltipProvider delayDuration={200}>
150
+ <div className="grid grid-cols-7 gap-1">
151
+ {weeks.flat().map((day, idx) => {
152
+ if (!day) {
153
+ return <div key={`empty-${idx}`} className="aspect-square" />;
154
+ }
155
+
156
+ const isToday = day.date === new Date().toISOString().split('T')[0];
157
+
158
+ return (
159
+ <Tooltip key={day.date}>
160
+ <TooltipTrigger asChild>
161
+ <button
162
+ className={cn(
163
+ 'aspect-square rounded-sm transition-all duration-150 relative flex items-center justify-center text-xs font-medium',
164
+ STATUS_COLORS[day.status],
165
+ day.status === 'off_day' ? 'text-muted-foreground/40' : 'text-white',
166
+ day.status === 'scheduled' && 'text-muted-foreground',
167
+ isToday && 'ring-2 ring-primary ring-offset-1 ring-offset-background',
168
+ day.round ? 'cursor-pointer' : 'cursor-default',
169
+ )}
170
+ onClick={() => {
171
+ if (day.round) {
172
+ router.push(`/rounds/${day.round.id}`);
173
+ }
174
+ }}
175
+ >
176
+ {day.day}
177
+ </button>
178
+ </TooltipTrigger>
179
+ <TooltipContent side="top" className="text-xs">
180
+ <div className="space-y-0.5">
181
+ <p className="font-medium">{day.date} — {STATUS_LABELS[day.status]}</p>
182
+ {day.round && (
183
+ <>
184
+ <p>{day.round.completedDepartments}/{day.round.totalDepartments} departments</p>
185
+ {day.round.totalIssuesReported > 0 && (
186
+ <p>{day.round.totalIssuesReported} issues reported</p>
187
+ )}
188
+ {day.round.totalNotes > 0 && (
189
+ <p>{day.round.totalNotes} notes</p>
190
+ )}
191
+ </>
192
+ )}
193
+ </div>
194
+ </TooltipContent>
195
+ </Tooltip>
196
+ );
197
+ })}
198
+ </div>
199
+ </TooltipProvider>
200
+ </div>
201
+
202
+ {/* Legend & Summary */}
203
+ <div className="flex flex-wrap items-center justify-between gap-4">
204
+ <div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
205
+ <span className="flex items-center gap-1">
206
+ <span className="w-3 h-3 rounded-sm bg-green-500" /> Completed
207
+ </span>
208
+ <span className="flex items-center gap-1">
209
+ <span className="w-3 h-3 rounded-sm bg-yellow-400" /> Partial
210
+ </span>
211
+ <span className="flex items-center gap-1">
212
+ <span className="w-3 h-3 rounded-sm bg-red-400" /> Missed
213
+ </span>
214
+ <span className="flex items-center gap-1">
215
+ <span className="w-3 h-3 rounded-sm bg-muted border" /> Off Day
216
+ </span>
217
+ </div>
218
+ <div className="text-sm font-medium">
219
+ <span className={cn(
220
+ summary.complianceRate >= 80 ? 'text-green-600 dark:text-green-400' :
221
+ summary.complianceRate >= 50 ? 'text-yellow-600 dark:text-yellow-400' :
222
+ 'text-red-600 dark:text-red-400'
223
+ )}>
224
+ {summary.complianceRate}% compliance
225
+ </span>
226
+ <span className="text-muted-foreground ml-1">
227
+ ({summary.completed}/{summary.totalWorkingDays} working days)
228
+ </span>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ );
233
+ }