@burdenoff/microfe-billing 2026.624.1 → 2026.624.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +125 -150
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js +146 -153
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js +164 -179
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/EarningsPage.js +229 -291
- package/dist/billing/modules/earnings/pages/EarningsPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +69 -74
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutsPage.js +142 -148
- package/dist/billing/modules/earnings/pages/PayoutsPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/SettingsPage.js +125 -117
- package/dist/billing/modules/settings/pages/SettingsPage.js.map +1 -1
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js +78 -66
- package/dist/billing/modules/settings/pages/TeamPermissionsPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js +68 -83
- package/dist/billing/modules/subscriptions/components/ManageSeatsModal.js.map +1 -1
- package/dist/billing/shared/ui/MetricCard.js +1 -1
- package/dist/billing/shared/ui/MetricCard.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamPermissionsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Team & Permissions Page\n *\n * Lets a Billing Admin manage billing-scope role assignments for a specific\n * billing account. Calls global-rbac-svc directly via the same gateway URL\n * used by the rest of the billing module.\n *\n * Scope: `scopeType: \"billing\"`, `scopeId: <billingAccountId>`\n * Roles cloned per billing account at creation time:\n * - Billing Admin (full access to this billing account)\n * - Billing Viewer (read-only access to this billing account)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, ActorIdentity, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useBillingAccounts } from '../../dashboard/hooks/useDashboard';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\nimport { useActorProfiles } from '../../../shared/hooks';\n\n// Basic UUID-v1-through-v5 shape — accept any 8-4-4-4-12 hex pattern.\n// We intentionally don't strictly validate the version nibble; the rbac\n// service is the authoritative ID validator.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Role {\n id: string;\n name: string;\n description?: string | null;\n priority: number;\n isSystem: boolean;\n}\n\ninterface ActorRoleAssignment {\n id: string;\n actorId: string;\n actorType: string;\n roleId: string;\n scopeId: string;\n isActive: boolean;\n expiresAt?: string | null;\n role?: Role;\n}\n\ninterface ActorRbacSummary {\n actorId: string;\n actorType: string;\n roleAssignments: ActorRoleAssignment[];\n effectivePermissionCount?: number;\n}\n\nconst ROLES_QUERY = `\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n`;\n\nconst ACTORS_QUERY = `\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n`;\n\nconst ASSIGN_ROLE_MUTATION = `\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n`;\n\nconst REMOVE_ROLE_MUTATION = `\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n`;\n\nexport const TeamPermissionsPage: FC = () => {\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Resolution priority: URL `?billingAccountId=` → persisted localStorage →\n // default/first available account. Pass `billingAccounts` so first-time\n // users with empty localStorage still land on the default account\n // (without it the hook's auto-select effect can't fire and the page\n // dead-ends on \"No billing account selected\").\n const { billingAccounts } = useBillingAccounts();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n\n // Share the resolved account with `useBillingPermissions` so its\n // billing-scope permissions fetch observes the same ID. Without this,\n // a first-time user (no URL param, empty localStorage) would see the\n // page resolve to the default account here while the permissions hook\n // still sat at null — billing-scope-only admins would see AccessDenied\n // until a navigation/reload propagated the localStorage write.\n const permissions = useBillingPermissions({ billingAccountIdOverride: billingAccountId });\n\n const [roles, setRoles] = useState<Role[]>([]);\n const [actors, setActors] = useState<ActorRbacSummary[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Add-member form\n const [newActorId, setNewActorId] = useState('');\n const [newRoleId, setNewRoleId] = useState('');\n const [isAdding, setIsAdding] = useState(false);\n\n const gqlPost = useCallback(\n async <T,>(query: string, variables: Record<string, unknown>): Promise<T> => {\n return directBillingGraphqlRequest<T>({\n apiGatewayUrl,\n authToken,\n orgId,\n query,\n variables,\n });\n },\n [apiGatewayUrl, authToken, orgId]\n );\n\n const loadData = useCallback(async () => {\n if (!billingAccountId) return;\n setIsLoading(true);\n setError(null);\n try {\n const [rolesResp, actorsResp] = await Promise.all([\n gqlPost<{ roles: Role[] }>(ROLES_QUERY, { scopeId: billingAccountId }),\n gqlPost<{ actors: ActorRbacSummary[] }>(ACTORS_QUERY, {\n scopeId: billingAccountId,\n }),\n ]);\n setRoles(rolesResp.roles ?? []);\n setActors(actorsResp.actors ?? []);\n // Default the role-picker to \"Billing Viewer\" if available, else first role\n const viewer = (rolesResp.roles ?? []).find((r) => /viewer/i.test(r.name));\n setNewRoleId(viewer?.id ?? rolesResp.roles?.[0]?.id ?? '');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load team data');\n } finally {\n setIsLoading(false);\n }\n }, [billingAccountId, gqlPost]);\n\n useEffect(() => {\n void loadData();\n }, [loadData]);\n\n const sortedActors = useMemo(\n () =>\n [...actors].sort((a, b) => {\n const priA = Math.max(...a.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n const priB = Math.max(...b.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n return priB - priA;\n }),\n [actors]\n );\n\n const profileInputs = useMemo(\n () =>\n sortedActors.map((a) => ({\n actorId: a.actorId,\n actorType: a.actorType,\n })),\n [sortedActors]\n );\n const profileMap = useActorProfiles(profileInputs);\n\n const billingAccountName = useMemo(() => {\n if (!billingAccountId) return undefined;\n return billingAccounts.find((acct) => acct.id === billingAccountId)?.name ?? undefined;\n }, [billingAccountId, billingAccounts]);\n\n const handleAddMember = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!billingAccountId || !newActorId.trim() || !newRoleId) return;\n const trimmedActorId = newActorId.trim();\n if (!UUID_RE.test(trimmedActorId)) {\n setError('User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.');\n return;\n }\n setIsAdding(true);\n setError(null);\n try {\n await gqlPost(ASSIGN_ROLE_MUTATION, {\n input: {\n actorId: trimmedActorId,\n actorType: 'USER',\n roleId: newRoleId,\n scopeId: billingAccountId,\n },\n });\n setNewActorId('');\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to assign role');\n } finally {\n setIsAdding(false);\n }\n };\n\n const handleRemoveAssignment = async (assignmentId: string) => {\n if (!confirm('Remove this role assignment?')) return;\n setError(null);\n try {\n await gqlPost(REMOVE_ROLE_MUTATION, { assignmentId });\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove role');\n }\n };\n\n if (!permissions.canManageBillingAccount) {\n return (\n <AccessDenied message=\"You don't have permission to manage team and permissions for this billing account.\" />\n );\n }\n\n if (!billingAccountId) {\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Select a billing account to manage who can access it.\"\n />\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-6 text-sm text-text-secondary\">\n No billing account selected. Pick one from the billing account selector to manage its\n members and roles.\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Control who can manage this billing account and what they can do.\"\n />\n\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 text-sm\">\n <span className=\"font-medium text-text-primary\">Billing account:</span>{' '}\n <span className=\"text-text-primary\">{billingAccountName ?? 'Unnamed account'}</span>\n {billingAccountId ? (\n <details className=\"mt-2 text-xs text-text-secondary\">\n <summary className=\"cursor-pointer\">Account ID</summary>\n <span className=\"mt-1 inline-block font-mono\">{billingAccountId}</span>\n </details>\n ) : null}\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-status-error-border/30 bg-status-error-bg/5 p-4 text-sm text-status-error-text\">\n {error}\n </div>\n )}\n\n {/* Add member form */}\n <form\n onSubmit={(e) => void handleAddMember(e)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-text-primary\">Add member</h3>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-3\">\n <div className=\"md:col-span-2\">\n <label htmlFor=\"newActorId\" className=\"block text-xs font-medium text-text-secondary\">\n User ID\n </label>\n <input\n id=\"newActorId\"\n type=\"text\"\n value={newActorId}\n onChange={(e) => setNewActorId(e.target.value)}\n placeholder=\"Paste the user ID from their profile page\"\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n required\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n Find this on the user's profile page.\n </p>\n </div>\n <div>\n <label htmlFor=\"newRoleId\" className=\"block text-xs font-medium text-text-secondary\">\n Role\n </label>\n <select\n id=\"newRoleId\"\n value={newRoleId}\n onChange={(e) => setNewRoleId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n required\n >\n {roles.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={isAdding || !newActorId.trim() || !newRoleId}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text disabled:opacity-50\"\n >\n {isAdding ? 'Adding…' : 'Add member'}\n </button>\n </form>\n\n {/* Members table */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"border-b border-border-subtle px-4 py-3\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Members</h3>\n <p className=\"text-xs text-text-secondary\">\n Each member's role applies only to this billing account.\n </p>\n </div>\n {isLoading ? (\n <div className=\"p-6 text-sm text-text-secondary\">Loading…</div>\n ) : sortedActors.length === 0 ? (\n <div className=\"p-6 text-sm text-text-secondary\">No members assigned yet.</div>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle text-left text-xs text-text-secondary\">\n <th className=\"px-4 py-2\">Member</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Roles</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {sortedActors.map((actor) => (\n <tr key={actor.actorId} className=\"border-b border-border-subtle last:border-0\">\n <td className=\"px-4 py-3\">\n <ActorIdentity\n profile={\n profileMap[actor.actorId] ?? {\n actorId: actor.actorId,\n actorType: actor.actorType,\n displayName:\n actor.actorType.toLowerCase() === 'user'\n ? `User ${actor.actorId.slice(0, 8)}…`\n : actor.actorType,\n secondaryLabel: actor.actorId.slice(0, 12),\n resolved: false,\n }\n }\n variant=\"cell\"\n />\n </td>\n <td className=\"px-4 py-3 text-xs capitalize text-text-secondary\">\n {actor.actorType.toLowerCase()}\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {actor.roleAssignments\n .filter((ra) => ra.isActive)\n .map((ra) => (\n <span\n key={ra.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border-subtle bg-bg-sunken px-2 py-0.5 text-xs\"\n >\n {ra.role?.name ?? ra.roleId}\n <button\n type=\"button\"\n onClick={() => void handleRemoveAssignment(ra.id)}\n className=\"text-text-secondary hover:text-status-error-text\"\n aria-label={`Remove ${ra.role?.name ?? 'role'}`}\n >\n ×\n </button>\n </span>\n ))}\n </div>\n </td>\n <td className=\"px-4 py-3 text-right\">\n <span className=\"text-xs text-text-secondary\">\n {actor.effectivePermissionCount ?? '—'} permissions\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {/* Roles reference */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Available roles</h3>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Roles are cloned from the system templates when a billing account is created.\n </p>\n <ul className=\"mt-3 space-y-2\">\n {roles.map((r) => (\n <li\n key={r.id}\n className=\"flex items-start gap-3 rounded-md border border-border-subtle bg-bg-surface p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-text-primary\">{r.name}</div>\n {r.description && (\n <p className=\"mt-1 text-xs text-text-secondary\">{r.description}</p>\n )}\n </div>\n <span className=\"text-xs text-text-secondary\">priority {r.priority}</span>\n </li>\n ))}\n </ul>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TeamPermissionsPage.js","names":[],"sources":["../../../../../src/billing/modules/settings/pages/TeamPermissionsPage.tsx"],"sourcesContent":["/**\n * Settings Module - Team & Permissions Page\n *\n * Lets a Billing Admin manage billing-scope role assignments for a specific\n * billing account. Calls global-rbac-svc directly via the same gateway URL\n * used by the rest of the billing module.\n *\n * Scope: `scopeType: \"billing\"`, `scopeId: <billingAccountId>`\n * Roles cloned per billing account at creation time:\n * - Billing Admin (full access to this billing account)\n * - Billing Viewer (read-only access to this billing account)\n */\nimport { useCallback, useEffect, useMemo, useState, type FC } from 'react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { AccessDenied, ActorIdentity, PageHeader } from '../../../shared/components';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingAccountSelection } from '../../../hooks/useBillingAccountSelection';\nimport { useBillingAccounts } from '../../dashboard/hooks/useDashboard';\nimport { directBillingGraphqlRequest } from '../../../shared/utils';\nimport { useActorProfiles } from '../../../shared/hooks';\n\n// Basic UUID-v1-through-v5 shape — accept any 8-4-4-4-12 hex pattern.\n// We intentionally don't strictly validate the version nibble; the rbac\n// service is the authoritative ID validator.\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Role {\n id: string;\n name: string;\n description?: string | null;\n priority: number;\n isSystem: boolean;\n}\n\ninterface ActorRoleAssignment {\n id: string;\n actorId: string;\n actorType: string;\n roleId: string;\n scopeId: string;\n isActive: boolean;\n expiresAt?: string | null;\n role?: Role;\n}\n\ninterface ActorRbacSummary {\n actorId: string;\n actorType: string;\n roleAssignments: ActorRoleAssignment[];\n effectivePermissionCount?: number;\n}\n\nconst ROLES_QUERY = `\n query BillingScopeRoles($scopeId: String!) {\n roles(scopeId: $scopeId) {\n id\n name\n description\n priority\n isSystem\n }\n }\n`;\n\nconst ACTORS_QUERY = `\n query BillingScopeActors($scopeId: String!) {\n actors(scopeId: $scopeId) {\n actorId\n actorType\n roleAssignments {\n id\n actorId\n actorType\n roleId\n scopeId\n isActive\n expiresAt\n role { id name priority }\n }\n }\n }\n`;\n\nconst ASSIGN_ROLE_MUTATION = `\n mutation AssignBillingRole($input: AssignRoleInput!) {\n assignRoleToActor(input: $input) {\n id\n actorId\n roleId\n scopeId\n }\n }\n`;\n\nconst REMOVE_ROLE_MUTATION = `\n mutation RemoveBillingRole($assignmentId: String!) {\n removeRoleFromActor(assignmentId: $assignmentId)\n }\n`;\n\nexport const TeamPermissionsPage: FC = () => {\n const { apiGatewayUrl, authToken, orgId } = useBilling();\n\n // Resolution priority: URL `?billingAccountId=` → persisted localStorage →\n // default/first available account. Pass `billingAccounts` so first-time\n // users with empty localStorage still land on the default account\n // (without it the hook's auto-select effect can't fire and the page\n // dead-ends on \"No billing account selected\").\n const { billingAccounts } = useBillingAccounts();\n const { selectedAccountId } = useBillingAccountSelection({\n orgId,\n billingAccounts,\n syncFromUrl: true,\n urlParamName: 'billingAccountId',\n });\n const billingAccountId = selectedAccountId ?? undefined;\n\n // Share the resolved account with `useBillingPermissions` so its\n // billing-scope permissions fetch observes the same ID. Without this,\n // a first-time user (no URL param, empty localStorage) would see the\n // page resolve to the default account here while the permissions hook\n // still sat at null — billing-scope-only admins would see AccessDenied\n // until a navigation/reload propagated the localStorage write.\n const permissions = useBillingPermissions({ billingAccountIdOverride: billingAccountId });\n\n const [roles, setRoles] = useState<Role[]>([]);\n const [actors, setActors] = useState<ActorRbacSummary[]>([]);\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Add-member form\n const [newActorId, setNewActorId] = useState('');\n const [newRoleId, setNewRoleId] = useState('');\n const [isAdding, setIsAdding] = useState(false);\n const [removeAssignmentId, setRemoveAssignmentId] = useState<string | null>(null);\n\n const gqlPost = useCallback(\n async <T,>(query: string, variables: Record<string, unknown>): Promise<T> => {\n return directBillingGraphqlRequest<T>({\n apiGatewayUrl,\n authToken,\n orgId,\n query,\n variables,\n });\n },\n [apiGatewayUrl, authToken, orgId]\n );\n\n const loadData = useCallback(async () => {\n if (!billingAccountId) return;\n setIsLoading(true);\n setError(null);\n try {\n const [rolesResp, actorsResp] = await Promise.all([\n gqlPost<{ roles: Role[] }>(ROLES_QUERY, { scopeId: billingAccountId }),\n gqlPost<{ actors: ActorRbacSummary[] }>(ACTORS_QUERY, {\n scopeId: billingAccountId,\n }),\n ]);\n setRoles(rolesResp.roles ?? []);\n setActors(actorsResp.actors ?? []);\n // Default the role-picker to \"Billing Viewer\" if available, else first role\n const viewer = (rolesResp.roles ?? []).find((r) => /viewer/i.test(r.name));\n setNewRoleId(viewer?.id ?? rolesResp.roles?.[0]?.id ?? '');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load team data');\n } finally {\n setIsLoading(false);\n }\n }, [billingAccountId, gqlPost]);\n\n useEffect(() => {\n void loadData();\n }, [loadData]);\n\n const sortedActors = useMemo(\n () =>\n [...actors].sort((a, b) => {\n const priA = Math.max(...a.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n const priB = Math.max(...b.roleAssignments.map((ra) => ra.role?.priority ?? 0), 0);\n return priB - priA;\n }),\n [actors]\n );\n\n const profileInputs = useMemo(\n () =>\n sortedActors.map((a) => ({\n actorId: a.actorId,\n actorType: a.actorType,\n })),\n [sortedActors]\n );\n const profileMap = useActorProfiles(profileInputs);\n\n const billingAccountName = useMemo(() => {\n if (!billingAccountId) return undefined;\n return billingAccounts.find((acct) => acct.id === billingAccountId)?.name ?? undefined;\n }, [billingAccountId, billingAccounts]);\n\n const handleAddMember = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!billingAccountId || !newActorId.trim() || !newRoleId) return;\n const trimmedActorId = newActorId.trim();\n if (!UUID_RE.test(trimmedActorId)) {\n setError('User ID must be a UUID (e.g. 1a2b3c4d-...). Get it from the user profile page.');\n return;\n }\n setIsAdding(true);\n setError(null);\n try {\n await gqlPost(ASSIGN_ROLE_MUTATION, {\n input: {\n actorId: trimmedActorId,\n actorType: 'USER',\n roleId: newRoleId,\n scopeId: billingAccountId,\n },\n });\n setNewActorId('');\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to assign role');\n } finally {\n setIsAdding(false);\n }\n };\n\n const handleRemoveAssignment = async (assignmentId: string) => {\n setError(null);\n try {\n await gqlPost(REMOVE_ROLE_MUTATION, { assignmentId });\n await loadData();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove role');\n }\n };\n\n if (!permissions.canManageBillingAccount) {\n return (\n <AccessDenied message=\"You don't have permission to manage team and permissions for this billing account.\" />\n );\n }\n\n if (!billingAccountId) {\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Select a billing account to manage who can access it.\"\n />\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-6 text-sm text-text-secondary\">\n No billing account selected. Pick one from the billing account selector to manage its\n members and roles.\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n <PageHeader\n title=\"Team & Permissions\"\n description=\"Control who can manage this billing account and what they can do.\"\n />\n\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 text-sm\">\n <span className=\"font-medium text-text-primary\">Billing account:</span>{' '}\n <span className=\"text-text-primary\">{billingAccountName ?? 'Unnamed account'}</span>\n {billingAccountId ? (\n <details className=\"mt-2 text-xs text-text-secondary\">\n <summary className=\"cursor-pointer\">Account ID</summary>\n <span className=\"mt-1 inline-block font-mono\">{billingAccountId}</span>\n </details>\n ) : null}\n </div>\n\n {error && (\n <div className=\"rounded-lg border border-status-error-border/30 bg-status-error-bg/5 p-4 text-sm text-status-error-text\">\n {error}\n </div>\n )}\n\n {/* Add member form */}\n <form\n onSubmit={(e) => void handleAddMember(e)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 space-y-3\"\n >\n <h3 className=\"text-sm font-semibold text-text-primary\">Add member</h3>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-3\">\n <div className=\"md:col-span-2\">\n <label htmlFor=\"newActorId\" className=\"block text-xs font-medium text-text-secondary\">\n User ID\n </label>\n <input\n id=\"newActorId\"\n type=\"text\"\n value={newActorId}\n onChange={(e) => setNewActorId(e.target.value)}\n placeholder=\"Paste the user ID from their profile page\"\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n required\n />\n <p className=\"mt-1 text-[11px] text-text-secondary\">\n Find this on the user's profile page.\n </p>\n </div>\n <div>\n <label htmlFor=\"newRoleId\" className=\"block text-xs font-medium text-text-secondary\">\n Role\n </label>\n <select\n id=\"newRoleId\"\n value={newRoleId}\n onChange={(e) => setNewRoleId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n required\n >\n {roles.map((r) => (\n <option key={r.id} value={r.id}>\n {r.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n <button\n type=\"submit\"\n disabled={isAdding || !newActorId.trim() || !newRoleId}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text disabled:opacity-50\"\n >\n {isAdding ? 'Adding…' : 'Add member'}\n </button>\n </form>\n\n {/* Members table */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"border-b border-border-subtle px-4 py-3\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Members</h3>\n <p className=\"text-xs text-text-secondary\">\n Each member's role applies only to this billing account.\n </p>\n </div>\n {isLoading ? (\n <div className=\"p-6 text-sm text-text-secondary\">Loading…</div>\n ) : sortedActors.length === 0 ? (\n <div className=\"p-6 text-sm text-text-secondary\">No members assigned yet.</div>\n ) : (\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle text-left text-xs text-text-secondary\">\n <th className=\"px-4 py-2\">Member</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Roles</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {sortedActors.map((actor) => (\n <tr key={actor.actorId} className=\"border-b border-border-subtle last:border-0\">\n <td className=\"px-4 py-3\">\n <ActorIdentity\n profile={\n profileMap[actor.actorId] ?? {\n actorId: actor.actorId,\n actorType: actor.actorType,\n displayName:\n actor.actorType.toLowerCase() === 'user'\n ? `User ${actor.actorId.slice(0, 8)}…`\n : actor.actorType,\n secondaryLabel: actor.actorId.slice(0, 12),\n resolved: false,\n }\n }\n variant=\"cell\"\n />\n </td>\n <td className=\"px-4 py-3 text-xs capitalize text-text-secondary\">\n {actor.actorType.toLowerCase()}\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex flex-wrap gap-1\">\n {actor.roleAssignments\n .filter((ra) => ra.isActive)\n .map((ra) => (\n <span\n key={ra.id}\n className=\"inline-flex items-center gap-1 rounded-full border border-border-subtle bg-bg-sunken px-2 py-0.5 text-xs\"\n >\n {ra.role?.name ?? ra.roleId}\n <button\n type=\"button\"\n onClick={() => setRemoveAssignmentId(ra.id)}\n className=\"text-text-secondary hover:text-status-error-text\"\n aria-label={`Remove ${ra.role?.name ?? 'role'}`}\n >\n ×\n </button>\n </span>\n ))}\n </div>\n </td>\n <td className=\"px-4 py-3 text-right\">\n <span className=\"text-xs text-text-secondary\">\n {actor.effectivePermissionCount ?? '—'} permissions\n </span>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {/* Roles reference */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Available roles</h3>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Roles are cloned from the system templates when a billing account is created.\n </p>\n <ul className=\"mt-3 space-y-2\">\n {roles.map((r) => (\n <li\n key={r.id}\n className=\"flex items-start gap-3 rounded-md border border-border-subtle bg-bg-surface p-3\"\n >\n <div className=\"flex-1\">\n <div className=\"text-sm font-medium text-text-primary\">{r.name}</div>\n {r.description && (\n <p className=\"mt-1 text-xs text-text-secondary\">{r.description}</p>\n )}\n </div>\n <span className=\"text-xs text-text-secondary\">priority {r.priority}</span>\n </li>\n ))}\n </ul>\n </div>\n\n <AlertDialog\n open={removeAssignmentId !== null}\n onOpenChange={(open) => {\n if (!open) setRemoveAssignmentId(null);\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Remove role assignment?</AlertDialogTitle>\n <AlertDialogDescription>\n Remove this role assignment? The member will lose the access this role grants for\n this billing account.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={() => {\n const id = removeAssignmentId;\n setRemoveAssignmentId(null);\n if (id) void handleRemoveAssignment(id);\n }}\n >\n Remove\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -3,73 +3,61 @@ import { useEffect as t, useState as n } from "react";
|
|
|
3
3
|
import { AlertCircle as r, Loader2 as i, Minus as a, Plus as o, Users as s } from "lucide-react";
|
|
4
4
|
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
5
5
|
import { useI18n as u } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
6
|
+
import { ResponsiveDialog as d, ResponsiveDialogContent as f, ResponsiveDialogFooter as p, ResponsiveDialogHeader as m, ResponsiveDialogTitle as h } from "@burdenoff/fe-libs/ui";
|
|
6
7
|
//#region src/billing/modules/subscriptions/components/ManageSeatsModal.tsx
|
|
7
|
-
var
|
|
8
|
-
let { t:
|
|
9
|
-
let n =
|
|
8
|
+
var g = ({ subscription: g, onClose: _, onConfirm: v, isLoading: y = !1 }) => {
|
|
9
|
+
let { t: b } = u(), x = (e, t) => {
|
|
10
|
+
let n = b(e);
|
|
10
11
|
return n === e ? t : n;
|
|
11
|
-
},
|
|
12
|
+
}, S = g.plan, C = S?.minSeats ?? 1, w = S?.maxSeats ?? 999, T = S?.pricePerSeat ?? 0, E = S?.currency ?? "USD", D = g.seatCount ?? C, [O, k] = n(D), [A, j] = n(null);
|
|
12
13
|
t(() => {
|
|
13
|
-
|
|
14
|
-
}, [
|
|
15
|
-
let
|
|
16
|
-
function
|
|
17
|
-
|
|
14
|
+
k(D);
|
|
15
|
+
}, [D]);
|
|
16
|
+
let M = O - D, N = O * T, P = D * T, F = g.paymentGateway === "razorpay";
|
|
17
|
+
function I() {
|
|
18
|
+
k((e) => Math.max(C, e - 1)), j(null);
|
|
18
19
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
20
|
+
function L() {
|
|
21
|
+
k((e) => Math.min(w, e + 1)), j(null);
|
|
21
22
|
}
|
|
22
|
-
function
|
|
23
|
+
function R(e) {
|
|
23
24
|
let t = parseInt(e, 10);
|
|
24
|
-
isNaN(t) || (t <
|
|
25
|
+
isNaN(t) || (t < C ? (j(`Minimum ${C} seat${C === 1 ? "" : "s"} required for this plan.`), k(t)) : t > w ? (j(`Maximum ${w} seats allowed.`), k(t)) : (j(null), k(t)));
|
|
25
26
|
}
|
|
26
|
-
async function
|
|
27
|
-
if (!(
|
|
28
|
-
if (
|
|
29
|
-
|
|
27
|
+
async function z() {
|
|
28
|
+
if (!(O < C || O > w)) {
|
|
29
|
+
if (O === D) {
|
|
30
|
+
_();
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
j(null);
|
|
33
34
|
try {
|
|
34
|
-
await
|
|
35
|
+
await v(O), _();
|
|
35
36
|
} catch (e) {
|
|
36
|
-
|
|
37
|
+
j(e instanceof Error ? e.message : "Failed to update seats.");
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
return /* @__PURE__ */ c(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
return /* @__PURE__ */ c(d, {
|
|
42
|
+
open: !0,
|
|
43
|
+
onOpenChange: (e) => {
|
|
44
|
+
!e && !y && _();
|
|
45
|
+
},
|
|
46
|
+
children: /* @__PURE__ */ l(f, {
|
|
47
|
+
className: "sm:max-w-md",
|
|
47
48
|
children: [
|
|
49
|
+
/* @__PURE__ */ c(m, { children: /* @__PURE__ */ l(h, {
|
|
50
|
+
className: "flex items-center gap-2",
|
|
51
|
+
children: [/* @__PURE__ */ c(s, { className: "size-5 text-text-link" }), x("billing.subscriptions.manageSeats", "Manage Seats")]
|
|
52
|
+
}) }),
|
|
48
53
|
/* @__PURE__ */ l("div", {
|
|
49
|
-
className: "
|
|
50
|
-
children: [/* @__PURE__ */ l("div", {
|
|
51
|
-
className: "flex items-center gap-2",
|
|
52
|
-
children: [/* @__PURE__ */ c(s, { className: "size-5 text-text-link" }), /* @__PURE__ */ c("h2", {
|
|
53
|
-
id: "manage-seats-title",
|
|
54
|
-
className: "text-lg font-semibold text-text-primary",
|
|
55
|
-
children: g("billing.subscriptions.manageSeats", "Manage Seats")
|
|
56
|
-
})]
|
|
57
|
-
}), /* @__PURE__ */ c("button", {
|
|
58
|
-
type: "button",
|
|
59
|
-
onClick: f,
|
|
60
|
-
className: "text-text-secondary hover:text-text-primary transition-colors",
|
|
61
|
-
"aria-label": "Close manage seats dialog",
|
|
62
|
-
children: "✕"
|
|
63
|
-
})]
|
|
64
|
-
}),
|
|
65
|
-
/* @__PURE__ */ l("div", {
|
|
66
|
-
className: "p-6 space-y-6",
|
|
54
|
+
className: "space-y-6",
|
|
67
55
|
children: [
|
|
68
56
|
/* @__PURE__ */ l("div", {
|
|
69
57
|
className: "flex items-center justify-between text-sm text-text-secondary",
|
|
70
58
|
children: [/* @__PURE__ */ c("span", { children: "Current seats" }), /* @__PURE__ */ c("span", {
|
|
71
59
|
className: "font-medium text-text-primary",
|
|
72
|
-
children:
|
|
60
|
+
children: D
|
|
73
61
|
})]
|
|
74
62
|
}),
|
|
75
63
|
/* @__PURE__ */ l("div", {
|
|
@@ -82,91 +70,88 @@ var d = ({ subscription: d, onClose: f, onConfirm: p, isLoading: m = !1 }) => {
|
|
|
82
70
|
children: [
|
|
83
71
|
/* @__PURE__ */ c("button", {
|
|
84
72
|
type: "button",
|
|
85
|
-
onClick:
|
|
86
|
-
disabled:
|
|
73
|
+
onClick: I,
|
|
74
|
+
disabled: O <= C || y,
|
|
87
75
|
className: "size-9 rounded-button border border-border-subtle flex items-center justify-center hover:bg-action-ghost-bgHover disabled:opacity-40 disabled:cursor-not-allowed transition-colors",
|
|
88
76
|
"aria-label": "Decrease seat count",
|
|
89
77
|
children: /* @__PURE__ */ c(a, { className: "size-4" })
|
|
90
78
|
}),
|
|
91
79
|
/* @__PURE__ */ c("input", {
|
|
92
80
|
type: "number",
|
|
93
|
-
value:
|
|
94
|
-
min:
|
|
95
|
-
max:
|
|
96
|
-
onChange: (e) =>
|
|
97
|
-
disabled:
|
|
81
|
+
value: O,
|
|
82
|
+
min: C,
|
|
83
|
+
max: w,
|
|
84
|
+
onChange: (e) => R(e.target.value),
|
|
85
|
+
disabled: y,
|
|
98
86
|
className: "w-20 h-9 text-center border border-border-subtle rounded-button bg-bg-surface text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50"
|
|
99
87
|
}),
|
|
100
88
|
/* @__PURE__ */ c("button", {
|
|
101
89
|
type: "button",
|
|
102
|
-
onClick:
|
|
103
|
-
disabled:
|
|
90
|
+
onClick: L,
|
|
91
|
+
disabled: O >= w || y,
|
|
104
92
|
className: "size-9 rounded-button border border-border-subtle flex items-center justify-center hover:bg-action-ghost-bgHover disabled:opacity-40 disabled:cursor-not-allowed transition-colors",
|
|
105
93
|
"aria-label": "Increase seat count",
|
|
106
94
|
children: /* @__PURE__ */ c(o, { className: "size-4" })
|
|
107
95
|
}),
|
|
108
96
|
/* @__PURE__ */ c("span", {
|
|
109
97
|
className: "text-sm text-text-secondary",
|
|
110
|
-
children:
|
|
98
|
+
children: C === w ? `(${C} fixed)` : `(min ${C}${w < 999 ? `, max ${w}` : ""})`
|
|
111
99
|
})
|
|
112
100
|
]
|
|
113
101
|
})]
|
|
114
102
|
}),
|
|
115
|
-
|
|
103
|
+
T > 0 && /* @__PURE__ */ l("div", {
|
|
116
104
|
className: "rounded-lg bg-bg-sunken border border-border-subtle p-4 space-y-2 text-sm",
|
|
117
105
|
children: [
|
|
118
106
|
/* @__PURE__ */ l("div", {
|
|
119
107
|
className: "flex justify-between text-text-secondary",
|
|
120
|
-
children: [/* @__PURE__ */ c("span", { children: "Per seat" }), /* @__PURE__ */ l("span", { children: [e(
|
|
108
|
+
children: [/* @__PURE__ */ c("span", { children: "Per seat" }), /* @__PURE__ */ l("span", { children: [e(T, E), " / month"] })]
|
|
121
109
|
}),
|
|
122
110
|
/* @__PURE__ */ l("div", {
|
|
123
111
|
className: "flex justify-between text-text-secondary",
|
|
124
|
-
children: [/* @__PURE__ */ c("span", { children: "Current total" }), /* @__PURE__ */ l("span", { children: [e(
|
|
112
|
+
children: [/* @__PURE__ */ c("span", { children: "Current total" }), /* @__PURE__ */ l("span", { children: [e(P, E), " / month"] })]
|
|
125
113
|
}),
|
|
126
114
|
/* @__PURE__ */ l("div", {
|
|
127
115
|
className: "border-t border-border-subtle pt-2 flex justify-between font-medium text-text-primary",
|
|
128
116
|
children: [/* @__PURE__ */ c("span", { children: "New total" }), /* @__PURE__ */ l("span", {
|
|
129
|
-
className:
|
|
130
|
-
children: [e(
|
|
117
|
+
className: M > 0 ? "text-status-error-text" : M < 0 ? "text-status-success-text" : "",
|
|
118
|
+
children: [e(N, E), " / month"]
|
|
131
119
|
})]
|
|
132
120
|
}),
|
|
133
|
-
|
|
121
|
+
M !== 0 && /* @__PURE__ */ c("div", {
|
|
134
122
|
className: "text-xs text-text-secondary",
|
|
135
|
-
children:
|
|
123
|
+
children: M > 0 ? `+${M} seat${M === 1 ? "" : "s"} — +${e(M * T, E)}/month` : `${M} seat${Math.abs(M) === 1 ? "" : "s"} — ${e(M * T, E)}/month`
|
|
136
124
|
})
|
|
137
125
|
]
|
|
138
126
|
}),
|
|
139
|
-
|
|
127
|
+
F && M > 0 && /* @__PURE__ */ l("div", {
|
|
140
128
|
className: "flex gap-2 p-3 rounded-button bg-status-warning-bg-subtle border border-status-warning-border text-sm text-status-warning-text",
|
|
141
129
|
children: [/* @__PURE__ */ c(r, { className: "size-4 mt-0.5 shrink-0" }), /* @__PURE__ */ c("span", { children: "Seat additions apply from the next billing cycle. A separate prorated charge will be raised for the remaining days in the current cycle." })]
|
|
142
130
|
}),
|
|
143
|
-
|
|
131
|
+
A && /* @__PURE__ */ l("div", {
|
|
144
132
|
className: "flex gap-2 p-3 rounded-button bg-status-error-bg-subtle border border-status-error-border text-sm text-status-error-text",
|
|
145
|
-
children: [/* @__PURE__ */ c(r, { className: "size-4 mt-0.5 shrink-0" }), /* @__PURE__ */ c("span", { children:
|
|
133
|
+
children: [/* @__PURE__ */ c(r, { className: "size-4 mt-0.5 shrink-0" }), /* @__PURE__ */ c("span", { children: A })]
|
|
146
134
|
})
|
|
147
135
|
]
|
|
148
136
|
}),
|
|
149
|
-
/* @__PURE__ */ l("
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
children: [m && /* @__PURE__ */ c(i, { className: "size-4 animate-spin" }), C === S ? "No change" : D > 0 ? `Add ${D} seat${D === 1 ? "" : "s"}` : `Remove ${Math.abs(D)} seat${Math.abs(D) === 1 ? "" : "s"}`]
|
|
163
|
-
})]
|
|
164
|
-
})
|
|
137
|
+
/* @__PURE__ */ l(p, { children: [/* @__PURE__ */ c("button", {
|
|
138
|
+
type: "button",
|
|
139
|
+
onClick: _,
|
|
140
|
+
disabled: y,
|
|
141
|
+
className: "px-4 py-2 text-sm rounded-button border border-border-subtle hover:bg-action-ghost-bgHover text-text-primary transition-colors disabled:opacity-50",
|
|
142
|
+
children: "Cancel"
|
|
143
|
+
}), /* @__PURE__ */ l("button", {
|
|
144
|
+
type: "button",
|
|
145
|
+
onClick: z,
|
|
146
|
+
disabled: y || O === D || O < C || O > w,
|
|
147
|
+
className: "px-4 py-2 text-sm rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2",
|
|
148
|
+
children: [y && /* @__PURE__ */ c(i, { className: "size-4 animate-spin" }), O === D ? "No change" : M > 0 ? `Add ${M} seat${M === 1 ? "" : "s"}` : `Remove ${Math.abs(M)} seat${Math.abs(M) === 1 ? "" : "s"}`]
|
|
149
|
+
})] })
|
|
165
150
|
]
|
|
166
151
|
})
|
|
167
152
|
});
|
|
168
153
|
};
|
|
169
154
|
//#endregion
|
|
170
|
-
export {
|
|
155
|
+
export { g as ManageSeatsModal };
|
|
171
156
|
|
|
172
157
|
//# sourceMappingURL=ManageSeatsModal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ManageSeatsModal.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/components/ManageSeatsModal.tsx"],"sourcesContent":["/**\n * ManageSeatsModal\n * Lets users update the seat count on a PER_SEAT subscription.\n * Shows current seats, min/max bounds, and a proration notice for Razorpay.\n */\n\nimport { type FC, useState, useEffect } from 'react';\nimport { Minus, Plus, Users, AlertCircle, Loader2 } from 'lucide-react';\nimport { formatCurrency } from '../../../shared/utils/format';\nimport type { BillingSubscription } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ninterface ManageSeatsModalProps {\n subscription: BillingSubscription;\n onClose: () => void;\n onConfirm: (seatCount: number) => Promise<void>;\n isLoading?: boolean;\n}\n\nexport const ManageSeatsModal: FC<ManageSeatsModalProps> = ({\n subscription,\n onClose,\n onConfirm,\n isLoading = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const plan = subscription.plan;\n const minSeats = plan?.minSeats ?? 1;\n const maxSeats = plan?.maxSeats ?? 999;\n const pricePerSeat = plan?.pricePerSeat ?? 0;\n const currency = plan?.currency ?? 'USD';\n const currentSeats = subscription.seatCount ?? minSeats;\n\n const [newSeatCount, setNewSeatCount] = useState(currentSeats);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n setNewSeatCount(currentSeats);\n }, [currentSeats]);\n\n const seatDelta = newSeatCount - currentSeats;\n const newMonthlyTotal = newSeatCount * pricePerSeat;\n const currentMonthlyTotal = currentSeats * pricePerSeat;\n const isRazorpay = subscription.paymentGateway === 'razorpay';\n\n function decrement() {\n setNewSeatCount((n) => Math.max(minSeats, n - 1));\n setError(null);\n }\n\n function increment() {\n setNewSeatCount((n) => Math.min(maxSeats, n + 1));\n setError(null);\n }\n\n function handleInputChange(value: string) {\n const parsed = parseInt(value, 10);\n if (isNaN(parsed)) return;\n if (parsed < minSeats) {\n setError(`Minimum ${minSeats} seat${minSeats !== 1 ? 's' : ''} required for this plan.`);\n setNewSeatCount(parsed);\n } else if (parsed > maxSeats) {\n setError(`Maximum ${maxSeats} seats allowed.`);\n setNewSeatCount(parsed);\n } else {\n setError(null);\n setNewSeatCount(parsed);\n }\n }\n\n async function handleConfirm() {\n if (newSeatCount < minSeats || newSeatCount > maxSeats) return;\n if (newSeatCount === currentSeats) {\n onClose();\n return;\n }\n setError(null);\n try {\n await onConfirm(newSeatCount);\n onClose();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update seats.');\n }\n }\n\n return (\n <div className=\"fixed inset-0 bg-overlay-scrim flex items-center justify-center z-50 p-4\">\n <div\n className=\"bg-bg-surface border border-border-subtle rounded-card shadow-[var(--shadow-elevation-4)] w-full max-w-md\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"manage-seats-title\"\n >\n {/* Header */}\n <div className=\"flex items-center justify-between p-6 border-b border-border-subtle\">\n <div className=\"flex items-center gap-2\">\n <Users className=\"size-5 text-text-link\" />\n <h2 id=\"manage-seats-title\" className=\"text-lg font-semibold text-text-primary\">\n {tr('billing.subscriptions.manageSeats', 'Manage Seats')}\n </h2>\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"text-text-secondary hover:text-text-primary transition-colors\"\n aria-label=\"Close manage seats dialog\"\n >\n ✕\n </button>\n </div>\n\n {/* Body */}\n <div className=\"p-6 space-y-6\">\n {/* Current vs new */}\n <div className=\"flex items-center justify-between text-sm text-text-secondary\">\n <span>Current seats</span>\n <span className=\"font-medium text-text-primary\">{currentSeats}</span>\n </div>\n\n {/* Seat selector */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-text-primary\">New seat count</label>\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={decrement}\n disabled={newSeatCount <= minSeats || isLoading}\n className=\"size-9 rounded-button border border-border-subtle flex items-center justify-center hover:bg-action-ghost-bgHover disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n aria-label=\"Decrease seat count\"\n >\n <Minus className=\"size-4\" />\n </button>\n <input\n type=\"number\"\n value={newSeatCount}\n min={minSeats}\n max={maxSeats}\n onChange={(e) => handleInputChange(e.target.value)}\n disabled={isLoading}\n className=\"w-20 h-9 text-center border border-border-subtle rounded-button bg-bg-surface text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n />\n <button\n type=\"button\"\n onClick={increment}\n disabled={newSeatCount >= maxSeats || isLoading}\n className=\"size-9 rounded-button border border-border-subtle flex items-center justify-center hover:bg-action-ghost-bgHover disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n aria-label=\"Increase seat count\"\n >\n <Plus className=\"size-4\" />\n </button>\n <span className=\"text-sm text-text-secondary\">\n {minSeats === maxSeats\n ? `(${minSeats} fixed)`\n : `(min ${minSeats}${maxSeats < 999 ? `, max ${maxSeats}` : ''})`}\n </span>\n </div>\n </div>\n\n {/* Pricing breakdown */}\n {pricePerSeat > 0 && (\n <div className=\"rounded-lg bg-bg-sunken border border-border-subtle p-4 space-y-2 text-sm\">\n <div className=\"flex justify-between text-text-secondary\">\n <span>Per seat</span>\n <span>{formatCurrency(pricePerSeat, currency)} / month</span>\n </div>\n <div className=\"flex justify-between text-text-secondary\">\n <span>Current total</span>\n <span>{formatCurrency(currentMonthlyTotal, currency)} / month</span>\n </div>\n <div className=\"border-t border-border-subtle pt-2 flex justify-between font-medium text-text-primary\">\n <span>New total</span>\n <span\n className={\n seatDelta > 0\n ? 'text-status-error-text'\n : seatDelta < 0\n ? 'text-status-success-text'\n : ''\n }\n >\n {formatCurrency(newMonthlyTotal, currency)} / month\n </span>\n </div>\n {seatDelta !== 0 && (\n <div className=\"text-xs text-text-secondary\">\n {seatDelta > 0\n ? `+${seatDelta} seat${seatDelta !== 1 ? 's' : ''} — +${formatCurrency(seatDelta * pricePerSeat, currency)}/month`\n : `${seatDelta} seat${Math.abs(seatDelta) !== 1 ? 's' : ''} — ${formatCurrency(seatDelta * pricePerSeat, currency)}/month`}\n </div>\n )}\n </div>\n )}\n\n {/* Razorpay proration notice */}\n {isRazorpay && seatDelta > 0 && (\n <div className=\"flex gap-2 p-3 rounded-button bg-status-warning-bg-subtle border border-status-warning-border text-sm text-status-warning-text\">\n <AlertCircle className=\"size-4 mt-0.5 shrink-0\" />\n <span>\n Seat additions apply from the next billing cycle. A separate prorated charge will be\n raised for the remaining days in the current cycle.\n </span>\n </div>\n )}\n\n {/* Error */}\n {error && (\n <div className=\"flex gap-2 p-3 rounded-button bg-status-error-bg-subtle border border-status-error-border text-sm text-status-error-text\">\n <AlertCircle className=\"size-4 mt-0.5 shrink-0\" />\n <span>{error}</span>\n </div>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-end gap-3 p-6 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={onClose}\n disabled={isLoading}\n className=\"px-4 py-2 text-sm rounded-button border border-border-subtle hover:bg-action-ghost-bgHover text-text-primary transition-colors disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={\n isLoading ||\n newSeatCount === currentSeats ||\n newSeatCount < minSeats ||\n newSeatCount > maxSeats\n }\n className=\"px-4 py-2 text-sm rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2\"\n >\n {isLoading && <Loader2 className=\"size-4 animate-spin\" />}\n {newSeatCount === currentSeats\n ? 'No change'\n : seatDelta > 0\n ? `Add ${seatDelta} seat${seatDelta !== 1 ? 's' : ''}`\n : `Remove ${Math.abs(seatDelta)} seat${Math.abs(seatDelta) !== 1 ? 's' : ''}`}\n </button>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ManageSeatsModal.js","names":[],"sources":["../../../../../src/billing/modules/subscriptions/components/ManageSeatsModal.tsx"],"sourcesContent":["/**\n * ManageSeatsModal\n * Lets users update the seat count on a PER_SEAT subscription.\n * Shows current seats, min/max bounds, and a proration notice for Razorpay.\n */\n\nimport { type FC, useState, useEffect } from 'react';\nimport { Minus, Plus, Users, AlertCircle, Loader2 } from 'lucide-react';\nimport { formatCurrency } from '../../../shared/utils/format';\nimport type { BillingSubscription } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\ninterface ManageSeatsModalProps {\n subscription: BillingSubscription;\n onClose: () => void;\n onConfirm: (seatCount: number) => Promise<void>;\n isLoading?: boolean;\n}\n\nexport const ManageSeatsModal: FC<ManageSeatsModalProps> = ({\n subscription,\n onClose,\n onConfirm,\n isLoading = false,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const plan = subscription.plan;\n const minSeats = plan?.minSeats ?? 1;\n const maxSeats = plan?.maxSeats ?? 999;\n const pricePerSeat = plan?.pricePerSeat ?? 0;\n const currency = plan?.currency ?? 'USD';\n const currentSeats = subscription.seatCount ?? minSeats;\n\n const [newSeatCount, setNewSeatCount] = useState(currentSeats);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n setNewSeatCount(currentSeats);\n }, [currentSeats]);\n\n const seatDelta = newSeatCount - currentSeats;\n const newMonthlyTotal = newSeatCount * pricePerSeat;\n const currentMonthlyTotal = currentSeats * pricePerSeat;\n const isRazorpay = subscription.paymentGateway === 'razorpay';\n\n function decrement() {\n setNewSeatCount((n) => Math.max(minSeats, n - 1));\n setError(null);\n }\n\n function increment() {\n setNewSeatCount((n) => Math.min(maxSeats, n + 1));\n setError(null);\n }\n\n function handleInputChange(value: string) {\n const parsed = parseInt(value, 10);\n if (isNaN(parsed)) return;\n if (parsed < minSeats) {\n setError(`Minimum ${minSeats} seat${minSeats !== 1 ? 's' : ''} required for this plan.`);\n setNewSeatCount(parsed);\n } else if (parsed > maxSeats) {\n setError(`Maximum ${maxSeats} seats allowed.`);\n setNewSeatCount(parsed);\n } else {\n setError(null);\n setNewSeatCount(parsed);\n }\n }\n\n async function handleConfirm() {\n if (newSeatCount < minSeats || newSeatCount > maxSeats) return;\n if (newSeatCount === currentSeats) {\n onClose();\n return;\n }\n setError(null);\n try {\n await onConfirm(newSeatCount);\n onClose();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to update seats.');\n }\n }\n\n return (\n <ResponsiveDialog\n open\n onOpenChange={(open) => {\n if (!open && !isLoading) onClose();\n }}\n >\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n {/* Header */}\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle className=\"flex items-center gap-2\">\n <Users className=\"size-5 text-text-link\" />\n {tr('billing.subscriptions.manageSeats', 'Manage Seats')}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n\n {/* Body */}\n <div className=\"space-y-6\">\n {/* Current vs new */}\n <div className=\"flex items-center justify-between text-sm text-text-secondary\">\n <span>Current seats</span>\n <span className=\"font-medium text-text-primary\">{currentSeats}</span>\n </div>\n\n {/* Seat selector */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-text-primary\">New seat count</label>\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={decrement}\n disabled={newSeatCount <= minSeats || isLoading}\n className=\"size-9 rounded-button border border-border-subtle flex items-center justify-center hover:bg-action-ghost-bgHover disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n aria-label=\"Decrease seat count\"\n >\n <Minus className=\"size-4\" />\n </button>\n <input\n type=\"number\"\n value={newSeatCount}\n min={minSeats}\n max={maxSeats}\n onChange={(e) => handleInputChange(e.target.value)}\n disabled={isLoading}\n className=\"w-20 h-9 text-center border border-border-subtle rounded-button bg-bg-surface text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n />\n <button\n type=\"button\"\n onClick={increment}\n disabled={newSeatCount >= maxSeats || isLoading}\n className=\"size-9 rounded-button border border-border-subtle flex items-center justify-center hover:bg-action-ghost-bgHover disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n aria-label=\"Increase seat count\"\n >\n <Plus className=\"size-4\" />\n </button>\n <span className=\"text-sm text-text-secondary\">\n {minSeats === maxSeats\n ? `(${minSeats} fixed)`\n : `(min ${minSeats}${maxSeats < 999 ? `, max ${maxSeats}` : ''})`}\n </span>\n </div>\n </div>\n\n {/* Pricing breakdown */}\n {pricePerSeat > 0 && (\n <div className=\"rounded-lg bg-bg-sunken border border-border-subtle p-4 space-y-2 text-sm\">\n <div className=\"flex justify-between text-text-secondary\">\n <span>Per seat</span>\n <span>{formatCurrency(pricePerSeat, currency)} / month</span>\n </div>\n <div className=\"flex justify-between text-text-secondary\">\n <span>Current total</span>\n <span>{formatCurrency(currentMonthlyTotal, currency)} / month</span>\n </div>\n <div className=\"border-t border-border-subtle pt-2 flex justify-between font-medium text-text-primary\">\n <span>New total</span>\n <span\n className={\n seatDelta > 0\n ? 'text-status-error-text'\n : seatDelta < 0\n ? 'text-status-success-text'\n : ''\n }\n >\n {formatCurrency(newMonthlyTotal, currency)} / month\n </span>\n </div>\n {seatDelta !== 0 && (\n <div className=\"text-xs text-text-secondary\">\n {seatDelta > 0\n ? `+${seatDelta} seat${seatDelta !== 1 ? 's' : ''} — +${formatCurrency(seatDelta * pricePerSeat, currency)}/month`\n : `${seatDelta} seat${Math.abs(seatDelta) !== 1 ? 's' : ''} — ${formatCurrency(seatDelta * pricePerSeat, currency)}/month`}\n </div>\n )}\n </div>\n )}\n\n {/* Razorpay proration notice */}\n {isRazorpay && seatDelta > 0 && (\n <div className=\"flex gap-2 p-3 rounded-button bg-status-warning-bg-subtle border border-status-warning-border text-sm text-status-warning-text\">\n <AlertCircle className=\"size-4 mt-0.5 shrink-0\" />\n <span>\n Seat additions apply from the next billing cycle. A separate prorated charge will be\n raised for the remaining days in the current cycle.\n </span>\n </div>\n )}\n\n {/* Error */}\n {error && (\n <div className=\"flex gap-2 p-3 rounded-button bg-status-error-bg-subtle border border-status-error-border text-sm text-status-error-text\">\n <AlertCircle className=\"size-4 mt-0.5 shrink-0\" />\n <span>{error}</span>\n </div>\n )}\n </div>\n\n {/* Footer */}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={onClose}\n disabled={isLoading}\n className=\"px-4 py-2 text-sm rounded-button border border-border-subtle hover:bg-action-ghost-bgHover text-text-primary transition-colors disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleConfirm}\n disabled={\n isLoading ||\n newSeatCount === currentSeats ||\n newSeatCount < minSeats ||\n newSeatCount > maxSeats\n }\n className=\"px-4 py-2 text-sm rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2\"\n >\n {isLoading && <Loader2 className=\"size-4 animate-spin\" />}\n {newSeatCount === currentSeats\n ? 'No change'\n : seatDelta > 0\n ? `Add ${seatDelta} seat${seatDelta !== 1 ? 's' : ''}`\n : `Remove ${Math.abs(seatDelta)} seat${Math.abs(seatDelta) !== 1 ? 's' : ''}`}\n </button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n};\n"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ function i({ label: i, value: a, icon: o, trend: s, onClick: c, isActive: l = !1
|
|
|
18
18
|
children: a
|
|
19
19
|
}),
|
|
20
20
|
s && /* @__PURE__ */ n("p", {
|
|
21
|
-
className: r("mt-1 text-xs font-medium", s.isPositive ? "text-status-success-text" : "text-status-error-text"),
|
|
21
|
+
className: r("mt-1 text-xs font-medium tabular-nums", s.isPositive ? "text-status-success-text" : "text-status-error-text"),
|
|
22
22
|
children: [
|
|
23
23
|
s.isPositive ? "+" : "",
|
|
24
24
|
s.value,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MetricCard.js","names":[],"sources":["../../../../src/billing/shared/ui/MetricCard.tsx"],"sourcesContent":["/**\n * Boff UI — MetricCard primitive\n *\n * Stat/metric card following the Boff UI recipe: small muted label, large\n * tabular-nums value, optional trend, and an optional icon in an accent-soft\n * tile. One question per card. Can render as a button when `onClick` is\n * provided (used by the usage dashboard's filterable stats).\n *\n * Visual-only primitive.\n */\n\nimport type { ReactNode } from 'react';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\ninterface MetricCardTrend {\n value: number;\n isPositive: boolean;\n}\n\ninterface MetricCardProps {\n label: ReactNode;\n value: ReactNode;\n icon?: ReactNode;\n trend?: MetricCardTrend;\n /** When provided, renders an interactive button (with selected ring when active). */\n onClick?: () => void;\n isActive?: boolean;\n className?: string;\n /** Passthrough for data-tour and other data-* hooks. */\n [key: `data-${string}`]: string | undefined;\n}\n\nexport function MetricCard({\n label,\n value,\n icon,\n trend,\n onClick,\n isActive = false,\n className,\n ...rest\n}: MetricCardProps) {\n const base = cn(\n 'rounded-card border border-border-subtle bg-bg-surface shadow-elevation-1 p-5 text-left',\n onClick &&\n 'w-full transition-all duration-200 ease-[var(--motion-easing-out)] hover:border-border-strong hover:shadow-elevation-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n isActive && 'ring-1 ring-[var(--color-focus-ring)] border-border-strong',\n className\n );\n\n const inner = (\n <>\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm text-text-secondary\">{label}</p>\n {icon && (\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n {icon}\n </span>\n )}\n </div>\n <p className=\"mt-2 text-2xl font-semibold tabular-nums text-text-primary\">{value}</p>\n {trend && (\n <p\n className={cn(\n 'mt-1 text-xs font-medium',\n trend.isPositive ? 'text-status-success-text' : 'text-status-error-text'\n )}\n >\n {trend.isPositive ? '+' : ''}\n {trend.value}%\n </p>\n )}\n </>\n );\n\n if (onClick) {\n return (\n <button type=\"button\" onClick={onClick} className={base} {...rest}>\n {inner}\n </button>\n );\n }\n\n return (\n <div className={base} {...rest}>\n {inner}\n </div>\n );\n}\n"],"mappings":";;;AAgCA,SAAgB,EAAW,EACzB,UACA,UACA,SACA,UACA,YACA,cAAW,IACX,cACA,GAAG,KACe;CAClB,IAAM,IAAO,EACX,2FACA,KACE,uNACF,KAAY,8DACZ,EACD,EAEK,IACJ,kBAAA,GAAA,EAAA,UAAA;EACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAA+B;IAAU,CAAA,EACrD,KACC,kBAAC,QAAD;IAAM,WAAU;cACb;IACI,CAAA,CAEL;;EACN,kBAAC,KAAD;GAAG,WAAU;aAA8D;GAAU,CAAA;EACpF,KACC,kBAAC,KAAD;GACE,WAAW,EACT,
|
|
1
|
+
{"version":3,"file":"MetricCard.js","names":[],"sources":["../../../../src/billing/shared/ui/MetricCard.tsx"],"sourcesContent":["/**\n * Boff UI — MetricCard primitive\n *\n * Stat/metric card following the Boff UI recipe: small muted label, large\n * tabular-nums value, optional trend, and an optional icon in an accent-soft\n * tile. One question per card. Can render as a button when `onClick` is\n * provided (used by the usage dashboard's filterable stats).\n *\n * Visual-only primitive.\n */\n\nimport type { ReactNode } from 'react';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\ninterface MetricCardTrend {\n value: number;\n isPositive: boolean;\n}\n\ninterface MetricCardProps {\n label: ReactNode;\n value: ReactNode;\n icon?: ReactNode;\n trend?: MetricCardTrend;\n /** When provided, renders an interactive button (with selected ring when active). */\n onClick?: () => void;\n isActive?: boolean;\n className?: string;\n /** Passthrough for data-tour and other data-* hooks. */\n [key: `data-${string}`]: string | undefined;\n}\n\nexport function MetricCard({\n label,\n value,\n icon,\n trend,\n onClick,\n isActive = false,\n className,\n ...rest\n}: MetricCardProps) {\n const base = cn(\n 'rounded-card border border-border-subtle bg-bg-surface shadow-elevation-1 p-5 text-left',\n onClick &&\n 'w-full transition-all duration-200 ease-[var(--motion-easing-out)] hover:border-border-strong hover:shadow-elevation-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n isActive && 'ring-1 ring-[var(--color-focus-ring)] border-border-strong',\n className\n );\n\n const inner = (\n <>\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm text-text-secondary\">{label}</p>\n {icon && (\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n {icon}\n </span>\n )}\n </div>\n <p className=\"mt-2 text-2xl font-semibold tabular-nums text-text-primary\">{value}</p>\n {trend && (\n <p\n className={cn(\n 'mt-1 text-xs font-medium tabular-nums',\n trend.isPositive ? 'text-status-success-text' : 'text-status-error-text'\n )}\n >\n {trend.isPositive ? '+' : ''}\n {trend.value}%\n </p>\n )}\n </>\n );\n\n if (onClick) {\n return (\n <button type=\"button\" onClick={onClick} className={base} {...rest}>\n {inner}\n </button>\n );\n }\n\n return (\n <div className={base} {...rest}>\n {inner}\n </div>\n );\n}\n"],"mappings":";;;AAgCA,SAAgB,EAAW,EACzB,UACA,UACA,SACA,UACA,YACA,cAAW,IACX,cACA,GAAG,KACe;CAClB,IAAM,IAAO,EACX,2FACA,KACE,uNACF,KAAY,8DACZ,EACD,EAEK,IACJ,kBAAA,GAAA,EAAA,UAAA;EACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,KAAD;IAAG,WAAU;cAA+B;IAAU,CAAA,EACrD,KACC,kBAAC,QAAD;IAAM,WAAU;cACb;IACI,CAAA,CAEL;;EACN,kBAAC,KAAD;GAAG,WAAU;aAA8D;GAAU,CAAA;EACpF,KACC,kBAAC,KAAD;GACE,WAAW,EACT,yCACA,EAAM,aAAa,6BAA6B,yBACjD;aAJH;IAMG,EAAM,aAAa,MAAM;IACzB,EAAM;IAAM;IACX;;EAEL,EAAA,CAAA;AAWL,QARI,IAEA,kBAAC,UAAD;EAAQ,MAAK;EAAkB;EAAS,WAAW;EAAM,GAAI;YAC1D;EACM,CAAA,GAKX,kBAAC,OAAD;EAAK,WAAW;EAAM,GAAI;YACvB;EACG,CAAA"}
|