backend_shopify_data4autos/app/routes/app.managebrand.jsx
2025-07-12 17:20:28 +00:00

1119 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData } from "@remix-run/react";
import {
Page,
Layout,
IndexTable,
Card,
Thumbnail,
TextContainer,
Spinner,
Button,
TextField,
Banner,
InlineError,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { TitleBar } from "@shopify/app-bridge-react";
export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
const accessToken = await getTurn14AccessTokenFromMetafield(request);
const res = await admin.graphql(`{
shop {
metafield(namespace: "turn14", key: "selected_brands") {
value
}
}
}`);
const data = await res.json();
const rawValue = data?.data?.shop?.metafield?.value;
let brands = [];
try {
brands = JSON.parse(rawValue);
} catch (err) {
console.error("❌ Failed to parse metafield value:", err);
}
return json({ brands, accessToken });
};
// export const action = async ({ request }) => {
// const { admin } = await authenticate.admin(request);
// const formData = await request.formData();
// const brandId = formData.get("brandId");
// const rawCount = formData.get("productCount");
// const productCount = parseInt(rawCount, 10) || 10;
// const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
// const accessToken = await getTurn14AccessTokenFromMetafield(request);
// // Fetch items from Turn14 API
// const itemsRes = await fetch(
// `https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`,
// {
// headers: {
// Authorization: `Bearer ${accessToken}`,
// "Content-Type": "application/json",
// },
// }
// );
// const itemsData = await itemsRes.json();
// function slugify(str) {
// return str
// .toString()
// .trim()
// .toLowerCase()
// .replace(/[^a-z0-9]+/g, '-')
// .replace(/^-+|-+$/g, '');
// }
// const items = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : [];
// const results = [];
// for (const item of items) {
// const attrs = item.attributes;
// // Build and normalize collection titles
// const category = attrs.category;
// const subcategory = attrs.subcategory || "";
// const brand = attrs.brand;
// const subcats = subcategory
// .split(/[,\/]/)
// .map((s) => s.trim())
// .filter(Boolean);
// const collectionTitles = Array.from(
// new Set([category, ...subcats, brand].filter(Boolean))
// );
// // Find or create collections, collect their IDs
// const collectionIds = [];
// for (const title of collectionTitles) {
// const lookupRes = await admin.graphql(`
// {
// collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") {
// nodes { id }
// }
// }
// `);
// const lookupJson = await lookupRes.json();
// const existing = lookupJson.data.collections.nodes;
// if (existing.length) {
// collectionIds.push(existing[0].id);
// } else {
// const createColRes = await admin.graphql(`
// mutation($input: CollectionInput!) {
// collectionCreate(input: $input) {
// collection { id }
// userErrors { field message }
// }
// }
// `, { variables: { input: { title } } });
// const createColJson = await createColRes.json();
// const errs = createColJson.data.collectionCreate.userErrors;
// if (errs.length) {
// throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`);
// }
// collectionIds.push(createColJson.data.collectionCreate.collection.id);
// }
// }
// // Build tags
// const tags = [
// attrs.category,
// ...subcats,
// attrs.brand,
// attrs.part_number,
// attrs.mfr_part_number,
// attrs.price_group,
// attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
// attrs.barcode
// ].filter(Boolean).map((t) => t.trim());
// // Prepare media inputs
// const mediaInputs = (attrs.files || [])
// .filter((f) => f.type === "Image" && f.url)
// .map((file) => ({
// originalSource: file.url,
// mediaContentType: "IMAGE",
// alt: `${attrs.product_name} — ${file.media_content}`,
// }));
// // Pick the longest "Market Description" or fallback to part_description
// const marketDescs = (attrs.descriptions || [])
// .filter((d) => d.type === "Market Description")
// .map((d) => d.description);
// const descriptionHtml = marketDescs.length
// ? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
// : attrs.part_description;
// // Create product + attach to collections + add media
// const createProdRes = await admin.graphql(`
// mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
// productCreate(product: $prod, media: $media) {
// product {
// id
// variants(first: 1) {
// nodes { id inventoryItem { id } }
// }
// }
// userErrors { field message }
// }
// }
// `, {
// variables: {
// prod: {
// title: attrs.product_name,
// descriptionHtml: descriptionHtml,
// vendor: attrs.brand,
// productType: attrs.category,
// handle: slugify(attrs.part_number || attrs.product_name),
// tags,
// collectionsToJoin: collectionIds,
// status: "ACTIVE",
// },
// media: mediaInputs,
// },
// });
// const createProdJson = await createProdRes.json();
// const prodErrs = createProdJson.data.productCreate.userErrors;
// if (prodErrs.length) {
// const taken = prodErrs.some((e) => /already in use/i.test(e.message));
// if (taken) {
// results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
// continue;
// }
// throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
// }
// const product = createProdJson.data.productCreate.product;
// const variantNode = product.variants.nodes[0];
// const variantId = variantNode.id;
// const inventoryItemId = variantNode.inventoryItem.id;
// // Bulk-update variant (price, compare-at, barcode)
// const price = parseFloat(attrs.price) || 1000;
// const comparePrice = parseFloat(attrs.compare_price) || null;
// const barcode = attrs.barcode || "";
// const bulkRes = await admin.graphql(`
// mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
// productVariantsBulkUpdate(productId: $productId, variants: $variants) {
// productVariants { id price compareAtPrice barcode }
// userErrors { field message }
// }
// }
// `, {
// variables: {
// productId: product.id,
// variants: [{
// id: variantId,
// price,
// ...(comparePrice !== null && { compareAtPrice: comparePrice }),
// ...(barcode && { barcode }),
// }],
// },
// });
// const bulkJson = await bulkRes.json();
// const bulkErrs = bulkJson.data.productVariantsBulkUpdate.userErrors;
// if (bulkErrs.length) {
// throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(", ")}`);
// }
// const updatedVariant = bulkJson.data.productVariantsBulkUpdate.productVariants[0];
// // Update inventory item (SKU, cost & weight)
// const costPerItem = parseFloat(attrs.purchase_cost) || 0;
// const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
// const invRes = await admin.graphql(`
// mutation($id: ID!, $input: InventoryItemInput!) {
// inventoryItemUpdate(id: $id, input: $input) {
// inventoryItem {
// id
// sku
// measurement {
// weight { value unit }
// }
// }
// userErrors { field message }
// }
// }
// `, {
// variables: {
// id: inventoryItemId,
// input: {
// sku: attrs.part_number,
// cost: costPerItem,
// measurement: {
// weight: { value: weightValue, unit: "POUNDS" }
// },
// },
// },
// });
// const invJson = await invRes.json();
// const invErrs = invJson.data.inventoryItemUpdate.userErrors;
// if (invErrs.length) {
// throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
// }
// const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
// // Collect results
// results.push({
// productId: product.id,
// variant: {
// id: updatedVariant.id,
// price: updatedVariant.price,
// compareAtPrice: updatedVariant.compareAtPrice,
// sku: inventoryItem.sku,
// barcode: updatedVariant.barcode,
// weight: inventoryItem.measurement.weight.value,
// weightUnit: inventoryItem.measurement.weight.unit,
// },
// collections: collectionTitles,
// tags,
// });
// }
// return json({ success: true, results });
// };
// export const action = async ({ request }) => {
// const { admin } = await authenticate.admin(request);
// const formData = await request.formData();
// const brandId = formData.get("brandId");
// const rawCount = formData.get("productCount");
// const productCount = parseInt(rawCount, 10) || 10;
// const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
// const accessToken = await getTurn14AccessTokenFromMetafield(request);
// // Fetch items from Turn14 API
// console.log("Fetching items from Turn14 API...");
// const itemsRes = await fetch(
// `https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`,
// {
// headers: {
// Authorization: `Bearer ${accessToken}`,
// "Content-Type": "application/json",
// },
// }
// );
// const itemsData = await itemsRes.json();
// console.log("Items data fetched:", itemsData);
// function slugify(str) {
// return str
// .toString()
// .trim()
// .toLowerCase()
// .replace(/[^a-z0-9]+/g, '-')
// .replace(/^-+|-+$/g, '');
// }
// const items = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : [];
// console.log(`Processing ${items.length} items...`);
// const results = [];
// for (const item of items) {
// const attrs = item.attributes;
// console.log("Processing item:", attrs);
// // Build and normalize collection titles
// const category = attrs.category;
// const subcategory = attrs.subcategory || "";
// const brand = attrs.brand;
// const subcats = subcategory
// .split(/[,\/]/)
// .map((s) => s.trim())
// .filter(Boolean);
// const collectionTitles = Array.from(
// new Set([category, ...subcats, brand].filter(Boolean))
// );
// console.log("Collection Titles:", collectionTitles);
// // Find or create collections, collect their IDs
// const collectionIds = [];
// for (const title of collectionTitles) {
// console.log(`Searching for collection with title: ${title}`);
// const lookupRes = await admin.graphql(`
// {
// collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") {
// nodes { id }
// }
// }
// `);
// const lookupJson = await lookupRes.json();
// console.log("Lookup response for collections:", lookupJson);
// const existing = lookupJson.data.collections.nodes;
// if (existing.length) {
// console.log(`Found existing collection for title: ${title}`);
// collectionIds.push(existing[0].id);
// } else {
// console.log(`Creating new collection for title: ${title}`);
// const createColRes = await admin.graphql(`
// mutation($input: CollectionInput!) {
// collectionCreate(input: $input) {
// collection { id }
// userErrors { field message }
// }
// }
// `, { variables: { input: { title } } });
// const createColJson = await createColRes.json();
// console.log("Create collection response:", createColJson);
// const errs = createColJson.data.collectionCreate.userErrors;
// if (errs.length) {
// throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`);
// }
// collectionIds.push(createColJson.data.collectionCreate.collection.id);
// }
// }
// // Build tags
// const tags = [
// attrs.category,
// ...subcats,
// attrs.brand,
// attrs.part_number,
// attrs.mfr_part_number,
// attrs.price_group,
// attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
// attrs.barcode
// ].filter(Boolean).map((t) => t.trim());
// console.log("Tags:", tags);
// // Prepare media inputs
// const mediaInputs = (attrs.files || [])
// .filter((f) => f.type === "Image" && f.url)
// .map((file) => ({
// originalSource: file.url,
// mediaContentType: "IMAGE",
// alt: `${attrs.product_name} — ${file.media_content}`,
// }));
// console.log("Media inputs:", mediaInputs);
// // Pick the longest "Market Description" or fallback to part_description
// const marketDescs = (attrs.descriptions || [])
// .filter((d) => d.type === "Market Description")
// .map((d) => d.description);
// const descriptionHtml = marketDescs.length
// ? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
// : attrs.part_description;
// console.log("Description HTML:", descriptionHtml);
// // Create product + attach to collections + add media
// const createProdRes = await admin.graphql(`
// mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
// productCreate(product: $prod, media: $media) {
// product {
// id
// variants(first: 1) {
// nodes { id inventoryItem { id } }
// }
// }
// userErrors { field message }
// }
// }
// `, {
// variables: {
// prod: {
// title: attrs.product_name,
// descriptionHtml: descriptionHtml,
// vendor: attrs.brand,
// productType: attrs.category,
// handle: slugify(attrs.part_number || attrs.product_name),
// tags,
// collectionsToJoin: collectionIds,
// status: "ACTIVE",
// },
// media: mediaInputs,
// },
// });
// const createProdJson = await createProdRes.json();
// console.log("Create product response:", createProdJson);
// const prodErrs = createProdJson.data.productCreate.userErrors;
// if (prodErrs.length) {
// const taken = prodErrs.some((e) => /already in use/i.test(e.message));
// if (taken) {
// results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
// continue;
// }
// throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
// }
// const product = createProdJson.data.productCreate.product;
// const variantNode = product.variants.nodes[0];
// const variantId = variantNode.id;
// const inventoryItemId = variantNode.inventoryItem.id;
// // Fetch the Online Store publication ID
// console.log("Fetching Online Store publication ID...");
// const publicationsRes = await admin.graphql(`
// query {
// publications(first: 10) {
// edges {
// node {
// id
// name
// }
// }
// }
// }
// `);
// const publicationsJson = await publicationsRes.json();
// console.log("Publications response:", publicationsJson);
// const onlineStorePublication = publicationsJson.data.publications.edges.find(pub => pub.node.name === 'Online Store');
// console.log("Online Store Publication:", onlineStorePublication);
// const onlineStorePublicationId = onlineStorePublication ? onlineStorePublication.node.id : null;
// if (onlineStorePublicationId) {
// console.log("Publishing product to Online Store...");
// // Publish the product to the Online Store
// const publishRes = await admin.graphql(`
// mutation($id: ID!, $publicationId: ID!) {
// publishablePublish(id: $id, input: { publicationId: $publicationId }) {
// publishable {
// ... on Product {
// id
// title
// status
// }
// }
// userErrors { field message }
// }
// }
// `, {
// variables: {
// id: product.id,
// publicationId: onlineStorePublicationId,
// },
// });
// const publishJson = await publishRes.json();
// console.log("Publish response:", publishJson);
// const publishErrs = publishJson.data.publishablePublish.userErrors;
// if (publishErrs.length) {
// throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(", ")}`);
// }
// } else {
// throw new Error("Online Store publication not found.");
// }
// // Update inventory item (SKU, cost & weight)
// const costPerItem = parseFloat(attrs.purchase_cost) || 0;
// const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
// console.log("Updating inventory item...");
// const invRes = await admin.graphql(`
// mutation($id: ID!, $input: InventoryItemInput!) {
// inventoryItemUpdate(id: $id, input: $input) {
// inventoryItem {
// id
// sku
// measurement {
// weight { value unit }
// }
// }
// userErrors { field message }
// }
// }
// `, {
// variables: {
// id: inventoryItemId,
// input: {
// sku: attrs.part_number,
// cost: costPerItem,
// measurement: {
// weight: { value: weightValue, unit: "POUNDS" }
// },
// },
// },
// });
// const invJson = await invRes.json();
// console.log("Inventory update response:", invJson);
// const invErrs = invJson.data.inventoryItemUpdate.userErrors;
// if (invErrs.length) {
// throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
// }
// const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
// results.push({
// productId: product.id,
// variant: {
// id: variantId, // Use the variantId from variantNode
// price: variantNode.price, // Use price from variantNode
// compareAtPrice: variantNode.compareAtPrice, // Use compareAtPrice from variantNode
// sku: inventoryItem.sku, // SKU from the updated inventory item
// barcode: variantNode.barcode, // Barcode from the variant
// weight: inventoryItem.measurement.weight.value, // Weight from inventory item
// weightUnit: inventoryItem.measurement.weight.unit, // Weight unit from inventory item
// },
// collections: collectionTitles,
// tags,
// });
// }
// return json({ success: true, results });
// };
export const action = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const formData = await request.formData();
const brandId = formData.get("brandId");
const rawCount = formData.get("productCount");
const productCount = parseInt(rawCount, 10) || 10;
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
const accessToken = await getTurn14AccessTokenFromMetafield(request);
// Fetch items from Turn14 API
console.log("Fetching items from Turn14 API...");
const itemsRes = await fetch(
`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
}
);
const itemsData = await itemsRes.json();
console.log("Items data fetched:", itemsData);
function slugify(str) {
return str
.toString()
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
const items = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : [];
console.log(`Processing ${items.length} items...`);
const results = [];
for (const item of items) {
const attrs = item.attributes;
console.log("Processing item:", attrs);
// Build and normalize collection titles
const category = attrs.category;
const subcategory = attrs.subcategory || "";
const brand = attrs.brand;
const subcats = subcategory
.split(/[,\/]/)
.map((s) => s.trim())
.filter(Boolean);
const collectionTitles = Array.from(
new Set([category, ...subcats, brand].filter(Boolean))
);
console.log("Collection Titles:", collectionTitles);
// Find or create collections, collect their IDs
const collectionIds = [];
for (const title of collectionTitles) {
console.log(`Searching for collection with title: ${title}`);
const lookupRes = await admin.graphql(`
{
collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") {
nodes { id }
}
}
`);
const lookupJson = await lookupRes.json();
console.log("Lookup response for collections:", lookupJson);
const existing = lookupJson.data?.collections?.nodes || [];
if (existing.length) {
console.log(`Found existing collection for title: ${title}`);
collectionIds.push(existing[0]?.id); // Use optional chaining
} else {
console.log(`Creating new collection for title: ${title}`);
const createColRes = await admin.graphql(`
mutation($input: CollectionInput!) {
collectionCreate(input: $input) {
collection { id }
userErrors { field message }
}
}`, { variables: { input: { title } } });
const createColJson = await createColRes.json();
console.log("Create collection response:", createColJson);
const errs = createColJson.data?.collectionCreate?.userErrors || [];
if (errs.length) {
throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`);
}
collectionIds.push(createColJson.data?.collectionCreate?.collection?.id); // Optional chaining here too
}
}
// Build tags
const tags = [
attrs.category,
...subcats,
attrs.brand,
attrs.part_number,
attrs.mfr_part_number,
attrs.price_group,
attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
attrs.barcode
].filter(Boolean).map((t) => t.trim());
console.log("Tags:", tags);
// Prepare media inputs
const mediaInputs = (attrs.files || [])
.filter((f) => f.type === "Image" && f.url)
.map((file) => ({
originalSource: file.url,
mediaContentType: "IMAGE",
alt: `${attrs.product_name}${file.media_content}`,
}));
console.log("Media inputs:", mediaInputs);
// Pick the longest "Market Description" or fallback to part_description
const marketDescs = (attrs.descriptions || [])
.filter((d) => d.type === "Market Description")
.map((d) => d.description);
const descriptionHtml = marketDescs.length
? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
: attrs.part_description;
console.log("Description HTML:", descriptionHtml);
// Create product + attach to collections + add media
// const createProdRes = await admin.graphql(`
// mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
// productCreate(product: $prod, media: $media) {
// product {
// id
// variants(first: 1) {
// nodes { id inventoryItem { id } price compareAtPrice barcode }
// }
// }
// userErrors { field message }
// }
// }
// `, {
// variables: {
// prod: {
// title: attrs.product_name,
// descriptionHtml: descriptionHtml,
// vendor: attrs.brand,
// productType: attrs.category,
// handle: slugify(attrs.part_number || attrs.product_name),
// tags,
// collectionsToJoin: collectionIds,
// status: "ACTIVE",
// },
// media: mediaInputs,
// },
// });
const createProdRes = await admin.graphql(`
mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
productCreate(product: $prod, media: $media) {
product {
id
variants(first: 1) {
nodes { id inventoryItem { id } price compareAtPrice barcode }
}
}
userErrors { field message }
}
}
`, {
variables: {
prod: {
title: attrs.product_name,
descriptionHtml: descriptionHtml,
vendor: attrs.brand,
productType: attrs.category,
handle: slugify(item.id+"-"+attrs.mfr_part_number || attrs.product_name),
tags,
collectionsToJoin: collectionIds,
status: "ACTIVE",
},
media: mediaInputs,
},
});
const createProdJson = await createProdRes.json();
console.log("Create product response:", createProdJson);
const prodErrs = createProdJson.data?.productCreate?.userErrors || [];
if (prodErrs.length) {
const taken = prodErrs.some((e) => /already in use/i.test(e.message));
if (taken) {
results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
continue;
}
throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
}
const product = createProdJson.data.productCreate.product;
const variantNode = product.variants?.nodes?.[0];
if (!variantNode) {
console.error("Variant node is undefined for product:", product.id);
continue;
}
const variantId = variantNode.id;
const inventoryItemId = variantNode.inventoryItem?.id;
// Fetch the Online Store publication ID
console.log("Fetching Online Store publication ID...");
const publicationsRes = await admin.graphql(`
query {
publications(first: 10) {
edges {
node {
id
name
}
}
}
}
`);
const publicationsJson = await publicationsRes.json();
console.log("Publications response:", publicationsJson);
const onlineStorePublication = publicationsJson.data.publications.edges.find(pub => pub.node.name === 'Online Store');
const onlineStorePublicationId = onlineStorePublication ? onlineStorePublication.node.id : null;
if (onlineStorePublicationId) {
console.log("Publishing product to Online Store...");
const publishRes = await admin.graphql(`
mutation($id: ID!, $publicationId: ID!) {
publishablePublish(id: $id, input: { publicationId: $publicationId }) {
publishable {
... on Product {
id
title
status
}
}
userErrors { field message }
}
}
`, {
variables: {
id: product.id,
publicationId: onlineStorePublicationId,
},
});
const publishJson = await publishRes.json();
console.log("Publish response:", publishJson);
const publishErrs = publishJson.data.publishablePublish.userErrors;
if (publishErrs.length) {
throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(", ")}`);
}
} else {
throw new Error("Online Store publication not found.");
}
// Update inventory item (SKU, cost & weight)
const costPerItem = parseFloat(attrs.purchase_cost) || 0;
const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
console.log("Updating inventory item...");
const invRes = await admin.graphql(`
mutation($id: ID!, $input: InventoryItemInput!) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
sku
measurement {
weight { value unit }
}
}
userErrors { field message }
}
}
`, {
variables: {
id: inventoryItemId,
input: {
sku: attrs.part_number,
cost: costPerItem,
measurement: {
weight: { value: weightValue, unit: "POUNDS" }
},
},
},
});
const invJson = await invRes.json();
console.log("Inventory update response:", invJson);
const invErrs = invJson.data.inventoryItemUpdate.userErrors;
if (invErrs.length) {
throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
}
const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
// Collect results
results.push({
productId: product.id,
variant: {
id: variantId,
price: variantNode.price,
compareAtPrice: variantNode.compareAtPrice,
sku: inventoryItem.sku,
barcode: variantNode.barcode,
weight: inventoryItem.measurement.weight.value,
weightUnit: inventoryItem.measurement.weight.unit,
},
collections: collectionTitles,
tags,
});
}
return json({ success: true, results });
};
export default function ManageBrandProducts() {
const actionData = useActionData();
const { brands, accessToken } = useLoaderData();
const [expandedBrand, setExpandedBrand] = useState(null);
const [itemsMap, setItemsMap] = useState({});
const [loadingMap, setLoadingMap] = useState({});
const [productCount, setProductCount] = useState("10");
const [initialLoad, setInitialLoad] = useState(true);
const toggleAllBrands = async () => {
for (const brand of brands) {
await toggleBrandItems(brand.id);
}
};
useEffect(() => {
if (initialLoad && brands.length > 0) {
toggleAllBrands();
setInitialLoad(false);
}
}, [brands, initialLoad]);
// const toggleBrandItems = async (brandId) => {
// const isExpanded = expandedBrand === brandId;
// if (isExpanded) {
// setExpandedBrand(null);
// } else {
// setExpandedBrand(brandId);
// if (!itemsMap[brandId]) {
// setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
// try {
// const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
// headers: {
// Authorization: `Bearer ${accessToken}`,
// "Content-Type": "application/json",
// },
// });
// const data = await res.json();
// setItemsMap((prev) => ({ ...prev, [brandId]: data }));
// } catch (err) {
// console.error("Error fetching items:", err);
// }
// setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
// }
// }
// };
const toggleBrandItems = async (brandId) => {
const isExpanded = expandedBrand === brandId;
if (isExpanded) {
setExpandedBrand(null);
} else {
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const data = await res.json();
// Ensure we have an array of valid items
const validItems = Array.isArray(data)
? data.filter(item => item && item.id && item.attributes)
: [];
setItemsMap((prev) => ({ ...prev, [brandId]: validItems }));
} catch (err) {
console.error("Error fetching items:", err);
setItemsMap((prev) => ({ ...prev, [brandId]: [] })); // Set empty array on error
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
}
};
return (
<Page title="Data4Autos Turn14 Manage Brand Products">
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
{brands.length === 0 ? (
<Layout.Section>
<Card sectioned>
<p>No brands selected yet.</p>
</Card>
</Layout.Section>
) : (
<Layout.Section>
<Card>
<IndexTable
resourceName={{ singular: "brand", plural: "brands" }}
itemCount={brands.length}
headings={[
{ title: "Brand ID" },
{ title: "Logo" },
{ title: "Action" },
{ title: "Products Count" },
]}
selectable={false}
>
{brands.map((brand, index) => (
<IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}>
<IndexTable.Cell>{brand.id}</IndexTable.Cell>
<IndexTable.Cell>
<Thumbnail
source={
brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="small"
/>
</IndexTable.Cell>
<IndexTable.Cell>
<Button onClick={() => toggleBrandItems(brand.id)}>
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button>
</IndexTable.Cell>
<IndexTable.Cell>{itemsMap[brand.id]?.length || 0}</IndexTable.Cell>
</IndexTable.Row>
))}
</IndexTable>
</Card>
</Layout.Section>
)}
{brands.map(
(brand) =>
expandedBrand === brand.id && (
<Layout.Section fullWidth key={brand.id + "-expanded"}>
<Card sectioned>
{actionData?.success && (
<Banner title="✅ Product created!" status="success">
<p>
{actionData.results.map((r) => (
<span key={r.variant.id}>
Product {r.productId} Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})<br />
</span>
))}
</p>
</Banner>
)}
</Card>
<Card title={`Items from ${brand.name}`} sectioned>
{loadingMap[brand.id] ? (
<Spinner accessibilityLabel="Loading items" size="small" />
) : (
<div style={{ paddingTop: "1rem" }}>
<Form method="post">
<input type="hidden" name="brandId" value={brand.id} />
<TextField
label="Number of products to add"
type="number"
name="productCount"
value={productCount}
onChange={setProductCount}
autoComplete="off"
/>
<Button submit primary style={{ marginTop: "1rem" }}>
Add First {productCount} Products to Store
</Button>
</Form>
{/* <p>Total Products Available: {(itemsMap[brand.id] || []).length}</p>
{(
itemsMap[brand.id] && itemsMap[brand.id].length > 0
? itemsMap[brand.id]
: []
).map((item) => (
<Card key={item?.id} title={item?.attributes.product_name} sectioned>
<Layout>
<Layout.Section oneThird>
<Thumbnail
source={
item?.attributes.thumbnail ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={item?.attributes.product_name}
size="large"
/>
</Layout.Section>
<Layout.Section>
<TextContainer spacing="tight">
<p><strong>Part Number:</strong> {item?.attributes.part_number}</p>
<p><strong>Category:</strong> {item?.attributes.category} &gt; {item?.attributes.subcategory}</p>
<p><strong>Price:</strong> ${item?.attributes.price}</p>
<p><strong>Description:</strong> {item?.attributes.part_description}</p>
</TextContainer>
</Layout.Section>
</Layout>
</Card>
))} */}
{(
itemsMap[brand.id] && itemsMap[brand.id].length > 0
? itemsMap[brand.id].filter(item => item && item.id) // Filter out null/undefined items
: []
).map((item) => (
<Card key={item.id} title={item?.attributes?.product_name || 'Untitled Product'} sectioned>
<Layout>
<Layout.Section oneThird>
<Thumbnail
source={
item?.attributes?.thumbnail ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={item?.attributes?.product_name || 'Product image'}
size="large"
/>
</Layout.Section>
<Layout.Section>
<TextContainer spacing="tight">
<p><strong>Part Number:</strong> {item?.attributes?.part_number || 'N/A'}</p>
<p><strong>Category:</strong> {item?.attributes?.category || 'N/A'} &gt; {item?.attributes?.subcategory || 'N/A'}</p>
<p><strong>Price:</strong> ${item?.attributes?.price || '0.00'}</p>
<p><strong>Description:</strong> {item?.attributes?.part_description || 'No description available'}</p>
</TextContainer>
</Layout.Section>
</Layout>
</Card>
))}
</div>
)}
</Card>
</Layout.Section>
)
)}
</Layout>
</Page>
);
}