From 8c9e8a95757c782c424ae6230e06c1bf29fa07d9 Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 6 Dec 2025 00:20:00 +0800 Subject: [PATCH] feat: enhance tenant management with search functionality and DTO updates - Introduced search capability in tenant listing and management components. - Updated SuperAdminTenantController to utilize new DTOs for tenant ID and query parameters. - Enhanced tenant repository and service to support search filtering. - Modified frontend components to include a search input for better tenant management experience. Signed-off-by: Innei --- .../super-admin-tenants.controller.ts | 45 ++++++----- .../platform/super-admin/super-admin.dto.ts | 23 +++++- .../platform/tenant/tenant.repository.ts | 10 ++- .../modules/platform/tenant/tenant.service.ts | 2 + .../dashboard/src/modules/super-admin/api.ts | 1 + .../components/SuperAdminTenantManager.tsx | 80 ++++++++++++++----- .../src/modules/super-admin/types.ts | 1 + 7 files changed, 115 insertions(+), 47 deletions(-) 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 8046e989..a30ca92a 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 @@ -10,7 +10,13 @@ import { desc, eq } from 'drizzle-orm' import type { BillingPlanId } from '../billing/billing-plan.types' import { DataManagementService } from '../data-management/data-management.service' -import { UpdateTenantBanDto, UpdateTenantPlanDto } from './super-admin.dto' +import { + ListTenantsQueryDto, + TenantIdParamDto, + TenantPhotosQueryDto, + UpdateTenantBanDto, + UpdateTenantPlanDto, +} from './super-admin.dto' @Controller('super-admin/tenants') @Roles('superadmin') @@ -25,13 +31,13 @@ export class SuperAdminTenantController { ) {} @Get('/:tenantId/photos') - async getTenantPhotos(@Param('tenantId') tenantId: string, @Query('limit') limit = '20') { + async getTenantPhotos(@Param() params: TenantIdParamDto, @Query() query: TenantPhotosQueryDto) { const photos = await this.db .get() .select() .from(photoAssets) - .where(eq(photoAssets.tenantId, tenantId)) - .limit(Number(limit)) + .where(eq(photoAssets.tenantId, params.tenantId)) + .limit(query.limit) .orderBy(desc(photoAssets.createdAt)) return { @@ -43,20 +49,15 @@ export class SuperAdminTenantController { } @Get('/') - async listTenants( - @Query('page') page = '1', - @Query('limit') limit = '20', - @Query('status') status?: string, - @Query('sortBy') sortBy?: 'createdAt' | 'name', - @Query('sortDir') sortDir?: 'asc' | 'desc', - ) { + async listTenants(@Query() query: ListTenantsQueryDto) { const [tenantResult, plans] = await Promise.all([ this.tenantService.listTenants({ - page: Number(page), - limit: Number(limit), - status: status as any, - sortBy, - sortDir, + page: query.page, + limit: query.limit, + search: query.search, + status: query.status, + sortBy: query.sortBy, + sortDir: query.sortDir, }), Promise.resolve(this.billingPlanService.getPlanDefinitions()), ]) @@ -77,19 +78,19 @@ export class SuperAdminTenantController { } @Patch('/:tenantId/plan') - async updateTenantPlan(@Param('tenantId') tenantId: string, @Body() dto: UpdateTenantPlanDto) { - await this.billingPlanService.updateTenantPlan(tenantId, dto.planId as BillingPlanId) + async updateTenantPlan(@Param() params: TenantIdParamDto, @Body() dto: UpdateTenantPlanDto) { + await this.billingPlanService.updateTenantPlan(params.tenantId, dto.planId as BillingPlanId) return { updated: true } } @Patch('/:tenantId/ban') - async updateTenantBan(@Param('tenantId') tenantId: string, @Body() dto: UpdateTenantBanDto) { - await this.tenantService.setBanned(tenantId, dto.banned) + async updateTenantBan(@Param() params: TenantIdParamDto, @Body() dto: UpdateTenantBanDto) { + await this.tenantService.setBanned(params.tenantId, dto.banned) return { updated: true } } @Delete('/:tenantId') - async deleteTenant(@Param('tenantId') tenantId: string) { - return await this.dataManagementService.deleteTenantAccountById(tenantId) + async deleteTenant(@Param() params: TenantIdParamDto) { + return await this.dataManagementService.deleteTenantAccountById(params.tenantId) } } 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 6da165fd..fd58c822 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 @@ -1,4 +1,4 @@ -import { createZodDto } from '@afilmory/framework' +import { createZodDto, createZodSchemaDto } from '@afilmory/framework' import { BILLING_PLAN_IDS } from 'core/modules/platform/billing/billing-plan.constants' import type { BillingPlanId } from 'core/modules/platform/billing/billing-plan.types' import { z } from 'zod' @@ -96,3 +96,24 @@ const updateTenantBanSchema = z.object({ }) export class UpdateTenantBanDto extends createZodDto(updateTenantBanSchema) {} + +const tenantIdParamSchema = z.object({ + tenantId: z.string().trim().min(1), +}) + +const listTenantsQuerySchema = z.object({ + page: z.coerce.number().int().positive().default(1), + limit: z.coerce.number().int().positive().default(20), + search: z.string().trim().optional(), + status: z.enum(['pending', 'active', 'inactive', 'suspended']).optional(), + sortBy: z.enum(['createdAt', 'name']).optional(), + sortDir: z.enum(['asc', 'desc']).optional(), +}) + +const tenantPhotosQuerySchema = z.object({ + limit: z.coerce.number().int().positive().default(20), +}) + +export class TenantIdParamDto extends createZodSchemaDto(tenantIdParamSchema) {} +export class ListTenantsQueryDto extends createZodSchemaDto(listTenantsQuerySchema) {} +export class TenantPhotosQueryDto extends createZodSchemaDto(tenantPhotosQuerySchema) {} 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 0718569c..cd0af4fd 100644 --- a/be/apps/core/src/modules/platform/tenant/tenant.repository.ts +++ b/be/apps/core/src/modules/platform/tenant/tenant.repository.ts @@ -3,7 +3,7 @@ import { RESERVED_TENANT_SLUGS } from '@afilmory/utils' import { DbAccessor } from 'core/database/database.provider' import { BizException, ErrorCode } from 'core/errors' import type { BillingPlanId } from 'core/modules/platform/billing/billing-plan.types' -import { and, asc, count, desc, eq, notInArray } from 'drizzle-orm' +import { and, asc, count, desc, eq, ilike, notInArray, or } from 'drizzle-orm' import { injectable } from 'tsyringe' import type { TenantAggregate, TenantRecord } from './tenant.types' @@ -79,12 +79,13 @@ export class TenantRepository { async listTenants(options: { page: number limit: number + search?: string status?: TenantRecord['status'] sortBy?: 'createdAt' | 'name' sortDir?: 'asc' | 'desc' }): Promise<{ items: TenantAggregate[]; total: number }> { const db = this.dbAccessor.get() - const { page, limit, status, sortBy = 'createdAt', sortDir = 'desc' } = options + const { page, limit, search, status, sortBy = 'createdAt', sortDir = 'desc' } = options const conditions = [notInArray(tenants.slug, RESERVED_TENANT_SLUGS)] @@ -92,6 +93,11 @@ export class TenantRepository { conditions.push(eq(tenants.status, status)) } + if (search) { + const searchLike = `%${search}%` + conditions.push(or(ilike(tenants.name, searchLike), ilike(tenants.slug, searchLike))) + } + const where = and(...conditions) const [total] = await db.select({ count: count() }).from(tenants).where(where) 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 9fdfc2bb..2b03e659 100644 --- a/be/apps/core/src/modules/platform/tenant/tenant.service.ts +++ b/be/apps/core/src/modules/platform/tenant/tenant.service.ts @@ -118,6 +118,7 @@ export class TenantService { async listTenants(options?: { page?: number limit?: number + search?: string status?: TenantRecord['status'] sortBy?: 'createdAt' | 'name' sortDir?: 'asc' | 'desc' @@ -125,6 +126,7 @@ export class TenantService { return await this.repository.listTenants({ page: options?.page ?? 1, limit: options?.limit ?? 20, + search: options?.search, status: options?.status, sortBy: options?.sortBy, sortDir: options?.sortDir, diff --git a/be/apps/dashboard/src/modules/super-admin/api.ts b/be/apps/dashboard/src/modules/super-admin/api.ts index acfd86dc..dad1be4e 100644 --- a/be/apps/dashboard/src/modules/super-admin/api.ts +++ b/be/apps/dashboard/src/modules/super-admin/api.ts @@ -47,6 +47,7 @@ export async function fetchSuperAdminTenants( if (params) { if (params.page) query.set('page', String(params.page)) if (params.limit) query.set('limit', String(params.limit)) + if (params.search) query.set('search', params.search) if (params.status) query.set('status', params.status) if (params.sortBy) query.set('sortBy', params.sortBy) if (params.sortDir) query.set('sortDir', params.sortDir) 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 5be4d38e..d8564c9c 100644 --- a/be/apps/dashboard/src/modules/super-admin/components/SuperAdminTenantManager.tsx +++ b/be/apps/dashboard/src/modules/super-admin/components/SuperAdminTenantManager.tsx @@ -1,8 +1,18 @@ -import { Button, Modal, Prompt, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@afilmory/ui' +import { + Button, + Input, + Modal, + Prompt, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@afilmory/ui' import { Spring } from '@afilmory/utils' -import { ArrowDownIcon, ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon, RefreshCcwIcon } from 'lucide-react' +import { ArrowDownIcon, ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon, RefreshCcwIcon, SearchIcon } from 'lucide-react' import { m } from 'motion/react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -30,10 +40,21 @@ export function SuperAdminTenantManager() { const [status, setStatus] = useState('all') const [sortBy, setSortBy] = useState('createdAt') const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc') + const [search, setSearch] = useState('') + const [debouncedSearch, setDebouncedSearch] = useState('') + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(search) + setPage(1) + }, 300) + return () => clearTimeout(timer) + }, [search]) const tenantsQuery = useSuperAdminTenantsQuery({ page, limit, + search: debouncedSearch, status: status === 'all' ? undefined : status, sortBy, sortDir, @@ -163,25 +184,36 @@ export function SuperAdminTenantManager() { return (
-
- +
+
+ +
+
+ + setSearch(e.target.value)} + /> +