Просмотр исходного кода

feat(dashboard): Support for split orders (#3829)

Michael Bromley 3 месяцев назад
Родитель
Сommit
f31c3862e9

+ 302 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx

@@ -0,0 +1,302 @@
+import { CustomFieldsForm } from '@/vdb/components/shared/custom-fields-form.js';
+import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
+import { Button } from '@/vdb/components/ui/button.js';
+import { DropdownMenuItem } from '@/vdb/components/ui/dropdown-menu.js';
+import {
+    Page,
+    PageActionBar,
+    PageActionBarRight,
+    PageBlock,
+    PageLayout,
+    PageTitle,
+} from '@/vdb/framework/layout-engine/page-layout.js';
+import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
+import { api } from '@/vdb/graphql/api.js';
+import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
+import { Trans, useLingui } from '@/vdb/lib/trans.js';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Link, useNavigate } from '@tanstack/react-router';
+import { ResultOf } from 'gql.tada';
+import { Pencil, User } from 'lucide-react';
+import { useMemo } from 'react';
+import { toast } from 'sonner';
+import {
+    orderDetailDocument,
+    setOrderCustomFieldsDocument,
+    transitionOrderToStateDocument,
+} from '../orders.graphql.js';
+import { canAddFulfillment, shouldShowAddManualPaymentButton } from '../utils/order-utils.js';
+import { AddManualPaymentDialog } from './add-manual-payment-dialog.js';
+import { FulfillOrderDialog } from './fulfill-order-dialog.js';
+import { FulfillmentDetails } from './fulfillment-details.js';
+import { OrderAddress } from './order-address.js';
+import { OrderHistoryContainer } from './order-history/order-history-container.js';
+import { orderHistoryQueryKey } from './order-history/use-order-history.js';
+import { OrderTable } from './order-table.js';
+import { OrderTaxSummary } from './order-tax-summary.js';
+import { PaymentDetails } from './payment-details.js';
+import { getTypeForState, StateTransitionControl } from './state-transition-control.js';
+import { useTransitionOrderToState } from './use-transition-order-to-state.js';
+
+export type OrderDetail = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
+
+export interface OrderDetailSharedProps {
+    // Required props
+    pageId: string;
+    orderId: string;
+    // Title customization
+    titleSlot?: (order: OrderDetail) => React.ReactNode;
+    // Optional content slots
+    beforeOrderTable?: (order: OrderDetail) => React.ReactNode;
+}
+
+function DefaultOrderTitle({ entity }: { entity: any }) {
+    return <>{entity?.code ?? ''}</>;
+}
+
+/**
+ * @description
+ * Shared functionality between the order and seller order detail pages.
+ */
+export function OrderDetailShared({
+    pageId,
+    orderId,
+    titleSlot,
+    beforeOrderTable,
+}: Readonly<OrderDetailSharedProps>) {
+    const { i18n } = useLingui();
+    const navigate = useNavigate();
+    const queryClient = useQueryClient();
+
+    const { form, submitHandler, entity, refreshEntity } = useDetailPage({
+        pageId,
+        queryDocument: orderDetailDocument,
+        updateDocument: setOrderCustomFieldsDocument,
+        setValuesForUpdate: (entity: any) => {
+            return {
+                id: entity.id,
+                customFields: entity.customFields,
+            };
+        },
+        params: { id: orderId },
+        onSuccess: async () => {
+            toast(i18n.t('Successfully updated order'));
+            form.reset(form.getValues());
+        },
+        onError: err => {
+            toast(i18n.t('Failed to update order'), {
+                description: err instanceof Error ? err.message : 'Unknown error',
+            });
+        },
+    });
+
+    const { transitionToState } = useTransitionOrderToState(entity?.id);
+    const transitionOrderToStateMutation = useMutation({
+        mutationFn: api.mutate(transitionOrderToStateDocument),
+    });
+
+    const customFieldConfig = useCustomFieldConfig('Order');
+
+    const stateTransitionActions = useMemo(() => {
+        if (!entity) {
+            return [];
+        }
+        return entity.nextStates.map((state: string) => ({
+            label: `Transition to ${state}`,
+            type: getTypeForState(state),
+            onClick: async () => {
+                const transitionError = await transitionToState(state);
+                if (transitionError) {
+                    toast(i18n.t('Failed to transition order to state'), {
+                        description: transitionError,
+                    });
+                } else {
+                    refreshOrderAndHistory();
+                }
+            },
+        }));
+    }, [entity, transitionToState, i18n]);
+
+    if (!entity) {
+        return null;
+    }
+
+    const handleModifyClick = async () => {
+        try {
+            await transitionOrderToStateMutation.mutateAsync({
+                id: entity.id,
+                state: 'Modifying',
+            });
+            const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
+            await queryClient.invalidateQueries({ queryKey });
+            await navigate({ to: `/orders/$id/modify`, params: { id: entity.id } });
+        } catch (error) {
+            toast(i18n.t('Failed to modify order'), {
+                description: error instanceof Error ? error.message : 'Unknown error',
+            });
+        }
+    };
+
+    const nextStates = entity.nextStates;
+    const showAddPaymentButton = shouldShowAddManualPaymentButton(entity);
+    const showFulfillButton = canAddFulfillment(entity);
+
+    async function refreshOrderAndHistory() {
+        if (entity) {
+            const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
+            await queryClient.invalidateQueries({ queryKey });
+            queryClient.refetchQueries({ queryKey: orderHistoryQueryKey(entity.id) });
+        }
+    }
+
+    return (
+        <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
+            <PageTitle>{titleSlot?.(entity) || <DefaultOrderTitle entity={entity} />}</PageTitle>
+            <PageActionBar>
+                <PageActionBarRight
+                    dropdownMenuItems={[
+                        ...(nextStates.includes('Modifying')
+                            ? [
+                                  {
+                                      component: () => (
+                                          <DropdownMenuItem onClick={handleModifyClick}>
+                                              <Pencil className="w-4 h-4" />
+                                              <Trans>Modify</Trans>
+                                          </DropdownMenuItem>
+                                      ),
+                                  },
+                              ]
+                            : []),
+                    ]}
+                >
+                    {showAddPaymentButton && (
+                        <PermissionGuard requires={['UpdateOrder']}>
+                            <AddManualPaymentDialog
+                                order={entity}
+                                onSuccess={() => {
+                                    refreshEntity();
+                                }}
+                            />
+                        </PermissionGuard>
+                    )}
+                    {showFulfillButton && (
+                        <PermissionGuard requires={['UpdateOrder']}>
+                            <FulfillOrderDialog
+                                order={entity}
+                                onSuccess={() => {
+                                    refreshOrderAndHistory();
+                                }}
+                            />
+                        </PermissionGuard>
+                    )}
+                </PageActionBarRight>
+            </PageActionBar>
+            <PageLayout>
+                {/* Main Column Blocks */}
+                {beforeOrderTable?.(entity)}
+                <PageBlock column="main" blockId="order-table">
+                    <OrderTable order={entity} pageId={pageId} />
+                </PageBlock>
+                <PageBlock column="main" blockId="tax-summary" title={<Trans>Tax summary</Trans>}>
+                    <OrderTaxSummary order={entity} />
+                </PageBlock>
+                {customFieldConfig?.length ? (
+                    <PageBlock column="main" blockId="custom-fields">
+                        <CustomFieldsForm entityType="Order" control={form.control} />
+                        <div className="flex justify-end">
+                            <Button
+                                type="submit"
+                                disabled={!form.formState.isDirty || !form.formState.isValid}
+                            >
+                                <Trans>Save</Trans>
+                            </Button>
+                        </div>
+                    </PageBlock>
+                ) : null}
+                <PageBlock column="main" blockId="payment-details" title={<Trans>Payment details</Trans>}>
+                    <div className="grid lg:grid-cols-2 gap-4">
+                        {entity?.payments?.map((payment: any) => (
+                            <PaymentDetails
+                                key={payment.id}
+                                payment={payment}
+                                currencyCode={entity.currencyCode}
+                                onSuccess={refreshOrderAndHistory}
+                            />
+                        ))}
+                    </div>
+                </PageBlock>
+                <PageBlock column="main" blockId="order-history" title={<Trans>Order history</Trans>}>
+                    <OrderHistoryContainer orderId={orderId} />
+                </PageBlock>
+
+                {/* Side Column Blocks */}
+                <PageBlock column="side" blockId="state">
+                    <StateTransitionControl
+                        currentState={entity?.state}
+                        actions={stateTransitionActions}
+                        isLoading={transitionOrderToStateMutation.isPending}
+                    />
+                </PageBlock>
+                <PageBlock column="side" blockId="customer" title={<Trans>Customer</Trans>}>
+                    {entity?.customer ? (
+                        <Button variant="ghost" asChild>
+                            <Link to={`/customers/${entity.customer.id}`}>
+                                <User className="w-4 h-4" />
+                                {entity.customer.firstName} {entity.customer.lastName}
+                            </Link>
+                        </Button>
+                    ) : (
+                        <div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
+                            <Trans>No customer</Trans>
+                        </div>
+                    )}
+                    <div className="mt-4 divide-y">
+                        {entity?.shippingAddress && (
+                            <div className="pb-6">
+                                <div className="font-medium">
+                                    <Trans>Shipping address</Trans>
+                                </div>
+                                <OrderAddress address={entity.shippingAddress} />
+                            </div>
+                        )}
+                        {entity?.billingAddress && (
+                            <div className="pt-4">
+                                <div className="font-medium">
+                                    <Trans>Billing address</Trans>
+                                </div>
+                                <OrderAddress address={entity.billingAddress} />
+                            </div>
+                        )}
+                    </div>
+                </PageBlock>
+                <PageBlock
+                    column="side"
+                    blockId="fulfillment-details"
+                    title={<Trans>Fulfillment details</Trans>}
+                >
+                    {entity?.fulfillments?.length && entity.fulfillments.length > 0 ? (
+                        <div className="space-y-2">
+                            {entity?.fulfillments?.map((fulfillment: any) => (
+                                <FulfillmentDetails
+                                    key={fulfillment.id}
+                                    order={entity}
+                                    fulfillment={fulfillment}
+                                    onSuccess={() => {
+                                        refreshEntity();
+                                        queryClient.refetchQueries({
+                                            queryKey: orderHistoryQueryKey(entity.id),
+                                        });
+                                    }}
+                                />
+                            ))}
+                        </div>
+                    ) : (
+                        <div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
+                            <Trans>No fulfillments</Trans>
+                        </div>
+                    )}
+                </PageBlock>
+            </PageLayout>
+        </Page>
+    );
+}

