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

feat(dashboard): Add settings list views (#3428)

David Höck 10 месяцев назад
Родитель
Сommit
0ceeee15af
34 измененных файлов с 1886 добавлено и 220 удалено
  1. 1 0
      package-lock.json
  2. 6 3
      packages/dashboard/src/components/data-table/data-table-faceted-filter.tsx
  3. 1 0
      packages/dashboard/src/components/data-table/data-table.tsx
  4. 14 0
      packages/dashboard/src/components/shared/detail-page-button.tsx
  5. 3 1
      packages/dashboard/src/components/ui/button.tsx
  6. 3 3
      packages/dashboard/src/framework/defaults.ts
  7. 1 1
      packages/dashboard/src/framework/layout-engine/page-layout.tsx
  8. 660 212
      packages/dashboard/src/routeTree.gen.ts
  9. 38 0
      packages/dashboard/src/routes/_authenticated/_administrators/administrators.graphql.ts
  10. 75 0
      packages/dashboard/src/routes/_authenticated/_administrators/administrators.tsx
  11. 42 0
      packages/dashboard/src/routes/_authenticated/_channels/channels.graphql.ts
  12. 53 0
      packages/dashboard/src/routes/_authenticated/_channels/channels.tsx
  13. 25 0
      packages/dashboard/src/routes/_authenticated/_countries/countries.graphql.ts
  14. 66 0
      packages/dashboard/src/routes/_authenticated/_countries/countries.tsx
  15. 27 0
      packages/dashboard/src/routes/_authenticated/_payment-methods/payment-methods.graphql.ts
  16. 55 0
      packages/dashboard/src/routes/_authenticated/_payment-methods/payment-methods.tsx
  17. 55 0
      packages/dashboard/src/routes/_authenticated/_roles/components/expandable-permissions.tsx
  18. 31 0
      packages/dashboard/src/routes/_authenticated/_roles/roles.graphql.ts
  19. 79 0
      packages/dashboard/src/routes/_authenticated/_roles/roles.tsx
  20. 24 0
      packages/dashboard/src/routes/_authenticated/_sellers/sellers.graphql.ts
  21. 49 0
      packages/dashboard/src/routes/_authenticated/_sellers/sellers.tsx
  22. 32 0
      packages/dashboard/src/routes/_authenticated/_shipping-methods/components/test-shipping-method-dialog.tsx
  23. 27 0
      packages/dashboard/src/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts
  24. 54 0
      packages/dashboard/src/routes/_authenticated/_shipping-methods/shipping-methods.tsx
  25. 25 0
      packages/dashboard/src/routes/_authenticated/_stock-locations/stock-locations.graphql.ts
  26. 46 0
      packages/dashboard/src/routes/_authenticated/_stock-locations/stock-locations.tsx
  27. 25 0
      packages/dashboard/src/routes/_authenticated/_tax-categories/tax-categories.graphql.ts
  28. 64 0
      packages/dashboard/src/routes/_authenticated/_tax-categories/tax-categories.tsx
  29. 38 0
      packages/dashboard/src/routes/_authenticated/_tax-rates/tax-rates.graphql.ts
  30. 111 0
      packages/dashboard/src/routes/_authenticated/_tax-rates/tax-rates.tsx
  31. 0 0
      packages/dashboard/src/routes/_authenticated/_zones/components/zone-countries-add.tsx
  32. 71 0
      packages/dashboard/src/routes/_authenticated/_zones/components/zone-countries-sheet.tsx
  33. 43 0
      packages/dashboard/src/routes/_authenticated/_zones/zones.graphql.ts
  34. 42 0
      packages/dashboard/src/routes/_authenticated/_zones/zones.tsx

+ 1 - 0
package-lock.json

@@ -4609,6 +4609,7 @@
     },
     "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
       "version": "1.3.0",
