Selaa lähdekoodia

feat(dashboard): Order detail first iteration

Michael Bromley 10 kuukautta sitten
vanhempi
sitoutus
af3fe68070
24 muutettua tiedostoa jossa 1450 lisäystä ja 43 poistoa
  1. 0 19
      packages/dashboard/src/components/data-display/asset.tsx
  2. 56 0
      packages/dashboard/src/components/shared/order-addresses.tsx
  3. 4 3
      packages/dashboard/src/components/shared/vendure-image.tsx
  4. 2 2
      packages/dashboard/src/framework/component-registry/component-registry.tsx
  5. 1 1
      packages/dashboard/src/framework/form-engine/use-generated-form.tsx
  6. 4 3
      packages/dashboard/src/framework/layout-engine/page-layout.tsx
  7. 14 10
      packages/dashboard/src/framework/page/use-detail-page.ts
  8. 2 2
      packages/dashboard/src/hooks/use-local-format.ts
  9. 27 0
      packages/dashboard/src/routeTree.gen.ts
  10. 58 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-address.tsx
  11. 7 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-history.tsx
  12. 3 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-history/index.ts
  13. 47 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-history/note-editor.tsx
  14. 64 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-history/order-history-container.tsx
  15. 292 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-history/order-history.tsx
  16. 136 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-history/use-order-history.ts
  17. 169 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-table.tsx
  18. 39 0
      packages/dashboard/src/routes/_authenticated/_orders/components/order-tax-summary.tsx
  19. 61 0
      packages/dashboard/src/routes/_authenticated/_orders/components/payment-details.tsx
  20. 292 0
      packages/dashboard/src/routes/_authenticated/_orders/orders.graphql.ts
  21. 1 1
      packages/dashboard/src/routes/_authenticated/_orders/orders.tsx
  22. 142 0
      packages/dashboard/src/routes/_authenticated/_orders/orders_.$id.tsx
  23. 1 1
      packages/dashboard/src/routes/_authenticated/_products/products.tsx
  24. 28 1
      packages/dev-server/dev-config.ts

+ 0 - 19
packages/dashboard/src/components/data-display/asset.tsx

@@ -1,19 +0,0 @@
-import { Image } from "lucide-react";
-
-export interface AssetLike {
-    preview: string;
-    name?: string;
-    focalPoint?: { x: number; y: number };
-}
-
-export function AssetThumbnail({ value }: { value?: AssetLike }) {
-    if (!value) {
-        // Placeholder for missing asset
-        return <div className="w-[50px] h-[50px] bg-muted rounded-sm flex items-center justify-center"><Image className="w-8 h-8 text-muted-foreground" /></div>;
-    }
-    let url = value.preview + '?preset=tiny';
-    if (value.focalPoint) {
-        url += `&fpx=${value.focalPoint.x}&fpy=${value.focalPoint.y}`;
-    }
-    return <img src={url} alt={value.name} className="rounded-sm" />;
-}

+ 56 - 0
packages/dashboard/src/components/shared/order-addresses.tsx

@@ -0,0 +1,56 @@
+import { FC } from 'react';
+import { AddressDisplay } from './address-display';
+import type { OrderAddress } from '@/graphql/types';
+
+interface OrderAddressesProps {
+  shippingAddress?: OrderAddress | null;
+  billingAddress?: OrderAddress | null;
+  customerName?: string;
+  showBothAddresses?: boolean;
+}
+
+export const OrderAddresses: FC<OrderAddressesProps> = ({
+  shippingAddress,
+  billingAddress,
+  customerName,
+  showBothAddresses = true,
+}) => {
+  const title = customerName ? `${customerName}'s Address` : undefined;
+
+  return (
+    <AddressDisplay
+      shippingAddress={shippingAddress || undefined}
+      billingAddress={billingAddress || undefined}
+      showBothAddresses={showBothAddresses}
+      title={title}
+    />
+  );
+};
+
+// Example usage with the GraphQL fragment
+/*
+import { orderDetailDocument } from '@/routes/_authenticated/_orders/orders.graphql';
+
+const OrderDetails: FC<{ orderId: string }> = ({ orderId }) => {
+  const { data } = useSuspenseQuery(orderDetailDocument, { variables: { id: orderId } });
+  
+  return (
+    <div>
+      <h2 className="text-xl font-semibold mb-4">Order {data.order.code}</h2>
+      
+      <div className="my-6">
+        <h3 className="text-lg font-medium mb-3">Addresses</h3>
+        <OrderAddresses
+          shippingAddress={data.order.shippingAddress}
+          billingAddress={data.order.billingAddress}
+          customerName={`${data.order.customer.firstName} ${data.order.customer.lastName}`}
+        />
+      </div>
+      
+      {/* Other order details *//*}
+    </div>
+  );
+};
+*/
+
+export default OrderAddresses; 

+ 4 - 3
packages/dashboard/src/components/shared/vendure-image.tsx

@@ -1,6 +1,7 @@
+import { cn } from '@/lib/utils.js';
 import React from 'react';
 
