@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.
Files changed (89) hide show
  1. package/dist/admin/api/index.d.ts +2 -2
  2. package/dist/admin/api/index.d.ts.map +1 -1
  3. package/dist/admin/api/index.js +2 -2
  4. package/dist/admin/api/index.js.map +1 -1
  5. package/dist/admin/api/orders.d.ts +49 -0
  6. package/dist/admin/api/orders.d.ts.map +1 -1
  7. package/dist/admin/api/orders.js +293 -30
  8. package/dist/admin/api/orders.js.map +1 -1
  9. package/dist/admin/api/settings.d.ts.map +1 -1
  10. package/dist/admin/api/settings.js +6 -1
  11. package/dist/admin/api/settings.js.map +1 -1
  12. package/dist/admin/api/shipping.d.ts +49 -0
  13. package/dist/admin/api/shipping.d.ts.map +1 -1
  14. package/dist/admin/api/shipping.js +144 -0
  15. package/dist/admin/api/shipping.js.map +1 -1
  16. package/dist/admin/components/OrderDetails.d.ts.map +1 -1
  17. package/dist/admin/components/OrderDetails.js +69 -1
  18. package/dist/admin/components/OrderDetails.js.map +1 -1
  19. package/dist/admin/components/ShippingSettings.d.ts.map +1 -1
  20. package/dist/admin/components/ShippingSettings.js +313 -134
  21. package/dist/admin/components/ShippingSettings.js.map +1 -1
  22. package/dist/admin/types.d.ts +13 -0
  23. package/dist/admin/types.d.ts.map +1 -1
  24. package/dist/checkout/api/colissimo-relays.d.ts +24 -0
  25. package/dist/checkout/api/colissimo-relays.d.ts.map +1 -0
  26. package/dist/checkout/api/colissimo-relays.js +60 -0
  27. package/dist/checkout/api/colissimo-relays.js.map +1 -0
  28. package/dist/checkout/api/manual.d.ts.map +1 -1
  29. package/dist/checkout/api/manual.js +21 -5
  30. package/dist/checkout/api/manual.js.map +1 -1
  31. package/dist/checkout/api/shipping.d.ts.map +1 -1
  32. package/dist/checkout/api/shipping.js +60 -82
  33. package/dist/checkout/api/shipping.js.map +1 -1
  34. package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
  35. package/dist/checkout/components/CheckoutFlow.js +142 -3
  36. package/dist/checkout/components/CheckoutFlow.js.map +1 -1
  37. package/dist/checkout/components/ShippingOptions.d.ts +8 -1
  38. package/dist/checkout/components/ShippingOptions.d.ts.map +1 -1
  39. package/dist/checkout/components/ShippingOptions.js +40 -11
  40. package/dist/checkout/components/ShippingOptions.js.map +1 -1
  41. package/dist/checkout/components/colissimo-logo.d.ts +12 -0
  42. package/dist/checkout/components/colissimo-logo.d.ts.map +1 -0
  43. package/dist/checkout/components/colissimo-logo.js +12 -0
  44. package/dist/checkout/components/colissimo-logo.js.map +1 -0
  45. package/dist/checkout/server/create-checkout-session.d.ts.map +1 -1
  46. package/dist/checkout/server/create-checkout-session.js +55 -13
  47. package/dist/checkout/server/create-checkout-session.js.map +1 -1
  48. package/dist/checkout/server/handle-webhook.js +7 -3
  49. package/dist/checkout/server/handle-webhook.js.map +1 -1
  50. package/dist/checkout/server/order-service.d.ts.map +1 -1
  51. package/dist/checkout/server/order-service.js +25 -0
  52. package/dist/checkout/server/order-service.js.map +1 -1
  53. package/dist/checkout/server/pricing.d.ts +20 -0
  54. package/dist/checkout/server/pricing.d.ts.map +1 -1
  55. package/dist/checkout/server/pricing.js +51 -5
  56. package/dist/checkout/server/pricing.js.map +1 -1
  57. package/dist/checkout/types.d.ts +57 -1
  58. package/dist/checkout/types.d.ts.map +1 -1
  59. package/dist/core/db/queries.d.ts +157 -35
  60. package/dist/core/db/queries.d.ts.map +1 -1
  61. package/dist/core/db/queries.js +223 -1
  62. package/dist/core/db/queries.js.map +1 -1
  63. package/dist/core/db/schema.d.ts +323 -5
  64. package/dist/core/db/schema.d.ts.map +1 -1
  65. package/dist/core/db/schema.js +70 -0
  66. package/dist/core/db/schema.js.map +1 -1
  67. package/dist/core/i18n/messages/en.d.ts.map +1 -1
  68. package/dist/core/i18n/messages/en.js +9 -0
  69. package/dist/core/i18n/messages/en.js.map +1 -1
  70. package/dist/core/i18n/messages/fr.d.ts.map +1 -1
  71. package/dist/core/i18n/messages/fr.js +9 -0
  72. package/dist/core/i18n/messages/fr.js.map +1 -1
  73. package/dist/shipping/colissimo.d.ts +166 -0
  74. package/dist/shipping/colissimo.d.ts.map +1 -0
  75. package/dist/shipping/colissimo.js +424 -0
  76. package/dist/shipping/colissimo.js.map +1 -0
  77. package/dist/shipping/index.d.ts +4 -1
  78. package/dist/shipping/index.d.ts.map +1 -1
  79. package/dist/shipping/index.js +3 -1
  80. package/dist/shipping/index.js.map +1 -1
  81. package/docs/changelog/epoch-07-shipping-carriers.md +125 -0
  82. package/package.json +6 -1
  83. package/templates/store-template/.claude/skills/manage-products/SKILL.md +1 -1
  84. package/templates/store-template/app/api/admin/orders/[id]/colissimo-defaults/route.ts +6 -0
  85. package/templates/store-template/app/api/admin/orders/[id]/label/route.ts +6 -0
  86. package/templates/store-template/app/api/admin/orders/[id]/ship-with-colissimo/route.ts +6 -0
  87. package/templates/store-template/app/api/admin/shipping/colissimo/route.ts +10 -0
  88. package/templates/store-template/app/api/checkout/colissimo-relays/route.ts +4 -0
  89. 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
