Data4Autos-Shopify-Frontend/app/routes/app.managebrand_2408.jsx
2025-08-29 02:47:12 +00:00

736 lines
25 KiB
JavaScript

import React, { useEffect, useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData } from "@remix-run/react";
import {
Page,
Layout,
IndexTable,
Card,
Thumbnail,
TextContainer,
Spinner,
Button,
TextField,
Banner,
InlineError,
Toast,
Frame,
Select,
ProgressBar,
Checkbox,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { TitleBar } from "@shopify/app-bridge-react";
async function checkShopExists(shop) {
try {
const resp = await fetch(
`https://backend.data4autos.com/checkisshopdataexists/${shop}`
);
const data = await resp.json();
return data.status === 1; // ✅ true if shop exists, false otherwise
} catch (err) {
console.error("Error checking shop:", err);
return false; // default to false if error
}
}
export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
const accessToken = await getTurn14AccessTokenFromMetafield(request);
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);
}
const { session } = await authenticate.admin(request);
const shop = session.shop;
return json({ brands, accessToken, shop });
};
const makes_list_raw = [
'Alfa Romeo',
'Ferrari',
'Dodge',
'Subaru',
'Toyota',
'Volkswagen',
'Volvo',
'Audi',
'BMW',
'Buick',
'Cadillac',
'Chevrolet',
'Chrysler',
'CX Automotive',
'Nissan',
'Ford',
'Hyundai',
'Infiniti',
'Lexus',
'Mercury',
'Mazda',
'Oldsmobile',
'Plymouth',
'Pontiac',
'Rolls-Royce',
'Eagle',
'Lincoln',
'Mercedes-Benz',
'GMC',
'Saab',
'Honda',
'Saturn',
'Mitsubishi',
'Isuzu',
'Jeep',
'AM General',
'Geo',
'Suzuki',
'E. P. Dutton, Inc.',
'Land Rover',
'PAS, Inc',
'Acura',
'Jaguar',
'Lotus',
'Grumman Olson',
'Porsche',
'American Motors Corporation',
'Kia',
'Lamborghini',
'Panoz Auto-Development',
'Maserati',
'Saleen',
'Aston Martin',
'Dabryan Coach Builders Inc',
'Federal Coach',
'Vector',
'Bentley',
'Daewoo',
'Qvale',
'Roush Performance',
'Autokraft Limited',
'Bertone',
'Panther Car Company Limited',
'Texas Coach Company',
'TVR Engineering Ltd',
'Morgan',
'MINI',
'Yugo',
'BMW Alpina',
'Renault',
'Bitter Gmbh and Co. Kg',
'Scion',
'Maybach',
'Lambda Control Systems',
'Merkur',
'Peugeot',
'Spyker',
'London Coach Co Inc',
'Hummer',
'Bugatti',
'Pininfarina',
'Shelby',
'Saleen Performance',
'smart',
'Tecstar, LP',
'Kenyon Corporation Of America',
'Avanti Motor Corporation',
'Bill Dovell Motor Car Company',
'Import Foreign Auto Sales Inc',
'S and S Coach Company E.p. Dutton',
'Superior Coaches Div E.p. Dutton',
'Vixen Motor Company',
'Volga Associated Automobile',
'Wallace Environmental',
'Import Trade Services',
'J.K. Motors',
'Panos',
'Quantum Technologies',
'London Taxi',
'Red Shift Ltd.',
'Ruf Automobile Gmbh',
'Excalibur Autos',
'Mahindra',
'VPG',
'Fiat',
'Sterling',
'Azure Dynamics',
'McLaren Automotive',
'Ram',
'CODA Automotive',
'Fisker',
'Tesla',
'Mcevoy Motors',
'BYD',
'ASC Incorporated',
'SRT',
'CCC Engineering',
'Mobility Ventures LLC',
'Pagani',
'Genesis',
'Karma',
'Koenigsegg',
'Aurora Cars Ltd',
'RUF Automobile',
'Dacia',
'STI',
'Daihatsu',
'Polestar',
'Kandi',
'Rivian',
'Lucid',
'JBA Motorcars, Inc.',
'Lordstown',
'Vinfast',
'INEOS Automotive',
'Bugatti Rimac',
'Grumman Allied Industries',
'Environmental Rsch and Devp Corp',
'Evans Automobiles',
'Laforza Automobile Inc',
'General Motors',
'Consulier Industries Inc',
'Goldacre',
'Isis Imports Ltd',
'PAS Inc - GMC'
];
const makes_list = makes_list_raw.sort();
export const action = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const formData = await request.formData();
const brandId = formData.get("brandId");
const rawCount = formData.get("productCount");
const selectedProductIds = JSON.parse(formData.get("selectedProductIds") || "[]");
const productCount = parseInt(rawCount, 10) || 10;
const { getTurn14AccessTokenFromMetafield } = await import("../utils/turn14Token.server");
const accessToken = await getTurn14AccessTokenFromMetafield(request);
const { session } = await authenticate.admin(request);
const shop = session.shop;
const resp = await fetch("https://backend.data4autos.com/manageProducts", {
method: "POST",
headers: {
"Content-Type": "application/json",
"shop-domain": shop,
},
body: JSON.stringify({
shop,
brandID: brandId,
turn14accessToken: accessToken,
productCount,
selectedProductIds
}),
});
console.log("Response from manageProducts:", resp.status, resp.statusText);
if (!resp.ok) {
const err = await resp.text();
return json({ error: err }, { status: resp.status });
}
const { processId, status } = await resp.json();
console.log("Process ID:", processId, "Status:", status);
return json({ success: true, processId, status });
};
export default function ManageBrandProducts() {
const actionData = useActionData();
const { shop, brands, accessToken } = useLoaderData();
const [expandedBrand, setExpandedBrand] = useState(null);
const [itemsMap, setItemsMap] = useState({});
const [loadingMap, setLoadingMap] = useState({});
const [productCount, setProductCount] = useState("10");
const [initialLoad, setInitialLoad] = useState(true);
const [toastActive, setToastActive] = useState(false);
const [polling, setPolling] = useState(false);
const [status, setStatus] = useState(actionData?.status || "");
const [processId, setProcessId] = useState(actionData?.processId || null);
const [progress, setProgress] = useState(0);
const [totalProducts, setTotalProducts] = useState(0);
const [processedProducts, setProcessedProducts] = useState(0);
const [currentProduct, setCurrentProduct] = useState(null);
const [results, setResults] = useState([]);
const [detail, setDetail] = useState("");
const [filterregulatstock, setfilterregulatstock] = useState(false)
const [Turn14Enabled, setTurn14Enabled] = useState("12345"); // null | true | false
useEffect(() => {
if (!shop) {
console.log("⚠️ shop is undefined or empty");
return;
}
(async () => {
const result = await checkShopExists(shop);
console.log("✅ API status result:", result, "| shop:", shop);
setTurn14Enabled(result);
})();
}, [shop]);
useEffect(() => {
if (actionData?.processId) {
setProcessId(actionData.processId);
setStatus(actionData.status || "processing");
setToastActive(true);
}
}, [actionData]);
const checkStatus = async () => {
setPolling(true);
try {
const response = await fetch(`https://backend.data4autos.com/manageProducts/status/${processId}`);
const data = await response.json();
setStatus(data.status);
setDetail(data.detail);
setProgress(data.progress);
setTotalProducts(data.stats.total);
setProcessedProducts(data.stats.processed);
setCurrentProduct(data.current);
if (data.results) {
setResults(data.results);
}
// Continue polling if still processing
if (data.status !== 'done' && data.status !== 'error') {
setTimeout(checkStatus, 2000);
} else {
setPolling(false);
}
} catch (error) {
setPolling(false);
setStatus('error');
setDetail('Failed to check status');
console.error('Error checking status:', error);
}
};
useEffect(() => {
let interval;
if (status?.includes("processing") && processId) {
interval = setInterval(checkStatus, 5000);
}
return () => clearInterval(interval);
}, [status, processId]);
const toggleAllBrands = async () => {
for (const brand of brands) {
await toggleBrandItems(brand.id);
}
};
useEffect(() => {
if (initialLoad && brands.length > 0) {
toggleAllBrands();
setInitialLoad(false);
}
}, [brands, initialLoad]);
const toggleBrandItems = async (brandId) => {
const isExpanded = expandedBrand === brandId;
if (isExpanded) {
setExpandedBrand(null);
} else {
setExpandedBrand(brandId);
if (!itemsMap[brandId]) {
setLoadingMap((prev) => ({ ...prev, [brandId]: true }));
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const data = await res.json();
const dataitems = data.items
const validItems = Array.isArray(dataitems)
? dataitems.filter(item => item && item.id && item.attributes)
: [];
setItemsMap((prev) => ({ ...prev, [brandId]: validItems }));
} catch (err) {
console.error("Error fetching items:", err);
setItemsMap((prev) => ({ ...prev, [brandId]: [] }));
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
}
};
const toastMarkup = toastActive ? (
<Toast
content={status.includes("completed") ?
"Products imported successfully!" :
`Status: ${status}`}
onDismiss={() => setToastActive(false)}
/>
) : null;
const [filters, setFilters] = useState({ make: '', model: '', year: '', drive: '', baseModel: '' });
const handleFilterChange = (field) => (value) => {
setFilters((prev) => ({ ...prev, [field]: value }));
};
const applyFitmentFilters = (items) => {
return items.filter((item) => {
const tags = item?.attributes?.fitmmentTags || {};
const productName = item?.attributes?.product_name || '';
const brand = item?.attributes?.brand || '';
const partDescription = item?.attributes?.part_description || '';
const descriptions = item?.attributes?.descriptions || [];
// // Logging the item being checked and the filters
// console.log("Checking item:", item.id); // Log the item's ID or some unique identifier
// console.log("Filters being applied:", filters);
// // Log the values for each field being checked
// console.log("Checking tags:", tags);
// console.log("Checking product name:", productName);
// console.log("Checking brand:", brand);
// console.log("Checking part description:", partDescription);
// console.log("Checking descriptions:", descriptions.map((desc) => desc.description));
// Create the result for each check
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));
// console.log(`Make check result: ${makeMatch}`);
const modelMatch = !filters.model || tags.model?.includes(filters.model) || productName.includes(filters.model) || brand.includes(filters.model) || descriptions.some((desc) => desc.description.includes(filters.model));
// console.log(`Model check result: ${modelMatch}`);
const yearMatch = !filters.year || tags.year?.includes(filters.year) || productName.includes(filters.year) || brand.includes(filters.year) || descriptions.some((desc) => desc.description.includes(filters.year));
/// console.log(`Year check result: ${yearMatch}`);
const driveMatch = !filters.drive || tags.drive?.includes(filters.drive) || productName.includes(filters.drive) || brand.includes(filters.drive) || descriptions.some((desc) => desc.description.includes(filters.drive));
// console.log(`Drive check result: ${driveMatch}`);
const baseModelMatch = !filters.baseModel || tags.baseModel?.includes(filters.baseModel) || productName.includes(filters.baseModel) || brand.includes(filters.baseModel) || descriptions.some((desc) => desc.description.includes(filters.baseModel));
// console.log(`Base Model check result: ${baseModelMatch}`);
// Combine all the conditions
var isMatch = makeMatch && modelMatch && yearMatch && driveMatch && baseModelMatch// && item.attributes.regular_stock
if (filterregulatstock) {
isMatch = isMatch && item?.attributes?.regular_stock
}
// Log the result of the check (whether item matches the filter or not)
// console.log(`Item ${item.id} match: ${isMatch}`);
// Return the item if it matches
return isMatch;
});
};
// const applyFitmentFilters = (items) => {
// return items.filter((item) => {
// const tags = item?.attributes?.fitmmentTags || {};
// return (
// (!filters.make || tags.make?.includes(filters.make)) &&
// (!filters.model || tags.model?.includes(filters.model)) &&
// (!filters.year || tags.year?.includes(filters.year)) &&
// (!filters.drive || tags.drive?.includes(filters.drive)) &&
// (!filters.baseModel || tags.baseModel?.includes(filters.baseModel))
// );
// });
// };
const selectedProductIds = []
return (
<Frame>
<Page title="Data4Autos Turn14 Manage Brand Products" fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
<p>
<strong>Turn 14 Status:</strong> {Turn14Enabled}
</p>
{brands.length === 0 ? (
<Layout.Section>
<Card sectioned>
<p>No brands selected yet.</p>
</Card>
</Layout.Section>
) : (
<Layout.Section>
<Card>
<IndexTable
resourceName={{ singular: "brand", plural: "brands" }}
itemCount={brands.length}
headings={[
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand ID</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Name</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Brand Logo</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Action</div> },
{ title: <div style={{ fontWeight: 600, fontSize: "16px", background: "#f4f6f8", padding: "15px 8px", borderRadius: "4px" }}>Products Count</div> },
]}
selectable={false}
>
{brands.map((brand, index) => {
return (
<IndexTable.Row id={brand.id.toString()} key={brand.id} position={index}>
<IndexTable.Cell>{brand.id}</IndexTable.Cell>
<IndexTable.Cell>{brand.name}</IndexTable.Cell>
<IndexTable.Cell>
<Thumbnail
source={
brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="medium"
/>
</IndexTable.Cell>
<IndexTable.Cell>
<Button onClick={() => toggleBrandItems(brand.id)} variant="primary">
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button>
</IndexTable.Cell>
<IndexTable.Cell>
<span
style={{
display: "inline-block",
background: "#00d1ff29", // light teal background
color: "#00d1ff", // dark teal text
padding: "4px 8px",
borderRadius: "12px",
fontWeight: "600",
fontSize: "14px",
minWidth: "28px",
textAlign: "center"
}}
>
{itemsMap[brand.id]?.length || 0}
</span>
</IndexTable.Cell>
</IndexTable.Row>
)
})}
</IndexTable>
</Card>
</Layout.Section>
)}
{brands.map((brand) => {
const filteredItems = applyFitmentFilters(itemsMap[brand.id] || []);
// console.log("Filtered items for brand", brand.id, ":", filteredItems.map((item) => item.id));
const uniqueTags = {
make: new Set(),
model: new Set(),
year: new Set(),
drive: new Set(),
baseModel: new Set(),
};
(itemsMap[brand.id] || []).forEach(item => {
const tags = item?.attributes?.fitmmentTags || {};
Object.keys(uniqueTags).forEach(key => {
(tags[key] || []).forEach(val => uniqueTags[key].add(val));
});
});
return (
expandedBrand === brand.id &&
(
<Layout.Section fullWidth key={brand.id + "-expanded"}>
{processId && (
<Card sectioned>
<div style={{ marginBottom: "1rem", padding: "1rem", border: "1px solid #e1e1e1", borderRadius: "4px" }}>
<p>
<strong>Process ID:</strong> {processId}
</p>
<div style={{ margin: "1rem 0" }}>
<p>
<strong>Status:</strong> {status || ""}
</p>
{progress > 0 && (
<div style={{ marginTop: "0.5rem" }}>
<ProgressBar
progress={progress}
color={
status === 'error' ? 'critical' :
status === 'done' ? 'success' : 'highlight'
}
/>
<p style={{ marginTop: "0.25rem", fontSize: "0.85rem" }}>
{processedProducts} of {totalProducts} products processed
{currentProduct && ` - Current: ${currentProduct.name} (${currentProduct.number}/${currentProduct.total})`}
</p>
</div>
)}
</div>
{status === 'done' && results.length > 0 && (
<div style={{ marginTop: "1rem" }}>
<p>
<strong>Results:</strong> {results.length} products processed successfully
</p>
</div>
)}
{status === 'error' && (
<div style={{ marginTop: "1rem", color: "#ff4d4f" }}>
<strong>Error:</strong> {detail}
</div>
)}
<Button
onClick={checkStatus}
loading={polling} variant="primary" size="large"
style={{ marginTop: "1rem" }}
>
{status === 'done' ? 'View Results' : 'Check Status'}
</Button>
</div>
</Card>
)}
<Card title={`Items from ${brand.name}`} sectioned>
{loadingMap[brand.id] ? (
<Spinner accessibilityLabel="Loading items" size="small" />
) : (
<div style={{ paddingTop: "1rem" }}>
<Form method="post">
<input
type="hidden"
name="selectedProductIds"
value={JSON.stringify(filteredItems.map((item) => item.id))}
/>
<input type="hidden" name="brandId" value={brand.id} />
<div style={{ display: "flex", gap: "1rem", alignItems: "end" }}>
<TextField
label="Number of products in Selected Filter Make"
type="number"
name="productCount"
value={filteredItems.length}
onChange={(value) => setProductCount(value)}
autoComplete="off"
/>
<Checkbox
label="Filter Only the Regular Stock"
checked={filterregulatstock}
onChange={() => { setfilterregulatstock(!filterregulatstock) }}
/>
<Button
submit
primary variant="primary" size="large"
style={{ marginTop: "1rem" }}
loading={status?.includes("processing")}
>
Add First {filteredItems.length} Products from {filters.make} to Store
</Button>
</div>
</Form>
<div style={{ padding: "20px 0px" }}>
<Card title="Filter Products by Fitment Tags" sectioned >
<Layout>
<Layout.Section oneThird>
<Select
label="Make"
options={[{ label: 'All', value: '' }, ...Array.from(makes_list).map(m => ({ label: m, value: m }))]}
onChange={handleFilterChange('make')}
value={filters.make}
/>
</Layout.Section>
</Layout>
</Card>
</div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: 16,
}}>
{filteredItems.map((item) => (
<Card key={item.id} title={item?.attributes?.product_name || 'Untitled Product'} sectioned>
<Layout>
<Layout.Section oneThird>
<Thumbnail
source={
item?.attributes?.thumbnail ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={item?.attributes?.product_name || 'Product image'}
size="large"
/>
</Layout.Section>
<Layout.Section>
<TextContainer spacing="tight">
<p><strong>Part Number:</strong> {item?.attributes?.part_number || 'N/A'}</p>
<p><strong>Category:</strong> {item?.attributes?.category || 'N/A'} &gt; {item?.attributes?.subcategory || 'N/A'}</p>
<p><strong>Price:</strong> ${item?.attributes?.price || '0.00'}</p>
<p><strong>Description:</strong> {item?.attributes?.part_description || 'No description available'}</p>
</TextContainer>
</Layout.Section>
</Layout>
</Card>
))}
</div>
</div>
)}
</Card>
</Layout.Section>
)
)
})}
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}