1000 lines
37 KiB
JavaScript
1000 lines
37 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
||
import { json } from "@remix-run/node";
|
||
import { useLoaderData, Form, useActionData } from "@remix-run/react";
|
||
import {
|
||
Page,
|
||
Layout,
|
||
IndexTable,
|
||
Card,
|
||
Thumbnail,
|
||
TextContainer,
|
||
Spinner,
|
||
Button,
|
||
TextField,
|
||
Banner,
|
||
InlineError,
|
||
Toast,
|
||
Frame,
|
||
Select,
|
||
ProgressBar,
|
||
Checkbox,
|
||
Text,
|
||
ChoiceList,
|
||
Popover,
|
||
OptionList,
|
||
} from "@shopify/polaris";
|
||
import { authenticate } from "../shopify.server";
|
||
import { TitleBar } from "@shopify/app-bridge-react";
|
||
|
||
const styles = {
|
||
gridContainer: {
|
||
display: 'grid',
|
||
gridTemplateColumns: '1fr 1fr', // Two equal columns
|
||
gap: '10px', // Space between items
|
||
},
|
||
gridItem: {
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
},
|
||
gridFullWidthItem: {
|
||
gridColumn: 'span 2', // This takes up the full width (for Description)
|
||
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; // ✅ true if shop exists, false otherwise
|
||
} catch (err) {
|
||
console.error("Error checking shop:", err);
|
||
return false; // default to false if error
|
||
}
|
||
}
|
||
|
||
|
||
|
||
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;
|
||
|
||
var accessToken = ""
|
||
try {
|
||
accessToken = await getTurn14AccessTokenFromMetafield(request);
|
||
} catch (err) {
|
||
return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop });
|
||
console.error("Error getting Turn14 access token:", err);
|
||
// Proceeding with empty accessToken
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
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 });
|
||
};
|
||
|
||
|
||
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 { 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 { shop, brands, accessToken } = useLoaderData();
|
||
const [expandedBrand, setExpandedBrand] = useState(null);
|
||
const [itemsMap, setItemsMap] = useState({});
|
||
const [loadingMap, setLoadingMap] = useState({});
|
||
const [productCount, setProductCount] = useState("10");
|
||
const [initialLoad, setInitialLoad] = useState(true);
|
||
const [toastActive, setToastActive] = useState(false);
|
||
const [polling, setPolling] = useState(false);
|
||
const [status, setStatus] = useState(actionData?.status || "");
|
||
const [processId, setProcessId] = useState(actionData?.processId || null);
|
||
const [progress, setProgress] = useState(0);
|
||
const [totalProducts, setTotalProducts] = useState(0);
|
||
const [processedProducts, setProcessedProducts] = useState(0);
|
||
const [currentProduct, setCurrentProduct] = useState(null);
|
||
const [results, setResults] = useState([]);
|
||
const [detail, setDetail] = useState("");
|
||
|
||
|
||
|
||
|
||
const [Turn14Enabled, setTurn14Enabled] = useState("12345"); // null | true | 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 () => {
|
||
setPolling(true);
|
||
try {
|
||
const response = await fetch(`https://backend.data4autos.com/manageProducts/status/${processId}`);
|
||
const data = await response.json();
|
||
|
||
setStatus(data.status);
|
||
setDetail(data.detail);
|
||
setProgress(data.progress);
|
||
setTotalProducts(data.stats.total);
|
||
setProcessedProducts(data.stats.processed);
|
||
setCurrentProduct(data.current);
|
||
|
||
if (data.results) {
|
||
setResults(data.results);
|
||
}
|
||
|
||
// Continue polling if still processing
|
||
if (data.status !== 'done' && data.status !== 'error') {
|
||
setTimeout(checkStatus, 2000);
|
||
} else {
|
||
setPolling(false);
|
||
}
|
||
} catch (error) {
|
||
setPolling(false);
|
||
setStatus('error');
|
||
setDetail('Failed to check status');
|
||
console.error('Error checking status:', error);
|
||
}
|
||
};
|
||
useEffect(() => {
|
||
let interval;
|
||
if (status?.includes("processing") && processId) {
|
||
interval = setInterval(checkStatus, 5000);
|
||
}
|
||
return () => clearInterval(interval);
|
||
}, [status, processId]);
|
||
|
||
const toggleAllBrands = async () => {
|
||
for (const brand of brands) {
|
||
await toggleBrandItems(brand.id);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (initialLoad && brands.length > 0) {
|
||
toggleAllBrands();
|
||
setInitialLoad(false);
|
||
}
|
||
}, [brands, initialLoad]);
|
||
|
||
const toggleBrandItems = async (brandId) => {
|
||
const isExpanded = expandedBrand === brandId;
|
||
if (isExpanded) {
|
||
setExpandedBrand(null);
|
||
} else {
|
||
setExpandedBrand(brandId);
|
||
if (!itemsMap[brandId]) {
|
||
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
|
||
try {
|
||
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${accessToken}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
});
|
||
const data = await res.json();
|
||
const dataitems = data.items
|
||
const validItems = Array.isArray(dataitems)
|
||
? dataitems.filter(item => item && item.id && item.attributes)
|
||
: [];
|
||
setItemsMap((prev) => ({ ...prev, [brandId]: validItems }));
|
||
} catch (err) {
|
||
console.error("Error fetching items:", err);
|
||
setItemsMap((prev) => ({ ...prev, [brandId]: [] }));
|
||
}
|
||
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
|
||
}
|
||
}
|
||
};
|
||
|
||
const toastMarkup = toastActive ? (
|
||
<Toast
|
||
content={status.includes("completed") ?
|
||
"Products imported successfully!" :
|
||
`Status: ${status}`}
|
||
onDismiss={() => setToastActive(false)}
|
||
/>
|
||
) : null;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
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 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 partDescription = item?.attributes?.part_description || '';
|
||
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));
|
||
// console.log(`Model check result: ${modelMatch}`);
|
||
|
||
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));
|
||
/// console.log(`Year check result: ${yearMatch}`);
|
||
|
||
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));
|
||
// console.log(`Drive check result: ${driveMatch}`);
|
||
|
||
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));
|
||
// console.log(`Base Model check result: ${baseModelMatch}`);
|
||
|
||
// Combine all the conditions
|
||
var isMatch = makeMatch && modelMatch && yearMatch && driveMatch && baseModelMatch// && item.attributes.regular_stock
|
||
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 selectedProductIds = []
|
||
|
||
|
||
|
||
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 [popoverActive, setPopoverActive] = useState(false);
|
||
|
||
const togglePopover = () => setPopoverActive((active) => !active);
|
||
|
||
|
||
const activator = (
|
||
<Button onClick={togglePopover} disclosure>
|
||
{filters.make.length > 0
|
||
? `Selected (${filters.make.length})`
|
||
: "Select Makes"}
|
||
</Button>
|
||
);
|
||
// If Turn14 is explicitly NOT connected, show a lightweight call-to-action screen
|
||
if (Turn14Enabled === false) {
|
||
// Fallback if items array is not loaded yet
|
||
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" background="critical" />
|
||
<Layout>
|
||
<Layout.Section>
|
||
<Card>
|
||
<div style={{ padding: 24, textAlign: "center" }}>
|
||
<Text as="h1" variant="headingLg">
|
||
Turn14 isn’t connected yet
|
||
</Text>
|
||
<div style={{ marginTop: 8 }}>
|
||
<Text as="p" variant="bodyMd">
|
||
This shop hasn’t been configured with Turn14 / Data4Autos. To get started, open Settings and complete the connection.
|
||
</Text>
|
||
</div>
|
||
|
||
{/* Primary actions */}
|
||
<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, you’ll be able to browse brands and sync collections.
|
||
</Text>
|
||
</div>
|
||
|
||
{/* Secondary links */}
|
||
<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>
|
||
{/* <p>
|
||
<strong>Turn 14 Status:</strong>{" "}
|
||
{Turn14Enabled === true
|
||
? "✅ Turn14 x Shopify Connected!"
|
||
: Turn14Enabled === false
|
||
? "❌ Turn14 x Shopify Connection Doesn't Exists"
|
||
: "Checking..."}
|
||
</p> */}
|
||
|
||
{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) => {
|
||
|
||
return (
|
||
|
||
<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">
|
||
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
|
||
</Button>
|
||
</IndexTable.Cell>
|
||
<IndexTable.Cell>
|
||
<span
|
||
style={{
|
||
display: "inline-block",
|
||
background: "#00d1ff29", // light teal background
|
||
color: "#00d1ff", // dark teal text
|
||
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>
|
||
)}
|
||
|
||
{brands.map((brand) => {
|
||
const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []);
|
||
// console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.id));
|
||
const uniqueTags = {
|
||
make: new Set(),
|
||
model: new Set(),
|
||
year: new Set(),
|
||
drive: new Set(),
|
||
baseModel: new Set(),
|
||
};
|
||
|
||
(itemsMap[brand.id] || []).forEach(item => {
|
||
const tags = item?.attributes?.fitmmentTags || {};
|
||
Object.keys(uniqueTags).forEach(key => {
|
||
(tags[key] || []).forEach(val => uniqueTags[key].add(val));
|
||
});
|
||
});
|
||
|
||
return (
|
||
|
||
expandedBrand === brand.id &&
|
||
|
||
(
|
||
<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"
|
||
style={{ marginTop: "1rem" }}
|
||
>
|
||
{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" }}>
|
||
<TextField
|
||
label="Number of products in Selected Filter Make"
|
||
type="number"
|
||
name="productCount"
|
||
value={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
|
||
primary variant="primary" size="large"
|
||
style={{ marginTop: "1rem" }}
|
||
loading={status?.includes("processing")}
|
||
>
|
||
Add First {filteredItems.length} Products from {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={filters.make}
|
||
/>
|
||
</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={handleFilterChange}
|
||
options={[
|
||
{ label: "All", value: "ALL" },
|
||
...Array.from(makes_list).map((m) => ({
|
||
label: m,
|
||
value: m,
|
||
})),
|
||
]}
|
||
selected={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">
|
||
<p><strong>Part Number:</strong> {item?.attributes?.part_number || 'N/A'}</p>
|
||
|
||
<p><strong>Category:</strong> {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}</p>
|
||
<p><strong>Price:</strong> ${item?.attributes?.price || '0.00'}</p>
|
||
<p><strong>Description:</strong> {item?.attributes?.part_description || 'No description available'}</p>
|
||
<p><strong>Inventory Quantity:</strong> {item?.inventoryQuantity || 'N/A'}</p>
|
||
<p><strong>Regular Stock : </strong> {item?.attributes?.regular_stock === true ? "YES" : "NO" || 'N/A'}</p>
|
||
<p><strong>LTL Freight Required :</strong> {item?.attributes?.ltl_freight_required === true ? "YES" : "NO" || 'N/A'}</p>
|
||
<p><strong>Is Clearance Item :</strong> {item?.attributes?.is_clearance_item === true ? "YES" : "NO" || 'N/A'}</p>
|
||
<p><strong>Is Air Freight Prohibited :</strong> {item?.attributes?.is_air_freight_prohibited === true ? "YES" : "NO" || 'N/A'}</p>
|
||
<p><strong>No. Of Images :</strong> {item?.attributes?.files.length || 'N/A'}</p>
|
||
</TextContainer>
|
||
</Layout.Section> */}
|
||
<Layout.Section>
|
||
<TextContainer spacing="tight">
|
||
<div style={styles.gridContainer}>
|
||
{/* Part Number */}
|
||
<div style={styles.gridItem}>
|
||
<strong>Part Number:</strong> {item?.attributes?.part_number || 'N/A'}
|
||
</div>
|
||
|
||
{/* Category & Subcategory */}
|
||
<div style={styles.gridItem}>
|
||
<strong>Category:</strong> {item?.attributes?.category || 'N/A'} > {item?.attributes?.subcategory || 'N/A'}
|
||
</div>
|
||
|
||
{/* Price */}
|
||
<div style={styles.gridItem}>
|
||
<strong>Price:</strong> ${item?.attributes?.price || '0.00'}
|
||
</div>
|
||
|
||
{/* Description (1 column) */}
|
||
<div style={styles.gridFullWidthItem}>
|
||
<strong>Description:</strong> {item?.attributes?.part_description || 'No description available'}
|
||
</div>
|
||
|
||
{/* Inventory Quantity */}
|
||
<div style={styles.gridItem}>
|
||
<strong>Inventory Quantity:</strong> {item?.inventoryQuantity || 'N/A'}
|
||
</div>
|
||
|
||
{/* Regular Stock */}
|
||
<div style={styles.gridItem}>
|
||
<strong>Regular Stock:</strong> {item?.attributes?.regular_stock === true ? "YES" : "NO" || 'N/A'}
|
||
</div>
|
||
|
||
{/* LTL Freight Required */}
|
||
<div style={styles.gridItem}>
|
||
<strong>LTL Freight Required:</strong> {item?.attributes?.ltl_freight_required === true ? "YES" : "NO" || 'N/A'}
|
||
</div>
|
||
|
||
{/* Clearance Item */}
|
||
<div style={styles.gridItem}>
|
||
<strong>Is Clearance Item:</strong> {item?.attributes?.is_clearance_item === true ? "YES" : "NO" || 'N/A'}
|
||
</div>
|
||
|
||
{/* Air Freight Prohibited */}
|
||
<div style={styles.gridItem}>
|
||
<strong>Is Air Freight Prohibited:</strong> {item?.attributes?.is_air_freight_prohibited === true ? "YES" : "NO" || 'N/A'}
|
||
</div>
|
||
|
||
{/* Number of Images */}
|
||
<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>
|
||
);
|
||
} |