diff --git a/app/routes/app.brands.jsx b/app/routes/app.brands.jsx index 730f03d..fcb8ce7 100644 --- a/app/routes/app.brands.jsx +++ b/app/routes/app.brands.jsx @@ -1,5 +1,5 @@ import { json } from "@remix-run/node"; -import { useLoaderData, useFetcher } from "@remix-run/react"; +import { useLoaderData, Form, useActionData } from "@remix-run/react"; import { Page, Layout, @@ -21,7 +21,7 @@ export const loader = async ({ request }) => { const accessToken = await getTurn14AccessTokenFromMetafield(request); const { admin } = await authenticate.admin(request); - // Get brands + // fetch brands const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { headers: { Authorization: `Bearer ${accessToken}`, @@ -33,7 +33,7 @@ export const loader = async ({ request }) => { return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 }); } - // Get collections + // fetch Shopify collections const gqlRaw = await admin.graphql(` { collections(first: 100) { @@ -41,230 +41,129 @@ export const loader = async ({ request }) => { node { id title - handle } } } } `); const gql = await gqlRaw.json(); - const collections = gql?.data?.collections?.edges?.map((e) => e.node) || []; + const collections = gql?.data?.collections?.edges.map(e => e.node) || []; - return json({ - brands: brandJson.data, - collections, - }); + return json({ brands: brandJson.data, collections }); }; export const action = async ({ request }) => { const formData = await request.formData(); const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]"); + const selectedOldBrands = JSON.parse(formData.get("selectedOldBrands") || "[]"); + const { session } = await authenticate.admin(request); + const shop = session.shop; // "veloxautomotive.myshopify.com" - const { admin } = await authenticate.admin(request); + selectedBrands.forEach(brand => { + delete brand.pricegroups; + }); - // 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) || []; + selectedOldBrands.forEach(brand => { + delete brand.pricegroups; + }); - 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 } - } - } - `); - } + const resp = await fetch("https://backend.data4autos.com/managebrands", { + method: "POST", + headers: { + "Content-Type": "application/json", + "shop-domain": shop, + }, + body: JSON.stringify({ shop, selectedBrands, selectedOldBrands }), + }); + + if (!resp.ok) { + const err = await resp.text(); + return json({ error: err }, { status: resp.status }); } - // 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 } - // } - // } - // `); - // } - // } - - - // for (const brand of selectedBrands) { - // const exists = existingCollections.find( - // (c) => c.title.toLowerCase() === brand.name.toLowerCase() - // ); - // if (!exists) { - // const escapedName = brand.name.replace(/"/g, '\\"'); - // // Only build the image block if there's a logo URL: - // const imageBlock = brand.logo - // ? ` - // image: { - // altText: "${escapedName} Logo", - // src: "${brand.logo}" - // } - // ` - // : ""; - - // await admin.graphql(` - // mutation { - // collectionCreate(input: { - // title: "${escapedName}", - // descriptionHtml: "Products from brand ${escapedName}" - // ${imageBlock} - // }) { - // collection { id } - // userErrors { message } - // } - // } - // `); - // } - // } - - const fallbackLogo = - "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"; - - for (const brand of selectedBrands) { - const exists = existingCollections.find( - (c) => c.title.toLowerCase() === brand.name.toLowerCase() - ); - if (exists) continue; - - const escapedName = brand.name.replace(/"/g, '\\"'); - const logoSrc = brand.logo || fallbackLogo; - - await admin.graphql(` - mutation { - collectionCreate(input: { - title: "${escapedName}", - descriptionHtml: "Products from brand ${escapedName}", - image: { - altText: "${escapedName} Logo", - src: "${logoSrc}" - } - }) { - 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 }); + const { processId, status } = await resp.json(); + return json({ processId, status }); }; export default function BrandsPage() { const { brands, collections } = useLoaderData(); - const fetcher = useFetcher(); - const isSubmitting = fetcher.state === "submitting"; - const [toastActive, setToastActive] = useState(false); + const actionData = useActionData() || {}; + const [selectedIdsold, setSelectedIdsold] = useState([]) + const [selectedIds, setSelectedIds] = useState(() => { + const titles = new Set(collections.map(c => c.title.toLowerCase())); + return brands + .filter(b => titles.has(b.name.toLowerCase())) + .map(b => b.id); + }); + 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); + const [toastActive, setToastActive] = useState(false); + const [polling, setPolling] = useState(false); + const [status, setStatus] = useState(actionData.status || ""); + + + useEffect(() => { + const selids = selectedIds + console.log("Selected IDS : ", selids) + setSelectedIdsold(selids) + }, [toastActive]); + useEffect(() => { const term = search.toLowerCase(); - setFilteredBrands(brands.filter((b) => b.name.toLowerCase().includes(term))); + setFilteredBrands(brands.filter(b => b.name.toLowerCase().includes(term))); }, [search, brands]); useEffect(() => { - if (fetcher.data?.success) { + if (actionData.status) { + setStatus(actionData.status); setToastActive(true); } - }, [fetcher.data]); + }, [actionData.status]); - const toggleSelect = (id) => { - setSelectedIds((prev) => - prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + const checkStatus = async () => { + if (!actionData.processId) return; + setPolling(true); + const resp = await fetch( + `https://backend.data4autos.com/managebrands/status/${actionData.processId}`, + { headers: { "shop-domain": window.shopify.shop || "" } } ); + const jsonBody = await resp.json(); + setStatus( + jsonBody.status + (jsonBody.detail ? ` (${jsonBody.detail})` : "") + ); + setPolling(false); }; + const toggleSelect = id => + setSelectedIds(prev => + prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] + ); + + const allFilteredSelected = + filteredBrands.length > 0 && + filteredBrands.every(b => selectedIds.includes(b.id)); + const toggleSelectAll = () => { - const filteredBrandIds = filteredBrands.map(b => b.id); - const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id)); - + const ids = filteredBrands.map(b => b.id); if (allFilteredSelected) { - // Deselect all filtered brands - setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id))); + setSelectedIds(prev => prev.filter(id => !ids.includes(id))); } else { - // Select all filtered brands - setSelectedIds(prev => { - const combined = new Set([...prev, ...filteredBrandIds]); - return Array.from(combined); - }); + setSelectedIds(prev => Array.from(new Set([...prev, ...ids]))); } }; + var isSubmitting; + console.log("actionData", actionData); + if (actionData.status) { + isSubmitting = !actionData.status && !actionData.error && !actionData.processId; + } else { + isSubmitting = false; + } + console.log("isSubmitting", isSubmitting); + const toastMarkup = toastActive ? ( ) : null; - const selectedBrands = brands.filter((b) => selectedIds.includes(b.id)); - const allFilteredSelected = filteredBrands.length > 0 && - filteredBrands.every(brand => selectedIds.includes(brand.id)); - + const selectedBrands = brands.filter(b => selectedIds.includes(b.id)); + const selectedOldBrands = brands.filter(b => selectedIdsold.includes(b.id)); + console.log("123456", selectedOldBrands) return ( + - - + +
- - +
+ -
+
+ + {(actionData.processId || false) && ( +
+

+ Process ID: {actionData.processId} +

+

+ Status: {status || "β€”"} +

+ +
+ )} -
- -
+ +
@@ -321,12 +235,12 @@ export default function BrandsPage() { style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", - gap: "16px", + gap: 16, }} > - {filteredBrands.map((brand) => ( + {filteredBrands.map(brand => ( -
+
-
- {brand.name} -
+ {brand.name}
))} + +
- - + {toastMarkup} ); -} \ No newline at end of file +} diff --git a/app/routes/app.jsx b/app/routes/app.jsx index 76a31bc..61a82c3 100644 --- a/app/routes/app.jsx +++ b/app/routes/app.jsx @@ -63,6 +63,7 @@ export default function App() { 🏷️ Brands πŸ“¦ Manage Brands πŸ†˜ Help + πŸ†˜ Testing diff --git a/app/routes/app.managebrand copy 2.jsx b/app/routes/app.managebrand copy 2.jsx new file mode 100644 index 0000000..fff7e29 --- /dev/null +++ b/app/routes/app.managebrand copy 2.jsx @@ -0,0 +1,376 @@ +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, + Toast, + Frame, + + ProgressBar, +} 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); + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + const resp = await fetch("https://backend.data4autos.com/manageProducts", { + method: "POST", + headers: { + "Content-Type": "application/json", + "shop-domain": shop, + }, + body: JSON.stringify({ + shop, + brandID: brandId, + turn14accessToken: accessToken, + productCount + }), + }); + + console.log("Response from manageProducts:", resp.status, resp.statusText); + if (!resp.ok) { + const err = await resp.text(); + return json({ error: err }, { status: resp.status }); + } + + const { processId, status } = await resp.json(); + console.log("Process ID:", processId, "Status:", status); + return json({ success: true, processId, status }); +}; + +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 [toastActive, setToastActive] = useState(false); + const [polling, setPolling] = useState(false); + const [status, setStatus] = useState(actionData?.status || ""); + const [processId, setProcessId] = useState(actionData?.processId || null); + const [progress, setProgress] = useState(0); + const [totalProducts, setTotalProducts] = useState(0); + const [processedProducts, setProcessedProducts] = useState(0); + const [currentProduct, setCurrentProduct] = useState(null); + const [results, setResults] = useState([]); + const [detail, setDetail] = useState(""); + + useEffect(() => { + if (actionData?.processId) { + setProcessId(actionData.processId); + setStatus(actionData.status || "processing"); + setToastActive(true); + } + }, [actionData]); + +const checkStatus = async () => { + setPolling(true); + try { + const response = await fetch(`https://backend.data4autos.com/manageProducts/status/${processId}`); + const data = await response.json(); + + setStatus(data.status); + setDetail(data.detail); + setProgress(data.progress); + setTotalProducts(data.stats.total); + setProcessedProducts(data.stats.processed); + setCurrentProduct(data.current); + + if (data.results) { + setResults(data.results); + } + + // Continue polling if still processing + if (data.status !== 'done' && data.status !== 'error') { + setTimeout(checkStatus, 2000); + } else { + setPolling(false); + } + } catch (error) { + setPolling(false); + setStatus('error'); + setDetail('Failed to check status'); + console.error('Error checking status:', error); + } +}; + useEffect(() => { + let interval; + if (status?.includes("processing") && processId) { + interval = setInterval(checkStatus, 5000); + } + return () => clearInterval(interval); + }, [status, processId]); + + 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(); + 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]: [] })); + } + setLoadingMap((prev) => ({ ...prev, [brandId]: false })); + } + } + }; + + const toastMarkup = toastActive ? ( + setToastActive(false)} + /> + ) : null; + + return ( + + + + + {brands.length === 0 ? ( + + +

No brands selected yet.

+
+
+ ) : ( + + + + {brands.map((brand, index) => ( + + {brand.id} + + + + + + + {itemsMap[brand.id]?.length || 0} + + ))} + + + + )} + + {brands.map( + (brand) => + expandedBrand === brand.id && ( + + + {processId && ( +
+

+ Process ID: {processId} +

+ +
+

+ Status: {status || "β€”"} +

+ + {progress > 0 && ( +
+ +

+ {processedProducts} of {totalProducts} products processed + {currentProduct && ` - Current: ${currentProduct.name} (${currentProduct.number}/${currentProduct.total})`} +

+
+ )} +
+ + {status === 'done' && results.length > 0 && ( +
+

+ Results: {results.length} products processed successfully +

+
+ )} + + {status === 'error' && ( +
+ Error: {detail} +
+ )} + + +
+ )} +
+ + + {loadingMap[brand.id] ? ( + + ) : ( +
+
+ + setProductCount(value)} + autoComplete="off" + /> + + + + {( + itemsMap[brand.id] && itemsMap[brand.id].length > 0 + ? itemsMap[brand.id].filter(item => item && item.id) + : [] + ).map((item) => ( + + + + + + + +

Part Number: {item?.attributes?.part_number || 'N/A'}

+

Category: {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}

+

Price: ${item?.attributes?.price || '0.00'}

+

Description: {item?.attributes?.part_description || 'No description available'}

+
+
+
+
+ ))} +
+ )} +
+
+ ) + )} +
+ {toastMarkup} +
+ + ); +} \ No newline at end of file diff --git a/app/routes/app.managebrand copy.jsx b/app/routes/app.managebrand copy.jsx index f5dc9cb..701d5d9 100644 --- a/app/routes/app.managebrand copy.jsx +++ b/app/routes/app.managebrand copy.jsx @@ -1,4 +1,3 @@ - import React, { useEffect, useState } from "react"; import { json } from "@remix-run/node"; import { useLoaderData, Form, useActionData } from "@remix-run/react"; @@ -13,6 +12,7 @@ import { Button, TextField, Banner, + InlineError, } from "@shopify/polaris"; import { authenticate } from "../shopify.server"; import { TitleBar } from "@shopify/app-bridge-react"; @@ -42,35 +42,896 @@ export const loader = async ({ request }) => { 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("0"); - - - - + const [productCount, setProductCount] = useState("10"); 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 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; @@ -78,11 +939,9 @@ useEffect(() => { 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/brandallitems/${brandId}`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -90,18 +949,19 @@ useEffect(() => { }, }); const data = await res.json(); - setProductCount(data.length) - setItemsMap((prev) => ({ ...prev, [brandId]: data })); + // 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 })); - } else { - setProductCount(itemsMap[brandId].length) } } }; - return ( @@ -144,7 +1004,7 @@ useEffect(() => { {expandedBrand === brand.id ? "Hide Products" : "Show Products"} - {itemsMap[brand.id]?.length || 0} + {itemsMap[brand.id]?.length || 0} ))} @@ -168,15 +1028,12 @@ useEffect(() => {

)} -
{loadingMap[brand.id] ? ( ) : ( - -
@@ -192,7 +1049,7 @@ useEffect(() => { Add First {productCount} Products to Store
- Total Products Count : {(itemsMap[brand.id] || []).length} + {/*

Total Products Available: {(itemsMap[brand.id] || []).length}

{( itemsMap[brand.id] && itemsMap[brand.id].length > 0 ? itemsMap[brand.id] @@ -214,6 +1071,37 @@ useEffect(() => {

Part Number: {item?.attributes.part_number}

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

+

Price: ${item?.attributes.price}

+

Description: {item?.attributes.part_description}

+
+ + + + ))} */} + + {( + itemsMap[brand.id] && itemsMap[brand.id].length > 0 + ? itemsMap[brand.id].filter(item => item && item.id) // Filter out null/undefined items + : [] + ).map((item) => ( + + + + + + + +

Part Number: {item?.attributes?.part_number || 'N/A'}

+

Category: {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}

+

Price: ${item?.attributes?.price || '0.00'}

+

Description: {item?.attributes?.part_description || 'No description available'}

@@ -228,4 +1116,4 @@ useEffect(() => { ); -} +} \ No newline at end of file diff --git a/app/routes/app.managebrand.jsx b/app/routes/app.managebrand.jsx index 701d5d9..d876521 100644 --- a/app/routes/app.managebrand.jsx +++ b/app/routes/app.managebrand.jsx @@ -13,6 +13,10 @@ import { TextField, Banner, InlineError, + Toast, + Frame, + Select, + ProgressBar, } from "@shopify/polaris"; import { authenticate } from "../shopify.server"; import { TitleBar } from "@shopify/app-bridge-react"; @@ -42,850 +46,46 @@ export const loader = async ({ request }) => { 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 selectedProductIds = JSON.parse(formData.get("selectedProductIds") || "[]"); 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); + const { session } = await authenticate.admin(request); + const shop = session.shop; - function slugify(str) { - return str - .toString() - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); + const resp = await fetch("https://backend.data4autos.com/manageProducts", { + method: "POST", + headers: { + "Content-Type": "application/json", + "shop-domain": shop, + }, + body: JSON.stringify({ + shop, + brandID: brandId, + turn14accessToken: accessToken, + productCount, + selectedProductIds + }), + }); + + console.log("Response from manageProducts:", resp.status, resp.statusText); + if (!resp.ok) { + const err = await resp.text(); + return json({ error: err }, { status: resp.status }); } - 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 }); + const { processId, status } = await resp.json(); + console.log("Process ID:", processId, "Status:", status); + return json({ success: true, processId, status }); }; - export default function ManageBrandProducts() { const actionData = useActionData(); const { brands, accessToken } = useLoaderData(); @@ -894,6 +94,62 @@ export default function ManageBrandProducts() { const [loadingMap, setLoadingMap] = useState({}); const [productCount, setProductCount] = useState("10"); const [initialLoad, setInitialLoad] = useState(true); + const [toastActive, setToastActive] = useState(false); + const [polling, setPolling] = useState(false); + const [status, setStatus] = useState(actionData?.status || ""); + const [processId, setProcessId] = useState(actionData?.processId || null); + const [progress, setProgress] = useState(0); + const [totalProducts, setTotalProducts] = useState(0); + const [processedProducts, setProcessedProducts] = useState(0); + const [currentProduct, setCurrentProduct] = useState(null); + const [results, setResults] = useState([]); + const [detail, setDetail] = useState(""); + + useEffect(() => { + if (actionData?.processId) { + setProcessId(actionData.processId); + setStatus(actionData.status || "processing"); + setToastActive(true); + } + }, [actionData]); + + const checkStatus = async () => { + setPolling(true); + try { + const response = await fetch(`https://backend.data4autos.com/manageProducts/status/${processId}`); + const data = await response.json(); + + setStatus(data.status); + setDetail(data.detail); + setProgress(data.progress); + setTotalProducts(data.stats.total); + setProcessedProducts(data.stats.processed); + setCurrentProduct(data.current); + + if (data.results) { + setResults(data.results); + } + + // Continue polling if still processing + if (data.status !== 'done' && data.status !== 'error') { + setTimeout(checkStatus, 2000); + } else { + setPolling(false); + } + } catch (error) { + setPolling(false); + setStatus('error'); + setDetail('Failed to check status'); + console.error('Error checking status:', error); + } + }; + useEffect(() => { + let interval; + if (status?.includes("processing") && processId) { + interval = setInterval(checkStatus, 5000); + } + return () => clearInterval(interval); + }, [status, processId]); const toggleAllBrands = async () => { for (const brand of brands) { @@ -908,31 +164,6 @@ export default function ManageBrandProducts() { } }, [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) { @@ -942,178 +173,280 @@ export default function ManageBrandProducts() { if (!itemsMap[brandId]) { setLoadingMap((prev) => ({ ...prev, [brandId]: true })); try { - const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, { + const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${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) + const dataitems = data.items + const validItems = Array.isArray(dataitems) + ? dataitems.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 + setItemsMap((prev) => ({ ...prev, [brandId]: [] })); } setLoadingMap((prev) => ({ ...prev, [brandId]: false })); } } }; + + const toastMarkup = toastActive ? ( + setToastActive(false)} + /> + ) : null; + + + + + + + + + + + const [filters, setFilters] = useState({ make: '', model: '', year: '', drive: '', baseModel: '' }); + + const handleFilterChange = (field) => (value) => { + setFilters((prev) => ({ ...prev, [field]: value })); + }; + + const applyFitmentFilters = (items) => { + return items.filter((item) => { + const tags = item?.attributes?.fitmmentTags || {}; + return ( + (!filters.make || tags.make?.includes(filters.make)) && + (!filters.model || tags.model?.includes(filters.model)) && + (!filters.year || tags.year?.includes(filters.year)) && + (!filters.drive || tags.drive?.includes(filters.drive)) && + (!filters.baseModel || tags.baseModel?.includes(filters.baseModel)) + ); + }); + }; + + + const selectedProductIds = [] + + return ( - - - - {brands.length === 0 ? ( - - -

No brands selected yet.

-
-
- ) : ( - - - - {brands.map((brand, index) => ( - - {brand.id} - - - - - - - {itemsMap[brand.id]?.length || 0} - - ))} - - - - )} + + + + + {brands.length === 0 ? ( + + +

No brands selected yet.

+
+
+ ) : ( + + + + {brands.map((brand, index) => { - {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})
-
- ))} -

-
- )} -
+ return ( - - {loadingMap[brand.id] ? ( - - ) : ( -
-
- - - + + {itemsMap[brand.id]?.length || 0} + + ) + })} + + + + )} + + {brands.map((brand) => { + const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []); + console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.id)); + const uniqueTags = { + make: new Set(), + model: new Set(), + year: new Set(), + drive: new Set(), + baseModel: new Set(), + }; + + (itemsMap[brand.id] || []).forEach(item => { + const tags = item?.attributes?.fitmmentTags || {}; + Object.keys(uniqueTags).forEach(key => { + (tags[key] || []).forEach(val => uniqueTags[key].add(val)); + }); + }); + + return ( + + expandedBrand === brand.id && + + ( + + + {processId && ( +
+

+ Process ID: {processId} +

+ +
+

+ Status: {status || "β€”"} +

+ + {progress > 0 && ( +
+ +

+ {processedProducts} of {totalProducts} products processed + {currentProduct && ` - Current: ${currentProduct.name} (${currentProduct.number}/${currentProduct.total})`} +

+
+ )} +
+ + {status === 'done' && results.length > 0 && ( +
+

+ Results: {results.length} products processed successfully +

+
+ )} + + {status === 'error' && ( +
+ Error: {detail} +
+ )} + + - - {/*

Total Products Available: {(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}

-

Price: ${item?.attributes.price}

-

Description: {item?.attributes.part_description}

-
-
-
-
- ))} */} +
+ )} +
- {( - itemsMap[brand.id] && itemsMap[brand.id].length > 0 - ? itemsMap[brand.id].filter(item => item && item.id) // Filter out null/undefined items - : [] - ).map((item) => ( - + + {loadingMap[brand.id] ? ( + + ) : ( +
+
+ item.id))} + /> + + setProductCount(value)} + autoComplete="off" + /> + + + + - ({ label: m, value: m }))]} + onChange={handleFilterChange('make')} + value={filters.make} /> - - -

