@rovela-ai/sdk 0.19.8 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/api/index.d.ts +2 -2
- package/dist/admin/api/index.d.ts.map +1 -1
- package/dist/admin/api/index.js +2 -2
- package/dist/admin/api/index.js.map +1 -1
- package/dist/admin/api/orders.d.ts +49 -0
- package/dist/admin/api/orders.d.ts.map +1 -1
- package/dist/admin/api/orders.js +293 -30
- package/dist/admin/api/orders.js.map +1 -1
- package/dist/admin/api/settings.d.ts.map +1 -1
- package/dist/admin/api/settings.js +6 -1
- package/dist/admin/api/settings.js.map +1 -1
- package/dist/admin/api/shipping.d.ts +49 -0
- package/dist/admin/api/shipping.d.ts.map +1 -1
- package/dist/admin/api/shipping.js +144 -0
- package/dist/admin/api/shipping.js.map +1 -1
- package/dist/admin/components/OrderDetails.d.ts.map +1 -1
- package/dist/admin/components/OrderDetails.js +69 -1
- package/dist/admin/components/OrderDetails.js.map +1 -1
- package/dist/admin/components/ShippingSettings.d.ts.map +1 -1
- package/dist/admin/components/ShippingSettings.js +313 -134
- package/dist/admin/components/ShippingSettings.js.map +1 -1
- package/dist/admin/types.d.ts +13 -0
- package/dist/admin/types.d.ts.map +1 -1
- package/dist/checkout/api/colissimo-relays.d.ts +24 -0
- package/dist/checkout/api/colissimo-relays.d.ts.map +1 -0
- package/dist/checkout/api/colissimo-relays.js +60 -0
- package/dist/checkout/api/colissimo-relays.js.map +1 -0
- package/dist/checkout/api/manual.d.ts.map +1 -1
- package/dist/checkout/api/manual.js +21 -5
- package/dist/checkout/api/manual.js.map +1 -1
- package/dist/checkout/api/shipping.d.ts.map +1 -1
- package/dist/checkout/api/shipping.js +60 -82
- package/dist/checkout/api/shipping.js.map +1 -1
- package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
- package/dist/checkout/components/CheckoutFlow.js +142 -3
- package/dist/checkout/components/CheckoutFlow.js.map +1 -1
- package/dist/checkout/components/ShippingOptions.d.ts +8 -1
- package/dist/checkout/components/ShippingOptions.d.ts.map +1 -1
- package/dist/checkout/components/ShippingOptions.js +40 -11
- package/dist/checkout/components/ShippingOptions.js.map +1 -1
- package/dist/checkout/components/colissimo-logo.d.ts +12 -0
- package/dist/checkout/components/colissimo-logo.d.ts.map +1 -0
- package/dist/checkout/components/colissimo-logo.js +12 -0
- package/dist/checkout/components/colissimo-logo.js.map +1 -0
- package/dist/checkout/server/create-checkout-session.d.ts.map +1 -1
- package/dist/checkout/server/create-checkout-session.js +55 -13
- package/dist/checkout/server/create-checkout-session.js.map +1 -1
- package/dist/checkout/server/handle-webhook.js +7 -3
- package/dist/checkout/server/handle-webhook.js.map +1 -1
- package/dist/checkout/server/order-service.d.ts.map +1 -1
- package/dist/checkout/server/order-service.js +25 -0
- package/dist/checkout/server/order-service.js.map +1 -1
- package/dist/checkout/server/pricing.d.ts +20 -0
- package/dist/checkout/server/pricing.d.ts.map +1 -1
- package/dist/checkout/server/pricing.js +51 -5
- package/dist/checkout/server/pricing.js.map +1 -1
- package/dist/checkout/types.d.ts +57 -1
- package/dist/checkout/types.d.ts.map +1 -1
- package/dist/core/db/queries.d.ts +157 -35
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +223 -1
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/db/schema.d.ts +323 -5
- package/dist/core/db/schema.d.ts.map +1 -1
- package/dist/core/db/schema.js +70 -0
- package/dist/core/db/schema.js.map +1 -1
- package/dist/core/i18n/messages/en.d.ts.map +1 -1
- package/dist/core/i18n/messages/en.js +9 -0
- package/dist/core/i18n/messages/en.js.map +1 -1
- package/dist/core/i18n/messages/fr.d.ts.map +1 -1
- package/dist/core/i18n/messages/fr.js +9 -0
- package/dist/core/i18n/messages/fr.js.map +1 -1
- package/dist/shipping/colissimo.d.ts +166 -0
- package/dist/shipping/colissimo.d.ts.map +1 -0
- package/dist/shipping/colissimo.js +424 -0
- package/dist/shipping/colissimo.js.map +1 -0
- package/dist/shipping/index.d.ts +4 -1
- package/dist/shipping/index.d.ts.map +1 -1
- package/dist/shipping/index.js +3 -1
- package/dist/shipping/index.js.map +1 -1
- package/docs/changelog/epoch-07-shipping-carriers.md +125 -0
- package/package.json +6 -1
- package/templates/store-template/.claude/skills/manage-products/SKILL.md +1 -1
- package/templates/store-template/app/api/admin/orders/[id]/colissimo-defaults/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/[id]/label/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/[id]/ship-with-colissimo/route.ts +6 -0
- package/templates/store-template/app/api/admin/shipping/colissimo/route.ts +10 -0
- package/templates/store-template/app/api/checkout/colissimo-relays/route.ts +4 -0
- package/templates/store-template/package.json +1 -1
|
@@ -16,6 +16,7 @@ import { adminToast } from './AdminToaster';
|
|
|
16
16
|
import { useStoreSettingsOptional } from '../../core/StoreSettingsProvider';
|
|
17
17
|
import { getCurrencySymbol, formatPrice } from '../../core/utils';
|
|
18
18
|
import { COMMON_COUNTRIES, US_STATES } from '../../checkout/constants';
|
|
19
|
+
import { COLISSIMO_LOGO_DATA_URI } from '../../checkout/components/colissimo-logo';
|
|
19
20
|
const DEFAULT_FROM_ADDRESS = {
|
|
20
21
|
name: '',
|
|
21
22
|
company: '',
|
|
@@ -47,7 +48,14 @@ function CountryTag({ code, onRemove, }) {
|
|
|
47
48
|
function CarrierCard({ carrier, onEdit, onDelete, onAddZone, onEditZone, onDeleteZone, onAddRate, onEditRate, onDeleteRate, formatCurrency, }) {
|
|
48
49
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
49
50
|
const zones = carrier.zones || [];
|
|
50
|
-
|
|
51
|
+
const provider = carrier.provider || 'manual';
|
|
52
|
+
return (_jsxs("div", { className: "rounded-lg border border-[hsl(var(--admin-border))] overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between bg-[hsl(var(--admin-primary))]/10 px-4 py-3", children: [_jsxs("button", { type: "button", onClick: () => setIsExpanded(!isExpanded), className: "flex items-center gap-3 text-left flex-1", children: [provider === 'colissimo' ? (
|
|
53
|
+
// The full logo carries the wordmark — it IS the identity. The
|
|
54
|
+
// name renders only when the merchant renamed the carrier.
|
|
55
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
56
|
+
_jsx("img", { src: COLISSIMO_LOGO_DATA_URI, alt: "Colissimo", className: "h-7 w-auto shrink-0 object-contain" })) : provider === 'shippo' ? (_jsx(Plug, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" })) : (_jsx(Truck, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" })), _jsxs("div", { children: [!(provider === 'colissimo' && carrier.name.trim().toLowerCase() === 'colissimo') && (_jsx("span", { className: "font-semibold text-[hsl(var(--admin-foreground))]", children: carrier.name })), provider === 'manual' && (_jsxs("span", { className: "ml-2 text-sm text-[hsl(var(--admin-foreground-muted))]", children: ["(", carrier.code, ")"] })), carrier.description && (_jsx("p", { className: "text-sm text-[hsl(var(--admin-foreground-muted))]", children: carrier.description }))] }), isExpanded ? (_jsx(ChevronUp, { className: "h-4 w-4 text-[hsl(var(--admin-foreground-muted))]" })) : (_jsx(ChevronDown, { className: "h-4 w-4 text-[hsl(var(--admin-foreground-muted))]" }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [provider === 'shippo' && (_jsx("span", { className: "admin-badge admin-badge-neutral", children: "Shippo" })), !carrier.isActive && (_jsx("span", { className: "admin-badge admin-badge-neutral", children: "Inactive" })), _jsxs("span", { className: "text-xs text-[hsl(var(--admin-foreground-muted))]", children: [zones.length, " zone", zones.length !== 1 ? 's' : ''] }), _jsx("button", { type: "button", onClick: onEdit, className: "admin-btn admin-btn-ghost admin-btn-sm", title: "Edit carrier", children: _jsx(Edit2, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: onDelete, className: "admin-btn admin-btn-ghost admin-btn-sm text-[hsl(var(--admin-error))]", title: "Delete carrier", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })] }), isExpanded && provider === 'colissimo' && (_jsx("div", { className: "border-b border-[hsl(var(--admin-border))] bg-[hsl(var(--admin-card))] px-4 py-4", children: _jsx(ColissimoPanel, {}) })), isExpanded && provider === 'shippo' && (_jsx("div", { className: "border-b border-[hsl(var(--admin-border))] bg-[hsl(var(--admin-card))] px-4 py-4", children: _jsx(ShippoPanel, {}) })), isExpanded && (_jsxs("div", { className: "bg-[hsl(var(--admin-card))]", children: [zones.length === 0 ? (_jsx("div", { className: "px-4 py-6", children: _jsx(AdminEmptyState, { icon: _jsx(Package, {}), title: "No zones yet", description: provider === 'shippo'
|
|
57
|
+
? 'Shippo quotes rates live at checkout — zones are optional extra flat rates.'
|
|
58
|
+
: 'Add a shipping zone to start configuring rates for this carrier.' }) })) : (_jsx("div", { className: "divide-y divide-[hsl(var(--admin-border))]", children: zones.map((zone) => (_jsx(ZoneCard, { zone: zone, onEdit: () => onEditZone(zone), onDelete: () => onDeleteZone(zone), onAddRate: () => onAddRate(zone.id), onEditRate: (rate) => onEditRate(zone.id, rate), onDeleteRate: onDeleteRate, formatCurrency: formatCurrency }, zone.id))) })), _jsx("div", { className: "px-4 py-3 border-t border-[hsl(var(--admin-border))]", children: _jsxs("button", { type: "button", onClick: onAddZone, className: "flex items-center gap-2 text-sm text-[hsl(var(--admin-primary))] hover:text-[hsl(var(--admin-primary-hover))]", children: [_jsx(Plus, { className: "h-4 w-4" }), "Add shipping zone"] }) })] }))] }));
|
|
51
59
|
}
|
|
52
60
|
// =============================================================================
|
|
53
61
|
// Zone Card Component
|
|
@@ -77,24 +85,13 @@ export function ShippingSettings({ className }) {
|
|
|
77
85
|
const [shippingEnabled, setShippingEnabled] = useState(true);
|
|
78
86
|
const [shippingCountries, setShippingCountries] = useState(['US', 'CA']);
|
|
79
87
|
const [countryInput, setCountryInput] = useState('');
|
|
80
|
-
// Carriers
|
|
88
|
+
// Carriers — THE registry (carrier-first architecture; provider config
|
|
89
|
+
// panels render inside each carrier card, no global shipping mode).
|
|
81
90
|
const [carriers, setCarriers] = useState([]);
|
|
82
|
-
// Shippo configuration
|
|
83
|
-
const [shippingMode, setShippingMode] = useState('manual');
|
|
84
|
-
const [shippoTestMode, setShippoTestMode] = useState(true);
|
|
85
|
-
const [shippoFromAddress, setShippoFromAddress] = useState(DEFAULT_FROM_ADDRESS);
|
|
86
|
-
const [shippoDefaultParcel, setShippoDefaultParcel] = useState(DEFAULT_PARCEL);
|
|
87
91
|
// Document dirty tracking (AdminSaveBar contract). shippingEnabled is NOT
|
|
88
|
-
// tracked: its toggle commits immediately on click.
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const [isValidatingShippo, setIsValidatingShippo] = useState(false);
|
|
92
|
-
// Shippo API key management
|
|
93
|
-
const [shippoApiKeyInput, setShippoApiKeyInput] = useState('');
|
|
94
|
-
const [isSavingApiKey, setIsSavingApiKey] = useState(false);
|
|
95
|
-
const [hasStoredKey, setHasStoredKey] = useState(false);
|
|
96
|
-
const [maskedKey, setMaskedKey] = useState(null);
|
|
97
|
-
const [isDeletingApiKey, setIsDeletingApiKey] = useState(false);
|
|
92
|
+
// tracked: its toggle commits immediately on click. Provider config
|
|
93
|
+
// (Shippo/Colissimo panels) is transactional inside each carrier card.
|
|
94
|
+
const { isDirty, baselineValue, rebaseline } = useDirtyState({ shippingCountries }, !isFetching);
|
|
98
95
|
// Modals
|
|
99
96
|
const [showCarrierForm, setShowCarrierForm] = useState(false);
|
|
100
97
|
const [editingCarrier, setEditingCarrier] = useState(null);
|
|
@@ -137,19 +134,6 @@ export function ShippingSettings({ className }) {
|
|
|
137
134
|
: storeCountry
|
|
138
135
|
? [storeCountry]
|
|
139
136
|
: ['US', 'CA']);
|
|
140
|
-
setShippingMode(settings.shippingMode || 'manual');
|
|
141
|
-
setShippoTestMode(settings.shippoTestMode ?? true);
|
|
142
|
-
if (settings.shippoFromAddress) {
|
|
143
|
-
setShippoFromAddress(settings.shippoFromAddress);
|
|
144
|
-
}
|
|
145
|
-
else if (storeCountry) {
|
|
146
|
-
// Fresh Shippo setup: pre-fill the from-address country with the
|
|
147
|
-
// store's home country instead of the hardcoded US default.
|
|
148
|
-
setShippoFromAddress({ ...DEFAULT_FROM_ADDRESS, country: storeCountry });
|
|
149
|
-
}
|
|
150
|
-
if (settings.shippoDefaultParcel) {
|
|
151
|
-
setShippoDefaultParcel(settings.shippoDefaultParcel);
|
|
152
|
-
}
|
|
153
137
|
}
|
|
154
138
|
else {
|
|
155
139
|
showToast('error', settingsRes.error);
|
|
@@ -179,11 +163,6 @@ export function ShippingSettings({ className }) {
|
|
|
179
163
|
body: JSON.stringify({
|
|
180
164
|
shippingEnabled,
|
|
181
165
|
shippingCountries,
|
|
182
|
-
// Shippo settings
|
|
183
|
-
shippingMode,
|
|
184
|
-
shippoTestMode,
|
|
185
|
-
shippoFromAddress,
|
|
186
|
-
shippoDefaultParcel,
|
|
187
166
|
}),
|
|
188
167
|
});
|
|
189
168
|
if (!res.ok) {
|
|
@@ -192,71 +171,7 @@ export function ShippingSettings({ className }) {
|
|
|
192
171
|
// The AdminSaveBar's saved morph IS the feedback — no success toast.
|
|
193
172
|
rebaseline();
|
|
194
173
|
return { success: true };
|
|
195
|
-
}, [shippingEnabled, shippingCountries,
|
|
196
|
-
const handleValidateShippoKey = useCallback(async () => {
|
|
197
|
-
setIsValidatingShippo(true);
|
|
198
|
-
setShippoKeyValid(null);
|
|
199
|
-
const res = await fetchAdminApi('/api/admin/shipping/shippo/validate', { method: 'POST' });
|
|
200
|
-
setIsValidatingShippo(false);
|
|
201
|
-
if (!res.ok) {
|
|
202
|
-
setShippoKeyValid(false);
|
|
203
|
-
setHasStoredKey(false);
|
|
204
|
-
setMaskedKey(null);
|
|
205
|
-
showToast('error', res.error);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
setShippoKeyValid(res.data.valid ?? false);
|
|
209
|
-
setHasStoredKey(res.data.hasStoredKey ?? false);
|
|
210
|
-
setMaskedKey(res.data.maskedKey ?? null);
|
|
211
|
-
if (!res.data.valid && res.data.error) {
|
|
212
|
-
showToast('error', res.data.error);
|
|
213
|
-
}
|
|
214
|
-
}, [showToast]);
|
|
215
|
-
// Auto-validate Shippo API key when mode is 'shippo' after initial load.
|
|
216
|
-
// Pattern C — async validation triggered by data arrival.
|
|
217
|
-
// Alternative is a data-fetching library handling derived async state.
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
if (!isFetching && shippingMode === 'shippo' && shippoKeyValid === null) {
|
|
220
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
221
|
-
handleValidateShippoKey();
|
|
222
|
-
}
|
|
223
|
-
}, [isFetching, shippingMode, shippoKeyValid, handleValidateShippoKey]);
|
|
224
|
-
const handleSaveShippoApiKey = useCallback(async () => {
|
|
225
|
-
if (!shippoApiKeyInput.trim())
|
|
226
|
-
return;
|
|
227
|
-
setIsSavingApiKey(true);
|
|
228
|
-
const res = await fetchAdminApi('/api/admin/shipping/shippo/api-key', {
|
|
229
|
-
method: 'POST',
|
|
230
|
-
headers: { 'Content-Type': 'application/json' },
|
|
231
|
-
body: JSON.stringify({ apiKey: shippoApiKeyInput }),
|
|
232
|
-
});
|
|
233
|
-
setIsSavingApiKey(false);
|
|
234
|
-
if (!res.ok) {
|
|
235
|
-
showToast('error', res.error);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
setHasStoredKey(true);
|
|
239
|
-
setMaskedKey(res.data.maskedKey ?? null);
|
|
240
|
-
setShippoApiKeyInput('');
|
|
241
|
-
setShippoKeyValid(true);
|
|
242
|
-
showToast('success', 'API key saved and verified!');
|
|
243
|
-
}, [shippoApiKeyInput, showToast]);
|
|
244
|
-
const handleRemoveApiKey = useCallback(async () => {
|
|
245
|
-
setIsDeletingApiKey(true);
|
|
246
|
-
const res = await fetchAdminApi('/api/admin/shipping/shippo/api-key', {
|
|
247
|
-
method: 'DELETE',
|
|
248
|
-
});
|
|
249
|
-
setIsDeletingApiKey(false);
|
|
250
|
-
if (!res.ok) {
|
|
251
|
-
showToast('error', res.error);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
setHasStoredKey(false);
|
|
255
|
-
setMaskedKey(null);
|
|
256
|
-
showToast('success', 'API key removed');
|
|
257
|
-
// Re-validate to check if env var exists
|
|
258
|
-
handleValidateShippoKey();
|
|
259
|
-
}, [handleValidateShippoKey, showToast]);
|
|
174
|
+
}, [shippingEnabled, shippingCountries, rebaseline]);
|
|
260
175
|
const handleAddCountry = useCallback(() => {
|
|
261
176
|
const code = countryInput.toUpperCase().trim();
|
|
262
177
|
if (code && code.length === 2 && !shippingCountries.includes(code)) {
|
|
@@ -344,17 +259,13 @@ export function ShippingSettings({ className }) {
|
|
|
344
259
|
// ---------------------------------------------------------------------------
|
|
345
260
|
// Render
|
|
346
261
|
// ---------------------------------------------------------------------------
|
|
347
|
-
return (_jsxs("div", { className: `space-y-6 ${className || ''}`, children: [_jsx(AdminPageHeader, { title: "Shipping", subtitle: "Carriers, zones and rates, and whether shipping is offered at checkout." }), _jsx("div", { className: "admin-card rounded-lg p-6", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "h-10 w-10 rounded-full bg-[hsl(var(--admin-primary))]/10 flex items-center justify-center", children: _jsx(Package, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }) }), _jsxs("div", { children: [_jsx("h3", { className: "admin-card-title", children: "Enable Shipping" }), _jsx("p", { className: "admin-text-muted text-sm", children: "Turn off to hide shipping options at checkout" })] })] }), _jsx(AdminSwitch, { checked: shippingEnabled, onChange: handleToggleShippingEnabled, "aria-label": "Enable shipping" })] }) }), shippingEnabled && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "admin-card rounded-lg p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-
|
|
348
|
-
? 'border-[hsl(var(--admin-primary))] bg-[hsl(var(--admin-primary-light))]'
|
|
349
|
-
: 'border-[hsl(var(--admin-border))] hover:border-[hsl(var(--admin-primary))]'}`, children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Package, { className: "h-5 w-5" }), _jsx("span", { className: "font-medium", children: "Manual Rates" })] }), _jsx("p", { className: "text-sm text-[hsl(var(--admin-foreground-muted))]", children: "Configure carriers, zones, and rates manually." })] }), _jsxs("button", { type: "button", onClick: () => setShippingMode('shippo'), className: `p-4 rounded-lg border-2 text-left transition-colors ${shippingMode === 'shippo'
|
|
350
|
-
? 'border-[hsl(var(--admin-primary))] bg-[hsl(var(--admin-primary-light))]'
|
|
351
|
-
: 'border-[hsl(var(--admin-border))] hover:border-[hsl(var(--admin-primary))]'}`, children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Plug, { className: "h-5 w-5" }), _jsx("span", { className: "font-medium", children: "Shippo (Carrier Rates)" })] }), _jsx("p", { className: "text-sm text-[hsl(var(--admin-foreground-muted))]", children: "Real-time rates from USPS, UPS, FedEx, and more." })] })] })] }), _jsxs("div", { className: "admin-card rounded-lg p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-6", children: [_jsx(Globe, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }), _jsx("h3", { className: "admin-card-title", children: "General Settings" })] }), _jsx("div", { className: "space-y-6", children: _jsxs("div", { children: [_jsxs("label", { className: "admin-label", children: [_jsx(MapPin, { className: "inline h-4 w-4 mr-1" }), "Shipping Countries"] }), _jsx("p", { className: "admin-text-muted text-sm mb-2", children: "Countries where you offer shipping" }), _jsx("div", { className: "flex flex-wrap gap-2 mb-3", children: shippingCountries.map((code) => (_jsx(CountryTag, { code: code, onRemove: () => handleRemoveCountry(code) }, code))) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(AdminSelect, { value: countryInput, onChange: setCountryInput, placeholder: "Select a country\u2026", options: COMMON_COUNTRIES.filter((c) => !shippingCountries.includes(c.code)).map((country) => ({
|
|
262
|
+
return (_jsxs("div", { className: `space-y-6 ${className || ''}`, children: [_jsx(AdminPageHeader, { title: "Shipping", subtitle: "Carriers, zones and rates, and whether shipping is offered at checkout." }), _jsx("div", { className: "admin-card rounded-lg p-6", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "h-10 w-10 rounded-full bg-[hsl(var(--admin-primary))]/10 flex items-center justify-center", children: _jsx(Package, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }) }), _jsxs("div", { children: [_jsx("h3", { className: "admin-card-title", children: "Enable Shipping" }), _jsx("p", { className: "admin-text-muted text-sm", children: "Turn off to hide shipping options at checkout" })] })] }), _jsx(AdminSwitch, { checked: shippingEnabled, onChange: handleToggleShippingEnabled, "aria-label": "Enable shipping" })] }) }), shippingEnabled && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "admin-card rounded-lg p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-6", children: [_jsx(Globe, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }), _jsx("h3", { className: "admin-card-title", children: "General Settings" })] }), _jsx("div", { className: "space-y-6", children: _jsxs("div", { children: [_jsxs("label", { className: "admin-label", children: [_jsx(MapPin, { className: "inline h-4 w-4 mr-1" }), "Shipping Countries"] }), _jsx("p", { className: "admin-text-muted text-sm mb-2", children: "Countries where you offer shipping" }), _jsx("div", { className: "flex flex-wrap gap-2 mb-3", children: shippingCountries.map((code) => (_jsx(CountryTag, { code: code, onRemove: () => handleRemoveCountry(code) }, code))) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(AdminSelect, { value: countryInput, onChange: setCountryInput, placeholder: "Select a country\u2026", options: COMMON_COUNTRIES.filter((c) => !shippingCountries.includes(c.code)).map((country) => ({
|
|
352
263
|
value: country.code,
|
|
353
264
|
label: `${country.name} (${country.code})`,
|
|
354
|
-
})), className: "w-full max-w-xs" }), _jsx("button", { type: "button", onClick: handleAddCountry, disabled: !countryInput, className: "admin-btn admin-btn-secondary disabled:opacity-50", children: "Add" })] })] }) })] }),
|
|
265
|
+
})), className: "w-full max-w-xs" }), _jsx("button", { type: "button", onClick: handleAddCountry, disabled: !countryInput, className: "admin-btn admin-btn-secondary disabled:opacity-50", children: "Add" })] })] }) })] }), _jsxs("div", { className: "admin-card rounded-lg p-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-6", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Truck, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }), _jsx("h3", { className: "admin-card-title", children: "Carriers" })] }), _jsxs("button", { type: "button", onClick: () => {
|
|
355
266
|
setEditingCarrier(null);
|
|
356
267
|
setShowCarrierForm(true);
|
|
357
|
-
}, className: "admin-btn admin-btn-primary admin-btn-sm", children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), "Add carrier"] })] }), _jsx("p", { className: "admin-text-muted text-sm mb-4", children: "
|
|
268
|
+
}, className: "admin-btn admin-btn-primary admin-btn-sm", children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), "Add carrier"] })] }), _jsx("p", { className: "admin-text-muted text-sm mb-4", children: "Every carrier ships with its own zones and rates \u2014 price by destination, cart amount and customer group. Integrated carriers (Shippo, Colissimo) also carry their connection settings here." }), carriers.length === 0 ? (_jsx(AdminEmptyState, { icon: _jsx(Truck, {}), title: "No carriers yet", description: "Add your first carrier to start offering shipping rates." })) : (_jsx("div", { className: "space-y-4", children: carriers.map((carrier) => (_jsx(CarrierCard, { carrier: carrier, onEdit: () => {
|
|
358
269
|
setEditingCarrier(carrier);
|
|
359
270
|
setShowCarrierForm(true);
|
|
360
271
|
}, onDelete: () => handleDeleteCarrierClick(carrier), onAddZone: () => {
|
|
@@ -364,34 +275,16 @@ export function ShippingSettings({ className }) {
|
|
|
364
275
|
setEditingZone({ carrierId: carrier.id, zone });
|
|
365
276
|
setShowZoneForm(true);
|
|
366
277
|
}, onDeleteZone: handleDeleteZoneClick, onAddRate: (zoneId) => {
|
|
367
|
-
setEditingRate({ zoneId });
|
|
278
|
+
setEditingRate({ zoneId, provider: carrier.provider || 'manual' });
|
|
368
279
|
setShowRateForm(true);
|
|
369
280
|
}, onEditRate: (zoneId, rate) => {
|
|
370
|
-
setEditingRate({ zoneId, rate });
|
|
281
|
+
setEditingRate({ zoneId, provider: carrier.provider || 'manual', rate });
|
|
371
282
|
setShowRateForm(true);
|
|
372
|
-
}, onDeleteRate: handleDeleteRateClick, formatCurrency: formatCurrency }, carrier.id))) }))] })
|
|
373
|
-
? 'Verifying…'
|
|
374
|
-
: hasStoredKey && shippoKeyValid === false
|
|
375
|
-
? 'API Key Invalid'
|
|
376
|
-
: 'API Key Required' }), _jsx("p", { className: "text-sm text-[hsl(var(--admin-foreground-muted))]", children: isValidatingShippo
|
|
377
|
-
? 'Checking your Shippo connection…'
|
|
378
|
-
: hasStoredKey && shippoKeyValid === false
|
|
379
|
-
? `Current key (${maskedKey}) is invalid. Enter a new key.`
|
|
380
|
-
: 'Enter your Shippo API key to enable carrier rate shipping.' })] })] }), !isValidatingShippo && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "password", value: shippoApiKeyInput, onChange: (e) => setShippoApiKeyInput(e.target.value), placeholder: "shippo_live_\u2026", className: "admin-input flex-1" }), _jsx("button", { type: "button", onClick: handleSaveShippoApiKey, disabled: isSavingApiKey || !shippoApiKeyInput.trim(), className: "admin-btn admin-btn-primary disabled:opacity-50", children: isSavingApiKey ? (_jsx("span", { className: "admin-spinner admin-spinner-sm" })) : ('Save & Verify') })] }), _jsxs("p", { className: "text-xs text-[hsl(var(--admin-foreground-muted))]", children: ["Get your API key from", ' ', _jsxs("a", { href: "https://apps.goshippo.com/settings/api", target: "_blank", rel: "noopener noreferrer", className: "text-[hsl(var(--admin-primary))] hover:underline inline-flex items-center gap-1", children: ["Shippo Dashboard", _jsx(ExternalLink, { className: "h-3 w-3" })] })] })] }))] }))] }), _jsxs("div", { className: "flex items-center justify-between mt-4 pt-4 border-t border-[hsl(var(--admin-border))]", children: [_jsxs("div", { children: [_jsx("label", { className: "admin-label", children: "Test Mode" }), _jsx("p", { className: "admin-text-muted text-sm", children: "Use Shippo test API for development (free API calls)" })] }), _jsx(AdminSwitch, { checked: shippoTestMode, onChange: setShippoTestMode, "aria-label": "Test mode" })] })] }), _jsxs("div", { className: "admin-card rounded-lg p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-4", children: [_jsx(MapPin, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }), _jsx("h3", { className: "admin-card-title", children: "Ship-From Address" })] }), _jsx("p", { className: "admin-text-muted text-sm mb-4", children: "Your warehouse or fulfillment center address for shipping calculations." }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-name", className: "admin-label", children: "Name" }), _jsx("input", { type: "text", id: "shippo-name", value: shippoFromAddress.name, onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, name: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "John Smith" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-company", className: "admin-label", children: "Company (optional)" }), _jsx("input", { type: "text", id: "shippo-company", value: shippoFromAddress.company || '', onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, company: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "Acme Inc." })] }), _jsxs("div", { className: "md:col-span-2", children: [_jsx("label", { htmlFor: "shippo-street1", className: "admin-label", children: "Street Address" }), _jsx("input", { type: "text", id: "shippo-street1", value: shippoFromAddress.street1, onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, street1: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "123 Main St" })] }), _jsxs("div", { className: "md:col-span-2", children: [_jsx("label", { htmlFor: "shippo-street2", className: "admin-label", children: "Apt, Suite, etc. (optional)" }), _jsx("input", { type: "text", id: "shippo-street2", value: shippoFromAddress.street2 || '', onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, street2: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "Suite 100" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-city", className: "admin-label", children: "City" }), _jsx("input", { type: "text", id: "shippo-city", value: shippoFromAddress.city, onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, city: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "San Francisco" })] }), shippoFromAddress.country === 'US' && (_jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-state", className: "admin-label", children: "State" }), _jsx(AdminSelect, { id: "shippo-state", value: shippoFromAddress.state, onChange: (value) => setShippoFromAddress((prev) => ({ ...prev, state: value })), placeholder: "Select state\u2026", options: US_STATES.map((state) => ({
|
|
381
|
-
value: state.code,
|
|
382
|
-
label: state.name,
|
|
383
|
-
})), className: "mt-1 w-full" })] })), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-zip", className: "admin-label", children: "ZIP Code" }), _jsx("input", { type: "text", id: "shippo-zip", value: shippoFromAddress.zip, onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, zip: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "94102" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-country", className: "admin-label", children: "Country" }), _jsx(AdminSelect, { id: "shippo-country", value: shippoFromAddress.country, onChange: (value) => setShippoFromAddress((prev) => ({ ...prev, country: value })), options: COMMON_COUNTRIES.map((country) => ({
|
|
384
|
-
value: country.code,
|
|
385
|
-
label: country.name,
|
|
386
|
-
})), className: "mt-1 w-full" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-phone", className: "admin-label", children: "Phone (optional)" }), _jsx("input", { type: "tel", id: "shippo-phone", value: shippoFromAddress.phone || '', onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, phone: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "+1 555 123 4567" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-email", className: "admin-label", children: "Email (optional)" }), _jsx("input", { type: "email", id: "shippo-email", value: shippoFromAddress.email || '', onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, email: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "shipping@example.com" })] })] })] }), _jsxs("div", { className: "admin-card rounded-lg p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-4", children: [_jsx(Box, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }), _jsx("h3", { className: "admin-card-title", children: "Default Package Size" })] }), _jsx("p", { className: "admin-text-muted text-sm mb-4", children: "Default package dimensions for rate calculations. You can override these per order." }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "parcel-length", className: "admin-label", children: "Length (in)" }), _jsx("input", { type: "number", id: "parcel-length", value: shippoDefaultParcel.length, onChange: (e) => setShippoDefaultParcel((prev) => ({ ...prev, length: parseFloat(e.target.value) || 0 })), min: "0", step: "0.5", className: "admin-input mt-1 w-full" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "parcel-width", className: "admin-label", children: "Width (in)" }), _jsx("input", { type: "number", id: "parcel-width", value: shippoDefaultParcel.width, onChange: (e) => setShippoDefaultParcel((prev) => ({ ...prev, width: parseFloat(e.target.value) || 0 })), min: "0", step: "0.5", className: "admin-input mt-1 w-full" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "parcel-height", className: "admin-label", children: "Height (in)" }), _jsx("input", { type: "number", id: "parcel-height", value: shippoDefaultParcel.height, onChange: (e) => setShippoDefaultParcel((prev) => ({ ...prev, height: parseFloat(e.target.value) || 0 })), min: "0", step: "0.5", className: "admin-input mt-1 w-full" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "parcel-weight", className: "admin-label", children: "Weight (oz)" }), _jsx("input", { type: "number", id: "parcel-weight", value: shippoDefaultParcel.weight, onChange: (e) => setShippoDefaultParcel((prev) => ({ ...prev, weight: parseFloat(e.target.value) || 0 })), min: "0", step: "1", className: "admin-input mt-1 w-full" })] })] }), _jsx("p", { className: "admin-helper-text mt-2", children: "Typical small box: 10\u00D78\u00D74 in, 16 oz" })] })] }))] })), shippingEnabled && (_jsx(AdminSaveBar, { isDirty: isDirty, onSave: handleSaveSettings, onDiscard: () => {
|
|
283
|
+
}, onDeleteRate: handleDeleteRateClick, formatCurrency: formatCurrency }, carrier.id))) }))] })] })), shippingEnabled && (_jsx(AdminSaveBar, { isDirty: isDirty, onSave: handleSaveSettings, onDiscard: () => {
|
|
387
284
|
if (!baselineValue)
|
|
388
285
|
return;
|
|
389
286
|
setShippingCountries(baselineValue.shippingCountries);
|
|
390
|
-
|
|
391
|
-
setShippoTestMode(baselineValue.shippoTestMode);
|
|
392
|
-
setShippoFromAddress(baselineValue.shippoFromAddress);
|
|
393
|
-
setShippoDefaultParcel(baselineValue.shippoDefaultParcel);
|
|
394
|
-
} })), showCarrierForm && (_jsx(CarrierFormModal, { carrier: editingCarrier, onClose: () => {
|
|
287
|
+
} })), showCarrierForm && (_jsx(CarrierFormModal, { carrier: editingCarrier, existingProviders: carriers.map((c) => c.provider || 'manual'), onClose: () => {
|
|
395
288
|
setShowCarrierForm(false);
|
|
396
289
|
setEditingCarrier(null);
|
|
397
290
|
}, onSuccess: () => {
|
|
@@ -405,7 +298,7 @@ export function ShippingSettings({ className }) {
|
|
|
405
298
|
setShowZoneForm(false);
|
|
406
299
|
setEditingZone(null);
|
|
407
300
|
fetchData();
|
|
408
|
-
} })), showRateForm && editingRate && (_jsx(RateFormModal, { zoneId: editingRate.zoneId, rate: editingRate.rate, onClose: () => {
|
|
301
|
+
} })), showRateForm && editingRate && (_jsx(RateFormModal, { zoneId: editingRate.zoneId, provider: editingRate.provider, rate: editingRate.rate, onClose: () => {
|
|
409
302
|
setShowRateForm(false);
|
|
410
303
|
setEditingRate(null);
|
|
411
304
|
}, onSuccess: () => {
|
|
@@ -421,14 +314,42 @@ export function ShippingSettings({ className }) {
|
|
|
421
314
|
// =============================================================================
|
|
422
315
|
// Carrier Form Modal (NEW)
|
|
423
316
|
// =============================================================================
|
|
424
|
-
|
|
317
|
+
const PROVIDER_CHOICES = [
|
|
318
|
+
{
|
|
319
|
+
value: 'manual',
|
|
320
|
+
label: 'Manual rates',
|
|
321
|
+
hint: 'You set the prices — zones, cart thresholds, customer groups.',
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
value: 'colissimo',
|
|
325
|
+
label: 'Colissimo (La Poste)',
|
|
326
|
+
hint: 'Your rates + label generation and point relais delivery in France.',
|
|
327
|
+
defaults: { name: 'Colissimo', code: 'colissimo' },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
value: 'shippo',
|
|
331
|
+
label: 'Shippo',
|
|
332
|
+
hint: 'Live carrier rates (USPS, UPS, FedEx…) quoted at checkout.',
|
|
333
|
+
defaults: { name: 'Carrier rates', code: 'shippo' },
|
|
334
|
+
},
|
|
335
|
+
];
|
|
336
|
+
function CarrierFormModal({ carrier, existingProviders = [], onClose, onSuccess, }) {
|
|
425
337
|
const isEdit = !!carrier;
|
|
426
338
|
const [isLoading, setIsLoading] = useState(false);
|
|
427
339
|
const [error, setError] = useState(null);
|
|
340
|
+
const [provider, setProvider] = useState(carrier?.provider || 'manual');
|
|
428
341
|
const [name, setName] = useState(carrier?.name || '');
|
|
429
342
|
const [code, setCode] = useState(carrier?.code || '');
|
|
430
343
|
const [description, setDescription] = useState(carrier?.description || '');
|
|
431
344
|
const [isActive, setIsActive] = useState(carrier?.isActive ?? true);
|
|
345
|
+
const handleProviderChange = (value) => {
|
|
346
|
+
setProvider(value);
|
|
347
|
+
const choice = PROVIDER_CHOICES.find((c) => c.value === value);
|
|
348
|
+
if (!isEdit && choice?.defaults) {
|
|
349
|
+
setName((prev) => prev || choice.defaults.name);
|
|
350
|
+
setCode((prev) => prev || choice.defaults.code);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
432
353
|
// Auto-generate code from name
|
|
433
354
|
const handleNameChange = (value) => {
|
|
434
355
|
setName(value);
|
|
@@ -457,6 +378,8 @@ function CarrierFormModal({ carrier, onClose, onSuccess, }) {
|
|
|
457
378
|
name,
|
|
458
379
|
code,
|
|
459
380
|
description: description || undefined,
|
|
381
|
+
// Provider is set at creation and immutable afterwards.
|
|
382
|
+
...(isEdit ? {} : { provider }),
|
|
460
383
|
isActive,
|
|
461
384
|
}),
|
|
462
385
|
});
|
|
@@ -467,7 +390,14 @@ function CarrierFormModal({ carrier, onClose, onSuccess, }) {
|
|
|
467
390
|
}
|
|
468
391
|
onSuccess();
|
|
469
392
|
};
|
|
470
|
-
return (_jsx(AdminModal, { open: true, onClose: onClose, busy: isLoading, width: "md", title: isEdit ? 'Edit shipping carrier' : 'Add shipping carrier', showClose: true, footer: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: onClose, disabled: isLoading, className: "admin-btn admin-btn-secondary", children: "Cancel" }), _jsx("button", { type: "submit", form: "carrier-form", disabled: isLoading || !name.trim() || !code.trim(), className: "admin-btn admin-btn-primary", children: isLoading ? 'Saving…' : isEdit ? 'Update carrier' : 'Create carrier' })] }), children: _jsx("form", { id: "carrier-form", onSubmit: handleSubmit, children: _jsxs("div", { className: "admin-modal-body", children: [error && (_jsx("div", { className: "admin-alert admin-alert-error mb-4", children: _jsx("div", { className: "admin-alert-description", children: error }) })), _jsxs("div", { className: "space-y-4", children: [
|
|
393
|
+
return (_jsx(AdminModal, { open: true, onClose: onClose, busy: isLoading, width: "md", title: isEdit ? 'Edit shipping carrier' : 'Add shipping carrier', showClose: true, footer: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: onClose, disabled: isLoading, className: "admin-btn admin-btn-secondary", children: "Cancel" }), _jsx("button", { type: "submit", form: "carrier-form", disabled: isLoading || !name.trim() || !code.trim(), className: "admin-btn admin-btn-primary", children: isLoading ? 'Saving…' : isEdit ? 'Update carrier' : 'Create carrier' })] }), children: _jsx("form", { id: "carrier-form", onSubmit: handleSubmit, children: _jsxs("div", { className: "admin-modal-body", children: [error && (_jsx("div", { className: "admin-alert admin-alert-error mb-4", children: _jsx("div", { className: "admin-alert-description", children: error }) })), _jsxs("div", { className: "space-y-4", children: [!isEdit && (_jsxs("div", { children: [_jsx("label", { className: "admin-label admin-label-required", children: "Carrier type" }), _jsx("div", { className: "mt-1 space-y-2", children: PROVIDER_CHOICES.map((choice) => {
|
|
394
|
+
const taken = choice.value !== 'manual' && existingProviders.includes(choice.value);
|
|
395
|
+
return (_jsxs("label", { className: `flex items-start gap-3 rounded-lg border p-3 ${taken
|
|
396
|
+
? 'cursor-not-allowed opacity-50 border-[hsl(var(--admin-border))]'
|
|
397
|
+
: provider === choice.value
|
|
398
|
+
? 'cursor-pointer border-[hsl(var(--admin-primary))] bg-[hsl(var(--admin-primary-light))]'
|
|
399
|
+
: 'cursor-pointer border-[hsl(var(--admin-border))] hover:border-[hsl(var(--admin-primary))]/50'}`, children: [_jsx("input", { type: "radio", name: "carrier-provider", value: choice.value, checked: provider === choice.value, disabled: taken, onChange: () => handleProviderChange(choice.value), className: "mt-1" }), _jsxs("span", { children: [_jsxs("span", { className: "block text-sm font-medium", children: [choice.label, taken && (_jsx("span", { className: "ml-2 text-xs text-[hsl(var(--admin-foreground-muted))]", children: "already added" }))] }), _jsx("span", { className: "block text-xs text-[hsl(var(--admin-foreground-muted))]", children: choice.hint })] })] }, choice.value));
|
|
400
|
+
}) })] })), _jsxs("div", { children: [_jsx("label", { htmlFor: "carrierName", className: "admin-label admin-label-required", children: "Carrier Name" }), _jsx("input", { type: "text", id: "carrierName", value: name, onChange: (e) => handleNameChange(e.target.value), className: "admin-input mt-1 w-full", placeholder: "e.g., USPS, UPS, FedEx", required: true })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "carrierCode", className: "admin-label admin-label-required", children: "Carrier Code" }), _jsx("input", { type: "text", id: "carrierCode", value: code, onChange: (e) => setCode(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '')), className: "admin-input mt-1 w-full", placeholder: "e.g., usps, ups, fedex", required: true, maxLength: 50 }), _jsx("p", { className: "admin-helper-text mt-1", children: "Unique identifier (lowercase, letters, numbers, hyphens only)" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "carrierDescription", className: "admin-label", children: "Description" }), _jsx("input", { type: "text", id: "carrierDescription", value: description, onChange: (e) => setDescription(e.target.value), className: "admin-input mt-1 w-full", placeholder: "e.g., United States Postal Service", maxLength: 500 })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { className: "admin-label", children: "Active" }), _jsx(AdminSwitch, { checked: isActive, onChange: setIsActive, "aria-label": "Active" })] })] })] }) }) }));
|
|
471
401
|
}
|
|
472
402
|
// =============================================================================
|
|
473
403
|
// Zone Form Modal (UPDATED - requires carrierId)
|
|
@@ -524,7 +454,7 @@ function ZoneFormModal({ carrierId, zone, onClose, onSuccess, }) {
|
|
|
524
454
|
// =============================================================================
|
|
525
455
|
// Rate Form Modal
|
|
526
456
|
// =============================================================================
|
|
527
|
-
function RateFormModal({ zoneId, rate, onClose, onSuccess, currencySymbol, }) {
|
|
457
|
+
function RateFormModal({ zoneId, provider = 'manual', rate, onClose, onSuccess, currencySymbol, }) {
|
|
528
458
|
const isEdit = !!rate;
|
|
529
459
|
const [isLoading, setIsLoading] = useState(false);
|
|
530
460
|
const [error, setError] = useState(null);
|
|
@@ -536,6 +466,8 @@ function RateFormModal({ zoneId, rate, onClose, onSuccess, currencySymbol, }) {
|
|
|
536
466
|
const [maxOrderAmount, setMaxOrderAmount] = useState(rate?.maxOrderAmount != null ? String(rate.maxOrderAmount) : '');
|
|
537
467
|
const [isActive, setIsActive] = useState(rate?.isActive ?? true);
|
|
538
468
|
const [visibleToGroups, setVisibleToGroups] = useState(rate?.visibleToGroups ?? []);
|
|
469
|
+
// Delivery type (colissimo carriers): home delivery or pickup point (relay).
|
|
470
|
+
const [fulfillment, setFulfillment] = useState(rate?.fulfillment ?? 'home');
|
|
539
471
|
// Free shipping is simply a $0 rate (optionally gated by a "free over X"
|
|
540
472
|
// minimum). Surfacing it as an explicit mode means merchants don't have to
|
|
541
473
|
// discover the price-0 trick. An existing rate priced at 0 opens in this mode.
|
|
@@ -574,6 +506,7 @@ function RateFormModal({ zoneId, rate, onClose, onSuccess, currencySymbol, }) {
|
|
|
574
506
|
maxOrderAmount: maxOrderAmount ? parseFloat(maxOrderAmount) : undefined,
|
|
575
507
|
isActive,
|
|
576
508
|
visibleToGroups,
|
|
509
|
+
...(provider === 'colissimo' ? { fulfillment } : {}),
|
|
577
510
|
}),
|
|
578
511
|
});
|
|
579
512
|
setIsLoading(false);
|
|
@@ -583,8 +516,254 @@ function RateFormModal({ zoneId, rate, onClose, onSuccess, currencySymbol, }) {
|
|
|
583
516
|
}
|
|
584
517
|
onSuccess();
|
|
585
518
|
};
|
|
586
|
-
return (_jsx(AdminModal, { open: true, onClose: onClose, busy: isLoading, width: "md", title: isEdit ? 'Edit shipping rate' : 'Add shipping rate', showClose: true, footer: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: onClose, disabled: isLoading, className: "admin-btn admin-btn-secondary", children: "Cancel" }), _jsx("button", { type: "submit", form: "shipping-rate-form", disabled: isLoading || !name.trim() || (!isFree && !price), className: "admin-btn admin-btn-primary", children: isLoading ? 'Saving…' : isEdit ? 'Update rate' : 'Create rate' })] }), children: _jsx("form", { id: "shipping-rate-form", onSubmit: handleSubmit, children: _jsxs("div", { className: "admin-modal-body", children: [error && (_jsx("div", { className: "admin-alert admin-alert-error mb-4", children: _jsx("div", { className: "admin-alert-description", children: error }) })), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "admin-label", children: "
|
|
519
|
+
return (_jsx(AdminModal, { open: true, onClose: onClose, busy: isLoading, width: "md", title: isEdit ? 'Edit shipping rate' : 'Add shipping rate', showClose: true, footer: _jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: onClose, disabled: isLoading, className: "admin-btn admin-btn-secondary", children: "Cancel" }), _jsx("button", { type: "submit", form: "shipping-rate-form", disabled: isLoading || !name.trim() || (!isFree && !price), className: "admin-btn admin-btn-primary", children: isLoading ? 'Saving…' : isEdit ? 'Update rate' : 'Create rate' })] }), children: _jsx("form", { id: "shipping-rate-form", onSubmit: handleSubmit, children: _jsxs("div", { className: "admin-modal-body", children: [error && (_jsx("div", { className: "admin-alert admin-alert-error mb-4", children: _jsx("div", { className: "admin-alert-description", children: error }) })), _jsxs("div", { className: "space-y-4", children: [provider === 'colissimo' && (_jsxs("div", { children: [_jsx("label", { className: "admin-label", children: "Delivery type" }), _jsxs("div", { className: "mt-1 grid grid-cols-2 gap-2", children: [_jsx("button", { type: "button", onClick: () => setFulfillment('home'), className: `admin-btn ${fulfillment === 'home' ? 'admin-btn-primary' : 'admin-btn-secondary'}`, children: "Home delivery" }), _jsx("button", { type: "button", onClick: () => {
|
|
520
|
+
setFulfillment('relay');
|
|
521
|
+
if (!name.trim())
|
|
522
|
+
setName('Point relais');
|
|
523
|
+
}, className: `admin-btn ${fulfillment === 'relay' ? 'admin-btn-primary' : 'admin-btn-secondary'}`, children: "Pickup point (relais)" })] }), fulfillment === 'relay' && (_jsx("p", { className: "admin-helper-text mt-1", children: "Customers pick a Colissimo point relais at checkout. A French mobile number is required on the order." }))] })), _jsxs("div", { children: [_jsx("label", { className: "admin-label", children: "Rate type" }), _jsxs("div", { className: "mt-1 grid grid-cols-2 gap-2", children: [_jsx("button", { type: "button", onClick: () => selectRateType(false), className: `admin-btn ${!isFree ? 'admin-btn-primary' : 'admin-btn-secondary'}`, children: "Paid rate" }), _jsx("button", { type: "button", onClick: () => selectRateType(true), className: `admin-btn ${isFree ? 'admin-btn-primary' : 'admin-btn-secondary'}`, children: "Free shipping" })] })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "rateName", className: "admin-label admin-label-required", children: "Rate Name" }), _jsx("input", { type: "text", id: "rateName", value: name, onChange: (e) => setName(e.target.value), className: "admin-input mt-1 w-full", placeholder: "e.g., Standard Shipping", required: true })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "rateDescription", className: "admin-label", children: "Description" }), _jsx("input", { type: "text", id: "rateDescription", value: description, onChange: (e) => setDescription(e.target.value), className: "admin-input mt-1 w-full", placeholder: "e.g., Delivery in 5-7 business days" })] }), isFree ? (_jsxs("div", { children: [_jsx("label", { className: "admin-label", children: "Price" }), _jsx("div", { className: "admin-input mt-1 flex items-center text-[hsl(var(--admin-foreground-muted))]", children: "Free \u2014 no shipping charge to the customer" })] })) : (_jsxs("div", { children: [_jsx("label", { htmlFor: "ratePrice", className: "admin-label admin-label-required", children: "Price" }), _jsxs("div", { className: "admin-input-with-addon mt-1", children: [_jsx("span", { className: "admin-input-addon-left", children: currencySymbol }), _jsx("input", { type: "number", id: "ratePrice", value: price, onChange: (e) => setPrice(e.target.value), min: "0", step: "0.01", className: "admin-input w-full pl-7", placeholder: "0.00", required: true })] })] })), _jsxs("div", { children: [_jsx("label", { htmlFor: "estimatedDays", className: "admin-label", children: "Estimated Delivery (days)" }), _jsx("input", { type: "text", id: "estimatedDays", value: estimatedDays, onChange: (e) => setEstimatedDays(e.target.value), className: "admin-input mt-1 w-full", placeholder: "e.g., 3-5" }), _jsx("p", { className: "admin-helper-text mt-1", children: "Format: single number (5) or range (3-5)" })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "minOrderAmount", className: "admin-label", children: isFree ? 'Free above' : 'Min Order Amount' }), _jsxs("div", { className: "admin-input-with-addon mt-1", children: [_jsx("span", { className: "admin-input-addon-left", children: currencySymbol }), _jsx("input", { type: "number", id: "minOrderAmount", value: minOrderAmount, onChange: (e) => setMinOrderAmount(e.target.value), min: "0", step: "0.01", className: "admin-input w-full pl-7", placeholder: "0.00" })] })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "maxOrderAmount", className: "admin-label", children: "Max Order Amount" }), _jsxs("div", { className: "admin-input-with-addon mt-1", children: [_jsx("span", { className: "admin-input-addon-left", children: currencySymbol }), _jsx("input", { type: "number", id: "maxOrderAmount", value: maxOrderAmount, onChange: (e) => setMaxOrderAmount(e.target.value), min: "0", step: "0.01", className: "admin-input w-full pl-7", placeholder: "No limit" })] })] })] }), _jsx("p", { className: "admin-helper-text", children: isFree
|
|
587
524
|
? 'Set a minimum order for free shipping, or leave both blank to always ship free.'
|
|
588
525
|
: 'Leave empty for no restrictions' }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { className: "admin-label", children: "Active" }), _jsx(AdminSwitch, { checked: isActive, onChange: setIsActive, "aria-label": "Active" })] })] }), _jsx(GroupVisibilityPicker, { value: visibleToGroups, onChange: setVisibleToGroups, noun: "shipping rate" })] }) }) }));
|
|
589
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Colissimo connection panel — renders INSIDE the colissimo carrier card
|
|
529
|
+
* (transactional editor: its own Save & Verify, outside the page's
|
|
530
|
+
* AdminSaveBar document flow). The password is write-only: the server never
|
|
531
|
+
* returns it (getColissimoConfig exposes hasPassword only), and saving
|
|
532
|
+
* without re-typing it keeps the stored one. Relay PRICING is not here —
|
|
533
|
+
* relay offers are ordinary rates (delivery type "Pickup point") under the
|
|
534
|
+
* carrier's zones below.
|
|
535
|
+
*/
|
|
536
|
+
function ColissimoPanel() {
|
|
537
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
538
|
+
const [config, setConfig] = useState(null);
|
|
539
|
+
// Form state
|
|
540
|
+
const [contractNumber, setContractNumber] = useState('');
|
|
541
|
+
const [password, setPassword] = useState('');
|
|
542
|
+
const [sender, setSender] = useState({
|
|
543
|
+
companyName: '', line2: '', zipCode: '', city: '', countryCode: 'FR',
|
|
544
|
+
});
|
|
545
|
+
const [defaultWeightKg, setDefaultWeightKg] = useState('0.5');
|
|
546
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
547
|
+
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
|
548
|
+
const [error, setError] = useState(null);
|
|
549
|
+
const isConnected = !!config && config.enabled && !!config.contractNumber && config.hasPassword;
|
|
550
|
+
const hydrateForm = useCallback((cfg) => {
|
|
551
|
+
setContractNumber(cfg.contractNumber || '');
|
|
552
|
+
if (cfg.senderAddress) {
|
|
553
|
+
setSender({
|
|
554
|
+
companyName: cfg.senderAddress.companyName || '',
|
|
555
|
+
line2: cfg.senderAddress.line2 || '',
|
|
556
|
+
zipCode: cfg.senderAddress.zipCode || '',
|
|
557
|
+
city: cfg.senderAddress.city || '',
|
|
558
|
+
countryCode: cfg.senderAddress.countryCode || 'FR',
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
if (cfg.defaultWeightKg)
|
|
562
|
+
setDefaultWeightKg(String(cfg.defaultWeightKg));
|
|
563
|
+
}, []);
|
|
564
|
+
// Pattern C — populate state from async-loaded data.
|
|
565
|
+
useEffect(() => {
|
|
566
|
+
let cancelled = false;
|
|
567
|
+
fetchAdminApi('/api/admin/shipping/colissimo').then((res) => {
|
|
568
|
+
if (cancelled)
|
|
569
|
+
return;
|
|
570
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
571
|
+
setIsLoading(false);
|
|
572
|
+
if (res.ok) {
|
|
573
|
+
setConfig(res.data.config);
|
|
574
|
+
hydrateForm(res.data.config);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
return () => {
|
|
578
|
+
cancelled = true;
|
|
579
|
+
};
|
|
580
|
+
}, [hydrateForm]);
|
|
581
|
+
const handleSave = useCallback(async () => {
|
|
582
|
+
setError(null);
|
|
583
|
+
if (!contractNumber.trim()) {
|
|
584
|
+
setError('Enter your Colissimo contract number.');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (!config?.hasPassword && !password.trim()) {
|
|
588
|
+
setError('Enter your Colissimo password.');
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
setIsSaving(true);
|
|
592
|
+
const res = await fetchAdminApi('/api/admin/shipping/colissimo', {
|
|
593
|
+
method: 'POST',
|
|
594
|
+
headers: { 'Content-Type': 'application/json' },
|
|
595
|
+
body: JSON.stringify({
|
|
596
|
+
contractNumber: contractNumber.trim(),
|
|
597
|
+
...(password.trim() ? { password: password.trim() } : {}),
|
|
598
|
+
enabled: true,
|
|
599
|
+
senderAddress: sender.companyName && sender.line2 && sender.zipCode && sender.city
|
|
600
|
+
? sender
|
|
601
|
+
: undefined,
|
|
602
|
+
defaultWeightKg: defaultWeightKg || undefined,
|
|
603
|
+
}),
|
|
604
|
+
});
|
|
605
|
+
setIsSaving(false);
|
|
606
|
+
if (!res.ok) {
|
|
607
|
+
setError(res.error);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
setPassword('');
|
|
611
|
+
adminToast.success('Colissimo connected');
|
|
612
|
+
// Re-fetch so the connected chip reflects server truth.
|
|
613
|
+
const refreshed = await fetchAdminApi('/api/admin/shipping/colissimo');
|
|
614
|
+
if (refreshed.ok) {
|
|
615
|
+
setConfig(refreshed.data.config);
|
|
616
|
+
hydrateForm(refreshed.data.config);
|
|
617
|
+
}
|
|
618
|
+
}, [contractNumber, password, sender, defaultWeightKg, config, hydrateForm]);
|
|
619
|
+
const handleDisconnect = useCallback(async () => {
|
|
620
|
+
setIsDisconnecting(true);
|
|
621
|
+
setError(null);
|
|
622
|
+
const res = await fetchAdminApi('/api/admin/shipping/colissimo', {
|
|
623
|
+
method: 'DELETE',
|
|
624
|
+
});
|
|
625
|
+
setIsDisconnecting(false);
|
|
626
|
+
if (!res.ok) {
|
|
627
|
+
setError(res.error);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
adminToast.success('Colissimo disconnected');
|
|
631
|
+
setConfig((prev) => (prev ? { ...prev, enabled: false, hasPassword: false } : prev));
|
|
632
|
+
setPassword('');
|
|
633
|
+
}, []);
|
|
634
|
+
if (isLoading)
|
|
635
|
+
return null;
|
|
636
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsx("h4", { className: "text-sm font-semibold text-[hsl(var(--admin-foreground))]", children: "Connection" }), isConnected && (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-sm text-[hsl(var(--admin-success))]", children: [_jsx(Check, { className: "h-4 w-4" }), " Connected"] }))] }), _jsx("p", { className: "admin-text-muted text-sm mb-4", children: "Colissimo generates shipping labels from your orders and delivers to points relais. Prices come from this carrier's zones and rates below \u2014 add a rate with delivery type \u201CPickup point\u201D to offer point relais at checkout." }), error && _jsx("div", { className: "admin-alert admin-alert-error mb-4", children: error }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { className: "admin-label", htmlFor: "colissimo-contract", children: "Contract number" }), _jsx("input", { id: "colissimo-contract", type: "text", value: contractNumber, onChange: (e) => setContractNumber(e.target.value), placeholder: "123456", autoComplete: "off", className: "admin-input", style: { width: '100%' } })] }), _jsxs("div", { children: [_jsxs("label", { className: "admin-label", htmlFor: "colissimo-password", children: ["Password ", config?.hasPassword && _jsx("span", { className: "admin-text-muted", children: "(stored \u2014 leave blank to keep)" })] }), _jsx("input", { id: "colissimo-password", type: "password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: config?.hasPassword ? '••••••••' : 'Colissimo password', autoComplete: "new-password", className: "admin-input", style: { width: '100%' } })] })] }), _jsxs("div", { children: [_jsx("label", { className: "admin-label", children: "Ship-from address" }), _jsxs("div", { className: "grid grid-cols-2 gap-3 mt-1", children: [_jsx("input", { type: "text", value: sender.companyName, onChange: (e) => setSender((p) => ({ ...p, companyName: e.target.value })), placeholder: "Company name", className: "admin-input", style: { width: '100%' } }), _jsx("input", { type: "text", value: sender.line2, onChange: (e) => setSender((p) => ({ ...p, line2: e.target.value })), placeholder: "Street address", className: "admin-input", style: { width: '100%' } }), _jsx("input", { type: "text", value: sender.zipCode, onChange: (e) => setSender((p) => ({ ...p, zipCode: e.target.value })), placeholder: "Postal code", className: "admin-input", style: { width: '100%' } }), _jsx("input", { type: "text", value: sender.city, onChange: (e) => setSender((p) => ({ ...p, city: e.target.value })), placeholder: "City", className: "admin-input", style: { width: '100%' } })] })] }), _jsx("div", { className: "grid grid-cols-2 gap-4", children: _jsxs("div", { children: [_jsx("label", { className: "admin-label", htmlFor: "colissimo-default-weight", children: "Default item weight (kg)" }), _jsx("input", { id: "colissimo-default-weight", type: "number", min: "0.01", max: "30", step: "0.01", value: defaultWeightKg, onChange: (e) => setDefaultWeightKg(e.target.value), className: "admin-input", style: { width: '100%' } }), _jsx("p", { className: "admin-helper-text mt-1", children: "Used for items without a weight custom field." })] }) }), _jsxs("div", { className: "flex items-center gap-2 pt-2", children: [_jsx("button", { type: "button", onClick: handleSave, disabled: isSaving, className: "admin-btn admin-btn-primary disabled:opacity-50", children: isSaving ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "admin-spinner admin-spinner-sm mr-2" }), "Verifying with Colissimo\u2026"] })) : ('Save & Verify') }), isConnected && (_jsx("button", { type: "button", onClick: handleDisconnect, disabled: isDisconnecting, className: "admin-btn admin-btn-ghost text-[hsl(var(--admin-error))] hover:bg-[hsl(var(--admin-error))]/10 disabled:opacity-50", children: isDisconnecting ? _jsx("span", { className: "admin-spinner admin-spinner-sm" }) : 'Disconnect' }))] }), _jsx("p", { className: "admin-helper-text", children: "Requires a Colissimo Entreprise contract. Credentials are verified with Colissimo before saving and stored server-side only." })] })] }));
|
|
637
|
+
}
|
|
638
|
+
// =============================================================================
|
|
639
|
+
// Shippo Panel (renders inside the shippo carrier card)
|
|
640
|
+
// =============================================================================
|
|
641
|
+
/**
|
|
642
|
+
* Shippo connection + configuration panel — renders INSIDE the shippo
|
|
643
|
+
* carrier card (transactional editor: its own Save, outside the page's
|
|
644
|
+
* AdminSaveBar document flow). Owns the API key lifecycle plus the
|
|
645
|
+
* ship-from address and default parcel used for live rate quotes.
|
|
646
|
+
*/
|
|
647
|
+
function ShippoPanel() {
|
|
648
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
649
|
+
const [shippoTestMode, setShippoTestMode] = useState(true);
|
|
650
|
+
const [shippoFromAddress, setShippoFromAddress] = useState(DEFAULT_FROM_ADDRESS);
|
|
651
|
+
const [shippoDefaultParcel, setShippoDefaultParcel] = useState(DEFAULT_PARCEL);
|
|
652
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
653
|
+
// API key lifecycle
|
|
654
|
+
const [shippoKeyValid, setShippoKeyValid] = useState(null);
|
|
655
|
+
const [isValidatingShippo, setIsValidatingShippo] = useState(false);
|
|
656
|
+
const [shippoApiKeyInput, setShippoApiKeyInput] = useState('');
|
|
657
|
+
const [isSavingApiKey, setIsSavingApiKey] = useState(false);
|
|
658
|
+
const [hasStoredKey, setHasStoredKey] = useState(false);
|
|
659
|
+
const [maskedKey, setMaskedKey] = useState(null);
|
|
660
|
+
const [isDeletingApiKey, setIsDeletingApiKey] = useState(false);
|
|
661
|
+
const handleValidateShippoKey = useCallback(async () => {
|
|
662
|
+
setIsValidatingShippo(true);
|
|
663
|
+
setShippoKeyValid(null);
|
|
664
|
+
const res = await fetchAdminApi('/api/admin/shipping/shippo/validate', { method: 'POST' });
|
|
665
|
+
setIsValidatingShippo(false);
|
|
666
|
+
if (!res.ok) {
|
|
667
|
+
setShippoKeyValid(false);
|
|
668
|
+
setHasStoredKey(false);
|
|
669
|
+
setMaskedKey(null);
|
|
670
|
+
adminToast.error(res.error);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
setShippoKeyValid(res.data.valid ?? false);
|
|
674
|
+
setHasStoredKey(res.data.hasStoredKey ?? false);
|
|
675
|
+
setMaskedKey(res.data.maskedKey ?? null);
|
|
676
|
+
}, []);
|
|
677
|
+
// Pattern C — populate state from async-loaded settings + validate the key.
|
|
678
|
+
useEffect(() => {
|
|
679
|
+
let cancelled = false;
|
|
680
|
+
fetchAdminApi('/api/admin/settings').then((res) => {
|
|
681
|
+
if (cancelled)
|
|
682
|
+
return;
|
|
683
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
684
|
+
setIsLoading(false);
|
|
685
|
+
if (!res.ok)
|
|
686
|
+
return;
|
|
687
|
+
const settings = res.data.settings || {};
|
|
688
|
+
const storeCountry = typeof settings.storeCountry === 'string' && settings.storeCountry.length === 2
|
|
689
|
+
? settings.storeCountry
|
|
690
|
+
: null;
|
|
691
|
+
setShippoTestMode(settings.shippoTestMode ?? true);
|
|
692
|
+
if (settings.shippoFromAddress) {
|
|
693
|
+
setShippoFromAddress(settings.shippoFromAddress);
|
|
694
|
+
}
|
|
695
|
+
else if (storeCountry) {
|
|
696
|
+
setShippoFromAddress({ ...DEFAULT_FROM_ADDRESS, country: storeCountry });
|
|
697
|
+
}
|
|
698
|
+
if (settings.shippoDefaultParcel) {
|
|
699
|
+
setShippoDefaultParcel(settings.shippoDefaultParcel);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
handleValidateShippoKey();
|
|
703
|
+
return () => {
|
|
704
|
+
cancelled = true;
|
|
705
|
+
};
|
|
706
|
+
}, [handleValidateShippoKey]);
|
|
707
|
+
const handleSaveShippoApiKey = useCallback(async () => {
|
|
708
|
+
if (!shippoApiKeyInput.trim())
|
|
709
|
+
return;
|
|
710
|
+
setIsSavingApiKey(true);
|
|
711
|
+
const res = await fetchAdminApi('/api/admin/shipping/shippo/api-key', {
|
|
712
|
+
method: 'POST',
|
|
713
|
+
headers: { 'Content-Type': 'application/json' },
|
|
714
|
+
body: JSON.stringify({ apiKey: shippoApiKeyInput }),
|
|
715
|
+
});
|
|
716
|
+
setIsSavingApiKey(false);
|
|
717
|
+
if (!res.ok) {
|
|
718
|
+
adminToast.error(res.error);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
setHasStoredKey(true);
|
|
722
|
+
setMaskedKey(res.data.maskedKey ?? null);
|
|
723
|
+
setShippoApiKeyInput('');
|
|
724
|
+
setShippoKeyValid(true);
|
|
725
|
+
adminToast.success('API key saved and verified');
|
|
726
|
+
}, [shippoApiKeyInput]);
|
|
727
|
+
const handleRemoveApiKey = useCallback(async () => {
|
|
728
|
+
setIsDeletingApiKey(true);
|
|
729
|
+
const res = await fetchAdminApi('/api/admin/shipping/shippo/api-key', {
|
|
730
|
+
method: 'DELETE',
|
|
731
|
+
});
|
|
732
|
+
setIsDeletingApiKey(false);
|
|
733
|
+
if (!res.ok) {
|
|
734
|
+
adminToast.error(res.error);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
setHasStoredKey(false);
|
|
738
|
+
setMaskedKey(null);
|
|
739
|
+
adminToast.success('API key removed');
|
|
740
|
+
// Re-validate to check if env var exists
|
|
741
|
+
handleValidateShippoKey();
|
|
742
|
+
}, [handleValidateShippoKey]);
|
|
743
|
+
const handleSaveConfig = useCallback(async () => {
|
|
744
|
+
setIsSaving(true);
|
|
745
|
+
const res = await fetchAdminApi('/api/admin/settings', {
|
|
746
|
+
method: 'PUT',
|
|
747
|
+
headers: { 'Content-Type': 'application/json' },
|
|
748
|
+
body: JSON.stringify({ shippoTestMode, shippoFromAddress, shippoDefaultParcel }),
|
|
749
|
+
});
|
|
750
|
+
setIsSaving(false);
|
|
751
|
+
if (!res.ok) {
|
|
752
|
+
adminToast.error(res.error);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
adminToast.success('Shippo settings saved');
|
|
756
|
+
}, [shippoTestMode, shippoFromAddress, shippoDefaultParcel]);
|
|
757
|
+
if (isLoading)
|
|
758
|
+
return null;
|
|
759
|
+
return (_jsxs("div", { className: "space-y-5", children: [_jsxs("div", { className: "p-4 rounded-lg border border-[hsl(var(--admin-border))] bg-[hsl(var(--admin-card))]", children: [shippoKeyValid === true ? (_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "h-10 w-10 rounded-full bg-[hsl(var(--admin-success))]/20 flex items-center justify-center", children: _jsx(Check, { className: "h-5 w-5 text-[hsl(var(--admin-success))]" }) }), _jsxs("div", { children: [_jsx("p", { className: "font-medium text-[hsl(var(--admin-foreground))]", children: "Connected" }), _jsx("p", { className: "text-sm text-[hsl(var(--admin-foreground-muted))]", children: hasStoredKey ? (_jsxs(_Fragment, { children: ["API key: ", _jsx("code", { className: "px-1 py-0.5 rounded bg-[hsl(var(--admin-background))] text-xs", children: maskedKey })] })) : ('Using environment variable') })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [hasStoredKey && (_jsx("button", { type: "button", onClick: handleRemoveApiKey, disabled: isDeletingApiKey, className: "admin-btn admin-btn-ghost admin-btn-sm text-[hsl(var(--admin-error))] hover:bg-[hsl(var(--admin-error))]/10", children: isDeletingApiKey ? _jsx("span", { className: "admin-spinner admin-spinner-sm" }) : 'Remove key' })), _jsx("button", { type: "button", onClick: handleValidateShippoKey, disabled: isValidatingShippo, className: "admin-btn admin-btn-secondary disabled:opacity-50", children: isValidatingShippo ? _jsx(RefreshCw, { className: "h-4 w-4 animate-spin" }) : 'Re-verify' })] })] })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "h-10 w-10 rounded-full bg-[hsl(var(--admin-warning))]/20 flex items-center justify-center", children: isValidatingShippo ? (_jsx("span", { className: "admin-spinner admin-spinner-sm" })) : (_jsx(Key, { className: "h-5 w-5 text-[hsl(var(--admin-warning))]" })) }), _jsxs("div", { children: [_jsx("p", { className: "font-medium text-[hsl(var(--admin-foreground))]", children: isValidatingShippo
|
|
760
|
+
? 'Verifying…'
|
|
761
|
+
: hasStoredKey && shippoKeyValid === false
|
|
762
|
+
? 'API Key Invalid'
|
|
763
|
+
: 'API Key Required' }), _jsx("p", { className: "text-sm text-[hsl(var(--admin-foreground-muted))]", children: isValidatingShippo
|
|
764
|
+
? 'Checking your Shippo connection…'
|
|
765
|
+
: hasStoredKey && shippoKeyValid === false
|
|
766
|
+
? `Current key (${maskedKey}) is invalid. Enter a new key.`
|
|
767
|
+
: 'Enter your Shippo API key to enable live carrier rates.' })] })] }), !isValidatingShippo && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "password", value: shippoApiKeyInput, onChange: (e) => setShippoApiKeyInput(e.target.value), placeholder: "shippo_live_\u2026", className: "admin-input flex-1" }), _jsx("button", { type: "button", onClick: handleSaveShippoApiKey, disabled: isSavingApiKey || !shippoApiKeyInput.trim(), className: "admin-btn admin-btn-primary disabled:opacity-50", children: isSavingApiKey ? _jsx("span", { className: "admin-spinner admin-spinner-sm" }) : 'Save & Verify' })] }), _jsxs("p", { className: "text-xs text-[hsl(var(--admin-foreground-muted))]", children: ["Get your API key from", ' ', _jsxs("a", { href: "https://apps.goshippo.com/settings/api", target: "_blank", rel: "noopener noreferrer", className: "text-[hsl(var(--admin-primary))] hover:underline inline-flex items-center gap-1", children: ["Shippo Dashboard", _jsx(ExternalLink, { className: "h-3 w-3" })] })] })] }))] })), _jsxs("div", { className: "flex items-center justify-between mt-4 pt-4 border-t border-[hsl(var(--admin-border))]", children: [_jsxs("div", { children: [_jsx("label", { className: "admin-label", children: "Test Mode" }), _jsx("p", { className: "admin-text-muted text-sm", children: "Use Shippo test API for development (free API calls)" })] }), _jsx(AdminSwitch, { checked: shippoTestMode, onChange: setShippoTestMode, "aria-label": "Test mode" })] })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-[hsl(var(--admin-primary))]" }), _jsx("h4", { className: "text-sm font-semibold text-[hsl(var(--admin-foreground))]", children: "Ship-From Address" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-name", className: "admin-label", children: "Name" }), _jsx("input", { type: "text", id: "shippo-name", value: shippoFromAddress.name, onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, name: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "John Smith" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-company", className: "admin-label", children: "Company (optional)" }), _jsx("input", { type: "text", id: "shippo-company", value: shippoFromAddress.company || '', onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, company: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "Acme Inc." })] }), _jsxs("div", { className: "md:col-span-2", children: [_jsx("label", { htmlFor: "shippo-street1", className: "admin-label", children: "Street Address" }), _jsx("input", { type: "text", id: "shippo-street1", value: shippoFromAddress.street1, onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, street1: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "123 Main St" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-city", className: "admin-label", children: "City" }), _jsx("input", { type: "text", id: "shippo-city", value: shippoFromAddress.city, onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, city: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "San Francisco" })] }), shippoFromAddress.country === 'US' && (_jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-state", className: "admin-label", children: "State" }), _jsx(AdminSelect, { id: "shippo-state", value: shippoFromAddress.state, onChange: (value) => setShippoFromAddress((prev) => ({ ...prev, state: value })), placeholder: "Select state\u2026", options: US_STATES.map((state) => ({ value: state.code, label: state.name })), className: "mt-1 w-full" })] })), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-zip", className: "admin-label", children: "ZIP Code" }), _jsx("input", { type: "text", id: "shippo-zip", value: shippoFromAddress.zip, onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, zip: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "94102" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-country", className: "admin-label", children: "Country" }), _jsx(AdminSelect, { id: "shippo-country", value: shippoFromAddress.country, onChange: (value) => setShippoFromAddress((prev) => ({ ...prev, country: value })), options: COMMON_COUNTRIES.map((country) => ({ value: country.code, label: country.name })), className: "mt-1 w-full" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "shippo-phone", className: "admin-label", children: "Phone (optional)" }), _jsx("input", { type: "tel", id: "shippo-phone", value: shippoFromAddress.phone || '', onChange: (e) => setShippoFromAddress((prev) => ({ ...prev, phone: e.target.value })), className: "admin-input mt-1 w-full", placeholder: "+1 555 123 4567" })] })] })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [_jsx(Box, { className: "h-4 w-4 text-[hsl(var(--admin-primary))]" }), _jsx("h4", { className: "text-sm font-semibold text-[hsl(var(--admin-foreground))]", children: "Default Package Size" })] }), _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "parcel-length", className: "admin-label", children: "Length (in)" }), _jsx("input", { type: "number", id: "parcel-length", value: shippoDefaultParcel.length, onChange: (e) => setShippoDefaultParcel((prev) => ({ ...prev, length: parseFloat(e.target.value) || 0 })), min: "0", step: "0.5", className: "admin-input mt-1 w-full" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "parcel-width", className: "admin-label", children: "Width (in)" }), _jsx("input", { type: "number", id: "parcel-width", value: shippoDefaultParcel.width, onChange: (e) => setShippoDefaultParcel((prev) => ({ ...prev, width: parseFloat(e.target.value) || 0 })), min: "0", step: "0.5", className: "admin-input mt-1 w-full" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "parcel-height", className: "admin-label", children: "Height (in)" }), _jsx("input", { type: "number", id: "parcel-height", value: shippoDefaultParcel.height, onChange: (e) => setShippoDefaultParcel((prev) => ({ ...prev, height: parseFloat(e.target.value) || 0 })), min: "0", step: "0.5", className: "admin-input mt-1 w-full" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "parcel-weight", className: "admin-label", children: "Weight (oz)" }), _jsx("input", { type: "number", id: "parcel-weight", value: shippoDefaultParcel.weight, onChange: (e) => setShippoDefaultParcel((prev) => ({ ...prev, weight: parseFloat(e.target.value) || 0 })), min: "0", step: "1", className: "admin-input mt-1 w-full" })] })] }), _jsx("p", { className: "admin-helper-text mt-2", children: "Typical small box: 10\u00D78\u00D74 in, 16 oz" })] }), _jsx("div", { children: _jsx("button", { type: "button", onClick: handleSaveConfig, disabled: isSaving, className: "admin-btn admin-btn-primary disabled:opacity-50", children: isSaving ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "admin-spinner admin-spinner-sm mr-2" }), "Saving\u2026"] })) : ('Save Shippo settings') }) })] }));
|
|
768
|
+
}
|
|
590
769
|
//# sourceMappingURL=ShippingSettings.js.map
|