- 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: [_jsx(Truck, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }), _jsxs("div", { children: [_jsx("span", { className: "font-semibold text-[hsl(var(--admin-foreground))]", children: carrier.name }), _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: [!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 && (_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: "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
+ 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 (NEW - carrier-first architecture)
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
- const { isDirty, baselineValue, rebaseline } = useDirtyState({ shippingCountries, shippingMode, shippoTestMode, shippoFromAddress, shippoDefaultParcel }, !isFetching);
90
- const [shippoKeyValid, setShippoKeyValid] = useState(null);
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, shippingMode, shippoTestMode, shippoFromAddress, shippoDefaultParcel, rebaseline]);
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-4", children: [_jsx(Truck, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }), _jsx("h3", { className: "admin-card-title", children: "Shipping Method" })] }), _jsx("p", { className: "admin-text-muted text-sm mb-4", children: "Choose how shipping rates are calculated at checkout." }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsxs("button", { type: "button", onClick: () => setShippingMode('manual'), className: `p-4 rounded-lg border-2 text-left transition-colors ${shippingMode === 'manual'
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" })] })] }) })] }), shippingMode === 'manual' && (_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: "Shipping Carriers" })] }), _jsxs("button", { type: "button", onClick: () => {
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: "Configure your shipping carriers, zones, and rates. Each carrier can have multiple zones, and each zone can have multiple rates." }), 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: () => {
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))) }))] })), shippingMode === 'shippo' && (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "admin-card rounded-lg p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-4", children: [_jsx(Plug, { className: "h-5 w-5 text-[hsl(var(--admin-primary))]" }), _jsx("h3", { className: "admin-card-title", children: "Shippo Connection" })] }), _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') })] })] })), shippoKeyValid !== true && (_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
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
- setShippingMode(baselineValue.shippingMode);
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
- function CarrierFormModal({ carrier, onClose, onSuccess, }) {
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: [_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" })] })] })] }) }) }));
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: "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
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