-export interface Asset {
+export interface AssetLike {
     id: string;
     preview: string; // Base URL of the asset
     name?: string | null;
@@ -12,7 +13,7 @@ export type ImageFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'avif' | null;
 export type ImageMode = 'crop' | 'resize' | null;
 
 export interface VendureImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
-    asset: Asset | null | undefined;
+    asset: AssetLike | null | undefined;
     preset?: ImagePreset;
     mode?: ImageMode;
     width?: number;
@@ -77,7 +78,7 @@ export function VendureImage({
         <img
             src={url.toString()}
             alt={alt || asset.name || ''}
-            className={className}
+            className={cn(className, 'rounded-sm')}
             style={style}
             loading="lazy"
             ref={ref}

+ 2 - 2
packages/dashboard/src/framework/component-registry/component-registry.tsx

@@ -1,10 +1,10 @@
-import { AssetThumbnail } from '@/components/data-display/asset.js';
 import { BooleanDisplayCheckbox } from '@/components/data-display/boolean.js';
 import { DateTime } from '@/components/data-display/date-time.js';
 import { Money } from '@/components/data-display/money.js';
 import { DateTimeInput } from '@/components/data-input/datetime-input.js';
 import { FacetValueInput } from '@/components/data-input/facet-value-input.js';
 import { MoneyInput } from '@/components/data-input/money-input.js';
+import { AssetLike, VendureImage } from '@/components/shared/vendure-image.js';
 import { Checkbox } from '@/components/ui/checkbox.js';
 import { Input } from '@/components/ui/input.js';
 import * as React from 'react';
@@ -27,7 +27,7 @@ export const COMPONENT_REGISTRY: ComponentRegistry = {
     dataDisplay: {
         'vendure:boolean': BooleanDisplayCheckbox,
         'vendure:dateTime': DateTime,
-        'vendure:asset': AssetThumbnail,
+        'vendure:asset': ({value}) => <VendureImage asset={value} preset="tiny" />,
         'vendure:money': Money,
     },
     dataInput: {

+ 1 - 1
packages/dashboard/src/framework/form-engine/use-generated-form.tsx

@@ -37,7 +37,7 @@ export function useGeneratedForm<
     const { document, entity, setValues, onSubmit } = options;
     const { activeChannel } = useChannel();
     const availableLanguages = useServerConfig()?.availableLanguages || [];
-    const updateFields = getOperationVariablesFields(document);
+    const updateFields = document ? getOperationVariablesFields(document) : [];
     const schema = createFormSchemaFromFields(updateFields);
     const defaultValues = getDefaultValuesFromFields(updateFields, activeChannel?.defaultLanguageCode);
     const processedEntity = ensureTranslationsForAllLanguages(entity, availableLanguages);

+ 4 - 3
packages/dashboard/src/framework/layout-engine/page-layout.tsx

@@ -12,6 +12,7 @@ export type PageBlockProps = {
     title?: React.ReactNode | string;
     description?: React.ReactNode | string;
     className?: string;
+    borderless?: boolean;
 };
 
 export type PageLayoutProps = {
@@ -55,16 +56,16 @@ export function PageActionBar({ children }: { children: React.ReactNode }) {
     return <div className="flex justify-between">{children}</div>;
 }
 
-export function PageBlock({ children, title, description }: PageBlockProps) {
+export function PageBlock({ children, title, description, borderless }: PageBlockProps) {
     return (
-        <Card className="w-full">
+        <Card className={cn('w-full')}>
             {title || description ? (
                 <CardHeader>
                     {title && <CardTitle>{title}</CardTitle>}
                     {description && <CardDescription>{description}</CardDescription>}
                 </CardHeader>
             ) : null}
-            <CardContent className={!title ? 'pt-6' : ''}>{children}</CardContent>
+            <CardContent className={cn(!title ? 'pt-6' : '', borderless && 'p-0')}>{children}</CardContent>
         </Card>
     );
 }

+ 14 - 10
packages/dashboard/src/framework/page/use-detail-page.ts

@@ -44,12 +44,12 @@ export interface DetailPageOptions<
      * @description
      * The document to create the entity.
      */
-    createDocument: C;
+    createDocument?: C;
     /**
      * @description
      * The document to update the entity.
      */
-    updateDocument: U;
+    updateDocument?: U;
     /**
      * @description
      * The function to set the values for the update document.
@@ -112,25 +112,29 @@ export function useDetailPage<
     const entity = detailQuery?.data[entityField];
 
     const createMutation = useMutation({
-        mutationFn: api.mutate(createDocument),
+        mutationFn: createDocument ? api.mutate(createDocument) : undefined,
         onSuccess: data => {
-            const createMutationName = getMutationName(createDocument);
-            onSuccess?.((data as any)[createMutationName]);
+            if (createDocument) {
+                const createMutationName = getMutationName(createDocument);
+                onSuccess?.((data as any)[createMutationName]);
+            }
         },
     });
 
     const updateMutation = useMutation({
-        mutationFn: api.mutate(updateDocument),
+        mutationFn: updateDocument ? api.mutate(updateDocument) : undefined,
         onSuccess: data => {
-            const updateMutationName = getMutationName(updateDocument);
-            onSuccess?.((data as any)[updateMutationName]);
-            void queryClient.invalidateQueries({ queryKey: detailQueryOptions.queryKey });
+            if (updateDocument) {
+                const updateMutationName = getMutationName(updateDocument);
+                onSuccess?.((data as any)[updateMutationName]);
+                void queryClient.invalidateQueries({ queryKey: detailQueryOptions.queryKey });
+            }
         },
         onError,
     });
 
     const { form, submitHandler } = useGeneratedForm({
-        document: isNew ? createDocument : updateDocument,
+        document: isNew ? (createDocument ?? updateDocument) : updateDocument,
         entity,
         setValues: setValuesForUpdate,
         onSubmit(values: any) {

+ 2 - 2
packages/dashboard/src/hooks/use-local-format.ts

@@ -55,8 +55,8 @@ export function useLocalFormat() {
         return i18n.number(value);
     };
 
-    const formatDate = (value: string | Date) => {
-        return i18n.date(value);
+    const formatDate = (value: string | Date, options?: Intl.DateTimeFormatOptions) => {
+        return i18n.date(value, options);
     };
 
     const formatLanguageName = (value: string): string => {

+ 27 - 0
packages/dashboard/src/routeTree.gen.ts

@@ -26,6 +26,7 @@ import { Route as AuthenticatedCollectionsCollectionsImport } from './routes/_au
 import { Route as AuthenticatedAssetsAssetsImport } from './routes/_authenticated/_assets/assets';
 import { Route as AuthenticatedProductsProductsIdImport } from './routes/_authenticated/_products/products_.$id';
 import { Route as AuthenticatedProductVariantsProductVariantsIdImport } from './routes/_authenticated/_product-variants/product-variants_.$id';
+import { Route as AuthenticatedOrdersOrdersIdImport } from './routes/_authenticated/_orders/orders_.$id';
 import { Route as AuthenticatedFacetsFacetsIdImport } from './routes/_authenticated/_facets/facets_.$id';
 import { Route as AuthenticatedCollectionsCollectionsIdImport } from './routes/_authenticated/_collections/collections_.$id';
 
@@ -122,6 +123,12 @@ const AuthenticatedProductVariantsProductVariantsIdRoute =
         getParentRoute: () => AuthenticatedRoute,
     } as any);
 
+const AuthenticatedOrdersOrdersIdRoute = AuthenticatedOrdersOrdersIdImport.update({
+    id: '/_orders/orders_/$id',
+    path: '/orders/$id',
+    getParentRoute: () => AuthenticatedRoute,
+} as any);
+
 const AuthenticatedFacetsFacetsIdRoute = AuthenticatedFacetsFacetsIdImport.update({
     id: '/_facets/facets_/$id',
     path: '/facets/$id',
@@ -243,6 +250,13 @@ declare module '@tanstack/react-router' {
             preLoaderRoute: typeof AuthenticatedFacetsFacetsIdImport;
             parentRoute: typeof AuthenticatedImport;
         };
+        '/_authenticated/_orders/orders_/$id': {
+            id: '/_authenticated/_orders/orders_/$id';
+            path: '/orders/$id';
+            fullPath: '/orders/$id';
+            preLoaderRoute: typeof AuthenticatedOrdersOrdersIdImport;
+            parentRoute: typeof AuthenticatedImport;
+        };
         '/_authenticated/_product-variants/product-variants_/$id': {
             id: '/_authenticated/_product-variants/product-variants_/$id';
             path: '/product-variants/$id';
@@ -275,6 +289,7 @@ interface AuthenticatedRouteChildren {
     AuthenticatedSystemJobQueueRoute: typeof AuthenticatedSystemJobQueueRoute;
     AuthenticatedCollectionsCollectionsIdRoute: typeof AuthenticatedCollectionsCollectionsIdRoute;
     AuthenticatedFacetsFacetsIdRoute: typeof AuthenticatedFacetsFacetsIdRoute;
+    AuthenticatedOrdersOrdersIdRoute: typeof AuthenticatedOrdersOrdersIdRoute;
     AuthenticatedProductVariantsProductVariantsIdRoute: typeof AuthenticatedProductVariantsProductVariantsIdRoute;
     AuthenticatedProductsProductsIdRoute: typeof AuthenticatedProductsProductsIdRoute;
 }
@@ -292,6 +307,7 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
     AuthenticatedSystemJobQueueRoute: AuthenticatedSystemJobQueueRoute,
     AuthenticatedCollectionsCollectionsIdRoute: AuthenticatedCollectionsCollectionsIdRoute,
     AuthenticatedFacetsFacetsIdRoute: AuthenticatedFacetsFacetsIdRoute,
+    AuthenticatedOrdersOrdersIdRoute: AuthenticatedOrdersOrdersIdRoute,
     AuthenticatedProductVariantsProductVariantsIdRoute: AuthenticatedProductVariantsProductVariantsIdRoute,
     AuthenticatedProductsProductsIdRoute: AuthenticatedProductsProductsIdRoute,
 };
@@ -314,6 +330,7 @@ export interface FileRoutesByFullPath {
     '/job-queue': typeof AuthenticatedSystemJobQueueRoute;
     '/collections/$id': typeof AuthenticatedCollectionsCollectionsIdRoute;
     '/facets/$id': typeof AuthenticatedFacetsFacetsIdRoute;
+    '/orders/$id': typeof AuthenticatedOrdersOrdersIdRoute;
     '/product-variants/$id': typeof AuthenticatedProductVariantsProductVariantsIdRoute;
     '/products/$id': typeof AuthenticatedProductsProductsIdRoute;
 }
@@ -333,6 +350,7 @@ export interface FileRoutesByTo {
     '/job-queue': typeof AuthenticatedSystemJobQueueRoute;
     '/collections/$id': typeof AuthenticatedCollectionsCollectionsIdRoute;
     '/facets/$id': typeof AuthenticatedFacetsFacetsIdRoute;
+    '/orders/$id': typeof AuthenticatedOrdersOrdersIdRoute;
     '/product-variants/$id': typeof AuthenticatedProductVariantsProductVariantsIdRoute;
     '/products/$id': typeof AuthenticatedProductsProductsIdRoute;
 }
@@ -354,6 +372,7 @@ export interface FileRoutesById {
     '/_authenticated/_system/job-queue': typeof AuthenticatedSystemJobQueueRoute;
     '/_authenticated/_collections/collections_/$id': typeof AuthenticatedCollectionsCollectionsIdRoute;
     '/_authenticated/_facets/facets_/$id': typeof AuthenticatedFacetsFacetsIdRoute;
+    '/_authenticated/_orders/orders_/$id': typeof AuthenticatedOrdersOrdersIdRoute;
     '/_authenticated/_product-variants/product-variants_/$id': typeof AuthenticatedProductVariantsProductVariantsIdRoute;
     '/_authenticated/_products/products_/$id': typeof AuthenticatedProductsProductsIdRoute;
 }
@@ -376,6 +395,7 @@ export interface FileRouteTypes {
         | '/job-queue'
         | '/collections/$id'
         | '/facets/$id'
+        | '/orders/$id'
         | '/product-variants/$id'
         | '/products/$id';
     fileRoutesByTo: FileRoutesByTo;
@@ -394,6 +414,7 @@ export interface FileRouteTypes {
         | '/job-queue'
         | '/collections/$id'
         | '/facets/$id'
+        | '/orders/$id'
         | '/product-variants/$id'
         | '/products/$id';
     id:
@@ -413,6 +434,7 @@ export interface FileRouteTypes {
         | '/_authenticated/_system/job-queue'
         | '/_authenticated/_collections/collections_/$id'
         | '/_authenticated/_facets/facets_/$id'
+        | '/_authenticated/_orders/orders_/$id'
         | '/_authenticated/_product-variants/product-variants_/$id'
         | '/_authenticated/_products/products_/$id';
     fileRoutesById: FileRoutesById;
@@ -458,6 +480,7 @@ export const routeTree = rootRoute._addFileChildren(rootRouteChildren)._addFileT
         "/_authenticated/_system/job-queue",
         "/_authenticated/_collections/collections_/$id",
         "/_authenticated/_facets/facets_/$id",
+        "/_authenticated/_orders/orders_/$id",
         "/_authenticated/_product-variants/product-variants_/$id",
         "/_authenticated/_products/products_/$id"
       ]
@@ -516,6 +539,10 @@ export const routeTree = rootRoute._addFileChildren(rootRouteChildren)._addFileT
       "filePath": "_authenticated/_facets/facets_.$id.tsx",
       "parent": "/_authenticated"
     },
+    "/_authenticated/_orders/orders_/$id": {
+      "filePath": "_authenticated/_orders/orders_.$id.tsx",
+      "parent": "/_authenticated"
+    },
     "/_authenticated/_product-variants/product-variants_/$id": {
       "filePath": "_authenticated/_product-variants/product-variants_.$id.tsx",
       "parent": "/_authenticated"

+ 58 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-address.tsx

@@ -0,0 +1,58 @@
+import { ResultOf } from 'gql.tada';
+import { orderAddressFragment } from '../orders.graphql.js';
+import { Phone } from 'lucide-react';
+import { Separator } from '@/components/ui/separator.js';
+import { Globe } from 'lucide-react';
+
+type OrderAddress = ResultOf<typeof orderAddressFragment>;
+
+export function OrderAddress({ address }: { address?: OrderAddress }) {
+    if (!address) {
+        return null;
+    }
+
+    const {
+        fullName,
+        company,
+        streetLine1,
+        streetLine2,
+        city,
+        province,
+        postalCode,
+        country,
+        countryCode,
+        phoneNumber,
+    } = address;
+
+    return (
+        <div className="space-y-2">
+            {fullName && <p className="font-medium">{fullName}</p>}
+            {company && <p className="text-sm text-muted-foreground">{company}</p>}
+
+            <div className="text-sm">
+                {streetLine1 && <p>{streetLine1}</p>}
+                {streetLine2 && <p>{streetLine2}</p>}
+                <p>{[city, province, postalCode].filter(Boolean).join(', ')}</p>
+                {country && (
+                    <div className="flex items-center gap-1.5 mt-1">
+                        <Globe className="h-3 w-3 text-muted-foreground" />
+                        <span>{country}</span>
+                        {countryCode && (
+                            <span className="text-xs text-muted-foreground">({countryCode})</span>
+                        )}
+                    </div>
+                )}
+            </div>
+
+            {phoneNumber && (
+                <>
+                    <Separator className="my-2" />
+                    <div className="flex items-center gap-1.5">
+                        <Phone className="h-3 w-3 text-muted-foreground" />
+                        <span className="text-sm">{phoneNumber}</span>
+                    </div>
+                </>
+            )}
+        </div>
+    );
+}

+ 7 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-history.tsx

@@ -0,0 +1,7 @@
+export function OrderHistory() {
+    return <div>
+        <Table>
+            
+        </Table>
+    </div>
+}

+ 3 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-history/index.ts

@@ -0,0 +1,3 @@
+export * from './order-history.js';
+export * from './order-history-container.js';
+export * from './use-order-history.js';

+ 47 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-history/note-editor.tsx

@@ -0,0 +1,47 @@
+import { Button } from '@/components/ui/button.js';
+import {
+    Dialog,
+    DialogContent,
+    DialogDescription,
+    DialogFooter,
+    DialogHeader,
+    DialogTitle,
+} from '@/components/ui/dialog.js';
+import { Textarea } from '@/components/ui/textarea.js';
+import { Trans } from '@lingui/react/macro';
+import { useState } from 'react';
+
+interface NoteEditorProps {
+    note: string;
+    open: boolean;
+    onOpenChange: (open: boolean) => void;
+    onNoteChange: (noteId: string, note: string, isPrivate: boolean) => void;
+    noteId: string;
+    isPrivate: boolean;
+}
+
+export function NoteEditor({ open, onOpenChange, note, onNoteChange, noteId, isPrivate }: NoteEditorProps) {
+    const [value, setValue] = useState(note);
+    const handleSave = () => {
+        onNoteChange(noteId, value, isPrivate);
+        onOpenChange(false);
+    };
+
+    return (
+        <Dialog open={open} onOpenChange={onOpenChange}>
+            <DialogContent>
+                <DialogHeader>
+                    <DialogTitle>
+                        <Trans>Edit Note</Trans>
+                    </DialogTitle>
+                </DialogHeader>
+                <Textarea value={value} onChange={e => setValue(e.target.value)} />
+                <DialogFooter>
+                    <Button onClick={() => handleSave()}>
+                        <Trans>Save</Trans>
+                    </Button>
+                </DialogFooter>
+            </DialogContent>
+        </Dialog>
+    );
+}

+ 64 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-history/order-history-container.tsx

@@ -0,0 +1,64 @@
+import React from 'react';
+import { OrderHistory } from './order-history.js';
+import { useOrderHistory } from './use-order-history.js';
+import { Skeleton } from '@/components/ui/skeleton.js';
+import { Alert, AlertDescription } from '@/components/ui/alert.js';
+import { TriangleAlert } from 'lucide-react';
+
+interface OrderHistoryContainerProps {
+  orderId: string;
+}
+
+export function OrderHistoryContainer({ orderId }: OrderHistoryContainerProps) {
+  const { 
+    historyEntries, 
+    order, 
+    loading, 
+    error, 
+    addNote, 
+    updateNote, 
+    deleteNote,
+  } = useOrderHistory(orderId);
+
+  if (loading && !order) {
+    return (
+      <div className="space-y-4">
+        <h2 className="text-xl font-semibold">Order history</h2>
+        <div className="space-y-2">
+          <Skeleton className="h-20 w-full" />
+          <Skeleton className="h-24 w-full" />
+          <Skeleton className="h-24 w-full" />
+        </div>
+      </div>
+    );
+  }
+
+  if (error) {
+    return (
+      <Alert variant="destructive">
+        <TriangleAlert className="h-4 w-4" />
+        <AlertDescription>
+          Error loading order history: {error.message}
+        </AlertDescription>
+      </Alert>
+    );
+  }
+
+  if (!order) {
+    return (
+      <Alert>
+        <AlertDescription>Order not found</AlertDescription>
+      </Alert>
+    );
+  }
+
+  return (
+    <OrderHistory
+      order={order}
+      historyEntries={historyEntries}
+      onAddNote={addNote}
+      onUpdateNote={updateNote}
+      onDeleteNote={deleteNote}
+    />
+  );
+} 

+ 292 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-history/order-history.tsx

@@ -0,0 +1,292 @@
+import { Badge } from '@/components/ui/badge.js';
+import { Button } from '@/components/ui/button.js';
+import { Checkbox } from '@/components/ui/checkbox.js';
+import {
+    DropdownMenu,
+    DropdownMenuContent,
+    DropdownMenuItem,
+    DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu.js';
+import { ScrollArea } from '@/components/ui/scroll-area.js';
+import { Separator } from '@/components/ui/separator.js';
+import { Textarea } from '@/components/ui/textarea.js';
+import { useLocalFormat } from '@/hooks/use-local-format.js';
+import { Trans } from '@lingui/react/macro';
+import {
+    ArrowRightToLine,
+    CheckIcon,
+    CreditCardIcon,
+    MoreVerticalIcon,
+    PencilIcon,
+    SquarePen,
+    TrashIcon,
+} from 'lucide-react';
+import { useState } from 'react';
+import { NoteEditor } from './note-editor.js';
+
+interface OrderHistoryProps {
+    order: {
+        id: string;
+        createdAt: string;
+        currencyCode: string;
+    };
+    historyEntries: Array<{
+        id: string;
+        type: string;
+        createdAt: string;
+        isPublic: boolean;
+        administrator?: {
+            id: string;
+            firstName: string;
+            lastName: string;
+        } | null;
+        data: any;
+    }>;
+    onAddNote: (note: string, isPrivate: boolean) => void;
+    onUpdateNote?: (entryId: string, note: string, isPrivate: boolean) => void;
+    onDeleteNote?: (entryId: string) => void;
+}
+
+// Helper function to get initials from a name
+const getInitials = (firstName: string, lastName: string) => {
+    return `${firstName.charAt(0)}${lastName.charAt(0)}`;
+};
+
+export function OrderHistory({
+    order,
+    historyEntries,
+    onAddNote,
+    onUpdateNote,
+    onDeleteNote,
+}: OrderHistoryProps) {
+    const [note, setNote] = useState('');
+    const [noteIsPrivate, setNoteIsPrivate] = useState(true);
+    const { formatDate } = useLocalFormat();
+    const [noteEditorOpen, setNoteEditorOpen] = useState(false);
+      const [noteEditorNote, setNoteEditorNote] = useState<{ noteId: string; note: string; isPrivate: boolean }>({ noteId: '', note: '', isPrivate: true });
+
+    const getTimelineIcon = (entry: OrderHistoryProps['historyEntries'][0]) => {
+        switch (entry.type) {
+            case 'ORDER_PAYMENT_TRANSITION':
+                return <CreditCardIcon className="h-4 w-4" />;
+            case 'ORDER_NOTE':
+                return <SquarePen className="h-4 w-4" />;
+            case 'ORDER_STATE_TRANSITION':
+                return <ArrowRightToLine className="h-4 w-4" />;
+            default:
+                return <CheckIcon className="h-4 w-4" />;
+        }
+    };
+
+    const getTitle = (entry: OrderHistoryProps['historyEntries'][0]) => {
+        switch (entry.type) {
+            case 'ORDER_PAYMENT_TRANSITION':
+                return <Trans>Payment settled</Trans>;
+            case 'ORDER_NOTE':
+                return <Trans>Note added</Trans>;
+            case 'ORDER_STATE_TRANSITION': {
+                if (entry.data.to === 'Delivered') {
+                    return <Trans>Order fulfilled</Trans>;
+                }
+                if (entry.data.to === 'Cancelled') {
+                    return <Trans>Order cancelled</Trans>;
+                }
+                return <Trans>Order transitioned</Trans>;
+            }
+            default:
+                return <Trans>{entry.type.replace(/_/g, ' ').toLowerCase()}</Trans>;
+        }
+    };
+
+    const handleAddNote = () => {
+        if (note.trim()) {
+            onAddNote(note, noteIsPrivate);
+            setNote('');
+        }
+    };
+
+    const formatDateTime = (date: string) => {
+        return formatDate(date, {
+            year: 'numeric',
+            month: 'long',
+            day: 'numeric',
+            hour: 'numeric',
+            minute: 'numeric',
+            second: 'numeric',
+        });
+    };
+
+    const onEditNote = (noteId: string, note: string, isPrivate: boolean) => {
+        setNoteEditorNote({ noteId, note, isPrivate });
+        setNoteEditorOpen(true);
+    };
+
+    const onEditNoteSave = (noteId: string, note: string, isPrivate: boolean) => {
+        onUpdateNote?.(noteId, note, isPrivate);
+        setNoteEditorOpen(false);
+    };
+
+    return (
+        <div className="space-y-4">
+            <div className="space-y-4">
+                {/* Add Note Section */}
+                <div className="border rounded-md p-4 bg-gray-50">
+                    <div className="flex flex-col space-y-4">
+                        <Textarea
+                            placeholder="Add a note..."
+                            value={note}
+                            onChange={e => setNote(e.target.value)}
+                            className="min-h-[80px] resize-none"
+                        />
+                        <div className="flex items-center justify-between">
+                            <div className="flex items-center space-x-2">
+                                <Checkbox
+                                    id="note-private"
+                                    checked={noteIsPrivate}
+                                    onCheckedChange={checked => setNoteIsPrivate(checked as boolean)}
+                                />
+                                <label
+                                    htmlFor="note-private"
+                                    className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+                                >
+                                    Note is private
+                                </label>
+                                <span
+                                    className={
+                                        noteIsPrivate ? 'text-gray-500 text-xs' : 'text-green-600 text-xs'
+                                    }
+                                >
+                                    {noteIsPrivate ? 'Visible to admins only' : 'Visible to customer'}
+                                </span>
+                            </div>
+                            <Button onClick={handleAddNote} disabled={!note.trim()} size="sm">
+                                Add note
+                            </Button>
+                        </div>
+                    </div>
+                </div>
+
+                {/* Timeline */}
+                <ScrollArea className=" pr-4">
+                    <div className="relative">
+                        <div className="absolute left-5 top-0 bottom-[44px] w-0.5 bg-gray-200" />
+
+                        {/* History entries */}
+                        {historyEntries.map((entry, index) => (
+                            <div key={entry.id} className="relative mb-4 pl-11">
+                                <div className="absolute left-0 w-10 flex items-center justify-center">
+                                    <div className={`rounded-full flex items-center justify-center h-6 w-6`}>
+                                        <div
+                                            className={`rounded-full bg-gray-200 text-muted-foreground flex items-center justify-center h-6 w-6`}
+                                        >
+                                            {getTimelineIcon(entry)}
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div className="bg-white px-4 rounded-md">
+                                    <div className="mt-2 text-sm text-gray-500 flex items-center">
+                                        <span>{formatDateTime(entry.createdAt)}</span>
+                                        {entry.administrator && (
+                                            <span className="ml-2">
+                                                {entry.administrator.firstName} {entry.administrator.lastName}
+                                            </span>
+                                        )}
+                                    </div>
+                                    <div className="flex items-start justify-between">
+                                        <div>
+                                            <div className="font-medium text-sm">{getTitle(entry)}</div>
+
+                                            {entry.type === 'ORDER_NOTE' && (
+                                                <div className="flex items-center space-x-2">
+                                                    <Badge
+                                                        variant={entry.isPublic ? 'outline' : 'secondary'}
+                                                        className="text-xs"
+                                                    >
+                                                        {entry.isPublic ? 'Public' : 'Private'}
+                                                    </Badge>
+                                                    <span>{entry.data.note}</span>
+                                                </div>
+                                            )}
+                                            <div className="text-sm text-muted-foreground">
+                                                {entry.type === 'ORDER_STATE_TRANSITION' && (
+                                                    <Trans>
+                                                        From {entry.data.from} to {entry.data.to}
+                                                    </Trans>
+                                                )}
+                                                {entry.type === 'ORDER_PAYMENT_TRANSITION' && (
+                                                    <Trans>
+                                                        Payment #{entry.data.paymentId} transitioned to{' '}
+                                                        {entry.data.to}
+                                                    </Trans>
+                                                )}
+                                            </div>
+                                        </div>
+
+                                        {entry.type === 'ORDER_NOTE' && (
+                                            <DropdownMenu>
+                                                <DropdownMenuTrigger asChild>
+                                                    <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
+                                                        <MoreVerticalIcon className="h-4 w-4" />
+                                                    </Button>
+                                                </DropdownMenuTrigger>
+                                                <DropdownMenuContent align="end">
+                                                    <DropdownMenuItem
+                                                        onClick={() =>
+                                                            onEditNote?.(
+                                                                entry.id,
+                                                                entry.data.note,
+                                                                !entry.isPublic,
+                                                            )
+                                                        }
+                                                        className="cursor-pointer"
+                                                    >
+                                                        <PencilIcon className="mr-2 h-4 w-4" />
+                                                        <Trans>Edit</Trans>
+                                                    </DropdownMenuItem>
+                                                    <Separator className="my-1" />
+                                                    <DropdownMenuItem
+                                                        onClick={() => onDeleteNote?.(entry.id)}
+                                                        className="cursor-pointer text-red-600 focus:text-red-600"
+                                                    >
+                                                        <TrashIcon className="mr-2 h-4 w-4" />
+                                                        <span>Delete</span>
+                                                    </DropdownMenuItem>
+                                                </DropdownMenuContent>
+                                            </DropdownMenu>
+                                        )}
+                                    </div>
+                                </div>
+                                <div className="border-b border-muted my-4 mx-4"></div>
+                            </div>
+                        ))}
+
+                        {/* Order created entry - always shown last */}
+                        <div className="relative mb-4 pl-11">
+                            <div className="absolute left-0 w-10 flex items-center justify-center">
+                                <div className="h-6 w-6 rounded-full flex items-center justify-center bg-green-100">
+                                    <CheckIcon className="h-4 w-4" />
+                                </div>
+                            </div>
+                            <div className="bg-white px-4 rounded-md">
+                                <div className="mt-2 text-sm text-gray-500">
+                                    {formatDateTime(order.createdAt)}
+                                </div>
+                                <div className="font-medium">Order created</div>
+                            </div>
+                        </div>
+                    </div>
+                </ScrollArea>
+                <NoteEditor
+                    key={noteEditorNote.noteId}
+                    note={noteEditorNote.note}
+                    onNoteChange={onEditNoteSave}
+                    open={noteEditorOpen}
+                    onOpenChange={setNoteEditorOpen}
+                    noteId={noteEditorNote.noteId}
+                    isPrivate={noteEditorNote.isPrivate}
+                />
+            </div>
+        </div>
+    );
+}

+ 136 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-history/use-order-history.ts

@@ -0,0 +1,136 @@
+import { api } from '@/graphql/api.js';
+import { graphql } from '@/graphql/graphql.js';
+import { useLingui } from '@lingui/react/macro';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { useState } from 'react';
+import { toast } from 'sonner';
+
+import { orderHistoryDocument } from '../../orders.graphql.js';
+
+// Simplified mutation definitions - adjust based on your actual schema
+const addOrderNoteDocument = graphql(`
+    mutation AddOrderNote($orderId: ID!, $note: String!, $isPublic: Boolean!) {
+        addNoteToOrder(input: { id: $orderId, note: $note, isPublic: $isPublic }) {
+            id
+            history(options: { take: 1, sort: { createdAt: DESC } }) {
+                items {
+                    id
+                    createdAt
+                    type
+                    isPublic
+                    data
+                }
+            }
+        }
+    }
+`);
+
+const updateOrderNoteDocument = graphql(`
+    mutation UpdateOrderNote($noteId: ID!, $note: String!, $isPublic: Boolean!) {
+        updateOrderNote(input: { noteId: $noteId, note: $note, isPublic: $isPublic }) {
+            id
+        }
+    }
+`);
+
+const deleteOrderNoteDocument = graphql(`
+    mutation DeleteOrderNote($noteId: ID!) {
+        deleteOrderNote(id: $noteId) {
+            result
+            message
+        }
+    }
+`);
+
+export function useOrderHistory(orderId: string) {
+    const [isLoading, setIsLoading] = useState(false);
+    const { i18n } = useLingui();
+
+    // Fetch order history
+    const {
+        data,
+        isLoading: isLoadingQuery,
+        error,
+        refetch,
+    } = useQuery({
+        queryFn: () =>
+            api.query(orderHistoryDocument, {
+                id: orderId,
+                options: {
+                    sort: { createdAt: 'DESC' },
+                },
+            }),
+        queryKey: ['OrderHistory', orderId],
+    });
+
+    // Add note mutation
+    const { mutate: addNoteMutation } = useMutation({
+        mutationFn: api.mutate(addOrderNoteDocument),
+        onSuccess: () => {
+            toast.success(i18n.t('Note added successfully'));
+            void refetch();
+        },
+        onError: () => {
+            toast.error(i18n.t('Failed to add note'));
+        },
+    });
+
+    const addNote = (note: string, isPrivate: boolean) => {
+        setIsLoading(true);
+        addNoteMutation({
+            orderId,
+            note,
+            isPublic: !isPrivate, // isPrivate in UI is the opposite of isPublic in API
+        });
+    };
+
+    // Update note mutation
+    const { mutate: updateNoteMutation } = useMutation({
+        mutationFn: api.mutate(updateOrderNoteDocument),
+        onSuccess: () => {
+            toast.success(i18n.t('Note updated successfully'));
+            void refetch();
+        },
+        onError: () => {
+            toast.error(i18n.t('Failed to update note'));
+        },
+    });
+    const updateNote = (noteId: string, note: string, isPrivate: boolean) => {
+        setIsLoading(true);
+
+        updateNoteMutation({
+            noteId,
+            note,
+            isPublic: !isPrivate, // isPrivate in UI is the opposite of isPublic in API
+        });
+    };
+
+    // Delete note mutation
+    const { mutate: deleteNoteMutation } = useMutation({
+        mutationFn: api.mutate(deleteOrderNoteDocument),
+        onSuccess: () => {
+            toast.success(i18n.t('Note deleted successfully'));
+            void refetch();
+        },
+        onError: () => {
+            toast.error(i18n.t('Failed to delete note'));
+        },
+    });
+    const deleteNote = (noteId: string) => {
+        setIsLoading(true);
+        deleteNoteMutation({
+            noteId,
+        });
+    };
+
+    return {
+        historyEntries: data?.order?.history?.items || [],
+        order: data?.order,
+        loading: isLoadingQuery || isLoading,
+        error,
+        addNote,
+        updateNote,
+        deleteNote,
+        refetch,
+    };
+}

+ 169 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-table.tsx

@@ -0,0 +1,169 @@
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.js';
+import { ResultOf } from '@/graphql/graphql.js';
+import {
+    ColumnDef,
+    flexRender,
+    getCoreRowModel,
+    useReactTable,
+    VisibilityState,
+} from '@tanstack/react-table';
+import { useState } from 'react';
+import { orderDetailDocument, orderLineFragment } from '../orders.graphql.js';
+import { VendureImage } from '@/components/shared/vendure-image.js';
+import { Money } from '@/components/data-display/money.js';
+import { Trans } from '@lingui/react/macro';
+
+type OrderFragment = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
+type OrderLineFragment = ResultOf<typeof orderLineFragment>;
+
+export interface OrderTableProps {
+    order: OrderFragment;
+}
+
+export function OrderTable({ order }: OrderTableProps) {
+    const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
+
+    const currencyCode = order.currencyCode;
+
+    const columns: ColumnDef<OrderLineFragment>[] = [
+        {
+            header: 'Image',
+            accessorKey: 'featuredAsset',
+            cell: ({ row }) => {
+                const asset = row.original.featuredAsset;
+                return <VendureImage asset={asset} preset="tiny" />;
+            },
+        },
+        {
+            header: 'Product',
+            accessorKey: 'productVariant.name',
+        },
+        {
+            header: 'SKU',
+            accessorKey: 'productVariant.sku',
+        },
+        {
+            header: 'Unit price',
+            accessorKey: 'unitPriceWithTax',
+            cell: ({ cell, row }) => {
+                const value = cell.getValue();
+                const netValue = row.original.unitPrice;
+                return (
+                    <div className="flex flex-col gap-1">
+                        <div>
+                            <Money value={value} currencyCode={currencyCode} />
+                        </div>
+                        <div className="text-xs text-muted-foreground">
+                            <Money value={netValue} currencyCode={currencyCode} />
+                        </div>
+                    </div>
+                );
+            },
+        },
+        {
+            header: 'Quantity',
+            accessorKey: 'quantity',
+        },
+        {
+            header: 'Total',
+            accessorKey: 'linePriceWithTax',
+            cell: ({ cell, row }) => {
+                const value = cell.getValue();
+                const netValue = row.original.linePrice;
+                return (
+                    <div className="flex flex-col gap-1">
+                        <div>
+                            <Money value={value} currencyCode={currencyCode} />
+                        </div>
+                        <div className="text-xs text-muted-foreground">
+                            <Money value={netValue} currencyCode={currencyCode} />
+                        </div>
+                    </div>
+                );
+            },
+        },
+    ];
+
+    const data = order.lines;
+
+    const table = useReactTable({
+        data,
+        columns,
+        getCoreRowModel: getCoreRowModel(),
+        rowCount: data.length,
+        onColumnVisibilityChange: setColumnVisibility,
+        state: {
+            columnVisibility,
+        },
+    });
+
+    return (
+        <div className="w-full">
+            <div className="">
+                <Table>
+                    <TableHeader>
+                        {table.getHeaderGroups().map(headerGroup => (
+                            <TableRow key={headerGroup.id}>
+                                {headerGroup.headers.map(header => {
+                                    return (
+                                        <TableHead key={header.id}>
+                                            {header.isPlaceholder
+                                                ? null
+                                                : flexRender(
+                                                      header.column.columnDef.header,
+                                                      header.getContext(),
+                                                  )}
+                                        </TableHead>
+                                    );
+                                })}
+                            </TableRow>
+                        ))}
+                    </TableHeader>
+                    <TableBody>
+                        {table.getRowModel().rows?.length ? (
+                            table.getRowModel().rows.map(row => (
+                                <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
+                                    {row.getVisibleCells().map(cell => (
+                                        <TableCell key={cell.id}>
+                                            {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                                        </TableCell>
+                                    ))}
+                                </TableRow>
+                            ))
+                        ) : (
+                            <TableRow>
+                                <TableCell colSpan={columns.length} className="h-24 text-center">
+                                    No results.
+                                </TableCell>
+                            </TableRow>
+                        )}
+                        <TableRow>
+                            <TableCell colSpan={columns.length - 1} className="h-12">
+                                <Trans>Sub total</Trans>
+                            </TableCell>
+                            <TableCell colSpan={1} className="h-12">
+                                <Money value={order.subTotalWithTax} currencyCode={currencyCode} />
+                            </TableCell>
+                        </TableRow>
+                        <TableRow>
+                            <TableCell colSpan={columns.length - 1} className="h-12">
+                                <Trans>Shipping</Trans>
+                            </TableCell>
+                            <TableCell colSpan={1} className="h-12">
+                                <Money value={order.shippingWithTax} currencyCode={currencyCode} />
+                            </TableCell>
+                        </TableRow>
+                        <TableRow>
+                            <TableCell colSpan={columns.length - 1} className="h-12 font-bold">
+                                <Trans>Total</Trans>
+                            </TableCell>
+                            <TableCell colSpan={1} className="h-12 font-bold">
+                                <Money value={order.totalWithTax} currencyCode={currencyCode} />
+                            </TableCell>
+                        </TableRow>
+                    </TableBody>
+                </Table>
+            </div>
+        </div>
+    );
+}

+ 39 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/order-tax-summary.tsx

@@ -0,0 +1,39 @@
+import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table.js";
+import { Trans } from "@lingui/react/macro";
+import { orderDetailFragment } from "../orders.graphql.js";
+import { ResultOf } from "gql.tada";
+import { useLocalFormat } from "@/hooks/use-local-format.js";
+
+export function OrderTaxSummary({ order }: { order: ResultOf<typeof orderDetailFragment> }) {
+    const { formatCurrency } = useLocalFormat();
+    return <div>
+        <Table>
+            <TableHeader>
+                <TableRow>
+                    <TableHead>
+                        <Trans>Description</Trans>
+                    </TableHead>
+                    <TableHead>
+                        <Trans>Tax rate</Trans>
+                    </TableHead>
+                    <TableHead>
+                        <Trans>Tax base</Trans>
+                    </TableHead>
+                    <TableHead>
+                        <Trans>Tax total</Trans>
+                    </TableHead>
+                </TableRow>
+            </TableHeader>
+            <TableBody>
+                {order.taxSummary.map(taxLine => (
+                    <TableRow key={taxLine.description}>
+                        <TableCell>{taxLine.description}</TableCell>
+                        <TableCell>{taxLine.taxRate}%</TableCell>
+                        <TableCell>{formatCurrency(taxLine.taxBase, order.currencyCode)}</TableCell>
+                        <TableCell>{formatCurrency(taxLine.taxTotal, order.currencyCode)}</TableCell>
+                    </TableRow>
+                ))}
+            </TableBody>
+        </Table>
+    </div>
+}

+ 61 - 0
packages/dashboard/src/routes/_authenticated/_orders/components/payment-details.tsx

@@ -0,0 +1,61 @@
+import { ResultOf } from '@/graphql/graphql.js';
+import { useLocalFormat } from '@/hooks/use-local-format.js';
+import { Trans } from '@lingui/react/macro';
+import { paymentWithRefundsFragment } from '../orders.graphql.js';
+
+type PaymentDetailsProps = {
+    payment: ResultOf<typeof paymentWithRefundsFragment>;
+    currencyCode: string;
+};
+
+export function PaymentDetails({ payment, currencyCode }: PaymentDetailsProps) {
+    const { formatCurrency, formatDate } = useLocalFormat();
+    const t = (key: string) => key;
+
+    return (
+        <div className="space-y-2">
+            <LabeledData label={<Trans>Payment method</Trans>} value={payment.method} />
+
+            <LabeledData label={<Trans>Amount</Trans>} value={formatCurrency(payment.amount, currencyCode)} />
+
+            <LabeledData label={<Trans>Created at</Trans>} value={formatDate(payment.createdAt)} />
+
+            {payment.transactionId && (
+                <LabeledData label={<Trans>Transaction ID</Trans>} value={payment.transactionId} />
+            )}
+
+            {/* We need to check if there is errorMessage field in the Payment type */}
+            {payment.errorMessage && (
+                <LabeledData
+                    label={<Trans>Error message</Trans>}
+                    value={payment.errorMessage}
+                    className="text-destructive"
+                />
+            )}
+
+            <LabeledData
+                label={<Trans>Payment metadata</Trans>}
+                value={
+                    <pre className="max-h-96 overflow-auto rounded-md bg-muted p-4 text-sm">
+                        {JSON.stringify(payment.metadata, null, 2)}
+                    </pre>
+                }
+            />
+        </div>
+    );
+}
+
+type LabeledDataProps = {
+    label: string | React.ReactNode;
+    value: React.ReactNode;
+    className?: string;
+};
+
+function LabeledData({ label, value, className }: LabeledDataProps) {
+    return (
+        <div className="">
+            <span className="font-medium text-muted-foreground text-sm">{label}</span>
+            <div className={`col-span-2 ${className}`}>{value}</div>
+        </div>
+    );
+}

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

@@ -1,4 +1,6 @@
+import { assetFragment } from '@/graphql/fragments.js';
 import { graphql } from '@/graphql/graphql.js';
+import { gql } from 'awesome-graphql-client';
 
 export const orderListDocument = graphql(`
     query GetOrders($options: OrderListOptions) {
@@ -30,3 +32,293 @@ export const orderListDocument = graphql(`
         }
     }
 `);
+
+export const discountFragment = graphql(`
+    fragment Discount on Discount {
+        adjustmentSource
+        amount
+        amountWithTax
+        description
+        type
+    }
+`);
+
+export const paymentFragment = graphql(`
+    fragment Payment on Payment {
+        id
+        createdAt
+        transactionId
+        amount
+        method
+        state
+        metadata
+    }
+`);
+
+export const refundFragment = graphql(`
+    fragment Refund on Refund {
+        id
+        state
+        items
+        shipping
+        adjustment
+        transactionId
+        paymentId
+    }
+`);
+
+export const orderAddressFragment = graphql(`
+    fragment OrderAddress on OrderAddress {
+        fullName
+        company
+        streetLine1
+        streetLine2
+        city
+        province
+        postalCode
+        country
+        countryCode
+        phoneNumber
+    }
+`);
+
+export const fulfillmentFragment = graphql(`
+    fragment Fulfillment on Fulfillment {
+        id
+        state
+        nextStates
+        createdAt
+        updatedAt
+        method
+        lines {
+            orderLineId
+            quantity
+        }
+        trackingCode
+    }
+`);
+
+export const paymentWithRefundsFragment = graphql(`
+    fragment PaymentWithRefunds on Payment {
+        id
+        createdAt
+        transactionId
+        amount
+        method
+        state
+        nextStates
+        errorMessage
+        metadata
+        refunds {
+            id
+            createdAt
+            state
+            items
+            adjustment
+            total
+            paymentId
+            reason
+            transactionId
+            method
+            metadata
+            lines {
+                orderLineId
+                quantity
+            }
+        }
+    }
+`);
+
+export const orderLineFragment = graphql(
+    `
+        fragment OrderLine on OrderLine {
+            id
+            createdAt
+            updatedAt
+            featuredAsset {
+                ...Asset
+            }
+            productVariant {
+                id
+                name
+                sku
+                trackInventory
+                stockOnHand
+            }
+            discounts {
+                ...Discount
+            }
+            fulfillmentLines {
+                fulfillmentId
+                quantity
+            }
+            unitPrice
+            unitPriceWithTax
+            proratedUnitPrice
+            proratedUnitPriceWithTax
+            quantity
+            orderPlacedQuantity
+            linePrice
+            lineTax
+            linePriceWithTax
+            discountedLinePrice
+            discountedLinePriceWithTax
+        }
+    `,
+    [assetFragment],
+);
+
+export const orderDetailFragment = graphql(
+    `
+        fragment OrderDetail on Order {
+            id
+            createdAt
+            updatedAt
+            type
+            aggregateOrder {
+                id
+                code
+            }
+            sellerOrders {
+                id
+                code
+                channels {
+                    id
+                    code
+                }
+            }
+            code
+            state
+            nextStates
+            active
+            couponCodes
+            customer {
+                id
+                firstName
+                lastName
+            }
+            lines {
+                ...OrderLine
+            }
+            surcharges {
+                id
+                sku
+                description
+                price
+                priceWithTax
+                taxRate
+            }
+            discounts {
+                ...Discount
+            }
+            promotions {
+                id
+                couponCode
+            }
+            subTotal
+            subTotalWithTax
+            total
+            totalWithTax
+            currencyCode
+            shipping
+            shippingWithTax
+            shippingLines {
+                id
+                discountedPriceWithTax
+                shippingMethod {
+                    id
+                    code
+                    name
+                    fulfillmentHandlerCode
+                    description
+                }
+            }
+            taxSummary {
+                description
+                taxBase
+                taxRate
+                taxTotal
+            }
+            shippingAddress {
+                ...OrderAddress
+            }
+            billingAddress {
+                ...OrderAddress
+            }
+            payments {
+                ...PaymentWithRefunds
+            }
+            fulfillments {
+                ...Fulfillment
+            }
+            modifications {
+                id
+                createdAt
+                isSettled
+                priceChange
+                note
+                payment {
+                    id
+                    amount
+                }
+                lines {
+                    orderLineId
+                    quantity
+                }
+                refund {
+                    id
+                    paymentId
+                    total
+                }
+                surcharges {
+                    id
+                }
+            }
+        }
+    `,
+    [
+        discountFragment,
+        orderAddressFragment,
+        fulfillmentFragment,
+        orderLineFragment,
+        paymentWithRefundsFragment,
+    ],
+);
+
+export const orderDetailDocument = graphql(
+    `
+        query GetOrder($id: ID!) {
+            order(id: $id) {
+                ...OrderDetail
+            }
+        }
+    `,
+    [orderDetailFragment],
+);
+
+export const orderHistoryDocument = graphql(`
+    query GetOrderHistory($id: ID!, $options: HistoryEntryListOptions) {
+        order(id: $id) {
+            id
+            createdAt
+            updatedAt
+            code
+            currencyCode
+            history(options: $options) {
+                totalItems
+                items {
+                    id
+                    type
+                    createdAt
+                    isPublic
+                    administrator {
+                        id
+                        firstName
+                        lastName
+                    }
+                    data
+                }
+            }
+        }
+    }
+`);

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_orders/orders.tsx

@@ -67,7 +67,7 @@ export function OrderListPage() {
                         const id = row.original.id;
                         return (
                             <Button asChild variant="ghost">
-                                <Link to={`/customers/${id}`}>{value}</Link>
+                                <Link to={`/orders/${id}`}>{value}</Link>
                             </Button>
                         );
                     },

+ 142 - 0
packages/dashboard/src/routes/_authenticated/_orders/orders_.$id.tsx

@@ -0,0 +1,142 @@
+import { ErrorPage } from '@/components/shared/error-page.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { Badge } from '@/components/ui/badge.js';
+import { Button } from '@/components/ui/button.js';
+import {
+    Form
+} from '@/components/ui/form.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import {
+    CustomFieldsPageBlock,
+    Page,
+    PageActionBar,
+    PageBlock,
+    PageLayout,
+    PageTitle,
+} from '@/framework/layout-engine/page-layout.js';
+import { getDetailQueryOptions, useDetailPage } from '@/framework/page/use-detail-page.js';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
+import { User } from 'lucide-react';
+import { toast } from 'sonner';
+import { OrderHistoryContainer } from './components/order-history/order-history-container.js';
+import { OrderTable } from './components/order-table.js';
+import { OrderTaxSummary } from './components/order-tax-summary.js';
+import { orderDetailDocument } from './orders.graphql.js';
+import { OrderAddress } from './components/order-address.js';
+import { PaymentDetails } from './components/payment-details.js';
+
+export const Route = createFileRoute('/_authenticated/_orders/orders_/$id')({
+    component: FacetDetailPage,
+    loader: async ({ context, params }) => {
+        const result = 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`);
+        }
+        return {
+            breadcrumb: [{ path: '/orders', label: 'Orders' }, result.order.code],
+        };
+    },
+    errorComponent: ({ error }) => <ErrorPage message={error.message} />,
+});
+
+export function FacetDetailPage() {
+    const params = Route.useParams();
+    const navigate = useNavigate();
+    const { i18n } = useLingui();
+
+    const { form, submitHandler, entity, isPending, refreshEntity } = useDetailPage({
+        queryDocument: addCustomFields(orderDetailDocument),
+        entityField: 'order',
+        // updateDocument: updateOrderDocument,
+        setValuesForUpdate: entity => {
+            return {
+                id: entity.id,
+                customFields: entity.customFields,
+            };
+        },
+        params: { id: params.id },
+        onSuccess: async data => {
+            toast(i18n.t('Successfully updated facet'), {
+                position: 'top-right',
+            });
+            form.reset(form.getValues());
+        },
+        onError: err => {
+            toast(i18n.t('Failed to update facet'), {
+                position: 'top-right',
+                description: err instanceof Error ? err.message : 'Unknown error',
+            });
+        },
+    });
+
+    return (
+        <Page>
+            <PageTitle>{entity?.code ?? ''}</PageTitle>
+            <Form {...form}>
+                <form onSubmit={submitHandler} className="space-y-8">
+                    <PageActionBar>
+                        <div></div>
+                        <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
+                            <Button
+                                type="submit"
+                                disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                            >
+                                <Trans>Update</Trans>
+                            </Button>
+                        </PermissionGuard>
+                    </PageActionBar>
+                    <PageLayout>
+                        <PageBlock column="main">
+                            <OrderTable order={entity} />
+                        </PageBlock>
+                        <PageBlock column="main" title={<Trans>Tax summary</Trans>}>
+                            <OrderTaxSummary order={entity} />
+                        </PageBlock>
+                        <CustomFieldsPageBlock column="main" entityType="Order" control={form.control} />
+                        <PageBlock column="main" title={<Trans>Order history</Trans>}>
+                            <OrderHistoryContainer orderId={entity.id} />
+                        </PageBlock>
+                        <PageBlock column="side" title={<Trans>State</Trans>}>
+                            <Badge variant="outline">{entity?.state}</Badge>
+                        </PageBlock>
+                        <PageBlock column="side" 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" title={<Trans>Payment details</Trans>}>
+                        {entity.payments.map(payment => (
+                            <PaymentDetails key={payment.id} payment={payment} currencyCode={entity.currencyCode} />
+                        ))}
+                        </PageBlock>
+                    </PageLayout>
+                </form>
+            </Form>
+        </Page>
+    );
+}

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_products/products.tsx

