@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,159 @@
1
+ import { LogEntry, LogLevel } from './types';
2
+
3
+ const ANSI = {
4
+ reset: '\x1b[0m',
5
+ bold: '\x1b[1m',
6
+ dim: '\x1b[2m',
7
+ cyan: '\x1b[36m',
8
+ green: '\x1b[32m',
9
+ yellow: '\x1b[33m',
10
+ red: '\x1b[31m',
11
+ magenta: '\x1b[35m',
12
+ gray: '\x1b[90m',
13
+ };
14
+
15
+ const LEVEL_COLORS: Record<LogLevel, string> = {
16
+ debug: ANSI.cyan,
17
+ info: ANSI.green,
18
+ warn: ANSI.yellow,
19
+ error: ANSI.red,
20
+ fatal: `${ANSI.bold}${ANSI.magenta}`,
21
+ };
22
+
23
+ const MAX_VALUE_LENGTH = 1000;
24
+
25
+ /**
26
+ * Returns a JSON replacer function that safely handles circular references and BigInt values.
27
+ */
28
+ function getCircularReplacer(): (key: string, value: unknown) => unknown {
29
+ const seen = new WeakSet();
30
+ return (_key: string, value: unknown) => {
31
+ if (typeof value === 'object' && value !== null) {
32
+ if (seen.has(value)) {
33
+ return '[Circular]';
34
+ }
35
+ seen.add(value);
36
+ }
37
+ if (typeof value === 'bigint') {
38
+ return value.toString();
39
+ }
40
+ return value;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Formats a Date/ISO timestamp to local HH:mm:ss.SSS representation.
46
+ */
47
+ function formatTimestamp(isoString: string): string {
48
+ try {
49
+ const date = new Date(isoString);
50
+ if (Number.isNaN(date.getTime())) {
51
+ return isoString;
52
+ }
53
+ const hours = String(date.getHours()).padStart(2, '0');
54
+ const minutes = String(date.getMinutes()).padStart(2, '0');
55
+ const seconds = String(date.getSeconds()).padStart(2, '0');
56
+ const millis = String(date.getMilliseconds()).padStart(3, '0');
57
+ return `${hours}:${minutes}:${seconds}.${millis}`;
58
+ } catch {
59
+ return isoString;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Truncates string representations that exceed 1000 characters.
65
+ */
66
+ function truncateValue(str: string): string {
67
+ if (str.length > MAX_VALUE_LENGTH) {
68
+ return `${str.slice(0, MAX_VALUE_LENGTH)}... [truncated]`;
69
+ }
70
+ return str;
71
+ }
72
+
73
+ /**
74
+ * Formats a single context property value safely, truncating if necessary.
75
+ */
76
+ function formatContextValue(value: unknown): string {
77
+ if (value === null) return 'null';
78
+ if (value === undefined) return 'undefined';
79
+ if (typeof value === 'string') return truncateValue(value);
80
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
81
+ return String(value);
82
+ }
83
+ if (typeof value === 'symbol') return value.toString();
84
+ if (typeof value === 'function') return '[Function]';
85
+ if (typeof value === 'object') {
86
+ try {
87
+ const json = JSON.stringify(value, getCircularReplacer());
88
+ return truncateValue(json);
89
+ } catch {
90
+ return truncateValue(String(value));
91
+ }
92
+ }
93
+ return truncateValue(String(value));
94
+ }
95
+
96
+ /**
97
+ * Single-line JSON formatter for production environments (machine-parseable).
98
+ * Handles circular references and never throws.
99
+ */
100
+ export function jsonFormatter(entry: LogEntry): string {
101
+ try {
102
+ return JSON.stringify(entry, getCircularReplacer());
103
+ } catch (error) {
104
+ return JSON.stringify({
105
+ level: entry.level,
106
+ message: entry.message,
107
+ timestamp: entry.timestamp,
108
+ module: entry.module,
109
+ formatError: error instanceof Error ? error.message : String(error),
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Colorized, human-readable formatter for development environments using ANSI escape codes.
116
+ *
117
+ * Output format:
118
+ * HH:mm:ss.SSS LEVEL [module] message
119
+ * key=value
120
+ * error stack
121
+ */
122
+ export function prettyFormatter(entry: LogEntry): string {
123
+ try {
124
+ const timeStr = `${ANSI.gray}${formatTimestamp(entry.timestamp)}${ANSI.reset}`;
125
+ const levelColor = LEVEL_COLORS[entry.level] || ANSI.reset;
126
+ const levelUpper = entry.level.toUpperCase().padEnd(5);
127
+ const levelStr = `${levelColor}${levelUpper}${ANSI.reset}`;
128
+ const moduleStr = entry.module ? `${ANSI.gray}[${entry.module}]${ANSI.reset} ` : '';
129
+ const messageStr = entry.message;
130
+
131
+ let output = `${timeStr} ${levelStr} ${moduleStr}${messageStr}`;
132
+
133
+ // Format context key=value pairs
134
+ if (entry.context && Object.keys(entry.context).length > 0) {
135
+ for (const [key, val] of Object.entries(entry.context)) {
136
+ output += `\n ${ANSI.dim}${key}=${ANSI.reset}${formatContextValue(val)}`;
137
+ }
138
+ }
139
+
140
+ // Format any extra top-level structured attributes
141
+ const standardKeys = new Set(['level', 'message', 'timestamp', 'module', 'context', 'error']);
142
+ for (const [key, val] of Object.entries(entry)) {
143
+ if (!standardKeys.has(key)) {
144
+ output += `\n ${ANSI.dim}${key}=${ANSI.reset}${formatContextValue(val)}`;
145
+ }
146
+ }
147
+
148
+ // Format error stack if present
149
+ if (entry.error) {
150
+ const errorContent = entry.error.stack || `${entry.error.name}: ${entry.error.message}`;
151
+ output += `\n${ANSI.red}${errorContent}${ANSI.reset}`;
152
+ }
153
+
154
+ return output;
155
+ } catch (err) {
156
+ // Fail-safe plain string output
157
+ return `${entry.timestamp} ${entry.level.toUpperCase()} ${entry.message}`;
158
+ }
159
+ }
@@ -0,0 +1,184 @@
1
+ import { Logger, LoggerOptions, LogEntry, LogLevel } from './types';
2
+ import { jsonFormatter, prettyFormatter } from './formatters';
3
+
4
+ export * from './types';
5
+ export { jsonFormatter, prettyFormatter } from './formatters';
6
+
7
+ const LOG_LEVELS: Record<LogLevel, number> = {
8
+ debug: 0,
9
+ info: 1,
10
+ warn: 2,
11
+ error: 3,
12
+ fatal: 4,
13
+ };
14
+
15
+ /**
16
+ * Safely parses and normalizes log level string.
17
+ */
18
+ function parseLogLevel(levelStr?: string): LogLevel | undefined {
19
+ if (!levelStr) return undefined;
20
+ const normalized = levelStr.toLowerCase() as LogLevel;
21
+ return normalized in LOG_LEVELS ? normalized : undefined;
22
+ }
23
+
24
+ // Determine environment
25
+ const isDev = process.env.NODE_ENV !== 'production';
26
+ const configuredLevel: LogLevel =
27
+ parseLogLevel(process.env.LOG_LEVEL) || (isDev ? 'debug' : 'info');
28
+ const formatter = isDev ? prettyFormatter : jsonFormatter;
29
+
30
+ /**
31
+ * Safely serializes an unknown error into structured name, message, stack details.
32
+ */
33
+ function serializeError(err: unknown): { name: string; message: string; stack?: string } | undefined {
34
+ if (!err) return undefined;
35
+
36
+ if (err instanceof Error) {
37
+ return {
38
+ name: err.name || 'Error',
39
+ message: err.message,
40
+ stack: err.stack,
41
+ };
42
+ }
43
+
44
+ if (typeof err === 'object') {
45
+ const errorObj = err as Record<string, unknown>;
46
+ return {
47
+ name: typeof errorObj.name === 'string' ? errorObj.name : 'UnknownError',
48
+ message: typeof errorObj.message === 'string' ? errorObj.message : String(err),
49
+ stack: typeof errorObj.stack === 'string' ? errorObj.stack : undefined,
50
+ };
51
+ }
52
+
53
+ return {
54
+ name: 'Error',
55
+ message: String(err),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Factory function creating a structured logger instance.
61
+ *
62
+ * @param options - Configuration options including module name, base context, and min log level.
63
+ * @returns A structured Logger instance.
64
+ */
65
+ export function createLogger(options?: LoggerOptions): Logger {
66
+ const minLevel: LogLevel = options?.minLevel || configuredLevel;
67
+ const minLevelNum = LOG_LEVELS[minLevel] ?? LOG_LEVELS.info;
68
+
69
+ const shouldLog = (level: LogLevel): boolean => {
70
+ const levelNum = LOG_LEVELS[level] ?? 0;
71
+ return levelNum >= minLevelNum;
72
+ };
73
+
74
+ const emit = (
75
+ level: LogLevel,
76
+ message: string,
77
+ err?: unknown,
78
+ data?: Record<string, unknown>
79
+ ): void => {
80
+ try {
81
+ if (!shouldLog(level)) return;
82
+
83
+ let parsedError: unknown = err;
84
+ let extraData = data;
85
+
86
+ // Detect if caller supplied a plain data object as the error argument
87
+ if (
88
+ err !== undefined &&
89
+ data === undefined &&
90
+ typeof err === 'object' &&
91
+ err !== null &&
92
+ !(err instanceof Error) &&
93
+ !('name' in err || 'message' in err || 'stack' in err)
94
+ ) {
95
+ extraData = err as Record<string, unknown>;
96
+ parsedError = undefined;
97
+ }
98
+
99
+ const mergedContext =
100
+ options?.context || extraData
101
+ ? { ...options?.context, ...extraData }
102
+ : undefined;
103
+
104
+ const entry: LogEntry = {
105
+ level,
106
+ message,
107
+ timestamp: new Date().toISOString(),
108
+ module: options?.module,
109
+ context: mergedContext,
110
+ error: serializeError(parsedError),
111
+ };
112
+
113
+ const formatted = formatter(entry);
114
+
115
+ switch (level) {
116
+ case 'debug':
117
+ case 'info':
118
+ console.log(formatted);
119
+ break;
120
+ case 'warn':
121
+ console.warn(formatted);
122
+ break;
123
+ case 'error':
124
+ case 'fatal':
125
+ console.error(formatted);
126
+ break;
127
+ }
128
+ } catch {
129
+ // Logger must never throw errors under any circumstances
130
+ try {
131
+ console.error(`[LOGGER FALLBACK] [${level.toUpperCase()}] ${message}`);
132
+ } catch {
133
+ // Silently swallow in ultimate fallback
134
+ }
135
+ }
136
+ };
137
+
138
+ return {
139
+ debug(message: string, data?: Record<string, unknown>): void {
140
+ emit('debug', message, undefined, data);
141
+ },
142
+ info(message: string, data?: Record<string, unknown>): void {
143
+ emit('info', message, undefined, data);
144
+ },
145
+ warn(message: string, data?: Record<string, unknown>): void {
146
+ emit('warn', message, undefined, data);
147
+ },
148
+ error(message: string, error?: unknown, data?: Record<string, unknown>): void {
149
+ emit('error', message, error, data);
150
+ },
151
+ fatal(message: string, error?: unknown, data?: Record<string, unknown>): void {
152
+ emit('fatal', message, error, data);
153
+ },
154
+ child(childOptions: { module?: string; context?: Record<string, unknown> }): Logger {
155
+ return createLogger({
156
+ module: childOptions.module
157
+ ? options?.module
158
+ ? `${options.module}:${childOptions.module}`
159
+ : childOptions.module
160
+ : options?.module,
161
+ context: {
162
+ ...options?.context,
163
+ ...childOptions.context,
164
+ },
165
+ minLevel: options?.minLevel,
166
+ });
167
+ },
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Root logger instance pre-configured for the application.
173
+ */
174
+ export const logger = createLogger({ module: 'app' });
175
+
176
+ /**
177
+ * Convenience function to create a logger instance for a specific module and context.
178
+ *
179
+ * @param module - Module identifier (e.g., 'auth', 'database', 'billing')
180
+ * @param context - Optional default contextual data
181
+ */
182
+ export function getLogger(module: string, context?: Record<string, unknown>): Logger {
183
+ return createLogger({ module, context });
184
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Structured Logger Types
3
+ * Zero-dependency, production-grade logging types for BEMMS application.
4
+ */
5
+
6
+ /**
7
+ * Log severity levels in increasing order of severity.
8
+ */
9
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
10
+
11
+ /**
12
+ * Structured log entry representation.
13
+ */
14
+ export interface LogEntry {
15
+ level: LogLevel;
16
+ message: string;
17
+ timestamp: string; // ISO 8601
18
+ module?: string;
19
+ context?: Record<string, unknown>;
20
+ error?: {
21
+ name: string;
22
+ message: string;
23
+ stack?: string;
24
+ };
25
+ // Additional structured data
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ /**
30
+ * Configuration options for initializing or deriving a logger instance.
31
+ */
32
+ export interface LoggerOptions {
33
+ module?: string;
34
+ context?: Record<string, unknown>;
35
+ minLevel?: LogLevel;
36
+ }
37
+
38
+ /**
39
+ * Structured logger interface.
40
+ */
41
+ export interface Logger {
42
+ debug(message: string, data?: Record<string, unknown>): void;
43
+ info(message: string, data?: Record<string, unknown>): void;
44
+ warn(message: string, data?: Record<string, unknown>): void;
45
+ error(message: string, error?: unknown, data?: Record<string, unknown>): void;
46
+ fatal(message: string, error?: unknown, data?: Record<string, unknown>): void;
47
+ child(options: { module?: string; context?: Record<string, unknown> }): Logger;
48
+ }
@@ -0,0 +1,71 @@
1
+ import { db } from '@/lib/db';
2
+ import { notifications } from '@/lib/db/schema';
3
+ import { users } from '@/lib/db/schema';
4
+ import { eq } from 'drizzle-orm';
5
+ import { sendPushToUser, type PushPayload } from './push';
6
+
7
+ interface CreateNotificationParams {
8
+ organizationId: string;
9
+ recipientUserId: string;
10
+ notificationType: 'ticket_assigned' | 'ticket_updated' | 'ticket_comment' |
11
+ 'pm_due_soon' | 'pm_overdue' | 'calibration_due' |
12
+ 'task_review_required' | 'task_approved' | 'task_rejected' |
13
+ 'device_status_changed' | 'sla_warning' | 'system_alert';
14
+ title: string;
15
+ body: string;
16
+ relatedEntityType?: string;
17
+ relatedEntityId?: string;
18
+ actionUrl?: string;
19
+ }
20
+
21
+ // Map notification types to preference categories
22
+ function getCategory(type: string): 'tickets' | 'maintenance' | 'devices' | 'system' {
23
+ if (type.startsWith('ticket_')) return 'tickets';
24
+ if (type.startsWith('pm_') || type.startsWith('task_') || type.startsWith('calibration_')) return 'maintenance';
25
+ if (type.startsWith('device_')) return 'devices';
26
+ return 'system';
27
+ }
28
+
29
+ export async function createNotification(params: CreateNotificationParams) {
30
+ // Insert notification into DB
31
+ const [notification] = await db.insert(notifications).values({
32
+ organizationId: params.organizationId,
33
+ recipientUserId: params.recipientUserId,
34
+ notificationType: params.notificationType,
35
+ title: params.title,
36
+ body: params.body,
37
+ relatedEntityType: params.relatedEntityType,
38
+ relatedEntityId: params.relatedEntityId,
39
+ actionUrl: params.actionUrl,
40
+ }).returning();
41
+
42
+ // Check user's notification preferences for push
43
+ const user = await db.query.users.findFirst({
44
+ where: eq(users.id, params.recipientUserId),
45
+ columns: { notificationPreferences: true },
46
+ });
47
+
48
+ const prefs = user?.notificationPreferences;
49
+ const category = getCategory(params.notificationType);
50
+
51
+ // Send push if enabled
52
+ const pushEnabled = prefs?.pushEnabled !== false; // Default to true
53
+ const categoryEnabled = prefs?.categories?.[category] !== false; // Default to true
54
+
55
+ if (pushEnabled && categoryEnabled) {
56
+ const payload: PushPayload = {
57
+ title: params.title,
58
+ body: params.body,
59
+ url: params.actionUrl || '/notifications',
60
+ tag: `${params.notificationType}-${params.relatedEntityId || notification.id}`,
61
+ };
62
+
63
+ // Fire and forget — don't block on push delivery
64
+ sendPushToUser(params.recipientUserId, payload).catch((err) => {
65
+ console.error('[Notification] Push delivery failed:', err);
66
+ });
67
+ }
68
+
69
+ return notification;
70
+ }
71
+