+      "extraneous": true,
       "inBundle": true,
       "license": "MIT",
       "engines": {

+ 6 - 3
packages/dashboard/src/components/data-table/data-table-faceted-filter.tsx

@@ -1,6 +1,6 @@
 import * as React from 'react';
 import { Column } from '@tanstack/react-table';
-import { Check, PlusCircle } from 'lucide-react';
+import { Check, FilterIcon, PlusCircle } from 'lucide-react';
 
 import { cn } from '@/lib/utils.js';
 import { Badge } from '@/components/ui/badge.js';
@@ -19,13 +19,14 @@ import { Separator } from '@/components/ui/separator.js';
 
 export interface DataTableFacetedFilterOption {
     label: string;
-    value: string;
+    value: any;
     icon?: React.ComponentType<{ className?: string }>;
 }
 
 export interface DataTableFacetedFilterProps<TData, TValue> {
     column?: Column<TData, TValue>;
     title?: string;
+    icon?: React.ComponentType<{ className?: string }>;
     options?: DataTableFacetedFilterOption[];
     optionsFn?: () => Promise<DataTableFacetedFilterOption[]>;
 }
@@ -33,6 +34,7 @@ export interface DataTableFacetedFilterProps<TData, TValue> {
 export function DataTableFacetedFilter<TData, TValue>({
     column,
     title,
+    icon,
     options,
     optionsFn,
 }: DataTableFacetedFilterProps<TData, TValue>) {
@@ -70,7 +72,8 @@ export function DataTableFacetedFilter<TData, TValue>({
         <Popover>
             <PopoverTrigger asChild>
                 <Button variant="outline" size="sm" className="h-8 border-dashed">
-                    <PlusCircle />
+                    {icon && <icon />}
+                    {!icon && <FilterIcon />}
                     {title}
                     {selectedValues?.size > 0 && (
                         <>

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

@@ -26,6 +26,7 @@ import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-tab
 
 export interface FacetedFilter {
     title: string;
+    icon?: React.ComponentType<{ className?: string }>;
     optionsFn?: () => Promise<DataTableFacetedFilterOption[]>;
     options?: DataTableFacetedFilterOption[];
 }

+ 14 - 0
packages/dashboard/src/components/shared/detail-page-button.tsx

@@ -0,0 +1,14 @@
+import { Link } from '@tanstack/react-router';
+import { Button } from '../ui/button.js';
+import { SquareArrowOutUpRightIcon } from 'lucide-react';
+
+export function DetailPageButton({ id, label }: { id: string; label: string }) {
+    return (
+        <Button asChild variant="ghost">
+            <Link to={`./${id}`}>
+                {label}
+                <SquareArrowOutUpRightIcon className="h-3 w-3 text-muted-foreground" />
+            </Link>
+        </Button>
+    );
+}

+ 3 - 1
packages/dashboard/src/components/ui/button.tsx

@@ -22,7 +22,9 @@ const buttonVariants = cva(
                 sm: 'h-8 rounded-md px-3 text-xs',
                 lg: 'h-10 rounded-md px-8',
                 icon: 'h-9 w-9',
-                'icon-sm': 'h-7 w-7',
+                xs: 'h-5 rounded-md px-2 text-xs',
+                'icon-sm': 'h-7 w-7 text-xs',
+                'icon-xs': 'h-5 w-5 text-xs',
             },
         },
         defaultVariants: {

+ 3 - 3
packages/dashboard/src/framework/defaults.ts

@@ -126,9 +126,9 @@ navMenu({
                     url: '/stock-locations',
                 },
                 {
-                    id: 'admin-users',
-                    title: 'Admin Users',
-                    url: '/admin-users',
+                    id: 'administrators',
+                    title: 'Administrators',
+                    url: '/administrators',
                 },
                 {
                     id: 'roles',

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

@@ -53,7 +53,7 @@ export function PageTitle({ children }: { children: React.ReactNode }) {
 }
 
 export function PageActionBar({ children }: { children: React.ReactNode }) {
-    return <div className="flex justify-between">{children}</div>;
+    return <div className="flex justify-between gap-2">{children}</div>;
 }
 
 export function PageBlock({ children, title, description, borderless }: PageBlockProps) {

Разница между файлами не показана из-за своего большого размера
+ 660 - 212
packages/dashboard/src/routeTree.gen.ts


+ 38 - 0
packages/dashboard/src/routes/_authenticated/_administrators/administrators.graphql.ts

@@ -0,0 +1,38 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const administratorItemFragment = graphql(`
+    fragment AdministratorItem on Administrator {
+        id
+        createdAt
+        updatedAt
+        firstName
+        lastName
+        emailAddress
+        user {
+            id
+            identifier
+            lastLogin
+            roles {
+                id
+                createdAt
+                updatedAt
+                code
+                description
+            }
+        }
+    }
+`);
+
+export const administratorListQuery = graphql(
+    `
+        query AdministratorList {
+            administrators {
+                items {
+                    ...AdministratorItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [administratorItemFragment],
+);

+ 75 - 0
packages/dashboard/src/routes/_authenticated/_administrators/administrators.tsx

@@ -0,0 +1,75 @@
+import { ListPage } from '@/framework/page/list-page.js';
+import { Link, createFileRoute } from '@tanstack/react-router';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { administratorListQuery } from './administrators.graphql.js';
+import { Trans } from '@lingui/react/macro';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { PlusIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { Badge } from '@/components/ui/badge.js';
+export const Route = createFileRoute('/_authenticated/_administrators/administrators')({
+    component: AdministratorListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Administrators</Trans> }),
+});
+
+function AdministratorListPage() {
+    return (
+        <ListPage
+            title="Administrators"
+            listQuery={addCustomFields(administratorListQuery)}
+            route={Route}
+            onSearchTermChange={searchTerm => {
+                return {
+                    firstName: { contains: searchTerm },
+                    lastName: { contains: searchTerm },
+                    emailAddress: { contains: searchTerm },
+                };
+            }}
+            additionalColumns={[
+                {
+                    id: 'name',
+                    header: 'Name',
+                    cell: ({ row }) => (
+                        <DetailPageButton
+                            id={row.original.id}
+                            label={`${row.original.firstName} ${row.original.lastName}`}
+                        />
+                    ),
+                },
+                {
+                    id: 'roles',
+                    header: 'Roles',
+                    cell: ({ row }) => {
+                        return (
+                            <div className="flex flex-wrap gap-2">
+                                {row.original.user.roles.map(role => {
+                                    return (
+                                        <Badge variant="secondary" key={role.id}>
+                                            {role.code}
+                                        </Badge>
+                                    );
+                                })}
+                            </div>
+                        );
+                    },
+                },
+            ]}
+            defaultVisibility={{
+                emailAddress: true,
+            }}
+        >
+            <PageActionBar>
+                <PermissionGuard requires={['CreateAdministrator']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon />
+                            New Administrator
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 42 - 0
packages/dashboard/src/routes/_authenticated/_channels/channels.graphql.ts

@@ -0,0 +1,42 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const channelItemFragment = graphql(`
+    fragment ChannelItem on Channel {
+        id
+        createdAt
+        updatedAt
+        code
+        token
+        pricesIncludeTax
+        availableCurrencyCodes
+        availableLanguageCodes
+        defaultCurrencyCode
+        defaultLanguageCode
+        defaultShippingZone {
+            id
+            name
+        }
+        defaultTaxZone {
+            id
+            name
+        }
+        seller {
+            id
+            name
+        }
+    }
+`);
+
+export const channelListQuery = graphql(
+    `
+        query ChannelList {
+            channels {
+                items {
+                    ...ChannelItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [channelItemFragment],
+);

+ 53 - 0
packages/dashboard/src/routes/_authenticated/_channels/channels.tsx

@@ -0,0 +1,53 @@
+import { Link, createFileRoute } from '@tanstack/react-router';
+import { channelListQuery } from './channels.graphql.js';
+import { ListPage } from '@/framework/page/list-page.js';
+import { Button } from '@/components/ui/button.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { PlusIcon } from 'lucide-react';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { Trans } from '@lingui/react/macro';
+
+export const Route = createFileRoute('/_authenticated/_channels/channels')({
+    component: ChannelListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Channels</Trans> }),
+});
+
+function ChannelListPage() {
+    return (
+        <ListPage
+            title="Channels"
+            listQuery={addCustomFields(channelListQuery)}
+            route={Route}
+            defaultVisibility={{
+                code: true,
+                token: true,
+            }}
+            onSearchTermChange={searchTerm => {
+                return {
+                    code: { contains: searchTerm },
+                };
+            }}
+            customizeColumns={{
+                code: {
+                    header: 'Code',
+                    cell: ({ row }) => {
+                        return <DetailPageButton id={row.original.id} label={row.original.code} />;
+                    },
+                },
+            }}
+        >
+            <PageActionBar>
+                <PermissionGuard requires={['CreateChannel']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon className="mr-2 h-4 w-4" />
+                            New Channel
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 25 - 0
packages/dashboard/src/routes/_authenticated/_countries/countries.graphql.ts

@@ -0,0 +1,25 @@
+import { graphql } from '@/graphql/graphql.js';
+import { gql } from 'graphql-tag';
+
+export const countryItemFragment = graphql(`
+    fragment CountryItem on Country {
+        id
+        name
+        code
+        enabled
+    }
+`);
+
+export const countriesListQuery = graphql(
+    `
+        query CountriesList {
+            countries {
+                items {
+                    ...CountryItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [countryItemFragment],
+);

+ 66 - 0
packages/dashboard/src/routes/_authenticated/_countries/countries.tsx

@@ -0,0 +1,66 @@
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { Trans } from '@lingui/react/macro';
+import { Link, createFileRoute } from '@tanstack/react-router';
+import { countriesListQuery } from './countries.graphql.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { ListPage } from '@/framework/page/list-page.js';
+import { Badge } from '@/components/ui/badge.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { PlusIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button.js';
+
+export const Route = createFileRoute('/_authenticated/_countries/countries')({
+    component: CountryListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Countries</Trans> }),
+});
+
+function CountryListPage() {
+    return (
+        <ListPage
+            listQuery={addCustomFields(countriesListQuery)}
+            route={Route}
+            title="Countries"
+            defaultVisibility={{
+                name: true,
+                code: true,
+                enabled: true,
+            }}
+            onSearchTermChange={searchTerm => {
+                return {
+                    name: {
+                        contains: searchTerm,
+                    },
+                    code: {
+                        contains: searchTerm,
+                    },
+                };
+            }}
+            customizeColumns={{
+                name: {
+                    header: 'Name',
+                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                },
+                enabled: {
+                    header: 'Enabled',
+                    cell: ({ row }) => (
+                        <Badge variant={row.original.enabled ? 'success' : 'destructive'}>
+                            {row.original.enabled ? 'Enabled' : 'Disabled'}
+                        </Badge>
+                    ),
+                },
+            }}
+        >
+            <PageActionBar>
+                <PermissionGuard requires={['CreateCountry']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon />
+                            <Trans>Add Country</Trans>
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 27 - 0
packages/dashboard/src/routes/_authenticated/_payment-methods/payment-methods.graphql.ts

@@ -0,0 +1,27 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const paymentMethodItemFragment = graphql(`
+    fragment PaymentMethodItem on PaymentMethod {
+        id
+        createdAt
+        updatedAt
+        name
+        description
+        code
+        enabled
+    }
+`);
+
+export const paymentMethodListQuery = graphql(
+    `
+        query PaymentMethodList {
+            paymentMethods {
+                items {
+                    ...PaymentMethodItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [paymentMethodItemFragment],
+);

+ 55 - 0
packages/dashboard/src/routes/_authenticated/_payment-methods/payment-methods.tsx

@@ -0,0 +1,55 @@
+import { Trans } from '@lingui/react/macro';
+import { createFileRoute } from '@tanstack/react-router';
+import { ListPage } from '@/framework/page/list-page.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { paymentMethodListQuery } from './payment-methods.graphql.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { Badge } from '@/components/ui/badge.js';
+
+export const Route = createFileRoute('/_authenticated/_payment-methods/payment-methods')({
+    component: PaymentMethodListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Payment Methods</Trans> }),
+});
+
+function PaymentMethodListPage() {
+    return (
+        <ListPage
+            listQuery={addCustomFields(paymentMethodListQuery)}
+            route={Route}
+            title="Payment Methods"
+            defaultVisibility={{
+                name: true,
+                code: true,
+                enabled: true,
+            }}
+            onSearchTermChange={searchTerm => {
+                return {
+                    name: { contains: searchTerm },
+                };
+            }}
+            facetedFilters={{
+                enabled: {
+                    title: 'Enabled',
+                    options: [
+                        { label: 'Enabled', value: true },
+                        { label: 'Disabled', value: false },
+                    ],
+                },
+            }}
+            customizeColumns={{
+                name: {
+                    header: 'Name',
+                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                },
+                enabled: {
+                    header: 'Enabled',
+                    cell: ({ row }) => (
+                        <Badge variant={row.original.enabled ? 'success' : 'destructive'}>
+                            <Trans>{row.original.enabled ? 'Enabled' : 'Disabled'}</Trans>
+                        </Badge>
+                    ),
+                },
+            }}
+        />
+    );
+}

+ 55 - 0
packages/dashboard/src/routes/_authenticated/_roles/components/expandable-permissions.tsx

@@ -0,0 +1,55 @@
+import { Badge } from '@/components/ui/badge.js';
+import { Button } from '@/components/ui/button.js';
+import {
+    Dialog,
+    DialogContent,
+    DialogDescription,
+    DialogHeader,
+    DialogTitle,
+    DialogTrigger,
+} from '@/components/ui/dialog.js';
+import { ScrollArea } from '@/components/ui/scroll-area.js';
+import { ResultOf } from 'gql.tada';
+import { PlusIcon } from 'lucide-react';
+import { useState } from 'react';
+import { roleItemFragment } from '../roles.graphql.js';
+
+export function ExpandablePermissions({ role }: { role: ResultOf<typeof roleItemFragment> }) {
+    const permissionsToPreview = role.permissions.slice(0, 3);
+
+    return (
+        <div className="flex flex-wrap gap-2 items-center">
+            {permissionsToPreview.map(permission => (
+                <Badge variant={'secondary'} key={permission}>
+                    {permission}
+                </Badge>
+            ))}
+            {role.permissions.length > permissionsToPreview.length && (
+                <Dialog>
+                    <DialogTrigger asChild>
+                        <Button size={'xs'} variant={'secondary'}>
+                            <PlusIcon /> {role.permissions.length - permissionsToPreview.length}
+                        </Button>
+                    </DialogTrigger>
+                    <DialogContent>
+                        <DialogHeader>
+                            <DialogTitle>Permissions of {role.code}</DialogTitle>
+                            <DialogDescription>
+                                {role.permissions.length} permissions in total.
+                            </DialogDescription>
+                        </DialogHeader>
+                        <ScrollArea className="max-h-[300px]">
+                            <div className="flex flex-wrap gap-2">
+                                {role.permissions.map(permission => (
+                                    <Badge variant={'secondary'} key={permission}>
+                                        {permission}
+                                    </Badge>
+                                ))}
+                            </div>
+                        </ScrollArea>
+                    </DialogContent>
+                </Dialog>
+            )}
+        </div>
+    );
+}

+ 31 - 0
packages/dashboard/src/routes/_authenticated/_roles/roles.graphql.ts

@@ -0,0 +1,31 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const roleItemFragment = graphql(`
+    fragment RoleItem on Role {
+        id
+        createdAt
+        updatedAt
+        code
+        description
+        permissions
+        channels {
+            id
+            code
+            token
+        }
+    }
+`);
+
+export const roleListQuery = graphql(
+    `
+        query RoleList {
+            roles {
+                items {
+                    ...RoleItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [roleItemFragment],
+);

+ 79 - 0
packages/dashboard/src/routes/_authenticated/_roles/roles.tsx

@@ -0,0 +1,79 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { Trans } from '@lingui/react/macro';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { roleListQuery } from './roles.graphql.js';
+import { ListPage } from '@/framework/page/list-page.js';
+import { ExpandablePermissions } from './components/expandable-permissions.js';
+import { Badge } from '@/components/ui/badge.js';
+import { LayersIcon, PlusIcon } from 'lucide-react';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { Button } from '@/components/ui/button.js';
+import { Link } from '@tanstack/react-router';
+export const Route = createFileRoute('/_authenticated/_roles/roles')({
+    component: RoleListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Roles</Trans> }),
+});
+
+const SYSTEM_ROLES = ['__super_admin_role__', '__customer_role__'];
+
+function RoleListPage() {
+    return (
+        <ListPage
+            title="Roles"
+            listQuery={addCustomFields(roleListQuery)}
+            route={Route}
+            defaultVisibility={{
+                description: true,
+                code: true,
+                channels: true,
+                permissions: true,
+            }}
+            customizeColumns={{
+                permissions: {
+                    header: 'Permissions',
+                    cell: ({ row }) => {
+                        if (SYSTEM_ROLES.includes(row.original.code)) {
+                            return (
+                                <span className="text-muted-foreground">
+                                    <Trans>This is a default Role and cannot be modified</Trans>
+                                </span>
+                            );
+                        }
+
+                        return <ExpandablePermissions role={row.original} />;
+                    },
+                },
+                channels: {
+                    header: 'Channels',
+                    cell: ({ row }) => {
+                        if (SYSTEM_ROLES.includes(row.original.code)) {
+                            return null;
+                        }
+
+                        return (
+                            <div className="flex flex-wrap gap-2">
+                                {row.original.channels.map(channel => (
+                                    <Badge variant="secondary" key={channel.code}>
+                                        <LayersIcon /> {channel.code}
+                                    </Badge>
+                                ))}
+                            </div>
+                        );
+                    },
+                },
+            }}
+        >
+            <PageActionBar>
+                <PermissionGuard requires={['CreateAdministrator']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon className="mr-2 h-4 w-4" />
+                            New Role
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 24 - 0
packages/dashboard/src/routes/_authenticated/_sellers/sellers.graphql.ts

@@ -0,0 +1,24 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const sellerItemFragment = graphql(`
+    fragment SellerItem on Seller {
+        id
+        createdAt
+        updatedAt
+        name
+    }
+`);
+
+export const sellerListQuery = graphql(
+    `
+        query SellerList {
+            sellers {
+                items {
+                    ...SellerItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [sellerItemFragment],
+);

+ 49 - 0
packages/dashboard/src/routes/_authenticated/_sellers/sellers.tsx

@@ -0,0 +1,49 @@
+import { ListPage } from '@/framework/page/list-page.js';
+import { Trans } from '@lingui/react/macro';
+import { Link, createFileRoute } from '@tanstack/react-router';
+import { sellerListQuery } from './sellers.graphql.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { PlusIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+export const Route = createFileRoute('/_authenticated/_sellers/sellers')({
+    component: SellerListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Sellers</Trans> }),
+});
+
+function SellerListPage() {
+    return (
+        <ListPage
+            listQuery={sellerListQuery}
+            route={Route}
+            title="Sellers"
+            defaultVisibility={{
+                name: true,
+            }}
+            onSearchTermChange={searchTerm => {
+                return {
+                    name: { contains: searchTerm },
+                };
+            }}
+            customizeColumns={{
+                name: {
+                    header: 'Name',
+                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                },
+            }}
+        >
+            <PageActionBar>
+                <div></div>
+                <PermissionGuard requires={['CreateSeller']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon className="mr-2 h-4 w-4" />
+                            New Seller
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 32 - 0
packages/dashboard/src/routes/_authenticated/_shipping-methods/components/test-shipping-method-dialog.tsx

@@ -0,0 +1,32 @@
+import { Button } from '@/components/ui/button.js';
+import {
+    Dialog,
+    DialogContent,
+    DialogDescription,
+    DialogHeader,
+    DialogTitle,
+    DialogTrigger,
+} from '@/components/ui/dialog.js';
+import { Trans } from '@lingui/react/macro';
+import { TestTube } from 'lucide-react';
+
+export function TestShippingMethodDialog() {
+    return (
+        <Dialog>
+            <DialogTrigger asChild>
+                <Button variant="secondary">
+                    <TestTube />
+                    <Trans>Test shipping method</Trans>
+                </Button>
+            </DialogTrigger>
+            <DialogContent className="min-w-[800px]">
+                <DialogHeader>
+                    <DialogTitle>Test shipping method</DialogTitle>
+                    <DialogDescription>
+                        Test your shipping method by simulating a new order.
+                    </DialogDescription>
+                </DialogHeader>
+            </DialogContent>
+        </Dialog>
+    );
+}

+ 27 - 0
packages/dashboard/src/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts

@@ -0,0 +1,27 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const shippingMethodItemFragment = graphql(`
+    fragment ShippingMethodItem on ShippingMethod {
+        id
+        name
+        code
+        description
+        fulfillmentHandlerCode
+        createdAt
+        updatedAt
+    }
+`);
+
+export const shippingMethodListQuery = graphql(
+    `
+        query ShippingMethodList {
+            shippingMethods {
+                items {
+                    ...ShippingMethodItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [shippingMethodItemFragment],
+);

+ 54 - 0
packages/dashboard/src/routes/_authenticated/_shipping-methods/shipping-methods.tsx

@@ -0,0 +1,54 @@
+import { Link, createFileRoute } from '@tanstack/react-router';
+import { Trans } from '@lingui/react/macro';
+import { ListPage } from '@/framework/page/list-page.js';
+import { shippingMethodListQuery } from './shipping-methods.graphql.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { PlusIcon, TestTube } from 'lucide-react';
+import { Button } from '@/components/ui/button.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { TestShippingMethodDialog } from './components/test-shipping-method-dialog.js';
+
+export const Route = createFileRoute('/_authenticated/_shipping-methods/shipping-methods')({
+    component: ShippingMethodListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Shipping Methods</Trans> }),
+});
+
+function ShippingMethodListPage() {
+    return (
+        <ListPage
+            listQuery={addCustomFields(shippingMethodListQuery)}
+            route={Route}
+            title="Shipping Methods"
+            defaultVisibility={{
+                name: true,
+                code: true,
+                fulfillmentHandlerCode: true,
+            }}
+            customizeColumns={{
+                name: {
+                    header: 'Name',
+                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                },
+            }}
+            onSearchTermChange={searchTerm => {
+                return {
+                    name: { contains: searchTerm },
+                };
+            }}
+        >
+            <PageActionBar>
+                <PermissionGuard requires={['CreateShippingMethod']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon className="mr-2 h-4 w-4" />
+                            New Shipping Method
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+                <TestShippingMethodDialog />
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 25 - 0
packages/dashboard/src/routes/_authenticated/_stock-locations/stock-locations.graphql.ts

@@ -0,0 +1,25 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const stockLocationItem = graphql(`
+    fragment StockLocationItem on StockLocation {
+        id
+        createdAt
+        updatedAt
+        name
+        description
+    }
+`);
+
+export const stockLocationListQuery = graphql(
+    `
+        query StockLocationList {
+            stockLocations {
+                items {
+                    ...StockLocationItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [stockLocationItem],
+);

+ 46 - 0
packages/dashboard/src/routes/_authenticated/_stock-locations/stock-locations.tsx

@@ -0,0 +1,46 @@
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { ListPage } from '@/framework/page/list-page.js';
+import { Link, createFileRoute } from '@tanstack/react-router';
+import { stockLocationListQuery } from './stock-locations.graphql.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { PlusIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+import { Trans } from '@lingui/react/macro';
+export const Route = createFileRoute('/_authenticated/_stock-locations/stock-locations')({
+    component: StockLocationListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Stock Locations</Trans> }),
+});
+
+function StockLocationListPage() {
+    return (
+        <ListPage
+            title="Stock Locations"
+            listQuery={addCustomFields(stockLocationListQuery)}
+            route={Route}
+            customizeColumns={{
+                name: {
+                    header: 'Name',
+                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                },
+            }}
+            onSearchTermChange={searchTerm => {
+                return {
+                    name: { contains: searchTerm },
+                };
+            }}
+        >
+            <PageActionBar>
+                <PermissionGuard requires={['CreateStockLocation']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon className="mr-2 h-4 w-4" />
+                            New Stock Location
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 25 - 0
packages/dashboard/src/routes/_authenticated/_tax-categories/tax-categories.graphql.ts

@@ -0,0 +1,25 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const taxCategoryItemFragment = graphql(`
+    fragment TaxCategoryItem on TaxCategory {
+        id
+        createdAt
+        updatedAt
+        name
+        isDefault
+    }
+`);
+
+export const taxCategoryListQuery = graphql(
+    `
+        query TaxCategoryList {
+            taxCategories {
+                items {
+                    ...TaxCategoryItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [taxCategoryItemFragment],
+);

+ 64 - 0
packages/dashboard/src/routes/_authenticated/_tax-categories/tax-categories.tsx

@@ -0,0 +1,64 @@
+import { Link, createFileRoute } from '@tanstack/react-router';
+import { Trans } from '@lingui/react/macro';
+import { ListPage } from '@/framework/page/list-page.js';
+import { taxCategoryListQuery } from './tax-categories.graphql.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { Badge } from '@/components/ui/badge.js';
+import { PlusIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+
+export const Route = createFileRoute('/_authenticated/_tax-categories/tax-categories')({
+    component: TaxCategoryListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Tax Categories</Trans> }),
+});
+
+function TaxCategoryListPage() {
+    return (
+        <ListPage
+            listQuery={addCustomFields(taxCategoryListQuery)}
+            route={Route}
+            title="Tax Categories"
+            defaultVisibility={{
+                name: true,
+                isDefault: true,
+            }}
+            onSearchTermChange={searchTerm => {
+                if (searchTerm === '') {
+                    return {};
+                }
+
+                return {
+                    name: { contains: searchTerm },
+                };
+            }}
+            customizeColumns={{
+                name: {
+                    header: 'Name',
+                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                },
+                isDefault: {
+                    header: 'Default',
+                    cell: ({ row }) => (
+                        <Badge variant={row.original.isDefault ? 'success' : 'destructive'}>
+                            <Trans>{row.original.isDefault ? 'Yes' : 'No'}</Trans>
+                        </Badge>
+                    ),
+                },
+            }}
+        >
+            <PageActionBar>
+                <PermissionGuard requires={['CreateTaxCategory']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon />
+                            <Trans>New Tax Category</Trans>
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 38 - 0
packages/dashboard/src/routes/_authenticated/_tax-rates/tax-rates.graphql.ts

@@ -0,0 +1,38 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const taxRateItemFragment = graphql(`
+    fragment TaxRateItem on TaxRate {
+        id
+        createdAt
+        updatedAt
+        name
+        enabled
+        value
+        category {
+            id
+            name
+        }
+        zone {
+            id
+            name
+        }
+        customerGroup {
+            id
+            name
+        }
+    }
+`);
+
+export const taxRateListQuery = graphql(
+    `
+        query TaxRateList {
+            taxRates {
+                items {
+                    ...TaxRateItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [taxRateItemFragment],
+);

+ 111 - 0
packages/dashboard/src/routes/_authenticated/_tax-rates/tax-rates.tsx

@@ -0,0 +1,111 @@
+import { Link, createFileRoute } from '@tanstack/react-router';
+import { Trans } from '@lingui/react/macro';
+import { ListPage } from '@/framework/page/list-page.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { taxRateListQuery } from './tax-rates.graphql.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { Badge } from '@/components/ui/badge.js';
+import { api } from '@/graphql/api.js';
+import { taxCategoryListQuery } from '../_tax-categories/tax-categories.graphql.js';
+import { zoneListQuery } from '../_zones/zones.graphql.js';
+import { PlusIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button.js';
+import { PermissionGuard } from '@/components/shared/permission-guard.js';
+import { PageActionBar } from '@/framework/layout-engine/page-layout.js';
+
+export const Route = createFileRoute('/_authenticated/_tax-rates/tax-rates')({
+    component: TaxRateListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Tax Rates</Trans> }),
+});
+
+function TaxRateListPage() {
+    return (
+        <ListPage
+            listQuery={addCustomFields(taxRateListQuery)}
+            route={Route}
+            title="Tax Rates"
+            defaultVisibility={{
+                name: true,
+                enabled: true,
+                category: true,
+                zone: true,
+                value: true,
+            }}
+            onSearchTermChange={searchTerm => {
+                if (searchTerm === '') {
+                    return {};
+                }
+
+                return {
+                    name: { contains: searchTerm },
+                };
+            }}
+            facetedFilters={{
+                enabled: {
+                    title: 'Enabled',
+                    options: [
+                        { label: 'Enabled', value: true },
+                        { label: 'Disabled', value: false },
+                    ],
+                },
+                category: {
+                    title: 'Category',
+                    optionsFn: async () => {
+                        const { taxCategories } = await api.query(taxCategoryListQuery);
+                        return taxCategories.items.map(category => ({
+                            label: category.name,
+                            value: category.id,
+                        }));
+                    },
+                },
+                zone: {
+                    title: 'Zone',
+                    optionsFn: async () => {
+                        const { zones } = await api.query(zoneListQuery);
+                        return zones.items.map(zone => ({
+                            label: zone.name,
+                            value: zone.id,
+                        }));
+                    },
+                },
+            }}
+            customizeColumns={{
+                name: {
+                    header: 'Name',
+                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                },
+                enabled: {
+                    header: 'Enabled',
+                    cell: ({ row }) => (
+                        <Badge variant={row.original.enabled ? 'success' : 'destructive'}>
+                            <Trans>{row.original.enabled ? 'Enabled' : 'Disabled'}</Trans>
+                        </Badge>
+                    ),
+                },
+                category: {
+                    header: 'Category',
+                    cell: ({ row }) => row.original.category?.name,
+                },
+                zone: {
+                    header: 'Zone',
+                    cell: ({ row }) => row.original.zone?.name,
+                },
+                value: {
+                    header: 'Value',
+                    cell: ({ row }) => `${row.original.value}%`,
+                },
+            }}
+        >
+            <PageActionBar>
+                <PermissionGuard requires={['CreateTaxRate']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon />
+                            <Trans>New Tax Rate</Trans>
+                        </Link>
+                    </Button>
+                </PermissionGuard>
+            </PageActionBar>
+        </ListPage>
+    );
+}

+ 0 - 0
packages/dashboard/src/routes/_authenticated/_zones/components/zone-countries-add.tsx


+ 71 - 0
packages/dashboard/src/routes/_authenticated/_zones/components/zone-countries-sheet.tsx

@@ -0,0 +1,71 @@
+import { Button } from '@/components/ui/button.js';
+import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet.js';
+import { zoneMembersQuery } from '../zones.graphql.js';
+import { api } from '@/graphql/api.js';
+import { useQuery } from '@tanstack/react-query';
+import { DataTable } from '@/components/data-table/data-table.js';
+import { ColumnDef } from '@tanstack/react-table';
+import { useMemo, useState } from 'react';
+import { ScrollArea } from '@/components/ui/scroll-area.js';
+
+interface ZoneCountriesSheetProps {
+    zoneId: string;
+    zoneName: string;
+    children?: React.ReactNode;
+}
+
+export function ZoneCountriesSheet({ zoneId, zoneName, children }: ZoneCountriesSheetProps) {
+    const { data } = useQuery({
+        queryKey: ['zone', zoneId],
+        queryFn: () => api.query(zoneMembersQuery, { zoneId }),
+    });
+
+    const [page, setPage] = useState(1);
+    const [pageSize, setPageSize] = useState(10);
+
+    const paginatedItems = useMemo(() => {
+        return data?.zone?.members?.slice((page - 1) * pageSize, page * pageSize);
+    }, [data, page, pageSize]);
+
+    const columns: ColumnDef<any>[] = [
+        {
+            header: 'Country',
+            accessorKey: 'name',
+        },
+        {
+            header: 'Enabled',
+            accessorKey: 'enabled',
+        },
+        {
+            header: 'Code',
+            accessorKey: 'code',
+        },
+    ];
+
+    return (
+        <Sheet>
+            <SheetTrigger asChild>
+                <Button variant="outline" size="sm" className="flex items-center gap-2">
+                    {children}
+                </Button>
+            </SheetTrigger>
+            <SheetContent className="min-w-[800px]">
+                <SheetHeader>
+                    <SheetTitle>{zoneName}</SheetTitle>
+                </SheetHeader>
+                <div className="flex items-center gap-2"></div>
+                <ScrollArea className="px-6 max-h-[600px]">
+                    <DataTable
+                        columns={columns}
+                        data={paginatedItems ?? []}
+                        onPageChange={(table, page, itemsPerPage) => {
+                            setPage(page);
+                            setPageSize(itemsPerPage);
+                        }}
+                        totalItems={data?.zone?.members?.length ?? 0}
+                    />
+                </ScrollArea>
+            </SheetContent>
+        </Sheet>
+    );
+}

+ 43 - 0
packages/dashboard/src/routes/_authenticated/_zones/zones.graphql.ts

@@ -0,0 +1,43 @@
+import { graphql } from '@/graphql/graphql.js';
+
+export const zoneItemFragment = graphql(`
+    fragment ZoneItem on Zone {
+        id
+        createdAt
+        updatedAt
+        name
+    }
+`);
+
+export const zoneListQuery = graphql(
+    `
+        query ZoneList {
+            zones {
+                items {
+                    ...ZoneItem
+                }
+                totalItems
+            }
+        }
+    `,
+    [zoneItemFragment],
+);
+
+export const zoneMembersQuery = graphql(`
+    query ZoneMembers($zoneId: ID!) {
+        zone(id: $zoneId) {
+            id
+            createdAt
+            updatedAt
+            name
+            members {
+                createdAt
+                updatedAt
+                id
+                name
+                code
+                enabled
+            }
+        }
+    }
+`);

+ 42 - 0
packages/dashboard/src/routes/_authenticated/_zones/zones.tsx

@@ -0,0 +1,42 @@
+import { Trans } from '@lingui/react/macro';
+import { createFileRoute } from '@tanstack/react-router';
+import { ListPage } from '@/framework/page/list-page.js';
+import { addCustomFields } from '@/framework/document-introspection/add-custom-fields.js';
+import { zoneListQuery } from './zones.graphql.js';
+import { DetailPageButton } from '@/components/shared/detail-page-button.js';
+import { ZoneCountriesSheet } from './components/zone-countries-sheet.js';
+
+export const Route = createFileRoute('/_authenticated/_zones/zones')({
+    component: ZoneListPage,
+    loader: () => ({ breadcrumb: () => <Trans>Zones</Trans> }),
+});
+
+function ZoneListPage() {
+    return (
+        <ListPage
+            listQuery={addCustomFields(zoneListQuery)}
+            route={Route}
+            title="Zones"
+            defaultVisibility={{
+                name: true,
+            }}
+            customizeColumns={{
+                name: {
+                    header: 'Name',
+                    cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
+                },
+            }}
+            additionalColumns={[
+                {
+                    id: 'regions',
+                    header: 'Regions',
+                    cell: ({ row }) => (
+                        <ZoneCountriesSheet zoneId={row.original.id} zoneName={row.original.name}>
+                            <Trans>Edit members</Trans>
+                        </ZoneCountriesSheet>
+                    ),
+                },
+            ]}
+        />
+    );
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов