diff --git a/be/apps/core/src/locales/ui-schema/en.ts b/be/apps/core/src/locales/ui-schema/en.ts index 402ad340..0358951c 100644 --- a/be/apps/core/src/locales/ui-schema/en.ts +++ b/be/apps/core/src/locales/ui-schema/en.ts @@ -280,12 +280,12 @@ const enUiSchema = { }, plans: { free: { - title: 'Free Plan (free)', + title: 'Free Plan (hobby)', description: 'Default starter tier for individuals and trials.', }, pro: { title: 'Pro Plan (pro)', - description: 'Professional tier reserved for the upcoming subscription release.', + description: 'Professional plan.', }, friend: { title: 'Friend Plan (friend)', diff --git a/be/apps/core/src/locales/ui-schema/zh-CN.ts b/be/apps/core/src/locales/ui-schema/zh-CN.ts index 38283dbf..7ec9807d 100644 --- a/be/apps/core/src/locales/ui-schema/zh-CN.ts +++ b/be/apps/core/src/locales/ui-schema/zh-CN.ts @@ -278,12 +278,12 @@ const zhCnUiSchema = { }, plans: { free: { - title: 'Free 计划(free)', + title: 'Free 计划(hobby)', description: '默认入门方案,适用于个人与试用场景。', }, pro: { title: 'Pro 计划(pro)', - description: '专业方案,预留给即将上线的订阅。', + description: '专业方案', }, friend: { title: 'Friend 计划(friend)', diff --git a/be/apps/core/src/modules/platform/super-admin/super-admin-tenants.controller.ts b/be/apps/core/src/modules/platform/super-admin/super-admin-tenants.controller.ts index a30ca92a..c861e568 100644 --- a/be/apps/core/src/modules/platform/super-admin/super-admin-tenants.controller.ts +++ b/be/apps/core/src/modules/platform/super-admin/super-admin-tenants.controller.ts @@ -3,6 +3,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Query } from '@afilmory/fr import { DbAccessor } from 'core/database/database.provider' import { Roles } from 'core/guards/roles.decorator' import { BypassResponseTransform } from 'core/interceptors/response-transform.decorator' +import { SystemSettingService } from 'core/modules/configuration/system-setting/system-setting.service' import { BillingPlanService } from 'core/modules/platform/billing/billing-plan.service' import { BillingUsageService } from 'core/modules/platform/billing/billing-usage.service' import { TenantService } from 'core/modules/platform/tenant/tenant.service' @@ -16,6 +17,7 @@ import { TenantPhotosQueryDto, UpdateTenantBanDto, UpdateTenantPlanDto, + UpdateTenantStoragePlanDto, } from './super-admin.dto' @Controller('super-admin/tenants') @@ -27,6 +29,7 @@ export class SuperAdminTenantController { private readonly dataManagementService: DataManagementService, private readonly billingPlanService: BillingPlanService, private readonly billingUsageService: BillingUsageService, + private readonly systemSettings: SystemSettingService, private readonly db: DbAccessor, ) {} @@ -50,7 +53,7 @@ export class SuperAdminTenantController { @Get('/') async listTenants(@Query() query: ListTenantsQueryDto) { - const [tenantResult, plans] = await Promise.all([ + const [tenantResult, plans, storagePlanCatalog] = await Promise.all([ this.tenantService.listTenants({ page: query.page, limit: query.limit, @@ -60,6 +63,7 @@ export class SuperAdminTenantController { sortDir: query.sortDir, }), Promise.resolve(this.billingPlanService.getPlanDefinitions()), + this.systemSettings.getStoragePlanCatalog(), ]) const { items: tenantAggregates, total } = tenantResult @@ -73,6 +77,10 @@ export class SuperAdminTenantController { usageTotals: usageTotalsMap[aggregate.tenant.id] ?? [], })), plans, + storagePlans: Object.entries(storagePlanCatalog).map(([id, def]) => ({ + id, + ...def, + })), total, } } @@ -83,6 +91,12 @@ export class SuperAdminTenantController { return { updated: true } } + @Patch('/:tenantId/storage-plan') + async updateTenantStoragePlan(@Param() params: TenantIdParamDto, @Body() dto: UpdateTenantStoragePlanDto) { + await this.tenantService.updateStoragePlan(params.tenantId, dto.storagePlanId) + return { updated: true } + } + @Patch('/:tenantId/ban') async updateTenantBan(@Param() params: TenantIdParamDto, @Body() dto: UpdateTenantBanDto) { await this.tenantService.setBanned(params.tenantId, dto.banned) diff --git a/be/apps/core/src/modules/platform/super-admin/super-admin.dto.ts b/be/apps/core/src/modules/platform/super-admin/super-admin.dto.ts index fd58c822..068f70d2 100644 --- a/be/apps/core/src/modules/platform/super-admin/super-admin.dto.ts +++ b/be/apps/core/src/modules/platform/super-admin/super-admin.dto.ts @@ -91,6 +91,12 @@ const updateTenantPlanSchema = z.object({ export class UpdateTenantPlanDto extends createZodDto(updateTenantPlanSchema) {} +const updateTenantStoragePlanSchema = z.object({ + storagePlanId: z.string().trim().min(1).nullable(), +}) + +export class UpdateTenantStoragePlanDto extends createZodDto(updateTenantStoragePlanSchema) {} + const updateTenantBanSchema = z.object({ banned: z.boolean(), }) diff --git a/be/apps/core/src/modules/platform/tenant/tenant.repository.ts b/be/apps/core/src/modules/platform/tenant/tenant.repository.ts index cd0af4fd..2df3c6ec 100644 --- a/be/apps/core/src/modules/platform/tenant/tenant.repository.ts +++ b/be/apps/core/src/modules/platform/tenant/tenant.repository.ts @@ -71,6 +71,11 @@ export class TenantRepository { await db.update(tenants).set({ planId, updatedAt: new Date().toISOString() }).where(eq(tenants.id, id)) } + async updateStoragePlan(id: string, storagePlanId: string | null): Promise { + const db = this.dbAccessor.get() + await db.update(tenants).set({ storagePlanId, updatedAt: new Date().toISOString() }).where(eq(tenants.id, id)) + } + async updateBanned(id: string, banned: boolean): Promise { const db = this.dbAccessor.get() await db.update(tenants).set({ banned, updatedAt: new Date().toISOString() }).where(eq(tenants.id, id)) diff --git a/be/apps/core/src/modules/platform/tenant/tenant.service.ts b/be/apps/core/src/modules/platform/tenant/tenant.service.ts index 2b03e659..102dbee5 100644 --- a/be/apps/core/src/modules/platform/tenant/tenant.service.ts +++ b/be/apps/core/src/modules/platform/tenant/tenant.service.ts @@ -137,6 +137,10 @@ export class TenantService { await this.repository.updateBanned(id, banned) } + async updateStoragePlan(id: string, storagePlanId: string | null): Promise { + await this.repository.updateStoragePlan(id, storagePlanId) + } + async isSlugAvailable(slug: string): Promise { const normalized = this.normalizeSlug(slug) if (!normalized) { diff --git a/be/apps/dashboard/src/modules/super-admin/api.ts b/be/apps/dashboard/src/modules/super-admin/api.ts index dad1be4e..ea018028 100644 --- a/be/apps/dashboard/src/modules/super-admin/api.ts +++ b/be/apps/dashboard/src/modules/super-admin/api.ts @@ -13,6 +13,7 @@ import type { UpdateSuperAdminSettingsPayload, UpdateTenantBanPayload, UpdateTenantPlanPayload, + UpdateTenantStoragePlanPayload, } from './types' const SUPER_ADMIN_SETTINGS_ENDPOINT = '/super-admin/settings' @@ -69,6 +70,13 @@ export async function updateSuperAdminTenantPlan(payload: UpdateTenantPlanPayloa }) } +export async function updateSuperAdminTenantStoragePlan(payload: UpdateTenantStoragePlanPayload): Promise { + await coreApi(`${SUPER_ADMIN_TENANTS_ENDPOINT}/${payload.tenantId}/storage-plan`, { + method: 'PATCH', + body: { storagePlanId: payload.storagePlanId }, + }) +} + export async function updateSuperAdminTenantBan(payload: UpdateTenantBanPayload): Promise { await coreApi(`${SUPER_ADMIN_TENANTS_ENDPOINT}/${payload.tenantId}/ban`, { method: 'PATCH', diff --git a/be/apps/dashboard/src/modules/super-admin/components/SuperAdminTenantManager.tsx b/be/apps/dashboard/src/modules/super-admin/components/SuperAdminTenantManager.tsx index d8564c9c..37ff742b 100644 --- a/be/apps/dashboard/src/modules/super-admin/components/SuperAdminTenantManager.tsx +++ b/be/apps/dashboard/src/modules/super-admin/components/SuperAdminTenantManager.tsx @@ -18,14 +18,16 @@ import { toast } from 'sonner' import { LinearBorderPanel } from '~/components/common/LinearBorderPanel' import { getRequestErrorMessage } from '~/lib/errors' +import { buildTenantUrl } from '~/modules/auth/utils/domain' import { useDeleteTenantMutation, useSuperAdminTenantsQuery, useUpdateTenantBanMutation, useUpdateTenantPlanMutation, + useUpdateTenantStoragePlanMutation, } from '../hooks' -import type { BillingPlanDefinition, SuperAdminTenantSummary } from '../types' +import type { BillingPlanDefinition, StoragePlanDefinition, SuperAdminTenantSummary } from '../types' import { TenantDetailModal } from './TenantDetailModal' import { TenantUsageCell } from './TenantUsageCell' @@ -60,6 +62,7 @@ export function SuperAdminTenantManager() { sortDir, }) const updatePlanMutation = useUpdateTenantPlanMutation() + const updateStoragePlanMutation = useUpdateTenantStoragePlanMutation() const updateBanMutation = useUpdateTenantBanMutation() const deleteTenantMutation = useDeleteTenantMutation() const { t } = useTranslation() @@ -69,6 +72,7 @@ export function SuperAdminTenantManager() { const { data } = tenantsQuery const plans = data?.plans ?? [] + const storagePlans = data?.storagePlans ?? [] const tenants = data?.tenants ?? [] const total = data?.total ?? 0 const totalPages = Math.ceil(total / limit) @@ -101,6 +105,26 @@ export function SuperAdminTenantManager() { ) } + const handleStoragePlanChange = (tenant: SuperAdminTenantSummary, storagePlanId: string | null) => { + const nextId = storagePlanId === 'default' ? null : storagePlanId + if (nextId === tenant.storagePlanId) { + return + } + updateStoragePlanMutation.mutate( + { tenantId: tenant.id, storagePlanId: nextId }, + { + onSuccess: () => { + toast.success(t('superadmin.tenants.toast.storage-plan-success', { name: tenant.name })) + }, + onError: (error) => { + toast.error(t('superadmin.tenants.toast.storage-plan-error'), { + description: error instanceof Error ? error.message : t('common.retry-later'), + }) + }, + }, + ) + } + const handleToggleBanned = (tenant: SuperAdminTenantSummary) => { const next = !tenant.banned updateBanMutation.mutate( @@ -125,6 +149,9 @@ export function SuperAdminTenantManager() { const isPlanUpdating = (tenantId: string) => updatePlanMutation.isPending && updatePlanMutation.variables?.tenantId === tenantId + const isStoragePlanUpdating = (tenantId: string) => + updateStoragePlanMutation.isPending && updateStoragePlanMutation.variables?.tenantId === tenantId + const isBanUpdating = (tenantId: string) => updateBanMutation.isPending && updateBanMutation.variables?.tenantId === tenantId @@ -248,9 +275,9 @@ export function SuperAdminTenantManager() { {t('superadmin.tenants.table.plan')} + {t('superadmin.tenants.table.storage-plan')} {t('superadmin.tenants.table.usage')} {t('superadmin.tenants.table.status')} - {t('superadmin.tenants.table.ban')} handleSort('createdAt')} @@ -267,7 +294,16 @@ export function SuperAdminTenantManager() { {tenants.map((tenant) => ( -
{tenant.name}
+
{tenant.slug}
@@ -278,6 +314,14 @@ export function SuperAdminTenantManager() { onChange={(nextPlan) => handlePlanChange(tenant, nextPlan)} /> + + handleStoragePlanChange(tenant, nextPlan)} + /> +
- - - {formatDateLabel(tenant.createdAt)} - +
+ + +
))} @@ -352,7 +396,7 @@ export function SuperAdminTenantManager() {
- {page} / {totalPages || 1} + {page} / {totalPages || 1}