Kaynağa Gözat

feat(dashboard): Customer detail view

Michael Bromley 10 ay önce
ebeveyn
işleme
c9f60b33a3
27 değiştirilmiş dosya ile 1673 ekleme ve 505 silme
  1. 137 93
      package-lock.json
  2. 1 0
      packages/dashboard/package.json
  3. 72 0
      packages/dashboard/src/components/data-input/customer-group-input.tsx
  4. 4 0
      packages/dashboard/src/components/data-table/data-table-pagination.tsx
  5. 58 0
      packages/dashboard/src/components/shared/confirmation-dialog.tsx
  6. 31 0
      packages/dashboard/src/components/shared/customer-group-chip.tsx
  7. 67 0
      packages/dashboard/src/components/shared/customer-group-selector.tsx
  8. 4 1
      packages/dashboard/src/components/shared/paginated-list-data-table.tsx
  9. 155 0
      packages/dashboard/src/components/ui/alert-dialog.tsx
  10. 7 1
      packages/dashboard/src/framework/page/list-page.tsx
  11. 1 5
      packages/dashboard/src/framework/page/use-detail-page.ts
  12. 150 70
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-address-card.tsx
  13. 350 304
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-address-form.tsx
  14. 4 0
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-group-controls.tsx
  15. 78 0
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-history/customer-history-container.tsx
  16. 77 0
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-history/customer-history.tsx
  17. 3 0
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-history/index.ts
  18. 149 0
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-history/use-customer-history.ts
  19. 88 0
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-order-table.tsx
  20. 8 1
      packages/dashboard/src/routes/_authenticated/_customers/components/customer-status-badge.tsx
  21. 76 0
      packages/dashboard/src/routes/_authenticated/_customers/customers.graphql.ts
  22. 2 5
      packages/dashboard/src/routes/_authenticated/_customers/customers.tsx
  23. 103 2
      packages/dashboard/src/routes/_authenticated/_customers/customers_.$id.tsx
  24. 1 1
      packages/dashboard/src/routes/_authenticated/_facets/facets_.$id.tsx
  25. 25 16
      packages/dashboard/src/routes/_authenticated/_orders/components/order-history/order-history-container.tsx
  26. 21 6
      packages/dashboard/src/routes/_authenticated/_orders/components/order-history/use-order-history.ts
  27. 1 0
      packages/dashboard/src/routes/_authenticated/_orders/orders.tsx

Dosya farkı çok büyük olduğundan ihmal edildi
+ 137 - 93
package-lock.json


+ 1 - 0
packages/dashboard/package.json

@@ -29,6 +29,7 @@
     "@hookform/resolvers": "^4.1.3",
     "@lingui/core": "^5.2.0",
     "@lingui/react": "^5.2.0",
+    "@radix-ui/react-alert-dialog": "^1.1.6",
     "@radix-ui/react-avatar": "^1.1.3",
     "@radix-ui/react-checkbox": "^1.1.4",
     "@radix-ui/react-collapsible": "^1.1.3",

+ 72 - 0
packages/dashboard/src/components/data-input/customer-group-input.tsx

@@ -0,0 +1,72 @@
+import { api } from '@/graphql/api.js';
+import { graphql } from '@/graphql/graphql.js';
+import { useQuery } from '@tanstack/react-query';
+import { CustomerGroupChip } from '../shared/customer-group-chip.js';
+import { CustomerGroupSelector } from '../shared/customer-group-selector.js';
+
+const customerGroupsDocument = graphql(`
+    query GetCustomerGroups($options: CustomerGroupListOptions) {
+        customerGroups(options: $options) {
+            items {
+                id
+                name
+            }
+        }
+    }
+`);
+
+export interface CustomerGroup {
+    id: string;
+    name: string;
+}
+
+export interface CustomerGroupInputProps {
+    value: string;
+    onChange: (value: string) => void;
+    readOnly?: boolean;
+}
+
+export function CustomerGroupInput(props: CustomerGroupInputProps) {
+    const ids = decodeIds(props.value);
+    const { data: groups } = useQuery({
+        queryKey: ['customerGroups', ids],
+        queryFn: () =>
+            api.query(customerGroupsDocument, {
+                options: {
+                    filter: {
+                        id: { in: ids },
+                    },
+                },
+            }),
+    });
+
+    const onValueSelectHandler = (value: CustomerGroup) => {
+        const newIds = new Set([...ids, value.id]);
+        props.onChange(JSON.stringify(Array.from(newIds)));
+    };
+
+    const onValueRemoveHandler = (id: string) => {
+        const newIds = new Set(ids.filter(existingId => existingId !== id));
+        props.onChange(JSON.stringify(Array.from(newIds)));
+    };
+
+    return (
+        <div>
+            <div className="flex flex-wrap gap-2 mb-2">
+                {groups?.customerGroups.items.map(group => (
+                    <CustomerGroupChip key={group.id} group={group} onRemove={onValueRemoveHandler} />
+                ))}
+            </div>
+
+            <CustomerGroupSelector onSelect={onValueSelectHandler} readOnly={props.readOnly} />
+        </div>
+    );
+}
+
+function decodeIds(idsString: string): string[] {
+    try {
+        return JSON.parse(idsString);
+    } catch (error) {
+        return [];
+    }
+}

+ 4 - 0
packages/dashboard/src/components/data-table/data-table-pagination.tsx

