From 2b15fd505e5b01a3c65ac3f309a6e8269764dbaf Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Sat, 13 Jun 2026 18:48:12 +1000 Subject: [PATCH] Phase 3: sync active Etsy listings into the product catalog - syncListings pages active listings, fetches each listing's inventory, and upserts Products (one per size variation, collapsing colour). Title/description/price/tags/SKU/quantity and etsyListingId are updated; printingCost/costOfGoods are preserved (user-owned). Old titles are kept as aliases so historical order matching still works. - POST /api/etsy/sync-catalog endpoint; Settings gains a 'Sync Catalog' button that refreshes products after running. - Uses existing listings_r scope (no reconnect needed). Co-Authored-By: Claude Fable 5 --- client/src/pages/Settings.tsx | 28 ++++- server/src/routes/etsy.ts | 19 ++++ server/src/services/etsyApi.ts | 188 +++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 1 deletion(-) diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index a376643..d1b49a7 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -2,8 +2,9 @@ import { useState, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { DataManager } from '../utils/dataManager'; import { setOrders } from '../store/slices/orderSlice'; +import { setProducts } from '../store/slices/productSlice'; import { MissingProductsModal } from '../components/MissingProductsModal'; -import { Trash2, Download, RefreshCw, AlertTriangle, Database, Store, Link2, Unlink, Wrench } from 'lucide-react'; +import { Trash2, Download, RefreshCw, AlertTriangle, Database, Store, Link2, Unlink, Wrench, Package } from 'lucide-react'; import toast from 'react-hot-toast'; import api from '../utils/api'; @@ -40,6 +41,7 @@ const Settings = () => { const [isSavingConfig, setIsSavingConfig] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isSyncing, setIsSyncing] = useState(false); + const [isSyncingCatalog, setIsSyncingCatalog] = useState(false); const [unmatchedItems, setUnmatchedItems] = useState([]); const [showResolveModal, setShowResolveModal] = useState(false); const [unknownDebits, setUnknownDebits] = useState([]); @@ -165,6 +167,21 @@ const Settings = () => { } }; + const handleSyncCatalog = async () => { + setIsSyncingCatalog(true); + try { + const res = await api.post('/etsy/sync-catalog'); + const { listingsSeen, productsCreated, productsUpdated } = res.data; + toast.success(`Catalog synced: ${listingsSeen} listings → ${productsCreated} new, ${productsUpdated} updated products`); + const productsRes = await api.get('/products?limit=1000'); + dispatch(setProducts(productsRes.data.products)); + } catch (error: any) { + toast.error(error.response?.data?.message || 'Catalog sync failed'); + } finally { + setIsSyncingCatalog(false); + } + }; + const handleEtsyDisconnect = async () => { if (!window.confirm('Disconnect your Etsy shop? Synced data stays; only the connection is removed.')) return; try { @@ -245,6 +262,15 @@ const Settings = () => { {isSyncing ? 'Syncing...' : 'Sync Orders Now'} +