useUnknownTagPrompt.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { useCallback, useEffect, useMemo, useState } from 'react';
  2. import { useMutation, useQueryClient } from '@tanstack/react-query';
  3. import type { Printer } from '../api/client';
  4. import { api } from '../api/client';
  5. import { useToast } from '../contexts/ToastContext';
  6. import { useAuth } from '../contexts/AuthContext';
  7. import { useTranslation } from 'react-i18next';
  8. import { getAmsLabel } from '../utils/amsHelpers';
  9. export interface UnknownTagDetail {
  10. printer_id: number;
  11. ams_id: number;
  12. tray_id: number;
  13. tag_uid?: string;
  14. tray_uuid?: string;
  15. // Backend-provided so the modal doesn't need to look up stale cached
  16. // `printerStatus` data — the React Query cache often lags the WS event by
  17. // several seconds while the new MQTT push is being applied.
  18. tray_type?: string | null;
  19. tray_color?: string | null;
  20. tray_sub_brands?: string | null;
  21. tray_count?: number | null;
  22. }
  23. export interface UnknownSpoolPrompt {
  24. printer_id: number;
  25. ams_id: number;
  26. tray_id: number;
  27. printer_name: string;
  28. ams_label: string;
  29. slot_label: string;
  30. material: string | null;
  31. color_hex: string | null;
  32. brand: string | null;
  33. }
  34. function slotKey(printer_id: number, ams_id: number, tray_id: number): string {
  35. return `${printer_id}|${ams_id}|${tray_id}`;
  36. }
  37. export function useUnknownTagPrompt() {
  38. const { t } = useTranslation();
  39. const queryClient = useQueryClient();
  40. const { showToast } = useToast();
  41. const { user, authEnabled } = useAuth();
  42. const [queue, setQueue] = useState<UnknownSpoolPrompt[]>([]);
  43. const isAuthed = !authEnabled || !!user;
  44. const buildPrompt = useCallback(
  45. (detail: UnknownTagDetail): UnknownSpoolPrompt | null => {
  46. // The backend only broadcasts unknown_tag for slots with real tray data,
  47. // and includes the relevant fields in the payload — no need to fall
  48. // back to the (often stale) cached printerStatus query for these.
  49. if (!detail.tray_type) return null;
  50. const printers = queryClient.getQueryData<Printer[]>(['printers']);
  51. const printer = printers?.find(p => p.id === detail.printer_id);
  52. const trayCount = detail.tray_count ?? 4;
  53. return {
  54. printer_id: detail.printer_id,
  55. ams_id: detail.ams_id,
  56. tray_id: detail.tray_id,
  57. printer_name: printer?.name ?? `Printer ${detail.printer_id}`,
  58. ams_label: getAmsLabel(detail.ams_id, trayCount),
  59. slot_label: `${t('inventory.unknownSpoolSlot', 'Slot')} ${detail.tray_id + 1}`,
  60. material: detail.tray_type ?? null,
  61. color_hex: detail.tray_color ?? null,
  62. brand: detail.tray_sub_brands ?? null,
  63. };
  64. },
  65. [queryClient, t],
  66. );
  67. useEffect(() => {
  68. if (!isAuthed) return;
  69. const handler = (e: Event) => {
  70. const ce = e as CustomEvent<UnknownTagDetail>;
  71. const detail = ce.detail;
  72. if (!detail) return;
  73. const prompt = buildPrompt(detail);
  74. if (!prompt) return;
  75. setQueue(prev => {
  76. // Don't double-queue the same slot — the backend dedupes per
  77. // (slot, tag) so repeat events here mean the user is still
  78. // looking at the same modal.
  79. const key = slotKey(prompt.printer_id, prompt.ams_id, prompt.tray_id);
  80. if (prev.some(p => slotKey(p.printer_id, p.ams_id, p.tray_id) === key)) {
  81. return prev;
  82. }
  83. return [...prev, prompt];
  84. });
  85. };
  86. window.addEventListener('unknown-tag', handler);
  87. return () => window.removeEventListener('unknown-tag', handler);
  88. }, [isAuthed, buildPrompt]);
  89. const current = queue[0] ?? null;
  90. const addMutation = useMutation({
  91. mutationFn: async (prompt: UnknownSpoolPrompt) => {
  92. const settings = queryClient.getQueryData<{ spoolman_enabled?: boolean }>(['settings']);
  93. if (settings?.spoolman_enabled) {
  94. await api.createSpoolmanSpoolFromSlot({
  95. printer_id: prompt.printer_id,
  96. ams_id: prompt.ams_id,
  97. tray_id: prompt.tray_id,
  98. });
  99. } else {
  100. await api.createSpoolFromSlot({
  101. printer_id: prompt.printer_id,
  102. ams_id: prompt.ams_id,
  103. tray_id: prompt.tray_id,
  104. });
  105. }
  106. },
  107. onSuccess: () => {
  108. showToast(t('inventory.addToInventorySuccess'), 'success');
  109. queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
  110. queryClient.invalidateQueries({ queryKey: ['spool-assignments'] });
  111. queryClient.invalidateQueries({ queryKey: ['spoolman-inventory-spools'] });
  112. queryClient.invalidateQueries({ queryKey: ['spoolman-slot-assignments'] });
  113. queryClient.invalidateQueries({ queryKey: ['linked-spools'] });
  114. setQueue(prev => prev.slice(1));
  115. },
  116. onError: (error: Error) => {
  117. showToast(error.message || t('inventory.addToInventoryFailed'), 'error');
  118. },
  119. });
  120. const confirm = useCallback(() => {
  121. if (!current || addMutation.isPending) return;
  122. addMutation.mutate(current);
  123. }, [current, addMutation]);
  124. const cancel = useCallback(() => {
  125. if (!current) return;
  126. setQueue(prev => prev.slice(1));
  127. }, [current]);
  128. return useMemo(
  129. () => ({
  130. prompt: current,
  131. isPending: addMutation.isPending,
  132. confirm,
  133. cancel,
  134. }),
  135. [current, addMutation.isPending, confirm, cancel],
  136. );
  137. }