From fa0d6eb57c5df76e2b4a8f57443e576591f89eb1 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 12 Jul 2025 17:20:28 +0000 Subject: [PATCH] 12-07-2025 backup --- app/routes/app._index copy.jsx | 608 ++++++++++++++++ app/routes/app._index.jsx | 708 +++++-------------- app/routes/app.brands copy 2.jsx | 295 ++++++++ app/routes/app.brands.jsx | 133 +++- app/routes/app.managebrand copy.jsx | 231 +++++++ app/routes/app.managebrand.jsx | 954 +++++++++++++++++++++++++- app/routes/app.managebrand_040725.jsx | 515 ++++++++++++++ shopify.app.toml | 4 +- 8 files changed, 2841 insertions(+), 607 deletions(-) create mode 100644 app/routes/app._index copy.jsx create mode 100644 app/routes/app.brands copy 2.jsx create mode 100644 app/routes/app.managebrand copy.jsx create mode 100644 app/routes/app.managebrand_040725.jsx diff --git a/app/routes/app._index copy.jsx b/app/routes/app._index copy.jsx new file mode 100644 index 0000000..e4e1ba1 --- /dev/null +++ b/app/routes/app._index copy.jsx @@ -0,0 +1,608 @@ +/* import { useEffect, useState, useCallback } from "react"; +import { useFetcher } from "@remix-run/react"; +import { + Page, + Layout, + Card, + Tabs, + Button, + BlockStack, + InlineStack, + Text, + Badge, + Link, +} from "@shopify/polaris"; +import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + await authenticate.admin(request); + + return null; +}; + +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + + return null; +}; + +export default function Index() { + const fetcher = useFetcher(); + const shopify = useAppBridge(); + const isLoading = + ["loading", "submitting"].includes(fetcher.state) && + fetcher.formMethod === "POST"; + const productId = fetcher.data?.product?.id.replace( + "gid://shopify/Product/", + "", + ); + + function TabsInsideOfACardExample() { + const [selected, setSelected] = useState(0); + + const handleTabChange = useCallback( + (selectedTabIndex) => setSelected(selectedTabIndex), + [], + ); + + const tabs = [ + { id: "settings", content: "⚙️ Settings" }, + { id: "brands", content: "🏷️ Brands" }, + { id: "manage", content: "📦 Manage Brands" }, + { id: "help", content: "🆘 Help" }, + { id: "login", content: "🔐 Login" }, + ]; + + return ( + + + +

Tab {selected} selected

+
+
+
+ ); +} + + useEffect(() => { + if (productId) { + shopify.toast.show("Product created"); + } + }, [productId, shopify]); + const generateProduct = () => fetcher.submit({}, { method: "POST" }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + + */ + +/* //woking code +import { useEffect, useState, useCallback } from "react"; +import { useFetcher } from "@remix-run/react"; +import { + Page, + Layout, + Card, + Tabs, + Button, + BlockStack, + InlineStack, + Text, + Badge, + Link, + LegacyCard, +} from "@shopify/polaris"; +import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + await authenticate.admin(request); + return null; +}; + +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + return null; +}; + +export default function Index() { + const fetcher = useFetcher(); + const shopify = useAppBridge(); + const isLoading = + ["loading", "submitting"].includes(fetcher.state) && + fetcher.formMethod === "POST"; + const productId = fetcher.data?.product?.id?.replace( + "gid://shopify/Product/", + "" + ); + + useEffect(() => { + // Temporarily disabling toast to avoid crash + // You can safely add App Bridge toast later + }, [productId, shopify]); + + const generateProduct = () => fetcher.submit({}, { method: "POST" }); + + // Tabs logic + const [selectedTab, setSelectedTab] = useState(0); + const handleTabChange = useCallback( + (selectedTabIndex) => setSelectedTab(selectedTabIndex), + [] + ); + + const tabs = [ + { + id: "settings-tab", + content: "⚙️ Settings", + panelID: "settings-content", + }, + { + id: "brands-tab", + content: "🏷️ Brands", + panelID: "brands-content", + }, + { + id: "manage-tab", + content: "📦 Manage Brands", + panelID: "manage-content", + }, + { + id: "help-tab", + content: "🆘 Help", + panelID: "help-content", + }, + { + id: "login-tab", + content: "🔐 Login", + panelID: "login-content", + }, + ]; + + return ( + + + + + + + + {selectedTab === 0 && ( + + Configure Turn14 integration settings. + + + + + )} + {selectedTab === 1 && ( + + View available brands from Turn14. + + + + + )} + {selectedTab === 2 && ( + + Manage your synced brand collections. + + + + + )} + {selectedTab === 3 && ( + + Help and documentation links. + + + + )} + {selectedTab === 4 && ( + + Login to manage Turn14 credentials. + + + + + )} + + + + + + + ); +} */ + + +/* import { Page, Layout, Card, Text, BlockStack } from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + await authenticate.admin(request); + return null; +}; + +export default function Index() { + return ( + + + + + + + + Welcome to your Turn14 integration dashboard! + + + Use the navigation in the left sidebar to manage settings, view brands, + sync collections, and more. + + + + + + + ); +} + */ + + +import { + Page, + Layout, + Card, + BlockStack, + Text, + Badge, + InlineStack, + Image, + Divider, +} from "@shopify/polaris"; +import { TitleBar } from "@shopify/app-bridge-react"; +import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists + +export default function Index() { + return ( + + + + + + + + Data4Autos Logo + + Welcome to your Turn14 Dashboard + + + + + + + + 🚀 Data4Autos Turn14 Integration gives you the power to sync + product brands, manage collections, and automate catalog setup directly from + Turn14 to your Shopify store. + + + + 🔧 Use the left sidebar to: + + + ⚙️ Manage API settings + 🏷️ Browse and import available brands + 📦 Sync brand collections to Shopify + 🔐 Handle secure Turn14 login credentials + + + + + + Status: Connected + Shopify x Turn14 + + + + Need help? Contact us at{" "} + support@data4autos.com + + + + + + + + ); +} + + + + +/* import { useEffect, useState, useCallback } from "react"; +import { useFetcher, useNavigate } from "@remix-run/react"; +import { + Page, + Layout, + Card, + Tabs, + LegacyCard, +} from "@shopify/polaris"; +import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + await authenticate.admin(request); + return null; +}; + +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + return null; +}; + +export default function Index() { + const fetcher = useFetcher(); + const shopify = useAppBridge(); + const navigate = useNavigate(); + + const productId = fetcher.data?.product?.id?.replace( + "gid://shopify/Product/", + "" + ); + + useEffect(() => { + // You can add toast messages here if needed + }, [productId, shopify]); + + const tabs = [ + { + id: "settings-tab", + content: "⚙️ Settings", + panelID: "settings-content", + to: "/app/settings", + }, + { + id: "brands-tab", + content: "🏷️ Brands", + panelID: "brands-content", + to: "/app/brands", + }, + { + id: "manage-tab", + content: "📦 Manage Brands", + panelID: "manage-content", + to: "/app/managebrand", + }, + { + id: "help-tab", + content: "🆘 Help", + panelID: "help-content", + to: "/app/help", + }, + { + id: "login-tab", + content: "🔐 Login", + panelID: "login-content", + to: "/app/login", + }, + ]; + + const [selectedTab, setSelectedTab] = useState(0); + + const handleTabChange = useCallback( + (selectedTabIndex) => { + setSelectedTab(selectedTabIndex); + navigate(tabs[selectedTabIndex].to); + }, + [navigate] + ); + + return ( + + + + + + + +

Redirecting to {tabs[selectedTab].content}...

+
+
+
+
+
+
+ ); +} + + */ + + + + + +/* import { useEffect, useState, useCallback } from "react"; +import { useFetcher } from "@remix-run/react"; +import { + Page, + Layout, + Card, + Tabs, + Button, + BlockStack, + InlineStack, + Text, + Badge, + Link, + LegacyCard, +} from "@shopify/polaris"; +import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; +import { authenticate } from "../shopify.server"; +import SettingsPage from "./app.settings"; // Adjust the path if needed + +export const loader = async ({ request }) => { + await authenticate.admin(request); + return null; +}; + +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + return null; +}; + +export default function Index() { + const fetcher = useFetcher(); + const shopify = useAppBridge(); + const isLoading = + ["loading", "submitting"].includes(fetcher.state) && + fetcher.formMethod === "POST"; + const productId = fetcher.data?.product?.id?.replace( + "gid://shopify/Product/", + "" + ); + + useEffect(() => { + // Toast placeholder (disabled) + }, [productId, shopify]); + + const generateProduct = () => fetcher.submit({}, { method: "POST" }); + + const [selectedTab, setSelectedTab] = useState(0); + const handleTabChange = useCallback( + (selectedTabIndex) => setSelectedTab(selectedTabIndex), + [] + ); + + const tabs = [ + { + id: "settings-tab", + content: "⚙️ Settings", + panelID: "settings-content", + }, + { + id: "brands-tab", + content: "🏷️ Brands", + panelID: "brands-content", + }, + { + id: "manage-tab", + content: "📦 Manage Brands", + panelID: "manage-content", + }, + { + id: "help-tab", + content: "🆘 Help", + panelID: "help-content", + }, + { + id: "login-tab", + content: "🔐 Login", + panelID: "login-content", + }, + ]; + + return ( + + + + + + + + + {selectedTab === 0 && ( + + )} + + {selectedTab === 1 && ( + + View available brands from Turn14. + + + + + )} + + {selectedTab === 2 && ( + + Manage your synced brand collections. + + + + + )} + + {selectedTab === 3 && ( + + Help and documentation links. + + + + )} + + {selectedTab === 4 && ( + + Login to manage Turn14 credentials. + + + + + )} + + + + + + + ); +} + */ \ No newline at end of file diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx index e4e1ba1..e841204 100644 --- a/app/routes/app._index.jsx +++ b/app/routes/app._index.jsx @@ -1,293 +1,6 @@ -/* import { useEffect, useState, useCallback } from "react"; -import { useFetcher } from "@remix-run/react"; -import { - Page, - Layout, - Card, - Tabs, - Button, - BlockStack, - InlineStack, - Text, - Badge, - Link, -} from "@shopify/polaris"; -import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; -import { authenticate } from "../shopify.server"; - -export const loader = async ({ request }) => { - await authenticate.admin(request); - - return null; -}; - -export const action = async ({ request }) => { - const { admin } = await authenticate.admin(request); - - return null; -}; - -export default function Index() { - const fetcher = useFetcher(); - const shopify = useAppBridge(); - const isLoading = - ["loading", "submitting"].includes(fetcher.state) && - fetcher.formMethod === "POST"; - const productId = fetcher.data?.product?.id.replace( - "gid://shopify/Product/", - "", - ); - - function TabsInsideOfACardExample() { - const [selected, setSelected] = useState(0); - - const handleTabChange = useCallback( - (selectedTabIndex) => setSelected(selectedTabIndex), - [], - ); - - const tabs = [ - { id: "settings", content: "⚙️ Settings" }, - { id: "brands", content: "🏷️ Brands" }, - { id: "manage", content: "📦 Manage Brands" }, - { id: "help", content: "🆘 Help" }, - { id: "login", content: "🔐 Login" }, - ]; - - return ( - - - -

Tab {selected} selected

-
-
-
- ); -} - - useEffect(() => { - if (productId) { - shopify.toast.show("Product created"); - } - }, [productId, shopify]); - const generateProduct = () => fetcher.submit({}, { method: "POST" }); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - - */ - -/* //woking code -import { useEffect, useState, useCallback } from "react"; -import { useFetcher } from "@remix-run/react"; -import { - Page, - Layout, - Card, - Tabs, - Button, - BlockStack, - InlineStack, - Text, - Badge, - Link, - LegacyCard, -} from "@shopify/polaris"; -import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; -import { authenticate } from "../shopify.server"; - -export const loader = async ({ request }) => { - await authenticate.admin(request); - return null; -}; - -export const action = async ({ request }) => { - const { admin } = await authenticate.admin(request); - return null; -}; - -export default function Index() { - const fetcher = useFetcher(); - const shopify = useAppBridge(); - const isLoading = - ["loading", "submitting"].includes(fetcher.state) && - fetcher.formMethod === "POST"; - const productId = fetcher.data?.product?.id?.replace( - "gid://shopify/Product/", - "" - ); - - useEffect(() => { - // Temporarily disabling toast to avoid crash - // You can safely add App Bridge toast later - }, [productId, shopify]); - - const generateProduct = () => fetcher.submit({}, { method: "POST" }); - - // Tabs logic - const [selectedTab, setSelectedTab] = useState(0); - const handleTabChange = useCallback( - (selectedTabIndex) => setSelectedTab(selectedTabIndex), - [] - ); - - const tabs = [ - { - id: "settings-tab", - content: "⚙️ Settings", - panelID: "settings-content", - }, - { - id: "brands-tab", - content: "🏷️ Brands", - panelID: "brands-content", - }, - { - id: "manage-tab", - content: "📦 Manage Brands", - panelID: "manage-content", - }, - { - id: "help-tab", - content: "🆘 Help", - panelID: "help-content", - }, - { - id: "login-tab", - content: "🔐 Login", - panelID: "login-content", - }, - ]; - - return ( - - - - - - - - {selectedTab === 0 && ( - - Configure Turn14 integration settings. - - - - - )} - {selectedTab === 1 && ( - - View available brands from Turn14. - - - - - )} - {selectedTab === 2 && ( - - Manage your synced brand collections. - - - - - )} - {selectedTab === 3 && ( - - Help and documentation links. - - - - )} - {selectedTab === 4 && ( - - Login to manage Turn14 credentials. - - - - - )} - - - - - - - ); -} */ - - -/* import { Page, Layout, Card, Text, BlockStack } from "@shopify/polaris"; -import { TitleBar } from "@shopify/app-bridge-react"; -import { authenticate } from "../shopify.server"; - -export const loader = async ({ request }) => { - await authenticate.admin(request); - return null; -}; - -export default function Index() { - return ( - - - - - - - - Welcome to your Turn14 integration dashboard! - - - Use the navigation in the left sidebar to manage settings, view brands, - sync collections, and more. - - - - - - - ); -} - */ - - +import React, { useState, useEffect } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData, useActionData, useSubmit } from "@remix-run/react"; import { Page, Layout, @@ -298,11 +11,127 @@ import { InlineStack, Image, Divider, + Button, + Modal, + TextField, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import data4autosLogo from "../assets/data4autos_logo.png"; // make sure this exists +import { authenticate } from "../shopify.server"; // Shopify server authentication + +import { Form } from "@remix-run/react"; + + + +// Loader to check subscription status +export const loader = async ({ request }) => { + const { admin } = await authenticate.admin(request); + + // Query the current subscription status + const resp = await admin.graphql(` + query { + currentAppInstallation { + activeSubscriptions { + id + status + trialDays + createdAt + currentPeriodEnd + } + } + } + `); + + const result = await resp.json(); + const subscription = result.data.currentAppInstallation.activeSubscriptions[0] || null; + + // For new users, there's no subscription. We will show a "Not subscribed" message. + if (!subscription) { + return json({ redirectToBilling: true, subscription: null }); + } + + // If no active or trial subscription, return redirect signal + if (subscription.status !== "ACTIVE" && subscription.status !== "TRIAL") { + return json({ redirectToBilling: true, subscription }); + } + + return json({ redirectToBilling: false, subscription }); +}; + +// Action to create subscription +export const action = async ({ request }) => { + console.log("Creating subscription..."); + const { admin } = await authenticate.admin(request); + + const createRes = await admin.graphql(` + mutation { + appSubscriptionCreate( + name: "Pro Plan", + returnUrl: "https://your-app.com/after-billing", + lineItems: [ + { + plan: { + appRecurringPricingDetails: { + price: { amount: 19.99, currencyCode: USD }, + interval: EVERY_30_DAYS + } + } + } + ], + trialDays: 7, # ✅ trialDays is a top-level argument! + test: true + ) { + confirmationUrl + appSubscription { + id + status + trialDays + } + userErrors { + field + message + } + } +} + `); + + const data = await createRes.json(); + console.log("Subscription creation response:", data); + if (data.errors || !data.data.appSubscriptionCreate.confirmationUrl) { + return json({ errors: ["Failed to create subscription."] }, { status: 400 }); + } + console.log("Subscription created successfully:", data.data.appSubscriptionCreate.confirmationUrl); + return json({ + confirmationUrl: data.data.appSubscriptionCreate.confirmationUrl + }); +}; export default function Index() { + const actionData = useActionData(); + const loaderData = useLoaderData(); + const submit = useSubmit(); // Use submit to trigger the action + const [activeModal, setActiveModal] = useState(false); + + const subscription = loaderData?.subscription; + + // useEffect(() => { + // console.log("Action data:", actionData); + // // If we have a confirmation URL, redirect to it + // if (actionData?.confirmationUrl) { + // window.location.href = actionData.confirmationUrl; // Redirect to Shopify's billing confirmation page + // } + // }, [actionData]); + + + useEffect(() => { + if (actionData?.confirmationUrl) { + window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer"); + setActiveModal(false); // close the modal + } + }, [actionData]); + const openModal = () => setActiveModal(true); + const closeModal = () => setActiveModal(false); + return ( @@ -351,258 +180,63 @@ export default function Index() { Need help? Contact us at{" "} support@data4autos.com + + + + {/* Modal for Subscription Info */} + { + // submit(null, { method: "post", form: document.getElementById("billing-form") }); + // }, + // }} + primaryAction={{ + content: "Proceed to Billing", + onAction: () => { + submit(null, { method: "post", form: document.getElementById("billing-form") }); + }, + }} + secondaryActions={[{ content: "Close", onAction: closeModal }]} + > +
+ + + + + + + + + +
+
); } - - - - -/* import { useEffect, useState, useCallback } from "react"; -import { useFetcher, useNavigate } from "@remix-run/react"; -import { - Page, - Layout, - Card, - Tabs, - LegacyCard, -} from "@shopify/polaris"; -import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; -import { authenticate } from "../shopify.server"; - -export const loader = async ({ request }) => { - await authenticate.admin(request); - return null; -}; - -export const action = async ({ request }) => { - const { admin } = await authenticate.admin(request); - return null; -}; - -export default function Index() { - const fetcher = useFetcher(); - const shopify = useAppBridge(); - const navigate = useNavigate(); - - const productId = fetcher.data?.product?.id?.replace( - "gid://shopify/Product/", - "" - ); - - useEffect(() => { - // You can add toast messages here if needed - }, [productId, shopify]); - - const tabs = [ - { - id: "settings-tab", - content: "⚙️ Settings", - panelID: "settings-content", - to: "/app/settings", - }, - { - id: "brands-tab", - content: "🏷️ Brands", - panelID: "brands-content", - to: "/app/brands", - }, - { - id: "manage-tab", - content: "📦 Manage Brands", - panelID: "manage-content", - to: "/app/managebrand", - }, - { - id: "help-tab", - content: "🆘 Help", - panelID: "help-content", - to: "/app/help", - }, - { - id: "login-tab", - content: "🔐 Login", - panelID: "login-content", - to: "/app/login", - }, - ]; - - const [selectedTab, setSelectedTab] = useState(0); - - const handleTabChange = useCallback( - (selectedTabIndex) => { - setSelectedTab(selectedTabIndex); - navigate(tabs[selectedTabIndex].to); - }, - [navigate] - ); - - return ( - - - - - - - -

Redirecting to {tabs[selectedTab].content}...

-
-
-
-
-
-
- ); -} - - */ - - - - - -/* import { useEffect, useState, useCallback } from "react"; -import { useFetcher } from "@remix-run/react"; -import { - Page, - Layout, - Card, - Tabs, - Button, - BlockStack, - InlineStack, - Text, - Badge, - Link, - LegacyCard, -} from "@shopify/polaris"; -import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; -import { authenticate } from "../shopify.server"; -import SettingsPage from "./app.settings"; // Adjust the path if needed - -export const loader = async ({ request }) => { - await authenticate.admin(request); - return null; -}; - -export const action = async ({ request }) => { - const { admin } = await authenticate.admin(request); - return null; -}; - -export default function Index() { - const fetcher = useFetcher(); - const shopify = useAppBridge(); - const isLoading = - ["loading", "submitting"].includes(fetcher.state) && - fetcher.formMethod === "POST"; - const productId = fetcher.data?.product?.id?.replace( - "gid://shopify/Product/", - "" - ); - - useEffect(() => { - // Toast placeholder (disabled) - }, [productId, shopify]); - - const generateProduct = () => fetcher.submit({}, { method: "POST" }); - - const [selectedTab, setSelectedTab] = useState(0); - const handleTabChange = useCallback( - (selectedTabIndex) => setSelectedTab(selectedTabIndex), - [] - ); - - const tabs = [ - { - id: "settings-tab", - content: "⚙️ Settings", - panelID: "settings-content", - }, - { - id: "brands-tab", - content: "🏷️ Brands", - panelID: "brands-content", - }, - { - id: "manage-tab", - content: "📦 Manage Brands", - panelID: "manage-content", - }, - { - id: "help-tab", - content: "🆘 Help", - panelID: "help-content", - }, - { - id: "login-tab", - content: "🔐 Login", - panelID: "login-content", - }, - ]; - - return ( - - - - - - - - - {selectedTab === 0 && ( - - )} - - {selectedTab === 1 && ( - - View available brands from Turn14. - - - - - )} - - {selectedTab === 2 && ( - - Manage your synced brand collections. - - - - - )} - - {selectedTab === 3 && ( - - Help and documentation links. - - - - )} - - {selectedTab === 4 && ( - - Login to manage Turn14 credentials. - - - - - )} - - - - - - - ); -} - */ \ No newline at end of file diff --git a/app/routes/app.brands copy 2.jsx b/app/routes/app.brands copy 2.jsx new file mode 100644 index 0000000..a057342 --- /dev/null +++ b/app/routes/app.brands copy 2.jsx @@ -0,0 +1,295 @@ +import { json } from "@remix-run/node"; +import { useLoaderData, useFetcher } from "@remix-run/react"; +import { + Page, + Layout, + Card, + TextField, + Checkbox, + Button, + Thumbnail, + Spinner, + Toast, + Frame, +} from "@shopify/polaris"; +import { useEffect, useState } from "react"; +import { TitleBar } from "@shopify/app-bridge-react"; +import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server"; +import { authenticate } from "../shopify.server"; + +export const loader = async ({ request }) => { + const accessToken = await getTurn14AccessTokenFromMetafield(request); + const { admin } = await authenticate.admin(request); + + // Get brands + const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + const brandJson = await brandRes.json(); + if (!brandRes.ok) { + return json({ error: brandJson.error || "Failed to fetch brands" }, { status: 500 }); + } + + // Get collections + const gqlRaw = await admin.graphql(` + { + collections(first: 100) { + edges { + node { + id + title + handle + } + } + } + } + `); + const gql = await gqlRaw.json(); + const collections = gql?.data?.collections?.edges?.map((e) => e.node) || []; + + return json({ + brands: brandJson.data, + collections, + }); +}; + +export const action = async ({ request }) => { + const formData = await request.formData(); + const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]"); + + const { admin } = await authenticate.admin(request); + + // Get current collections + const gqlRaw = await admin.graphql(` + { + collections(first: 100) { + edges { + node { + id + title + } + } + } + } + `); + const gql = await gqlRaw.json(); + const existingCollections = gql?.data?.collections?.edges?.map((e) => e.node) || []; + + const selectedTitles = selectedBrands.map((b) => b.name.toLowerCase()); + const logoMap = Object.fromEntries(selectedBrands.map(b => [b.name.toLowerCase(), b.logo])); + + // Delete unselected + for (const col of existingCollections) { + if (!selectedTitles.includes(col.title.toLowerCase())) { + await admin.graphql(` + mutation { + collectionDelete(input: { id: "${col.id}" }) { + deletedCollectionId + userErrors { message } + } + } + `); + } + } + + // Create new + for (const brand of selectedBrands) { + const exists = existingCollections.find( + (c) => c.title.toLowerCase() === brand.name.toLowerCase() + ); + if (!exists) { + const escapedName = brand.name.replace(/"/g, '\\"'); + const logo = brand.logo || ""; + + await admin.graphql(` + mutation { + collectionCreate(input: { + title: "${escapedName}", + descriptionHtml: "Products from brand ${escapedName}", + image: { + altText: "${escapedName} Logo", + src: "${logo}" + } + }) { + collection { id } + userErrors { message } + } + } + `); + } + } + + const shopDataRaw = await admin.graphql(` + { + shop { + id + } + } + `); + const shopRes = await admin.graphql(`{ shop { id } }`); + const shopJson = await shopRes.json(); + const shopId = shopJson?.data?.shop?.id; + + await admin.graphql(` + mutation { + metafieldsSet(metafields: [{ + namespace: "turn14", + key: "selected_brands", + type: "json", + ownerId: "${shopId}", + value: ${JSON.stringify(JSON.stringify(selectedBrands))} + }]) { + metafields { + id + } + userErrors { + message + } + } + } + `); + + return json({ success: true }); +}; + +export default function BrandsPage() { + const { brands, collections } = useLoaderData(); + const fetcher = useFetcher(); + const isSubmitting = fetcher.state === "submitting"; + const [toastActive, setToastActive] = useState(false); + const [search, setSearch] = useState(""); + + const collectionTitles = new Set(collections.map((c) => c.title.toLowerCase())); + const defaultSelected = brands + .filter((b) => collectionTitles.has(b.name.toLowerCase())) + .map((b) => b.id); + + const [selectedIds, setSelectedIds] = useState(defaultSelected); + const [filteredBrands, setFilteredBrands] = useState(brands); + + useEffect(() => { + const term = search.toLowerCase(); + setFilteredBrands(brands.filter((b) => b.name.toLowerCase().includes(term))); + }, [search, brands]); + + useEffect(() => { + if (fetcher.data?.success) { + setToastActive(true); + } + }, [fetcher.data]); + + const toggleSelect = (id) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ); + }; + + const 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/app.brands.jsx b/app/routes/app.brands.jsx index a057342..730f03d 100644 --- a/app/routes/app.brands.jsx +++ b/app/routes/app.brands.jsx @@ -96,32 +96,94 @@ export const action = async ({ request }) => { } // 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) { - const escapedName = brand.name.replace(/"/g, '\\"'); - const logo = brand.logo || ""; + if (exists) continue; - await admin.graphql(` - mutation { - collectionCreate(input: { - title: "${escapedName}", - descriptionHtml: "Products from brand ${escapedName}", - image: { - altText: "${escapedName} Logo", - src: "${logo}" - } - }) { - collection { id } - userErrors { message } - } + 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 { @@ -190,7 +252,7 @@ export default function BrandsPage() { const toggleSelectAll = () => { const filteredBrandIds = filteredBrands.map(b => b.id); const allFilteredSelected = filteredBrandIds.every(id => selectedIds.includes(id)); - + if (allFilteredSelected) { // Deselect all filtered brands setSelectedIds(prev => prev.filter(id => !filteredBrandIds.includes(id))); @@ -211,7 +273,7 @@ export default function BrandsPage() { ) : null; const selectedBrands = brands.filter((b) => selectedIds.includes(b.id)); - const allFilteredSelected = filteredBrands.length > 0 && + const allFilteredSelected = filteredBrands.length > 0 && filteredBrands.every(brand => selectedIds.includes(brand.id)); return ( @@ -219,6 +281,22 @@ export default function BrandsPage() { + + + + + +
- - - - - - + {toastMarkup} diff --git a/app/routes/app.managebrand copy.jsx b/app/routes/app.managebrand copy.jsx new file mode 100644 index 0000000..f5dc9cb --- /dev/null +++ b/app/routes/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.jsx b/app/routes/app.managebrand.jsx index f5dc9cb..701d5d9 100644 --- a/app/routes/app.managebrand.jsx +++ b/app/routes/app.managebrand.jsx @@ -1,4 +1,3 @@ - import React, { useEffect, useState } from "react"; import { json } from "@remix-run/node"; import { useLoaderData, Form, useActionData } from "@remix-run/react"; @@ -13,6 +12,7 @@ import { Button, TextField, Banner, + InlineError, } from "@shopify/polaris"; import { authenticate } from "../shopify.server"; import { TitleBar } from "@shopify/app-bridge-react"; @@ -42,35 +42,896 @@ export const loader = async ({ request }) => { return json({ brands, accessToken }); }; +// export const action = async ({ request }) => { +// const { admin } = await authenticate.admin(request); +// const formData = await request.formData(); +// const brandId = formData.get("brandId"); +// const rawCount = formData.get("productCount"); +// const productCount = parseInt(rawCount, 10) || 10; + +// const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); +// const accessToken = await getTurn14AccessTokenFromMetafield(request); + +// // Fetch items from Turn14 API +// const itemsRes = await fetch( +// `https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, +// { +// headers: { +// Authorization: `Bearer ${accessToken}`, +// "Content-Type": "application/json", +// }, +// } +// ); +// const itemsData = await itemsRes.json(); + +// function slugify(str) { +// return str +// .toString() +// .trim() +// .toLowerCase() +// .replace(/[^a-z0-9]+/g, '-') +// .replace(/^-+|-+$/g, ''); +// } + +// const items = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : []; +// const results = []; + +// for (const item of items) { +// const attrs = item.attributes; + +// // Build and normalize collection titles +// const category = attrs.category; +// const subcategory = attrs.subcategory || ""; + +// const brand = attrs.brand; +// const subcats = subcategory +// .split(/[,\/]/) +// .map((s) => s.trim()) +// .filter(Boolean); +// const collectionTitles = Array.from( +// new Set([category, ...subcats, brand].filter(Boolean)) +// ); + +// // Find or create collections, collect their IDs +// const collectionIds = []; +// for (const title of collectionTitles) { +// const lookupRes = await admin.graphql(` +// { +// collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") { +// nodes { id } +// } +// } +// `); +// const lookupJson = await lookupRes.json(); +// const existing = lookupJson.data.collections.nodes; +// if (existing.length) { +// collectionIds.push(existing[0].id); +// } else { +// const createColRes = await admin.graphql(` +// mutation($input: CollectionInput!) { +// collectionCreate(input: $input) { +// collection { id } +// userErrors { field message } +// } +// } +// `, { variables: { input: { title } } }); +// const createColJson = await createColRes.json(); +// const errs = createColJson.data.collectionCreate.userErrors; +// if (errs.length) { +// throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`); +// } +// collectionIds.push(createColJson.data.collectionCreate.collection.id); +// } +// } + +// // Build tags +// const tags = [ +// attrs.category, +// ...subcats, +// attrs.brand, +// attrs.part_number, +// attrs.mfr_part_number, +// attrs.price_group, +// attrs.units_per_sku && `${attrs.units_per_sku} per SKU`, +// attrs.barcode +// ].filter(Boolean).map((t) => t.trim()); + +// // Prepare media inputs +// const mediaInputs = (attrs.files || []) +// .filter((f) => f.type === "Image" && f.url) +// .map((file) => ({ +// originalSource: file.url, +// mediaContentType: "IMAGE", +// alt: `${attrs.product_name} — ${file.media_content}`, +// })); + +// // Pick the longest "Market Description" or fallback to part_description +// const marketDescs = (attrs.descriptions || []) +// .filter((d) => d.type === "Market Description") +// .map((d) => d.description); +// const descriptionHtml = marketDescs.length +// ? marketDescs.reduce((a, b) => (b.length > a.length ? b : a)) +// : attrs.part_description; + +// // Create product + attach to collections + add media +// const createProdRes = await admin.graphql(` +// mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) { +// productCreate(product: $prod, media: $media) { +// product { +// id +// variants(first: 1) { +// nodes { id inventoryItem { id } } +// } +// } +// userErrors { field message } +// } +// } +// `, { +// variables: { +// prod: { +// title: attrs.product_name, +// descriptionHtml: descriptionHtml, +// vendor: attrs.brand, +// productType: attrs.category, +// handle: slugify(attrs.part_number || attrs.product_name), +// tags, +// collectionsToJoin: collectionIds, +// status: "ACTIVE", +// }, +// media: mediaInputs, +// }, +// }); +// const createProdJson = await createProdRes.json(); +// const prodErrs = createProdJson.data.productCreate.userErrors; +// if (prodErrs.length) { +// const taken = prodErrs.some((e) => /already in use/i.test(e.message)); +// if (taken) { +// results.push({ skippedHandle: attrs.part_number, reason: "handle in use" }); +// continue; +// } +// throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`); +// } + +// const product = createProdJson.data.productCreate.product; +// const variantNode = product.variants.nodes[0]; +// const variantId = variantNode.id; +// const inventoryItemId = variantNode.inventoryItem.id; + +// // Bulk-update variant (price, compare-at, barcode) +// const price = parseFloat(attrs.price) || 1000; +// const comparePrice = parseFloat(attrs.compare_price) || null; +// const barcode = attrs.barcode || ""; + +// const bulkRes = await admin.graphql(` +// mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { +// productVariantsBulkUpdate(productId: $productId, variants: $variants) { +// productVariants { id price compareAtPrice barcode } +// userErrors { field message } +// } +// } +// `, { +// variables: { +// productId: product.id, +// variants: [{ +// id: variantId, +// price, +// ...(comparePrice !== null && { compareAtPrice: comparePrice }), +// ...(barcode && { barcode }), +// }], +// }, +// }); +// const bulkJson = await bulkRes.json(); +// const bulkErrs = bulkJson.data.productVariantsBulkUpdate.userErrors; +// if (bulkErrs.length) { +// throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(", ")}`); +// } +// const updatedVariant = bulkJson.data.productVariantsBulkUpdate.productVariants[0]; + +// // Update inventory item (SKU, cost & weight) +// const costPerItem = parseFloat(attrs.purchase_cost) || 0; +// const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0; + +// const invRes = await admin.graphql(` +// mutation($id: ID!, $input: InventoryItemInput!) { +// inventoryItemUpdate(id: $id, input: $input) { +// inventoryItem { +// id +// sku +// measurement { +// weight { value unit } +// } +// } +// userErrors { field message } +// } +// } +// `, { +// variables: { +// id: inventoryItemId, +// input: { +// sku: attrs.part_number, +// cost: costPerItem, +// measurement: { +// weight: { value: weightValue, unit: "POUNDS" } +// }, +// }, +// }, +// }); +// const invJson = await invRes.json(); +// const invErrs = invJson.data.inventoryItemUpdate.userErrors; +// if (invErrs.length) { +// throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`); +// } +// const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem; + +// // Collect results +// results.push({ +// productId: product.id, +// variant: { +// id: updatedVariant.id, +// price: updatedVariant.price, +// compareAtPrice: updatedVariant.compareAtPrice, +// sku: inventoryItem.sku, +// barcode: updatedVariant.barcode, +// weight: inventoryItem.measurement.weight.value, +// weightUnit: inventoryItem.measurement.weight.unit, +// }, +// collections: collectionTitles, +// tags, +// }); +// } + +// return json({ success: true, results }); +// }; + +// export const action = async ({ request }) => { +// const { admin } = await authenticate.admin(request); +// const formData = await request.formData(); +// const brandId = formData.get("brandId"); +// const rawCount = formData.get("productCount"); +// const productCount = parseInt(rawCount, 10) || 10; + +// const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); +// const accessToken = await getTurn14AccessTokenFromMetafield(request); + +// // Fetch items from Turn14 API +// console.log("Fetching items from Turn14 API..."); +// const itemsRes = await fetch( +// `https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, +// { +// headers: { +// Authorization: `Bearer ${accessToken}`, +// "Content-Type": "application/json", +// }, +// } +// ); +// const itemsData = await itemsRes.json(); +// console.log("Items data fetched:", itemsData); + +// function slugify(str) { +// return str +// .toString() +// .trim() +// .toLowerCase() +// .replace(/[^a-z0-9]+/g, '-') +// .replace(/^-+|-+$/g, ''); +// } + +// const items = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : []; +// console.log(`Processing ${items.length} items...`); +// const results = []; + +// for (const item of items) { +// const attrs = item.attributes; +// console.log("Processing item:", attrs); + +// // Build and normalize collection titles +// const category = attrs.category; +// const subcategory = attrs.subcategory || ""; +// const brand = attrs.brand; +// const subcats = subcategory +// .split(/[,\/]/) +// .map((s) => s.trim()) +// .filter(Boolean); +// const collectionTitles = Array.from( +// new Set([category, ...subcats, brand].filter(Boolean)) +// ); +// console.log("Collection Titles:", collectionTitles); + +// // Find or create collections, collect their IDs +// const collectionIds = []; +// for (const title of collectionTitles) { +// console.log(`Searching for collection with title: ${title}`); +// const lookupRes = await admin.graphql(` +// { +// collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") { +// nodes { id } +// } +// } +// `); +// const lookupJson = await lookupRes.json(); +// console.log("Lookup response for collections:", lookupJson); + +// const existing = lookupJson.data.collections.nodes; +// if (existing.length) { +// console.log(`Found existing collection for title: ${title}`); +// collectionIds.push(existing[0].id); +// } else { +// console.log(`Creating new collection for title: ${title}`); +// const createColRes = await admin.graphql(` +// mutation($input: CollectionInput!) { +// collectionCreate(input: $input) { +// collection { id } +// userErrors { field message } +// } +// } +// `, { variables: { input: { title } } }); +// const createColJson = await createColRes.json(); +// console.log("Create collection response:", createColJson); + +// const errs = createColJson.data.collectionCreate.userErrors; +// if (errs.length) { +// throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`); +// } +// collectionIds.push(createColJson.data.collectionCreate.collection.id); +// } +// } + +// // Build tags +// const tags = [ +// attrs.category, +// ...subcats, +// attrs.brand, +// attrs.part_number, +// attrs.mfr_part_number, +// attrs.price_group, +// attrs.units_per_sku && `${attrs.units_per_sku} per SKU`, +// attrs.barcode +// ].filter(Boolean).map((t) => t.trim()); +// console.log("Tags:", tags); + +// // Prepare media inputs +// const mediaInputs = (attrs.files || []) +// .filter((f) => f.type === "Image" && f.url) +// .map((file) => ({ +// originalSource: file.url, +// mediaContentType: "IMAGE", +// alt: `${attrs.product_name} — ${file.media_content}`, +// })); +// console.log("Media inputs:", mediaInputs); + +// // Pick the longest "Market Description" or fallback to part_description +// const marketDescs = (attrs.descriptions || []) +// .filter((d) => d.type === "Market Description") +// .map((d) => d.description); +// const descriptionHtml = marketDescs.length +// ? marketDescs.reduce((a, b) => (b.length > a.length ? b : a)) +// : attrs.part_description; +// console.log("Description HTML:", descriptionHtml); + +// // Create product + attach to collections + add media +// const createProdRes = await admin.graphql(` +// mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) { +// productCreate(product: $prod, media: $media) { +// product { +// id +// variants(first: 1) { +// nodes { id inventoryItem { id } } +// } +// } +// userErrors { field message } +// } +// } +// `, { +// variables: { +// prod: { +// title: attrs.product_name, +// descriptionHtml: descriptionHtml, +// vendor: attrs.brand, +// productType: attrs.category, +// handle: slugify(attrs.part_number || attrs.product_name), +// tags, +// collectionsToJoin: collectionIds, +// status: "ACTIVE", +// }, +// media: mediaInputs, +// }, +// }); +// const createProdJson = await createProdRes.json(); +// console.log("Create product response:", createProdJson); + +// const prodErrs = createProdJson.data.productCreate.userErrors; +// if (prodErrs.length) { +// const taken = prodErrs.some((e) => /already in use/i.test(e.message)); +// if (taken) { +// results.push({ skippedHandle: attrs.part_number, reason: "handle in use" }); +// continue; +// } +// throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`); +// } + +// const product = createProdJson.data.productCreate.product; +// const variantNode = product.variants.nodes[0]; +// const variantId = variantNode.id; +// const inventoryItemId = variantNode.inventoryItem.id; + +// // Fetch the Online Store publication ID +// console.log("Fetching Online Store publication ID..."); +// const publicationsRes = await admin.graphql(` +// query { +// publications(first: 10) { +// edges { +// node { +// id +// name +// } +// } +// } +// } +// `); +// const publicationsJson = await publicationsRes.json(); +// console.log("Publications response:", publicationsJson); + +// const onlineStorePublication = publicationsJson.data.publications.edges.find(pub => pub.node.name === 'Online Store'); +// console.log("Online Store Publication:", onlineStorePublication); + +// const onlineStorePublicationId = onlineStorePublication ? onlineStorePublication.node.id : null; +// if (onlineStorePublicationId) { +// console.log("Publishing product to Online Store..."); +// // Publish the product to the Online Store +// const publishRes = await admin.graphql(` +// mutation($id: ID!, $publicationId: ID!) { +// publishablePublish(id: $id, input: { publicationId: $publicationId }) { +// publishable { +// ... on Product { +// id +// title +// status +// } +// } +// userErrors { field message } +// } +// } +// `, { +// variables: { +// id: product.id, +// publicationId: onlineStorePublicationId, +// }, +// }); +// const publishJson = await publishRes.json(); +// console.log("Publish response:", publishJson); + +// const publishErrs = publishJson.data.publishablePublish.userErrors; +// if (publishErrs.length) { +// throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(", ")}`); +// } +// } else { +// throw new Error("Online Store publication not found."); +// } + +// // Update inventory item (SKU, cost & weight) +// const costPerItem = parseFloat(attrs.purchase_cost) || 0; +// const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0; + +// console.log("Updating inventory item..."); +// const invRes = await admin.graphql(` +// mutation($id: ID!, $input: InventoryItemInput!) { +// inventoryItemUpdate(id: $id, input: $input) { +// inventoryItem { +// id +// sku +// measurement { +// weight { value unit } +// } +// } +// userErrors { field message } +// } +// } +// `, { +// variables: { +// id: inventoryItemId, +// input: { +// sku: attrs.part_number, +// cost: costPerItem, +// measurement: { +// weight: { value: weightValue, unit: "POUNDS" } +// }, +// }, +// }, +// }); +// const invJson = await invRes.json(); +// console.log("Inventory update response:", invJson); + +// const invErrs = invJson.data.inventoryItemUpdate.userErrors; +// if (invErrs.length) { +// throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`); +// } +// const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem; + + + +// results.push({ +// productId: product.id, +// variant: { +// id: variantId, // Use the variantId from variantNode +// price: variantNode.price, // Use price from variantNode +// compareAtPrice: variantNode.compareAtPrice, // Use compareAtPrice from variantNode +// sku: inventoryItem.sku, // SKU from the updated inventory item +// barcode: variantNode.barcode, // Barcode from the variant +// weight: inventoryItem.measurement.weight.value, // Weight from inventory item +// weightUnit: inventoryItem.measurement.weight.unit, // Weight unit from inventory item +// }, +// collections: collectionTitles, +// tags, +// }); + + +// } + +// return json({ success: true, results }); +// }; + +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + const formData = await request.formData(); + const brandId = formData.get("brandId"); + const rawCount = formData.get("productCount"); + const productCount = parseInt(rawCount, 10) || 10; + + const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); + const accessToken = await getTurn14AccessTokenFromMetafield(request); + + // Fetch items from Turn14 API + console.log("Fetching items from Turn14 API..."); + const itemsRes = await fetch( + `https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + } + ); + const itemsData = await itemsRes.json(); + console.log("Items data fetched:", itemsData); + + function slugify(str) { + return str + .toString() + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + } + + const items = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : []; + console.log(`Processing ${items.length} items...`); + const results = []; + + for (const item of items) { + const attrs = item.attributes; + console.log("Processing item:", attrs); + + // Build and normalize collection titles + const category = attrs.category; + const subcategory = attrs.subcategory || ""; + const brand = attrs.brand; + const subcats = subcategory + .split(/[,\/]/) + .map((s) => s.trim()) + .filter(Boolean); + const collectionTitles = Array.from( + new Set([category, ...subcats, brand].filter(Boolean)) + ); + console.log("Collection Titles:", collectionTitles); + + // Find or create collections, collect their IDs + const collectionIds = []; + for (const title of collectionTitles) { + console.log(`Searching for collection with title: ${title}`); + const lookupRes = await admin.graphql(` + { + collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") { + nodes { id } + } + } + `); + const lookupJson = await lookupRes.json(); + console.log("Lookup response for collections:", lookupJson); + + const existing = lookupJson.data?.collections?.nodes || []; + if (existing.length) { + console.log(`Found existing collection for title: ${title}`); + collectionIds.push(existing[0]?.id); // Use optional chaining + } else { + console.log(`Creating new collection for title: ${title}`); + const createColRes = await admin.graphql(` + mutation($input: CollectionInput!) { + collectionCreate(input: $input) { + collection { id } + userErrors { field message } + } + }`, { variables: { input: { title } } }); + + const createColJson = await createColRes.json(); + console.log("Create collection response:", createColJson); + + const errs = createColJson.data?.collectionCreate?.userErrors || []; + if (errs.length) { + throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`); + } + collectionIds.push(createColJson.data?.collectionCreate?.collection?.id); // Optional chaining here too + } + } + + // Build tags + const tags = [ + attrs.category, + ...subcats, + attrs.brand, + attrs.part_number, + attrs.mfr_part_number, + attrs.price_group, + attrs.units_per_sku && `${attrs.units_per_sku} per SKU`, + attrs.barcode + ].filter(Boolean).map((t) => t.trim()); + console.log("Tags:", tags); + + // Prepare media inputs + const mediaInputs = (attrs.files || []) + .filter((f) => f.type === "Image" && f.url) + .map((file) => ({ + originalSource: file.url, + mediaContentType: "IMAGE", + alt: `${attrs.product_name} — ${file.media_content}`, + })); + console.log("Media inputs:", mediaInputs); + + // Pick the longest "Market Description" or fallback to part_description + const marketDescs = (attrs.descriptions || []) + .filter((d) => d.type === "Market Description") + .map((d) => d.description); + const descriptionHtml = marketDescs.length + ? marketDescs.reduce((a, b) => (b.length > a.length ? b : a)) + : attrs.part_description; + console.log("Description HTML:", descriptionHtml); + + // Create product + attach to collections + add media + // const createProdRes = await admin.graphql(` + // mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) { + // productCreate(product: $prod, media: $media) { + // product { + // id + // variants(first: 1) { + // nodes { id inventoryItem { id } price compareAtPrice barcode } + // } + // } + // userErrors { field message } + // } + // } + // `, { + // variables: { + // prod: { + // title: attrs.product_name, + // descriptionHtml: descriptionHtml, + // vendor: attrs.brand, + // productType: attrs.category, + // handle: slugify(attrs.part_number || attrs.product_name), + // tags, + // collectionsToJoin: collectionIds, + // status: "ACTIVE", + // }, + // media: mediaInputs, + // }, + // }); + + const createProdRes = await admin.graphql(` + mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) { + productCreate(product: $prod, media: $media) { + product { + id + variants(first: 1) { + nodes { id inventoryItem { id } price compareAtPrice barcode } + } + } + userErrors { field message } + } + } + `, { + variables: { + prod: { + title: attrs.product_name, + descriptionHtml: descriptionHtml, + vendor: attrs.brand, + productType: attrs.category, + handle: slugify(item.id+"-"+attrs.mfr_part_number || attrs.product_name), + tags, + collectionsToJoin: collectionIds, + status: "ACTIVE", + }, + media: mediaInputs, + }, + }); + + const createProdJson = await createProdRes.json(); + console.log("Create product response:", createProdJson); + + const prodErrs = createProdJson.data?.productCreate?.userErrors || []; + if (prodErrs.length) { + const taken = prodErrs.some((e) => /already in use/i.test(e.message)); + if (taken) { + results.push({ skippedHandle: attrs.part_number, reason: "handle in use" }); + continue; + } + throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`); + } + + const product = createProdJson.data.productCreate.product; + const variantNode = product.variants?.nodes?.[0]; + if (!variantNode) { + console.error("Variant node is undefined for product:", product.id); + continue; + } + + const variantId = variantNode.id; + const inventoryItemId = variantNode.inventoryItem?.id; + + // Fetch the Online Store publication ID + console.log("Fetching Online Store publication ID..."); + const publicationsRes = await admin.graphql(` + query { + publications(first: 10) { + edges { + node { + id + name + } + } + } + } + `); + const publicationsJson = await publicationsRes.json(); + console.log("Publications response:", publicationsJson); + + const onlineStorePublication = publicationsJson.data.publications.edges.find(pub => pub.node.name === 'Online Store'); + const onlineStorePublicationId = onlineStorePublication ? onlineStorePublication.node.id : null; + if (onlineStorePublicationId) { + console.log("Publishing product to Online Store..."); + const publishRes = await admin.graphql(` + mutation($id: ID!, $publicationId: ID!) { + publishablePublish(id: $id, input: { publicationId: $publicationId }) { + publishable { + ... on Product { + id + title + status + } + } + userErrors { field message } + } + } + `, { + variables: { + id: product.id, + publicationId: onlineStorePublicationId, + }, + }); + const publishJson = await publishRes.json(); + console.log("Publish response:", publishJson); + + const publishErrs = publishJson.data.publishablePublish.userErrors; + if (publishErrs.length) { + throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(", ")}`); + } + } else { + throw new Error("Online Store publication not found."); + } + + // Update inventory item (SKU, cost & weight) + const costPerItem = parseFloat(attrs.purchase_cost) || 0; + const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0; + + console.log("Updating inventory item..."); + const invRes = await admin.graphql(` + mutation($id: ID!, $input: InventoryItemInput!) { + inventoryItemUpdate(id: $id, input: $input) { + inventoryItem { + id + sku + measurement { + weight { value unit } + } + } + userErrors { field message } + } + } + `, { + variables: { + id: inventoryItemId, + input: { + sku: attrs.part_number, + cost: costPerItem, + measurement: { + weight: { value: weightValue, unit: "POUNDS" } + }, + }, + }, + }); + const invJson = await invRes.json(); + console.log("Inventory update response:", invJson); + + const invErrs = invJson.data.inventoryItemUpdate.userErrors; + if (invErrs.length) { + throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`); + } + const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem; + + // Collect results + results.push({ + productId: product.id, + variant: { + id: variantId, + price: variantNode.price, + compareAtPrice: variantNode.compareAtPrice, + sku: inventoryItem.sku, + barcode: variantNode.barcode, + weight: inventoryItem.measurement.weight.value, + weightUnit: inventoryItem.measurement.weight.unit, + }, + collections: collectionTitles, + tags, + }); + } + + return json({ success: true, results }); +}; + + export default function ManageBrandProducts() { const actionData = useActionData(); const { brands, accessToken } = useLoaderData(); const [expandedBrand, setExpandedBrand] = useState(null); const [itemsMap, setItemsMap] = useState({}); const [loadingMap, setLoadingMap] = useState({}); - const [productCount, setProductCount] = useState("0"); - - - - + const [productCount, setProductCount] = useState("10"); const [initialLoad, setInitialLoad] = useState(true); -// Function to toggle all brands -const toggleAllBrands = async () => { - for (const brand of brands) { - await toggleBrandItems(brand.id); - } -}; - -// Run on initial load -useEffect(() => { - if (initialLoad && brands.length > 0) { - toggleAllBrands(); - setInitialLoad(false); - } -}, [brands, initialLoad]); + const toggleAllBrands = async () => { + for (const brand of brands) { + await toggleBrandItems(brand.id); + } + }; + useEffect(() => { + if (initialLoad && brands.length > 0) { + toggleAllBrands(); + setInitialLoad(false); + } + }, [brands, initialLoad]); + // const toggleBrandItems = async (brandId) => { + // const isExpanded = expandedBrand === brandId; + // if (isExpanded) { + // setExpandedBrand(null); + // } else { + // setExpandedBrand(brandId); + // if (!itemsMap[brandId]) { + // setLoadingMap((prev) => ({ ...prev, [brandId]: true })); + // try { + // const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, { + // headers: { + // Authorization: `Bearer ${accessToken}`, + // "Content-Type": "application/json", + // }, + // }); + // const data = await res.json(); + // setItemsMap((prev) => ({ ...prev, [brandId]: data })); + // } catch (err) { + // console.error("Error fetching items:", err); + // } + // setLoadingMap((prev) => ({ ...prev, [brandId]: false })); + // } + // } + // }; const toggleBrandItems = async (brandId) => { const isExpanded = expandedBrand === brandId; @@ -78,11 +939,9 @@ useEffect(() => { setExpandedBrand(null); } else { setExpandedBrand(brandId); - if (!itemsMap[brandId]) { setLoadingMap((prev) => ({ ...prev, [brandId]: true })); try { - // const res = await fetch(`https://turn14.data4autos.com/v1/items/brand/${brandId}?page=1`, { const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -90,18 +949,19 @@ useEffect(() => { }, }); const data = await res.json(); - setProductCount(data.length) - setItemsMap((prev) => ({ ...prev, [brandId]: data })); + // Ensure we have an array of valid items + const validItems = Array.isArray(data) + ? data.filter(item => item && item.id && item.attributes) + : []; + setItemsMap((prev) => ({ ...prev, [brandId]: validItems })); } catch (err) { console.error("Error fetching items:", err); + setItemsMap((prev) => ({ ...prev, [brandId]: [] })); // Set empty array on error } setLoadingMap((prev) => ({ ...prev, [brandId]: false })); - } else { - setProductCount(itemsMap[brandId].length) } } }; - return ( @@ -144,7 +1004,7 @@ useEffect(() => { {expandedBrand === brand.id ? "Hide Products" : "Show Products"} - {itemsMap[brand.id]?.length || 0} + {itemsMap[brand.id]?.length || 0} ))} @@ -168,15 +1028,12 @@ useEffect(() => {

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

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

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

