@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,909 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useState, useTransition } from "react"
5
+ import { useRouter } from "next/navigation"
6
+ import {
7
+ Table,
8
+ TableHeader,
9
+ TableBody,
10
+ TableFooter,
11
+ TableHead,
12
+ TableRow,
13
+ TableCell,
14
+ } from "@/components/ui/table"
15
+ import { Button } from "@/components/ui/button"
16
+ import { Input } from "@/components/ui/input"
17
+ import {
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from "@/components/ui/select"
24
+ import {
25
+ Card,
26
+ CardHeader,
27
+ CardTitle,
28
+ CardDescription,
29
+ CardContent,
30
+ } from "@/components/ui/card"
31
+ import { Badge } from "@/components/ui/badge"
32
+ import {
33
+ addMaintenancePart,
34
+ deleteMaintenancePart,
35
+ addMaintenanceCost,
36
+ deleteMaintenanceCost,
37
+ } from "@/lib/actions/checklists"
38
+ import { cn } from "@/lib/utils"
39
+ import {
40
+ Plus,
41
+ Trash2,
42
+ Package,
43
+ Receipt,
44
+ DollarSign,
45
+ AlertCircle,
46
+ X,
47
+ Check,
48
+ Calculator,
49
+ Loader2,
50
+ } from "lucide-react"
51
+
52
+ // ── Types & Helpers ──────────────────────────────────────────────────────────
53
+
54
+ export interface MaintenancePartItem {
55
+ id: string
56
+ maintenanceTaskId?: string
57
+ maintenanceRecordId?: string | null
58
+ partNumber: string
59
+ partName: string
60
+ quantity: number
61
+ unitCost?: number | null
62
+ currency?: string | null
63
+ supplierName?: string | null
64
+ notes?: string | null
65
+ createdAt?: Date | string | null
66
+ }
67
+
68
+ export interface MaintenanceCostItem {
69
+ id: string
70
+ maintenanceTaskId?: string
71
+ maintenanceRecordId?: string | null
72
+ costType: "part" | "vendor_service" | "labor" | "transport" | "other" | string
73
+ amount: number
74
+ currency?: string | null
75
+ vendorName?: string | null
76
+ invoiceNumber?: string | null
77
+ description?: string | null
78
+ recordedAt?: Date | string | null
79
+ }
80
+
81
+ const COST_TYPE_LABELS: Record<string, string> = {
82
+ part: "Part / Consumable",
83
+ vendor_service: "Vendor Service",
84
+ labor: "Labor",
85
+ transport: "Transport / Freight",
86
+ other: "Other",
87
+ }
88
+
89
+ const COST_TYPE_BADGES: Record<string, string> = {
90
+ part: "bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800",
91
+ vendor_service: "bg-purple-100 text-purple-800 border-purple-300 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800",
92
+ labor: "bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800",
93
+ transport: "bg-teal-100 text-teal-800 border-teal-300 dark:bg-teal-950 dark:text-teal-300 dark:border-teal-800",
94
+ other: "bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700",
95
+ }
96
+
97
+ /**
98
+ * Formats an integer amount in cents into currency (e.g. 1250 cents -> "$12.50").
99
+ */
100
+ export function formatCurrency(cents?: number | null, currency = "USD"): string {
101
+ if (cents === undefined || cents === null || isNaN(cents)) return "$0.00"
102
+ const dollars = cents / 100
103
+ try {
104
+ return new Intl.NumberFormat("en-US", {
105
+ style: "currency",
106
+ currency: currency || "USD",
107
+ minimumFractionDigits: 2,
108
+ maximumFractionDigits: 2,
109
+ }).format(dollars)
110
+ } catch {
111
+ return `$${dollars.toFixed(2)}`
112
+ }
113
+ }
114
+
115
+ // ── 1. PartsManager Component ─────────────────────────────────────────────────
116
+
117
+ export interface PartsManagerProps {
118
+ taskId: string
119
+ parts: MaintenancePartItem[] | any[]
120
+ editable: boolean
121
+ className?: string
122
+ }
123
+
124
+ export function PartsManager({
125
+ taskId,
126
+ parts = [],
127
+ editable = true,
128
+ className,
129
+ }: PartsManagerProps) {
130
+ const router = useRouter()
131
+ const [isPending, startTransition] = useTransition()
132
+ const [isAdding, setIsAdding] = useState(false)
133
+ const [deletingId, setDeletingId] = useState<string | null>(null)
134
+ const [error, setError] = useState<string | null>(null)
135
+
136
+ // Inline form state
137
+ const [partNumber, setPartNumber] = useState("")
138
+ const [partName, setPartName] = useState("")
139
+ const [quantity, setQuantity] = useState("1")
140
+ const [unitCostInput, setUnitCostInput] = useState("")
141
+ const [supplierName, setSupplierName] = useState("")
142
+ const [notes, setNotes] = useState("")
143
+
144
+ const resetForm = () => {
145
+ setPartNumber("")
146
+ setPartName("")
147
+ setQuantity("1")
148
+ setUnitCostInput("")
149
+ setSupplierName("")
150
+ setNotes("")
151
+ setError(null)
152
+ setIsAdding(false)
153
+ }
154
+
155
+ // Calculate live total for parts
156
+ const totalPartsCost = parts.reduce((sum, p) => {
157
+ const qty = Number(p.quantity) || 1
158
+ const cost = Number(p.unitCost) || 0
159
+ return sum + cost * qty
160
+ }, 0)
161
+
162
+ const handleAddPart = async (e: React.FormEvent) => {
163
+ e.preventDefault()
164
+ if (!partNumber.trim()) {
165
+ setError("Part number is required")
166
+ return
167
+ }
168
+ if (!partName.trim()) {
169
+ setError("Part name is required")
170
+ return
171
+ }
172
+
173
+ const qtyNum = parseInt(quantity, 10)
174
+ if (isNaN(qtyNum) || qtyNum < 1) {
175
+ setError("Quantity must be at least 1")
176
+ return
177
+ }
178
+
179
+ let unitCostCents: number | undefined = undefined
180
+ if (unitCostInput.trim() !== "") {
181
+ const parsedDollars = parseFloat(unitCostInput)
182
+ if (isNaN(parsedDollars) || parsedDollars < 0) {
183
+ setError("Unit cost must be a valid non-negative number")
184
+ return
185
+ }
186
+ unitCostCents = Math.round(parsedDollars * 100)
187
+ }
188
+
189
+ setError(null)
190
+
191
+ startTransition(async () => {
192
+ const res = await addMaintenancePart(taskId, {
193
+ partNumber: partNumber.trim(),
194
+ partName: partName.trim(),
195
+ quantity: qtyNum,
196
+ unitCost: unitCostCents,
197
+ currency: "USD",
198
+ supplierName: supplierName.trim() || undefined,
199
+ notes: notes.trim() || undefined,
200
+ })
201
+
202
+ if (res?.success) {
203
+ resetForm()
204
+ router.refresh()
205
+ } else {
206
+ setError(res?.error || "Failed to add spare part")
207
+ }
208
+ })
209
+ }
210
+
211
+ const handleDeletePart = async (partId: string) => {
212
+ setError(null)
213
+ setDeletingId(partId)
214
+
215
+ startTransition(async () => {
216
+ const res = await deleteMaintenancePart(partId)
217
+ if (res?.success) {
218
+ router.refresh()
219
+ } else {
220
+ setError(res?.error || "Failed to delete spare part")
221
+ }
222
+ setDeletingId(null)
223
+ })
224
+ }
225
+
226
+ return (
227
+ <div className={cn("space-y-4", className)}>
228
+ <div className="flex items-center justify-between">
229
+ <div className="flex items-center gap-2">
230
+ <Package className="h-5 w-5 text-primary" />
231
+ <h3 className="text-base font-semibold text-foreground">Spare Parts Used</h3>
232
+ <Badge variant="secondary" className="text-xs font-medium">
233
+ {parts.length} {parts.length === 1 ? "item" : "items"}
234
+ </Badge>
235
+ </div>
236
+ {editable && !isAdding && (
237
+ <Button
238
+ type="button"
239
+ size="sm"
240
+ onClick={() => {
241
+ setError(null)
242
+ setIsAdding(true)
243
+ }}
244
+ className="h-8 gap-1 text-xs"
245
+ >
246
+ <Plus className="h-3.5 w-3.5" />
247
+ Add Part
248
+ </Button>
249
+ )}
250
+ </div>
251
+
252
+ {error && (
253
+ <div className="flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-xs text-destructive">
254
+ <AlertCircle className="h-4 w-4 shrink-0" />
255
+ <span>{error}</span>
256
+ <Button
257
+ type="button"
258
+ variant="ghost"
259
+ size="icon"
260
+ onClick={() => setError(null)}
261
+ className="ml-auto h-5 w-5 text-destructive hover:bg-transparent"
262
+ >
263
+ <X className="h-3 w-3" />
264
+ </Button>
265
+ </div>
266
+ )}
267
+
268
+ <div className="overflow-hidden rounded-lg border bg-card shadow-sm">
269
+ <Table>
270
+ <TableHeader>
271
+ <TableRow className="bg-muted/50 hover:bg-muted/50">
272
+ <TableHead className="w-[140px] font-semibold">Part #</TableHead>
273
+ <TableHead className="font-semibold">Part Name</TableHead>
274
+ <TableHead className="w-[70px] text-center font-semibold">Qty</TableHead>
275
+ <TableHead className="w-[110px] text-right font-semibold">Unit Cost</TableHead>
276
+ <TableHead className="w-[110px] text-right font-semibold">Total</TableHead>
277
+ <TableHead className="w-[160px] font-semibold">Supplier</TableHead>
278
+ {editable && <TableHead className="w-[60px] text-center font-semibold">Action</TableHead>}
279
+ </TableRow>
280
+ </TableHeader>
281
+ <TableBody>
282
+ {parts.length === 0 && !isAdding ? (
283
+ <TableRow>
284
+ <TableCell
285
+ colSpan={editable ? 7 : 6}
286
+ className="h-24 text-center text-sm text-muted-foreground"
287
+ >
288
+ No spare parts recorded for this task.
289
+ </TableCell>
290
+ </TableRow>
291
+ ) : (
292
+ parts.map((part) => {
293
+ const qty = Number(part.quantity) || 1
294
+ const unitCost = Number(part.unitCost) || 0
295
+ const rowTotal = qty * unitCost
296
+ const isDeleting = deletingId === part.id
297
+
298
+ return (
299
+ <TableRow key={part.id} className="hover:bg-muted/30">
300
+ <TableCell className="font-mono text-xs font-medium text-foreground">
301
+ {part.partNumber}
302
+ </TableCell>
303
+ <TableCell>
304
+ <div className="text-sm font-medium text-foreground">{part.partName}</div>
305
+ {part.notes && (
306
+ <div className="text-xs text-muted-foreground line-clamp-1">{part.notes}</div>
307
+ )}
308
+ </TableCell>
309
+ <TableCell className="text-center text-sm font-medium">
310
+ {qty}
311
+ </TableCell>
312
+ <TableCell className="text-right font-mono text-xs text-muted-foreground">
313
+ {formatCurrency(part.unitCost, part.currency || "USD")}
314
+ </TableCell>
315
+ <TableCell className="text-right font-mono text-xs font-semibold text-foreground">
316
+ {formatCurrency(rowTotal, part.currency || "USD")}
317
+ </TableCell>
318
+ <TableCell className="text-xs text-muted-foreground">
319
+ {part.supplierName || "—"}
320
+ </TableCell>
321
+ {editable && (
322
+ <TableCell className="text-center">
323
+ <Button
324
+ type="button"
325
+ variant="ghost"
326
+ size="icon"
327
+ disabled={isDeleting || isPending}
328
+ onClick={() => handleDeletePart(part.id)}
329
+ className="h-7 w-7 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
330
+ title="Delete part"
331
+ >
332
+ {isDeleting ? (
333
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
334
+ ) : (
335
+ <Trash2 className="h-3.5 w-3.5" />
336
+ )}
337
+ </Button>
338
+ </TableCell>
339
+ )}
340
+ </TableRow>
341
+ )
342
+ })
343
+ )}
344
+
345
+ {/* Inline Add Part Form */}
346
+ {isAdding && (
347
+ <TableRow className="bg-primary/5 border-primary/20">
348
+ <TableCell className="p-2">
349
+ <Input
350
+ placeholder="Part #"
351
+ value={partNumber}
352
+ onChange={(e) => setPartNumber(e.target.value)}
353
+ disabled={isPending}
354
+ className="h-8 text-xs font-mono"
355
+ autoFocus
356
+ />
357
+ </TableCell>
358
+ <TableCell className="p-2">
359
+ <Input
360
+ placeholder="Part Name"
361
+ value={partName}
362
+ onChange={(e) => setPartName(e.target.value)}
363
+ disabled={isPending}
364
+ className="h-8 text-xs"
365
+ />
366
+ </TableCell>
367
+ <TableCell className="p-2">
368
+ <Input
369
+ type="number"
370
+ min="1"
371
+ placeholder="1"
372
+ value={quantity}
373
+ onChange={(e) => setQuantity(e.target.value)}
374
+ disabled={isPending}
375
+ className="h-8 text-xs text-center"
376
+ />
377
+ </TableCell>
378
+ <TableCell className="p-2">
379
+ <Input
380
+ type="number"
381
+ step="0.01"
382
+ min="0"
383
+ placeholder="0.00"
384
+ value={unitCostInput}
385
+ onChange={(e) => setUnitCostInput(e.target.value)}
386
+ disabled={isPending}
387
+ className="h-8 text-xs text-right font-mono"
388
+ />
389
+ </TableCell>
390
+ <TableCell className="p-2 text-right font-mono text-xs font-semibold text-muted-foreground">
391
+ {formatCurrency(
392
+ (parseInt(quantity || "0", 10) || 0) *
393
+ Math.round((parseFloat(unitCostInput || "0") || 0) * 100)
394
+ )}
395
+ </TableCell>
396
+ <TableCell className="p-2">
397
+ <Input
398
+ placeholder="Supplier (optional)"
399
+ value={supplierName}
400
+ onChange={(e) => setSupplierName(e.target.value)}
401
+ disabled={isPending}
402
+ className="h-8 text-xs"
403
+ />
404
+ </TableCell>
405
+ <TableCell className="p-2 text-center">
406
+ <div className="flex items-center justify-center gap-1">
407
+ <Button
408
+ type="button"
409
+ size="icon"
410
+ variant="default"
411
+ disabled={isPending}
412
+ onClick={handleAddPart}
413
+ className="h-7 w-7 bg-primary text-primary-foreground"
414
+ title="Save part"
415
+ >
416
+ {isPending ? (
417
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
418
+ ) : (
419
+ <Check className="h-3.5 w-3.5" />
420
+ )}
421
+ </Button>
422
+ <Button
423
+ type="button"
424
+ size="icon"
425
+ variant="ghost"
426
+ disabled={isPending}
427
+ onClick={resetForm}
428
+ className="h-7 w-7 text-muted-foreground hover:bg-muted"
429
+ title="Cancel"
430
+ >
431
+ <X className="h-3.5 w-3.5" />
432
+ </Button>
433
+ </div>
434
+ </TableCell>
435
+ </TableRow>
436
+ )}
437
+ </TableBody>
438
+
439
+ <TableFooter>
440
+ <TableRow className="bg-muted/30 font-semibold">
441
+ <TableCell colSpan={4} className="text-right text-xs uppercase tracking-wider text-muted-foreground">
442
+ Total Spare Parts Cost:
443
+ </TableCell>
444
+ <TableCell className="text-right font-mono text-sm font-bold text-foreground">
445
+ {formatCurrency(totalPartsCost)}
446
+ </TableCell>
447
+ <TableCell colSpan={editable ? 2 : 1} />
448
+ </TableRow>
449
+ </TableFooter>
450
+ </Table>
451
+ </div>
452
+ </div>
453
+ )
454
+ }
455
+
456
+ // ── 2. CostsManager Component ─────────────────────────────────────────────────
457
+
458
+ export interface CostsManagerProps {
459
+ taskId: string
460
+ costs: MaintenanceCostItem[] | any[]
461
+ editable: boolean
462
+ className?: string
463
+ }
464
+
465
+ export function CostsManager({
466
+ taskId,
467
+ costs = [],
468
+ editable = true,
469
+ className,
470
+ }: CostsManagerProps) {
471
+ const router = useRouter()
472
+ const [isPending, startTransition] = useTransition()
473
+ const [isAdding, setIsAdding] = useState(false)
474
+ const [deletingId, setDeletingId] = useState<string | null>(null)
475
+ const [error, setError] = useState<string | null>(null)
476
+
477
+ // Inline form state
478
+ const [costType, setCostType] = useState<string>("vendor_service")
479
+ const [amountInput, setAmountInput] = useState("")
480
+ const [vendorName, setVendorName] = useState("")
481
+ const [invoiceNumber, setInvoiceNumber] = useState("")
482
+ const [description, setDescription] = useState("")
483
+
484
+ const resetForm = () => {
485
+ setCostType("vendor_service")
486
+ setAmountInput("")
487
+ setVendorName("")
488
+ setInvoiceNumber("")
489
+ setDescription("")
490
+ setError(null)
491
+ setIsAdding(false)
492
+ }
493
+
494
+ // Calculate live total for additional costs
495
+ const totalOtherCosts = costs.reduce((sum, c) => sum + (Number(c.amount) || 0), 0)
496
+
497
+ const handleAddCost = async (e: React.FormEvent) => {
498
+ e.preventDefault()
499
+ if (!amountInput.trim()) {
500
+ setError("Cost amount is required")
501
+ return
502
+ }
503
+
504
+ const parsedDollars = parseFloat(amountInput)
505
+ if (isNaN(parsedDollars) || parsedDollars < 0) {
506
+ setError("Amount must be a valid non-negative number")
507
+ return
508
+ }
509
+
510
+ const amountCents = Math.round(parsedDollars * 100)
511
+ setError(null)
512
+
513
+ startTransition(async () => {
514
+ const res = await addMaintenanceCost(taskId, {
515
+ costType: costType as any,
516
+ amount: amountCents,
517
+ currency: "USD",
518
+ vendorName: vendorName.trim() || undefined,
519
+ invoiceNumber: invoiceNumber.trim() || undefined,
520
+ description: description.trim() || undefined,
521
+ })
522
+
523
+ if (res?.success) {
524
+ resetForm()
525
+ router.refresh()
526
+ } else {
527
+ setError(res?.error || "Failed to add cost item")
528
+ }
529
+ })
530
+ }
531
+
532
+ const handleDeleteCost = async (costId: string) => {
533
+ setError(null)
534
+ setDeletingId(costId)
535
+
536
+ startTransition(async () => {
537
+ const res = await deleteMaintenanceCost(costId)
538
+ if (res?.success) {
539
+ router.refresh()
540
+ } else {
541
+ setError(res?.error || "Failed to delete cost item")
542
+ }
543
+ setDeletingId(null)
544
+ })
545
+ }
546
+
547
+ return (
548
+ <div className={cn("space-y-4", className)}>
549
+ <div className="flex items-center justify-between">
550
+ <div className="flex items-center gap-2">
551
+ <Receipt className="h-5 w-5 text-primary" />
552
+ <h3 className="text-base font-semibold text-foreground">Additional Service & Labor Costs</h3>
553
+ <Badge variant="secondary" className="text-xs font-medium">
554
+ {costs.length} {costs.length === 1 ? "entry" : "entries"}
555
+ </Badge>
556
+ </div>
557
+ {editable && !isAdding && (
558
+ <Button
559
+ type="button"
560
+ size="sm"
561
+ onClick={() => {
562
+ setError(null)
563
+ setIsAdding(true)
564
+ }}
565
+ className="h-8 gap-1 text-xs"
566
+ >
567
+ <Plus className="h-3.5 w-3.5" />
568
+ Add Cost
569
+ </Button>
570
+ )}
571
+ </div>
572
+
573
+ {error && (
574
+ <div className="flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-xs text-destructive">
575
+ <AlertCircle className="h-4 w-4 shrink-0" />
576
+ <span>{error}</span>
577
+ <Button
578
+ type="button"
579
+ variant="ghost"
580
+ size="icon"
581
+ onClick={() => setError(null)}
582
+ className="ml-auto h-5 w-5 text-destructive hover:bg-transparent"
583
+ >
584
+ <X className="h-3 w-3" />
585
+ </Button>
586
+ </div>
587
+ )}
588
+
589
+ <div className="overflow-hidden rounded-lg border bg-card shadow-sm">
590
+ <Table>
591
+ <TableHeader>
592
+ <TableRow className="bg-muted/50 hover:bg-muted/50">
593
+ <TableHead className="w-[160px] font-semibold">Cost Type</TableHead>
594
+ <TableHead className="w-[120px] text-right font-semibold">Amount</TableHead>
595
+ <TableHead className="w-[180px] font-semibold">Vendor / Provider</TableHead>
596
+ <TableHead className="w-[140px] font-semibold">Invoice #</TableHead>
597
+ <TableHead className="font-semibold">Description</TableHead>
598
+ {editable && <TableHead className="w-[60px] text-center font-semibold">Action</TableHead>}
599
+ </TableRow>
600
+ </TableHeader>
601
+ <TableBody>
602
+ {costs.length === 0 && !isAdding ? (
603
+ <TableRow>
604
+ <TableCell
605
+ colSpan={editable ? 6 : 5}
606
+ className="h-24 text-center text-sm text-muted-foreground"
607
+ >
608
+ No additional service or labor costs recorded.
609
+ </TableCell>
610
+ </TableRow>
611
+ ) : (
612
+ costs.map((cost) => {
613
+ const isDeleting = deletingId === cost.id
614
+ const badgeColor = COST_TYPE_BADGES[cost.costType] || "bg-muted text-muted-foreground"
615
+ const typeLabel = COST_TYPE_LABELS[cost.costType] || cost.costType
616
+
617
+ return (
618
+ <TableRow key={cost.id} className="hover:bg-muted/30">
619
+ <TableCell>
620
+ <Badge variant="outline" className={cn("text-xs font-medium", badgeColor)}>
621
+ {typeLabel}
622
+ </Badge>
623
+ </TableCell>
624
+ <TableCell className="text-right font-mono text-xs font-semibold text-foreground">
625
+ {formatCurrency(cost.amount, cost.currency || "USD")}
626
+ </TableCell>
627
+ <TableCell className="text-xs font-medium text-foreground">
628
+ {cost.vendorName || "—"}
629
+ </TableCell>
630
+ <TableCell className="font-mono text-xs text-muted-foreground">
631
+ {cost.invoiceNumber || "—"}
632
+ </TableCell>
633
+ <TableCell className="text-xs text-muted-foreground">
634
+ {cost.description || "—"}
635
+ </TableCell>
636
+ {editable && (
637
+ <TableCell className="text-center">
638
+ <Button
639
+ type="button"
640
+ variant="ghost"
641
+ size="icon"
642
+ disabled={isDeleting || isPending}
643
+ onClick={() => handleDeleteCost(cost.id)}
644
+ className="h-7 w-7 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
645
+ title="Delete cost entry"
646
+ >
647
+ {isDeleting ? (
648
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
649
+ ) : (
650
+ <Trash2 className="h-3.5 w-3.5" />
651
+ )}
652
+ </Button>
653
+ </TableCell>
654
+ )}
655
+ </TableRow>
656
+ )
657
+ })
658
+ )}
659
+
660
+ {/* Inline Add Cost Form */}
661
+ {isAdding && (
662
+ <TableRow className="bg-primary/5 border-primary/20">
663
+ <TableCell className="p-2">
664
+ <Select value={costType} onValueChange={setCostType} disabled={isPending}>
665
+ <SelectTrigger className="h-8 text-xs bg-background">
666
+ <SelectValue placeholder="Select type" />
667
+ </SelectTrigger>
668
+ <SelectContent>
669
+ <SelectItem value="vendor_service">Vendor Service</SelectItem>
670
+ <SelectItem value="labor">Labor</SelectItem>
671
+ <SelectItem value="part">Part / Consumable</SelectItem>
672
+ <SelectItem value="transport">Transport</SelectItem>
673
+ <SelectItem value="other">Other</SelectItem>
674
+ </SelectContent>
675
+ </Select>
676
+ </TableCell>
677
+ <TableCell className="p-2">
678
+ <Input
679
+ type="number"
680
+ step="0.01"
681
+ min="0"
682
+ placeholder="0.00"
683
+ value={amountInput}
684
+ onChange={(e) => setAmountInput(e.target.value)}
685
+ disabled={isPending}
686
+ className="h-8 text-xs text-right font-mono bg-background"
687
+ autoFocus
688
+ />
689
+ </TableCell>
690
+ <TableCell className="p-2">
691
+ <Input
692
+ placeholder="Vendor / Provider"
693
+ value={vendorName}
694
+ onChange={(e) => setVendorName(e.target.value)}
695
+ disabled={isPending}
696
+ className="h-8 text-xs bg-background"
697
+ />
698
+ </TableCell>
699
+ <TableCell className="p-2">
700
+ <Input
701
+ placeholder="Invoice #"
702
+ value={invoiceNumber}
703
+ onChange={(e) => setInvoiceNumber(e.target.value)}
704
+ disabled={isPending}
705
+ className="h-8 text-xs font-mono bg-background"
706
+ />
707
+ </TableCell>
708
+ <TableCell className="p-2">
709
+ <Input
710
+ placeholder="Cost description / notes"
711
+ value={description}
712
+ onChange={(e) => setDescription(e.target.value)}
713
+ disabled={isPending}
714
+ className="h-8 text-xs bg-background"
715
+ />
716
+ </TableCell>
717
+ <TableCell className="p-2 text-center">
718
+ <div className="flex items-center justify-center gap-1">
719
+ <Button
720
+ type="button"
721
+ size="icon"
722
+ variant="default"
723
+ disabled={isPending}
724
+ onClick={handleAddCost}
725
+ className="h-7 w-7 bg-primary text-primary-foreground"
726
+ title="Save cost"
727
+ >
728
+ {isPending ? (
729
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
730
+ ) : (
731
+ <Check className="h-3.5 w-3.5" />
732
+ )}
733
+ </Button>
734
+ <Button
735
+ type="button"
736
+ size="icon"
737
+ variant="ghost"
738
+ disabled={isPending}
739
+ onClick={resetForm}
740
+ className="h-7 w-7 text-muted-foreground hover:bg-muted"
741
+ title="Cancel"
742
+ >
743
+ <X className="h-3.5 w-3.5" />
744
+ </Button>
745
+ </div>
746
+ </TableCell>
747
+ </TableRow>
748
+ )}
749
+ </TableBody>
750
+
751
+ <TableFooter>
752
+ <TableRow className="bg-muted/30 font-semibold">
753
+ <TableCell className="text-right text-xs uppercase tracking-wider text-muted-foreground">
754
+ Total Other Costs:
755
+ </TableCell>
756
+ <TableCell className="text-right font-mono text-sm font-bold text-foreground">
757
+ {formatCurrency(totalOtherCosts)}
758
+ </TableCell>
759
+ <TableCell colSpan={editable ? 4 : 3} />
760
+ </TableRow>
761
+ </TableFooter>
762
+ </Table>
763
+ </div>
764
+ </div>
765
+ )
766
+ }
767
+
768
+ // ── 3. PartsCostsSummary Component ────────────────────────────────────────────
769
+
770
+ export interface PartsCostsSummaryProps {
771
+ parts?: MaintenancePartItem[] | any[]
772
+ costs?: MaintenanceCostItem[] | any[]
773
+ totalPartsCost?: number
774
+ totalOtherCosts?: number
775
+ grandTotal?: number
776
+ currency?: string
777
+ className?: string
778
+ }
779
+
780
+ export function PartsCostsSummary({
781
+ parts = [],
782
+ costs = [],
783
+ totalPartsCost,
784
+ totalOtherCosts,
785
+ grandTotal,
786
+ currency = "USD",
787
+ className,
788
+ }: PartsCostsSummaryProps) {
789
+ // Compute totals if not provided explicitly
790
+ const calculatedParts =
791
+ totalPartsCost !== undefined
792
+ ? totalPartsCost
793
+ : parts.reduce((sum, p) => sum + (Number(p.unitCost) || 0) * (Number(p.quantity) || 1), 0)
794
+
795
+ const calculatedCosts =
796
+ totalOtherCosts !== undefined
797
+ ? totalOtherCosts
798
+ : costs.reduce((sum, c) => sum + (Number(c.amount) || 0), 0)
799
+
800
+ const calculatedGrandTotal =
801
+ grandTotal !== undefined ? grandTotal : calculatedParts + calculatedCosts
802
+
803
+ const partsCount = parts.length
804
+ const costsCount = costs.length
805
+
806
+ return (
807
+ <Card className={cn("overflow-hidden border-border shadow-sm", className)}>
808
+ <CardHeader className="bg-muted/40 border-b pb-3 pt-4">
809
+ <div className="flex items-center justify-between">
810
+ <div className="flex items-center gap-2">
811
+ <Calculator className="h-4 w-4 text-primary" />
812
+ <CardTitle className="text-sm font-semibold tracking-tight text-foreground">
813
+ Cost & Resource Summary
814
+ </CardTitle>
815
+ </div>
816
+ <Badge variant="outline" className="font-mono text-xs">
817
+ {currency}
818
+ </Badge>
819
+ </div>
820
+ <CardDescription className="text-xs">
821
+ Financial total for parts and service execution
822
+ </CardDescription>
823
+ </CardHeader>
824
+
825
+ <CardContent className="p-4 pt-4">
826
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
827
+ {/* Total Parts */}
828
+ <div className="rounded-lg border bg-background p-3">
829
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
830
+ <span className="flex items-center gap-1.5 font-medium">
831
+ <Package className="h-3.5 w-3.5 text-blue-500" />
832
+ Spare Parts
833
+ </span>
834
+ <span className="text-[11px]">
835
+ {partsCount} {partsCount === 1 ? "item" : "items"}
836
+ </span>
837
+ </div>
838
+ <div className="mt-1.5 font-mono text-lg font-bold text-foreground">
839
+ {formatCurrency(calculatedParts, currency)}
840
+ </div>
841
+ </div>
842
+
843
+ {/* Total Other Costs */}
844
+ <div className="rounded-lg border bg-background p-3">
845
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
846
+ <span className="flex items-center gap-1.5 font-medium">
847
+ <Receipt className="h-3.5 w-3.5 text-purple-500" />
848
+ Service & Labor
849
+ </span>
850
+ <span className="text-[11px]">
851
+ {costsCount} {costsCount === 1 ? "entry" : "entries"}
852
+ </span>
853
+ </div>
854
+ <div className="mt-1.5 font-mono text-lg font-bold text-foreground">
855
+ {formatCurrency(calculatedCosts, currency)}
856
+ </div>
857
+ </div>
858
+
859
+ {/* Grand Total */}
860
+ <div className="rounded-lg border border-primary/30 bg-primary/5 p-3 dark:bg-primary/10">
861
+ <div className="flex items-center justify-between text-xs text-primary font-semibold">
862
+ <span className="flex items-center gap-1.5">
863
+ <DollarSign className="h-3.5 w-3.5" />
864
+ Grand Total
865
+ </span>
866
+ <Badge variant="default" className="text-[10px] h-4 px-1 bg-primary">
867
+ Final
868
+ </Badge>
869
+ </div>
870
+ <div className="mt-1.5 font-mono text-lg font-extrabold text-primary">
871
+ {formatCurrency(calculatedGrandTotal, currency)}
872
+ </div>
873
+ </div>
874
+ </div>
875
+ </CardContent>
876
+ </Card>
877
+ )
878
+ }
879
+
880
+ // ── Combined Container Component ──────────────────────────────────────────────
881
+
882
+ export interface PartsCostsManagerCombinedProps {
883
+ taskId: string
884
+ parts?: MaintenancePartItem[] | any[]
885
+ costs?: MaintenanceCostItem[] | any[]
886
+ editable?: boolean
887
+ currency?: string
888
+ className?: string
889
+ }
890
+
891
+ export function PartsCostsManager({
892
+ taskId,
893
+ parts = [],
894
+ costs = [],
895
+ editable = true,
896
+ currency = "USD",
897
+ className,
898
+ }: PartsCostsManagerCombinedProps) {
899
+ return (
900
+ <div className={cn("space-y-6", className)}>
901
+ <PartsManager taskId={taskId} parts={parts} editable={editable} />
902
+ <CostsManager taskId={taskId} costs={costs} editable={editable} />
903
+ <PartsCostsSummary parts={parts} costs={costs} currency={currency} />
904
+ </div>
905
+ )
906
+ }
907
+
908
+ export default PartsCostsManager
909
+