improvement: time period dropdown added to appropriate dashboard charts.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsISO8601, IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||
import { IsIn, IsISO8601, IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
const WEEK_MONTH = ['week', 'month'] as const;
|
||||
const WEEK_MONTH_YEAR = ['week', 'month', 'year'] as const;
|
||||
|
||||
export class TodaySummaryQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Start of the local day range (ISO 8601). Defaults to UTC midnight today.',
|
||||
@@ -30,4 +33,39 @@ export class TodaySummaryQueryDto {
|
||||
@Min(-840)
|
||||
@Max(840)
|
||||
utcOffsetMinutes?: number;
|
||||
|
||||
@ApiPropertyOptional({ enum: WEEK_MONTH })
|
||||
@IsOptional()
|
||||
@IsIn(WEEK_MONTH)
|
||||
appointmentsWeekPeriod?: 'week' | 'month';
|
||||
|
||||
@ApiPropertyOptional({ enum: WEEK_MONTH })
|
||||
@IsOptional()
|
||||
@IsIn(WEEK_MONTH)
|
||||
appointmentsWeekMinePeriod?: 'week' | 'month';
|
||||
|
||||
@ApiPropertyOptional({ enum: WEEK_MONTH })
|
||||
@IsOptional()
|
||||
@IsIn(WEEK_MONTH)
|
||||
labTaskActivityPeriod?: 'week' | 'month';
|
||||
|
||||
@ApiPropertyOptional({ enum: WEEK_MONTH })
|
||||
@IsOptional()
|
||||
@IsIn(WEEK_MONTH)
|
||||
casesDuePeriod?: 'week' | 'month';
|
||||
|
||||
@ApiPropertyOptional({ enum: WEEK_MONTH_YEAR })
|
||||
@IsOptional()
|
||||
@IsIn(WEEK_MONTH_YEAR)
|
||||
treatmentMixPeriod?: 'week' | 'month' | 'year';
|
||||
|
||||
@ApiPropertyOptional({ enum: WEEK_MONTH_YEAR })
|
||||
@IsOptional()
|
||||
@IsIn(WEEK_MONTH_YEAR)
|
||||
casePartnersPeriod?: 'week' | 'month' | 'year';
|
||||
|
||||
@ApiPropertyOptional({ enum: WEEK_MONTH_YEAR })
|
||||
@IsOptional()
|
||||
@IsIn(WEEK_MONTH_YEAR)
|
||||
efficiencyPeriod?: 'week' | 'month' | 'year';
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ export class TodayService {
|
||||
organizationId,
|
||||
to,
|
||||
query.utcOffsetMinutes,
|
||||
resolveWeekMonthPeriod(query.appointmentsWeekPeriod, 'week'),
|
||||
charts,
|
||||
),
|
||||
);
|
||||
@@ -158,6 +159,7 @@ export class TodayService {
|
||||
userId,
|
||||
!membership.isOwner,
|
||||
to,
|
||||
resolveWeekMonthYearPeriod(query.casePartnersPeriod, 'month'),
|
||||
charts,
|
||||
),
|
||||
);
|
||||
@@ -177,7 +179,13 @@ export class TodayService {
|
||||
);
|
||||
tasks.push(this.loadLabCasesPendingSend(organizationId, widgets));
|
||||
tasks.push(
|
||||
this.loadTreatmentMixWeek(organizationId, to, locale, charts),
|
||||
this.loadTreatmentMixWeek(
|
||||
organizationId,
|
||||
to,
|
||||
locale,
|
||||
resolveWeekMonthYearPeriod(query.treatmentMixPeriod, 'week'),
|
||||
charts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,6 +205,7 @@ export class TodayService {
|
||||
userId,
|
||||
to,
|
||||
query.utcOffsetMinutes,
|
||||
resolveWeekMonthPeriod(query.appointmentsWeekMinePeriod, 'week'),
|
||||
charts,
|
||||
),
|
||||
);
|
||||
@@ -234,6 +243,7 @@ export class TodayService {
|
||||
organizationId,
|
||||
from,
|
||||
query.utcOffsetMinutes,
|
||||
resolveWeekMonthPeriod(query.casesDuePeriod, 'week'),
|
||||
charts,
|
||||
),
|
||||
);
|
||||
@@ -247,6 +257,7 @@ export class TodayService {
|
||||
userId,
|
||||
false,
|
||||
to,
|
||||
resolveWeekMonthYearPeriod(query.casePartnersPeriod, 'month'),
|
||||
charts,
|
||||
),
|
||||
);
|
||||
@@ -266,6 +277,7 @@ export class TodayService {
|
||||
organizationId,
|
||||
to,
|
||||
query.utcOffsetMinutes,
|
||||
resolveWeekMonthPeriod(query.labTaskActivityPeriod, 'week'),
|
||||
charts,
|
||||
),
|
||||
);
|
||||
@@ -279,11 +291,19 @@ export class TodayService {
|
||||
}
|
||||
|
||||
if (membership.isOwner) {
|
||||
const efficiencyPeriod = resolveWeekMonthYearPeriod(
|
||||
query.efficiencyPeriod,
|
||||
'month',
|
||||
);
|
||||
if (orgType === 'CLINIC') {
|
||||
tasks.push(this.loadClinicEfficiencyReport(organizationId, to, charts));
|
||||
tasks.push(
|
||||
this.loadClinicEfficiencyReport(organizationId, to, efficiencyPeriod, charts),
|
||||
);
|
||||
}
|
||||
if (orgType === 'LAB') {
|
||||
tasks.push(this.loadLabEfficiencyReport(organizationId, to, charts));
|
||||
tasks.push(
|
||||
this.loadLabEfficiencyReport(organizationId, to, efficiencyPeriod, charts),
|
||||
);
|
||||
}
|
||||
tasks.push(
|
||||
this.buildSubscriptionWidget(organizationId, membership.organization).then(
|
||||
@@ -318,15 +338,18 @@ export class TodayService {
|
||||
organizationId: string,
|
||||
rangeEnd: Date,
|
||||
locale: CatalogLocale,
|
||||
period: ChartPeriodWeekMonthYear,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const weekStart = new Date(rangeEnd.getTime() - 7 * 86_400_000);
|
||||
const windowStart = new Date(
|
||||
rangeEnd.getTime() - daysForChartPeriod(period) * 86_400_000,
|
||||
);
|
||||
const grouped = await this.prisma.treatmentDetail.groupBy({
|
||||
by: ['treatmentType'],
|
||||
where: {
|
||||
treatment: {
|
||||
organizationId,
|
||||
treatmentAt: { gte: weekStart, lt: rangeEnd },
|
||||
treatmentAt: { gte: windowStart, lt: rangeEnd },
|
||||
},
|
||||
},
|
||||
_count: { _all: true },
|
||||
@@ -647,6 +670,7 @@ export class TodayService {
|
||||
private async loadClinicEfficiencyReport(
|
||||
organizationId: string,
|
||||
rangeEnd: Date,
|
||||
period: ChartPeriodWeekMonthYear,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const members = await this.getActiveEditAccessMembers(
|
||||
@@ -654,12 +678,14 @@ export class TodayService {
|
||||
'TAB_TREATMENT_EDIT',
|
||||
);
|
||||
|
||||
const monthStart = new Date(rangeEnd.getTime() - 30 * 86_400_000);
|
||||
const windowStart = new Date(
|
||||
rangeEnd.getTime() - daysForChartPeriod(period) * 86_400_000,
|
||||
);
|
||||
const grouped = await this.prisma.treatment.groupBy({
|
||||
by: ['providerUserId'],
|
||||
where: {
|
||||
organizationId,
|
||||
treatmentAt: { gte: monthStart, lt: rangeEnd },
|
||||
treatmentAt: { gte: windowStart, lt: rangeEnd },
|
||||
providerUserId: { in: members.map((member) => member.userId) },
|
||||
},
|
||||
_count: { _all: true },
|
||||
@@ -681,6 +707,7 @@ export class TodayService {
|
||||
private async loadLabEfficiencyReport(
|
||||
labOrganizationId: string,
|
||||
rangeEnd: Date,
|
||||
period: ChartPeriodWeekMonthYear,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const members = await this.getActiveEditAccessMembers(
|
||||
@@ -688,12 +715,14 @@ export class TodayService {
|
||||
'TAB_TASKS_EDIT',
|
||||
);
|
||||
|
||||
const monthStart = new Date(rangeEnd.getTime() - 30 * 86_400_000);
|
||||
const windowStart = new Date(
|
||||
rangeEnd.getTime() - daysForChartPeriod(period) * 86_400_000,
|
||||
);
|
||||
const grouped = await this.prisma.labCaseTaskStatusEvent.groupBy({
|
||||
by: ['changedByUserId'],
|
||||
where: {
|
||||
toStatus: LabTaskStatus.COMPLETED,
|
||||
changedAt: { gte: monthStart, lt: rangeEnd },
|
||||
changedAt: { gte: windowStart, lt: rangeEnd },
|
||||
changedByUserId: { in: members.map((member) => member.userId) },
|
||||
task: {
|
||||
labCase: {
|
||||
@@ -926,9 +955,15 @@ export class TodayService {
|
||||
labOrganizationId: string,
|
||||
rangeStart: Date,
|
||||
utcOffsetMinutes: number | undefined,
|
||||
period: ChartPeriodWeekMonth,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const dayBuckets = buildNextSevenLocalDayBuckets(rangeStart, utcOffsetMinutes);
|
||||
const dayBuckets = buildLocalDayBuckets(
|
||||
rangeStart,
|
||||
daysForChartPeriod(period),
|
||||
'forward',
|
||||
utcOffsetMinutes,
|
||||
);
|
||||
const weekStart = dayBuckets[0]?.start ?? rangeStart;
|
||||
const weekEnd = dayBuckets[dayBuckets.length - 1]?.end ?? rangeStart;
|
||||
const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;
|
||||
@@ -1034,12 +1069,14 @@ export class TodayService {
|
||||
organizationId: string,
|
||||
rangeEnd: Date,
|
||||
utcOffsetMinutes: number | undefined,
|
||||
period: ChartPeriodWeekMonth,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
charts.appointmentsWeekAll = await this.loadAppointmentsWeekSeries(
|
||||
organizationId,
|
||||
rangeEnd,
|
||||
utcOffsetMinutes,
|
||||
period,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1048,12 +1085,14 @@ export class TodayService {
|
||||
providerUserId: string,
|
||||
rangeEnd: Date,
|
||||
utcOffsetMinutes: number | undefined,
|
||||
period: ChartPeriodWeekMonth,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
charts.appointmentsWeekMine = await this.loadAppointmentsWeekSeries(
|
||||
organizationId,
|
||||
rangeEnd,
|
||||
utcOffsetMinutes,
|
||||
period,
|
||||
providerUserId,
|
||||
);
|
||||
}
|
||||
@@ -1062,9 +1101,15 @@ export class TodayService {
|
||||
organizationId: string,
|
||||
rangeEnd: Date,
|
||||
utcOffsetMinutes: number | undefined,
|
||||
period: ChartPeriodWeekMonth,
|
||||
providerUserId?: string,
|
||||
): Promise<ChartBucket[]> {
|
||||
const dayBuckets = buildLastSevenLocalDayBuckets(rangeEnd, utcOffsetMinutes);
|
||||
const dayBuckets = buildLocalDayBuckets(
|
||||
rangeEnd,
|
||||
daysForChartPeriod(period),
|
||||
'lookback',
|
||||
utcOffsetMinutes,
|
||||
);
|
||||
const weekStart = dayBuckets[0]?.start ?? rangeEnd;
|
||||
const weekEnd = rangeEnd;
|
||||
|
||||
@@ -1101,9 +1146,15 @@ export class TodayService {
|
||||
labOrganizationId: string,
|
||||
rangeEnd: Date,
|
||||
utcOffsetMinutes: number | undefined,
|
||||
period: ChartPeriodWeekMonth,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const dayBuckets = buildLastSevenLocalDayBuckets(rangeEnd, utcOffsetMinutes);
|
||||
const dayBuckets = buildLocalDayBuckets(
|
||||
rangeEnd,
|
||||
daysForChartPeriod(period),
|
||||
'lookback',
|
||||
utcOffsetMinutes,
|
||||
);
|
||||
const weekStart = dayBuckets[0]?.start ?? rangeEnd;
|
||||
const weekEnd = rangeEnd;
|
||||
const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;
|
||||
@@ -1168,9 +1219,12 @@ export class TodayService {
|
||||
userId: string,
|
||||
scopeToUser: boolean,
|
||||
rangeEnd: Date,
|
||||
period: ChartPeriodWeekMonthYear,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const rangeStart = new Date(rangeEnd.getTime() - 30 * 86_400_000);
|
||||
const rangeStart = new Date(
|
||||
rangeEnd.getTime() - daysForChartPeriod(period) * 86_400_000,
|
||||
);
|
||||
|
||||
const cases = await this.prisma.labCase.findMany({
|
||||
where: {
|
||||
@@ -1363,51 +1417,64 @@ function aggregateCount(
|
||||
|
||||
type LocalDayBucket = { code: string; label: string; start: Date; end: Date };
|
||||
|
||||
function buildLastSevenLocalDayBuckets(
|
||||
rangeEnd: Date,
|
||||
type ChartPeriodWeekMonth = 'week' | 'month';
|
||||
type ChartPeriodWeekMonthYear = 'week' | 'month' | 'year';
|
||||
|
||||
function daysForChartPeriod(period: ChartPeriodWeekMonthYear): 7 | 30 | 365 {
|
||||
switch (period) {
|
||||
case 'month':
|
||||
return 30;
|
||||
case 'year':
|
||||
return 365;
|
||||
default:
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveWeekMonthPeriod(
|
||||
value: string | undefined,
|
||||
fallback: ChartPeriodWeekMonth,
|
||||
): ChartPeriodWeekMonth {
|
||||
return value === 'week' || value === 'month' ? value : fallback;
|
||||
}
|
||||
|
||||
function resolveWeekMonthYearPeriod(
|
||||
value: string | undefined,
|
||||
fallback: ChartPeriodWeekMonthYear,
|
||||
): ChartPeriodWeekMonthYear {
|
||||
return value === 'week' || value === 'month' || value === 'year' ? value : fallback;
|
||||
}
|
||||
|
||||
function buildLocalDayBuckets(
|
||||
anchor: Date,
|
||||
count: number,
|
||||
direction: 'lookback' | 'forward',
|
||||
utcOffsetMinutes?: number,
|
||||
): LocalDayBucket[] {
|
||||
const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;
|
||||
const dayMs = 86_400_000;
|
||||
const buckets: LocalDayBucket[] = [];
|
||||
|
||||
for (let index = 0; index < 7; index += 1) {
|
||||
const start = new Date(rangeEnd.getTime() - (7 - index) * dayMs);
|
||||
const end = new Date(start.getTime() + dayMs);
|
||||
const code = localDayKeyFromDate(start, offsetMs);
|
||||
buckets.push({
|
||||
code,
|
||||
label: code,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
if (direction === 'lookback') {
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const start = new Date(anchor.getTime() - (count - index) * dayMs);
|
||||
const end = new Date(start.getTime() + dayMs);
|
||||
const code = localDayKeyFromDate(start, offsetMs);
|
||||
buckets.push({ code, label: code, start, end });
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
function buildNextSevenLocalDayBuckets(
|
||||
rangeStart: Date,
|
||||
utcOffsetMinutes?: number,
|
||||
): LocalDayBucket[] {
|
||||
const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;
|
||||
const dayMs = 86_400_000;
|
||||
const localMs = rangeStart.getTime() + offsetMs;
|
||||
const localMs = anchor.getTime() + offsetMs;
|
||||
const local = new Date(localMs);
|
||||
local.setUTCHours(0, 0, 0, 0);
|
||||
const dayStart = new Date(local.getTime() - offsetMs);
|
||||
const buckets: LocalDayBucket[] = [];
|
||||
|
||||
for (let index = 0; index < 7; index += 1) {
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const start = new Date(dayStart.getTime() + index * dayMs);
|
||||
const end = new Date(start.getTime() + dayMs);
|
||||
const code = localDayKeyFromDate(start, offsetMs);
|
||||
buckets.push({
|
||||
code,
|
||||
label: code,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
buckets.push({ code, label: code, start, end });
|
||||
}
|
||||
|
||||
return buckets;
|
||||
|
||||
Reference in New Issue
Block a user