Data4Autos-Shopify-Frontend/app/routes/app.managebrand.jsx
2026-04-17 21:19:17 +00:00

1387 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import React, { useEffect, useMemo, useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData, useNavigate } from "@remix-run/react";
import {
Page,
Layout,
IndexTable,
Card,
Thumbnail,
TextContainer,
Spinner,
Button,
TextField,
Banner,
Toast,
Frame,
Select,
ProgressBar,
Checkbox,
Text,
Popover,
OptionList,
InlineStack,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { TitleBar } from "@shopify/app-bridge-react";
const PLAN_NAME = "Starter Sync";
const ALLOWED_STATUSES = ["ACTIVE", "TRIAL"];
const styles = {
gridContainer: {
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "10px",
},
gridItem: {
display: "flex",
flexDirection: "column",
},
gridFullWidthItem: {
gridColumn: "span 2",
display: "flex",
flexDirection: "column",
},
};
async function checkShopExists(shop) {
try {
const resp = await fetch(
`https://backend.data4autos.com/checkisshopdataexists/${shop}`
);
const data = await resp.json();
return data.status === 1;
} catch (err) {
console.error("Error checking shop:", err);
return false;
}
}
function getIntervalLabel(interval) {
switch (interval) {
case "ANNUAL":
return "Every 12 months";
case "EVERY_30_DAYS":
return "Every 30 days";
default:
return interval || "N/A";
}
}
function formatMoney(amount, currencyCode = "USD") {
if (amount == null) return "N/A";
return `${currencyCode} ${Number(amount).toFixed(2)}`;
}
function formatDate(date) {
if (!date) return "N/A";
return new Date(date).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
async function getSubscriptionDetails(request) {
const { admin, session } = await authenticate.admin(request);
const shop = session.shop;
const resp = await admin.graphql(`
query CurrentSubscriptionDetails {
currentAppInstallation {
activeSubscriptions {
id
name
status
test
createdAt
trialDays
currentPeriodEnd
lineItems {
id
plan {
pricingDetails {
__typename
... on AppRecurringPricing {
interval
price {
amount
currencyCode
}
}
}
}
}
}
}
}
`);
const result = await resp.json();
const subscriptions =
result?.data?.currentAppInstallation?.activeSubscriptions || [];
const subscription =
subscriptions.find((sub) => ALLOWED_STATUSES.includes(sub.status)) ||
subscriptions[0] ||
null;
const recurringPricing =
subscription?.lineItems?.find(
(item) =>
item?.plan?.pricingDetails?.__typename === "AppRecurringPricing"
)?.plan?.pricingDetails || null;
const isSubscribed =
!!subscription && ALLOWED_STATUSES.includes(subscription.status);
return {
shop,
isSubscribed,
subscription: subscription
? {
id: subscription.id,
name: subscription.name || PLAN_NAME,
status: subscription.status,
test: subscription.test ?? false,
createdAt: subscription.createdAt,
trialDays: subscription.trialDays ?? 0,
currentPeriodEnd: subscription.currentPeriodEnd,
interval: recurringPricing?.interval || null,
priceAmount: recurringPricing?.price?.amount || null,
currencyCode: recurringPricing?.price?.currencyCode || "USD",
}
: null,
};
}
export const loader = async ({ request }) => {
const { getTurn14AccessTokenFromMetafield } = await import(
"../utils/turn14Token.server"
);
const { admin } = await authenticate.admin(request);
const { session } = await authenticate.admin(request);
const shop = session.shop;
const { isSubscribed, subscription } = await getSubscriptionDetails(request);
let accessToken = "";
try {
accessToken = await getTurn14AccessTokenFromMetafield(request);
} catch (err) {
console.error("Error getting Turn14 access token:", err);
return json({
brands: [],
accessToken: "",
shop,
isSubscribed,
subscription,
});
}
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,
shop,
isSubscribed,
subscription,
});
};
const makes_list_raw = [
"Alfa Romeo",
"Ferrari",
"Dodge",
"Subaru",
"Toyota",
"Volkswagen",
"Volvo",
"Audi",
"BMW",
"Buick",
"Cadillac",
"Chevrolet",
"Chrysler",
"CX Automotive",
"Nissan",
"Ford",
"Hyundai",
"Infiniti",
"Lexus",
"Mercury",
"Mazda",
"Oldsmobile",
"Plymouth",
"Pontiac",
"Rolls-Royce",
"Eagle",
"Lincoln",
"Mercedes-Benz",
"GMC",
"Saab",
"Honda",
"Saturn",
"Mitsubishi",
"Isuzu",
"Jeep",
"AM General",
"Geo",
"Suzuki",
"E. P. Dutton, Inc.",
"Land Rover",
"PAS, Inc",
"Acura",
"Jaguar",
"Lotus",
"Grumman Olson",
"Porsche",
"American Motors Corporation",
"Kia",
"Lamborghini",
"Panoz Auto-Development",
"Maserati",
"Saleen",
"Aston Martin",
"Dabryan Coach Builders Inc",
"Federal Coach",
"Vector",
"Bentley",
"Daewoo",
"Qvale",
"Roush Performance",
"Autokraft Limited",
"Bertone",
"Panther Car Company Limited",
"Texas Coach Company",
"TVR Engineering Ltd",
"Morgan",
"MINI",
"Yugo",
"BMW Alpina",
"Renault",
"Bitter Gmbh and Co. Kg",
"Scion",
"Maybach",
"Lambda Control Systems",
"Merkur",
"Peugeot",
"Spyker",
"London Coach Co Inc",
"Hummer",
"Bugatti",
"Pininfarina",
"Shelby",
"Saleen Performance",
"smart",
"Tecstar, LP",
"Kenyon Corporation Of America",
"Avanti Motor Corporation",
"Bill Dovell Motor Car Company",
"Import Foreign Auto Sales Inc",
"S and S Coach Company E.p. Dutton",
"Superior Coaches Div E.p. Dutton",
"Vixen Motor Company",
"Volga Associated Automobile",
"Wallace Environmental",
"Import Trade Services",
"J.K. Motors",
"Panos",
"Quantum Technologies",
"London Taxi",
"Red Shift Ltd.",
"Ruf Automobile Gmbh",
"Excalibur Autos",
"Mahindra",
"VPG",
"Fiat",
"Sterling",
"Azure Dynamics",
"McLaren Automotive",
"Ram",
"CODA Automotive",
"Fisker",
"Tesla",
"Mcevoy Motors",
"BYD",
"ASC Incorporated",
"SRT",
"CCC Engineering",
"Mobility Ventures LLC",
"Pagani",
"Genesis",
"Karma",
"Koenigsegg",
"Aurora Cars Ltd",
"RUF Automobile",
"Dacia",
"STI",
"Daihatsu",
"Polestar",
"Kandi",
"Rivian",
"Lucid",
"JBA Motorcars, Inc.",
"Lordstown",
"Vinfast",
"INEOS Automotive",
"Bugatti Rimac",
"Grumman Allied Industries",
"Environmental Rsch and Devp Corp",
"Evans Automobiles",
"Laforza Automobile Inc",
"General Motors",
"Consulier Industries Inc",
"Goldacre",
"Isis Imports Ltd",
"PAS Inc - GMC",
];
const makes_list = makes_list_raw.sort();
export const action = async ({ request }) => {
const { isSubscribed } = await getSubscriptionDetails(request);
if (!isSubscribed) {
return json(
{
error:
"An active subscription or free trial is required to add products.",
},
{ status: 403 }
);
}
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);
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,
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 { 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 navigate = useNavigate();
const { shop, brands, accessToken, isSubscribed, subscription } =
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("");
const [Turn14Enabled, setTurn14Enabled] = useState("12345");
const [filters, setFilters] = useState({
make: "",
model: "",
year: "",
drive: "",
baseModel: "",
});
const [filterregulatstock, setfilterregulatstock] = useState(false);
const [isFilter_EnableZeroStock, set_isFilter_EnableZeroStock] =
useState(true);
const [isFilter_IncludeLtlFreightRequired, setisFilter_IncludeLtlFreightRequired] =
useState(true);
const [isFilter_Excludeclearance_item, setisFilter_Excludeclearance_item] =
useState(false);
const [
isFilter_Excludeair_freight_prohibited,
setisFilter_Excludeair_freight_prohibited,
] = useState(false);
const [isFilter_IncludeProductWithNoImages, setisFilter_IncludeProductWithNoImages] =
useState(true);
const [popoverActive, setPopoverActive] = useState(false);
useEffect(() => {
if (!shop) {
console.log("⚠️ shop is undefined or empty");
return;
}
(async () => {
const result = await checkShopExists(shop);
console.log("✅ API status result:", result, "| shop:", shop);
setTurn14Enabled(result);
})();
}, [shop]);
useEffect(() => {
if (actionData?.processId) {
setProcessId(actionData.processId);
setStatus(actionData.status || "processing");
setToastActive(true);
}
}, [actionData]);
const checkStatus = async () => {
if (!processId) return;
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 || 0);
setProcessedProducts(data.stats?.processed || 0);
setCurrentProduct(data.current);
if (data.results) {
setResults(data.results);
}
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 && isSubscribed) {
toggleAllBrands();
setInitialLoad(false);
}
}, [brands, initialLoad, isSubscribed]);
const toggleBrandItems = async (brandId) => {
if (!isSubscribed) return;
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 ? (
<Toast
content={
status.includes("completed")
? "Products imported successfully!"
: `Status: ${status}`
}
onDismiss={() => setToastActive(false)}
/>
) : null;
const handleFilterChange = (field) => (value) => {
setFilters((prev) => ({ ...prev, [field]: value }));
};
const applyFitmentFilters = (items) => {
return items.filter((item) => {
const tags = item?.attributes?.fitmmentTags || {};
const productName = item?.attributes?.product_name || "";
const brand = item?.attributes?.brand || "";
const descriptions = item?.attributes?.descriptions || [];
const makeMatch =
!filters.make ||
tags.make?.includes(filters.make) ||
productName.includes(filters.make) ||
brand.includes(filters.make) ||
descriptions.some((desc) => desc.description.includes(filters.make));
const modelMatch =
!filters.model ||
tags.model?.includes(filters.model) ||
productName.includes(filters.model) ||
brand.includes(filters.model) ||
descriptions.some((desc) => desc.description.includes(filters.model));
const yearMatch =
!filters.year ||
tags.year?.includes(filters.year) ||
productName.includes(filters.year) ||
brand.includes(filters.year) ||
descriptions.some((desc) => desc.description.includes(filters.year));
const driveMatch =
!filters.drive ||
tags.drive?.includes(filters.drive) ||
productName.includes(filters.drive) ||
brand.includes(filters.drive) ||
descriptions.some((desc) => desc.description.includes(filters.drive));
const baseModelMatch =
!filters.baseModel ||
tags.baseModel?.includes(filters.baseModel) ||
productName.includes(filters.baseModel) ||
brand.includes(filters.baseModel) ||
descriptions.some((desc) =>
desc.description.includes(filters.baseModel)
);
let isMatch =
makeMatch && modelMatch && yearMatch && driveMatch && baseModelMatch;
if (filterregulatstock) {
isMatch = isMatch && item?.attributes?.regular_stock;
}
if (!isFilter_EnableZeroStock) {
isMatch = isMatch && item?.inventoryQuantity > 0;
}
if (!isFilter_IncludeLtlFreightRequired) {
isMatch = isMatch && item?.attributes?.ltl_freight_required !== true;
}
if (isFilter_Excludeclearance_item) {
isMatch = isMatch && item?.attributes?.clearance_item !== true;
}
if (isFilter_Excludeair_freight_prohibited) {
isMatch =
isMatch && item?.attributes?.air_freight_prohibited !== true;
}
if (!isFilter_IncludeProductWithNoImages) {
isMatch =
isMatch &&
item?.attributes?.files &&
item?.attributes?.files.length > 0;
}
return isMatch;
});
};
const shopDomain = (shop || "").split(".")[0];
const items = [
{
icon: "⚙️",
text: "Manage API settings",
link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/settings`,
},
{
icon: "🏷️",
text: "Browse and import available brands",
link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands`,
},
{
icon: "📦",
text: "Sync brand collections to Shopify",
link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand`,
},
{
icon: "🔐",
text: "Handle secure Turn14 login credentials",
link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/help`,
},
];
const togglePopover = () => setPopoverActive((active) => !active);
const activator = (
<Button onClick={togglePopover} disclosure disabled={!isSubscribed}>
{Array.isArray(filters.make) && filters.make.length > 0
? `Selected (${filters.make.length})`
: "Select Makes"}
</Button>
);
const trialDaysLeft = useMemo(() => {
if (!subscription?.trialDays || !subscription?.createdAt) return null;
if (subscription.status !== "TRIAL") return null;
const created = new Date(subscription.createdAt);
const trialEnd = new Date(created);
trialEnd.setDate(trialEnd.getDate() + subscription.trialDays);
const now = new Date();
const msLeft = trialEnd.getTime() - now.getTime();
const daysLeft = Math.ceil(msLeft / (1000 * 60 * 60 * 24));
return Math.max(0, daysLeft);
}, [subscription]);
if (Turn14Enabled === false) {
const safeItems =
Array.isArray(items) && items.length >= 4
? items
: [
{ link: "#", icon: "🔗", text: "Connect Turn14" },
{ link: "#", icon: "⚙️", text: "Settings" },
{ link: "#", icon: "❓", text: "Help" },
{ link: "#", icon: "📄", text: "Documentation" },
];
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
<Layout.Section>
<Card>
<div style={{ padding: 24, textAlign: "center" }}>
<Text as="h1" variant="headingLg">
Turn14 isnt connected yet
</Text>
<div style={{ marginTop: 8 }}>
<Text as="p" variant="bodyMd">
This shop hasnt been configured with Turn14 / Data4Autos.
To get started, open Settings and complete the connection.
</Text>
</div>
<div
style={{
marginTop: 24,
display: "flex",
gap: 16,
justifyContent: "center",
flexWrap: "wrap",
}}
>
<a
href={items[0].link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none" }}
>
<Text as="h6" variant="headingMd" fontWeight="bold">
{items[0].icon} {items[0].text}
</Text>
</a>
<a
href={items[3].link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none" }}
>
<Text as="h6" variant="headingMd" fontWeight="bold">
{items[3].icon} {items[3].text}
</Text>
</a>
</div>
<div style={{ marginTop: 28 }}>
<Text as="p" variant="bodySm" tone="subdued">
Once connected, youll be able to browse brands and sync
collections.
</Text>
</div>
<div
style={{
marginTop: 20,
display: "flex",
gap: 16,
justifyContent: "center",
flexWrap: "wrap",
}}
>
<a
href={safeItems[1].link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none" }}
>
<Text as="p" variant="bodyMd">
{safeItems[1].icon} {safeItems[1].text}
</Text>
</a>
<a
href={safeItems[2].link}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none" }}
>
<Text as="p" variant="bodyMd">
{safeItems[2].icon} {safeItems[2].text}
</Text>
</a>
</div>
</div>
</Card>
</Layout.Section>
</Layout>
</Page>
</Frame>
);
}
return (
<Frame>
<Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
{!isSubscribed && (
<Layout.Section>
<Banner title="Subscription required" tone="warning">
<p>
This feature is available only for merchants with an active
subscription or during the free trial period.
</p>
<div style={{ marginTop: 12 }}>
<p>
<strong>Current status:</strong>{" "}
{subscription?.status || "Not subscribed"}
</p>
<p>
<strong>Plan:</strong> {subscription?.name || PLAN_NAME}
</p>
<p>
<strong>Billing:</strong>{" "}
{getIntervalLabel(subscription?.interval)}
</p>
<p>
<strong>Price:</strong>{" "}
{formatMoney(
subscription?.priceAmount,
subscription?.currencyCode
)}
</p>
<p>
<strong>Next renewal / period end:</strong>{" "}
{formatDate(subscription?.currentPeriodEnd)}
</p>
<p>
<strong>Trial days left:</strong>{" "}
{trialDaysLeft != null ? `${trialDaysLeft} day(s)` : "N/A"}
</p>
</div>
<div style={{ marginTop: 16 }}>
<InlineStack gap="300">
<Button variant="primary" onClick={() => navigate("/app")}>
Go to Home Page
</Button>
</InlineStack>
</div>
</Banner>
</Layout.Section>
)}
{actionData?.error && (
<Layout.Section>
<Banner title="Action blocked" tone="critical">
<p>{actionData.error}</p>
</Banner>
</Layout.Section>
)}
{brands.length === 0 ? (
<Layout.Section>
<Card sectioned>
<p>No brands selected yet.</p>
</Card>
</Layout.Section>
) : (
<Layout.Section>
<Card>
<IndexTable
resourceName={{ singular: "brand", plural: "brands" }}
itemCount={brands.length}
headings={[
{
title: (
<div
style={{
fontWeight: 600,
fontSize: "16px",
background: "#f4f6f8",
padding: "15px 8px",
borderRadius: "4px",
}}
>
Brand ID
</div>
),
},
{
title: (
<div
style={{
fontWeight: 600,
fontSize: "16px",
background: "#f4f6f8",
padding: "15px 8px",
borderRadius: "4px",
}}
>
Brand Name
</div>
),
},
{
title: (
<div
style={{
fontWeight: 600,
fontSize: "16px",
background: "#f4f6f8",
padding: "15px 8px",
borderRadius: "4px",
}}
>
Brand Logo
</div>
),
},
{
title: (
<div
style={{
fontWeight: 600,
fontSize: "16px",
background: "#f4f6f8",
padding: "15px 8px",
borderRadius: "4px",
}}
>
Action
</div>
),
},
{
title: (
<div
style={{
fontWeight: 600,
fontSize: "16px",
background: "#f4f6f8",
padding: "15px 8px",
borderRadius: "4px",
}}
>
Products Count
</div>
),
},
]}
selectable={false}
>
{brands.map((brand, index) => (
<IndexTable.Row
id={brand.id.toString()}
key={brand.id}
position={index}
>
<IndexTable.Cell>{brand.id}</IndexTable.Cell>
<IndexTable.Cell>{brand.name}</IndexTable.Cell>
<IndexTable.Cell>
<Thumbnail
source={
brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="medium"
/>
</IndexTable.Cell>
<IndexTable.Cell>
<Button
onClick={() => toggleBrandItems(brand.id)}
variant="primary"
disabled={!isSubscribed}
>
{expandedBrand === brand.id
? "Hide Products"
: "Show Products"}
</Button>
</IndexTable.Cell>
<IndexTable.Cell>
<span
style={{
display: "inline-block",
background: "#00d1ff29",
color: "#00d1ff",
padding: "4px 8px",
borderRadius: "12px",
fontWeight: "600",
fontSize: "14px",
minWidth: "28px",
textAlign: "center",
}}
>
{itemsMap[brand.id]?.length || 0}
</span>
</IndexTable.Cell>
</IndexTable.Row>
))}
</IndexTable>
</Card>
</Layout.Section>
)}
{isSubscribed &&
brands.map((brand) => {
const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []);
return (
expandedBrand === brand.id && (
<Layout.Section fullWidth key={brand.id + "-expanded"}>
{processId && (
<Card sectioned>
<div
style={{
marginBottom: "1rem",
padding: "1rem",
border: "1px solid #e1e1e1",
borderRadius: "4px",
}}
>
<p>
<strong>Process ID:</strong> {processId}
</p>
<div style={{ margin: "1rem 0" }}>
<p>
<strong>Status:</strong> {status || ""}
</p>
{progress > 0 && (
<div style={{ marginTop: "0.5rem" }}>
<ProgressBar
progress={progress}
color={
status === "error"
? "critical"
: status === "done"
? "success"
: "highlight"
}
/>
<p
style={{
marginTop: "0.25rem",
fontSize: "0.85rem",
}}
>
{processedProducts} of {totalProducts} products
processed
{currentProduct &&
` - Current: ${currentProduct.name} (${currentProduct.number}/${currentProduct.total})`}
</p>
</div>
)}
</div>
{status === "done" && results.length > 0 && (
<div style={{ marginTop: "1rem" }}>
<p>
<strong>Results:</strong> {results.length} products
processed successfully
</p>
</div>
)}
{status === "error" && (
<div style={{ marginTop: "1rem", color: "#ff4d4f" }}>
<strong>Error:</strong> {detail}
</div>
)}
<Button
onClick={checkStatus}
loading={polling}
variant="primary"
size="large"
>
{status === "done" ? "View Results" : "Check Status"}
</Button>
</div>
</Card>
)}
<Card title={`Items from ${brand.name}`} sectioned>
{loadingMap[brand.id] ? (
<Spinner accessibilityLabel="Loading items" size="small" />
) : (
<div style={{ paddingTop: "1rem" }}>
<Form method="post">
<input
type="hidden"
name="selectedProductIds"
value={JSON.stringify(
filteredItems.map((item) => item.id)
)}
/>
<input type="hidden" name="brandId" value={brand.id} />
<div
style={{
display: "flex",
gap: "1rem",
alignItems: "end",
flexWrap: "wrap",
}}
>
<TextField
label="Number of products in Selected Filter Make"
type="number"
name="productCount"
value={String(filteredItems.length)}
onChange={(value) => setProductCount(value)}
autoComplete="off"
/>
<Checkbox
label="Enable Zero Stock"
checked={isFilter_EnableZeroStock}
onChange={() =>
set_isFilter_EnableZeroStock(
!isFilter_EnableZeroStock
)
}
/>
<Checkbox
label="Filter Only the Regular Stock"
checked={filterregulatstock}
onChange={() =>
setfilterregulatstock(!filterregulatstock)
}
/>
<Checkbox
label="Include LTL Freight Required"
checked={isFilter_IncludeLtlFreightRequired}
onChange={() =>
setisFilter_IncludeLtlFreightRequired(
!isFilter_IncludeLtlFreightRequired
)
}
/>
<Checkbox
label="Exclude Clearance Item"
checked={isFilter_Excludeclearance_item}
onChange={() =>
setisFilter_Excludeclearance_item(
!isFilter_Excludeclearance_item
)
}
/>
<Checkbox
label="Exclude Air Freight Prohibited"
checked={isFilter_Excludeair_freight_prohibited}
onChange={() =>
setisFilter_Excludeair_freight_prohibited(
!isFilter_Excludeair_freight_prohibited
)
}
/>
<Checkbox
label="Include Products With No Images"
checked={isFilter_IncludeProductWithNoImages}
onChange={() =>
setisFilter_IncludeProductWithNoImages(
!isFilter_IncludeProductWithNoImages
)
}
/>
<Button
submit
variant="primary"
size="large"
loading={status?.includes("processing")}
disabled={!isSubscribed}
>
Add First {filteredItems.length} Products from{" "}
{Array.isArray(filters.make)
? filters.make.join(", ")
: filters.make}{" "}
to Store
</Button>
</div>
</Form>
<div style={{ padding: "20px 0px" }}>
<Card title="Filter Products by Fitment Tags" sectioned>
<Layout>
<Layout.Section oneThird>
<Select
label="Make"
options={[
{ label: "All", value: "" },
...Array.from(makes_list).map((m) => ({
label: m,
value: m,
})),
]}
onChange={handleFilterChange("make")}
value={Array.isArray(filters.make) ? "" : filters.make}
disabled={!isSubscribed}
/>
</Layout.Section>
</Layout>
</Card>
<Card title="Filter Products by Fitment Tags" sectioned>
<Layout>
<Layout.Section oneThird>
<Popover
active={popoverActive}
activator={activator}
onClose={togglePopover}
>
<OptionList
title="Make"
onChange={(selected) =>
setFilters((prev) => ({
...prev,
make: selected,
}))
}
options={[
{ label: "All", value: "ALL" },
...Array.from(makes_list).map((m) => ({
label: m,
value: m,
})),
]}
selected={
Array.isArray(filters.make)
? filters.make
: filters.make
? [filters.make]
: []
}
allowMultiple
/>
</Popover>
</Layout.Section>
</Layout>
</Card>
</div>
<div
style={{
display: "grid",
gridTemplateColumns:
"repeat(auto-fill, minmax(300px, 1fr))",
gap: 16,
}}
>
{filteredItems.map((item) => (
<Card
key={item.id}
title={
item?.attributes?.product_name ||
"Untitled Product"
}
sectioned
>
<Layout>
<Layout.Section oneThird>
<Thumbnail
source={
item?.attributes?.thumbnail ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={
item?.attributes?.product_name ||
"Product image"
}
size="large"
/>
</Layout.Section>
<Layout.Section>
<TextContainer spacing="tight">
<div style={styles.gridContainer}>
<div style={styles.gridItem}>
<strong>Part Number:</strong>{" "}
{item?.attributes?.part_number || "N/A"}
</div>
<div style={styles.gridItem}>
<strong>Category:</strong>{" "}
{item?.attributes?.category || "N/A"} &gt;{" "}
{item?.attributes?.subcategory || "N/A"}
</div>
<div style={styles.gridItem}>
<strong>Price:</strong> $
{item?.attributes?.price || "0.00"}
</div>
<div style={styles.gridFullWidthItem}>
<strong>Description:</strong>{" "}
{item?.attributes?.part_description ||
"No description available"}
</div>
<div style={styles.gridItem}>
<strong>Inventory Quantity:</strong>{" "}
{item?.inventoryQuantity || "N/A"}
</div>
<div style={styles.gridItem}>
<strong>Regular Stock:</strong>{" "}
{item?.attributes?.regular_stock === true
? "YES"
: "NO"}
</div>
<div style={styles.gridItem}>
<strong>LTL Freight Required:</strong>{" "}
{item?.attributes?.ltl_freight_required ===
true
? "YES"
: "NO"}
</div>
<div style={styles.gridItem}>
<strong>Is Clearance Item:</strong>{" "}
{item?.attributes?.is_clearance_item ===
true
? "YES"
: "NO"}
</div>
<div style={styles.gridItem}>
<strong>Is Air Freight Prohibited:</strong>{" "}
{item?.attributes
?.is_air_freight_prohibited === true
? "YES"
: "NO"}
</div>
<div style={styles.gridItem}>
<strong>No. Of Images:</strong>{" "}
{item?.attributes?.files?.length || "N/A"}
</div>
</div>
</TextContainer>
</Layout.Section>
</Layout>
</Card>
))}
</div>
</div>
)}
</Card>
</Layout.Section>
)
);
})}
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}