@burdenoff/microfe-store 2026.918.3 → 2026.1013.2
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.
- package/dist/pages/AdminFulfillmentLogsPage.js +1 -1
- package/dist/pages/AdminFulfillmentLogsPage.js.map +1 -1
- package/dist/pages/AdminOrdersPage.js +1 -1
- package/dist/pages/AdminOrdersPage.js.map +1 -1
- package/dist/pages/AdminStoresPage.js +1 -1
- package/dist/pages/AdminStoresPage.js.map +1 -1
- package/dist/pages/CreateSourcingRequestPage.js +1 -1
- package/dist/pages/CreateSourcingRequestPage.js.map +1 -1
- package/dist/pages/SellerSourcingOffersPage.js +1 -1
- package/dist/pages/SellerSourcingOffersPage.js.map +1 -1
- package/dist/pages/SellerSourcingOpportunityDetailPage.js +1 -1
- package/dist/pages/SellerSourcingOpportunityDetailPage.js.map +1 -1
- package/dist/pages/SourcingRequestsPage.js +1 -1
- package/dist/pages/SourcingRequestsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -116,7 +116,7 @@ var x = () => {
|
|
|
116
116
|
onChange: (e) => {
|
|
117
117
|
I(e.target.value), P(0);
|
|
118
118
|
},
|
|
119
|
-
className: "rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary",
|
|
119
|
+
className: "rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
120
120
|
children: _.map((e) => /* @__PURE__ */ p("option", {
|
|
121
121
|
value: e.value,
|
|
122
122
|
children: e.label
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminFulfillmentLogsPage.js","names":[],"sources":["../../src/pages/AdminFulfillmentLogsPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { ArrowLeft, RefreshCw, Truck, CheckCircle2, XCircle } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\ninterface FulfillmentAttempt {\n id: string;\n attemptNumber: number;\n carrier: string;\n succeeded: boolean;\n errorMessage: string | null;\n errorStatusCode: number | null;\n durationMs: number | null;\n occurredAt: string;\n}\n\nconst SUCCEEDED_OPTIONS: { value: '' | 'true' | 'false'; label: string }[] = [\n { value: '', label: 'All results' },\n { value: 'false', label: 'Failed only' },\n { value: 'true', label: 'Succeeded only' },\n];\n\nconst ORDER_FULFILLMENT_ATTEMPTS_QUERY = /* GraphQL */ `\n query AdminOrderFulfillmentAttempts(\n $limit: Int!\n $offset: Int!\n $orderId: ID\n $carrier: String\n $succeeded: Boolean\n ) {\n orderFulfillmentAttempts(\n limit: $limit\n offset: $offset\n orderId: $orderId\n carrier: $carrier\n succeeded: $succeeded\n ) {\n totalCount\n attempts {\n id\n attemptNumber\n carrier\n succeeded\n errorMessage\n errorStatusCode\n durationMs\n occurredAt\n }\n }\n }\n`;\n\nconst PAGE_SIZE = 25;\n\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n });\n}\n\n/**\n * Admin-only log of every carrier shipment-creation attempt (every Temporal\n * retry, not just the last failure) — the \"why did this order's shipment\n * fail\" debugging surface that OrderFulfillment.failureReason alone can't\n * answer, since it only ever holds the last attempt's message.\n */\nexport const AdminFulfillmentLogsPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n const [searchParams] = useSearchParams();\n\n const [attempts, setAttempts] = useState<FulfillmentAttempt[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [page, setPage] = useState(0);\n const [succeededFilter, setSucceededFilter] = useState<'' | 'true' | 'false'>('');\n const [carrierFilter, setCarrierFilter] = useState('');\n const [orderId, setOrderId] = useState(searchParams.get('orderId') ?? '');\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{\n orderFulfillmentAttempts: { totalCount: number; attempts: FulfillmentAttempt[] };\n }>({\n gateway: 'global',\n query: ORDER_FULFILLMENT_ATTEMPTS_QUERY,\n variables: {\n limit: PAGE_SIZE,\n offset: page * PAGE_SIZE,\n orderId: orderId.trim() || undefined,\n carrier: carrierFilter.trim() || undefined,\n succeeded: succeededFilter === '' ? undefined : succeededFilter === 'true',\n },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load fulfillment logs');\n }\n setTotal(result.data?.orderFulfillmentAttempts.totalCount ?? 0);\n setAttempts(result.data?.orderFulfillmentAttempts.attempts ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load fulfillment logs');\n } finally {\n setLoading(false);\n }\n }, [authToken, page, orderId, carrierFilter, succeededFilter]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/dashboard`)}\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <Truck className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Fulfillment logs</h1>\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {total}\n </span>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => void load()}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n disabled={loading}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <PagePurpose className=\"mb-4\">\n Every carrier shipment-creation attempt for every physical order — including retries\n Temporal made after a failure — so you can see what happened, why, and whether it\n eventually succeeded. OrderFulfillment.failureReason only ever shows the last attempt;\n this is the full history.\n </PagePurpose>\n\n <div className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center\">\n <select\n aria-label=\"Filter by result\"\n value={succeededFilter}\n onChange={(e) => {\n setSucceededFilter(e.target.value as '' | 'true' | 'false');\n setPage(0);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n >\n {SUCCEEDED_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <input\n type=\"text\"\n aria-label=\"Filter by carrier\"\n placeholder=\"Carrier (e.g. shiprocket)…\"\n value={carrierFilter}\n onChange={(e) => {\n setCarrierFilter(e.target.value);\n setPage(0);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary sm:w-48\"\n />\n <input\n type=\"search\"\n aria-label=\"Filter by order ID\"\n placeholder=\"Order ID…\"\n value={orderId}\n onChange={(e) => {\n setOrderId(e.target.value);\n setPage(0);\n }}\n className=\"flex-1 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </div>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading && attempts.length === 0 ? (\n <output className=\"block py-16 text-center text-text-secondary\">\n Loading fulfillment logs…\n </output>\n ) : attempts.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No fulfillment attempts\"\n description=\"Carrier shipment-creation attempts will show up here as physical orders are placed.\"\n />\n ) : (\n <>\n <div className=\"overflow-hidden rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-surface-muted text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">Attempt #</th>\n <th className=\"px-4 py-2\">Carrier</th>\n <th className=\"px-4 py-2\">Result</th>\n <th className=\"px-4 py-2\">Error</th>\n <th className=\"px-4 py-2 text-right\">Duration</th>\n <th className=\"px-4 py-2\">Occurred</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-surface-raised\">\n {attempts.map((attempt) => (\n <tr key={attempt.id} className=\"text-text-primary align-top\">\n <td className=\"px-4 py-2 font-mono text-xs\">{attempt.attemptNumber}</td>\n <td className=\"px-4 py-2 text-xs\">{attempt.carrier}</td>\n <td className=\"px-4 py-2\">\n {attempt.succeeded ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-surface-success-muted px-2 py-0.5 text-xs font-medium text-text-success\">\n <CheckCircle2 className=\"size-3\" />\n Succeeded\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-surface-danger-muted px-2 py-0.5 text-xs font-medium text-text-danger\">\n <XCircle className=\"size-3\" />\n Failed{attempt.errorStatusCode ? ` (${attempt.errorStatusCode})` : ''}\n </span>\n )}\n </td>\n <td className=\"max-w-md px-4 py-2 text-xs text-text-secondary\">\n {attempt.errorMessage ? (\n <span title={attempt.errorMessage} className=\"line-clamp-2\">\n {attempt.errorMessage}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n )}\n </td>\n <td className=\"px-4 py-2 text-right tabular-nums text-xs text-text-secondary\">\n {attempt.durationMs !== null ? `${attempt.durationMs}ms` : '—'}\n </td>\n <td className=\"px-4 py-2 text-xs text-text-secondary whitespace-nowrap\">\n {formatDate(attempt.occurredAt)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n\n {total > PAGE_SIZE && (\n <div className=\"mt-4 flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of{' '}\n {total}\n </span>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => p - 1)}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-surface-hover disabled:opacity-40\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n disabled={(page + 1) * PAGE_SIZE >= total}\n onClick={() => setPage((p) => p + 1)}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-surface-hover disabled:opacity-40\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n </>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdminFulfillmentLogsPage.js","names":[],"sources":["../../src/pages/AdminFulfillmentLogsPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { ArrowLeft, RefreshCw, Truck, CheckCircle2, XCircle } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\ninterface FulfillmentAttempt {\n id: string;\n attemptNumber: number;\n carrier: string;\n succeeded: boolean;\n errorMessage: string | null;\n errorStatusCode: number | null;\n durationMs: number | null;\n occurredAt: string;\n}\n\nconst SUCCEEDED_OPTIONS: { value: '' | 'true' | 'false'; label: string }[] = [\n { value: '', label: 'All results' },\n { value: 'false', label: 'Failed only' },\n { value: 'true', label: 'Succeeded only' },\n];\n\nconst ORDER_FULFILLMENT_ATTEMPTS_QUERY = /* GraphQL */ `\n query AdminOrderFulfillmentAttempts(\n $limit: Int!\n $offset: Int!\n $orderId: ID\n $carrier: String\n $succeeded: Boolean\n ) {\n orderFulfillmentAttempts(\n limit: $limit\n offset: $offset\n orderId: $orderId\n carrier: $carrier\n succeeded: $succeeded\n ) {\n totalCount\n attempts {\n id\n attemptNumber\n carrier\n succeeded\n errorMessage\n errorStatusCode\n durationMs\n occurredAt\n }\n }\n }\n`;\n\nconst PAGE_SIZE = 25;\n\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n });\n}\n\n/**\n * Admin-only log of every carrier shipment-creation attempt (every Temporal\n * retry, not just the last failure) — the \"why did this order's shipment\n * fail\" debugging surface that OrderFulfillment.failureReason alone can't\n * answer, since it only ever holds the last attempt's message.\n */\nexport const AdminFulfillmentLogsPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n const [searchParams] = useSearchParams();\n\n const [attempts, setAttempts] = useState<FulfillmentAttempt[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [page, setPage] = useState(0);\n const [succeededFilter, setSucceededFilter] = useState<'' | 'true' | 'false'>('');\n const [carrierFilter, setCarrierFilter] = useState('');\n const [orderId, setOrderId] = useState(searchParams.get('orderId') ?? '');\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{\n orderFulfillmentAttempts: { totalCount: number; attempts: FulfillmentAttempt[] };\n }>({\n gateway: 'global',\n query: ORDER_FULFILLMENT_ATTEMPTS_QUERY,\n variables: {\n limit: PAGE_SIZE,\n offset: page * PAGE_SIZE,\n orderId: orderId.trim() || undefined,\n carrier: carrierFilter.trim() || undefined,\n succeeded: succeededFilter === '' ? undefined : succeededFilter === 'true',\n },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load fulfillment logs');\n }\n setTotal(result.data?.orderFulfillmentAttempts.totalCount ?? 0);\n setAttempts(result.data?.orderFulfillmentAttempts.attempts ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load fulfillment logs');\n } finally {\n setLoading(false);\n }\n }, [authToken, page, orderId, carrierFilter, succeededFilter]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/dashboard`)}\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <Truck className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Fulfillment logs</h1>\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {total}\n </span>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => void load()}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n disabled={loading}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <PagePurpose className=\"mb-4\">\n Every carrier shipment-creation attempt for every physical order — including retries\n Temporal made after a failure — so you can see what happened, why, and whether it\n eventually succeeded. OrderFulfillment.failureReason only ever shows the last attempt;\n this is the full history.\n </PagePurpose>\n\n <div className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center\">\n <select\n aria-label=\"Filter by result\"\n value={succeededFilter}\n onChange={(e) => {\n setSucceededFilter(e.target.value as '' | 'true' | 'false');\n setPage(0);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n {SUCCEEDED_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <input\n type=\"text\"\n aria-label=\"Filter by carrier\"\n placeholder=\"Carrier (e.g. shiprocket)…\"\n value={carrierFilter}\n onChange={(e) => {\n setCarrierFilter(e.target.value);\n setPage(0);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary sm:w-48\"\n />\n <input\n type=\"search\"\n aria-label=\"Filter by order ID\"\n placeholder=\"Order ID…\"\n value={orderId}\n onChange={(e) => {\n setOrderId(e.target.value);\n setPage(0);\n }}\n className=\"flex-1 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </div>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading && attempts.length === 0 ? (\n <output className=\"block py-16 text-center text-text-secondary\">\n Loading fulfillment logs…\n </output>\n ) : attempts.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No fulfillment attempts\"\n description=\"Carrier shipment-creation attempts will show up here as physical orders are placed.\"\n />\n ) : (\n <>\n <div className=\"overflow-hidden rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-surface-muted text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">Attempt #</th>\n <th className=\"px-4 py-2\">Carrier</th>\n <th className=\"px-4 py-2\">Result</th>\n <th className=\"px-4 py-2\">Error</th>\n <th className=\"px-4 py-2 text-right\">Duration</th>\n <th className=\"px-4 py-2\">Occurred</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-surface-raised\">\n {attempts.map((attempt) => (\n <tr key={attempt.id} className=\"text-text-primary align-top\">\n <td className=\"px-4 py-2 font-mono text-xs\">{attempt.attemptNumber}</td>\n <td className=\"px-4 py-2 text-xs\">{attempt.carrier}</td>\n <td className=\"px-4 py-2\">\n {attempt.succeeded ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-surface-success-muted px-2 py-0.5 text-xs font-medium text-text-success\">\n <CheckCircle2 className=\"size-3\" />\n Succeeded\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-surface-danger-muted px-2 py-0.5 text-xs font-medium text-text-danger\">\n <XCircle className=\"size-3\" />\n Failed{attempt.errorStatusCode ? ` (${attempt.errorStatusCode})` : ''}\n </span>\n )}\n </td>\n <td className=\"max-w-md px-4 py-2 text-xs text-text-secondary\">\n {attempt.errorMessage ? (\n <span title={attempt.errorMessage} className=\"line-clamp-2\">\n {attempt.errorMessage}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n )}\n </td>\n <td className=\"px-4 py-2 text-right tabular-nums text-xs text-text-secondary\">\n {attempt.durationMs !== null ? `${attempt.durationMs}ms` : '—'}\n </td>\n <td className=\"px-4 py-2 text-xs text-text-secondary whitespace-nowrap\">\n {formatDate(attempt.occurredAt)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n\n {total > PAGE_SIZE && (\n <div className=\"mt-4 flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of{' '}\n {total}\n </span>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => p - 1)}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-surface-hover disabled:opacity-40\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n disabled={(page + 1) * PAGE_SIZE >= total}\n onClick={() => setPage((p) => p + 1)}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-surface-hover disabled:opacity-40\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n </>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -153,7 +153,7 @@ var E = () => {
|
|
|
153
153
|
onChange: (e) => {
|
|
154
154
|
B(e.target.value), R(0);
|
|
155
155
|
},
|
|
156
|
-
className: "rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary",
|
|
156
|
+
className: "rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
157
157
|
children: y.map((e) => /* @__PURE__ */ h("option", {
|
|
158
158
|
value: e.value,
|
|
159
159
|
children: e.label
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminOrdersPage.js","names":[],"sources":["../../src/pages/AdminOrdersPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport {\n ArrowLeft,\n RefreshCw,\n Package,\n ExternalLink,\n Flag,\n Loader2,\n ScrollText,\n} from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useNavigate } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { formatPrice } from '../utils';\n\ntype OrderStatus = 'PENDING' | 'PROCESSED' | 'FAILED' | 'REFUNDED';\ntype FulfillmentStatus =\n | 'PENDING'\n | 'PROCESSING'\n | 'READY_TO_SHIP'\n | 'SHIPPED'\n | 'IN_TRANSIT'\n | 'OUT_FOR_DELIVERY'\n | 'DELIVERED'\n | 'FAILED'\n | 'RETURNED'\n | 'CANCELLED';\ntype IssueCategory = 'OUT_OF_STOCK' | 'CANNOT_FULFILL' | 'WRONG_ADDRESS' | 'OTHER';\n\ninterface IssueFlag {\n id: string;\n category: IssueCategory;\n note: string | null;\n status: 'OPEN' | 'RESOLVED';\n raisedByOrgId: string;\n createdAt: string;\n}\n\ninterface Fulfillment {\n status: FulfillmentStatus;\n carrier: string | null;\n trackingNumber: string | null;\n trackingUrl: string | null;\n}\n\ninterface AdminOrder {\n id: string;\n status: OrderStatus;\n userId: string;\n total: number;\n currency: string;\n createdAt: string;\n fulfillment: Fulfillment | null;\n issueFlags: IssueFlag[];\n}\n\nconst ORDER_STATUS_OPTIONS: { value: OrderStatus | ''; label: string }[] = [\n { value: '', label: 'All statuses' },\n { value: 'PENDING', label: 'Pending' },\n { value: 'PROCESSED', label: 'Processed' },\n { value: 'FAILED', label: 'Failed' },\n { value: 'REFUNDED', label: 'Refunded' },\n];\n\nconst FULFILLMENT_STYLES: Record<FulfillmentStatus, string> = {\n PENDING: 'bg-surface-warning-muted text-text-warning',\n PROCESSING: 'bg-surface-warning-muted text-text-warning',\n READY_TO_SHIP: 'bg-surface-accent-muted text-text-accent',\n SHIPPED: 'bg-surface-accent-muted text-text-accent',\n IN_TRANSIT: 'bg-surface-accent-muted text-text-accent',\n OUT_FOR_DELIVERY: 'bg-surface-accent-muted text-text-accent',\n DELIVERED: 'bg-surface-success-muted text-text-success',\n FAILED: 'bg-surface-danger-muted text-text-danger',\n RETURNED: 'bg-surface-danger-muted text-text-danger',\n CANCELLED: 'bg-surface-muted text-text-secondary',\n};\n\nconst ISSUE_CATEGORY_LABELS: Record<IssueCategory, string> = {\n OUT_OF_STOCK: 'Out of stock',\n CANNOT_FULFILL: 'Cannot fulfill',\n WRONG_ADDRESS: 'Wrong address',\n OTHER: 'Other',\n};\n\nconst ADMIN_PHYSICAL_ORDERS_QUERY = /* GraphQL */ `\n query AdminPhysicalOrders($page: Int!, $limit: Int!, $status: OrderStatus, $search: String) {\n adminPhysicalOrders(page: $page, limit: $limit, status: $status, search: $search) {\n total\n orders {\n id\n status\n userId\n total\n currency\n createdAt\n fulfillment {\n status\n carrier\n trackingNumber\n trackingUrl\n }\n issueFlags {\n id\n category\n note\n status\n raisedByOrgId\n createdAt\n }\n }\n }\n }\n`;\n\nconst RESOLVE_ORDER_ISSUE_MUTATION = /* GraphQL */ `\n mutation AdminResolveOrderIssue($id: ID!) {\n resolveOrderIssueFlag(id: $id) {\n id\n status\n }\n }\n`;\n\nconst PAGE_SIZE = 25;\n\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\n/**\n * Admin-only visibility into physical (fulfillment-tracked) orders —\n * order/payment status, carrier/tracking, and any seller-reported issue\n * flags. Read-only for now: no cancel or refund action here (differs\n * between credits and Razorpay, waits on a proper store refund flow —\n * see BOFF-7192). Cancellation stays manual via the Shiprocket dashboard.\n */\nexport const AdminOrdersPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n\n const [orders, setOrders] = useState<AdminOrder[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [page, setPage] = useState(0);\n const [statusFilter, setStatusFilter] = useState<OrderStatus | ''>('');\n const [search, setSearch] = useState('');\n const [resolvingFlagId, setResolvingFlagId] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{\n adminPhysicalOrders: { total: number; orders: AdminOrder[] };\n }>({\n gateway: 'global',\n query: ADMIN_PHYSICAL_ORDERS_QUERY,\n variables: {\n page: page + 1,\n limit: PAGE_SIZE,\n status: statusFilter || undefined,\n search: search.trim() || undefined,\n },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load orders');\n }\n setTotal(result.data?.adminPhysicalOrders.total ?? 0);\n setOrders(result.data?.adminPhysicalOrders.orders ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load orders');\n } finally {\n setLoading(false);\n }\n }, [authToken, page, statusFilter, search]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const resolveFlag = async (flagId: string) => {\n setResolvingFlagId(flagId);\n try {\n await graphqlFetch({\n gateway: 'global',\n query: RESOLVE_ORDER_ISSUE_MUTATION,\n variables: { id: flagId },\n authToken: authToken || undefined,\n });\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to resolve issue');\n } finally {\n setResolvingFlagId(null);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/dashboard`)}\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <Package className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Physical orders</h1>\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {total}\n </span>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => void load()}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n disabled={loading}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <PagePurpose className=\"mb-4\">\n Payment/order status and Shiprocket fulfillment status for every physical order, plus any\n issue a seller has flagged (out of stock, cannot fulfill, wrong address). Read-only —\n cancellation and refunds still happen manually via the Shiprocket dashboard for now.\n </PagePurpose>\n\n <div className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center\">\n <select\n aria-label=\"Filter by status\"\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as OrderStatus | '');\n setPage(0);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n >\n {ORDER_STATUS_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <input\n type=\"search\"\n aria-label=\"Search by order ID, buyer, or tracking number\"\n placeholder=\"Search order ID, buyer, or tracking number…\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(0);\n }}\n className=\"flex-1 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </div>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading && orders.length === 0 ? (\n <output className=\"block py-16 text-center text-text-secondary\">Loading orders…</output>\n ) : orders.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No physical orders\"\n description=\"Orders with a physical line item will show up here once customers make purchases.\"\n />\n ) : (\n <>\n <div className=\"overflow-hidden rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-surface-muted text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">Order</th>\n <th className=\"px-4 py-2\">Buyer</th>\n <th className=\"px-4 py-2 text-right\">Total</th>\n <th className=\"px-4 py-2\">Status</th>\n <th className=\"px-4 py-2\">Fulfillment</th>\n <th className=\"px-4 py-2\">Tracking</th>\n <th className=\"px-4 py-2\">Issue</th>\n <th className=\"px-4 py-2\">Placed</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-surface-raised\">\n {orders.map((order) => {\n const openFlag = order.issueFlags.find((f) => f.status === 'OPEN');\n return (\n <tr key={order.id} className=\"text-text-primary align-top\">\n <td className=\"px-4 py-2 font-mono text-xs\">\n {order.id.slice(0, 8).toUpperCase()}\n </td>\n <td className=\"px-4 py-2 font-mono text-xs text-text-secondary\">\n {order.userId.slice(0, 8)}…\n </td>\n <td className=\"px-4 py-2 text-right tabular-nums\">\n {formatPrice(order.total, order.currency)}\n </td>\n <td className=\"px-4 py-2\">\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {order.status}\n </span>\n </td>\n <td className=\"px-4 py-2\">\n <div className=\"flex items-center gap-1.5\">\n {order.fulfillment ? (\n <span\n className={`rounded-full px-2 py-0.5 text-xs font-medium ${FULFILLMENT_STYLES[order.fulfillment.status]}`}\n >\n {order.fulfillment.status.replace(/_/g, ' ')}\n </span>\n ) : (\n <span\n className=\"rounded-full bg-surface-danger-muted px-2 py-0.5 text-xs font-medium text-text-danger\"\n title=\"No fulfillment record — the post-payment fulfillment step never ran for this order\"\n >\n Not started\n </span>\n )}\n <button\n type=\"button\"\n title=\"View fulfillment logs for this order\"\n onClick={() =>\n navigate(`${basePath}/fulfillment-logs?orderId=${order.id}`)\n }\n className=\"text-text-muted hover:text-text-primary\"\n >\n <ScrollText className=\"size-3.5\" />\n </button>\n </div>\n </td>\n <td className=\"px-4 py-2 text-xs\">\n {order.fulfillment?.trackingNumber ? (\n <a\n href={order.fulfillment.trackingUrl ?? '#'}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1 text-text-accent hover:underline\"\n >\n {order.fulfillment.carrier ?? 'Carrier'}\n <ExternalLink className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-text-muted\">—</span>\n )}\n </td>\n <td className=\"px-4 py-2\">\n {openFlag ? (\n <div className=\"flex flex-col items-start gap-1\">\n <span\n title={openFlag.note ?? undefined}\n className=\"flex items-center gap-1 rounded-full bg-surface-warning-muted px-2 py-0.5 text-xs font-medium text-text-warning\"\n >\n <Flag className=\"size-3\" />\n {ISSUE_CATEGORY_LABELS[openFlag.category]}\n </span>\n <button\n type=\"button\"\n onClick={() => void resolveFlag(openFlag.id)}\n disabled={resolvingFlagId === openFlag.id}\n className=\"text-xs font-medium text-text-secondary underline hover:text-text-primary disabled:opacity-50\"\n >\n {resolvingFlagId === openFlag.id ? (\n <Loader2 className=\"size-3 animate-spin\" />\n ) : (\n 'Mark resolved'\n )}\n </button>\n </div>\n ) : (\n <span className=\"text-text-muted\">—</span>\n )}\n </td>\n <td className=\"px-4 py-2 text-xs text-text-secondary whitespace-nowrap\">\n {formatDate(order.createdAt)}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n\n {total > PAGE_SIZE && (\n <div className=\"mt-4 flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of{' '}\n {total}\n </span>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => p - 1)}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-surface-hover disabled:opacity-40\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n disabled={(page + 1) * PAGE_SIZE >= total}\n onClick={() => setPage((p) => p + 1)}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-surface-hover disabled:opacity-40\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n </>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdminOrdersPage.js","names":[],"sources":["../../src/pages/AdminOrdersPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport {\n ArrowLeft,\n RefreshCw,\n Package,\n ExternalLink,\n Flag,\n Loader2,\n ScrollText,\n} from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useNavigate } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { formatPrice } from '../utils';\n\ntype OrderStatus = 'PENDING' | 'PROCESSED' | 'FAILED' | 'REFUNDED';\ntype FulfillmentStatus =\n | 'PENDING'\n | 'PROCESSING'\n | 'READY_TO_SHIP'\n | 'SHIPPED'\n | 'IN_TRANSIT'\n | 'OUT_FOR_DELIVERY'\n | 'DELIVERED'\n | 'FAILED'\n | 'RETURNED'\n | 'CANCELLED';\ntype IssueCategory = 'OUT_OF_STOCK' | 'CANNOT_FULFILL' | 'WRONG_ADDRESS' | 'OTHER';\n\ninterface IssueFlag {\n id: string;\n category: IssueCategory;\n note: string | null;\n status: 'OPEN' | 'RESOLVED';\n raisedByOrgId: string;\n createdAt: string;\n}\n\ninterface Fulfillment {\n status: FulfillmentStatus;\n carrier: string | null;\n trackingNumber: string | null;\n trackingUrl: string | null;\n}\n\ninterface AdminOrder {\n id: string;\n status: OrderStatus;\n userId: string;\n total: number;\n currency: string;\n createdAt: string;\n fulfillment: Fulfillment | null;\n issueFlags: IssueFlag[];\n}\n\nconst ORDER_STATUS_OPTIONS: { value: OrderStatus | ''; label: string }[] = [\n { value: '', label: 'All statuses' },\n { value: 'PENDING', label: 'Pending' },\n { value: 'PROCESSED', label: 'Processed' },\n { value: 'FAILED', label: 'Failed' },\n { value: 'REFUNDED', label: 'Refunded' },\n];\n\nconst FULFILLMENT_STYLES: Record<FulfillmentStatus, string> = {\n PENDING: 'bg-surface-warning-muted text-text-warning',\n PROCESSING: 'bg-surface-warning-muted text-text-warning',\n READY_TO_SHIP: 'bg-surface-accent-muted text-text-accent',\n SHIPPED: 'bg-surface-accent-muted text-text-accent',\n IN_TRANSIT: 'bg-surface-accent-muted text-text-accent',\n OUT_FOR_DELIVERY: 'bg-surface-accent-muted text-text-accent',\n DELIVERED: 'bg-surface-success-muted text-text-success',\n FAILED: 'bg-surface-danger-muted text-text-danger',\n RETURNED: 'bg-surface-danger-muted text-text-danger',\n CANCELLED: 'bg-surface-muted text-text-secondary',\n};\n\nconst ISSUE_CATEGORY_LABELS: Record<IssueCategory, string> = {\n OUT_OF_STOCK: 'Out of stock',\n CANNOT_FULFILL: 'Cannot fulfill',\n WRONG_ADDRESS: 'Wrong address',\n OTHER: 'Other',\n};\n\nconst ADMIN_PHYSICAL_ORDERS_QUERY = /* GraphQL */ `\n query AdminPhysicalOrders($page: Int!, $limit: Int!, $status: OrderStatus, $search: String) {\n adminPhysicalOrders(page: $page, limit: $limit, status: $status, search: $search) {\n total\n orders {\n id\n status\n userId\n total\n currency\n createdAt\n fulfillment {\n status\n carrier\n trackingNumber\n trackingUrl\n }\n issueFlags {\n id\n category\n note\n status\n raisedByOrgId\n createdAt\n }\n }\n }\n }\n`;\n\nconst RESOLVE_ORDER_ISSUE_MUTATION = /* GraphQL */ `\n mutation AdminResolveOrderIssue($id: ID!) {\n resolveOrderIssueFlag(id: $id) {\n id\n status\n }\n }\n`;\n\nconst PAGE_SIZE = 25;\n\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\n/**\n * Admin-only visibility into physical (fulfillment-tracked) orders —\n * order/payment status, carrier/tracking, and any seller-reported issue\n * flags. Read-only for now: no cancel or refund action here (differs\n * between credits and Razorpay, waits on a proper store refund flow —\n * see BOFF-7192). Cancellation stays manual via the Shiprocket dashboard.\n */\nexport const AdminOrdersPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n\n const [orders, setOrders] = useState<AdminOrder[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [page, setPage] = useState(0);\n const [statusFilter, setStatusFilter] = useState<OrderStatus | ''>('');\n const [search, setSearch] = useState('');\n const [resolvingFlagId, setResolvingFlagId] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{\n adminPhysicalOrders: { total: number; orders: AdminOrder[] };\n }>({\n gateway: 'global',\n query: ADMIN_PHYSICAL_ORDERS_QUERY,\n variables: {\n page: page + 1,\n limit: PAGE_SIZE,\n status: statusFilter || undefined,\n search: search.trim() || undefined,\n },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load orders');\n }\n setTotal(result.data?.adminPhysicalOrders.total ?? 0);\n setOrders(result.data?.adminPhysicalOrders.orders ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load orders');\n } finally {\n setLoading(false);\n }\n }, [authToken, page, statusFilter, search]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const resolveFlag = async (flagId: string) => {\n setResolvingFlagId(flagId);\n try {\n await graphqlFetch({\n gateway: 'global',\n query: RESOLVE_ORDER_ISSUE_MUTATION,\n variables: { id: flagId },\n authToken: authToken || undefined,\n });\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to resolve issue');\n } finally {\n setResolvingFlagId(null);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/dashboard`)}\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <Package className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Physical orders</h1>\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {total}\n </span>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => void load()}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n disabled={loading}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <PagePurpose className=\"mb-4\">\n Payment/order status and Shiprocket fulfillment status for every physical order, plus any\n issue a seller has flagged (out of stock, cannot fulfill, wrong address). Read-only —\n cancellation and refunds still happen manually via the Shiprocket dashboard for now.\n </PagePurpose>\n\n <div className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center\">\n <select\n aria-label=\"Filter by status\"\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as OrderStatus | '');\n setPage(0);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n {ORDER_STATUS_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <input\n type=\"search\"\n aria-label=\"Search by order ID, buyer, or tracking number\"\n placeholder=\"Search order ID, buyer, or tracking number…\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(0);\n }}\n className=\"flex-1 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </div>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading && orders.length === 0 ? (\n <output className=\"block py-16 text-center text-text-secondary\">Loading orders…</output>\n ) : orders.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No physical orders\"\n description=\"Orders with a physical line item will show up here once customers make purchases.\"\n />\n ) : (\n <>\n <div className=\"overflow-hidden rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-surface-muted text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">Order</th>\n <th className=\"px-4 py-2\">Buyer</th>\n <th className=\"px-4 py-2 text-right\">Total</th>\n <th className=\"px-4 py-2\">Status</th>\n <th className=\"px-4 py-2\">Fulfillment</th>\n <th className=\"px-4 py-2\">Tracking</th>\n <th className=\"px-4 py-2\">Issue</th>\n <th className=\"px-4 py-2\">Placed</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-surface-raised\">\n {orders.map((order) => {\n const openFlag = order.issueFlags.find((f) => f.status === 'OPEN');\n return (\n <tr key={order.id} className=\"text-text-primary align-top\">\n <td className=\"px-4 py-2 font-mono text-xs\">\n {order.id.slice(0, 8).toUpperCase()}\n </td>\n <td className=\"px-4 py-2 font-mono text-xs text-text-secondary\">\n {order.userId.slice(0, 8)}…\n </td>\n <td className=\"px-4 py-2 text-right tabular-nums\">\n {formatPrice(order.total, order.currency)}\n </td>\n <td className=\"px-4 py-2\">\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {order.status}\n </span>\n </td>\n <td className=\"px-4 py-2\">\n <div className=\"flex items-center gap-1.5\">\n {order.fulfillment ? (\n <span\n className={`rounded-full px-2 py-0.5 text-xs font-medium ${FULFILLMENT_STYLES[order.fulfillment.status]}`}\n >\n {order.fulfillment.status.replace(/_/g, ' ')}\n </span>\n ) : (\n <span\n className=\"rounded-full bg-surface-danger-muted px-2 py-0.5 text-xs font-medium text-text-danger\"\n title=\"No fulfillment record — the post-payment fulfillment step never ran for this order\"\n >\n Not started\n </span>\n )}\n <button\n type=\"button\"\n title=\"View fulfillment logs for this order\"\n onClick={() =>\n navigate(`${basePath}/fulfillment-logs?orderId=${order.id}`)\n }\n className=\"text-text-muted hover:text-text-primary\"\n >\n <ScrollText className=\"size-3.5\" />\n </button>\n </div>\n </td>\n <td className=\"px-4 py-2 text-xs\">\n {order.fulfillment?.trackingNumber ? (\n <a\n href={order.fulfillment.trackingUrl ?? '#'}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1 text-text-accent hover:underline\"\n >\n {order.fulfillment.carrier ?? 'Carrier'}\n <ExternalLink className=\"size-3\" />\n </a>\n ) : (\n <span className=\"text-text-muted\">—</span>\n )}\n </td>\n <td className=\"px-4 py-2\">\n {openFlag ? (\n <div className=\"flex flex-col items-start gap-1\">\n <span\n title={openFlag.note ?? undefined}\n className=\"flex items-center gap-1 rounded-full bg-surface-warning-muted px-2 py-0.5 text-xs font-medium text-text-warning\"\n >\n <Flag className=\"size-3\" />\n {ISSUE_CATEGORY_LABELS[openFlag.category]}\n </span>\n <button\n type=\"button\"\n onClick={() => void resolveFlag(openFlag.id)}\n disabled={resolvingFlagId === openFlag.id}\n className=\"text-xs font-medium text-text-secondary underline hover:text-text-primary disabled:opacity-50\"\n >\n {resolvingFlagId === openFlag.id ? (\n <Loader2 className=\"size-3 animate-spin\" />\n ) : (\n 'Mark resolved'\n )}\n </button>\n </div>\n ) : (\n <span className=\"text-text-muted\">—</span>\n )}\n </td>\n <td className=\"px-4 py-2 text-xs text-text-secondary whitespace-nowrap\">\n {formatDate(order.createdAt)}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n\n {total > PAGE_SIZE && (\n <div className=\"mt-4 flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of{' '}\n {total}\n </span>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => p - 1)}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-surface-hover disabled:opacity-40\"\n >\n Previous\n </button>\n <button\n type=\"button\"\n disabled={(page + 1) * PAGE_SIZE >= total}\n onClick={() => setPage((p) => p + 1)}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-surface-hover disabled:opacity-40\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n </>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -248,7 +248,7 @@ var D = "\n query AdminStores {\n stores {\n id\n slug\n name
|
|
|
248
248
|
type: e.target.value
|
|
249
249
|
}),
|
|
250
250
|
disabled: !!G,
|
|
251
|
-
className: "mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60",
|
|
251
|
+
className: "mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
252
252
|
children: [
|
|
253
253
|
/* @__PURE__ */ p("option", {
|
|
254
254
|
value: "PUBLIC",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminStoresPage.js","names":[],"sources":["../../src/pages/AdminStoresPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { ArrowLeft, Plus, Pencil, Trash2, RefreshCw, Store as StoreIcon } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useNavigate } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\nimport {\n AlertDialog,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogCancel,\n AlertDialogAction,\n GlassCard,\n PagePurpose,\n IllustratedEmptyState,\n NextSteps,\n} from '@burdenoff/fe-libs/ui';\nimport { MarkdownEditor } from '../components/MarkdownEditor';\n\ntype StoreRecord = {\n id: string;\n slug: string;\n name: string;\n type: 'PUBLIC' | 'PRIVATE' | 'ENTERPRISE';\n tenantId?: string | null;\n isPublic: boolean;\n requiresApproval: boolean;\n allowUnverifiedPublishers: boolean;\n currency: string;\n logoUrl?: string | null;\n description?: string | null;\n createdAt: string;\n};\n\nconst STORES_QUERY = /* GraphQL */ `\n query AdminStores {\n stores {\n id\n slug\n name\n type\n tenantId\n isPublic\n requiresApproval\n allowUnverifiedPublishers\n currency\n logoUrl\n description\n createdAt\n }\n }\n`;\n\nconst CREATE_STORE_MUTATION = /* GraphQL */ `\n mutation CreateStore($input: CreateStoreInput!) {\n createStore(input: $input) {\n id\n slug\n name\n type\n }\n }\n`;\n\nconst UPDATE_STORE_MUTATION = /* GraphQL */ `\n mutation UpdateStore($id: ID!, $input: UpdateStoreInput!) {\n updateStore(id: $id, input: $input) {\n id\n name\n isPublic\n requiresApproval\n }\n }\n`;\n\nconst DELETE_STORE_MUTATION = /* GraphQL */ `\n mutation DeleteStore($id: ID!) {\n deleteStore(id: $id)\n }\n`;\n\ntype FormState = {\n slug: string;\n name: string;\n type: StoreRecord['type'];\n tenantId: string;\n isPublic: boolean;\n requiresApproval: boolean;\n allowUnverifiedPublishers: boolean;\n currency: string;\n description: string;\n};\n\nconst emptyForm: FormState = {\n slug: '',\n name: '',\n type: 'PRIVATE',\n tenantId: '',\n isPublic: false,\n requiresApproval: false,\n allowUnverifiedPublishers: true,\n currency: 'USD',\n description: '',\n};\n\nexport const AdminStoresPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n const [stores, setStores] = useState<StoreRecord[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [showForm, setShowForm] = useState(false);\n const [form, setForm] = useState<FormState>(emptyForm);\n const [editingId, setEditingId] = useState<string | null>(null);\n const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{ stores: StoreRecord[] }>({\n gateway: 'global',\n query: STORES_QUERY,\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load stores');\n }\n setStores(result.data?.stores ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load stores');\n } finally {\n setLoading(false);\n }\n }, [authToken]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const openCreate = () => {\n setEditingId(null);\n setForm(emptyForm);\n setShowForm(true);\n };\n\n const openEdit = (s: StoreRecord) => {\n setEditingId(s.id);\n setForm({\n slug: s.slug,\n name: s.name,\n type: s.type,\n tenantId: s.tenantId ?? '',\n isPublic: s.isPublic,\n requiresApproval: s.requiresApproval,\n allowUnverifiedPublishers: s.allowUnverifiedPublishers,\n currency: s.currency,\n description: s.description ?? '',\n });\n setShowForm(true);\n };\n\n const submit = async () => {\n if (!form.slug.trim() || !form.name.trim()) {\n setError('Slug and name are required');\n return;\n }\n setError(null);\n try {\n if (editingId) {\n await graphqlFetch({\n gateway: 'global',\n query: UPDATE_STORE_MUTATION,\n variables: {\n id: editingId,\n input: {\n name: form.name,\n isPublic: form.isPublic,\n requiresApproval: form.requiresApproval,\n allowUnverifiedPublishers: form.allowUnverifiedPublishers,\n currency: form.currency,\n description: form.description || null,\n },\n },\n authToken: authToken || undefined,\n });\n } else {\n await graphqlFetch({\n gateway: 'global',\n query: CREATE_STORE_MUTATION,\n variables: {\n input: {\n slug: form.slug,\n name: form.name,\n type: form.type,\n tenantId: form.tenantId || null,\n isPublic: form.isPublic,\n requiresApproval: form.requiresApproval,\n allowUnverifiedPublishers: form.allowUnverifiedPublishers,\n currency: form.currency,\n description: form.description || null,\n },\n },\n authToken: authToken || undefined,\n });\n }\n setShowForm(false);\n setEditingId(null);\n setForm(emptyForm);\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Save failed');\n }\n };\n\n const remove = async (id: string) => {\n try {\n await graphqlFetch({\n gateway: 'global',\n query: DELETE_STORE_MUTATION,\n variables: { id },\n authToken: authToken || undefined,\n });\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Delete failed');\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/dashboard`)}\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <StoreIcon className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Store registry</h1>\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {stores.length}\n </span>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => void load()}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n disabled={loading}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={openCreate}\n className=\"inline-flex items-center gap-2 rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n <Plus className=\"size-4\" />\n Create store\n </button>\n </div>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <PagePurpose className=\"mb-4\">\n A store is a storefront that decides which products a given audience can see and buy. Use\n this registry to spin up a public marketplace, a tenant-scoped enterprise store, or a\n private opt-in catalog — and to control whether submissions auto-publish or wait for\n approval, and whether unverified publishers may submit. One store can power a whole\n product's app marketplace.\n </PagePurpose>\n\n <NextSteps\n storageKey=\"store-admin-stores\"\n steps={[\n {\n id: 'create-store',\n label: 'Create your first store',\n description: 'Choose a type (public, private, or enterprise) and a slug.',\n onClick: openCreate,\n },\n {\n id: 'approval-policy',\n label: 'Set the submission policy',\n description: 'Decide if new products require approval before going live.',\n onClick: openCreate,\n },\n {\n id: 'moderate',\n label: 'Review incoming submissions',\n description: 'Head to the submission queue to approve or reject products.',\n onClick: () => navigate(`${basePath}/submissions`),\n },\n ]}\n />\n\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {showForm && (\n <GlassCard treatment=\"plain\" className=\"mb-6 p-4\">\n <h2 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {editingId ? 'Edit store' : 'Create store'}\n </h2>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Slug</span>\n <input\n type=\"text\"\n aria-label=\"Slug\"\n value={form.slug}\n onChange={(e) => setForm({ ...form, slug: e.target.value })}\n disabled={!!editingId}\n required\n aria-required=\"true\"\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Name</span>\n <input\n type=\"text\"\n aria-label=\"Name\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n required\n aria-required=\"true\"\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Type</span>\n <select\n aria-label=\"Store type\"\n value={form.type}\n onChange={(e) => setForm({ ...form, type: e.target.value as FormState['type'] })}\n disabled={!!editingId}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n >\n <option value=\"PUBLIC\">Public</option>\n <option value=\"PRIVATE\">Private (opt-in)</option>\n <option value=\"ENTERPRISE\">Enterprise (tenant-scoped)</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Currency</span>\n <input\n type=\"text\"\n aria-label=\"Currency\"\n value={form.currency}\n onChange={(e) => setForm({ ...form, currency: e.target.value })}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </label>\n {form.type !== 'PUBLIC' && (\n <label className=\"block md:col-span-2\">\n <span className=\"text-xs text-text-secondary\">Tenant ID (optional)</span>\n <input\n type=\"text\"\n aria-label=\"Tenant ID\"\n value={form.tenantId}\n onChange={(e) => setForm({ ...form, tenantId: e.target.value })}\n disabled={!!editingId}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n />\n </label>\n )}\n <MarkdownEditor\n className=\"md:col-span-2\"\n id=\"store-description\"\n label=\"Description\"\n value={form.description}\n onChange={(value) => setForm({ ...form, description: value })}\n rows={2}\n />\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Publicly visible\"\n checked={form.isPublic}\n onChange={(e) => setForm({ ...form, isPublic: e.target.checked })}\n />\n <span className=\"text-sm text-text-primary\">Publicly visible</span>\n </label>\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Requires admin approval\"\n checked={form.requiresApproval}\n onChange={(e) => setForm({ ...form, requiresApproval: e.target.checked })}\n />\n <span className=\"text-sm text-text-primary\">Requires admin approval</span>\n </label>\n <label className=\"flex items-center gap-2 md:col-span-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Allow unverified publishers to submit\"\n checked={form.allowUnverifiedPublishers}\n onChange={(e) =>\n setForm({ ...form, allowUnverifiedPublishers: e.target.checked })\n }\n />\n <span className=\"text-sm text-text-primary\">\n Allow unverified publishers to submit\n </span>\n </label>\n </div>\n <div className=\"mt-4 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setShowForm(false);\n setEditingId(null);\n setForm(emptyForm);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => void submit()}\n className=\"rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n {editingId ? 'Save changes' : 'Create'}\n </button>\n </div>\n </GlassCard>\n )}\n\n {loading && stores.length === 0 ? (\n <output className=\"block py-16 text-center text-text-secondary\">Loading stores…</output>\n ) : stores.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No stores yet\"\n description=\"Create your first store to give an audience a place to discover and install products.\"\n action={\n <button\n type=\"button\"\n onClick={openCreate}\n className=\"inline-flex items-center gap-2 rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n <Plus className=\"size-4\" />\n Create store\n </button>\n }\n />\n ) : (\n <div className=\"overflow-hidden rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-surface-muted text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">Slug</th>\n <th className=\"px-4 py-2\">Name</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Public</th>\n <th className=\"px-4 py-2\">Approval</th>\n <th className=\"px-4 py-2\">Currency</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-surface-raised\">\n {stores.map((s) => (\n <tr key={s.id} className=\"text-text-primary\">\n <td className=\"px-4 py-2 font-mono text-xs\">{s.slug}</td>\n <td className=\"px-4 py-2\">{s.name}</td>\n <td className=\"px-4 py-2\">\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {s.type}\n </span>\n </td>\n <td className=\"px-4 py-2 text-xs\">{s.isPublic ? 'Yes' : 'No'}</td>\n <td className=\"px-4 py-2 text-xs\">\n {s.requiresApproval ? 'Required' : 'Auto-publish'}\n </td>\n <td className=\"px-4 py-2 text-xs\">{s.currency}</td>\n <td className=\"px-4 py-2 text-right\">\n <button\n type=\"button\"\n onClick={() => openEdit(s)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-default px-2 py-1 text-xs text-text-primary hover:bg-surface-hover\"\n >\n <Pencil className=\"size-3\" />\n Edit\n </button>\n {s.id !== 'store_public_default' && (\n <button\n type=\"button\"\n onClick={() => setPendingDeleteId(s.id)}\n className=\"ml-2 inline-flex items-center gap-1 rounded-md border border-border-danger px-2 py-1 text-xs text-text-danger hover:bg-surface-danger-muted\"\n >\n <Trash2 className=\"size-3\" />\n Delete\n </button>\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </main>\n\n <AlertDialog\n open={pendingDeleteId !== null}\n onOpenChange={(open) => !open && setPendingDeleteId(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete this store?</AlertDialogTitle>\n <AlertDialogDescription>This is a soft delete.</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel onClick={() => setPendingDeleteId(null)}>Cancel</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-surface-danger text-text-on-danger hover:bg-surface-danger-hover\"\n onClick={() => {\n if (pendingDeleteId) void remove(pendingDeleteId);\n setPendingDeleteId(null);\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdminStoresPage.js","names":[],"sources":["../../src/pages/AdminStoresPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { ArrowLeft, Plus, Pencil, Trash2, RefreshCw, Store as StoreIcon } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useNavigate } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\nimport {\n AlertDialog,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogCancel,\n AlertDialogAction,\n GlassCard,\n PagePurpose,\n IllustratedEmptyState,\n NextSteps,\n} from '@burdenoff/fe-libs/ui';\nimport { MarkdownEditor } from '../components/MarkdownEditor';\n\ntype StoreRecord = {\n id: string;\n slug: string;\n name: string;\n type: 'PUBLIC' | 'PRIVATE' | 'ENTERPRISE';\n tenantId?: string | null;\n isPublic: boolean;\n requiresApproval: boolean;\n allowUnverifiedPublishers: boolean;\n currency: string;\n logoUrl?: string | null;\n description?: string | null;\n createdAt: string;\n};\n\nconst STORES_QUERY = /* GraphQL */ `\n query AdminStores {\n stores {\n id\n slug\n name\n type\n tenantId\n isPublic\n requiresApproval\n allowUnverifiedPublishers\n currency\n logoUrl\n description\n createdAt\n }\n }\n`;\n\nconst CREATE_STORE_MUTATION = /* GraphQL */ `\n mutation CreateStore($input: CreateStoreInput!) {\n createStore(input: $input) {\n id\n slug\n name\n type\n }\n }\n`;\n\nconst UPDATE_STORE_MUTATION = /* GraphQL */ `\n mutation UpdateStore($id: ID!, $input: UpdateStoreInput!) {\n updateStore(id: $id, input: $input) {\n id\n name\n isPublic\n requiresApproval\n }\n }\n`;\n\nconst DELETE_STORE_MUTATION = /* GraphQL */ `\n mutation DeleteStore($id: ID!) {\n deleteStore(id: $id)\n }\n`;\n\ntype FormState = {\n slug: string;\n name: string;\n type: StoreRecord['type'];\n tenantId: string;\n isPublic: boolean;\n requiresApproval: boolean;\n allowUnverifiedPublishers: boolean;\n currency: string;\n description: string;\n};\n\nconst emptyForm: FormState = {\n slug: '',\n name: '',\n type: 'PRIVATE',\n tenantId: '',\n isPublic: false,\n requiresApproval: false,\n allowUnverifiedPublishers: true,\n currency: 'USD',\n description: '',\n};\n\nexport const AdminStoresPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n const [stores, setStores] = useState<StoreRecord[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [showForm, setShowForm] = useState(false);\n const [form, setForm] = useState<FormState>(emptyForm);\n const [editingId, setEditingId] = useState<string | null>(null);\n const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{ stores: StoreRecord[] }>({\n gateway: 'global',\n query: STORES_QUERY,\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load stores');\n }\n setStores(result.data?.stores ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load stores');\n } finally {\n setLoading(false);\n }\n }, [authToken]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const openCreate = () => {\n setEditingId(null);\n setForm(emptyForm);\n setShowForm(true);\n };\n\n const openEdit = (s: StoreRecord) => {\n setEditingId(s.id);\n setForm({\n slug: s.slug,\n name: s.name,\n type: s.type,\n tenantId: s.tenantId ?? '',\n isPublic: s.isPublic,\n requiresApproval: s.requiresApproval,\n allowUnverifiedPublishers: s.allowUnverifiedPublishers,\n currency: s.currency,\n description: s.description ?? '',\n });\n setShowForm(true);\n };\n\n const submit = async () => {\n if (!form.slug.trim() || !form.name.trim()) {\n setError('Slug and name are required');\n return;\n }\n setError(null);\n try {\n if (editingId) {\n await graphqlFetch({\n gateway: 'global',\n query: UPDATE_STORE_MUTATION,\n variables: {\n id: editingId,\n input: {\n name: form.name,\n isPublic: form.isPublic,\n requiresApproval: form.requiresApproval,\n allowUnverifiedPublishers: form.allowUnverifiedPublishers,\n currency: form.currency,\n description: form.description || null,\n },\n },\n authToken: authToken || undefined,\n });\n } else {\n await graphqlFetch({\n gateway: 'global',\n query: CREATE_STORE_MUTATION,\n variables: {\n input: {\n slug: form.slug,\n name: form.name,\n type: form.type,\n tenantId: form.tenantId || null,\n isPublic: form.isPublic,\n requiresApproval: form.requiresApproval,\n allowUnverifiedPublishers: form.allowUnverifiedPublishers,\n currency: form.currency,\n description: form.description || null,\n },\n },\n authToken: authToken || undefined,\n });\n }\n setShowForm(false);\n setEditingId(null);\n setForm(emptyForm);\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Save failed');\n }\n };\n\n const remove = async (id: string) => {\n try {\n await graphqlFetch({\n gateway: 'global',\n query: DELETE_STORE_MUTATION,\n variables: { id },\n authToken: authToken || undefined,\n });\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Delete failed');\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/dashboard`)}\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <StoreIcon className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Store registry</h1>\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {stores.length}\n </span>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => void load()}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n disabled={loading}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={openCreate}\n className=\"inline-flex items-center gap-2 rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n <Plus className=\"size-4\" />\n Create store\n </button>\n </div>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <PagePurpose className=\"mb-4\">\n A store is a storefront that decides which products a given audience can see and buy. Use\n this registry to spin up a public marketplace, a tenant-scoped enterprise store, or a\n private opt-in catalog — and to control whether submissions auto-publish or wait for\n approval, and whether unverified publishers may submit. One store can power a whole\n product's app marketplace.\n </PagePurpose>\n\n <NextSteps\n storageKey=\"store-admin-stores\"\n steps={[\n {\n id: 'create-store',\n label: 'Create your first store',\n description: 'Choose a type (public, private, or enterprise) and a slug.',\n onClick: openCreate,\n },\n {\n id: 'approval-policy',\n label: 'Set the submission policy',\n description: 'Decide if new products require approval before going live.',\n onClick: openCreate,\n },\n {\n id: 'moderate',\n label: 'Review incoming submissions',\n description: 'Head to the submission queue to approve or reject products.',\n onClick: () => navigate(`${basePath}/submissions`),\n },\n ]}\n />\n\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {showForm && (\n <GlassCard treatment=\"plain\" className=\"mb-6 p-4\">\n <h2 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {editingId ? 'Edit store' : 'Create store'}\n </h2>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Slug</span>\n <input\n type=\"text\"\n aria-label=\"Slug\"\n value={form.slug}\n onChange={(e) => setForm({ ...form, slug: e.target.value })}\n disabled={!!editingId}\n required\n aria-required=\"true\"\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Name</span>\n <input\n type=\"text\"\n aria-label=\"Name\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n required\n aria-required=\"true\"\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Type</span>\n <select\n aria-label=\"Store type\"\n value={form.type}\n onChange={(e) => setForm({ ...form, type: e.target.value as FormState['type'] })}\n disabled={!!editingId}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"PUBLIC\">Public</option>\n <option value=\"PRIVATE\">Private (opt-in)</option>\n <option value=\"ENTERPRISE\">Enterprise (tenant-scoped)</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Currency</span>\n <input\n type=\"text\"\n aria-label=\"Currency\"\n value={form.currency}\n onChange={(e) => setForm({ ...form, currency: e.target.value })}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </label>\n {form.type !== 'PUBLIC' && (\n <label className=\"block md:col-span-2\">\n <span className=\"text-xs text-text-secondary\">Tenant ID (optional)</span>\n <input\n type=\"text\"\n aria-label=\"Tenant ID\"\n value={form.tenantId}\n onChange={(e) => setForm({ ...form, tenantId: e.target.value })}\n disabled={!!editingId}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n />\n </label>\n )}\n <MarkdownEditor\n className=\"md:col-span-2\"\n id=\"store-description\"\n label=\"Description\"\n value={form.description}\n onChange={(value) => setForm({ ...form, description: value })}\n rows={2}\n />\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Publicly visible\"\n checked={form.isPublic}\n onChange={(e) => setForm({ ...form, isPublic: e.target.checked })}\n />\n <span className=\"text-sm text-text-primary\">Publicly visible</span>\n </label>\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Requires admin approval\"\n checked={form.requiresApproval}\n onChange={(e) => setForm({ ...form, requiresApproval: e.target.checked })}\n />\n <span className=\"text-sm text-text-primary\">Requires admin approval</span>\n </label>\n <label className=\"flex items-center gap-2 md:col-span-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Allow unverified publishers to submit\"\n checked={form.allowUnverifiedPublishers}\n onChange={(e) =>\n setForm({ ...form, allowUnverifiedPublishers: e.target.checked })\n }\n />\n <span className=\"text-sm text-text-primary\">\n Allow unverified publishers to submit\n </span>\n </label>\n </div>\n <div className=\"mt-4 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setShowForm(false);\n setEditingId(null);\n setForm(emptyForm);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => void submit()}\n className=\"rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n {editingId ? 'Save changes' : 'Create'}\n </button>\n </div>\n </GlassCard>\n )}\n\n {loading && stores.length === 0 ? (\n <output className=\"block py-16 text-center text-text-secondary\">Loading stores…</output>\n ) : stores.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No stores yet\"\n description=\"Create your first store to give an audience a place to discover and install products.\"\n action={\n <button\n type=\"button\"\n onClick={openCreate}\n className=\"inline-flex items-center gap-2 rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n <Plus className=\"size-4\" />\n Create store\n </button>\n }\n />\n ) : (\n <div className=\"overflow-hidden rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-surface-muted text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">Slug</th>\n <th className=\"px-4 py-2\">Name</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Public</th>\n <th className=\"px-4 py-2\">Approval</th>\n <th className=\"px-4 py-2\">Currency</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-surface-raised\">\n {stores.map((s) => (\n <tr key={s.id} className=\"text-text-primary\">\n <td className=\"px-4 py-2 font-mono text-xs\">{s.slug}</td>\n <td className=\"px-4 py-2\">{s.name}</td>\n <td className=\"px-4 py-2\">\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {s.type}\n </span>\n </td>\n <td className=\"px-4 py-2 text-xs\">{s.isPublic ? 'Yes' : 'No'}</td>\n <td className=\"px-4 py-2 text-xs\">\n {s.requiresApproval ? 'Required' : 'Auto-publish'}\n </td>\n <td className=\"px-4 py-2 text-xs\">{s.currency}</td>\n <td className=\"px-4 py-2 text-right\">\n <button\n type=\"button\"\n onClick={() => openEdit(s)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-default px-2 py-1 text-xs text-text-primary hover:bg-surface-hover\"\n >\n <Pencil className=\"size-3\" />\n Edit\n </button>\n {s.id !== 'store_public_default' && (\n <button\n type=\"button\"\n onClick={() => setPendingDeleteId(s.id)}\n className=\"ml-2 inline-flex items-center gap-1 rounded-md border border-border-danger px-2 py-1 text-xs text-text-danger hover:bg-surface-danger-muted\"\n >\n <Trash2 className=\"size-3\" />\n Delete\n </button>\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </main>\n\n <AlertDialog\n open={pendingDeleteId !== null}\n onOpenChange={(open) => !open && setPendingDeleteId(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete this store?</AlertDialogTitle>\n <AlertDialogDescription>This is a soft delete.</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel onClick={() => setPendingDeleteId(null)}>Cancel</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-surface-danger text-text-on-danger hover:bg-surface-danger-hover\"\n onClick={() => {\n if (pendingDeleteId) void remove(pendingDeleteId);\n setPendingDeleteId(null);\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -180,7 +180,7 @@ var O = () => {
|
|
|
180
180
|
children: ["Unit", /* @__PURE__ */ y("select", {
|
|
181
181
|
value: k.unit,
|
|
182
182
|
onChange: R("unit"),
|
|
183
|
-
className: c
|
|
183
|
+
className: `${c} scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary`,
|
|
184
184
|
children: i.map((e) => /* @__PURE__ */ y("option", {
|
|
185
185
|
value: e,
|
|
186
186
|
children: o(e)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateSourcingRequestPage.js","names":[],"sources":["../../src/pages/CreateSourcingRequestPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport type { ChangeEvent, FC, FormEvent } from 'react';\nimport { useNavigate } from 'react-router';\nimport { FilePlus2, Loader2, PackageSearch } from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SourcingUnit } from '../generated/global-types';\nimport type { CreateSourcingRequestInput } from '../generated/global-types';\nimport { useStore } from '../providers/StoreProvider';\nimport { useSourcingApi } from '../hooks/useSourcing';\nimport {\n ContextRequiredPanel,\n helpClassName,\n inputClassName,\n joinStorePath,\n labelClassName,\n primaryButtonClassName,\n secondaryButtonClassName,\n SOURCING_UNITS,\n SourcingPageHeader,\n formatSourcingUnit,\n toErrorMessage,\n} from '../components/sourcing/SourcingUi';\n\ninterface RequestFormState {\n productName: string;\n category: string;\n summary: string;\n quantity: string;\n unit: SourcingUnit;\n deliveryWindowStart: string;\n deliveryWindowEnd: string;\n deliveryCountry: string;\n deliveryRegion: string;\n deliveryCity: string;\n targetUnitPrice: string;\n}\n\ntype FieldName = keyof RequestFormState;\ntype FieldErrors = Partial<Record<FieldName, string>>;\n\nconst INITIAL_FORM: RequestFormState = {\n productName: '',\n category: '',\n summary: '',\n quantity: '',\n unit: SourcingUnit.Unit,\n deliveryWindowStart: '',\n deliveryWindowEnd: '',\n deliveryCountry: '',\n deliveryRegion: '',\n deliveryCity: '',\n targetUnitPrice: '',\n};\n\nconst MONEY_PATTERN = /^(?:0|[1-9]\\d{0,15})(?:\\.\\d{1,2})?$/;\nconst CATEGORY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nfunction normalizeCategory(value: string): string {\n return value\n .trim()\n .toLowerCase()\n .replaceAll(/[_\\s]+/g, '-')\n .replaceAll(/-+/g, '-');\n}\n\nfunction validate(form: RequestFormState): FieldErrors {\n const errors: FieldErrors = {};\n const productName = form.productName.trim();\n const category = normalizeCategory(form.category);\n const quantity = Number(form.quantity);\n const start = new Date(form.deliveryWindowStart);\n const end = new Date(form.deliveryWindowEnd);\n\n if (productName.length < 2 || productName.length > 160) {\n errors.productName = 'Enter a product name between 2 and 160 characters.';\n }\n if (category.length < 2 || category.length > 64 || !CATEGORY_PATTERN.test(category)) {\n errors.category = 'Use 2–64 lowercase letters, numbers, and single hyphens.';\n }\n if (form.summary.trim().length > 1000) {\n errors.summary = 'Summary cannot exceed 1,000 characters.';\n }\n if (!Number.isSafeInteger(quantity) || quantity < 1 || quantity > 1_000_000) {\n errors.quantity = 'Quantity must be a whole number from 1 to 1,000,000.';\n }\n if (!form.deliveryWindowStart || Number.isNaN(start.getTime())) {\n errors.deliveryWindowStart = 'Choose a valid delivery window start.';\n }\n if (!form.deliveryWindowEnd || Number.isNaN(end.getTime()) || end <= start) {\n errors.deliveryWindowEnd = 'Delivery window end must be after its start.';\n }\n if (!/^[A-Za-z]{2}$/.test(form.deliveryCountry.trim())) {\n errors.deliveryCountry = 'Use a two-letter country code, such as IN or US.';\n }\n if (form.deliveryRegion.trim().length > 120) {\n errors.deliveryRegion = 'Region cannot exceed 120 characters.';\n }\n if (form.deliveryCity.trim().length > 120) {\n errors.deliveryCity = 'City cannot exceed 120 characters.';\n }\n if (\n form.targetUnitPrice &&\n (!MONEY_PATTERN.test(form.targetUnitPrice.trim()) || Number(form.targetUnitPrice) <= 0)\n ) {\n errors.targetUnitPrice = 'Enter a positive amount with no more than two decimal places.';\n }\n\n return errors;\n}\n\nexport const CreateSourcingRequestPage: FC = () => {\n const navigate = useNavigate();\n const { basePath } = useStore();\n const { createBuyerRequest, hasOrganizationContext } = useSourcingApi();\n const [form, setForm] = useState<RequestFormState>(INITIAL_FORM);\n const [errors, setErrors] = useState<FieldErrors>({});\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n const listPath = joinStorePath(basePath, 'sourcing/requests');\n\n const updateField =\n <TField extends FieldName>(field: TField) =>\n (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {\n const value = event.target.value;\n setForm((current) => ({ ...current, [field]: value }));\n setErrors((current) => ({ ...current, [field]: undefined }));\n };\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (!hasOrganizationContext || submitting) return;\n\n const validationErrors = validate(form);\n setErrors(validationErrors);\n setSubmitError(null);\n if (Object.keys(validationErrors).length > 0) return;\n\n const optional = (value: string) => value.trim() || undefined;\n const input: CreateSourcingRequestInput = {\n productName: form.productName.trim(),\n category: normalizeCategory(form.category),\n summary: optional(form.summary),\n quantity: Number(form.quantity),\n unit: form.unit,\n deliveryWindowStart: new Date(form.deliveryWindowStart).toISOString(),\n deliveryWindowEnd: new Date(form.deliveryWindowEnd).toISOString(),\n deliveryCountry: form.deliveryCountry.trim().toUpperCase(),\n deliveryRegion: optional(form.deliveryRegion),\n deliveryCity: optional(form.deliveryCity),\n targetUnitPrice: optional(form.targetUnitPrice),\n targetCurrency: form.targetUnitPrice.trim() ? 'USD' : undefined,\n };\n\n setSubmitting(true);\n try {\n const request = await createBuyerRequest(input);\n navigate(joinStorePath(basePath, `sourcing/requests/${request.id}`), { replace: true });\n } catch (error) {\n setSubmitError(toErrorMessage(error, 'The sourcing request could not be created.'));\n } finally {\n setSubmitting(false);\n }\n };\n\n const fieldError = (field: FieldName) => {\n const message = errors[field];\n return message ? (\n <p id={`${field}-error`} className=\"mt-1 text-xs text-status-error-text\">\n {message}\n </p>\n ) : null;\n };\n\n return (\n <div className=\"min-h-full bg-surface\">\n <SourcingPageHeader\n title=\"Create sourcing request\"\n description=\"Share only the purchase details verified publishers need to prepare an offer.\"\n icon={<FilePlus2 className=\"size-5\" aria-hidden=\"true\" />}\n onBack={() => navigate(listPath)}\n />\n\n <main className=\"mx-auto max-w-4xl px-4 py-6 sm:px-6\">\n <PagePurpose className=\"mb-6\">\n Your user and active organization own this request. Billing identity is resolved securely\n by Store; it is never entered in this form. Publishers see the product, quantity,\n destination, delivery window, and optional target price only.\n </PagePurpose>\n\n {!hasOrganizationContext ? (\n <ContextRequiredPanel\n title=\"Choose an organization before creating a request\"\n description=\"Use the app context switcher to select a global organization with a billing account. You do not need to select or create a farm workspace.\"\n />\n ) : (\n <form onSubmit={(event) => void handleSubmit(event)} noValidate>\n <GlassCard className=\"p-5 sm:p-6\">\n <section aria-labelledby=\"request-product-heading\">\n <div className=\"flex items-center gap-2\">\n <PackageSearch className=\"size-5 text-accent-primary\" aria-hidden=\"true\" />\n <h2\n id=\"request-product-heading\"\n className=\"text-lg font-semibold text-text-primary\"\n >\n Product needed\n </h2>\n </div>\n <div className=\"mt-5 grid gap-5 sm:grid-cols-2\">\n <label className={`${labelClassName} sm:col-span-2`}>\n Product name\n <input\n type=\"text\"\n value={form.productName}\n onChange={updateField('productName')}\n required\n maxLength={160}\n autoComplete=\"off\"\n aria-invalid={Boolean(errors.productName)}\n aria-describedby={errors.productName ? 'productName-error' : undefined}\n placeholder=\"Example: Organic basmati rice\"\n className={inputClassName}\n />\n {fieldError('productName')}\n </label>\n\n <label className={labelClassName}>\n Category\n <input\n type=\"text\"\n value={form.category}\n onChange={updateField('category')}\n onBlur={() =>\n setForm((current) => ({\n ...current,\n category: normalizeCategory(current.category),\n }))\n }\n required\n maxLength={64}\n autoComplete=\"off\"\n aria-invalid={Boolean(errors.category)}\n aria-describedby={errors.category ? 'category-error' : 'category-help'}\n placeholder=\"food-grains\"\n className={inputClassName}\n />\n {fieldError('category')}\n {!errors.category ? (\n <p id=\"category-help\" className={helpClassName}>\n A short category slug; spaces are converted to hyphens.\n </p>\n ) : null}\n </label>\n\n <label className={labelClassName}>\n Quantity\n <input\n type=\"number\"\n value={form.quantity}\n onChange={updateField('quantity')}\n required\n min={1}\n max={1_000_000}\n step={1}\n inputMode=\"numeric\"\n aria-invalid={Boolean(errors.quantity)}\n aria-describedby={errors.quantity ? 'quantity-error' : undefined}\n className={inputClassName}\n />\n {fieldError('quantity')}\n </label>\n\n <label className={labelClassName}>\n Unit\n <select\n value={form.unit}\n onChange={updateField('unit')}\n className={inputClassName}\n >\n {SOURCING_UNITS.map((unit) => (\n <option key={unit} value={unit}>\n {formatSourcingUnit(unit)}\n </option>\n ))}\n </select>\n </label>\n\n <label className={`${labelClassName} sm:col-span-2`}>\n Summary <span className=\"font-normal text-text-muted\">(optional)</span>\n <textarea\n value={form.summary}\n onChange={updateField('summary')}\n maxLength={1000}\n rows={4}\n aria-invalid={Boolean(errors.summary)}\n aria-describedby={errors.summary ? 'summary-error' : 'summary-help'}\n placeholder=\"Add grade, packaging, certification, or other offer requirements.\"\n className={`${inputClassName} resize-y`}\n />\n {fieldError('summary')}\n {!errors.summary ? (\n <p id=\"summary-help\" className={helpClassName}>\n {form.summary.length}/1,000 characters\n </p>\n ) : null}\n </label>\n <div\n role=\"note\"\n className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-4 text-sm text-status-warning-text sm:col-span-2\"\n >\n <p className=\"font-semibold\">This summary is visible to eligible publishers.</p>\n <p className=\"mt-1 leading-6\">\n Do not include personal or contact details, trust or compliance records,\n payment or billing information, credentials, private addresses, or other\n confidential data. Share only product and delivery requirements needed to\n prepare an offer.\n </p>\n </div>\n </div>\n </section>\n\n <section\n aria-labelledby=\"request-delivery-heading\"\n className=\"mt-8 border-t border-border-subtle pt-6\"\n >\n <h2\n id=\"request-delivery-heading\"\n className=\"text-lg font-semibold text-text-primary\"\n >\n Delivery\n </h2>\n <div className=\"mt-5 grid gap-5 sm:grid-cols-2\">\n <label className={labelClassName}>\n Window starts\n <input\n type=\"datetime-local\"\n value={form.deliveryWindowStart}\n onChange={updateField('deliveryWindowStart')}\n required\n aria-invalid={Boolean(errors.deliveryWindowStart)}\n aria-describedby={\n errors.deliveryWindowStart ? 'deliveryWindowStart-error' : undefined\n }\n className={inputClassName}\n />\n {fieldError('deliveryWindowStart')}\n </label>\n <label className={labelClassName}>\n Window ends\n <input\n type=\"datetime-local\"\n value={form.deliveryWindowEnd}\n onChange={updateField('deliveryWindowEnd')}\n required\n min={form.deliveryWindowStart || undefined}\n aria-invalid={Boolean(errors.deliveryWindowEnd)}\n aria-describedby={\n errors.deliveryWindowEnd ? 'deliveryWindowEnd-error' : undefined\n }\n className={inputClassName}\n />\n {fieldError('deliveryWindowEnd')}\n </label>\n <label className={labelClassName}>\n Country code\n <input\n type=\"text\"\n value={form.deliveryCountry}\n onChange={updateField('deliveryCountry')}\n required\n minLength={2}\n maxLength={2}\n autoComplete=\"country\"\n aria-invalid={Boolean(errors.deliveryCountry)}\n aria-describedby={\n errors.deliveryCountry ? 'deliveryCountry-error' : 'deliveryCountry-help'\n }\n placeholder=\"IN\"\n className={`${inputClassName} uppercase`}\n />\n {fieldError('deliveryCountry')}\n {!errors.deliveryCountry ? (\n <p id=\"deliveryCountry-help\" className={helpClassName}>\n ISO two-letter country code.\n </p>\n ) : null}\n </label>\n <label className={labelClassName}>\n Region or state <span className=\"font-normal text-text-muted\">(optional)</span>\n <input\n type=\"text\"\n value={form.deliveryRegion}\n onChange={updateField('deliveryRegion')}\n maxLength={120}\n autoComplete=\"address-level1\"\n aria-invalid={Boolean(errors.deliveryRegion)}\n aria-describedby={errors.deliveryRegion ? 'deliveryRegion-error' : undefined}\n className={inputClassName}\n />\n {fieldError('deliveryRegion')}\n </label>\n <label className={labelClassName}>\n City <span className=\"font-normal text-text-muted\">(optional)</span>\n <input\n type=\"text\"\n value={form.deliveryCity}\n onChange={updateField('deliveryCity')}\n maxLength={120}\n autoComplete=\"address-level2\"\n aria-invalid={Boolean(errors.deliveryCity)}\n aria-describedby={errors.deliveryCity ? 'deliveryCity-error' : undefined}\n className={inputClassName}\n />\n {fieldError('deliveryCity')}\n </label>\n </div>\n </section>\n\n <section\n aria-labelledby=\"request-budget-heading\"\n className=\"mt-8 border-t border-border-subtle pt-6\"\n >\n <h2 id=\"request-budget-heading\" className=\"text-lg font-semibold text-text-primary\">\n Target price <span className=\"font-normal text-text-muted\">(optional)</span>\n </h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n This is a target unit price, not an order total. Store calculates the accepted\n order total from the seller's persisted offer.\n </p>\n <div className=\"mt-5 grid gap-5 sm:grid-cols-2\">\n <label className={labelClassName}>\n Target unit price\n <input\n type=\"text\"\n value={form.targetUnitPrice}\n onChange={updateField('targetUnitPrice')}\n inputMode=\"decimal\"\n maxLength={19}\n aria-invalid={Boolean(errors.targetUnitPrice)}\n aria-describedby={\n errors.targetUnitPrice ? 'targetUnitPrice-error' : 'targetUnitPrice-help'\n }\n placeholder=\"125.50\"\n className={inputClassName}\n />\n {fieldError('targetUnitPrice')}\n {!errors.targetUnitPrice ? (\n <p id=\"targetUnitPrice-help\" className={helpClassName}>\n Positive amount with up to two decimal places.\n </p>\n ) : null}\n </label>\n <div>\n <span className={labelClassName}>Settlement currency</span>\n <div\n className={`${inputClassName} flex items-center bg-surface-sunken font-semibold`}\n >\n USD\n </div>\n <p className={helpClassName}>\n Store sourcing offers, orders, earnings, and payouts currently settle in USD.\n </p>\n </div>\n </div>\n </section>\n\n {submitError ? (\n <div\n role=\"alert\"\n className=\"mt-6 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n {submitError}\n </div>\n ) : null}\n\n <div className=\"mt-7 flex flex-col-reverse gap-3 border-t border-border-subtle pt-5 sm:flex-row sm:justify-end\">\n <button\n type=\"button\"\n onClick={() => navigate(listPath)}\n disabled={submitting}\n className={secondaryButtonClassName}\n >\n Cancel\n </button>\n <button type=\"submit\" disabled={submitting} className={primaryButtonClassName}>\n {submitting ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <FilePlus2 className=\"size-4\" aria-hidden=\"true\" />\n )}\n {submitting ? 'Creating request…' : 'Create request'}\n </button>\n </div>\n </GlassCard>\n </form>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CreateSourcingRequestPage.js","names":[],"sources":["../../src/pages/CreateSourcingRequestPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport type { ChangeEvent, FC, FormEvent } from 'react';\nimport { useNavigate } from 'react-router';\nimport { FilePlus2, Loader2, PackageSearch } from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SourcingUnit } from '../generated/global-types';\nimport type { CreateSourcingRequestInput } from '../generated/global-types';\nimport { useStore } from '../providers/StoreProvider';\nimport { useSourcingApi } from '../hooks/useSourcing';\nimport {\n ContextRequiredPanel,\n helpClassName,\n inputClassName,\n joinStorePath,\n labelClassName,\n primaryButtonClassName,\n secondaryButtonClassName,\n SOURCING_UNITS,\n SourcingPageHeader,\n formatSourcingUnit,\n toErrorMessage,\n} from '../components/sourcing/SourcingUi';\n\ninterface RequestFormState {\n productName: string;\n category: string;\n summary: string;\n quantity: string;\n unit: SourcingUnit;\n deliveryWindowStart: string;\n deliveryWindowEnd: string;\n deliveryCountry: string;\n deliveryRegion: string;\n deliveryCity: string;\n targetUnitPrice: string;\n}\n\ntype FieldName = keyof RequestFormState;\ntype FieldErrors = Partial<Record<FieldName, string>>;\n\nconst INITIAL_FORM: RequestFormState = {\n productName: '',\n category: '',\n summary: '',\n quantity: '',\n unit: SourcingUnit.Unit,\n deliveryWindowStart: '',\n deliveryWindowEnd: '',\n deliveryCountry: '',\n deliveryRegion: '',\n deliveryCity: '',\n targetUnitPrice: '',\n};\n\nconst MONEY_PATTERN = /^(?:0|[1-9]\\d{0,15})(?:\\.\\d{1,2})?$/;\nconst CATEGORY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nfunction normalizeCategory(value: string): string {\n return value\n .trim()\n .toLowerCase()\n .replaceAll(/[_\\s]+/g, '-')\n .replaceAll(/-+/g, '-');\n}\n\nfunction validate(form: RequestFormState): FieldErrors {\n const errors: FieldErrors = {};\n const productName = form.productName.trim();\n const category = normalizeCategory(form.category);\n const quantity = Number(form.quantity);\n const start = new Date(form.deliveryWindowStart);\n const end = new Date(form.deliveryWindowEnd);\n\n if (productName.length < 2 || productName.length > 160) {\n errors.productName = 'Enter a product name between 2 and 160 characters.';\n }\n if (category.length < 2 || category.length > 64 || !CATEGORY_PATTERN.test(category)) {\n errors.category = 'Use 2–64 lowercase letters, numbers, and single hyphens.';\n }\n if (form.summary.trim().length > 1000) {\n errors.summary = 'Summary cannot exceed 1,000 characters.';\n }\n if (!Number.isSafeInteger(quantity) || quantity < 1 || quantity > 1_000_000) {\n errors.quantity = 'Quantity must be a whole number from 1 to 1,000,000.';\n }\n if (!form.deliveryWindowStart || Number.isNaN(start.getTime())) {\n errors.deliveryWindowStart = 'Choose a valid delivery window start.';\n }\n if (!form.deliveryWindowEnd || Number.isNaN(end.getTime()) || end <= start) {\n errors.deliveryWindowEnd = 'Delivery window end must be after its start.';\n }\n if (!/^[A-Za-z]{2}$/.test(form.deliveryCountry.trim())) {\n errors.deliveryCountry = 'Use a two-letter country code, such as IN or US.';\n }\n if (form.deliveryRegion.trim().length > 120) {\n errors.deliveryRegion = 'Region cannot exceed 120 characters.';\n }\n if (form.deliveryCity.trim().length > 120) {\n errors.deliveryCity = 'City cannot exceed 120 characters.';\n }\n if (\n form.targetUnitPrice &&\n (!MONEY_PATTERN.test(form.targetUnitPrice.trim()) || Number(form.targetUnitPrice) <= 0)\n ) {\n errors.targetUnitPrice = 'Enter a positive amount with no more than two decimal places.';\n }\n\n return errors;\n}\n\nexport const CreateSourcingRequestPage: FC = () => {\n const navigate = useNavigate();\n const { basePath } = useStore();\n const { createBuyerRequest, hasOrganizationContext } = useSourcingApi();\n const [form, setForm] = useState<RequestFormState>(INITIAL_FORM);\n const [errors, setErrors] = useState<FieldErrors>({});\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n const listPath = joinStorePath(basePath, 'sourcing/requests');\n\n const updateField =\n <TField extends FieldName>(field: TField) =>\n (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {\n const value = event.target.value;\n setForm((current) => ({ ...current, [field]: value }));\n setErrors((current) => ({ ...current, [field]: undefined }));\n };\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (!hasOrganizationContext || submitting) return;\n\n const validationErrors = validate(form);\n setErrors(validationErrors);\n setSubmitError(null);\n if (Object.keys(validationErrors).length > 0) return;\n\n const optional = (value: string) => value.trim() || undefined;\n const input: CreateSourcingRequestInput = {\n productName: form.productName.trim(),\n category: normalizeCategory(form.category),\n summary: optional(form.summary),\n quantity: Number(form.quantity),\n unit: form.unit,\n deliveryWindowStart: new Date(form.deliveryWindowStart).toISOString(),\n deliveryWindowEnd: new Date(form.deliveryWindowEnd).toISOString(),\n deliveryCountry: form.deliveryCountry.trim().toUpperCase(),\n deliveryRegion: optional(form.deliveryRegion),\n deliveryCity: optional(form.deliveryCity),\n targetUnitPrice: optional(form.targetUnitPrice),\n targetCurrency: form.targetUnitPrice.trim() ? 'USD' : undefined,\n };\n\n setSubmitting(true);\n try {\n const request = await createBuyerRequest(input);\n navigate(joinStorePath(basePath, `sourcing/requests/${request.id}`), { replace: true });\n } catch (error) {\n setSubmitError(toErrorMessage(error, 'The sourcing request could not be created.'));\n } finally {\n setSubmitting(false);\n }\n };\n\n const fieldError = (field: FieldName) => {\n const message = errors[field];\n return message ? (\n <p id={`${field}-error`} className=\"mt-1 text-xs text-status-error-text\">\n {message}\n </p>\n ) : null;\n };\n\n return (\n <div className=\"min-h-full bg-surface\">\n <SourcingPageHeader\n title=\"Create sourcing request\"\n description=\"Share only the purchase details verified publishers need to prepare an offer.\"\n icon={<FilePlus2 className=\"size-5\" aria-hidden=\"true\" />}\n onBack={() => navigate(listPath)}\n />\n\n <main className=\"mx-auto max-w-4xl px-4 py-6 sm:px-6\">\n <PagePurpose className=\"mb-6\">\n Your user and active organization own this request. Billing identity is resolved securely\n by Store; it is never entered in this form. Publishers see the product, quantity,\n destination, delivery window, and optional target price only.\n </PagePurpose>\n\n {!hasOrganizationContext ? (\n <ContextRequiredPanel\n title=\"Choose an organization before creating a request\"\n description=\"Use the app context switcher to select a global organization with a billing account. You do not need to select or create a farm workspace.\"\n />\n ) : (\n <form onSubmit={(event) => void handleSubmit(event)} noValidate>\n <GlassCard className=\"p-5 sm:p-6\">\n <section aria-labelledby=\"request-product-heading\">\n <div className=\"flex items-center gap-2\">\n <PackageSearch className=\"size-5 text-accent-primary\" aria-hidden=\"true\" />\n <h2\n id=\"request-product-heading\"\n className=\"text-lg font-semibold text-text-primary\"\n >\n Product needed\n </h2>\n </div>\n <div className=\"mt-5 grid gap-5 sm:grid-cols-2\">\n <label className={`${labelClassName} sm:col-span-2`}>\n Product name\n <input\n type=\"text\"\n value={form.productName}\n onChange={updateField('productName')}\n required\n maxLength={160}\n autoComplete=\"off\"\n aria-invalid={Boolean(errors.productName)}\n aria-describedby={errors.productName ? 'productName-error' : undefined}\n placeholder=\"Example: Organic basmati rice\"\n className={inputClassName}\n />\n {fieldError('productName')}\n </label>\n\n <label className={labelClassName}>\n Category\n <input\n type=\"text\"\n value={form.category}\n onChange={updateField('category')}\n onBlur={() =>\n setForm((current) => ({\n ...current,\n category: normalizeCategory(current.category),\n }))\n }\n required\n maxLength={64}\n autoComplete=\"off\"\n aria-invalid={Boolean(errors.category)}\n aria-describedby={errors.category ? 'category-error' : 'category-help'}\n placeholder=\"food-grains\"\n className={inputClassName}\n />\n {fieldError('category')}\n {!errors.category ? (\n <p id=\"category-help\" className={helpClassName}>\n A short category slug; spaces are converted to hyphens.\n </p>\n ) : null}\n </label>\n\n <label className={labelClassName}>\n Quantity\n <input\n type=\"number\"\n value={form.quantity}\n onChange={updateField('quantity')}\n required\n min={1}\n max={1_000_000}\n step={1}\n inputMode=\"numeric\"\n aria-invalid={Boolean(errors.quantity)}\n aria-describedby={errors.quantity ? 'quantity-error' : undefined}\n className={inputClassName}\n />\n {fieldError('quantity')}\n </label>\n\n <label className={labelClassName}>\n Unit\n <select\n value={form.unit}\n onChange={updateField('unit')}\n className={`${inputClassName} scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary`}\n >\n {SOURCING_UNITS.map((unit) => (\n <option key={unit} value={unit}>\n {formatSourcingUnit(unit)}\n </option>\n ))}\n </select>\n </label>\n\n <label className={`${labelClassName} sm:col-span-2`}>\n Summary <span className=\"font-normal text-text-muted\">(optional)</span>\n <textarea\n value={form.summary}\n onChange={updateField('summary')}\n maxLength={1000}\n rows={4}\n aria-invalid={Boolean(errors.summary)}\n aria-describedby={errors.summary ? 'summary-error' : 'summary-help'}\n placeholder=\"Add grade, packaging, certification, or other offer requirements.\"\n className={`${inputClassName} resize-y`}\n />\n {fieldError('summary')}\n {!errors.summary ? (\n <p id=\"summary-help\" className={helpClassName}>\n {form.summary.length}/1,000 characters\n </p>\n ) : null}\n </label>\n <div\n role=\"note\"\n className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-4 text-sm text-status-warning-text sm:col-span-2\"\n >\n <p className=\"font-semibold\">This summary is visible to eligible publishers.</p>\n <p className=\"mt-1 leading-6\">\n Do not include personal or contact details, trust or compliance records,\n payment or billing information, credentials, private addresses, or other\n confidential data. Share only product and delivery requirements needed to\n prepare an offer.\n </p>\n </div>\n </div>\n </section>\n\n <section\n aria-labelledby=\"request-delivery-heading\"\n className=\"mt-8 border-t border-border-subtle pt-6\"\n >\n <h2\n id=\"request-delivery-heading\"\n className=\"text-lg font-semibold text-text-primary\"\n >\n Delivery\n </h2>\n <div className=\"mt-5 grid gap-5 sm:grid-cols-2\">\n <label className={labelClassName}>\n Window starts\n <input\n type=\"datetime-local\"\n value={form.deliveryWindowStart}\n onChange={updateField('deliveryWindowStart')}\n required\n aria-invalid={Boolean(errors.deliveryWindowStart)}\n aria-describedby={\n errors.deliveryWindowStart ? 'deliveryWindowStart-error' : undefined\n }\n className={inputClassName}\n />\n {fieldError('deliveryWindowStart')}\n </label>\n <label className={labelClassName}>\n Window ends\n <input\n type=\"datetime-local\"\n value={form.deliveryWindowEnd}\n onChange={updateField('deliveryWindowEnd')}\n required\n min={form.deliveryWindowStart || undefined}\n aria-invalid={Boolean(errors.deliveryWindowEnd)}\n aria-describedby={\n errors.deliveryWindowEnd ? 'deliveryWindowEnd-error' : undefined\n }\n className={inputClassName}\n />\n {fieldError('deliveryWindowEnd')}\n </label>\n <label className={labelClassName}>\n Country code\n <input\n type=\"text\"\n value={form.deliveryCountry}\n onChange={updateField('deliveryCountry')}\n required\n minLength={2}\n maxLength={2}\n autoComplete=\"country\"\n aria-invalid={Boolean(errors.deliveryCountry)}\n aria-describedby={\n errors.deliveryCountry ? 'deliveryCountry-error' : 'deliveryCountry-help'\n }\n placeholder=\"IN\"\n className={`${inputClassName} uppercase`}\n />\n {fieldError('deliveryCountry')}\n {!errors.deliveryCountry ? (\n <p id=\"deliveryCountry-help\" className={helpClassName}>\n ISO two-letter country code.\n </p>\n ) : null}\n </label>\n <label className={labelClassName}>\n Region or state <span className=\"font-normal text-text-muted\">(optional)</span>\n <input\n type=\"text\"\n value={form.deliveryRegion}\n onChange={updateField('deliveryRegion')}\n maxLength={120}\n autoComplete=\"address-level1\"\n aria-invalid={Boolean(errors.deliveryRegion)}\n aria-describedby={errors.deliveryRegion ? 'deliveryRegion-error' : undefined}\n className={inputClassName}\n />\n {fieldError('deliveryRegion')}\n </label>\n <label className={labelClassName}>\n City <span className=\"font-normal text-text-muted\">(optional)</span>\n <input\n type=\"text\"\n value={form.deliveryCity}\n onChange={updateField('deliveryCity')}\n maxLength={120}\n autoComplete=\"address-level2\"\n aria-invalid={Boolean(errors.deliveryCity)}\n aria-describedby={errors.deliveryCity ? 'deliveryCity-error' : undefined}\n className={inputClassName}\n />\n {fieldError('deliveryCity')}\n </label>\n </div>\n </section>\n\n <section\n aria-labelledby=\"request-budget-heading\"\n className=\"mt-8 border-t border-border-subtle pt-6\"\n >\n <h2 id=\"request-budget-heading\" className=\"text-lg font-semibold text-text-primary\">\n Target price <span className=\"font-normal text-text-muted\">(optional)</span>\n </h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n This is a target unit price, not an order total. Store calculates the accepted\n order total from the seller's persisted offer.\n </p>\n <div className=\"mt-5 grid gap-5 sm:grid-cols-2\">\n <label className={labelClassName}>\n Target unit price\n <input\n type=\"text\"\n value={form.targetUnitPrice}\n onChange={updateField('targetUnitPrice')}\n inputMode=\"decimal\"\n maxLength={19}\n aria-invalid={Boolean(errors.targetUnitPrice)}\n aria-describedby={\n errors.targetUnitPrice ? 'targetUnitPrice-error' : 'targetUnitPrice-help'\n }\n placeholder=\"125.50\"\n className={inputClassName}\n />\n {fieldError('targetUnitPrice')}\n {!errors.targetUnitPrice ? (\n <p id=\"targetUnitPrice-help\" className={helpClassName}>\n Positive amount with up to two decimal places.\n </p>\n ) : null}\n </label>\n <div>\n <span className={labelClassName}>Settlement currency</span>\n <div\n className={`${inputClassName} flex items-center bg-surface-sunken font-semibold`}\n >\n USD\n </div>\n <p className={helpClassName}>\n Store sourcing offers, orders, earnings, and payouts currently settle in USD.\n </p>\n </div>\n </div>\n </section>\n\n {submitError ? (\n <div\n role=\"alert\"\n className=\"mt-6 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n {submitError}\n </div>\n ) : null}\n\n <div className=\"mt-7 flex flex-col-reverse gap-3 border-t border-border-subtle pt-5 sm:flex-row sm:justify-end\">\n <button\n type=\"button\"\n onClick={() => navigate(listPath)}\n disabled={submitting}\n className={secondaryButtonClassName}\n >\n Cancel\n </button>\n <button type=\"submit\" disabled={submitting} className={primaryButtonClassName}>\n {submitting ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <FilePlus2 className=\"size-4\" aria-hidden=\"true\" />\n )}\n {submitting ? 'Creating request…' : 'Create request'}\n </button>\n </div>\n </GlassCard>\n </form>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -79,7 +79,7 @@ var N = () => {
|
|
|
79
79
|
}), /* @__PURE__ */ O("select", {
|
|
80
80
|
value: U,
|
|
81
81
|
onChange: (e) => W(M(e.target.value)),
|
|
82
|
-
className: "mt-1.5 w-full rounded-lg border border-border-default bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg focus:ring-2 focus:ring-action-primary-bg/20",
|
|
82
|
+
className: "mt-1.5 w-full rounded-lg border border-border-default bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg focus:ring-2 focus:ring-action-primary-bg/20 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
83
83
|
children: [
|
|
84
84
|
/* @__PURE__ */ D("option", {
|
|
85
85
|
value: "",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SellerSourcingOffersPage.js","names":[],"sources":["../../src/pages/SellerSourcingOffersPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport type { FC } from 'react';\nimport { useNavigate } from 'react-router';\nimport {\n ArrowRight,\n CalendarDays,\n CircleDollarSign,\n MapPin,\n PackageSearch,\n RefreshCw,\n TicketCheck,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SourcingOfferStatus } from '../generated/global-types';\nimport { useStore } from '../providers/StoreProvider';\nimport { type SellerSourcingOffer, useSourcingApi } from '../hooks/useSourcing';\nimport { usePublisherSourcingAccess } from '../providers/SourcingAccessProvider';\nimport {\n ContextRequiredPanel,\n formatDate,\n formatDateTime,\n formatLocation,\n formatMoney,\n formatSourcingUnit,\n joinStorePath,\n primaryButtonClassName,\n secondaryButtonClassName,\n SourcingEmptyState,\n SourcingErrorState,\n SourcingLoadingState,\n SourcingPageHeader,\n SourcingStatusBadge,\n toErrorMessage,\n} from '../components/sourcing/SourcingUi';\n\nconst PAGE_SIZE = 30;\ntype OfferFilter = SourcingOfferStatus | '';\n\nfunction parseOfferFilter(value: string): OfferFilter {\n switch (value) {\n case SourcingOfferStatus.Active:\n return SourcingOfferStatus.Active;\n case SourcingOfferStatus.Accepted:\n return SourcingOfferStatus.Accepted;\n case SourcingOfferStatus.Rejected:\n return SourcingOfferStatus.Rejected;\n default:\n return '';\n }\n}\n\nexport const SellerSourcingOffersPage: FC = () => {\n const navigate = useNavigate();\n const { basePath } = useStore();\n const {\n profile,\n loading: accessLoading,\n error: accessError,\n isEligible,\n refresh: refreshAccess,\n } = usePublisherSourcingAccess();\n const { listSellerOffers } = useSourcingApi();\n const [offers, setOffers] = useState<SellerSourcingOffer[]>([]);\n const [status, setStatus] = useState<OfferFilter>('');\n const [offset, setOffset] = useState(0);\n const [hasMore, setHasMore] = useState(false);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const load = useCallback(\n async (nextOffset = 0, append = false) => {\n if (!isEligible) return;\n setLoading(true);\n setError(null);\n try {\n const result = await listSellerOffers({\n status: status || undefined,\n limit: PAGE_SIZE,\n offset: nextOffset,\n });\n setOffers((current) => (append ? [...current, ...result] : result));\n setOffset(nextOffset);\n setHasMore(result.length === PAGE_SIZE);\n } catch (loadError) {\n setError(toErrorMessage(loadError, 'Your sourcing offers could not be loaded.'));\n } finally {\n setLoading(false);\n }\n },\n [isEligible, listSellerOffers, status]\n );\n\n useEffect(() => {\n void load();\n }, [load]);\n\n return (\n <div className=\"min-h-full bg-surface\">\n <SourcingPageHeader\n title=\"My sourcing offers\"\n description=\"Track and update the offers submitted by your verified Store Publisher.\"\n icon={<TicketCheck className=\"size-5\" aria-hidden=\"true\" />}\n actions={\n <button\n type=\"button\"\n onClick={() => navigate(joinStorePath(basePath, 'publisher/sourcing/opportunities'))}\n className={primaryButtonClassName}\n >\n <PackageSearch className=\"size-4\" aria-hidden=\"true\" />\n Find opportunities\n </button>\n }\n />\n\n <main className=\"mx-auto max-w-6xl px-4 py-6 sm:px-6\">\n <PagePurpose className=\"mb-6\">\n Active offers can be updated while their request remains open. Accepted and rejected\n offers are final. When a buyer accepts your offer, the resulting Store order and\n fulfillment status appear here without exposing another publisher's private offer.\n </PagePurpose>\n\n {accessLoading ? (\n <SourcingLoadingState label=\"Checking Store Publisher access…\" />\n ) : accessError ? (\n <SourcingErrorState\n title=\"Publisher access could not be checked\"\n message={accessError}\n onRetry={() => void refreshAccess()}\n />\n ) : !profile ? (\n <ContextRequiredPanel\n title=\"Store Publisher profile required\"\n description=\"A Store Publisher profile is required to submit or review sourcing offers.\"\n />\n ) : !isEligible ? (\n <ContextRequiredPanel\n title=\"Verified publisher status required\"\n description={`The publisher profile “${profile.name}” is currently ${profile.verificationStatus.toLowerCase().replaceAll('_', ' ')}. Offers are available only to active verified publishers.`}\n />\n ) : (\n <>\n <div className=\"mb-5 flex flex-col gap-3 rounded-xl border border-border-subtle bg-surface-elevated p-4 sm:flex-row sm:items-end sm:justify-between\">\n <label className=\"block sm:w-64\">\n <span className=\"text-sm font-medium text-text-primary\">Offer status</span>\n <select\n value={status}\n onChange={(event) => setStatus(parseOfferFilter(event.target.value))}\n className=\"mt-1.5 w-full rounded-lg border border-border-default bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg focus:ring-2 focus:ring-action-primary-bg/20\"\n >\n <option value=\"\">All statuses</option>\n <option value={SourcingOfferStatus.Active}>Active</option>\n <option value={SourcingOfferStatus.Accepted}>Accepted</option>\n <option value={SourcingOfferStatus.Rejected}>Rejected</option>\n </select>\n </label>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={loading}\n className={secondaryButtonClassName}\n >\n <RefreshCw\n className={`size-4 ${loading ? 'animate-spin' : ''}`}\n aria-hidden=\"true\"\n />\n Refresh\n </button>\n </div>\n\n {loading && offers.length === 0 ? (\n <SourcingLoadingState label=\"Loading your sourcing offers…\" />\n ) : error && offers.length === 0 ? (\n <SourcingErrorState\n title=\"Offers could not be loaded\"\n message={error}\n onRetry={() => void load()}\n />\n ) : offers.length === 0 ? (\n <SourcingEmptyState\n title={status ? 'No offers match this status' : 'No sourcing offers yet'}\n description={\n status\n ? 'Choose another status to review the rest of your offers.'\n : 'Browse open buyer opportunities to create your first sourcing offer.'\n }\n action={\n <button\n type=\"button\"\n onClick={() =>\n navigate(joinStorePath(basePath, 'publisher/sourcing/opportunities'))\n }\n className={primaryButtonClassName}\n >\n <PackageSearch className=\"size-4\" aria-hidden=\"true\" />\n Browse opportunities\n </button>\n }\n />\n ) : (\n <>\n {error ? (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n {error}\n </div>\n ) : null}\n <ul className=\"grid gap-4 md:grid-cols-2\">\n {offers.map((offer) => (\n <li key={offer.id}>\n <GlassCard className=\"flex h-full flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-semibold uppercase tracking-wide text-accent-primary\">\n {offer.request?.category ?? 'Sourcing request'}\n </p>\n <h2 className=\"mt-1 line-clamp-2 text-lg font-semibold text-text-primary\">\n {offer.request?.productName ?? `Request ${offer.requestId}`}\n </h2>\n </div>\n <SourcingStatusBadge status={offer.status} />\n </div>\n\n <dl className=\"mt-5 grid gap-4 text-sm sm:grid-cols-2\">\n <div className=\"flex gap-2\">\n <CircleDollarSign\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Your unit price</dt>\n <dd className=\"font-semibold text-text-primary\">\n {formatMoney(offer.unitPrice, offer.currency)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <PackageSearch\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Quantity</dt>\n <dd className=\"font-medium text-text-primary\">\n {offer.quantity.toLocaleString()} {formatSourcingUnit(offer.unit)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <CalendarDays\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Promised delivery</dt>\n <dd className=\"font-medium text-text-primary\">\n {formatDateTime(offer.deliveryDate)}\n </dd>\n </div>\n </div>\n {offer.request ? (\n <div className=\"flex gap-2\">\n <MapPin\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Destination</dt>\n <dd className=\"font-medium text-text-primary\">\n {formatLocation({\n country: offer.request.deliveryCountry,\n region: offer.request.deliveryRegion,\n city: offer.request.deliveryCity,\n })}\n </dd>\n </div>\n </div>\n ) : null}\n </dl>\n\n {offer.message ? (\n <p className=\"mt-4 line-clamp-3 rounded-lg bg-surface-sunken p-3 text-sm leading-6 text-text-secondary\">\n {offer.message}\n </p>\n ) : null}\n\n {offer.order ? (\n <div className=\"mt-4 rounded-lg border border-status-success-border bg-status-success-bg-subtle p-3\">\n <p className=\"text-xs font-medium text-status-success-text\">\n Store order {offer.order.id}\n </p>\n <p className=\"mt-1 text-sm font-semibold text-text-primary\">\n {formatMoney(offer.order.total, offer.order.currency)} ·{' '}\n {offer.order.status}\n </p>\n {offer.order.fulfillment ? (\n <p className=\"mt-1 text-xs text-text-secondary\">\n Fulfillment: {offer.order.fulfillment.status.replaceAll('_', ' ')}\n </p>\n ) : null}\n </div>\n ) : null}\n\n <div className=\"mt-auto flex items-center justify-between gap-3 border-t border-border-subtle pt-4\">\n <p className=\"text-xs text-text-muted\">\n Updated {formatDate(offer.updatedAt)}\n </p>\n <button\n type=\"button\"\n onClick={() =>\n navigate(\n joinStorePath(\n basePath,\n `publisher/sourcing/opportunities/${offer.requestId}`\n )\n )\n }\n className={secondaryButtonClassName}\n >\n {offer.status === SourcingOfferStatus.Active\n ? 'Review or update'\n : 'View details'}\n <ArrowRight className=\"size-4\" aria-hidden=\"true\" />\n </button>\n </div>\n </GlassCard>\n </li>\n ))}\n </ul>\n\n {hasMore ? (\n <div className=\"mt-6 flex justify-center\">\n <button\n type=\"button\"\n onClick={() => void load(offset + PAGE_SIZE, true)}\n disabled={loading}\n className={secondaryButtonClassName}\n >\n {loading ? 'Loading…' : 'Load more'}\n </button>\n </div>\n ) : null}\n </>\n )}\n </>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SellerSourcingOffersPage.js","names":[],"sources":["../../src/pages/SellerSourcingOffersPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport type { FC } from 'react';\nimport { useNavigate } from 'react-router';\nimport {\n ArrowRight,\n CalendarDays,\n CircleDollarSign,\n MapPin,\n PackageSearch,\n RefreshCw,\n TicketCheck,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SourcingOfferStatus } from '../generated/global-types';\nimport { useStore } from '../providers/StoreProvider';\nimport { type SellerSourcingOffer, useSourcingApi } from '../hooks/useSourcing';\nimport { usePublisherSourcingAccess } from '../providers/SourcingAccessProvider';\nimport {\n ContextRequiredPanel,\n formatDate,\n formatDateTime,\n formatLocation,\n formatMoney,\n formatSourcingUnit,\n joinStorePath,\n primaryButtonClassName,\n secondaryButtonClassName,\n SourcingEmptyState,\n SourcingErrorState,\n SourcingLoadingState,\n SourcingPageHeader,\n SourcingStatusBadge,\n toErrorMessage,\n} from '../components/sourcing/SourcingUi';\n\nconst PAGE_SIZE = 30;\ntype OfferFilter = SourcingOfferStatus | '';\n\nfunction parseOfferFilter(value: string): OfferFilter {\n switch (value) {\n case SourcingOfferStatus.Active:\n return SourcingOfferStatus.Active;\n case SourcingOfferStatus.Accepted:\n return SourcingOfferStatus.Accepted;\n case SourcingOfferStatus.Rejected:\n return SourcingOfferStatus.Rejected;\n default:\n return '';\n }\n}\n\nexport const SellerSourcingOffersPage: FC = () => {\n const navigate = useNavigate();\n const { basePath } = useStore();\n const {\n profile,\n loading: accessLoading,\n error: accessError,\n isEligible,\n refresh: refreshAccess,\n } = usePublisherSourcingAccess();\n const { listSellerOffers } = useSourcingApi();\n const [offers, setOffers] = useState<SellerSourcingOffer[]>([]);\n const [status, setStatus] = useState<OfferFilter>('');\n const [offset, setOffset] = useState(0);\n const [hasMore, setHasMore] = useState(false);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const load = useCallback(\n async (nextOffset = 0, append = false) => {\n if (!isEligible) return;\n setLoading(true);\n setError(null);\n try {\n const result = await listSellerOffers({\n status: status || undefined,\n limit: PAGE_SIZE,\n offset: nextOffset,\n });\n setOffers((current) => (append ? [...current, ...result] : result));\n setOffset(nextOffset);\n setHasMore(result.length === PAGE_SIZE);\n } catch (loadError) {\n setError(toErrorMessage(loadError, 'Your sourcing offers could not be loaded.'));\n } finally {\n setLoading(false);\n }\n },\n [isEligible, listSellerOffers, status]\n );\n\n useEffect(() => {\n void load();\n }, [load]);\n\n return (\n <div className=\"min-h-full bg-surface\">\n <SourcingPageHeader\n title=\"My sourcing offers\"\n description=\"Track and update the offers submitted by your verified Store Publisher.\"\n icon={<TicketCheck className=\"size-5\" aria-hidden=\"true\" />}\n actions={\n <button\n type=\"button\"\n onClick={() => navigate(joinStorePath(basePath, 'publisher/sourcing/opportunities'))}\n className={primaryButtonClassName}\n >\n <PackageSearch className=\"size-4\" aria-hidden=\"true\" />\n Find opportunities\n </button>\n }\n />\n\n <main className=\"mx-auto max-w-6xl px-4 py-6 sm:px-6\">\n <PagePurpose className=\"mb-6\">\n Active offers can be updated while their request remains open. Accepted and rejected\n offers are final. When a buyer accepts your offer, the resulting Store order and\n fulfillment status appear here without exposing another publisher's private offer.\n </PagePurpose>\n\n {accessLoading ? (\n <SourcingLoadingState label=\"Checking Store Publisher access…\" />\n ) : accessError ? (\n <SourcingErrorState\n title=\"Publisher access could not be checked\"\n message={accessError}\n onRetry={() => void refreshAccess()}\n />\n ) : !profile ? (\n <ContextRequiredPanel\n title=\"Store Publisher profile required\"\n description=\"A Store Publisher profile is required to submit or review sourcing offers.\"\n />\n ) : !isEligible ? (\n <ContextRequiredPanel\n title=\"Verified publisher status required\"\n description={`The publisher profile “${profile.name}” is currently ${profile.verificationStatus.toLowerCase().replaceAll('_', ' ')}. Offers are available only to active verified publishers.`}\n />\n ) : (\n <>\n <div className=\"mb-5 flex flex-col gap-3 rounded-xl border border-border-subtle bg-surface-elevated p-4 sm:flex-row sm:items-end sm:justify-between\">\n <label className=\"block sm:w-64\">\n <span className=\"text-sm font-medium text-text-primary\">Offer status</span>\n <select\n value={status}\n onChange={(event) => setStatus(parseOfferFilter(event.target.value))}\n className=\"mt-1.5 w-full rounded-lg border border-border-default bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg focus:ring-2 focus:ring-action-primary-bg/20 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"\">All statuses</option>\n <option value={SourcingOfferStatus.Active}>Active</option>\n <option value={SourcingOfferStatus.Accepted}>Accepted</option>\n <option value={SourcingOfferStatus.Rejected}>Rejected</option>\n </select>\n </label>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={loading}\n className={secondaryButtonClassName}\n >\n <RefreshCw\n className={`size-4 ${loading ? 'animate-spin' : ''}`}\n aria-hidden=\"true\"\n />\n Refresh\n </button>\n </div>\n\n {loading && offers.length === 0 ? (\n <SourcingLoadingState label=\"Loading your sourcing offers…\" />\n ) : error && offers.length === 0 ? (\n <SourcingErrorState\n title=\"Offers could not be loaded\"\n message={error}\n onRetry={() => void load()}\n />\n ) : offers.length === 0 ? (\n <SourcingEmptyState\n title={status ? 'No offers match this status' : 'No sourcing offers yet'}\n description={\n status\n ? 'Choose another status to review the rest of your offers.'\n : 'Browse open buyer opportunities to create your first sourcing offer.'\n }\n action={\n <button\n type=\"button\"\n onClick={() =>\n navigate(joinStorePath(basePath, 'publisher/sourcing/opportunities'))\n }\n className={primaryButtonClassName}\n >\n <PackageSearch className=\"size-4\" aria-hidden=\"true\" />\n Browse opportunities\n </button>\n }\n />\n ) : (\n <>\n {error ? (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n {error}\n </div>\n ) : null}\n <ul className=\"grid gap-4 md:grid-cols-2\">\n {offers.map((offer) => (\n <li key={offer.id}>\n <GlassCard className=\"flex h-full flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-semibold uppercase tracking-wide text-accent-primary\">\n {offer.request?.category ?? 'Sourcing request'}\n </p>\n <h2 className=\"mt-1 line-clamp-2 text-lg font-semibold text-text-primary\">\n {offer.request?.productName ?? `Request ${offer.requestId}`}\n </h2>\n </div>\n <SourcingStatusBadge status={offer.status} />\n </div>\n\n <dl className=\"mt-5 grid gap-4 text-sm sm:grid-cols-2\">\n <div className=\"flex gap-2\">\n <CircleDollarSign\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Your unit price</dt>\n <dd className=\"font-semibold text-text-primary\">\n {formatMoney(offer.unitPrice, offer.currency)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <PackageSearch\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Quantity</dt>\n <dd className=\"font-medium text-text-primary\">\n {offer.quantity.toLocaleString()} {formatSourcingUnit(offer.unit)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <CalendarDays\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Promised delivery</dt>\n <dd className=\"font-medium text-text-primary\">\n {formatDateTime(offer.deliveryDate)}\n </dd>\n </div>\n </div>\n {offer.request ? (\n <div className=\"flex gap-2\">\n <MapPin\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Destination</dt>\n <dd className=\"font-medium text-text-primary\">\n {formatLocation({\n country: offer.request.deliveryCountry,\n region: offer.request.deliveryRegion,\n city: offer.request.deliveryCity,\n })}\n </dd>\n </div>\n </div>\n ) : null}\n </dl>\n\n {offer.message ? (\n <p className=\"mt-4 line-clamp-3 rounded-lg bg-surface-sunken p-3 text-sm leading-6 text-text-secondary\">\n {offer.message}\n </p>\n ) : null}\n\n {offer.order ? (\n <div className=\"mt-4 rounded-lg border border-status-success-border bg-status-success-bg-subtle p-3\">\n <p className=\"text-xs font-medium text-status-success-text\">\n Store order {offer.order.id}\n </p>\n <p className=\"mt-1 text-sm font-semibold text-text-primary\">\n {formatMoney(offer.order.total, offer.order.currency)} ·{' '}\n {offer.order.status}\n </p>\n {offer.order.fulfillment ? (\n <p className=\"mt-1 text-xs text-text-secondary\">\n Fulfillment: {offer.order.fulfillment.status.replaceAll('_', ' ')}\n </p>\n ) : null}\n </div>\n ) : null}\n\n <div className=\"mt-auto flex items-center justify-between gap-3 border-t border-border-subtle pt-4\">\n <p className=\"text-xs text-text-muted\">\n Updated {formatDate(offer.updatedAt)}\n </p>\n <button\n type=\"button\"\n onClick={() =>\n navigate(\n joinStorePath(\n basePath,\n `publisher/sourcing/opportunities/${offer.requestId}`\n )\n )\n }\n className={secondaryButtonClassName}\n >\n {offer.status === SourcingOfferStatus.Active\n ? 'Review or update'\n : 'View details'}\n <ArrowRight className=\"size-4\" aria-hidden=\"true\" />\n </button>\n </div>\n </GlassCard>\n </li>\n ))}\n </ul>\n\n {hasMore ? (\n <div className=\"mt-6 flex justify-center\">\n <button\n type=\"button\"\n onClick={() => void load(offset + PAGE_SIZE, true)}\n disabled={loading}\n className={secondaryButtonClassName}\n >\n {loading ? 'Loading…' : 'Load more'}\n </button>\n </div>\n ) : null}\n </>\n )}\n </>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -338,8 +338,8 @@ var C = () => {
|
|
|
338
338
|
value: R.storeProductId,
|
|
339
339
|
onChange: Q("storeProductId"),
|
|
340
340
|
disabled: D,
|
|
341
|
+
className: `${d} scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary`,
|
|
341
342
|
"aria-describedby": O ? "store-product-error" : "store-product-help",
|
|
342
|
-
className: d,
|
|
343
343
|
children: [/* @__PURE__ */ y("option", {
|
|
344
344
|
value: "",
|
|
345
345
|
children: D ? "Loading published products…" : "No linked Store product"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SellerSourcingOpportunityDetailPage.js","names":[],"sources":["../../src/pages/SellerSourcingOpportunityDetailPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\nimport type { ChangeEvent, FC, FormEvent } from 'react';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n BadgeCheck,\n CalendarDays,\n Check,\n CircleDollarSign,\n Loader2,\n MapPin,\n PackageSearch,\n Save,\n TicketCheck,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SourcingOfferStatus } from '../generated/global-types';\nimport type { UpsertSourcingOfferInput } from '../generated/global-types';\nimport { useStore } from '../providers/StoreProvider';\nimport {\n type SellerSourcingOfferDetail,\n type SellerSourcingOpportunityDetail,\n useSourcingApi,\n} from '../hooks/useSourcing';\nimport { usePublisherSourcingAccess } from '../providers/SourcingAccessProvider';\nimport {\n ContextRequiredPanel,\n formatDateTime,\n formatLocation,\n formatMoney,\n formatSourcingUnit,\n helpClassName,\n inputClassName,\n joinStorePath,\n labelClassName,\n primaryButtonClassName,\n secondaryButtonClassName,\n SourcingErrorState,\n SourcingLoadingState,\n SourcingPageHeader,\n SourcingStatusBadge,\n toDateTimeLocal,\n toErrorMessage,\n} from '../components/sourcing/SourcingUi';\n\ninterface OfferFormState {\n unitPrice: string;\n deliveryDate: string;\n storeProductId: string;\n message: string;\n}\n\ntype OfferField = keyof OfferFormState;\ntype OfferErrors = Partial<Record<OfferField, string>>;\n\nconst EMPTY_FORM: OfferFormState = {\n unitPrice: '',\n deliveryDate: '',\n storeProductId: '',\n message: '',\n};\nconst MONEY_PATTERN = /^(?:0|[1-9]\\d{0,15})(?:\\.\\d{1,2})?$/;\n\nfunction validateOffer(\n form: OfferFormState,\n opportunity: SellerSourcingOpportunityDetail\n): OfferErrors {\n const errors: OfferErrors = {};\n const amount = form.unitPrice.trim();\n const delivery = new Date(form.deliveryDate);\n const start = new Date(opportunity.deliveryWindowStart);\n const end = new Date(opportunity.deliveryWindowEnd);\n\n if (!MONEY_PATTERN.test(amount) || Number(amount) <= 0) {\n errors.unitPrice = 'Enter a positive unit price with no more than two decimal places.';\n }\n if (\n !form.deliveryDate ||\n Number.isNaN(delivery.getTime()) ||\n delivery < start ||\n delivery > end\n ) {\n errors.deliveryDate = 'Choose a delivery date within the buyer’s requested window.';\n }\n if (form.message.trim().length > 1000) {\n errors.message = 'Offer message cannot exceed 1,000 characters.';\n }\n return errors;\n}\n\nexport const SellerSourcingOpportunityDetailPage: FC = () => {\n const navigate = useNavigate();\n const { requestId } = useParams<{ requestId: string }>();\n const { basePath } = useStore();\n const {\n profile,\n products,\n productsLoading,\n productsError,\n loading: accessLoading,\n error: accessError,\n isEligible,\n refresh: refreshAccess,\n refreshProducts,\n } = usePublisherSourcingAccess();\n const { getOpportunity, upsertOffer } = useSourcingApi();\n const [opportunity, setOpportunity] = useState<SellerSourcingOpportunityDetail | null>(null);\n const [offer, setOffer] = useState<SellerSourcingOfferDetail | null>(null);\n const [isOpen, setIsOpen] = useState(false);\n const [form, setForm] = useState<OfferFormState>(EMPTY_FORM);\n const [errors, setErrors] = useState<OfferErrors>({});\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [success, setSuccess] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n const opportunitiesPath = joinStorePath(basePath, 'publisher/sourcing/opportunities');\n\n const load = useCallback(\n async (initializeForm = false) => {\n if (!isEligible || !requestId) return;\n setLoading(true);\n setError(null);\n try {\n const result = await getOpportunity(requestId);\n setOpportunity(result.opportunity);\n setOffer(result.offer);\n setIsOpen(result.isOpen);\n if (!result.opportunity) {\n setError('This sourcing opportunity is no longer available.');\n } else if (initializeForm) {\n setForm({\n unitPrice: result.offer?.unitPrice ?? '',\n deliveryDate: result.offer\n ? toDateTimeLocal(result.offer.deliveryDate)\n : toDateTimeLocal(result.opportunity.deliveryWindowStart),\n storeProductId: result.offer?.storeProductId ?? '',\n message: result.offer?.message ?? '',\n });\n }\n } catch (loadError) {\n setError(toErrorMessage(loadError, 'The sourcing opportunity could not be loaded.'));\n } finally {\n setLoading(false);\n }\n },\n [getOpportunity, isEligible, requestId]\n );\n\n useEffect(() => {\n void load(true);\n }, [load]);\n\n const publishedProducts = useMemo(\n () =>\n [...products]\n .filter((product) => product.status === 'PUBLISHED')\n .sort((left, right) => left.name.localeCompare(right.name)),\n [products]\n );\n\n const updateField =\n (field: OfferField) =>\n (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {\n setForm((current) => ({ ...current, [field]: event.target.value }));\n setErrors((current) => ({ ...current, [field]: undefined }));\n setSuccess(null);\n };\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (!opportunity || !requestId || !isOpen || submitting) return;\n\n const validationErrors = validateOffer(form, opportunity);\n setErrors(validationErrors);\n setSubmitError(null);\n setSuccess(null);\n if (Object.keys(validationErrors).length > 0) return;\n\n const input: UpsertSourcingOfferInput = {\n requestId,\n quantity: opportunity.quantity,\n unit: opportunity.unit,\n unitPrice: form.unitPrice.trim(),\n currency: 'USD',\n deliveryDate: new Date(form.deliveryDate).toISOString(),\n storeProductId: form.storeProductId || undefined,\n message: form.message.trim() || undefined,\n };\n\n setSubmitting(true);\n try {\n const saved = await upsertOffer(input);\n setSuccess(offer ? 'Your offer was updated.' : 'Your offer was submitted.');\n setOffer(saved);\n await load(false);\n } catch (saveError) {\n setSubmitError(\n toErrorMessage(\n saveError,\n 'Your offer could not be saved. The request may have closed while you were editing.'\n )\n );\n } finally {\n setSubmitting(false);\n }\n };\n\n const fieldError = (field: OfferField) => {\n const message = errors[field];\n return message ? (\n <p id={`${field}-error`} className=\"mt-1 text-xs text-status-error-text\">\n {message}\n </p>\n ) : null;\n };\n\n const canEdit = isOpen && (!offer || offer.status === SourcingOfferStatus.Active);\n\n return (\n <div className=\"min-h-full bg-surface\">\n <SourcingPageHeader\n title={opportunity?.productName ?? 'Sourcing opportunity'}\n description=\"Review the sanitized buyer request and create or update your publisher’s offer.\"\n icon={<TicketCheck className=\"size-5\" aria-hidden=\"true\" />}\n onBack={() => navigate(opportunitiesPath)}\n actions={\n <button\n type=\"button\"\n onClick={() => navigate(joinStorePath(basePath, 'publisher/sourcing/offers'))}\n className={secondaryButtonClassName}\n >\n My offers\n </button>\n }\n />\n\n <main className=\"mx-auto max-w-6xl px-4 py-6 sm:px-6\">\n <PagePurpose className=\"mb-6\">\n Quantity and unit are fixed by the buyer request. Store validates publisher identity, USD\n settlement price, delivery window, and optional linked product on every save. You can\n update one active offer until the request closes.\n </PagePurpose>\n\n {accessLoading ? (\n <SourcingLoadingState label=\"Checking Store Publisher access…\" />\n ) : accessError ? (\n <SourcingErrorState\n title=\"Publisher access could not be checked\"\n message={accessError}\n onRetry={() => void refreshAccess()}\n />\n ) : !profile ? (\n <ContextRequiredPanel\n title=\"Store Publisher profile required\"\n description=\"Only users who own a verified Store Publisher profile can view this opportunity or submit an offer.\"\n />\n ) : !isEligible ? (\n <ContextRequiredPanel\n title=\"Verified publisher status required\"\n description={`The publisher profile “${profile.name}” is currently ${profile.verificationStatus.toLowerCase().replaceAll('_', ' ')}.`}\n />\n ) : loading && !opportunity ? (\n <SourcingLoadingState label=\"Loading opportunity and your offer…\" />\n ) : error && !opportunity ? (\n <SourcingErrorState\n title=\"Opportunity unavailable\"\n message={error}\n onRetry={requestId ? () => void load(true) : undefined}\n />\n ) : opportunity ? (\n <>\n {submitError ? (\n <div\n role=\"alert\"\n className=\"mb-5 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n {submitError}\n </div>\n ) : null}\n {success ? (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"mb-5 flex items-center gap-2 rounded-lg border border-status-success-border bg-status-success-bg-subtle px-4 py-3 text-sm text-status-success-text\"\n >\n <Check className=\"size-4\" aria-hidden=\"true\" />\n {success}\n </div>\n ) : null}\n\n {offer?.order ? (\n <EmphasisPanel className=\"mb-6\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm font-semibold text-status-success-text\">\n This offer was accepted\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Store order {offer.order.id} ·{' '}\n {formatMoney(offer.order.total, offer.order.currency)} · {offer.order.status}\n </p>\n </div>\n {offer.order.fulfillment ? (\n <span className=\"rounded-full border border-border-default bg-surface-sunken px-3 py-1.5 text-xs font-medium text-text-secondary\">\n Fulfillment {offer.order.fulfillment.status.replaceAll('_', ' ')}\n </span>\n ) : null}\n </div>\n </EmphasisPanel>\n ) : null}\n\n <div className=\"grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_minmax(340px,0.8fr)]\">\n <GlassCard className=\"p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <p className=\"text-sm font-medium text-accent-primary\">\n {opportunity.category}\n </p>\n <h2 className=\"mt-1 text-xl font-semibold text-text-primary\">\n {opportunity.productName}\n </h2>\n </div>\n <span\n className={`rounded-full border px-2.5 py-1 text-xs font-semibold ${\n isOpen\n ? 'border-status-success-border bg-status-success-bg-subtle text-status-success-text'\n : 'border-border-default bg-surface-sunken text-text-secondary'\n }`}\n >\n {isOpen ? 'OPEN' : 'CLOSED'}\n </span>\n </div>\n\n {opportunity.summary ? (\n <p className=\"mt-4 whitespace-pre-wrap text-sm leading-6 text-text-secondary\">\n {opportunity.summary}\n </p>\n ) : null}\n\n <dl className=\"mt-6 grid gap-5 border-t border-border-subtle pt-5 sm:grid-cols-2\">\n <div className=\"flex gap-3\">\n <PackageSearch\n className=\"mt-0.5 size-5 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Required quantity</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {opportunity.quantity.toLocaleString()}{' '}\n {formatSourcingUnit(opportunity.unit)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-3\">\n <CircleDollarSign\n className=\"mt-0.5 size-5 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Buyer target</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatMoney(opportunity.targetUnitPrice, opportunity.targetCurrency)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-3\">\n <CalendarDays\n className=\"mt-0.5 size-5 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Delivery window</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDateTime(opportunity.deliveryWindowStart)} –{' '}\n {formatDateTime(opportunity.deliveryWindowEnd)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-3\">\n <MapPin className=\"mt-0.5 size-5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <div>\n <dt className=\"text-xs text-text-muted\">Destination</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatLocation({\n country: opportunity.deliveryCountry,\n region: opportunity.deliveryRegion,\n city: opportunity.deliveryCity,\n })}\n </dd>\n </div>\n </div>\n </dl>\n </GlassCard>\n\n {canEdit ? (\n <form onSubmit={(event) => void handleSubmit(event)} noValidate>\n <GlassCard className=\"p-5 sm:p-6\">\n <div className=\"flex items-center justify-between gap-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {offer ? 'Update your offer' : 'Create an offer'}\n </h2>\n <p className=\"mt-1 text-xs text-text-muted\">Offering as {profile.name}</p>\n </div>\n <BadgeCheck\n className=\"size-5 text-accent-primary\"\n aria-label=\"Verified publisher\"\n />\n </div>\n\n <div className=\"mt-5 space-y-5\">\n <label className={labelClassName}>\n Unit price\n <div className=\"mt-1.5 grid grid-cols-[1fr_96px] gap-2\">\n <input\n type=\"text\"\n value={form.unitPrice}\n onChange={updateField('unitPrice')}\n inputMode=\"decimal\"\n required\n maxLength={19}\n aria-invalid={Boolean(errors.unitPrice)}\n aria-describedby={\n errors.unitPrice ? 'unitPrice-error' : 'unitPrice-help'\n }\n placeholder=\"125.50\"\n className={`${inputClassName} mt-0`}\n />\n <span\n aria-label=\"Offer currency\"\n className=\"mt-0 flex items-center justify-center rounded-md border border-border-default bg-surface-sunken px-3 text-sm font-semibold text-text-secondary\"\n >\n USD\n </span>\n </div>\n {fieldError('unitPrice')}\n {!errors.unitPrice ? (\n <p id=\"unitPrice-help\" className={helpClassName}>\n Price per {formatSourcingUnit(opportunity.unit).toLowerCase()}. Store\n sourcing currently settles in USD.\n </p>\n ) : null}\n </label>\n\n <label className={labelClassName}>\n Promised delivery date\n <input\n type=\"datetime-local\"\n value={form.deliveryDate}\n onChange={updateField('deliveryDate')}\n required\n min={toDateTimeLocal(opportunity.deliveryWindowStart)}\n max={toDateTimeLocal(opportunity.deliveryWindowEnd)}\n aria-invalid={Boolean(errors.deliveryDate)}\n aria-describedby={\n errors.deliveryDate ? 'deliveryDate-error' : 'deliveryDate-help'\n }\n className={inputClassName}\n />\n {fieldError('deliveryDate')}\n {!errors.deliveryDate ? (\n <p id=\"deliveryDate-help\" className={helpClassName}>\n Must fall inside the buyer’s delivery window.\n </p>\n ) : null}\n </label>\n\n <label className={labelClassName}>\n Link a published product{' '}\n <span className=\"font-normal text-text-muted\">(optional)</span>\n <select\n value={form.storeProductId}\n onChange={updateField('storeProductId')}\n disabled={productsLoading}\n aria-describedby={\n productsError ? 'store-product-error' : 'store-product-help'\n }\n className={inputClassName}\n >\n <option value=\"\">\n {productsLoading\n ? 'Loading published products…'\n : 'No linked Store product'}\n </option>\n {publishedProducts.map((product) => (\n <option key={product.id} value={product.id}>\n {product.name}\n </option>\n ))}\n </select>\n {productsError ? (\n <div\n id=\"store-product-error\"\n role=\"alert\"\n className=\"mt-2 flex flex-wrap items-center gap-2 text-xs text-status-error-text\"\n >\n <span>{productsError}</span>\n <button\n type=\"button\"\n onClick={() => void refreshProducts()}\n className=\"font-semibold underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg\"\n >\n Try again\n </button>\n </div>\n ) : (\n <p id=\"store-product-help\" className={helpClassName}>\n Only products owned by this publisher and currently published are\n accepted. The link is reference metadata and does not reserve catalog\n inventory or create digital entitlements.\n </p>\n )}\n </label>\n\n <label className={labelClassName}>\n Message <span className=\"font-normal text-text-muted\">(optional)</span>\n <textarea\n value={form.message}\n onChange={updateField('message')}\n maxLength={1000}\n rows={4}\n aria-invalid={Boolean(errors.message)}\n aria-describedby={errors.message ? 'message-error' : 'message-help'}\n placeholder=\"Add packaging, quality, or delivery notes relevant to this offer.\"\n className={`${inputClassName} resize-y`}\n />\n {fieldError('message')}\n {!errors.message ? (\n <p id=\"message-help\" className={helpClassName}>\n {form.message.length}/1,000 characters\n </p>\n ) : null}\n </label>\n </div>\n\n <button\n type=\"submit\"\n disabled={submitting}\n className={`${primaryButtonClassName} mt-6 w-full`}\n >\n {submitting ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : offer ? (\n <Save className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <TicketCheck className=\"size-4\" aria-hidden=\"true\" />\n )}\n {submitting ? 'Saving offer…' : offer ? 'Update offer' : 'Submit offer'}\n </button>\n </GlassCard>\n </form>\n ) : offer ? (\n <GlassCard className=\"p-5 sm:p-6\">\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Your offer</h2>\n <p className=\"mt-1 text-xs text-text-muted\">\n Updated {formatDateTime(offer.updatedAt)}\n </p>\n </div>\n <SourcingStatusBadge status={offer.status} />\n </div>\n <dl className=\"mt-5 space-y-4 text-sm\">\n <div>\n <dt className=\"text-xs text-text-muted\">Unit price</dt>\n <dd className=\"mt-0.5 text-lg font-semibold text-text-primary\">\n {formatMoney(offer.unitPrice, offer.currency)}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-muted\">Promised delivery</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDateTime(offer.deliveryDate)}\n </dd>\n </div>\n {offer.message ? (\n <div>\n <dt className=\"text-xs text-text-muted\">Message</dt>\n <dd className=\"mt-1 whitespace-pre-wrap text-text-secondary\">\n {offer.message}\n </dd>\n </div>\n ) : null}\n </dl>\n </GlassCard>\n ) : (\n <ContextRequiredPanel\n title=\"This request is closed\"\n description=\"The buyer matched or canceled this request before your publisher submitted an offer.\"\n />\n )}\n </div>\n </>\n ) : null}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SellerSourcingOpportunityDetailPage.js","names":[],"sources":["../../src/pages/SellerSourcingOpportunityDetailPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\nimport type { ChangeEvent, FC, FormEvent } from 'react';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n BadgeCheck,\n CalendarDays,\n Check,\n CircleDollarSign,\n Loader2,\n MapPin,\n PackageSearch,\n Save,\n TicketCheck,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SourcingOfferStatus } from '../generated/global-types';\nimport type { UpsertSourcingOfferInput } from '../generated/global-types';\nimport { useStore } from '../providers/StoreProvider';\nimport {\n type SellerSourcingOfferDetail,\n type SellerSourcingOpportunityDetail,\n useSourcingApi,\n} from '../hooks/useSourcing';\nimport { usePublisherSourcingAccess } from '../providers/SourcingAccessProvider';\nimport {\n ContextRequiredPanel,\n formatDateTime,\n formatLocation,\n formatMoney,\n formatSourcingUnit,\n helpClassName,\n inputClassName,\n joinStorePath,\n labelClassName,\n primaryButtonClassName,\n secondaryButtonClassName,\n SourcingErrorState,\n SourcingLoadingState,\n SourcingPageHeader,\n SourcingStatusBadge,\n toDateTimeLocal,\n toErrorMessage,\n} from '../components/sourcing/SourcingUi';\n\ninterface OfferFormState {\n unitPrice: string;\n deliveryDate: string;\n storeProductId: string;\n message: string;\n}\n\ntype OfferField = keyof OfferFormState;\ntype OfferErrors = Partial<Record<OfferField, string>>;\n\nconst EMPTY_FORM: OfferFormState = {\n unitPrice: '',\n deliveryDate: '',\n storeProductId: '',\n message: '',\n};\nconst MONEY_PATTERN = /^(?:0|[1-9]\\d{0,15})(?:\\.\\d{1,2})?$/;\n\nfunction validateOffer(\n form: OfferFormState,\n opportunity: SellerSourcingOpportunityDetail\n): OfferErrors {\n const errors: OfferErrors = {};\n const amount = form.unitPrice.trim();\n const delivery = new Date(form.deliveryDate);\n const start = new Date(opportunity.deliveryWindowStart);\n const end = new Date(opportunity.deliveryWindowEnd);\n\n if (!MONEY_PATTERN.test(amount) || Number(amount) <= 0) {\n errors.unitPrice = 'Enter a positive unit price with no more than two decimal places.';\n }\n if (\n !form.deliveryDate ||\n Number.isNaN(delivery.getTime()) ||\n delivery < start ||\n delivery > end\n ) {\n errors.deliveryDate = 'Choose a delivery date within the buyer’s requested window.';\n }\n if (form.message.trim().length > 1000) {\n errors.message = 'Offer message cannot exceed 1,000 characters.';\n }\n return errors;\n}\n\nexport const SellerSourcingOpportunityDetailPage: FC = () => {\n const navigate = useNavigate();\n const { requestId } = useParams<{ requestId: string }>();\n const { basePath } = useStore();\n const {\n profile,\n products,\n productsLoading,\n productsError,\n loading: accessLoading,\n error: accessError,\n isEligible,\n refresh: refreshAccess,\n refreshProducts,\n } = usePublisherSourcingAccess();\n const { getOpportunity, upsertOffer } = useSourcingApi();\n const [opportunity, setOpportunity] = useState<SellerSourcingOpportunityDetail | null>(null);\n const [offer, setOffer] = useState<SellerSourcingOfferDetail | null>(null);\n const [isOpen, setIsOpen] = useState(false);\n const [form, setForm] = useState<OfferFormState>(EMPTY_FORM);\n const [errors, setErrors] = useState<OfferErrors>({});\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [success, setSuccess] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n const opportunitiesPath = joinStorePath(basePath, 'publisher/sourcing/opportunities');\n\n const load = useCallback(\n async (initializeForm = false) => {\n if (!isEligible || !requestId) return;\n setLoading(true);\n setError(null);\n try {\n const result = await getOpportunity(requestId);\n setOpportunity(result.opportunity);\n setOffer(result.offer);\n setIsOpen(result.isOpen);\n if (!result.opportunity) {\n setError('This sourcing opportunity is no longer available.');\n } else if (initializeForm) {\n setForm({\n unitPrice: result.offer?.unitPrice ?? '',\n deliveryDate: result.offer\n ? toDateTimeLocal(result.offer.deliveryDate)\n : toDateTimeLocal(result.opportunity.deliveryWindowStart),\n storeProductId: result.offer?.storeProductId ?? '',\n message: result.offer?.message ?? '',\n });\n }\n } catch (loadError) {\n setError(toErrorMessage(loadError, 'The sourcing opportunity could not be loaded.'));\n } finally {\n setLoading(false);\n }\n },\n [getOpportunity, isEligible, requestId]\n );\n\n useEffect(() => {\n void load(true);\n }, [load]);\n\n const publishedProducts = useMemo(\n () =>\n [...products]\n .filter((product) => product.status === 'PUBLISHED')\n .sort((left, right) => left.name.localeCompare(right.name)),\n [products]\n );\n\n const updateField =\n (field: OfferField) =>\n (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {\n setForm((current) => ({ ...current, [field]: event.target.value }));\n setErrors((current) => ({ ...current, [field]: undefined }));\n setSuccess(null);\n };\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (!opportunity || !requestId || !isOpen || submitting) return;\n\n const validationErrors = validateOffer(form, opportunity);\n setErrors(validationErrors);\n setSubmitError(null);\n setSuccess(null);\n if (Object.keys(validationErrors).length > 0) return;\n\n const input: UpsertSourcingOfferInput = {\n requestId,\n quantity: opportunity.quantity,\n unit: opportunity.unit,\n unitPrice: form.unitPrice.trim(),\n currency: 'USD',\n deliveryDate: new Date(form.deliveryDate).toISOString(),\n storeProductId: form.storeProductId || undefined,\n message: form.message.trim() || undefined,\n };\n\n setSubmitting(true);\n try {\n const saved = await upsertOffer(input);\n setSuccess(offer ? 'Your offer was updated.' : 'Your offer was submitted.');\n setOffer(saved);\n await load(false);\n } catch (saveError) {\n setSubmitError(\n toErrorMessage(\n saveError,\n 'Your offer could not be saved. The request may have closed while you were editing.'\n )\n );\n } finally {\n setSubmitting(false);\n }\n };\n\n const fieldError = (field: OfferField) => {\n const message = errors[field];\n return message ? (\n <p id={`${field}-error`} className=\"mt-1 text-xs text-status-error-text\">\n {message}\n </p>\n ) : null;\n };\n\n const canEdit = isOpen && (!offer || offer.status === SourcingOfferStatus.Active);\n\n return (\n <div className=\"min-h-full bg-surface\">\n <SourcingPageHeader\n title={opportunity?.productName ?? 'Sourcing opportunity'}\n description=\"Review the sanitized buyer request and create or update your publisher’s offer.\"\n icon={<TicketCheck className=\"size-5\" aria-hidden=\"true\" />}\n onBack={() => navigate(opportunitiesPath)}\n actions={\n <button\n type=\"button\"\n onClick={() => navigate(joinStorePath(basePath, 'publisher/sourcing/offers'))}\n className={secondaryButtonClassName}\n >\n My offers\n </button>\n }\n />\n\n <main className=\"mx-auto max-w-6xl px-4 py-6 sm:px-6\">\n <PagePurpose className=\"mb-6\">\n Quantity and unit are fixed by the buyer request. Store validates publisher identity, USD\n settlement price, delivery window, and optional linked product on every save. You can\n update one active offer until the request closes.\n </PagePurpose>\n\n {accessLoading ? (\n <SourcingLoadingState label=\"Checking Store Publisher access…\" />\n ) : accessError ? (\n <SourcingErrorState\n title=\"Publisher access could not be checked\"\n message={accessError}\n onRetry={() => void refreshAccess()}\n />\n ) : !profile ? (\n <ContextRequiredPanel\n title=\"Store Publisher profile required\"\n description=\"Only users who own a verified Store Publisher profile can view this opportunity or submit an offer.\"\n />\n ) : !isEligible ? (\n <ContextRequiredPanel\n title=\"Verified publisher status required\"\n description={`The publisher profile “${profile.name}” is currently ${profile.verificationStatus.toLowerCase().replaceAll('_', ' ')}.`}\n />\n ) : loading && !opportunity ? (\n <SourcingLoadingState label=\"Loading opportunity and your offer…\" />\n ) : error && !opportunity ? (\n <SourcingErrorState\n title=\"Opportunity unavailable\"\n message={error}\n onRetry={requestId ? () => void load(true) : undefined}\n />\n ) : opportunity ? (\n <>\n {submitError ? (\n <div\n role=\"alert\"\n className=\"mb-5 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n {submitError}\n </div>\n ) : null}\n {success ? (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"mb-5 flex items-center gap-2 rounded-lg border border-status-success-border bg-status-success-bg-subtle px-4 py-3 text-sm text-status-success-text\"\n >\n <Check className=\"size-4\" aria-hidden=\"true\" />\n {success}\n </div>\n ) : null}\n\n {offer?.order ? (\n <EmphasisPanel className=\"mb-6\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm font-semibold text-status-success-text\">\n This offer was accepted\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Store order {offer.order.id} ·{' '}\n {formatMoney(offer.order.total, offer.order.currency)} · {offer.order.status}\n </p>\n </div>\n {offer.order.fulfillment ? (\n <span className=\"rounded-full border border-border-default bg-surface-sunken px-3 py-1.5 text-xs font-medium text-text-secondary\">\n Fulfillment {offer.order.fulfillment.status.replaceAll('_', ' ')}\n </span>\n ) : null}\n </div>\n </EmphasisPanel>\n ) : null}\n\n <div className=\"grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_minmax(340px,0.8fr)]\">\n <GlassCard className=\"p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <p className=\"text-sm font-medium text-accent-primary\">\n {opportunity.category}\n </p>\n <h2 className=\"mt-1 text-xl font-semibold text-text-primary\">\n {opportunity.productName}\n </h2>\n </div>\n <span\n className={`rounded-full border px-2.5 py-1 text-xs font-semibold ${\n isOpen\n ? 'border-status-success-border bg-status-success-bg-subtle text-status-success-text'\n : 'border-border-default bg-surface-sunken text-text-secondary'\n }`}\n >\n {isOpen ? 'OPEN' : 'CLOSED'}\n </span>\n </div>\n\n {opportunity.summary ? (\n <p className=\"mt-4 whitespace-pre-wrap text-sm leading-6 text-text-secondary\">\n {opportunity.summary}\n </p>\n ) : null}\n\n <dl className=\"mt-6 grid gap-5 border-t border-border-subtle pt-5 sm:grid-cols-2\">\n <div className=\"flex gap-3\">\n <PackageSearch\n className=\"mt-0.5 size-5 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Required quantity</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {opportunity.quantity.toLocaleString()}{' '}\n {formatSourcingUnit(opportunity.unit)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-3\">\n <CircleDollarSign\n className=\"mt-0.5 size-5 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Buyer target</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatMoney(opportunity.targetUnitPrice, opportunity.targetCurrency)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-3\">\n <CalendarDays\n className=\"mt-0.5 size-5 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Delivery window</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDateTime(opportunity.deliveryWindowStart)} –{' '}\n {formatDateTime(opportunity.deliveryWindowEnd)}\n </dd>\n </div>\n </div>\n <div className=\"flex gap-3\">\n <MapPin className=\"mt-0.5 size-5 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <div>\n <dt className=\"text-xs text-text-muted\">Destination</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatLocation({\n country: opportunity.deliveryCountry,\n region: opportunity.deliveryRegion,\n city: opportunity.deliveryCity,\n })}\n </dd>\n </div>\n </div>\n </dl>\n </GlassCard>\n\n {canEdit ? (\n <form onSubmit={(event) => void handleSubmit(event)} noValidate>\n <GlassCard className=\"p-5 sm:p-6\">\n <div className=\"flex items-center justify-between gap-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {offer ? 'Update your offer' : 'Create an offer'}\n </h2>\n <p className=\"mt-1 text-xs text-text-muted\">Offering as {profile.name}</p>\n </div>\n <BadgeCheck\n className=\"size-5 text-accent-primary\"\n aria-label=\"Verified publisher\"\n />\n </div>\n\n <div className=\"mt-5 space-y-5\">\n <label className={labelClassName}>\n Unit price\n <div className=\"mt-1.5 grid grid-cols-[1fr_96px] gap-2\">\n <input\n type=\"text\"\n value={form.unitPrice}\n onChange={updateField('unitPrice')}\n inputMode=\"decimal\"\n required\n maxLength={19}\n aria-invalid={Boolean(errors.unitPrice)}\n aria-describedby={\n errors.unitPrice ? 'unitPrice-error' : 'unitPrice-help'\n }\n placeholder=\"125.50\"\n className={`${inputClassName} mt-0`}\n />\n <span\n aria-label=\"Offer currency\"\n className=\"mt-0 flex items-center justify-center rounded-md border border-border-default bg-surface-sunken px-3 text-sm font-semibold text-text-secondary\"\n >\n USD\n </span>\n </div>\n {fieldError('unitPrice')}\n {!errors.unitPrice ? (\n <p id=\"unitPrice-help\" className={helpClassName}>\n Price per {formatSourcingUnit(opportunity.unit).toLowerCase()}. Store\n sourcing currently settles in USD.\n </p>\n ) : null}\n </label>\n\n <label className={labelClassName}>\n Promised delivery date\n <input\n type=\"datetime-local\"\n value={form.deliveryDate}\n onChange={updateField('deliveryDate')}\n required\n min={toDateTimeLocal(opportunity.deliveryWindowStart)}\n max={toDateTimeLocal(opportunity.deliveryWindowEnd)}\n aria-invalid={Boolean(errors.deliveryDate)}\n aria-describedby={\n errors.deliveryDate ? 'deliveryDate-error' : 'deliveryDate-help'\n }\n className={inputClassName}\n />\n {fieldError('deliveryDate')}\n {!errors.deliveryDate ? (\n <p id=\"deliveryDate-help\" className={helpClassName}>\n Must fall inside the buyer’s delivery window.\n </p>\n ) : null}\n </label>\n\n <label className={labelClassName}>\n Link a published product{' '}\n <span className=\"font-normal text-text-muted\">(optional)</span>\n <select\n value={form.storeProductId}\n onChange={updateField('storeProductId')}\n disabled={productsLoading}\n className={`${inputClassName} scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary`}\n aria-describedby={\n productsError ? 'store-product-error' : 'store-product-help'\n }\n >\n <option value=\"\">\n {productsLoading\n ? 'Loading published products…'\n : 'No linked Store product'}\n </option>\n {publishedProducts.map((product) => (\n <option key={product.id} value={product.id}>\n {product.name}\n </option>\n ))}\n </select>\n {productsError ? (\n <div\n id=\"store-product-error\"\n role=\"alert\"\n className=\"mt-2 flex flex-wrap items-center gap-2 text-xs text-status-error-text\"\n >\n <span>{productsError}</span>\n <button\n type=\"button\"\n onClick={() => void refreshProducts()}\n className=\"font-semibold underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg\"\n >\n Try again\n </button>\n </div>\n ) : (\n <p id=\"store-product-help\" className={helpClassName}>\n Only products owned by this publisher and currently published are\n accepted. The link is reference metadata and does not reserve catalog\n inventory or create digital entitlements.\n </p>\n )}\n </label>\n\n <label className={labelClassName}>\n Message <span className=\"font-normal text-text-muted\">(optional)</span>\n <textarea\n value={form.message}\n onChange={updateField('message')}\n maxLength={1000}\n rows={4}\n aria-invalid={Boolean(errors.message)}\n aria-describedby={errors.message ? 'message-error' : 'message-help'}\n placeholder=\"Add packaging, quality, or delivery notes relevant to this offer.\"\n className={`${inputClassName} resize-y`}\n />\n {fieldError('message')}\n {!errors.message ? (\n <p id=\"message-help\" className={helpClassName}>\n {form.message.length}/1,000 characters\n </p>\n ) : null}\n </label>\n </div>\n\n <button\n type=\"submit\"\n disabled={submitting}\n className={`${primaryButtonClassName} mt-6 w-full`}\n >\n {submitting ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : offer ? (\n <Save className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <TicketCheck className=\"size-4\" aria-hidden=\"true\" />\n )}\n {submitting ? 'Saving offer…' : offer ? 'Update offer' : 'Submit offer'}\n </button>\n </GlassCard>\n </form>\n ) : offer ? (\n <GlassCard className=\"p-5 sm:p-6\">\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Your offer</h2>\n <p className=\"mt-1 text-xs text-text-muted\">\n Updated {formatDateTime(offer.updatedAt)}\n </p>\n </div>\n <SourcingStatusBadge status={offer.status} />\n </div>\n <dl className=\"mt-5 space-y-4 text-sm\">\n <div>\n <dt className=\"text-xs text-text-muted\">Unit price</dt>\n <dd className=\"mt-0.5 text-lg font-semibold text-text-primary\">\n {formatMoney(offer.unitPrice, offer.currency)}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs text-text-muted\">Promised delivery</dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">\n {formatDateTime(offer.deliveryDate)}\n </dd>\n </div>\n {offer.message ? (\n <div>\n <dt className=\"text-xs text-text-muted\">Message</dt>\n <dd className=\"mt-1 whitespace-pre-wrap text-text-secondary\">\n {offer.message}\n </dd>\n </div>\n ) : null}\n </dl>\n </GlassCard>\n ) : (\n <ContextRequiredPanel\n title=\"This request is closed\"\n description=\"The buyer matched or canceled this request before your publisher submitted an offer.\"\n />\n )}\n </div>\n </>\n ) : null}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -73,7 +73,7 @@ var P = 30, F = () => {
|
|
|
73
73
|
}), /* @__PURE__ */ j("select", {
|
|
74
74
|
value: V,
|
|
75
75
|
onChange: (e) => H(e.target.value),
|
|
76
|
-
className: "mt-1.5 w-full rounded-lg border border-border-default bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg focus:ring-2 focus:ring-action-primary-bg/20",
|
|
76
|
+
className: "mt-1.5 w-full rounded-lg border border-border-default bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg focus:ring-2 focus:ring-action-primary-bg/20 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
77
77
|
children: [
|
|
78
78
|
/* @__PURE__ */ A("option", {
|
|
79
79
|
value: "",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SourcingRequestsPage.js","names":[],"sources":["../../src/pages/SourcingRequestsPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\nimport type { FC } from 'react';\nimport { useNavigate } from 'react-router';\nimport {\n ArrowRight,\n CalendarDays,\n FilePlus2,\n FileText,\n MapPin,\n PackageSearch,\n RefreshCw,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SourcingRequestStatus } from '../generated/global-types';\nimport { useStore } from '../providers/StoreProvider';\nimport { type BuyerSourcingRequestListItem, useSourcingApi } from '../hooks/useSourcing';\nimport {\n ContextRequiredPanel,\n formatDate,\n formatLocation,\n formatMoney,\n formatSourcingUnit,\n joinStorePath,\n primaryButtonClassName,\n secondaryButtonClassName,\n SourcingEmptyState,\n SourcingErrorState,\n SourcingLoadingState,\n SourcingPageHeader,\n SourcingStatusBadge,\n toErrorMessage,\n} from '../components/sourcing/SourcingUi';\n\nconst PAGE_SIZE = 30;\n\ntype RequestFilter = SourcingRequestStatus | '';\n\nexport const SourcingRequestsPage: FC = () => {\n const navigate = useNavigate();\n const { basePath } = useStore();\n const { hasOrganizationContext, listBuyerRequests } = useSourcingApi();\n const [requests, setRequests] = useState<BuyerSourcingRequestListItem[]>([]);\n const [status, setStatus] = useState<RequestFilter>('');\n const [offset, setOffset] = useState(0);\n const [hasMore, setHasMore] = useState(false);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const load = useCallback(\n async (nextOffset = 0, append = false) => {\n if (!hasOrganizationContext) {\n setRequests([]);\n setHasMore(false);\n setLoading(false);\n return;\n }\n\n setLoading(true);\n setError(null);\n try {\n const result = await listBuyerRequests({\n status: status || undefined,\n limit: PAGE_SIZE,\n offset: nextOffset,\n });\n setRequests((current) => (append ? [...current, ...result] : result));\n setOffset(nextOffset);\n setHasMore(result.length === PAGE_SIZE);\n } catch (loadError) {\n setError(toErrorMessage(loadError, 'Failed to load sourcing requests.'));\n } finally {\n setLoading(false);\n }\n },\n [hasOrganizationContext, listBuyerRequests, status]\n );\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const counts = useMemo(\n () =>\n requests.reduce(\n (result, request) => {\n result.total += 1;\n result.offers += request.offers.filter((offer) => offer.status === 'ACTIVE').length;\n return result;\n },\n { total: 0, offers: 0 }\n ),\n [requests]\n );\n\n const createPath = joinStorePath(basePath, 'sourcing/requests/new');\n\n return (\n <div className=\"min-h-full bg-surface\">\n <SourcingPageHeader\n title=\"Sourcing requests\"\n description=\"Request products from verified Store publishers and compare private offers for your organization.\"\n icon={<PackageSearch className=\"size-5\" aria-hidden=\"true\" />}\n actions={\n <button\n type=\"button\"\n onClick={() => navigate(createPath)}\n disabled={!hasOrganizationContext}\n className={primaryButtonClassName}\n >\n <FilePlus2 className=\"size-4\" aria-hidden=\"true\" />\n New request\n </button>\n }\n />\n\n <main className=\"mx-auto max-w-6xl px-4 py-6 sm:px-6\">\n <PagePurpose className=\"mb-6\">\n Create one request for the exact quantity, delivery window, and destination you need.\n Offers remain private to your organization. Accepting one offer creates a Store order and\n closes the request; all other offers are rejected automatically.\n </PagePurpose>\n\n {!hasOrganizationContext ? (\n <ContextRequiredPanel\n title=\"Choose an organization\"\n description=\"Sourcing requests use your active global organization and its billing account. Choose an organization in the app switcher; a farm workspace is not required.\"\n />\n ) : (\n <>\n <div className=\"mb-5 flex flex-col gap-3 rounded-xl border border-border-subtle bg-surface-elevated p-4 sm:flex-row sm:items-end sm:justify-between\">\n <label className=\"block sm:max-w-xs sm:flex-1\">\n <span className=\"text-sm font-medium text-text-primary\">Request status</span>\n <select\n value={status}\n onChange={(event) => setStatus(event.target.value as RequestFilter)}\n className=\"mt-1.5 w-full rounded-lg border border-border-default bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg focus:ring-2 focus:ring-action-primary-bg/20\"\n >\n <option value=\"\">All statuses</option>\n <option value={SourcingRequestStatus.Open}>Open</option>\n <option value={SourcingRequestStatus.Matched}>Matched</option>\n <option value={SourcingRequestStatus.Canceled}>Canceled</option>\n </select>\n </label>\n <div className=\"flex flex-wrap items-center gap-3 text-sm text-text-secondary\">\n <span>{counts.total} shown</span>\n <span aria-hidden=\"true\">·</span>\n <span>{counts.offers} active offers</span>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={loading}\n className={secondaryButtonClassName}\n >\n <RefreshCw\n className={`size-4 ${loading ? 'animate-spin' : ''}`}\n aria-hidden=\"true\"\n />\n Refresh\n </button>\n </div>\n </div>\n\n {loading && requests.length === 0 ? (\n <SourcingLoadingState label=\"Loading sourcing requests…\" />\n ) : error && requests.length === 0 ? (\n <SourcingErrorState\n title=\"Sourcing requests could not be loaded\"\n message={error}\n onRetry={() => void load()}\n />\n ) : requests.length === 0 ? (\n <SourcingEmptyState\n title={status ? 'No requests match this status' : 'No sourcing requests yet'}\n description={\n status\n ? 'Choose another status or create a new request.'\n : 'Describe what your organization needs and verified publishers can respond with offers.'\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigate(createPath)}\n className={primaryButtonClassName}\n >\n <FilePlus2 className=\"size-4\" aria-hidden=\"true\" />\n Create request\n </button>\n }\n />\n ) : (\n <>\n {error ? (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n {error}\n </div>\n ) : null}\n <ul className=\"grid gap-4 md:grid-cols-2\">\n {requests.map((request) => {\n const activeOffers = request.offers.filter(\n (offer) => offer.status === 'ACTIVE'\n ).length;\n return (\n <li key={request.id}>\n <GlassCard className=\"flex h-full flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-accent-primary\">\n <FileText className=\"size-4\" aria-hidden=\"true\" />\n <span className=\"truncate\">{request.category}</span>\n </div>\n <h2 className=\"mt-2 line-clamp-2 text-lg font-semibold text-text-primary\">\n {request.productName}\n </h2>\n </div>\n <SourcingStatusBadge status={request.status} />\n </div>\n\n <dl className=\"mt-5 grid gap-3 text-sm sm:grid-cols-2\">\n <div className=\"flex items-start gap-2\">\n <PackageSearch\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Quantity</dt>\n <dd className=\"font-medium text-text-primary\">\n {request.quantity.toLocaleString()}{' '}\n {formatSourcingUnit(request.unit)}\n </dd>\n </div>\n </div>\n <div className=\"flex items-start gap-2\">\n <MapPin\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Deliver to</dt>\n <dd className=\"font-medium text-text-primary\">\n {formatLocation({\n country: request.deliveryCountry,\n region: request.deliveryRegion,\n city: request.deliveryCity,\n })}\n </dd>\n </div>\n </div>\n <div className=\"flex items-start gap-2 sm:col-span-2\">\n <CalendarDays\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Delivery window</dt>\n <dd className=\"font-medium text-text-primary\">\n {formatDate(request.deliveryWindowStart)} –{' '}\n {formatDate(request.deliveryWindowEnd)}\n </dd>\n </div>\n </div>\n </dl>\n\n <div className=\"mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-border-subtle pt-4\">\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {activeOffers} active {activeOffers === 1 ? 'offer' : 'offers'}\n </p>\n <p className=\"text-xs text-text-muted\">\n Target{' '}\n {formatMoney(request.targetUnitPrice, request.targetCurrency)}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() =>\n navigate(joinStorePath(basePath, `sourcing/requests/${request.id}`))\n }\n className={secondaryButtonClassName}\n >\n View details\n <ArrowRight className=\"size-4\" aria-hidden=\"true\" />\n </button>\n </div>\n </GlassCard>\n </li>\n );\n })}\n </ul>\n\n {hasMore ? (\n <div className=\"mt-6 flex justify-center\">\n <button\n type=\"button\"\n onClick={() => void load(offset + PAGE_SIZE, true)}\n disabled={loading}\n className={secondaryButtonClassName}\n >\n {loading ? 'Loading…' : 'Load more'}\n </button>\n </div>\n ) : null}\n </>\n )}\n </>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SourcingRequestsPage.js","names":[],"sources":["../../src/pages/SourcingRequestsPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\nimport type { FC } from 'react';\nimport { useNavigate } from 'react-router';\nimport {\n ArrowRight,\n CalendarDays,\n FilePlus2,\n FileText,\n MapPin,\n PackageSearch,\n RefreshCw,\n} from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { SourcingRequestStatus } from '../generated/global-types';\nimport { useStore } from '../providers/StoreProvider';\nimport { type BuyerSourcingRequestListItem, useSourcingApi } from '../hooks/useSourcing';\nimport {\n ContextRequiredPanel,\n formatDate,\n formatLocation,\n formatMoney,\n formatSourcingUnit,\n joinStorePath,\n primaryButtonClassName,\n secondaryButtonClassName,\n SourcingEmptyState,\n SourcingErrorState,\n SourcingLoadingState,\n SourcingPageHeader,\n SourcingStatusBadge,\n toErrorMessage,\n} from '../components/sourcing/SourcingUi';\n\nconst PAGE_SIZE = 30;\n\ntype RequestFilter = SourcingRequestStatus | '';\n\nexport const SourcingRequestsPage: FC = () => {\n const navigate = useNavigate();\n const { basePath } = useStore();\n const { hasOrganizationContext, listBuyerRequests } = useSourcingApi();\n const [requests, setRequests] = useState<BuyerSourcingRequestListItem[]>([]);\n const [status, setStatus] = useState<RequestFilter>('');\n const [offset, setOffset] = useState(0);\n const [hasMore, setHasMore] = useState(false);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const load = useCallback(\n async (nextOffset = 0, append = false) => {\n if (!hasOrganizationContext) {\n setRequests([]);\n setHasMore(false);\n setLoading(false);\n return;\n }\n\n setLoading(true);\n setError(null);\n try {\n const result = await listBuyerRequests({\n status: status || undefined,\n limit: PAGE_SIZE,\n offset: nextOffset,\n });\n setRequests((current) => (append ? [...current, ...result] : result));\n setOffset(nextOffset);\n setHasMore(result.length === PAGE_SIZE);\n } catch (loadError) {\n setError(toErrorMessage(loadError, 'Failed to load sourcing requests.'));\n } finally {\n setLoading(false);\n }\n },\n [hasOrganizationContext, listBuyerRequests, status]\n );\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const counts = useMemo(\n () =>\n requests.reduce(\n (result, request) => {\n result.total += 1;\n result.offers += request.offers.filter((offer) => offer.status === 'ACTIVE').length;\n return result;\n },\n { total: 0, offers: 0 }\n ),\n [requests]\n );\n\n const createPath = joinStorePath(basePath, 'sourcing/requests/new');\n\n return (\n <div className=\"min-h-full bg-surface\">\n <SourcingPageHeader\n title=\"Sourcing requests\"\n description=\"Request products from verified Store publishers and compare private offers for your organization.\"\n icon={<PackageSearch className=\"size-5\" aria-hidden=\"true\" />}\n actions={\n <button\n type=\"button\"\n onClick={() => navigate(createPath)}\n disabled={!hasOrganizationContext}\n className={primaryButtonClassName}\n >\n <FilePlus2 className=\"size-4\" aria-hidden=\"true\" />\n New request\n </button>\n }\n />\n\n <main className=\"mx-auto max-w-6xl px-4 py-6 sm:px-6\">\n <PagePurpose className=\"mb-6\">\n Create one request for the exact quantity, delivery window, and destination you need.\n Offers remain private to your organization. Accepting one offer creates a Store order and\n closes the request; all other offers are rejected automatically.\n </PagePurpose>\n\n {!hasOrganizationContext ? (\n <ContextRequiredPanel\n title=\"Choose an organization\"\n description=\"Sourcing requests use your active global organization and its billing account. Choose an organization in the app switcher; a farm workspace is not required.\"\n />\n ) : (\n <>\n <div className=\"mb-5 flex flex-col gap-3 rounded-xl border border-border-subtle bg-surface-elevated p-4 sm:flex-row sm:items-end sm:justify-between\">\n <label className=\"block sm:max-w-xs sm:flex-1\">\n <span className=\"text-sm font-medium text-text-primary\">Request status</span>\n <select\n value={status}\n onChange={(event) => setStatus(event.target.value as RequestFilter)}\n className=\"mt-1.5 w-full rounded-lg border border-border-default bg-surface px-3 py-2 text-sm text-text-primary outline-none focus:border-action-primary-bg focus:ring-2 focus:ring-action-primary-bg/20 scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"\">All statuses</option>\n <option value={SourcingRequestStatus.Open}>Open</option>\n <option value={SourcingRequestStatus.Matched}>Matched</option>\n <option value={SourcingRequestStatus.Canceled}>Canceled</option>\n </select>\n </label>\n <div className=\"flex flex-wrap items-center gap-3 text-sm text-text-secondary\">\n <span>{counts.total} shown</span>\n <span aria-hidden=\"true\">·</span>\n <span>{counts.offers} active offers</span>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={loading}\n className={secondaryButtonClassName}\n >\n <RefreshCw\n className={`size-4 ${loading ? 'animate-spin' : ''}`}\n aria-hidden=\"true\"\n />\n Refresh\n </button>\n </div>\n </div>\n\n {loading && requests.length === 0 ? (\n <SourcingLoadingState label=\"Loading sourcing requests…\" />\n ) : error && requests.length === 0 ? (\n <SourcingErrorState\n title=\"Sourcing requests could not be loaded\"\n message={error}\n onRetry={() => void load()}\n />\n ) : requests.length === 0 ? (\n <SourcingEmptyState\n title={status ? 'No requests match this status' : 'No sourcing requests yet'}\n description={\n status\n ? 'Choose another status or create a new request.'\n : 'Describe what your organization needs and verified publishers can respond with offers.'\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigate(createPath)}\n className={primaryButtonClassName}\n >\n <FilePlus2 className=\"size-4\" aria-hidden=\"true\" />\n Create request\n </button>\n }\n />\n ) : (\n <>\n {error ? (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle px-4 py-3 text-sm text-status-error-text\"\n >\n {error}\n </div>\n ) : null}\n <ul className=\"grid gap-4 md:grid-cols-2\">\n {requests.map((request) => {\n const activeOffers = request.offers.filter(\n (offer) => offer.status === 'ACTIVE'\n ).length;\n return (\n <li key={request.id}>\n <GlassCard className=\"flex h-full flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-accent-primary\">\n <FileText className=\"size-4\" aria-hidden=\"true\" />\n <span className=\"truncate\">{request.category}</span>\n </div>\n <h2 className=\"mt-2 line-clamp-2 text-lg font-semibold text-text-primary\">\n {request.productName}\n </h2>\n </div>\n <SourcingStatusBadge status={request.status} />\n </div>\n\n <dl className=\"mt-5 grid gap-3 text-sm sm:grid-cols-2\">\n <div className=\"flex items-start gap-2\">\n <PackageSearch\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Quantity</dt>\n <dd className=\"font-medium text-text-primary\">\n {request.quantity.toLocaleString()}{' '}\n {formatSourcingUnit(request.unit)}\n </dd>\n </div>\n </div>\n <div className=\"flex items-start gap-2\">\n <MapPin\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Deliver to</dt>\n <dd className=\"font-medium text-text-primary\">\n {formatLocation({\n country: request.deliveryCountry,\n region: request.deliveryRegion,\n city: request.deliveryCity,\n })}\n </dd>\n </div>\n </div>\n <div className=\"flex items-start gap-2 sm:col-span-2\">\n <CalendarDays\n className=\"mt-0.5 size-4 shrink-0 text-text-muted\"\n aria-hidden=\"true\"\n />\n <div>\n <dt className=\"text-xs text-text-muted\">Delivery window</dt>\n <dd className=\"font-medium text-text-primary\">\n {formatDate(request.deliveryWindowStart)} –{' '}\n {formatDate(request.deliveryWindowEnd)}\n </dd>\n </div>\n </div>\n </dl>\n\n <div className=\"mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-border-subtle pt-4\">\n <div>\n <p className=\"text-sm font-medium text-text-primary\">\n {activeOffers} active {activeOffers === 1 ? 'offer' : 'offers'}\n </p>\n <p className=\"text-xs text-text-muted\">\n Target{' '}\n {formatMoney(request.targetUnitPrice, request.targetCurrency)}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() =>\n navigate(joinStorePath(basePath, `sourcing/requests/${request.id}`))\n }\n className={secondaryButtonClassName}\n >\n View details\n <ArrowRight className=\"size-4\" aria-hidden=\"true\" />\n </button>\n </div>\n </GlassCard>\n </li>\n );\n })}\n </ul>\n\n {hasMore ? (\n <div className=\"mt-6 flex justify-center\">\n <button\n type=\"button\"\n onClick={() => void load(offset + PAGE_SIZE, true)}\n disabled={loading}\n className={secondaryButtonClassName}\n >\n {loading ? 'Loading…' : 'Load more'}\n </button>\n </div>\n ) : null}\n </>\n )}\n </>\n )}\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|