improvement: some new gadgets added to dashboard. some new functionality added to existed gadgets.
This commit is contained in:
@@ -2,12 +2,12 @@ import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { LabOrgGuard } from '../../common/guards/lab-org.guard';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { TreatmentCatalogModule } from '../treatment-catalog/treatment-catalog.module';
|
||||
import { ProsthesisCatalogModule } from '../prosthesis-catalog/prosthesis-catalog.module';
|
||||
import { CasesController } from './cases.controller';
|
||||
import { CasesService } from './cases.service';
|
||||
|
||||
@Module({
|
||||
imports: [TreatmentCatalogModule, NotificationsModule],
|
||||
imports: [ProsthesisCatalogModule, NotificationsModule],
|
||||
controllers: [CasesController],
|
||||
providers: [CasesService, PrismaService, LabOrgGuard],
|
||||
exports: [CasesService],
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CatalogLabelService,
|
||||
normalizeCatalogLocale,
|
||||
} from '../catalog/catalog-label.service';
|
||||
import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.service';
|
||||
import { ProsthesisCatalogService } from '../prosthesis-catalog/prosthesis-catalog.service';
|
||||
import { normalizeTeeth } from '../treatments/treatment.utils';
|
||||
import { ListLabCasesDto, UpdateLabCaseImportantDto, AssignLabCaseTaskDto } from './dto/cases.dto';
|
||||
import {
|
||||
@@ -92,7 +92,7 @@ type LabCaseTaskWithRelations = Prisma.LabCaseTaskGetPayload<{
|
||||
export class CasesService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly treatmentCatalog: TreatmentCatalogService,
|
||||
private readonly prosthesisCatalog: ProsthesisCatalogService,
|
||||
private readonly catalogLabels: CatalogLabelService,
|
||||
private readonly labCaseActivity: LabCaseActivityService,
|
||||
) {}
|
||||
@@ -107,8 +107,8 @@ export class CasesService {
|
||||
async list(labOrganizationId: string, actorUserId: string, query: ListLabCasesDto) {
|
||||
await this.assertCanReadCases(actorUserId, labOrganizationId);
|
||||
|
||||
if (query.treatmentType) {
|
||||
this.treatmentCatalog.assertKnownTreatmentType(query.treatmentType);
|
||||
if (query.prosthesisTypeCode) {
|
||||
this.prosthesisCatalog.assertKnownProsthesisType(query.prosthesisTypeCode);
|
||||
}
|
||||
|
||||
const page = query.page ?? 1;
|
||||
@@ -127,12 +127,7 @@ export class CasesService {
|
||||
patient: { select: { id: true, firstName: true, lastName: true, mobile: true } },
|
||||
},
|
||||
},
|
||||
details: {
|
||||
include: {
|
||||
detail: { select: { treatmentType: true } },
|
||||
},
|
||||
},
|
||||
tasks: { select: { id: true, status: true } },
|
||||
tasks: { select: { id: true, status: true, prosthesisTypeCode: true, teeth: true } },
|
||||
},
|
||||
orderBy: [{ sentAt: 'desc' }],
|
||||
skip,
|
||||
@@ -169,43 +164,48 @@ export class CasesService {
|
||||
async listFilterOptions(labOrganizationId: string, actorUserId: string) {
|
||||
await this.assertCanReadCases(actorUserId, labOrganizationId);
|
||||
|
||||
const rows = await this.prisma.labCase.findMany({
|
||||
where: {
|
||||
sentAt: { not: null },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
},
|
||||
select: {
|
||||
treatment: {
|
||||
select: {
|
||||
organization: { select: { id: true, name: true } },
|
||||
const [clinicRows, taskRows] = await Promise.all([
|
||||
this.prisma.labCase.findMany({
|
||||
where: {
|
||||
sentAt: { not: null },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
},
|
||||
select: {
|
||||
treatment: {
|
||||
select: {
|
||||
organization: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
details: {
|
||||
select: { detail: { select: { treatmentType: true } } },
|
||||
}),
|
||||
this.prisma.labCaseTask.findMany({
|
||||
where: {
|
||||
labCase: {
|
||||
sentAt: { not: null },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
select: { prosthesisTypeCode: true },
|
||||
distinct: ['prosthesisTypeCode'],
|
||||
}),
|
||||
]);
|
||||
|
||||
const clinicsById = new Map<string, { id: string; name: string }>();
|
||||
const typeCodes = new Set<string>();
|
||||
|
||||
for (const row of rows) {
|
||||
for (const row of clinicRows) {
|
||||
clinicsById.set(row.treatment.organization.id, row.treatment.organization);
|
||||
for (const link of row.details) {
|
||||
typeCodes.add(link.detail.treatmentType);
|
||||
}
|
||||
}
|
||||
|
||||
const catalog = await this.treatmentCatalog.list();
|
||||
const treatmentTypes = catalog
|
||||
.filter((entry) => entry.labDependent && typeCodes.has(entry.code))
|
||||
.map((entry) => ({ code: entry.code, labDependent: entry.labDependent }));
|
||||
const typeCodes = new Set(taskRows.map((row) => row.prosthesisTypeCode));
|
||||
const catalog = await this.prosthesisCatalog.list();
|
||||
const prosthesisTypes = catalog
|
||||
.filter((entry) => typeCodes.has(entry.code))
|
||||
.map((entry) => ({ code: entry.code }));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
clinics: [...clinicsById.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
treatmentTypes,
|
||||
prosthesisTypes,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -216,8 +216,8 @@ export class CasesService {
|
||||
labOrganizationId: string,
|
||||
query: ListLabCasesDto,
|
||||
) {
|
||||
if (query.treatmentType) {
|
||||
this.treatmentCatalog.assertKnownTreatmentType(query.treatmentType);
|
||||
if (query.prosthesisTypeCode) {
|
||||
this.prosthesisCatalog.assertKnownProsthesisType(query.prosthesisTypeCode);
|
||||
}
|
||||
|
||||
const page = query.page ?? 1;
|
||||
@@ -240,12 +240,7 @@ export class CasesService {
|
||||
patient: { select: { id: true, firstName: true, lastName: true, mobile: true } },
|
||||
},
|
||||
},
|
||||
details: {
|
||||
include: {
|
||||
detail: { select: { treatmentType: true } },
|
||||
},
|
||||
},
|
||||
tasks: { select: { id: true, status: true } },
|
||||
tasks: { select: { id: true, status: true, prosthesisTypeCode: true, teeth: true } },
|
||||
},
|
||||
orderBy: [{ sentAt: 'desc' }],
|
||||
skip,
|
||||
@@ -516,10 +511,10 @@ export class CasesService {
|
||||
...(query.clinicOrganizationId
|
||||
? { treatment: { organizationId: query.clinicOrganizationId } }
|
||||
: {}),
|
||||
...(query.treatmentType
|
||||
...(query.prosthesisTypeCode
|
||||
? {
|
||||
details: {
|
||||
some: { detail: { treatmentType: query.treatmentType } },
|
||||
tasks: {
|
||||
some: { prosthesisTypeCode: query.prosthesisTypeCode },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@@ -565,10 +560,14 @@ export class CasesService {
|
||||
organization: { id: string; name: string };
|
||||
patient: { id: string; firstName: string; lastName: string; mobile: string };
|
||||
};
|
||||
details: Array<{ detail: { treatmentType: string } }>;
|
||||
tasks: Array<{ id: string; status: LabTaskStatus }>;
|
||||
tasks: Array<{
|
||||
id: string;
|
||||
status: LabTaskStatus;
|
||||
prosthesisTypeCode: string;
|
||||
teeth: Prisma.JsonValue;
|
||||
}>;
|
||||
}) {
|
||||
const treatmentType = lc.details[0]?.detail.treatmentType ?? null;
|
||||
const prosthesisGroups = this.buildProsthesisGroupsFromTasks(lc.tasks);
|
||||
const completedTasks = lc.tasks.filter((t) => t.status === LabTaskStatus.COMPLETED).length;
|
||||
|
||||
return {
|
||||
@@ -584,7 +583,7 @@ export class CasesService {
|
||||
lastName: lc.treatment.patient.lastName,
|
||||
mobile: lc.treatment.patient.mobile,
|
||||
},
|
||||
treatmentType,
|
||||
prosthesisGroups,
|
||||
taskProgress: {
|
||||
completed: completedTasks,
|
||||
total: lc.tasks.length,
|
||||
@@ -651,6 +650,29 @@ export class CasesService {
|
||||
};
|
||||
}
|
||||
|
||||
private buildProsthesisGroupsFromTasks(
|
||||
tasks: Array<{ prosthesisTypeCode: string; teeth: Prisma.JsonValue }>,
|
||||
): Array<{ prosthesisTypeCode: string; teeth: string[] }> {
|
||||
const prosthesisByCode = new Map<string, string[]>();
|
||||
|
||||
for (const task of tasks) {
|
||||
if (!task.prosthesisTypeCode) continue;
|
||||
const teeth = normalizeTaskTeeth(task.teeth);
|
||||
const list = prosthesisByCode.get(task.prosthesisTypeCode) ?? [];
|
||||
list.push(...teeth);
|
||||
prosthesisByCode.set(task.prosthesisTypeCode, list);
|
||||
}
|
||||
|
||||
return [...prosthesisByCode.entries()]
|
||||
.map(([prosthesisTypeCode, teeth]) => ({
|
||||
prosthesisTypeCode,
|
||||
teeth: [...new Set(teeth)].sort((a, b) =>
|
||||
a.localeCompare(b, undefined, { numeric: true }),
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.prosthesisTypeCode.localeCompare(b.prosthesisTypeCode));
|
||||
}
|
||||
|
||||
private groupTasks(
|
||||
tasks: LabCaseTaskWithRelations[],
|
||||
prosthesisLabels: Map<string, string>,
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ListLabCasesDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
treatmentType?: string;
|
||||
prosthesisTypeCode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
|
||||
@@ -94,6 +94,17 @@ export class ListLabTasksDto {
|
||||
@IsBoolean()
|
||||
overdue?: boolean;
|
||||
|
||||
/** When true, only tasks with no assignee. */
|
||||
@IsOptional()
|
||||
@Transform(toBoolean)
|
||||
@IsBoolean()
|
||||
unassignedOnly?: boolean;
|
||||
|
||||
/** Narrow list to a single prosthesis type code. */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
prosthesisTypeCode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['date', 'status', 'clinic', 'patient', 'important', 'prosthesis', 'taskType', 'dueDate'])
|
||||
sortBy?: TaskSortField;
|
||||
@@ -172,6 +183,17 @@ export class LocateTaskPageDto {
|
||||
@IsBoolean()
|
||||
overdue?: boolean;
|
||||
|
||||
/** When true, only tasks with no assignee. */
|
||||
@IsOptional()
|
||||
@Transform(toBoolean)
|
||||
@IsBoolean()
|
||||
unassignedOnly?: boolean;
|
||||
|
||||
/** Narrow list to a single prosthesis type code. */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
prosthesisTypeCode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['date', 'status', 'clinic', 'patient', 'important', 'prosthesis', 'taskType', 'dueDate'])
|
||||
sortBy?: TaskSortField;
|
||||
|
||||
@@ -337,6 +337,10 @@ export class TasksService {
|
||||
},
|
||||
...(status !== undefined ? { status } : {}),
|
||||
...(query.assignedToMe ? { assigneeUserId: actorUserId } : {}),
|
||||
...(query.unassignedOnly ? { assigneeUserId: null } : {}),
|
||||
...(query.prosthesisTypeCode?.trim()
|
||||
? { prosthesisTypeCode: query.prosthesisTypeCode.trim() }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const stepCompleted = query.stepCompleted?.trim();
|
||||
@@ -385,6 +389,8 @@ export class TasksService {
|
||||
stepCompleted: query.stepCompleted,
|
||||
assignedToMe: query.assignedToMe,
|
||||
overdue: query.overdue,
|
||||
unassignedOnly: query.unassignedOnly,
|
||||
prosthesisTypeCode: query.prosthesisTypeCode,
|
||||
sortBy: query.sortBy,
|
||||
sortDir: query.sortDir,
|
||||
limit: query.limit,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type CatalogLocale,
|
||||
} from '../catalog/catalog-label.service';
|
||||
import { StaffWorkingHoursService } from '../staff/staff-working-hours.service';
|
||||
import { startOfUtcDay } from '../../common/lab-case-due-date';
|
||||
import { TodaySummaryQueryDto } from './dto/today-summary-query.dto';
|
||||
|
||||
type ChartBucket = { code: string; label: string; count: number };
|
||||
@@ -52,6 +53,7 @@ type TodayCharts = {
|
||||
appointmentsWeekMine?: ChartBucket[];
|
||||
labTaskActivityWeek?: StackedDayBucket[];
|
||||
casePartnersMonth?: PartnerCasesBucket[];
|
||||
casesDueWeek?: ChartBucket[];
|
||||
efficiencyReport?: ChartBucket[];
|
||||
};
|
||||
|
||||
@@ -88,9 +90,11 @@ type TodayWidgets = {
|
||||
casesInProgress?: { count: number };
|
||||
tasksInProgress?: { count: number };
|
||||
importantTasks?: { count: number };
|
||||
overdueCases?: { count: number };
|
||||
unassignedTasks?: { count: number };
|
||||
pendingConnections?: { count: number };
|
||||
pendingStaffInvites?: { count: number };
|
||||
providersWithoutWorkingHours?: { count: number };
|
||||
providersWithoutWorkingHours?: { count: number; membershipIds: string[] };
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -225,6 +229,14 @@ export class TodayService {
|
||||
);
|
||||
tasks.push(this.loadCasesInProgress(organizationId, widgets));
|
||||
tasks.push(this.loadCaseCompletion(organizationId, charts));
|
||||
tasks.push(
|
||||
this.loadCasesDueWeek(
|
||||
organizationId,
|
||||
from,
|
||||
query.utcOffsetMinutes,
|
||||
charts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.canEditCases(membership.isOwner, permissionNames)) {
|
||||
@@ -243,6 +255,8 @@ export class TodayService {
|
||||
if (this.canViewTasks(membership.isOwner, permissionNames)) {
|
||||
tasks.push(this.loadTasksInProgress(organizationId, widgets));
|
||||
tasks.push(this.loadImportantTasks(organizationId, widgets));
|
||||
tasks.push(this.loadOverdueCases(organizationId, widgets));
|
||||
tasks.push(this.loadUnassignedTasks(organizationId, widgets));
|
||||
tasks.push(this.loadTasksByProsthesis(organizationId, locale, charts));
|
||||
}
|
||||
|
||||
@@ -540,6 +554,32 @@ export class TodayService {
|
||||
widgets.importantTasks = { count };
|
||||
}
|
||||
|
||||
private async loadOverdueCases(labOrganizationId: string, widgets: TodayWidgets) {
|
||||
const count = await this.prisma.labCase.count({
|
||||
where: {
|
||||
sentAt: { not: null },
|
||||
dueDate: { not: null, lt: startOfUtcDay() },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
tasks: { some: { status: LabTaskStatus.IN_PROGRESS } },
|
||||
},
|
||||
});
|
||||
widgets.overdueCases = { count };
|
||||
}
|
||||
|
||||
private async loadUnassignedTasks(labOrganizationId: string, widgets: TodayWidgets) {
|
||||
const count = await this.prisma.labCaseTask.count({
|
||||
where: {
|
||||
status: LabTaskStatus.IN_PROGRESS,
|
||||
assigneeUserId: null,
|
||||
labCase: {
|
||||
sentAt: { not: null },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
},
|
||||
},
|
||||
});
|
||||
widgets.unassignedTasks = { count };
|
||||
}
|
||||
|
||||
private async loadPendingConnections(organizationId: string, widgets: TodayWidgets) {
|
||||
const links = await this.prisma.organizationLink.findMany({
|
||||
where: {
|
||||
@@ -772,17 +812,47 @@ export class TodayService {
|
||||
widgets.pendingStaffInvites = { count };
|
||||
}
|
||||
|
||||
private async listTreatmentParticipatingMembers(organizationId: string) {
|
||||
return this.prisma.membership.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
OR: [{ isOwner: true }, { isActive: true }],
|
||||
permissions: {
|
||||
some: {
|
||||
permission: { name: 'TAB_TREATMENT_EDIT' },
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
isOwner: true,
|
||||
user: { select: { id: true, name: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async loadAppointmentsByProvider(
|
||||
organizationId: string,
|
||||
from: Date,
|
||||
to: Date,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const members = await this.listTreatmentParticipatingMembers(organizationId);
|
||||
|
||||
if (members.length === 0) {
|
||||
charts.appointmentsByProvider = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const participatingUserIds = new Set(members.map((member) => member.user.id));
|
||||
const ownerMember = members.find((member) => member.isOwner);
|
||||
|
||||
const appointments = await this.prisma.appointment.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
startAt: { lt: to },
|
||||
endAt: { gt: from },
|
||||
providerUserId: { in: [...participatingUserIds] },
|
||||
},
|
||||
select: { providerUserId: true },
|
||||
});
|
||||
@@ -795,25 +865,33 @@ export class TodayService {
|
||||
);
|
||||
}
|
||||
|
||||
if (countsByProvider.size === 0) {
|
||||
charts.appointmentsByProvider = [];
|
||||
return;
|
||||
const rows = members.map((member) => ({
|
||||
userId: member.user.id,
|
||||
label: member.user.name,
|
||||
count: countsByProvider.get(member.user.id) ?? 0,
|
||||
isOwner: member.isOwner,
|
||||
}));
|
||||
|
||||
const sorted = [...rows].sort((a, b) => b.count - a.count);
|
||||
let top = sorted.slice(0, 8);
|
||||
|
||||
if (ownerMember) {
|
||||
const ownerUserId = ownerMember.user.id;
|
||||
const ownerInTop = top.some((row) => row.userId === ownerUserId);
|
||||
if (!ownerInTop) {
|
||||
const ownerRow = rows.find((row) => row.userId === ownerUserId)!;
|
||||
if (top.length >= 8) {
|
||||
top = [...top.slice(0, 7), ownerRow];
|
||||
} else {
|
||||
top = [...top, ownerRow];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...countsByProvider.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 8);
|
||||
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: { id: { in: sorted.map(([userId]) => userId) } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const nameById = new Map(users.map((user) => [user.id, user.name]));
|
||||
|
||||
charts.appointmentsByProvider = sorted.map(([userId, count]) => ({
|
||||
code: userId,
|
||||
label: nameById.get(userId) ?? userId,
|
||||
count,
|
||||
charts.appointmentsByProvider = top.map((row) => ({
|
||||
code: row.userId,
|
||||
label: row.label,
|
||||
count: row.count,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -821,21 +899,10 @@ export class TodayService {
|
||||
organizationId: string,
|
||||
widgets: TodayWidgets,
|
||||
) {
|
||||
const members = await this.prisma.membership.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
OR: [{ isOwner: true }, { isActive: true }],
|
||||
permissions: {
|
||||
some: {
|
||||
permission: { name: 'TAB_TREATMENT_EDIT' },
|
||||
},
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
const members = await this.listTreatmentParticipatingMembers(organizationId);
|
||||
|
||||
if (members.length === 0) {
|
||||
widgets.providersWithoutWorkingHours = { count: 0 };
|
||||
widgets.providersWithoutWorkingHours = { count: 0, membershipIds: [] };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -844,12 +911,52 @@ export class TodayService {
|
||||
members.map((member) => member.id),
|
||||
);
|
||||
|
||||
const count = members.filter((member) => {
|
||||
const missingHoursMembers = members.filter((member) => {
|
||||
const blocks = scheduleBlocksByMembership.get(member.id) ?? [];
|
||||
return blocks.length === 0;
|
||||
}).length;
|
||||
});
|
||||
|
||||
widgets.providersWithoutWorkingHours = { count };
|
||||
widgets.providersWithoutWorkingHours = {
|
||||
count: missingHoursMembers.length,
|
||||
membershipIds: missingHoursMembers.map((member) => member.id),
|
||||
};
|
||||
}
|
||||
|
||||
private async loadCasesDueWeek(
|
||||
labOrganizationId: string,
|
||||
rangeStart: Date,
|
||||
utcOffsetMinutes: number | undefined,
|
||||
charts: TodayCharts,
|
||||
) {
|
||||
const dayBuckets = buildNextSevenLocalDayBuckets(rangeStart, utcOffsetMinutes);
|
||||
const weekStart = dayBuckets[0]?.start ?? rangeStart;
|
||||
const weekEnd = dayBuckets[dayBuckets.length - 1]?.end ?? rangeStart;
|
||||
const offsetMs = (utcOffsetMinutes ?? 0) * 60_000;
|
||||
const counts = new Map(dayBuckets.map((bucket) => [bucket.code, 0]));
|
||||
|
||||
const cases = await this.prisma.labCase.findMany({
|
||||
where: {
|
||||
dueDate: { not: null, gte: weekStart, lt: weekEnd },
|
||||
sentAt: { not: null },
|
||||
sends: { some: { organizationId: labOrganizationId } },
|
||||
tasks: { some: { status: LabTaskStatus.IN_PROGRESS } },
|
||||
},
|
||||
select: { dueDate: true },
|
||||
});
|
||||
|
||||
for (const labCase of cases) {
|
||||
if (!labCase.dueDate) continue;
|
||||
const dayKey = localDayKeyFromDate(labCase.dueDate, offsetMs);
|
||||
if (counts.has(dayKey)) {
|
||||
counts.set(dayKey, (counts.get(dayKey) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
charts.casesDueWeek = dayBuckets.map((bucket) => ({
|
||||
code: bucket.code,
|
||||
label: bucket.label,
|
||||
count: counts.get(bucket.code) ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadCasesInProgress(labOrganizationId: string, widgets: TodayWidgets) {
|
||||
@@ -1279,6 +1386,33 @@ function buildLastSevenLocalDayBuckets(
|
||||
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 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) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
function localDayKeyFromDate(date: Date, offsetMs: number): string {
|
||||
const localMs = date.getTime() + offsetMs;
|
||||
const local = new Date(localMs);
|
||||
|
||||
Reference in New Issue
Block a user