Part Number: {item?.attributes.part_number}

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

+

Price: ${item?.attributes.price}

+

Description: {item?.attributes.part_description}

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

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

+

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

+

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

+

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

@@ -228,4 +1116,4 @@ useEffect(() => { ); -} +} \ No newline at end of file diff --git a/app/routes/app.managebrand_040725.jsx b/app/routes/app.managebrand_040725.jsx new file mode 100644 index 0000000..e6f826d --- /dev/null +++ b/app/routes/app.managebrand_040725.jsx @@ -0,0 +1,515 @@ +import React, { useEffect, useState } from "react"; +import { json } from "@remix-run/node"; +import { useLoaderData, Form, useActionData } from "@remix-run/react"; +import { + Page, + Layout, + IndexTable, + Card, + Thumbnail, + TextContainer, + Spinner, + Button, + TextField, + Banner, + InlineError, +} from "@shopify/polaris"; +import { authenticate } from "../shopify.server"; +import { TitleBar } from "@shopify/app-bridge-react"; + +export const loader = async ({ request }) => { + const { admin } = await authenticate.admin(request); + const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); + const accessToken = await getTurn14AccessTokenFromMetafield(request); + + const res = await admin.graphql(`{ + shop { + metafield(namespace: "turn14", key: "selected_brands") { + value + } + } + }`); + const data = await res.json(); + const rawValue = data?.data?.shop?.metafield?.value; + + let brands = []; + try { + brands = JSON.parse(rawValue); + } catch (err) { + console.error("❌ Failed to parse metafield value:", err); + } + + return json({ brands, accessToken }); +}; + +export const action = async ({ request }) => { + const { admin } = await authenticate.admin(request); + const formData = await request.formData(); + const brandId = formData.get("brandId"); + const rawCount = formData.get("productCount"); + const productCount = parseInt(rawCount, 10) || 10; + + const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server"); + const accessToken = await getTurn14AccessTokenFromMetafield(request); + + // Fetch items from Turn14 API + const itemsRes = await fetch( + `https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + } + ); + const itemsData = await itemsRes.json(); + + function slugify(str) { + return str + .toString() + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + } + + const items = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : []; + const results = []; + + for (const item of items) { + const attrs = item.attributes; + + // Build and normalize collection titles + const category = attrs.category; + const subcategory = attrs.subcategory || ""; + const brand = attrs.brand; + const subcats = subcategory + .split(/[,\/]/) + .map((s) => s.trim()) + .filter(Boolean); + const collectionTitles = Array.from( + new Set([category, ...subcats, brand].filter(Boolean)) + ); + + // Find or create collections, collect their IDs + const collectionIds = []; + for (const title of collectionTitles) { + const lookupRes = await admin.graphql(` + { + collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") { + nodes { id } + } + } + `); + const lookupJson = await lookupRes.json(); + const existing = lookupJson.data.collections.nodes; + if (existing.length) { + collectionIds.push(existing[0].id); + } else { + const createColRes = await admin.graphql(` + mutation($input: CollectionInput!) { + collectionCreate(input: $input) { + collection { id } + userErrors { field message } + } + } + `, { variables: { input: { title } } }); + const createColJson = await createColRes.json(); + const errs = createColJson.data.collectionCreate.userErrors; + if (errs.length) { + throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`); + } + collectionIds.push(createColJson.data.collectionCreate.collection.id); + } + } + + // Build tags + const tags = [ + attrs.category, + ...subcats, + attrs.brand, + attrs.part_number, + attrs.mfr_part_number, + attrs.price_group, + attrs.units_per_sku && `${attrs.units_per_sku} per SKU`, + attrs.barcode + ].filter(Boolean).map((t) => t.trim()); + + // Prepare media inputs + const mediaInputs = (attrs.files || []) + .filter((f) => f.type === "Image" && f.url) + .map((file) => ({ + originalSource: file.url, + mediaContentType: "IMAGE", + alt: `${attrs.product_name} — ${file.media_content}`, + })); + + // Pick the longest "Market Description" or fallback to part_description + const marketDescs = (attrs.descriptions || []) + .filter((d) => d.type === "Market Description") + .map((d) => d.description); + const descriptionHtml = marketDescs.length + ? marketDescs.reduce((a, b) => (b.length > a.length ? b : a)) + : attrs.part_description; + + // Create product + attach to collections + add media + const createProdRes = await admin.graphql(` + mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) { + productCreate(product: $prod, media: $media) { + product { + id + variants(first: 1) { + nodes { id inventoryItem { id } } + } + } + userErrors { field message } + } + } + `, { + variables: { + prod: { + title: attrs.product_name, + descriptionHtml: descriptionHtml, + vendor: attrs.brand, + productType: attrs.category, + handle: slugify(attrs.part_number || attrs.product_name), + tags, + collectionsToJoin: collectionIds, + status: "ACTIVE", + }, + media: mediaInputs, + }, + }); + const createProdJson = await createProdRes.json(); + const prodErrs = createProdJson.data.productCreate.userErrors; + if (prodErrs.length) { + const taken = prodErrs.some((e) => /already in use/i.test(e.message)); + if (taken) { + results.push({ skippedHandle: attrs.part_number, reason: "handle in use" }); + continue; + } + throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`); + } + + const product = createProdJson.data.productCreate.product; + const variantNode = product.variants.nodes[0]; + const variantId = variantNode.id; + const inventoryItemId = variantNode.inventoryItem.id; + + // Bulk-update variant (price, compare-at, barcode) + const price = parseFloat(attrs.price) || 1000; + const comparePrice = parseFloat(attrs.compare_price) || null; + const barcode = attrs.barcode || ""; + + const bulkRes = await admin.graphql(` + mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { + productVariantsBulkUpdate(productId: $productId, variants: $variants) { + productVariants { id price compareAtPrice barcode } + userErrors { field message } + } + } + `, { + variables: { + productId: product.id, + variants: [{ + id: variantId, + price, + ...(comparePrice !== null && { compareAtPrice: comparePrice }), + ...(barcode && { barcode }), + }], + }, + }); + const bulkJson = await bulkRes.json(); + const bulkErrs = bulkJson.data.productVariantsBulkUpdate.userErrors; + if (bulkErrs.length) { + throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(", ")}`); + } + const updatedVariant = bulkJson.data.productVariantsBulkUpdate.productVariants[0]; + + // Update inventory item (SKU, cost & weight) + const costPerItem = parseFloat(attrs.purchase_cost) || 0; + const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0; + + const invRes = await admin.graphql(` + mutation($id: ID!, $input: InventoryItemInput!) { + inventoryItemUpdate(id: $id, input: $input) { + inventoryItem { + id + sku + measurement { + weight { value unit } + } + } + userErrors { field message } + } + } + `, { + variables: { + id: inventoryItemId, + input: { + sku: attrs.part_number, + cost: costPerItem, + measurement: { + weight: { value: weightValue, unit: "POUNDS" } + }, + }, + }, + }); + const invJson = await invRes.json(); + const invErrs = invJson.data.inventoryItemUpdate.userErrors; + if (invErrs.length) { + throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`); + } + const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem; + + // Collect results + results.push({ + productId: product.id, + variant: { + id: updatedVariant.id, + price: updatedVariant.price, + compareAtPrice: updatedVariant.compareAtPrice, + sku: inventoryItem.sku, + barcode: updatedVariant.barcode, + weight: inventoryItem.measurement.weight.value, + weightUnit: inventoryItem.measurement.weight.unit, + }, + collections: collectionTitles, + tags, + }); + } + + return json({ success: true, results }); +}; + +export default function ManageBrandProducts() { + const actionData = useActionData(); + const { brands, accessToken } = useLoaderData(); + const [expandedBrand, setExpandedBrand] = useState(null); + const [itemsMap, setItemsMap] = useState({}); + const [loadingMap, setLoadingMap] = useState({}); + const [productCount, setProductCount] = useState("10"); + const [initialLoad, setInitialLoad] = useState(true); + + const toggleAllBrands = async () => { + for (const brand of brands) { + await toggleBrandItems(brand.id); + } + }; + + useEffect(() => { + if (initialLoad && brands.length > 0) { + toggleAllBrands(); + setInitialLoad(false); + } + }, [brands, initialLoad]); + + // const toggleBrandItems = async (brandId) => { + // const isExpanded = expandedBrand === brandId; + // if (isExpanded) { + // setExpandedBrand(null); + // } else { + // setExpandedBrand(brandId); + // if (!itemsMap[brandId]) { + // setLoadingMap((prev) => ({ ...prev, [brandId]: true })); + // try { + // const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, { + // headers: { + // Authorization: `Bearer ${accessToken}`, + // "Content-Type": "application/json", + // }, + // }); + // const data = await res.json(); + // setItemsMap((prev) => ({ ...prev, [brandId]: data })); + // } catch (err) { + // console.error("Error fetching items:", err); + // } + // setLoadingMap((prev) => ({ ...prev, [brandId]: false })); + // } + // } + // }; + + const toggleBrandItems = async (brandId) => { + const isExpanded = expandedBrand === brandId; + if (isExpanded) { + setExpandedBrand(null); + } else { + setExpandedBrand(brandId); + if (!itemsMap[brandId]) { + setLoadingMap((prev) => ({ ...prev, [brandId]: true })); + try { + const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + const data = await res.json(); + // Ensure we have an array of valid items + const validItems = Array.isArray(data) + ? data.filter(item => item && item.id && item.attributes) + : []; + setItemsMap((prev) => ({ ...prev, [brandId]: validItems })); + } catch (err) { + console.error("Error fetching items:", err); + setItemsMap((prev) => ({ ...prev, [brandId]: [] })); // Set empty array on error + } + setLoadingMap((prev) => ({ ...prev, [brandId]: false })); + } + } + }; + return ( + + + + {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 Available: {(itemsMap[brand.id] || []).length}

+ {( + itemsMap[brand.id] && itemsMap[brand.id].length > 0 + ? itemsMap[brand.id] + : [] + ).map((item) => ( + + + + + + + +

Part Number: {item?.attributes.part_number}

+

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

+

Price: ${item?.attributes.price}

+

Description: {item?.attributes.part_description}

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

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'}

+
+
+
+
+ ))} +
+ )} +
+
+ ) + )} +
+
+ ); +} \ No newline at end of file diff --git a/shopify.app.toml b/shopify.app.toml index fbb8a86..4ccfa50 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" +scopes = "read_inventory,read_products,write_inventory,write_products,read_publications,write_publications" [auth] -redirect_urls = ["https://shopify.data4autos.com/auth/callback", "https://shopify.data4autos.com/auth/shopify"] # Update this line as well +redirect_urls = ["https://backend.dine360.ca/auth/callback"] # Update this line as well