@spree/seller-dashboard 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/vite-plugin/index.js +51 -0
  4. package/package.json +64 -0
  5. package/src/api-client.ts +203 -0
  6. package/src/components/centered-message.tsx +8 -0
  7. package/src/components/finance/balance-summary.tsx +69 -0
  8. package/src/components/onboarding-nav-badge.tsx +38 -0
  9. package/src/components/orders/fulfillment-deliveries.tsx +116 -0
  10. package/src/components/orders/fulfillment-delivery-dialog.tsx +82 -0
  11. package/src/components/orders/fulfillment-edit-dialog.tsx +95 -0
  12. package/src/components/orders/fulfillment-fulfill-form.tsx +132 -0
  13. package/src/components/orders/fulfillment-label-upload-dialog.tsx +95 -0
  14. package/src/components/orders/fulfillment-split-dialog.tsx +142 -0
  15. package/src/components/orders/fulfillments-card.tsx +289 -0
  16. package/src/components/orders/line-label.ts +28 -0
  17. package/src/components/orders/order-cancel-dialog.tsx +74 -0
  18. package/src/components/orders/order-cards.tsx +297 -0
  19. package/src/components/orders/order-earnings-card.tsx +99 -0
  20. package/src/components/orders/order-notes-cards.tsx +83 -0
  21. package/src/components/orders/order-payment-card.tsx +94 -0
  22. package/src/components/orders/post-sale-cards.tsx +347 -0
  23. package/src/components/orders/post-sale-create-dialogs.tsx +201 -0
  24. package/src/components/orders/returns-card.tsx +258 -0
  25. package/src/components/orders/shipping-label-row.tsx +124 -0
  26. package/src/components/product-status-card.tsx +111 -0
  27. package/src/components/read-row.tsx +16 -0
  28. package/src/components/retryable-error.tsx +23 -0
  29. package/src/components/seller-address-card.tsx +123 -0
  30. package/src/components/seller-business-card.tsx +152 -0
  31. package/src/components/seller-returns-location-card.tsx +151 -0
  32. package/src/components/seller-switcher.tsx +124 -0
  33. package/src/components/seller-tax-identifiers-card.tsx +63 -0
  34. package/src/create-router.ts +32 -0
  35. package/src/hooks/use-fulfillments.ts +155 -0
  36. package/src/hooks/use-ledger.ts +46 -0
  37. package/src/hooks/use-order.ts +62 -0
  38. package/src/hooks/use-post-sale.ts +202 -0
  39. package/src/hooks/use-reasons.ts +34 -0
  40. package/src/hooks/use-stock-locations.ts +23 -0
  41. package/src/hooks/use-store-timezone.ts +26 -0
  42. package/src/i18n.ts +26 -0
  43. package/src/index.ts +27 -0
  44. package/src/lib/packing-slip.ts +109 -0
  45. package/src/locales/en.json +841 -0
  46. package/src/nav/default.ts +108 -0
  47. package/src/nav/settings.ts +114 -0
  48. package/src/pages/accept-invitation.tsx +127 -0
  49. package/src/pages/delivery-methods.tsx +647 -0
  50. package/src/pages/earnings.tsx +44 -0
  51. package/src/pages/forgot-password.tsx +100 -0
  52. package/src/pages/home.tsx +132 -0
  53. package/src/pages/login.tsx +94 -0
  54. package/src/pages/onboarding.tsx +539 -0
  55. package/src/pages/order.tsx +119 -0
  56. package/src/pages/orders.tsx +33 -0
  57. package/src/pages/payout.tsx +154 -0
  58. package/src/pages/payouts.tsx +32 -0
  59. package/src/pages/policies.tsx +292 -0
  60. package/src/pages/product.tsx +250 -0
  61. package/src/pages/products.tsx +228 -0
  62. package/src/pages/profile.tsx +389 -0
  63. package/src/pages/reset-password.tsx +105 -0
  64. package/src/pages/seller-picker.tsx +46 -0
  65. package/src/pages/team.tsx +468 -0
  66. package/src/panel-chrome.tsx +36 -0
  67. package/src/routeTree.gen.ts +590 -0
  68. package/src/routes/__root.tsx +15 -0
  69. package/src/routes/_authenticated/$sellerId/earnings/index.tsx +15 -0
  70. package/src/routes/_authenticated/$sellerId/index.tsx +6 -0
  71. package/src/routes/_authenticated/$sellerId/onboarding.tsx +6 -0
  72. package/src/routes/_authenticated/$sellerId/orders/$orderId.tsx +6 -0
  73. package/src/routes/_authenticated/$sellerId/orders/index.tsx +15 -0
  74. package/src/routes/_authenticated/$sellerId/payouts/$payoutId.tsx +7 -0
  75. package/src/routes/_authenticated/$sellerId/payouts/index.tsx +15 -0
  76. package/src/routes/_authenticated/$sellerId/products/$productId.tsx +6 -0
  77. package/src/routes/_authenticated/$sellerId/products/index.tsx +23 -0
  78. package/src/routes/_authenticated/$sellerId/products/new.tsx +6 -0
  79. package/src/routes/_authenticated/$sellerId/profile.tsx +6 -0
  80. package/src/routes/_authenticated/$sellerId/settings/delivery-methods.tsx +14 -0
  81. package/src/routes/_authenticated/$sellerId/settings/index.tsx +18 -0
  82. package/src/routes/_authenticated/$sellerId/settings/package-types.tsx +21 -0
  83. package/src/routes/_authenticated/$sellerId/settings/policies.tsx +14 -0
  84. package/src/routes/_authenticated/$sellerId/settings/stock-locations.tsx +22 -0
  85. package/src/routes/_authenticated/$sellerId/settings/team.tsx +6 -0
  86. package/src/routes/_authenticated/$sellerId/settings.tsx +18 -0
  87. package/src/routes/_authenticated/$sellerId.tsx +49 -0
  88. package/src/routes/_authenticated/index.tsx +48 -0
  89. package/src/routes/_authenticated.tsx +13 -0
  90. package/src/routes/accept-invitation.$invitationId.tsx +19 -0
  91. package/src/routes/forgot-password.tsx +15 -0
  92. package/src/routes/login.tsx +15 -0
  93. package/src/routes/reset-password.tsx +21 -0
  94. package/src/schemas/fulfillment.ts +31 -0
  95. package/src/seller-dashboard.tsx +50 -0
  96. package/src/styles.css +18 -0
  97. package/src/tables/delivery-methods.tsx +68 -0
  98. package/src/tables/orders.tsx +116 -0
  99. package/src/tables/payouts.tsx +87 -0
  100. package/src/tables/policies.tsx +37 -0
  101. package/src/tables/products.tsx +56 -0
  102. package/src/tables/transfers.tsx +118 -0
  103. package/src/vite/index.ts +87 -0
