- 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] ? (
-
- ) : (
-
+ )}
+
+
+ )
)
- )}
-
-
+ })}
+
+ {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] ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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.settings.jsx b/app/routes/app.settings.jsx
index 5f50384..e4217a6 100644
--- a/app/routes/app.settings.jsx
+++ b/app/routes/app.settings.jsx
@@ -1,6 +1,8 @@
- import { json } from "@remix-run/node";
+// app/routes/store-credentials.jsx
+
+import { json, redirect } from "@remix-run/node";
import { useLoaderData, useActionData, Form } from "@remix-run/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import {
Page,
Layout,
@@ -13,50 +15,131 @@ import {
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.data4autos.com/auth/callback";
+const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa";
+
export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request);
-
- // Fetch shop info and stored credentials
- const gqlResponse = await admin.graphql(`
+ const resp = await admin.graphql(`
{
shop {
id
name
- metafield(namespace: "turn14", key: "credentials") {
- value
- }
+ 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;
-
+ const { data } = await resp.json();
let creds = {};
- if (metafieldRaw) {
- try {
- creds = JSON.parse(metafieldRaw);
- } catch (err) {
- console.error("Failed to parse stored credentials:", err);
- }
+ if (data.shop.metafield?.value) {
+ try { creds = JSON.parse(data.shop.metafield.value); } catch { }
}
-
- return json({ shopName, creds });
+ //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 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;
+ // βββ Turn14 token exchange βββ
+ const clientId = formData.get("client_id");
+ const clientSecret = formData.get("client_secret");
+ const shopResp = await admin.graphql(`{ shop { id name } }`);
+ const shopJson = await shopResp.json();
+ const shopId = shopJson.data.shop.id;
+ const shopName = shopJson.data.shop.name;
- // Get Turn14 token
+ let tokenData;
try {
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
method: "POST",
@@ -67,100 +150,88 @@ export const action = async ({ request }) => {
client_secret: clientSecret,
}),
});
-
- const tokenData = await tokenRes.json();
-
+ tokenData = await tokenRes.json();
if (!tokenRes.ok) {
- return json({
- success: false,
- error: tokenData.error || "Failed to fetch access token",
- });
+ throw new Error(tokenData.error || "Failed to fetch Turn14 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",
- });
+ return json({ success: false, error: err.message });
}
+
+ // βββ Upsert to 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 });
+ }
+
+ // βββ Build the Shopify OAuth URL and return it βββ
+ 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 json({
+ success: true,
+ confirmationUrl: installUrl,
+ });
};
-export default function SettingsPage({ standalone = true }) {
- const loaderData = useLoaderData();
+
+
+export default function StoreCredentials() {
+ const { shopName, shopId, savedCreds } = 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;
+ 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 (
-
-
+
+
- Connected Shop: {shopName}
+ Shop: {shopName}
+ {/* ββ TURN14 FORM ββ */}
{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