@burdenoff/microfe-movethewheels 2026.920.2 → 2026.920.3

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.
@@ -2,5 +2,26 @@
2
2
  function e(e, t) {
3
3
  return (e.endsWith("/") ? e.slice(0, -1) : e) + (t.startsWith("/") ? t : `/${t}`);
4
4
  }
5
+ function t(e, t) {
6
+ if (!t || Object.keys(t).length === 0) return e;
7
+ let n = new URLSearchParams();
8
+ for (let [e, r] of Object.entries(t)) r != null && r !== "" && n.set(e, String(r));
9
+ let r = n.toString();
10
+ return r ? `${e}?${r}` : e;
11
+ }
12
+ var n = [/^orders\/new$/, /^orders\/[^/?#]+\/edit$/];
13
+ function r(e) {
14
+ if (!e || e.length > 200 || e.includes("://") || e.startsWith("/")) return null;
15
+ let [t] = e.split(/[?#]/, 1);
16
+ return n.some((e) => e.test(t)) ? t : null;
17
+ }
18
+ function i(e, n) {
19
+ let i = r(e);
20
+ return i ? t(i, { customerId: n }) : e;
21
+ }
22
+ function a(e) {
23
+ let n = r(e);
24
+ return n ? t("customers/new", { returnTo: n }) : "customers/new";
25
+ }
5
26
  //#endregion
6
- export { e as joinPath };
27
+ export { a as buildCustomerCreatePath, i as buildOrderCustomerPath, r as getSafeCustomerReturnPath, e as joinPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-movethewheels",
3
- "version": "2026.920.2",
3
+ "version": "2026.920.3",
4
4
  "description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,307 @@
1
+ import { useState, type ChangeEvent, type FormEvent } from 'react';
2
+ import { Plus, RefreshCw, UserPlus } from 'lucide-react';
3
+ import {
4
+ Button,
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ Input,
12
+ Label,
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from './ui';
19
+ import type { Customer } from '../types';
20
+ import { FormError } from './FormError';
21
+
22
+ interface CustomerPickerProps {
23
+ customers: Customer[];
24
+ value: string;
25
+ onValueChange: (value: string) => void;
26
+ onCreated: (customer: Customer) => void;
27
+ createCustomer: (customer: Partial<Customer>) => Promise<Customer>;
28
+ isLoading: boolean;
29
+ error: Error | null;
30
+ onRetry: () => void;
31
+ onOpenCustomers?: () => void;
32
+ selectedCustomer?: Customer | null;
33
+ disabled?: boolean;
34
+ }
35
+
36
+ interface CustomerFormState {
37
+ name: string;
38
+ type: Customer['type'];
39
+ email: string;
40
+ phone: string;
41
+ company: string;
42
+ }
43
+
44
+ const EMPTY_FORM: CustomerFormState = {
45
+ name: '',
46
+ type: 'b2c',
47
+ email: '',
48
+ phone: '',
49
+ company: '',
50
+ };
51
+
52
+ function mergeCustomers(customers: Customer[], selectedCustomer?: Customer | null): Customer[] {
53
+ if (!selectedCustomer || customers.some((customer) => customer.id === selectedCustomer.id)) {
54
+ return customers;
55
+ }
56
+ return [selectedCustomer, ...customers];
57
+ }
58
+
59
+ export function CustomerPicker({
60
+ customers,
61
+ value,
62
+ onValueChange,
63
+ onCreated,
64
+ createCustomer,
65
+ isLoading,
66
+ error,
67
+ onRetry,
68
+ onOpenCustomers,
69
+ selectedCustomer,
70
+ disabled = false,
71
+ }: CustomerPickerProps) {
72
+ const [isCreateOpen, setIsCreateOpen] = useState(false);
73
+ const [form, setForm] = useState<CustomerFormState>(EMPTY_FORM);
74
+ const [isCreating, setIsCreating] = useState(false);
75
+ const [createError, setCreateError] = useState<string | null>(null);
76
+ const options = mergeCustomers(customers, selectedCustomer);
77
+
78
+ const openCreateDialog = () => {
79
+ setCreateError(null);
80
+ setForm(EMPTY_FORM);
81
+ setIsCreateOpen(true);
82
+ };
83
+
84
+ const closeCreateDialog = () => {
85
+ if (isCreating) return;
86
+ setIsCreateOpen(false);
87
+ };
88
+
89
+ const handleCreate = async (event: FormEvent<HTMLFormElement>) => {
90
+ event.preventDefault();
91
+ event.stopPropagation();
92
+ const name = form.name.trim();
93
+ if (!name) {
94
+ setCreateError('Customer name is required.');
95
+ return;
96
+ }
97
+
98
+ setIsCreating(true);
99
+ setCreateError(null);
100
+ try {
101
+ const created = await createCustomer({
102
+ name,
103
+ type: form.type,
104
+ email: form.email.trim() || undefined,
105
+ phone: form.phone.trim() || undefined,
106
+ company: form.company.trim() || undefined,
107
+ });
108
+ onCreated(created);
109
+ setIsCreateOpen(false);
110
+ setForm(EMPTY_FORM);
111
+ } catch (creationError) {
112
+ setCreateError(
113
+ creationError instanceof Error
114
+ ? creationError.message
115
+ : 'Could not create the customer. Please try again.'
116
+ );
117
+ } finally {
118
+ setIsCreating(false);
119
+ }
120
+ };
121
+
122
+ return (
123
+ <div className="space-y-2">
124
+ <div className="flex items-center justify-between gap-2">
125
+ <Label htmlFor="customer">Customer</Label>
126
+ <Button
127
+ type="button"
128
+ variant="ghost"
129
+ size="sm"
130
+ onClick={openCreateDialog}
131
+ disabled={disabled || isLoading}
132
+ >
133
+ <Plus size={14} className="mr-1" />
134
+ Add customer
135
+ </Button>
136
+ </div>
137
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-start">
138
+ <Select
139
+ value={value}
140
+ onValueChange={onValueChange}
141
+ disabled={disabled || isLoading || Boolean(error)}
142
+ >
143
+ <SelectTrigger id="customer" className="flex-1">
144
+ <SelectValue placeholder={isLoading ? 'Loading customers...' : 'Select customer'} />
145
+ </SelectTrigger>
146
+ <SelectContent>
147
+ {options.map((customer) => (
148
+ <SelectItem key={customer.id} value={customer.id}>
149
+ {customer.name} {customer.email ? `(${customer.email})` : ''}
150
+ </SelectItem>
151
+ ))}
152
+ </SelectContent>
153
+ </Select>
154
+ {error && (
155
+ <Button type="button" variant="outline" size="sm" onClick={onRetry}>
156
+ <RefreshCw size={14} className="mr-1" />
157
+ Retry
158
+ </Button>
159
+ )}
160
+ </div>
161
+ {error ? (
162
+ <div className="flex flex-wrap items-center gap-2">
163
+ <p role="alert" className="text-sm text-status-error-text">
164
+ Customers could not be loaded. Retry or add a customer to continue.
165
+ </p>
166
+ {onOpenCustomers && (
167
+ <Button type="button" variant="outline" size="sm" onClick={onOpenCustomers}>
168
+ Open customer page
169
+ </Button>
170
+ )}
171
+ </div>
172
+ ) : !isLoading && options.length === 0 ? (
173
+ <div className="rounded-lg border border-border-seam bg-bg-sunken/40 p-3">
174
+ <div className="flex items-start gap-2">
175
+ <UserPlus size={16} className="mt-0.5 shrink-0 text-text-secondary" />
176
+ <div>
177
+ <p className="text-sm font-medium text-text-primary">Add a customer to continue</p>
178
+ <p className="text-sm text-text-secondary">
179
+ The customer will be selected automatically and your order details will stay here.
180
+ </p>
181
+ </div>
182
+ </div>
183
+ <Button
184
+ type="button"
185
+ size="sm"
186
+ className="mt-3"
187
+ onClick={openCreateDialog}
188
+ disabled={disabled}
189
+ >
190
+ <Plus size={14} className="mr-1.5" />
191
+ Create customer
192
+ </Button>
193
+ </div>
194
+ ) : null}
195
+
196
+ <Dialog
197
+ open={isCreateOpen}
198
+ onOpenChange={(open: boolean) => (open ? setIsCreateOpen(true) : closeCreateDialog())}
199
+ >
200
+ <DialogContent>
201
+ <DialogHeader>
202
+ <DialogTitle>Create customer</DialogTitle>
203
+ <DialogDescription>
204
+ Add the customer without leaving this order. They will be selected automatically.
205
+ </DialogDescription>
206
+ </DialogHeader>
207
+ <form onSubmit={handleCreate} className="space-y-4">
208
+ <FormError message={createError} />
209
+ <div className="space-y-2">
210
+ <Label htmlFor="quick-customer-name">Name</Label>
211
+ <Input
212
+ id="quick-customer-name"
213
+ value={form.name}
214
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
215
+ setForm((previous) => ({ ...previous, name: event.target.value }))
216
+ }
217
+ placeholder="Acme Corp or Jane Doe"
218
+ autoFocus
219
+ required
220
+ />
221
+ </div>
222
+ <div className="space-y-2">
223
+ <Label htmlFor="quick-customer-type">Customer type</Label>
224
+ <Select
225
+ value={form.type}
226
+ onValueChange={(value: string) =>
227
+ setForm((previous) => ({ ...previous, type: value as Customer['type'] }))
228
+ }
229
+ >
230
+ <SelectTrigger id="quick-customer-type">
231
+ <SelectValue />
232
+ </SelectTrigger>
233
+ <SelectContent>
234
+ <SelectItem value="b2c">B2C (Individual)</SelectItem>
235
+ <SelectItem value="b2b">B2B (Business)</SelectItem>
236
+ </SelectContent>
237
+ </Select>
238
+ </div>
239
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
240
+ <div className="space-y-2">
241
+ <Label htmlFor="quick-customer-email">Email</Label>
242
+ <Input
243
+ id="quick-customer-email"
244
+ type="email"
245
+ value={form.email}
246
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
247
+ setForm((previous) => ({ ...previous, email: event.target.value }))
248
+ }
249
+ placeholder="customer@example.com"
250
+ />
251
+ </div>
252
+ <div className="space-y-2">
253
+ <Label htmlFor="quick-customer-phone">Phone</Label>
254
+ <Input
255
+ id="quick-customer-phone"
256
+ value={form.phone}
257
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
258
+ setForm((previous) => ({ ...previous, phone: event.target.value }))
259
+ }
260
+ placeholder="+1-555-0100"
261
+ />
262
+ </div>
263
+ </div>
264
+ <div className="space-y-2">
265
+ <Label htmlFor="quick-customer-company">Company</Label>
266
+ <Input
267
+ id="quick-customer-company"
268
+ value={form.company}
269
+ onChange={(event: ChangeEvent<HTMLInputElement>) =>
270
+ setForm((previous) => ({ ...previous, company: event.target.value }))
271
+ }
272
+ placeholder="Company name (optional)"
273
+ />
274
+ </div>
275
+ <DialogFooter>
276
+ {onOpenCustomers && (
277
+ <Button
278
+ type="button"
279
+ variant="outline"
280
+ onClick={() => {
281
+ closeCreateDialog();
282
+ onOpenCustomers();
283
+ }}
284
+ disabled={isCreating}
285
+ >
286
+ Use customer page
287
+ </Button>
288
+ )}
289
+ <Button
290
+ type="button"
291
+ variant="outline"
292
+ onClick={closeCreateDialog}
293
+ disabled={isCreating}
294
+ >
295
+ Cancel
296
+ </Button>
297
+ <Button type="submit" disabled={isCreating}>
298
+ <Plus size={16} />
299
+ {isCreating ? 'Creating...' : 'Create and select'}
300
+ </Button>
301
+ </DialogFooter>
302
+ </form>
303
+ </DialogContent>
304
+ </Dialog>
305
+ </div>
306
+ );
307
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { useEffect, useState } from 'react';
8
- import { useParams } from 'react-router';
8
+ import { useParams, useSearchParams } from 'react-router';
9
9
  import { Package, ArrowLeft, Save, Plus, Trash2, Users, MapPin, CalendarClock } from 'lucide-react';
10
10
  import { PageLayout, EmptyState } from '../components/PageLayout';
11
11
  import { FormError } from '../components/FormError';
@@ -27,9 +27,11 @@ import {
27
27
  } from '../components/ui';
28
28
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
29
29
  import { useOrder, useOrders } from '../hooks/useOrders';
30
- import { useCustomers } from '../hooks/useCustomers';
30
+ import { useCustomer, useCustomers } from '../hooks/useCustomers';
31
31
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
32
- import { joinPath } from '../utils/navigation';
32
+ import { joinPath, buildCustomerCreatePath } from '../utils/navigation';
33
+ import { CustomerPicker } from '../components/CustomerPicker';
34
+ import type { Customer } from '../types';
33
35
 
34
36
  /**
35
37
  * `<input type="datetime-local">` needs a `YYYY-MM-DDTHH:mm` string in LOCAL
@@ -49,15 +51,25 @@ function toDateTimeLocalValue(date: Date | undefined): string {
49
51
 
50
52
  export default function CreateOrderPage() {
51
53
  const { orderId } = useParams<{ orderId: string }>();
54
+ const [searchParams] = useSearchParams();
55
+ const prefilledCustomerId = searchParams.get('customerId')?.trim() || undefined;
52
56
  const { basePath, navigate } = useMoveTheWheels();
53
57
  const { order } = useOrder(orderId);
54
58
  const { createOrder, updateOrder } = useOrders();
55
- const { customers } = useCustomers({ limit: 100 });
59
+ const {
60
+ customers,
61
+ createCustomer,
62
+ isLoading: customersLoading,
63
+ error: customersError,
64
+ refetch: refetchCustomers,
65
+ } = useCustomers({ limit: 100 });
66
+ const { customer: linkedCustomer } = useCustomer(prefilledCustomerId ?? order?.customerId);
56
67
 
57
68
  const isEditing = !!orderId && !!order;
58
69
 
70
+ const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null);
59
71
  const [formData, setFormData] = useState({
60
- customerId: order?.customerId || '',
72
+ customerId: prefilledCustomerId || order?.customerId || '',
61
73
  priority: order?.priority || 'normal',
62
74
  pickupAddress: {
63
75
  street: order?.pickupAddress.street || '',
@@ -81,6 +93,18 @@ export default function CreateOrderPage() {
81
93
  const [isSubmitting, setIsSubmitting] = useState(false);
82
94
  const [formError, setFormError] = useState<string | null>(null);
83
95
 
96
+ useEffect(() => {
97
+ if (!linkedCustomer) {
98
+ return;
99
+ }
100
+ setSelectedCustomer((previous) =>
101
+ previous?.id === linkedCustomer.id ? previous : linkedCustomer
102
+ );
103
+ setFormData((previous) =>
104
+ previous.customerId ? previous : { ...previous, customerId: linkedCustomer.id }
105
+ );
106
+ }, [linkedCustomer]);
107
+
84
108
  // Prefill the form from the loaded order when editing. `order` arrives
85
109
  // asynchronously after the initial render, so the useState initializers alone
86
110
  // leave the fields blank until this syncs them.
@@ -112,15 +136,24 @@ export default function CreateOrderPage() {
112
136
  }, [order]);
113
137
 
114
138
  const goBack = () => {
115
- if (navigate) {
116
- navigate(joinPath(basePath, 'orders'));
139
+ if (!navigate) {
140
+ return;
141
+ }
142
+ if (!isEditing && prefilledCustomerId) {
143
+ navigate(joinPath(basePath, `customers/${prefilledCustomerId}`));
144
+ return;
117
145
  }
146
+ navigate(joinPath(basePath, 'orders'));
118
147
  };
119
148
 
149
+ const getSelectedCustomer = () =>
150
+ selectedCustomer ?? customers.find((customer) => customer.id === formData.customerId);
151
+
120
152
  const validate = (): string | null => {
121
- const selectedCustomer = customers.find((c) => c.id === formData.customerId);
122
- if (!formData.customerId || !selectedCustomer) {
123
- return 'Select a customer for this order.';
153
+ if (!formData.customerId || !getSelectedCustomer()) {
154
+ return customersError
155
+ ? 'Customers could not be loaded. Retry or create a customer before continuing.'
156
+ : 'Select a customer for this order.';
124
157
  }
125
158
  if (!formData.pickupAddress.street.trim()) {
126
159
  return 'Enter a pickup street address.';
@@ -147,9 +180,13 @@ export default function CreateOrderPage() {
147
180
  return;
148
181
  }
149
182
 
150
- const selectedCustomer = customers.find((c) => c.id === formData.customerId);
183
+ const selectedCustomer = getSelectedCustomer();
151
184
  if (!selectedCustomer) {
152
- setFormError('Select a customer for this order.');
185
+ setFormError(
186
+ customersError
187
+ ? 'Customers could not be loaded. Retry or create a customer before continuing.'
188
+ : 'Select a customer for this order.'
189
+ );
153
190
  return;
154
191
  }
155
192
 
@@ -242,6 +279,14 @@ export default function CreateOrderPage() {
242
279
  }));
243
280
  };
244
281
 
282
+ const goToCustomerCreate = () => {
283
+ if (!navigate) {
284
+ return;
285
+ }
286
+ const orderPath = isEditing && orderId ? `orders/${orderId}/edit` : 'orders/new';
287
+ navigate(joinPath(basePath, buildCustomerCreatePath(orderPath)));
288
+ };
289
+
245
290
  return (
246
291
  <PageLayout
247
292
  title={isEditing ? 'Edit Order' : 'Create Order'}
@@ -253,7 +298,7 @@ export default function CreateOrderPage() {
253
298
  <ArrowLeft size={16} />
254
299
  Cancel
255
300
  </Button>
256
- <Button onClick={handleSubmit} disabled={isSubmitting}>
301
+ <Button onClick={handleSubmit} disabled={isSubmitting || customersLoading}>
257
302
  <Save size={16} />
258
303
  {isSubmitting ? 'Saving...' : isEditing ? 'Save Changes' : 'Create Order'}
259
304
  </Button>
@@ -304,24 +349,32 @@ export default function CreateOrderPage() {
304
349
  <h2 className="text-base font-semibold text-text-primary mb-4">Order Details</h2>
305
350
  <div className="space-y-4">
306
351
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
307
- <div className="space-y-2">
308
- <Label htmlFor="customer">Customer</Label>
309
- <Select
310
- value={formData.customerId}
311
- onValueChange={(value) => updateField('customerId', value)}
312
- >
313
- <SelectTrigger id="customer">
314
- <SelectValue placeholder="Select customer" />
315
- </SelectTrigger>
316
- <SelectContent>
317
- {customers.map((customer) => (
318
- <SelectItem key={customer.id} value={customer.id}>
319
- {customer.name} ({customer.email})
320
- </SelectItem>
321
- ))}
322
- </SelectContent>
323
- </Select>
324
- </div>
352
+ <CustomerPicker
353
+ customers={customers}
354
+ value={formData.customerId}
355
+ onValueChange={(value) => {
356
+ if (!value) {
357
+ return;
358
+ }
359
+ updateField('customerId', value);
360
+ setSelectedCustomer(
361
+ customers.find((customer) => customer.id === value) ??
362
+ (selectedCustomer?.id === value ? selectedCustomer : null)
363
+ );
364
+ setFormError(null);
365
+ }}
366
+ onCreated={(customer) => {
367
+ setSelectedCustomer(customer);
368
+ updateField('customerId', customer.id);
369
+ setFormError(null);
370
+ }}
371
+ createCustomer={createCustomer}
372
+ isLoading={customersLoading}
373
+ error={customersError}
374
+ onRetry={refetchCustomers}
375
+ onOpenCustomers={goToCustomerCreate}
376
+ selectedCustomer={selectedCustomer}
377
+ />
325
378
  <div className="space-y-2">
326
379
  <Label htmlFor="priority">Priority</Label>
327
380
  <Select
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { useEffect, useState } from 'react';
6
- import { useParams } from 'react-router';
6
+ import { useParams, useSearchParams } from 'react-router';
7
7
  import {
8
8
  Users,
9
9
  ArrowLeft,
@@ -40,7 +40,7 @@ import { useCustomer, useCustomers } from '../hooks/useCustomers';
40
40
  import { useOrders } from '../hooks/useOrders';
41
41
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
42
42
  import { formatAddress, formatCurrency, formatDate } from '../utils/formatters';
43
- import { joinPath } from '../utils/navigation';
43
+ import { joinPath, buildOrderCustomerPath, getSafeCustomerReturnPath } from '../utils/navigation';
44
44
  import { FormError } from '../components/FormError';
45
45
  import { Notice, useNotice } from '../components/Notice';
46
46
 
@@ -62,6 +62,8 @@ function safeHttpUrl(raw: string | undefined): string | null {
62
62
 
63
63
  function NewCustomerForm() {
64
64
  const { basePath, navigate } = useMoveTheWheels();
65
+ const [searchParams] = useSearchParams();
66
+ const returnTo = getSafeCustomerReturnPath(searchParams.get('returnTo'));
65
67
  const { createCustomer } = useCustomers();
66
68
  const [form, setForm] = useState({
67
69
  name: '',
@@ -91,7 +93,10 @@ function NewCustomerForm() {
91
93
  phone: form.phone.trim() || undefined,
92
94
  company: form.company.trim() || undefined,
93
95
  });
94
- navigate?.(joinPath(basePath, `customers/${created.id}`));
96
+ const destination = returnTo
97
+ ? buildOrderCustomerPath(returnTo, created.id)
98
+ : `customers/${created.id}`;
99
+ navigate?.(joinPath(basePath, destination));
95
100
  } catch (err) {
96
101
  console.error('Failed to create customer:', err);
97
102
  setError(err instanceof Error ? err.message : 'Failed to create customer. Please try again.');
@@ -330,7 +335,7 @@ export default function CustomerDetailsPage() {
330
335
  label: 'Book a delivery for this customer',
331
336
  description: 'Start a new order pre-filled for this account.',
332
337
  icon: <FileText size={16} />,
333
- onClick: () => goTo('orders/new'),
338
+ onClick: () => goTo(buildOrderCustomerPath('orders/new', customer.id)),
334
339
  },
335
340
  {
336
341
  id: 'edit',
@@ -475,7 +480,11 @@ export default function CustomerDetailsPage() {
475
480
  <CardTitle className="flex items-center gap-2">
476
481
  <Package size={18} /> Orders
477
482
  </CardTitle>
478
- <Button variant="outline" size="sm" onClick={() => goTo('orders/new')}>
483
+ <Button
484
+ variant="outline"
485
+ size="sm"
486
+ onClick={() => goTo(buildOrderCustomerPath('orders/new', customer.id))}
487
+ >
479
488
  <FileText size={14} /> New order
480
489
  </Button>
481
490
  </CardHeader>
@@ -508,7 +517,7 @@ export default function CustomerDetailsPage() {
508
517
  <p className="text-text-secondary">No orders yet for this customer</p>
509
518
  <button
510
519
  type="button"
511
- onClick={() => goTo('orders/new')}
520
+ onClick={() => goTo(buildOrderCustomerPath('orders/new', customer.id))}
512
521
  className="mt-1 text-sm text-primary hover:underline"
513
522
  >
514
523
  Book their first delivery
@@ -68,3 +68,35 @@ export function parseQueryParams(search: string): Record<string, string> {
68
68
  });
69
69
  return params;
70
70
  }
71
+
72
+ const CUSTOMER_RETURN_PATHS = [/^orders\/new$/, /^orders\/[^/?#]+\/edit$/];
73
+
74
+ /**
75
+ * Return only an internal order destination for customer create-and-return flows.
76
+ * Query strings are intentionally discarded so callers cannot smuggle a second
77
+ * return target or an open redirect through the customer form.
78
+ */
79
+ export function getSafeCustomerReturnPath(value: string | null | undefined): string | null {
80
+ if (!value || value.length > 200 || value.includes('://') || value.startsWith('/')) {
81
+ return null;
82
+ }
83
+
84
+ const [pathname] = value.split(/[?#]/, 1);
85
+ return CUSTOMER_RETURN_PATHS.some((pattern) => pattern.test(pathname)) ? pathname : null;
86
+ }
87
+
88
+ /**
89
+ * Build an order route with a customer preselected after a customer is created.
90
+ */
91
+ export function buildOrderCustomerPath(orderPath: string, customerId: string): string {
92
+ const safeOrderPath = getSafeCustomerReturnPath(orderPath);
93
+ return safeOrderPath ? buildUrl(safeOrderPath, { customerId }) : orderPath;
94
+ }
95
+
96
+ /**
97
+ * Build the local customer creation route while preserving a safe order return path.
98
+ */
99
+ export function buildCustomerCreatePath(orderPath: string): string {
100
+ const safeOrderPath = getSafeCustomerReturnPath(orderPath);
101
+ return safeOrderPath ? buildUrl('customers/new', { returnTo: safeOrderPath }) : 'customers/new';
102
+ }