@@ -0,0 +1,87 @@
1
+ import { defineTable, formatStoreDateTime } from '@spree/dashboard-core'
2
+ import { ResourceNameCell, StatusBadge } from '@spree/dashboard-ui'
3
+ import { HandCoinsIcon } from '@spree/dashboard-ui/icons'
4
+ import type { Payout } from '@spree/seller-sdk'
5
+ import i18n from 'i18next'
6
+ import { useStoreTimezone } from '../hooks/use-store-timezone'
7
+
8
+ /**
9
+ * A ledger row's date in the marketplace's timezone.
10
+ *
11
+ * Its own component because a table definition is module-level and has no
12
+ * access to hooks, while `render` is called inside the table's JSX — so a
13
+ * cell may use them like any other component.
14
+ */
15
+ function LedgerDate({ iso }: { iso: string }) {
16
+ const timezone = useStoreTimezone()
17
+
18
+ return <>{formatStoreDateTime(iso, timezone)}</>
19
+ }
20
+
21
+ const STATUSES = ['pending', 'processing', 'completed', 'failed', 'unresolved'] as const
22
+
23
+ defineTable<Payout>('seller-payouts', {
24
+ title: i18n.t('payouts.title'),
25
+ searchParam: 'reference_cont',
26
+ searchPlaceholder: i18n.t('payouts.search_placeholder'),
27
+ defaultSort: { field: 'created_at', direction: 'desc' },
28
+ emptyIcon: <HandCoinsIcon className="size-8 text-muted-foreground" />,
29
+ emptyMessage: i18n.t('payouts.empty'),
30
+ columns: [
31
+ {
32
+ key: 'created_at',
33
+ label: i18n.t('payouts.columns.date'),
34
+ sortable: true,
35
+ filterable: true,
36
+ filterType: 'date',
37
+ quickFilter: true,
38
+ default: true,
39
+ render: (payout) => (
40
+ <ResourceNameCell
41
+ id={payout.id}
42
+ dataAttr="data-payout-id"
43
+ name={<LedgerDate iso={payout.created_at} />}
44
+ />
45
+ ),
46
+ },
47
+ {
48
+ key: 'status',
49
+ label: i18n.t('payouts.columns.status'),
50
+ sortable: true,
51
+ filterable: true,
52
+ filterType: 'enum',
53
+ filterOptions: STATUSES.map((status) => ({
54
+ value: status,
55
+ label: i18n.t(`earnings.statuses.${status}`),
56
+ })),
57
+ quickFilter: true,
58
+ default: true,
59
+ render: (payout) => (
60
+ <StatusBadge
61
+ status={payout.status}
62
+ label={i18n.t(`earnings.statuses.${payout.status}`, { defaultValue: payout.status })}
63
+ />
64
+ ),
65
+ },
66
+ {
67
+ key: 'transfers_count',
68
+ label: i18n.t('payouts.columns.earnings'),
69
+ default: true,
70
+ render: (payout) => payout.transfers_count,
71
+ },
72
+ {
73
+ key: 'reference',
74
+ label: i18n.t('payouts.columns.reference'),
75
+ default: true,
76
+ render: (payout) => payout.reference ?? '—',
77
+ },
78
+ {
79
+ key: 'amount',
80
+ label: i18n.t('payouts.columns.amount'),
81
+ sortable: true,
82
+ default: true,
83
+ className: 'text-right tabular-nums',
84
+ render: (payout) => payout.display_amount,
85
+ },
86
+ ],
87
+ })
@@ -0,0 +1,37 @@
1
+ import { defineTable } from '@spree/dashboard-core'
2
+ import { RelativeTime, ResourceNameCell } from '@spree/dashboard-ui'
3
+ import { ScrollTextIcon } from '@spree/dashboard-ui/icons'
4
+ import type { Policy } from '@spree/seller-sdk'
5
+ import i18n from 'i18next'
6
+
7
+ defineTable<Policy>('seller-policies', {
8
+ title: i18n.t('policies.title'),
9
+ searchParam: 'name_cont',
10
+ searchPlaceholder: i18n.t('policies.search_placeholder'),
11
+ defaultSort: { field: 'name', direction: 'asc' },
12
+ emptyIcon: <ScrollTextIcon className="size-8 text-muted-foreground" />,
13
+ emptyMessage: i18n.t('policies.empty.title'),
14
+ columns: [
15
+ {
16
+ key: 'name',
17
+ label: i18n.t('policies.columns.name'),
18
+ sortable: true,
19
+ default: true,
20
+ render: (policy) => (
21
+ <ResourceNameCell
22
+ id={policy.id}
23
+ dataAttr="data-policy-id"
24
+ name={policy.name}
25
+ secondary={policy.slug}
26
+ />
27
+ ),
28
+ },
29
+ {
30
+ key: 'updated_at',
31
+ label: i18n.t('policies.columns.updated'),
32
+ sortable: true,
33
+ default: true,
34
+ render: (policy) => <RelativeTime iso={policy.updated_at} />,
35
+ },
36
+ ],
37
+ })
@@ -0,0 +1,56 @@
1
+ import { defineTable } from '@spree/dashboard-core'
2
+ import { ResourceNameCell, StatusBadge, Thumbnail } from '@spree/dashboard-ui'
3
+ import { PackageIcon } from '@spree/dashboard-ui/icons'
4
+ import type { Product } from '@spree/seller-sdk'
5
+ import i18n from 'i18next'
6
+
7
+ /** Mirrors the operator dashboard's product statuses. */
8
+ const PRODUCT_STATUSES = ['active', 'draft', 'proposed', 'rejected', 'archived'] as const
9
+
10
+ defineTable<Product>('seller-products', {
11
+ title: i18n.t('products.title'),
12
+ searchParam: 'name_cont',
13
+ searchPlaceholder: i18n.t('products.search_placeholder'),
14
+ defaultSort: { field: 'created_at', direction: 'desc' },
15
+ emptyIcon: <PackageIcon className="size-8 text-muted-foreground" />,
16
+ emptyMessage: i18n.t('products.empty'),
17
+ columns: [
18
+ {
19
+ key: 'name',
20
+ label: i18n.t('products.columns.name'),
21
+ sortable: true,
22
+ default: true,
23
+ render: (product) => (
24
+ <div className="flex items-center gap-3">
25
+ <Thumbnail src={product.thumbnail_url} alt={product.name} />
26
+ <ResourceNameCell id={product.id} dataAttr="data-product-id" name={product.name} />
27
+ </div>
28
+ ),
29
+ },
30
+ {
31
+ key: 'status',
32
+ label: i18n.t('products.columns.status'),
33
+ sortable: true,
34
+ filterable: true,
35
+ filterType: 'enum',
36
+ filterOptions: PRODUCT_STATUSES.map((status) => ({
37
+ value: status,
38
+ label: i18n.t(`products.statuses.${status}`),
39
+ })),
40
+ quickFilter: true,
41
+ default: true,
42
+ render: (product) => (
43
+ <StatusBadge
44
+ status={product.status}
45
+ label={i18n.t(`products.statuses.${product.status}`)}
46
+ />
47
+ ),
48
+ },
49
+ {
50
+ key: 'price',
51
+ label: i18n.t('products.columns.price'),
52
+ default: true,
53
+ render: (product) => product.price?.display_amount ?? '—',
54
+ },
55
+ ],
56
+ })
@@ -0,0 +1,118 @@
1
+ import { defineTable, formatStoreDateTime, useTenantId } from '@spree/dashboard-core'
2
+ import { StatusBadge } from '@spree/dashboard-ui'
3
+ import { BanknoteIcon } from '@spree/dashboard-ui/icons'
4
+ import type { Transfer } from '@spree/seller-sdk'
5
+ import { Link } from '@tanstack/react-router'
6
+ import i18n from 'i18next'
7
+ import { useStoreTimezone } from '../hooks/use-store-timezone'
8
+
9
+ /**
10
+ * The sale behind an earning — what a seller checking a figure wants next.
11
+ *
12
+ * Its own component because a table definition is module-level and holds no
13
+ * router context, while the link needs the seller whose panel this is. That
14
+ * is what `useTenantId` answers, and a cell is rendered inside React like any
15
+ * other component.
16
+ */
17
+ function OrderCell({ transfer }: { transfer: Transfer }) {
18
+ const sellerId = useTenantId()
19
+
20
+ if (!transfer.order_id) return <>—</>
21
+
22
+ return (
23
+ <Link
24
+ to="/$sellerId/orders/$orderId"
25
+ params={{ sellerId, orderId: transfer.order_id }}
26
+ className="no-underline"
27
+ >
28
+ {transfer.order_number ?? transfer.order_id}
29
+ </Link>
30
+ )
31
+ }
32
+
33
+ /**
34
+ * A ledger row's date in the marketplace's timezone.
35
+ *
36
+ * Its own component because a table definition is module-level and has no
37
+ * access to hooks, while `render` is called inside the table's JSX — so a
38
+ * cell may use them like any other component.
39
+ */
40
+ function LedgerDate({ iso }: { iso: string }) {
41
+ const timezone = useStoreTimezone()
42
+
43
+ return <>{formatStoreDateTime(iso, timezone)}</>
44
+ }
45
+
46
+ const STATUSES = ['pending', 'processing', 'completed', 'failed', 'unresolved'] as const
47
+ const KINDS = ['earning', 'refund_reversal'] as const
48
+
49
+ defineTable<Transfer>('seller-transfers', {
50
+ title: i18n.t('earnings.transfers.title'),
51
+ // Without this the toolbar sends `name_cont`, which the ledger does not
52
+ // whitelist — Ransack drops an unknown condition, so the box would look
53
+ // like it worked and return everything.
54
+ searchParam: 'reference_cont',
55
+ searchPlaceholder: i18n.t('earnings.search_placeholder'),
56
+ defaultSort: { field: 'created_at', direction: 'desc' },
57
+ emptyIcon: <BanknoteIcon className="size-8 text-muted-foreground" />,
58
+ emptyMessage: i18n.t('earnings.transfers.empty'),
59
+ columns: [
60
+ {
61
+ key: 'created_at',
62
+ label: i18n.t('earnings.columns.date'),
63
+ sortable: true,
64
+ filterable: true,
65
+ filterType: 'date',
66
+ quickFilter: true,
67
+ default: true,
68
+ render: (transfer) => <LedgerDate iso={transfer.created_at} />,
69
+ },
70
+ {
71
+ key: 'order_number',
72
+ label: i18n.t('earnings.columns.order'),
73
+ default: true,
74
+ render: (transfer) => <OrderCell transfer={transfer} />,
75
+ },
76
+ {
77
+ key: 'kind',
78
+ label: i18n.t('earnings.columns.kind'),
79
+ filterable: true,
80
+ filterType: 'enum',
81
+ filterOptions: KINDS.map((kind) => ({
82
+ value: kind,
83
+ label: i18n.t(`earnings.kinds.${kind}`),
84
+ })),
85
+ quickFilter: true,
86
+ default: true,
87
+ render: (transfer) =>
88
+ i18n.t(`earnings.kinds.${transfer.kind}`, { defaultValue: transfer.kind }),
89
+ },
90
+ {
91
+ key: 'status',
92
+ label: i18n.t('earnings.columns.status'),
93
+ sortable: true,
94
+ filterable: true,
95
+ filterType: 'enum',
96
+ filterOptions: STATUSES.map((status) => ({
97
+ value: status,
98
+ label: i18n.t(`earnings.statuses.${status}`),
99
+ })),
100
+ quickFilter: true,
101
+ default: true,
102
+ render: (transfer) => (
103
+ <StatusBadge
104
+ status={transfer.status}
105
+ label={i18n.t(`earnings.statuses.${transfer.status}`, { defaultValue: transfer.status })}
106
+ />
107
+ ),
108
+ },
109
+ {
110
+ key: 'amount',
111
+ label: i18n.t('earnings.columns.amount'),
112
+ sortable: true,
113
+ default: true,
114
+ className: 'text-right tabular-nums',
115
+ render: (transfer) => transfer.display_amount,
116
+ },
117
+ ],
118
+ })
@@ -0,0 +1,87 @@
1
+ import { createRequire } from 'node:module'
2
+ import path from 'node:path'
3
+ import {
4
+ type SpreeDashboardPluginOptions as CoreOptions,
5
+ spreeDashboardPlugin as spreeDashboardCorePlugin,
6
+ } from '@spree/dashboard-core/vite'
7
+ import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
8
+ import { index, layout, physical, rootRoute, route } from '@tanstack/virtual-file-routes'
9
+ import type { PluginOption } from 'vite'
10
+
11
+ export interface SpreeSellerDashboardPluginOptions extends CoreOptions {
12
+ /**
13
+ * Where the composed route tree is generated, relative to the host root.
14
+ * Commit this file: it regenerates on every dev start/build, and its diff
15
+ * on a `@spree/seller-dashboard` upgrade shows exactly which pages the
16
+ * upgrade added or moved.
17
+ */
18
+ generatedRouteTree?: string
19
+ /**
20
+ * Host project root. Defaults to `process.cwd()` — correct when Vite runs
21
+ * from the package directory, which is how the starter operates.
22
+ */
23
+ root?: string
24
+ }
25
+
26
+ /**
27
+ * Vite integration for the seller panel.
28
+ *
29
+ * The Tailwind and plugin-discovery half comes from `@spree/dashboard-core`
30
+ * unchanged — neither is admin-specific. Only the route skeleton differs,
31
+ * because a seller's panel is scoped by seller where the operator's is scoped
32
+ * by store.
33
+ */
34
+ export function spreeSellerDashboardPlugin(
35
+ options: SpreeSellerDashboardPluginOptions = {},
36
+ ): PluginOption[] {
37
+ const hostRoot = options.root ?? process.cwd()
38
+ return [
39
+ // This shell is always scanned, whatever the host passes. Core defaults to
40
+ // the operator's dashboard, which a seller's host does not install — and a
41
+ // host naming its own shell is adding one, not replacing the panel whose
42
+ // components it renders. Overwriting instead of merging drops the panel's
43
+ // classes from the production CSS, and only in a built bundle.
44
+ ...spreeDashboardCorePlugin({
45
+ ...options,
46
+ shellPackages: ['@spree/seller-dashboard', ...(options.shellPackages ?? [])],
47
+ }),
48
+ sellerRouterPlugin(hostRoot, options),
49
+ ]
50
+ }
51
+
52
+ /**
53
+ * The TanStack Router generator, configured with a virtual route config
54
+ * mirroring the panel's layout skeleton: public sign-in, then an
55
+ * authenticated branch whose pages all hang off a seller.
56
+ */
57
+ function sellerRouterPlugin(hostRoot: string, options: SpreeSellerDashboardPluginOptions) {
58
+ const fromHost = createRequire(path.join(hostRoot, 'package.json'))
59
+ // Resolve the shell's routes directory from wherever the host's
60
+ // `@spree/seller-dashboard` lives (workspace symlink or npm install). The
61
+ // `.` export maps to a file in src/, so `routes/` is its sibling.
62
+ const shellEntry = fromHost.resolve('@spree/seller-dashboard')
63
+ const shellRoutesDir = path.join(path.dirname(shellEntry), 'routes')
64
+
65
+ const virtualRouteConfig = rootRoute('__root.tsx', [
66
+ route('/login', 'login.tsx'),
67
+ route('/forgot-password', 'forgot-password.tsx'),
68
+ route('/reset-password', 'reset-password.tsx'),
69
+ route('/accept-invitation/$invitationId', 'accept-invitation.$invitationId.tsx'),
70
+ layout('_authenticated.tsx', [
71
+ index('_authenticated/index.tsx'),
72
+ route('/$sellerId', '_authenticated/$sellerId.tsx', [
73
+ physical('', '_authenticated/$sellerId'),
74
+ ]),
75
+ ]),
76
+ ])
77
+
78
+ return TanStackRouterVite({
79
+ target: 'react',
80
+ virtualRouteConfig,
81
+ routesDirectory: shellRoutesDir,
82
+ generatedRouteTree: path.resolve(
83
+ hostRoot,
84
+ options.generatedRouteTree ?? 'src/routeTree.gen.ts',
85
+ ),
86
+ })
87
+ }