@@ -42,6 +42,7 @@ export function DataTablePagination<TData>({ table }: DataTablePaginationProps<T
                 <div className="flex items-center space-x-2">
                     <Button
                         variant="outline"
+                        type="button"
                         className="hidden h-8 w-8 p-0 lg:flex"
                         onClick={() => table.setPageIndex(0)}
                         disabled={!table.getCanPreviousPage()}
@@ -51,6 +52,7 @@ export function DataTablePagination<TData>({ table }: DataTablePaginationProps<T
                     </Button>
                     <Button
                         variant="outline"
+                        type="button"
                         className="h-8 w-8 p-0"
                         onClick={() => table.previousPage()}
                         disabled={!table.getCanPreviousPage()}
@@ -60,6 +62,7 @@ export function DataTablePagination<TData>({ table }: DataTablePaginationProps<T
                     </Button>
                     <Button
                         variant="outline"
+                        type="button"
                         className="h-8 w-8 p-0"
                         onClick={() => table.nextPage()}
                         disabled={!table.getCanNextPage()}
@@ -69,6 +72,7 @@ export function DataTablePagination<TData>({ table }: DataTablePaginationProps<T
                     </Button>
                     <Button
                         variant="outline"
+                        type="button"
                         className="hidden h-8 w-8 p-0 lg:flex"
                         onClick={() => table.setPageIndex(table.getPageCount() - 1)}
                         disabled={!table.getCanNextPage()}

+ 58 - 0
packages/dashboard/src/components/shared/confirmation-dialog.tsx

@@ -0,0 +1,58 @@
+import {
+    AlertDialog,
+    AlertDialogAction,
+    AlertDialogCancel,
+    AlertDialogContent,
+    AlertDialogDescription,
+    AlertDialogFooter,
+    AlertDialogHeader,
+    AlertDialogTitle,
+    AlertDialogTrigger,
+} from '@/components/ui/alert-dialog.js';
+import { Trans } from '@lingui/react/macro';
+import { useState } from 'react';
+
+export function ConfirmationDialog({
+    title,
+    description,
+    onConfirm,
+    children,
+    confirmText,
+    cancelText,
+}: {
+    title: string;
+    description: string;
+    onConfirm: () => void;
+    confirmText?: string;
+    cancelText?: string;
+    children: React.ReactNode;
+}) {
+    const [open, setOpen] = useState(false);
+    return (
+        <AlertDialog open={open} onOpenChange={setOpen}>
+            <AlertDialogTrigger asChild onClick={() => setOpen(true)}>
+                {children}
+            </AlertDialogTrigger>
+            <AlertDialogContent>
+                <AlertDialogHeader>
+                    <AlertDialogTitle>{title}</AlertDialogTitle>
+                    <AlertDialogDescription>{description}</AlertDialogDescription>
+                </AlertDialogHeader>
+                <AlertDialogFooter>
+                    <AlertDialogCancel onClick={() => setOpen(false)}>
+                        {cancelText ?? <Trans>Cancel</Trans>}
+                    </AlertDialogCancel>
+                    <AlertDialogAction
+                        type="button"
+                        onClick={() => {
+                            onConfirm();
+                            setOpen(false);
+                        }}
+                    >
+                        {confirmText ?? <Trans>Continue</Trans>}
+                    </AlertDialogAction>
+                </AlertDialogFooter>
+            </AlertDialogContent>
+        </AlertDialog>
+    );
+}

+ 31 - 0
packages/dashboard/src/components/shared/customer-group-chip.tsx

@@ -0,0 +1,31 @@
+import { X } from 'lucide-react';
+import { CustomerGroup } from './customer-group-selector.js';
+import { Badge } from '../ui/badge.js';
+
+export function CustomerGroupChip({
+    group,
+    onRemove,
+}: {
+    group: CustomerGroup;
+    onRemove?: (id: string) => void;
+}) {
+    return (
+        <Badge
+            key={group.id}
+            variant="secondary"
+            className="flex items-center gap-2 py-0.5 pl-2 pr-1 h-6 hover:bg-secondary/80"
+        >
+            {group.name}
+            {onRemove && (
+                <button
+                    type="button"
+                    className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded-full hover:bg-muted/30"
+                    onClick={() => onRemove(group.id)}
+                    aria-label={`Remove ${group.name}`}
+                >
+                    <X className="h-3 w-3" />
+                </button>
+            )}
+        </Badge>
+    );
+}

+ 67 - 0
packages/dashboard/src/components/shared/customer-group-selector.tsx

@@ -0,0 +1,67 @@
+import { Button } from '@/components/ui/button.js';
+import { Command, CommandItem, CommandList } from '@/components/ui/command.js';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover.js';
+import { api } from '@/graphql/api.js';
+import { graphql } from '@/graphql/graphql.js';
+import { Trans } from '@lingui/react/macro';
+import { useQuery } from '@tanstack/react-query';
+import { Plus } from 'lucide-react';
+import { useState } from 'react';
+
+const customerGroupsDocument = graphql(`
+    query GetCustomerGroups($options: CustomerGroupListOptions) {
+        customerGroups(options: $options) {
+            items {
+                id
+                name
+            }
+        }
+    }
+`);
+
+export interface CustomerGroup {
+    id: string;
+    name: string;
+}
+
+export interface CustomerGroupSelectorProps {
+    onSelect: (value: CustomerGroup) => void;
+    readOnly?: boolean;
+}
+
+export function CustomerGroupSelector(props: CustomerGroupSelectorProps) {
+    const [open, setOpen] = useState(false);
+
+    const { data: groups } = useQuery({
+        queryKey: ['customerGroups'],
+        queryFn: () =>
+            api.query(customerGroupsDocument, {
+                options: {
+                    sort: { name: 'ASC' },
+                },
+            }),
+        staleTime: 1000 * 60 * 5,
+    });
+
+    return (
+        <Popover open={open} onOpenChange={setOpen}>
+            <PopoverTrigger asChild>
+                <Button variant="outline" size="sm" type="button" disabled={props.readOnly} className="gap-2">
+                    <Plus className="h-4 w-4" />
+                    <Trans>Add customer groups</Trans>
+                </Button>
+            </PopoverTrigger>
+            <PopoverContent className="p-0" align="start">
+                <Command shouldFilter={false}>
+                    <CommandList>
+                        {groups?.customerGroups.items.map(group => (
+                            <CommandItem key={group.id} onSelect={() => props.onSelect(group)}>
+                                {group.name}
+                            </CommandItem>
+                        ))}
+                    </CommandList>
+                </Command>
+            </PopoverContent>
+        </Popover>
+    );
+}

+ 4 - 1
packages/dashboard/src/components/shared/paginated-list-data-table.tsx

@@ -92,7 +92,7 @@ export type PaginatedListKeys<
 }[keyof PaginatedListItemFields<T, Path>];
 
 export type CustomizeColumnConfig<T extends TypedDocumentNode<any, any>> = {
-    [Key in keyof PaginatedListItemFields<T>]?: Partial<ColumnDef<PaginatedListItemFields<T>>>;
+    [Key in keyof PaginatedListItemFields<T>]?: Partial<ColumnDef<PaginatedListItemFields<T>, PaginatedListItemFields<T>[Key]>>;
 };
 
 export type FacetedFilterConfig<T extends TypedDocumentNode<any, any>> = {
@@ -212,6 +212,8 @@ export function PaginatedListDataTable<
     const [debouncedSearchTerm] = useDebounce(searchTerm, 500);
     const queryClient = useQueryClient();
 
+
+
     const sort = sorting?.reduce((acc: any, sort: ColumnSort) => {
         const direction = sort.desc ? 'DESC' : 'ASC';
         const field = sort.id;
@@ -222,6 +224,7 @@ export function PaginatedListDataTable<
         return { ...acc, [field]: direction };
     }, {});
 
+
     const filter = columnFilters?.length
         ? {
               _and: columnFilters.map(f => {

+ 155 - 0
packages/dashboard/src/components/ui/alert-dialog.tsx

@@ -0,0 +1,155 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+function AlertDialog({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
+  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
+}
+
+function AlertDialogTrigger({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
+  return (
+    <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
+  )
+}
+
+function AlertDialogPortal({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
+  return (
+    <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
+  )
+}
+
+function AlertDialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
+  return (
+    <AlertDialogPrimitive.Overlay
+      data-slot="alert-dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
+  return (
+    <AlertDialogPortal>
+      <AlertDialogOverlay />
+      <AlertDialogPrimitive.Content
+        data-slot="alert-dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      />
+    </AlertDialogPortal>
+  )
+}
+
+function AlertDialogHeader({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogFooter({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
+  return (
+    <AlertDialogPrimitive.Title
+      data-slot="alert-dialog-title"
+      className={cn("text-lg font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
+  return (
+    <AlertDialogPrimitive.Description
+      data-slot="alert-dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogAction({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
+  return (
+    <AlertDialogPrimitive.Action
+      className={cn(buttonVariants(), className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogCancel({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
+  return (
+    <AlertDialogPrimitive.Cancel
+      className={cn(buttonVariants({ variant: "outline" }), className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  AlertDialog,
+  AlertDialogPortal,
+  AlertDialogOverlay,
+  AlertDialogTrigger,
+  AlertDialogContent,
+  AlertDialogHeader,
+  AlertDialogFooter,
+  AlertDialogTitle,
+  AlertDialogDescription,
+  AlertDialogAction,
+  AlertDialogCancel,
+}

+ 7 - 1
packages/dashboard/src/framework/page/list-page.tsx

@@ -38,6 +38,7 @@ export interface ListPageProps<
     customizeColumns?: CustomizeColumnConfig<T>;
     additionalColumns?: AC;
     defaultColumnOrder?: (keyof ListQueryFields<T> | keyof AC)[];
+    defaultSort?: SortingState;
     defaultVisibility?: Partial<Record<keyof ListQueryFields<T> | keyof AC, boolean>>;
     children?: React.ReactNode;
     facetedFilters?: FacetedFilterConfig<T>;
@@ -55,6 +56,7 @@ export function ListPage<
     customizeColumns,
     additionalColumns,
     defaultColumnOrder,
+    defaultSort,
     route: routeOrFn,
     defaultVisibility,
     onSearchTermChange,
@@ -70,13 +72,17 @@ export function ListPage<
         itemsPerPage: routeSearch.perPage ? parseInt(routeSearch.perPage) : 10,
     };
 
-    const sorting: SortingState = (routeSearch.sort ?? '').split(',').map((s: string) => {
+    const sorting: SortingState = (routeSearch.sort ?? '').split(',').filter(s => s.length).map((s: string) => {
         return {
             id: s.replace(/^-/, ''),
             desc: s.startsWith('-'),
         };
     });
 
+    if (defaultSort && !sorting.length) {
+        sorting.push(...defaultSort);
+    }
+
     function sortToString(sortingStates?: SortingState) {
         return sortingStates?.map(s => `${s.desc ? '-' : ''}${s.id}`).join(',');
     }

+ 1 - 5
packages/dashboard/src/framework/page/use-detail-page.ts

@@ -180,15 +180,11 @@ export function useDetailPage<
         },
     });
 
-    const refreshEntity = useCallback(() => {
-        void queryClient.invalidateQueries({ queryKey: detailQueryOptions.queryKey });
-    }, [queryClient, detailQueryOptions.queryKey]);
-
     return {
         form,
         submitHandler,
         entity,
         isPending: updateMutation.isPending || detailQuery?.isPending,
-        refreshEntity,
+        refreshEntity: detailQuery.refetch,
     };
 }

+ 150 - 70
packages/dashboard/src/routes/_authenticated/_customers/components/customer-address-card.tsx

@@ -1,75 +1,155 @@
-import { ResultOf } from "@/graphql/graphql.js";
+import { ResultOf } from '@/graphql/graphql.js';
 
-import { addressFragment } from "../customers.graphql.js";
-import { DialogContent, Dialog, DialogTrigger, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog.js";
-import { Trans } from "@lingui/react/macro";
-import { CustomerAddressForm } from "./customer-address-form.js";
-import { EditIcon, TrashIcon } from "lucide-react";
+import { addressFragment, deleteCustomerAddressDocument, updateCustomerAddressDocument } from '../customers.graphql.js';
+import {
+    DialogContent,
+    Dialog,
+    DialogTrigger,
+    DialogHeader,
+    DialogTitle,
+    DialogDescription,
+} from '@/components/ui/dialog.js';
+import { Trans } from '@lingui/react/macro';
+import { useLingui } from '@lingui/react/macro';
+import { AddressFormValues, CustomerAddressForm } from './customer-address-form.js';
+import { EditIcon, TrashIcon } from 'lucide-react';
+import { useState } from 'react';
+import { Badge } from '@/components/ui/badge.js';
+import { api } from '@/graphql/api.js';
+import { useMutation } from '@tanstack/react-query';
+import { toast } from 'sonner';
+import { ConfirmationDialog } from '@/components/shared/confirmation-dialog.js';
 
-export function CustomerAddressCard({ 
-  address, 
-  editable = false,
-  deletable = false
-}: { 
-  address: ResultOf<typeof addressFragment>
-  editable?: boolean
-  deletable?: boolean
+export function CustomerAddressCard({
+    address,
+    editable = false,
+    deletable = false,
+    onUpdate,
+    onDelete,
+}: {
+    address: ResultOf<typeof addressFragment>;
+    editable?: boolean;
+    deletable?: boolean;
+    onUpdate?: () => void;
+    onDelete?: () => void;
 }) {
-  return (
-    <div className="border border-border rounded-md p-4 relative">
-      {(address.defaultShippingAddress || address.defaultBillingAddress) && (
-        <div className="absolute top-2 right-2 flex gap-1">
-          {address.defaultShippingAddress && (
-            <span className="text-xs px-2 py-1 bg-primary/10 text-primary rounded-md">Default Shipping</span>
-          )}
-          {address.defaultBillingAddress && (
-            <span className="text-xs px-2 py-1 bg-primary/10 text-primary rounded-md">Default Billing</span>
-          )}
-        </div>
-      )}
-      
-      <div className="flex flex-col gap-1">
-        <div className="font-semibold">{address.fullName}</div>
-        {address.company && <div>{address.company}</div>}
-        <div>{address.streetLine1}</div>
-        {address.streetLine2 && <div>{address.streetLine2}</div>}
-        <div>
-          {address.city}
-          {address.province && `, ${address.province}`}
-          {address.postalCode && ` ${address.postalCode}`}
-        </div>
-        <div>{address.country.name}</div>
-        {address.phoneNumber && <div>{address.phoneNumber}</div>}
-      </div>
+    const [open, setOpen] = useState(false);
+    const { i18n } = useLingui();
+    const { mutate: deleteAddress } = useMutation({
+        mutationFn: api.mutate(deleteCustomerAddressDocument),
+        onSuccess: () => {
+            toast.success(i18n.t('Address deleted successfully'));
+            onDelete?.();
+        },
+        onError: () => {
+            toast.error(i18n.t('Failed to delete address'));
+        },
+    }); 
+    
+    // Set up mutation for updating address
+    const { mutate: updateAddress } = useMutation({
+        mutationFn: api.mutate(updateCustomerAddressDocument),
+        onSuccess: () => {
+            toast.success(i18n.t('Address updated successfully'));
+            onUpdate?.();
+        },
+        onError: error => {
+            toast.error(i18n.t('Failed to update address'));
+            console.error('Error updating address:', error);
+        },
+    });
+
+    // Form submission handler
+    const onSubmit = (values: AddressFormValues) => {
+        // Type assertion to handle the mutation parameters
+        updateAddress({
+            input: {
+                id: values.id,
+                fullName: values.fullName,
+                company: values.company,
+                streetLine1: values.streetLine1,
+                streetLine2: values.streetLine2,
+                city: values.city,
+                province: values.province,
+                postalCode: values.postalCode,
+                countryCode: values.countryCode,
+                phoneNumber: values.phoneNumber,
+                defaultShippingAddress: values.defaultShippingAddress,
+                defaultBillingAddress: values.defaultBillingAddress,
+                customFields: values.customFields,
+            },
+        } as any);
+        setOpen(false);
+    };
+
+    return (
+        <div className="border border-border rounded-md p-4 relative text-sm">
+            {(address.defaultShippingAddress || address.defaultBillingAddress) && (
+                <div className="flex flex-wrap gap-1 mb-2">
+                    {address.defaultShippingAddress && (
+                        <Badge className="text-xs px-2 py-1 bg-primary/10 text-primary rounded-md">
+                            Default Shipping
+                        </Badge>
+                    )}
+                    {address.defaultBillingAddress && (
+                        <Badge className="text-xs px-2 py-1 bg-primary/10 text-primary rounded-md">
+                            Default Billing
+                        </Badge>
+                    )}
+                </div>
+            )}
+
+            <div className="flex flex-col gap-1">
+                <div className="font-semibold">{address.fullName}</div>
+                {address.company && <div>{address.company}</div>}
+                <div>{address.streetLine1}</div>
+                {address.streetLine2 && <div>{address.streetLine2}</div>}
+                <div>
+                    {address.city}
+                    {address.province && `, ${address.province}`}
+                </div>
+                <div>{address.postalCode}</div>
+                <div>{address.country.name}</div>
+                {address.phoneNumber && <div>{address.phoneNumber}</div>}
+            </div>
 
-      {(editable || deletable) && (
-        <div className="flex gap-2 mt-3 pt-3 border-t border-border">
-          {editable && (
-            <Dialog>
-            <DialogTrigger>
-                <EditIcon className="w-4 h-4"/>
-            </DialogTrigger>
-            <DialogContent>
-              <DialogHeader>
-                <DialogTitle>Edit Address</DialogTitle>
-                <DialogDescription>
-                  <Trans>Edit the address details below.</Trans>
-                </DialogDescription>
-              </DialogHeader>
-              <CustomerAddressForm address={address} />
-            </DialogContent>
-          </Dialog>
-          )}
-          {deletable && (
-            <button 
-              onClick={() => {}}
-              className="text-xs text-muted-foreground hover:text-foreground"
-            >
-              <TrashIcon className="w-4 h-4 text-destructive"/>
-            </button>
-          )}
+            {(editable || deletable) && (
+                <div className="flex gap-4 mt-3 pt-3 border-t border-border">
+                    {editable && (
+                        <Dialog open={open} onOpenChange={setOpen}>
+                            <DialogTrigger>
+                                <EditIcon className="w-4 h-4" />
+                            </DialogTrigger>
+                            <DialogContent>
+                                <DialogHeader>
+                                    <DialogTitle>
+                                        <Trans>Edit Address</Trans>
+                                    </DialogTitle>
+                                    <DialogDescription>
+                                        <Trans>Edit the address details below.</Trans>
+                                    </DialogDescription>
+                                </DialogHeader>
+                                <CustomerAddressForm
+                                    address={address}
+                                    onSubmit={onSubmit}
+                                />
+                            </DialogContent>
+                        </Dialog>
+                    )}
+                    {deletable && (
+                        <ConfirmationDialog
+                            title={i18n.t('Delete Address')}
+                            description={i18n.t('Are you sure you want to delete this address?')}
+                            onConfirm={() => {
+                                deleteAddress({ id: address.id });
+                                onDelete?.();
+                            }}
+                        >
+                            <TrashIcon className="w-4 h-4 text-destructive" />
+                        </ConfirmationDialog>
+                    )}
+                </div>
+            )}
         </div>
-      )}
-    </div>
-  );
-}
+    );
+}

+ 350 - 304
packages/dashboard/src/routes/_authenticated/_customers/components/customer-address-form.tsx

@@ -1,342 +1,388 @@
 import { Button } from '@/components/ui/button.js';
 import { Checkbox } from '@/components/ui/checkbox.js';
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form.js';
+import {
+    Form,
+    FormControl,
+    FormDescription,
+    FormField,
+    FormItem,
+    FormLabel,
+    FormMessage,
+} from '@/components/ui/form.js';
 import { Input } from '@/components/ui/input.js';
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
 import { api } from '@/graphql/api.js';
-import { graphql, VariablesOf } from '@/graphql/graphql.js';
+import { graphql, ResultOf } from '@/graphql/graphql.js';
+import { zodResolver } from '@hookform/resolvers/zod';
 import { Trans, useLingui } from '@lingui/react/macro';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { useForm, ControllerRenderProps, FieldPath } from 'react-hook-form';
+import { useQuery } from '@tanstack/react-query';
+import { ControllerRenderProps, FieldPath, useForm } from 'react-hook-form';
 import { z } from 'zod';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { toast } from 'sonner';
-import { updateCustomerAddressDocument } from '../customers.graphql.js';
+import { addressFragment } from '../customers.graphql.js';
 
 // Query document to fetch available countries
 const getAvailableCountriesDocument = graphql(`
-  query GetAvailableCountries {
-    countries(options: { filter: { enabled: { eq: true } } }) {
-      items {
-        id
-        code
-        name
-      }
+    query GetAvailableCountries {
+        countries(options: { filter: { enabled: { eq: true } } }) {
+            items {
+                id
+                code
+                name
+            }
+        }
     }
-  }
 `);
 
 // Define the form schema using zod
 const addressFormSchema = z.object({
-  id: z.string(),
-  fullName: z.string().optional(),
-  company: z.string().optional(),
-  streetLine1: z.string().min(1, { message: "Street address is required" }),
-  streetLine2: z.string().optional(),
-  city: z.string().min(1, { message: "City is required" }),
-  province: z.string().optional(),
-  postalCode: z.string().optional(),
-  countryCode: z.string().min(1, { message: "Country is required" }),
-  phoneNumber: z.string().optional(),
-  defaultShippingAddress: z.boolean().default(false),
-  defaultBillingAddress: z.boolean().default(false),
-  customFields: z.any().optional()
+    id: z.string(),
+    fullName: z.string().optional(),
+    company: z.string().optional(),
+    streetLine1: z.string().min(1, { message: 'Street address is required' }),
+    streetLine2: z.string().optional(),
+    city: z.string().min(1, { message: 'City is required' }),
+    province: z.string().optional(),
+    postalCode: z.string().optional(),
+    countryCode: z.string().min(1, { message: 'Country is required' }),
+    phoneNumber: z.string().optional(),
+    defaultShippingAddress: z.boolean().default(false),
+    defaultBillingAddress: z.boolean().default(false),
+    customFields: z.any().optional(),
 });
 
-type AddressFormValues = z.infer<typeof addressFormSchema>;
+export type AddressFormValues = z.infer<typeof addressFormSchema>;
 
 interface CustomerAddressFormProps {
-  address?: VariablesOf<typeof updateCustomerAddressDocument>['input'];
-  onSuccess?: () => void;
-  onCancel?: () => void;
+    address?: ResultOf<typeof addressFragment>;
+    onSubmit?: (values: AddressFormValues) => void;
+    onCancel?: () => void;
 }
 
-export function CustomerAddressForm({ address, onSuccess, onCancel }: CustomerAddressFormProps) {
-  const { i18n } = useLingui();
-  const queryClient = useQueryClient();
+export function CustomerAddressForm({ address, onSubmit, onCancel }: CustomerAddressFormProps) {
+    const { i18n } = useLingui();
 
-  // Fetch available countries
-  const { data: countriesData, isLoading: isLoadingCountries } = useQuery({
-    queryKey: ['availableCountries'],
-    queryFn: () => api.query(getAvailableCountriesDocument),
-  });
-
-  // Set up form with react-hook-form and zod
-  const form = useForm<AddressFormValues>({
-    resolver: zodResolver(addressFormSchema),
-    defaultValues: {
-      id: address?.id || '',
-      fullName: address?.fullName || '',
-      company: address?.company || '',
-      streetLine1: address?.streetLine1 || '',
-      streetLine2: address?.streetLine2 || '',
-      city: address?.city || '',
-      province: address?.province || '',
-      postalCode: address?.postalCode || '',
-      countryCode: address?.countryCode || '',
-      phoneNumber: address?.phoneNumber || '',
-      defaultShippingAddress: address?.defaultShippingAddress || false,
-      defaultBillingAddress: address?.defaultBillingAddress || false,
-      customFields: address?.customFields || {}
-    }
-  });
+    // Fetch available countries
+    const { data: countriesData, isLoading: isLoadingCountries } = useQuery({
+        queryKey: ['availableCountries'],
+        queryFn: () => api.query(getAvailableCountriesDocument),
+        staleTime: 1000 * 60 * 60 * 24, // 24 hours
+    });
 
-  // Set up mutation for updating address
-  const { mutate: updateAddress, isPending } = useMutation({
-    mutationFn: api.mutate(updateCustomerAddressDocument),
-    onSuccess: () => {
-      toast.success(i18n.t('Address updated successfully'));
-      // Invalidate customer detail query to refresh the data
-      queryClient.invalidateQueries({ queryKey: ['GetCustomerDetail'] });
-      if (onSuccess) {
-        onSuccess();
-      }
-    },
-    onError: (error) => {
-      toast.error(i18n.t('Failed to update address'));
-      console.error('Error updating address:', error);
-    }
-  });
+    // Set up form with react-hook-form and zod
+    const form = useForm<AddressFormValues>({
+        resolver: zodResolver(addressFormSchema),
+        values: {
+            id: address?.id || '',
+            fullName: address?.fullName || '',
+            company: address?.company || '',
+            streetLine1: address?.streetLine1 || '',
+            streetLine2: address?.streetLine2 || '',
+            city: address?.city || '',
+            province: address?.province || '',
+            postalCode: address?.postalCode || '',
+            countryCode: address?.country.code || '',
+            phoneNumber: address?.phoneNumber || '',
+            defaultShippingAddress: address?.defaultShippingAddress || false,
+            defaultBillingAddress: address?.defaultBillingAddress || false,
+            customFields: (address as any)?.customFields || {},
+        },
+    });
 
-  // Form submission handler
-  const onSubmit = (values: AddressFormValues) => {
-    // Type assertion to handle the mutation parameters
-    updateAddress({
-      input: {
-        id: values.id,
-        fullName: values.fullName,
-        company: values.company,
-        streetLine1: values.streetLine1,
-        streetLine2: values.streetLine2,
-        city: values.city,
-        province: values.province,
-        postalCode: values.postalCode,
-        countryCode: values.countryCode,
-        phoneNumber: values.phoneNumber,
-        defaultShippingAddress: values.defaultShippingAddress,
-        defaultBillingAddress: values.defaultBillingAddress,
-        customFields: values.customFields
-      }
-    } as any);
-  };
+    return (
+        <Form {...form}>
+            <form
+                onSubmit={e => {
+                    e.stopPropagation();
+                    onSubmit && form.handleSubmit(onSubmit)(e);
+                }}
+                className="space-y-4"
+            >
+                <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                    {/* Full Name */}
+                    <FormField
+                        control={form.control}
+                        name="fullName"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Full Name</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <Input placeholder="John Doe" {...field} value={field.value || ''} />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
 
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
-        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
-          {/* Full Name */}
-          <FormField
-            control={form.control}
-            name="fullName"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>Full Name</Trans></FormLabel>
-                <FormControl>
-                  <Input placeholder="John Doe" {...field} value={field.value || ''} />
-                </FormControl>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
+                    {/* Company */}
+                    <FormField
+                        control={form.control}
+                        name="company"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Company</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <Input
+                                        placeholder="Company (optional)"
+                                        {...field}
+                                        value={field.value || ''}
+                                    />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
 
-          {/* Company */}
-          <FormField
-            control={form.control}
-            name="company"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>Company</Trans></FormLabel>
-                <FormControl>
-                  <Input placeholder="Company (optional)" {...field} value={field.value || ''} />
-                </FormControl>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
+                    {/* Street Line 1 */}
+                    <FormField
+                        control={form.control}
+                        name="streetLine1"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Street Address</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <Input placeholder="123 Main St" {...field} value={field.value || ''} />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
 
-          {/* Street Line 1 */}
-          <FormField
-            control={form.control}
-            name="streetLine1"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>Street Address</Trans></FormLabel>
-                <FormControl>
-                  <Input placeholder="123 Main St" {...field} value={field.value || ''} />
-                </FormControl>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
+                    {/* Street Line 2 */}
+                    <FormField
+                        control={form.control}
+                        name="streetLine2"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Apartment, suite, etc.</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <Input
+                                        placeholder="Apt 4B (optional)"
+                                        {...field}
+                                        value={field.value || ''}
+                                    />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
 
-          {/* Street Line 2 */}
-          <FormField
-            control={form.control}
-            name="streetLine2"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>Apartment, suite, etc.</Trans></FormLabel>
-                <FormControl>
-                  <Input placeholder="Apt 4B (optional)" {...field} value={field.value || ''} />
-                </FormControl>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
+                    {/* City */}
+                    <FormField
+                        control={form.control}
+                        name="city"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>City</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <Input placeholder="City" {...field} value={field.value || ''} />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
 
-          {/* City */}
-          <FormField
-            control={form.control}
-            name="city"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>City</Trans></FormLabel>
-                <FormControl>
-                  <Input placeholder="City" {...field} value={field.value || ''} />
-                </FormControl>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
+                    {/* Province/State */}
+                    <FormField
+                        control={form.control}
+                        name="province"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>State/Province</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <Input
+                                        placeholder="State/Province (optional)"
+                                        {...field}
+                                        value={field.value || ''}
+                                    />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
 
-          {/* Province/State */}
-          <FormField
-            control={form.control}
-            name="province"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>State/Province</Trans></FormLabel>
-                <FormControl>
-                  <Input placeholder="State/Province (optional)" {...field} value={field.value || ''} />
-                </FormControl>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
+                    {/* Postal Code */}
+                    <FormField
+                        control={form.control}
+                        name="postalCode"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Postal Code</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <Input placeholder="Postal Code" {...field} value={field.value || ''} />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
 
-          {/* Postal Code */}
-          <FormField
-            control={form.control}
-            name="postalCode"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>Postal Code</Trans></FormLabel>
-                <FormControl>
-                  <Input placeholder="Postal Code" {...field} value={field.value || ''} />
-                </FormControl>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
+                    {/* Country */}
+                    <FormField
+                        control={form.control}
+                        name="countryCode"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Country</Trans>
+                                </FormLabel>
+                                <Select
+                                    onValueChange={field.onChange}
+                                    defaultValue={field.value || undefined}
+                                    value={field.value || undefined}
+                                    disabled={isLoadingCountries}
+                                >
+                                    <FormControl>
+                                        <SelectTrigger>
+                                            <SelectValue placeholder={i18n.t('Select a country')} />
+                                        </SelectTrigger>
+                                    </FormControl>
+                                    <SelectContent>
+                                        {countriesData?.countries.items.map(country => (
+                                            <SelectItem key={country.code} value={country.code}>
+                                                {country.name}
+                                            </SelectItem>
+                                        ))}
+                                    </SelectContent>
+                                </Select>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
 
-          {/* Country */}
-          <FormField
-            control={form.control}
-            name="countryCode"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>Country</Trans></FormLabel>
-                <Select 
-                  onValueChange={field.onChange} 
-                  defaultValue={field.value || undefined}
-                  disabled={isLoadingCountries}
-                >
-                  <FormControl>
-                    <SelectTrigger>
-                      <SelectValue placeholder="Select a country" />
-                    </SelectTrigger>
-                  </FormControl>
-                  <SelectContent>
-                    {countriesData?.countries.items.map((country) => (
-                      <SelectItem key={country.code} value={country.code}>
-                        {country.name}
-                      </SelectItem>
-                    ))}
-                  </SelectContent>
-                </Select>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
+                    {/* Phone Number */}
+                    <FormField
+                        control={form.control}
+                        name="phoneNumber"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem>
+                                <FormLabel>
+                                    <Trans>Phone Number</Trans>
+                                </FormLabel>
+                                <FormControl>
+                                    <Input
+                                        placeholder="Phone (optional)"
+                                        {...field}
+                                        value={field.value || ''}
+                                    />
+                                </FormControl>
+                                <FormMessage />
+                            </FormItem>
+                        )}
+                    />
+                </div>
 
-          {/* Phone Number */}
-          <FormField
-            control={form.control}
-            name="phoneNumber"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem>
-                <FormLabel><Trans>Phone Number</Trans></FormLabel>
-                <FormControl>
-                  <Input placeholder="Phone (optional)" {...field} value={field.value || ''} />
-                </FormControl>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
-        </div>
+                {/* Default Address Checkboxes */}
+                <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
+                    <FormField
+                        control={form.control}
+                        name="defaultShippingAddress"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                                <FormControl>
+                                    <Checkbox checked={field.value} onCheckedChange={field.onChange} />
+                                </FormControl>
+                                <div className="space-y-1 leading-none">
+                                    <FormLabel>
+                                        <Trans>Default Shipping Address</Trans>
+                                    </FormLabel>
+                                    <FormDescription>
+                                        <Trans>Use as the default shipping address</Trans>
+                                    </FormDescription>
+                                </div>
+                            </FormItem>
+                        )}
+                    />
 
-        {/* Default Address Checkboxes */}
-        <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2">
-          <FormField
-            control={form.control}
-            name="defaultShippingAddress"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem className="flex flex-row items-start space-x-3 space-y-0">
-                <FormControl>
-                  <Checkbox
-                    checked={field.value}
-                    onCheckedChange={field.onChange}
-                  />
-                </FormControl>
-                <div className="space-y-1 leading-none">
-                  <FormLabel><Trans>Default Shipping Address</Trans></FormLabel>
-                  <FormDescription>
-                    <Trans>Use as the default shipping address</Trans>
-                  </FormDescription>
+                    <FormField
+                        control={form.control}
+                        name="defaultBillingAddress"
+                        render={({
+                            field,
+                        }: {
+                            field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>>;
+                        }) => (
+                            <FormItem className="flex flex-row items-start space-x-3 space-y-0">
+                                <FormControl>
+                                    <Checkbox checked={field.value} onCheckedChange={field.onChange} />
+                                </FormControl>
+                                <div className="space-y-1 leading-none">
+                                    <FormLabel>
+                                        <Trans>Default Billing Address</Trans>
+                                    </FormLabel>
+                                    <FormDescription>
+                                        <Trans>Use as the default billing address</Trans>
+                                    </FormDescription>
+                                </div>
+                            </FormItem>
+                        )}
+                    />
                 </div>
-              </FormItem>
-            )}
-          />
 
-          <FormField
-            control={form.control}
-            name="defaultBillingAddress"
-            render={({ field }: { field: ControllerRenderProps<AddressFormValues, FieldPath<AddressFormValues>> }) => (
-              <FormItem className="flex flex-row items-start space-x-3 space-y-0">
-                <FormControl>
-                  <Checkbox
-                    checked={field.value}
-                    onCheckedChange={field.onChange}
-                  />
-                </FormControl>
-                <div className="space-y-1 leading-none">
-                  <FormLabel><Trans>Default Billing Address</Trans></FormLabel>
-                  <FormDescription>
-                    <Trans>Use as the default billing address</Trans>
-                  </FormDescription>
+                {/* Form Actions */}
+                <div className="flex justify-end gap-2 pt-4">
+                    {onCancel && (
+                        <Button type="button" variant="outline" onClick={onCancel}>
+                            <Trans>Cancel</Trans>
+                        </Button>
+                    )}
+                    <Button type="submit">
+                        <Trans>Save Address</Trans>
+                    </Button>
                 </div>
-              </FormItem>
-            )}
-          />
-        </div>
-
-        {/* Form Actions */}
-        <div className="flex justify-end gap-2 pt-4">
-          {onCancel && (
-            <Button type="button" variant="outline" onClick={onCancel}>
-              <Trans>Cancel</Trans>
-            </Button>
-          )}
-          <Button type="submit" disabled={isPending}>
-            {isPending ? (
-              <Trans>Saving...</Trans>
-            ) : (
-              <Trans>Save Address</Trans>
-            )}
-          </Button>
-        </div>
-      </form>
-    </Form>
-  );
+            </form>
+        </Form>
+    );
 }
-

+ 4 - 0
packages/dashboard/src/routes/_authenticated/_customers/components/customer-group-controls.tsx

@@ -0,0 +1,4 @@
+export function CustomerGroupControls() {
+    return <div>CustomerGroupControls</div>;
+}
+

+ 78 - 0
packages/dashboard/src/routes/_authenticated/_customers/components/customer-history/customer-history-container.tsx

@@ -0,0 +1,78 @@
+import { Alert, AlertDescription } from '@/components/ui/alert.js';
+import { Skeleton } from '@/components/ui/skeleton.js';
+import { Trans } from '@lingui/react/macro';
+import { TriangleAlert } from 'lucide-react';
+import { CustomerHistory } from './customer-history.js';
+import { useCustomerHistory } from './use-customer-history.js';
+import { Button } from '@/components/ui/button.js';
+
+interface CustomerHistoryContainerProps {
+    customerId: string;
+}
+
+export function CustomerHistoryContainer({ customerId }: CustomerHistoryContainerProps) {
+    const {
+        historyEntries,
+        customer,
+        loading,
+        error,
+        addNote,
+        updateNote,
+        deleteNote,
+        fetchNextPage,
+        hasNextPage,
+    } = useCustomerHistory({ customerId, pageSize: 10 });
+
+    if (loading && !customer) {
+        return (
+            <div className="space-y-4">
+                <h2 className="text-xl font-semibold">
+                    <Trans>Customer history</Trans>
+                </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>
+                    <Trans>Error loading customer history: {error.message}</Trans>
+                </AlertDescription>
+            </Alert>
+        );
+    }
+
+    if (!customer) {
+        return (
+            <Alert>
+                <AlertDescription>
+                    <Trans>Customer not found</Trans>
+                </AlertDescription>
+            </Alert>
+        );
+    }
+
+    return (
+        <>
+            <CustomerHistory
+                customer={customer}
+                historyEntries={historyEntries ?? []}
+                onAddNote={addNote}
+                onUpdateNote={updateNote}
+                onDeleteNote={deleteNote}
+            />
+            {hasNextPage && (
+                <Button type="button" variant="outline" onClick={() => fetchNextPage()}>
+                    <Trans>Load more</Trans>
+                </Button>
+            )}
+        </>
+    );
+}

+ 77 - 0
packages/dashboard/src/routes/_authenticated/_customers/components/customer-history/customer-history.tsx

@@ -0,0 +1,77 @@
+import { Badge } from '@/components/ui/badge.js';
+import { Trans } from '@lingui/react/macro';
+import { ArrowRightToLine, CheckIcon, CreditCardIcon, SquarePen } from 'lucide-react';
+import { HistoryEntry, HistoryEntryItem } from '@/components/shared/history-timeline/history-entry.js';
+import { HistoryNoteInput } from '@/components/shared/history-timeline/history-note-input.js';
+import { HistoryTimeline } from '@/components/shared/history-timeline/history-timeline.js';
+
+interface CustomerHistoryProps {
+    customer: {
+        id: string;
+    };
+    historyEntries: Array<HistoryEntryItem>;
+    onAddNote: (note: string, isPrivate: boolean) => void;
+    onUpdateNote?: (entryId: string, note: string, isPrivate: boolean) => void;
+    onDeleteNote?: (entryId: string) => void;
+}
+
+export function CustomerHistory({ historyEntries, onAddNote, onUpdateNote, onDeleteNote }: CustomerHistoryProps) {
+    const getTimelineIcon = (entry: CustomerHistoryProps['historyEntries'][0]) => {
+        switch (entry.type) {
+            case 'CUSTOMER_NOTE':
+                return <SquarePen className="h-4 w-4" />;
+            default:
+                return <CheckIcon className="h-4 w-4" />;
+        }
+    };
+
+    const getTitle = (entry: CustomerHistoryProps['historyEntries'][0]) => {
+        switch (entry.type) {
+            case 'CUSTOMER_NOTE':
+                return <Trans>Note added</Trans>;
+            default:
+                return <Trans>{entry.type.replace(/_/g, ' ').toLowerCase()}</Trans>;
+        }
+    };
+
+    return (
+        <div className="">
+            <div className="mb-4">
+                <HistoryNoteInput onAddNote={onAddNote} />
+            </div>
+            <HistoryTimeline onEditNote={onUpdateNote} onDeleteNote={onDeleteNote}>
+                {historyEntries.map(entry => (
+                    <HistoryEntry
+                        key={entry.id}
+                        entry={entry}
+                        isNoteEntry={entry.type === 'CUSTOMER_NOTE'}
+                        timelineIcon={getTimelineIcon(entry)}
+                        title={getTitle(entry)}
+                    >
+                        {entry.type === 'CUSTOMER_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 === 'CUSTOMER_NOTE' && (
+                                <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>
+                    </HistoryEntry>
+                ))}
+                
+            </HistoryTimeline>
+        </div>
+    );
+}

+ 3 - 0
packages/dashboard/src/routes/_authenticated/_customers/components/customer-history/index.ts

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

+ 149 - 0
packages/dashboard/src/routes/_authenticated/_customers/components/customer-history/use-customer-history.ts

@@ -0,0 +1,149 @@
+import { api } from '@/graphql/api.js';
+import { graphql } from '@/graphql/graphql.js';
+import { useLingui } from '@lingui/react/macro';
+import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
+import { useState } from 'react';
+import { toast } from 'sonner';
+
+import { customerHistoryDocument } from '../../customers.graphql.js';
+// Simplified mutation definitions - adjust based on your actual schema
+const addCustomerNoteDocument = graphql(`
+    mutation AddCustomerNote($customerId: ID!, $note: String!, $isPublic: Boolean!) {
+        addNoteToCustomer(input: { id: $customerId, note: $note, isPublic: $isPublic }) {
+            id
+            history(options: { take: 1, sort: { createdAt: DESC } }) {
+                items {
+                    id
+                    createdAt
+                    type
+                    isPublic
+                    data
+                }
+            }
+        }
+    }
+`);
+
+const updateCustomerNoteDocument = graphql(`
+    mutation UpdateCustomerNote($noteId: ID!, $note: String!) {
+        updateCustomerNote(input: { noteId: $noteId, note: $note }) {
+            id
+        }
+    }
+`);
+
+const deleteCustomerNoteDocument = graphql(`
+    mutation DeleteCustomerNote($noteId: ID!) {
+        deleteCustomerNote(id: $noteId) {
+            result
+            message
+        }
+    }
+`);
+
+export function useCustomerHistory({ customerId, pageSize = 10 }: { customerId: string; pageSize?: number }) {
+    const [isLoading, setIsLoading] = useState(false);
+    const { i18n } = useLingui();
+
+    // Fetch order history
+    const {
+        data,
+        isLoading: isLoadingQuery,
+        error,
+        refetch,
+        fetchNextPage,
+        hasNextPage,
+    } = useInfiniteQuery({
+        queryFn: ({ pageParam = 0 }) =>
+            api.query(customerHistoryDocument, {
+                id: customerId,
+                options: {
+                    sort: { createdAt: 'DESC' },
+                    skip: pageParam * pageSize,
+                    take: pageSize,
+                },
+            }),
+        queryKey: ['CustomerHistory', customerId],
+        initialPageParam: 0,
+        getNextPageParam: (lastPage, pages, lastPageParam) => {
+            const totalItems = lastPage.customer?.history?.totalItems ?? 0;
+            const currentMaxItem = (lastPageParam + 1) * pageSize;
+            const nextPage = lastPageParam + 1;
+            return currentMaxItem < totalItems ? nextPage : undefined;
+        },
+    });
+
+    // Add note mutation
+    const { mutate: addNoteMutation } = useMutation({
+        mutationFn: api.mutate(addCustomerNoteDocument),
+        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({
+            customerId,
+            note,
+            isPublic: !isPrivate,
+        });
+    };
+
+    // Update note mutation
+    const { mutate: updateNoteMutation } = useMutation({
+        mutationFn: api.mutate(updateCustomerNoteDocument),
+        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,
+        });
+    };
+
+    // Delete note mutation
+    const { mutate: deleteNoteMutation } = useMutation({
+        mutationFn: api.mutate(deleteCustomerNoteDocument),
+        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,
+        });
+    };
+
+    const historyEntries = data?.pages.flatMap(page => page.customer?.history?.items).filter(x => x != null);
+
+    return {
+        historyEntries,
+        customer: data?.pages[0]?.customer,
+        loading: isLoadingQuery || isLoading,
+        error,
+        addNote,
+        updateNote,
+        deleteNote,
+        refetch,
+        fetchNextPage,
+        hasNextPage,
+    };
+}

+ 88 - 0
packages/dashboard/src/routes/_authenticated/_customers/components/customer-order-table.tsx

@@ -0,0 +1,88 @@
+import { Money } from "@/components/data-display/money.js";
+import { PaginatedListDataTable } from "@/components/shared/paginated-list-data-table.js";
+import { Badge } from "@/components/ui/badge.js";
+import { Button } from "@/components/ui/button.js";
+import { Link } from "@tanstack/react-router";
+import { ColumnFiltersState, SortingState } from "@tanstack/react-table";
+import { useState } from "react";
+import { customerOrderListDocument } from "../customers.graphql.js";
+
+interface CustomerOrderTableProps {
+    customerId: string;
+}
+
+export function CustomerOrderTable({ customerId }: CustomerOrderTableProps) {
+    const [page, setPage] = useState(1);
+    const [pageSize, setPageSize] = useState(10);
+    const [sorting, setSorting] = useState<SortingState>([{ id: 'orderPlacedAt', desc: true }]);
+    const [filters, setFilters] = useState<ColumnFiltersState>([]);
+
+    return <PaginatedListDataTable
+        listQuery={customerOrderListDocument}
+        transformVariables={variables => {
+            return {
+                ...variables,
+                customerId,
+            };
+        }}
+        defaultVisibility={{
+            id: false,
+            createdAt: false,
+            updatedAt: false,
+            type: false,
+            currencyCode: false,
+            total: false,
+        }}
+        customizeColumns={{
+            total: {
+                header: 'Total',
+                cell: ({ cell, row }) => {
+                    const value = cell.getValue();
+                    const currencyCode = row.original.currencyCode;
+                    return <Money value={value} currencyCode={currencyCode} />;
+                },
+            },
+            totalWithTax: {
+                header: 'Total with Tax',
+                cell: ({ cell, row }) => {
+                    const value = cell.getValue();
+                    const currencyCode = row.original.currencyCode;
+                    return <Money value={value} currencyCode={currencyCode} />;
+                },
+            },
+            state: {
+                header: 'State',
+                cell: ({ cell }) => {
+                    const value = cell.getValue() as string;
+                    return <Badge variant="outline">{value}</Badge>;
+                },
+            },
+            code: {
+                header: 'Code',
+                cell: ({ cell, row }) => {
+                    const value = cell.getValue() as string;
+                    const id = row.original.id;
+                    return (
+                        <Button asChild variant="ghost">
+                            <Link to={`/orders/${id}`}>{value}</Link>
+                        </Button>
+                    );
+                },
+            },
+        }}
+        page={page}
+        itemsPerPage={pageSize}
+        sorting={sorting}
+        columnFilters={filters}
+        onPageChange={(_, page, perPage) => {
+            setPage(page);
+            setPageSize(perPage);
+        }}
+        onSortChange={(_, sorting) => {
+            setSorting(sorting);
+        }}
+        onFilterChange={(_, filters) => {
+            setFilters(filters);
+        }}
+    />;
+}

+ 8 - 1
packages/dashboard/src/routes/_authenticated/_customers/components/customer-status-badge.tsx

@@ -2,7 +2,14 @@ import { Badge } from '@/components/ui/badge.js';
 import { BadgeX, BadgeCheck } from 'lucide-react';
 import { Trans } from '@lingui/react/macro';
 
-export function CustomerStatusBadge({ status }: { status: 'guest' | 'registered' | 'verified' }) {
+export type CustomerStatus = 'guest' | 'registered' | 'verified';
+
+export interface CustomerStatusBadgeProps {
+    user?: { verified: boolean } | null;
+}
+
+export function CustomerStatusBadge({ user }: CustomerStatusBadgeProps) {
+    const status = user ? (user.verified ? 'verified' : 'registered') : 'guest';
     return (
         <Badge variant="outline">
             {status === 'verified' ? (

+ 76 - 0
packages/dashboard/src/routes/_authenticated/_customers/customers.graphql.ts

@@ -56,6 +56,10 @@ export const customerDetailDocument = graphql(
                 lastName
                 phoneNumber
                 emailAddress
+                groups {
+                    id
+                    name
+                }
                 user {
                     id
                     identifier
@@ -71,6 +75,29 @@ export const customerDetailDocument = graphql(
     [addressFragment],
 );
 
+export const customerOrderListDocument = graphql(`
+    query GetCustomerOrderList($options: OrderListOptions, $customerId: ID!) {
+        customer(id: $customerId) {
+            id
+            orders(options: $options) {
+                items {
+                    id
+                    createdAt
+                    updatedAt
+                    type
+                    code
+                    orderPlacedAt
+                    state
+                    total
+                    totalWithTax
+                    currencyCode
+                }
+                totalItems
+            }
+        }
+    }
+`);
+
 export const createCustomerDocument = graphql(`
     mutation CreateCustomer($input: CreateCustomerInput!) {
         createCustomer(input: $input) {
@@ -116,3 +143,52 @@ export const updateCustomerAddressDocument = graphql(`
         }
     }
 `);
+
+export const deleteCustomerAddressDocument = graphql(`
+    mutation DeleteCustomerAddress($id: ID!) {
+        deleteCustomerAddress(id: $id) {
+            success
+        }
+    }
+`);
+
+export const customerHistoryDocument = graphql(`
+    query GetCustomerHistory($id: ID!, $options: HistoryEntryListOptions) {
+        customer(id: $id) {
+            id
+            createdAt
+            updatedAt
+            history(options: $options) {
+                totalItems
+                items {
+                    id
+                    type
+                    createdAt
+                    isPublic
+                    administrator {
+                        id
+                        firstName
+                        lastName
+                    }
+                    data
+                }
+            }
+        }
+    }
+`);
+
+export const addCustomerToGroupDocument = graphql(`
+    mutation AddCustomerToGroup($customerId: ID!, $groupId: ID!) {
+        addCustomersToGroup(customerIds: [$customerId], customerGroupId: $groupId) {
+            id
+        }
+    }
+`);
+
+export const removeCustomerFromGroupDocument = graphql(`
+    mutation RemoveCustomerFromGroup($customerId: ID!, $groupId: ID!) {
+        removeCustomersFromGroup(customerIds: [$customerId], customerGroupId: $groupId) {
+            id
+        }
+    }
+`);

+ 2 - 5
packages/dashboard/src/routes/_authenticated/_customers/customers.tsx

@@ -39,11 +39,8 @@ export function CustomerListPage() {
                 user: {
                     header: 'Status',
                     cell: ({ cell }) => {
-                        const value = cell.getValue() as ResultOf<
-                            typeof customerListDocument
-                        >['customers']['items'][number]['user'];
-                        const status = value ? (value.verified ? 'verified' : 'registered') : 'guest';
-                        return <CustomerStatusBadge status={status} />;
+                        const value = cell.getValue();
+                        return <CustomerStatusBadge user={value?.user} />;
                     },
                 },
             }}

+ 103 - 2
packages/dashboard/src/routes/_authenticated/_customers/customers_.$id.tsx

@@ -23,8 +23,22 @@ import {
     createCustomerDocument,
     customerDetailDocument,
     updateCustomerDocument,
+    createCustomerAddressDocument,
+    removeCustomerFromGroupDocument,
+    addCustomerToGroupDocument,
 } from './customers.graphql.js';
 import { CustomerAddressCard } from './components/customer-address-card.js';
+import { DialogHeader, DialogTitle, DialogDescription, DialogTrigger, DialogContent, Dialog } from '@/components/ui/dialog.js';
+import { EditIcon, Plus } from 'lucide-react';
+import { CustomerAddressForm } from './components/customer-address-form.js';
+import { useState } from 'react';
+import { api } from '@/graphql/api.js';
+import { useMutation } from '@tanstack/react-query';
+import { CustomerOrderTable } from './components/customer-order-table.js';
+import { CustomerHistoryContainer } from './components/customer-history/customer-history-container.js';
+import { CustomerGroupSelector } from '@/components/shared/customer-group-selector.js';
+import { CustomerGroupChip } from '@/components/shared/customer-group-chip.js';
+import { CustomerStatusBadge } from './components/customer-status-badge.js';
 
 export const Route = createFileRoute('/_authenticated/_customers/customers_/$id')({
     component: CustomerDetailPage,
@@ -58,8 +72,9 @@ export function CustomerDetailPage() {
     const navigate = useNavigate();
     const creatingNewEntity = params.id === NEW_ENTITY_PATH;
     const { i18n } = useLingui();
+    const [newAddressOpen, setNewAddressOpen] = useState(false);
 
-    const { form, submitHandler, entity, isPending } = useDetailPage({
+    const { form, submitHandler, entity, isPending, refreshEntity } = useDetailPage({
         queryDocument: addCustomFields(customerDetailDocument),
         entityField: 'customer',
         createDocument: createCustomerDocument,
@@ -101,6 +116,37 @@ export function CustomerDetailPage() {
         },
     });
 
+    const { mutate: createAddress } = useMutation({
+        mutationFn: api.mutate(createCustomerAddressDocument),
+        onSuccess: () => {
+            setNewAddressOpen(false);
+            refreshEntity();
+        },
+        onError: () => {
+            toast(i18n.t('Failed to create address'), { position: 'top-right' });
+        },
+    });
+
+    const { mutate: addCustomerToGroup } = useMutation({
+        mutationFn: api.mutate(addCustomerToGroupDocument),
+        onSuccess: () => {
+            refreshEntity();
+        },
+        onError: () => {
+            toast(i18n.t('Failed to add customer to group'), { position: 'top-right' });
+        },
+    });
+
+    const { mutate: removeCustomerFromGroup } = useMutation({
+        mutationFn: api.mutate(removeCustomerFromGroupDocument),
+        onSuccess: () => {
+            refreshEntity();
+        },
+        onError: () => {
+            toast(i18n.t('Failed to remove customer from group'), { position: 'top-right' });
+        },
+    });
+
     const customerName = entity ? `${entity.firstName} ${entity.lastName}` : '';
 
     return (
@@ -203,16 +249,71 @@ export function CustomerDetailPage() {
                         </PageBlock>
                         <CustomFieldsPageBlock column="main" entityType="Customer" control={form.control} />
                         <PageBlock column="main" title={<Trans>Addresses</Trans>}>
-                            <div className="md:grid md:grid-cols-2 gap-4">
+                            <div className="md:grid md:grid-cols-2 gap-4 mb-4">
                                 {entity?.addresses?.map(address => (
                                     <CustomerAddressCard
                                         key={address.id}
                                         address={address}
                                         editable
                                         deletable
+                                        onUpdate={() => {
+                                            refreshEntity();
+                                        }}
+                                        onDelete={() => {
+                                            refreshEntity();
+                                        }}
+                                    />
+                                ))}
+                            </div>
+                            <Dialog open={newAddressOpen} onOpenChange={setNewAddressOpen}>
+                                <DialogTrigger asChild>
+                                    <Button variant="outline">
+                                        <Plus className="w-4 h-4" /> <Trans>Add new address</Trans>
+                                    </Button>
+                                </DialogTrigger>
+                                <DialogContent>
+                                    <DialogHeader>
+                                        <DialogTitle>
+                                            <Trans>Add new address</Trans>
+                                        </DialogTitle>
+                                        <DialogDescription>
+                                            <Trans>Add a new address to the customer.</Trans>
+                                        </DialogDescription>
+                                    </DialogHeader>
+                                    <CustomerAddressForm
+                                        onSubmit={values => {
+                                            const { id, ...input } = values;
+                                            createAddress({
+                                                customerId: entity.id,
+                                                input,
+                                            });
+                                        }}
+                                    />
+                                </DialogContent>
+                            </Dialog>
+                        </PageBlock>
+                        <PageBlock column="main" title={<Trans>Orders</Trans>}>
+                            <CustomerOrderTable customerId={entity.id} />
+                        </PageBlock>
+                        <PageBlock column="main" title={<Trans>Customer history</Trans>}>
+                            <CustomerHistoryContainer customerId={entity.id} />
+                        </PageBlock>
+                        <PageBlock column="side" title={<Trans>Status</Trans>}>
+                            <CustomerStatusBadge user={entity.user} />
+                        </PageBlock>
+                        <PageBlock column="side" title={<Trans>Customer groups</Trans>}>
+                            <div className={`flex flex-col gap-2 ${entity?.groups?.length > 0 ? 'mb-2' : ''}`}>
+                                {entity?.groups?.map(group => (
+                                    <CustomerGroupChip
+                                        key={group.id}
+                                        group={group}
+                                        onRemove={groupId => removeCustomerFromGroup({ customerId: entity.id, groupId })}
                                     />
                                 ))}
                             </div>
+                            <CustomerGroupSelector
+                                onSelect={group => addCustomerToGroup({ customerId: entity.id, groupId: group.id })}
+                            />
                         </PageBlock>
                     </PageLayout>
                 </form>

+ 1 - 1
packages/dashboard/src/routes/_authenticated/_facets/facets_.$id.tsx

@@ -110,7 +110,7 @@ export function FacetDetailPage() {
         <Page>
             <PageTitle>{creatingNewEntity ? <Trans>New facet</Trans> : (entity?.name ?? '')}</PageTitle>
             <Form {...form}>
-                <form onSubmit={submitHandler} className="space-y-8">
+                <form onSubmit={submitHandler} className="space-y-8"> 
                     <PageActionBar>
                         <ContentLanguageSelector />
                         <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>

+ 25 - 16
packages/dashboard/src/routes/_authenticated/_orders/components/order-history/order-history-container.tsx

@@ -1,10 +1,10 @@
-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 { Skeleton } from '@/components/ui/skeleton.js';
 import { TriangleAlert } from 'lucide-react';
-
+import { OrderHistory } from './order-history.js';
+import { useOrderHistory } from './use-order-history.js';
+import { Trans } from '@lingui/react/macro';
+import { Button } from '@/components/ui/button.js';
 interface OrderHistoryContainerProps {
   orderId: string;
 }
@@ -18,12 +18,14 @@ export function OrderHistoryContainer({ orderId }: OrderHistoryContainerProps) {
     addNote, 
     updateNote, 
     deleteNote,
-  } = useOrderHistory(orderId);
+    fetchNextPage,
+    hasNextPage,
+  } = useOrderHistory({ orderId, pageSize: 10 });
 
   if (loading && !order) {
     return (
       <div className="space-y-4">
-        <h2 className="text-xl font-semibold">Order history</h2>
+        <h2 className="text-xl font-semibold"><Trans>Order history</Trans></h2>
         <div className="space-y-2">
           <Skeleton className="h-20 w-full" />
           <Skeleton className="h-24 w-full" />
@@ -38,7 +40,7 @@ export function OrderHistoryContainer({ orderId }: OrderHistoryContainerProps) {
       <Alert variant="destructive">
         <TriangleAlert className="h-4 w-4" />
         <AlertDescription>
-          Error loading order history: {error.message}
+          <Trans>Error loading order history: {error.message}</Trans>
         </AlertDescription>
       </Alert>
     );
@@ -47,18 +49,25 @@ export function OrderHistoryContainer({ orderId }: OrderHistoryContainerProps) {
   if (!order) {
     return (
       <Alert>
-        <AlertDescription>Order not found</AlertDescription>
+        <AlertDescription><Trans>Order not found</Trans></AlertDescription>
       </Alert>
     );
   }
 
   return (
-    <OrderHistory
-      order={order}
-      historyEntries={historyEntries}
-      onAddNote={addNote}
-      onUpdateNote={updateNote}
-      onDeleteNote={deleteNote}
-    />
+    <>
+      <OrderHistory
+        order={order}
+        historyEntries={historyEntries ?? []}
+        onAddNote={addNote}
+        onUpdateNote={updateNote}
+        onDeleteNote={deleteNote}
+      />
+      {hasNextPage && (
+        <Button type="button" variant="outline" onClick={() => fetchNextPage()}>
+          <Trans>Load more</Trans>
+        </Button>
+      )}
+    </>
   );
 } 

+ 21 - 6
packages/dashboard/src/routes/_authenticated/_orders/components/order-history/use-order-history.ts

@@ -1,7 +1,7 @@
 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 { useMutation, useInfiniteQuery } from '@tanstack/react-query';
 import { useState } from 'react';
 import { toast } from 'sonner';
 
@@ -42,7 +42,7 @@ const deleteOrderNoteDocument = graphql(`
     }
 `);
 
-export function useOrderHistory(orderId: string) {
+export function useOrderHistory({ orderId, pageSize = 10 }: { orderId: string; pageSize?: number }) {
     const [isLoading, setIsLoading] = useState(false);
     const { i18n } = useLingui();
 
@@ -52,15 +52,26 @@ export function useOrderHistory(orderId: string) {
         isLoading: isLoadingQuery,
         error,
         refetch,
-    } = useQuery({
-        queryFn: () =>
+        fetchNextPage,
+        hasNextPage,
+    } = useInfiniteQuery({
+        queryFn: ({ pageParam = 0 }) =>
             api.query(orderHistoryDocument, {
                 id: orderId,
                 options: {
                     sort: { createdAt: 'DESC' },
+                    skip: pageParam * pageSize,
+                    take: pageSize,
                 },
             }),
         queryKey: ['OrderHistory', orderId],
+        initialPageParam: 0,
+        getNextPageParam: (lastPage, _pages, lastPageParam) => {
+            const totalItems = lastPage.order?.history?.totalItems ?? 0;
+            const currentMaxItem = (lastPageParam + 1) * pageSize;
+            const nextPage = lastPageParam + 1;
+            return currentMaxItem < totalItems ? nextPage : undefined;
+        },
     });
 
     // Add note mutation
@@ -123,14 +134,18 @@ export function useOrderHistory(orderId: string) {
         });
     };
 
+    const historyEntries = data?.pages.flatMap(page => page.order?.history?.items).filter(x => x != null);
+
     return {
-        historyEntries: data?.order?.history?.items || [],
-        order: data?.order,
+        historyEntries,
+        order: data?.pages[0]?.order,
         loading: isLoadingQuery || isLoading,
         error,
         addNote,
         updateNote,
         deleteNote,
         refetch,
+        fetchNextPage,
+        hasNextPage,
     };
 }

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

@@ -28,6 +28,7 @@ export function OrderListPage() {
                     },
                 };
             }}
+            defaultSort={[{ id: 'orderPlacedAt', desc: true }]}
             transformVariables={variables => {
                 return {
                     ...variables,

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor