681 lines
37 KiB
JavaScript
681 lines
37 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,
|
||
Card,
|
||
Spinner,
|
||
Button,
|
||
TextField,
|
||
Banner,
|
||
Toast,
|
||
Frame,
|
||
Select,
|
||
ProgressBar,
|
||
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"];
|
||
|
||
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) {
|
||
if (interval === "ANNUAL") return "Every 12 months";
|
||
if (interval === "EVERY_30_DAYS") return "Every 30 days";
|
||
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;
|
||
let { isSubscribed, subscription } = await getSubscriptionDetails(request);
|
||
if (!isSubscribed) {
|
||
try {
|
||
const far = await fetch(`https://backend.data4autos.com/free-access/${encodeURIComponent(shop)}`);
|
||
const fad = await far.json();
|
||
if (fad.allowed === true) isSubscribed = true;
|
||
} catch {}
|
||
}
|
||
|
||
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) {}
|
||
|
||
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 { session } = await authenticate.admin(request);
|
||
const shop = session.shop;
|
||
let { isSubscribed } = await getSubscriptionDetails(request);
|
||
if (!isSubscribed) {
|
||
try {
|
||
const far = await fetch(`https://backend.data4autos.com/free-access/${encodeURIComponent(shop)}`);
|
||
const fad = await far.json();
|
||
if (fad.allowed === true) isSubscribed = true;
|
||
} catch {}
|
||
}
|
||
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 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 });
|
||
};
|
||
|
||
// ─── Toggle filter pill ───────────────────────────────────────────────────────
|
||
function FilterPill({ label, checked, onChange, disabled }) {
|
||
return (
|
||
<div
|
||
onClick={() => !disabled && onChange(!checked)}
|
||
style={{
|
||
display: "inline-flex", alignItems: "center", gap: 6,
|
||
padding: "6px 14px", borderRadius: 20, cursor: disabled ? "default" : "pointer",
|
||
border: `1px solid ${checked ? "#2563eb" : "#e5e7eb"}`,
|
||
background: checked ? "#eff6ff" : "#fafafa",
|
||
fontSize: 13, fontWeight: checked ? 700 : 500,
|
||
color: checked ? "#1d4ed8" : "#374151",
|
||
transition: "all 0.15s",
|
||
userSelect: "none",
|
||
}}
|
||
>
|
||
<div style={{ width: 16, height: 16, borderRadius: "50%", background: checked ? "#2563eb" : "#e5e7eb", border: `2px solid ${checked ? "#2563eb" : "#d1d5db"}`, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
||
{checked && <span style={{ color: "#fff", fontSize: 9, fontWeight: 900 }}>✓</span>}
|
||
</div>
|
||
{label}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Product card ─────────────────────────────────────────────────────────────
|
||
function ProductCard({ item }) {
|
||
const attrs = item?.attributes || {};
|
||
const qty = item?.inventoryQuantity || 0;
|
||
const inStock = qty > 0;
|
||
return (
|
||
<div style={{ background: "#fff", border: "1px solid #e5e7eb", borderRadius: 12, overflow: "hidden", boxShadow: "0 1px 4px rgba(0,0,0,0.04)" }}>
|
||
<div style={{ background: "#f8fafc", padding: "12px 14px", display: "flex", alignItems: "center", gap: 12 }}>
|
||
<img
|
||
src={attrs.thumbnail || "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"}
|
||
alt={attrs.product_name || "Product"}
|
||
style={{ width: 56, height: 56, objectFit: "contain", borderRadius: 8, background: "#fff", border: "1px solid #e5e7eb" }}
|
||
/>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ fontWeight: 700, fontSize: 13, color: "#111827", lineHeight: 1.3, overflow: "hidden", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }}>
|
||
{attrs.product_name || "Untitled Product"}
|
||
</div>
|
||
<div style={{ fontSize: 12, color: "#9ca3af", marginTop: 2 }}>{attrs.part_number || "—"}</div>
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: "12px 14px" }}>
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "6px 12px", fontSize: 12 }}>
|
||
<div><span style={{ color: "#9ca3af" }}>Category: </span><span style={{ fontWeight: 600, color: "#374151" }}>{attrs.category || "—"}</span></div>
|
||
<div><span style={{ color: "#9ca3af" }}>Subcategory: </span><span style={{ fontWeight: 600, color: "#374151" }}>{attrs.subcategory || "—"}</span></div>
|
||
<div>
|
||
<span style={{ color: "#9ca3af" }}>Price: </span>
|
||
<span style={{ fontWeight: 700, color: "#15803d" }}>${attrs.price || "0.00"}</span>
|
||
</div>
|
||
<div>
|
||
<span style={{ color: "#9ca3af" }}>Stock: </span>
|
||
<span style={{ fontWeight: 700, color: inStock ? "#15803d" : "#dc2626" }}>{qty} {inStock ? "✓" : "✗"}</span>
|
||
</div>
|
||
<div style={{ gridColumn: "span 2" }}>
|
||
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 4 }}>
|
||
{attrs.regular_stock && <span style={{ background: "#f0fdf4", border: "1px solid #bbf7d0", color: "#15803d", borderRadius: 4, padding: "2px 6px", fontSize: 11, fontWeight: 600 }}>Regular</span>}
|
||
{attrs.ltl_freight_required && <span style={{ background: "#fffbeb", border: "1px solid #fde68a", color: "#d97706", borderRadius: 4, padding: "2px 6px", fontSize: 11, fontWeight: 600 }}>LTL</span>}
|
||
{attrs.clearance_item && <span style={{ background: "#faf5ff", border: "1px solid #e9d5ff", color: "#7c3aed", borderRadius: 4, padding: "2px 6px", fontSize: 11, fontWeight: 600 }}>Clearance</span>}
|
||
{attrs.air_freight_prohibited && <span style={{ background: "#fff1f2", border: "1px solid #fecdd3", color: "#dc2626", borderRadius: 4, padding: "2px 6px", fontSize: 11, fontWeight: 600 }}>No Air</span>}
|
||
{attrs.files?.length > 0 && <span style={{ background: "#f0f9ff", border: "1px solid #bae6fd", color: "#0369a1", borderRadius: 4, padding: "2px 6px", fontSize: 11, fontWeight: 600 }}>{attrs.files.length} imgs</span>}
|
||
</div>
|
||
</div>
|
||
{attrs.part_description && (
|
||
<div style={{ gridColumn: "span 2", color: "#6b7280", fontSize: 12, lineHeight: 1.4, marginTop: 2, overflow: "hidden", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }}>
|
||
{attrs.part_description}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─── Main component ───────────────────────────────────────────────────────────
|
||
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 [importing, setImporting] = useState(false);
|
||
|
||
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) 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);
|
||
setImporting(false);
|
||
}
|
||
if (actionData?.error) setImporting(false);
|
||
}, [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");
|
||
}
|
||
};
|
||
|
||
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 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 trialDaysLeft = useMemo(() => {
|
||
if (!subscription?.trialDays || !subscription?.createdAt || subscription.status !== "TRIAL") return null;
|
||
const created = new Date(subscription.createdAt);
|
||
const trialEnd = new Date(created);
|
||
trialEnd.setDate(trialEnd.getDate() + subscription.trialDays);
|
||
return Math.max(0, Math.ceil((trialEnd.getTime() - Date.now()) / (1000 * 60 * 60 * 24)));
|
||
}, [subscription]);
|
||
|
||
const activeMakeLabel = Array.isArray(filters.make) && filters.make.length > 0 ? `Makes (${filters.make.length})` : "Select Makes";
|
||
|
||
if (Turn14Enabled === false) {
|
||
return (
|
||
<Frame>
|
||
<Page fullWidth>
|
||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||
<div style={{ background: "linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)", borderRadius: 16, padding: "28px 32px", marginBottom: 24, color: "#fff" }}>
|
||
<div style={{ fontSize: 22, fontWeight: 800 }}>📦 Manage Brand Products</div>
|
||
</div>
|
||
<Card>
|
||
<div style={{ padding: 48, textAlign: "center" }}>
|
||
<div style={{ fontSize: 48, marginBottom: 12 }}>🔌</div>
|
||
<Text as="h1" variant="headingLg">Turn14 not connected yet</Text>
|
||
<div style={{ marginTop: 8, color: "#6b7280", marginBottom: 24 }}>Complete the Turn14 connection in Settings to start managing products.</div>
|
||
<div style={{ display: "flex", gap: 12, justifyContent: "center" }}>
|
||
<Button variant="primary" onClick={() => navigate("/app/settings")}>Go to Settings</Button>
|
||
<Button onClick={() => navigate("/app/help")}>View Help</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</Page>
|
||
</Frame>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Frame>
|
||
<Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
|
||
<TitleBar title="Data4Autos Turn14 Integration" />
|
||
|
||
{/* Header */}
|
||
<div style={{ background: "linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)", borderRadius: 16, padding: "28px 32px", marginBottom: 24, color: "#fff", display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
|
||
<div>
|
||
<div style={{ fontSize: 22, fontWeight: 800 }}>📦 Manage Brand Products</div>
|
||
<div style={{ fontSize: 14, opacity: 0.8, marginTop: 4 }}>{brands.length} brand{brands.length !== 1 ? "s" : ""} selected · {shop}</div>
|
||
</div>
|
||
<div style={{ display: "flex", gap: 10 }}>
|
||
<button onClick={() => navigate("/app/dashboard")} style={{ background: "rgba(255,255,255,0.15)", border: "1px solid rgba(255,255,255,0.3)", borderRadius: 8, color: "#fff", padding: "8px 16px", cursor: "pointer", fontWeight: 600, fontSize: 13 }}>
|
||
📊 Live Dashboard
|
||
</button>
|
||
<button onClick={() => navigate("/app/brands")} style={{ background: "rgba(255,255,255,0.1)", border: "1px solid rgba(255,255,255,0.2)", borderRadius: 8, color: "#fff", padding: "8px 16px", cursor: "pointer", fontWeight: 600, fontSize: 13 }}>
|
||
+ Add Brands
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Subscription warning */}
|
||
{!isSubscribed && (
|
||
<div style={{ background: "#fffbeb", border: "1px solid #fde68a", borderRadius: 12, padding: "16px 20px", marginBottom: 20 }}>
|
||
<div style={{ fontWeight: 700, color: "#b45309", marginBottom: 6 }}>⚠️ Active subscription required</div>
|
||
<div style={{ fontSize: 13, color: "#92400e", marginBottom: 12 }}>Import is only available with an active subscription or free trial.</div>
|
||
<Button variant="primary" onClick={() => navigate("/app")}>Manage Subscription</Button>
|
||
</div>
|
||
)}
|
||
|
||
{actionData?.error && (
|
||
<div style={{ background: "#fff1f2", border: "1px solid #fecdd3", borderRadius: 12, padding: "14px 18px", marginBottom: 20, color: "#dc2626" }}>
|
||
⛔ {actionData.error}
|
||
</div>
|
||
)}
|
||
|
||
{/* Live import status bar */}
|
||
{processId && (
|
||
<div style={{ background: status === "error" ? "#fff1f2" : status === "done" ? "#f0fdf4" : "linear-gradient(135deg, #0f172a, #1e3a5f)", border: `1px solid ${status === "error" ? "#fecdd3" : status === "done" ? "#bbf7d0" : "transparent"}`, borderRadius: 12, padding: "16px 20px", marginBottom: 20, color: status === "error" || status === "done" ? undefined : "#fff" }}>
|
||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12, marginBottom: progress > 0 ? 12 : 0 }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||
{polling && <Spinner size="small" />}
|
||
<div>
|
||
<div style={{ fontWeight: 700, fontSize: 15 }}>
|
||
{status === "done" ? "✅ Import Complete" : status === "error" ? "⛔ Import Error" : "⚙️ Import in progress…"}
|
||
</div>
|
||
{detail && <div style={{ fontSize: 13, opacity: 0.75, marginTop: 2 }}>{detail}</div>}
|
||
{currentProduct && (
|
||
<div style={{ fontSize: 13, opacity: 0.75, marginTop: 2 }}>
|
||
Processing: {currentProduct.name} ({currentProduct.number}/{currentProduct.total})
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div style={{ display: "flex", gap: 8 }}>
|
||
<button onClick={() => navigate("/app/dashboard")} style={{ background: "rgba(255,255,255,0.15)", border: "1px solid rgba(255,255,255,0.3)", borderRadius: 8, color: "inherit", padding: "7px 14px", cursor: "pointer", fontWeight: 600, fontSize: 13 }}>
|
||
View Dashboard
|
||
</button>
|
||
<button onClick={checkStatus} style={{ background: "rgba(255,255,255,0.1)", border: "1px solid rgba(255,255,255,0.2)", borderRadius: 8, color: "inherit", padding: "7px 14px", cursor: "pointer", fontWeight: 600, fontSize: 13 }}>
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{progress > 0 && (
|
||
<div>
|
||
<ProgressBar progress={progress} tone={status === "error" ? "critical" : status === "done" ? "success" : "highlight"} />
|
||
<div style={{ fontSize: 12, opacity: 0.7, marginTop: 6 }}>{processedProducts} of {totalProducts} products processed</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Brands list */}
|
||
{brands.length === 0 ? (
|
||
<Card>
|
||
<div style={{ padding: 48, textAlign: "center" }}>
|
||
<div style={{ fontSize: 48, marginBottom: 12 }}>🏷️</div>
|
||
<Text as="h2" variant="headingMd">No brands selected yet</Text>
|
||
<div style={{ marginTop: 8, color: "#6b7280", marginBottom: 20 }}>Go to the Brands page to select which brands you want to manage.</div>
|
||
<Button variant="primary" onClick={() => navigate("/app/brands")}>Select Brands</Button>
|
||
</div>
|
||
</Card>
|
||
) : (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||
{brands.map((brand) => {
|
||
const isExpanded = expandedBrand === brand.id;
|
||
const isLoading = loadingMap[brand.id];
|
||
const rawItems = itemsMap[brand.id] || [];
|
||
const filteredItems = isExpanded ? applyFitmentFilters(rawItems) : [];
|
||
|
||
return (
|
||
<div key={brand.id} style={{ background: "#fff", border: `2px solid ${isExpanded ? "#2563eb" : "#e5e7eb"}`, borderRadius: 14, overflow: "hidden", boxShadow: isExpanded ? "0 4px 16px rgba(37,99,235,0.10)" : "0 1px 4px rgba(0,0,0,0.04)" }}>
|
||
{/* Brand row */}
|
||
<div style={{ display: "flex", alignItems: "center", padding: "14px 20px", gap: 16, cursor: isSubscribed ? "pointer" : "default", background: isExpanded ? "#f0f7ff" : "#fff" }} onClick={() => toggleBrandItems(brand.id)}>
|
||
<img
|
||
src={brand.logo || "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"}
|
||
alt={brand.name}
|
||
style={{ width: 48, height: 48, objectFit: "contain", borderRadius: 8, border: "1px solid #e5e7eb", background: "#f8fafc" }}
|
||
/>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontWeight: 800, fontSize: 16, color: isExpanded ? "#1d4ed8" : "#111827" }}>{brand.name}</div>
|
||
<div style={{ fontSize: 12, color: "#9ca3af" }}>ID: {brand.id}</div>
|
||
</div>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||
{rawItems.length > 0 && (
|
||
<div style={{ background: "#eff6ff", border: "1px solid #bfdbfe", borderRadius: 20, padding: "4px 12px", fontSize: 13, fontWeight: 700, color: "#1d4ed8" }}>
|
||
{rawItems.length} products
|
||
</div>
|
||
)}
|
||
{isLoading && <Spinner size="small" />}
|
||
<div style={{ width: 28, height: 28, background: isExpanded ? "#2563eb" : "#f3f4f6", borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", transition: "all 0.2s" }}>
|
||
<span style={{ color: isExpanded ? "#fff" : "#6b7280", fontSize: 12, transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)", display: "block", transition: "transform 0.2s" }}>▼</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Expanded section */}
|
||
{isExpanded && (
|
||
<div style={{ borderTop: "2px solid #dbeafe", background: "#fafcff" }}>
|
||
{isLoading ? (
|
||
<div style={{ padding: 32, textAlign: "center" }}>
|
||
<Spinner />
|
||
<div style={{ marginTop: 10, color: "#6b7280", fontSize: 13 }}>Loading products from Turn14…</div>
|
||
</div>
|
||
) : (
|
||
<div style={{ padding: "20px 20px 28px" }}>
|
||
|
||
{/* Filter panel */}
|
||
<div style={{ background: "#fff", border: "1px solid #e5e7eb", borderRadius: 12, padding: "16px 20px", marginBottom: 20 }}>
|
||
<div style={{ fontWeight: 700, fontSize: 14, color: "#374151", marginBottom: 14 }}>🔎 Fitment Filters</div>
|
||
|
||
{/* Make selector row */}
|
||
<div style={{ display: "flex", gap: 12, alignItems: "flex-end", flexWrap: "wrap", marginBottom: 14 }}>
|
||
<div style={{ minWidth: 240 }}>
|
||
<Select
|
||
label="Vehicle Make"
|
||
options={[{ label: "All Makes", value: "" }, ...makes_list.map((m) => ({ label: m, value: m }))]}
|
||
onChange={handleFilterChange("make")}
|
||
value={Array.isArray(filters.make) ? "" : filters.make}
|
||
disabled={!isSubscribed}
|
||
/>
|
||
</div>
|
||
<Popover
|
||
active={popoverActive}
|
||
activator={
|
||
<Button onClick={() => setPopoverActive((a) => !a)} disclosure disabled={!isSubscribed}>
|
||
{activeMakeLabel}
|
||
</Button>
|
||
}
|
||
onClose={() => setPopoverActive(false)}
|
||
>
|
||
<OptionList
|
||
title="Makes (multi-select)"
|
||
onChange={(selected) => setFilters((prev) => ({ ...prev, make: selected }))}
|
||
options={[{ label: "All", value: "ALL" }, ...makes_list.map((m) => ({ label: m, value: m }))]}
|
||
selected={Array.isArray(filters.make) ? filters.make : filters.make ? [filters.make] : []}
|
||
allowMultiple
|
||
/>
|
||
</Popover>
|
||
</div>
|
||
|
||
{/* Toggle pills */}
|
||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||
<FilterPill label="Include Zero Stock" checked={isFilter_EnableZeroStock} onChange={set_isFilter_EnableZeroStock} disabled={!isSubscribed} />
|
||
<FilterPill label="Regular Stock Only" checked={filterregulatstock} onChange={setfilterregulatstock} disabled={!isSubscribed} />
|
||
<FilterPill label="Include LTL Freight" checked={isFilter_IncludeLtlFreightRequired} onChange={setisFilter_IncludeLtlFreightRequired} disabled={!isSubscribed} />
|
||
<FilterPill label="Exclude Clearance" checked={isFilter_Excludeclearance_item} onChange={setisFilter_Excludeclearance_item} disabled={!isSubscribed} />
|
||
<FilterPill label="Exclude No-Air Freight" checked={isFilter_Excludeair_freight_prohibited} onChange={setisFilter_Excludeair_freight_prohibited} disabled={!isSubscribed} />
|
||
<FilterPill label="Has Images" checked={!isFilter_IncludeProductWithNoImages} onChange={(v) => setisFilter_IncludeProductWithNoImages(!v)} disabled={!isSubscribed} />
|
||
</div>
|
||
|
||
{/* Results count */}
|
||
<div style={{ marginTop: 12, fontSize: 13, color: "#6b7280" }}>
|
||
Showing <strong style={{ color: "#1d4ed8" }}>{filteredItems.length}</strong> of <strong>{rawItems.length}</strong> products
|
||
</div>
|
||
</div>
|
||
|
||
{/* Import form */}
|
||
<Form method="post" onSubmit={() => setImporting(true)}>
|
||
<input type="hidden" name="selectedProductIds" value={JSON.stringify(filteredItems.map((item) => item.id))} />
|
||
<input type="hidden" name="brandId" value={brand.id} />
|
||
<input type="hidden" name="productCount" value={String(filteredItems.length)} />
|
||
<button
|
||
type="submit"
|
||
disabled={importing || !isSubscribed || filteredItems.length === 0}
|
||
style={{ background: importing || !isSubscribed || filteredItems.length === 0 ? "#9ca3af" : "linear-gradient(135deg, #1d4ed8, #2563eb)", border: "none", borderRadius: 10, color: "#fff", padding: "13px 24px", cursor: importing || !isSubscribed || filteredItems.length === 0 ? "not-allowed" : "pointer", fontWeight: 700, fontSize: 15, display: "inline-flex", alignItems: "center", gap: 8, marginBottom: 20, width: "100%" }}
|
||
>
|
||
{importing && <Spinner size="small" />}
|
||
{importing
|
||
? "Starting import…"
|
||
: `🚀 Import ${filteredItems.length} Products from ${brand.name} to Shopify`}
|
||
</button>
|
||
</Form>
|
||
|
||
{/* Product grid */}
|
||
{filteredItems.length === 0 ? (
|
||
<div style={{ textAlign: "center", padding: 32, color: "#9ca3af" }}>
|
||
<div style={{ fontSize: 32, marginBottom: 8 }}>🔍</div>
|
||
<div style={{ fontSize: 14, fontWeight: 600 }}>No products match current filters</div>
|
||
<div style={{ fontSize: 12, marginTop: 4 }}>Try adjusting the fitment or stock filters above</div>
|
||
</div>
|
||
) : (
|
||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 14 }}>
|
||
{filteredItems.map((item) => <ProductCard key={item.id} item={item} />)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{toastActive && (
|
||
<Toast
|
||
content={status?.includes("completed") ? "Products imported successfully!" : `Import started: ${status}`}
|
||
onDismiss={() => setToastActive(false)}
|
||
/>
|
||
)}
|
||
</Page>
|
||
</Frame>
|
||
);
|
||
}
|