@isoftdata/svelte-ecommerce 1.0.0-beta.0 → 1.0.0-beta.2

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.
@@ -1,986 +1,852 @@
1
- <script lang="ts">
2
- import type { i18n } from 'i18next'
3
- import { getContext } from 'svelte'
4
-
5
- import Button from '@isoftdata/svelte-button'
6
- import CurrencyInput from '@isoftdata/svelte-currency-input'
7
- import Input from '@isoftdata/svelte-input'
8
- import Modal from '@isoftdata/svelte-modal'
9
- import Select from '@isoftdata/svelte-select'
10
- import Textarea from '@isoftdata/svelte-textarea'
11
- import PolicyList from './PolicyList.svelte'
12
- import { ThumbnailGrid } from '@isoftdata/svelte-attachments'
13
- import UserPrompt from '@isoftdata/svelte-user-prompt'
14
- import { v4 as uuid } from '@lukeed/uuid'
15
- import { ecommercePartnerStaticData } from './data/index.js'
16
- import type { PackageType } from './index.js'
17
- import { buildEbayListing } from './helpers/index.js'
18
-
19
- import type {
20
- EbayCategory,
21
- EbayPolicy,
22
- EcommerceCondition,
23
- ExtendedEcommerceConditionMap,
24
- EcommercePartnerConfiguration,
25
- ExtendedEbayCategoryMap,
26
- FileItem,
27
- InventoryListingDetail,
28
- InventoryRow,
29
- InventoryType,
30
- InventoryTypeListingDefaults,
31
- NewInventoryListingDetail,
32
- PolicyRowWithChecked,
33
- } from './utils.js'
34
- import { translate as defaultTranslate } from '@isoftdata/utility-string'
35
- const { t: translate } = getContext<i18n>('i18next') || { t: defaultTranslate }
36
-
37
- //
38
- interface Props {
39
- ebayCategoryList: EbayCategory[]
40
- ebayCategoryMapList: ExtendedEbayCategoryMap[]
41
- ebayPolicyList: EbayPolicy[]
42
- ecommerceConditionMapList: ExtendedEcommerceConditionMap[]
43
- ecommerceConditionList: EcommerceCondition[]
44
- ecommercePartnerConfigurationList: EcommercePartnerConfiguration[]
45
- listingList: InventoryListingDetail[]
46
- partFileList: FileItem[]
47
- part: InventoryRow
48
- inventoryTypeWithDefaultConfigList: InventoryTypeListingDefaults[]
49
- inventoryTypeList: InventoryType[]
50
- save: (data: InventoryListingDetail | NewInventoryListingDetail) => Promise<void>
51
- }
52
-
53
- let {
54
- ebayCategoryList,
55
- ebayCategoryMapList,
56
- ebayPolicyList,
57
- ecommerceConditionMapList,
58
- ecommerceConditionList,
59
- ecommercePartnerConfigurationList,
60
- listingList,
61
- partFileList,
62
- part,
63
- inventoryTypeWithDefaultConfigList,
64
- inventoryTypeList,
65
- save = async (_: InventoryListingDetail | NewInventoryListingDetail) => {},
66
- }: Props = $props()
67
-
68
- let disableFields = $derived(!part.isWorldViewable)
69
- let fulfillmentPolicyList = $derived.by(getFulfillmentPolicyList)
70
- let lengthUnits = $derived.by(getPartnerLengthUnits)
71
- let packageTypes = $derived.by(getPartnerPackageTypes)
72
- let selectedEcommercePartnerId = $state(1)
73
- let selectedPartnerConfig = $derived.by(getSelectedPartnerConfig)
74
- let shippingFieldRequired = true // TODO: in the eBay world, this will need to be set if using calculated shipping but we don't know how we'll check that
75
- let show = $state(false)
76
- let showElement = $derived(selectedPartnerConfig?.name === 'ebay')
77
- let uploadedListingImages = $derived.by(getListedImages)
78
- let userPrompt: UserPrompt | undefined = $state(undefined)
79
- let weightUnits = $derived.by(getPartnerWeightUnits)
80
-
81
- // For images - moved before getOrCreateEcommerceListingObject to avoid initialization error
82
- let newEcommerceListing: NewInventoryListingDetail = Object.freeze({
83
- // Listing details
84
- active: false,
85
- //inventoryListingDetailId: null,
86
- convertedListingDetails: null,
87
- duration: 'GTC',
88
- ecommercePartnerId: 0,
89
- ecommerceCategoryId: null,
90
- ecommerceConditionId: null,
91
- ecommerceConditionDescription: null,
92
- fulfillmentTime: null,
93
- fulfillmentTimeUnit: null,
94
- listingDescription: null,
95
- listingStatus: 'pending',
96
- listingTitle: null,
97
- // Inventory specifics
98
- imageUrls: [],
99
- inventoryDescription: null,
100
- inventoryId: 0,
101
- lastUpdate: '',
102
- manufacturerPartNumber: null,
103
- message: [],
104
- price: null,
105
- quantity: null,
106
- sku: null,
107
- storeId: 0,
108
- upc: '',
109
- weight: null,
110
- weightUnit: null,
111
- shippingLength: null,
112
- shippingWidth: null,
113
- shippingHeight: null,
114
- shippingLengthUnit: null,
115
- shippingPackage: null,
116
- // Partner specifics that don't fit into the other categories
117
- partnerSpecificDetails: {
118
- brand: null,
119
- duration: 'GTC',
120
- listingFormat: 'FIXED_PRICE',
121
- fulfillmentPolicy: null,
122
- marketplaceId: 'EBAY_MOTORS',
123
- merchantLocationKey: null,
124
- oemNumber: null,
125
- paymentPolicy: null,
126
- returnPolicy: null,
127
- },
128
- })
129
-
130
- let listing: InventoryListingDetail | NewInventoryListingDetail = $state(buildListingFromCurrentState()) // Had to explicitly set the type here
131
- $inspect(listing)
132
- // New function to build listing using buildEbayListing helper
133
- function buildListingFromCurrentState(): InventoryListingDetail | NewInventoryListingDetail {
134
- // Find existing listing for the selected partner
135
- const existingListing =
136
- listingList.find(listing => listing.ecommercePartnerId === selectedEcommercePartnerId) || newEcommerceListing
137
-
138
- // Find matching category mapping
139
- const categoryMapping = ebayCategoryMapList.find(mapping => mapping.typeNum === part.inventoryTypeId)
140
-
141
- // Find matching condition mapping
142
- const conditionMapping = ecommerceConditionMapList.find(mapping => mapping.itrackCondition === part.condition)
143
-
144
- // Find matching inventory type
145
- const inventoryType = inventoryTypeList.find(type => type.inventoryTypeId === part.inventoryTypeId)
146
-
147
- // Convert InventoryTypeListingDefaults to ExtendedInventoryTypeListingDefaults format
148
- const extendedInventoryTypeListingDefaults = inventoryTypeWithDefaultConfigList.map(config => ({
149
- ...config,
150
- categoryName: '', // This will need to be populated from actual category data
151
- inventoryType: config.inventoryTypeId,
152
- inventoryTypeName: inventoryType?.name || '',
153
- }))
154
-
155
- // Build the input for buildEbayListing
156
- const buildInput = {
157
- categoryMapping,
158
- conditionMapping,
159
- ecommercePartnerConfiguration: selectedPartnerConfig,
160
- existingListing,
161
- inventoryRow: part,
162
- inventoryTypeListingDefaults: extendedInventoryTypeListingDefaults,
163
- inventoryType,
164
- partFileList,
165
- }
166
-
167
- // Call buildEbayListing and return the result, or fallback to newEcommerceListing
168
- const result = buildEbayListing(buildInput)
169
- return result || newEcommerceListing
170
- }
171
-
172
- // This was originally a derived.by but then I got errors on the input binds
173
- function getFulfillmentPolicyList(): PolicyRowWithChecked[] {
174
- const policies = ebayPolicyList?.filter(policy => policy.policyType === 'fulfillment') || []
175
- const selectedPolicies = listing.partnerSpecificDetails.fulfillmentPolicy || []
176
- return policies.map(policy => ({
177
- ...policy,
178
- checked: Array.isArray(selectedPolicies)
179
- ? selectedPolicies.includes(policy.policyId)
180
- : selectedPolicies === policy.policyId,
181
- }))
182
- }
183
-
184
- function getPaymentPolicyListWithChecked(): PolicyRowWithChecked[] {
185
- const policies = ebayPolicyList?.filter(policy => policy.policyType === 'payment') || []
186
- const selectedPolicies = listing.partnerSpecificDetails.paymentPolicy || []
187
- return policies.map(policy => ({
188
- ...policy,
189
- checked: Array.isArray(selectedPolicies)
190
- ? selectedPolicies.includes(policy.policyId)
191
- : selectedPolicies === policy.policyId,
192
- }))
193
- }
194
-
195
- function getReturnPolicyListWithChecked(): PolicyRowWithChecked[] {
196
- const policies = ebayPolicyList?.filter(policy => policy.policyType === 'return') || []
197
- const selectedPolicies = listing.partnerSpecificDetails.returnPolicy || []
198
- return policies.map(policy => ({
199
- ...policy,
200
- checked: Array.isArray(selectedPolicies)
201
- ? selectedPolicies.includes(policy.policyId)
202
- : selectedPolicies === policy.policyId,
203
- }))
204
- }
205
-
206
- function handleFulfillmentPoliciesChange(selectedIds: string[]) {
207
- listing.partnerSpecificDetails.fulfillmentPolicy = selectedIds
208
- handleSave()
209
- }
210
-
211
- function handlePaymentPoliciesChange(selectedIds: string[]) {
212
- listing.partnerSpecificDetails.paymentPolicy = selectedIds
213
- handleSave()
214
- }
215
-
216
- function handleReturnPoliciesChange(selectedIds: string[]) {
217
- listing.partnerSpecificDetails.returnPolicy = selectedIds
218
- handleSave()
219
- }
220
- // Helper that returns any images that have successfully uploaded to eBay to show on Ecommerce page
221
- function getListedImages() {
222
- // Early return if we don't have the necessary data
223
- if (!partFileList.length || !listingList.length) {
224
- return []
225
- }
226
- // See if there's an existing listing row (should be outside of a race condition where part's still saving when they switched tabs)
227
- const existingInventoryListingDetailRow = listingList.find(
228
- listing => listing.ecommercePartnerId === selectedEcommercePartnerId,
229
- )
230
- const uploadedImages = existingInventoryListingDetailRow?.convertedListingDetails?.imageUrls
231
- if (uploadedImages?.length) {
232
- const matchingFilesWithUrl = partFileList.filter(partFile => {
233
- const matchingUploadedUrl = uploadedImages.find(image => image.fileId === partFile.fileId)
234
- return matchingUploadedUrl !== undefined
235
- })
236
- return matchingFilesWithUrl.map(file => ({
237
- ...file,
238
- uuid: uuid(),
239
- }))
240
- }
241
- return []
242
- }
243
-
244
- function getPartnerPackageTypes(): PackageType[] | [] {
245
- const partnerName = selectedPartnerConfig?.name?.toLowerCase()
246
- if (!partnerName || !(partnerName in ecommercePartnerStaticData)) {
247
- return []
248
- }
249
- return ecommercePartnerStaticData[partnerName as keyof typeof ecommercePartnerStaticData].packageTypes
250
- }
251
-
252
- function getPartnerWeightUnits(): string[] {
253
- const partnerName = selectedPartnerConfig?.name?.toLowerCase()
254
- if (!partnerName || !(partnerName in ecommercePartnerStaticData)) {
255
- return []
256
- }
257
- return ecommercePartnerStaticData[partnerName as keyof typeof ecommercePartnerStaticData].weightUnits
258
- }
259
- function getPartnerLengthUnits(): string[] {
260
- const partnerName = selectedPartnerConfig?.name?.toLowerCase()
261
- if (!partnerName || !(partnerName in ecommercePartnerStaticData)) {
262
- return []
263
- }
264
- return ecommercePartnerStaticData[partnerName as keyof typeof ecommercePartnerStaticData].lengthUnits
265
- }
266
-
267
- function getPaymentPolicyList() {
268
- return ebayPolicyList?.filter(policy => policy.policyType === 'payment') || []
269
- }
270
-
271
- function getReturnPolicyList() {
272
- return ebayPolicyList?.filter(policy => policy.policyType === 'return') || []
273
- }
274
- // TODO: This is a temporary function to get the selected partner config, probably not needed in derived
275
- function getSelectedPartnerConfig(): EcommercePartnerConfiguration {
276
- const matchingPartnerConfig = ecommercePartnerConfigurationList.find(
277
- row => row.ecommercePartnerId === selectedEcommercePartnerId,
278
- )
279
- if (!matchingPartnerConfig) {
280
- throw new Error('No matching partner config found')
281
- }
282
- // Ensure defaults property exists
283
- if (!matchingPartnerConfig.defaults) {
284
- matchingPartnerConfig.defaults = {
285
- global: {
286
- listingDescriptionTemplate: '',
287
- listingDuration: 'GTC',
288
- listingFormat: 'FIXED_PRICE',
289
- listingTitleTemplate: '',
290
- },
291
- store: [],
292
- }
293
- }
294
- return matchingPartnerConfig
295
- }
296
- // Function to find matching ecommerce condition from part.condition
297
- function findMatchingEcommerceConditionId(): number | null {
298
- if (!part || !ecommerceConditionList) return null
299
- // TODO: First, check if existing listing and if it has a condition
300
-
301
- // If no part condition, use default if exists
302
- // if (!part.condition) {
303
- // return selectedPartnerConfig?.defaults?.defaultConditionId ?? null
304
- // }
305
-
306
- const matchingMapping = ecommerceConditionMapList.find(mapping => mapping.itrackCondition === part.condition)
307
-
308
- return matchingMapping?.ecommerceConditionId ?? null
309
- }
310
- function findMatchingPartTypeListingConfig(): InventoryTypeListingDefaults | null {
311
- if (!part || !inventoryTypeWithDefaultConfigList) return null
312
- const matchingConfig = inventoryTypeWithDefaultConfigList.find(
313
- config =>
314
- config.ecommercePartnerId === selectedEcommercePartnerId && config.inventoryTypeId === part.inventoryTypeId,
315
- )
316
- return matchingConfig ?? null
317
- }
318
-
319
- function findMatchingPartTypeName(): string | null {
320
- return inventoryTypeList.find(type => type.inventoryTypeId === part.inventoryTypeId)?.name ?? null
321
- }
322
-
323
- function findMatchingLocationKey(): string | null {
324
- if (!selectedPartnerConfig || !part.storeId) {
325
- return null
326
- }
327
- const matchingStoreConfig = selectedPartnerConfig.defaults?.store?.find(
328
- (store: any) => store.storeId === part.storeId,
329
- )
330
- if (matchingStoreConfig) {
331
- return matchingStoreConfig.merchantLocationKey ?? null
332
- }
333
- return null
334
- }
335
-
336
- function findMatchingEcommerceConditionDescription(): string | null {
337
- if (!part || !ecommerceConditionList) return null
338
- // TODO: First, check if existing listing and if it has a condition description set
339
-
340
- // If no part condition, use default if exists
341
- // if (!part.condition) {
342
- // return selectedPartnerConfig?.defaults?.defaultConditionDescription ?? null
343
- // }
344
-
345
- const matchingMapping = ecommerceConditionMapList.find(mapping => mapping.itrackCondition === part.condition)
346
-
347
- return matchingMapping?.description ?? null
348
- }
349
-
350
- function findMatchingEbayCategoryId(): number | null {
351
- if (!part || !ebayCategoryMapList) return null
352
- const matchingMapping = ebayCategoryMapList.find(mapping => mapping.typeNum === part.inventoryTypeId)
353
- return matchingMapping?.ebayCategoryId ?? null
354
- }
355
-
356
- function getMessageType(messageObject: any): string {
357
- if (!messageObject) {
358
- return ''
359
- }
360
- // Try to parse if it's a JSON string
361
- let parsedObject = messageObject
362
- if (typeof messageObject === 'string') {
363
- try {
364
- parsedObject = JSON.parse(messageObject)
365
- } catch {
366
- // If parsing fails but there's data, return 'text'
367
- return messageObject.trim() ? 'text' : 'empty'
368
- }
369
- }
370
-
371
- // If it's an array, get the first object and its first property value
372
- if (Array.isArray(parsedObject)) {
373
- if (parsedObject.length > 0) {
374
- const firstItem = parsedObject[0]
375
- if (typeof firstItem === 'object' && firstItem !== null) {
376
- const keys = Object.keys(firstItem)
377
- if (keys.length > 0) {
378
- const firstKey = keys[0]
379
- return firstItem[firstKey]
380
- }
381
- }
382
- // If first item isn't an object, return 'array'
383
- return parsedObject[0]
384
- }
385
- return ''
386
- }
387
-
388
- // If it's an object, get the first property value
389
- if (typeof parsedObject === 'object' && parsedObject !== null) {
390
- const keys = Object.keys(parsedObject)
391
- if (keys.length > 0) {
392
- const firstKey = keys[0]
393
- return parsedObject[firstKey]
394
- }
395
- }
396
-
397
- // Fallback for any other case with data
398
- return 'unknown'
399
- }
400
-
401
- function formatMessageForDisplay(messageObject: any): string[] {
402
- if (!messageObject) {
403
- return ['No message available']
404
- }
405
-
406
- // If it's an array, extract messages from each item
407
- if (Array.isArray(messageObject)) {
408
- if (messageObject.length === 0) {
409
- return ['No messages']
410
- }
411
-
412
- const allMessages: string[] = []
413
-
414
- messageObject.forEach(item => {
415
- if (typeof item === 'object' && item !== null) {
416
- // Check if it has a 'messages' property (array of actual messages)
417
- if (item.messages && Array.isArray(item.messages)) {
418
- allMessages.push(...item.messages)
419
- } else if (item.message) {
420
- // Single message property
421
- allMessages.push(item.message)
422
- } else {
423
- // Fallback to string representation
424
- allMessages.push(JSON.stringify(item))
425
- }
426
- } else {
427
- allMessages.push(String(item))
428
- }
429
- })
430
- //return allMessages
431
- //return allMessages.join('\n---\n')
432
- return allMessages
433
- }
434
-
435
- // If it's an object, check for message properties
436
- if (typeof messageObject === 'object' && messageObject !== null) {
437
- if (messageObject.messages && Array.isArray(messageObject.messages)) {
438
- return messageObject.messages.join('\n')
439
- } else {
440
- return [JSON.stringify(messageObject, null, 2)]
441
- }
442
- }
443
-
444
- // Fallback for any other type
445
- return [String(messageObject)]
446
- }
447
-
448
- // Handle clicking on listing ID to open external link
449
- // TODO: this isn't very friendly to other partners
450
- function handleListingIdClick(listing: InventoryListingDetail | NewInventoryListingDetail) {
451
- // TODO: Implement logic to construct the appropriate URL based on partner
452
- // For eBay, this might be something like: https://www.ebay.com/itm/{listingId}
453
- const listingId = listing.convertedListingDetails?.listingId
454
- if (selectedPartnerConfig?.name?.toLowerCase() === 'ebay' && listingId) {
455
- // For eBay, open the listing page (this is a placeholder URL structure)
456
- const production = false // TODO: figure out how/where we decide to set this and how to make certain components aware of it
457
- // The api call for get item returns the following format for sandbox https://sandbox.ebay.com/itm/${sku}-/${listingId}
458
- const ebayUrl = production ? `https://ebay.com/itm/${listingId}` : `https://sandbox.ebay.com/itm/${listingId}`
459
- window.open(ebayUrl, '_blank')
460
- }
461
- }
462
-
463
- async function handleSave() {
464
- // If required fields are filled out, set a flag so Agg can pick it up and process it
465
- if (!listing) return
466
- await save(listing)
467
- }
468
-
469
- // Taken from Basic.svelte
470
- function zeroIfNull(value: number | null) {
471
- return value ?? 0
472
- }
473
- </script>
474
-
475
- <div class="form-row">
476
- <div class="col-lg d-flex flex-wrap">
477
- <div class="card hightlight-card mt-4 w-100">
478
- <div class="card-header">{translate('ecommerce.configuration.listingStatus', 'Listing Status')}</div>
479
- {#if listing}
480
- <div class="card-body">
481
- <div class="d-flex flex-wrap align-items-center mb-2">
482
- <div
483
- class="d-flex align-items-center"
484
- style="margin-right: 2rem;"
485
- >
486
- <strong style="margin-right: 0.5rem;">{translate('ecommerce.configuration.status', 'Status')}:</strong>
487
- <span
488
- class="badge"
489
- class:badge-danger={listing.listingStatus === 'error'}
490
- class:badge-warn={listing.listingStatus === 'cancelled'}
491
- class:badge-primary={listing.listingStatus === 'pending'}
492
- class:badge-success={listing.listingStatus === 'listed'}
493
- >
494
- {listing.listingStatus}
495
- </span>
496
- </div>
497
-
498
- <div
499
- class="d-flex align-items-center"
500
- style="margin-right: 2rem;"
501
- >
502
- <strong style="margin-right: 0.5rem;"
503
- >{translate('ecommerce.configuration.lastUpdate', 'Last Update')}:</strong
504
- >
505
- <span>{listing.lastUpdate ? new Date(listing.lastUpdate).toLocaleString() : 'N/A'}</span>
506
- </div>
507
-
508
- {#if listing?.convertedListingDetails?.listingId}
509
- <div class="d-flex align-items-center">
510
- <Button
511
- outline
512
- size="sm"
513
- onclick={() => handleListingIdClick(listing)}
514
- >{translate('ecommerce.configuration.viewOnEbay', 'View On eBay')}</Button
515
- >
516
- </div>
517
- {/if}
518
- {#if listing.message}
519
- <div class="d-flex align-items-center">
520
- <strong style="margin-right: 0.5rem;">{translate('ecommerce.configuration.message', 'Message')}:</strong
521
- >
522
-
523
- <span>{getMessageType(listing.message)}</span>
524
- </div>
525
- {/if}
526
- {#if listing.listingStatus === 'error'}
527
- <div
528
- class="d-flex align-items-center"
529
- style="margin-left: 2rem;"
530
- >
531
- <Button
532
- outline
533
- style="margin-right: 0.5rem;"
534
- color="success"
535
- size="sm"
536
- iconClass="search"
537
- disabled={false}
538
- onclick={() => {
539
- show = true
540
- }}
541
- >
542
- {translate('ecommerce.configuration.viewErrors', 'View Errors')}
543
- </Button>
544
- </div>
545
- {/if}
546
- </div>
547
- </div>
548
- {/if}
549
- </div>
550
- </div>
551
- </div>
552
- <div class="form-row">
553
- <div class="col-lg d-flex flex-wrap">
554
- <div class="card hightlight-card mt-4 w-100">
555
- <div class="card-header">
556
- {translate('ecommerce.configuration.uploadedListingImages', 'Uploaded Listing Images')}
557
- </div>
558
- <div class="card-body">
559
- <div class="form-row">
560
- <div class="col-md">
561
- {#if !listing?.imageUrls?.length && !uploadedListingImages.length}
562
- <p>No attachments found for this part</p>
563
- {:else if listing?.imageUrls?.length && !uploadedListingImages.length && listing.listingStatus === 'pending'}
564
- <p>Images waiting for eBay sync</p>
565
- {:else}
566
- <ThumbnailGrid
567
- fileList={uploadedListingImages}
568
- modificationDisabled={true}
569
- hidePublicFeatures={true}
570
- hideRankFeatures={true}
571
- />
572
- {/if}
573
- </div>
574
- </div>
575
- </div>
576
- </div>
577
- </div>
578
- </div>
579
- <div class="form-row">
580
- <div class="col-lg d-flex flex-wrap">
581
- <div class="card hightlight-card mt-4 w-100">
582
- <div class="card-header">{translate('ecommerce.configuration.partDetails', 'Part Details')}</div>
583
- <div class="card-body">
584
- <div class="form-row">
585
- <div class="col-md">
586
- <div class="form-row">
587
- <div class="col">
588
- <Input
589
- id="sku"
590
- type="text"
591
- label={translate('ecommerce.configuration.ebaySku', 'Ebay SKU')}
592
- autocomplete="off"
593
- placeholder=""
594
- disabled={true}
595
- required={true}
596
- value={listing.sku}
597
- />
598
- </div>
599
- <div class="col">
600
- <Input
601
- id="store"
602
- type="text"
603
- label={translate('ecommerce.configuration.store', 'Store')}
604
- autocomplete="off"
605
- placeholder=""
606
- disabled={true}
607
- required={true}
608
- value={listing.storeId}
609
- />
610
- </div>
611
- <div class="col">
612
- <Input
613
- id="quantity"
614
- inputmode="decimal"
615
- type="number"
616
- class={zeroIfNull(part.quantity) > zeroIfNull(part.maximumQuantity) ||
617
- zeroIfNull(part.quantity) < zeroIfNull(part.minimumQuantity)
618
- ? 'border-danger'
619
- : ''}
620
- label={translate('ecommerce.configuration.quantity', 'Quantity')}
621
- hint={translate('ecommerce.configuration.onHand', 'On Hand')}
622
- disabled={true}
623
- required={true}
624
- value={listing.quantity}
625
- ></Input>
626
- </div>
627
- <div class="col">
628
- <CurrencyInput
629
- id="retail-price"
630
- label={translate('ecommerce.configuration.price', 'Price')}
631
- name="retailprice"
632
- disabled={true}
633
- required={true}
634
- class={(listing.price ?? 0) === 0 ? 'is-invalid' : ''}
635
- value={(listing.price ?? 0).toString()}
636
- />
637
- </div>
638
- </div>
639
- <div class="form-row">
640
- <div class="col">
641
- <Input
642
- id="brand"
643
- type="text"
644
- label={translate('ecommerce.configuration.brand', 'Brand')}
645
- autocomplete="off"
646
- placeholder=""
647
- disabled={disableFields}
648
- bind:value={listing.partnerSpecificDetails.brand}
649
- />
650
- </div>
651
- <div class="col">
652
- <Input
653
- id="mpn"
654
- type="text"
655
- label={translate('ecommerce.configuration.manufPartNumber', 'Manuf. Part Number')}
656
- autocomplete="off"
657
- placeholder=""
658
- disabled={disableFields}
659
- bind:value={listing.manufacturerPartNumber}
660
- />
661
- </div>
662
- <div class="col">
663
- <Input
664
- id="brand"
665
- type="text"
666
- label={translate('ecommerce.configuration.upc', 'UPC')}
667
- autocomplete="off"
668
- placeholder=""
669
- disabled={disableFields}
670
- bind:value={listing.upc}
671
- />
672
- </div>
673
- </div>
674
- <div class="form-row">
675
- <div class="col">
676
- <Select
677
- label={translate('ecommerce.configuration.condition', 'Condition')}
678
- showEmptyOption={true}
679
- emptyValue={null}
680
- emptyText={translate('ecommerce.configuration.selectCondition', 'Select Condition')}
681
- disabled={false}
682
- required={true}
683
- bind:value={listing.ecommerceConditionId}
684
- >
685
- {#each ecommerceConditionList as ecommerceCondition}
686
- <option value={ecommerceCondition.ecommerceConditionId}>{ecommerceCondition.name}</option>
687
- {/each}
688
- </Select>
689
- </div>
690
- <div class="col">
691
- <Input
692
- id="conditionDescription"
693
- type="text"
694
- label={translate('ecommerce.configuration.conditionDescription', 'Condition Description')}
695
- autocomplete="off"
696
- placeholder=""
697
- required={true}
698
- disabled={false}
699
- bind:value={listing.ecommerceConditionDescription}
700
- />
701
- </div>
702
- </div>
703
- <div class="form-row">
704
- <div class="col">
705
- <Textarea
706
- label={translate('ecommerce.configuration.partDescription', 'Part Description')}
707
- rows={4}
708
- disabled={false}
709
- required={true}
710
- class={!listing.inventoryDescription || listing.inventoryDescription.trim() === ''
711
- ? 'is-invalid'
712
- : ''}
713
- bind:value={listing.inventoryDescription}
714
- />
715
- </div>
716
- </div>
717
- </div>
718
- </div>
719
- </div>
720
- </div>
721
- <div class="card hightlight-card mt-4 w-100">
722
- <div class="card-header">{translate('ecommerce.configuration.shippingInfo', 'Shipping Info')}</div>
723
- <div class="card-body">
724
- <div class="form-row">
725
- <div class="col-md">
726
- <Input
727
- id="shipping-weight"
728
- inputmode="decimal"
729
- type="number"
730
- label={translate('ecommerce.configuration.weight', 'Weight')}
731
- disabled={false}
732
- required={shippingFieldRequired}
733
- bind:value={listing.weight}
734
- ></Input>
735
- </div>
736
- <div class="col-md">
737
- <Select
738
- id="shipping-weight-unit"
739
- label={translate('ecommerce.configuration.weightUnit', 'Weight Unit')}
740
- showEmptyOption={true}
741
- emptyValue={null}
742
- emptyText={translate('ecommerce.configuration.selectUnit', 'Select Unit')}
743
- disabled={false}
744
- required={shippingFieldRequired}
745
- bind:value={listing.weightUnit}
746
- >
747
- {#each weightUnits as unit}
748
- <option value={unit}>{unit}</option>
749
- {/each}
750
- </Select>
751
- </div>
752
- </div>
753
- <div class="form-row">
754
- <div class="col-md">
755
- <Input
756
- id="shipping-length"
757
- inputmode="decimal"
758
- type="number"
759
- label={translate('ecommerce.configuration.length', 'Length')}
760
- disabled={false}
761
- required={shippingFieldRequired}
762
- bind:value={listing.shippingLength}
763
- ></Input>
764
- </div>
765
- <div class="col-md">
766
- <Input
767
- id="shipping-width"
768
- inputmode="decimal"
769
- type="number"
770
- label={translate('ecommerce.configuration.width', 'Width')}
771
- disabled={false}
772
- required={shippingFieldRequired}
773
- bind:value={listing.shippingWidth}
774
- ></Input>
775
- </div>
776
- </div>
777
- <div class="form-row">
778
- <div class="col-md">
779
- <Input
780
- id="shipping-height"
781
- inputmode="decimal"
782
- type="number"
783
- label={translate('ecommerce.configuration.height', 'Height')}
784
- disabled={false}
785
- required={shippingFieldRequired}
786
- bind:value={listing.shippingHeight}
787
- ></Input>
788
- </div>
789
- <div class="col-md">
790
- <Select
791
- id="shipping-length-unit"
792
- label={translate('ecommerce.configuration.lengthUnit', 'Length Unit')}
793
- showEmptyOption={true}
794
- emptyValue={null}
795
- emptyText={translate('ecommerce.configuration.selectUnit', 'Select Unit')}
796
- disabled={false}
797
- required={shippingFieldRequired}
798
- bind:value={listing.shippingLengthUnit}
799
- >
800
- {#each lengthUnits as unit}
801
- <option value={unit}>{unit}</option>
802
- {/each}
803
- </Select>
804
- </div>
805
- </div>
806
- <div class="form-row">
807
- <div class="col-md-6">
808
- <Select
809
- label={translate('ecommerce.configuration.packageType', 'Package Type')}
810
- showEmptyOption={true}
811
- emptyValue={null}
812
- emptyText={translate('ecommerce.configuration.selectPackage', 'Select Package')}
813
- required={shippingFieldRequired}
814
- bind:value={listing.shippingPackage}
815
- >
816
- {#each packageTypes as pkg}
817
- <option value={pkg.partnerValue}>{pkg.name}</option>
818
- {/each}
819
- </Select>
820
- </div>
821
- </div>
822
- </div>
823
- </div>
824
- </div>
825
- <div
826
- class="col-lg flex-wrap"
827
- class:d-flex={showElement}
828
- class:d-none={!showElement}
829
- >
830
- <div class="card hightlight-card mt-4 w-100">
831
- <div class="card-header">{translate('ecommerce.configuration.listingDetails', 'Listing Details')}</div>
832
- <div class="card-body">
833
- <div class="form-row">
834
- <div class="col">
835
- <Input
836
- id="listingTitle"
837
- type="text"
838
- label={translate('ecommerce.configuration.title', 'Title')}
839
- autocomplete="off"
840
- placeholder=""
841
- disabled={false}
842
- required={true}
843
- bind:value={listing.listingTitle}
844
- />
845
- </div>
846
- </div>
847
- <div class="form-row">
848
- <div class="col">
849
- <Textarea
850
- label={translate('ecommerce.configuration.description', 'Description')}
851
- rows={6}
852
- disabled={false}
853
- required={true}
854
- class={!listing.listingDescription || listing.listingDescription.trim() === '' ? 'is-invalid' : ''}
855
- bind:value={listing.listingDescription}
856
- />
857
- </div>
858
- </div>
859
- <div class="form-row">
860
- <div class="col">
861
- <Select
862
- label={translate('ecommerce.configuration.ebayCategory', 'Ebay Category')}
863
- showEmptyOption={true}
864
- emptyValue={null}
865
- emptyText={translate('ecommerce.configuration.selectCategory', 'Select Category')}
866
- disabled={false}
867
- required={true}
868
- validation={{
869
- validator: value => {
870
- if (!value) {
871
- false
872
- }
873
- return true
874
- },
875
- }}
876
- bind:value={listing.ecommerceCategoryId}
877
- >
878
- {#each ebayCategoryList as category}
879
- <option value={category.ebayCategoryId}>{category.name}</option>
880
- {/each}
881
- </Select>
882
- </div>
883
- </div>
884
- <div class="form-row">
885
- <div class="col">
886
- <Input
887
- id="fulfillmentTIme"
888
- type="number"
889
- label={translate('ecommerce.configuration.fulfillmentTime', 'Fulfillment Time')}
890
- autocomplete="off"
891
- placeholder=""
892
- disabled={false}
893
- onchange={handleSave}
894
- required={true}
895
- bind:value={listing.fulfillmentTime}
896
- />
897
- </div>
898
- <div class="col">
899
- <Select
900
- label={translate('ecommerce.configuration.fulfillmentTimeUnit', 'Fulfillment Time Unit')}
901
- showEmptyOption={true}
902
- emptyValue={null}
903
- emptyText={translate('ecommerce.configuration.selectUnit', 'Select Unit')}
904
- disabled={false}
905
- required={true}
906
- bind:value={listing.fulfillmentTimeUnit}
907
- >
908
- {#each ecommercePartnerStaticData.ebay.timeUnits as unit}
909
- <option value={unit}>{unit}</option>
910
- {/each}
911
- </Select>
912
- </div>
913
- </div>
914
- <PolicyList
915
- title={translate('ecommerce.configuration.fulfillmentPolicies', 'Fulfillment Policies')}
916
- policies={fulfillmentPolicyList}
917
- selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.fulfillmentPolicy)
918
- ? listing.partnerSpecificDetails.fulfillmentPolicy
919
- : listing.partnerSpecificDetails.fulfillmentPolicy
920
- ? [listing.partnerSpecificDetails.fulfillmentPolicy]
921
- : []}
922
- onSelectionChange={handleFulfillmentPoliciesChange}
923
- />
924
-
925
- <PolicyList
926
- title={translate('ecommerce.configuration.paymentPolicies', 'Payment Policies')}
927
- policies={getPaymentPolicyListWithChecked()}
928
- selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.paymentPolicy)
929
- ? listing.partnerSpecificDetails.paymentPolicy
930
- : listing.partnerSpecificDetails.paymentPolicy
931
- ? [listing.partnerSpecificDetails.paymentPolicy]
932
- : []}
933
- onSelectionChange={handlePaymentPoliciesChange}
934
- />
935
-
936
- <PolicyList
937
- title={translate('ecommerce.configuration.returnPolicies', 'Return Policies')}
938
- policies={getReturnPolicyListWithChecked()}
939
- selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.returnPolicy)
940
- ? listing.partnerSpecificDetails.returnPolicy
941
- : listing.partnerSpecificDetails.returnPolicy
942
- ? [listing.partnerSpecificDetails.returnPolicy]
943
- : []}
944
- onSelectionChange={handleReturnPoliciesChange}
945
- />
946
- </div>
947
- </div>
948
- </div>
949
- </div>
950
- <div class="form-row">
951
- <div class="col-lg d-flex flex-wrap mt-4">
952
- <Button
953
- outline
954
- color="success"
955
- size="sm"
956
- iconClass="plus"
957
- onclick={async () => {
958
- if (
959
- await userPrompt?.confirm(
960
- 'Listing defaults will be overwritten and part or configuration changes will not update the listing',
961
- )
962
- ) {
963
- handleSave()
964
- }
965
- }}
966
- disabled={false}
967
- >
968
- {translate('ecommerce.configuration.manualSave', 'Manual Save')}
969
- </Button>
970
- </div>
971
- </div>
972
-
973
- <UserPrompt bind:this={userPrompt} />
974
- <Modal
975
- bind:show
976
- title={translate('ecommerce.configuration.messageList', 'Message List')}
977
- modalSize="lg"
978
- confirmButtonText={translate('ecommerce.configuration.close', 'Close')}
979
- cancelShown={false}
980
- confirm={() => (show = false)}
981
- close={() => (show = false)}
982
- >
983
- {#each formatMessageForDisplay(listing.message) as msg}
984
- <div>{msg}</div>
985
- {/each}
986
- </Modal>
1
+ <script lang="ts">
2
+ import type { i18n } from 'i18next'
3
+ import { getContext } from 'svelte'
4
+
5
+ import Button from '@isoftdata/svelte-button'
6
+ import CurrencyInput from '@isoftdata/svelte-currency-input'
7
+ import Input from '@isoftdata/svelte-input'
8
+ import Modal from '@isoftdata/svelte-modal'
9
+ import Select from '@isoftdata/svelte-select'
10
+ import Textarea from '@isoftdata/svelte-textarea'
11
+ import PolicyList from './PolicyList.svelte'
12
+ import { ThumbnailGrid } from '@isoftdata/svelte-attachments'
13
+ import UserPrompt from '@isoftdata/svelte-user-prompt'
14
+ import { v4 as uuid } from '@lukeed/uuid'
15
+ import { ecommercePartnerStaticData } from './data/index.js'
16
+ import type { PackageType } from './index.js'
17
+ import { buildEbayListing } from './helpers/index.js'
18
+
19
+ import type {
20
+ EbayCategory,
21
+ EbayPolicy,
22
+ EcommerceCondition,
23
+ ExtendedEcommerceConditionMap,
24
+ EcommercePartnerConfiguration,
25
+ ExtendedEbayCategoryMap,
26
+ FileItem,
27
+ InventoryListingDetail,
28
+ InventoryRow,
29
+ InventoryType,
30
+ InventoryTypeListingDefaults,
31
+ NewInventoryListingDetail,
32
+ PolicyRowWithChecked,
33
+ } from './utils.js'
34
+ import { translate as defaultTranslate } from '@isoftdata/utility-string'
35
+ const { t: translate } = getContext<i18n>('i18next') || { t: defaultTranslate }
36
+
37
+ //
38
+ interface Props {
39
+ ebayCategoryList: Array<EbayCategory>
40
+ ebayCategoryMapList: Array<ExtendedEbayCategoryMap>
41
+ ebayPolicyList: Array<EbayPolicy>
42
+ ecommerceConditionMapList: Array<ExtendedEcommerceConditionMap>
43
+ ecommerceConditionList: Array<EcommerceCondition>
44
+ ecommercePartnerConfigurationList: Array<EcommercePartnerConfiguration>
45
+ listingList: Array<InventoryListingDetail>
46
+ partFileList: Array<FileItem>
47
+ part: InventoryRow
48
+ inventoryTypeWithDefaultConfigList: Array<InventoryTypeListingDefaults>
49
+ inventoryTypeList: Array<InventoryType>
50
+ save: (data: InventoryListingDetail | NewInventoryListingDetail) => Promise<void>
51
+ }
52
+
53
+ let {
54
+ ebayCategoryList,
55
+ ebayCategoryMapList,
56
+ ebayPolicyList,
57
+ ecommerceConditionMapList,
58
+ ecommerceConditionList,
59
+ ecommercePartnerConfigurationList,
60
+ listingList,
61
+ partFileList,
62
+ part,
63
+ inventoryTypeWithDefaultConfigList,
64
+ inventoryTypeList,
65
+ save = async (_: InventoryListingDetail | NewInventoryListingDetail) => {},
66
+ }: Props = $props()
67
+
68
+ let disableFields = $derived(!part.isWorldViewable)
69
+ let fulfillmentPolicyList = $derived.by(getFulfillmentPolicyList)
70
+ let lengthUnits = $derived.by(getPartnerLengthUnits)
71
+ let packageTypes = $derived.by(getPartnerPackageTypes)
72
+ let selectedEcommercePartnerId = $state(1)
73
+ let selectedPartnerConfig = $derived.by(getSelectedPartnerConfig)
74
+ let shippingFieldRequired = true // TODO: in the eBay world, this will need to be set if using calculated shipping but we don't know how we'll check that
75
+ let show = $state(false)
76
+ let showElement = $derived(selectedPartnerConfig?.name === 'ebay')
77
+ let uploadedListingImages = $derived.by(getListedImages)
78
+ let userPrompt: UserPrompt | undefined = $state(undefined)
79
+ let weightUnits = $derived.by(getPartnerWeightUnits)
80
+
81
+ // For images - moved before getOrCreateEcommerceListingObject to avoid initialization error
82
+ let newEcommerceListing = Object.freeze({
83
+ // Listing details
84
+ active: false,
85
+ //inventoryListingDetailId: null,
86
+ convertedListingDetails: null,
87
+ duration: 'GTC',
88
+ ecommercePartnerId: 0,
89
+ ecommerceCategoryId: null,
90
+ ecommerceConditionId: null,
91
+ ecommerceConditionDescription: null,
92
+ fulfillmentTime: null,
93
+ fulfillmentTimeUnit: null,
94
+ listingDescription: null,
95
+ listingStatus: 'pending',
96
+ listingTitle: null,
97
+ // Inventory specifics
98
+ imageUrls: [],
99
+ inventoryDescription: null,
100
+ inventoryId: 0,
101
+ lastUpdate: '',
102
+ manufacturerPartNumber: null,
103
+ message: [],
104
+ price: null,
105
+ quantity: null,
106
+ sku: null,
107
+ storeId: 0,
108
+ upc: '',
109
+ weight: null,
110
+ weightUnit: null,
111
+ shippingLength: null,
112
+ shippingWidth: null,
113
+ shippingHeight: null,
114
+ shippingLengthUnit: null,
115
+ shippingPackage: null,
116
+ // Partner specifics that don't fit into the other categories
117
+ partnerSpecificDetails: {
118
+ brand: null,
119
+ duration: 'GTC',
120
+ listingFormat: 'FIXED_PRICE',
121
+ fulfillmentPolicy: null,
122
+ marketplaceId: 'EBAY_MOTORS',
123
+ merchantLocationKey: null,
124
+ oemNumber: null,
125
+ paymentPolicy: null,
126
+ returnPolicy: null,
127
+ },
128
+ } as NewInventoryListingDetail)
129
+
130
+ let listing: InventoryListingDetail | NewInventoryListingDetail = $state(buildListingFromCurrentState()) // Had to explicitly set the type here
131
+ // New function to build listing using buildEbayListing helper
132
+ function buildListingFromCurrentState(): InventoryListingDetail | NewInventoryListingDetail {
133
+ // Find existing listing for the selected partner
134
+ const existingListing =
135
+ listingList.find(listing => listing.ecommercePartnerId === selectedEcommercePartnerId) || newEcommerceListing
136
+
137
+ // Find matching category mapping
138
+ const categoryMapping = ebayCategoryMapList.find(mapping => mapping.typeNum === part.inventoryTypeId)
139
+
140
+ // Find matching condition mapping
141
+ const conditionMapping = ecommerceConditionMapList.find(mapping => mapping.itrackCondition === part.condition)
142
+
143
+ // Find matching inventory type
144
+ const inventoryType = inventoryTypeList.find(type => type.inventoryTypeId === part.inventoryTypeId)
145
+
146
+ // Convert InventoryTypeListingDefaults to ExtendedInventoryTypeListingDefaults format
147
+ const extendedInventoryTypeListingDefaults = inventoryTypeWithDefaultConfigList.map(config => ({
148
+ ...config,
149
+ categoryName: '', // This will need to be populated from actual category data
150
+ inventoryType: config.inventoryTypeId,
151
+ inventoryTypeName: inventoryType?.name || '',
152
+ }))
153
+
154
+ // Build the input for buildEbayListing
155
+ const buildInput = {
156
+ categoryMapping,
157
+ conditionMapping,
158
+ ecommercePartnerConfiguration: selectedPartnerConfig,
159
+ existingListing,
160
+ inventoryRow: part,
161
+ inventoryTypeListingDefaults: extendedInventoryTypeListingDefaults,
162
+ inventoryType,
163
+ partFileList,
164
+ }
165
+
166
+ // Call buildEbayListing and return the result, or fallback to newEcommerceListing
167
+ const result = buildEbayListing(buildInput)
168
+ return result || newEcommerceListing
169
+ }
170
+
171
+ // This was originally a derived.by but then I got errors on the input binds
172
+ function getFulfillmentPolicyList(): Array<PolicyRowWithChecked> {
173
+ const policies = ebayPolicyList.filter(policy => policy.policyType === 'fulfillment')
174
+ const selectedPolicies = listing.partnerSpecificDetails.fulfillmentPolicy || []
175
+ return policies.map(policy => ({
176
+ ...policy,
177
+ checked: selectedPolicies.includes(policy.policyId),
178
+ }))
179
+ }
180
+
181
+ function getPaymentPolicyListWithChecked(): Array<PolicyRowWithChecked> {
182
+ const policies = ebayPolicyList?.filter(policy => policy.policyType === 'payment') || []
183
+ const selectedPolicies = listing.partnerSpecificDetails.paymentPolicy || []
184
+ return policies.map(policy => ({
185
+ ...policy,
186
+ checked: selectedPolicies.includes(policy.policyId),
187
+ }))
188
+ }
189
+
190
+ function getReturnPolicyListWithChecked(): Array<PolicyRowWithChecked> {
191
+ const policies = ebayPolicyList?.filter(policy => policy.policyType === 'return') || []
192
+ const selectedPolicies = listing.partnerSpecificDetails.returnPolicy || []
193
+ return policies.map(policy => ({
194
+ ...policy,
195
+ checked: selectedPolicies.includes(policy.policyId),
196
+ }))
197
+ }
198
+
199
+ function handleFulfillmentPoliciesChange(selectedIds: Array<string>) {
200
+ listing.partnerSpecificDetails.fulfillmentPolicy = selectedIds
201
+ handleSave()
202
+ }
203
+
204
+ function handlePaymentPoliciesChange(selectedIds: Array<string>) {
205
+ listing.partnerSpecificDetails.paymentPolicy = selectedIds
206
+ handleSave()
207
+ }
208
+
209
+ function handleReturnPoliciesChange(selectedIds: Array<string>) {
210
+ listing.partnerSpecificDetails.returnPolicy = selectedIds
211
+ handleSave()
212
+ }
213
+ // Helper that returns any images that have successfully uploaded to eBay to show on Ecommerce page
214
+ function getListedImages() {
215
+ // Early return if we don't have the necessary data
216
+ if (!partFileList.length || !listingList.length) {
217
+ return []
218
+ }
219
+ // See if there's an existing listing row (should be outside of a race condition where part's still saving when they switched tabs)
220
+ const existingInventoryListingDetailRow = listingList.find(
221
+ listing => listing.ecommercePartnerId === selectedEcommercePartnerId,
222
+ )
223
+ const uploadedImages = existingInventoryListingDetailRow?.convertedListingDetails?.imageUrls
224
+ if (uploadedImages?.length) {
225
+ const matchingFilesWithUrl = partFileList.filter(partFile => {
226
+ const matchingUploadedUrl = uploadedImages.find(image => image.fileId === partFile.fileId)
227
+ return matchingUploadedUrl !== undefined
228
+ })
229
+ return matchingFilesWithUrl.map(file => ({
230
+ ...file,
231
+ uuid: uuid(),
232
+ }))
233
+ }
234
+ return []
235
+ }
236
+
237
+ function getPartnerPackageTypes(): Array<PackageType> | [] {
238
+ const partnerName = selectedPartnerConfig?.name?.toLowerCase()
239
+ if (!partnerName || !(partnerName in ecommercePartnerStaticData)) {
240
+ return []
241
+ }
242
+ return ecommercePartnerStaticData[partnerName as keyof typeof ecommercePartnerStaticData].packageTypes
243
+ }
244
+
245
+ function getPartnerWeightUnits(): Array<string> {
246
+ const partnerName = selectedPartnerConfig?.name?.toLowerCase()
247
+ if (!partnerName || !(partnerName in ecommercePartnerStaticData)) {
248
+ return []
249
+ }
250
+ return ecommercePartnerStaticData[partnerName as keyof typeof ecommercePartnerStaticData].weightUnits
251
+ }
252
+ function getPartnerLengthUnits(): Array<string> {
253
+ const partnerName = selectedPartnerConfig?.name?.toLowerCase()
254
+ if (!partnerName || !(partnerName in ecommercePartnerStaticData)) {
255
+ return []
256
+ }
257
+ return ecommercePartnerStaticData[partnerName as keyof typeof ecommercePartnerStaticData].lengthUnits
258
+ }
259
+
260
+ // TODO: This is a temporary function to get the selected partner config, probably not needed in derived
261
+ function getSelectedPartnerConfig(): EcommercePartnerConfiguration {
262
+ const matchingPartnerConfig = ecommercePartnerConfigurationList.find(
263
+ row => row.ecommercePartnerId === selectedEcommercePartnerId,
264
+ )
265
+ if (!matchingPartnerConfig) {
266
+ throw new Error('No matching partner config found')
267
+ }
268
+ // Ensure defaults property exists
269
+ if (!matchingPartnerConfig.defaults) {
270
+ matchingPartnerConfig.defaults = {
271
+ global: {
272
+ listingDescriptionTemplate: '',
273
+ listingDuration: 'GTC',
274
+ listingFormat: 'FIXED_PRICE',
275
+ listingTitleTemplate: '',
276
+ },
277
+ store: [],
278
+ }
279
+ }
280
+ return matchingPartnerConfig
281
+ }
282
+
283
+ function getMessageType(messageObject: any): string {
284
+ if (!messageObject) {
285
+ return ''
286
+ }
287
+ // Try to parse if it's a JSON string
288
+ let parsedObject = messageObject
289
+ if (typeof messageObject === 'string') {
290
+ try {
291
+ parsedObject = JSON.parse(messageObject)
292
+ } catch {
293
+ // If parsing fails but there's data, return 'text'
294
+ return messageObject.trim() ? 'text' : 'empty'
295
+ }
296
+ }
297
+
298
+ // If it's an array, get the first object and its first property value
299
+ if (Array.isArray(parsedObject)) {
300
+ if (parsedObject.length > 0) {
301
+ const firstItem = parsedObject[0]
302
+ if (typeof firstItem === 'object' && firstItem !== null) {
303
+ const keys = Object.keys(firstItem)
304
+ if (keys.length > 0) {
305
+ const firstKey = keys[0]
306
+ return firstItem[firstKey]
307
+ }
308
+ }
309
+ // If first item isn't an object, return 'array'
310
+ return parsedObject[0]
311
+ }
312
+ return ''
313
+ }
314
+
315
+ // If it's an object, get the first property value
316
+ if (typeof parsedObject === 'object' && parsedObject !== null) {
317
+ const keys = Object.keys(parsedObject)
318
+ if (keys.length > 0) {
319
+ const firstKey = keys[0]
320
+ return parsedObject[firstKey]
321
+ }
322
+ }
323
+
324
+ // Fallback for any other case with data
325
+ return 'unknown'
326
+ }
327
+
328
+ function formatMessageForDisplay(messageObject: any): Array<string> {
329
+ if (!messageObject) {
330
+ return ['No message available']
331
+ }
332
+
333
+ // If it's an array, extract messages from each item
334
+ if (Array.isArray(messageObject)) {
335
+ if (messageObject.length === 0) {
336
+ return ['No messages']
337
+ }
338
+
339
+ const allMessages: Array<string> = []
340
+
341
+ messageObject.forEach(item => {
342
+ if (typeof item === 'object' && item !== null) {
343
+ // Check if it has a 'messages' property (array of actual messages)
344
+ if (item.messages && Array.isArray(item.messages)) {
345
+ allMessages.push(...item.messages)
346
+ } else if (item.message) {
347
+ // Single message property
348
+ allMessages.push(item.message)
349
+ } else {
350
+ // Fallback to string representation
351
+ allMessages.push(JSON.stringify(item))
352
+ }
353
+ } else {
354
+ allMessages.push(String(item))
355
+ }
356
+ })
357
+ //return allMessages
358
+ //return allMessages.join('\n---\n')
359
+ return allMessages
360
+ }
361
+
362
+ // If it's an object, check for message properties
363
+ if (typeof messageObject === 'object' && messageObject !== null) {
364
+ if (messageObject.messages && Array.isArray(messageObject.messages)) {
365
+ return messageObject.messages.join('\n')
366
+ } else {
367
+ return [JSON.stringify(messageObject, null, 2)]
368
+ }
369
+ }
370
+
371
+ // Fallback for any other type
372
+ return [String(messageObject)]
373
+ }
374
+
375
+ // Handle clicking on listing ID to open external link
376
+ // TODO: this isn't very friendly to other partners
377
+ function handleListingIdClick(listing: InventoryListingDetail | NewInventoryListingDetail) {
378
+ // TODO: Implement logic to construct the appropriate URL based on partner
379
+ // For eBay, this might be something like: https://www.ebay.com/itm/{listingId}
380
+ const listingId = listing.convertedListingDetails?.listingId
381
+ if (selectedPartnerConfig?.name?.toLowerCase() === 'ebay' && listingId) {
382
+ // For eBay, open the listing page (this is a placeholder URL structure)
383
+ const production = false // TODO: figure out how/where we decide to set this and how to make certain components aware of it
384
+ // The api call for get item returns the following format for sandbox https://sandbox.ebay.com/itm/${sku}-/${listingId}
385
+ const ebayUrl = production ? `https://ebay.com/itm/${listingId}` : `https://sandbox.ebay.com/itm/${listingId}`
386
+ window.open(ebayUrl, '_blank')
387
+ }
388
+ }
389
+
390
+ async function handleSave() {
391
+ // If required fields are filled out, set a flag so Agg can pick it up and process it
392
+ if (!listing) return
393
+ await save(listing)
394
+ }
395
+
396
+ // Taken from Basic.svelte
397
+ function zeroIfNull(value: number | null) {
398
+ return value ?? 0
399
+ }
400
+ </script>
401
+
402
+ <div class="form-row">
403
+ <div class="col-lg d-flex flex-wrap">
404
+ <div class="card hightlight-card mt-4 w-100">
405
+ <div class="card-header">{translate('ecommerce.configuration.listingStatus', 'Listing Status')}</div>
406
+ {#if listing}
407
+ <div class="card-body">
408
+ <div class="d-flex flex-wrap align-items-center mb-2">
409
+ <div
410
+ class="d-flex align-items-center"
411
+ style="margin-right: 2rem;"
412
+ >
413
+ <strong style="margin-right: 0.5rem;">{translate('ecommerce.configuration.status', 'Status')}:</strong>
414
+ <span
415
+ class="badge"
416
+ class:badge-danger={listing.listingStatus === 'error'}
417
+ class:badge-warn={listing.listingStatus === 'cancelled'}
418
+ class:badge-primary={listing.listingStatus === 'pending'}
419
+ class:badge-success={listing.listingStatus === 'listed'}
420
+ >
421
+ {listing.listingStatus}
422
+ </span>
423
+ </div>
424
+
425
+ <div
426
+ class="d-flex align-items-center"
427
+ style="margin-right: 2rem;"
428
+ >
429
+ <strong style="margin-right: 0.5rem;"
430
+ >{translate('ecommerce.configuration.lastUpdate', 'Last Update')}:</strong
431
+ >
432
+ <span>{listing.lastUpdate ? new Date(listing.lastUpdate).toLocaleString() : 'N/A'}</span>
433
+ </div>
434
+
435
+ {#if listing?.convertedListingDetails?.listingId}
436
+ <div class="d-flex align-items-center">
437
+ <Button
438
+ outline
439
+ size="sm"
440
+ onclick={() => handleListingIdClick(listing)}
441
+ >{translate('ecommerce.configuration.viewOnEbay', 'View On eBay')}</Button
442
+ >
443
+ </div>
444
+ {/if}
445
+ {#if listing.message}
446
+ <div class="d-flex align-items-center">
447
+ <strong style="margin-right: 0.5rem;">{translate('ecommerce.configuration.message', 'Message')}:</strong
448
+ >
449
+
450
+ <span>{getMessageType(listing.message)}</span>
451
+ </div>
452
+ {/if}
453
+ {#if listing.listingStatus === 'error'}
454
+ <div
455
+ class="d-flex align-items-center"
456
+ style="margin-left: 2rem;"
457
+ >
458
+ <Button
459
+ outline
460
+ style="margin-right: 0.5rem;"
461
+ color="success"
462
+ size="sm"
463
+ iconClass="search"
464
+ onclick={() => {
465
+ show = true
466
+ }}
467
+ >
468
+ {translate('ecommerce.configuration.viewErrors', 'View Errors')}...
469
+ </Button>
470
+ </div>
471
+ {/if}
472
+ </div>
473
+ </div>
474
+ {/if}
475
+ </div>
476
+ </div>
477
+ </div>
478
+ <div class="form-row">
479
+ <div class="col-lg d-flex flex-wrap">
480
+ <div class="card hightlight-card mt-4 w-100">
481
+ <div class="card-header">
482
+ {translate('ecommerce.configuration.uploadedListingImages', 'Uploaded Listing Images')}
483
+ </div>
484
+ <div class="card-body">
485
+ <div class="form-row">
486
+ <div class="col-md">
487
+ {#if !listing?.imageUrls?.length && !uploadedListingImages.length}
488
+ <p>{translate('ecommerce.configuration.noAttachmentsFound', 'No attachments found for this part')}</p>
489
+ {:else if listing?.imageUrls?.length && !uploadedListingImages.length && listing.listingStatus === 'pending'}
490
+ <p>{translate('ecommerce.configuration.waitingEbaySync', 'Images waiting for eBay sync')}</p>
491
+ {:else}
492
+ <ThumbnailGrid
493
+ fileList={uploadedListingImages}
494
+ modificationDisabled
495
+ hidePublicFeatures
496
+ hideRankFeatures
497
+ />
498
+ {/if}
499
+ </div>
500
+ </div>
501
+ </div>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ <div class="form-row">
506
+ <div class="col-lg d-flex flex-wrap">
507
+ <div class="card hightlight-card mt-4 w-100">
508
+ <div class="card-header">{translate('ecommerce.configuration.partDetails', 'Part Details')}</div>
509
+ <div class="card-body">
510
+ <div class="form-row">
511
+ <div class="col-md">
512
+ <div class="form-row">
513
+ <div class="col">
514
+ <Input
515
+ type="text"
516
+ label={translate('ecommerce.configuration.ebaySku', 'Ebay SKU')}
517
+ disabled={true}
518
+ required={true}
519
+ value={listing.sku}
520
+ />
521
+ </div>
522
+ <div class="col">
523
+ <Input
524
+ type="text"
525
+ label={translate('ecommerce.configuration.store', 'Store')}
526
+ disabled={true}
527
+ required={true}
528
+ value={listing.storeId}
529
+ />
530
+ </div>
531
+ <div class="col">
532
+ <Input
533
+ inputmode="decimal"
534
+ type="number"
535
+ class={zeroIfNull(part.quantity) > zeroIfNull(part.maximumQuantity) ||
536
+ zeroIfNull(part.quantity) < zeroIfNull(part.minimumQuantity)
537
+ ? 'border-danger'
538
+ : ''}
539
+ label={translate('ecommerce.configuration.quantity', 'Quantity')}
540
+ hint={translate('ecommerce.configuration.onHand', 'On Hand')}
541
+ disabled={true}
542
+ required={true}
543
+ value={listing.quantity}
544
+ ></Input>
545
+ </div>
546
+ <div class="col">
547
+ <CurrencyInput
548
+ label={translate('ecommerce.configuration.price', 'Price')}
549
+ name="retailprice"
550
+ disabled={true}
551
+ required={true}
552
+ class={(listing.price ?? 0) === 0 ? 'is-invalid' : ''}
553
+ value={(listing.price ?? 0).toString()}
554
+ />
555
+ </div>
556
+ </div>
557
+ <div class="form-row">
558
+ <div class="col">
559
+ <Input
560
+ type="text"
561
+ label={translate('ecommerce.configuration.brand', 'Brand')}
562
+ disabled={disableFields}
563
+ bind:value={listing.partnerSpecificDetails.brand}
564
+ />
565
+ </div>
566
+ <div class="col">
567
+ <Input
568
+ type="text"
569
+ label={translate('ecommerce.configuration.manufPartNumber', 'Manuf. Part Number')}
570
+ disabled={disableFields}
571
+ bind:value={listing.manufacturerPartNumber}
572
+ />
573
+ </div>
574
+ <div class="col">
575
+ <Input
576
+ type="text"
577
+ label={translate('ecommerce.configuration.upc', 'UPC')}
578
+ disabled={disableFields}
579
+ bind:value={listing.upc}
580
+ />
581
+ </div>
582
+ </div>
583
+ <div class="form-row">
584
+ <div class="col">
585
+ <Select
586
+ label={translate('ecommerce.configuration.condition', 'Condition')}
587
+ emptyText="-- {translate('ecommerce.configuration.selectCondition', 'Select Condition')} --"
588
+ required={true}
589
+ bind:value={listing.ecommerceConditionId}
590
+ >
591
+ {#each ecommerceConditionList as ecommerceCondition}
592
+ <option value={ecommerceCondition.ecommerceConditionId}>{ecommerceCondition.name}</option>
593
+ {/each}
594
+ </Select>
595
+ </div>
596
+ <div class="col">
597
+ <Input
598
+ type="text"
599
+ label={translate('ecommerce.configuration.conditionDescription', 'Condition Description')}
600
+ required={true}
601
+ bind:value={listing.ecommerceConditionDescription}
602
+ />
603
+ </div>
604
+ </div>
605
+ <div class="form-row">
606
+ <div class="col">
607
+ <Textarea
608
+ label={translate('ecommerce.configuration.partDescription', 'Part Description')}
609
+ rows={4}
610
+ required={true}
611
+ class={{ 'is-invalid': !listing.inventoryDescription?.trim() }}
612
+ bind:value={listing.inventoryDescription}
613
+ />
614
+ </div>
615
+ </div>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+ <div class="card hightlight-card mt-4 w-100">
621
+ <div class="card-header">{translate('ecommerce.configuration.shippingInfo', 'Shipping Info')}</div>
622
+ <div class="card-body">
623
+ <div class="form-row">
624
+ <div class="col-md">
625
+ <Input
626
+ inputmode="decimal"
627
+ type="number"
628
+ label={translate('ecommerce.configuration.weight', 'Weight')}
629
+ required={shippingFieldRequired}
630
+ bind:value={listing.weight}
631
+ ></Input>
632
+ </div>
633
+ <div class="col-md">
634
+ <Select
635
+ label={translate('ecommerce.configuration.weightUnit', 'Weight Unit')}
636
+ emptyText="-- {translate('ecommerce.configuration.selectUnit', 'Select Unit')} --"
637
+ required={shippingFieldRequired}
638
+ bind:value={listing.weightUnit}
639
+ >
640
+ {#each weightUnits as unit}
641
+ <option value={unit}>{unit}</option>
642
+ {/each}
643
+ </Select>
644
+ </div>
645
+ </div>
646
+ <div class="form-row">
647
+ <div class="col-md">
648
+ <Input
649
+ inputmode="decimal"
650
+ type="number"
651
+ label={translate('ecommerce.configuration.length', 'Length')}
652
+ required={shippingFieldRequired}
653
+ bind:value={listing.shippingLength}
654
+ ></Input>
655
+ </div>
656
+ <div class="col-md">
657
+ <Input
658
+ inputmode="decimal"
659
+ type="number"
660
+ label={translate('ecommerce.configuration.width', 'Width')}
661
+ required={shippingFieldRequired}
662
+ bind:value={listing.shippingWidth}
663
+ ></Input>
664
+ </div>
665
+ </div>
666
+ <div class="form-row">
667
+ <div class="col-md">
668
+ <Input
669
+ inputmode="decimal"
670
+ type="number"
671
+ label={translate('ecommerce.configuration.height', 'Height')}
672
+ required={shippingFieldRequired}
673
+ bind:value={listing.shippingHeight}
674
+ ></Input>
675
+ </div>
676
+ <div class="col-md">
677
+ <Select
678
+ label={translate('ecommerce.configuration.lengthUnit', 'Length Unit')}
679
+ emptyText="-- {translate('ecommerce.configuration.selectUnit', 'Select Unit')} --"
680
+ required={shippingFieldRequired}
681
+ bind:value={listing.shippingLengthUnit}
682
+ >
683
+ {#each lengthUnits as unit}
684
+ <option value={unit}>{unit}</option>
685
+ {/each}
686
+ </Select>
687
+ </div>
688
+ </div>
689
+ <div class="form-row">
690
+ <div class="col-md-6">
691
+ <Select
692
+ label={translate('ecommerce.configuration.packageType', 'Package Type')}
693
+ emptyText="-- {translate('ecommerce.configuration.selectPackage', 'Select Package')} --"
694
+ required={shippingFieldRequired}
695
+ bind:value={listing.shippingPackage}
696
+ >
697
+ {#each packageTypes as pkg}
698
+ <option value={pkg.partnerValue}>{pkg.name}</option>
699
+ {/each}
700
+ </Select>
701
+ </div>
702
+ </div>
703
+ </div>
704
+ </div>
705
+ </div>
706
+ <div
707
+ class="col-lg flex-wrap"
708
+ class:d-flex={showElement}
709
+ class:d-none={!showElement}
710
+ >
711
+ <div class="card hightlight-card mt-4 w-100">
712
+ <div class="card-header">{translate('ecommerce.configuration.listingDetails', 'Listing Details')}</div>
713
+ <div class="card-body">
714
+ <div class="form-row">
715
+ <div class="col">
716
+ <Input
717
+ type="text"
718
+ label={translate('ecommerce.configuration.title', 'Title')}
719
+ required={true}
720
+ bind:value={listing.listingTitle}
721
+ />
722
+ </div>
723
+ </div>
724
+ <div class="form-row">
725
+ <div class="col">
726
+ <Textarea
727
+ label={translate('ecommerce.configuration.description', 'Description')}
728
+ rows={6}
729
+ required={true}
730
+ class={!listing.listingDescription || listing.listingDescription.trim() === '' ? 'is-invalid' : ''}
731
+ bind:value={listing.listingDescription}
732
+ />
733
+ </div>
734
+ </div>
735
+ <div class="form-row">
736
+ <div class="col">
737
+ <Select
738
+ label={translate('ecommerce.configuration.ebayCategory', 'Ebay Category')}
739
+ emptyText="-- {translate('ecommerce.configuration.selectCategory', 'Select Category')} --"
740
+ required={true}
741
+ validation={{
742
+ validator: value => {
743
+ if (!value) {
744
+ false
745
+ }
746
+ return true
747
+ },
748
+ }}
749
+ bind:value={listing.ecommerceCategoryId}
750
+ >
751
+ {#each ebayCategoryList as category}
752
+ <option value={category.ebayCategoryId}>{category.name}</option>
753
+ {/each}
754
+ </Select>
755
+ </div>
756
+ </div>
757
+ <div class="form-row">
758
+ <div class="col">
759
+ <Input
760
+ type="number"
761
+ label={translate('ecommerce.configuration.fulfillmentTime', 'Fulfillment Time')}
762
+ onchange={handleSave}
763
+ required={true}
764
+ bind:value={listing.fulfillmentTime}
765
+ />
766
+ </div>
767
+ <div class="col">
768
+ <Select
769
+ label={translate('ecommerce.configuration.fulfillmentTimeUnit', 'Fulfillment Time Unit')}
770
+ emptyText="-- {translate('ecommerce.configuration.selectUnit', 'Select Unit')} --"
771
+ required={true}
772
+ bind:value={listing.fulfillmentTimeUnit}
773
+ >
774
+ {#each ecommercePartnerStaticData.ebay.timeUnits as unit}
775
+ <option value={unit}>{unit}</option>
776
+ {/each}
777
+ </Select>
778
+ </div>
779
+ </div>
780
+ <PolicyList
781
+ title={translate('ecommerce.configuration.fulfillmentPolicies', 'Fulfillment Policies')}
782
+ policies={fulfillmentPolicyList}
783
+ selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.fulfillmentPolicy)
784
+ ? listing.partnerSpecificDetails.fulfillmentPolicy
785
+ : listing.partnerSpecificDetails.fulfillmentPolicy
786
+ ? [listing.partnerSpecificDetails.fulfillmentPolicy]
787
+ : []}
788
+ onSelectionChange={handleFulfillmentPoliciesChange}
789
+ />
790
+
791
+ <PolicyList
792
+ title={translate('ecommerce.configuration.paymentPolicies', 'Payment Policies')}
793
+ policies={getPaymentPolicyListWithChecked()}
794
+ selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.paymentPolicy)
795
+ ? listing.partnerSpecificDetails.paymentPolicy
796
+ : listing.partnerSpecificDetails.paymentPolicy
797
+ ? [listing.partnerSpecificDetails.paymentPolicy]
798
+ : []}
799
+ onSelectionChange={handlePaymentPoliciesChange}
800
+ />
801
+
802
+ <PolicyList
803
+ title={translate('ecommerce.configuration.returnPolicies', 'Return Policies')}
804
+ policies={getReturnPolicyListWithChecked()}
805
+ selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.returnPolicy)
806
+ ? listing.partnerSpecificDetails.returnPolicy
807
+ : listing.partnerSpecificDetails.returnPolicy
808
+ ? [listing.partnerSpecificDetails.returnPolicy]
809
+ : []}
810
+ onSelectionChange={handleReturnPoliciesChange}
811
+ />
812
+ </div>
813
+ </div>
814
+ </div>
815
+ </div>
816
+ <div class="form-row">
817
+ <div class="col-lg d-flex flex-wrap mt-4">
818
+ <Button
819
+ outline
820
+ color="success"
821
+ size="sm"
822
+ iconClass="plus"
823
+ onclick={async () => {
824
+ if (
825
+ await userPrompt?.confirm(
826
+ translate(
827
+ 'ecommerce.configuration.saveConfirm',
828
+ 'Listing defaults will be overwritten and part or configuration changes will not update the listing',
829
+ ),
830
+ )
831
+ ) {
832
+ handleSave()
833
+ }
834
+ }}
835
+ >
836
+ {translate('ecommerce.configuration.manualSave', 'Manual Save')}
837
+ </Button>
838
+ </div>
839
+ </div>
840
+
841
+ <UserPrompt bind:this={userPrompt} />
842
+ <Modal
843
+ bind:show
844
+ title={translate('ecommerce.configuration.messageList', 'Message List')}
845
+ modalSize="lg"
846
+ confirmShown={false}
847
+ close={() => (show = false)}
848
+ >
849
+ {#each formatMessageForDisplay(listing.message) as msg}
850
+ <div>{msg}</div>
851
+ {/each}
852
+ </Modal>