diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index b7e513f80..a17235eab 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -55,7 +55,8 @@ "trial_expiry_upgrade_tip_one": "Your trial will expire in {{count}} day. Upgrade to keep your services online.", "trial_expiry_upgrade_tip_other": "Your trial will expire in {{count}} days. Upgrade to keep your services online.", "upgrade_button": "Upgrade Plan", - "upgrade_tip": "To upgrade your plan, you can visit the Cost Center." + "upgrade_tip": "To upgrade your plan, you can visit the Cost Center.", + "can_not_manage_payments": "Please contact the namespace owner to update or cancel the subscription." }, "balance_text": "Balance", "bank_name": "Bank", diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index 7f38b30c0..a68c7c989 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -54,7 +54,8 @@ "trial_expiry_upgrade_tip_one": "你的试用将在 {{count}} 天后过期. 升级套餐以保证服务在线.", "trial_expiry_upgrade_tip_other": "你的试用将在 {{count}} 天后过期. 升级套餐以保证服务在线.", "upgrade_button": "升级套餐", - "upgrade_tip": "跳转到费用中心以升级套餐" + "upgrade_tip": "跳转到费用中心以升级套餐", + "can_not_manage_payments": "请联系空间管理员执行取消或升级订阅操作" }, "balance_text": "余额", "bank_name": "开户银行", diff --git a/frontend/desktop/src/components/SecondaryLinks/index.tsx b/frontend/desktop/src/components/SecondaryLinks/index.tsx index e18529faa..a5fb8710c 100644 --- a/frontend/desktop/src/components/SecondaryLinks/index.tsx +++ b/frontend/desktop/src/components/SecondaryLinks/index.tsx @@ -95,6 +95,7 @@ export default function SecondaryLinks() { const isFreePlan = (subscription?.PlanName || '').toLowerCase() === 'free'; const isCancelled = !!subscription?.CancelAtPeriodEnd && !isFreePlan; const isDebt = subscription?.Status?.toLowerCase() === 'debt'; + const canManagePayment = subscription?.role === 'OWNER'; useEffect(() => { if (workspace) { @@ -126,7 +127,7 @@ export default function SecondaryLinks() { - layoutConfig?.common.subscriptionEnabled + layoutConfig?.common.subscriptionEnabled && canManagePayment ? openCostCenterApp('upgrade') : openCostCenterApp() } @@ -144,7 +145,7 @@ export default function SecondaryLinks() { fontWeight={'500'} cursor={'pointer'} onClick={() => - layoutConfig?.common.subscriptionEnabled + layoutConfig?.common.subscriptionEnabled && canManagePayment ? openCostCenterApp('upgrade') : openCostCenterApp() } diff --git a/frontend/desktop/src/components/account/BalancePopover.tsx b/frontend/desktop/src/components/account/BalancePopover.tsx index a7d4cfabd..83f986b83 100644 --- a/frontend/desktop/src/components/account/BalancePopover.tsx +++ b/frontend/desktop/src/components/account/BalancePopover.tsx @@ -13,7 +13,7 @@ import useSessionStore from '@/stores/session'; import { useSubscriptionStore } from '@/stores/subscription'; import { useMemo, useEffect } from 'react'; import { Sparkles } from 'lucide-react'; -import { Button, cn, Separator } from '@sealos/shadcn-ui'; +import { Tooltip, Button, cn, Separator, TooltipContent, TooltipTrigger } from '@sealos/shadcn-ui'; import { WorkspaceSubscription } from '@/types/plan'; import Decimal from 'decimal.js'; import { useQuery } from '@tanstack/react-query'; @@ -75,6 +75,8 @@ export function BalancePopover({ const isFreePlan = (subscription?.PlanName || '').toLowerCase() === 'free'; const isCancelled = !!subscription?.CancelAtPeriodEnd && !isFreePlan; + const canManagePayment = subscription?.role === 'OWNER'; + const formatDate = (dateStr?: string) => { if (!dateStr) return 'N/A'; return new Date(dateStr).toLocaleString('zh-CN', { @@ -255,10 +257,24 @@ export function BalancePopover({ )} - + + +
+ +
+
+ +

{t('common:balance_popover.can_not_manage_payments')}

+
+
)} diff --git a/frontend/desktop/src/components/account/cost.tsx b/frontend/desktop/src/components/account/cost.tsx deleted file mode 100644 index 25c1dea88..000000000 --- a/frontend/desktop/src/components/account/cost.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { getAmount } from '@/api/auth'; -import { getUserBilling } from '@/api/platform'; -import useAppStore from '@/stores/app'; -import { useConfigStore } from '@/stores/config'; -import useSessionStore from '@/stores/session'; -import { formatMoney } from '@/utils/format'; -import { - Accordion, - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, - Box, - Center, - Flex, - Text, - useBreakpointValue -} from '@chakra-ui/react'; -import { CurrencySymbol, MonitorIcon } from '@sealos/ui'; -import { useQuery } from '@tanstack/react-query'; -import { Decimal } from 'decimal.js'; -import { useTranslation } from 'next-i18next'; -import { useMemo } from 'react'; -import CustomTooltip from '../AppDock/CustomTooltip'; -import { blurBackgroundStyles } from '../desktop_content'; -import Monitor from '../desktop_content/monitor'; -import { ClockIcon, HelpIcon, InfiniteIcon } from '../icons'; -import { BalancePopover } from './BalancePopover'; - -/** - * @deprecated This component is deprecated. Use Cost Center to monitor balance instead. - */ -export default function Cost() { - const { t } = useTranslation(); - const rechargeEnabled = useConfigStore().commonConfig?.rechargeEnabled; - const openApp = useAppStore((s) => s.openApp); - const installApp = useAppStore((s) => s.installedApps); - const { session } = useSessionStore(); - const user = session?.user; - const isLargerThanXl = useBreakpointValue({ base: true, xl: false }); - const currencySymbol = useConfigStore( - (state) => state.layoutConfig?.currencySymbol || 'shellCoin' - ); - - const { data } = useQuery({ - queryKey: ['getAmount', { userId: user?.userCrUid }], - queryFn: getAmount, - enabled: !!user, - staleTime: 60 * 1000 - }); - - const { data: billing } = useQuery(['getUserBilling'], () => getUserBilling(), { - cacheTime: 5 * 60 * 1000, - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false - }); - - const balance = useMemo(() => { - let realBalance = new Decimal(data?.data?.balance || 0); - if (data?.data?.deductionBalance) { - realBalance = realBalance.minus(new Decimal(data.data.deductionBalance)); - } - return realBalance.toNumber(); - }, [data]); - - const calculations = useMemo(() => { - const prevDayAmount = new Decimal(billing?.data?.prevDayTime || 0); - const estimatedNextMonthAmount = prevDayAmount.times(30).toNumber(); - const _balance = new Decimal(balance || 0); - - let estimatedDaysUsable; - if (_balance.isNegative()) { - estimatedDaysUsable = 0; - } else if (prevDayAmount.isZero()) { - estimatedDaysUsable = Number.POSITIVE_INFINITY; - } else { - estimatedDaysUsable = _balance.div(prevDayAmount).ceil().toNumber(); - } - - return { - prevMonthAmount: new Decimal(billing?.data?.prevMonthTime || 0).toNumber(), - estimatedNextMonthAmount, - estimatedDaysUsable - }; - }, [billing?.data?.prevDayTime, billing?.data?.prevMonthTime, , balance]); - - return ( - - - { - const costcenter = installApp.find((t) => t.key === 'system-costcenter'); - if (!costcenter) return; - openApp(costcenter); - }} - openCostCenterTopup={() => {}} - > - - - - {t('common:balance_popover.balance')} - - - - {formatMoney(balance).toFixed(2)} - - - - - {rechargeEnabled && ( -
{ - e.stopPropagation(); - const costcenter = installApp.find((t) => t.key === 'system-costcenter'); - if (!costcenter) return; - openApp(costcenter, { - query: { - openRecharge: 'true' - } - }); - }} - color={'rgba(255, 255, 255, 0.90)'} - cursor={'pointer'} - > - {t('common:charge')} -
- )} -
-
- {calculations && ( - - - - - {t('common:expected_used')} - - - {calculations.estimatedDaysUsable === Number.POSITIVE_INFINITY ? ( - <> - {t('common:day')} - - ) : ( - <> - {calculations.estimatedDaysUsable} {t('common:day')} - - )} - - - -
- - {t('common:used_last_month')} - - - {formatMoney(calculations.prevMonthAmount).toFixed(2)} - - -
- -
- - - {t('common:expected_to_use_next_month')} - - - - - - - - - {formatMoney(calculations.estimatedNextMonthAmount).toFixed(2)} - - -
-
- )} -
- - {isLargerThanXl && ( - - - - - - {t('common:monitor')} - - - - - - - - - - )} -
- ); -} diff --git a/frontend/desktop/src/types/plan.ts b/frontend/desktop/src/types/plan.ts index 4256001b5..9cb7da35b 100644 --- a/frontend/desktop/src/types/plan.ts +++ b/frontend/desktop/src/types/plan.ts @@ -16,6 +16,9 @@ export const OperatorSchema = z.enum([ ]); export type Operator = z.infer; +export const WorkspaceRoleSchema = z.enum(['MANAGER', 'DEVELOPER', 'OWNER']); +export type WorkspaceRole = z.infer; + export const StripeInfoSchema = z.object({ subscriptionId: z.string(), customerId: z.string() @@ -68,7 +71,8 @@ export const WorkspaceSubscriptionSchema = z.object({ UpdateAt: z.string(), ExpireAt: z.string().nullable(), Traffic: z.array(z.any()).nullable(), - type: SubscriptionTypeSchema + type: SubscriptionTypeSchema, + role: WorkspaceRoleSchema }); export type WorkspaceSubscription = z.infer; diff --git a/frontend/providers/costcenter/public/locales/en/common.json b/frontend/providers/costcenter/public/locales/en/common.json index 67a5dc7ce..1748caf88 100644 --- a/frontend/providers/costcenter/public/locales/en/common.json +++ b/frontend/providers/costcenter/public/locales/en/common.json @@ -445,5 +445,6 @@ "jcb": "JCB", "unionpay": "UnionPay" }, - "downgrade_warning_message": "Please ensure resources remain within {{planName}} plan limits by {{date}} to avoid charges." + "downgrade_warning_message": "Please ensure resources remain within {{planName}} plan limits by {{date}} to avoid charges.", + "can_not_manage_payments": "Please contact the namespace owner to update or cancel the subscription." } diff --git a/frontend/providers/costcenter/public/locales/zh/common.json b/frontend/providers/costcenter/public/locales/zh/common.json index 210777528..dd05e1edc 100644 --- a/frontend/providers/costcenter/public/locales/zh/common.json +++ b/frontend/providers/costcenter/public/locales/zh/common.json @@ -442,5 +442,6 @@ "jcb": "JCB", "unionpay": "银联" }, - "downgrade_warning_message": "请确保资源在 {{date}} 之前保持在 {{planName}} 套餐限制内,以避免产生费用。" + "downgrade_warning_message": "请确保资源在 {{date}} 之前保持在 {{planName}} 套餐限制内,以避免产生费用。", + "can_not_manage_payments": "请联系空间管理员执行取消或升级订阅操作" } diff --git a/frontend/providers/costcenter/src/components/plan/CardInfoSection.tsx b/frontend/providers/costcenter/src/components/plan/CardInfoSection.tsx index f957b049b..6fb11ee33 100644 --- a/frontend/providers/costcenter/src/components/plan/CardInfoSection.tsx +++ b/frontend/providers/costcenter/src/components/plan/CardInfoSection.tsx @@ -1,4 +1,4 @@ -import { Button } from '@sealos/shadcn-ui'; +import { Button, Tooltip, TooltipContent, TooltipTrigger } from '@sealos/shadcn-ui'; import { useQuery, useMutation } from '@tanstack/react-query'; import { getCardInfo, createCardManageSession } from '@/api/plan'; import { useTranslation } from 'next-i18next'; @@ -8,6 +8,7 @@ import useBillingStore from '@/stores/billing'; import { BankCardIcon } from '../BankCardIcon'; import { BankCardBrand } from '../BankCardBrand'; import { openInNewWindow } from '@/utils/windowUtils'; +import usePlanStore from '@/stores/plan'; interface CardInfoSectionProps { workspace?: string; @@ -21,6 +22,9 @@ export function CardInfoSection({ workspace, regionDomain }: CardInfoSectionProp const { getRegion } = useBillingStore(); const region = getRegion(); + const subscriptionData = usePlanStore((state) => state.subscriptionData); + const canManagePayment = subscriptionData?.subscription.role === 'OWNER'; + const effectiveWorkspace = workspace || session?.user?.nsid || ''; const effectiveRegionDomain = regionDomain || region?.domain || ''; @@ -133,14 +137,25 @@ export function CardInfoSection({ workspace, regionDomain }: CardInfoSectionProp {hasCard && ( - + + +
+ +
+
+ +

{t('common:can_not_manage_payments')}

+
+
)} diff --git a/frontend/providers/costcenter/src/components/plan/PlanHeader.tsx b/frontend/providers/costcenter/src/components/plan/PlanHeader.tsx index 66d385421..daa8dd58b 100644 --- a/frontend/providers/costcenter/src/components/plan/PlanHeader.tsx +++ b/frontend/providers/costcenter/src/components/plan/PlanHeader.tsx @@ -54,6 +54,7 @@ export function PlanHeader({ children, onRenewSuccess }: PlanHeaderProps) { const plans = plansData?.plans; const subscription = subscriptionData?.subscription; + const canManagePayment = subscription?.role === 'OWNER'; const lastTransaction = lastTransactionData?.transaction; const planName = subscription?.PlanName || t('common:free_plan'); const isFreePlan = (subscription?.PlanName || '').toLowerCase() === 'free'; @@ -196,9 +197,16 @@ export function PlanHeader({ children, onRenewSuccess }: PlanHeaderProps) { {children?.({ trigger: ( - + + + + + +

{t('common:can_not_manage_payments')}

+
+
) })} @@ -222,74 +230,81 @@ export function PlanHeader({ children, onRenewSuccess }: PlanHeaderProps) { -
- {isNormal && !isFreePlan && ( - - )} - {isCancelled && ( - + )} + {isCancelled && ( + - )} + resumePlanMutation.mutate({ + workspace: subscription.Workspace, + regionDomain: subscription.RegionDomain, + planName: subscription.PlanName, + payMethod, + operator: 'resumed' + }); + }} + > + + {t('common:renew_plan')} + + )} - {/* Keep UpgradePlanDialog mounted even when cancelled, so message-driven open works */} - {children?.({ - trigger: isCancelled ? ( - - ) : ( - - ) - })} -
+ {/* Keep UpgradePlanDialog mounted even when cancelled, so message-driven open works */} + {children?.({ + trigger: isCancelled ? ( + + ) : ( + + ) + })} + + + +

{t('common:can_not_manage_payments')}

+
+ diff --git a/frontend/providers/costcenter/src/types/plan.ts b/frontend/providers/costcenter/src/types/plan.ts index ce3142735..bfba096f9 100644 --- a/frontend/providers/costcenter/src/types/plan.ts +++ b/frontend/providers/costcenter/src/types/plan.ts @@ -17,6 +17,9 @@ export const OperatorSchema = z.enum([ ]); export type Operator = z.infer; +export const WorkspaceRoleSchema = z.enum(['MANAGER', 'DEVELOPER', 'OWNER']); +export type WorkspaceRole = z.infer; + // Stripe 信息 export const StripeInfoSchema = z.object({ subscriptionId: z.string(), @@ -95,7 +98,8 @@ export const WorkspaceSubscriptionSchema = z.object({ ExpireAt: z.string().nullable(), Traffic: z.array(z.any()).nullable(), type: SubscriptionTypeSchema, - InvoiceInfo: InvoiceInfoSchema.optional() + InvoiceInfo: InvoiceInfoSchema.optional(), + role: WorkspaceRoleSchema }); export type WorkspaceSubscription = z.infer;