Greeting and summary of the day, the coming week, the latest across my projects, who is online now, the week in numbers (mine and the team's, with a streak), sprints under way, «Retomar», project cards with their activity, what to pick up when nothing is mine and first steps for newcomers. REST (GET /api/me/home), GraphQL (myHome) and MCP (get_my_week). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
4.0 KiB
TypeScript
114 lines
4.0 KiB
TypeScript
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
|
import {
|
|
HomeReadModel,
|
|
OnlineReadModel,
|
|
} from '@task-stream/api/tracker/domain';
|
|
import { computeHome, HomeProjectInput, lastDays } from '../analytics/home';
|
|
import { CalendarFeedsRepository } from '../ports/calendar-feeds.repository';
|
|
import { FindActivityRepository } from '../ports/find-activity.repository';
|
|
import { FindIssuesRepository } from '../ports/find-issues.repository';
|
|
import { FindProjectsRepository } from '../ports/find-projects.repository';
|
|
import { FindReleasesRepository } from '../ports/find-releases.repository';
|
|
import { FindSprintsRepository } from '../ports/find-sprints.repository';
|
|
import { FindTimelineRepository } from '../ports/find-timeline.repository';
|
|
import { PresenceStore } from '../ports/presence.store';
|
|
import { PRESENCE_TTL_MS } from '../presence.service';
|
|
import { GetMyHomeQuery } from './get-my-home.query';
|
|
import { guestOnly } from './read-access';
|
|
|
|
/** How much activity the home page lists. */
|
|
const ACTIVITY = 30;
|
|
|
|
@QueryHandler(GetMyHomeQuery)
|
|
export class GetMyHomeQueryHandler
|
|
implements IQueryHandler<GetMyHomeQuery, HomeReadModel>
|
|
{
|
|
constructor(
|
|
private readonly findProjects: FindProjectsRepository,
|
|
private readonly findIssues: FindIssuesRepository,
|
|
private readonly findTimeline: FindTimelineRepository,
|
|
private readonly findSprints: FindSprintsRepository,
|
|
private readonly findReleases: FindReleasesRepository,
|
|
private readonly findActivity: FindActivityRepository,
|
|
private readonly feeds: CalendarFeedsRepository,
|
|
private readonly presence: PresenceStore,
|
|
) {}
|
|
|
|
async execute(query: GetMyHomeQuery): Promise<HomeReadModel> {
|
|
const me = query.viewerId;
|
|
const now = new Date();
|
|
const projects = (await this.findProjects.findByMember(me)).filter(
|
|
(p) => !p.archivedAt,
|
|
);
|
|
// Project-wide views only of the projects I see whole (not as a guest).
|
|
const whole = projects.filter((p) => !guestOnly(p, me));
|
|
const days = lastDays(now);
|
|
const [inputs, activity, counts, feed, online] = await Promise.all([
|
|
Promise.all(
|
|
projects.map(async (project) => {
|
|
const [issues, timeline, sprints, releases] = await Promise.all([
|
|
this.findIssues.findByProject(project.id),
|
|
this.findTimeline.findByProject(project.id),
|
|
this.findSprints.findByProject(project.id),
|
|
this.findReleases.findByProject(project.id),
|
|
]);
|
|
return { project, issues, timeline, sprints, releases };
|
|
}),
|
|
),
|
|
this.findActivity.findAcross(
|
|
whole.map((p) => p.id),
|
|
ACTIVITY,
|
|
),
|
|
this.findActivity.dailyCounts(
|
|
whole.map((p) => p.id),
|
|
days[0],
|
|
),
|
|
this.feeds.findByUser(me),
|
|
Promise.all(
|
|
whole.map(async (project) =>
|
|
(
|
|
await this.presence.listProject(
|
|
project.id,
|
|
now.getTime() - PRESENCE_TTL_MS,
|
|
)
|
|
)
|
|
.filter((entry) => entry.user.id !== me)
|
|
.map(
|
|
(entry): OnlineReadModel => ({
|
|
user: entry.user,
|
|
projectKey: project.key,
|
|
issueKey: entry.issue?.key ?? null,
|
|
issueTitle: entry.issue?.title ?? null,
|
|
editing: entry.editing,
|
|
at: entry.at,
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
]);
|
|
const perDay = (projectId: string) =>
|
|
days.map(
|
|
(day) =>
|
|
counts.find((c) => c.projectId === projectId && c.day === day)
|
|
?.count ?? 0,
|
|
);
|
|
return computeHome({
|
|
viewerId: me,
|
|
projects: inputs.map(
|
|
(i): HomeProjectInput => ({ ...i, activity: perDay(i.project.id) }),
|
|
),
|
|
activity,
|
|
hasCalendar: !!feed,
|
|
// One line per person: the latest ticket they are on.
|
|
online: online
|
|
.flat()
|
|
.sort((a, b) => b.at.localeCompare(a.at))
|
|
.filter(
|
|
(entry, i, all) =>
|
|
all.findIndex((e) => e.user.id === entry.user.id) === i,
|
|
),
|
|
now,
|
|
});
|
|
}
|
|
}
|