1387 lines
46 KiB
JavaScript
1387 lines
46 KiB
JavaScript
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 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>
|
||
|
||
<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>
|
||
|
||
<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"} >{" "}
|
||
{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>
|
||
);
|
||
} |