Case completion and tasks completion gadgets added.
This commit is contained in:
@@ -30,20 +30,29 @@ type StackedDayBucket = {
|
||||
received: number;
|
||||
};
|
||||
|
||||
type CaseCompletionChart = {
|
||||
type CompletionGaugeChart = {
|
||||
completed: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
};
|
||||
|
||||
type PartnerCasesBucket = {
|
||||
code: string;
|
||||
label: string;
|
||||
completed: number;
|
||||
pending: number;
|
||||
};
|
||||
|
||||
type TodayCharts = {
|
||||
treatmentMixWeek?: ChartBucket[];
|
||||
tasksByProsthesis?: ChartBucket[];
|
||||
appointmentsByProvider?: ChartBucket[];
|
||||
caseCompletion?: CaseCompletionChart;
|
||||
caseCompletion?: CompletionGaugeChart;
|
||||
treatmentPlanCompletion?: CompletionGaugeChart;
|
||||
appointmentsWeekAll?: ChartBucket[];
|
||||
appointmentsWeekMine?: ChartBucket[];
|
||||
labTaskActivityWeek?: StackedDayBucket[];
|
||||
casePartnersMonth?: PartnerCasesBucket[];
|
||||
efficiencyReport?: ChartBucket[];
|
||||
};
|
||||
|
||||
@@ -138,6 +147,27 @@ export class TodayService {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.canEditTreatment(membership.isOwner, permissionNames)) {
|
||||
tasks.push(
|
||||
this.loadCasePartnersMonth(
|
||||
'CLINIC',
|
||||
organizationId,
|
||||
userId,
|
||||
!membership.isOwner,
|
||||
to,
|
||||
charts,
|
||||
),
|
||||
);
|
||||
tasks.push(
|
||||
this.loadTreatmentPlanCompletion(
|
||||
organizationId,
|
||||
userId,
|
||||
!membership.isOwner,
|
||||
charts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.canViewTreatment(membership.isOwner, permissionNames)) {
|
||||
tasks.push(
|
||||
this.loadTreatmentsToday(organizationId, from, to, widgets),
|
||||
@@ -198,6 +228,19 @@ export class TodayService {
|
||||
tasks.push(this.loadCaseCompletion(organizationId, charts));
|
||||
}
|
||||
|
||||
if (this.canEditCases(membership.isOwner, permissionNames)) {
|
||||
tasks.push(
|
||||
this.loadCasePartnersMonth(
|
||||
'LAB',
|
||||
organizationId,
|
||||
userId,
|
||||
false,
|
||||
to,
|
||||
charts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.canViewTasks(membership.isOwner, permissionNames)) {
|
||||
tasks.push(this.loadTasksInProgress(organizationId, widgets));
|
||||
tasks.push(this.loadImportantTasks(organizationId, widgets));
|
||||
@@ -515,10 +558,10 @@ export class TodayService {
|
||||
widgets.pendingConnections = { count };
|
||||
}
|
||||
|
||||
private async getActiveEditAccessUserIds(
|
||||
private async getActiveEditAccessMembers(
|
||||
organizationId: string,
|
||||
editPermission: 'TAB_TREATMENT_EDIT' | 'TAB_TASKS_EDIT',
|
||||
): Promise<string[]> {
|
||||
): Promise<Array<{ userId: string; isOwner: boolean }>> {
|
||||
const members = await this.prisma.membership.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
@@ -533,10 +576,42 @@ export class TodayService {
|
||||
},
|
||||
],
|
||||
},
|
||||
select: { userId: true },
|
||||
select: { userId: true, isOwner: true },
|
||||
});
|
||||
|
||||
return members.map((member) => member.userId);
|
||||
return members.map((member) => ({
|
||||
userId: member.userId,
|
||||
isOwner: member.isOwner,
|
||||
}));
|
||||
}
|
||||
|
||||
private async buildEfficiencyReportRows(
|
||||
members: Array<{ userId: string; isOwner: boolean }>,
|
||||
countsByUser: Map<string, number>,
|
||||
): Promise<ChartBucket[] | undefined> {
|
||||
if (members.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const userIds = members.map((member) => member.userId);
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const nameById = new Map(users.map((user) => [user.id, user.name]));
|
||||
|
||||
const rows = members
|
||||
.map((member) => ({
|
||||
code: member.userId,
|
||||
label: nameById.get(member.userId) ?? member.userId,
|
||||
count: countsByUser.get(member.userId) ?? 0,
|
||||
isOwner: member.isOwner,
|
||||
}))
|
||||
.filter((row) => !row.isOwner || row.count > 0)
|
||||
.map(({ code, label, count }) => ({ code, label, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
return rows.length >= 2 ? rows : undefined;
|
||||
}
|
||||
|
||||
private async loadClinicEfficiencyReport(
|
||||
@@ -544,13 +619,10 @@ export class TodayService {
|
||||
rangeEnd: Date,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const eligibleUserIds = await this.getActiveEditAccessUserIds(
|
||||
const members = await this.getActiveEditAccessMembers(
|
||||
organizationId,
|
||||
'TAB_TREATMENT_EDIT',
|
||||
);
|
||||
if (eligibleUserIds.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monthStart = new Date(rangeEnd.getTime() - 30 * 86_400_000);
|
||||
const grouped = await this.prisma.treatment.groupBy({
|
||||
@@ -558,31 +630,22 @@ export class TodayService {
|
||||
where: {
|
||||
organizationId,
|
||||
treatmentAt: { gte: monthStart, lt: rangeEnd },
|
||||
providerUserId: { in: eligibleUserIds },
|
||||
providerUserId: { in: members.map((member) => member.userId) },
|
||||
},
|
||||
_count: { _all: true },
|
||||
});
|
||||
|
||||
const countsByUser = new Map(
|
||||
eligibleUserIds.map((userId) => [userId, 0]),
|
||||
members.map((member) => [member.userId, 0]),
|
||||
);
|
||||
for (const row of grouped) {
|
||||
countsByUser.set(row.providerUserId, aggregateCount(row._count));
|
||||
}
|
||||
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: { id: { in: eligibleUserIds } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const nameById = new Map(users.map((user) => [user.id, user.name]));
|
||||
|
||||
charts.efficiencyReport = eligibleUserIds
|
||||
.map((userId) => ({
|
||||
code: userId,
|
||||
label: nameById.get(userId) ?? userId,
|
||||
count: countsByUser.get(userId) ?? 0,
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
const report = await this.buildEfficiencyReportRows(members, countsByUser);
|
||||
if (report) {
|
||||
charts.efficiencyReport = report;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadLabEfficiencyReport(
|
||||
@@ -590,13 +653,10 @@ export class TodayService {
|
||||
rangeEnd: Date,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const eligibleUserIds = await this.getActiveEditAccessUserIds(
|
||||
const members = await this.getActiveEditAccessMembers(
|
||||
labOrganizationId,
|
||||
'TAB_TASKS_EDIT',
|
||||
);
|
||||
if (eligibleUserIds.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monthStart = new Date(rangeEnd.getTime() - 30 * 86_400_000);
|
||||
const grouped = await this.prisma.labCaseTaskStatusEvent.groupBy({
|
||||
@@ -604,7 +664,7 @@ export class TodayService {
|
||||
where: {
|
||||
toStatus: LabTaskStatus.COMPLETED,
|
||||
changedAt: { gte: monthStart, lt: rangeEnd },
|
||||
changedByUserId: { in: eligibleUserIds },
|
||||
changedByUserId: { in: members.map((member) => member.userId) },
|
||||
task: {
|
||||
labCase: {
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
@@ -615,26 +675,17 @@ export class TodayService {
|
||||
});
|
||||
|
||||
const countsByUser = new Map(
|
||||
eligibleUserIds.map((userId) => [userId, 0]),
|
||||
members.map((member) => [member.userId, 0]),
|
||||
);
|
||||
for (const row of grouped) {
|
||||
if (!row.changedByUserId) continue;
|
||||
countsByUser.set(row.changedByUserId, aggregateCount(row._count));
|
||||
}
|
||||
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: { id: { in: eligibleUserIds } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const nameById = new Map(users.map((user) => [user.id, user.name]));
|
||||
|
||||
charts.efficiencyReport = eligibleUserIds
|
||||
.map((userId) => ({
|
||||
code: userId,
|
||||
label: nameById.get(userId) ?? userId,
|
||||
count: countsByUser.get(userId) ?? 0,
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
const report = await this.buildEfficiencyReportRows(members, countsByUser);
|
||||
if (report) {
|
||||
charts.efficiencyReport = report;
|
||||
}
|
||||
}
|
||||
|
||||
private async buildSubscriptionWidget(
|
||||
@@ -845,13 +896,42 @@ export class TodayService {
|
||||
select: { status: true },
|
||||
});
|
||||
|
||||
const total = tasks.length;
|
||||
const completed = tasks.filter(
|
||||
(task) => task.status === LabTaskStatus.COMPLETED,
|
||||
).length;
|
||||
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
charts.caseCompletion = this.buildCompletionGauge(
|
||||
tasks.filter((task) => task.status === LabTaskStatus.COMPLETED).length,
|
||||
tasks.length,
|
||||
);
|
||||
}
|
||||
|
||||
charts.caseCompletion = { completed, total, percent };
|
||||
private async loadTreatmentPlanCompletion(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
scopeToUser: boolean,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const appointmentWhere = {
|
||||
organizationId,
|
||||
...(scopeToUser ? { providerUserId: userId } : {}),
|
||||
};
|
||||
|
||||
const [total, completed] = await Promise.all([
|
||||
this.prisma.appointment.count({ where: appointmentWhere }),
|
||||
this.prisma.appointment.count({
|
||||
where: {
|
||||
...appointmentWhere,
|
||||
treatment: { details: { some: {} } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
charts.treatmentPlanCompletion = this.buildCompletionGauge(completed, total);
|
||||
}
|
||||
|
||||
private buildCompletionGauge(completed: number, total: number): CompletionGaugeChart {
|
||||
return {
|
||||
completed,
|
||||
total,
|
||||
percent: total > 0 ? Math.round((completed / total) * 100) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
private async loadAppointmentsWeekAll(
|
||||
@@ -986,6 +1066,86 @@ export class TodayService {
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadCasePartnersMonth(
|
||||
orgType: 'CLINIC' | 'LAB',
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
scopeToUser: boolean,
|
||||
rangeEnd: Date,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const rangeStart = new Date(rangeEnd.getTime() - 30 * 86_400_000);
|
||||
|
||||
const cases = await this.prisma.labCase.findMany({
|
||||
where: {
|
||||
sentAt: { gte: rangeStart, lt: rangeEnd },
|
||||
...(orgType === 'CLINIC'
|
||||
? {
|
||||
destinationOrganizationId: { not: null },
|
||||
treatment: {
|
||||
organizationId,
|
||||
...(scopeToUser ? { providerUserId: userId } : {}),
|
||||
},
|
||||
}
|
||||
: {
|
||||
sends: { some: { organizationId } },
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
destinationOrganizationId: true,
|
||||
tasks: { select: { status: true } },
|
||||
treatment: { select: { organizationId: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const countsByPartner = new Map<string, { completed: number; total: number }>();
|
||||
|
||||
for (const labCase of cases) {
|
||||
const partnerId =
|
||||
orgType === 'CLINIC'
|
||||
? labCase.destinationOrganizationId
|
||||
: labCase.treatment.organizationId;
|
||||
if (!partnerId) continue;
|
||||
|
||||
const isCompleted =
|
||||
labCase.tasks.length > 0 &&
|
||||
labCase.tasks.every((task) => task.status === LabTaskStatus.COMPLETED);
|
||||
|
||||
const entry = countsByPartner.get(partnerId) ?? { completed: 0, total: 0 };
|
||||
entry.total += 1;
|
||||
if (isCompleted) entry.completed += 1;
|
||||
countsByPartner.set(partnerId, entry);
|
||||
}
|
||||
|
||||
if (countsByPartner.size === 0) {
|
||||
charts.casePartnersMonth = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = [...countsByPartner.entries()]
|
||||
.map(([code, counts]) => ({
|
||||
code,
|
||||
completed: counts.completed,
|
||||
pending: counts.total - counts.completed,
|
||||
total: counts.total,
|
||||
}))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.slice(0, 8);
|
||||
|
||||
const partners = await this.prisma.organization.findMany({
|
||||
where: { id: { in: sorted.map((row) => row.code) } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const nameById = new Map(partners.map((org) => [org.id, org.name]));
|
||||
|
||||
charts.casePartnersMonth = sorted.map((row) => ({
|
||||
code: row.code,
|
||||
label: nameById.get(row.code) ?? row.code,
|
||||
completed: row.completed,
|
||||
pending: row.pending,
|
||||
}));
|
||||
}
|
||||
|
||||
private getRequesterOrganizationId(sharedDataTypes: unknown): string | null {
|
||||
if (!sharedDataTypes || typeof sharedDataTypes !== 'object') {
|
||||
return null;
|
||||
@@ -1086,6 +1246,16 @@ export class TodayService {
|
||||
);
|
||||
}
|
||||
|
||||
private canEditTreatment(isOwner: boolean, names: string[]): boolean {
|
||||
if (isOwner) return true;
|
||||
return names.includes('TAB_TREATMENT_EDIT');
|
||||
}
|
||||
|
||||
private canEditCases(isOwner: boolean, names: string[]): boolean {
|
||||
if (isOwner) return true;
|
||||
return names.includes('TAB_CASES_EDIT');
|
||||
}
|
||||
|
||||
private canManageOrganizations(isOwner: boolean, names: string[]): boolean {
|
||||
if (isOwner) return true;
|
||||
return names.includes('TAB_ORGANIZATIONS_EDIT');
|
||||
|
||||
Reference in New Issue
Block a user