@burdenoff/microfe-movethewheels 2026.929.3 → 2026.930.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CustomerPicker.js +52 -48
- package/dist/components/EntityCombobox.js +25 -23
- package/dist/components/OrderItemRow.js +123 -78
- package/dist/generated/wspace-types.d.ts +1547 -64
- package/dist/hooks/useCustomers.js +55 -26
- package/dist/hooks/useFleet.js +236 -157
- package/dist/hooks/useInventory.js +214 -153
- package/dist/hooks/useMultiModalShipments.js +30 -26
- package/dist/hooks/useRoutes.js +64 -24
- package/dist/hooks/useTracking.js +14 -16
- package/dist/movethewheels/components/CustomerPicker.d.ts +5 -1
- package/dist/movethewheels/components/EntityCombobox.d.ts +6 -3
- package/dist/movethewheels/components/OrderItemRow.d.ts +7 -1
- package/dist/movethewheels/hooks/useCustomers.d.ts +3 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +15 -2
- package/dist/movethewheels/hooks/useInventory.d.ts +15 -12
- package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +1 -0
- package/dist/movethewheels/hooks/useRoutes.d.ts +7 -1
- package/dist/movethewheels/types/index.d.ts +33 -23
- package/dist/movethewheels/utils/formatters.d.ts +1 -1
- package/dist/movethewheels/utils/orderValidation.d.ts +11 -0
- package/dist/pages/CreateOrderPage.js +349 -297
- package/dist/pages/CustomerDetailsPage.js +6 -6
- package/dist/pages/CustomersPage.js +2 -2
- package/dist/pages/DashboardPage.js +1 -1
- package/dist/pages/DriverDetailsPage.js +340 -332
- package/dist/pages/DriversPage.js +7 -5
- package/dist/pages/FleetPage.js +1 -1
- package/dist/pages/InventoryPage.js +11 -12
- package/dist/pages/LiveTrackingPage.js +4 -4
- package/dist/pages/MultiModalShipmentPage.js +139 -125
- package/dist/pages/OrderDetailsPage.js +132 -140
- package/dist/pages/ProductDetailsPage.js +408 -249
- package/dist/pages/ProductsPage.js +5 -5
- package/dist/pages/RouteDetailsPage.js +208 -210
- package/dist/pages/RoutesPage.js +2 -2
- package/dist/pages/TrackingPage.js +16 -9
- package/dist/pages/WarehouseDetailsPage.js +166 -163
- package/dist/pages/WarehousesPage.js +29 -21
- package/dist/utils/orderValidation.js +18 -0
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +1762 -64
- package/src/movethewheels/components/CustomerPicker.tsx +12 -0
- package/src/movethewheels/components/EntityCombobox.tsx +30 -11
- package/src/movethewheels/components/OrderItemRow.tsx +44 -0
- package/src/movethewheels/hooks/useCustomers.ts +45 -9
- package/src/movethewheels/hooks/useFleet.ts +214 -36
- package/src/movethewheels/hooks/useInventory.ts +185 -49
- package/src/movethewheels/hooks/useMultiModalShipments.ts +9 -7
- package/src/movethewheels/hooks/useRoutes.ts +87 -16
- package/src/movethewheels/hooks/useTracking.ts +23 -15
- package/src/movethewheels/pages/CreateOrderPage.tsx +66 -2
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +14 -8
- package/src/movethewheels/pages/CustomersPage.tsx +5 -2
- package/src/movethewheels/pages/DashboardPage.tsx +3 -1
- package/src/movethewheels/pages/DriverDetailsPage.tsx +25 -4
- package/src/movethewheels/pages/DriversPage.tsx +8 -2
- package/src/movethewheels/pages/FleetPage.tsx +1 -1
- package/src/movethewheels/pages/InventoryPage.tsx +20 -16
- package/src/movethewheels/pages/LiveTrackingPage.tsx +6 -4
- package/src/movethewheels/pages/MultiModalShipmentPage.tsx +15 -1
- package/src/movethewheels/pages/OrderDetailsPage.tsx +25 -25
- package/src/movethewheels/pages/ProductDetailsPage.tsx +228 -49
- package/src/movethewheels/pages/ProductsPage.tsx +13 -5
- package/src/movethewheels/pages/RouteDetailsPage.tsx +24 -12
- package/src/movethewheels/pages/RoutesPage.tsx +4 -2
- package/src/movethewheels/pages/TrackingPage.tsx +32 -12
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +42 -29
- package/src/movethewheels/pages/WarehousesPage.tsx +23 -15
- package/src/movethewheels/types/index.ts +36 -24
- package/src/movethewheels/utils/formatters.ts +1 -1
- package/src/movethewheels/utils/orderValidation.ts +53 -0
|
@@ -27,8 +27,12 @@ interface CustomerPickerProps {
|
|
|
27
27
|
onCreated: (customer: Customer) => void;
|
|
28
28
|
createCustomer: (customer: Partial<Customer>) => Promise<Customer>;
|
|
29
29
|
isLoading: boolean;
|
|
30
|
+
isLoadingMore?: boolean;
|
|
30
31
|
error: Error | null;
|
|
31
32
|
onRetry: () => void;
|
|
33
|
+
onLoadMore?: () => void | Promise<void>;
|
|
34
|
+
hasMore?: boolean;
|
|
35
|
+
totalCount?: number;
|
|
32
36
|
onOpenCustomers?: () => void;
|
|
33
37
|
selectedCustomer?: Customer | null;
|
|
34
38
|
/** Called with the (already debounced) query text as the user types in the
|
|
@@ -60,8 +64,12 @@ export function CustomerPicker({
|
|
|
60
64
|
onCreated,
|
|
61
65
|
createCustomer,
|
|
62
66
|
isLoading,
|
|
67
|
+
isLoadingMore = false,
|
|
63
68
|
error,
|
|
64
69
|
onRetry,
|
|
70
|
+
onLoadMore,
|
|
71
|
+
hasMore = false,
|
|
72
|
+
totalCount,
|
|
65
73
|
onOpenCustomers,
|
|
66
74
|
selectedCustomer,
|
|
67
75
|
onSearchQueryChange,
|
|
@@ -153,6 +161,10 @@ export function CustomerPicker({
|
|
|
153
161
|
disabled={disabled || Boolean(error)}
|
|
154
162
|
error={error ? 'Customers could not be loaded' : undefined}
|
|
155
163
|
onSearchChange={onSearchQueryChange}
|
|
164
|
+
hasMore={hasMore}
|
|
165
|
+
totalCount={totalCount}
|
|
166
|
+
onLoadMore={onLoadMore}
|
|
167
|
+
loadingMore={isLoadingMore}
|
|
156
168
|
className="flex-1"
|
|
157
169
|
/>
|
|
158
170
|
{error && (
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* already proven by `microfe-product`'s `ProductCombobox`.
|
|
9
9
|
*
|
|
10
10
|
* Every list this feeds (`useRoutes`, `useCustomers`, `useVehicles`,
|
|
11
|
-
* `useDrivers
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* `useDrivers`, and product pickers) is capped to a small server page (`limit`,
|
|
12
|
+
* default 20). When the source exposes `onLoadMore`, the accessible Load more
|
|
13
|
+
* control requests the next server page and appends it without losing the
|
|
14
|
+
* current selection.
|
|
15
15
|
*
|
|
16
16
|
* - **Internal search** (pass `onSearchChange`): the combobox renders its own
|
|
17
17
|
* debounced `CommandInput` and calls back with the debounced query so the
|
|
@@ -76,10 +76,13 @@ interface EntityComboboxProps<T> {
|
|
|
76
76
|
/** Server-side search callback (debounced). Omit to hide the built-in search
|
|
77
77
|
* input when a caller already provides one elsewhere for the same list. */
|
|
78
78
|
onSearchChange?: (value: string) => void;
|
|
79
|
-
/** True when the source hook
|
|
80
|
-
* paired with `totalCount` to render a "keep typing to narrow" hint. */
|
|
79
|
+
/** True when the source hook has another server page available. */
|
|
81
80
|
hasMore?: boolean;
|
|
82
81
|
totalCount?: number;
|
|
82
|
+
/** Fetches and appends the next server page without replacing the current options. */
|
|
83
|
+
onLoadMore?: () => void | Promise<void>;
|
|
84
|
+
/** True while the next server page is being fetched. */
|
|
85
|
+
loadingMore?: boolean;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
export function EntityCombobox<T>({
|
|
@@ -104,6 +107,8 @@ export function EntityCombobox<T>({
|
|
|
104
107
|
onSearchChange,
|
|
105
108
|
hasMore = false,
|
|
106
109
|
totalCount,
|
|
110
|
+
onLoadMore,
|
|
111
|
+
loadingMore = false,
|
|
107
112
|
}: EntityComboboxProps<T>) {
|
|
108
113
|
const [open, setOpen] = useState(false);
|
|
109
114
|
const [search, setSearch] = useState('');
|
|
@@ -140,7 +145,7 @@ export function EntityCombobox<T>({
|
|
|
140
145
|
className
|
|
141
146
|
);
|
|
142
147
|
|
|
143
|
-
const
|
|
148
|
+
const canLoadMore = Boolean(onLoadMore && hasMore);
|
|
144
149
|
|
|
145
150
|
return (
|
|
146
151
|
<Popover
|
|
@@ -223,10 +228,24 @@ export function EntityCombobox<T>({
|
|
|
223
228
|
</CommandItem>
|
|
224
229
|
);
|
|
225
230
|
})}
|
|
226
|
-
{
|
|
227
|
-
<div className="
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
{canLoadMore && (
|
|
232
|
+
<div className="border-t border-border-seam p-2">
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
className="w-full rounded-md px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/10 focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)] disabled:cursor-not-allowed disabled:opacity-60"
|
|
236
|
+
onClick={() => void onLoadMore?.()}
|
|
237
|
+
disabled={loading || loadingMore}
|
|
238
|
+
aria-busy={loadingMore}
|
|
239
|
+
aria-label={
|
|
240
|
+
loadingMore
|
|
241
|
+
? 'Loading more options'
|
|
242
|
+
: `Load more options${typeof totalCount === 'number' ? ` (${options.length} of ${totalCount} shown)` : ''}`
|
|
243
|
+
}
|
|
244
|
+
>
|
|
245
|
+
{loadingMore
|
|
246
|
+
? 'Loading more…'
|
|
247
|
+
: `Load more${typeof totalCount === 'number' ? ` (${options.length} of ${totalCount})` : ''}`}
|
|
248
|
+
</button>
|
|
230
249
|
</div>
|
|
231
250
|
)}
|
|
232
251
|
</CommandList>
|
|
@@ -12,12 +12,18 @@ import { Button, Input, Label } from './ui';
|
|
|
12
12
|
import { EntityCombobox } from './EntityCombobox';
|
|
13
13
|
import { useProduct, useProductStockLevels } from '../hooks/useInventory';
|
|
14
14
|
import type { Dimensions, OrderItem, Product } from '../types';
|
|
15
|
+
import type { OrderItemFieldErrors } from '../utils/orderValidation';
|
|
15
16
|
|
|
16
17
|
interface OrderItemRowProps {
|
|
17
18
|
item: OrderItem;
|
|
18
19
|
index: number;
|
|
19
20
|
products: Product[];
|
|
20
21
|
productsLoading: boolean;
|
|
22
|
+
productsLoadingMore?: boolean;
|
|
23
|
+
productsHasMore?: boolean;
|
|
24
|
+
productsTotalCount?: number;
|
|
25
|
+
onLoadMoreProducts?: () => void | Promise<void>;
|
|
26
|
+
errors?: OrderItemFieldErrors;
|
|
21
27
|
onUpdate: (index: number, field: string, value: unknown) => void;
|
|
22
28
|
onRemove: (index: number) => void;
|
|
23
29
|
}
|
|
@@ -37,6 +43,11 @@ export function OrderItemRow({
|
|
|
37
43
|
index,
|
|
38
44
|
products,
|
|
39
45
|
productsLoading,
|
|
46
|
+
productsLoadingMore = false,
|
|
47
|
+
productsHasMore = false,
|
|
48
|
+
productsTotalCount,
|
|
49
|
+
onLoadMoreProducts,
|
|
50
|
+
errors = {},
|
|
40
51
|
onUpdate,
|
|
41
52
|
onRemove,
|
|
42
53
|
}: OrderItemRowProps) {
|
|
@@ -80,6 +91,17 @@ export function OrderItemRow({
|
|
|
80
91
|
});
|
|
81
92
|
};
|
|
82
93
|
|
|
94
|
+
const fieldError = (field: keyof OrderItemFieldErrors) =>
|
|
95
|
+
errors[field] ? (
|
|
96
|
+
<p
|
|
97
|
+
id={`item-${field}-${item.id}-error`}
|
|
98
|
+
className="text-xs text-status-error-text"
|
|
99
|
+
role="alert"
|
|
100
|
+
>
|
|
101
|
+
{errors[field]}
|
|
102
|
+
</p>
|
|
103
|
+
) : null;
|
|
104
|
+
|
|
83
105
|
return (
|
|
84
106
|
<div
|
|
85
107
|
className="rounded-card border border-border-subtle bg-bg-surface p-4 shadow-elevation-1 sm:p-5"
|
|
@@ -131,6 +153,10 @@ export function OrderItemRow({
|
|
|
131
153
|
placeholder="Pick from your product catalog"
|
|
132
154
|
emptyText="No products found."
|
|
133
155
|
noneLabel="Enter details manually"
|
|
156
|
+
hasMore={productsHasMore}
|
|
157
|
+
totalCount={productsTotalCount}
|
|
158
|
+
onLoadMore={onLoadMoreProducts}
|
|
159
|
+
loadingMore={productsLoadingMore}
|
|
134
160
|
/>
|
|
135
161
|
{item.productId && !stockLoading && (
|
|
136
162
|
<p className="mt-2 text-xs text-text-secondary" role="status">
|
|
@@ -149,7 +175,10 @@ export function OrderItemRow({
|
|
|
149
175
|
onUpdate(index, 'name', e.target.value)
|
|
150
176
|
}
|
|
151
177
|
placeholder="Product name"
|
|
178
|
+
aria-invalid={Boolean(errors.name)}
|
|
179
|
+
aria-describedby={errors.name ? `item-name-${item.id}-error` : undefined}
|
|
152
180
|
/>
|
|
181
|
+
{fieldError('name')}
|
|
153
182
|
</div>
|
|
154
183
|
<div className="space-y-2 lg:col-span-3">
|
|
155
184
|
<Label htmlFor={`item-sku-${item.id}`}>SKU</Label>
|
|
@@ -160,7 +189,10 @@ export function OrderItemRow({
|
|
|
160
189
|
onUpdate(index, 'sku', e.target.value)
|
|
161
190
|
}
|
|
162
191
|
placeholder="SKU-001"
|
|
192
|
+
aria-invalid={Boolean(errors.sku)}
|
|
193
|
+
aria-describedby={errors.sku ? `item-sku-${item.id}-error` : undefined}
|
|
163
194
|
/>
|
|
195
|
+
{fieldError('sku')}
|
|
164
196
|
</div>
|
|
165
197
|
<div className="space-y-2 lg:col-span-2">
|
|
166
198
|
<Label htmlFor={`item-quantity-${item.id}`}>Quantity</Label>
|
|
@@ -172,7 +204,10 @@ export function OrderItemRow({
|
|
|
172
204
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
173
205
|
onUpdate(index, 'quantity', parseInt(e.target.value, 10) || 1)
|
|
174
206
|
}
|
|
207
|
+
aria-invalid={Boolean(errors.quantity)}
|
|
208
|
+
aria-describedby={errors.quantity ? `item-quantity-${item.id}-error` : undefined}
|
|
175
209
|
/>
|
|
210
|
+
{fieldError('quantity')}
|
|
176
211
|
</div>
|
|
177
212
|
<div className="space-y-2 lg:col-span-2">
|
|
178
213
|
<Label htmlFor={`item-weight-${item.id}`}>Weight (kg)</Label>
|
|
@@ -185,7 +220,10 @@ export function OrderItemRow({
|
|
|
185
220
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
186
221
|
onUpdate(index, 'weight', parseFloat(e.target.value) || 0)
|
|
187
222
|
}
|
|
223
|
+
aria-invalid={Boolean(errors.weight)}
|
|
224
|
+
aria-describedby={errors.weight ? `item-weight-${item.id}-error` : undefined}
|
|
188
225
|
/>
|
|
226
|
+
{fieldError('weight')}
|
|
189
227
|
</div>
|
|
190
228
|
<div className="space-y-2 lg:col-span-1">
|
|
191
229
|
<Label htmlFor={`item-value-${item.id}`}>Value ($)</Label>
|
|
@@ -198,7 +236,10 @@ export function OrderItemRow({
|
|
|
198
236
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
199
237
|
onUpdate(index, 'value', parseFloat(e.target.value) || 0)
|
|
200
238
|
}
|
|
239
|
+
aria-invalid={Boolean(errors.value)}
|
|
240
|
+
aria-describedby={errors.value ? `item-value-${item.id}-error` : undefined}
|
|
201
241
|
/>
|
|
242
|
+
{fieldError('value')}
|
|
202
243
|
</div>
|
|
203
244
|
</div>
|
|
204
245
|
|
|
@@ -221,7 +262,10 @@ export function OrderItemRow({
|
|
|
221
262
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
|
222
263
|
updateDimension(field, e.target.value)
|
|
223
264
|
}
|
|
265
|
+
aria-invalid={Boolean(errors[field])}
|
|
266
|
+
aria-describedby={errors[field] ? `item-${field}-${item.id}-error` : undefined}
|
|
224
267
|
/>
|
|
268
|
+
{fieldError(field)}
|
|
225
269
|
</div>
|
|
226
270
|
))}
|
|
227
271
|
</div>
|
|
@@ -14,8 +14,11 @@ interface UseCustomersResult {
|
|
|
14
14
|
totalCustomers: number;
|
|
15
15
|
hasMore: boolean;
|
|
16
16
|
isLoading: boolean;
|
|
17
|
+
/** True while an additional server page is appended. */
|
|
18
|
+
isLoadingMore: boolean;
|
|
17
19
|
error: Error | null;
|
|
18
20
|
refetch: () => void;
|
|
21
|
+
loadMore: () => Promise<void>;
|
|
19
22
|
createCustomer: (customer: Partial<Customer>) => Promise<Customer>;
|
|
20
23
|
updateCustomer: (id: string, updates: Partial<Customer>) => Promise<Customer>;
|
|
21
24
|
deleteCustomer: (id: string) => Promise<void>;
|
|
@@ -125,6 +128,7 @@ const DELETE_CUSTOMER_MUTATION = `
|
|
|
125
128
|
`;
|
|
126
129
|
|
|
127
130
|
function mapCustomer(customer: ApiCustomer): Customer {
|
|
131
|
+
const address = customer.address ?? {};
|
|
128
132
|
return {
|
|
129
133
|
id: customer.id,
|
|
130
134
|
name: customer.name,
|
|
@@ -132,17 +136,14 @@ function mapCustomer(customer: ApiCustomer): Customer {
|
|
|
132
136
|
phone: customer.phone ?? '',
|
|
133
137
|
type: customer.type.toLowerCase() === 'b2b' ? 'b2b' : 'b2c',
|
|
134
138
|
company: customer.company ?? undefined,
|
|
135
|
-
address:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
country: '',
|
|
141
|
-
...(customer.address ?? {}),
|
|
142
|
-
},
|
|
139
|
+
address: Object.fromEntries(
|
|
140
|
+
Object.entries(address).filter(
|
|
141
|
+
([, value]) => typeof value === 'string' && value.trim().length > 0
|
|
142
|
+
)
|
|
143
|
+
),
|
|
143
144
|
businessLicense: customer.businessLicense ?? undefined,
|
|
144
145
|
taxId: customer.taxId ?? undefined,
|
|
145
|
-
rating: customer.rating ??
|
|
146
|
+
rating: customer.rating ?? undefined,
|
|
146
147
|
createdAt: new Date(customer.createdAt),
|
|
147
148
|
updatedAt: new Date(customer.updatedAt),
|
|
148
149
|
};
|
|
@@ -165,6 +166,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
165
166
|
const [totalCustomers, setTotalCustomers] = useState(0);
|
|
166
167
|
const [hasMore, setHasMore] = useState(false);
|
|
167
168
|
const [isLoading, setIsLoading] = useState(false);
|
|
169
|
+
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
168
170
|
const [error, setError] = useState<Error | null>(null);
|
|
169
171
|
|
|
170
172
|
const graphQLOptions = useMemo(
|
|
@@ -206,6 +208,38 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
206
208
|
}
|
|
207
209
|
}, [filters, graphQLOptions, limit, offset]);
|
|
208
210
|
|
|
211
|
+
const loadMore = useCallback(async () => {
|
|
212
|
+
if (!hasMore || isLoading || isLoadingMore) return;
|
|
213
|
+
const nextOffset = offset + customers.length;
|
|
214
|
+
setIsLoadingMore(true);
|
|
215
|
+
setError(null);
|
|
216
|
+
try {
|
|
217
|
+
const data = await executeGraphQL<{
|
|
218
|
+
customers: ApiCustomer[];
|
|
219
|
+
customersCount: number;
|
|
220
|
+
}>(graphQLOptions, LIST_CUSTOMERS_QUERY, {
|
|
221
|
+
filter: {
|
|
222
|
+
type: filters?.type,
|
|
223
|
+
search: filters?.search,
|
|
224
|
+
},
|
|
225
|
+
pagination: { limit, offset: nextOffset },
|
|
226
|
+
});
|
|
227
|
+
const mapped = data.customers.map(mapCustomer);
|
|
228
|
+
setTotalCustomers(data.customersCount);
|
|
229
|
+
setHasMore(nextOffset + mapped.length < data.customersCount);
|
|
230
|
+
setCustomers((previous) => {
|
|
231
|
+
const existingIds = new Set(previous.map((customer) => customer.id));
|
|
232
|
+
return [...previous, ...mapped.filter((customer) => !existingIds.has(customer.id))];
|
|
233
|
+
});
|
|
234
|
+
} catch (fetchError) {
|
|
235
|
+
setError(
|
|
236
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load more customers')
|
|
237
|
+
);
|
|
238
|
+
} finally {
|
|
239
|
+
setIsLoadingMore(false);
|
|
240
|
+
}
|
|
241
|
+
}, [customers.length, filters, graphQLOptions, hasMore, isLoading, isLoadingMore, limit, offset]);
|
|
242
|
+
|
|
209
243
|
useEffect(() => {
|
|
210
244
|
void loadCustomers();
|
|
211
245
|
}, [loadCustomers]);
|
|
@@ -280,8 +314,10 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
280
314
|
totalCustomers,
|
|
281
315
|
hasMore,
|
|
282
316
|
isLoading,
|
|
317
|
+
isLoadingMore,
|
|
283
318
|
error,
|
|
284
319
|
refetch,
|
|
320
|
+
loadMore,
|
|
285
321
|
createCustomer,
|
|
286
322
|
updateCustomer,
|
|
287
323
|
deleteCustomer,
|