@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.
- package/LICENSE +22 -0
- package/README.md +203 -2
- package/drizzle.config.ts +11 -0
- package/next.config.ts +51 -0
- package/package.json +99 -3
- package/postcss.config.mjs +7 -0
- package/public/hospital_demo_devices.csv +32 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/manifest.webmanifest +31 -0
- package/public/sw.js +177 -0
- package/src/app/(auth)/layout.tsx +20 -0
- package/src/app/(auth)/login/page.tsx +135 -0
- package/src/app/(dashboard)/admin/audit-logs/page.tsx +178 -0
- package/src/app/(dashboard)/admin/data/page.tsx +37 -0
- package/src/app/(dashboard)/admin/departments/[deptId]/edit/page.tsx +206 -0
- package/src/app/(dashboard)/admin/departments/[deptId]/page.tsx +71 -0
- package/src/app/(dashboard)/admin/departments/new/page.tsx +6 -0
- package/src/app/(dashboard)/admin/departments/page.tsx +6 -0
- package/src/app/(dashboard)/admin/general/page.tsx +374 -0
- package/src/app/(dashboard)/admin/hospitals/[hospitalId]/edit/page.tsx +304 -0
- package/src/app/(dashboard)/admin/hospitals/[hospitalId]/page.tsx +94 -0
- package/src/app/(dashboard)/admin/hospitals/new/page.tsx +302 -0
- package/src/app/(dashboard)/admin/hospitals/page.tsx +6 -0
- package/src/app/(dashboard)/admin/layout.tsx +49 -0
- package/src/app/(dashboard)/admin/organizations/[orgId]/edit/page.tsx +172 -0
- package/src/app/(dashboard)/admin/organizations/[orgId]/page.tsx +82 -0
- package/src/app/(dashboard)/admin/organizations/new/page.tsx +150 -0
- package/src/app/(dashboard)/admin/organizations/page.tsx +6 -0
- package/src/app/(dashboard)/admin/page.tsx +258 -0
- package/src/app/(dashboard)/admin/roles/page.tsx +153 -0
- package/src/app/(dashboard)/admin/settings/page.tsx +69 -0
- package/src/app/(dashboard)/admin/users/[userId]/edit/page.tsx +153 -0
- package/src/app/(dashboard)/admin/users/[userId]/page.tsx +317 -0
- package/src/app/(dashboard)/admin/users/invite/page.tsx +3 -0
- package/src/app/(dashboard)/admin/users/new/page.tsx +5 -0
- package/src/app/(dashboard)/admin/users/page.tsx +23 -0
- package/src/app/(dashboard)/dashboard/page.tsx +462 -0
- package/src/app/(dashboard)/departments/[deptId]/page.tsx +39 -0
- package/src/app/(dashboard)/departments/new/page.tsx +330 -0
- package/src/app/(dashboard)/departments/page.tsx +52 -0
- package/src/app/(dashboard)/devices/[id]/edit/page.tsx +131 -0
- package/src/app/(dashboard)/devices/[id]/page.tsx +408 -0
- package/src/app/(dashboard)/devices/[id]/qr/page.tsx +54 -0
- package/src/app/(dashboard)/devices/new/page.tsx +382 -0
- package/src/app/(dashboard)/devices/page.tsx +80 -0
- package/src/app/(dashboard)/engineering-team/page.tsx +41 -0
- package/src/app/(dashboard)/layout.tsx +43 -0
- package/src/app/(dashboard)/maintenance/checklists/page.tsx +57 -0
- package/src/app/(dashboard)/maintenance/page.tsx +231 -0
- package/src/app/(dashboard)/maintenance/plans/page.tsx +142 -0
- package/src/app/(dashboard)/maintenance/tasks/[id]/page.tsx +997 -0
- package/src/app/(dashboard)/maintenance/tasks/page.tsx +119 -0
- package/src/app/(dashboard)/more/page.tsx +310 -0
- package/src/app/(dashboard)/my-department/page.tsx +151 -0
- package/src/app/(dashboard)/notifications/notification-list.tsx +103 -0
- package/src/app/(dashboard)/notifications/page.tsx +59 -0
- package/src/app/(dashboard)/profile/page.tsx +27 -0
- package/src/app/(dashboard)/reports/costs/page.tsx +127 -0
- package/src/app/(dashboard)/reports/history/page.tsx +105 -0
- package/src/app/(dashboard)/reports/inventory/page.tsx +116 -0
- package/src/app/(dashboard)/reports/maintenance/page.tsx +136 -0
- package/src/app/(dashboard)/reports/page.tsx +120 -0
- package/src/app/(dashboard)/reports/tickets/page.tsx +139 -0
- package/src/app/(dashboard)/reports/workload/page.tsx +135 -0
- package/src/app/(dashboard)/rounds/[id]/page.tsx +197 -0
- package/src/app/(dashboard)/rounds/history/page.tsx +82 -0
- package/src/app/(dashboard)/rounds/page.tsx +107 -0
- package/src/app/(dashboard)/scan/[code]/page.tsx +154 -0
- package/src/app/(dashboard)/scan/page.tsx +73 -0
- package/src/app/(dashboard)/team/page.tsx +42 -0
- package/src/app/(dashboard)/tickets/[id]/page.tsx +465 -0
- package/src/app/(dashboard)/tickets/create/page.tsx +722 -0
- package/src/app/(dashboard)/tickets/page.tsx +55 -0
- package/src/app/(dashboard)/tickets/triage/page.tsx +161 -0
- package/src/app/api/admin/data/export/route.ts +316 -0
- package/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/src/app/api/health/route.ts +46 -0
- package/src/app/api/notifications/sse/route.ts +91 -0
- package/src/app/api/notifications/subscribe/route.ts +54 -0
- package/src/app/api/notifications/vapid-key/route.ts +7 -0
- package/src/app/api/reports/export/route.ts +102 -0
- package/src/app/api/tickets/[id]/attachments/route.ts +129 -0
- package/src/app/api/tickets/[id]/live-sync/route.ts +52 -0
- package/src/app/error.tsx +54 -0
- package/src/app/globals.css +89 -0
- package/src/app/layout.tsx +59 -0
- package/src/app/not-found.tsx +32 -0
- package/src/app/page.tsx +5 -0
- package/src/components/admin/add-department-user-dialog.tsx +759 -0
- package/src/components/admin/add-engineer-dialog.tsx +773 -0
- package/src/components/admin/add-user-type-dialog.tsx +89 -0
- package/src/components/admin/audit-log-export.tsx +113 -0
- package/src/components/admin/audit-log-filters.tsx +255 -0
- package/src/components/admin/audit-log-table.tsx +383 -0
- package/src/components/admin/data-management/danger-zone.tsx +301 -0
- package/src/components/admin/data-management/data-management-tabs.tsx +61 -0
- package/src/components/admin/data-management/export-center.tsx +301 -0
- package/src/components/admin/data-management/import-wizard.tsx +558 -0
- package/src/components/admin/settings-forms/features-form.tsx +115 -0
- package/src/components/admin/settings-forms/health-panel.tsx +182 -0
- package/src/components/admin/settings-forms/localization-form.tsx +203 -0
- package/src/components/admin/settings-forms/retention-form.tsx +164 -0
- package/src/components/admin/settings-forms/security-form.tsx +186 -0
- package/src/components/admin/system-settings-tabs.tsx +88 -0
- package/src/components/admin/user-role-dialog.tsx +293 -0
- package/src/components/admin/user-status-dialog.tsx +166 -0
- package/src/components/admin/users-page-client.tsx +514 -0
- package/src/components/dashboard/action-buttons.tsx +93 -0
- package/src/components/dashboard/admin-alerts.tsx +86 -0
- package/src/components/dashboard/availability-alerts.tsx +65 -0
- package/src/components/dashboard/maintenance-due-list.tsx +87 -0
- package/src/components/dashboard/round-widget.tsx +110 -0
- package/src/components/dashboard/stat-card.tsx +38 -0
- package/src/components/dashboard/ticket-list-compact.tsx +86 -0
- package/src/components/departments/department-cards-view.tsx +244 -0
- package/src/components/departments/department-detail-view.tsx +467 -0
- package/src/components/departments/department-visuals.tsx +190 -0
- package/src/components/departments/my-department-view.tsx +333 -0
- package/src/components/devices/change-status-dialog.tsx +146 -0
- package/src/components/devices/device-action-buttons.tsx +125 -0
- package/src/components/devices/device-availability-banner.tsx +59 -0
- package/src/components/devices/device-badges.tsx +55 -0
- package/src/components/devices/device-documents-tab.tsx +93 -0
- package/src/components/devices/device-maintenance-tab.tsx +113 -0
- package/src/components/devices/device-mini-qr.tsx +109 -0
- package/src/components/devices/device-qr-card.tsx +187 -0
- package/src/components/devices/device-signatures-tab.tsx +607 -0
- package/src/components/devices/device-tickets-tab.tsx +139 -0
- package/src/components/devices/device-timelines.tsx +124 -0
- package/src/components/devices/qr-camera-scanner.tsx +167 -0
- package/src/components/devices/qr-code-display.tsx +139 -0
- package/src/components/devices/quick-add-category-dialog.tsx +135 -0
- package/src/components/devices/quick-add-manufacturer-dialog.tsx +212 -0
- package/src/components/devices/transfer-device-dialog.tsx +190 -0
- package/src/components/layout/header.tsx +80 -0
- package/src/components/layout/mobile-nav.tsx +91 -0
- package/src/components/layout/more-item-badge.tsx +15 -0
- package/src/components/layout/nav-badge.tsx +72 -0
- package/src/components/layout/sidebar.tsx +297 -0
- package/src/components/maintenance/maintenance-badges.tsx +579 -0
- package/src/components/maintenance/maintenance-nav.tsx +107 -0
- package/src/components/maintenance/parts-costs-manager.tsx +909 -0
- package/src/components/maintenance/signature-card.tsx +413 -0
- package/src/components/maintenance/task-action-bar.tsx +955 -0
- package/src/components/notifications/notification-bell.tsx +367 -0
- package/src/components/notifications/notification-item.tsx +93 -0
- package/src/components/notifications/notification-preferences.tsx +182 -0
- package/src/components/profile/user-profile-view.tsx +626 -0
- package/src/components/providers/nav-counts-provider.tsx +235 -0
- package/src/components/providers/session-provider.tsx +7 -0
- package/src/components/providers/theme-provider.tsx +124 -0
- package/src/components/pwa/install-prompt.tsx +82 -0
- package/src/components/pwa/sw-register.tsx +43 -0
- package/src/components/reports/export-button.tsx +62 -0
- package/src/components/reports/filter-chips.tsx +47 -0
- package/src/components/reports/summary-cards.tsx +40 -0
- package/src/components/reports/transparency-header.tsx +54 -0
- package/src/components/rounds/department-visit-sheet.tsx +199 -0
- package/src/components/rounds/mention-textarea.tsx +187 -0
- package/src/components/rounds/round-calendar.tsx +233 -0
- package/src/components/rounds/round-department-list.tsx +281 -0
- package/src/components/rounds/round-history-client.tsx +218 -0
- package/src/components/rounds/start-round-card.tsx +184 -0
- package/src/components/shared/confirm-dialog.tsx +67 -0
- package/src/components/shared/data-table.tsx +104 -0
- package/src/components/shared/page-header.tsx +20 -0
- package/src/components/shared/sign-out-button.tsx +52 -0
- package/src/components/shared/status-badge.tsx +42 -0
- package/src/components/shared/table-search.tsx +55 -0
- package/src/components/signatures/sign-action-dialog.tsx +253 -0
- package/src/components/signatures/signature-history.tsx +121 -0
- package/src/components/signatures/signature-status-panel.tsx +164 -0
- package/src/components/team/department-coverage.tsx +141 -0
- package/src/components/team/edit-engineer-dialog.tsx +242 -0
- package/src/components/team/engineer-card.tsx +180 -0
- package/src/components/team/engineer-details-dialog.tsx +432 -0
- package/src/components/team/schedule-calendar.tsx +204 -0
- package/src/components/team/staff-engineer-team-view.tsx +368 -0
- package/src/components/team/team-header-stats.tsx +88 -0
- package/src/components/team/team-page-client.tsx +216 -0
- package/src/components/theme/theme-selector-card.tsx +63 -0
- package/src/components/theme/theme-toggle.tsx +65 -0
- package/src/components/tickets/staff-status-label.tsx +58 -0
- package/src/components/tickets/staff-ticket-card.tsx +109 -0
- package/src/components/tickets/staff-ticket-progress-tracker.tsx +405 -0
- package/src/components/tickets/staff-tickets-view.tsx +182 -0
- package/src/components/tickets/ticket-action-buttons.tsx +873 -0
- package/src/components/tickets/ticket-auto-refresh.tsx +197 -0
- package/src/components/tickets/ticket-badges.tsx +43 -0
- package/src/components/tickets/ticket-card.tsx +197 -0
- package/src/components/tickets/ticket-cards-view.tsx +395 -0
- package/src/components/tickets/ticket-comment-section.tsx +684 -0
- package/src/components/tickets/ticket-dispatch-status.tsx +184 -0
- package/src/components/tickets/ticket-list-auto-refresh.tsx +55 -0
- package/src/components/tickets/ticket-reporter-profile.tsx +235 -0
- package/src/components/tickets/ticket-signatures-panel.tsx +32 -0
- package/src/components/tickets/ticket-timeline.tsx +131 -0
- package/src/components/tickets/ticket-triage-panel.tsx +124 -0
- package/src/components/tickets/tickets-nav.tsx +107 -0
- package/src/components/ui/avatar.tsx +43 -0
- package/src/components/ui/badge.tsx +34 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +77 -0
- package/src/components/ui/checkbox.tsx +31 -0
- package/src/components/ui/dialog.tsx +123 -0
- package/src/components/ui/dropdown-menu.tsx +126 -0
- package/src/components/ui/form.tsx +179 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/popover.tsx +34 -0
- package/src/components/ui/progress.tsx +36 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +131 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +121 -0
- package/src/components/ui/tabs.tsx +62 -0
- package/src/components/ui/textarea.tsx +23 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/lib/actions/__tests__/devices.test.ts +176 -0
- package/src/lib/actions/__tests__/signatures.test.ts +303 -0
- package/src/lib/actions/__tests__/tickets.test.ts +243 -0
- package/src/lib/actions/audit-logs.ts +298 -0
- package/src/lib/actions/checklists.ts +429 -0
- package/src/lib/actions/dashboard.ts +358 -0
- package/src/lib/actions/data-management.ts +723 -0
- package/src/lib/actions/departments.ts +369 -0
- package/src/lib/actions/device-categories.ts +117 -0
- package/src/lib/actions/devices.ts +748 -0
- package/src/lib/actions/engineer-rounds.ts +973 -0
- package/src/lib/actions/engineers.ts +285 -0
- package/src/lib/actions/hospitals.ts +148 -0
- package/src/lib/actions/locations.ts +115 -0
- package/src/lib/actions/maintenance-plans.ts +316 -0
- package/src/lib/actions/maintenance-tasks.ts +415 -0
- package/src/lib/actions/manufacturers.ts +132 -0
- package/src/lib/actions/nav-counts.ts +144 -0
- package/src/lib/actions/notifications.ts +136 -0
- package/src/lib/actions/organizations.ts +99 -0
- package/src/lib/actions/qr.ts +147 -0
- package/src/lib/actions/report-history.ts +77 -0
- package/src/lib/actions/reports.ts +192 -0
- package/src/lib/actions/roles.ts +32 -0
- package/src/lib/actions/signature-auth.ts +377 -0
- package/src/lib/actions/signatures.ts +532 -0
- package/src/lib/actions/system-settings.ts +274 -0
- package/src/lib/actions/tickets.ts +1445 -0
- package/src/lib/actions/users.ts +740 -0
- package/src/lib/audit/index.ts +103 -0
- package/src/lib/auth/__tests__/rbac.test.ts +33 -0
- package/src/lib/auth/index.ts +338 -0
- package/src/lib/auth/password.ts +10 -0
- package/src/lib/auth/rbac.ts +103 -0
- package/src/lib/auth/types.ts +57 -0
- package/src/lib/constants/departments.ts +16 -0
- package/src/lib/constants/manufacturers.ts +32 -0
- package/src/lib/db/index.ts +23 -0
- package/src/lib/db/schema/audit-logs.ts +42 -0
- package/src/lib/db/schema/departments.ts +39 -0
- package/src/lib/db/schema/device-categories.ts +38 -0
- package/src/lib/db/schema/devices.ts +195 -0
- package/src/lib/db/schema/engineer-profiles.ts +63 -0
- package/src/lib/db/schema/engineer-rounds.ts +115 -0
- package/src/lib/db/schema/enums.ts +148 -0
- package/src/lib/db/schema/hospitals.ts +34 -0
- package/src/lib/db/schema/index.ts +24 -0
- package/src/lib/db/schema/locations.ts +46 -0
- package/src/lib/db/schema/maintenance-plans.ts +99 -0
- package/src/lib/db/schema/maintenance-records.ts +161 -0
- package/src/lib/db/schema/maintenance-tasks.ts +196 -0
- package/src/lib/db/schema/manufacturers.ts +60 -0
- package/src/lib/db/schema/notifications.ts +48 -0
- package/src/lib/db/schema/organizations.ts +24 -0
- package/src/lib/db/schema/report-history.ts +28 -0
- package/src/lib/db/schema/roles.ts +59 -0
- package/src/lib/db/schema/sessions.ts +46 -0
- package/src/lib/db/schema/signature-events.ts +48 -0
- package/src/lib/db/schema/signature-policies.ts +60 -0
- package/src/lib/db/schema/signatures.ts +87 -0
- package/src/lib/db/schema/tickets.ts +169 -0
- package/src/lib/db/schema/user-assignments.ts +85 -0
- package/src/lib/db/schema/users.ts +39 -0
- package/src/lib/db/seed.ts +131 -0
- package/src/lib/logger/__tests__/logger.test.ts +154 -0
- package/src/lib/logger/formatters.ts +159 -0
- package/src/lib/logger/index.ts +184 -0
- package/src/lib/logger/types.ts +48 -0
- package/src/lib/notifications/create.ts +71 -0
- package/src/lib/notifications/dispatcher.ts +490 -0
- package/src/lib/notifications/index.ts +7 -0
- package/src/lib/notifications/push-sender.ts +128 -0
- package/src/lib/notifications/push.ts +64 -0
- package/src/lib/notifications/triggers.ts +166 -0
- package/src/lib/utils/__tests__/crypto.test.ts +30 -0
- package/src/lib/utils/__tests__/export.test.ts +55 -0
- package/src/lib/utils/__tests__/nav-badge.test.ts +147 -0
- package/src/lib/utils/__tests__/staff-status.test.ts +50 -0
- package/src/lib/utils/attestations.ts +19 -0
- package/src/lib/utils/crypto.ts +18 -0
- package/src/lib/utils/csv-parser.ts +181 -0
- package/src/lib/utils/export.ts +45 -0
- package/src/lib/utils/nav-badge.ts +225 -0
- package/src/lib/utils/sound.ts +152 -0
- package/src/lib/utils/staff-status.ts +115 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/__tests__/auth.test.ts +127 -0
- package/src/lib/validators/__tests__/data-management.test.ts +182 -0
- package/src/lib/validators/__tests__/devices-profile.test.ts +129 -0
- package/src/lib/validators/__tests__/devices.test.ts +98 -0
- package/src/lib/validators/__tests__/engineer-rounds.test.ts +170 -0
- package/src/lib/validators/__tests__/maintenance.test.ts +178 -0
- package/src/lib/validators/__tests__/signatures.test.ts +154 -0
- package/src/lib/validators/__tests__/system-settings.test.ts +119 -0
- package/src/lib/validators/__tests__/tickets.test.ts +176 -0
- package/src/lib/validators/__tests__/users.test.ts +92 -0
- package/src/lib/validators/auth.ts +32 -0
- package/src/lib/validators/data-management.ts +67 -0
- package/src/lib/validators/departments.ts +14 -0
- package/src/lib/validators/devices.ts +94 -0
- package/src/lib/validators/engineer-rounds.ts +70 -0
- package/src/lib/validators/hospitals.ts +17 -0
- package/src/lib/validators/locations.ts +15 -0
- package/src/lib/validators/maintenance.ts +211 -0
- package/src/lib/validators/organizations.ts +11 -0
- package/src/lib/validators/reports.ts +31 -0
- package/src/lib/validators/signatures.ts +49 -0
- package/src/lib/validators/system-settings.ts +55 -0
- package/src/lib/validators/tickets.ts +125 -0
- package/src/lib/validators/users.ts +43 -0
- package/src/middleware.ts +45 -0
- package/tailwind.config.ts +76 -0
- 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
|
+
|