@swatcha/mcp-server 0.1.0 → 0.2.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/dist/client.d.ts +23 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -1
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/apps/image-picker.html +0 -16
- package/src/apps/image-picker.tsx +0 -423
- package/src/client.ts +0 -187
- package/src/index.ts +0 -772
- package/tsconfig.app.json +0 -16
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -16
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
import { createRoot } from 'react-dom/client';
|
|
2
|
-
import { useState, useCallback } from 'react';
|
|
3
|
-
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react';
|
|
4
|
-
import type { CallToolResult } from '@modelcontextprotocol/ext-apps';
|
|
5
|
-
|
|
6
|
-
// ============================================================================
|
|
7
|
-
// Types
|
|
8
|
-
// ============================================================================
|
|
9
|
-
|
|
10
|
-
interface ImageCandidate {
|
|
11
|
-
imageUrl: string;
|
|
12
|
-
thumbnailUrl: string;
|
|
13
|
-
title: string;
|
|
14
|
-
sourceDomain: string;
|
|
15
|
-
sourceUrl?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface ProductEntry {
|
|
19
|
-
productId: string;
|
|
20
|
-
productName: string;
|
|
21
|
-
supplier?: string;
|
|
22
|
-
candidates: ImageCandidate[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface PickerData {
|
|
26
|
-
products: ProductEntry[];
|
|
27
|
-
batchLabel?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ============================================================================
|
|
31
|
-
// Styles
|
|
32
|
-
// ============================================================================
|
|
33
|
-
|
|
34
|
-
const styles = {
|
|
35
|
-
body: {
|
|
36
|
-
fontFamily: 'var(--font-sans, system-ui, sans-serif)',
|
|
37
|
-
background: 'var(--color-background-primary, #1a1a2e)',
|
|
38
|
-
color: 'var(--color-text-primary, #e2e8f0)',
|
|
39
|
-
padding: 16,
|
|
40
|
-
},
|
|
41
|
-
header: {
|
|
42
|
-
display: 'flex' as const,
|
|
43
|
-
justifyContent: 'space-between' as const,
|
|
44
|
-
alignItems: 'baseline' as const,
|
|
45
|
-
marginBottom: 16,
|
|
46
|
-
},
|
|
47
|
-
title: { fontSize: 16, fontWeight: 600 },
|
|
48
|
-
batchInfo: {
|
|
49
|
-
fontSize: 12,
|
|
50
|
-
color: 'var(--color-text-secondary, #94a3b8)',
|
|
51
|
-
},
|
|
52
|
-
productSection: (isLast: boolean) => ({
|
|
53
|
-
marginBottom: 20,
|
|
54
|
-
paddingBottom: 16,
|
|
55
|
-
borderBottom: isLast ? 'none' : '1px solid var(--color-border, #334155)',
|
|
56
|
-
}),
|
|
57
|
-
productHeader: {
|
|
58
|
-
display: 'flex' as const,
|
|
59
|
-
justifyContent: 'space-between' as const,
|
|
60
|
-
alignItems: 'baseline' as const,
|
|
61
|
-
marginBottom: 8,
|
|
62
|
-
},
|
|
63
|
-
productName: { fontSize: 14, fontWeight: 500 },
|
|
64
|
-
productSupplier: {
|
|
65
|
-
fontSize: 12,
|
|
66
|
-
color: 'var(--color-text-secondary, #94a3b8)',
|
|
67
|
-
},
|
|
68
|
-
statusBadge: (state: 'selected' | 'skipped') => ({
|
|
69
|
-
fontSize: 11,
|
|
70
|
-
padding: '2px 8px',
|
|
71
|
-
borderRadius: 10,
|
|
72
|
-
background:
|
|
73
|
-
state === 'selected'
|
|
74
|
-
? 'rgba(124, 58, 237, 0.15)'
|
|
75
|
-
: 'rgba(148, 163, 184, 0.1)',
|
|
76
|
-
color: state === 'selected' ? '#7c3aed' : 'var(--color-text-secondary, #94a3b8)',
|
|
77
|
-
}),
|
|
78
|
-
imagesRow: {
|
|
79
|
-
display: 'flex' as const,
|
|
80
|
-
gap: 8,
|
|
81
|
-
overflowX: 'auto' as const,
|
|
82
|
-
padding: '6px 2px',
|
|
83
|
-
},
|
|
84
|
-
imageCard: (isSelected: boolean) => ({
|
|
85
|
-
flex: '0 0 auto',
|
|
86
|
-
width: 130,
|
|
87
|
-
border: `2px solid ${isSelected ? '#7c3aed' : 'transparent'}`,
|
|
88
|
-
borderRadius: 8,
|
|
89
|
-
overflow: 'hidden' as const,
|
|
90
|
-
cursor: 'pointer' as const,
|
|
91
|
-
background: 'var(--color-background-secondary, #2a2a4a)',
|
|
92
|
-
transition: 'border-color 0.15s, transform 0.15s, opacity 0.15s',
|
|
93
|
-
transform: isSelected ? 'scale(1.05)' : 'scale(1)',
|
|
94
|
-
opacity: isSelected ? 1 : 0.6,
|
|
95
|
-
position: 'relative' as const,
|
|
96
|
-
}),
|
|
97
|
-
checkmark: {
|
|
98
|
-
position: 'absolute' as const,
|
|
99
|
-
top: 4,
|
|
100
|
-
right: 4,
|
|
101
|
-
width: 22,
|
|
102
|
-
height: 22,
|
|
103
|
-
borderRadius: '50%',
|
|
104
|
-
background: '#7c3aed',
|
|
105
|
-
display: 'flex' as const,
|
|
106
|
-
alignItems: 'center' as const,
|
|
107
|
-
justifyContent: 'center' as const,
|
|
108
|
-
zIndex: 1,
|
|
109
|
-
},
|
|
110
|
-
img: {
|
|
111
|
-
width: '100%',
|
|
112
|
-
height: 90,
|
|
113
|
-
objectFit: 'cover' as const,
|
|
114
|
-
display: 'block' as const,
|
|
115
|
-
},
|
|
116
|
-
meta: {
|
|
117
|
-
padding: '4px 6px',
|
|
118
|
-
fontSize: 10,
|
|
119
|
-
whiteSpace: 'nowrap' as const,
|
|
120
|
-
overflow: 'hidden' as const,
|
|
121
|
-
textOverflow: 'ellipsis' as const,
|
|
122
|
-
},
|
|
123
|
-
metaLink: {
|
|
124
|
-
color: 'var(--color-text-secondary, #94a3b8)',
|
|
125
|
-
textDecoration: 'none' as const,
|
|
126
|
-
},
|
|
127
|
-
metaText: {
|
|
128
|
-
color: 'var(--color-text-secondary, #94a3b8)',
|
|
129
|
-
},
|
|
130
|
-
skipLink: {
|
|
131
|
-
display: 'inline-block' as const,
|
|
132
|
-
marginTop: 6,
|
|
133
|
-
fontSize: 11,
|
|
134
|
-
color: 'var(--color-text-secondary, #94a3b8)',
|
|
135
|
-
cursor: 'pointer' as const,
|
|
136
|
-
textDecoration: 'underline' as const,
|
|
137
|
-
textUnderlineOffset: 2,
|
|
138
|
-
background: 'none',
|
|
139
|
-
border: 'none',
|
|
140
|
-
padding: 0,
|
|
141
|
-
fontFamily: 'inherit',
|
|
142
|
-
},
|
|
143
|
-
footer: {
|
|
144
|
-
marginTop: 16,
|
|
145
|
-
display: 'flex' as const,
|
|
146
|
-
gap: 8,
|
|
147
|
-
alignItems: 'center' as const,
|
|
148
|
-
},
|
|
149
|
-
btnPrimary: {
|
|
150
|
-
padding: '8px 16px',
|
|
151
|
-
border: 'none' as const,
|
|
152
|
-
borderRadius: 6,
|
|
153
|
-
fontSize: 13,
|
|
154
|
-
fontWeight: 500,
|
|
155
|
-
cursor: 'pointer' as const,
|
|
156
|
-
background: '#7c3aed',
|
|
157
|
-
color: 'white',
|
|
158
|
-
fontFamily: 'inherit',
|
|
159
|
-
},
|
|
160
|
-
status: {
|
|
161
|
-
fontSize: 12,
|
|
162
|
-
color: 'var(--color-text-secondary, #94a3b8)',
|
|
163
|
-
},
|
|
164
|
-
error: {
|
|
165
|
-
fontSize: 12,
|
|
166
|
-
color: '#ef4444',
|
|
167
|
-
marginTop: 8,
|
|
168
|
-
},
|
|
169
|
-
imgError: {
|
|
170
|
-
width: '100%',
|
|
171
|
-
height: 90,
|
|
172
|
-
display: 'flex' as const,
|
|
173
|
-
alignItems: 'center' as const,
|
|
174
|
-
justifyContent: 'center' as const,
|
|
175
|
-
fontSize: 11,
|
|
176
|
-
color: 'var(--color-text-secondary, #94a3b8)',
|
|
177
|
-
background: 'var(--color-background-secondary, #2a2a4a)',
|
|
178
|
-
},
|
|
179
|
-
} as const;
|
|
180
|
-
|
|
181
|
-
// ============================================================================
|
|
182
|
-
// Components
|
|
183
|
-
// ============================================================================
|
|
184
|
-
|
|
185
|
-
function CheckmarkIcon() {
|
|
186
|
-
return (
|
|
187
|
-
<svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round">
|
|
188
|
-
<polyline points="20 6 9 17 4 12" />
|
|
189
|
-
</svg>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function ImageCard({
|
|
194
|
-
candidate,
|
|
195
|
-
isSelected,
|
|
196
|
-
onClick,
|
|
197
|
-
}: {
|
|
198
|
-
candidate: ImageCandidate;
|
|
199
|
-
isSelected: boolean;
|
|
200
|
-
onClick: () => void;
|
|
201
|
-
}) {
|
|
202
|
-
const [imgError, setImgError] = useState(false);
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<div style={styles.imageCard(isSelected)} onClick={onClick}>
|
|
206
|
-
{isSelected && (
|
|
207
|
-
<div style={styles.checkmark}>
|
|
208
|
-
<CheckmarkIcon />
|
|
209
|
-
</div>
|
|
210
|
-
)}
|
|
211
|
-
{imgError ? (
|
|
212
|
-
<div style={styles.imgError}>Failed to load</div>
|
|
213
|
-
) : (
|
|
214
|
-
<img
|
|
215
|
-
src={candidate.thumbnailUrl || candidate.imageUrl}
|
|
216
|
-
alt={candidate.title}
|
|
217
|
-
loading="lazy"
|
|
218
|
-
style={styles.img}
|
|
219
|
-
onError={() => setImgError(true)}
|
|
220
|
-
/>
|
|
221
|
-
)}
|
|
222
|
-
<div style={styles.meta}>
|
|
223
|
-
{candidate.sourceUrl ? (
|
|
224
|
-
<a
|
|
225
|
-
href={candidate.sourceUrl}
|
|
226
|
-
target="_blank"
|
|
227
|
-
rel="noopener noreferrer"
|
|
228
|
-
onClick={(e) => e.stopPropagation()}
|
|
229
|
-
style={styles.metaLink}
|
|
230
|
-
>
|
|
231
|
-
{candidate.sourceDomain}
|
|
232
|
-
</a>
|
|
233
|
-
) : (
|
|
234
|
-
<span style={styles.metaText}>{candidate.sourceDomain}</span>
|
|
235
|
-
)}
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function ProductSection({
|
|
242
|
-
product,
|
|
243
|
-
isLast,
|
|
244
|
-
selection,
|
|
245
|
-
onSelect,
|
|
246
|
-
onSkip,
|
|
247
|
-
}: {
|
|
248
|
-
product: ProductEntry;
|
|
249
|
-
isLast: boolean;
|
|
250
|
-
selection: string | null;
|
|
251
|
-
onSelect: (imageUrl: string) => void;
|
|
252
|
-
onSkip: () => void;
|
|
253
|
-
}) {
|
|
254
|
-
const isSkipped = selection === null;
|
|
255
|
-
const selectedIdx = product.candidates.findIndex((c) => c.imageUrl === selection);
|
|
256
|
-
const statusText =
|
|
257
|
-
product.candidates.length === 0
|
|
258
|
-
? 'No candidates'
|
|
259
|
-
: isSkipped
|
|
260
|
-
? 'Skipped'
|
|
261
|
-
: `Image ${selectedIdx + 1} selected`;
|
|
262
|
-
const statusState: 'selected' | 'skipped' =
|
|
263
|
-
product.candidates.length === 0 || isSkipped ? 'skipped' : 'selected';
|
|
264
|
-
|
|
265
|
-
return (
|
|
266
|
-
<div style={styles.productSection(isLast)}>
|
|
267
|
-
<div style={styles.productHeader}>
|
|
268
|
-
<div>
|
|
269
|
-
<span style={styles.productName}>{product.productName}</span>
|
|
270
|
-
{product.supplier && (
|
|
271
|
-
<span style={styles.productSupplier}> from {product.supplier}</span>
|
|
272
|
-
)}
|
|
273
|
-
</div>
|
|
274
|
-
<span style={styles.statusBadge(statusState)}>{statusText}</span>
|
|
275
|
-
</div>
|
|
276
|
-
|
|
277
|
-
{product.candidates.length > 0 && (
|
|
278
|
-
<>
|
|
279
|
-
<div style={styles.imagesRow}>
|
|
280
|
-
{product.candidates.map((candidate, idx) => (
|
|
281
|
-
<ImageCard
|
|
282
|
-
key={idx}
|
|
283
|
-
candidate={candidate}
|
|
284
|
-
isSelected={selection === candidate.imageUrl}
|
|
285
|
-
onClick={() => onSelect(candidate.imageUrl)}
|
|
286
|
-
/>
|
|
287
|
-
))}
|
|
288
|
-
</div>
|
|
289
|
-
<button style={styles.skipLink} onClick={onSkip}>
|
|
290
|
-
Skip this product
|
|
291
|
-
</button>
|
|
292
|
-
</>
|
|
293
|
-
)}
|
|
294
|
-
</div>
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function ImagePickerApp() {
|
|
299
|
-
const [pickerData, setPickerData] = useState<PickerData | null>(null);
|
|
300
|
-
const [selections, setSelections] = useState<Record<string, string | null>>({});
|
|
301
|
-
const [submitted, setSubmitted] = useState(false);
|
|
302
|
-
const [error, setError] = useState<string | null>(null);
|
|
303
|
-
|
|
304
|
-
const { app, isConnected, error: connError } = useApp({
|
|
305
|
-
appInfo: { name: 'ImagePicker', version: '0.1.0' },
|
|
306
|
-
capabilities: {},
|
|
307
|
-
onAppCreated: (createdApp) => {
|
|
308
|
-
createdApp.ontoolresult = (params: CallToolResult) => {
|
|
309
|
-
const data = params.structuredContent as PickerData | undefined;
|
|
310
|
-
if (!data?.products?.length) return;
|
|
311
|
-
|
|
312
|
-
setPickerData(data);
|
|
313
|
-
|
|
314
|
-
const initial: Record<string, string | null> = {};
|
|
315
|
-
data.products.forEach((p) => {
|
|
316
|
-
initial[p.productId] = p.candidates.length > 0 ? p.candidates[0].imageUrl : null;
|
|
317
|
-
});
|
|
318
|
-
setSelections(initial);
|
|
319
|
-
};
|
|
320
|
-
createdApp.onteardown = async () => ({});
|
|
321
|
-
},
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
useHostStyles(app);
|
|
325
|
-
|
|
326
|
-
const handleSelect = useCallback((productId: string, imageUrl: string) => {
|
|
327
|
-
setSelections((prev) => ({ ...prev, [productId]: imageUrl }));
|
|
328
|
-
}, []);
|
|
329
|
-
|
|
330
|
-
const handleSkip = useCallback((productId: string) => {
|
|
331
|
-
setSelections((prev) => ({ ...prev, [productId]: null }));
|
|
332
|
-
}, []);
|
|
333
|
-
|
|
334
|
-
const handleSave = useCallback(async () => {
|
|
335
|
-
if (!app || !pickerData) return;
|
|
336
|
-
|
|
337
|
-
const lines = pickerData.products.map((product) => {
|
|
338
|
-
const imageUrl = selections[product.productId];
|
|
339
|
-
return imageUrl
|
|
340
|
-
? `- "${product.productName}" (${product.productId}): ${imageUrl}`
|
|
341
|
-
: `- "${product.productName}" (${product.productId}): SKIP`;
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
const message = `Image selections:\n${lines.join('\n')}\n\nPlease update each product with its selected image URL. Skip products marked SKIP.`;
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
await app.sendMessage({
|
|
348
|
-
role: 'user',
|
|
349
|
-
content: [{ type: 'text', text: message }],
|
|
350
|
-
});
|
|
351
|
-
setSubmitted(true);
|
|
352
|
-
} catch (err) {
|
|
353
|
-
setError(`Failed to send: ${err instanceof Error ? err.message : String(err)}`);
|
|
354
|
-
}
|
|
355
|
-
}, [app, pickerData, selections]);
|
|
356
|
-
|
|
357
|
-
if (connError) {
|
|
358
|
-
return (
|
|
359
|
-
<div style={styles.body}>
|
|
360
|
-
<div style={styles.error}>Connection failed: {connError.message}</div>
|
|
361
|
-
</div>
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
if (!isConnected) {
|
|
365
|
-
return (
|
|
366
|
-
<div style={styles.body}>
|
|
367
|
-
<span style={styles.status}>Connecting...</span>
|
|
368
|
-
</div>
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
if (!pickerData) {
|
|
372
|
-
return (
|
|
373
|
-
<div style={styles.body}>
|
|
374
|
-
<span style={styles.status}>Waiting for image candidates...</span>
|
|
375
|
-
</div>
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const selectedCount = Object.values(selections).filter((v) => v !== null).length;
|
|
380
|
-
const total = pickerData.products.length;
|
|
381
|
-
|
|
382
|
-
return (
|
|
383
|
-
<div style={styles.body}>
|
|
384
|
-
<div style={styles.header}>
|
|
385
|
-
<h2 style={styles.title}>Select Product Images</h2>
|
|
386
|
-
{pickerData.batchLabel && (
|
|
387
|
-
<span style={styles.batchInfo}>{pickerData.batchLabel}</span>
|
|
388
|
-
)}
|
|
389
|
-
</div>
|
|
390
|
-
|
|
391
|
-
{pickerData.products.map((product, idx) => (
|
|
392
|
-
<ProductSection
|
|
393
|
-
key={product.productId}
|
|
394
|
-
product={product}
|
|
395
|
-
isLast={idx === pickerData.products.length - 1}
|
|
396
|
-
selection={selections[product.productId]}
|
|
397
|
-
onSelect={(imageUrl) => handleSelect(product.productId, imageUrl)}
|
|
398
|
-
onSkip={() => handleSkip(product.productId)}
|
|
399
|
-
/>
|
|
400
|
-
))}
|
|
401
|
-
|
|
402
|
-
<div style={styles.footer}>
|
|
403
|
-
{!submitted ? (
|
|
404
|
-
<button style={styles.btnPrimary} onClick={handleSave}>
|
|
405
|
-
Save Selections ({selectedCount}/{total})
|
|
406
|
-
</button>
|
|
407
|
-
) : (
|
|
408
|
-
<span style={styles.status}>
|
|
409
|
-
Selections sent — click send in the chat to confirm.
|
|
410
|
-
</span>
|
|
411
|
-
)}
|
|
412
|
-
</div>
|
|
413
|
-
|
|
414
|
-
{error && <div style={styles.error}>{error}</div>}
|
|
415
|
-
</div>
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// ============================================================================
|
|
420
|
-
// Mount
|
|
421
|
-
// ============================================================================
|
|
422
|
-
|
|
423
|
-
createRoot(document.getElementById('root')!).render(<ImagePickerApp />);
|
package/src/client.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
interface ApiResponse<T = unknown> {
|
|
2
|
-
data?: T;
|
|
3
|
-
error?: { code: string; message: string; details?: unknown };
|
|
4
|
-
pagination?: { cursor: string | null; hasMore: boolean };
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export class SwatchaClient {
|
|
8
|
-
private baseUrl: string;
|
|
9
|
-
private apiKey: string;
|
|
10
|
-
|
|
11
|
-
constructor(baseUrl: string, apiKey: string) {
|
|
12
|
-
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
13
|
-
this.apiKey = apiKey;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
private async request<T>(
|
|
17
|
-
method: string,
|
|
18
|
-
path: string,
|
|
19
|
-
body?: unknown
|
|
20
|
-
): Promise<ApiResponse<T>> {
|
|
21
|
-
const url = `${this.baseUrl}/api/v1${path}`;
|
|
22
|
-
const headers: Record<string, string> = {
|
|
23
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const response = await fetch(url, {
|
|
28
|
-
method,
|
|
29
|
-
headers,
|
|
30
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const json = (await response.json()) as ApiResponse<T>;
|
|
34
|
-
|
|
35
|
-
if (!response.ok) {
|
|
36
|
-
throw new Error(json.error?.message || `API error: ${response.status}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return json;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Selections
|
|
43
|
-
async listSelections() {
|
|
44
|
-
return this.request('GET', '/selections');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async getSelection(id: string) {
|
|
48
|
-
return this.request('GET', `/selections/${id}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async createSelection(data: {
|
|
52
|
-
name: string;
|
|
53
|
-
clientId: string;
|
|
54
|
-
status?: string;
|
|
55
|
-
notes?: string;
|
|
56
|
-
siteAddress?: string;
|
|
57
|
-
}) {
|
|
58
|
-
return this.request('POST', '/selections', data);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async updateSelection(
|
|
62
|
-
id: string,
|
|
63
|
-
data: { name?: string; status?: string; notes?: string; siteAddress?: string }
|
|
64
|
-
) {
|
|
65
|
-
return this.request('PATCH', `/selections/${id}`, data);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async deleteSelection(id: string) {
|
|
69
|
-
return this.request('DELETE', `/selections/${id}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Selection Products
|
|
73
|
-
async listSelectionProducts(selectionId: string) {
|
|
74
|
-
return this.request('GET', `/selections/${selectionId}/products`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async addProductToSelection(
|
|
78
|
-
selectionId: string,
|
|
79
|
-
data: {
|
|
80
|
-
productId: string;
|
|
81
|
-
quantity?: string;
|
|
82
|
-
location?: string;
|
|
83
|
-
locationId?: string;
|
|
84
|
-
notes?: string;
|
|
85
|
-
}
|
|
86
|
-
) {
|
|
87
|
-
return this.request('POST', `/selections/${selectionId}/products`, data);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async updateSelectionProduct(
|
|
91
|
-
selectionId: string,
|
|
92
|
-
productId: string,
|
|
93
|
-
data: { quantity?: string; location?: string; locationId?: string; notes?: string }
|
|
94
|
-
) {
|
|
95
|
-
return this.request(
|
|
96
|
-
'PATCH',
|
|
97
|
-
`/selections/${selectionId}/products/${productId}`,
|
|
98
|
-
data
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async removeProductFromSelection(selectionId: string, productId: string) {
|
|
103
|
-
return this.request(
|
|
104
|
-
'DELETE',
|
|
105
|
-
`/selections/${selectionId}/products/${productId}`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Products (Catalog)
|
|
110
|
-
async listProducts(query?: string) {
|
|
111
|
-
const qs = query ? `?q=${encodeURIComponent(query)}` : '';
|
|
112
|
-
return this.request('GET', `/products${qs}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async getProduct(id: string) {
|
|
116
|
-
return this.request('GET', `/products/${id}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async createProduct(data: {
|
|
120
|
-
name: string;
|
|
121
|
-
sku?: string;
|
|
122
|
-
price?: number;
|
|
123
|
-
supplierId?: string;
|
|
124
|
-
imageUrl?: string;
|
|
125
|
-
description?: string;
|
|
126
|
-
}) {
|
|
127
|
-
return this.request('POST', '/products', data);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async updateProduct(
|
|
131
|
-
id: string,
|
|
132
|
-
data: {
|
|
133
|
-
name?: string;
|
|
134
|
-
sku?: string;
|
|
135
|
-
price?: number;
|
|
136
|
-
supplierId?: string;
|
|
137
|
-
imageUrl?: string;
|
|
138
|
-
}
|
|
139
|
-
) {
|
|
140
|
-
return this.request('PATCH', `/products/${id}`, data);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Clients
|
|
144
|
-
async listClients() {
|
|
145
|
-
return this.request('GET', '/clients');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async createClient(data: { name: string }) {
|
|
149
|
-
return this.request('POST', '/clients', data);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Suppliers
|
|
153
|
-
async listSuppliers() {
|
|
154
|
-
return this.request('GET', '/suppliers');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async createSupplier(data: { name: string; websiteUrl?: string }) {
|
|
158
|
-
return this.request('POST', '/suppliers', data);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Locations
|
|
162
|
-
async listLocations() {
|
|
163
|
-
return this.request('GET', '/locations');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async createLocation(data: { name: string }) {
|
|
167
|
-
return this.request('POST', '/locations', data);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Quota
|
|
171
|
-
async getQuota() {
|
|
172
|
-
return this.request('GET', '/quota');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Image Search
|
|
176
|
-
async searchProductImages(productName: string, options?: {
|
|
177
|
-
supplier?: string;
|
|
178
|
-
supplierUrl?: string;
|
|
179
|
-
limit?: number;
|
|
180
|
-
}) {
|
|
181
|
-
const params = new URLSearchParams({ product: productName });
|
|
182
|
-
if (options?.supplier) params.append('supplier', options.supplier);
|
|
183
|
-
if (options?.supplierUrl) params.append('supplierUrl', options.supplierUrl);
|
|
184
|
-
if (options?.limit) params.append('limit', String(options.limit));
|
|
185
|
-
return this.request('GET', `/images/search?${params}`);
|
|
186
|
-
}
|
|
187
|
-
}
|