@isoftdata/svelte-ecommerce 1.0.0-beta.4 → 1.0.0-beta.6

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,852 +1,854 @@
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>
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
+ pricingModifier: 100,
277
+ defaultPriceType: ''
278
+ },
279
+ store: [],
280
+ }
281
+ }
282
+ return matchingPartnerConfig
283
+ }
284
+
285
+ function getMessageType(messageObject: any): string {
286
+ if (!messageObject) {
287
+ return ''
288
+ }
289
+ // Try to parse if it's a JSON string
290
+ let parsedObject = messageObject
291
+ if (typeof messageObject === 'string') {
292
+ try {
293
+ parsedObject = JSON.parse(messageObject)
294
+ } catch {
295
+ // If parsing fails but there's data, return 'text'
296
+ return messageObject.trim() ? 'text' : 'empty'
297
+ }
298
+ }
299
+
300
+ // If it's an array, get the first object and its first property value
301
+ if (Array.isArray(parsedObject)) {
302
+ if (parsedObject.length > 0) {
303
+ const firstItem = parsedObject[0]
304
+ if (typeof firstItem === 'object' && firstItem !== null) {
305
+ const keys = Object.keys(firstItem)
306
+ if (keys.length > 0) {
307
+ const firstKey = keys[0]
308
+ return firstItem[firstKey]
309
+ }
310
+ }
311
+ // If first item isn't an object, return 'array'
312
+ return parsedObject[0]
313
+ }
314
+ return ''
315
+ }
316
+
317
+ // If it's an object, get the first property value
318
+ if (typeof parsedObject === 'object' && parsedObject !== null) {
319
+ const keys = Object.keys(parsedObject)
320
+ if (keys.length > 0) {
321
+ const firstKey = keys[0]
322
+ return parsedObject[firstKey]
323
+ }
324
+ }
325
+
326
+ // Fallback for any other case with data
327
+ return 'unknown'
328
+ }
329
+
330
+ function formatMessageForDisplay(messageObject: any): Array<string> {
331
+ if (!messageObject) {
332
+ return ['No message available']
333
+ }
334
+
335
+ // If it's an array, extract messages from each item
336
+ if (Array.isArray(messageObject)) {
337
+ if (messageObject.length === 0) {
338
+ return ['No messages']
339
+ }
340
+
341
+ const allMessages: Array<string> = []
342
+
343
+ messageObject.forEach(item => {
344
+ if (typeof item === 'object' && item !== null) {
345
+ // Check if it has a 'messages' property (array of actual messages)
346
+ if (item.messages && Array.isArray(item.messages)) {
347
+ allMessages.push(...item.messages)
348
+ } else if (item.message) {
349
+ // Single message property
350
+ allMessages.push(item.message)
351
+ } else {
352
+ // Fallback to string representation
353
+ allMessages.push(JSON.stringify(item))
354
+ }
355
+ } else {
356
+ allMessages.push(String(item))
357
+ }
358
+ })
359
+ //return allMessages
360
+ //return allMessages.join('\n---\n')
361
+ return allMessages
362
+ }
363
+
364
+ // If it's an object, check for message properties
365
+ if (typeof messageObject === 'object' && messageObject !== null) {
366
+ if (messageObject.messages && Array.isArray(messageObject.messages)) {
367
+ return messageObject.messages.join('\n')
368
+ } else {
369
+ return [JSON.stringify(messageObject, null, 2)]
370
+ }
371
+ }
372
+
373
+ // Fallback for any other type
374
+ return [String(messageObject)]
375
+ }
376
+
377
+ // Handle clicking on listing ID to open external link
378
+ // TODO: this isn't very friendly to other partners
379
+ function handleListingIdClick(listing: InventoryListingDetail | NewInventoryListingDetail) {
380
+ // TODO: Implement logic to construct the appropriate URL based on partner
381
+ // For eBay, this might be something like: https://www.ebay.com/itm/{listingId}
382
+ const listingId = listing.convertedListingDetails?.listingId
383
+ if (selectedPartnerConfig?.name?.toLowerCase() === 'ebay' && listingId) {
384
+ // For eBay, open the listing page (this is a placeholder URL structure)
385
+ const production = false // TODO: figure out how/where we decide to set this and how to make certain components aware of it
386
+ // The api call for get item returns the following format for sandbox https://sandbox.ebay.com/itm/${sku}-/${listingId}
387
+ const ebayUrl = production ? `https://ebay.com/itm/${listingId}` : `https://sandbox.ebay.com/itm/${listingId}`
388
+ window.open(ebayUrl, '_blank')
389
+ }
390
+ }
391
+
392
+ async function handleSave() {
393
+ // If required fields are filled out, set a flag so Agg can pick it up and process it
394
+ if (!listing) return
395
+ await save(listing)
396
+ }
397
+
398
+ // Taken from Basic.svelte
399
+ function zeroIfNull(value: number | null) {
400
+ return value ?? 0
401
+ }
402
+ </script>
403
+
404
+ <div class="form-row">
405
+ <div class="col-lg d-flex flex-wrap">
406
+ <div class="card hightlight-card mt-4 w-100">
407
+ <div class="card-header">{translate('ecommerce.configuration.listingStatus', 'Listing Status')}</div>
408
+ {#if listing}
409
+ <div class="card-body">
410
+ <div class="d-flex flex-wrap align-items-center mb-2">
411
+ <div
412
+ class="d-flex align-items-center"
413
+ style="margin-right: 2rem;"
414
+ >
415
+ <strong style="margin-right: 0.5rem;">{translate('ecommerce.configuration.status', 'Status')}:</strong>
416
+ <span
417
+ class="badge"
418
+ class:badge-danger={listing.listingStatus === 'error'}
419
+ class:badge-warn={listing.listingStatus === 'cancelled'}
420
+ class:badge-primary={listing.listingStatus === 'pending'}
421
+ class:badge-success={listing.listingStatus === 'listed'}
422
+ >
423
+ {listing.listingStatus}
424
+ </span>
425
+ </div>
426
+
427
+ <div
428
+ class="d-flex align-items-center"
429
+ style="margin-right: 2rem;"
430
+ >
431
+ <strong style="margin-right: 0.5rem;"
432
+ >{translate('ecommerce.configuration.lastUpdate', 'Last Update')}:</strong
433
+ >
434
+ <span>{listing.lastUpdate ? new Date(listing.lastUpdate).toLocaleString() : 'N/A'}</span>
435
+ </div>
436
+
437
+ {#if listing?.convertedListingDetails?.listingId}
438
+ <div class="d-flex align-items-center">
439
+ <Button
440
+ outline
441
+ size="sm"
442
+ onclick={() => handleListingIdClick(listing)}
443
+ >{translate('ecommerce.configuration.viewOnEbay', 'View On eBay')}</Button
444
+ >
445
+ </div>
446
+ {/if}
447
+ {#if listing.message}
448
+ <div class="d-flex align-items-center">
449
+ <strong style="margin-right: 0.5rem;">{translate('ecommerce.configuration.message', 'Message')}:</strong
450
+ >
451
+
452
+ <span>{getMessageType(listing.message)}</span>
453
+ </div>
454
+ {/if}
455
+ {#if listing.listingStatus === 'error'}
456
+ <div
457
+ class="d-flex align-items-center"
458
+ style="margin-left: 2rem;"
459
+ >
460
+ <Button
461
+ outline
462
+ style="margin-right: 0.5rem;"
463
+ color="success"
464
+ size="sm"
465
+ iconClass="search"
466
+ onclick={() => {
467
+ show = true
468
+ }}
469
+ >
470
+ {translate('ecommerce.configuration.viewErrors', 'View Errors')}...
471
+ </Button>
472
+ </div>
473
+ {/if}
474
+ </div>
475
+ </div>
476
+ {/if}
477
+ </div>
478
+ </div>
479
+ </div>
480
+ <div class="form-row">
481
+ <div class="col-lg d-flex flex-wrap">
482
+ <div class="card hightlight-card mt-4 w-100">
483
+ <div class="card-header">
484
+ {translate('ecommerce.configuration.uploadedListingImages', 'Uploaded Listing Images')}
485
+ </div>
486
+ <div class="card-body">
487
+ <div class="form-row">
488
+ <div class="col-md">
489
+ {#if !listing?.imageUrls?.length && !uploadedListingImages.length}
490
+ <p>{translate('ecommerce.configuration.noAttachmentsFound', 'No attachments found for this part')}</p>
491
+ {:else if listing?.imageUrls?.length && !uploadedListingImages.length && listing.listingStatus === 'pending'}
492
+ <p>{translate('ecommerce.configuration.waitingEbaySync', 'Images waiting for eBay sync')}</p>
493
+ {:else}
494
+ <ThumbnailGrid
495
+ fileList={uploadedListingImages}
496
+ modificationDisabled
497
+ hidePublicFeatures
498
+ hideRankFeatures
499
+ />
500
+ {/if}
501
+ </div>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ </div>
506
+ </div>
507
+ <div class="form-row">
508
+ <div class="col-lg d-flex flex-wrap">
509
+ <div class="card hightlight-card mt-4 w-100">
510
+ <div class="card-header">{translate('ecommerce.configuration.partDetails', 'Part Details')}</div>
511
+ <div class="card-body">
512
+ <div class="form-row">
513
+ <div class="col-md">
514
+ <div class="form-row">
515
+ <div class="col">
516
+ <Input
517
+ type="text"
518
+ label={translate('ecommerce.configuration.ebaySku', 'Ebay SKU')}
519
+ disabled={true}
520
+ required={true}
521
+ value={listing.sku}
522
+ />
523
+ </div>
524
+ <div class="col">
525
+ <Input
526
+ type="text"
527
+ label={translate('ecommerce.configuration.store', 'Store')}
528
+ disabled={true}
529
+ required={true}
530
+ value={listing.storeId}
531
+ />
532
+ </div>
533
+ <div class="col">
534
+ <Input
535
+ inputmode="decimal"
536
+ type="number"
537
+ class={zeroIfNull(part.quantity) > zeroIfNull(part.maximumQuantity) ||
538
+ zeroIfNull(part.quantity) < zeroIfNull(part.minimumQuantity)
539
+ ? 'border-danger'
540
+ : ''}
541
+ label={translate('ecommerce.configuration.quantity', 'Quantity')}
542
+ hint={translate('ecommerce.configuration.onHand', 'On Hand')}
543
+ disabled={true}
544
+ required={true}
545
+ value={listing.quantity}
546
+ ></Input>
547
+ </div>
548
+ <div class="col">
549
+ <CurrencyInput
550
+ label={translate('ecommerce.configuration.price', 'Price')}
551
+ name="retailprice"
552
+ disabled={true}
553
+ required={true}
554
+ class={(listing.price ?? 0) === 0 ? 'is-invalid' : ''}
555
+ value={(listing.price ?? 0).toString()}
556
+ />
557
+ </div>
558
+ </div>
559
+ <div class="form-row">
560
+ <div class="col">
561
+ <Input
562
+ type="text"
563
+ label={translate('ecommerce.configuration.brand', 'Brand')}
564
+ disabled={disableFields}
565
+ bind:value={listing.partnerSpecificDetails.brand}
566
+ />
567
+ </div>
568
+ <div class="col">
569
+ <Input
570
+ type="text"
571
+ label={translate('ecommerce.configuration.manufPartNumber', 'Manuf. Part Number')}
572
+ disabled={disableFields}
573
+ bind:value={listing.manufacturerPartNumber}
574
+ />
575
+ </div>
576
+ <div class="col">
577
+ <Input
578
+ type="text"
579
+ label={translate('ecommerce.configuration.upc', 'UPC')}
580
+ disabled={disableFields}
581
+ bind:value={listing.upc}
582
+ />
583
+ </div>
584
+ </div>
585
+ <div class="form-row">
586
+ <div class="col">
587
+ <Select
588
+ label={translate('ecommerce.configuration.condition', 'Condition')}
589
+ emptyText="-- {translate('ecommerce.configuration.selectCondition', 'Select Condition')} --"
590
+ required={true}
591
+ bind:value={listing.ecommerceConditionId}
592
+ >
593
+ {#each ecommerceConditionList as ecommerceCondition}
594
+ <option value={ecommerceCondition.ecommerceConditionId}>{ecommerceCondition.name}</option>
595
+ {/each}
596
+ </Select>
597
+ </div>
598
+ <div class="col">
599
+ <Input
600
+ type="text"
601
+ label={translate('ecommerce.configuration.conditionDescription', 'Condition Description')}
602
+ required={true}
603
+ bind:value={listing.ecommerceConditionDescription}
604
+ />
605
+ </div>
606
+ </div>
607
+ <div class="form-row">
608
+ <div class="col">
609
+ <Textarea
610
+ label={translate('ecommerce.configuration.partDescription', 'Part Description')}
611
+ rows={4}
612
+ required={true}
613
+ class={{ 'is-invalid': !listing.inventoryDescription?.trim() }}
614
+ bind:value={listing.inventoryDescription}
615
+ />
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+ </div>
621
+ </div>
622
+ <div class="card hightlight-card mt-4 w-100">
623
+ <div class="card-header">{translate('ecommerce.configuration.shippingInfo', 'Shipping Info')}</div>
624
+ <div class="card-body">
625
+ <div class="form-row">
626
+ <div class="col-md">
627
+ <Input
628
+ inputmode="decimal"
629
+ type="number"
630
+ label={translate('ecommerce.configuration.weight', 'Weight')}
631
+ required={shippingFieldRequired}
632
+ bind:value={listing.weight}
633
+ ></Input>
634
+ </div>
635
+ <div class="col-md">
636
+ <Select
637
+ label={translate('ecommerce.configuration.weightUnit', 'Weight Unit')}
638
+ emptyText="-- {translate('ecommerce.configuration.selectUnit', 'Select Unit')} --"
639
+ required={shippingFieldRequired}
640
+ bind:value={listing.weightUnit}
641
+ >
642
+ {#each weightUnits as unit}
643
+ <option value={unit}>{unit}</option>
644
+ {/each}
645
+ </Select>
646
+ </div>
647
+ </div>
648
+ <div class="form-row">
649
+ <div class="col-md">
650
+ <Input
651
+ inputmode="decimal"
652
+ type="number"
653
+ label={translate('ecommerce.configuration.length', 'Length')}
654
+ required={shippingFieldRequired}
655
+ bind:value={listing.shippingLength}
656
+ ></Input>
657
+ </div>
658
+ <div class="col-md">
659
+ <Input
660
+ inputmode="decimal"
661
+ type="number"
662
+ label={translate('ecommerce.configuration.width', 'Width')}
663
+ required={shippingFieldRequired}
664
+ bind:value={listing.shippingWidth}
665
+ ></Input>
666
+ </div>
667
+ </div>
668
+ <div class="form-row">
669
+ <div class="col-md">
670
+ <Input
671
+ inputmode="decimal"
672
+ type="number"
673
+ label={translate('ecommerce.configuration.height', 'Height')}
674
+ required={shippingFieldRequired}
675
+ bind:value={listing.shippingHeight}
676
+ ></Input>
677
+ </div>
678
+ <div class="col-md">
679
+ <Select
680
+ label={translate('ecommerce.configuration.lengthUnit', 'Length Unit')}
681
+ emptyText="-- {translate('ecommerce.configuration.selectUnit', 'Select Unit')} --"
682
+ required={shippingFieldRequired}
683
+ bind:value={listing.shippingLengthUnit}
684
+ >
685
+ {#each lengthUnits as unit}
686
+ <option value={unit}>{unit}</option>
687
+ {/each}
688
+ </Select>
689
+ </div>
690
+ </div>
691
+ <div class="form-row">
692
+ <div class="col-md-6">
693
+ <Select
694
+ label={translate('ecommerce.configuration.packageType', 'Package Type')}
695
+ emptyText="-- {translate('ecommerce.configuration.selectPackage', 'Select Package')} --"
696
+ required={shippingFieldRequired}
697
+ bind:value={listing.shippingPackage}
698
+ >
699
+ {#each packageTypes as pkg}
700
+ <option value={pkg.partnerValue}>{pkg.name}</option>
701
+ {/each}
702
+ </Select>
703
+ </div>
704
+ </div>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ <div
709
+ class="col-lg flex-wrap"
710
+ class:d-flex={showElement}
711
+ class:d-none={!showElement}
712
+ >
713
+ <div class="card hightlight-card mt-4 w-100">
714
+ <div class="card-header">{translate('ecommerce.configuration.listingDetails', 'Listing Details')}</div>
715
+ <div class="card-body">
716
+ <div class="form-row">
717
+ <div class="col">
718
+ <Input
719
+ type="text"
720
+ label={translate('ecommerce.configuration.title', 'Title')}
721
+ required={true}
722
+ bind:value={listing.listingTitle}
723
+ />
724
+ </div>
725
+ </div>
726
+ <div class="form-row">
727
+ <div class="col">
728
+ <Textarea
729
+ label={translate('ecommerce.configuration.description', 'Description')}
730
+ rows={6}
731
+ required={true}
732
+ class={!listing.listingDescription || listing.listingDescription.trim() === '' ? 'is-invalid' : ''}
733
+ bind:value={listing.listingDescription}
734
+ />
735
+ </div>
736
+ </div>
737
+ <div class="form-row">
738
+ <div class="col">
739
+ <Select
740
+ label={translate('ecommerce.configuration.ebayCategory', 'Ebay Category')}
741
+ emptyText="-- {translate('ecommerce.configuration.selectCategory', 'Select Category')} --"
742
+ required={true}
743
+ validation={{
744
+ validator: value => {
745
+ if (!value) {
746
+ false
747
+ }
748
+ return true
749
+ },
750
+ }}
751
+ bind:value={listing.ecommerceCategoryId}
752
+ >
753
+ {#each ebayCategoryList as category}
754
+ <option value={category.ebayCategoryId}>{category.name}</option>
755
+ {/each}
756
+ </Select>
757
+ </div>
758
+ </div>
759
+ <div class="form-row">
760
+ <div class="col">
761
+ <Input
762
+ type="number"
763
+ label={translate('ecommerce.configuration.fulfillmentTime', 'Fulfillment Time')}
764
+ onchange={handleSave}
765
+ required={true}
766
+ bind:value={listing.fulfillmentTime}
767
+ />
768
+ </div>
769
+ <div class="col">
770
+ <Select
771
+ label={translate('ecommerce.configuration.fulfillmentTimeUnit', 'Fulfillment Time Unit')}
772
+ emptyText="-- {translate('ecommerce.configuration.selectUnit', 'Select Unit')} --"
773
+ required={true}
774
+ bind:value={listing.fulfillmentTimeUnit}
775
+ >
776
+ {#each ecommercePartnerStaticData.ebay.timeUnits as unit}
777
+ <option value={unit}>{unit}</option>
778
+ {/each}
779
+ </Select>
780
+ </div>
781
+ </div>
782
+ <PolicyList
783
+ title={translate('ecommerce.configuration.fulfillmentPolicies', 'Fulfillment Policies')}
784
+ policies={fulfillmentPolicyList}
785
+ selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.fulfillmentPolicy)
786
+ ? listing.partnerSpecificDetails.fulfillmentPolicy
787
+ : listing.partnerSpecificDetails.fulfillmentPolicy
788
+ ? [listing.partnerSpecificDetails.fulfillmentPolicy]
789
+ : []}
790
+ onSelectionChange={handleFulfillmentPoliciesChange}
791
+ />
792
+
793
+ <PolicyList
794
+ title={translate('ecommerce.configuration.paymentPolicies', 'Payment Policies')}
795
+ policies={getPaymentPolicyListWithChecked()}
796
+ selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.paymentPolicy)
797
+ ? listing.partnerSpecificDetails.paymentPolicy
798
+ : listing.partnerSpecificDetails.paymentPolicy
799
+ ? [listing.partnerSpecificDetails.paymentPolicy]
800
+ : []}
801
+ onSelectionChange={handlePaymentPoliciesChange}
802
+ />
803
+
804
+ <PolicyList
805
+ title={translate('ecommerce.configuration.returnPolicies', 'Return Policies')}
806
+ policies={getReturnPolicyListWithChecked()}
807
+ selectedPolicyIds={Array.isArray(listing.partnerSpecificDetails.returnPolicy)
808
+ ? listing.partnerSpecificDetails.returnPolicy
809
+ : listing.partnerSpecificDetails.returnPolicy
810
+ ? [listing.partnerSpecificDetails.returnPolicy]
811
+ : []}
812
+ onSelectionChange={handleReturnPoliciesChange}
813
+ />
814
+ </div>
815
+ </div>
816
+ </div>
817
+ </div>
818
+ <div class="form-row">
819
+ <div class="col-lg d-flex flex-wrap mt-4">
820
+ <Button
821
+ outline
822
+ color="success"
823
+ size="sm"
824
+ iconClass="plus"
825
+ onclick={async () => {
826
+ if (
827
+ await userPrompt?.confirm(
828
+ translate(
829
+ 'ecommerce.configuration.saveConfirm',
830
+ 'Listing defaults will be overwritten and part or configuration changes will not update the listing',
831
+ ),
832
+ )
833
+ ) {
834
+ handleSave()
835
+ }
836
+ }}
837
+ >
838
+ {translate('ecommerce.configuration.manualSave', 'Manual Save')}
839
+ </Button>
840
+ </div>
841
+ </div>
842
+
843
+ <UserPrompt bind:this={userPrompt} />
844
+ <Modal
845
+ bind:show
846
+ title={translate('ecommerce.configuration.messageList', 'Message List')}
847
+ modalSize="lg"
848
+ confirmShown={false}
849
+ close={() => (show = false)}
850
+ >
851
+ {#each formatMessageForDisplay(listing.message) as msg}
852
+ <div>{msg}</div>
853
+ {/each}
854
+ </Modal>