qrAssignTarget.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /**
  2. * Scan-to-location assignment (#1574).
  3. *
  4. * The user picks a target (an AMS slot or a storage location), then scans a
  5. * spool's label QR with the in-page camera. The scanned QR encodes a deeplink
  6. * such as `https://<host>/inventory?spool=42`; we extract the spool id from it
  7. * and assign that spool to the chosen target — no navigation, no edit modal.
  8. */
  9. import type { SlotFilament } from './assignMismatch';
  10. export type AssignTarget =
  11. | {
  12. kind: 'ams';
  13. printerId: number;
  14. printerName: string;
  15. amsId: number;
  16. trayId: number;
  17. isExternal: boolean;
  18. /** Human-readable slot label, e.g. "A2", "HT-A", "Ext". */
  19. label: string;
  20. /** What the printer reported in the slot when it was picked, for the material check. */
  21. filament: SlotFilament;
  22. }
  23. | {
  24. kind: 'storage';
  25. /** Structured Location id — the canonical storage model (`location_id`). */
  26. locationId: number;
  27. /** Location name, for the target chip and the success toast. */
  28. locationName: string;
  29. };
  30. /**
  31. * The spool a scanned label names. A Bambuddy label (`…/inventory?spool=42`)
  32. * carries the id of the inventory it was printed from — built-in or Spoolman,
  33. * whichever Bambuddy was using. Spoolman's own labels carry a Spoolman id:
  34. * `web+spoolman:s-42`, or `…/spool/show/42` when Spoolman prints URLs.
  35. */
  36. export type ScannedSpool = { source: 'bambuddy' | 'spoolman'; id: number };
  37. function positiveId(text: string | null | undefined): number | null {
  38. if (!text || !/^\d+$/.test(text)) return null;
  39. const n = Number(text);
  40. return Number.isSafeInteger(n) && n > 0 ? n : null;
  41. }
  42. /**
  43. * Read a scanned QR payload. Returns null for anything that isn't a spool
  44. * label — including 0, negatives, and non-spool QRs — so the caller can keep
  45. * scanning instead of acting on garbage.
  46. *
  47. * A payload marked as Spoolman's (its scheme, or a /spool/show/ path) is read
  48. * as a Spoolman label or not at all: `web+spoolman:s-x?spool=5` is not
  49. * Bambuddy spool 5.
  50. *
  51. * A Bambuddy label is the deeplink the label printer emits (any host/scheme),
  52. * a bare query fragment (`?spool=42` / `spool=42`), or a bare numeric string.
  53. */
  54. export function parseSpoolQr(raw: string): ScannedSpool | null {
  55. const text = (raw ?? '').trim();
  56. if (!text) return null;
  57. if (/^web\+spoolman:/i.test(text)) {
  58. const id = positiveId(text.match(/^web\+spoolman:s-(\d+)$/i)?.[1]);
  59. return id ? { source: 'spoolman', id } : null;
  60. }
  61. if (/\/spool\/show\//i.test(text)) {
  62. let path = text;
  63. try {
  64. path = new URL(text).pathname;
  65. } catch {
  66. /* not a full URL — match the path as written */
  67. }
  68. const id = positiveId(path.match(/\/spool\/show\/(\d+)\/?$/i)?.[1]);
  69. return id ? { source: 'spoolman', id } : null;
  70. }
  71. // Whole payload is just digits.
  72. if (/^\d+$/.test(text)) {
  73. const id = positiveId(text);
  74. return id ? { source: 'bambuddy', id } : null;
  75. }
  76. // Try a real URL first (handles any host/scheme + extra params).
  77. let spoolParam: string | null = null;
  78. try {
  79. spoolParam = new URL(text).searchParams.get('spool');
  80. } catch {
  81. /* not a full URL — handled by the regex fallback below */
  82. }
  83. // Fall back to a query-style match anywhere in the string — also covers a URL
  84. // that parsed but carried `spool` in a hash fragment (e.g. a hash router).
  85. if (!spoolParam) {
  86. const m = text.match(/(?:[?&]|\b)spool=(\d+)/);
  87. spoolParam = m ? m[1] : null;
  88. }
  89. const id = positiveId(spoolParam);
  90. return id ? { source: 'bambuddy', id } : null;
  91. }
  92. /**
  93. * The id to assign in the inventory in use, or null when the label is for an
  94. * inventory that isn't: a Spoolman label means nothing to the built-in one. A
  95. * Bambuddy label is taken in either mode, as it carries the id of whichever
  96. * inventory printed it.
  97. */
  98. export function scannedSpoolId(spool: ScannedSpool, spoolmanMode: boolean): number | null {
  99. return spool.source === 'spoolman' && !spoolmanMode ? null : spool.id;
  100. }