improvement: added a history action button to connected orgs row inordr to see a brief report of the relevant cases between two orgs and the status of each related task.
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { LabOrgGuard } from '../../common/guards/lab-org.guard';
|
||||
import { TreatmentCatalogModule } from '../treatment-catalog/treatment-catalog.module';
|
||||
import { CasesController } from './cases.controller';
|
||||
import { CasesService } from './cases.service';
|
||||
|
||||
@Module({
|
||||
imports: [TreatmentCatalogModule],
|
||||
controllers: [CasesController],
|
||||
providers: [CasesService, PrismaService, LabOrgGuard],
|
||||
exports: [CasesService],
|
||||
})
|
||||
export class CasesModule {}
|
||||
|
||||
@@ -156,6 +156,86 @@ export class CasesService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Cases exchanged between one clinic and one lab (Organizations connection history). */
|
||||
async listBetweenOrganizations(
|
||||
clinicOrganizationId: string,
|
||||
labOrganizationId: string,
|
||||
query: ListLabCasesDto,
|
||||
) {
|
||||
if (query.treatmentType) {
|
||||
this.treatmentCatalog.assertKnownTreatmentType(query.treatmentType);
|
||||
}
|
||||
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(Math.max(query.limit ?? 20, 1), 100);
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: Prisma.LabCaseWhereInput = {
|
||||
...this.buildListWhere(labOrganizationId, query),
|
||||
treatment: { organizationId: clinicOrganizationId },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
};
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.labCase.findMany({
|
||||
where,
|
||||
include: {
|
||||
treatment: {
|
||||
include: {
|
||||
organization: { select: { id: true, name: true } },
|
||||
patient: { select: { id: true, firstName: true, lastName: true, mobile: true } },
|
||||
},
|
||||
},
|
||||
details: {
|
||||
include: {
|
||||
detail: { select: { treatmentType: true } },
|
||||
},
|
||||
},
|
||||
tasks: { select: { id: true, status: true } },
|
||||
},
|
||||
orderBy: [{ sentAt: 'desc' }],
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.labCase.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: items.map((lc) => this.mapLabCaseListItem(lc)),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.max(1, Math.ceil(total / limit)),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getOneBetweenOrganizations(
|
||||
labCaseId: string,
|
||||
clinicOrganizationId: string,
|
||||
labOrganizationId: string,
|
||||
) {
|
||||
const labCase = await this.prisma.labCase.findFirst({
|
||||
where: {
|
||||
id: labCaseId,
|
||||
sentAt: { not: null },
|
||||
treatment: { organizationId: clinicOrganizationId },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
},
|
||||
include: labCaseListInclude,
|
||||
});
|
||||
|
||||
if (!labCase) {
|
||||
throw new NotFoundException('Case not found');
|
||||
}
|
||||
|
||||
return { success: true, data: this.mapLabCaseDetail(labCase) };
|
||||
}
|
||||
|
||||
async getOne(labCaseId: string, labOrganizationId: string, actorUserId: string) {
|
||||
await this.assertCanReadCases(actorUserId, labOrganizationId);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { InviteOrganizationDto } from './dto/invite-organization.dto';
|
||||
import { PreviewOrganizationInviteDto } from './dto/preview-organization-invite.dto';
|
||||
import { RespondConnectionRequestDto } from './dto/respond-connection-request.dto';
|
||||
import { OrganizationService } from './organization.service';
|
||||
import { ListLabCasesDto } from '../cases/dto/cases.dto';
|
||||
|
||||
/**
|
||||
* Counterpart orgs (clinic↔lab).
|
||||
@@ -121,6 +122,40 @@ export class OrganizationController {
|
||||
return this.organizationService.deleteConnection(req.user.id, organizationId, connectionId);
|
||||
}
|
||||
|
||||
@Get('connections/:connectionId/cases')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'List cases exchanged with a connected organization' })
|
||||
listConnectionCases(
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
@Param('connectionId') connectionId: string,
|
||||
@Query() query: ListLabCasesDto,
|
||||
) {
|
||||
const organizationId = this.organizationService.getOrganizationIdFromUser(req.user);
|
||||
return this.organizationService.listConnectionCases(
|
||||
req.user.id,
|
||||
organizationId,
|
||||
connectionId,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('connections/:connectionId/cases/:caseId')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Get one case exchanged with a connected organization' })
|
||||
getConnectionCase(
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
@Param('connectionId') connectionId: string,
|
||||
@Param('caseId') caseId: string,
|
||||
) {
|
||||
const organizationId = this.organizationService.getOrganizationIdFromUser(req.user);
|
||||
return this.organizationService.getConnectionCase(
|
||||
req.user.id,
|
||||
organizationId,
|
||||
connectionId,
|
||||
caseId,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('invitations/:invitationId/link')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Get a shareable invite link for a pending invitation' })
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { CasesModule } from '../cases/cases.module';
|
||||
import { OrganizationController } from './organization.controller';
|
||||
import { OrganizationService } from './organization.service';
|
||||
|
||||
@Module({
|
||||
imports: [CasesModule],
|
||||
controllers: [OrganizationController],
|
||||
providers: [OrganizationService, PrismaService],
|
||||
})
|
||||
|
||||
@@ -9,6 +9,8 @@ import { LinkStatus } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { ListLabCasesDto } from '../cases/dto/cases.dto';
|
||||
import { CasesService } from '../cases/cases.service';
|
||||
import { AcceptOrganizationInviteDto } from './dto/accept-organization-invite.dto';
|
||||
import { CreateConnectionRequestDto } from './dto/create-connection-request.dto';
|
||||
import { InviteOrganizationDto } from './dto/invite-organization.dto';
|
||||
@@ -28,7 +30,10 @@ import { RespondConnectionRequestDto } from './dto/respond-connection-request.dt
|
||||
*/
|
||||
@Injectable()
|
||||
export class OrganizationService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly casesService: CasesService,
|
||||
) {}
|
||||
|
||||
getOrganizationIdFromUser(user: { organizationId?: string }) {
|
||||
if (!user?.organizationId) {
|
||||
@@ -340,6 +345,64 @@ export class OrganizationService {
|
||||
};
|
||||
}
|
||||
|
||||
async listConnectionCases(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
connectionId: string,
|
||||
query: ListLabCasesDto,
|
||||
) {
|
||||
const actor = await this.getActorMembership(userId, organizationId);
|
||||
if (!actor || !this.canEditOrganizations(actor)) {
|
||||
throw new ForbiddenException('You do not have permission to manage organizations');
|
||||
}
|
||||
|
||||
const { clinicOrganizationId, labOrganizationId, counterpart } =
|
||||
await this.resolveActiveConnectionParties(connectionId, organizationId, actor);
|
||||
|
||||
const result = await this.casesService.listBetweenOrganizations(
|
||||
clinicOrganizationId,
|
||||
labOrganizationId,
|
||||
query,
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: {
|
||||
...result.data,
|
||||
counterpart,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getConnectionCase(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
connectionId: string,
|
||||
caseId: string,
|
||||
) {
|
||||
const actor = await this.getActorMembership(userId, organizationId);
|
||||
if (!actor || !this.canEditOrganizations(actor)) {
|
||||
throw new ForbiddenException('You do not have permission to manage organizations');
|
||||
}
|
||||
|
||||
const { clinicOrganizationId, labOrganizationId, counterpart } =
|
||||
await this.resolveActiveConnectionParties(connectionId, organizationId, actor);
|
||||
|
||||
const result = await this.casesService.getOneBetweenOrganizations(
|
||||
caseId,
|
||||
clinicOrganizationId,
|
||||
labOrganizationId,
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: {
|
||||
...result.data,
|
||||
counterpart,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Re-issue a shareable URL for a pending invitation (rotates token; previous URL stops working). */
|
||||
async getInvitationLink(userId: string, organizationId: string, invitationId: string) {
|
||||
const actor = await this.getActorMembership(userId, organizationId);
|
||||
@@ -640,6 +703,58 @@ export class OrganizationService {
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveActiveConnectionParties(
|
||||
connectionId: string,
|
||||
organizationId: string,
|
||||
actor: {
|
||||
organization: { type: { name: string } };
|
||||
},
|
||||
) {
|
||||
const link = await this.prisma.organizationLink.findFirst({
|
||||
where: {
|
||||
id: connectionId,
|
||||
status: LinkStatus.ACTIVE,
|
||||
OR: [{ organizationAId: organizationId }, { organizationBId: organizationId }],
|
||||
},
|
||||
include: {
|
||||
organizationA: { select: { id: true, name: true, type: true } },
|
||||
organizationB: { select: { id: true, name: true, type: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!link) {
|
||||
throw new NotFoundException('Connected organization not found');
|
||||
}
|
||||
|
||||
const counterpart =
|
||||
link.organizationAId === organizationId ? link.organizationB : link.organizationA;
|
||||
|
||||
const orgType = actor.organization.type.name;
|
||||
if (orgType === 'CLINIC') {
|
||||
if (counterpart.type.name !== 'LAB') {
|
||||
throw new BadRequestException('Counterpart organization is not a lab');
|
||||
}
|
||||
return {
|
||||
clinicOrganizationId: organizationId,
|
||||
labOrganizationId: counterpart.id,
|
||||
counterpart: { id: counterpart.id, name: counterpart.name },
|
||||
};
|
||||
}
|
||||
|
||||
if (orgType === 'LAB') {
|
||||
if (counterpart.type.name !== 'CLINIC') {
|
||||
throw new BadRequestException('Counterpart organization is not a clinic');
|
||||
}
|
||||
return {
|
||||
clinicOrganizationId: counterpart.id,
|
||||
labOrganizationId: organizationId,
|
||||
counterpart: { id: counterpart.id, name: counterpart.name },
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestException('Unknown organization type');
|
||||
}
|
||||
|
||||
private async getActorMembership(userId: string, organizationId: string) {
|
||||
return this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId },
|
||||
|
||||
Reference in New Issue
Block a user