backend_shopify_data4autos/app/routes/app.managebrand1.jsx
metatroncubeswdev 6cb1d01b0c Inital commit
2025-06-27 18:48:53 -04:00

430 lines
14 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.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData } from "@remix-run/react";
import {
Page,
Layout,
Card,
Thumbnail,
TextContainer,
Spinner,
Button,
TextField,
Banner,
InlineError,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
// Load selected brands and access token from Shopify metafield
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 });
};
// Handle adding products for a specific brand
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);
// Fetch items from Turn14 API
const itemsRes = await fetch(
`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
}
);
const itemsData = await itemsRes.json();
function slugify(str) {
return str
.toString()
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
const items1 = Array.isArray(itemsData) ? itemsData.slice(0, productCount) : [];
const results = [];
for (const item of items1) {
const attrs = item.attributes;
// 0⃣ Build and normalize collection titles
const category = attrs.category;
const subcategory = attrs.subcategory || "";
const brand = attrs.brand;
const subcats = subcategory
.split(/[,\/]/)
.map((s) => s.trim())
.filter(Boolean);
const collectionTitles = Array.from(
new Set([category, ...subcats, brand].filter(Boolean))
);
// 1⃣ Find or create collections, collect their IDs
const collectionIds = [];
for (const title of collectionTitles) {
// lookup
const lookupRes = await admin.graphql(`
{
collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") {
nodes { id }
}
}
`);
const lookupJson = await lookupRes.json();
const existing = lookupJson.data.collections.nodes;
if (existing.length) {
collectionIds.push(existing[0].id);
} else {
// create
const createColRes = await admin.graphql(`
mutation($input: CollectionInput!) {
collectionCreate(input: $input) {
collection { id }
userErrors { field message }
}
}
`, { variables: { input: { title } } });
const createColJson = await createColRes.json();
const errs = createColJson.data.collectionCreate.userErrors;
if (errs.length) {
throw new Error(`Could not create collection "${title}": ${errs.map(e => e.message).join(", ")}`);
}
collectionIds.push(createColJson.data.collectionCreate.collection.id);
}
}
// 2⃣ Build tags
const tags = [
attrs.category,
...subcats,
attrs.brand,
attrs.part_number,
attrs.mfr_part_number,
attrs.price_group,
attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
attrs.barcode
].filter(Boolean).map((t) => t.trim());
// 3⃣ Prepare media inputs
const mediaInputs = (attrs.files || [])
.filter((f) => f.type === "Image" && f.url)
.map((file) => ({
originalSource: file.url,
mediaContentType: "IMAGE",
alt: `${attrs.product_name}${file.media_content}`,
}));
// 2⃣ Pick the longest “Market Description” or fallback to part_description
const marketDescs = (attrs.descriptions || [])
.filter((d) => d.type === "Market Description")
.map((d) => d.description);
const descriptionHtml = marketDescs.length
? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
: attrs.part_description;
// 4⃣ Create product + attach to collections + add media
const createProdRes = await admin.graphql(`
mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
productCreate(product: $prod, media: $media) {
product {
id
variants(first: 1) {
nodes { id inventoryItem { id } }
}
}
userErrors { field message }
}
}
`, {
variables: {
prod: {
title: attrs.product_name,
descriptionHtml: descriptionHtml,
vendor: attrs.brand,
productType: attrs.category,
handle: slugify(attrs.part_number || attrs.product_name),
tags,
collectionsToJoin: collectionIds,
status: "ACTIVE",
},
media: mediaInputs,
},
});
const createProdJson = await createProdRes.json();
const prodErrs = createProdJson.data.productCreate.userErrors;
if (prodErrs.length) {
const taken = prodErrs.some((e) => /already in use/i.test(e.message));
if (taken) {
results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
continue;
}
throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
}
const product = createProdJson.data.productCreate.product;
const variantNode = product.variants.nodes[0];
const variantId = variantNode.id;
const inventoryItemId = variantNode.inventoryItem.id;
// 5⃣ Bulk-update variant (price, compare-at, barcode)
const price = parseFloat(attrs.price) || 1000;
const comparePrice = parseFloat(attrs.compare_price) || null;
const barcode = attrs.barcode || "";
const bulkRes = await admin.graphql(`
mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants { id price compareAtPrice barcode }
userErrors { field message }
}
}
`, {
variables: {
productId: product.id,
variants: [{
id: variantId,
price,
...(comparePrice !== null && { compareAtPrice: comparePrice }),
...(barcode && { barcode }),
}],
},
});
const bulkJson = await bulkRes.json();
const bulkErrs = bulkJson.data.productVariantsBulkUpdate.userErrors;
if (bulkErrs.length) {
throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(", ")}`);
}
const updatedVariant = bulkJson.data.productVariantsBulkUpdate.productVariants[0];
// 6⃣ Update inventory item (SKU, cost & weight)
const costPerItem = parseFloat(attrs.purchase_cost) || 0;
const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
const invRes = await admin.graphql(`
mutation($id: ID!, $input: InventoryItemInput!) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
sku
measurement {
weight { value unit }
}
}
userErrors { field message }
}
}
`, {
variables: {
id: inventoryItemId,
input: {
sku: attrs.part_number,
cost: costPerItem,
measurement: {
weight: { value: weightValue, unit: "POUNDS" }
},
},
},
});
const invJson = await invRes.json();
const invErrs = invJson.data.inventoryItemUpdate.userErrors;
if (invErrs.length) {
throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
}
const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
// 7⃣ Collect results
results.push({
productId: product.id,
variant: {
id: updatedVariant.id,
price: updatedVariant.price,
compareAtPrice: updatedVariant.compareAtPrice,
sku: inventoryItem.sku,
barcode: updatedVariant.barcode,
weight: inventoryItem.measurement.weight.value,
weightUnit: inventoryItem.measurement.weight.unit,
},
collections: collectionTitles,
tags,
});
}
return json({ success: true, results });
};
// Main React component for managing brand products
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 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/brand/${brandId}?page=1`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const data = await res.json();
setItemsMap((prev) => ({ ...prev, [brandId]: data }));
} catch (err) {
console.error("Error fetching items:", err);
}
setLoadingMap((prev) => ({ ...prev, [brandId]: false }));
}
}
};
return (
<Page title="Manage Brand Products">
<Layout>
{brands.length === 0 && (
<Layout.Section>
<Card sectioned>
<p>No brands selected yet.</p>
</Card>
</Layout.Section>
)}
{brands.map((brand) => (
<React.Fragment key={brand.id}>
<Layout.Section oneThird>
<Card title={brand.name} sectioned>
<Thumbnail
source={
brand.logo ||
"https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"
}
alt={brand.name}
size="large"
/>
<TextContainer spacing="tight">
<p><strong>ID:</strong> {brand.id}</p>
</TextContainer>
<Button fullWidth onClick={() => toggleBrandItems(brand.id)}>
{expandedBrand === brand.id ? "Hide Products" : "Show Products"}
</Button>
</Card>
</Layout.Section>
{expandedBrand === brand.id && (
<Layout.Section fullWidth>
<Card sectioned>
{actionData?.success && (
<Banner title="✅ Product created!" status="success">
<p>
{actionData.results.map((r) => (
<span key={r.variant.id}>
Product {r.productId} Variant {r.variant.id} @ ${r.variant.price} (SKU: {r.variant.sku})<br />
</span>
))}
</p>
</Banner>
)}
<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={setProductCount}
autoComplete="off"
/>
<Button submit primary style={{ marginTop: '1rem' }}>
Add First {productCount} Products to Store
</Button>
</Form>
</Card>
<Card title={`Items from ${brand.name}`} sectioned>
{loadingMap[brand.id] ? (
<Spinner accessibilityLabel="Loading items" size="small" />
) : (
<div style={{ paddingTop: "1rem" }}>
{(itemsMap[brand.id] || []).map((item) => (
<Card key={item.id} title={item.attributes.product_name} 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}
size="large"
/>
</Layout.Section>
<Layout.Section>
<TextContainer spacing="tight">
<p><strong>Part Number:</strong> {item.attributes.part_number}</p>
<p><strong>Category:</strong> {item.attributes.category} &gt; {item.attributes.subcategory}</p>
</TextContainer>
</Layout.Section>
</Layout>
</Card>
))}
</div>
)}
</Card>
</Layout.Section>
)}
</React.Fragment>
))}
</Layout>
</Page>
);
}