+ 16 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx

@@ -13,6 +13,22 @@ export function OrderTableTotals({ order, columnCount }: Readonly<OrderTableTota
 
 
     return (
     return (
         <>
         <>
+            {order.surcharges?.length > 0
+                ? order.surcharges.map((surcharge, index) => (
+                      <TableRow key={`${surcharge.description}-${index}`}>
+                          <TableCell colSpan={columnCount - 1} className="h-12">
+                              <Trans>Surcharge</Trans>: {surcharge.description}
+                          </TableCell>
+                          <TableCell colSpan={1} className="h-12">
+                              <MoneyGrossNet
+                                  priceWithTax={surcharge.priceWithTax}
+                                  price={surcharge.price}
+                                  currencyCode={currencyCode}
+                              />
+                          </TableCell>
+                      </TableRow>
+                  ))
+                : null}
             {order.discounts?.length > 0
             {order.discounts?.length > 0
                 ? order.discounts.map((discount, index) => (
                 ? order.discounts.map((discount, index) => (
                       <TableRow key={`${discount.description}-${index}`}>
                       <TableRow key={`${discount.description}-${index}`}>

+ 61 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx

@@ -0,0 +1,61 @@
+import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
+import { Badge } from '@/vdb/components/ui/badge.js';
+import { api } from '@/vdb/graphql/api.js';
+import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
+import { useQuery } from '@tanstack/react-query';
+import { sellerOrdersDocument } from '../orders.graphql.js';
+import { getSeller } from '../utils/order-utils.js';
+
+export interface SellerOrdersCardProps {
+    orderId: string;
+}
+
+export function SellerOrdersCard({ orderId }: Readonly<SellerOrdersCardProps>) {
+    const { formatCurrency } = useLocalFormat();
+    const { data, isLoading, error } = useQuery({
+        queryKey: ['seller-orders', orderId],
+        queryFn: () => api.query(sellerOrdersDocument, { orderId }),
+    });
+
+    if (isLoading) {
+        return (
+            <div className="animate-pulse space-y-2">
+                {Array.from({ length: 2 }).map((_, i) => (
+                    <div key={i} className="h-16 bg-muted rounded-md" />
+                ))}
+            </div>
+        );
+    }
+
+    if (error || !data?.order || !data.order.sellerOrders?.length) {
+        return null;
+    }
+
+    return (
+        <div className="flex flex-col gap-4 divide-y">
+            {data.order.sellerOrders.map(sellerOrder => {
+                const seller = getSeller(sellerOrder);
+
+                return (
+                    <div key={sellerOrder.id} className="p-3 -mx-3 pb-6 transition-colors">
+                        <div className="flex justify-between items-center">
+                            <DetailPageButton
+                                label={sellerOrder.code}
+                                href={`/orders/${orderId}/seller-orders/${sellerOrder.id}`}
+                            />
+                            <div className="text-sm font-medium">
+                                {formatCurrency(sellerOrder.totalWithTax, sellerOrder.currencyCode)}
+                            </div>
+                        </div>
+                        <div className="flex justify-between mt-1">
+                            <div className="flex gap-2">
+                                {seller && <Badge variant={'secondary'}>{seller.name}</Badge>}
+                            </div>
+                            <Badge variant={'secondary'}>{sellerOrder.state}</Badge>
+                        </div>
+                    </div>
+                );
+            })}
+        </div>
+    );
+}

+ 17 - 10
packages/dashboard/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx

@@ -1,13 +1,20 @@
+import { Alert, AlertDescription } from '@/vdb/components/ui/alert.js';
+import { Button } from '@/vdb/components/ui/button.js';
+import {
+    Dialog,
+    DialogContent,
+    DialogDescription,
+    DialogFooter,
+    DialogHeader,
+    DialogTitle,
+} from '@/vdb/components/ui/dialog.js';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
 import { api } from '@/vdb/graphql/api.js';
 import { api } from '@/vdb/graphql/api.js';
-import { useMutation, useQuery } from '@tanstack/react-query';
-import { orderHistoryDocument, transitionOrderToStateDocument } from '../orders.graphql.js';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/vdb/components/ui/dialog.js';
 import { Trans } from '@/vdb/lib/trans.js';
 import { Trans } from '@/vdb/lib/trans.js';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
-import { useState } from 'react';
-import { Button } from '@/vdb/components/ui/button.js';
-import { Alert, AlertDescription } from '@/vdb/components/ui/alert.js';
+import { useMutation, useQuery } from '@tanstack/react-query';
 import { ResultOf } from 'gql.tada';
 import { ResultOf } from 'gql.tada';
+import { useState } from 'react';
+import { orderHistoryDocument, transitionOrderToStateDocument } from '../orders.graphql.js';
 
 
 /**
 /**
  * Returns the state the order was in before it entered 'Modifying'.
  * Returns the state the order was in before it entered 'Modifying'.
@@ -29,7 +36,7 @@ export function useTransitionOrderToState(orderId: string | undefined) {
             });
             });
             const items = result.order?.history?.items ?? [];
             const items = result.order?.history?.items ?? [];
             const modifyingEntry = items.find(i => i.data?.to === 'Modifying');
             const modifyingEntry = items.find(i => i.data?.to === 'Modifying');
-            return modifyingEntry ? (modifyingEntry.data?.from as string | undefined) : undefined;
+            return modifyingEntry ? (modifyingEntry.data?.from as string | undefined) : '';
         },
         },
         enabled: !!orderId,
         enabled: !!orderId,
     });
     });
@@ -48,7 +55,7 @@ export function useTransitionOrderToState(orderId: string | undefined) {
             }
             }
         }
         }
         return undefined;
         return undefined;
-    }
+    };
 
 
     const transitionToPreModifyingState = async () => {
     const transitionToPreModifyingState = async () => {
         if (data && orderId) {
         if (data && orderId) {
@@ -70,7 +77,7 @@ export function useTransitionOrderToState(orderId: string | undefined) {
                     onSuccessFn?.();
                     onSuccessFn?.();
                 }
                 }
             },
             },
-            onError: (error) => {
+            onError: error => {
                 setTransitionError(error.message);
                 setTransitionError(error.message);
             },
             },
         });
         });

+ 31 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/orders.graphql.ts

@@ -189,6 +189,13 @@ export const orderDetailFragment = graphql(
                     code
                     code
                 }
                 }
             }
             }
+            channels {
+                id
+                code
+                seller {
+                    name
+                }
+            }
             code
             code
             state
             state
             nextStates
             nextStates
@@ -722,3 +729,27 @@ export const setOrderCustomFieldsDocument = graphql(`
         }
         }
     }
     }
 `);
 `);
+
+export const sellerOrdersDocument = graphql(`
+    query GetSellerOrders($orderId: ID!) {
+        order(id: $orderId) {
+            id
+            sellerOrders {
+                id
+                code
+                state
+                orderPlacedAt
+                currencyCode
+                totalWithTax
+                channels {
+                    id
+                    code
+                    seller {
+                        id
+                        name
+                    }
+                }
+            }
+        }
+    }
+`);

+ 50 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx

@@ -0,0 +1,50 @@
+import { ErrorPage } from '@/vdb/components/shared/error-page.js';
+import { Badge } from '@/vdb/components/ui/badge.js';
+import { Button } from '@/vdb/components/ui/button.js';
+import { Trans } from '@/vdb/lib/trans.js';
+import { createFileRoute, Link } from '@tanstack/react-router';
+import { ArrowLeft } from 'lucide-react';
+import { OrderDetail, OrderDetailShared } from './components/order-detail-shared.js';
+import { loadSellerOrder } from './utils/order-detail-loaders.js';
+import { getSeller } from './utils/order-utils.js';
+
+export const Route = createFileRoute(
+    '/_authenticated/_orders/orders_/$aggregateOrderId_/seller-orders/$sellerOrderId',
+)({
+    component: SellerOrderDetailPage,
+    loader: ({ context, params }) => loadSellerOrder(context, params),
+    errorComponent: ({ error }) => <ErrorPage message={error.message} />,
+});
+
+function SellerOrderDetailPage() {
+    const params = Route.useParams();
+
+    const titleSlot = (order: OrderDetail) => {
+        const seller = getSeller(order);
+        return (
+            <div className="flex items-center gap-2">
+                <Button variant="ghost" size="sm" asChild>
+                    <Link
+                        to="/orders/$aggregateOrderId"
+                        params={{ aggregateOrderId: params.aggregateOrderId }}
+                    >
+                        <ArrowLeft className="h-4 w-4" />
+                    </Link>
+                </Button>
+                {order.code ?? ''}
+                <Badge variant="secondary">
+                    <Trans>Seller order</Trans>
+                </Badge>
+                {seller && <Badge variant="outline">{seller.name}</Badge>}
+            </div>
+        );
+    };
+
+    return (
+        <OrderDetailShared
+            pageId="seller-order-detail"
+            orderId={params.sellerOrderId}
+            titleSlot={titleSlot}
+        />
+    );
+}

+ 17 - 290
packages/dashboard/src/app/routes/_authenticated/_orders/orders_.$id.tsx

@@ -1,303 +1,30 @@
-import { CustomFieldsForm } from '@/vdb/components/shared/custom-fields-form.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
-import { Button } from '@/vdb/components/ui/button.js';
-import { DropdownMenuItem } from '@/vdb/components/ui/dropdown-menu.js';
-import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
-import {
-    Page,
-    PageActionBar,
-    PageActionBarRight,
-    PageBlock,
-    PageLayout,
-    PageTitle,
-} from '@/vdb/framework/layout-engine/page-layout.js';
-import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
-import { api } from '@/vdb/graphql/api.js';
-import { ResultOf } from '@/vdb/graphql/graphql.js';
-import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
-import { Trans, useLingui } from '@/vdb/lib/trans.js';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
-import { Pencil, User } from 'lucide-react';
-import { useMemo } from 'react';
-import { toast } from 'sonner';
-import { AddManualPaymentDialog } from './components/add-manual-payment-dialog.js';
-import { FulfillOrderDialog } from './components/fulfill-order-dialog.js';
-import { FulfillmentDetails } from './components/fulfillment-details.js';
-import { OrderAddress } from './components/order-address.js';
-import { OrderHistoryContainer } from './components/order-history/order-history-container.js';
-import { orderHistoryQueryKey } from './components/order-history/use-order-history.js';
-import { OrderTable } from './components/order-table.js';
-import { OrderTaxSummary } from './components/order-tax-summary.js';
-import { PaymentDetails } from './components/payment-details.js';
-import { getTypeForState, StateTransitionControl } from './components/state-transition-control.js';
-import { useTransitionOrderToState } from './components/use-transition-order-to-state.js';
-import {
-    orderDetailDocument,
-    setOrderCustomFieldsDocument,
-    transitionOrderToStateDocument,
-} from './orders.graphql.js';
-import { canAddFulfillment, shouldShowAddManualPaymentButton } from './utils/order-utils.js';
-
-const pageId = 'order-detail';
+import { PageBlock } from '@/vdb/framework/layout-engine/page-layout.js';
+import { Trans } from '@/vdb/lib/trans.js';
+import { createFileRoute } from '@tanstack/react-router';
+import { OrderDetailShared } from './components/order-detail-shared.js';
+import { SellerOrdersCard } from './components/seller-orders-card.js';
+import { loadRegularOrder } from './utils/order-detail-loaders.js';
 
 
 export const Route = createFileRoute('/_authenticated/_orders/orders_/$id')({
 export const Route = createFileRoute('/_authenticated/_orders/orders_/$id')({
     component: OrderDetailPage,
     component: OrderDetailPage,
-    loader: async ({ context, params }) => {
-        if (!params.id) {
-            throw new Error('ID param is required');
-        }
-
-        const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
-            getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
-            { id: params.id },
-        );
-
-        if (!result.order) {
-            throw new Error(`Order with the ID ${params.id} was not found`);
-        }
-
-        if (result.order.state === 'Draft') {
-            throw redirect({
-                to: `/orders/draft/${params.id}`,
-            });
-        }
-
-        if (result.order.state === 'Modifying') {
-            throw redirect({
-                to: `/orders/${params.id}/modify`,
-            });
-        }
-
-        return {
-            breadcrumb: [{ path: '/orders', label: <Trans>Orders</Trans> }, result.order.code],
-        };
-    },
+    loader: ({ context, params }) => loadRegularOrder(context, params),
     errorComponent: ({ error }) => <ErrorPage message={error.message} />,
     errorComponent: ({ error }) => <ErrorPage message={error.message} />,
 });
 });
 
 
 function OrderDetailPage() {
 function OrderDetailPage() {
     const params = Route.useParams();
     const params = Route.useParams();
-    const { i18n } = useLingui();
-    const navigate = useNavigate();
-    const queryClient = useQueryClient();
-    const { form, submitHandler, entity, refreshEntity } = useDetailPage({
-        pageId,
-        queryDocument: orderDetailDocument,
-        updateDocument: setOrderCustomFieldsDocument,
-        setValuesForUpdate: (entity: any) => {
-            return {
-                id: entity.id,
-                customFields: entity.customFields,
-            };
-        },
-        params: { id: params.id },
-        onSuccess: async () => {
-            toast(i18n.t('Successfully updated order'));
-            form.reset(form.getValues());
-        },
-        onError: err => {
-            toast(i18n.t('Failed to update order'), {
-                description: err instanceof Error ? err.message : 'Unknown error',
-            });
-        },
-    });
-    const { transitionToState } = useTransitionOrderToState(entity?.id);
-    const transitionOrderToStateMutation = useMutation({
-        mutationFn: api.mutate(transitionOrderToStateDocument),
-    });
-    const customFieldConfig = useCustomFieldConfig('Order');
-    const stateTransitionActions = useMemo(() => {
-        if (!entity) {
-            return [];
-        }
-        return entity.nextStates.map(state => ({
-            label: `Transition to ${state}`,
-            type: getTypeForState(state),
-            onClick: async () => {
-                const transitionError = await transitionToState(state);
-                if (transitionError) {
-                    toast(i18n.t('Failed to transition order to state'), {
-                        description: transitionError,
-                    });
-                } else {
-                    refreshOrderAndHistory();
-                }
-            },
-        }));
-    }, [entity, transitionToState, i18n]);
-
-    if (!entity) {
-        return null;
-    }
-
-    const handleModifyClick = async () => {
-        try {
-            await transitionOrderToStateMutation.mutateAsync({
-                id: entity.id,
-                state: 'Modifying',
-            });
-            const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
-            await queryClient.invalidateQueries({ queryKey });
-            await navigate({ to: `/orders/$id/modify`, params: { id: entity.id } });
-        } catch (error) {
-            toast(i18n.t('Failed to modify order'), {
-                description: error instanceof Error ? error.message : 'Unknown error',
-            });
-        }
-    };
-
-    const nextStates = entity.nextStates;
-    const showAddPaymentButton = shouldShowAddManualPaymentButton(entity);
-    const showFulfillButton = canAddFulfillment(entity);
-
-    async function refreshOrderAndHistory() {
-        if (entity) {
-            const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
-            await queryClient.invalidateQueries({ queryKey });
-            queryClient.refetchQueries({ queryKey: orderHistoryQueryKey(entity.id) });
-        }
-    }
-
     return (
     return (
-        <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
-            <PageTitle>{entity?.code ?? ''}</PageTitle>
-            <PageActionBar>
-                <PageActionBarRight
-                    dropdownMenuItems={[
-                        ...(nextStates.includes('Modifying')
-                            ? [
-                                  {
-                                      component: () => (
-                                          <DropdownMenuItem onClick={handleModifyClick}>
-                                              <Pencil className="w-4 h-4" />
-                                              <Trans>Modify</Trans>
-                                          </DropdownMenuItem>
-                                      ),
-                                  },
-                              ]
-                            : []),
-                    ]}
-                >
-                    {showAddPaymentButton && (
-                        <PermissionGuard requires={['UpdateOrder']}>
-                            <AddManualPaymentDialog
-                                order={entity}
-                                onSuccess={() => {
-                                    refreshEntity();
-                                }}
-                            />
-                        </PermissionGuard>
-                    )}
-                    {showFulfillButton && (
-                        <PermissionGuard requires={['UpdateOrder']}>
-                            <FulfillOrderDialog
-                                order={entity}
-                                onSuccess={() => {
-                                    refreshOrderAndHistory();
-                                }}
-                            />
-                        </PermissionGuard>
-                    )}
-                </PageActionBarRight>
-            </PageActionBar>
-            <PageLayout>
-                <PageBlock column="main" blockId="order-table">
-                    <OrderTable order={entity} pageId={pageId} />
-                </PageBlock>
-                <PageBlock column="main" blockId="tax-summary" title={<Trans>Tax summary</Trans>}>
-                    <OrderTaxSummary order={entity} />
-                </PageBlock>
-                {customFieldConfig?.length ? (
-                    <PageBlock column="main" blockId="custom-fields">
-                        <CustomFieldsForm entityType="Order" control={form.control} />
-                        <div className="flex justify-end">
-                            <Button
-                                type="submit"
-                                disabled={!form.formState.isDirty || !form.formState.isValid}
-                            >
-                                Save
-                            </Button>
-                        </div>
+        <OrderDetailShared
+            pageId="order-detail"
+            orderId={params.id}
+            beforeOrderTable={order =>
+                order.sellerOrders?.length ? (
+                    <PageBlock column="main" blockId="seller-orders" title={<Trans>Seller orders</Trans>}>
+                        <SellerOrdersCard orderId={params.id} />
                     </PageBlock>
                     </PageBlock>
-                ) : null}
-                <PageBlock column="main" blockId="payment-details" title={<Trans>Payment details</Trans>}>
-                    <div className="grid lg:grid-cols-2 gap-4">
-                        {entity?.payments?.map(payment => (
-                            <PaymentDetails
-                                key={payment.id}
-                                payment={payment}
-                                currencyCode={entity.currencyCode}
-                                onSuccess={() => refreshOrderAndHistory()}
-                            />
-                        ))}
-                    </div>
-                </PageBlock>
-                <PageBlock column="main" blockId="order-history" title={<Trans>Order history</Trans>}>
-                    <OrderHistoryContainer orderId={entity.id} />
-                </PageBlock>
-                <PageBlock column="side" blockId="state">
-                    <StateTransitionControl
-                        currentState={entity?.state}
-                        actions={stateTransitionActions}
-                        isLoading={transitionOrderToStateMutation.isPending}
-                    />
-                </PageBlock>
-                <PageBlock column="side" blockId="customer" title={<Trans>Customer</Trans>}>
-                    <Button variant="ghost" asChild>
-                        <Link to={`/customers/${entity?.customer?.id}`}>
-                            <User className="w-4 h-4" />
-                            {entity?.customer?.firstName} {entity?.customer?.lastName}
-                        </Link>
-                    </Button>
-                    <div className="mt-4 divide-y">
-                        {entity?.shippingAddress && (
-                            <div className="pb-6">
-                                <div className="font-medium">
-                                    <Trans>Shipping address</Trans>
-                                </div>
-                                <OrderAddress address={entity.shippingAddress} />
-                            </div>
-                        )}
-                        {entity?.billingAddress && (
-                            <div className="pt-4">
-                                <div className="font-medium">
-                                    <Trans>Billing address</Trans>
-                                </div>
-                                <OrderAddress address={entity.billingAddress} />
-                            </div>
-                        )}
-                    </div>
-                </PageBlock>
-                <PageBlock
-                    column="side"
-                    blockId="fulfillment-details"
-                    title={<Trans>Fulfillment details</Trans>}
-                >
-                    {entity?.fulfillments?.length && entity.fulfillments.length > 0 ? (
-                        <div className="space-y-2">
-                            {entity?.fulfillments?.map(fulfillment => (
-                                <FulfillmentDetails
-                                    key={fulfillment.id}
-                                    order={entity}
-                                    fulfillment={fulfillment}
-                                    onSuccess={() => {
-                                        refreshEntity();
-                                        queryClient.refetchQueries({
-                                            queryKey: orderHistoryQueryKey(entity.id),
-                                        });
-                                    }}
-                                />
-                            ))}
-                        </div>
-                    ) : (
-                        <div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
-                            <Trans>No fulfillments</Trans>
-                        </div>
-                    )}
-                </PageBlock>
-            </PageLayout>
-        </Page>
+                ) : undefined
+            }
+        />
     );
     );
 }
 }

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

@@ -1,6 +1,5 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
 import {
 import {
     Page,
     Page,
     PageActionBar,
     PageActionBar,
@@ -13,8 +12,8 @@ import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-d
 import { api } from '@/vdb/graphql/api.js';
 import { api } from '@/vdb/graphql/api.js';
 import { Trans, useLingui } from '@/vdb/lib/trans.js';
 import { Trans, useLingui } from '@/vdb/lib/trans.js';
 import { useQuery, useQueryClient } from '@tanstack/react-query';
 import { useQuery, useQueryClient } from '@tanstack/react-query';
-import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
-import { ResultOf, VariablesOf } from 'gql.tada';
+import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
+import { VariablesOf } from 'gql.tada';
 import { User } from 'lucide-react';
 import { User } from 'lucide-react';
 import { useEffect, useState } from 'react';
 import { useEffect, useState } from 'react';
 import { toast } from 'sonner';
 import { toast } from 'sonner';
@@ -29,6 +28,7 @@ import {
     modifyOrderDocument,
     modifyOrderDocument,
     orderDetailDocument,
     orderDetailDocument,
 } from './orders.graphql.js';
 } from './orders.graphql.js';
+import { loadModifyingOrder } from './utils/order-detail-loaders.js';
 import { AddressFragment, Order } from './utils/order-types.js';
 import { AddressFragment, Order } from './utils/order-types.js';
 
 
 const pageId = 'order-modify';
 const pageId = 'order-modify';
@@ -36,39 +36,7 @@ type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
 
 
 export const Route = createFileRoute('/_authenticated/_orders/orders_/$id_/modify')({
 export const Route = createFileRoute('/_authenticated/_orders/orders_/$id_/modify')({
     component: ModifyOrderPage,
     component: ModifyOrderPage,
-    loader: async ({ context, params }) => {
-        if (!params.id) {
-            throw new Error('ID param is required');
-        }
-
-        const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
-            getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
-            { id: params.id },
-        );
-
-        if (!result.order) {
-            throw new Error(`Order with the ID ${params.id} was not found`);
-        }
-
-        if (result.order.state === 'Draft') {
-            throw redirect({
-                to: `/orders/draft/${params.id}`,
-            });
-        }
-        if (result.order.state !== 'Modifying') {
-            throw redirect({
-                to: `/orders/${params.id}`,
-            });
-        }
-
-        return {
-            breadcrumb: [
-                { path: '/orders', label: <Trans>Orders</Trans> },
-                result.order.code,
-                { label: <Trans>Modify</Trans> },
-            ],
-        };
-    },
+    loader: async ({ context, params }) => loadModifyingOrder(context, params),
     errorComponent: ({ error }) => <ErrorPage message={error.message} />,
     errorComponent: ({ error }) => <ErrorPage message={error.message} />,
 });
 });
 
 
@@ -385,10 +353,10 @@ function ModifyOrderPage() {
     }
     }
 
 
     // On successful state transition, invalidate the order detail query and navigate to the order detail page
     // On successful state transition, invalidate the order detail query and navigate to the order detail page
-    const onSuccess = () => {
+    const onSuccess = async () => {
         const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
         const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
-        queryClient.invalidateQueries({ queryKey });
-        navigate({ to: `/orders/$id`, params: { id: entity?.id } });
+        await queryClient.invalidateQueries({ queryKey });
+        await navigate({ to: `/orders/$id`, params: { id: entity?.id } });
     };
     };
 
 
     const handleCancelModificationClick = async () => {
     const handleCancelModificationClick = async () => {

+ 4 - 26
packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx

@@ -15,11 +15,11 @@ import {
     PageLayout,
     PageLayout,
     PageTitle,
     PageTitle,
 } from '@/vdb/framework/layout-engine/page-layout.js';
 } from '@/vdb/framework/layout-engine/page-layout.js';
-import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
+import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
 import { api } from '@/vdb/graphql/api.js';
 import { api } from '@/vdb/graphql/api.js';
 import { Trans, useLingui } from '@/vdb/lib/trans.js';
 import { Trans, useLingui } from '@/vdb/lib/trans.js';
 import { useMutation, useQuery } from '@tanstack/react-query';
 import { useMutation, useQuery } from '@tanstack/react-query';
-import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
+import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
 import { ResultOf } from 'gql.tada';
 import { ResultOf } from 'gql.tada';
 import { User } from 'lucide-react';
 import { User } from 'lucide-react';
 import { toast } from 'sonner';
 import { toast } from 'sonner';
@@ -44,33 +44,11 @@ import {
     unsetBillingAddressForDraftOrderDocument,
     unsetBillingAddressForDraftOrderDocument,
     unsetShippingAddressForDraftOrderDocument,
     unsetShippingAddressForDraftOrderDocument,
 } from './orders.graphql.js';
 } from './orders.graphql.js';
+import { loadDraftOrder } from './utils/order-detail-loaders.js';
 
 
 export const Route = createFileRoute('/_authenticated/_orders/orders_/draft/$id')({
 export const Route = createFileRoute('/_authenticated/_orders/orders_/draft/$id')({
     component: DraftOrderPage,
     component: DraftOrderPage,
-    loader: async ({ context, params }) => {
-        if (!params.id) {
-            throw new Error('ID param is required');
-        }
-
-        const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
-            getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
-            { id: params.id },
-        );
-
-        if (!result.order) {
-            throw new Error(`Order with the ID ${params.id} was not found`);
-        }
-
-        if (result.order.state !== 'Draft') {
-            throw redirect({
-                to: `/orders/${params.id}`,
-            });
-        }
-
-        return {
-            breadcrumb: [{ path: '/orders', label: <Trans>Orders</Trans> }, result.order.code],
-        };
-    },
+    loader: ({ context, params }) => loadDraftOrder(context, params),
     errorComponent: ({ error }) => <ErrorPage message={error.message} />,
     errorComponent: ({ error }) => <ErrorPage message={error.message} />,
 });
 });
 
 

+ 129 - 0
packages/dashboard/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx

@@ -0,0 +1,129 @@
+import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
+import { getDetailQueryOptions } from '@/vdb/framework/page/use-detail-page.js';
+import { ResultOf } from '@/vdb/graphql/graphql.js';
+import { Trans } from '@/vdb/lib/trans.js';
+import { redirect } from '@tanstack/react-router';
+import { OrderDetail } from '../components/order-detail-shared.js';
+import { orderDetailDocument } from '../orders.graphql.js';
+
+export async function commonRegularOrderLoader(context: any, params: { id: string }): Promise<OrderDetail> {
+    if (!params.id) {
+        throw new Error('ID param is required');
+    }
+
+    const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
+        getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
+    );
+
+    if (!result.order) {
+        throw new Error(`Order with the ID ${params.id} was not found`);
+    }
+
+    if (result.order.state === 'Draft') {
+        throw redirect({
+            to: `/orders/draft/${params.id}`,
+        });
+    }
+    return result.order;
+}
+
+export async function loadRegularOrder(context: any, params: { id: string }) {
+    const order = await commonRegularOrderLoader(context, params);
+
+    if (order.state === 'Modifying') {
+        throw redirect({
+            to: `/orders/${params.id}/modify`,
+        });
+    }
+
+    return {
+        breadcrumb: [{ path: '/orders', label: <Trans>Orders</Trans> }, order.code],
+    };
+}
+
+export async function loadDraftOrder(context: any, params: { id: string }) {
+    const order = await commonRegularOrderLoader(context, params);
+
+    if (order.state !== 'Draft') {
+        throw redirect({
+            to: `/orders/${params.id}`,
+        });
+    }
+
+    return {
+        breadcrumb: [{ path: '/orders', label: <Trans>Orders</Trans> }, order.code],
+    };
+}
+
+export async function loadModifyingOrder(context: any, params: { id: string }) {
+    const order = await commonRegularOrderLoader(context, params);
+    if (order.state !== 'Modifying') {
+        throw redirect({
+            to: `/orders/${params.id}`,
+        });
+    }
+
+    return {
+        breadcrumb: [
+            { path: '/orders', label: <Trans>Orders</Trans> },
+            order.code,
+            { label: <Trans>Modify</Trans> },
+        ],
+    };
+}
+
+export async function loadSellerOrder(
+    context: any,
+    params: { aggregateOrderId: string; sellerOrderId: string },
+) {
+    if (!params.sellerOrderId || !params.aggregateOrderId) {
+        throw new Error('Both seller order ID and aggregate order ID params are required');
+    }
+
+    const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
+        getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.sellerOrderId }),
+    );
+
+    if (!result.order) {
+        throw new Error(`Seller order with the ID ${params.sellerOrderId} was not found`);
+    }
+
+    // Verify this is actually a seller order by checking if it has an aggregateOrder
+    if (!result.order.aggregateOrder) {
+        throw new Error(`Order ${params.sellerOrderId} is not a seller order`);
+    }
+
+    // Verify the aggregate order ID matches
+    if (result.order.aggregateOrder.id !== params.aggregateOrderId) {
+        throw new Error(
+            `Seller order ${params.sellerOrderId} does not belong to aggregate order ${params.aggregateOrderId}`,
+        );
+    }
+
+    if (result.order.state === 'Draft') {
+        throw redirect({
+            to: `/orders/draft/${params.sellerOrderId}`,
+        });
+    }
+
+    if (result.order.state === 'Modifying') {
+        throw redirect({
+            to: `/orders/${params.sellerOrderId}/modify`,
+        });
+    }
+
+    return {
+        breadcrumb: [
+            { path: '/orders', label: <Trans>Orders</Trans> },
+            {
+                path: `/orders/${params.aggregateOrderId}`,
+                label: result.order.aggregateOrder.code,
+            },
+            {
+                path: `/orders/${params.aggregateOrderId}`,
+                label: 'Seller orders',
+            },
+            result.order.code,
+        ],
+    };
+}

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

@@ -1,3 +1,5 @@
+import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
+
 import { Fulfillment, Order, Payment } from './order-types.js';
 import { Fulfillment, Order, Payment } from './order-types.js';
 
 
 /**
 /**
@@ -76,3 +78,9 @@ export function canAddFulfillment(order: Order): boolean {
         isFulfillableState
         isFulfillableState
     );
     );
 }
 }
+
+export function getSeller<T>(order: { channels: Array<{ code: string; seller: T }> }) {
+    // Find the seller channel (non-default channel)
+    const sellerChannel = order.channels.find(channel => channel.code !== DEFAULT_CHANNEL_CODE);
+    return sellerChannel?.seller;
+}

+ 6 - 4
packages/dashboard/src/lib/components/shared/detail-page-button.tsx

@@ -13,7 +13,7 @@ import { Button } from '../ui/button.js';
  * ```tsx
  * ```tsx
  * // Basic usage with ID (relative navigation)
  * // Basic usage with ID (relative navigation)
  * <DetailPageButton id="123" label="Product Name" />
  * <DetailPageButton id="123" label="Product Name" />
- * 
+ *
  *
  *
  * @example
  * @example
  * ```tsx
  * ```tsx
@@ -36,18 +36,20 @@ export function DetailPageButton({
     label,
     label,
     disabled,
     disabled,
     search,
     search,
-}: {
+    className,
+}: Readonly<{
     label: string | React.ReactNode;
     label: string | React.ReactNode;
     id?: string;
     id?: string;
     href?: string;
     href?: string;
     disabled?: boolean;
     disabled?: boolean;
     search?: Record<string, string>;
     search?: Record<string, string>;
-}) {
+    className?: string;
+}>) {
     if (!id && !href) {
     if (!id && !href) {
         return <span>{label}</span>;
         return <span>{label}</span>;
     }
     }
     return (
     return (
-        <Button asChild variant="ghost" disabled={disabled}>
+        <Button asChild variant="ghost" disabled={disabled} className={className}>
             <Link to={href ?? `./${id}`} search={search ?? {}} preload={false}>
             <Link to={href ?? `./${id}`} search={search ?? {}} preload={false}>
                 {label}
                 {label}
                 {!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}
                 {!disabled && <ChevronRight className="h-3 w-3 text-muted-foreground" />}