Part Number: {item?.attributes?.part_number || 'N/A'}

-

Category: {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}

-

Price: ${item?.attributes?.price || '0.00'}

-

Description: {item?.attributes?.part_description || 'No description available'}

-
-
+
- ))} -
- )} -
-
+ + {filteredItems.map((item) => ( + + + + + + + +

Part Number: {item?.attributes?.part_number || 'N/A'}

+

Category: {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}

+

Price: ${item?.attributes?.price || '0.00'}

+

Description: {item?.attributes?.part_description || 'No description available'}

+
+
+
+
+ ))} + +
+ )} +
+
+ ) ) - )} -
-
+ })} +
+ {toastMarkup} +
+ ); } \ No newline at end of file diff --git a/app/routes/app.managebrand_070825.jsx b/app/routes/app.managebrand_070825.jsx new file mode 100644 index 0000000..16ce308 --- /dev/null +++ b/app/routes/app.managebrand_070825.jsx @@ -0,0 +1,475 @@ +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, + Toast, + Frame, + Select, + ProgressBar, +} 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); + + const { session } = await authenticate.admin(request); + const shop = session.shop; + + const resp = await fetch("https://backend.data4autos.com/manageProducts", { + method: "POST", + headers: { + "Content-Type": "application/json", + "shop-domain": shop, + }, + body: JSON.stringify({ + shop, + brandID: brandId, + turn14accessToken: accessToken, + productCount + }), + }); + + console.log("Response from manageProducts:", resp.status, resp.statusText); + if (!resp.ok) { + const err = await resp.text(); + return json({ error: err }, { status: resp.status }); + } + + const { processId, status } = await resp.json(); + console.log("Process ID:", processId, "Status:", status); + return json({ success: true, processId, status }); +}; + +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 [toastActive, setToastActive] = useState(false); + const [polling, setPolling] = useState(false); + const [status, setStatus] = useState(actionData?.status || ""); + const [processId, setProcessId] = useState(actionData?.processId || null); + const [progress, setProgress] = useState(0); + const [totalProducts, setTotalProducts] = useState(0); + const [processedProducts, setProcessedProducts] = useState(0); + const [currentProduct, setCurrentProduct] = useState(null); + const [results, setResults] = useState([]); + const [detail, setDetail] = useState(""); + + useEffect(() => { + if (actionData?.processId) { + setProcessId(actionData.processId); + setStatus(actionData.status || "processing"); + setToastActive(true); + } + }, [actionData]); + + const checkStatus = async () => { + setPolling(true); + try { + const response = await fetch(`https://backend.data4autos.com/manageProducts/status/${processId}`); + const data = await response.json(); + + setStatus(data.status); + setDetail(data.detail); + setProgress(data.progress); + setTotalProducts(data.stats.total); + setProcessedProducts(data.stats.processed); + setCurrentProduct(data.current); + + if (data.results) { + setResults(data.results); + } + + // Continue polling if still processing + if (data.status !== 'done' && data.status !== 'error') { + setTimeout(checkStatus, 2000); + } else { + setPolling(false); + } + } catch (error) { + setPolling(false); + setStatus('error'); + setDetail('Failed to check status'); + console.error('Error checking status:', error); + } + }; + useEffect(() => { + let interval; + if (status?.includes("processing") && processId) { + interval = setInterval(checkStatus, 5000); + } + return () => clearInterval(interval); + }, [status, processId]); + + 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/brandallitemswithfitment/${brandId}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + const data = await res.json(); + const dataitems = data.items + const validItems = Array.isArray(dataitems) + ? dataitems.filter(item => item && item.id && item.attributes) + : []; + setItemsMap((prev) => ({ ...prev, [brandId]: validItems })); + } catch (err) { + console.error("Error fetching items:", err); + setItemsMap((prev) => ({ ...prev, [brandId]: [] })); + } + setLoadingMap((prev) => ({ ...prev, [brandId]: false })); + } + } + }; + + const toastMarkup = toastActive ? ( + setToastActive(false)} + /> + ) : null; + + + + + + + + + + + const [filters, setFilters] = useState({ make: '', model: '', year: '', drive: '', baseModel: '' }); + + const handleFilterChange = (field) => (value) => { + setFilters((prev) => ({ ...prev, [field]: value })); + }; + + const applyFitmentFilters = (items) => { + return items.filter((item) => { + const tags = item?.attributes?.fitmmentTags || {}; + return ( + (!filters.make || tags.make?.includes(filters.make)) && + (!filters.model || tags.model?.includes(filters.model)) && + (!filters.year || tags.year?.includes(filters.year)) && + (!filters.drive || tags.drive?.includes(filters.drive)) && + (!filters.baseModel || tags.baseModel?.includes(filters.baseModel)) + ); + }); + }; + + + + + + return ( + + + + + {brands.length === 0 ? ( + + +

No brands selected yet.

+
+
+ ) : ( + + + + {brands.map((brand, index) => { + + return ( + + + {brand.id} + + + + + + + {itemsMap[brand.id]?.length || 0} + + ) + })} + + + + )} + + {brands.map((brand) => { + const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []); + const uniqueTags = { + make: new Set(), + model: new Set(), + year: new Set(), + drive: new Set(), + baseModel: new Set(), + }; + + (itemsMap[brand.id] || []).forEach(item => { + const tags = item?.attributes?.fitmmentTags || {}; + Object.keys(uniqueTags).forEach(key => { + (tags[key] || []).forEach(val => uniqueTags[key].add(val)); + }); + }); + + return ( + + expandedBrand === brand.id && + + ( + + + {processId && ( +
+

+ Process ID: {processId} +

+ +
+

+ Status: {status || "β€”"} +

+ + {progress > 0 && ( +
+ +

+ {processedProducts} of {totalProducts} products processed + {currentProduct && ` - Current: ${currentProduct.name} (${currentProduct.number}/${currentProduct.total})`} +

+
+ )} +
+ + {status === 'done' && results.length > 0 && ( +
+

+ Results: {results.length} products processed successfully +

+
+ )} + + {status === 'error' && ( +
+ Error: {detail} +
+ )} + + +
+ )} +
+ + + {loadingMap[brand.id] ? ( + + ) : ( +
+
+ + setProductCount(value)} + autoComplete="off" + /> + + + + + + + ({ label: m, value: m }))]} + onChange={handleFilterChange('model')} + value={filters.model} + /> + + + ({ label: d, value: d }))]} + onChange={handleFilterChange('drive')} + value={filters.drive} + /> + + +
{actionData?.error && ( - + )} - {displayToken && ( - -

βœ… Connection Successful

- {/* - {displayToken} - */} + {connected && ( + +

βœ… Turn14 connected successfully!

+ + {/* β€”β€” SHOPIFY INSTALL FORM β€”β€” */} + {/*
+ + +
+ +
+
*/}
)}
@@ -210,220 +283,3 @@ export default function SettingsPage({ standalone = true }) { ); } - -/* -import { json } from "@remix-run/node"; -import { useLoaderData, useActionData, Form } from "@remix-run/react"; -import { useState } from "react"; -import { - Page, - Layout, - Card, - TextField, - Button, - InlineError, - BlockStack, - Text, -} from "@shopify/polaris"; -import { authenticate } from "../shopify.server"; - -export const loader = async ({ request }) => { - const { admin } = await authenticate.admin(request); - - const gqlResponse = await admin.graphql(` - { - shop { - id - name - metafield(namespace: "turn14", key: "credentials") { - value - } - } - } - `); - - const shopData = await gqlResponse.json(); - const shopName = shopData?.data?.shop?.name || "Unknown Shop"; - const metafieldRaw = shopData?.data?.shop?.metafield?.value; - - let creds = {}; - if (metafieldRaw) { - try { - creds = JSON.parse(metafieldRaw); - } catch (err) { - console.error("Failed to parse stored credentials:", err); - } - } - - return json({ shopName, creds }); -}; - -export const action = async ({ request }) => { - const formData = await request.formData(); - const clientId = formData.get("client_id") || ""; - const clientSecret = formData.get("client_secret") || ""; - - const { admin } = await authenticate.admin(request); - - const shopInfo = await admin.graphql(`{ shop { id } }`); - const shopId = (await shopInfo.json())?.data?.shop?.id; - - try { - const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - grant_type: "client_credentials", - client_id: clientId, - client_secret: clientSecret, - }), - }); - - const tokenData = await tokenRes.json(); - - if (!tokenRes.ok) { - return json({ - success: false, - error: tokenData.error || "Failed to fetch access token", - }); - } - - const accessToken = tokenData.access_token; - const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); - - const credentials = { - clientId, - clientSecret, - accessToken, - expiresAt, - }; - - const mutation = ` - mutation { - metafieldsSet(metafields: [ - { - ownerId: "${shopId}" - namespace: "turn14" - key: "credentials" - type: "json" - value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}" - } - ]) { - metafields { - key - value - } - userErrors { - field - message - } - } - } - `; - - const saveRes = await admin.graphql(mutation); - const result = await saveRes.json(); - - if (result?.data?.metafieldsSet?.userErrors?.length) { - return json({ - success: false, - error: result.data.metafieldsSet.userErrors[0].message, - }); - } - - return json({ - success: true, - clientId, - clientSecret, - accessToken, - }); - } catch (err) { - console.error("Turn14 token fetch failed:", err); - return json({ - success: false, - error: "Network or unexpected error occurred", - }); - } -}; - -export default function SettingsPage({ standalone = true }) { - const loaderData = useLoaderData(); - const actionData = useActionData(); - - const savedCreds = loaderData?.creds || {}; - const shopName = loaderData?.shopName || "Shop"; - - const [clientId, setClientId] = useState( - actionData?.clientId || savedCreds.clientId || "" - ); - const [clientSecret, setClientSecret] = useState( - actionData?.clientSecret || savedCreds.clientSecret || "" - ); - const displayToken = actionData?.accessToken || savedCreds.accessToken; - - const content = ( - - - - - Connected Shop: {shopName} - -
- - - - - -
- - {actionData?.error && ( - - )} - - {displayToken && ( - - - βœ… Access token: - - - {displayToken} - - - )} -
-
-
-
- ); - - return standalone ? {content} : content; -} - */ \ No newline at end of file diff --git a/app/routes/app.testing.jsx b/app/routes/app.testing.jsx new file mode 100644 index 0000000..5a9d345 --- /dev/null +++ b/app/routes/app.testing.jsx @@ -0,0 +1,95 @@ +import { + Page, + Layout, + Card, + Text, + BlockStack, + Link, + Button, + Collapsible, +} from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { useState, useCallback } from "react"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + await authenticate.admin(request); + return null; +}; + +export default function HelpPage() { + const [openIndex, setOpenIndex] = useState(null); + + const toggle = useCallback((index) => { + setOpenIndex((prev) => (prev === index ? null : index)); + }, []); + + const faqs = [ + { + title: "πŸ“Œ How do I connect my Turn14 account?", + content: + "Go to the Settings page, enter your Turn14 Client ID and Secret, then click 'Save & Connect'. A green badge will confirm successful connection.", + }, + { + title: "πŸ“¦ Where can I import brands from?", + content: + "Use the 'Brands' tab in the left menu to view and import available brands from Turn14 into your Shopify store.", + }, + { + title: "πŸ”„ How do I sync brand collections?", + content: + "In the 'Manage Brands' section, select the brands and hit 'Sync to Shopify'. A manual collection will be created or updated.", + }, + { + title: "πŸ” Is my Turn14 API key secure?", + content: + "Yes. The credentials are stored using Shopify’s encrypted storage (metafields), ensuring they are safe and secure.", + }, + ]; + + return ( + + + + + + + + Need Help? You’re in the Right Place! + + + This section covers frequently asked questions about the Data4Autos + Turn14 integration app. + + + {faqs.map((faq, index) => ( +
+ + + + {faq.content} + + +
+ ))} + + + Still have questions? Email us at{" "} + + support@data4autos.com + + +
+
+
+
+
+ ); +} diff --git a/app/routes/app._index copy.jsx b/app/routes/backup/app._index copy.jsx similarity index 100% rename from app/routes/app._index copy.jsx rename to app/routes/backup/app._index copy.jsx diff --git a/app/routes/app.brands copy 2.jsx b/app/routes/backup/app.brands copy 2.jsx similarity index 98% rename from app/routes/app.brands copy 2.jsx rename to app/routes/backup/app.brands copy 2.jsx index a057342..fda448a 100644 --- a/app/routes/app.brands copy 2.jsx +++ b/app/routes/backup/app.brands copy 2.jsx @@ -14,8 +14,8 @@ import { } 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"; +import { getTurn14AccessTokenFromMetafield } from "../../utils/turn14Token.server"; +import { authenticate } from "../../shopify.server"; export const loader = async ({ request }) => { const accessToken = await getTurn14AccessTokenFromMetafield(request); diff --git a/app/routes/backup/app.brands copy 3.jsx b/app/routes/backup/app.brands copy 3.jsx new file mode 100644 index 0000000..93911fd --- /dev/null +++ b/app/routes/backup/app.brands copy 3.jsx @@ -0,0 +1,279 @@ +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") || "[]"); + + // get the shop domain from the headers (as you mentioned) + const shop = request.headers.get("shop-domain") || ""; + + // make the POST to your backend + const resp = await fetch("https://backend.dine360.ca/managebrands", { + method: "POST", + headers: { + "Content-Type": "application/json", + "shop-domain": shop, + }, + body: JSON.stringify({ shop, selectedBrands }), + }); + console.log("Request to Home:", { shop, selectedBrands }); + console.log("Request headers:", { "shop-domain": shop }); + console.log("Request body:", { selectedBrands }); + console.log("Response status:", resp.status); + console.log("Response headers:", resp.headers); + console.log("Response URL:", resp.url); + console.log("Response status text:", resp.statusText); + console.log("Response ok:", resp.ok); + console.log("Response type:", resp.type); + console.log("Response redirected:", resp.redirected); + console.log("Response from backend:", resp); + + if (!resp.ok) { + const err = await resp.text(); + return json({ error: err }, { status: resp.status }); + } + + const { processId, status } = await resp.json(); + + // return the processId (and initial status if you like) to the client + return json({ processId, status }); +}; +export default function BrandsPage() { + + const actionData = useActionData(); + const [status, setStatus] = useState(actionData?.status || ""); + const [polling, setPolling] = useState(false); + + // the processId returned from the action + const processId = actionData?.processId; + + async function checkStatus() { + if (!processId) return; + setPolling(true); + + const resp = await fetch( + `https://backend.dine360.ca/managebrands/status/${processId}`, + { + headers: { "shop-domain": window.shopify.shop || "" }, + } + ); + const json = await resp.json(); + setStatus(json.status + (json.detail ? ` (${json.detail})` : "")); + setPolling(false); + } + + + 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 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 ? ( + setToastActive(false)} + /> + ) : null; + + const selectedBrands = brands.filter((b) => selectedIds.includes(b.id)); + const allFilteredSelected = filteredBrands.length > 0 && + filteredBrands.every(brand => selectedIds.includes(brand.id)); + + return ( + + + + + + + + + + + +
+ +
+ +
+
+
+ + +
+ {filteredBrands.map((brand) => ( + +
+ toggleSelect(brand.id)} + /> + +
+ {brand.name} +
+
+
+ ))} + + {processId && ( +
+

+ Process ID: {processId} +

+

+ Status: {status || "β€”"} +

+ +
+ )} + +
+
+ + +
+ {toastMarkup} +
+ + ); +} \ No newline at end of file diff --git a/app/routes/app.brands copy.jsx b/app/routes/backup/app.brands copy.jsx similarity index 98% rename from app/routes/app.brands copy.jsx rename to app/routes/backup/app.brands copy.jsx index 85e25cc..21651b5 100644 --- a/app/routes/app.brands copy.jsx +++ b/app/routes/backup/app.brands copy.jsx @@ -14,8 +14,8 @@ import { } 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"; +import { getTurn14AccessTokenFromMetafield } from "../../utils/turn14Token.server"; +import { authenticate } from "../../shopify.server"; export const loader = async ({ request }) => { const accessToken = await getTurn14AccessTokenFromMetafield(request); diff --git a/app/routes/backup/app.brands_140725.jsx b/app/routes/backup/app.brands_140725.jsx new file mode 100644 index 0000000..fcaea38 --- /dev/null +++ b/app/routes/backup/app.brands_140725.jsx @@ -0,0 +1,382 @@ +import { json } from "@remix-run/node"; +import { useLoaderData, useFetcher, useActionData } 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 }) => { + + return json({ success: true }); + const formData = await request.formData(); + const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]"); + + + const { session } = await authenticate.admin(request); + const shop = session.shop; // "veloxautomotive.myshopify.com" + + // make the POST to your backend + const resp = await fetch("https://backend.dine360.ca/managebrands", { + method: "POST", + headers: { + "Content-Type": "application/json", + "shop-domain": shop, + }, + body: JSON.stringify({ shop, selectedBrands }), + }); + + if (!resp.ok) { + const err = await resp.text(); + return json({ error: err }, { status: resp.status }); + } + + const { processId, status } = await resp.json(); + + console.log("Process ID:", processId); + console.log("Status:", status); + return json({ processId, status }); + 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 } + } + } + `); + } + } + + + + const fallbackLogo = + "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"; + + for (const brand of selectedBrands) { + const exists = existingCollections.find( + (c) => c.title.toLowerCase() === brand.name.toLowerCase() + ); + if (exists) continue; + + const escapedName = brand.name.replace(/"/g, '\\"'); + const logoSrc = brand.logo || fallbackLogo; + + await admin.graphql(` + mutation { + collectionCreate(input: { + title: "${escapedName}", + descriptionHtml: "Products from brand ${escapedName}", + image: { + altText: "${escapedName} Logo", + src: "${logoSrc}" + } + }) { + 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 fetcher1 = useFetcher(); + const actionData = fetcher1.data; + + const [status, setStatus] = useState(actionData?.status || ""); + const [polling, setPolling] = useState(false); + + console.log("Action Data:", actionData); + // the processId returned from the action + const processId = actionData?.processId; + + + useEffect(() => { + console.log("Action Data:", fetcher.data); + }, [fetcher1.data]); + + + async function checkStatus() { + if (!processId) return; + setPolling(true); + + const resp = await fetch( + `https://backend.dine360.ca/managebrands/status/${processId}`, + { + headers: { "shop-domain": window.shopify.shop || "" }, + } + ); + const json = await resp.json(); + setStatus(json.status + (json.detail ? ` (${json.detail})` : "")); + setPolling(false); + } + + 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 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 ? ( + setToastActive(false)} + /> + ) : null; + + const selectedBrands = brands.filter((b) => selectedIds.includes(b.id)); + const allFilteredSelected = filteredBrands.length > 0 && + filteredBrands.every(brand => selectedIds.includes(brand.id)); + + return ( + + + + + + + + + + + +
+ +
+ +
+
+
+ + +
+ {filteredBrands.map((brand) => ( + +
+ toggleSelect(brand.id)} + /> + +
+ {brand.name} +
+
+
+ ))} + + + {processId && ( +
+

+ Process ID: {processId} +

+

+ Status: {status || "β€”"} +

+ +
+ )} + +
+
+ + +
+ {toastMarkup} +
+ + ); +} \ No newline at end of file diff --git a/app/routes/backup/app.brands_new.jsx b/app/routes/backup/app.brands_new.jsx new file mode 100644 index 0000000..5f81ff5 --- /dev/null +++ b/app/routes/backup/app.brands_new.jsx @@ -0,0 +1,358 @@ +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 } + // } + // } + // `); + // } + // } + + + // for (const brand of selectedBrands) { + // const exists = existingCollections.find( + // (c) => c.title.toLowerCase() === brand.name.toLowerCase() + // ); + // if (!exists) { + // const escapedName = brand.name.replace(/"/g, '\\"'); + // // Only build the image block if there's a logo URL: + // const imageBlock = brand.logo + // ? ` + // image: { + // altText: "${escapedName} Logo", + // src: "${brand.logo}" + // } + // ` + // : ""; + + // await admin.graphql(` + // mutation { + // collectionCreate(input: { + // title: "${escapedName}", + // descriptionHtml: "Products from brand ${escapedName}" + // ${imageBlock} + // }) { + // collection { id } + // userErrors { message } + // } + // } + // `); + // } + // } + + const fallbackLogo = + "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"; + + for (const brand of selectedBrands) { + const exists = existingCollections.find( + (c) => c.title.toLowerCase() === brand.name.toLowerCase() + ); + if (exists) continue; + + const escapedName = brand.name.replace(/"/g, '\\"'); + const logoSrc = brand.logo || fallbackLogo; + + await admin.graphql(` + mutation { + collectionCreate(input: { + title: "${escapedName}", + descriptionHtml: "Products from brand ${escapedName}", + image: { + altText: "${escapedName} Logo", + src: "${logoSrc}" + } + }) { + 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 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 ? ( + setToastActive(false)} + /> + ) : null; + + const selectedBrands = brands.filter((b) => selectedIds.includes(b.id)); + const allFilteredSelected = filteredBrands.length > 0 && + filteredBrands.every(brand => selectedIds.includes(brand.id)); + + return ( + + + + + + + + + + + +
+ +
+ +
+
+
+ + +
+ {filteredBrands.map((brand) => ( + +
+ toggleSelect(brand.id)} + /> + +
+ {brand.name} +
+
+
+ ))} +
+
+ + +
+ {toastMarkup} +
+ + ); +} \ No newline at end of file diff --git a/app/routes/backup/app.managebrand copy.jsx b/app/routes/backup/app.managebrand copy.jsx new file mode 100644 index 0000000..e2a5946 --- /dev/null +++ b/app/routes/backup/app.managebrand copy.jsx @@ -0,0 +1,231 @@ + +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, +} 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("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; + 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`, { + 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) + } + } + }; + + return ( + + + + {brands.length === 0 ? ( + + +

No brands selected yet.

+
+
+ ) : ( + + + + {brands.map((brand, index) => ( + + {brand.id} + + + + + + + {itemsMap[brand.id]?.length || 0} + + ))} + + + + )} + + {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] ? ( + + ) : ( + + +
+
+ + + + + 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}

