@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,100 @@
1
+ import { Button, Field, FieldError, FieldGroup, FieldLabel, Input } from '@spree/dashboard-ui'
2
+ import { Link } from '@tanstack/react-router'
3
+ import { useState } from 'react'
4
+ import { useForm } from 'react-hook-form'
5
+ import { useTranslation } from 'react-i18next'
6
+ import { sellerClient } from '../api-client'
7
+
8
+ interface ForgotValues {
9
+ email: string
10
+ }
11
+
12
+ /**
13
+ * Asking for a reset link.
14
+ *
15
+ * Its own surface rather than the dashboard's: the link has to open the seller
16
+ * panel, and a seller sent to the staff dashboard would land on a form that
17
+ * resets them into an admin session they cannot use.
18
+ */
19
+ export function ForgotPasswordPage() {
20
+ const { t } = useTranslation()
21
+ const [sent, setSent] = useState(false)
22
+ const [error, setError] = useState<string | null>(null)
23
+ const form = useForm<ForgotValues>({ defaultValues: { email: '' } })
24
+
25
+ async function onSubmit(values: ForgotValues) {
26
+ setError(null)
27
+ try {
28
+ await sellerClient().auth.requestPasswordReset({
29
+ email: values.email,
30
+ // Where this panel serves its reset page. The server honours it only
31
+ // if the origin is one the marketplace has vouched for, and resolves
32
+ // the panel origin itself otherwise.
33
+ redirect_url: `${window.location.origin}/reset-password`,
34
+ })
35
+ // Confirmed either way — the API never reveals whether the address
36
+ // matched, and neither may this page.
37
+ setSent(true)
38
+ } catch {
39
+ setError(t('forgot_password.failed'))
40
+ }
41
+ }
42
+
43
+ const { errors, isSubmitting } = form.formState
44
+
45
+ if (sent) {
46
+ return (
47
+ <div className="flex min-h-screen items-center justify-center p-6">
48
+ <div className="w-full max-w-sm">
49
+ <h1 className="font-medium text-xl">{t('forgot_password.sent_title')}</h1>
50
+ <p className="mt-1 mb-6 text-muted-foreground text-sm">
51
+ {t('forgot_password.sent_subtitle')}
52
+ </p>
53
+ <Link to="/login" className="text-sm underline underline-offset-4">
54
+ {t('forgot_password.back_to_login')}
55
+ </Link>
56
+ </div>
57
+ </div>
58
+ )
59
+ }
60
+
61
+ return (
62
+ <div className="flex min-h-screen items-center justify-center p-6">
63
+ <div className="w-full max-w-sm">
64
+ <h1 className="font-medium text-xl">{t('forgot_password.title')}</h1>
65
+ <p className="mt-1 mb-6 text-muted-foreground text-sm">{t('forgot_password.subtitle')}</p>
66
+
67
+ <form onSubmit={form.handleSubmit(onSubmit)}>
68
+ <FieldGroup>
69
+ {error && (
70
+ <p className="text-destructive text-sm" role="alert">
71
+ {error}
72
+ </p>
73
+ )}
74
+
75
+ <Field>
76
+ <FieldLabel htmlFor="email">{t('forgot_password.email')}</FieldLabel>
77
+ <Input
78
+ id="email"
79
+ type="email"
80
+ autoComplete="email"
81
+ autoFocus
82
+ aria-invalid={!!errors.email || undefined}
83
+ {...form.register('email', { required: true })}
84
+ />
85
+ <FieldError errors={[errors.email]} />
86
+ </Field>
87
+
88
+ <Button type="submit" disabled={isSubmitting}>
89
+ {isSubmitting ? t('forgot_password.submitting') : t('forgot_password.submit')}
90
+ </Button>
91
+
92
+ <Link to="/login" className="text-sm underline underline-offset-4">
93
+ {t('forgot_password.back_to_login')}
94
+ </Link>
95
+ </FieldGroup>
96
+ </form>
97
+ </div>
98
+ </div>
99
+ )
100
+ }
@@ -0,0 +1,132 @@
1
+ import { PageHeader, progressPercentage } from '@spree/dashboard-core'
2
+ import {
3
+ Button,
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ Progress,
10
+ StatusBadge,
11
+ } from '@spree/dashboard-ui'
12
+ import { ChevronRightIcon, PackageIcon, UsersIcon } from '@spree/dashboard-ui/icons'
13
+ import { useQuery } from '@tanstack/react-query'
14
+ import { Link, useParams } from '@tanstack/react-router'
15
+ import type { ReactNode } from 'react'
16
+ import { useTranslation } from 'react-i18next'
17
+ import { sellerClient } from '../api-client'
18
+ import { CenteredMessage } from '../components/centered-message'
19
+
20
+ /**
21
+ * Where a seller lands: how they stand with the marketplace, what is left to
22
+ * do, and the counts they would otherwise open a page to see.
23
+ *
24
+ * The operator's home is an analytics dashboard — a sales chart and top
25
+ * products. A seller has no analytics endpoint yet (orders are a later
26
+ * phase), and inventing widgets over data that does not exist would be worse
27
+ * than a smaller page that is true. This shows what the panel can actually
28
+ * answer today; the sales card lands with the orders surface.
29
+ */
30
+ export function HomePage() {
31
+ const { t } = useTranslation()
32
+ const { sellerId } = useParams({ from: '/_authenticated/$sellerId' })
33
+
34
+ const { data: profile, isLoading } = useQuery({
35
+ queryKey: ['seller', sellerId, 'profile'],
36
+ queryFn: () => sellerClient().profile.get(),
37
+ })
38
+
39
+ const { data: onboarding } = useQuery({
40
+ queryKey: ['seller', sellerId, 'onboarding'],
41
+ queryFn: () => sellerClient().onboarding.get(),
42
+ })
43
+
44
+ const { data: team } = useQuery({
45
+ queryKey: ['seller', sellerId, 'team'],
46
+ queryFn: () => sellerClient().team.list(),
47
+ })
48
+
49
+ if (isLoading) return <CenteredMessage>{t('common.loading')}</CenteredMessage>
50
+ if (!profile) return <CenteredMessage>{t('common.error')}</CenteredMessage>
51
+
52
+ const progress = onboarding?.progress
53
+ const setupDone = !progress || progress.total === 0 || progress.done === progress.total
54
+
55
+ return (
56
+ <div className="flex flex-col gap-6">
57
+ <PageHeader title={t('home.title', { name: profile.name })} subtitle={t('home.subtitle')} />
58
+
59
+ {/* Setup leads while it is unfinished — until a seller is approved it is
60
+ the only thing on this page that matters. It drops away once done
61
+ rather than sitting at 100% forever. */}
62
+ {!setupDone && progress && (
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle>{t('home.setup_title')}</CardTitle>
66
+ <CardDescription>
67
+ {t('home.setup_description', { done: progress.done, total: progress.total })}
68
+ </CardDescription>
69
+ </CardHeader>
70
+ <CardContent className="flex flex-col gap-4">
71
+ <Progress value={progressPercentage(progress)} />
72
+ <div>
73
+ <Button size="sm" asChild>
74
+ <Link to="/$sellerId/onboarding" params={{ sellerId }}>
75
+ {t('home.setup_cta')}
76
+ <ChevronRightIcon className="size-4" />
77
+ </Link>
78
+ </Button>
79
+ </div>
80
+ </CardContent>
81
+ </Card>
82
+ )}
83
+
84
+ <div className="grid gap-4 sm:grid-cols-3">
85
+ <StatCard label={t('home.status')}>
86
+ <StatusBadge status={profile.status} />
87
+ </StatCard>
88
+ <StatCard label={t('home.products')} icon={<PackageIcon className="size-4" />}>
89
+ {profile.products_count}
90
+ </StatCard>
91
+ <StatCard label={t('home.team')} icon={<UsersIcon className="size-4" />}>
92
+ {team?.data?.length ?? '—'}
93
+ </StatCard>
94
+ </div>
95
+
96
+ <Card>
97
+ <CardHeader>
98
+ <CardTitle>{t('home.selling_title')}</CardTitle>
99
+ </CardHeader>
100
+ <CardContent>
101
+ <p className="text-muted-foreground text-sm">
102
+ {profile.sellable ? t('home.selling_yes') : t('home.selling_no')}
103
+ </p>
104
+ </CardContent>
105
+ </Card>
106
+ </div>
107
+ )
108
+ }
109
+
110
+ function StatCard({
111
+ label,
112
+ icon,
113
+ children,
114
+ }: {
115
+ label: string
116
+ icon?: ReactNode
117
+ children: ReactNode
118
+ }) {
119
+ return (
120
+ <Card>
121
+ <CardHeader className="pb-2">
122
+ <CardDescription className="flex items-center gap-2">
123
+ {icon}
124
+ {label}
125
+ </CardDescription>
126
+ </CardHeader>
127
+ <CardContent>
128
+ <div className="font-medium text-2xl">{children}</div>
129
+ </CardContent>
130
+ </Card>
131
+ )
132
+ }
@@ -0,0 +1,94 @@
1
+ import { useAuth } from '@spree/dashboard-core'
2
+ import { Button, Field, FieldError, FieldGroup, FieldLabel, Input } from '@spree/dashboard-ui'
3
+ import { Link } from '@tanstack/react-router'
4
+ import { useState } from 'react'
5
+ import { useForm } from 'react-hook-form'
6
+ import { useTranslation } from 'react-i18next'
7
+
8
+ interface LoginValues {
9
+ email: string
10
+ password: string
11
+ }
12
+
13
+ /**
14
+ * Sign-in for sellers.
15
+ *
16
+ * Its own surface rather than the admin login: the two read different
17
+ * strategy registries against the same user class, so a marketplace can
18
+ * require SSO for its own staff while sellers still use a password.
19
+ */
20
+ export function LoginPage() {
21
+ const { t } = useTranslation()
22
+ const { login } = useAuth()
23
+ const [error, setError] = useState<string | null>(null)
24
+ const form = useForm<LoginValues>({ defaultValues: { email: '', password: '' } })
25
+
26
+ async function onSubmit(values: LoginValues) {
27
+ setError(null)
28
+ try {
29
+ await login(values.email, values.password)
30
+ // Where to go next is the index route's decision — it already knows how
31
+ // to skip the picker for a seller who runs exactly one seller, and
32
+ // duplicating that here would leave two rules to keep in step.
33
+ } catch {
34
+ // The API refuses a staff member who runs no seller with the same
35
+ // status as a bad password, so the panel says the same thing: it must
36
+ // not become a way to discover which accounts exist.
37
+ setError(t('login.failed'))
38
+ }
39
+ }
40
+
41
+ const { errors, isSubmitting } = form.formState
42
+
43
+ return (
44
+ <div className="flex min-h-screen items-center justify-center p-6">
45
+ <div className="w-full max-w-sm">
46
+ <h1 className="font-medium text-xl">{t('login.title')}</h1>
47
+ <p className="mt-1 mb-6 text-muted-foreground text-sm">{t('login.subtitle')}</p>
48
+
49
+ <form onSubmit={form.handleSubmit(onSubmit)}>
50
+ <FieldGroup>
51
+ {error && (
52
+ <p className="text-destructive text-sm" role="alert">
53
+ {error}
54
+ </p>
55
+ )}
56
+
57
+ <Field>
58
+ <FieldLabel htmlFor="email">{t('login.email')}</FieldLabel>
59
+ <Input
60
+ id="email"
61
+ type="email"
62
+ autoComplete="email"
63
+ autoFocus
64
+ aria-invalid={!!errors.email || undefined}
65
+ {...form.register('email', { required: true })}
66
+ />
67
+ <FieldError errors={[errors.email]} />
68
+ </Field>
69
+
70
+ <Field>
71
+ <FieldLabel htmlFor="password">{t('login.password')}</FieldLabel>
72
+ <Input
73
+ id="password"
74
+ type="password"
75
+ autoComplete="current-password"
76
+ aria-invalid={!!errors.password || undefined}
77
+ {...form.register('password', { required: true })}
78
+ />
79
+ <FieldError errors={[errors.password]} />
80
+ </Field>
81
+
82
+ <Button type="submit" disabled={isSubmitting}>
83
+ {isSubmitting ? t('login.submitting') : t('login.submit')}
84
+ </Button>
85
+
86
+ <Link to="/forgot-password" className="text-sm underline underline-offset-4">
87
+ {t('login.forgot_password')}
88
+ </Link>
89
+ </FieldGroup>
90
+ </form>
91
+ </div>
92
+ </div>
93
+ )
94
+ }