Browse Source

fix(dashboard): Fix setting quantities during order modification

Michael Bromley 3 months ago
parent
commit
a92e079235

+ 17 - 8
packages/dashboard/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx

@@ -5,7 +5,7 @@ import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/vdb/components/ui/table.js';
 import { ResultOf } from '@/vdb/graphql/graphql.js';
-import { Trans } from '@lingui/react/macro';
+import { Trans, useLingui } from '@lingui/react/macro';
 import {
     ColumnDef,
     flexRender,
@@ -68,26 +68,33 @@ export function EditOrderTable({
     displayTotals = true,
 }: Readonly<OrderTableProps>) {
     const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
+    const { t } = useLingui();
     const currencyCode = order.currencyCode;
     const columns: ColumnDef<OrderLineFragment & { customFields?: Record<string, any> }>[] = [
         {
-            header: 'Image',
+            header: '',
             accessorKey: 'featuredAsset',
             cell: ({ row }) => {
                 const asset = row.original.featuredAsset;
-                return <VendureImage asset={asset} preset="tiny" />;
+                const removing = row.original.quantity === 0;
+                return <VendureImage className={removing ? 'opacity-50' : ''} asset={asset} preset="tiny" />;
             },
         },
         {
-            header: 'Product',
+            header: t`Product`,
             accessorKey: 'productVariant.name',
+            cell: ({ row }) => {
+                const value = row.original.productVariant.name;
+                const removing = row.original.quantity === 0;
+                return <div className={removing ? 'text-muted-foreground' : ''}>{value}</div>;
+            },
         },
         {
-            header: 'SKU',
+            header: t`SKU`,
             accessorKey: 'productVariant.sku',
         },
         {
-            header: 'Unit price',
+            header: t`Unit price`,
             accessorKey: 'unitPriceWithTax',
             cell: ({ row }) => {
                 const value = row.original.unitPriceWithTax;
@@ -96,7 +103,7 @@ export function EditOrderTable({
             },
         },
         {
-            header: 'Quantity',
+            header: t`Quantity`,
             accessorKey: 'quantity',
             cell: ({ row }) => {
                 return (
@@ -104,6 +111,7 @@ export function EditOrderTable({
                         <Input
                             type="number"
                             value={row.original.quantity}
+                            min={0}
                             onChange={e =>
                                 onAdjustLine({
                                     lineId: row.original.id,
@@ -116,6 +124,7 @@ export function EditOrderTable({
                             variant="outline"
                             type="button"
                             size="icon"
+                            disabled={row.original.quantity === 0}
                             onClick={() => onRemoveLine({ lineId: row.original.id })}
                         >
                             <Trash2 />
@@ -137,7 +146,7 @@ export function EditOrderTable({
             },
         },
         {
-            header: 'Total',
+            header: t`Total`,
             accessorKey: 'linePriceWithTax',
             cell: ({ row }) => {
                 const value = row.original.linePriceWithTax;

+ 2 - 1
packages/dashboard/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx

@@ -42,7 +42,8 @@ export function OrderModificationSummary({
             if (
                 orig &&
                 (adj.quantity !== orig.quantity ||
-                    JSON.stringify(adj.customFields) !== JSON.stringify((orig as any).customFields))
+                    JSON.stringify(adj.customFields) !== JSON.stringify((orig as any).customFields)) &&
+                adj.quantity > 0
             ) {
                 return {
                     orderLineId: adj.orderLineId,

+ 34 - 272
packages/dashboard/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx

@@ -13,9 +13,8 @@ import { api } from '@/vdb/graphql/api.js';
 import { Trans, useLingui } from '@lingui/react/macro';
 import { useQuery, useQueryClient } from '@tanstack/react-query';
 import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
-import { VariablesOf } from 'gql.tada';
 import { User } from 'lucide-react';
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
 import { toast } from 'sonner';
 import { CustomerAddressSelector } from './components/customer-address-selector.js';
 import { EditOrderTable } from './components/edit-order-table.js';
@@ -23,16 +22,13 @@ import { OrderAddress } from './components/order-address.js';
 import { OrderModificationPreviewDialog } from './components/order-modification-preview-dialog.js';
 import { OrderModificationSummary } from './components/order-modification-summary.js';
 import { useTransitionOrderToState } from './components/use-transition-order-to-state.js';
-import {
-    draftOrderEligibleShippingMethodsDocument,
-    modifyOrderDocument,
-    orderDetailDocument,
-} from './orders.graphql.js';
+import { draftOrderEligibleShippingMethodsDocument, orderDetailDocument } from './orders.graphql.js';
 import { loadModifyingOrder } from './utils/order-detail-loaders.js';
-import { AddressFragment, Order } from './utils/order-types.js';
+import { AddressFragment } from './utils/order-types.js';
+import { computePendingOrder } from './utils/order-utils.js';
+import { useModifyOrder } from './utils/use-modify-order.js';
 
 const pageId = 'order-modify';
-type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
 
 export const Route = createFileRoute('/_authenticated/_orders/orders_/$id_/modify')({
     component: ModifyOrderPage,
@@ -40,34 +36,6 @@ export const Route = createFileRoute('/_authenticated/_orders/orders_/$id_/modif
     errorComponent: ({ error }) => <ErrorPage message={error.message} />,
 });
 
-// --- AddedLine type for added items ---
-interface AddedLine {
-    id: string;
-    featuredAsset?: any;
-    productVariant: {
-        id: string;
-        name: string;
-        sku: string;
-    };
-    unitPrice: number;
-    unitPriceWithTax: number;
-    quantity: number;
-    linePrice: number;
-    linePriceWithTax: number;
-}
-
-// --- ProductVariantInfo type ---
-type ProductVariantInfo = {
-    productVariantId: string;
-    productVariantName: string;
-    sku: string;
-    productAsset: {
-        preview: string;
-    };
-    price?: number;
-    priceWithTax?: number;
-};
-
 function ModifyOrderPage() {
     const params = Route.useParams();
     const navigate = useNavigate({ from: '/orders/$id/modify' });
@@ -103,254 +71,48 @@ function ModifyOrderPage() {
     const { transitionToPreModifyingState, ManuallySelectNextState, selectNextState, transitionToState } =
         useTransitionOrderToState(entity?.id ?? '');
 
-    // --- Modification intent state ---
-
-    const [modifyOrderInput, setModifyOrderInput] = useState<ModifyOrderInput>({
-        orderId: '',
-        addItems: [],
-        adjustOrderLines: [],
-        surcharges: [],
-        note: '',
-        couponCodes: [],
-        options: {
-            recalculateShipping: true,
-        },
-        dryRun: true,
-    } satisfies ModifyOrderInput);
-
-    useEffect(() => {
-        setModifyOrderInput(prev => ({
-            ...prev,
-            orderId: entity?.id ?? '',
-            couponCodes: entity?.couponCodes ?? [],
-        }));
-    }, [entity?.id]);
-
-    // --- Added variants info state ---
-    const [addedVariants, setAddedVariants] = useState<Map<string, ProductVariantInfo>>(new Map());
-
-    // --- Handlers update modifyOrderInput ---
-    function handleAddItem(variant: ProductVariantInfo) {
-        setModifyOrderInput(prev => ({
-            ...prev,
-            addItems: [...(prev.addItems ?? []), { productVariantId: variant.productVariantId, quantity: 1 }],
-        }));
-        setAddedVariants(prev => {
-            const newMap = new Map(prev);
-            newMap.set(variant.productVariantId, variant);
-            return newMap;
-        });
-    }
-
-    function handleAdjustLine({
-        lineId,
-        quantity,
-        customFields,
-    }: {
-        lineId: string;
-        quantity: number;
-        customFields: Record<string, any>;
-    }) {
-        // Check if this is an added line
-        if (lineId.startsWith('added-')) {
-            const productVariantId = lineId.replace('added-', '');
-            setModifyOrderInput(prev => ({
-                ...prev,
-                addItems: (prev.addItems ?? []).map(item =>
-                    item.productVariantId === productVariantId ? { ...item, quantity } : item,
-                ),
-            }));
-        } else {
-            let normalizedCustomFields: any = customFields;
-            delete normalizedCustomFields.__entityId__;
-            if (Object.keys(normalizedCustomFields).length === 0) {
-                normalizedCustomFields = undefined;
-            }
-            setModifyOrderInput(prev => {
-                const existing = (prev.adjustOrderLines ?? []).find(l => l.orderLineId === lineId);
-                const adjustOrderLines = existing
-                    ? (prev.adjustOrderLines ?? []).map(l =>
-                          l.orderLineId === lineId
-                              ? { ...l, quantity, customFields: normalizedCustomFields }
-                              : l,
-                      )
-                    : [
-                          ...(prev.adjustOrderLines ?? []),
-                          { orderLineId: lineId, quantity, customFields: normalizedCustomFields },
-                      ];
-                return { ...prev, adjustOrderLines };
-            });
-        }
-    }
-
-    function handleRemoveLine({ lineId }: { lineId: string }) {
-        if (lineId.startsWith('added-')) {
-            const productVariantId = lineId.replace('added-', '');
-            setModifyOrderInput(prev => ({
-                ...prev,
-                addItems: (prev.addItems ?? []).filter(item => item.productVariantId !== productVariantId),
-            }));
-            setAddedVariants(prev => {
-                const newMap = new Map(prev);
-                newMap.delete(productVariantId);
-                return newMap;
-            });
-        } else {
-            setModifyOrderInput(prev => {
-                const existingAdjustment = (prev.adjustOrderLines ?? []).find(l => l.orderLineId === lineId);
-                const adjustOrderLines = existingAdjustment
-                    ? (prev.adjustOrderLines ?? []).map(l =>
-                          l.orderLineId === lineId ? { ...l, quantity: 0 } : l,
-                      )
-                    : [...(prev.adjustOrderLines ?? []), { orderLineId: lineId, quantity: 0 }];
-                return {
-                    ...prev,
-                    adjustOrderLines,
-                };
-            });
-        }
-    }
-
-    function handleSetShippingMethod({ shippingMethodId }: { shippingMethodId: string }) {
-        setModifyOrderInput(prev => ({
-            ...prev,
-            shippingMethodIds: [shippingMethodId],
-        }));
-    }
-
-    function handleApplyCouponCode({ couponCode }: { couponCode: string }) {
-        setModifyOrderInput(prev => ({
-            ...prev,
-            couponCodes: prev.couponCodes?.includes(couponCode)
-                ? prev.couponCodes
-                : [...(prev.couponCodes ?? []), couponCode],
-        }));
-    }
-
-    function handleRemoveCouponCode({ couponCode }: { couponCode: string }) {
-        setModifyOrderInput(prev => ({
-            ...prev,
-            couponCodes: (prev.couponCodes ?? []).filter(code => code !== couponCode),
-        }));
-    }
+    // Use the custom hook for order modification logic
+    const {
+        modifyOrderInput,
+        addedVariants,
+        addItem,
+        adjustLine,
+        removeLine,
+        setShippingMethod,
+        applyCouponCode,
+        removeCouponCode,
+        updateShippingAddress: updateShippingAddressInInput,
+        updateBillingAddress: updateBillingAddressInInput,
+        hasModifications,
+    } = useModifyOrder(entity);
 
     // --- Address editing state ---
     const [editingShippingAddress, setEditingShippingAddress] = useState(false);
     const [editingBillingAddress, setEditingBillingAddress] = useState(false);
 
-    function orderAddressToModifyOrderInput(
-        address: AddressFragment,
-    ): ModifyOrderInput['updateShippingAddress'] {
-        return {
-            streetLine1: address.streetLine1,
-            streetLine2: address.streetLine2,
-            city: address.city,
-            countryCode: address.country.code,
-            fullName: address.fullName,
-            postalCode: address.postalCode,
-            province: address.province,
-            company: address.company,
-            phoneNumber: address.phoneNumber,
-        };
-    }
-
     // --- Address selection handlers ---
     function handleSelectShippingAddress(address: AddressFragment) {
-        setModifyOrderInput(prev => ({
-            ...prev,
-            updateShippingAddress: orderAddressToModifyOrderInput(address),
-        }));
+        updateShippingAddressInInput(address);
         setEditingShippingAddress(false);
     }
 
     function handleSelectBillingAddress(address: AddressFragment) {
-        setModifyOrderInput(prev => ({
-            ...prev,
-            updateBillingAddress: orderAddressToModifyOrderInput(address),
-        }));
+        updateBillingAddressInInput(address);
         setEditingBillingAddress(false);
     }
 
-    // --- Utility: compute pending order for display ---
-    function computePendingOrder(input: ModifyOrderInput): Order | null {
-        if (!entity) {
-            return null;
-        }
-        // Adjust lines
-        const lines = entity.lines.map(line => {
-            const adjust = input.adjustOrderLines?.find(l => l.orderLineId === line.id);
-            return adjust
-                ? { ...line, quantity: adjust.quantity, customFields: (adjust as any).customFields }
-                : line;
-        });
-        // Add new items (as AddedLine)
-        const addedLines = input.addItems
-            ?.map(item => {
-                const variantInfo = addedVariants.get(item.productVariantId);
-                return variantInfo
-                    ? ({
-                          id: `added-${item.productVariantId}`,
-                          featuredAsset: variantInfo.productAsset ?? null,
-                          productVariant: {
-                              id: variantInfo.productVariantId,
-                              name: variantInfo.productVariantName,
-                              sku: variantInfo.sku,
-                          },
-                          unitPrice: variantInfo.price ?? 0,
-                          unitPriceWithTax: variantInfo.priceWithTax ?? 0,
-                          quantity: item.quantity,
-                          linePrice: (variantInfo.price ?? 0) * item.quantity,
-                          linePriceWithTax: (variantInfo.priceWithTax ?? 0) * item.quantity,
-                      } as unknown as Order['lines'][number])
-                    : null;
-            })
-            .filter(x => x != null);
-        return {
-            ...entity,
-            lines: [...lines, ...(addedLines ?? [])],
-            couponCodes: input.couponCodes ?? [],
-            shippingLines: input.shippingMethodIds
-                ? input.shippingMethodIds
-                      .map(shippingMethodId => {
-                          const shippingMethod =
-                              eligibleShippingMethods?.eligibleShippingMethodsForDraftOrder.find(
-                                  method => method.id === shippingMethodId,
-                              );
-                          if (!shippingMethod) {
-                              return;
-                          }
-                          return {
-                              shippingMethod: {
-                                  ...shippingMethod,
-                                  fulfillmentHandlerCode: 'manual',
-                              },
-                              discountedPriceWithTax: shippingMethod?.priceWithTax ?? 0,
-                              id: shippingMethodId,
-                          };
-                      })
-                      .filter(x => x !== undefined)
-                : entity.shippingLines,
-        };
-    }
-
     const [previewOpen, setPreviewOpen] = useState(false);
 
     if (!entity) {
         return null;
     }
 
-    const pendingOrder = computePendingOrder(modifyOrderInput);
-    const hasModifications =
-        (modifyOrderInput.addItems?.length ?? 0) > 0 ||
-        (modifyOrderInput.adjustOrderLines?.length ?? 0) > 0 ||
-        (modifyOrderInput.couponCodes?.length ?? 0) > 0 ||
-        (modifyOrderInput.shippingMethodIds?.length ?? 0) > 0 ||
-        modifyOrderInput.updateShippingAddress ||
-        modifyOrderInput.updateBillingAddress;
-
-    if (!pendingOrder) {
-        return null;
-    }
+    const pendingOrder = computePendingOrder(
+        entity,
+        modifyOrderInput,
+        addedVariants,
+        eligibleShippingMethods?.eligibleShippingMethodsForDraftOrder,
+    );
 
     // On successful state transition, invalidate the order detail query and navigate to the order detail page
     const onSuccess = async () => {
@@ -405,12 +167,12 @@ function ModifyOrderPage() {
                         eligibleShippingMethods={
                             eligibleShippingMethods?.eligibleShippingMethodsForDraftOrder ?? []
                         }
-                        onAddItem={handleAddItem}
-                        onAdjustLine={handleAdjustLine}
-                        onRemoveLine={handleRemoveLine}
-                        onSetShippingMethod={handleSetShippingMethod}
-                        onApplyCouponCode={handleApplyCouponCode}
-                        onRemoveCouponCode={handleRemoveCouponCode}
+                        onAddItem={addItem}
+                        onAdjustLine={adjustLine}
+                        onRemoveLine={removeLine}
+                        onSetShippingMethod={setShippingMethod}
+                        onApplyCouponCode={applyCouponCode}
+                        onRemoveCouponCode={removeCouponCode}
                         displayTotals={false}
                     />
                 </PageBlock>
@@ -449,7 +211,7 @@ function ModifyOrderPage() {
                 </PageBlock>
                 <PageBlock column="side" blockId="customer" title={<Trans>Customer</Trans>}>
                     {entity.customer ? (
-                        <Button variant="ghost" asChild>
+                        <Button variant="outline" asChild>
                             <Link to={`/customers/${entity?.customer?.id}`}>
                                 <User className="w-4 h-4" />
                                 {entity?.customer?.firstName} {entity?.customer?.lastName}

+ 73 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-utils.ts

@@ -1,6 +1,12 @@
 import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
+import { VariablesOf } from 'gql.tada';
+
+import { modifyOrderDocument } from '../orders.graphql.js';
 
 import { Fulfillment, Order, Payment } from './order-types.js';
+import { ProductVariantInfo } from './use-modify-order.js';
+
+type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
 
 /**
  * Calculates the outstanding payment amount for an order
@@ -84,3 +90,70 @@ export function getSeller<T>(order: { channels: Array<{ code: string; seller: T
     const sellerChannel = order.channels.find(channel => channel.code !== DEFAULT_CHANNEL_CODE);
     return sellerChannel?.seller;
 }
+
+/**
+ * Computes a pending order based on the current order and modification input
+ */
+export function computePendingOrder(
+    order: Order,
+    input: ModifyOrderInput,
+    addedVariants: Map<string, ProductVariantInfo>,
+    eligibleShippingMethods?: Array<{ id: string; name: string; priceWithTax: number }>,
+): Order {
+    // Adjust lines
+    const lines = order.lines.map(line => {
+        const adjust = input.adjustOrderLines?.find(l => l.orderLineId === line.id);
+        return adjust
+            ? { ...line, quantity: adjust.quantity, customFields: (adjust as any).customFields }
+            : line;
+    });
+
+    // Add new items (as AddedLine)
+    const addedLines = input.addItems
+        ?.map(item => {
+            const variantInfo = addedVariants.get(item.productVariantId);
+            return variantInfo
+                ? ({
+                      id: `added-${item.productVariantId}`,
+                      featuredAsset: variantInfo.productAsset ?? null,
+                      productVariant: {
+                          id: variantInfo.productVariantId,
+                          name: variantInfo.productVariantName,
+                          sku: variantInfo.sku,
+                      },
+                      unitPrice: variantInfo.price ?? 0,
+                      unitPriceWithTax: variantInfo.priceWithTax ?? 0,
+                      quantity: item.quantity,
+                      linePrice: (variantInfo.price ?? 0) * item.quantity,
+                      linePriceWithTax: (variantInfo.priceWithTax ?? 0) * item.quantity,
+                  } as unknown as Order['lines'][number])
+                : null;
+        })
+        .filter(x => x != null);
+
+    return {
+        ...order,
+        lines: [...lines, ...(addedLines ?? [])],
+        couponCodes: input.couponCodes ?? [],
+        shippingLines: input.shippingMethodIds
+            ? input.shippingMethodIds
+                  .map(shippingMethodId => {
+                      const shippingMethod = eligibleShippingMethods?.find(
+                          method => method.id === shippingMethodId,
+                      );
+                      if (!shippingMethod) {
+                          return;
+                      }
+                      return {
+                          shippingMethod: {
+                              ...shippingMethod,
+                              fulfillmentHandlerCode: 'manual',
+                          },
+                          discountedPriceWithTax: shippingMethod?.priceWithTax ?? 0,
+                          id: shippingMethodId,
+                      } as any;
+                  })
+                  .filter(x => x !== undefined)
+            : order.shippingLines,
+    };
+}

+ 312 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts

@@ -0,0 +1,312 @@
+import { VariablesOf } from 'gql.tada';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+import { modifyOrderDocument } from '../orders.graphql.js';
+
+import { AddressFragment, Order } from './order-types.js';
+
+type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
+
+export type ProductVariantInfo = {
+    productVariantId: string;
+    productVariantName: string;
+    sku: string;
+    productAsset: {
+        preview: string;
+    };
+    price?: number;
+    priceWithTax?: number;
+};
+
+export interface UseModifyOrderReturn {
+    modifyOrderInput: ModifyOrderInput;
+    addedVariants: Map<string, ProductVariantInfo>;
+    addItem: (variant: ProductVariantInfo) => void;
+    adjustLine: (params: {
+        lineId: string;
+        quantity: number;
+        customFields: Record<string, any> | undefined;
+    }) => void;
+    removeLine: (params: { lineId: string }) => void;
+    setShippingMethod: (params: { shippingMethodId: string }) => void;
+    applyCouponCode: (params: { couponCode: string }) => void;
+    removeCouponCode: (params: { couponCode: string }) => void;
+    updateShippingAddress: (address: AddressFragment) => void;
+    updateBillingAddress: (address: AddressFragment) => void;
+    hasModifications: boolean;
+}
+
+/**
+ * Custom hook to manage order modification state and operations
+ */
+export function useModifyOrder(order: Order | null | undefined): UseModifyOrderReturn {
+    const [modifyOrderInput, setModifyOrderInput] = useState<ModifyOrderInput>({
+        orderId: '',
+        addItems: [],
+        adjustOrderLines: [],
+        surcharges: [],
+        note: '',
+        couponCodes: [],
+        options: {
+            recalculateShipping: true,
+        },
+        dryRun: true,
+    });
+
+    const [addedVariants, setAddedVariants] = useState<Map<string, ProductVariantInfo>>(new Map());
+
+    // Sync orderId and couponCodes when order changes
+    useEffect(() => {
+        setModifyOrderInput(prev => ({
+            ...prev,
+            orderId: order?.id ?? '',
+            couponCodes: order?.couponCodes ?? [],
+        }));
+    }, [order?.id]);
+
+    // Add item or increment existing line
+    const addItem = useCallback(
+        (variant: ProductVariantInfo) => {
+            setModifyOrderInput(prev => {
+                const existingLine = order?.lines.find(
+                    line => line.productVariant.id === variant.productVariantId,
+                );
+
+                if (existingLine) {
+                    const existingAdjustment = prev.adjustOrderLines?.find(
+                        adj => adj.orderLineId === existingLine.id,
+                    );
+
+                    if (existingAdjustment) {
+                        const newQuantity = existingAdjustment.quantity + 1;
+
+                        // If back to original quantity, remove from adjustments
+                        if (newQuantity === existingLine.quantity) {
+                            return {
+                                ...prev,
+                                adjustOrderLines: (prev.adjustOrderLines ?? []).filter(
+                                    adj => adj.orderLineId !== existingLine.id,
+                                ),
+                            };
+                        }
+
+                        return {
+                            ...prev,
+                            adjustOrderLines:
+                                prev.adjustOrderLines?.map(adj =>
+                                    adj.orderLineId === existingLine.id
+                                        ? { ...adj, quantity: newQuantity }
+                                        : adj,
+                                ) ?? [],
+                        };
+                    } else {
+                        return {
+                            ...prev,
+                            adjustOrderLines: [
+                                ...(prev.adjustOrderLines ?? []),
+                                { orderLineId: existingLine.id, quantity: existingLine.quantity + 1 },
+                            ],
+                        };
+                    }
+                } else {
+                    return {
+                        ...prev,
+                        addItems: [
+                            ...(prev.addItems ?? []),
+                            { productVariantId: variant.productVariantId, quantity: 1 },
+                        ],
+                    };
+                }
+            });
+
+            setAddedVariants(prev => {
+                const newMap = new Map(prev);
+                newMap.set(variant.productVariantId, variant);
+                return newMap;
+            });
+        },
+        [order],
+    );
+
+    // Adjust line quantity or custom fields
+    const adjustLine = useCallback(
+        ({
+            lineId,
+            quantity,
+            customFields,
+        }: {
+            lineId: string;
+            quantity: number;
+            customFields: Record<string, any> | undefined;
+        }) => {
+            if (lineId.startsWith('added-')) {
+                const productVariantId = lineId.replace('added-', '');
+                setModifyOrderInput(prev => ({
+                    ...prev,
+                    addItems: (prev.addItems ?? []).map(item =>
+                        item.productVariantId === productVariantId ? { ...item, quantity } : item,
+                    ),
+                }));
+            } else {
+                let normalizedCustomFields: Record<string, any> | undefined = customFields;
+                if (normalizedCustomFields) {
+                    delete normalizedCustomFields.__entityId__;
+                }
+                if (normalizedCustomFields && Object.keys(normalizedCustomFields).length === 0) {
+                    normalizedCustomFields = undefined;
+                }
+
+                setModifyOrderInput(prev => {
+                    const originalLine = order?.lines.find(l => l.id === lineId);
+                    const isBackToOriginal = originalLine && originalLine.quantity === quantity;
+
+                    const originalCustomFields = (originalLine as any)?.customFields;
+                    const customFieldsChanged =
+                        JSON.stringify(normalizedCustomFields) !== JSON.stringify(originalCustomFields);
+
+                    const existing = (prev.adjustOrderLines ?? []).find(l => l.orderLineId === lineId);
+
+                    // If back to original and custom fields unchanged, remove from adjustments
+                    if (isBackToOriginal && !customFieldsChanged) {
+                        return {
+                            ...prev,
+                            adjustOrderLines: (prev.adjustOrderLines ?? []).filter(
+                                l => l.orderLineId !== lineId,
+                            ),
+                        };
+                    }
+
+                    const adjustOrderLines = existing
+                        ? (prev.adjustOrderLines ?? []).map(l =>
+                              l.orderLineId === lineId
+                                  ? { ...l, quantity, customFields: normalizedCustomFields }
+                                  : l,
+                          )
+                        : [
+                              ...(prev.adjustOrderLines ?? []),
+                              { orderLineId: lineId, quantity, customFields: normalizedCustomFields },
+                          ];
+                    return { ...prev, adjustOrderLines };
+                });
+            }
+        },
+        [order],
+    );
+
+    // Remove line
+    const removeLine = useCallback(({ lineId }: { lineId: string }) => {
+        if (lineId.startsWith('added-')) {
+            const productVariantId = lineId.replace('added-', '');
+            setModifyOrderInput(prev => ({
+                ...prev,
+                addItems: (prev.addItems ?? []).filter(item => item.productVariantId !== productVariantId),
+            }));
+            setAddedVariants(prev => {
+                const newMap = new Map(prev);
+                newMap.delete(productVariantId);
+                return newMap;
+            });
+        } else {
+            setModifyOrderInput(prev => {
+                const existingAdjustment = (prev.adjustOrderLines ?? []).find(l => l.orderLineId === lineId);
+                const adjustOrderLines = existingAdjustment
+                    ? (prev.adjustOrderLines ?? []).map(l =>
+                          l.orderLineId === lineId ? { ...l, quantity: 0 } : l,
+                      )
+                    : [...(prev.adjustOrderLines ?? []), { orderLineId: lineId, quantity: 0 }];
+                return {
+                    ...prev,
+                    adjustOrderLines,
+                };
+            });
+        }
+    }, []);
+
+    // Set shipping method
+    const setShippingMethod = useCallback(({ shippingMethodId }: { shippingMethodId: string }) => {
+        setModifyOrderInput(prev => ({
+            ...prev,
+            shippingMethodIds: [shippingMethodId],
+        }));
+    }, []);
+
+    // Apply coupon code
+    const applyCouponCode = useCallback(({ couponCode }: { couponCode: string }) => {
+        setModifyOrderInput(prev => ({
+            ...prev,
+            couponCodes: prev.couponCodes?.includes(couponCode)
+                ? prev.couponCodes
+                : [...(prev.couponCodes ?? []), couponCode],
+        }));
+    }, []);
+
+    // Remove coupon code
+    const removeCouponCode = useCallback(({ couponCode }: { couponCode: string }) => {
+        setModifyOrderInput(prev => ({
+            ...prev,
+            couponCodes: (prev.couponCodes ?? []).filter(code => code !== couponCode),
+        }));
+    }, []);
+
+    // Update shipping address
+    const updateShippingAddress = useCallback((address: AddressFragment) => {
+        setModifyOrderInput(prev => ({
+            ...prev,
+            updateShippingAddress: {
+                streetLine1: address.streetLine1,
+                streetLine2: address.streetLine2,
+                city: address.city,
+                countryCode: address.country.code,
+                fullName: address.fullName,
+                postalCode: address.postalCode,
+                province: address.province,
+                company: address.company,
+                phoneNumber: address.phoneNumber,
+            },
+        }));
+    }, []);
+
+    // Update billing address
+    const updateBillingAddress = useCallback((address: AddressFragment) => {
+        setModifyOrderInput(prev => ({
+            ...prev,
+            updateBillingAddress: {
+                streetLine1: address.streetLine1,
+                streetLine2: address.streetLine2,
+                city: address.city,
+                countryCode: address.country.code,
+                fullName: address.fullName,
+                postalCode: address.postalCode,
+                province: address.province,
+                company: address.company,
+                phoneNumber: address.phoneNumber,
+            },
+        }));
+    }, []);
+
+    // Check if there are modifications
+    const hasModifications = useMemo(() => {
+        return (
+            (modifyOrderInput.addItems?.length ?? 0) > 0 ||
+            (modifyOrderInput.adjustOrderLines?.length ?? 0) > 0 ||
+            (modifyOrderInput.couponCodes?.length ?? 0) > 0 ||
+            (modifyOrderInput.shippingMethodIds?.length ?? 0) > 0 ||
+            !!modifyOrderInput.updateShippingAddress ||
+            !!modifyOrderInput.updateBillingAddress
+        );
+    }, [modifyOrderInput]);
+
+    return {
+        modifyOrderInput,
+        addedVariants,
+        addItem,
+        adjustLine,
+        removeLine,
+        setShippingMethod,
+        applyCouponCode,
+        removeCouponCode,
+        updateShippingAddress,
+        updateBillingAddress,
+        hasModifications,
+    };
+}