diff --git a/app/routes/app.brands copy.jsx b/app/routes/app.brands copy.jsx new file mode 100644 index 0000000..85e25cc --- /dev/null +++ b/app/routes/app.brands copy.jsx @@ -0,0 +1,273 @@ +import { json } from "@remix-run/node"; +import { useLoaderData, useFetcher } from "@remix-run/react"; +import { + Page, + Layout, + Card, + TextField, + Checkbox, + Button, + Thumbnail, + Spinner, + Toast, + Frame, +} from "@shopify/polaris"; +import { useEffect, useState } from "react"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + const accessToken = await getTurn14AccessTokenFromMetafield(request); + const { admin } = await authenticate.admin(request); + + // Get brands + const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + const brandJson = await brandRes.json(); + if (!brandRes.ok) { + return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 }); + } + + // Get collections + const gqlRaw = await admin.graphql(` + { + collections(first: 100) { + edges { + node { + id + title + handle + } + } + } + } + `); + const gql = await gqlRaw.json(); + const collections = gql?.data?.collections?.edges?.map((e) => e.node) || []; + + return json({ + brands: brandJson.data, + collections, + }); +}; + +export const action = async ({ request }) => { + const formData = await request.formData(); + const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]"); + + const { admin } = await authenticate.admin(request); + + // Get current collections + const gqlRaw = await admin.graphql(` + { + collections(first: 100) { + edges { + node { + id + title + } + } + } + } + `); + const gql = await gqlRaw.json(); + const existingCollections = gql?.data?.collections?.edges?.map((e) => e.node) || []; + + const selectedTitles = selectedBrands.map((b) => b.name.toLowerCase()); + const logoMap = Object.fromEntries(selectedBrands.map(b => [b.name.toLowerCase(), b.logo])); + + // Delete unselected + for (const col of existingCollections) { + if (!selectedTitles.includes(col.title.toLowerCase())) { + await admin.graphql(` + mutation { + collectionDelete(input: { id: "${col.id}" }) { + deletedCollectionId + userErrors { message } + } + } + `); + } + } + + + // Create new + for (const brand of selectedBrands) { + const exists = existingCollections.find( + (c) => c.title.toLowerCase() === brand.name.toLowerCase() + ); + if (!exists) { + const escapedName = brand.name.replace(/"/g, '\\"'); + const logo = brand.logo || ""; + + await admin.graphql(` + mutation { + collectionCreate(input: { + title: "${escapedName}", + descriptionHtml: "Products from brand ${escapedName}", + image: { + altText: "${escapedName} Logo", + src: "${logo}" + } + }) { + collection { id } + userErrors { message } + } + } + `); + } + } + +const shopDataRaw = await admin.graphql(` + { + shop { + id + } + } +`); +const shopRes = await admin.graphql(`{ shop { id } }`); +const shopJson = await shopRes.json(); +const shopId = shopJson?.data?.shop?.id; + +await admin.graphql(` + mutation { + metafieldsSet(metafields: [{ + namespace: "turn14", + key: "selected_brands", + type: "json", + ownerId: "${shopId}", + value: ${JSON.stringify(JSON.stringify(selectedBrands))} + }]) { + metafields { + id + } + userErrors { + message + } + } + } +`); + + + + return json({ success: true }); + +}; + +export default function BrandsPage() { + const { brands, collections } = useLoaderData(); + const fetcher = useFetcher(); + const isSubmitting = fetcher.state === "submitting"; + const [toastActive, setToastActive] = useState(false); + const [search, setSearch] = useState(""); + + const collectionTitles = new Set(collections.map((c) => c.title.toLowerCase())); + const defaultSelected = brands + .filter((b) => collectionTitles.has(b.name.toLowerCase())) + .map((b) => b.id); + + const [selectedIds, setSelectedIds] = useState(defaultSelected); + const [filteredBrands, setFilteredBrands] = useState(brands); + + useEffect(() => { + const term = search.toLowerCase(); + setFilteredBrands(brands.filter((b) => b.name.toLowerCase().includes(term))); + }, [search, brands]); + + useEffect(() => { + if (fetcher.data?.success) { + setToastActive(true); + } + }, [fetcher.data]); + + const toggleSelect = (id) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ); + }; + + const toastMarkup = toastActive ? ( + setToastActive(false)} + /> + ) : null; + + const selectedBrands = brands.filter((b) => selectedIds.includes(b.id)); + + return ( + + + + + +
+ +
+ + +
+ {filteredBrands.map((brand) => ( + +
+ toggleSelect(brand.id)} + /> + +
+ {brand.name} +
+
+
+ ))} +
+
+ + + + + + + +
+ {toastMarkup} +
+ + ); +} diff --git a/app/routes/app.brands.jsx b/app/routes/app.brands.jsx index 85e25cc..a057342 100644 --- a/app/routes/app.brands.jsx +++ b/app/routes/app.brands.jsx @@ -95,7 +95,6 @@ export const action = async ({ request }) => { } } - // Create new for (const brand of selectedBrands) { const exists = existingCollections.find( @@ -123,40 +122,37 @@ export const action = async ({ request }) => { } } -const shopDataRaw = await admin.graphql(` - { - shop { - id - } - } -`); -const shopRes = await admin.graphql(`{ shop { id } }`); -const shopJson = await shopRes.json(); -const shopId = shopJson?.data?.shop?.id; - -await admin.graphql(` - mutation { - metafieldsSet(metafields: [{ - namespace: "turn14", - key: "selected_brands", - type: "json", - ownerId: "${shopId}", - value: ${JSON.stringify(JSON.stringify(selectedBrands))} - }]) { - metafields { + const shopDataRaw = await admin.graphql(` + { + shop { id } - userErrors { - message + } + `); + const shopRes = await admin.graphql(`{ shop { id } }`); + const shopJson = await shopRes.json(); + const shopId = shopJson?.data?.shop?.id; + + await admin.graphql(` + mutation { + metafieldsSet(metafields: [{ + namespace: "turn14", + key: "selected_brands", + type: "json", + ownerId: "${shopId}", + value: ${JSON.stringify(JSON.stringify(selectedBrands))} + }]) { + metafields { + id + } + userErrors { + message + } } } - } -`); - - + `); return json({ success: true }); - }; export default function BrandsPage() { @@ -191,6 +187,22 @@ export default function BrandsPage() { ); }; + const toggleSelectAll = () => { + const filteredBrandIds = filteredBrands.map(b => b.id); + const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id)); + + if (allFilteredSelected) { + // Deselect all filtered brands + setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id))); + } else { + // Select all filtered brands + setSelectedIds(prev => { + const combined = new Set([...prev, ...filteredBrandIds]); + return Array.from(combined); + }); + } + }; + const toastMarkup = toastActive ? ( selectedIds.includes(b.id)); + const allFilteredSelected = filteredBrands.length > 0 && + filteredBrands.every(brand => selectedIds.includes(brand.id)); return ( @@ -206,14 +220,22 @@ export default function BrandsPage() { -
- +
+ +
+ +
+
@@ -270,4 +292,4 @@ export default function BrandsPage() { ); -} +} \ No newline at end of file diff --git a/app/routes/app.managebrand.jsx b/app/routes/app.managebrand.jsx index 5e29109..f5dc9cb 100644 --- a/app/routes/app.managebrand.jsx +++ b/app/routes/app.managebrand.jsx @@ -1,435 +1,5 @@ -/* import React, { useState } from "react"; -import { json } from "@remix-run/node"; -import { useLoaderData, Form, useActionData } from "@remix-run/react"; -import { - Page, - Layout, - Card, - Thumbnail, - TextContainer, - Spinner, - Button, - TextField, - Banner, - InlineError, -} from "@shopify/polaris"; -import { authenticate } from "../shopify.server"; -import { TitleBar } from "@shopify/app-bridge-react"; -// Load selected brands and access token from Shopify metafield -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 }); -}; - -// Handle adding products for a specific brand -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 items1 = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : []; - const results = []; - for (const item of items1) { - const attrs = item.attributes; - - // 0️⃣ 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)) - ); - - // 1️⃣ Find or create collections, collect their IDs - const collectionIds = []; - for (const title of collectionTitles) { - // lookup - 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 { - // create - 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); - } - } - - // 2️⃣ 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()); - - // 3️⃣ 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}`, - })); - - - // 2️⃣ 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; - - // 4️⃣ 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; - - // 5️⃣ 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]; - - // 6️⃣ 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; - - // 7️⃣ 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 }); -}; - -// Main React component for managing brand products -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 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/brand/${brandId}?page=1`, { - 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 })); - } - } - }; - - return ( - - - - {brands.length === 0 && ( - - -

No brands selected yet.

-
-
- )} - - {brands.map((brand) => ( - - - - - -

ID: {brand.id}

-
- -
-
- - {expandedBrand === brand.id && ( - - - {actionData?.success && ( - -

- {actionData.results.map((r) => ( - - Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})
-
- ))} -

-
- )} -
- - - - -
- - - {loadingMap[brand.id] ? ( - - ) : ( -
- {(itemsMap[brand.id] || []).map((item) => ( - - - - - - - -

Part Number: {item.attributes.part_number}

-

Category: {item.attributes.category} > {item.attributes.subcategory}

-
-
-
-
- ))} -
- )} -
-
- )} -
- ))} -
-
- ); -} - */ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { json } from "@remix-run/node"; import { useLoaderData, Form, useActionData } from "@remix-run/react"; import { @@ -478,7 +48,29 @@ export default function ManageBrandProducts() { const [expandedBrand, setExpandedBrand] = useState(null); const [itemsMap, setItemsMap] = useState({}); const [loadingMap, setLoadingMap] = useState({}); - const [productCount, setProductCount] = useState("10"); + const [productCount, setProductCount] = useState("0"); + + + + + const [initialLoad, setInitialLoad] = useState(true); + +// Function to toggle all brands +const toggleAllBrands = async () => { + for (const brand of brands) { + await toggleBrandItems(brand.id); + } +}; + +// Run on initial load +useEffect(() => { + if (initialLoad && brands.length > 0) { + toggleAllBrands(); + setInitialLoad(false); + } +}, [brands, initialLoad]); + + const toggleBrandItems = async (brandId) => { const isExpanded = expandedBrand === brandId; @@ -486,21 +78,26 @@ export default function ManageBrandProducts() { setExpandedBrand(null); } else { setExpandedBrand(brandId); + if (!itemsMap[brandId]) { setLoadingMap((prev) => ({ ...prev, [brandId]: true })); try { - const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, { + // const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, { + 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(); + setProductCount(data.length) setItemsMap((prev) => ({ ...prev, [brandId]: data })); } catch (err) { console.error("Error fetching items:", err); } setLoadingMap((prev) => ({ ...prev, [brandId]: false })); + } else { + setProductCount(itemsMap[brandId].length) } } }; @@ -525,6 +122,7 @@ export default function ManageBrandProducts() { { title: "Brand ID" }, { title: "Logo" }, { title: "Action" }, + { title: "Products Count" }, ]} selectable={false} > @@ -546,6 +144,7 @@ export default function ManageBrandProducts() { {expandedBrand === brand.id ? "Hide Products" : "Show Products"} + {itemsMap[brand.id]?.length || 0} ))} @@ -569,44 +168,52 @@ export default function ManageBrandProducts() {

)} -
- - - - + {loadingMap[brand.id] ? ( ) : ( + +
- {(itemsMap[brand.id] || []).map((item) => ( - +
+ + + + + Total Products Count : {(itemsMap[brand.id] || []).length} + {( + itemsMap[brand.id] && itemsMap[brand.id].length > 0 + ? itemsMap[brand.id] + : [] + ).map((item) => ( + -

Part Number: {item.attributes.part_number}

-

Category: {item.attributes.category} > {item.attributes.subcategory}

+

Part Number: {item?.attributes.part_number}

+

Category: {item?.attributes.category} > {item?.attributes.subcategory}

diff --git a/app/routes/app.managebrand_bak.jsx b/app/routes/app.managebrand_bak.jsx index 1e6954a..01207a7 100644 --- a/app/routes/app.managebrand_bak.jsx +++ b/app/routes/app.managebrand_bak.jsx @@ -260,6 +260,8 @@ export default function ManageBrandProducts() { ) : (
+ Total Products Count : {(itemsMap[brand.id] || []).length} + {(itemsMap[brand.id] || []).map(item => ( diff --git a/app/routes/app.managebrand_bak_300625.jsx b/app/routes/app.managebrand_bak_300625.jsx new file mode 100644 index 0000000..5e29109 --- /dev/null +++ b/app/routes/app.managebrand_bak_300625.jsx @@ -0,0 +1,624 @@ +/* import React, { useState } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData, Form, useActionData } from "@remix-run/react"; +import { + Page, + Layout, + Card, + Thumbnail, + TextContainer, + Spinner, + Button, + TextField, + Banner, + InlineError, +} from "@shopify/polaris"; +import { authenticate } from "../shopify.server"; +import { TitleBar } from "@shopify/app-bridge-react"; +// Load selected brands and access token from Shopify metafield +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 }); +}; + +// Handle adding products for a specific brand +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 items1 = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : []; + const results = []; + for (const item of items1) { + const attrs = item.attributes; + + // 0️⃣ 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)) + ); + + // 1️⃣ Find or create collections, collect their IDs + const collectionIds = []; + for (const title of collectionTitles) { + // lookup + 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 { + // create + 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); + } + } + + // 2️⃣ 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()); + + // 3️⃣ 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}`, + })); + + + // 2️⃣ 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; + + // 4️⃣ 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; + + // 5️⃣ 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]; + + // 6️⃣ 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; + + // 7️⃣ 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 }); +}; + +// Main React component for managing brand products +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 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/brand/${brandId}?page=1`, { + 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 })); + } + } + }; + + return ( + + + + {brands.length === 0 && ( + + +

No brands selected yet.

+
+
+ )} + + {brands.map((brand) => ( + + + + + +

ID: {brand.id}

+
+ +
+
+ + {expandedBrand === brand.id && ( + + + {actionData?.success && ( + +

+ {actionData.results.map((r) => ( + + Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})
+
+ ))} +

+
+ )} +
+ + + + +
+ + + {loadingMap[brand.id] ? ( + + ) : ( +
+ {(itemsMap[brand.id] || []).map((item) => ( + + + + + + + +

Part Number: {item.attributes.part_number}

+

Category: {item.attributes.category} > {item.attributes.subcategory}

+
+
+
+
+ ))} +
+ )} +
+
+ )} +
+ ))} +
+
+ ); +} + */ +import React, { 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, +} 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 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 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/brand/${brandId}?page=1`, { + 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 })); + } + } + }; + + return ( + + + + {brands.length === 0 ? ( + + +

No brands selected yet.

+
+
+ ) : ( + + + + {brands.map((brand, index) => ( + + {brand.id} + + + + + + + + ))} + + + + )} + + {brands.map( + (brand) => + expandedBrand === brand.id && ( + + + {actionData?.success && ( + +

+ {actionData.results.map((r) => ( + + Product {r.productId} – Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})
+
+ ))} +

+
+ )} +
+ + + + +
+ + + {loadingMap[brand.id] ? ( + + ) : ( +
+ {(itemsMap[brand.id] || []).map((item) => ( + + + + + + + +

Part Number: {item.attributes.part_number}

+

Category: {item.attributes.category} > {item.attributes.subcategory}

+
+
+
+
+ ))} +
+ )} +
+
+ ) + )} +
+
+ ); +} diff --git a/app/routes/app.settings.jsx b/app/routes/app.settings.jsx index 6f8924e..5f50384 100644 --- a/app/routes/app.settings.jsx +++ b/app/routes/app.settings.jsx @@ -111,6 +111,8 @@ export const action = async ({ request }) => { } `; + + const saveRes = await admin.graphql(mutation); const result = await saveRes.json(); @@ -177,7 +179,7 @@ export default function SettingsPage({ standalone = true }) { />
@@ -190,8 +192,8 @@ export default function SettingsPage({ standalone = true }) { {displayToken && ( -

✅ Access token:

- ✅ Connection Successful

+ {/* {displayToken} - +
*/}
)}
diff --git a/shopify.app.toml b/shopify.app.toml index 5761663..fbb8a86 100644 --- a/shopify.app.toml +++ b/shopify.app.toml @@ -1,6 +1,6 @@ client_id = "b7534c980967bad619cfdb9d3f837cfa" name = "turn14-test" -handle = "turn14-test-1" +handle = "d4a-turn14" application_url = "https://shopify.data4autos.com" # Update this line embedded = true