+
+
+
+
+ ))} +
+ )} +
+
+ ) + )} +
+
+ ); +} diff --git a/app/routes/app.managebrand_040725.jsx b/app/routes/backup/app.managebrand_040725.jsx similarity index 99% rename from app/routes/app.managebrand_040725.jsx rename to app/routes/backup/app.managebrand_040725.jsx index e6f826d..71b089e 100644 --- a/app/routes/app.managebrand_040725.jsx +++ b/app/routes/backup/app.managebrand_040725.jsx @@ -14,12 +14,12 @@ import { Banner, InlineError, } from "@shopify/polaris"; -import { authenticate } from "../shopify.server"; +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 { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server"); const accessToken = await getTurn14AccessTokenFromMetafield(request); const res = await admin.graphql(`{ @@ -49,7 +49,7 @@ export const action = async ({ request }) => { const rawCount = formData.get("productCount"); const productCount = parseInt(rawCount, 10) || 10; - const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); + const { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server"); const accessToken = await getTurn14AccessTokenFromMetafield(request); // Fetch items from Turn14 API diff --git a/app/routes/app.managebrand_bak.jsx b/app/routes/backup/app.managebrand_bak.jsx similarity index 99% rename from app/routes/app.managebrand_bak.jsx rename to app/routes/backup/app.managebrand_bak.jsx index 01207a7..89292d5 100644 --- a/app/routes/app.managebrand_bak.jsx +++ b/app/routes/backup/app.managebrand_bak.jsx @@ -12,11 +12,11 @@ import { TextField, } from "@shopify/polaris"; import { useState } from "react"; -import { authenticate } from "../shopify.server"; +import { authenticate } from "../../shopify.server"; export const loader = async ({ request }) => { const { admin } = await authenticate.admin(request); - const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); + const { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server"); const accessToken = await getTurn14AccessTokenFromMetafield(request); const res = await admin.graphql(` diff --git a/app/routes/app.managebrand_bak_300625.jsx b/app/routes/backup/app.managebrand_bak_300625.jsx similarity index 99% rename from app/routes/app.managebrand_bak_300625.jsx rename to app/routes/backup/app.managebrand_bak_300625.jsx index 5e29109..5236a52 100644 --- a/app/routes/app.managebrand_bak_300625.jsx +++ b/app/routes/backup/app.managebrand_bak_300625.jsx @@ -444,12 +444,12 @@ import { TextField, Banner, } from "@shopify/polaris"; -import { authenticate } from "../shopify.server"; +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 { getTurn14AccessTokenFromMetafield } = await import("../../utils/turn14Token.server"); const accessToken = await getTurn14AccessTokenFromMetafield(request); const res = await admin.graphql(`{ diff --git a/app/routes/backup/app.settings copy.jsx b/app/routes/backup/app.settings copy.jsx new file mode 100644 index 0000000..cb8fc4c --- /dev/null +++ b/app/routes/backup/app.settings copy.jsx @@ -0,0 +1,429 @@ + import { json } from "@remix-run/node"; +import { useLoaderData, useActionData, Form } from "@remix-run/react"; +import { useState } from "react"; +import { + Page, + Layout, + Card, + TextField, + Button, + TextContainer, + InlineError, +} from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { authenticate } from "../../shopify.server"; + +export const loader = async ({ request }) => { + const { admin } = await authenticate.admin(request); + + // Fetch shop info and stored credentials + const gqlResponse = await admin.graphql(` + { + shop { + id + name + metafield(namespace: "turn14", key: "credentials") { + value + } + } + } + `); + const shopData = await gqlResponse.json(); + + const shopName = shopData?.data?.shop?.name || "Unknown Shop"; + const metafieldRaw = shopData?.data?.shop?.metafield?.value; + + let creds = {}; + if (metafieldRaw) { + try { + creds = JSON.parse(metafieldRaw); + } catch (err) { + console.error("Failed to parse stored credentials:", err); + } + } + + return json({ shopName, creds }); +}; + +export const action = async ({ request }) => { + const formData = await request.formData(); + const clientId = formData.get("client_id") || ""; + const clientSecret = formData.get("client_secret") || ""; + + const { admin } = await authenticate.admin(request); + + // Fetch shop ID + const shopInfo = await admin.graphql(`{ shop { id } }`); + const shopId = (await shopInfo.json())?.data?.shop?.id; + + // Get Turn14 token + try { + const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + }), + }); + + const tokenData = await tokenRes.json(); + + if (!tokenRes.ok) { + return json({ + success: false, + error: tokenData.error || "Failed to fetch access token", + }); + } + + const accessToken = tokenData.access_token; + const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); + + const credentials = { + clientId, + clientSecret, + accessToken, + expiresAt, + }; + + // Upsert as metafield in Shopify + const mutation = ` + mutation { + metafieldsSet(metafields: [ + { + ownerId: "${shopId}" + namespace: "turn14" + key: "credentials" + type: "json" + value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}" + } + ]) { + metafields { + key + value + } + userErrors { + field + message + } + } + } + `; + + + + const saveRes = await admin.graphql(mutation); + const result = await saveRes.json(); + + if (result?.data?.metafieldsSet?.userErrors?.length) { + return json({ + success: false, + error: result.data.metafieldsSet.userErrors[0].message, + }); + } + + return json({ + success: true, + clientId, + clientSecret, + accessToken, + }); + + } catch (err) { + console.error("Turn14 token fetch failed:", err); + return json({ + success: false, + error: "Network or unexpected error occurred", + }); + } +}; + +export default function SettingsPage({ standalone = true }) { + const loaderData = useLoaderData(); + const actionData = useActionData(); + + const savedCreds = loaderData?.creds || {}; + const shopName = loaderData?.shopName || "Shop"; + + const [clientId, setClientId] = useState(actionData?.clientId || savedCreds.clientId || ""); + const [clientSecret, setClientSecret] = useState(actionData?.clientSecret || savedCreds.clientSecret || ""); + const displayToken = actionData?.accessToken || savedCreds.accessToken; + + return ( + + + + + + +

