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:
2026-07-07 17:14:31 +03:30
parent cb63ced4e3
commit b2d40b3e97
21 changed files with 467 additions and 269 deletions

View File

@@ -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');
}
}