improvement: users can now comment on a case and it's details and have an option to make it visible for clinics too.
This commit is contained in:
@@ -447,7 +447,6 @@ export class CasesService {
|
||||
return {
|
||||
id: lc.id,
|
||||
sentAt: lc.sentAt?.toISOString() ?? null,
|
||||
labComment: lc.labComment,
|
||||
clinic: lc.treatment.organization,
|
||||
patient: lc.treatment.patient,
|
||||
appointmentStartAt: lc.treatment.appointment?.startAt.toISOString() ?? null,
|
||||
|
||||
@@ -74,11 +74,11 @@ export class LabCaseCommentsService {
|
||||
return { success: true, data: this.mapComment(updated, LabCaseCommentSide.LAB) };
|
||||
}
|
||||
|
||||
// ---------- Clinic side (connection access is validated by caller) ----------
|
||||
// ---------- Clinic side (connection history) ----------
|
||||
|
||||
async listForClinic(caseId: string, clinicOrganizationId: string) {
|
||||
await this.assertClinicOwnsCase(caseId, clinicOrganizationId);
|
||||
const comments = await this.fetchComments(caseId, { visibleOnly: true });
|
||||
await this.assertClinicOwnsCase(caseId, clinicOrganizationId, { requireSent: true });
|
||||
const comments = await this.fetchCommentsForClinicViewer(caseId);
|
||||
return {
|
||||
success: true,
|
||||
data: comments.map((c) => this.mapComment(c, LabCaseCommentSide.CLINIC)),
|
||||
@@ -91,7 +91,7 @@ export class LabCaseCommentsService {
|
||||
actorUserId: string,
|
||||
dto: CreateLabCaseCommentDto,
|
||||
) {
|
||||
await this.assertClinicOwnsCase(caseId, clinicOrganizationId);
|
||||
await this.assertClinicOwnsCase(caseId, clinicOrganizationId, { requireSent: true });
|
||||
const created = await this.prisma.labCaseComment.create({
|
||||
data: {
|
||||
labCaseId: caseId,
|
||||
@@ -99,7 +99,6 @@ export class LabCaseCommentsService {
|
||||
authorOrganizationId: clinicOrganizationId,
|
||||
authorSide: LabCaseCommentSide.CLINIC,
|
||||
body: dto.body.trim(),
|
||||
// Clinic-authored comments are inherently visible to the clinic.
|
||||
visibleToClinic: true,
|
||||
},
|
||||
include: commentInclude,
|
||||
@@ -107,8 +106,60 @@ export class LabCaseCommentsService {
|
||||
return { success: true, data: this.mapComment(created, LabCaseCommentSide.CLINIC) };
|
||||
}
|
||||
|
||||
// ---------- Clinic side (treatment dispatch — unsent cases allowed) ----------
|
||||
|
||||
async listForClinicTreatmentCase(
|
||||
caseId: string,
|
||||
clinicOrganizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertClinicTreatmentAccess(caseId, clinicOrganizationId, actorUserId);
|
||||
const comments = await this.fetchCommentsForClinicViewer(caseId);
|
||||
return {
|
||||
success: true,
|
||||
data: comments.map((c) => this.mapComment(c, LabCaseCommentSide.CLINIC)),
|
||||
};
|
||||
}
|
||||
|
||||
async addForClinicTreatmentCase(
|
||||
caseId: string,
|
||||
clinicOrganizationId: string,
|
||||
actorUserId: string,
|
||||
dto: CreateLabCaseCommentDto,
|
||||
) {
|
||||
await this.assertClinicTreatmentAccess(caseId, clinicOrganizationId, actorUserId);
|
||||
const created = await this.prisma.labCaseComment.create({
|
||||
data: {
|
||||
labCaseId: caseId,
|
||||
authorUserId: actorUserId,
|
||||
authorOrganizationId: clinicOrganizationId,
|
||||
authorSide: LabCaseCommentSide.CLINIC,
|
||||
body: dto.body.trim(),
|
||||
visibleToClinic: true,
|
||||
},
|
||||
include: commentInclude,
|
||||
});
|
||||
return { success: true, data: this.mapComment(created, LabCaseCommentSide.CLINIC) };
|
||||
}
|
||||
|
||||
async countForCase(caseId: string) {
|
||||
const count = await this.prisma.labCaseComment.count({ where: { labCaseId: caseId } });
|
||||
return { success: true, data: { count } };
|
||||
}
|
||||
|
||||
// ---------- Helpers ----------
|
||||
|
||||
private fetchCommentsForClinicViewer(caseId: string) {
|
||||
return this.prisma.labCaseComment.findMany({
|
||||
where: {
|
||||
labCaseId: caseId,
|
||||
OR: [{ visibleToClinic: true }, { authorSide: LabCaseCommentSide.CLINIC }],
|
||||
},
|
||||
include: commentInclude,
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
private fetchComments(caseId: string, opts?: { visibleOnly?: boolean }) {
|
||||
return this.prisma.labCaseComment.findMany({
|
||||
where: {
|
||||
@@ -121,6 +172,7 @@ export class LabCaseCommentsService {
|
||||
}
|
||||
|
||||
private mapComment(comment: CommentWithRelations, viewerSide: LabCaseCommentSide) {
|
||||
const showVisibilityStatus = viewerSide === LabCaseCommentSide.LAB;
|
||||
return {
|
||||
id: comment.id,
|
||||
body: comment.body,
|
||||
@@ -129,10 +181,10 @@ export class LabCaseCommentsService {
|
||||
authorOrganizationName: comment.authorOrganization?.name ?? null,
|
||||
visibleToClinic: comment.visibleToClinic,
|
||||
createdAt: comment.createdAt.toISOString(),
|
||||
// Only lab viewers can toggle visibility, and only on lab-authored comments.
|
||||
canToggleVisibility:
|
||||
viewerSide === LabCaseCommentSide.LAB &&
|
||||
comment.authorSide === LabCaseCommentSide.LAB,
|
||||
showVisibilityStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -167,11 +219,15 @@ export class LabCaseCommentsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async assertClinicOwnsCase(caseId: string, clinicOrganizationId: string) {
|
||||
private async assertClinicOwnsCase(
|
||||
caseId: string,
|
||||
clinicOrganizationId: string,
|
||||
opts?: { requireSent?: boolean },
|
||||
) {
|
||||
const labCase = await this.prisma.labCase.findFirst({
|
||||
where: {
|
||||
id: caseId,
|
||||
sentAt: { not: null },
|
||||
...(opts?.requireSent ? { sentAt: { not: null } } : {}),
|
||||
treatment: { organizationId: clinicOrganizationId },
|
||||
},
|
||||
select: { id: true },
|
||||
@@ -180,4 +236,25 @@ export class LabCaseCommentsService {
|
||||
throw new NotFoundException('Case not found');
|
||||
}
|
||||
}
|
||||
|
||||
private async assertClinicTreatmentAccess(
|
||||
caseId: string,
|
||||
clinicOrganizationId: string,
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertClinicOwnsCase(caseId, clinicOrganizationId);
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { userId: actorUserId, organizationId: clinicOrganizationId, isActive: true },
|
||||
include: { permissions: { include: { permission: true } } },
|
||||
});
|
||||
if (!membership) {
|
||||
throw new ForbiddenException('You are not a member of this organization');
|
||||
}
|
||||
if (membership.isOwner) return;
|
||||
const names = membership.permissions.map((p) => p.permission.name);
|
||||
if (names.includes('TAB_TREATMENT_READ') || names.includes('TAB_TREATMENT_EDIT')) {
|
||||
return;
|
||||
}
|
||||
throw new ForbiddenException('You do not have access to treatment cases');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +71,6 @@ export class SaveLabCaseDto {
|
||||
@IsUUID()
|
||||
destinationOrganizationId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(5000)
|
||||
labComment?: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsUUID(undefined, { each: true })
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
SaveTreatmentDraftDto,
|
||||
SaveTreatmentLabCasesDto,
|
||||
} from './dto/treatment.dto';
|
||||
import { CreateLabCaseCommentDto } from '../lab-case-comments/dto/lab-case-comment.dto';
|
||||
import { LabCaseCommentsService } from '../lab-case-comments/lab-case-comments.service';
|
||||
import { TreatmentsService } from './treatments.service';
|
||||
|
||||
@ApiTags('treatments')
|
||||
@@ -30,7 +32,10 @@ import { TreatmentsService } from './treatments.service';
|
||||
@UseGuards(JwtAuthGuard, ClinicOrgGuard)
|
||||
@Controller('treatments')
|
||||
export class TreatmentsController {
|
||||
constructor(private readonly treatmentsService: TreatmentsService) {}
|
||||
constructor(
|
||||
private readonly treatmentsService: TreatmentsService,
|
||||
private readonly commentsService: LabCaseCommentsService,
|
||||
) {}
|
||||
|
||||
@Get('linked-organizations')
|
||||
@ApiOperation({ summary: 'List active linked counterpart organizations (TAB_TREATMENT_READ)' })
|
||||
@@ -194,4 +199,34 @@ export class TreatmentsController {
|
||||
req.user.language,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('lab-cases/:labCaseId/comments')
|
||||
@ApiOperation({ summary: 'List comments for a lab case during treatment dispatch' })
|
||||
listLabCaseComments(
|
||||
@Param('labCaseId') labCaseId: string,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.commentsService.listForClinicTreatmentCase(
|
||||
labCaseId,
|
||||
organizationId,
|
||||
req.user.id,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('lab-cases/:labCaseId/comments')
|
||||
@ApiOperation({ summary: 'Add a comment to a lab case during treatment dispatch' })
|
||||
addLabCaseComment(
|
||||
@Param('labCaseId') labCaseId: string,
|
||||
@Body() dto: CreateLabCaseCommentDto,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.commentsService.addForClinicTreatmentCase(
|
||||
labCaseId,
|
||||
organizationId,
|
||||
req.user.id,
|
||||
dto,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { ClinicOrgGuard } from '../../common/guards/clinic-org.guard';
|
||||
import { ProsthesisCatalogModule } from '../prosthesis-catalog/prosthesis-catalog.module';
|
||||
import { LabCaseCommentsModule } from '../lab-case-comments/lab-case-comments.module';
|
||||
import { TreatmentsController } from './treatments.controller';
|
||||
import { TreatmentsService } from './treatments.service';
|
||||
|
||||
@Module({
|
||||
imports: [ProsthesisCatalogModule],
|
||||
imports: [ProsthesisCatalogModule, LabCaseCommentsModule],
|
||||
controllers: [TreatmentsController],
|
||||
providers: [TreatmentsService, PrismaService, ClinicOrgGuard],
|
||||
})
|
||||
|
||||
@@ -397,7 +397,6 @@ export class TreatmentsService {
|
||||
clientKey: lc.clientId,
|
||||
sortOrder: index,
|
||||
destinationOrganizationId: lc.destinationOrganizationId ?? null,
|
||||
labComment: lc.labComment?.trim() || null,
|
||||
},
|
||||
})
|
||||
: await tx.labCase.create({
|
||||
@@ -406,7 +405,6 @@ export class TreatmentsService {
|
||||
clientKey: lc.clientId,
|
||||
sortOrder: index,
|
||||
destinationOrganizationId: lc.destinationOrganizationId ?? null,
|
||||
labComment: lc.labComment?.trim() || null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -675,7 +673,6 @@ export class TreatmentsService {
|
||||
clientKey: string | null;
|
||||
sortOrder: number;
|
||||
destinationOrganizationId: string | null;
|
||||
labComment: string | null;
|
||||
sentAt: Date | null;
|
||||
details: Array<{
|
||||
treatmentDetailId: string;
|
||||
@@ -754,7 +751,6 @@ export class TreatmentsService {
|
||||
clientKey?: string | null;
|
||||
sortOrder?: number;
|
||||
destinationOrganizationId?: string | null;
|
||||
labComment?: string | null;
|
||||
sentAt?: Date | null;
|
||||
details?: Array<{
|
||||
treatmentDetailId: string;
|
||||
@@ -775,7 +771,6 @@ export class TreatmentsService {
|
||||
id: lc.id,
|
||||
clientId: lc.clientKey ?? lc.id,
|
||||
destinationOrganizationId: lc.destinationOrganizationId ?? null,
|
||||
labComment: lc.labComment ?? null,
|
||||
sentAt: lc.sentAt?.toISOString() ?? null,
|
||||
treatmentDetailIds: lc.details?.map((d) => d.treatmentDetailId) ?? [],
|
||||
details: (lc.details ?? []).map((d) => ({
|
||||
|
||||
Reference in New Issue
Block a user