@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,108 @@
1
+ import { hasVisibleSettingsEntries, i18n, nav } from '@spree/dashboard-core'
2
+ import {
3
+ BanknoteIcon,
4
+ ClipboardCheckIcon,
5
+ HandCoinsIcon,
6
+ HomeIcon,
7
+ PackageIcon,
8
+ SettingsIcon,
9
+ StoreIcon,
10
+ TagIcon,
11
+ } from '@spree/dashboard-ui/icons'
12
+ import { OnboardingNavBadge } from '../components/onboarding-nav-badge'
13
+
14
+ // The panel's built-in sidebar. Registered into the same registry a plugin
15
+ // writes to, so a marketplace can remove, reorder or add beside these with
16
+ // `defineDashboardPlugin({ nav: ... })` instead of forking the chrome.
17
+ //
18
+ // Positions leave room on either side (100/200/…) exactly as the operator's
19
+ // dashboard does, so a plugin slots between built-ins without renumbering.
20
+ //
21
+ // Subjects are the seller-branch CanCanCan subjects the API's `/me`
22
+ // serializes; an entry a member cannot act on is hidden by the same
23
+ // permission filter the operator's sidebar uses.
24
+
25
+ nav.add({
26
+ key: 'home',
27
+ label: i18n.t('nav.home'),
28
+ path: '/',
29
+ icon: HomeIcon,
30
+ position: 50,
31
+ subject: 'seller_profile',
32
+ })
33
+
34
+ // High in the rail while a seller is still being admitted: until they are
35
+ // approved it is the only thing that matters. A marketplace that admits
36
+ // sellers some other way removes it like any other entry.
37
+ nav.add({
38
+ key: 'onboarding',
39
+ label: i18n.t('nav.onboarding'),
40
+ path: '/onboarding',
41
+ icon: ClipboardCheckIcon,
42
+ position: 100,
43
+ subject: 'seller_profile',
44
+ badge: OnboardingNavBadge,
45
+ })
46
+
47
+ nav.add({
48
+ key: 'products',
49
+ label: i18n.t('nav.products'),
50
+ path: '/products',
51
+ icon: TagIcon,
52
+ position: 110,
53
+ subject: 'Spree::Product',
54
+ })
55
+
56
+ nav.add({
57
+ key: 'orders',
58
+ label: i18n.t('nav.orders'),
59
+ path: '/orders',
60
+ icon: PackageIcon,
61
+ position: 120,
62
+ subject: 'Spree::Order',
63
+ })
64
+
65
+ // The seller's own books. Two entries rather than one because they answer
66
+ // different questions: what an order earned, and whether last week's
67
+ // settlement went out. Both gated on the ledger key, so a teammate given
68
+ // orders but not money sees neither.
69
+ nav.add({
70
+ key: 'earnings',
71
+ label: i18n.t('nav.earnings'),
72
+ path: '/earnings',
73
+ icon: BanknoteIcon,
74
+ position: 130,
75
+ subject: 'seller_earnings',
76
+ })
77
+
78
+ nav.add({
79
+ key: 'payouts',
80
+ label: i18n.t('nav.payouts'),
81
+ path: '/payouts',
82
+ icon: HandCoinsIcon,
83
+ position: 140,
84
+ subject: 'seller_earnings',
85
+ })
86
+
87
+ nav.add({
88
+ key: 'profile',
89
+ label: i18n.t('nav.profile'),
90
+ path: '/profile',
91
+ icon: StoreIcon,
92
+ position: 150,
93
+ subject: 'seller_profile',
94
+ })
95
+
96
+ // Settings opens the landing page (and the secondary rail on wide viewports).
97
+ // The entries inside it live in `nav/settings.ts`. Bottom section and gated
98
+ // on there being something to see, exactly as the operator's dashboard does
99
+ // it — a member with no settings authority never opens an empty shell.
100
+ nav.add({
101
+ key: 'settings',
102
+ label: i18n.t('nav.settings'),
103
+ path: '/settings',
104
+ icon: SettingsIcon,
105
+ if: ({ permissions }) => hasVisibleSettingsEntries(permissions),
106
+ section: 'bottom',
107
+ position: 100,
108
+ })
@@ -0,0 +1,114 @@
1
+ import { i18n, settingsNav } from '@spree/dashboard-core'
2
+ import {
3
+ PackageIcon,
4
+ ScrollTextIcon,
5
+ TruckIcon,
6
+ UsersRoundIcon,
7
+ WarehouseIcon,
8
+ } from '@spree/dashboard-ui/icons'
9
+
10
+ // The panel's settings rail, registered into the same registry the operator's
11
+ // dashboard writes to — so a marketplace adds, removes or reorders entries
12
+ // with `defineDashboardPlugin` rather than forking the chrome.
13
+ //
14
+ // Deliberately short. Most of what an operator configures is the
15
+ // marketplace's, not a seller's; what belongs here is what a seller owns and
16
+ // only they can answer.
17
+
18
+ settingsNav.addGroup({
19
+ key: 'fulfillment',
20
+ labelKey: 'admin.settings_nav.groups.fulfillment',
21
+ position: 200,
22
+ })
23
+
24
+ settingsNav.addGroup({
25
+ key: 'legal',
26
+ label: i18n.t('nav.legal'),
27
+ position: 250,
28
+ })
29
+
30
+ settingsNav.addGroup({
31
+ key: 'team',
32
+ labelKey: 'admin.settings_nav.groups.team',
33
+ position: 300,
34
+ })
35
+
36
+ settingsNav.add({
37
+ key: 'settings.stock-locations',
38
+ labelKey: 'admin.settings_nav.items.stock_locations',
39
+ descriptionKey: 'admin.settings_nav.descriptions.stock_locations',
40
+ keywords: ['warehouse', 'inventory', 'fulfillment', 'pickup'],
41
+ path: '/stock-locations',
42
+ icon: WarehouseIcon,
43
+ group: 'fulfillment',
44
+ position: 100,
45
+ // The seller-branch subject the API's `/me` serializes. A member without
46
+ // stock permission never sees the entry.
47
+ subject: 'Spree::StockLocation',
48
+ })
49
+
50
+ // How this seller's goods ship. Beside stock locations because the two are
51
+ // one question asked twice — where the goods are, and how they leave.
52
+ settingsNav.add({
53
+ key: 'settings.delivery-methods',
54
+ label: i18n.t('nav.delivery_methods'),
55
+ descriptionKey: 'admin.settings_nav.descriptions.delivery_methods',
56
+ keywords: ['shipping', 'rates', 'carriers', 'delivery'],
57
+ path: '/delivery-methods',
58
+ icon: TruckIcon,
59
+ group: 'fulfillment',
60
+ position: 200,
61
+ // The seller-branch subject the API's `/me` serializes. A member without
62
+ // delivery permission never sees the entry.
63
+ subject: 'Spree::DeliveryMethod',
64
+ })
65
+
66
+ // What this seller packs their goods into. Beside the other two because the
67
+ // three answer one question together: where the goods are, how they leave,
68
+ // and what they leave in (docs/plans/6.0-seller-package-types.md).
69
+ settingsNav.add({
70
+ key: 'settings.package-types',
71
+ label: i18n.t('nav.package_types'),
72
+ descriptionKey: 'admin.settings_nav.descriptions.package_types',
73
+ keywords: ['box', 'carton', 'pallet', 'container', 'packaging', 'parcel'],
74
+ path: '/package-types',
75
+ icon: PackageIcon,
76
+ group: 'fulfillment',
77
+ position: 300,
78
+ // The seller-branch subject the API's `/me` serializes. A member without
79
+ // packaging permission never sees the entry.
80
+ subject: 'Spree::PackageType',
81
+ })
82
+
83
+ // Who runs this seller. Under settings rather than in the main rail, matching
84
+ // where the operator's dashboard puts its own staff page: managing people is
85
+ // configuration, not day-to-day selling.
86
+ settingsNav.add({
87
+ key: 'settings.team',
88
+ label: i18n.t('nav.team'),
89
+ descriptionKey: 'admin.settings_nav.descriptions.team',
90
+ keywords: ['users', 'staff', 'invite', 'members'],
91
+ path: '/team',
92
+ icon: UsersRoundIcon,
93
+ group: 'team',
94
+ position: 100,
95
+ subject: 'seller_profile',
96
+ action: 'update',
97
+ })
98
+
99
+ // The seller's own legal documents. Under settings because publishing them is
100
+ // configuration a seller does once, not day-to-day selling — and because what
101
+ // they must publish is the marketplace's onboarding checklist, which links
102
+ // here.
103
+ settingsNav.add({
104
+ key: 'settings.policies',
105
+ label: i18n.t('nav.policies'),
106
+ descriptionKey: 'admin.settings_nav.descriptions.policies',
107
+ keywords: ['legal', 'terms', 'privacy', 'returns', 'refund'],
108
+ path: '/policies',
109
+ icon: ScrollTextIcon,
110
+ group: 'legal',
111
+ position: 100,
112
+ subject: 'seller_profile',
113
+ action: 'update',
114
+ })
@@ -0,0 +1,127 @@
1
+ import { useAuth } from '@spree/dashboard-core'
2
+ import { Button, Field, FieldError, FieldGroup, FieldLabel, Input } from '@spree/dashboard-ui'
3
+ import { useQuery } from '@tanstack/react-query'
4
+ import { useNavigate } from '@tanstack/react-router'
5
+ import { useState } from 'react'
6
+ import { useForm } from 'react-hook-form'
7
+ import { useTranslation } from 'react-i18next'
8
+ import { sellerClient } from '../api-client'
9
+ import { CenteredMessage } from '../components/centered-message'
10
+
11
+ interface AcceptValues {
12
+ password: string
13
+ password_confirmation: string
14
+ first_name: string
15
+ last_name: string
16
+ }
17
+
18
+ /**
19
+ * Joining a seller's team from the emailed link.
20
+ *
21
+ * The invitation id and token in the URL are the whole credential — nobody is
22
+ * signed in yet. Accepting returns a seller session, so the new member lands
23
+ * in the panel rather than being bounced to a login form.
24
+ */
25
+ export function AcceptInvitationPage({
26
+ invitationId,
27
+ token,
28
+ }: {
29
+ invitationId: string
30
+ token?: string
31
+ }) {
32
+ const { t } = useTranslation()
33
+ const { acceptInvitation } = useAuth()
34
+ const navigate = useNavigate()
35
+ const [error, setError] = useState<string | null>(null)
36
+
37
+ const {
38
+ data: invitation,
39
+ isLoading,
40
+ error: lookupError,
41
+ } = useQuery({
42
+ queryKey: ['seller', 'invitation', invitationId],
43
+ queryFn: () => sellerClient().auth.lookupInvitation(invitationId, token ?? ''),
44
+ enabled: Boolean(token),
45
+ retry: false,
46
+ })
47
+
48
+ const form = useForm<AcceptValues>({
49
+ defaultValues: { password: '', password_confirmation: '', first_name: '', last_name: '' },
50
+ })
51
+
52
+ if (!token) return <CenteredMessage>{t('accept_invitation.invalid_link')}</CenteredMessage>
53
+ if (isLoading) return <CenteredMessage>{t('common.loading')}</CenteredMessage>
54
+ if (lookupError || !invitation) {
55
+ return <CenteredMessage>{t('accept_invitation.not_found')}</CenteredMessage>
56
+ }
57
+
58
+ async function onSubmit(values: AcceptValues) {
59
+ setError(null)
60
+ try {
61
+ await acceptInvitation(invitationId, token ?? '', values)
62
+ // The index route decides where to go from there — it already knows how
63
+ // to skip the picker for someone who runs exactly one seller. Without
64
+ // this the form simply sits there after a successful accept, and the
65
+ // obvious next move is to submit again, against an invitation that has
66
+ // now been consumed.
67
+ navigate({ to: '/', replace: true })
68
+ } catch {
69
+ setError(t('accept_invitation.failed'))
70
+ }
71
+ }
72
+
73
+ const { errors, isSubmitting } = form.formState
74
+
75
+ return (
76
+ <div className="flex min-h-screen items-center justify-center p-6">
77
+ <div className="w-full max-w-sm">
78
+ <h1 className="font-medium text-xl">{t('accept_invitation.title')}</h1>
79
+ <p className="mt-1 mb-6 text-muted-foreground text-sm">
80
+ {t('accept_invitation.subtitle', { email: invitation.email })}
81
+ </p>
82
+
83
+ <form onSubmit={form.handleSubmit(onSubmit)}>
84
+ <FieldGroup>
85
+ <Field>
86
+ <FieldLabel htmlFor="first_name">{t('accept_invitation.first_name')}</FieldLabel>
87
+ <Input id="first_name" {...form.register('first_name')} />
88
+ </Field>
89
+
90
+ <Field>
91
+ <FieldLabel htmlFor="last_name">{t('accept_invitation.last_name')}</FieldLabel>
92
+ <Input id="last_name" {...form.register('last_name')} />
93
+ </Field>
94
+
95
+ <Field>
96
+ <FieldLabel htmlFor="password">{t('accept_invitation.password')}</FieldLabel>
97
+ <Input
98
+ id="password"
99
+ type="password"
100
+ aria-invalid={!!errors.password || undefined}
101
+ {...form.register('password', { required: true })}
102
+ />
103
+ <FieldError errors={[errors.password]} />
104
+ </Field>
105
+
106
+ <Field>
107
+ <FieldLabel htmlFor="password_confirmation">
108
+ {t('accept_invitation.password_confirmation')}
109
+ </FieldLabel>
110
+ <Input
111
+ id="password_confirmation"
112
+ type="password"
113
+ {...form.register('password_confirmation')}
114
+ />
115
+ </Field>
116
+
117
+ {error && <p className="text-destructive text-sm">{error}</p>}
118
+
119
+ <Button type="submit" disabled={isSubmitting}>
120
+ {isSubmitting ? t('accept_invitation.submitting') : t('accept_invitation.submit')}
121
+ </Button>
122
+ </FieldGroup>
123
+ </form>
124
+ </div>
125
+ </div>
126
+ )
127
+ }