import { defineRouteConfig } from "@medusajs/admin-sdk"; import { ArrowDownTray, ArrowUpTray, CheckCircleSolid, XCircleSolid } from "@medusajs/icons"; import { Badge, Button, Container, Heading, Text, toast } from "@medusajs/ui"; import { useRef, useState } from "react"; import { sdk } from "../../lib/sdk"; // Importador masivo via CSV -- pensado para cargar varios productos de // una sola vez (comprados en Amazon/eBay/etc, no dropshipping // automatizado -- ver conversacion sobre por que se descarto raspar // tiendas de terceros). Las fotos se descargan del origen y se re-alojan // en el mismo MinIO/S3 del NAS que ya usa el resto del catalogo. type RowResult = { row: number; title: string; ok: boolean; error?: string; }; function downloadTemplate() { window.open("/admin/product-import", "_blank"); } const ProductImportPanel = () => { const inputRef = useRef(null); const [loading, setLoading] = useState(false); const [results, setResults] = useState(null); async function handleFile(file: File) { const csv = await file.text(); setLoading(true); setResults(null); try { const data = await sdk.client.fetch<{ results?: RowResult[]; error?: string }>("/admin/product-import", { method: "post", body: { csv }, }); if (data.error) { toast.error("No se pudo procesar el archivo", { description: data.error }); return; } setResults(data.results ?? []); const okCount = data.results?.filter((r) => r.ok).length ?? 0; const failCount = (data.results?.length ?? 0) - okCount; if (failCount === 0) { toast.success(`${okCount} productos importados`); } else { toast.warning(`${okCount} importados, ${failCount} con error`, { description: "Revisa el detalle abajo.", }); } } catch (error) { toast.error("No se pudo procesar el archivo", { description: (error as Error).message }); } finally { setLoading(false); if (inputRef.current) inputRef.current.value = ""; } } return (
Importación de productos Carga varios productos a la vez desde un archivo CSV, con fotos, descripción y precio.
{ const file = event.target.files?.[0]; if (file) handleFile(file); }} />
Columnas: titulo, descripcion, precio, categoria, coleccion, cantidad, fotos. Varias fotos se separan con ; dentro de la misma celda. categoria/coleccion deben escribirse igual que en la tienda o se dejan vacías. Cada foto se descarga y se guarda en el almacenamiento propio de la tienda (no queda enlazada al sitio de origen).
{results && (
{results.map((result) => ( ))}
Fila Producto Detalle
{result.ok ? ( ) : ( )} {result.row} {result.title} {result.ok ? ( Importado ) : ( {result.error} )}
)}
); }; export const config = defineRouteConfig({ label: "Importación", icon: ArrowUpTray, }); export default ProductImportPanel;