backend_shopify_data4autos/app/routes/app.managebrand copy 2.jsx
2025-08-07 06:22:27 +00:00

376 lines
14 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,
ProgressBar,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { TitleBar } from "@shopify/app-bridge-react";
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);
}
return json({ brands, accessToken });
};
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 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
}),
});
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 { 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("");
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/brandallitems/${brandId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const data = await res.json();
const validItems = Array.isArray(data)
? data.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;
return (
<Frame>
<Page title="Data4Autos Turn14 Manage Brand Products">
<TitleBar title="Data4Autos Turn14 Integration" />
<Layout>
{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: "Brand ID" },
{ title: "Logo" },
{ title: "Action" },
{ title: "Products Count" },
]}
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>
<Thumbnail
source={
brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="small"
/>
</IndexTable.Cell>
<IndexTable.Cell>
<Button onClick={() => toggleBrandItems(brand.id)}>
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button>
</IndexTable.Cell>
<IndexTable.Cell>{itemsMap[brand.id]?.length || 0}</IndexTable.Cell>
</IndexTable.Row>
))}
</IndexTable>
</Card>
</Layout.Section>
)}
{brands.map(
(brand) =>
expandedBrand === brand.id && (
<Layout.Section fullWidth key={brand.id + "-expanded"}>
<Card sectioned>
{processId && (
<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}
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="brandId" value={brand.id} />
<TextField
label="Number of products to add"
type="number"
name="productCount"
value={productCount}
onChange={(value) => setProductCount(value)}
autoComplete="off"
/>
<Button
submit
primary
style={{ marginTop: "1rem" }}
loading={status?.includes("processing")}
>
Add First {productCount} Products to Store
</Button>
</Form>
{(
itemsMap[brand.id] && itemsMap[brand.id].length > 0
? itemsMap[brand.id].filter(item => item && item.id)
: []
).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>
)}
</Card>
</Layout.Section>
)
)}
</Layout>
{toastMarkup}
</Page>
</Frame>
);
}