@jytextiles/medusa-plugin-faire-store-sync 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/__tests__/ingest-faire-order-support.spec.js +74 -0
- package/.medusa/server/src/__tests__/webhook.spec.js +77 -0
- package/.medusa/server/src/admin/index.js +960 -0
- package/.medusa/server/src/admin/index.mjs +959 -0
- package/.medusa/server/src/api/admin/faire/auth/authorize/route.js +12 -0
- package/.medusa/server/src/api/admin/faire/auth/callback/route.js +25 -0
- package/.medusa/server/src/api/admin/faire/auth/disconnect/route.js +12 -0
- package/.medusa/server/src/api/admin/faire/brand/route.js +14 -0
- package/.medusa/server/src/api/admin/faire/ingest/orders/[batchId]/route.js +27 -0
- package/.medusa/server/src/api/admin/faire/ingest/orders/route.js +20 -0
- package/.medusa/server/src/api/admin/faire/products/route.js +16 -0
- package/.medusa/server/src/api/admin/faire/settings/route.js +19 -0
- package/.medusa/server/src/api/admin/faire/status/product/[id]/route.js +30 -0
- package/.medusa/server/src/api/admin/faire/status/route.js +46 -0
- package/.medusa/server/src/api/admin/faire/sync/bulk/[batchId]/route.js +29 -0
- package/.medusa/server/src/api/admin/faire/sync/bulk/route.js +20 -0
- package/.medusa/server/src/api/admin/faire/sync/product/[id]/route.js +14 -0
- package/.medusa/server/src/api/admin/faire/syncs/[id]/route.js +32 -0
- package/.medusa/server/src/api/admin/faire/syncs/route.js +18 -0
- package/.medusa/server/src/api/webhooks/faire/route.js +125 -0
- package/.medusa/server/src/jobs/pull-faire-orders.js +32 -0
- package/.medusa/server/src/jobs/refresh-faire-token.js +25 -0
- package/.medusa/server/src/lib/faire-client.js +321 -0
- package/.medusa/server/src/lib/types.js +10 -0
- package/.medusa/server/src/lib/webhook.js +58 -0
- package/.medusa/server/src/links/product-faire-sync-link.js +57 -0
- package/.medusa/server/src/modules/faire-sync/index.js +15 -0
- package/.medusa/server/src/modules/faire-sync/migrations/Migration20260707211346.js +34 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-order.js +21 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-account.js +17 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-batch.js +18 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-record.js +22 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-settings.js +26 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-webhook-event.js +21 -0
- package/.medusa/server/src/modules/faire-sync/service.js +222 -0
- package/.medusa/server/src/subscribers/faire-inventory-sync.js +87 -0
- package/.medusa/server/src/subscribers/faire-listing-events.js +46 -0
- package/.medusa/server/src/subscribers/faire-order-ingest.js +54 -0
- package/.medusa/server/src/subscribers/faire-order-lifecycle.js +71 -0
- package/.medusa/server/src/subscribers/faire-product-status-sync.js +71 -0
- package/.medusa/server/src/workflows/index.js +21 -0
- package/.medusa/server/src/workflows/ingest-faire-order-support.js +76 -0
- package/.medusa/server/src/workflows/ingest-faire-order.js +148 -0
- package/.medusa/server/src/workflows/ingest-faire-orders-bulk.js +124 -0
- package/.medusa/server/src/workflows/refresh-faire-token.js +20 -0
- package/.medusa/server/src/workflows/sync-product-to-faire.js +262 -0
- package/.medusa/server/src/workflows/sync-products-to-faire.js +90 -0
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/package.json +84 -0
- package/scripts/postbuild.js +36 -0
- package/src/__tests__/ingest-faire-order-support.spec.ts +77 -0
- package/src/__tests__/webhook.spec.ts +81 -0
- package/src/admin/lib/api.ts +88 -0
- package/src/admin/routes/settings/faire/[id]/page.tsx +184 -0
- package/src/admin/routes/settings/faire/hooks/use-faire-sync-columns.tsx +80 -0
- package/src/admin/routes/settings/faire/page.tsx +445 -0
- package/src/admin/routes/settings/faire/settings/page.tsx +286 -0
- package/src/admin/routes/settings/oauth/faire/callback/page.tsx +67 -0
- package/src/admin/widgets/faire-product-widget.tsx +180 -0
- package/src/api/admin/faire/auth/authorize/route.ts +10 -0
- package/src/api/admin/faire/auth/callback/route.ts +29 -0
- package/src/api/admin/faire/auth/disconnect/route.ts +10 -0
- package/src/api/admin/faire/brand/route.ts +12 -0
- package/src/api/admin/faire/ingest/orders/[batchId]/route.ts +31 -0
- package/src/api/admin/faire/ingest/orders/route.ts +29 -0
- package/src/api/admin/faire/products/route.ts +14 -0
- package/src/api/admin/faire/settings/route.ts +17 -0
- package/src/api/admin/faire/status/product/[id]/route.ts +30 -0
- package/src/api/admin/faire/status/route.ts +47 -0
- package/src/api/admin/faire/sync/bulk/[batchId]/route.ts +33 -0
- package/src/api/admin/faire/sync/bulk/route.ts +27 -0
- package/src/api/admin/faire/sync/product/[id]/route.ts +13 -0
- package/src/api/admin/faire/syncs/[id]/route.ts +32 -0
- package/src/api/admin/faire/syncs/route.ts +17 -0
- package/src/api/webhooks/faire/route.ts +133 -0
- package/src/jobs/pull-faire-orders.ts +30 -0
- package/src/jobs/refresh-faire-token.ts +23 -0
- package/src/lib/faire-client.ts +436 -0
- package/src/lib/types.ts +117 -0
- package/src/lib/webhook.ts +85 -0
- package/src/links/product-faire-sync-link.ts +56 -0
- package/src/modules/faire-sync/index.ts +11 -0
- package/src/modules/faire-sync/migrations/.snapshot-medusa-faire-sync.json +1486 -0
- package/src/modules/faire-sync/migrations/Migration20260707211346.ts +43 -0
- package/src/modules/faire-sync/models/faire-order.ts +20 -0
- package/src/modules/faire-sync/models/faire-sync-account.ts +16 -0
- package/src/modules/faire-sync/models/faire-sync-batch.ts +17 -0
- package/src/modules/faire-sync/models/faire-sync-record.ts +21 -0
- package/src/modules/faire-sync/models/faire-sync-settings.ts +25 -0
- package/src/modules/faire-sync/models/faire-webhook-event.ts +20 -0
- package/src/modules/faire-sync/service.ts +252 -0
- package/src/subscribers/faire-inventory-sync.ts +97 -0
- package/src/subscribers/faire-listing-events.ts +48 -0
- package/src/subscribers/faire-order-ingest.ts +61 -0
- package/src/subscribers/faire-order-lifecycle.ts +81 -0
- package/src/subscribers/faire-product-status-sync.ts +76 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/ingest-faire-order-support.ts +96 -0
- package/src/workflows/ingest-faire-order.ts +212 -0
- package/src/workflows/ingest-faire-orders-bulk.ts +168 -0
- package/src/workflows/refresh-faire-token.ts +28 -0
- package/src/workflows/sync-product-to-faire.ts +375 -0
- package/src/workflows/sync-products-to-faire.ts +133 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
|
2
|
+
import { BuildingStorefront } from "@medusajs/icons"
|
|
3
|
+
import {
|
|
4
|
+
Alert,
|
|
5
|
+
Button,
|
|
6
|
+
Container,
|
|
7
|
+
DataTable,
|
|
8
|
+
DataTableFilteringState,
|
|
9
|
+
DataTablePaginationState,
|
|
10
|
+
Heading,
|
|
11
|
+
Label,
|
|
12
|
+
Skeleton,
|
|
13
|
+
StatusBadge,
|
|
14
|
+
Text,
|
|
15
|
+
Toaster,
|
|
16
|
+
toast,
|
|
17
|
+
createDataTableFilterHelper,
|
|
18
|
+
useDataTable,
|
|
19
|
+
} from "@medusajs/ui"
|
|
20
|
+
import {
|
|
21
|
+
keepPreviousData,
|
|
22
|
+
useMutation,
|
|
23
|
+
useQuery,
|
|
24
|
+
useQueryClient,
|
|
25
|
+
} from "@tanstack/react-query"
|
|
26
|
+
import { useState } from "react"
|
|
27
|
+
import { useNavigate } from "react-router-dom"
|
|
28
|
+
import { faireApi } from "../../../lib/api"
|
|
29
|
+
import { useFaireSyncColumns, FaireSyncRecord } from "./hooks/use-faire-sync-columns"
|
|
30
|
+
|
|
31
|
+
const PAGE_SIZE = 10
|
|
32
|
+
const STATUS_KEY = ["faire", "status"]
|
|
33
|
+
|
|
34
|
+
type Status = {
|
|
35
|
+
connected: boolean
|
|
36
|
+
account: any | null
|
|
37
|
+
settings: any
|
|
38
|
+
readiness: {
|
|
39
|
+
connected: boolean
|
|
40
|
+
brand: boolean
|
|
41
|
+
wholesale_pricing: boolean
|
|
42
|
+
shipping_policy: boolean
|
|
43
|
+
ready_to_publish: boolean
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FaireSettingsPage = () => {
|
|
48
|
+
const navigate = useNavigate()
|
|
49
|
+
const queryClient = useQueryClient()
|
|
50
|
+
|
|
51
|
+
const [pagination, setPagination] = useState<DataTablePaginationState>({
|
|
52
|
+
pageIndex: 0,
|
|
53
|
+
pageSize: PAGE_SIZE,
|
|
54
|
+
})
|
|
55
|
+
const [filtering, setFiltering] = useState<DataTableFilteringState>({})
|
|
56
|
+
|
|
57
|
+
// ── Queries ───────────────────────────────────────────────────────────────
|
|
58
|
+
const statusQuery = useQuery({
|
|
59
|
+
queryKey: STATUS_KEY,
|
|
60
|
+
queryFn: () => faireApi.status() as Promise<Status>,
|
|
61
|
+
})
|
|
62
|
+
const status = statusQuery.data
|
|
63
|
+
const connected = !!status?.connected
|
|
64
|
+
|
|
65
|
+
const syncsQuery = useQuery({
|
|
66
|
+
queryKey: ["faire", "syncs", pagination, filtering],
|
|
67
|
+
placeholderData: keepPreviousData,
|
|
68
|
+
queryFn: async () => {
|
|
69
|
+
const statusFilter = (filtering.status as any)?.[0] as string | undefined
|
|
70
|
+
const res = await faireApi.listSyncs({
|
|
71
|
+
take: pagination.pageSize,
|
|
72
|
+
skip: pagination.pageIndex * pagination.pageSize,
|
|
73
|
+
status: statusFilter,
|
|
74
|
+
})
|
|
75
|
+
return { syncs: (res as any).syncs || [], count: (res as any).count || 0 }
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// ── Mutations ───────────────────────────────────────────────────────────────
|
|
80
|
+
const connectMutation = useMutation({
|
|
81
|
+
mutationFn: () => faireApi.authorize(),
|
|
82
|
+
onSuccess: (res: any) => {
|
|
83
|
+
window.open(res.authorization_url, "_blank")
|
|
84
|
+
toast.info("Faire authorization opened", {
|
|
85
|
+
description: "Complete the authorization in the new tab.",
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
onError: (err: any) =>
|
|
89
|
+
toast.error("Failed to start Faire authorization", {
|
|
90
|
+
description: err.message,
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const disconnectMutation = useMutation({
|
|
95
|
+
mutationFn: () => faireApi.disconnect(),
|
|
96
|
+
onSuccess: () => {
|
|
97
|
+
queryClient.invalidateQueries({ queryKey: STATUS_KEY })
|
|
98
|
+
toast.success("Disconnected from Faire")
|
|
99
|
+
},
|
|
100
|
+
onError: (err: any) =>
|
|
101
|
+
toast.error("Failed to disconnect", { description: err.message }),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// ── DataTable ───────────────────────────────────────────────────────────────
|
|
105
|
+
const columns = useFaireSyncColumns()
|
|
106
|
+
const filterHelper = createDataTableFilterHelper<FaireSyncRecord>()
|
|
107
|
+
const filters = [
|
|
108
|
+
filterHelper.accessor("status", {
|
|
109
|
+
type: "select",
|
|
110
|
+
label: "Status",
|
|
111
|
+
options: [
|
|
112
|
+
{ label: "Success", value: "success" },
|
|
113
|
+
{ label: "Draft", value: "draft" },
|
|
114
|
+
{ label: "Failed", value: "failed" },
|
|
115
|
+
{ label: "Pending", value: "pending" },
|
|
116
|
+
{ label: "Syncing", value: "syncing" },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
const table = useDataTable({
|
|
122
|
+
data: syncsQuery.data?.syncs ?? [],
|
|
123
|
+
columns,
|
|
124
|
+
rowCount: syncsQuery.data?.count ?? 0,
|
|
125
|
+
getRowId: (row: FaireSyncRecord) => row.id,
|
|
126
|
+
onRowClick: (_: any, row: FaireSyncRecord) => {
|
|
127
|
+
navigate(`/settings/faire/${row.id}`)
|
|
128
|
+
},
|
|
129
|
+
isLoading: syncsQuery.isLoading,
|
|
130
|
+
filters,
|
|
131
|
+
pagination: {
|
|
132
|
+
state: pagination,
|
|
133
|
+
onPaginationChange: setPagination,
|
|
134
|
+
},
|
|
135
|
+
filtering: {
|
|
136
|
+
state: filtering,
|
|
137
|
+
onFilteringChange: setFiltering,
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (statusQuery.isLoading) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="flex flex-col gap-4">
|
|
144
|
+
<Container className="divide-y p-0">
|
|
145
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
146
|
+
<Skeleton className="h-6 w-32" />
|
|
147
|
+
<Skeleton className="h-7 w-28" />
|
|
148
|
+
</div>
|
|
149
|
+
<div className="grid grid-cols-4 gap-4 px-6 py-4">
|
|
150
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
151
|
+
<Skeleton key={i} className="h-10 w-full" />
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
</Container>
|
|
155
|
+
<Container className="p-0">
|
|
156
|
+
<div className="px-6 py-4">
|
|
157
|
+
<Skeleton className="h-5 w-32" />
|
|
158
|
+
</div>
|
|
159
|
+
<div className="flex flex-col gap-3 px-6 pb-6">
|
|
160
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
161
|
+
<Skeleton key={i} className="h-10 w-full" />
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</Container>
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="flex flex-col gap-4">
|
|
171
|
+
{statusQuery.isError && (
|
|
172
|
+
<Alert variant="error">
|
|
173
|
+
{(statusQuery.error as any)?.message || "Failed to load Faire status"}
|
|
174
|
+
</Alert>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{/* Connection */}
|
|
178
|
+
<Container className="divide-y p-0">
|
|
179
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
180
|
+
<div className="flex flex-col gap-1">
|
|
181
|
+
<Heading level="h1">Faire Sync</Heading>
|
|
182
|
+
<Text className="text-ui-fg-subtle">
|
|
183
|
+
Connect your Faire brand and sync Medusa products, inventory and
|
|
184
|
+
orders bidirectionally.
|
|
185
|
+
</Text>
|
|
186
|
+
</div>
|
|
187
|
+
{connected ? (
|
|
188
|
+
<div className="flex items-center gap-3">
|
|
189
|
+
<StatusBadge color="green">Connected</StatusBadge>
|
|
190
|
+
<Button
|
|
191
|
+
size="small"
|
|
192
|
+
variant="secondary"
|
|
193
|
+
onClick={() => navigate("/settings/faire/settings")}
|
|
194
|
+
>
|
|
195
|
+
Sync settings
|
|
196
|
+
</Button>
|
|
197
|
+
<Button
|
|
198
|
+
size="small"
|
|
199
|
+
variant="danger"
|
|
200
|
+
onClick={() => disconnectMutation.mutate()}
|
|
201
|
+
isLoading={disconnectMutation.isPending}
|
|
202
|
+
>
|
|
203
|
+
Disconnect
|
|
204
|
+
</Button>
|
|
205
|
+
</div>
|
|
206
|
+
) : (
|
|
207
|
+
<Button
|
|
208
|
+
size="small"
|
|
209
|
+
onClick={() => connectMutation.mutate()}
|
|
210
|
+
isLoading={connectMutation.isPending}
|
|
211
|
+
>
|
|
212
|
+
Connect Faire
|
|
213
|
+
</Button>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
{connected && status?.account && (
|
|
217
|
+
<div className="px-6 py-4 grid grid-cols-4 gap-4">
|
|
218
|
+
<InfoField label="Brand" value={status.account.brand_name} />
|
|
219
|
+
<InfoField label="Brand ID" value={status.account.brand_id} />
|
|
220
|
+
<InfoField label="Currency" value={status.account.currency || "—"} />
|
|
221
|
+
<InfoField
|
|
222
|
+
label="Token expires"
|
|
223
|
+
value={
|
|
224
|
+
status.account.token_expires_at
|
|
225
|
+
? new Date(status.account.token_expires_at).toLocaleString()
|
|
226
|
+
: "Does not expire"
|
|
227
|
+
}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</Container>
|
|
232
|
+
|
|
233
|
+
{/* Bulk operations */}
|
|
234
|
+
<BulkOperations connected={connected} />
|
|
235
|
+
|
|
236
|
+
{/* Recent syncs */}
|
|
237
|
+
<Container className="divide-y p-0">
|
|
238
|
+
<DataTable instance={table}>
|
|
239
|
+
<DataTable.Toolbar className="flex flex-col md:flex-row justify-between gap-y-4 px-6 py-4">
|
|
240
|
+
<div>
|
|
241
|
+
<Heading>Recent syncs</Heading>
|
|
242
|
+
<Text className="text-ui-fg-subtle" size="small">
|
|
243
|
+
Latest product sync attempts. Click a row for details.
|
|
244
|
+
</Text>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="flex items-center gap-x-2">
|
|
247
|
+
<DataTable.FilterMenu tooltip="Filter syncs" />
|
|
248
|
+
<Button
|
|
249
|
+
size="small"
|
|
250
|
+
variant="secondary"
|
|
251
|
+
onClick={() => syncsQuery.refetch()}
|
|
252
|
+
isLoading={syncsQuery.isFetching}
|
|
253
|
+
>
|
|
254
|
+
Refresh
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
</DataTable.Toolbar>
|
|
258
|
+
<DataTable.Table />
|
|
259
|
+
<DataTable.Pagination />
|
|
260
|
+
</DataTable>
|
|
261
|
+
</Container>
|
|
262
|
+
|
|
263
|
+
<Toaster />
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const InfoField = ({ label, value }: { label: string; value: string }) => (
|
|
269
|
+
<div className="flex flex-col gap-1">
|
|
270
|
+
<Label>{label}</Label>
|
|
271
|
+
<Text>{value}</Text>
|
|
272
|
+
</div>
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Bulk operations card: push many products to Faire, and pull Faire orders into
|
|
277
|
+
* Medusa. Both run as long-running background workflows; progress is polled.
|
|
278
|
+
*/
|
|
279
|
+
const BulkOperations = ({ connected }: { connected: boolean }) => {
|
|
280
|
+
const [productIds, setProductIds] = useState("")
|
|
281
|
+
const [pushBatch, setPushBatch] = useState<string | null>(null)
|
|
282
|
+
const [pullBatch, setPullBatch] = useState<string | null>(null)
|
|
283
|
+
|
|
284
|
+
const pushMutation = useMutation({
|
|
285
|
+
mutationFn: (ids: string[]) => faireApi.syncBulk(ids),
|
|
286
|
+
onSuccess: (res: any) => {
|
|
287
|
+
setPushBatch(res.batch_id)
|
|
288
|
+
toast.success("Bulk product sync started", {
|
|
289
|
+
description: "Running in the background. Polling progress…",
|
|
290
|
+
})
|
|
291
|
+
},
|
|
292
|
+
onError: (err: any) =>
|
|
293
|
+
toast.error("Failed to start bulk sync", { description: err.message }),
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const pullMutation = useMutation({
|
|
297
|
+
mutationFn: () => faireApi.ingestOrders(),
|
|
298
|
+
onSuccess: (res: any) => {
|
|
299
|
+
setPullBatch(res.batch_id)
|
|
300
|
+
toast.success("Faire order pull started", {
|
|
301
|
+
description: "Running in the background. Polling progress…",
|
|
302
|
+
})
|
|
303
|
+
},
|
|
304
|
+
onError: (err: any) =>
|
|
305
|
+
toast.error("Failed to start order pull", { description: err.message }),
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Poll both batches until finished.
|
|
309
|
+
const pushStatus = useQuery({
|
|
310
|
+
queryKey: ["faire", "bulk", pushBatch],
|
|
311
|
+
enabled: !!pushBatch,
|
|
312
|
+
refetchInterval: (q) =>
|
|
313
|
+
(q.state.data as any)?.progress?.finished ? false : 3000,
|
|
314
|
+
queryFn: () => faireApi.bulkStatus(pushBatch!),
|
|
315
|
+
})
|
|
316
|
+
const pullStatus = useQuery({
|
|
317
|
+
queryKey: ["faire", "ingest", pullBatch],
|
|
318
|
+
enabled: !!pullBatch,
|
|
319
|
+
refetchInterval: (q) =>
|
|
320
|
+
(q.state.data as any)?.progress?.finished ? false : 3000,
|
|
321
|
+
queryFn: () => faireApi.ingestStatus(pullBatch!),
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const pushProgress = (pushStatus.data as any)?.progress
|
|
325
|
+
const pullProgress = (pullStatus.data as any)?.progress
|
|
326
|
+
|
|
327
|
+
const handlePush = () => {
|
|
328
|
+
const ids = productIds
|
|
329
|
+
.split(/[\s,]+/)
|
|
330
|
+
.map((s) => s.trim())
|
|
331
|
+
.filter(Boolean)
|
|
332
|
+
if (!ids.length) {
|
|
333
|
+
toast.error("Enter at least one product id")
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
pushMutation.mutate(ids)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<Container className="divide-y p-0">
|
|
341
|
+
<div className="px-6 py-4">
|
|
342
|
+
<Heading>Bulk operations</Heading>
|
|
343
|
+
<Text className="text-ui-fg-subtle" size="small">
|
|
344
|
+
Push many products to Faire, or pull wholesale orders from Faire into
|
|
345
|
+
Medusa. Both run as long-running background workflows.
|
|
346
|
+
</Text>
|
|
347
|
+
</div>
|
|
348
|
+
<div className="px-6 py-4 flex flex-col gap-6">
|
|
349
|
+
{/* Push products */}
|
|
350
|
+
<div className="flex flex-col gap-3">
|
|
351
|
+
<div className="flex flex-col gap-1">
|
|
352
|
+
<Label>Push products to Faire</Label>
|
|
353
|
+
<Text className="text-ui-fg-subtle" size="small">
|
|
354
|
+
Comma- or line-separated Medusa product IDs.
|
|
355
|
+
</Text>
|
|
356
|
+
</div>
|
|
357
|
+
<textarea
|
|
358
|
+
className="border-ui-border-base rounded-lg border px-3 py-2 text-sm min-h-[72px]"
|
|
359
|
+
placeholder="prod_01..., prod_02..."
|
|
360
|
+
value={productIds}
|
|
361
|
+
onChange={(e) => setProductIds(e.target.value)}
|
|
362
|
+
disabled={!connected || pushMutation.isPending}
|
|
363
|
+
/>
|
|
364
|
+
<div className="flex items-center gap-3">
|
|
365
|
+
<Button
|
|
366
|
+
size="small"
|
|
367
|
+
onClick={handlePush}
|
|
368
|
+
disabled={!connected || pushMutation.isPending}
|
|
369
|
+
isLoading={pushMutation.isPending}
|
|
370
|
+
>
|
|
371
|
+
Start bulk push
|
|
372
|
+
</Button>
|
|
373
|
+
{pushBatch && pushProgress && (
|
|
374
|
+
<BulkProgress
|
|
375
|
+
label="Push"
|
|
376
|
+
progress={pushProgress}
|
|
377
|
+
status={(pushStatus.data as any)?.batch?.status}
|
|
378
|
+
/>
|
|
379
|
+
)}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{/* Pull orders */}
|
|
384
|
+
<div className="flex flex-col gap-3">
|
|
385
|
+
<div className="flex flex-col gap-1">
|
|
386
|
+
<Label>Pull Faire orders into Medusa</Label>
|
|
387
|
+
<Text className="text-ui-fg-subtle" size="small">
|
|
388
|
+
Ingests all available Faire orders as Medusa orders (idempotent).
|
|
389
|
+
</Text>
|
|
390
|
+
</div>
|
|
391
|
+
<div className="flex items-center gap-3">
|
|
392
|
+
<Button
|
|
393
|
+
size="small"
|
|
394
|
+
variant="secondary"
|
|
395
|
+
onClick={() => pullMutation.mutate()}
|
|
396
|
+
disabled={!connected || pullMutation.isPending}
|
|
397
|
+
isLoading={pullMutation.isPending}
|
|
398
|
+
>
|
|
399
|
+
Start order pull
|
|
400
|
+
</Button>
|
|
401
|
+
{pullBatch && pullProgress && (
|
|
402
|
+
<BulkProgress
|
|
403
|
+
label="Pull"
|
|
404
|
+
progress={pullProgress}
|
|
405
|
+
status={(pullStatus.data as any)?.batch?.status}
|
|
406
|
+
/>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</Container>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const BulkProgress = ({
|
|
416
|
+
label,
|
|
417
|
+
progress,
|
|
418
|
+
status,
|
|
419
|
+
}: {
|
|
420
|
+
label: string
|
|
421
|
+
progress: { total: number; done: number; pct: number; finished: boolean }
|
|
422
|
+
status?: string
|
|
423
|
+
}) => (
|
|
424
|
+
<div className="flex items-center gap-3">
|
|
425
|
+
<StatusBadge color={progress.finished ? (status === "failed" ? "red" : "green") : "orange"}>
|
|
426
|
+
{label}: {progress.done}/{progress.total || "?"} ({progress.pct}%)
|
|
427
|
+
</StatusBadge>
|
|
428
|
+
{progress.finished && (
|
|
429
|
+
<Text className="text-ui-fg-subtle" size="small">
|
|
430
|
+
{status === "failed" ? "Completed with errors" : "Done"}
|
|
431
|
+
</Text>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
export const config = defineRouteConfig({
|
|
437
|
+
label: "Faire",
|
|
438
|
+
icon: BuildingStorefront,
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
export const handle = {
|
|
442
|
+
breadcrumb: () => "Faire Sync",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export default FaireSettingsPage
|