@open-mercato/core 0.4.9-develop-ce96cffe00 → 0.4.9-develop-5d884303ad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/auth/lib/setup-app.js +17 -1
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -12
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +5 -1
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/points/route.js +59 -0
- package/dist/modules/shipping_carriers/api/points/route.js.map +7 -0
- package/dist/modules/shipping_carriers/api/providers/route.js +38 -0
- package/dist/modules/shipping_carriers/api/providers/route.js.map +7 -0
- package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.js +90 -0
- package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.js.map +7 -0
- package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.js +8 -0
- package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.js.map +7 -0
- package/dist/modules/shipping_carriers/data/validators.js +17 -2
- package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.js +76 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js +243 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.js +134 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.js +70 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.js +32 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.js +37 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +92 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +212 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/types.js +1 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/types.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipping-service.js +36 -3
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/status-sync.js +7 -0
- package/dist/modules/shipping_carriers/lib/status-sync.js.map +2 -2
- package/package.json +7 -4
- package/src/modules/auth/lib/setup-app.ts +22 -0
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -9
- package/src/modules/shipping_carriers/api/cancel/route.ts +5 -1
- package/src/modules/shipping_carriers/api/points/route.ts +57 -0
- package/src/modules/shipping_carriers/api/providers/route.ts +35 -0
- package/src/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.ts +4 -0
- package/src/modules/shipping_carriers/backend/shipping-carriers/create/page.tsx +93 -0
- package/src/modules/shipping_carriers/data/validators.ts +15 -0
- package/src/modules/shipping_carriers/i18n/de.json +66 -0
- package/src/modules/shipping_carriers/i18n/en.json +66 -0
- package/src/modules/shipping_carriers/i18n/es.json +66 -0
- package/src/modules/shipping_carriers/i18n/pl.json +66 -0
- package/src/modules/shipping_carriers/lib/adapter.ts +20 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.tsx +72 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.tsx +343 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.tsx +213 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.tsx +82 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.tsx +54 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.tsx +46 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +153 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +312 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/types.ts +76 -0
- package/src/modules/shipping_carriers/lib/shipping-service.ts +53 -3
- package/src/modules/shipping_carriers/lib/status-sync.ts +7 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
4
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'
|
|
6
|
+
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
7
|
+
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
8
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
9
|
+
import { Package, CheckCircle2 } from 'lucide-react'
|
|
10
|
+
import type { Address, PackageDimension, ShippingRate, LabelFormat, ContactInfo } from '../types'
|
|
11
|
+
|
|
12
|
+
export type ConfirmStepProps = {
|
|
13
|
+
rates: ShippingRate[]
|
|
14
|
+
ratesError: string | null
|
|
15
|
+
selectedRate: ShippingRate | null
|
|
16
|
+
selectedProvider: string
|
|
17
|
+
origin: Address
|
|
18
|
+
destination: Address
|
|
19
|
+
packages: PackageDimension[]
|
|
20
|
+
labelFormat: LabelFormat
|
|
21
|
+
senderContact: ContactInfo
|
|
22
|
+
receiverContact: ContactInfo
|
|
23
|
+
targetPoint: string
|
|
24
|
+
c2cSendingMethod: string
|
|
25
|
+
isSubmitting: boolean
|
|
26
|
+
onRateSelect: (rate: ShippingRate) => void
|
|
27
|
+
onBack: () => void
|
|
28
|
+
onSubmit: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const formatAddress = (addr: Address) =>
|
|
32
|
+
[addr.line1, addr.city, addr.postalCode, addr.countryCode].filter(Boolean).join(', ')
|
|
33
|
+
|
|
34
|
+
export const ConfirmStep = (props: ConfirmStepProps) => {
|
|
35
|
+
const {
|
|
36
|
+
rates, ratesError, selectedRate, selectedProvider,
|
|
37
|
+
origin, destination, packages, labelFormat,
|
|
38
|
+
senderContact, receiverContact, targetPoint, c2cSendingMethod,
|
|
39
|
+
isSubmitting, onRateSelect, onBack, onSubmit,
|
|
40
|
+
} = props
|
|
41
|
+
const t = useT()
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<section className="space-y-6">
|
|
45
|
+
{ratesError ? (
|
|
46
|
+
<div className="space-y-3">
|
|
47
|
+
<ErrorMessage label={ratesError} />
|
|
48
|
+
<Button type="button" variant="outline" size="sm" onClick={onBack}>
|
|
49
|
+
{t('shipping_carriers.create.action.editConfiguration', 'Edit configuration')}
|
|
50
|
+
</Button>
|
|
51
|
+
</div>
|
|
52
|
+
) : null}
|
|
53
|
+
|
|
54
|
+
{rates.length === 0 && !ratesError ? (
|
|
55
|
+
<Card>
|
|
56
|
+
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
|
57
|
+
{t('shipping_carriers.create.noRates', 'No rates available for this route and package configuration.')}
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
) : (
|
|
61
|
+
<Card>
|
|
62
|
+
<CardHeader>
|
|
63
|
+
<CardTitle className="text-base">
|
|
64
|
+
{t('shipping_carriers.create.section.selectRate', 'Select service')}
|
|
65
|
+
</CardTitle>
|
|
66
|
+
</CardHeader>
|
|
67
|
+
<CardContent>
|
|
68
|
+
<div className="space-y-2">
|
|
69
|
+
{rates.map((rate) => {
|
|
70
|
+
const isSelected = selectedRate?.serviceCode === rate.serviceCode
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
key={rate.serviceCode}
|
|
74
|
+
type="button"
|
|
75
|
+
className={`flex w-full cursor-pointer items-center justify-between rounded-lg border p-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
76
|
+
isSelected ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'
|
|
77
|
+
}`}
|
|
78
|
+
onClick={() => onRateSelect(rate)}
|
|
79
|
+
>
|
|
80
|
+
<div className="flex items-center gap-3">
|
|
81
|
+
<Package className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
82
|
+
<div>
|
|
83
|
+
<p className="font-medium">{rate.serviceName}</p>
|
|
84
|
+
<p className="text-xs text-muted-foreground">{rate.serviceCode}</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="flex items-center gap-3">
|
|
88
|
+
{rate.estimatedDays !== undefined ? (
|
|
89
|
+
<span className="text-xs text-muted-foreground">
|
|
90
|
+
{t('shipping_carriers.create.estimatedDays', '{days} day(s)', { days: rate.estimatedDays })}
|
|
91
|
+
</span>
|
|
92
|
+
) : null}
|
|
93
|
+
<Badge variant="secondary">
|
|
94
|
+
{rate.amount.toFixed(2)} {rate.currencyCode}
|
|
95
|
+
</Badge>
|
|
96
|
+
{isSelected ? <CheckCircle2 className="h-4 w-4 text-primary" /> : null}
|
|
97
|
+
</div>
|
|
98
|
+
</button>
|
|
99
|
+
)
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
</CardContent>
|
|
103
|
+
</Card>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
<Card>
|
|
107
|
+
<CardHeader>
|
|
108
|
+
<CardTitle className="text-base">
|
|
109
|
+
{t('shipping_carriers.create.section.summary', 'Summary')}
|
|
110
|
+
</CardTitle>
|
|
111
|
+
</CardHeader>
|
|
112
|
+
<CardContent>
|
|
113
|
+
<dl className="grid gap-2 text-sm sm:grid-cols-2">
|
|
114
|
+
<div>
|
|
115
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
116
|
+
{t('shipping_carriers.create.summary.carrier', 'Carrier')}
|
|
117
|
+
</dt>
|
|
118
|
+
<dd className="mt-0.5 capitalize">{selectedProvider.replace(/_/g, ' ')}</dd>
|
|
119
|
+
</div>
|
|
120
|
+
{selectedRate ? (
|
|
121
|
+
<div>
|
|
122
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
123
|
+
{t('shipping_carriers.create.summary.service', 'Service')}
|
|
124
|
+
</dt>
|
|
125
|
+
<dd className="mt-0.5">{selectedRate.serviceName}</dd>
|
|
126
|
+
</div>
|
|
127
|
+
) : null}
|
|
128
|
+
<div>
|
|
129
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
130
|
+
{t('shipping_carriers.create.summary.from', 'From')}
|
|
131
|
+
</dt>
|
|
132
|
+
<dd className="mt-0.5">{formatAddress(origin)}</dd>
|
|
133
|
+
</div>
|
|
134
|
+
<div>
|
|
135
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
136
|
+
{t('shipping_carriers.create.summary.to', 'To')}
|
|
137
|
+
</dt>
|
|
138
|
+
<dd className="mt-0.5">{formatAddress(destination)}</dd>
|
|
139
|
+
</div>
|
|
140
|
+
<div>
|
|
141
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
142
|
+
{t('shipping_carriers.create.summary.packages', 'Packages')}
|
|
143
|
+
</dt>
|
|
144
|
+
<dd className="mt-0.5">{packages.length}</dd>
|
|
145
|
+
</div>
|
|
146
|
+
<div>
|
|
147
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
148
|
+
{t('shipping_carriers.create.summary.labelFormat', 'Label format')}
|
|
149
|
+
</dt>
|
|
150
|
+
<dd className="mt-0.5 uppercase">{labelFormat}</dd>
|
|
151
|
+
</div>
|
|
152
|
+
{(senderContact.phone || senderContact.email) ? (
|
|
153
|
+
<div>
|
|
154
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
155
|
+
{t('shipping_carriers.create.summary.senderContact', 'Sender contact')}
|
|
156
|
+
</dt>
|
|
157
|
+
<dd className="mt-0.5">
|
|
158
|
+
{[senderContact.phone, senderContact.email].filter(Boolean).join(' / ')}
|
|
159
|
+
</dd>
|
|
160
|
+
</div>
|
|
161
|
+
) : null}
|
|
162
|
+
{(receiverContact.phone || receiverContact.email) ? (
|
|
163
|
+
<div>
|
|
164
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
165
|
+
{t('shipping_carriers.create.summary.receiverContact', 'Receiver contact')}
|
|
166
|
+
</dt>
|
|
167
|
+
<dd className="mt-0.5">
|
|
168
|
+
{[receiverContact.phone, receiverContact.email].filter(Boolean).join(' / ')}
|
|
169
|
+
</dd>
|
|
170
|
+
</div>
|
|
171
|
+
) : null}
|
|
172
|
+
{targetPoint ? (
|
|
173
|
+
<div>
|
|
174
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
175
|
+
{t('shipping_carriers.create.summary.targetPoint', 'Locker point')}
|
|
176
|
+
</dt>
|
|
177
|
+
<dd className="mt-0.5">{targetPoint}</dd>
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
{c2cSendingMethod ? (
|
|
181
|
+
<div>
|
|
182
|
+
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
183
|
+
{t('shipping_carriers.create.summary.c2cSendingMethod', 'C2C sending method')}
|
|
184
|
+
</dt>
|
|
185
|
+
<dd className="mt-0.5">{c2cSendingMethod}</dd>
|
|
186
|
+
</div>
|
|
187
|
+
) : null}
|
|
188
|
+
</dl>
|
|
189
|
+
</CardContent>
|
|
190
|
+
</Card>
|
|
191
|
+
|
|
192
|
+
<div className="flex justify-between gap-3">
|
|
193
|
+
<Button type="button" variant="outline" onClick={onBack} disabled={isSubmitting}>
|
|
194
|
+
{t('shipping_carriers.create.back', 'Back')}
|
|
195
|
+
</Button>
|
|
196
|
+
<Button
|
|
197
|
+
type="button"
|
|
198
|
+
onClick={onSubmit}
|
|
199
|
+
disabled={!selectedRate || isSubmitting}
|
|
200
|
+
>
|
|
201
|
+
{isSubmitting ? (
|
|
202
|
+
<>
|
|
203
|
+
<Spinner className="mr-2 h-4 w-4" />
|
|
204
|
+
{t('shipping_carriers.create.submitting', 'Creating shipment...')}
|
|
205
|
+
</>
|
|
206
|
+
) : (
|
|
207
|
+
t('shipping_carriers.create.submit', 'Create shipment')
|
|
208
|
+
)}
|
|
209
|
+
</Button>
|
|
210
|
+
</div>
|
|
211
|
+
</section>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
4
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import type { PackageDimension, PackageEditorProps } from '../types'
|
|
7
|
+
|
|
8
|
+
const PACKAGE_FIELDS = ['weightKg', 'lengthCm', 'widthCm', 'heightCm'] as const
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PACKAGE: PackageDimension = { weightKg: 1, lengthCm: 20, widthCm: 15, heightCm: 10 }
|
|
11
|
+
|
|
12
|
+
export const PackageEditor = (props: PackageEditorProps) => {
|
|
13
|
+
const { packages, onChange, disabled } = props
|
|
14
|
+
const t = useT()
|
|
15
|
+
|
|
16
|
+
const fieldLabel = (field: keyof PackageDimension) => {
|
|
17
|
+
const labels: Record<keyof PackageDimension, string> = {
|
|
18
|
+
weightKg: t('shipping_carriers.create.package.weightKg', 'Weight (kg)'),
|
|
19
|
+
lengthCm: t('shipping_carriers.create.package.lengthCm', 'Length (cm)'),
|
|
20
|
+
widthCm: t('shipping_carriers.create.package.widthCm', 'Width (cm)'),
|
|
21
|
+
heightCm: t('shipping_carriers.create.package.heightCm', 'Height (cm)'),
|
|
22
|
+
}
|
|
23
|
+
return labels[field]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const updatePackage = (index: number, field: keyof PackageDimension, raw: string) => {
|
|
27
|
+
const value = parseFloat(raw)
|
|
28
|
+
onChange(packages.map((pkg, idx) =>
|
|
29
|
+
idx === index ? { ...pkg, [field]: Number.isNaN(value) ? 0 : value } : pkg,
|
|
30
|
+
))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const addPackage = () => onChange([...packages, DEFAULT_PACKAGE])
|
|
34
|
+
|
|
35
|
+
const removePackage = (index: number) => onChange(packages.filter((_, idx) => idx !== index))
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-3">
|
|
39
|
+
{packages.map((pkg, index) => (
|
|
40
|
+
<div key={index} className="rounded-lg border bg-muted/20 p-3">
|
|
41
|
+
<div className="mb-2 flex items-center justify-between">
|
|
42
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
43
|
+
{t('shipping_carriers.create.package.label', 'Package')} {index + 1}
|
|
44
|
+
</span>
|
|
45
|
+
{packages.length > 1 ? (
|
|
46
|
+
<Button
|
|
47
|
+
type="button"
|
|
48
|
+
variant="ghost"
|
|
49
|
+
size="sm"
|
|
50
|
+
className="h-auto px-2 py-0.5 text-xs text-red-600 hover:text-red-700"
|
|
51
|
+
onClick={() => removePackage(index)}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
>
|
|
54
|
+
{t('shipping_carriers.create.package.remove', 'Remove')}
|
|
55
|
+
</Button>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
<div className="grid gap-3 sm:grid-cols-4">
|
|
59
|
+
{PACKAGE_FIELDS.map((field) => (
|
|
60
|
+
<div key={field}>
|
|
61
|
+
<label className="mb-1 block text-xs font-medium text-muted-foreground">
|
|
62
|
+
{fieldLabel(field)}
|
|
63
|
+
</label>
|
|
64
|
+
<Input
|
|
65
|
+
type="number"
|
|
66
|
+
min="0.01"
|
|
67
|
+
step="0.01"
|
|
68
|
+
value={pkg[field]}
|
|
69
|
+
onChange={(event) => updatePackage(index, field, event.target.value)}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
<Button type="button" variant="outline" size="sm" onClick={addPackage} disabled={disabled}>
|
|
78
|
+
{t('shipping_carriers.create.package.add', '+ Add package')}
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { match, P } from 'ts-pattern'
|
|
4
|
+
import { Card, CardContent } from '@open-mercato/ui/primitives/card'
|
|
5
|
+
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
6
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
7
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import { Truck } from 'lucide-react'
|
|
9
|
+
import type { Provider } from '../types'
|
|
10
|
+
|
|
11
|
+
export type ProviderStepProps = {
|
|
12
|
+
isLoading: boolean
|
|
13
|
+
error: string | null
|
|
14
|
+
providers: Provider[]
|
|
15
|
+
onSelect: (providerKey: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ProviderStep = (props: ProviderStepProps) => {
|
|
19
|
+
const { providers, onSelect } = props
|
|
20
|
+
const t = useT()
|
|
21
|
+
|
|
22
|
+
return match(props)
|
|
23
|
+
.with({ isLoading: true }, () => (
|
|
24
|
+
<div className="flex justify-center py-8"><Spinner /></div>
|
|
25
|
+
))
|
|
26
|
+
.with({ error: P.string }, ({ error }) => (
|
|
27
|
+
<ErrorMessage label={error} />
|
|
28
|
+
))
|
|
29
|
+
.with({ providers: [] }, () => (
|
|
30
|
+
<Card>
|
|
31
|
+
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
|
32
|
+
{t('shipping_carriers.create.noProviders', 'No shipping providers are configured. Enable a carrier integration first.')}
|
|
33
|
+
</CardContent>
|
|
34
|
+
</Card>
|
|
35
|
+
))
|
|
36
|
+
.otherwise(() => (
|
|
37
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
38
|
+
{providers.map((provider) => (
|
|
39
|
+
<button
|
|
40
|
+
key={provider.providerKey}
|
|
41
|
+
type="button"
|
|
42
|
+
className="flex cursor-pointer items-center gap-3 rounded-lg border bg-card p-4 text-left transition-colors hover:border-primary hover:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
43
|
+
onClick={() => onSelect(provider.providerKey)}
|
|
44
|
+
>
|
|
45
|
+
<Truck className="h-6 w-6 shrink-0 text-muted-foreground" />
|
|
46
|
+
<div>
|
|
47
|
+
<p className="font-medium capitalize">{provider.providerKey.replace(/_/g, ' ')}</p>
|
|
48
|
+
<p className="text-xs text-muted-foreground">{provider.providerKey}</p>
|
|
49
|
+
</div>
|
|
50
|
+
</button>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
))
|
|
54
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { match } from 'ts-pattern'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import type { WizardStep } from '../types'
|
|
7
|
+
|
|
8
|
+
const STEP_ORDER: WizardStep[] = ['provider', 'configure', 'confirm']
|
|
9
|
+
|
|
10
|
+
export type WizardNavProps = {
|
|
11
|
+
step: WizardStep
|
|
12
|
+
onNavigate: (step: WizardStep) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const WizardNav = (props: WizardNavProps) => {
|
|
16
|
+
const { step, onNavigate } = props
|
|
17
|
+
const t = useT()
|
|
18
|
+
const stepIndex = STEP_ORDER.indexOf(step)
|
|
19
|
+
|
|
20
|
+
const stepLabels: Record<WizardStep, string> = {
|
|
21
|
+
provider: t('shipping_carriers.create.step.provider', 'Select carrier'),
|
|
22
|
+
configure: t('shipping_carriers.create.step.configure', 'Configure shipment'),
|
|
23
|
+
confirm: t('shipping_carriers.create.step.confirm', 'Select service & confirm'),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<nav className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
28
|
+
{STEP_ORDER.map((stepId, index) => (
|
|
29
|
+
<React.Fragment key={stepId}>
|
|
30
|
+
{index > 0 ? <span aria-hidden>›</span> : null}
|
|
31
|
+
<span
|
|
32
|
+
className={match({ isCurrent: stepId === step, isPast: index < stepIndex })
|
|
33
|
+
.with({ isCurrent: true }, () => 'font-medium text-foreground')
|
|
34
|
+
.with({ isPast: true }, () => 'cursor-pointer hover:text-foreground')
|
|
35
|
+
.otherwise(() => '')}
|
|
36
|
+
onClick={() => {
|
|
37
|
+
if (index < stepIndex) onNavigate(stepId)
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{index + 1}. {stepLabels[stepId]}
|
|
41
|
+
</span>
|
|
42
|
+
</React.Fragment>
|
|
43
|
+
))}
|
|
44
|
+
</nav>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { match, P } from 'ts-pattern'
|
|
2
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
3
|
+
import type { Provider, Address, PackageDimension, ShippingRate, DocumentAddress, DropOffPoint } from '../types'
|
|
4
|
+
|
|
5
|
+
export type FetchProvidersResult =
|
|
6
|
+
| { ok: true; providers: Provider[] }
|
|
7
|
+
| { ok: false; error: string }
|
|
8
|
+
|
|
9
|
+
export type FetchOrderAddressesResult =
|
|
10
|
+
| { ok: true; items: DocumentAddress[] }
|
|
11
|
+
| { ok: false }
|
|
12
|
+
|
|
13
|
+
export type FetchRatesParams = {
|
|
14
|
+
providerKey: string
|
|
15
|
+
origin: Address
|
|
16
|
+
destination: Address
|
|
17
|
+
packages: PackageDimension[]
|
|
18
|
+
receiverPhone?: string
|
|
19
|
+
receiverEmail?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type FetchRatesResult =
|
|
23
|
+
| { ok: true; rates: ShippingRate[] }
|
|
24
|
+
| { ok: false; error: string }
|
|
25
|
+
|
|
26
|
+
export type CreateShipmentParams = {
|
|
27
|
+
providerKey: string
|
|
28
|
+
orderId: string
|
|
29
|
+
origin: Address
|
|
30
|
+
destination: Address
|
|
31
|
+
packages: PackageDimension[]
|
|
32
|
+
serviceCode: string
|
|
33
|
+
labelFormat: string
|
|
34
|
+
senderPhone?: string
|
|
35
|
+
senderEmail?: string
|
|
36
|
+
receiverPhone?: string
|
|
37
|
+
receiverEmail?: string
|
|
38
|
+
targetPoint?: string
|
|
39
|
+
c2cSendingMethod?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CreateShipmentResult =
|
|
43
|
+
| { ok: true }
|
|
44
|
+
| { ok: false; error: string }
|
|
45
|
+
|
|
46
|
+
export type FetchDropOffPointsParams = {
|
|
47
|
+
providerKey: string
|
|
48
|
+
query?: string
|
|
49
|
+
type?: string
|
|
50
|
+
postCode?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type FetchDropOffPointsResult =
|
|
54
|
+
| { ok: true; points: DropOffPoint[] }
|
|
55
|
+
| { ok: false; error: string }
|
|
56
|
+
|
|
57
|
+
export const fetchProviders = async (): Promise<FetchProvidersResult> => {
|
|
58
|
+
const call = await apiCall<{ providers: Provider[] }>(
|
|
59
|
+
'/api/shipping-carriers/providers',
|
|
60
|
+
undefined,
|
|
61
|
+
{ fallback: { providers: [] } },
|
|
62
|
+
)
|
|
63
|
+
return match(call)
|
|
64
|
+
.with({ ok: true, result: P.not(P.nullish) }, ({ result }) => ({
|
|
65
|
+
ok: true as const,
|
|
66
|
+
providers: result.providers,
|
|
67
|
+
}))
|
|
68
|
+
.otherwise(() => ({ ok: false as const, error: 'Failed to load shipping providers.' }))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const fetchOrderAddresses = async (orderId: string): Promise<FetchOrderAddressesResult> => {
|
|
72
|
+
const call = await apiCall<{ items: DocumentAddress[] }>(
|
|
73
|
+
`/api/sales/document-addresses?documentId=${orderId}&documentKind=order&pageSize=50`,
|
|
74
|
+
undefined,
|
|
75
|
+
{ fallback: { items: [] } },
|
|
76
|
+
)
|
|
77
|
+
return match(call)
|
|
78
|
+
.with({ ok: true, result: P.not(P.nullish) }, ({ result }) => ({
|
|
79
|
+
ok: true as const,
|
|
80
|
+
items: result.items,
|
|
81
|
+
}))
|
|
82
|
+
.otherwise(() => ({ ok: false as const }))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const fetchRates = async (params: FetchRatesParams): Promise<FetchRatesResult> => {
|
|
86
|
+
const { providerKey, origin, destination, packages, receiverPhone, receiverEmail } = params
|
|
87
|
+
const call = await apiCall<{ rates: ShippingRate[] }>(
|
|
88
|
+
'/api/shipping-carriers/rates',
|
|
89
|
+
{
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
providerKey,
|
|
94
|
+
origin,
|
|
95
|
+
destination,
|
|
96
|
+
packages,
|
|
97
|
+
...(receiverPhone !== undefined ? { receiverPhone } : {}),
|
|
98
|
+
...(receiverEmail !== undefined ? { receiverEmail } : {}),
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
{ fallback: { rates: [] } },
|
|
102
|
+
)
|
|
103
|
+
return match(call)
|
|
104
|
+
.with({ ok: true, result: P.not(P.nullish) }, ({ result }) => ({
|
|
105
|
+
ok: true as const,
|
|
106
|
+
rates: result.rates,
|
|
107
|
+
}))
|
|
108
|
+
.otherwise(({ result }) => ({
|
|
109
|
+
ok: false as const,
|
|
110
|
+
error: (result as { error?: string } | null)?.error ?? 'Failed to fetch shipping rates.',
|
|
111
|
+
}))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const createShipment = async (params: CreateShipmentParams): Promise<CreateShipmentResult> => {
|
|
115
|
+
const call = await apiCall(
|
|
116
|
+
'/api/shipping-carriers/shipments',
|
|
117
|
+
{
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify(params),
|
|
121
|
+
},
|
|
122
|
+
{ fallback: null },
|
|
123
|
+
)
|
|
124
|
+
return match(call)
|
|
125
|
+
.with({ ok: true }, () => ({ ok: true as const }))
|
|
126
|
+
.otherwise(({ result }) => ({
|
|
127
|
+
ok: false as const,
|
|
128
|
+
error: (result as { error?: string } | null)?.error ?? 'Failed to create shipment.',
|
|
129
|
+
}))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const fetchDropOffPoints = async (params: FetchDropOffPointsParams): Promise<FetchDropOffPointsResult> => {
|
|
133
|
+
const url = new URL('/api/shipping-carriers/points', 'http://placeholder')
|
|
134
|
+
url.searchParams.set('providerKey', params.providerKey)
|
|
135
|
+
if (params.query) url.searchParams.set('query', params.query)
|
|
136
|
+
if (params.type) url.searchParams.set('type', params.type)
|
|
137
|
+
if (params.postCode) url.searchParams.set('postCode', params.postCode)
|
|
138
|
+
|
|
139
|
+
const call = await apiCall<{ points: DropOffPoint[] }>(
|
|
140
|
+
`${url.pathname}${url.search}`,
|
|
141
|
+
undefined,
|
|
142
|
+
{ fallback: { points: [] } },
|
|
143
|
+
)
|
|
144
|
+
return match(call)
|
|
145
|
+
.with({ ok: true, result: P.not(P.nullish) }, ({ result }) => ({
|
|
146
|
+
ok: true as const,
|
|
147
|
+
points: result.points,
|
|
148
|
+
}))
|
|
149
|
+
.otherwise(({ result }) => ({
|
|
150
|
+
ok: false as const,
|
|
151
|
+
error: (result as { error?: string } | null)?.error ?? 'Failed to fetch drop-off points.',
|
|
152
|
+
}))
|
|
153
|
+
}
|