Data4Autos-Shopify-Frontend/app/routes/app.managebrand.jsx
MOHAN eefb2e612a fix: remove duplicate session/shop declarations in managebrand action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:53:35 +05:30

681 lines
37 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData, useNavigate } from "@remix-run/react";
import {
Page,
Layout,
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>
);
}