Add business team access controls and customer role filters.

Super-admins can assign Admin; owners/admins manage Editor/Viewer via team/access, and the customers list supports all/customers/managers filtering.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-10 21:31:28 +03:30
co-authored by Cursor
parent 2c4d02bd0e
commit e59db25814
8 changed files with 334 additions and 12 deletions
+35 -8
View File
@@ -21,6 +21,9 @@ type CustomerListRow = {
email: string | null;
createdAt: Date;
isEnabled: boolean;
businessMemberId: bigint | null;
isBusinessOwner: boolean | null;
teamRole: string | null;
};
@Injectable()
@@ -44,9 +47,18 @@ export class CustomersService {
const nameLike = query.name?.trim() ? `%${query.name.trim()}%` : null;
const cellLike = query.cellNumber?.trim() ? `%${query.cellNumber.trim()}%` : null;
const access = query.access ?? 'all';
const accessFilter =
access === 'customers'
? Prisma.sql`AND bc.id IS NOT NULL AND bu.id IS NULL`
: access === 'managers'
? Prisma.sql`AND bu.id IS NOT NULL`
: Prisma.sql`AND (bc.id IS NOT NULL OR bu.id IS NOT NULL)`;
const where = Prisma.sql`
WHERE bc.business_id = ${businessId}
WHERE 1=1
${accessFilter}
${nameLike ? Prisma.sql`
AND (
u.first_name ILIKE ${nameLike}
@@ -65,18 +77,28 @@ export class CustomersService {
u.first_name AS "firstName",
u.last_name AS "lastName",
u.email AS "email",
bc.created_at AS "createdAt",
bc.is_enabled AS "isEnabled"
FROM business_customers bc
JOIN users u ON u.id = bc.user_id
COALESCE(bc.created_at, bu.created_at) AS "createdAt",
COALESCE(bc.is_enabled, TRUE) AS "isEnabled",
bu.id AS "businessMemberId",
bu.is_owner AS "isBusinessOwner",
r.slug AS "teamRole"
FROM users u
LEFT JOIN business_customers bc
ON bc.user_id = u.id AND bc.business_id = ${businessId}
LEFT JOIN business_users bu
ON bu.user_id = u.id AND bu.business_id = ${businessId}
LEFT JOIN roles r ON r.id = bu.role_id
${where}
ORDER BY bc.created_at DESC
ORDER BY COALESCE(bc.created_at, bu.created_at) DESC
LIMIT ${pageSize} OFFSET ${skip}
`),
this.prisma.$queryRaw<{ total: number }[]>(Prisma.sql`
SELECT COUNT(*)::int AS "total"
FROM business_customers bc
JOIN users u ON u.id = bc.user_id
FROM users u
LEFT JOIN business_customers bc
ON bc.user_id = u.id AND bc.business_id = ${businessId}
LEFT JOIN business_users bu
ON bu.user_id = u.id AND bu.business_id = ${businessId}
${where}
`),
]);
@@ -91,6 +113,11 @@ export class CustomersService {
label: this.formatLabel(user),
createdAt: user.createdAt.toISOString(),
isEnabled: user.isEnabled,
businessMemberId: user.businessMemberId?.toString() ?? null,
isBusinessOwner: user.isBusinessOwner ?? false,
teamRole: user.isBusinessOwner
? 'business_owner'
: user.teamRole ?? null,
})),
total: totalRow[0]?.total ?? 0,
page,
+10 -1
View File
@@ -1,5 +1,8 @@
import { Type } from 'class-transformer';
import { IsInt, IsOptional, IsString, Min } from 'class-validator';
import { IsIn, IsInt, IsOptional, IsString, Min } from 'class-validator';
export const CUSTOMER_ACCESS_FILTERS = ['all', 'customers', 'managers'] as const;
export type CustomerAccessFilter = (typeof CUSTOMER_ACCESS_FILTERS)[number];
export class ListCustomersDto {
@IsOptional()
@@ -21,4 +24,10 @@ export class ListCustomersDto {
@IsOptional()
@IsString()
cellNumber?: string;
/** all = customers managers; customers = customers only; managers = staff/owners */
@IsOptional()
@IsString()
@IsIn([...CUSTOMER_ACCESS_FILTERS])
access?: CustomerAccessFilter;
}