Connected Shop: {shopName}

+
+ +
+ + +
+ +
+ + + {actionData?.error && ( + + + + )} + + {displayToken && ( + +

βœ… Connection Successful

+ {/* + {displayToken} + */} +
+ )} +
+
+
+
+ ); +} + +/* +import { json } from "@remix-run/node"; +import { useLoaderData, useActionData, Form } from "@remix-run/react"; +import { useState } from "react"; +import { + Page, + Layout, + Card, + TextField, + Button, + InlineError, + BlockStack, + Text, +} from "@shopify/polaris"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + const { admin } = await authenticate.admin(request); + + const gqlResponse = await admin.graphql(` + { + shop { + id + name + metafield(namespace: "turn14", key: "credentials") { + value + } + } + } + `); + + const shopData = await gqlResponse.json(); + const shopName = shopData?.data?.shop?.name || "Unknown Shop"; + const metafieldRaw = shopData?.data?.shop?.metafield?.value; + + let creds = {}; + if (metafieldRaw) { + try { + creds = JSON.parse(metafieldRaw); + } catch (err) { + console.error("Failed to parse stored credentials:", err); + } + } + + return json({ shopName, creds }); +}; + +export const action = async ({ request }) => { + const formData = await request.formData(); + const clientId = formData.get("client_id") || ""; + const clientSecret = formData.get("client_secret") || ""; + + const { admin } = await authenticate.admin(request); + + const shopInfo = await admin.graphql(`{ shop { id } }`); + const shopId = (await shopInfo.json())?.data?.shop?.id; + + try { + const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + }), + }); + + const tokenData = await tokenRes.json(); + + if (!tokenRes.ok) { + return json({ + success: false, + error: tokenData.error || "Failed to fetch access token", + }); + } + + const accessToken = tokenData.access_token; + const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); + + const credentials = { + clientId, + clientSecret, + accessToken, + expiresAt, + }; + + const mutation = ` + mutation { + metafieldsSet(metafields: [ + { + ownerId: "${shopId}" + namespace: "turn14" + key: "credentials" + type: "json" + value: "${JSON.stringify(credentials).replace(/"/g, '\\"')}" + } + ]) { + metafields { + key + value + } + userErrors { + field + message + } + } + } + `; + + const saveRes = await admin.graphql(mutation); + const result = await saveRes.json(); + + if (result?.data?.metafieldsSet?.userErrors?.length) { + return json({ + success: false, + error: result.data.metafieldsSet.userErrors[0].message, + }); + } + + return json({ + success: true, + clientId, + clientSecret, + accessToken, + }); + } catch (err) { + console.error("Turn14 token fetch failed:", err); + return json({ + success: false, + error: "Network or unexpected error occurred", + }); + } +}; + +export default function SettingsPage({ standalone = true }) { + const loaderData = useLoaderData(); + const actionData = useActionData(); + + const savedCreds = loaderData?.creds || {}; + const shopName = loaderData?.shopName || "Shop"; + + const [clientId, setClientId] = useState( + actionData?.clientId || savedCreds.clientId || "" + ); + const [clientSecret, setClientSecret] = useState( + actionData?.clientSecret || savedCreds.clientSecret || "" + ); + const displayToken = actionData?.accessToken || savedCreds.accessToken; + + const content = ( + + + + + Connected Shop: {shopName} + +
+ + + + + +
+ + {actionData?.error && ( + + )} + + {displayToken && ( + + + βœ… Access token: + + + {displayToken} + + + )} +
+
+
+
+ ); + + return standalone ? {content} : content; +} + */ \ No newline at end of file diff --git a/app/routes/backup/app.settings_working_bak.jsx b/app/routes/backup/app.settings_working_bak.jsx new file mode 100644 index 0000000..985b876 --- /dev/null +++ b/app/routes/backup/app.settings_working_bak.jsx @@ -0,0 +1,208 @@ +// app/routes/store-credentials.jsx + +import { json, redirect } from "@remix-run/node"; +import { useLoaderData, useActionData, Form } from "@remix-run/react"; +import { useEffect, useState } from "react"; +import { + Page, + Layout, + Card, + TextField, + Button, + TextContainer, + InlineError, +} from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { authenticate } from "../../shopify.server"; + +const SCOPES = [ + "read_inventory", + "read_products", + "write_inventory", + "write_products", + "read_publications", + "write_publications", +].join(","); +const REDIRECT_URI = "https://backend.dine360.ca/auth/callback"; +const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa"; + +export const loader = async ({ request }) => { + const { admin } = await authenticate.admin(request); + const resp = await admin.graphql(` + { + shop { + id + name + metafield(namespace: "turn14", key: "credentials") { value } + } + } + `); + const { data } = await resp.json(); + let creds = {}; + if (data.shop.metafield?.value) { + try { creds = JSON.parse(data.shop.metafield.value); } catch { } + } + creds = {}; + return json({ + shopName: data.shop.name, + shopId: data.shop.id, + savedCreds: creds, + }); +}; + +export const action = async ({ request }) => { + const formData = await request.formData(); + const { admin } = await authenticate.admin(request); + + // β€”β€”β€” Handle Shopify-install trigger β€”β€”β€” + if (formData.get("install_shopify") === "1") { + const shopName = formData.get("shop_name"); + const stateNonce = Math.random().toString(36).slice(2); + const installUrl = + `https://${shopName}.myshopify.com/admin/oauth/authorize` + + `?client_id=${CLIENT_ID}` + + `&scope=${SCOPES}` + + `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` + + `&state=${stateNonce}` + + `&grant_options%5B%5D=per-user`; + + // return the URL instead of redirecting + return json({ confirmationUrl: installUrl }); + } + + + // β€”β€”β€” Otherwise handle Turn14 token exchange β€”β€”β€” + const clientId = formData.get("client_id"); + const clientSecret = formData.get("client_secret"); + const shopInfo = await admin.graphql(`{ shop { id } }`); + const shopId = (await shopInfo.json()).data.shop.id; + + let tokenData; + try { + const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + }), + }); + tokenData = await tokenRes.json(); + if (!tokenRes.ok) { + throw new Error(tokenData.error || "Failed to fetch Turn14 token"); + } + } catch (err) { + return json({ success: false, error: err.message }); + } + + // upsert as Shopify metafield + const creds = { + clientId, + clientSecret, + accessToken: tokenData.access_token, + expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), + }; + const mutation = ` + mutation { + metafieldsSet(metafields: [{ + ownerId: "${shopId}", + namespace: "turn14", + key: "credentials", + type: "json", + value: "${JSON.stringify(creds).replace(/"/g, '\\"')}" + }]) { + userErrors { message } + } + } + `; + const saveRes = await admin.graphql(mutation); + const saveJson = await saveRes.json(); + const errs = saveJson.data.metafieldsSet.userErrors; + if (errs.length) { + return json({ success: false, error: errs[0].message }); + } + + return json({ success: true, creds }); +}; + +export default function StoreCredentials() { + const { shopName, shopId, savedCreds } = useLoaderData(); + const actionData = useActionData(); + + + useEffect(() => { + if (actionData?.confirmationUrl) { + window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer"); + } + }, [actionData?.confirmationUrl]); + + + const [clientId, setClientId] = useState(actionData?.creds?.clientId || savedCreds.clientId || ""); + const [clientSecret, setClientSecret] = useState(actionData?.creds?.clientSecret || savedCreds.clientSecret || ""); + const connected = actionData?.success || Boolean(savedCreds.accessToken); + + return ( + + + + + + +

