feature: Tasks tab added for lab organizations. tasks now can be assigned and their status can be updated by the assignee.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
-- Add task priority, timestamps, and Tasks tab permissions
|
||||
|
||||
ALTER TABLE "lab_case_tasks" ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 3;
|
||||
ALTER TABLE "lab_case_tasks" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
ALTER TABLE "lab_case_tasks" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
CREATE INDEX "lab_case_tasks_assigneeUserId_priority_createdAt_idx"
|
||||
ON "lab_case_tasks"("assigneeUserId", "priority", "createdAt");
|
||||
|
||||
INSERT INTO "features" ("id", "name", "description", "organizationTypeId")
|
||||
VALUES (gen_random_uuid(), 'Tasks', 'Lab task inbox', NULL)
|
||||
ON CONFLICT ("name") DO NOTHING;
|
||||
|
||||
INSERT INTO "permissions" ("id", "name", "description", "featureId")
|
||||
SELECT gen_random_uuid(), v.name, NULL, f.id
|
||||
FROM (VALUES
|
||||
('TAB_TASKS_READ'),
|
||||
('TAB_TASKS_EDIT')
|
||||
) AS v(name)
|
||||
CROSS JOIN "features" f
|
||||
WHERE f.name = 'Tasks'
|
||||
ON CONFLICT ("name") DO NOTHING;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Track when a task was assigned (for sorting and display)
|
||||
|
||||
ALTER TABLE "lab_case_tasks" ADD COLUMN "assignedAt" TIMESTAMP(3);
|
||||
|
||||
CREATE INDEX "lab_case_tasks_assignedAt_labCaseId_priority_idx"
|
||||
ON "lab_case_tasks"("assignedAt" DESC, "labCaseId" ASC, "priority" DESC);
|
||||
@@ -253,14 +253,21 @@ model LabCaseTask {
|
||||
stepOrder Int
|
||||
stepLabel String
|
||||
assigneeUserId String?
|
||||
assignedAt DateTime?
|
||||
priority Int @default(3)
|
||||
status LabTaskStatus @default(PENDING)
|
||||
|
||||
labCase LabCase @relation(fields: [labCaseId], references: [id], onDelete: Cascade)
|
||||
detail TreatmentDetail @relation(fields: [treatmentDetailId], references: [id], onDelete: Cascade)
|
||||
assignee User? @relation("LabCaseTaskAssignee", fields: [assigneeUserId], references: [id], onDelete: SetNull)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([labCaseId, tooth, treatmentType, stepOrder])
|
||||
@@index([labCaseId, status])
|
||||
@@index([assigneeUserId, priority, createdAt])
|
||||
@@index([assignedAt, labCaseId, priority])
|
||||
@@map("lab_case_tasks")
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,10 @@ async function main() {
|
||||
name: 'Cases',
|
||||
permissions: ['TAB_CASES_READ', 'TAB_CASES_EDIT'],
|
||||
},
|
||||
{
|
||||
name: 'Tasks',
|
||||
permissions: ['TAB_TASKS_READ', 'TAB_TASKS_EDIT'],
|
||||
},
|
||||
{
|
||||
name: 'Billing',
|
||||
permissions: ['TAB_BILLING_READ', 'TAB_BILLING_EDIT'],
|
||||
|
||||
@@ -12,6 +12,7 @@ import { OrganizationModule } from './modules/organization/organization.module';
|
||||
import { AppointmentsModule } from './modules/appointments/appointments.module';
|
||||
import { TreatmentsModule } from './modules/treatments/treatments.module';
|
||||
import { CasesModule } from './modules/cases/cases.module';
|
||||
import { TasksModule } from './modules/tasks/tasks.module';
|
||||
import { TreatmentCatalogModule } from './modules/treatment-catalog/treatment-catalog.module';
|
||||
|
||||
@Module({
|
||||
@@ -27,6 +28,7 @@ import { TreatmentCatalogModule } from './modules/treatment-catalog/treatment-ca
|
||||
AppointmentsModule,
|
||||
TreatmentsModule,
|
||||
CasesModule,
|
||||
TasksModule,
|
||||
StaffModule,
|
||||
OrganizationModule,
|
||||
AdminModule.forRoot(),
|
||||
|
||||
@@ -13,7 +13,12 @@ const CLINIC_ONLY_PERMISSIONS = new Set<string>([
|
||||
'TAB_TREATMENT_EDIT',
|
||||
]);
|
||||
|
||||
const LAB_ONLY_PERMISSIONS = new Set<string>(['TAB_CASES_READ', 'TAB_CASES_EDIT']);
|
||||
const LAB_ONLY_PERMISSIONS = new Set<string>([
|
||||
'TAB_CASES_READ',
|
||||
'TAB_CASES_EDIT',
|
||||
'TAB_TASKS_READ',
|
||||
'TAB_TASKS_EDIT',
|
||||
]);
|
||||
|
||||
const SHARED_PERMISSIONS = ALL_TAB_PERMISSIONS.filter(
|
||||
(p) => !CLINIC_ONLY_PERMISSIONS.has(p) && !LAB_ONLY_PERMISSIONS.has(p),
|
||||
|
||||
@@ -14,6 +14,8 @@ export const ALL_TAB_PERMISSIONS = [
|
||||
'TAB_TREATMENT_EDIT',
|
||||
'TAB_CASES_READ',
|
||||
'TAB_CASES_EDIT',
|
||||
'TAB_TASKS_READ',
|
||||
'TAB_TASKS_EDIT',
|
||||
'TAB_BILLING_READ',
|
||||
'TAB_BILLING_EDIT',
|
||||
'TAB_REPORTS_READ',
|
||||
@@ -43,6 +45,7 @@ const EDIT_TO_READ: Record<string, string> = {
|
||||
TAB_ORGANIZATIONS_EDIT: 'TAB_ORGANIZATIONS_READ',
|
||||
TAB_TREATMENT_EDIT: 'TAB_TREATMENT_READ',
|
||||
TAB_CASES_EDIT: 'TAB_CASES_READ',
|
||||
TAB_TASKS_EDIT: 'TAB_TASKS_READ',
|
||||
TAB_BILLING_EDIT: 'TAB_BILLING_READ',
|
||||
TAB_REPORTS_EDIT: 'TAB_REPORTS_READ',
|
||||
};
|
||||
|
||||
@@ -37,6 +37,8 @@ const ALL_PERMISSIONS = [
|
||||
'TAB_TREATMENT_EDIT',
|
||||
'TAB_CASES_READ',
|
||||
'TAB_CASES_EDIT',
|
||||
'TAB_TASKS_READ',
|
||||
'TAB_TASKS_EDIT',
|
||||
'TAB_BILLING_READ',
|
||||
'TAB_BILLING_EDIT',
|
||||
'TAB_REPORTS_READ',
|
||||
|
||||
@@ -50,7 +50,7 @@ export class CasesController {
|
||||
}
|
||||
|
||||
@Patch(':id/tasks/:taskId')
|
||||
@ApiOperation({ summary: 'Update task assignee or status' })
|
||||
@ApiOperation({ summary: 'Update task assignee or priority' })
|
||||
updateTask(
|
||||
@Param('id') id: string,
|
||||
@Param('taskId') taskId: string,
|
||||
|
||||
@@ -200,14 +200,19 @@ export class CasesService {
|
||||
}
|
||||
|
||||
if (dto.assigneeUserId !== undefined && dto.assigneeUserId !== null) {
|
||||
await this.ensureLabMember(dto.assigneeUserId, labOrganizationId);
|
||||
await this.ensureAssignableMember(dto.assigneeUserId, labOrganizationId);
|
||||
}
|
||||
|
||||
const updated = await this.prisma.labCaseTask.update({
|
||||
where: { id: taskId },
|
||||
data: {
|
||||
...(dto.assigneeUserId !== undefined ? { assigneeUserId: dto.assigneeUserId } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status } : {}),
|
||||
...(dto.assigneeUserId !== undefined
|
||||
? {
|
||||
assigneeUserId: dto.assigneeUserId,
|
||||
assignedAt: dto.assigneeUserId === null ? null : new Date(),
|
||||
}
|
||||
: {}),
|
||||
...(dto.priority !== undefined ? { priority: dto.priority } : {}),
|
||||
},
|
||||
include: {
|
||||
assignee: { select: { id: true, name: true, email: true } },
|
||||
@@ -222,18 +227,27 @@ export class CasesService {
|
||||
|
||||
const memberships = await this.prisma.membership.findMany({
|
||||
where: { organizationId: labOrganizationId, isActive: true },
|
||||
include: { user: { select: { id: true, name: true, email: true } } },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
permissions: { include: { permission: true } },
|
||||
},
|
||||
orderBy: [{ isOwner: 'desc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: memberships.map((m) => ({
|
||||
userId: m.user.id,
|
||||
name: m.user.name,
|
||||
email: m.user.email,
|
||||
isOwner: m.isOwner,
|
||||
})),
|
||||
data: memberships
|
||||
.filter((m) => {
|
||||
if (m.isOwner) return true;
|
||||
const names = m.permissions.map((p) => p.permission.name);
|
||||
return names.includes('TAB_TASKS_READ') || names.includes('TAB_TASKS_EDIT');
|
||||
})
|
||||
.map((m) => ({
|
||||
userId: m.user.id,
|
||||
name: m.user.name,
|
||||
email: m.user.email,
|
||||
isOwner: m.isOwner,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -377,7 +391,10 @@ export class CasesService {
|
||||
stepOrder: number;
|
||||
stepLabel: string;
|
||||
status: LabTaskStatus;
|
||||
priority: number;
|
||||
assigneeUserId: string | null;
|
||||
assignedAt: Date | null;
|
||||
createdAt: Date;
|
||||
assignee: { id: string; name: string; email: string } | null;
|
||||
}>,
|
||||
) {
|
||||
@@ -411,7 +428,10 @@ export class CasesService {
|
||||
stepOrder: number;
|
||||
stepLabel: string;
|
||||
status: LabTaskStatus;
|
||||
priority: number;
|
||||
assigneeUserId: string | null;
|
||||
assignedAt: Date | null;
|
||||
createdAt: Date;
|
||||
assignee: { id: string; name: string; email: string } | null;
|
||||
}) {
|
||||
return {
|
||||
@@ -421,6 +441,9 @@ export class CasesService {
|
||||
stepOrder: task.stepOrder,
|
||||
stepLabel: task.stepLabel,
|
||||
status: task.status,
|
||||
priority: task.priority,
|
||||
assignedAt: task.assignedAt?.toISOString() ?? null,
|
||||
createdAt: task.createdAt.toISOString(),
|
||||
assigneeUserId: task.assigneeUserId,
|
||||
assignee: task.assignee
|
||||
? { id: task.assignee.id, name: task.assignee.name, email: task.assignee.email }
|
||||
@@ -428,14 +451,20 @@ export class CasesService {
|
||||
};
|
||||
}
|
||||
|
||||
private async ensureLabMember(userId: string, labOrganizationId: string) {
|
||||
private async ensureAssignableMember(userId: string, labOrganizationId: string) {
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId: labOrganizationId, isActive: true },
|
||||
select: { id: true },
|
||||
include: { permissions: { include: { permission: true } } },
|
||||
});
|
||||
if (!membership) {
|
||||
throw new BadRequestException('Assignee must be an active member of this lab');
|
||||
}
|
||||
if (membership.isOwner) return;
|
||||
const names = membership.permissions.map((p) => p.permission.name);
|
||||
if (names.includes('TAB_TASKS_READ') || names.includes('TAB_TASKS_EDIT')) {
|
||||
return;
|
||||
}
|
||||
throw new BadRequestException('Assignee must have access to the Tasks tab');
|
||||
}
|
||||
|
||||
private async assertCanReadCases(userId: string, organizationId: string) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsDateString, IsEnum, IsInt, IsOptional, IsString, IsUUID, Max, Min, ValidateIf } from 'class-validator';
|
||||
import { LabTaskStatus } from '@prisma/client';
|
||||
import { IsDateString, IsInt, IsOptional, IsString, IsUUID, Max, Min, ValidateIf } from 'class-validator';
|
||||
|
||||
export class UpdateLabCaseTaskDto {
|
||||
@IsOptional()
|
||||
@@ -9,8 +8,11 @@ export class UpdateLabCaseTaskDto {
|
||||
assigneeUserId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LabTaskStatus)
|
||||
status?: LabTaskStatus;
|
||||
@Transform(({ value }) => Number(value))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export class ListLabCasesDto {
|
||||
@@ -46,4 +48,4 @@ export class ListLabCasesDto {
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit = 20;
|
||||
}
|
||||
}
|
||||
|
||||
23
backend/src/modules/tasks/dto/tasks.dto.ts
Normal file
23
backend/src/modules/tasks/dto/tasks.dto.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IsEnum, IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { LabTaskStatus } from '@prisma/client';
|
||||
|
||||
export class UpdateLabTaskDto {
|
||||
@IsEnum(LabTaskStatus)
|
||||
status: LabTaskStatus;
|
||||
}
|
||||
|
||||
export class ListLabTasksDto {
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => Number(value))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page = 1;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => Number(value))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit = 50;
|
||||
}
|
||||
32
backend/src/modules/tasks/tasks.controller.ts
Normal file
32
backend/src/modules/tasks/tasks.controller.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Body, Controller, Get, Param, Patch, Query, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { LabOrgGuard } from '../../common/guards/lab-org.guard';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { ListLabTasksDto, UpdateLabTaskDto } from './dto/tasks.dto';
|
||||
import { TasksService } from './tasks.service';
|
||||
|
||||
@ApiTags('tasks')
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@UseGuards(JwtAuthGuard, LabOrgGuard)
|
||||
@Controller('tasks')
|
||||
export class TasksController {
|
||||
constructor(private readonly tasksService: TasksService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'List lab tasks (owner: all, staff: assigned only)' })
|
||||
list(@Query() query: ListLabTasksDto, @Req() req) {
|
||||
const organizationId = this.tasksService.getOrganizationIdFromUser(req.user);
|
||||
return this.tasksService.list(organizationId, req.user.id, query);
|
||||
}
|
||||
|
||||
@Patch(':taskId')
|
||||
@ApiOperation({ summary: 'Update task status' })
|
||||
updateStatus(
|
||||
@Param('taskId') taskId: string,
|
||||
@Body() dto: UpdateLabTaskDto,
|
||||
@Req() req,
|
||||
) {
|
||||
const organizationId = this.tasksService.getOrganizationIdFromUser(req.user);
|
||||
return this.tasksService.updateStatus(taskId, dto, organizationId, req.user.id);
|
||||
}
|
||||
}
|
||||
9
backend/src/modules/tasks/tasks.module.ts
Normal file
9
backend/src/modules/tasks/tasks.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TasksController } from './tasks.controller';
|
||||
import { TasksService } from './tasks.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TasksController],
|
||||
providers: [TasksService],
|
||||
})
|
||||
export class TasksModule {}
|
||||
188
backend/src/modules/tasks/tasks.service.ts
Normal file
188
backend/src/modules/tasks/tasks.service.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { LabTaskStatus, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { ListLabTasksDto, UpdateLabTaskDto } from './dto/tasks.dto';
|
||||
|
||||
const taskListInclude = {
|
||||
assignee: { select: { id: true, name: true, email: true } },
|
||||
labCase: {
|
||||
include: {
|
||||
treatment: {
|
||||
include: {
|
||||
organization: { select: { id: true, name: true } },
|
||||
patient: { select: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.LabCaseTaskInclude;
|
||||
|
||||
@Injectable()
|
||||
export class TasksService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
getOrganizationIdFromUser(user: { organizationId?: string }) {
|
||||
if (!user?.organizationId) {
|
||||
throw new BadRequestException('Organization is not selected');
|
||||
}
|
||||
return user.organizationId;
|
||||
}
|
||||
|
||||
async list(labOrganizationId: string, actorUserId: string, query: ListLabTasksDto) {
|
||||
await this.assertCanReadTasks(actorUserId, labOrganizationId);
|
||||
|
||||
const membership = await this.getMembership(actorUserId, labOrganizationId);
|
||||
if (!membership) {
|
||||
throw new ForbiddenException('You are not a member of this organization');
|
||||
}
|
||||
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(Math.max(query.limit ?? 50, 1), 100);
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: Prisma.LabCaseTaskWhereInput = {
|
||||
labCase: {
|
||||
sentAt: { not: null },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
},
|
||||
...(membership.isOwner ? {} : { assigneeUserId: actorUserId }),
|
||||
};
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.labCaseTask.findMany({
|
||||
where,
|
||||
include: taskListInclude,
|
||||
orderBy: [
|
||||
{ assignedAt: { sort: 'desc', nulls: 'first' } },
|
||||
{ createdAt: 'desc' },
|
||||
{ labCaseId: 'asc' },
|
||||
{ priority: 'desc' },
|
||||
{ stepOrder: 'asc' },
|
||||
{ id: 'asc' },
|
||||
],
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.labCaseTask.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: items.map((task) => this.mapTaskListItem(task)),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.max(1, Math.ceil(total / limit)),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async updateStatus(
|
||||
taskId: string,
|
||||
dto: UpdateLabTaskDto,
|
||||
labOrganizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertCanEditTasks(actorUserId, labOrganizationId);
|
||||
|
||||
const membership = await this.getMembership(actorUserId, labOrganizationId);
|
||||
if (!membership) {
|
||||
throw new ForbiddenException('You are not a member of this organization');
|
||||
}
|
||||
|
||||
const task = await this.prisma.labCaseTask.findFirst({
|
||||
where: {
|
||||
id: taskId,
|
||||
labCase: {
|
||||
sentAt: { not: null },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
},
|
||||
},
|
||||
include: taskListInclude,
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundException('Task not found');
|
||||
}
|
||||
|
||||
if (!membership.isOwner && task.assigneeUserId !== actorUserId) {
|
||||
throw new ForbiddenException('You can only update tasks assigned to you');
|
||||
}
|
||||
|
||||
const updated = await this.prisma.labCaseTask.update({
|
||||
where: { id: taskId },
|
||||
data: { status: dto.status },
|
||||
include: taskListInclude,
|
||||
});
|
||||
|
||||
return { success: true, data: this.mapTaskListItem(updated) };
|
||||
}
|
||||
|
||||
private mapTaskListItem(
|
||||
task: Prisma.LabCaseTaskGetPayload<{ include: typeof taskListInclude }>,
|
||||
) {
|
||||
return {
|
||||
id: task.id,
|
||||
labCaseId: task.labCaseId,
|
||||
tooth: task.tooth,
|
||||
treatmentType: task.treatmentType,
|
||||
stepOrder: task.stepOrder,
|
||||
stepLabel: task.stepLabel,
|
||||
status: task.status,
|
||||
priority: task.priority,
|
||||
assignedAt: task.assignedAt?.toISOString() ?? null,
|
||||
createdAt: task.createdAt.toISOString(),
|
||||
assigneeUserId: task.assigneeUserId,
|
||||
assignee: task.assignee
|
||||
? { id: task.assignee.id, name: task.assignee.name, email: task.assignee.email }
|
||||
: null,
|
||||
clinic: task.labCase.treatment.organization,
|
||||
patient: {
|
||||
id: task.labCase.treatment.patient.id,
|
||||
firstName: task.labCase.treatment.patient.firstName,
|
||||
lastName: task.labCase.treatment.patient.lastName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async assertCanReadTasks(userId: string, organizationId: string) {
|
||||
const m = await this.getMembership(userId, organizationId);
|
||||
if (!m) {
|
||||
throw new ForbiddenException('You are not a member of this organization');
|
||||
}
|
||||
if (m.isOwner) return;
|
||||
const names = m.permissions.map((p) => p.permission.name);
|
||||
if (names.includes('TAB_TASKS_READ') || names.includes('TAB_TASKS_EDIT')) {
|
||||
return;
|
||||
}
|
||||
throw new ForbiddenException('You do not have access to tasks');
|
||||
}
|
||||
|
||||
private async assertCanEditTasks(userId: string, organizationId: string) {
|
||||
const m = await this.getMembership(userId, organizationId);
|
||||
if (!m) {
|
||||
throw new ForbiddenException('You are not a member of this organization');
|
||||
}
|
||||
if (m.isOwner) return;
|
||||
const names = m.permissions.map((p) => p.permission.name);
|
||||
if (names.includes('TAB_TASKS_EDIT')) {
|
||||
return;
|
||||
}
|
||||
throw new ForbiddenException('You cannot update tasks');
|
||||
}
|
||||
|
||||
private async getMembership(userId: string, organizationId: string) {
|
||||
return this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId, isActive: true },
|
||||
include: { permissions: { include: { permission: true } } },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user