@@ -35,7 +35,7 @@ export function ProductListPage() {
                 };
             }}
             listQuery={addCustomFields(productListDocument)}
-            route={Route}
+            route={Route} 
         >
             <PageActionBar>
                 <div></div>

+ 28 - 1
packages/dev-server/dev-config.ts

@@ -64,7 +64,34 @@ export const devConfig: VendureConfig = {
         paymentMethodHandlers: [dummyPaymentHandler],
     },
 
-    customFields: {},
+    customFields: {
+        Product: [
+            {
+                name: 'infoUrl',
+                type: 'string',
+                label: [{ languageCode: LanguageCode.en, value: 'Info URL' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Info URL' }],
+            },
+            {
+                name: 'downloadable',
+                type: 'boolean',
+                label: [{ languageCode: LanguageCode.en, value: 'Downloadable' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Downloadable' }],
+            },
+            {
+                name: 'shortName',
+                type: 'localeString',
+                label: [{ languageCode: LanguageCode.en, value: 'Short Name' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Short Name' }],
+            },
+            {
+                name: 'lastUpdated',
+                type: 'datetime',
+                label: [{ languageCode: LanguageCode.en, value: 'Last Updated' }],
+                description: [{ languageCode: LanguageCode.en, value: 'Last Updated' }],
+            },
+        ],
+    },
     logger: new DefaultLogger({ level: LogLevel.Info }),
     importExportOptions: {
         importAssetsDir: path.join(__dirname, 'import-assets'),