@burdenoff/microfe-store 2026.1013.1 → 2026.1013.3
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/components/InstallAppModal.js +1 -1
- package/dist/components/InstallAppModal.js.map +1 -1
- 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/MarketplaceHomePage.js +1 -1
- package/dist/pages/MarketplaceHomePage.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
|
@@ -445,7 +445,7 @@ var F = ({ isOpen: n, onClose: a, app: S, orderId: D, purchaseState: A = "free",
|
|
|
445
445
|
type: "selectVersion",
|
|
446
446
|
version: e.target.value
|
|
447
447
|
}),
|
|
448
|
-
className: "w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
448
|
+
className: "w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary scheme-normal dark:scheme-dark focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] [&_option]:bg-bg-surface [&_option]:text-text-primary",
|
|
449
449
|
children: [/* @__PURE__ */ h("option", {
|
|
450
450
|
value: "",
|
|
451
451
|
children: S.version ? L("installApp.latestWithVersion", "Latest ({{version}})", { version: S.version }) : L("installApp.latest", "Latest")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InstallAppModal.js","names":[],"sources":["../../src/components/InstallAppModal.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState, useEffect, useCallback, useReducer } from 'react';\nimport {\n Loader2,\n Building2,\n CheckCircle,\n AlertCircle,\n Info,\n ShieldCheck,\n ChevronRight,\n ArrowLeft,\n} from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useStore } from '../providers/StoreProvider';\nimport { useTr } from '../shared/hooks/useTr';\n\n// Local type definitions for workspace operations\ninterface Workspace {\n id: string;\n name: string;\n slug: string;\n type?: string;\n status?: string;\n organizationId?: string;\n}\n\ninterface WorkspaceList {\n items: Workspace[];\n total: number;\n}\n\ninterface InstallWorkspaceAppsResult {\n successCount: number;\n failedCount: number;\n results: Array<{\n success: boolean;\n workspaceId: string;\n app?: unknown;\n error?: string;\n }>;\n}\n\n// String queries for graphqlFetch\nconst MY_WORKSPACES_STRING = `query MyWorkspaces { myWorkspaces { items { id name slug type status organizationId } total } }`;\nconst INSTALL_WORKSPACE_APPS_STRING = `mutation InstallWorkspaceApps($workspaceIds: [ID!]!, $input: InstallWorkspaceAppInput!) { installWorkspaceApps(workspaceIds: $workspaceIds, input: $input) { successCount failedCount results { success workspaceId error } } }`;\nconst RECORD_APP_INSTALLATION_STRING = `mutation RecordAppInstallation($input: RecordAppInstallationInput!) { recordAppInstallation(input: $input) { id status workspaceId version } }`;\nconst INSTALL_PURCHASED_APP_STRING = `mutation InstallPurchasedApp($orderId: ID, $productId: String!, $workspaceId: String!, $version: String) { installPurchasedApp(orderId: $orderId, productId: $productId, workspaceId: $workspaceId, version: $version) { success alreadyInstalled installation { id productId workspaceId status version } error message } }`;\nconst CREATE_PRODUCT_ENTITLEMENT_STRING = `mutation CreateProductEntitlement($input: CreateProductEntitlementInput!) { createProductEntitlement(input: $input) { id status productId workspaceId } }`;\nconst INSTALL_APP_DEVPORTAL_STRING = `mutation InstallAppDevportal($input: InstallAppInput!) { installApp(input: $input) { id status workspaceId application { oauthAuthUrl } } }`;\nconst INSTALLABLE_APPLICATION_VERSIONS_STRING = `query InstallableApplicationVersions($applicationId: ID!) { installableApplicationVersions(applicationId: $applicationId) { version status isLatest } }`;\n\nconst STORE_DISTRIBUTED_TYPES = new Set([\n 'VIBEMODULE',\n 'NODE',\n 'CONSOLE',\n 'DASHBOARD',\n 'PARSER',\n 'DATASINK',\n]);\n\n// App types that require an explicit permissions consent step before install\nconst CONSENT_REQUIRED_TYPES = new Set(['INTEGRATION', 'API_INTEGRATION']);\n\nconst INSTALL_ERROR_MESSAGES: Record<string, string> = {\n ALREADY_INSTALLED: 'This app is already installed in the selected workspace.',\n QUOTA_EXCEEDED: 'Installation limit reached. Please upgrade your plan or contact support.',\n PERMISSION_DENIED: 'You do not have permission to install apps in this workspace.',\n NOT_FOUND: 'The app or workspace could not be found.',\n WORKSPACE_INACTIVE: 'The selected workspace is inactive.',\n ENTITLEMENT_EXPIRED: 'Your license for this app has expired.',\n UNSUPPORTED_TYPE: 'This product type cannot be installed in the selected workspace.',\n};\n\nfunction mapInstallError(raw: string): string {\n for (const [code, message] of Object.entries(INSTALL_ERROR_MESSAGES)) {\n if (raw.toUpperCase().includes(code)) return message;\n }\n return 'Installation failed. Please try again or contact support.';\n}\n\ntype InstallStep = 'workspace' | 'consent';\n\ninterface InstallState {\n step: InstallStep;\n selectedWorkspace: string | null;\n selectedVersion: string;\n installing: boolean;\n installError: string | null;\n installInfo: string | null;\n}\n\ntype InstallAction =\n | { type: 'reset' }\n | { type: 'selectWorkspace'; workspaceId: string }\n | { type: 'selectVersion'; version: string }\n | { type: 'advanceToConsent' }\n | { type: 'backToWorkspace' }\n | { type: 'startInstall' }\n | { type: 'finishInstall' }\n | { type: 'setError'; error: string }\n | { type: 'setInfo'; info: string };\n\nconst initialInstallState: InstallState = {\n step: 'workspace',\n selectedWorkspace: null,\n selectedVersion: '',\n installing: false,\n installError: null,\n installInfo: null,\n};\n\nfunction installReducer(state: InstallState, action: InstallAction): InstallState {\n switch (action.type) {\n case 'reset':\n return initialInstallState;\n case 'selectWorkspace':\n return { ...state, selectedWorkspace: action.workspaceId };\n case 'selectVersion':\n return { ...state, selectedVersion: action.version };\n case 'advanceToConsent':\n return { ...state, step: 'consent', installError: null };\n case 'backToWorkspace':\n return { ...state, step: 'workspace', installError: null };\n case 'startInstall':\n return { ...state, installing: true, installError: null, installInfo: null };\n case 'finishInstall':\n return { ...state, installing: false };\n case 'setError':\n return { ...state, installError: action.error };\n case 'setInfo':\n return { ...state, installInfo: action.info };\n default:\n return state;\n }\n}\n\nexport interface AppInfo {\n id: string;\n // devportal Application.id — equals StoreProduct.productId, used for installApp mutation\n applicationId?: string;\n name: string;\n slug?: string;\n icon?: string;\n type?: string;\n version?: string;\n permissions?: string[];\n}\n\ninterface InstallAppModalProps {\n isOpen: boolean;\n onClose: () => void;\n app: AppInfo;\n orderId?: string | null; // For purchased apps from orders page\n purchaseState?: 'free' | 'purchased';\n onInstallSuccess?: (workspaceId: string, workspaceName: string) => void;\n onInstallError?: (error: Error) => void;\n}\n\ninterface UseWorkspacesResult {\n workspaces: Workspace[];\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\n// NOTE: Inline Apollo clients removed. All GraphQL calls now use graphqlFetch\n// from fe-libs which automatically resolves gateway URLs and injects auth + locale headers.\n\n/**\n * Hook to fetch user's workspaces using graphqlFetch from fe-libs\n */\nfunction useWorkspaces(skip: boolean): UseWorkspacesResult {\n const { authToken } = useStore();\n const [workspaces, setWorkspaces] = useState<Workspace[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const fetchWorkspaces = useCallback(async () => {\n if (skip) return;\n\n setLoading(true);\n setError(null);\n\n try {\n const result = await graphqlFetch<{ myWorkspaces: WorkspaceList }>({\n gateway: 'global',\n query: MY_WORKSPACES_STRING,\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'GraphQL error');\n }\n\n setWorkspaces(result.data?.myWorkspaces?.items || []);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to fetch workspaces'));\n } finally {\n setLoading(false);\n }\n }, [authToken, skip]);\n\n useEffect(() => {\n fetchWorkspaces();\n }, [fetchWorkspaces]);\n\n return {\n workspaces,\n loading,\n error,\n refetch: fetchWorkspaces,\n };\n}\n\ninterface InstallableVersion {\n version: string;\n status: string;\n isLatest: boolean;\n}\n\n/**\n * Hook to fetch an app's installable (published/deprecated) versions, for\n * the optional version picker. Skipped entirely when there's no linked\n * devportal applicationId (e.g. sideloaded or legacy listings) — those\n * always install whatever the store listing's current version is.\n */\nfunction useInstallableVersions(\n applicationId: string | undefined,\n skip: boolean\n): { versions: InstallableVersion[]; loading: boolean } {\n const { authToken } = useStore();\n const [versions, setVersions] = useState<InstallableVersion[]>([]);\n const [loading, setLoading] = useState(false);\n\n useEffect(() => {\n if (skip || !applicationId) {\n setVersions([]);\n return;\n }\n\n let cancelled = false;\n setLoading(true);\n\n graphqlFetch<{ installableApplicationVersions: InstallableVersion[] }>({\n gateway: 'global',\n query: INSTALLABLE_APPLICATION_VERSIONS_STRING,\n variables: { applicationId },\n authToken: authToken || undefined,\n })\n .then((result) => {\n if (cancelled) return;\n if (result.errors?.length) {\n setVersions([]);\n return;\n }\n setVersions(result.data?.installableApplicationVersions ?? []);\n })\n .catch(() => {\n if (!cancelled) setVersions([]);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [applicationId, authToken, skip]);\n\n return { versions, loading };\n}\n\n/**\n * InstallAppModal component\n *\n * Modal for selecting a workspace to install an app into\n */\nexport const InstallAppModal: FC<InstallAppModalProps> = ({\n isOpen,\n onClose,\n app,\n orderId,\n purchaseState = 'free',\n onInstallSuccess,\n onInstallError,\n}) => {\n const tr = useTr();\n const { authToken, user } = useStore();\n const [installState, dispatchInstall] = useReducer(installReducer, initialInstallState);\n const { step, selectedWorkspace, selectedVersion, installing, installError, installInfo } =\n installState;\n\n const appType = app.type?.toUpperCase() ?? '';\n const requiresConsent = CONSENT_REQUIRED_TYPES.has(appType);\n const supportsVersionChoice = STORE_DISTRIBUTED_TYPES.has(appType);\n\n const { workspaces, loading, error } = useWorkspaces(!isOpen);\n const { versions: installableVersions } = useInstallableVersions(\n app.applicationId,\n !isOpen || !supportsVersionChoice\n );\n\n // Reset step and selection whenever the modal is closed/reopened\n useEffect(() => {\n if (!isOpen) {\n dispatchInstall({ type: 'reset' });\n }\n }, [isOpen]);\n\n // Filter workspaces by status (only show active)\n const filteredWorkspaces = workspaces.filter((ws) => ws.status === 'ACTIVE');\n\n /**\n * Install purchased app via Store Service (NEW FLOW)\n * Used when installing from orders page or any already-purchased product\n */\n const handleInstallFromStore = async () => {\n if (!selectedWorkspace) return;\n\n const workspace = filteredWorkspaces.find((ws) => ws.id === selectedWorkspace);\n if (!workspace) return;\n const purchasedBy = user?.id;\n\n dispatchInstall({ type: 'startInstall' });\n\n try {\n if (!orderId && purchaseState !== 'purchased') {\n if (!purchasedBy) {\n throw new Error('Unable to identify the current user for license creation');\n }\n\n await graphqlFetch({\n gateway: 'global',\n query: CREATE_PRODUCT_ENTITLEMENT_STRING,\n variables: {\n input: {\n productId: app.id,\n purchasedBy,\n workspaceId: workspace.id,\n metadata: {\n source: 'store-ui',\n productType: app.type,\n },\n },\n },\n authToken: authToken || undefined,\n });\n }\n\n // Call Store Service to install the purchased app\n const result = await graphqlFetch<{\n installPurchasedApp: {\n success: boolean;\n alreadyInstalled?: boolean;\n installation?: {\n id: string;\n productId: string;\n workspaceId: string;\n status: string;\n version: string;\n };\n error?: string;\n message?: string;\n };\n }>({\n gateway: 'global', // Store Service\n query: INSTALL_PURCHASED_APP_STRING,\n variables: {\n orderId: orderId ?? null,\n productId: app.id,\n workspaceId: workspace.id,\n version: selectedVersion || undefined,\n },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Installation failed');\n }\n\n const installResult = result.data?.installPurchasedApp;\n\n if (installResult?.success) {\n if (installResult.alreadyInstalled) {\n dispatchInstall({\n type: 'setInfo',\n info: installResult.message || 'Already installed on this workspace',\n });\n } else {\n onInstallSuccess?.(workspace.id, workspace.name);\n onClose();\n }\n } else {\n const rawMsg = installResult?.error || 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n }\n } catch (err) {\n const rawMsg = err instanceof Error ? err.message : 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n } finally {\n dispatchInstall({ type: 'finishInstall' });\n }\n };\n\n /**\n * Install app via Workspace Service (LEGACY FLOW)\n * Used for free apps or when installing without order\n */\n const handleInstallViaWorkspace = async () => {\n if (!selectedWorkspace) return;\n\n const workspace = filteredWorkspaces.find((ws) => ws.id === selectedWorkspace);\n if (!workspace) return;\n\n dispatchInstall({ type: 'startInstall' });\n\n try {\n // Step 1: Install to workspace service via graphqlFetch\n const installVars = {\n workspaceIds: [workspace.id],\n input: {\n appName: app.slug || app.name.toLowerCase().replace(/\\s+/g, '-'),\n displayName: app.name,\n version: app.version || '1.0.0',\n type: app.type || 'app',\n manifest: {\n productId: app.id,\n icon: app.icon,\n },\n },\n };\n\n const result = await graphqlFetch<{\n installWorkspaceApps: InstallWorkspaceAppsResult;\n }>({\n gateway: 'workspace',\n query: INSTALL_WORKSPACE_APPS_STRING,\n variables: installVars,\n authToken: authToken || undefined,\n workspaceId: workspace.id,\n workspaceToken: true,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Installation failed');\n }\n\n const installResult = result.data?.installWorkspaceApps;\n if (installResult?.successCount && installResult.successCount > 0) {\n // Step 2: Record installation in store service (for tracking)\n try {\n await graphqlFetch({\n gateway: 'global',\n query: RECORD_APP_INSTALLATION_STRING,\n variables: {\n input: {\n productId: app.id,\n workspaceId: workspace.id,\n workspaceName: workspace.name,\n workspaceType: workspace.type,\n organizationId: workspace.organizationId,\n version: app.version || '1.0.0',\n manifest: {\n name: app.name,\n productId: app.id,\n icon: app.icon,\n slug: app.slug,\n type: app.type,\n },\n },\n },\n authToken: authToken || undefined,\n });\n } catch {\n // Non-fatal: workspace installation succeeded; store record will sync later.\n }\n\n onInstallSuccess?.(workspace.id, workspace.name);\n onClose();\n } else {\n const rawMsg = installResult?.results?.[0]?.error || 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n }\n } catch (err) {\n const rawMsg = err instanceof Error ? err.message : 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n } finally {\n dispatchInstall({ type: 'finishInstall' });\n }\n };\n\n /**\n * Advance from workspace selection to consent step (for integration types),\n * or go straight to install (for other types).\n */\n const handleContinue = () => {\n if (requiresConsent) {\n dispatchInstall({ type: 'advanceToConsent' });\n } else {\n void handleInstall();\n }\n };\n\n /**\n * Install INTEGRATION / API_INTEGRATION apps via the devportal installApp mutation.\n * After a successful install, redirects the user to the app's callback URL with\n * ?installation_id=<id> appended so the integration server can complete the handshake.\n */\n const handleInstallIntegration = async () => {\n if (!selectedWorkspace) return;\n const workspace = filteredWorkspaces.find((ws) => ws.id === selectedWorkspace);\n if (!workspace) return;\n\n dispatchInstall({ type: 'startInstall' });\n try {\n const result = await graphqlFetch<{\n installApp: {\n id: string;\n status: string;\n workspaceId: string;\n application: { oauthAuthUrl?: string | null };\n };\n }>({\n gateway: 'global',\n query: INSTALL_APP_DEVPORTAL_STRING,\n variables: {\n input: {\n applicationId: app.applicationId ?? app.id,\n scope: 'WORKSPACE',\n workspaceId: workspace.id,\n consentedPermissions: app.permissions ?? [],\n },\n },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Installation failed');\n }\n\n const installResult = result.data?.installApp;\n if (!installResult) throw new Error('Installation failed');\n\n // Record the integration install in the store service so the product page\n // shows \"Installed\" and it appears in the Installed Apps management page.\n try {\n await graphqlFetch({\n gateway: 'global',\n query: RECORD_APP_INSTALLATION_STRING,\n variables: {\n input: {\n productId: app.id,\n workspaceId: workspace.id,\n workspaceName: workspace.name,\n workspaceType: workspace.type,\n organizationId: workspace.organizationId,\n version: app.version || '1.0.0',\n manifest: {\n name: app.name,\n applicationId: app.applicationId,\n productId: app.id,\n icon: app.icon,\n slug: app.slug,\n type: app.type,\n installationId: installResult.id,\n },\n },\n },\n authToken: authToken || undefined,\n });\n } catch {\n // Non-fatal — devportal installation succeeded; store record will sync later.\n }\n\n const callbackUrl = installResult.application?.oauthAuthUrl;\n if (callbackUrl) {\n window.open(`${callbackUrl}?installation_id=${installResult.id}`, '_blank');\n }\n\n onInstallSuccess?.(workspace.id, workspace.name);\n onClose();\n } catch (err) {\n const rawMsg = err instanceof Error ? err.message : 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n } finally {\n dispatchInstall({ type: 'finishInstall' });\n }\n };\n\n /**\n * Main install handler - routes to appropriate install function\n */\n const handleInstall = async () => {\n if (CONSENT_REQUIRED_TYPES.has(appType)) {\n // Integration apps use the devportal installApp mutation with post-install redirect.\n await handleInstallIntegration();\n } else if (purchaseState === 'purchased' || orderId || STORE_DISTRIBUTED_TYPES.has(appType)) {\n // Purchased products install through the store service.\n await handleInstallFromStore();\n } else {\n // Free products install through the workspace service.\n await handleInstallViaWorkspace();\n }\n };\n\n const selectedWorkspaceObj = filteredWorkspaces.find((ws) => ws.id === selectedWorkspace);\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>\n <DialogContent className=\"mx-4 w-full max-w-lg gap-0 p-0\">\n {/* Header */}\n <DialogHeader className=\"flex flex-row items-center justify-between space-y-0 border-b border-border-default px-6 py-4 text-left\">\n <div className=\"flex items-center gap-3\">\n {app.icon ? (\n <img src={app.icon} alt={app.name} className=\"size-10 rounded-lg object-cover\" />\n ) : (\n <div className=\"flex size-10 items-center justify-center rounded-lg bg-bg-sunken text-lg font-bold text-text-muted\">\n {app.name.charAt(0)}\n </div>\n )}\n <div>\n <DialogTitle className=\"text-lg font-semibold text-text-primary\">\n {step === 'consent'\n ? tr('installApp.reviewPermissions', 'Review Permissions')\n : tr('installApp.title', 'Install App')}\n </DialogTitle>\n <DialogDescription className=\"text-sm text-text-muted\">{app.name}</DialogDescription>\n </div>\n </div>\n {/* Step indicator for consent-gated types */}\n {requiresConsent && (\n <div className=\"flex items-center gap-1.5 text-xs text-text-muted shrink-0\">\n <span className={step === 'workspace' ? 'font-semibold text-text-primary' : ''}>\n 1\n </span>\n <ChevronRight className=\"size-3\" />\n <span className={step === 'consent' ? 'font-semibold text-text-primary' : ''}>2</span>\n </div>\n )}\n </DialogHeader>\n\n {/* ── Step 1: Workspace selection ── */}\n {step === 'workspace' && (\n <div className=\"px-6 py-4\">\n <p className=\"mb-4 text-text-secondary\">\n {tr(\n 'installApp.selectWorkspace',\n 'Select the workspace where you want to install this app:'\n )}\n </p>\n\n {/* Workspace List */}\n <div className=\"max-h-72 overflow-y-auto rounded-lg border border-border-seam\">\n {loading ? (\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"size-8 animate-spin text-text-muted\" />\n </div>\n ) : error ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <AlertCircle className=\"mb-2 size-8 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">\n {tr('installApp.failedToLoadWorkspaces', 'Failed to load workspaces')}\n </p>\n <p className=\"text-xs text-text-muted mt-1\">{error.message}</p>\n </div>\n ) : filteredWorkspaces.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <Building2 className=\"mb-2 size-8 text-text-muted\" />\n <p className=\"text-sm text-text-muted\">\n {tr('installApp.noWorkspaces', 'No workspaces available')}\n </p>\n </div>\n ) : (\n <div className=\"divide-y divide-border-default\">\n {filteredWorkspaces.map((workspace) => (\n <button\n type=\"button\"\n key={workspace.id}\n onClick={() =>\n dispatchInstall({ type: 'selectWorkspace', workspaceId: workspace.id })\n }\n className={`w-full cursor-pointer flex items-center gap-3 px-4 py-3 text-left transition-colors ${\n selectedWorkspace === workspace.id\n ? 'bg-action-primary-bg/10'\n : 'hover:bg-bg-sunken'\n }`}\n >\n <div className=\"flex size-10 items-center justify-center rounded-lg bg-action-primary-bg/10 text-text-link\">\n <Building2 className=\"size-5\" />\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">{workspace.name}</p>\n {workspace.slug ? (\n <p className=\"text-xs text-text-muted truncate\">/{workspace.slug}</p>\n ) : null}\n </div>\n {selectedWorkspace === workspace.id && (\n <CheckCircle className=\"size-5 flex-shrink-0 text-action-primary-bg\" />\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Optional version picker — only shown when the app has more\n than one installable (published/deprecated) version. */}\n {supportsVersionChoice && installableVersions.length > 1 && (\n <div className=\"mt-4\">\n <label\n htmlFor=\"install-app-version\"\n className=\"mb-1.5 block text-xs font-medium text-text-secondary\"\n >\n {tr('installApp.version', 'Version')}\n </label>\n <select\n id=\"install-app-version\"\n value={selectedVersion}\n onChange={(event) =>\n dispatchInstall({ type: 'selectVersion', version: event.target.value })\n }\n className=\"w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">\n {app.version\n ? tr('installApp.latestWithVersion', 'Latest ({{version}})', {\n version: app.version,\n })\n : tr('installApp.latest', 'Latest')}\n </option>\n {installableVersions\n .filter((v) => !v.isLatest)\n .map((v) => (\n <option key={v.version} value={v.version}>\n {tr('installApp.versionWithStatus', '{{version}} ({{status}})', {\n version: v.version,\n status: v.status,\n })}\n </option>\n ))}\n </select>\n </div>\n )}\n </div>\n )}\n\n {/* ── Step 2: Permissions consent (integration types only) ── */}\n {step === 'consent' && (\n <div className=\"px-6 py-4\">\n {/* Selected workspace reminder */}\n {selectedWorkspaceObj && (\n <div className=\"mb-4 flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\">\n <Building2 className=\"size-4 shrink-0 text-text-muted\" />\n <span className=\"text-sm text-text-secondary\">\n {tr('installApp.installingInto', 'Installing into')}{' '}\n <span className=\"font-medium text-text-primary\">{selectedWorkspaceObj.name}</span>\n </span>\n </div>\n )}\n\n {/* Shield header */}\n <div className=\"mb-4 flex items-center gap-2\">\n <ShieldCheck className=\"size-5 text-status-success-text\" />\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('installApp.permissionsRequested', 'Permissions requested')}\n </p>\n </div>\n\n <p className=\"mb-4 text-sm text-text-secondary\">\n {tr(\n 'installApp.consentDescription',\n 'By clicking {{button}}, you grant {{appName}} access to the following in the selected workspace:',\n {\n button: tr('installApp.acceptAndInstall', 'Accept & Install'),\n appName: app.name,\n }\n )}\n </p>\n\n {/* Permission list — specific scopes if available, generic grant otherwise */}\n <div className=\"rounded-lg border border-border-seam bg-bg-surface px-4 py-3\">\n {app.permissions && app.permissions.length > 0 ? (\n <ul className=\"space-y-2\">\n {app.permissions.map((perm) => (\n <li key={perm} className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <CheckCircle className=\"size-4 shrink-0 text-status-success-text\" />\n {perm}\n </li>\n ))}\n </ul>\n ) : (\n <ul className=\"space-y-2\">\n <li className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <CheckCircle className=\"size-4 shrink-0 text-status-success-text\" />\n {tr(\n 'installApp.permissionReadWrite',\n 'Read and write data within this workspace on your behalf'\n )}\n </li>\n <li className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <CheckCircle className=\"size-4 shrink-0 text-status-success-text\" />\n {tr(\n 'installApp.permissionWebhooks',\n 'Receive webhook events for workspace activity'\n )}\n </li>\n <li className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <CheckCircle className=\"size-4 shrink-0 text-status-success-text\" />\n {tr(\n 'installApp.permissionAuthorized',\n 'Act as an authorized integration within this workspace'\n )}\n </li>\n </ul>\n )}\n </div>\n\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'installApp.revokeHint',\n 'You can revoke access at any time from your Installed Apps settings.'\n )}\n </p>\n </div>\n )}\n\n {/* Footer */}\n <DialogFooter className=\"block border-t border-border-default px-6 py-4 sm:flex-col sm:items-stretch sm:space-x-0\">\n {installInfo && (\n <div className=\"mb-3 flex items-center gap-2 rounded-lg bg-status-info-bg-subtle px-3 py-2 text-sm text-status-info-text\">\n <Info className=\"size-4 flex-shrink-0\" />\n <span>{installInfo}</span>\n </div>\n )}\n {installError && (\n <div className=\"mb-3 flex items-center gap-2 rounded-lg bg-status-error-bg-subtle/10 px-3 py-2 text-sm text-status-error-text\">\n <AlertCircle className=\"size-4 flex-shrink-0\" />\n <span>{installError}</span>\n </div>\n )}\n <div className=\"flex items-center justify-end gap-3\">\n {step === 'consent' ? (\n <>\n <button\n type=\"button\"\n onClick={() => dispatchInstall({ type: 'backToWorkspace' })}\n disabled={installing}\n className=\"cursor-pointer inline-flex items-center gap-1.5 rounded-lg border border-border-default px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('common.back', 'Back')}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleInstall()}\n disabled={installing}\n className=\"cursor-pointer rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2\"\n >\n {installing ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('installApp.installing', 'Installing…')}\n </>\n ) : (\n tr('installApp.acceptAndInstall', 'Accept & Install')\n )}\n </button>\n </>\n ) : (\n <>\n <button\n type=\"button\"\n onClick={onClose}\n disabled={installing}\n className=\"cursor-pointer rounded-lg border border-border-default px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleContinue}\n disabled={!selectedWorkspace || installing}\n className=\"cursor-pointer rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2\"\n >\n {installing ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('installApp.installing', 'Installing…')}\n </>\n ) : requiresConsent ? (\n <>\n {tr('common.continue', 'Continue')}\n <ChevronRight className=\"size-4\" />\n </>\n ) : (\n tr('installApp.installApp', 'Install App')\n )}\n </button>\n </>\n )}\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n};\n"],"mappings":";;;;;;;;AAmDA,IAAM,IAAuB,mGACvB,KAAgC,mOAChC,IAAiC,kJACjC,IAA+B,gUAC/B,IAAoC,6JACpC,IAA+B,+IAC/B,IAA0C,2JAE1C,IAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACA;CACD,CAAC,EAGI,IAAyB,IAAI,IAAI,CAAC,eAAe,kBAAkB,CAAC,EAEpE,IAAiD;CACrD,mBAAmB;CACnB,gBAAgB;CAChB,mBAAmB;CACnB,WAAW;CACX,oBAAoB;CACpB,qBAAqB;CACrB,kBAAkB;CACnB;AAED,SAAS,EAAgB,GAAqB;AAC5C,MAAK,IAAM,CAAC,GAAM,MAAY,OAAO,QAAQ,EAAuB,CAClE,KAAI,EAAI,aAAa,CAAC,SAAS,EAAK,CAAE,QAAO;AAE/C,QAAO;;AAyBT,IAAM,IAAoC;CACxC,MAAM;CACN,mBAAmB;CACnB,iBAAiB;CACjB,YAAY;CACZ,cAAc;CACd,aAAa;CACd;AAED,SAAS,GAAe,GAAqB,GAAqC;AAChF,SAAQ,EAAO,MAAf;EACE,KAAK,QACH,QAAO;EACT,KAAK,kBACH,QAAO;GAAE,GAAG;GAAO,mBAAmB,EAAO;GAAa;EAC5D,KAAK,gBACH,QAAO;GAAE,GAAG;GAAO,iBAAiB,EAAO;GAAS;EACtD,KAAK,mBACH,QAAO;GAAE,GAAG;GAAO,MAAM;GAAW,cAAc;GAAM;EAC1D,KAAK,kBACH,QAAO;GAAE,GAAG;GAAO,MAAM;GAAa,cAAc;GAAM;EAC5D,KAAK,eACH,QAAO;GAAE,GAAG;GAAO,YAAY;GAAM,cAAc;GAAM,aAAa;GAAM;EAC9E,KAAK,gBACH,QAAO;GAAE,GAAG;GAAO,YAAY;GAAO;EACxC,KAAK,WACH,QAAO;GAAE,GAAG;GAAO,cAAc,EAAO;GAAO;EACjD,KAAK,UACH,QAAO;GAAE,GAAG;GAAO,aAAa,EAAO;GAAM;EAC/C,QACE,QAAO;;;AAuCb,SAAS,EAAc,GAAoC;CACzD,IAAM,EAAE,iBAAc,GAAU,EAC1B,CAAC,GAAY,KAAiB,EAAsB,EAAE,CAAC,EACvD,CAAC,GAAS,KAAc,EAAS,GAAM,EACvC,CAAC,GAAO,KAAY,EAAuB,KAAK,EAEhD,IAAkB,EAAY,YAAY;AAC1C,UAGJ;GADA,EAAW,GAAK,EAChB,EAAS,KAAK;AAEd,OAAI;IACF,IAAM,IAAS,MAAM,EAA8C;KACjE,SAAS;KACT,OAAO;KACP,WAAW,KAAa,KAAA;KACzB,CAAC;AAEF,QAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,IAAI,WAAW,gBAAgB;AAG/D,MAAc,EAAO,MAAM,cAAc,SAAS,EAAE,CAAC;YAC9C,GAAK;AACZ,MAAS,aAAe,QAAQ,IAAM,gBAAI,MAAM,6BAA6B,CAAC;aACtE;AACR,MAAW,GAAM;;;IAElB,CAAC,GAAW,EAAK,CAAC;AAMrB,QAJA,QAAgB;AACd,KAAiB;IAChB,CAAC,EAAgB,CAAC,EAEd;EACL;EACA;EACA;EACA,SAAS;EACV;;AAeH,SAAS,EACP,GACA,GACsD;CACtD,IAAM,EAAE,iBAAc,GAAU,EAC1B,CAAC,GAAU,KAAe,EAA+B,EAAE,CAAC,EAC5D,CAAC,GAAS,KAAc,EAAS,GAAM;AAqC7C,QAnCA,QAAgB;AACd,MAAI,KAAQ,CAAC,GAAe;AAC1B,KAAY,EAAE,CAAC;AACf;;EAGF,IAAI,IAAY;AAwBhB,SAvBA,EAAW,GAAK,EAEhB,EAAuE;GACrE,SAAS;GACT,OAAO;GACP,WAAW,EAAE,kBAAe;GAC5B,WAAW,KAAa,KAAA;GACzB,CAAC,CACC,MAAM,MAAW;AACZ,WACJ;QAAI,EAAO,QAAQ,QAAQ;AACzB,OAAY,EAAE,CAAC;AACf;;AAEF,MAAY,EAAO,MAAM,kCAAkC,EAAE,CAAC;;IAC9D,CACD,YAAY;AACX,GAAK,KAAW,EAAY,EAAE,CAAC;IAC/B,CACD,cAAc;AACb,GAAK,KAAW,EAAW,GAAM;IACjC,QAES;AACX,OAAY;;IAEb;EAAC;EAAe;EAAW;EAAK,CAAC,EAE7B;EAAE;EAAU;EAAS;;AAQ9B,IAAa,KAA6C,EACxD,WACA,YACA,QACA,YACA,mBAAgB,QAChB,qBACA,wBACI;CACJ,IAAM,IAAK,GAAO,EACZ,EAAE,cAAW,aAAS,GAAU,EAChC,CAAC,IAAc,KAAmB,EAAW,IAAgB,EAAoB,EACjF,EAAE,SAAM,sBAAmB,oBAAiB,eAAY,iBAAc,mBAC1E,IAEI,IAAU,EAAI,MAAM,aAAa,IAAI,IACrC,IAAkB,EAAuB,IAAI,EAAQ,EACrD,IAAwB,EAAwB,IAAI,EAAQ,EAE5D,EAAE,gBAAY,aAAS,aAAU,EAAc,CAAC,EAAO,EACvD,EAAE,UAAU,MAAwB,EACxC,EAAI,eACJ,CAAC,KAAU,CAAC,EACb;AAGD,SAAgB;AACd,EAAK,KACH,EAAgB,EAAE,MAAM,SAAS,CAAC;IAEnC,CAAC,EAAO,CAAC;CAGZ,IAAM,IAAqB,GAAW,QAAQ,MAAO,EAAG,WAAW,SAAS,EAMtE,KAAyB,YAAY;AACzC,MAAI,CAAC,EAAmB;EAExB,IAAM,IAAY,EAAmB,MAAM,MAAO,EAAG,OAAO,EAAkB;AAC9E,MAAI,CAAC,EAAW;EAChB,IAAM,IAAc,IAAM;AAE1B,IAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEzC,MAAI;AACF,OAAI,CAAC,KAAW,MAAkB,aAAa;AAC7C,QAAI,CAAC,EACH,OAAU,MAAM,2DAA2D;AAG7E,UAAM,EAAa;KACjB,SAAS;KACT,OAAO;KACP,WAAW,EACT,OAAO;MACL,WAAW,EAAI;MACf;MACA,aAAa,EAAU;MACvB,UAAU;OACR,QAAQ;OACR,aAAa,EAAI;OAClB;MACF,EACF;KACD,WAAW,KAAa,KAAA;KACzB,CAAC;;GAIJ,IAAM,IAAS,MAAM,EAclB;IACD,SAAS;IACT,OAAO;IACP,WAAW;KACT,SAAS,KAAW;KACpB,WAAW,EAAI;KACf,aAAa,EAAU;KACvB,SAAS,KAAmB,KAAA;KAC7B;IACD,WAAW,KAAa,KAAA;IACzB,CAAC;AAEF,OAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,IAAI,WAAW,sBAAsB;GAGrE,IAAM,IAAgB,EAAO,MAAM;AAEnC,OAAI,GAAe,QACjB,CAAI,EAAc,mBAChB,EAAgB;IACd,MAAM;IACN,MAAM,EAAc,WAAW;IAChC,CAAC,IAEF,IAAmB,EAAU,IAAI,EAAU,KAAK,EAChD,GAAS;QAEN;IACL,IAAM,IAAS,GAAe,SAAS;AAGvC,IADA,EAAgB;KAAE,MAAM;KAAY,OADnB,EAAgB,EAAO;KACa,CAAC,EACtD,IAAqB,MAAM,EAAO,CAAC;;WAE9B,GAAK;GACZ,IAAM,IAAS,aAAe,QAAQ,EAAI,UAAU;AAGpD,GADA,EAAgB;IAAE,MAAM;IAAY,OADnB,EAAgB,EAAO;IACa,CAAC,EACtD,IAAqB,MAAM,EAAO,CAAC;YAC3B;AACR,KAAgB,EAAE,MAAM,iBAAiB,CAAC;;IAQxC,KAA4B,YAAY;AAC5C,MAAI,CAAC,EAAmB;EAExB,IAAM,IAAY,EAAmB,MAAM,MAAO,EAAG,OAAO,EAAkB;AACzE,SAEL;KAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEzC,OAAI;IAgBF,IAAM,IAAS,MAAM,EAElB;KACD,SAAS;KACT,OAAO;KACP,WAnBkB;MAClB,cAAc,CAAC,EAAU,GAAG;MAC5B,OAAO;OACL,SAAS,EAAI,QAAQ,EAAI,KAAK,aAAa,CAAC,QAAQ,QAAQ,IAAI;OAChE,aAAa,EAAI;OACjB,SAAS,EAAI,WAAW;OACxB,MAAM,EAAI,QAAQ;OAClB,UAAU;QACR,WAAW,EAAI;QACf,MAAM,EAAI;QACX;OACF;MACF;KAQC,WAAW,KAAa,KAAA;KACxB,aAAa,EAAU;KACvB,gBAAgB;KACjB,CAAC;AAEF,QAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,IAAI,WAAW,sBAAsB;IAGrE,IAAM,IAAgB,EAAO,MAAM;AACnC,QAAI,GAAe,gBAAgB,EAAc,eAAe,GAAG;AAEjE,SAAI;AACF,YAAM,EAAa;OACjB,SAAS;OACT,OAAO;OACP,WAAW,EACT,OAAO;QACL,WAAW,EAAI;QACf,aAAa,EAAU;QACvB,eAAe,EAAU;QACzB,eAAe,EAAU;QACzB,gBAAgB,EAAU;QAC1B,SAAS,EAAI,WAAW;QACxB,UAAU;SACR,MAAM,EAAI;SACV,WAAW,EAAI;SACf,MAAM,EAAI;SACV,MAAM,EAAI;SACV,MAAM,EAAI;SACX;QACF,EACF;OACD,WAAW,KAAa,KAAA;OACzB,CAAC;aACI;AAKR,KADA,IAAmB,EAAU,IAAI,EAAU,KAAK,EAChD,GAAS;WACJ;KACL,IAAM,IAAS,GAAe,UAAU,IAAI,SAAS;AAGrD,KADA,EAAgB;MAAE,MAAM;MAAY,OADnB,EAAgB,EAAO;MACa,CAAC,EACtD,IAAqB,MAAM,EAAO,CAAC;;YAE9B,GAAK;IACZ,IAAM,IAAS,aAAe,QAAQ,EAAI,UAAU;AAGpD,IADA,EAAgB;KAAE,MAAM;KAAY,OADnB,EAAgB,EAAO;KACa,CAAC,EACtD,IAAqB,MAAM,EAAO,CAAC;aAC3B;AACR,MAAgB,EAAE,MAAM,iBAAiB,CAAC;;;IAQxC,WAAuB;AAC3B,EAAI,IACF,EAAgB,EAAE,MAAM,oBAAoB,CAAC,GAExC,GAAe;IASlB,KAA2B,YAAY;AAC3C,MAAI,CAAC,EAAmB;EACxB,IAAM,IAAY,EAAmB,MAAM,MAAO,EAAG,OAAO,EAAkB;AACzE,SAEL;KAAgB,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAI;IACF,IAAM,IAAS,MAAM,EAOlB;KACD,SAAS;KACT,OAAO;KACP,WAAW,EACT,OAAO;MACL,eAAe,EAAI,iBAAiB,EAAI;MACxC,OAAO;MACP,aAAa,EAAU;MACvB,sBAAsB,EAAI,eAAe,EAAE;MAC5C,EACF;KACD,WAAW,KAAa,KAAA;KACzB,CAAC;AAEF,QAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,IAAI,WAAW,sBAAsB;IAGrE,IAAM,IAAgB,EAAO,MAAM;AACnC,QAAI,CAAC,EAAe,OAAU,MAAM,sBAAsB;AAI1D,QAAI;AACF,WAAM,EAAa;MACjB,SAAS;MACT,OAAO;MACP,WAAW,EACT,OAAO;OACL,WAAW,EAAI;OACf,aAAa,EAAU;OACvB,eAAe,EAAU;OACzB,eAAe,EAAU;OACzB,gBAAgB,EAAU;OAC1B,SAAS,EAAI,WAAW;OACxB,UAAU;QACR,MAAM,EAAI;QACV,eAAe,EAAI;QACnB,WAAW,EAAI;QACf,MAAM,EAAI;QACV,MAAM,EAAI;QACV,MAAM,EAAI;QACV,gBAAgB,EAAc;QAC/B;OACF,EACF;MACD,WAAW,KAAa,KAAA;MACzB,CAAC;YACI;IAIR,IAAM,IAAc,EAAc,aAAa;AAM/C,IALI,KACF,OAAO,KAAK,GAAG,EAAY,mBAAmB,EAAc,MAAM,SAAS,EAG7E,IAAmB,EAAU,IAAI,EAAU,KAAK,EAChD,GAAS;YACF,GAAK;IACZ,IAAM,IAAS,aAAe,QAAQ,EAAI,UAAU;AAGpD,IADA,EAAgB;KAAE,MAAM;KAAY,OADnB,EAAgB,EAAO;KACa,CAAC,EACtD,IAAqB,MAAM,EAAO,CAAC;aAC3B;AACR,MAAgB,EAAE,MAAM,iBAAiB,CAAC;;;IAOxC,IAAgB,YAAY;AAChC,EAAI,EAAuB,IAAI,EAAQ,GAErC,MAAM,IAA0B,GACvB,MAAkB,eAAe,KAAW,EAAwB,IAAI,EAAQ,GAEzF,MAAM,IAAwB,GAG9B,MAAM,IAA2B;IAI/B,IAAuB,EAAmB,MAAM,MAAO,EAAG,OAAO,EAAkB;AAEzF,QACE,kBAAC,GAAD;EAAQ,MAAM;EAAQ,eAAe,MAAS,CAAC,KAAQ,GAAS;YAC9D,kBAAC,IAAD;GAAe,WAAU;aAAzB;IAEE,kBAAC,GAAD;KAAc,WAAU;eAAxB,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,EAAI,OACH,kBAAC,OAAD;OAAK,KAAK,EAAI;OAAM,KAAK,EAAI;OAAM,WAAU;OAAoC,CAAA,GAEjF,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAI,KAAK,OAAO,EAAE;OACf,CAAA,EAER,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAa,WAAU;iBACpB,MAAS,YACN,EAAG,gCAAgC,qBAAqB,GACxD,EAAG,oBAAoB,cAAc;OAC7B,CAAA,EACd,kBAAC,GAAD;OAAmB,WAAU;iBAA2B,EAAI;OAAyB,CAAA,CACjF,EAAA,CAAA,CACF;SAEL,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAW,MAAS,cAAc,oCAAoC;kBAAI;QAEzE,CAAA;OACP,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA;OACnC,kBAAC,QAAD;QAAM,WAAW,MAAS,YAAY,oCAAoC;kBAAI;QAAQ,CAAA;OAClF;QAEK;;IAGd,MAAS,eACR,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,8BACA,2DACD;OACC,CAAA;MAGJ,kBAAC,OAAD;OAAK,WAAU;iBACZ,KACC,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD,EAAS,WAAU,uCAAwC,CAAA;QACvD,CAAA,GACJ,IACF,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,GAAD,EAAa,WAAU,sCAAuC,CAAA;SAC9D,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAG,qCAAqC,4BAA4B;UACnE,CAAA;SACJ,kBAAC,KAAD;UAAG,WAAU;oBAAgC,EAAM;UAAY,CAAA;SAC3D;YACJ,EAAmB,WAAW,IAChC,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAW,WAAU,+BAAgC,CAAA,EACrD,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAG,2BAA2B,0BAA0B;SACvD,CAAA,CACA;YAEN,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAmB,KAAK,MACvB,kBAAC,UAAD;SACE,MAAK;SAEL,eACE,EAAgB;UAAE,MAAM;UAAmB,aAAa,EAAU;UAAI,CAAC;SAEzE,WAAW,uFACT,MAAsB,EAAU,KAC5B,4BACA;mBATR;UAYE,kBAAC,OAAD;WAAK,WAAU;qBACb,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA;WAC5B,CAAA;UACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,KAAD;YAAG,WAAU;sBAA0C,EAAU;YAAS,CAAA,EACzE,EAAU,OACT,kBAAC,KAAD;YAAG,WAAU;sBAAb,CAAgD,KAAE,EAAU,KAAS;gBACnE,KACA;;UACL,MAAsB,EAAU,MAC/B,kBAAC,GAAD,EAAa,WAAU,+CAAgD,CAAA;UAElE;WAtBF,EAAU,GAsBR,CACT;QACE,CAAA;OAEJ,CAAA;MAIL,KAAyB,EAAoB,SAAS,KACrD,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,SAAD;QACE,SAAQ;QACR,WAAU;kBAET,EAAG,sBAAsB,UAAU;QAC9B,CAAA,EACR,kBAAC,UAAD;QACE,IAAG;QACH,OAAO;QACP,WAAW,MACT,EAAgB;SAAE,MAAM;SAAiB,SAAS,EAAM,OAAO;SAAO,CAAC;QAEzE,WAAU;kBANZ,CAQE,kBAAC,UAAD;SAAQ,OAAM;mBACX,EAAI,UACD,EAAG,gCAAgC,wBAAwB,EACzD,SAAS,EAAI,SACd,CAAC,GACF,EAAG,qBAAqB,SAAS;SAC9B,CAAA,EACR,EACE,QAAQ,MAAM,CAAC,EAAE,SAAS,CAC1B,KAAK,MACJ,kBAAC,UAAD;SAAwB,OAAO,EAAE;mBAC9B,EAAG,gCAAgC,4BAA4B;UAC9D,SAAS,EAAE;UACX,QAAQ,EAAE;UACX,CAAC;SACK,EALI,EAAE,QAKN,CACT,CACG;UACL;;MAEJ;;IAIP,MAAS,aACR,kBAAC,OAAD;KAAK,WAAU;eAAf;MAEG,KACC,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAW,WAAU,mCAAoC,CAAA,EACzD,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAG,6BAA6B,kBAAkB;SAAE;SACrD,kBAAC,QAAD;UAAM,WAAU;oBAAiC,EAAqB;UAAY,CAAA;SAC7E;UACH;;MAIR,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,IAAD,EAAa,WAAU,mCAAoC,CAAA,EAC3D,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAG,mCAAmC,wBAAwB;QAC7D,CAAA,CACA;;MAEN,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,iCACA,oGACA;QACE,QAAQ,EAAG,+BAA+B,mBAAmB;QAC7D,SAAS,EAAI;QACd,CACF;OACC,CAAA;MAGJ,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAI,eAAe,EAAI,YAAY,SAAS,IAC3C,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAI,YAAY,KAAK,MACpB,kBAAC,MAAD;SAAe,WAAU;mBAAzB,CACE,kBAAC,GAAD,EAAa,WAAU,4CAA6C,CAAA,EACnE,EACE;WAHI,EAGJ,CACL;QACC,CAAA,GAEL,kBAAC,MAAD;QAAI,WAAU;kBAAd;SACE,kBAAC,MAAD;UAAI,WAAU;oBAAd,CACE,kBAAC,GAAD,EAAa,WAAU,4CAA6C,CAAA,EACnE,EACC,kCACA,2DACD,CACE;;SACL,kBAAC,MAAD;UAAI,WAAU;oBAAd,CACE,kBAAC,GAAD,EAAa,WAAU,4CAA6C,CAAA,EACnE,EACC,iCACA,gDACD,CACE;;SACL,kBAAC,MAAD;UAAI,WAAU;oBAAd,CACE,kBAAC,GAAD,EAAa,WAAU,4CAA6C,CAAA,EACnE,EACC,mCACA,yDACD,CACE;;SACF;;OAEH,CAAA;MAEN,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,yBACA,uEACD;OACC,CAAA;MACA;;IAIR,kBAAC,GAAD;KAAc,WAAU;eAAxB;MACG,KACC,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAM,WAAU,wBAAyB,CAAA,EACzC,kBAAC,QAAD,EAAA,UAAO,GAAmB,CAAA,CACtB;;MAEP,KACC,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,wBAAyB,CAAA,EAChD,kBAAC,QAAD,EAAA,UAAO,GAAoB,CAAA,CACvB;;MAER,kBAAC,OAAD;OAAK,WAAU;iBACZ,MAAS,YACR,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAgB,EAAE,MAAM,mBAAmB,CAAC;QAC3D,UAAU;QACV,WAAU;kBAJZ,CAME,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAC/B,EAAG,eAAe,OAAO,CACnB;WACT,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,KAAK,GAAe;QACnC,UAAU;QACV,WAAU;kBAET,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAC1C,EAAG,yBAAyB,cAAc,CAC1C,EAAA,CAAA,GAEH,EAAG,+BAA+B,mBAAmB;QAEhD,CAAA,CACR,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,UAAU;QACV,WAAU;kBAET,EAAG,iBAAiB,SAAS;QACvB,CAAA,EACT,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,UAAU,CAAC,KAAqB;QAChC,WAAU;kBAET,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAC1C,EAAG,yBAAyB,cAAc,CAC1C,EAAA,CAAA,GACD,IACF,kBAAA,GAAA,EAAA,UAAA,CACG,EAAG,mBAAmB,WAAW,EAClC,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA,CAClC,EAAA,CAAA,GAEH,EAAG,yBAAyB,cAAc;QAErC,CAAA,CACR,EAAA,CAAA;OAED,CAAA;MACO;;IACD;;EACT,CAAA"}
|
|
1
|
+
{"version":3,"file":"InstallAppModal.js","names":[],"sources":["../../src/components/InstallAppModal.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState, useEffect, useCallback, useReducer } from 'react';\nimport {\n Loader2,\n Building2,\n CheckCircle,\n AlertCircle,\n Info,\n ShieldCheck,\n ChevronRight,\n ArrowLeft,\n} from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { useStore } from '../providers/StoreProvider';\nimport { useTr } from '../shared/hooks/useTr';\n\n// Local type definitions for workspace operations\ninterface Workspace {\n id: string;\n name: string;\n slug: string;\n type?: string;\n status?: string;\n organizationId?: string;\n}\n\ninterface WorkspaceList {\n items: Workspace[];\n total: number;\n}\n\ninterface InstallWorkspaceAppsResult {\n successCount: number;\n failedCount: number;\n results: Array<{\n success: boolean;\n workspaceId: string;\n app?: unknown;\n error?: string;\n }>;\n}\n\n// String queries for graphqlFetch\nconst MY_WORKSPACES_STRING = `query MyWorkspaces { myWorkspaces { items { id name slug type status organizationId } total } }`;\nconst INSTALL_WORKSPACE_APPS_STRING = `mutation InstallWorkspaceApps($workspaceIds: [ID!]!, $input: InstallWorkspaceAppInput!) { installWorkspaceApps(workspaceIds: $workspaceIds, input: $input) { successCount failedCount results { success workspaceId error } } }`;\nconst RECORD_APP_INSTALLATION_STRING = `mutation RecordAppInstallation($input: RecordAppInstallationInput!) { recordAppInstallation(input: $input) { id status workspaceId version } }`;\nconst INSTALL_PURCHASED_APP_STRING = `mutation InstallPurchasedApp($orderId: ID, $productId: String!, $workspaceId: String!, $version: String) { installPurchasedApp(orderId: $orderId, productId: $productId, workspaceId: $workspaceId, version: $version) { success alreadyInstalled installation { id productId workspaceId status version } error message } }`;\nconst CREATE_PRODUCT_ENTITLEMENT_STRING = `mutation CreateProductEntitlement($input: CreateProductEntitlementInput!) { createProductEntitlement(input: $input) { id status productId workspaceId } }`;\nconst INSTALL_APP_DEVPORTAL_STRING = `mutation InstallAppDevportal($input: InstallAppInput!) { installApp(input: $input) { id status workspaceId application { oauthAuthUrl } } }`;\nconst INSTALLABLE_APPLICATION_VERSIONS_STRING = `query InstallableApplicationVersions($applicationId: ID!) { installableApplicationVersions(applicationId: $applicationId) { version status isLatest } }`;\n\nconst STORE_DISTRIBUTED_TYPES = new Set([\n 'VIBEMODULE',\n 'NODE',\n 'CONSOLE',\n 'DASHBOARD',\n 'PARSER',\n 'DATASINK',\n]);\n\n// App types that require an explicit permissions consent step before install\nconst CONSENT_REQUIRED_TYPES = new Set(['INTEGRATION', 'API_INTEGRATION']);\n\nconst INSTALL_ERROR_MESSAGES: Record<string, string> = {\n ALREADY_INSTALLED: 'This app is already installed in the selected workspace.',\n QUOTA_EXCEEDED: 'Installation limit reached. Please upgrade your plan or contact support.',\n PERMISSION_DENIED: 'You do not have permission to install apps in this workspace.',\n NOT_FOUND: 'The app or workspace could not be found.',\n WORKSPACE_INACTIVE: 'The selected workspace is inactive.',\n ENTITLEMENT_EXPIRED: 'Your license for this app has expired.',\n UNSUPPORTED_TYPE: 'This product type cannot be installed in the selected workspace.',\n};\n\nfunction mapInstallError(raw: string): string {\n for (const [code, message] of Object.entries(INSTALL_ERROR_MESSAGES)) {\n if (raw.toUpperCase().includes(code)) return message;\n }\n return 'Installation failed. Please try again or contact support.';\n}\n\ntype InstallStep = 'workspace' | 'consent';\n\ninterface InstallState {\n step: InstallStep;\n selectedWorkspace: string | null;\n selectedVersion: string;\n installing: boolean;\n installError: string | null;\n installInfo: string | null;\n}\n\ntype InstallAction =\n | { type: 'reset' }\n | { type: 'selectWorkspace'; workspaceId: string }\n | { type: 'selectVersion'; version: string }\n | { type: 'advanceToConsent' }\n | { type: 'backToWorkspace' }\n | { type: 'startInstall' }\n | { type: 'finishInstall' }\n | { type: 'setError'; error: string }\n | { type: 'setInfo'; info: string };\n\nconst initialInstallState: InstallState = {\n step: 'workspace',\n selectedWorkspace: null,\n selectedVersion: '',\n installing: false,\n installError: null,\n installInfo: null,\n};\n\nfunction installReducer(state: InstallState, action: InstallAction): InstallState {\n switch (action.type) {\n case 'reset':\n return initialInstallState;\n case 'selectWorkspace':\n return { ...state, selectedWorkspace: action.workspaceId };\n case 'selectVersion':\n return { ...state, selectedVersion: action.version };\n case 'advanceToConsent':\n return { ...state, step: 'consent', installError: null };\n case 'backToWorkspace':\n return { ...state, step: 'workspace', installError: null };\n case 'startInstall':\n return { ...state, installing: true, installError: null, installInfo: null };\n case 'finishInstall':\n return { ...state, installing: false };\n case 'setError':\n return { ...state, installError: action.error };\n case 'setInfo':\n return { ...state, installInfo: action.info };\n default:\n return state;\n }\n}\n\nexport interface AppInfo {\n id: string;\n // devportal Application.id — equals StoreProduct.productId, used for installApp mutation\n applicationId?: string;\n name: string;\n slug?: string;\n icon?: string;\n type?: string;\n version?: string;\n permissions?: string[];\n}\n\ninterface InstallAppModalProps {\n isOpen: boolean;\n onClose: () => void;\n app: AppInfo;\n orderId?: string | null; // For purchased apps from orders page\n purchaseState?: 'free' | 'purchased';\n onInstallSuccess?: (workspaceId: string, workspaceName: string) => void;\n onInstallError?: (error: Error) => void;\n}\n\ninterface UseWorkspacesResult {\n workspaces: Workspace[];\n loading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\n// NOTE: Inline Apollo clients removed. All GraphQL calls now use graphqlFetch\n// from fe-libs which automatically resolves gateway URLs and injects auth + locale headers.\n\n/**\n * Hook to fetch user's workspaces using graphqlFetch from fe-libs\n */\nfunction useWorkspaces(skip: boolean): UseWorkspacesResult {\n const { authToken } = useStore();\n const [workspaces, setWorkspaces] = useState<Workspace[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const fetchWorkspaces = useCallback(async () => {\n if (skip) return;\n\n setLoading(true);\n setError(null);\n\n try {\n const result = await graphqlFetch<{ myWorkspaces: WorkspaceList }>({\n gateway: 'global',\n query: MY_WORKSPACES_STRING,\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'GraphQL error');\n }\n\n setWorkspaces(result.data?.myWorkspaces?.items || []);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to fetch workspaces'));\n } finally {\n setLoading(false);\n }\n }, [authToken, skip]);\n\n useEffect(() => {\n fetchWorkspaces();\n }, [fetchWorkspaces]);\n\n return {\n workspaces,\n loading,\n error,\n refetch: fetchWorkspaces,\n };\n}\n\ninterface InstallableVersion {\n version: string;\n status: string;\n isLatest: boolean;\n}\n\n/**\n * Hook to fetch an app's installable (published/deprecated) versions, for\n * the optional version picker. Skipped entirely when there's no linked\n * devportal applicationId (e.g. sideloaded or legacy listings) — those\n * always install whatever the store listing's current version is.\n */\nfunction useInstallableVersions(\n applicationId: string | undefined,\n skip: boolean\n): { versions: InstallableVersion[]; loading: boolean } {\n const { authToken } = useStore();\n const [versions, setVersions] = useState<InstallableVersion[]>([]);\n const [loading, setLoading] = useState(false);\n\n useEffect(() => {\n if (skip || !applicationId) {\n setVersions([]);\n return;\n }\n\n let cancelled = false;\n setLoading(true);\n\n graphqlFetch<{ installableApplicationVersions: InstallableVersion[] }>({\n gateway: 'global',\n query: INSTALLABLE_APPLICATION_VERSIONS_STRING,\n variables: { applicationId },\n authToken: authToken || undefined,\n })\n .then((result) => {\n if (cancelled) return;\n if (result.errors?.length) {\n setVersions([]);\n return;\n }\n setVersions(result.data?.installableApplicationVersions ?? []);\n })\n .catch(() => {\n if (!cancelled) setVersions([]);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n\n return () => {\n cancelled = true;\n };\n }, [applicationId, authToken, skip]);\n\n return { versions, loading };\n}\n\n/**\n * InstallAppModal component\n *\n * Modal for selecting a workspace to install an app into\n */\nexport const InstallAppModal: FC<InstallAppModalProps> = ({\n isOpen,\n onClose,\n app,\n orderId,\n purchaseState = 'free',\n onInstallSuccess,\n onInstallError,\n}) => {\n const tr = useTr();\n const { authToken, user } = useStore();\n const [installState, dispatchInstall] = useReducer(installReducer, initialInstallState);\n const { step, selectedWorkspace, selectedVersion, installing, installError, installInfo } =\n installState;\n\n const appType = app.type?.toUpperCase() ?? '';\n const requiresConsent = CONSENT_REQUIRED_TYPES.has(appType);\n const supportsVersionChoice = STORE_DISTRIBUTED_TYPES.has(appType);\n\n const { workspaces, loading, error } = useWorkspaces(!isOpen);\n const { versions: installableVersions } = useInstallableVersions(\n app.applicationId,\n !isOpen || !supportsVersionChoice\n );\n\n // Reset step and selection whenever the modal is closed/reopened\n useEffect(() => {\n if (!isOpen) {\n dispatchInstall({ type: 'reset' });\n }\n }, [isOpen]);\n\n // Filter workspaces by status (only show active)\n const filteredWorkspaces = workspaces.filter((ws) => ws.status === 'ACTIVE');\n\n /**\n * Install purchased app via Store Service (NEW FLOW)\n * Used when installing from orders page or any already-purchased product\n */\n const handleInstallFromStore = async () => {\n if (!selectedWorkspace) return;\n\n const workspace = filteredWorkspaces.find((ws) => ws.id === selectedWorkspace);\n if (!workspace) return;\n const purchasedBy = user?.id;\n\n dispatchInstall({ type: 'startInstall' });\n\n try {\n if (!orderId && purchaseState !== 'purchased') {\n if (!purchasedBy) {\n throw new Error('Unable to identify the current user for license creation');\n }\n\n await graphqlFetch({\n gateway: 'global',\n query: CREATE_PRODUCT_ENTITLEMENT_STRING,\n variables: {\n input: {\n productId: app.id,\n purchasedBy,\n workspaceId: workspace.id,\n metadata: {\n source: 'store-ui',\n productType: app.type,\n },\n },\n },\n authToken: authToken || undefined,\n });\n }\n\n // Call Store Service to install the purchased app\n const result = await graphqlFetch<{\n installPurchasedApp: {\n success: boolean;\n alreadyInstalled?: boolean;\n installation?: {\n id: string;\n productId: string;\n workspaceId: string;\n status: string;\n version: string;\n };\n error?: string;\n message?: string;\n };\n }>({\n gateway: 'global', // Store Service\n query: INSTALL_PURCHASED_APP_STRING,\n variables: {\n orderId: orderId ?? null,\n productId: app.id,\n workspaceId: workspace.id,\n version: selectedVersion || undefined,\n },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Installation failed');\n }\n\n const installResult = result.data?.installPurchasedApp;\n\n if (installResult?.success) {\n if (installResult.alreadyInstalled) {\n dispatchInstall({\n type: 'setInfo',\n info: installResult.message || 'Already installed on this workspace',\n });\n } else {\n onInstallSuccess?.(workspace.id, workspace.name);\n onClose();\n }\n } else {\n const rawMsg = installResult?.error || 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n }\n } catch (err) {\n const rawMsg = err instanceof Error ? err.message : 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n } finally {\n dispatchInstall({ type: 'finishInstall' });\n }\n };\n\n /**\n * Install app via Workspace Service (LEGACY FLOW)\n * Used for free apps or when installing without order\n */\n const handleInstallViaWorkspace = async () => {\n if (!selectedWorkspace) return;\n\n const workspace = filteredWorkspaces.find((ws) => ws.id === selectedWorkspace);\n if (!workspace) return;\n\n dispatchInstall({ type: 'startInstall' });\n\n try {\n // Step 1: Install to workspace service via graphqlFetch\n const installVars = {\n workspaceIds: [workspace.id],\n input: {\n appName: app.slug || app.name.toLowerCase().replace(/\\s+/g, '-'),\n displayName: app.name,\n version: app.version || '1.0.0',\n type: app.type || 'app',\n manifest: {\n productId: app.id,\n icon: app.icon,\n },\n },\n };\n\n const result = await graphqlFetch<{\n installWorkspaceApps: InstallWorkspaceAppsResult;\n }>({\n gateway: 'workspace',\n query: INSTALL_WORKSPACE_APPS_STRING,\n variables: installVars,\n authToken: authToken || undefined,\n workspaceId: workspace.id,\n workspaceToken: true,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Installation failed');\n }\n\n const installResult = result.data?.installWorkspaceApps;\n if (installResult?.successCount && installResult.successCount > 0) {\n // Step 2: Record installation in store service (for tracking)\n try {\n await graphqlFetch({\n gateway: 'global',\n query: RECORD_APP_INSTALLATION_STRING,\n variables: {\n input: {\n productId: app.id,\n workspaceId: workspace.id,\n workspaceName: workspace.name,\n workspaceType: workspace.type,\n organizationId: workspace.organizationId,\n version: app.version || '1.0.0',\n manifest: {\n name: app.name,\n productId: app.id,\n icon: app.icon,\n slug: app.slug,\n type: app.type,\n },\n },\n },\n authToken: authToken || undefined,\n });\n } catch {\n // Non-fatal: workspace installation succeeded; store record will sync later.\n }\n\n onInstallSuccess?.(workspace.id, workspace.name);\n onClose();\n } else {\n const rawMsg = installResult?.results?.[0]?.error || 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n }\n } catch (err) {\n const rawMsg = err instanceof Error ? err.message : 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n } finally {\n dispatchInstall({ type: 'finishInstall' });\n }\n };\n\n /**\n * Advance from workspace selection to consent step (for integration types),\n * or go straight to install (for other types).\n */\n const handleContinue = () => {\n if (requiresConsent) {\n dispatchInstall({ type: 'advanceToConsent' });\n } else {\n void handleInstall();\n }\n };\n\n /**\n * Install INTEGRATION / API_INTEGRATION apps via the devportal installApp mutation.\n * After a successful install, redirects the user to the app's callback URL with\n * ?installation_id=<id> appended so the integration server can complete the handshake.\n */\n const handleInstallIntegration = async () => {\n if (!selectedWorkspace) return;\n const workspace = filteredWorkspaces.find((ws) => ws.id === selectedWorkspace);\n if (!workspace) return;\n\n dispatchInstall({ type: 'startInstall' });\n try {\n const result = await graphqlFetch<{\n installApp: {\n id: string;\n status: string;\n workspaceId: string;\n application: { oauthAuthUrl?: string | null };\n };\n }>({\n gateway: 'global',\n query: INSTALL_APP_DEVPORTAL_STRING,\n variables: {\n input: {\n applicationId: app.applicationId ?? app.id,\n scope: 'WORKSPACE',\n workspaceId: workspace.id,\n consentedPermissions: app.permissions ?? [],\n },\n },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Installation failed');\n }\n\n const installResult = result.data?.installApp;\n if (!installResult) throw new Error('Installation failed');\n\n // Record the integration install in the store service so the product page\n // shows \"Installed\" and it appears in the Installed Apps management page.\n try {\n await graphqlFetch({\n gateway: 'global',\n query: RECORD_APP_INSTALLATION_STRING,\n variables: {\n input: {\n productId: app.id,\n workspaceId: workspace.id,\n workspaceName: workspace.name,\n workspaceType: workspace.type,\n organizationId: workspace.organizationId,\n version: app.version || '1.0.0',\n manifest: {\n name: app.name,\n applicationId: app.applicationId,\n productId: app.id,\n icon: app.icon,\n slug: app.slug,\n type: app.type,\n installationId: installResult.id,\n },\n },\n },\n authToken: authToken || undefined,\n });\n } catch {\n // Non-fatal — devportal installation succeeded; store record will sync later.\n }\n\n const callbackUrl = installResult.application?.oauthAuthUrl;\n if (callbackUrl) {\n window.open(`${callbackUrl}?installation_id=${installResult.id}`, '_blank');\n }\n\n onInstallSuccess?.(workspace.id, workspace.name);\n onClose();\n } catch (err) {\n const rawMsg = err instanceof Error ? err.message : 'Installation failed';\n const errorMsg = mapInstallError(rawMsg);\n dispatchInstall({ type: 'setError', error: errorMsg });\n onInstallError?.(new Error(rawMsg));\n } finally {\n dispatchInstall({ type: 'finishInstall' });\n }\n };\n\n /**\n * Main install handler - routes to appropriate install function\n */\n const handleInstall = async () => {\n if (CONSENT_REQUIRED_TYPES.has(appType)) {\n // Integration apps use the devportal installApp mutation with post-install redirect.\n await handleInstallIntegration();\n } else if (purchaseState === 'purchased' || orderId || STORE_DISTRIBUTED_TYPES.has(appType)) {\n // Purchased products install through the store service.\n await handleInstallFromStore();\n } else {\n // Free products install through the workspace service.\n await handleInstallViaWorkspace();\n }\n };\n\n const selectedWorkspaceObj = filteredWorkspaces.find((ws) => ws.id === selectedWorkspace);\n\n return (\n <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>\n <DialogContent className=\"mx-4 w-full max-w-lg gap-0 p-0\">\n {/* Header */}\n <DialogHeader className=\"flex flex-row items-center justify-between space-y-0 border-b border-border-default px-6 py-4 text-left\">\n <div className=\"flex items-center gap-3\">\n {app.icon ? (\n <img src={app.icon} alt={app.name} className=\"size-10 rounded-lg object-cover\" />\n ) : (\n <div className=\"flex size-10 items-center justify-center rounded-lg bg-bg-sunken text-lg font-bold text-text-muted\">\n {app.name.charAt(0)}\n </div>\n )}\n <div>\n <DialogTitle className=\"text-lg font-semibold text-text-primary\">\n {step === 'consent'\n ? tr('installApp.reviewPermissions', 'Review Permissions')\n : tr('installApp.title', 'Install App')}\n </DialogTitle>\n <DialogDescription className=\"text-sm text-text-muted\">{app.name}</DialogDescription>\n </div>\n </div>\n {/* Step indicator for consent-gated types */}\n {requiresConsent && (\n <div className=\"flex items-center gap-1.5 text-xs text-text-muted shrink-0\">\n <span className={step === 'workspace' ? 'font-semibold text-text-primary' : ''}>\n 1\n </span>\n <ChevronRight className=\"size-3\" />\n <span className={step === 'consent' ? 'font-semibold text-text-primary' : ''}>2</span>\n </div>\n )}\n </DialogHeader>\n\n {/* ── Step 1: Workspace selection ── */}\n {step === 'workspace' && (\n <div className=\"px-6 py-4\">\n <p className=\"mb-4 text-text-secondary\">\n {tr(\n 'installApp.selectWorkspace',\n 'Select the workspace where you want to install this app:'\n )}\n </p>\n\n {/* Workspace List */}\n <div className=\"max-h-72 overflow-y-auto rounded-lg border border-border-seam\">\n {loading ? (\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"size-8 animate-spin text-text-muted\" />\n </div>\n ) : error ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <AlertCircle className=\"mb-2 size-8 text-status-error-text\" />\n <p className=\"text-sm text-status-error-text\">\n {tr('installApp.failedToLoadWorkspaces', 'Failed to load workspaces')}\n </p>\n <p className=\"text-xs text-text-muted mt-1\">{error.message}</p>\n </div>\n ) : filteredWorkspaces.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <Building2 className=\"mb-2 size-8 text-text-muted\" />\n <p className=\"text-sm text-text-muted\">\n {tr('installApp.noWorkspaces', 'No workspaces available')}\n </p>\n </div>\n ) : (\n <div className=\"divide-y divide-border-default\">\n {filteredWorkspaces.map((workspace) => (\n <button\n type=\"button\"\n key={workspace.id}\n onClick={() =>\n dispatchInstall({ type: 'selectWorkspace', workspaceId: workspace.id })\n }\n className={`w-full cursor-pointer flex items-center gap-3 px-4 py-3 text-left transition-colors ${\n selectedWorkspace === workspace.id\n ? 'bg-action-primary-bg/10'\n : 'hover:bg-bg-sunken'\n }`}\n >\n <div className=\"flex size-10 items-center justify-center rounded-lg bg-action-primary-bg/10 text-text-link\">\n <Building2 className=\"size-5\" />\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">{workspace.name}</p>\n {workspace.slug ? (\n <p className=\"text-xs text-text-muted truncate\">/{workspace.slug}</p>\n ) : null}\n </div>\n {selectedWorkspace === workspace.id && (\n <CheckCircle className=\"size-5 flex-shrink-0 text-action-primary-bg\" />\n )}\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Optional version picker — only shown when the app has more\n than one installable (published/deprecated) version. */}\n {supportsVersionChoice && installableVersions.length > 1 && (\n <div className=\"mt-4\">\n <label\n htmlFor=\"install-app-version\"\n className=\"mb-1.5 block text-xs font-medium text-text-secondary\"\n >\n {tr('installApp.version', 'Version')}\n </label>\n <select\n id=\"install-app-version\"\n value={selectedVersion}\n onChange={(event) =>\n dispatchInstall({ type: 'selectVersion', version: event.target.value })\n }\n className=\"w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary scheme-normal dark:scheme-dark focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] [&_option]:bg-bg-surface [&_option]:text-text-primary\"\n >\n <option value=\"\">\n {app.version\n ? tr('installApp.latestWithVersion', 'Latest ({{version}})', {\n version: app.version,\n })\n : tr('installApp.latest', 'Latest')}\n </option>\n {installableVersions\n .filter((v) => !v.isLatest)\n .map((v) => (\n <option key={v.version} value={v.version}>\n {tr('installApp.versionWithStatus', '{{version}} ({{status}})', {\n version: v.version,\n status: v.status,\n })}\n </option>\n ))}\n </select>\n </div>\n )}\n </div>\n )}\n\n {/* ── Step 2: Permissions consent (integration types only) ── */}\n {step === 'consent' && (\n <div className=\"px-6 py-4\">\n {/* Selected workspace reminder */}\n {selectedWorkspaceObj && (\n <div className=\"mb-4 flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\">\n <Building2 className=\"size-4 shrink-0 text-text-muted\" />\n <span className=\"text-sm text-text-secondary\">\n {tr('installApp.installingInto', 'Installing into')}{' '}\n <span className=\"font-medium text-text-primary\">{selectedWorkspaceObj.name}</span>\n </span>\n </div>\n )}\n\n {/* Shield header */}\n <div className=\"mb-4 flex items-center gap-2\">\n <ShieldCheck className=\"size-5 text-status-success-text\" />\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('installApp.permissionsRequested', 'Permissions requested')}\n </p>\n </div>\n\n <p className=\"mb-4 text-sm text-text-secondary\">\n {tr(\n 'installApp.consentDescription',\n 'By clicking {{button}}, you grant {{appName}} access to the following in the selected workspace:',\n {\n button: tr('installApp.acceptAndInstall', 'Accept & Install'),\n appName: app.name,\n }\n )}\n </p>\n\n {/* Permission list — specific scopes if available, generic grant otherwise */}\n <div className=\"rounded-lg border border-border-seam bg-bg-surface px-4 py-3\">\n {app.permissions && app.permissions.length > 0 ? (\n <ul className=\"space-y-2\">\n {app.permissions.map((perm) => (\n <li key={perm} className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <CheckCircle className=\"size-4 shrink-0 text-status-success-text\" />\n {perm}\n </li>\n ))}\n </ul>\n ) : (\n <ul className=\"space-y-2\">\n <li className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <CheckCircle className=\"size-4 shrink-0 text-status-success-text\" />\n {tr(\n 'installApp.permissionReadWrite',\n 'Read and write data within this workspace on your behalf'\n )}\n </li>\n <li className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <CheckCircle className=\"size-4 shrink-0 text-status-success-text\" />\n {tr(\n 'installApp.permissionWebhooks',\n 'Receive webhook events for workspace activity'\n )}\n </li>\n <li className=\"flex items-center gap-2.5 text-sm text-text-primary\">\n <CheckCircle className=\"size-4 shrink-0 text-status-success-text\" />\n {tr(\n 'installApp.permissionAuthorized',\n 'Act as an authorized integration within this workspace'\n )}\n </li>\n </ul>\n )}\n </div>\n\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'installApp.revokeHint',\n 'You can revoke access at any time from your Installed Apps settings.'\n )}\n </p>\n </div>\n )}\n\n {/* Footer */}\n <DialogFooter className=\"block border-t border-border-default px-6 py-4 sm:flex-col sm:items-stretch sm:space-x-0\">\n {installInfo && (\n <div className=\"mb-3 flex items-center gap-2 rounded-lg bg-status-info-bg-subtle px-3 py-2 text-sm text-status-info-text\">\n <Info className=\"size-4 flex-shrink-0\" />\n <span>{installInfo}</span>\n </div>\n )}\n {installError && (\n <div className=\"mb-3 flex items-center gap-2 rounded-lg bg-status-error-bg-subtle/10 px-3 py-2 text-sm text-status-error-text\">\n <AlertCircle className=\"size-4 flex-shrink-0\" />\n <span>{installError}</span>\n </div>\n )}\n <div className=\"flex items-center justify-end gap-3\">\n {step === 'consent' ? (\n <>\n <button\n type=\"button\"\n onClick={() => dispatchInstall({ type: 'backToWorkspace' })}\n disabled={installing}\n className=\"cursor-pointer inline-flex items-center gap-1.5 rounded-lg border border-border-default px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('common.back', 'Back')}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleInstall()}\n disabled={installing}\n className=\"cursor-pointer rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2\"\n >\n {installing ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('installApp.installing', 'Installing…')}\n </>\n ) : (\n tr('installApp.acceptAndInstall', 'Accept & Install')\n )}\n </button>\n </>\n ) : (\n <>\n <button\n type=\"button\"\n onClick={onClose}\n disabled={installing}\n className=\"cursor-pointer rounded-lg border border-border-default px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleContinue}\n disabled={!selectedWorkspace || installing}\n className=\"cursor-pointer rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2\"\n >\n {installing ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('installApp.installing', 'Installing…')}\n </>\n ) : requiresConsent ? (\n <>\n {tr('common.continue', 'Continue')}\n <ChevronRight className=\"size-4\" />\n </>\n ) : (\n tr('installApp.installApp', 'Install App')\n )}\n </button>\n </>\n )}\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n};\n"],"mappings":"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"}
|
|
@@ -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":";;;;;;;;;AA0DA,IAAM,IAAqE;CACzE;EAAE,OAAO;EAAI,OAAO;EAAgB;CACpC;EAAE,OAAO;EAAW,OAAO;EAAW;CACtC;EAAE,OAAO;EAAa,OAAO;EAAa;CAC1C;EAAE,OAAO;EAAU,OAAO;EAAU;CACpC;EAAE,OAAO;EAAY,OAAO;EAAY;CACzC,EAEK,IAAwD;CAC5D,SAAS;CACT,YAAY;CACZ,eAAe;CACf,SAAS;CACT,YAAY;CACZ,kBAAkB;CAClB,WAAW;CACX,QAAQ;CACR,UAAU;CACV,WAAW;CACZ,EAEK,IAAuD;CAC3D,cAAc;CACd,gBAAgB;CAChB,eAAe;CACf,OAAO;CACR,EAEK,IAA4C,6lBA8B5C,IAA6C,+HAS7C,IAAY;AAElB,SAAS,EAAW,GAAqB;AACvC,QAAO,IAAI,KAAK,EAAI,CAAC,mBAAmB,SAAS;EAC/C,MAAM;EACN,OAAO;EACP,KAAK;EACL,MAAM;EACN,QAAQ;EACT,CAAC;;AAUJ,IAAa,UAA4B;CACvC,IAAM,IAAW,GAAa,EACxB,EAAE,cAAW,gBAAa,GAAU,EAEpC,CAAC,GAAQ,KAAa,EAAuB,EAAE,CAAC,EAChD,CAAC,GAAO,KAAY,EAAS,EAAE,EAC/B,CAAC,GAAS,KAAc,EAAS,GAAM,EACvC,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAM,KAAW,EAAS,EAAE,EAC7B,CAAC,GAAc,KAAmB,EAA2B,GAAG,EAChE,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAiB,KAAsB,EAAwB,KAAK,EAErE,IAAO,EAAY,YAAY;AAEnC,EADA,EAAW,GAAK,EAChB,EAAS,KAAK;AACd,MAAI;GACF,IAAM,IAAS,MAAM,EAElB;IACD,SAAS;IACT,OAAO;IACP,WAAW;KACT,MAAM,IAAO;KACb,OAAO;KACP,QAAQ,KAAgB,KAAA;KACxB,QAAQ,EAAO,MAAM,IAAI,KAAA;KAC1B;IACD,WAAW,KAAa,KAAA;IACzB,CAAC;AACF,OAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,IAAI,WAAW,wBAAwB;AAGvE,GADA,EAAS,EAAO,MAAM,oBAAoB,SAAS,EAAE,EACrD,EAAU,EAAO,MAAM,oBAAoB,UAAU,EAAE,CAAC;WACjD,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,wBAAwB;YAC9D;AACR,KAAW,GAAM;;IAElB;EAAC;EAAW;EAAM;EAAc;EAAO,CAAC;AAE3C,SAAgB;AACT,KAAM;IACV,CAAC,EAAK,CAAC;CAEV,IAAM,IAAc,OAAO,MAAmB;AAC5C,IAAmB,EAAO;AAC1B,MAAI;AAOF,GANA,MAAM,EAAa;IACjB,SAAS;IACT,OAAO;IACP,WAAW,EAAE,IAAI,GAAQ;IACzB,WAAW,KAAa,KAAA;IACzB,CAAC,EACF,MAAM,GAAM;WACL,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,0BAA0B;YAChE;AACR,KAAmB,KAAK;;;AAI5B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GAAQ,WAAU;aAChB,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAS,GAAG,EAAS,YAAY;OAChD,WAAU;iBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAAA,OAEzB;UACT,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD,EAAS,WAAU,4BAA6B,CAAA;QAChD,kBAAC,MAAD;SAAI,WAAU;mBAA0C;SAAoB,CAAA;QAC5E,kBAAC,QAAD;SAAM,WAAU;mBACb;SACI,CAAA;QACH;SACF;SACN,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,KAAK,GAAM;MAC1B,WAAU;MACV,UAAU;gBAJZ,CAME,kBAAC,GAAD,EAAW,WAAW,UAAU,IAAU,iBAAiB,MAAQ,CAAA,EAAA,UAE5D;QACL;;IACF,CAAA;GACC,CAAA,EAET,kBAAC,QAAD;GAAM,WAAU;aAAhB;IACE,kBAAC,GAAD;KAAa,WAAU;eAAO;KAIhB,CAAA;IAEd,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,cAAW;MACX,OAAO;MACP,WAAW,MAAM;AAEf,OADA,EAAgB,EAAE,OAAO,MAA0B,EACnD,EAAQ,EAAE;;MAEZ,WAAU;gBAET,EAAqB,KAAK,MACzB,kBAAC,UAAD;OAAwB,OAAO,EAAI;iBAChC,EAAI;OACE,EAFI,EAAI,MAER,CACT;MACK,CAAA,EACT,kBAAC,SAAD;MACE,MAAK;MACL,cAAW;MACX,aAAY;MACZ,OAAO;MACP,WAAW,MAAM;AAEf,OADA,EAAU,EAAE,OAAO,MAAM,EACzB,EAAQ,EAAE;;MAEZ,WAAU;MACV,CAAA,CACE;;IAEL,KACC,kBAAC,OAAD;KACE,MAAK;KACL,WAAU;eAET;KACG,CAAA;IAGP,KAAW,EAAO,WAAW,IAC5B,kBAAC,UAAD;KAAQ,WAAU;eAA8C;KAAwB,CAAA,GACtF,EAAO,WAAW,IACpB,kBAAC,GAAD;KACE,cAAa;KACb,OAAM;KACN,aAAY;KACZ,CAAA,GAEF,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,SAAD;OAAO,WAAU;iBACf,kBAAC,MAAD,EAAA,UAAA;QACE,kBAAC,MAAD;SAAI,WAAU;mBAAY;SAAU,CAAA;QACpC,kBAAC,MAAD;SAAI,WAAU;mBAAY;SAAU,CAAA;QACpC,kBAAC,MAAD;SAAI,WAAU;mBAAuB;SAAU,CAAA;QAC/C,kBAAC,MAAD;SAAI,WAAU;mBAAY;SAAW,CAAA;QACrC,kBAAC,MAAD;SAAI,WAAU;mBAAY;SAAgB,CAAA;QAC1C,kBAAC,MAAD;SAAI,WAAU;mBAAY;SAAa,CAAA;QACvC,kBAAC,MAAD;SAAI,WAAU;mBAAY;SAAU,CAAA;QACpC,kBAAC,MAAD;SAAI,WAAU;mBAAY;SAAW,CAAA;QAClC,EAAA,CAAA;OACC,CAAA,EACR,kBAAC,SAAD;OAAO,WAAU;iBACd,EAAO,KAAK,MAAU;QACrB,IAAM,IAAW,EAAM,WAAW,MAAM,MAAM,EAAE,WAAW,OAAO;AAClE,eACE,kBAAC,MAAD;SAAmB,WAAU;mBAA7B;UACE,kBAAC,MAAD;WAAI,WAAU;qBACX,EAAM,GAAG,MAAM,GAAG,EAAE,CAAC,aAAa;WAChC,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBAAd,CACG,EAAM,OAAO,MAAM,GAAG,EAAE,EAAC,IACvB;;UACL,kBAAC,MAAD;WAAI,WAAU;qBACX,EAAY,EAAM,OAAO,EAAM,SAAS;WACtC,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBACZ,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAM;YACF,CAAA;WACJ,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBACZ,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACG,EAAM,cACL,kBAAC,QAAD;aACE,WAAW,gDAAgD,EAAmB,EAAM,YAAY;uBAE/F,EAAM,YAAY,OAAO,QAAQ,MAAM,IAAI;aACvC,CAAA,GAEP,kBAAC,QAAD;aACE,WAAU;aACV,OAAM;uBACP;aAEM,CAAA,EAET,kBAAC,UAAD;aACE,MAAK;aACL,OAAM;aACN,eACE,EAAS,GAAG,EAAS,4BAA4B,EAAM,KAAK;aAE9D,WAAU;uBAEV,kBAAC,GAAD,EAAY,WAAU,YAAa,CAAA;aAC5B,CAAA,CACL;;WACH,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBACX,EAAM,aAAa,iBAClB,kBAAC,KAAD;YACE,MAAM,EAAM,YAAY,eAAe;YACvC,QAAO;YACP,KAAI;YACJ,WAAU;sBAJZ,CAMG,EAAM,YAAY,WAAW,WAC9B,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA,CACjC;gBAEJ,kBAAC,QAAD;YAAM,WAAU;sBAAkB;YAAQ,CAAA;WAEzC,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBACX,IACC,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,QAAD;aACE,OAAO,EAAS,QAAQ,KAAA;aACxB,WAAU;uBAFZ,CAIE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,EAC1B,EAAsB,EAAS,UAC3B;gBACP,kBAAC,UAAD;aACE,MAAK;aACL,eAAe,KAAK,EAAY,EAAS,GAAG;aAC5C,UAAU,MAAoB,EAAS;aACvC,WAAU;uBAET,MAAoB,EAAS,KAC5B,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,GAE3C;aAEK,CAAA,CACL;gBAEN,kBAAC,QAAD;YAAM,WAAU;sBAAkB;YAAQ,CAAA;WAEzC,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBACX,EAAW,EAAM,UAAU;WACzB,CAAA;UACF;WAxFI,EAAM,GAwFV;SAEP;OACI,CAAA,CACF;;KACJ,CAAA,EAEL,IAAQ,KACP,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAAsC;OAC3B,IAAO,IAAY;OAAE;OAAE,KAAK,KAAK,IAAO,KAAK,GAAW,EAAM;OAAC;OAAI;OAC3E;OACI;SACP,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,MAAS;OACnB,eAAe,GAAS,MAAM,IAAI,EAAE;OACpC,WAAU;iBACX;OAEQ,CAAA,EACT,kBAAC,UAAD;OACE,MAAK;OACL,WAAW,IAAO,KAAK,KAAa;OACpC,eAAe,GAAS,MAAM,IAAI,EAAE;OACpC,WAAU;iBACX;OAEQ,CAAA,CACL;QACF;OAEP,EAAA,CAAA;IAEA;KACH"}
|
|
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",
|