jaxonday

Stock Take

Mar 26th, 2026 (edited)
107
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 53.29 KB | Software | 0 0
  1. import { render } from 'preact';
  2. import { useState, useEffect, useRef } from 'preact/hooks';
  3.  
  4. interface ProductVariant {
  5. id: string;
  6. sku: string;
  7. barcode: string;
  8. title: string;
  9. product: {
  10. title: string;
  11. };
  12. inventoryItem: {
  13. id: string;
  14. };
  15. }
  16.  
  17. interface ScannedItem {
  18. barcode: string;
  19. productTitle: string;
  20. variantTitle: string;
  21. sku: string;
  22. quantity: number;
  23. variantId?: string;
  24. }
  25.  
  26. interface CollectionVariant {
  27. barcode: string;
  28. productTitle: string;
  29. variantTitle: string;
  30. sku: string;
  31. inventoryQuantity: number;
  32. variantId: string;
  33. }
  34.  
  35. interface Collection {
  36. id: string;
  37. title: string;
  38. }
  39.  
  40. interface ComparisonItem {
  41. barcode: string;
  42. productTitle: string;
  43. variantTitle: string;
  44. sku: string;
  45. expectedQuantity: number | null;
  46. scannedQuantity: number;
  47. difference: number | null;
  48. variantId: string | null;
  49. selected: boolean;
  50. }
  51.  
  52. interface StockTakeSession {
  53. id: string;
  54. handle: string;
  55. name: string;
  56. createdDate: string;
  57. scannedItemsData: ScannedItem[];
  58. selectedCollectionId: string;
  59. lastUpdated: string;
  60. }
  61.  
  62. function Extension() {
  63. const [barcodeInput, setBarcodeInput] = useState<string>('');
  64. const [scannedItems, setScannedItems] = useState<ScannedItem[]>([]);
  65. const [collections, setCollections] = useState<Collection[]>([]);
  66. const [selectedCollectionId, setSelectedCollectionId] = useState<string>('');
  67. const [collectionVariants, setCollectionVariants] = useState<CollectionVariant[]>([]);
  68. const [loadingCollections, setLoadingCollections] = useState<boolean>(false);
  69. const [loadingCollection, setLoadingCollection] = useState<boolean>(false);
  70. const [scanning, setScanning] = useState<boolean>(false);
  71. const [error, setError] = useState<string>('');
  72. const [successMessage, setSuccessMessage] = useState<string>('');
  73. const [showOnlyDiscrepancies, setShowOnlyDiscrepancies] = useState<boolean>(false);
  74. const [sessions, setSessions] = useState<StockTakeSession[]>([]);
  75. const [currentSessionId, setCurrentSessionId] = useState<string>('');
  76. const [loadingSessions, setLoadingSessions] = useState<boolean>(false);
  77. const [newSessionName, setNewSessionName] = useState<string>('');
  78. const [savingSession, setSavingSession] = useState<boolean>(false);
  79. const [definitionReady, setDefinitionReady] = useState<boolean>(false);
  80. const [editedQuantities, setEditedQuantities] = useState<Map<string, number>>(new Map());
  81. const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
  82. const [applyingInventory, setApplyingInventory] = useState<boolean>(false);
  83. const [deletingSession, setDeletingSession] = useState<boolean>(false);
  84. const newSessionModalRef = useRef<any>(null);
  85. const confirmApplyModalRef = useRef<any>(null);
  86. const confirmDeleteModalRef = useRef<any>(null);
  87. const debounceTimerRef = useRef<number | null>(null);
  88. const barcodeValueRef = useRef<string>('');
  89. const currentSessionIdRef = useRef<string>('');
  90. const scannedItemsRef = useRef<ScannedItem[]>([]);
  91.  
  92. const focusBarcodeInput = () => {
  93. setTimeout(() => {
  94. try {
  95. const input = document.getElementById('barcode-input') as HTMLInputElement;
  96. if (input && typeof input.focus === 'function') {
  97. input.focus();
  98. }
  99. } catch (err) {
  100. // Focus failed
  101. }
  102. }, 100);
  103. };
  104.  
  105. useEffect(() => {
  106. initializeApp();
  107. }, []);
  108.  
  109. useEffect(() => {
  110. currentSessionIdRef.current = currentSessionId;
  111. }, [currentSessionId]);
  112.  
  113. useEffect(() => {
  114. scannedItemsRef.current = scannedItems;
  115. }, [scannedItems]);
  116.  
  117. useEffect(() => {
  118. if (currentSessionId && (scannedItems.length > 0 || selectedCollectionId)) {
  119. saveCurrentSession();
  120. }
  121. }, [scannedItems, selectedCollectionId]);
  122.  
  123. useEffect(() => {
  124. const handleKeyPress = (e: KeyboardEvent) => {
  125. const target = e.target as HTMLElement;
  126. if (target && target.id === 'barcode-input') {
  127. if (e.key === 'Enter' || e.key === 'Tab') {
  128. e.preventDefault();
  129. processBarcodeScann();
  130. }
  131. }
  132. };
  133.  
  134. document.addEventListener('keydown', handleKeyPress);
  135. return () => document.removeEventListener('keydown', handleKeyPress);
  136. }, []);
  137.  
  138. useEffect(() => {
  139. return () => {
  140. if (debounceTimerRef.current) {
  141. clearTimeout(debounceTimerRef.current);
  142. }
  143. };
  144. }, []);
  145.  
  146. const initializeApp = async () => {
  147. await ensureMetaobjectDefinition();
  148. await fetchSessions();
  149. await fetchCollections();
  150. };
  151.  
  152. const ensureMetaobjectDefinition = async () => {
  153. try {
  154. const checkQuery = `query {
  155. metaobjectDefinitionByType(type: \"sidekick_stock_take\") {
  156. id
  157. }
  158. }`;
  159.  
  160. const { data, errors } = await shopify.query(checkQuery);
  161.  
  162. if (errors && errors.length > 0) {
  163. const errorMessage = errors.map((e: any) => e.message).join(', ');
  164. setError(errorMessage);
  165. setDefinitionReady(false);
  166. return;
  167. }
  168.  
  169. if (data?.metaobjectDefinitionByType?.id) {
  170. setDefinitionReady(true);
  171. return;
  172. }
  173.  
  174. const createMutation = `mutation CreateMetaobjectDefinition($definition: MetaobjectDefinitionCreateInput!) {
  175. metaobjectDefinitionCreate(definition: $definition) {
  176. metaobjectDefinition {
  177. id
  178. type
  179. }
  180. userErrors {
  181. field
  182. message
  183. code
  184. }
  185. }
  186. }`;
  187.  
  188. const { data: createData, errors: createErrors } = await shopify.query(createMutation, {
  189. variables: {
  190. definition: {
  191. name: 'Stock Take Session',
  192. type: 'sidekick_stock_take',
  193. fieldDefinitions: [
  194. {
  195. name: 'Session Name',
  196. key: 'name',
  197. type: 'single_line_text_field',
  198. required: true,
  199. },
  200. {
  201. name: 'Created Date',
  202. key: 'created_date',
  203. type: 'date_time',
  204. required: true,
  205. },
  206. {
  207. name: 'Scanned Items Data',
  208. key: 'scanned_items_data',
  209. type: 'json',
  210. required: false,
  211. },
  212. {
  213. name: 'Selected Collection',
  214. key: 'selected_collection_id',
  215. type: 'collection_reference',
  216. required: false,
  217. },
  218. {
  219. name: 'Last Updated',
  220. key: 'last_updated',
  221. type: 'date_time',
  222. required: false,
  223. },
  224. ],
  225. },
  226. },
  227. });
  228.  
  229. if (createErrors && createErrors.length > 0) {
  230. const errorMessage = createErrors.map((e: any) => e.message).join(', ');
  231. setError(errorMessage);
  232. return;
  233. }
  234.  
  235. if (createData?.metaobjectDefinitionCreate?.userErrors?.length > 0) {
  236. const userErrorMessage = createData.metaobjectDefinitionCreate.userErrors
  237. .map((e: any) => (e.field ? `${e.field}: ${e.message}` : e.message))
  238. .join(', ');
  239. setError(userErrorMessage);
  240. setDefinitionReady(false);
  241. return;
  242. }
  243.  
  244. if (createData?.metaobjectDefinitionCreate?.metaobjectDefinition?.id) {
  245. setDefinitionReady(true);
  246. }
  247. } catch (err: any) {
  248. setError(err.message || 'Failed to initialize metaobject definition');
  249. setDefinitionReady(false);
  250. }
  251. };
  252.  
  253. const fetchSessions = async () => {
  254. setLoadingSessions(true);
  255.  
  256. try {
  257. const sessionsQuery = `query GetSessions($first: Int!, $type: String!) {
  258. metaobjects(first: $first, type: $type) {
  259. edges {
  260. node {
  261. id
  262. handle
  263. name: field(key: \"name\") { jsonValue }
  264. createdDate: field(key: \"created_date\") { jsonValue }
  265. scannedItemsData: field(key: \"scanned_items_data\") { jsonValue }
  266. selectedCollectionId: field(key: \"selected_collection_id\") { jsonValue }
  267. lastUpdated: field(key: \"last_updated\") { jsonValue }
  268. }
  269. }
  270. pageInfo {
  271. hasNextPage
  272. endCursor
  273. }
  274. }
  275. }`;
  276.  
  277. const { data, errors } = await shopify.query(sessionsQuery, {
  278. variables: {
  279. first: 250,
  280. type: 'sidekick_stock_take',
  281. },
  282. });
  283.  
  284. if (errors && errors.length > 0) {
  285. const errorMessage = errors.map((e: any) => e.message).join(', ');
  286. setError(errorMessage);
  287. return;
  288. }
  289.  
  290. if (data?.metaobjects) {
  291. const fetchedSessions = data.metaobjects.edges.map((edge: any) => ({
  292. id: edge.node.id,
  293. handle: edge.node.handle,
  294. name: edge.node.name?.jsonValue || '',
  295. createdDate: edge.node.createdDate?.jsonValue || '',
  296. scannedItemsData: edge.node.scannedItemsData?.jsonValue || [],
  297. selectedCollectionId: edge.node.selectedCollectionId?.jsonValue || '',
  298. lastUpdated: edge.node.lastUpdated?.jsonValue || '',
  299. }));
  300. setSessions(fetchedSessions);
  301. }
  302. } catch (err: any) {
  303. setError(err.message || 'Failed to load sessions');
  304. } finally {
  305. setLoadingSessions(false);
  306. }
  307. };
  308.  
  309. const createNewSession = async () => {
  310. const sessionName = newSessionName.trim();
  311.  
  312. if (!sessionName) {
  313. setError('Session name is required.');
  314. return;
  315. }
  316.  
  317. if (!definitionReady) {
  318. setError('Metaobject definition is not ready. Please wait and try again.');
  319. return;
  320. }
  321.  
  322. setSavingSession(true);
  323. setError('');
  324.  
  325. try {
  326. const timestamp = new Date().toISOString();
  327. const handle = `session-${Date.now()}`;
  328.  
  329. const upsertMutation = `mutation UpsertMetaobject($handle: MetaobjectHandleInput!, $metaobject: MetaobjectUpsertInput!) {
  330. metaobjectUpsert(handle: $handle, metaobject: $metaobject) {
  331. metaobject {
  332. id
  333. handle
  334. }
  335. userErrors {
  336. field
  337. message
  338. code
  339. }
  340. }
  341. }`;
  342.  
  343. const { data, errors } = await shopify.query(upsertMutation, {
  344. variables: {
  345. handle: {
  346. type: 'sidekick_stock_take',
  347. handle: handle,
  348. },
  349. metaobject: {
  350. fields: [
  351. { key: 'name', value: sessionName },
  352. { key: 'created_date', value: timestamp },
  353. { key: 'scanned_items_data', value: JSON.stringify([]) },
  354. { key: 'last_updated', value: timestamp },
  355. ],
  356. },
  357. },
  358. });
  359.  
  360. if (errors && errors.length > 0) {
  361. const errorMessage = errors.map((e: any) => e.message).join(', ');
  362. setError(errorMessage);
  363. return;
  364. }
  365.  
  366. if (data?.metaobjectUpsert?.userErrors?.length > 0) {
  367. const userErrorMessage = data.metaobjectUpsert.userErrors
  368. .map((e: any) => (e.field ? `${e.field}: ${e.message}` : e.message))
  369. .join(', ');
  370. setError(userErrorMessage);
  371. return;
  372. }
  373.  
  374. setNewSessionName('');
  375. newSessionModalRef.current?.hideOverlay();
  376. await fetchSessions();
  377.  
  378. if (data?.metaobjectUpsert?.metaobject?.id) {
  379. setCurrentSessionId(data.metaobjectUpsert.metaobject.id);
  380. setScannedItems([]);
  381. setSelectedCollectionId('');
  382. }
  383. } catch (err: any) {
  384. setError(err.message || 'Failed to create session');
  385. } finally {
  386. setSavingSession(false);
  387. }
  388. };
  389.  
  390. const deleteSession = async () => {
  391. if (!currentSessionId) return;
  392.  
  393. setDeletingSession(true);
  394. setError('');
  395.  
  396. try {
  397. const deleteMutation = `mutation DeleteMetaobject($id: ID!) {
  398. metaobjectDelete(id: $id) {
  399. deletedId
  400. userErrors {
  401. field
  402. message
  403. code
  404. }
  405. }
  406. }`;
  407.  
  408. const { data, errors } = await shopify.query(deleteMutation, {
  409. variables: {
  410. id: currentSessionId,
  411. },
  412. });
  413.  
  414. if (errors && errors.length > 0) {
  415. const errorMessage = errors.map((e: any) => e.message).join(', ');
  416. setError(errorMessage);
  417. return;
  418. }
  419.  
  420. if (data?.metaobjectDelete?.userErrors?.length > 0) {
  421. const userErrorMessage = data.metaobjectDelete.userErrors
  422. .map((e: any) => (e.field ? `${e.field}: ${e.message}` : e.message))
  423. .join(', ');
  424. setError(userErrorMessage);
  425. return;
  426. }
  427.  
  428. confirmDeleteModalRef.current?.hideOverlay();
  429. setCurrentSessionId('');
  430. setScannedItems([]);
  431. setSelectedCollectionId('');
  432. setCollectionVariants([]);
  433. await fetchSessions();
  434. setSuccessMessage('Session deleted successfully');
  435. setTimeout(() => setSuccessMessage(''), 3000);
  436. } catch (err: any) {
  437. setError(err.message || 'Failed to delete session');
  438. } finally {
  439. setDeletingSession(false);
  440. }
  441. };
  442.  
  443. const saveCurrentSession = async () => {
  444. if (!currentSessionId) return;
  445.  
  446. const currentSession = sessions.find((s) => s.id === currentSessionId);
  447. if (!currentSession) return;
  448.  
  449. try {
  450. const timestamp = new Date().toISOString();
  451.  
  452. const upsertMutation = `mutation UpsertMetaobject($handle: MetaobjectHandleInput!, $metaobject: MetaobjectUpsertInput!) {
  453. metaobjectUpsert(handle: $handle, metaobject: $metaobject) {
  454. metaobject {
  455. id
  456. }
  457. userErrors {
  458. field
  459. message
  460. code
  461. }
  462. }
  463. }`;
  464.  
  465. const fields: any[] = [
  466. { key: 'name', value: currentSession.name },
  467. { key: 'created_date', value: currentSession.createdDate },
  468. { key: 'scanned_items_data', value: JSON.stringify(scannedItems) },
  469. { key: 'last_updated', value: timestamp },
  470. ];
  471.  
  472. if (selectedCollectionId) {
  473. fields.push({ key: 'selected_collection_id', value: selectedCollectionId });
  474. }
  475.  
  476. const { data, errors } = await shopify.query(upsertMutation, {
  477. variables: {
  478. handle: {
  479. type: 'sidekick_stock_take',
  480. handle: currentSession.handle,
  481. },
  482. metaobject: {
  483. fields: fields,
  484. },
  485. },
  486. });
  487.  
  488. if (errors && errors.length > 0) {
  489. const errorMessage = errors.map((e: any) => e.message).join(', ');
  490. setError(errorMessage);
  491. }
  492.  
  493. if (data?.metaobjectUpsert?.userErrors?.length > 0) {
  494. const userErrorMessage = data.metaobjectUpsert.userErrors
  495. .map((e: any) => (e.field ? `${e.field}: ${e.message}` : e.message))
  496. .join(', ');
  497. setError(userErrorMessage);
  498. }
  499. } catch (err: any) {
  500. setError(err.message || 'Failed to save session');
  501. }
  502. };
  503.  
  504. const loadSession = (sessionId: string) => {
  505. const session = sessions.find((s) => s.id === sessionId);
  506. if (!session) return;
  507.  
  508. setCurrentSessionId(sessionId);
  509. setScannedItems(session.scannedItemsData || []);
  510. setSelectedCollectionId(session.selectedCollectionId || '');
  511.  
  512. if (session.selectedCollectionId) {
  513. fetchCollectionInventory(session.selectedCollectionId);
  514. }
  515. };
  516.  
  517. const playErrorBeep = () => {
  518. try {
  519. const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
  520. const oscillator = audioContext.createOscillator();
  521. const gainNode = audioContext.createGain();
  522.  
  523. oscillator.connect(gainNode);
  524. gainNode.connect(audioContext.destination);
  525.  
  526. oscillator.frequency.value = 400;
  527. oscillator.type = 'sine';
  528.  
  529. gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
  530. gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
  531.  
  532. oscillator.start(audioContext.currentTime);
  533. oscillator.stop(audioContext.currentTime + 0.2);
  534. } catch (err) {
  535. // Audio API not available
  536. }
  537. };
  538.  
  539. const fetchCollections = async () => {
  540. setLoadingCollections(true);
  541. setError('');
  542.  
  543. const allCollections: Collection[] = [];
  544. let hasNextPage: boolean = true;
  545. let cursor: string | null = null;
  546.  
  547. try {
  548. while (hasNextPage) {
  549. const collectionsQuery = `query GetCollections($first: Int!, $after: String) {
  550. collections(first: $first, after: $after) {
  551. edges {
  552. node {
  553. id
  554. title
  555. }
  556. }
  557. pageInfo {
  558. hasNextPage
  559. endCursor
  560. }
  561. }
  562. }`;
  563.  
  564. const { data, errors } = await shopify.query(collectionsQuery, {
  565. variables: {
  566. first: 250,
  567. after: cursor,
  568. },
  569. });
  570.  
  571. if (errors && errors.length > 0) {
  572. const errorMessage = errors.map((e: any) => e.message).join(', ');
  573. setError(errorMessage);
  574. break;
  575. }
  576.  
  577. if (data?.collections) {
  578. const fetchedCollections = data.collections.edges.map((edge: any) => edge.node);
  579. allCollections.push(...fetchedCollections);
  580. hasNextPage = data.collections.pageInfo.hasNextPage;
  581. cursor = data.collections.pageInfo.endCursor;
  582. } else {
  583. break;
  584. }
  585. }
  586.  
  587. setCollections(allCollections);
  588.  
  589. if (allCollections.length === 0) {
  590. setError('No collections found in your store.');
  591. }
  592. } catch (err: any) {
  593. setError(err.message || 'Failed to load collections');
  594. } finally {
  595. setLoadingCollections(false);
  596. }
  597. };
  598.  
  599. const fetchCollectionInventory = async (collectionId: string) => {
  600. setLoadingCollection(true);
  601. setError('');
  602. setCollectionVariants([]);
  603.  
  604. const allVariants: CollectionVariant[] = [];
  605. let hasNextProductPage: boolean = true;
  606. let productCursor: string | null = null;
  607.  
  608. try {
  609. while (hasNextProductPage) {
  610. const collectionQuery = `query GetCollection($id: ID!, $first: Int!, $after: String) {
  611. collection(id: $id) {
  612. id
  613. title
  614. products(first: $first, after: $after) {
  615. edges {
  616. node {
  617. id
  618. title
  619. variants(first: 100) {
  620. edges {
  621. node {
  622. id
  623. title
  624. sku
  625. barcode
  626. inventoryQuantity
  627. }
  628. }
  629. pageInfo {
  630. hasNextPage
  631. endCursor
  632. }
  633. }
  634. }
  635. }
  636. pageInfo {
  637. hasNextPage
  638. endCursor
  639. }
  640. }
  641. }
  642. }`;
  643.  
  644. const { data, errors } = await shopify.query(collectionQuery, {
  645. variables: {
  646. id: collectionId,
  647. first: 25,
  648. after: productCursor,
  649. },
  650. });
  651.  
  652. if (errors && errors.length > 0) {
  653. const errorMessage = errors.map((e: any) => e.message).join(', ');
  654. setError(errorMessage);
  655. break;
  656. }
  657.  
  658. if (data?.collection?.products) {
  659. const products = data.collection.products.edges.map((edge: any) => edge.node);
  660.  
  661. for (const product of products) {
  662. let hasNextVariantPage: boolean = product.variants.pageInfo.hasNextPage;
  663. let variantCursor: string | null = product.variants.pageInfo.endCursor;
  664.  
  665. const variants = product.variants.edges.map((edge: any) => edge.node);
  666.  
  667. for (const variant of variants) {
  668. if (variant.barcode) {
  669. allVariants.push({
  670. barcode: variant.barcode,
  671. productTitle: product.title,
  672. variantTitle: variant.title,
  673. sku: variant.sku || '',
  674. inventoryQuantity: variant.inventoryQuantity || 0,
  675. variantId: variant.id,
  676. });
  677. }
  678. }
  679.  
  680. while (hasNextVariantPage) {
  681. const variantQuery = `query GetProductVariants($productId: ID!, $first: Int!, $after: String) {
  682. product(id: $productId) {
  683. variants(first: $first, after: $after) {
  684. edges {
  685. node {
  686. id
  687. title
  688. sku
  689. barcode
  690. inventoryQuantity
  691. }
  692. }
  693. pageInfo {
  694. hasNextPage
  695. endCursor
  696. }
  697. }
  698. }
  699. }`;
  700.  
  701. const variantResult = await shopify.query(variantQuery, {
  702. variables: {
  703. productId: product.id,
  704. first: 100,
  705. after: variantCursor,
  706. },
  707. });
  708.  
  709. if (variantResult.errors && variantResult.errors.length > 0) {
  710. break;
  711. }
  712.  
  713. if (variantResult.data?.product?.variants) {
  714. const moreVariants = variantResult.data.product.variants.edges.map(
  715. (edge: any) => edge.node,
  716. );
  717.  
  718. for (const variant of moreVariants) {
  719. if (variant.barcode) {
  720. allVariants.push({
  721. barcode: variant.barcode,
  722. productTitle: product.title,
  723. variantTitle: variant.title,
  724. sku: variant.sku || '',
  725. inventoryQuantity: variant.inventoryQuantity || 0,
  726. variantId: variant.id,
  727. });
  728. }
  729. }
  730.  
  731. hasNextVariantPage = variantResult.data.product.variants.pageInfo.hasNextPage;
  732. variantCursor = variantResult.data.product.variants.pageInfo.endCursor;
  733. } else {
  734. break;
  735. }
  736. }
  737. }
  738.  
  739. hasNextProductPage = data.collection.products.pageInfo.hasNextPage;
  740. productCursor = data.collection.products.pageInfo.endCursor;
  741. } else {
  742. break;
  743. }
  744. }
  745.  
  746. setCollectionVariants(allVariants);
  747.  
  748. if (allVariants.length === 0) {
  749. setError('This collection has no products.');
  750. }
  751. } catch (err: any) {
  752. setError(err.message || 'Failed to load collection inventory');
  753. } finally {
  754. setLoadingCollection(false);
  755. }
  756. };
  757.  
  758. const handleBarcodeInput = (e: Event) => {
  759. const value = (e.currentTarget as HTMLInputElement).value;
  760.  
  761. if (debounceTimerRef.current) {
  762. clearTimeout(debounceTimerRef.current);
  763. }
  764.  
  765. if (value.endsWith('\r') || value.endsWith('\n')) {
  766. const cleanValue = value.replace(/[\r\n]+$/, '');
  767. barcodeValueRef.current = cleanValue;
  768. setBarcodeInput(cleanValue);
  769. processBarcodeScann();
  770. return;
  771. }
  772.  
  773. barcodeValueRef.current = value;
  774. setBarcodeInput(value);
  775.  
  776. if (value.trim().length > 0) {
  777. debounceTimerRef.current = setTimeout(() => {
  778. processBarcodeScann();
  779. }, 300);
  780. }
  781. };
  782.  
  783. const processBarcodeScann = async () => {
  784. if (debounceTimerRef.current) {
  785. clearTimeout(debounceTimerRef.current);
  786. debounceTimerRef.current = null;
  787. }
  788.  
  789. const barcode = barcodeValueRef.current.trim();
  790.  
  791. if (!barcode || !currentSessionIdRef.current) {
  792. return;
  793. }
  794.  
  795. setScanning(true);
  796. setError('');
  797. setSuccessMessage('');
  798.  
  799. try {
  800. const variantQuery = `query GetVariantByBarcode($query: String!, $first: Int!) {
  801. productVariants(first: $first, query: $query) {
  802. edges {
  803. node {
  804. id
  805. sku
  806. barcode
  807. title
  808. product {
  809. title
  810. }
  811. inventoryItem {
  812. id
  813. }
  814. }
  815. }
  816. }
  817. }`;
  818.  
  819. const { data, errors } = await shopify.query(variantQuery, {
  820. variables: {
  821. query: `barcode:${barcode}`,
  822. first: 1,
  823. },
  824. });
  825.  
  826. if (errors && errors.length > 0) {
  827. const errorMessage = errors.map((e: any) => e.message).join(', ');
  828. setError(errorMessage);
  829. barcodeValueRef.current = '';
  830. setBarcodeInput('');
  831. focusBarcodeInput();
  832. return;
  833. }
  834.  
  835. if (data?.productVariants?.edges?.length > 0) {
  836. const variant: ProductVariant = data.productVariants.edges[0].node;
  837.  
  838. const existingIndex = scannedItemsRef.current.findIndex(
  839. (item) => item.barcode === variant.barcode,
  840. );
  841.  
  842. if (existingIndex >= 0) {
  843. const updatedItems = [...scannedItemsRef.current];
  844. updatedItems[existingIndex].quantity += 1;
  845. setScannedItems(updatedItems);
  846. setSuccessMessage(`Quantity updated: ${variant.product.title} - ${variant.title}`);
  847. } else {
  848. setScannedItems([
  849. ...scannedItemsRef.current,
  850. {
  851. barcode: variant.barcode,
  852. productTitle: variant.product.title,
  853. variantTitle: variant.title,
  854. sku: variant.sku || '',
  855. quantity: 1,
  856. variantId: variant.id,
  857. },
  858. ]);
  859. setSuccessMessage(`Item added: ${variant.product.title} - ${variant.title}`);
  860. }
  861.  
  862. setTimeout(() => setSuccessMessage(''), 3000);
  863. barcodeValueRef.current = '';
  864. setBarcodeInput('');
  865. focusBarcodeInput();
  866. } else {
  867. playErrorBeep();
  868. setError('Barcode not found. Please check the barcode and try again.');
  869. barcodeValueRef.current = '';
  870. setBarcodeInput('');
  871. focusBarcodeInput();
  872. }
  873. } catch (err: any) {
  874. setError(err.message || 'Failed to scan barcode');
  875. barcodeValueRef.current = '';
  876. setBarcodeInput('');
  877. focusBarcodeInput();
  878. } finally {
  879. setScanning(false);
  880. }
  881. };
  882.  
  883. const handleClearScans = () => {
  884. setScannedItems([]);
  885. setError('');
  886. };
  887.  
  888. const handleSessionChange = (e: Event) => {
  889. const target = e.currentTarget as HTMLSelectElement;
  890. const sessionId = target.value;
  891.  
  892. if (sessionId) {
  893. loadSession(sessionId);
  894. } else {
  895. setCurrentSessionId('');
  896. setScannedItems([]);
  897. setSelectedCollectionId('');
  898. setCollectionVariants([]);
  899. }
  900. };
  901.  
  902. const handleCollectionChange = (e: Event) => {
  903. const target = e.currentTarget as HTMLSelectElement;
  904. const collectionId = target.value;
  905. setSelectedCollectionId(collectionId);
  906.  
  907. if (collectionId) {
  908. fetchCollectionInventory(collectionId);
  909. } else {
  910. setCollectionVariants([]);
  911. }
  912. };
  913.  
  914. const getComparisonData = (): ComparisonItem[] => {
  915. const comparisonMap = new Map<string, ComparisonItem>();
  916.  
  917. collectionVariants.forEach((variant) => {
  918. const editedQty = editedQuantities.get(variant.barcode);
  919. const scannedQty = editedQty !== undefined ? editedQty : 0;
  920.  
  921. comparisonMap.set(variant.barcode, {
  922. barcode: variant.barcode,
  923. productTitle: variant.productTitle,
  924. variantTitle: variant.variantTitle,
  925. sku: variant.sku,
  926. expectedQuantity: variant.inventoryQuantity,
  927. scannedQuantity: scannedQty,
  928. difference: null,
  929. variantId: variant.variantId,
  930. selected: selectedItems.has(variant.barcode),
  931. });
  932. });
  933.  
  934. scannedItems.forEach((item) => {
  935. const editedQty = editedQuantities.get(item.barcode);
  936. const scannedQty = editedQty !== undefined ? editedQty : item.quantity;
  937.  
  938. if (comparisonMap.has(item.barcode)) {
  939. const existing = comparisonMap.get(item.barcode)!;
  940. existing.scannedQuantity = scannedQty;
  941. existing.difference = scannedQty - (existing.expectedQuantity || 0);
  942. } else {
  943. comparisonMap.set(item.barcode, {
  944. barcode: item.barcode,
  945. productTitle: item.productTitle,
  946. variantTitle: item.variantTitle,
  947. sku: item.sku,
  948. expectedQuantity: null,
  949. scannedQuantity: scannedQty,
  950. difference: null,
  951. variantId: item.variantId || null,
  952. selected: selectedItems.has(item.barcode),
  953. });
  954. }
  955. });
  956.  
  957. comparisonMap.forEach((item) => {
  958. if (item.expectedQuantity !== null && item.difference === null) {
  959. item.difference = item.scannedQuantity - item.expectedQuantity;
  960. }
  961. });
  962.  
  963. return Array.from(comparisonMap.values());
  964. };
  965.  
  966. const handleQuantityEdit = (barcode: string, newQty: number) => {
  967. const updated = new Map(editedQuantities);
  968. updated.set(barcode, newQty);
  969. setEditedQuantities(updated);
  970. };
  971.  
  972. const handleSelectItem = (barcode: string, checked: boolean) => {
  973. const updated = new Set(selectedItems);
  974. if (checked) {
  975. updated.add(barcode);
  976. } else {
  977. updated.delete(barcode);
  978. }
  979. setSelectedItems(updated);
  980. };
  981.  
  982. const handleSelectAll = (checked: boolean) => {
  983. if (checked) {
  984. const allBarcodes = filteredComparisonData
  985. .filter((item) => item.difference !== 0 && item.variantId)
  986. .map((item) => item.barcode);
  987. setSelectedItems(new Set(allBarcodes));
  988. } else {
  989. setSelectedItems(new Set());
  990. }
  991. };
  992.  
  993. const applyInventoryChanges = async () => {
  994. setApplyingInventory(true);
  995. setError('');
  996.  
  997. try {
  998. const itemsToApply = filteredComparisonData.filter(
  999. (item) =>
  1000. selectedItems.has(item.barcode) && item.variantId && item.expectedQuantity !== null,
  1001. );
  1002.  
  1003. if (itemsToApply.length === 0) {
  1004. setError('No items selected to apply');
  1005. setApplyingInventory(false);
  1006. return;
  1007. }
  1008.  
  1009. const variantIds = itemsToApply.map((item) => item.variantId!);
  1010.  
  1011. const inventoryQuery = `query GetInventoryItems($ids: [ID!]!) {
  1012. nodes(ids: $ids) {
  1013. ... on ProductVariant {
  1014. id
  1015. inventoryItem {
  1016. id
  1017. inventoryLevels(first: 10) {
  1018. edges {
  1019. node {
  1020. location {
  1021. id
  1022. name
  1023. }
  1024. quantities(names: [\"available\"]) {
  1025. name
  1026. quantity
  1027. }
  1028. }
  1029. }
  1030. }
  1031. }
  1032. }
  1033. }
  1034. }`;
  1035.  
  1036. const { data: invData, errors: invErrors } = await shopify.query(inventoryQuery, {
  1037. variables: { ids: variantIds },
  1038. });
  1039.  
  1040. if (invErrors && invErrors.length > 0) {
  1041. const errorMessage = invErrors.map((e: any) => e.message).join(', ');
  1042. setError(errorMessage);
  1043. setApplyingInventory(false);
  1044. return;
  1045. }
  1046.  
  1047. const changes: any[] = [];
  1048.  
  1049. invData?.nodes?.forEach((node: any, index: number) => {
  1050. if (!node?.inventoryItem) return;
  1051.  
  1052. const item = itemsToApply[index];
  1053. const inventoryItemId = node.inventoryItem.id;
  1054. const levels = node.inventoryItem.inventoryLevels.edges;
  1055.  
  1056. if (levels.length > 0) {
  1057. const level = levels[0].node;
  1058. const locationId = level.location.id;
  1059. const currentQty = level.quantities[0]?.quantity || 0;
  1060. const delta = item.scannedQuantity - currentQty;
  1061.  
  1062. if (delta !== 0) {
  1063. changes.push({
  1064. delta: delta,
  1065. inventoryItemId: inventoryItemId,
  1066. locationId: locationId,
  1067. });
  1068. }
  1069. }
  1070. });
  1071.  
  1072. if (changes.length === 0) {
  1073. setSuccessMessage('No inventory changes needed');
  1074. setTimeout(() => setSuccessMessage(''), 3000);
  1075. confirmApplyModalRef.current?.hideOverlay();
  1076. setApplyingInventory(false);
  1077. return;
  1078. }
  1079.  
  1080. const adjustMutation = `mutation AdjustInventory($input: InventoryAdjustQuantitiesInput!) {
  1081. inventoryAdjustQuantities(input: $input) {
  1082. inventoryAdjustmentGroup {
  1083. id
  1084. reason
  1085. }
  1086. userErrors {
  1087. field
  1088. message
  1089. code
  1090. }
  1091. }
  1092. }`;
  1093.  
  1094. const { data: adjustData, errors: adjustErrors } = await shopify.query(adjustMutation, {
  1095. variables: {
  1096. input: {
  1097. reason: 'correction',
  1098. name: 'available',
  1099. changes: changes,
  1100. },
  1101. },
  1102. });
  1103.  
  1104. if (adjustErrors && adjustErrors.length > 0) {
  1105. const errorMessage = adjustErrors.map((e: any) => e.message).join(', ');
  1106. setError(errorMessage);
  1107. setApplyingInventory(false);
  1108. return;
  1109. }
  1110.  
  1111. if (adjustData?.inventoryAdjustQuantities?.userErrors?.length > 0) {
  1112. const userErrorMessage = adjustData.inventoryAdjustQuantities.userErrors
  1113. .map((e: any) => (e.field ? `${e.field}: ${e.message}` : e.message))
  1114. .join(', ');
  1115. setError(userErrorMessage);
  1116. setApplyingInventory(false);
  1117. return;
  1118. }
  1119.  
  1120. setSuccessMessage(`Successfully updated inventory for ${changes.length} items`);
  1121. setTimeout(() => setSuccessMessage(''), 5000);
  1122. confirmApplyModalRef.current?.hideOverlay();
  1123. setSelectedItems(new Set());
  1124.  
  1125. if (selectedCollectionId) {
  1126. await fetchCollectionInventory(selectedCollectionId);
  1127. }
  1128. } catch (err: any) {
  1129. setError(err.message || 'Failed to apply inventory changes');
  1130. } finally {
  1131. setApplyingInventory(false);
  1132. }
  1133. };
  1134.  
  1135. const generateCSV = (data: ComparisonItem[]): string => {
  1136. const escapeCSV = (value: string | number | null): string => {
  1137. if (value === null || value === undefined) return '';
  1138. const stringValue = String(value);
  1139. if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
  1140. return `"${stringValue.replace(/"/g, '""')}"`;
  1141. }
  1142. return stringValue;
  1143. };
  1144.  
  1145. const headers = [
  1146. 'Product',
  1147. 'Variant',
  1148. 'SKU',
  1149. 'Barcode',
  1150. 'Expected Quantity',
  1151. 'Scanned Quantity',
  1152. 'Difference',
  1153. ];
  1154.  
  1155. const rows = data.map((item) => [
  1156. escapeCSV(item.productTitle),
  1157. escapeCSV(item.variantTitle),
  1158. escapeCSV(item.sku),
  1159. escapeCSV(item.barcode),
  1160. escapeCSV(item.expectedQuantity !== null ? item.expectedQuantity : 'N/A'),
  1161. escapeCSV(item.scannedQuantity),
  1162. escapeCSV(item.difference !== null ? item.difference : 'N/A'),
  1163. ]);
  1164.  
  1165. const csvContent = [headers.join(','), ...rows.map((row) => row.join(','))].join('\n');
  1166. return csvContent;
  1167. };
  1168.  
  1169. const getCSVFilename = (): string => {
  1170. const currentSession = sessions.find((s) => s.id === currentSessionId);
  1171. const sessionName = currentSession?.name || 'stock-take';
  1172. const date = new Date().toISOString().split('T')[0];
  1173. const sanitizedName = sessionName.replace(/[^a-z0-9]/gi, '-').toLowerCase();
  1174. return `${sanitizedName}-${date}.csv`;
  1175. };
  1176.  
  1177. const comparisonData = getComparisonData();
  1178. const filteredComparisonData = showOnlyDiscrepancies
  1179. ? comparisonData.filter((item) => item.difference !== 0)
  1180. : comparisonData;
  1181.  
  1182. const selectedCount = filteredComparisonData.filter((item) =>
  1183. selectedItems.has(item.barcode),
  1184. ).length;
  1185.  
  1186. const csvDataUrl =
  1187. comparisonData.length > 0
  1188. ? `data:text/csv;charset=utf-8,${encodeURIComponent(generateCSV(comparisonData))}`
  1189. : '';
  1190.  
  1191. return (
  1192. <s-page heading="Stock Take">
  1193. {error && (
  1194. <s-banner tone="critical" dismissible onDismiss={() => setError('')}>
  1195. {error}
  1196. </s-banner>
  1197. )}
  1198.  
  1199. {successMessage && (
  1200. <s-banner tone="success" dismissible onDismiss={() => setSuccessMessage('')}>
  1201. {successMessage}
  1202. </s-banner>
  1203. )}
  1204.  
  1205. <s-section heading="Session Management">
  1206. <s-stack gap="base" direction="inline" alignItems="end">
  1207. <s-select
  1208. id="session-select"
  1209. label="Active Session"
  1210. value={currentSessionId}
  1211. onChange={handleSessionChange}
  1212. disabled={loadingSessions}
  1213. placeholder="Select a session"
  1214. >
  1215. <s-option value="">Select a session</s-option>
  1216. {sessions.map((session) => (
  1217. <s-option key={session.id} value={session.id}>
  1218. {session.name} ({new Date(session.createdDate).toLocaleDateString()})
  1219. </s-option>
  1220. ))}
  1221. </s-select>
  1222. <s-button
  1223. id="new-session-button"
  1224. variant="primary"
  1225. commandFor="new-session-modal"
  1226. command="--show"
  1227. disabled={!definitionReady}
  1228. >
  1229. New Stock Take
  1230. </s-button>
  1231. {currentSessionId && (
  1232. <s-button
  1233. id="delete-session-button"
  1234. variant="secondary"
  1235. tone="critical"
  1236. commandFor="confirm-delete-modal"
  1237. command="--show"
  1238. >
  1239. Delete Session
  1240. </s-button>
  1241. )}
  1242. </s-stack>
  1243. </s-section>
  1244.  
  1245. {!currentSessionId ? (
  1246. <s-section>
  1247. <s-text color="subdued">Create or select a stock take session to begin.</s-text>
  1248. </s-section>
  1249. ) : (
  1250. <>
  1251. <s-section heading="Scan Items">
  1252. <s-stack gap="base">
  1253. <s-stack direction="inline" gap="base" alignItems="end">
  1254. <s-text-field
  1255. id="barcode-input"
  1256. label="Scan Barcode"
  1257. value={barcodeInput}
  1258. onInput={handleBarcodeInput}
  1259. placeholder="Scan barcode or type and press Enter"
  1260. icon="barcode"
  1261. />
  1262. <s-button
  1263. id="clear-button"
  1264. variant="secondary"
  1265. onClick={handleClearScans}
  1266. disabled={scannedItems.length === 0}
  1267. >
  1268. Clear All Scans
  1269. </s-button>
  1270. </s-stack>
  1271.  
  1272. <s-section padding="none">
  1273. <s-box padding="base">
  1274. <s-heading>Scanned Items ({scannedItems.length})</s-heading>
  1275. </s-box>
  1276. <s-table id="scanned-table">
  1277. <s-table-header-row>
  1278. <s-table-header listSlot="primary">Product</s-table-header>
  1279. <s-table-header listSlot="secondary">Variant</s-table-header>
  1280. <s-table-header listSlot="labeled">SKU</s-table-header>
  1281. <s-table-header listSlot="labeled">Barcode</s-table-header>
  1282. <s-table-header listSlot="labeled" format="numeric">
  1283. Quantity
  1284. </s-table-header>
  1285. </s-table-header-row>
  1286. <s-table-body>
  1287. {scannedItems.length === 0 ? (
  1288. <s-table-row>
  1289. <s-table-cell>
  1290. <s-text color="subdued">
  1291. No items scanned yet. Scan a barcode to begin.
  1292. </s-text>
  1293. </s-table-cell>
  1294. </s-table-row>
  1295. ) : (
  1296. scannedItems.map((item, index) => (
  1297. <s-table-row key={`${item.barcode}-${index}`}>
  1298. <s-table-cell>{item.productTitle}</s-table-cell>
  1299. <s-table-cell>{item.variantTitle}</s-table-cell>
  1300. <s-table-cell>{item.sku}</s-table-cell>
  1301. <s-table-cell>{item.barcode}</s-table-cell>
  1302. <s-table-cell>{item.quantity}</s-table-cell>
  1303. </s-table-row>
  1304. ))
  1305. )}
  1306. </s-table-body>
  1307. </s-table>
  1308. </s-section>
  1309. </s-stack>
  1310. </s-section>
  1311.  
  1312. <s-section heading="Collection Inventory">
  1313. <s-stack gap="base">
  1314. <s-select
  1315. id="collection-select"
  1316. label="Select Collection"
  1317. value={selectedCollectionId}
  1318. onChange={handleCollectionChange}
  1319. disabled={loadingCollections}
  1320. placeholder="Choose a collection"
  1321. >
  1322. <s-option value="">Select a collection</s-option>
  1323. {collections.map((collection) => (
  1324. <s-option key={collection.id} value={collection.id}>
  1325. {collection.title}
  1326. </s-option>
  1327. ))}
  1328. </s-select>
  1329.  
  1330. {selectedCollectionId && (
  1331. <s-section padding="none">
  1332. <s-box padding="base">
  1333. <s-heading>Collection Items ({collectionVariants.length})</s-heading>
  1334. </s-box>
  1335. <s-table id="collection-table" loading={loadingCollection}>
  1336. <s-table-header-row>
  1337. <s-table-header listSlot="primary">Product</s-table-header>
  1338. <s-table-header listSlot="secondary">Variant</s-table-header>
  1339. <s-table-header listSlot="labeled">SKU</s-table-header>
  1340. <s-table-header listSlot="labeled">Barcode</s-table-header>
  1341. <s-table-header listSlot="labeled" format="numeric">
  1342. Expected Qty
  1343. </s-table-header>
  1344. </s-table-header-row>
  1345. <s-table-body>
  1346. {collectionVariants.length === 0 && !loadingCollection ? (
  1347. <s-table-row>
  1348. <s-table-cell>
  1349. <s-text color="subdued">No items in this collection.</s-text>
  1350. </s-table-cell>
  1351. </s-table-row>
  1352. ) : (
  1353. collectionVariants.map((variant, index) => (
  1354. <s-table-row key={`${variant.barcode}-${index}`}>
  1355. <s-table-cell>{variant.productTitle}</s-table-cell>
  1356. <s-table-cell>{variant.variantTitle}</s-table-cell>
  1357. <s-table-cell>{variant.sku}</s-table-cell>
  1358. <s-table-cell>{variant.barcode}</s-table-cell>
  1359. <s-table-cell>{variant.inventoryQuantity}</s-table-cell>
  1360. </s-table-row>
  1361. ))
  1362. )}
  1363. </s-table-body>
  1364. </s-table>
  1365. </s-section>
  1366. )}
  1367. </s-stack>
  1368. </s-section>
  1369.  
  1370. <s-section heading="Comparison">
  1371. {!selectedCollectionId ? (
  1372. <s-text color="subdued">Select a collection to compare scanned items.</s-text>
  1373. ) : (
  1374. <s-stack gap="base">
  1375. <s-stack direction="inline" gap="base" alignItems="center">
  1376. <s-checkbox
  1377. id="filter-checkbox"
  1378. label="Show only discrepancies"
  1379. checked={showOnlyDiscrepancies}
  1380. onChange={(e: Event) =>
  1381. setShowOnlyDiscrepancies((e.currentTarget as HTMLInputElement).checked)
  1382. }
  1383. />
  1384. {comparisonData.length > 0 && csvDataUrl && (
  1385. <s-link id="export-csv-link" href={csvDataUrl} download={getCSVFilename()}>
  1386. <s-button id="export-csv-button" variant="secondary">
  1387. Export to CSV
  1388. </s-button>
  1389. </s-link>
  1390. )}
  1391. {filteredComparisonData.filter((item) => item.difference !== 0 && item.variantId)
  1392. .length > 0 && (
  1393. <s-button
  1394. id="apply-inventory-button"
  1395. variant="primary"
  1396. commandFor="confirm-apply-modal"
  1397. command="--show"
  1398. disabled={selectedCount === 0}
  1399. >
  1400. Apply to Inventory ({selectedCount} selected)
  1401. </s-button>
  1402. )}
  1403. </s-stack>
  1404.  
  1405. <s-section padding="none">
  1406. <s-box padding="base">
  1407. <s-stack direction="inline" gap="base" alignItems="center">
  1408. <s-heading>Comparison Results ({filteredComparisonData.length})</s-heading>
  1409. {filteredComparisonData.filter(
  1410. (item) => item.difference !== 0 && item.variantId,
  1411. ).length > 0 && (
  1412. <s-checkbox
  1413. id="select-all-checkbox"
  1414. label="Select all"
  1415. checked={
  1416. selectedCount ===
  1417. filteredComparisonData.filter(
  1418. (item) => item.difference !== 0 && item.variantId,
  1419. ).length
  1420. }
  1421. onChange={(e: Event) =>
  1422. handleSelectAll((e.currentTarget as HTMLInputElement).checked)
  1423. }
  1424. />
  1425. )}
  1426. </s-stack>
  1427. </s-box>
  1428. <s-table id="comparison-table">
  1429. <s-table-header-row>
  1430. <s-table-header listSlot="labeled">Select</s-table-header>
  1431. <s-table-header listSlot="primary">Product</s-table-header>
  1432. <s-table-header listSlot="secondary">Variant</s-table-header>
  1433. <s-table-header listSlot="labeled">SKU</s-table-header>
  1434. <s-table-header listSlot="labeled">Barcode</s-table-header>
  1435. <s-table-header listSlot="labeled" format="numeric">
  1436. Expected
  1437. </s-table-header>
  1438. <s-table-header listSlot="labeled" format="numeric">
  1439. Scanned
  1440. </s-table-header>
  1441. <s-table-header listSlot="labeled" format="numeric">
  1442. Difference
  1443. </s-table-header>
  1444. </s-table-header-row>
  1445. <s-table-body>
  1446. {filteredComparisonData.length === 0 ? (
  1447. <s-table-row>
  1448. <s-table-cell>
  1449. <s-text color="subdued">
  1450. {showOnlyDiscrepancies
  1451. ? 'No discrepancies found. All scanned quantities match expected inventory.'
  1452. : 'No data to compare.'}
  1453. </s-text>
  1454. </s-table-cell>
  1455. </s-table-row>
  1456. ) : (
  1457. filteredComparisonData.map((item, index) => (
  1458. <s-table-row key={`${item.barcode}-${index}`}>
  1459. <s-table-cell>
  1460. {item.difference !== 0 && item.variantId ? (
  1461. <s-checkbox
  1462. id={`select-${item.barcode}`}
  1463. checked={item.selected}
  1464. onChange={(e: Event) =>
  1465. handleSelectItem(
  1466. item.barcode,
  1467. (e.currentTarget as HTMLInputElement).checked,
  1468. )
  1469. }
  1470. />
  1471. ) : null}
  1472. </s-table-cell>
  1473. <s-table-cell>{item.productTitle}</s-table-cell>
  1474. <s-table-cell>{item.variantTitle}</s-table-cell>
  1475. <s-table-cell>{item.sku}</s-table-cell>
  1476. <s-table-cell>{item.barcode}</s-table-cell>
  1477. <s-table-cell>
  1478. {item.expectedQuantity !== null ? item.expectedQuantity : 'N/A'}
  1479. </s-table-cell>
  1480. <s-table-cell>
  1481. <s-number-field
  1482. id={`qty-${item.barcode}`}
  1483. value={item.scannedQuantity.toString()}
  1484. onInput={(e: Event) =>
  1485. handleQuantityEdit(
  1486. item.barcode,
  1487. parseInt((e.currentTarget as HTMLInputElement).value) || 0,
  1488. )
  1489. }
  1490. min={0}
  1491. />
  1492. </s-table-cell>
  1493. <s-table-cell>
  1494. {item.difference !== null ? (
  1495. <s-badge
  1496. tone={
  1497. item.difference === 0
  1498. ? 'success'
  1499. : item.difference > 0
  1500. ? 'warning'
  1501. : 'critical'
  1502. }
  1503. >
  1504. {item.difference > 0 ? '+' : ''}
  1505. {item.difference}
  1506. </s-badge>
  1507. ) : (
  1508. <s-text color="subdued">N/A</s-text>
  1509. )}
  1510. </s-table-cell>
  1511. </s-table-row>
  1512. ))
  1513. )}
  1514. </s-table-body>
  1515. </s-table>
  1516. </s-section>
  1517. </s-stack>
  1518. )}
  1519. </s-section>
  1520. </>
  1521. )}
  1522.  
  1523. <s-modal ref={newSessionModalRef} id="new-session-modal" heading="Create New Stock Take">
  1524. <s-text-field
  1525. id="session-name-input"
  1526. label="Session Name"
  1527. value={newSessionName}
  1528. onInput={(e: Event) => setNewSessionName((e.currentTarget as HTMLInputElement).value)}
  1529. placeholder="e.g., Monthly Stock Take - January 2024"
  1530. />
  1531. <s-button
  1532. slot="secondary-actions"
  1533. id="cancel-session-button"
  1534. variant="secondary"
  1535. commandFor="new-session-modal"
  1536. command="--hide"
  1537. >
  1538. Cancel
  1539. </s-button>
  1540. <s-button
  1541. slot="primary-action"
  1542. id="create-session-button"
  1543. variant="primary"
  1544. loading={savingSession}
  1545. onClick={createNewSession}
  1546. disabled={!newSessionName.trim()}
  1547. >
  1548. Create Session
  1549. </s-button>
  1550. </s-modal>
  1551.  
  1552. <s-modal
  1553. ref={confirmApplyModalRef}
  1554. id="confirm-apply-modal"
  1555. heading="Confirm Inventory Update"
  1556. >
  1557. <s-text>
  1558. You are about to update inventory for {selectedCount} item(s). This will adjust the
  1559. available inventory to match the scanned quantities. Do you want to continue?
  1560. </s-text>
  1561. <s-button
  1562. slot="secondary-actions"
  1563. id="cancel-apply-button"
  1564. variant="secondary"
  1565. commandFor="confirm-apply-modal"
  1566. command="--hide"
  1567. >
  1568. Cancel
  1569. </s-button>
  1570. <s-button
  1571. slot="primary-action"
  1572. id="confirm-apply-button"
  1573. variant="primary"
  1574. loading={applyingInventory}
  1575. onClick={applyInventoryChanges}
  1576. >
  1577. Apply Changes
  1578. </s-button>
  1579. </s-modal>
  1580.  
  1581. <s-modal
  1582. ref={confirmDeleteModalRef}
  1583. id="confirm-delete-modal"
  1584. heading="Delete Stock Take Session"
  1585. >
  1586. <s-text>
  1587. Are you sure you want to delete this stock take session? This action cannot be undone.
  1588. </s-text>
  1589. <s-button
  1590. slot="secondary-actions"
  1591. id="cancel-delete-button"
  1592. variant="secondary"
  1593. commandFor="confirm-delete-modal"
  1594. command="--hide"
  1595. >
  1596. Cancel
  1597. </s-button>
  1598. <s-button
  1599. slot="primary-action"
  1600. id="confirm-delete-button"
  1601. variant="primary"
  1602. tone="critical"
  1603. loading={deletingSession}
  1604. onClick={deleteSession}
  1605. >
  1606. Delete Session
  1607. </s-button>
  1608. </s-modal>
  1609. </s-page>
  1610. );
  1611. }
  1612.  
  1613. export default (): void => render(<Extension />, document.body);
Advertisement
Add Comment
Please, Sign In to add comment