Shop: {shopName}

+
+ + {/* β€”β€” TURN14 FORM β€”β€” */} +
+ + + +
+ +
+ + + {actionData?.error && ( + + + + )} + + {connected && ( + +

βœ… Turn14 connected successfully!

+ + {/* β€”β€” SHOPIFY INSTALL FORM β€”β€” */} +
+ + +
+ +
+
+
+ )} +
+
+
+
+ ); +} diff --git a/package-lock.json b/package-lock.json index 9fb90fd..809aa6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@shopify/polaris": "^12.27.0", "@shopify/shopify-app-remix": "^3.7.0", "@shopify/shopify-app-session-storage-prisma": "^6.0.0", + "axios": "^1.10.0", "dotenv": "^17.0.0", "isbot": "^5.1.0", "prisma": "^6.2.1", @@ -5290,6 +5291,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/auto-bind": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", @@ -5325,6 +5332,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -5932,6 +5950,18 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -6375,6 +6405,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6773,7 +6812,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -8016,6 +8054,26 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -8056,6 +8114,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -12296,6 +12370,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", diff --git a/package.json b/package.json index a639117..dbd0b1f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@shopify/polaris": "^12.27.0", "@shopify/shopify-app-remix": "^3.7.0", "@shopify/shopify-app-session-storage-prisma": "^6.0.0", + "axios": "^1.10.0", "dotenv": "^17.0.0", "isbot": "^5.1.0", "prisma": "^6.2.1", diff --git a/shopify.app.toml b/shopify.app.toml index 4ccfa50..0cddaf0 100644 --- a/shopify.app.toml +++ b/shopify.app.toml @@ -20,7 +20,7 @@ api_version = "2025-04" uri = "/webhooks/app/uninstalled" [access_scopes] -scopes = "read_inventory,read_products,write_inventory,write_products,read_publications,write_publications" +scopes = "read_inventory,read_products,write_inventory,write_products,read_publications,write_publications,read_fulfillments,write_fulfillments" [auth] redirect_urls = ["https://backend.dine360.ca/auth/callback"] # Update this line as well