mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
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:
co-authored by
Cursor
parent
2c4d02bd0e
commit
e59db25814
@@ -17,6 +17,10 @@ Content resources: `products.*`, `categories.*`, `media.*`, `business.team.*`
|
|||||||
- `super_admin` → all permissions (bypasses business guard)
|
- `super_admin` → all permissions (bypasses business guard)
|
||||||
- Business owner (`isOwner=true`) → `business_owner` role permissions
|
- Business owner (`isOwner=true`) → `business_owner` role permissions
|
||||||
- Team member → permissions from `business_users.role_id` (admin/editor/viewer)
|
- Team member → permissions from `business_users.role_id` (admin/editor/viewer)
|
||||||
|
- Team **admin** may invite/update/remove **non-admin** staff only (cannot change or assign other admins)
|
||||||
|
- Team **editor** / **viewer** have no `business.team.*` manage permissions
|
||||||
|
- Only **super_admin** may assign or change the `admin` team role (owners/admins may assign editor/viewer)
|
||||||
|
- Owners cannot be changed via team APIs; use `PATCH .../team/access` for customer vs manager (staff)
|
||||||
|
|
||||||
## Adding a business-scoped endpoint
|
## Adding a business-scoped endpoint
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- Business team admins can manage non-admin staff (invite / update / remove).
|
||||||
|
-- Owners keep full team.* ; editors/viewers stay without team management.
|
||||||
|
|
||||||
|
INSERT INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r
|
||||||
|
JOIN permissions p ON p.slug IN (
|
||||||
|
'business.team.invite',
|
||||||
|
'business.team.update',
|
||||||
|
'business.team.remove'
|
||||||
|
)
|
||||||
|
WHERE r.slug = 'admin'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
@@ -369,11 +369,13 @@ Assigned via `business_users.role_id`:
|
|||||||
|
|
||||||
| Role | Slug | Typical access |
|
| Role | Slug | Typical access |
|
||||||
|------|------|----------------|
|
|------|------|----------------|
|
||||||
| Owner | `isOwner=true` | All `business_owner` permissions |
|
| Owner | `isOwner=true` | All `business_owner` permissions (incl. full team manage for editor/viewer) |
|
||||||
| Admin | `admin` | Full content + team read |
|
| Admin | `admin` | Full content + team manage for **non-admins**; only **super_admin** may assign this role |
|
||||||
| Editor | `editor` | Create/edit/publish content |
|
| Editor | `editor` | Create/edit/publish content (no team manage) |
|
||||||
| Viewer | `viewer` | Read-only |
|
| Viewer | `viewer` | Read-only |
|
||||||
|
|
||||||
|
Super-admin Users (business filter) and business Customers: change access is **Customer** vs **Manager**, then Admin/Editor/Viewer (Admin option super-admin only). Business owners are locked. API: `PATCH /businesses/:businessId/team/access`.
|
||||||
|
|
||||||
### Permission groups (seeded)
|
### Permission groups (seeded)
|
||||||
|
|
||||||
`business.*`, `domains.*`, `products.*`, `blogs.*`, `portfolios.*`, `media.*`, `categories.*`, `users.*`, `roles.manage`, `business.team.*`, `business_categories.*`
|
`business.*`, `domains.*`, `products.*`, `blogs.*`, `portfolios.*`, `media.*`, `categories.*`, `users.*`, `roles.manage`, `business.team.*`, `business_categories.*`
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { BusinessPermissionGuard } from '../auth/guards/business-permission.guar
|
|||||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
import { BusinessTeamService } from './business-team.service';
|
import { BusinessTeamService } from './business-team.service';
|
||||||
import { AddTeamMemberDto } from './dto/add-team-member.dto';
|
import { AddTeamMemberDto } from './dto/add-team-member.dto';
|
||||||
|
import { AssignTeamAccessDto } from './dto/assign-team-access.dto';
|
||||||
import { UpdateTeamMemberDto } from './dto/update-team-member.dto';
|
import { UpdateTeamMemberDto } from './dto/update-team-member.dto';
|
||||||
|
|
||||||
@Controller('businesses/:businessId/team')
|
@Controller('businesses/:businessId/team')
|
||||||
@@ -38,6 +39,17 @@ export class BusinessTeamController {
|
|||||||
return this.teamService.add(BigInt(businessId), user, dto);
|
return this.teamService.add(BigInt(businessId), user, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set customer vs staff (admin/editor/viewer) for an existing user. */
|
||||||
|
@Patch('access')
|
||||||
|
@RequireBusinessPermission('business.team.update')
|
||||||
|
assignAccess(
|
||||||
|
@Param('businessId') businessId: string,
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
@Body() dto: AssignTeamAccessDto,
|
||||||
|
) {
|
||||||
|
return this.teamService.assignAccess(BigInt(businessId), user, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Patch(':memberId')
|
@Patch(':memberId')
|
||||||
@RequireBusinessPermission('business.team.update')
|
@RequireBusinessPermission('business.team.update')
|
||||||
update(
|
update(
|
||||||
|
|||||||
@@ -14,8 +14,16 @@ import {
|
|||||||
import { PermissionsService } from '../auth/permissions.service';
|
import { PermissionsService } from '../auth/permissions.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { AddTeamMemberDto } from './dto/add-team-member.dto';
|
import { AddTeamMemberDto } from './dto/add-team-member.dto';
|
||||||
|
import { AssignTeamAccessDto } from './dto/assign-team-access.dto';
|
||||||
import { UpdateTeamMemberDto } from './dto/update-team-member.dto';
|
import { UpdateTeamMemberDto } from './dto/update-team-member.dto';
|
||||||
|
|
||||||
|
type TeamMembership = {
|
||||||
|
id: bigint;
|
||||||
|
userId: bigint;
|
||||||
|
isOwner: boolean;
|
||||||
|
role: { slug: string } | null;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BusinessTeamService {
|
export class BusinessTeamService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -69,6 +77,7 @@ export class BusinessTeamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.assertAssignableRole(dto.roleSlug);
|
this.assertAssignableRole(dto.roleSlug);
|
||||||
|
await this.assertCanAssignTeamRole(actor, businessId, null, dto.roleSlug);
|
||||||
|
|
||||||
const business = await this.prisma.business.findUnique({
|
const business = await this.prisma.business.findUnique({
|
||||||
where: { id: businessId },
|
where: { id: businessId },
|
||||||
@@ -175,6 +184,193 @@ export class BusinessTeamService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Super-admin / owner path: set a user as customer or staff (admin/editor/viewer)
|
||||||
|
* for one business. Owners cannot be changed.
|
||||||
|
*/
|
||||||
|
async assignAccess(
|
||||||
|
businessId: bigint,
|
||||||
|
actor: AuthUser,
|
||||||
|
dto: AssignTeamAccessDto,
|
||||||
|
) {
|
||||||
|
const userId = BigInt(dto.userId);
|
||||||
|
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||||
|
if (!user || !user.isActive) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const business = await this.prisma.business.findUnique({
|
||||||
|
where: { id: businessId },
|
||||||
|
});
|
||||||
|
if (!business?.isActive) {
|
||||||
|
throw new NotFoundException('Business not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const membership = await this.prisma.businessUser.findUnique({
|
||||||
|
where: { businessId_userId: { businessId, userId } },
|
||||||
|
include: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (membership?.isOwner) {
|
||||||
|
throw new ForbiddenException('Cannot change the role of a business owner');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.access === 'staff') {
|
||||||
|
if (!dto.roleSlug) {
|
||||||
|
throw new BadRequestException('roleSlug is required for staff access');
|
||||||
|
}
|
||||||
|
this.assertAssignableRole(dto.roleSlug);
|
||||||
|
|
||||||
|
const requiredPermission = membership
|
||||||
|
? 'business.team.update'
|
||||||
|
: 'business.team.invite';
|
||||||
|
const allowed = await this.permissions.hasBusinessPermission(
|
||||||
|
actor.id,
|
||||||
|
businessId,
|
||||||
|
requiredPermission,
|
||||||
|
);
|
||||||
|
if (!allowed) {
|
||||||
|
throw new ForbiddenException('You cannot assign team access');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.assertCanAssignTeamRole(
|
||||||
|
actor,
|
||||||
|
businessId,
|
||||||
|
membership,
|
||||||
|
dto.roleSlug,
|
||||||
|
);
|
||||||
|
|
||||||
|
const staffRole = await this.prisma.role.findUnique({
|
||||||
|
where: { slug: dto.roleSlug },
|
||||||
|
});
|
||||||
|
if (!staffRole) {
|
||||||
|
throw new BadRequestException('Invalid team role');
|
||||||
|
}
|
||||||
|
|
||||||
|
const businessStaffRole = await this.prisma.role.findUnique({
|
||||||
|
where: { slug: 'business_staff' },
|
||||||
|
});
|
||||||
|
if (!businessStaffRole) {
|
||||||
|
throw new Error('business_staff role is missing. Run migrations.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = await this.prisma.$transaction(async (tx) => {
|
||||||
|
const businessUser = membership
|
||||||
|
? await tx.businessUser.update({
|
||||||
|
where: { id: membership.id },
|
||||||
|
data: { roleId: staffRole.id },
|
||||||
|
include: { user: true, role: true },
|
||||||
|
})
|
||||||
|
: await tx.businessUser.create({
|
||||||
|
data: {
|
||||||
|
businessId,
|
||||||
|
userId,
|
||||||
|
isOwner: false,
|
||||||
|
roleId: staffRole.id,
|
||||||
|
invitedBy: actor.id,
|
||||||
|
},
|
||||||
|
include: { user: true, role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasStaffGlobalRole = await tx.userRole.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roleId: {
|
||||||
|
userId,
|
||||||
|
roleId: businessStaffRole.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!hasStaffGlobalRole) {
|
||||||
|
await tx.userRole.create({
|
||||||
|
data: { userId, roleId: businessStaffRole.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return businessUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Team access updated',
|
||||||
|
access: 'staff' as const,
|
||||||
|
member: {
|
||||||
|
id: member.id,
|
||||||
|
userId: member.user.id,
|
||||||
|
isOwner: false,
|
||||||
|
teamRole: member.role?.slug ?? dto.roleSlug,
|
||||||
|
permissions: await this.permissions.getPermissionsForBusiness(
|
||||||
|
member.user.id,
|
||||||
|
businessId,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const canRemove = await this.permissions.hasBusinessPermission(
|
||||||
|
actor.id,
|
||||||
|
businessId,
|
||||||
|
membership ? 'business.team.remove' : 'business.team.update',
|
||||||
|
);
|
||||||
|
if (!canRemove) {
|
||||||
|
throw new ForbiddenException('You cannot demote team members to customer');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.assertCanAssignTeamRole(actor, businessId, membership, null);
|
||||||
|
|
||||||
|
const customerRole = await this.prisma.role.findUnique({
|
||||||
|
where: { slug: 'customer' },
|
||||||
|
});
|
||||||
|
if (!customerRole) {
|
||||||
|
throw new Error('customer role is missing. Run migrations.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const businessStaffRole = await this.prisma.role.findUnique({
|
||||||
|
where: { slug: 'business_staff' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
if (membership) {
|
||||||
|
await tx.businessUser.delete({ where: { id: membership.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingCustomer = await tx.businessCustomer.findUnique({
|
||||||
|
where: { businessId_userId: { businessId, userId } },
|
||||||
|
});
|
||||||
|
if (!existingCustomer) {
|
||||||
|
await tx.businessCustomer.create({
|
||||||
|
data: { businessId, userId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCustomerRole = await tx.userRole.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roleId: { userId, roleId: customerRole.id },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!hasCustomerRole) {
|
||||||
|
await tx.userRole.create({
|
||||||
|
data: { userId, roleId: customerRole.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (businessStaffRole) {
|
||||||
|
const remainingStaff = await tx.businessUser.count({
|
||||||
|
where: { userId, isOwner: false },
|
||||||
|
});
|
||||||
|
if (remainingStaff === 0) {
|
||||||
|
await tx.userRole.deleteMany({
|
||||||
|
where: { userId, roleId: businessStaffRole.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'User set as customer for this business',
|
||||||
|
access: 'customer' as const,
|
||||||
|
member: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
businessId: bigint,
|
businessId: bigint,
|
||||||
memberId: bigint,
|
memberId: bigint,
|
||||||
@@ -205,6 +401,8 @@ export class BusinessTeamService {
|
|||||||
throw new ForbiddenException('Cannot change the role of a business owner');
|
throw new ForbiddenException('Cannot change the role of a business owner');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.assertCanAssignTeamRole(actor, businessId, member, dto.roleSlug);
|
||||||
|
|
||||||
const staffRole = await this.prisma.role.findUnique({
|
const staffRole = await this.prisma.role.findUnique({
|
||||||
where: { slug: dto.roleSlug },
|
where: { slug: dto.roleSlug },
|
||||||
});
|
});
|
||||||
@@ -244,6 +442,7 @@ export class BusinessTeamService {
|
|||||||
|
|
||||||
const member = await this.prisma.businessUser.findFirst({
|
const member = await this.prisma.businessUser.findFirst({
|
||||||
where: { id: memberId, businessId },
|
where: { id: memberId, businessId },
|
||||||
|
include: { role: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!member) {
|
if (!member) {
|
||||||
@@ -258,8 +457,24 @@ export class BusinessTeamService {
|
|||||||
throw new ForbiddenException('You cannot remove yourself');
|
throw new ForbiddenException('You cannot remove yourself');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.assertCanAssignTeamRole(actor, businessId, member, null);
|
||||||
|
|
||||||
await this.prisma.businessUser.delete({ where: { id: memberId } });
|
await this.prisma.businessUser.delete({ where: { id: memberId } });
|
||||||
|
|
||||||
|
const businessStaffRole = await this.prisma.role.findUnique({
|
||||||
|
where: { slug: 'business_staff' },
|
||||||
|
});
|
||||||
|
if (businessStaffRole) {
|
||||||
|
const remainingStaff = await this.prisma.businessUser.count({
|
||||||
|
where: { userId: member.userId, isOwner: false },
|
||||||
|
});
|
||||||
|
if (remainingStaff === 0) {
|
||||||
|
await this.prisma.userRole.deleteMany({
|
||||||
|
where: { userId: member.userId, roleId: businessStaffRole.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { message: 'Team member removed' };
|
return { message: 'Team member removed' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +489,29 @@ export class BusinessTeamService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only platform super admins may assign or change the `admin` team role.
|
||||||
|
* Owners and team admins may manage editor/viewer (and demote those to customer).
|
||||||
|
*/
|
||||||
|
private async assertCanAssignTeamRole(
|
||||||
|
actor: AuthUser,
|
||||||
|
businessId: bigint,
|
||||||
|
target: TeamMembership | null,
|
||||||
|
nextRoleSlug: string | null,
|
||||||
|
) {
|
||||||
|
if (await this.permissions.isSuperAdmin(actor.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target?.role?.slug === 'admin') {
|
||||||
|
throw new ForbiddenException('Cannot change the role of an admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextRoleSlug === 'admin') {
|
||||||
|
throw new ForbiddenException('Only a super admin can assign the admin role');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private assertAssignableRole(roleSlug: string): asserts roleSlug is AssignableTeamRole {
|
private assertAssignableRole(roleSlug: string): asserts roleSlug is AssignableTeamRole {
|
||||||
if (!ASSIGNABLE_TEAM_ROLES.includes(roleSlug as AssignableTeamRole)) {
|
if (!ASSIGNABLE_TEAM_ROLES.includes(roleSlug as AssignableTeamRole)) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { IsIn, IsOptional, IsString, Matches, ValidateIf } from 'class-validator';
|
||||||
|
import { ASSIGNABLE_TEAM_ROLES } from '../../auth/auth.types';
|
||||||
|
|
||||||
|
export class AssignTeamAccessDto {
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^\d+$/)
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['customer', 'staff'])
|
||||||
|
access!: 'customer' | 'staff';
|
||||||
|
|
||||||
|
@ValidateIf((dto: AssignTeamAccessDto) => dto.access === 'staff')
|
||||||
|
@IsString()
|
||||||
|
@IsIn([...ASSIGNABLE_TEAM_ROLES])
|
||||||
|
roleSlug?: string;
|
||||||
|
}
|
||||||
@@ -21,6 +21,9 @@ type CustomerListRow = {
|
|||||||
email: string | null;
|
email: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
businessMemberId: bigint | null;
|
||||||
|
isBusinessOwner: boolean | null;
|
||||||
|
teamRole: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -44,9 +47,18 @@ export class CustomersService {
|
|||||||
|
|
||||||
const nameLike = query.name?.trim() ? `%${query.name.trim()}%` : null;
|
const nameLike = query.name?.trim() ? `%${query.name.trim()}%` : null;
|
||||||
const cellLike = query.cellNumber?.trim() ? `%${query.cellNumber.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`
|
const where = Prisma.sql`
|
||||||
WHERE bc.business_id = ${businessId}
|
WHERE 1=1
|
||||||
|
${accessFilter}
|
||||||
${nameLike ? Prisma.sql`
|
${nameLike ? Prisma.sql`
|
||||||
AND (
|
AND (
|
||||||
u.first_name ILIKE ${nameLike}
|
u.first_name ILIKE ${nameLike}
|
||||||
@@ -65,18 +77,28 @@ export class CustomersService {
|
|||||||
u.first_name AS "firstName",
|
u.first_name AS "firstName",
|
||||||
u.last_name AS "lastName",
|
u.last_name AS "lastName",
|
||||||
u.email AS "email",
|
u.email AS "email",
|
||||||
bc.created_at AS "createdAt",
|
COALESCE(bc.created_at, bu.created_at) AS "createdAt",
|
||||||
bc.is_enabled AS "isEnabled"
|
COALESCE(bc.is_enabled, TRUE) AS "isEnabled",
|
||||||
FROM business_customers bc
|
bu.id AS "businessMemberId",
|
||||||
JOIN users u ON u.id = bc.user_id
|
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}
|
${where}
|
||||||
ORDER BY bc.created_at DESC
|
ORDER BY COALESCE(bc.created_at, bu.created_at) DESC
|
||||||
LIMIT ${pageSize} OFFSET ${skip}
|
LIMIT ${pageSize} OFFSET ${skip}
|
||||||
`),
|
`),
|
||||||
this.prisma.$queryRaw<{ total: number }[]>(Prisma.sql`
|
this.prisma.$queryRaw<{ total: number }[]>(Prisma.sql`
|
||||||
SELECT COUNT(*)::int AS "total"
|
SELECT COUNT(*)::int AS "total"
|
||||||
FROM business_customers bc
|
FROM users u
|
||||||
JOIN users u ON u.id = bc.user_id
|
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}
|
${where}
|
||||||
`),
|
`),
|
||||||
]);
|
]);
|
||||||
@@ -91,6 +113,11 @@ export class CustomersService {
|
|||||||
label: this.formatLabel(user),
|
label: this.formatLabel(user),
|
||||||
createdAt: user.createdAt.toISOString(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
isEnabled: user.isEnabled,
|
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,
|
total: totalRow[0]?.total ?? 0,
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Type } from 'class-transformer';
|
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 {
|
export class ListCustomersDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -21,4 +24,10 @@ export class ListCustomersDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
cellNumber?: string;
|
cellNumber?: string;
|
||||||
|
|
||||||
|
/** all = customers ∪ managers; customers = customers only; managers = staff/owners */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@IsIn([...CUSTOMER_ACCESS_FILTERS])
|
||||||
|
access?: CustomerAccessFilter;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user