useWorkBoardWebSocket.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. 'use client'
  2. import { useEffect } from 'react'
  3. import { useRealtime, useRealtimeState } from '@/features/realtime/RealtimeProvider'
  4. import { useWorkBoardStore } from '../store/workboardStore'
  5. import type { WorkBoardJob } from '../Actions/boardActions'
  6. export function useWorkBoardWebSocket() {
  7. const realtime = useRealtime()
  8. const { status } = useRealtimeState()
  9. // The board's "live" light says what the link is actually doing, not that
  10. // this hook has mounted.
  11. useEffect(() => {
  12. useWorkBoardStore
  13. .getState()
  14. .setConnection(
  15. status === 'ready' ? 'open' : status === 'connecting' ? 'connecting' : 'closed'
  16. )
  17. return () => useWorkBoardStore.getState().setConnection('closed')
  18. }, [status])
  19. useEffect(() => {
  20. if (!realtime) return
  21. return realtime.onLegacy('workboard', (raw) => {
  22. const store = useWorkBoardStore.getState()
  23. // The board's own events, which still travel on the workshop channel
  24. // (see lib/realtime for the record rooms that will replace them).
  25. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  26. const data = raw as any
  27. if (!data?.type) return
  28. switch (data.type) {
  29. case 'job_assigned':
  30. store.addJob(data.job as WorkBoardJob)
  31. // Remove from unassigned lists based on job type and id
  32. if (data.job.type === 'serviceRecord') {
  33. store.removeFromUnassigned(data.job.id, 'serviceRecord')
  34. } else if (data.job.type === 'inspection') {
  35. store.removeFromUnassigned(data.job.id, 'inspection')
  36. }
  37. break
  38. case 'job_moved':
  39. case 'job_scheduled':
  40. store.updateJob(data.job as WorkBoardJob)
  41. break
  42. case 'job_unassigned':
  43. store.removeJob(data.jobId)
  44. break
  45. case 'technician_created':
  46. store.addTechnician(data.technician)
  47. break
  48. case 'work_bay_created':
  49. case 'work_bay_updated':
  50. store.upsertWorkBay(data.workBay)
  51. break
  52. case 'work_bay_removed':
  53. store.removeWorkBay(data.workBayId as string)
  54. break
  55. case 'technician_updated':
  56. case 'technician_removed':
  57. // Reload full technician list for updates/removals
  58. import('../Actions/technicianActions')
  59. .then(({ getTechnicians }) => getTechnicians())
  60. .then((res) => {
  61. if (res.success && res.data) {
  62. store.setTechnicians(res.data as Parameters<typeof store.setTechnicians>[0])
  63. }
  64. })
  65. .catch(() => {
  66. // Offline for a moment: the board keeps the list it has
  67. })
  68. break
  69. case 'service_times_updated': {
  70. const { serviceRecordId, startDateTime, endDateTime } = data
  71. const updatedJobs = store.jobs.map((j) =>
  72. j.type === 'serviceRecord' && j.id === serviceRecordId
  73. ? { ...j, startDateTime, endDateTime }
  74. : j
  75. )
  76. store.setJobs(updatedJobs)
  77. break
  78. }
  79. case 'inspection_times_updated': {
  80. const { inspectionId, startDateTime, endDateTime } = data
  81. const updatedJobs = store.jobs.map((j) =>
  82. j.type === 'inspection' && j.id === inspectionId
  83. ? { ...j, startDateTime, endDateTime }
  84. : j
  85. )
  86. store.setJobs(updatedJobs)
  87. break
  88. }
  89. case 'job_status_changed': {
  90. const { serviceRecordId, inspectionId, status, serviceRecord } = data
  91. const activeStatuses = ['pending', 'in-progress', 'waiting-parts', 'scheduled']
  92. // Update the status on matching jobs in-place
  93. const updated = store.jobs.map((j) => {
  94. if (serviceRecordId && j.type === 'serviceRecord' && j.id === serviceRecordId) {
  95. return { ...j, status }
  96. }
  97. if (inspectionId && j.type === 'inspection' && j.id === inspectionId) {
  98. return { ...j, status }
  99. }
  100. return j
  101. })
  102. store.setJobs(updated)
  103. // Add/remove from unassigned pool for service records
  104. if (serviceRecordId && serviceRecord) {
  105. const isAssigned = store.jobs.some(
  106. (j) => j.type === 'serviceRecord' && j.id === serviceRecordId
  107. )
  108. const isAlreadyUnassigned = store.unassignedServiceRecords.some(
  109. (sr) => sr.id === serviceRecordId
  110. )
  111. if (activeStatuses.includes(status) && !isAssigned && !isAlreadyUnassigned) {
  112. store.addToUnassigned(serviceRecord, 'serviceRecord')
  113. } else if (!activeStatuses.includes(status)) {
  114. store.removeFromUnassigned(serviceRecordId, 'serviceRecord')
  115. }
  116. }
  117. break
  118. }
  119. }
  120. })
  121. }, [realtime])
  122. }