Files
ulitka/src/modules/history/application/use-cases/get-daily-issue-stats/get-daily-issue-stats.use-case.ts
T
vadimandClaude Opus 5.5 b1f6b61994
Gitea Actions / Deploy (push) Canceled after 5m13s
create issues fix
issue observers

Наблюдатели задачи видят её и комментируют, но не редактируют.

- таблица issue_observer, наблюдатели — часть сущности Issue
- видимость: reporter, assignee или наблюдатель (задачи, история, SSE)
- правка/удаление задачи и вложений — только reporter/assignee (403 наблюдателю)
- observers в create/update, DELETE /issues/:id/observers/me, фильтр role
- история изменений и уведомления новым наблюдателям

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:23:49 +03:00

74 lines
2.7 KiB
TypeScript

import { Inject, Injectable } from '@nestjs/common';
import { ISSUE_STATUSES, type IssueStatusValue } from '../../../../issue/domain/value-objects/issue-status.vo.js';
import {
ISSUE_HISTORY_REPOSITORY,
type IIssueHistoryRepository,
} from '../../../domain/repositories/issue-history.repository.interface.js';
import { StatsPeriod } from '../../../domain/value-objects/stats-period.vo.js';
import { DailyIssueStatsDayDto, DailyIssueStatsDto, IssueStatusCounts } from '../../dto/daily-issue-stats.dto.js';
import { GetDailyIssueStatsQuery } from './get-daily-issue-stats.query.js';
/**
* Статистика задач по дням, считается по истории изменений (issue_change).
* Видимость — как у списка задач: только задачи, где пользователь reporter/assignee/наблюдатель.
*/
@Injectable()
export class GetDailyIssueStatsUseCase {
constructor(
@Inject(ISSUE_HISTORY_REPOSITORY)
private readonly historyRepository: IIssueHistoryRepository,
) {}
async execute(query: GetDailyIssueStatsQuery): Promise<DailyIssueStatsDto> {
const period = StatsPeriod.create({ from: query.from, to: query.to, timeZone: query.timeZone });
const range = {
userId: query.userId,
projectId: query.projectId,
from: period.from,
to: period.to,
timeZone: period.timeZone,
};
const [events, statuses] = await Promise.all([
this.historyRepository.countDailyEvents(range),
this.historyRepository.countDailyStatuses(range),
]);
// Каждый день периода попадает в ответ, даже если в нём ничего не было
const days = new Map<string, DailyIssueStatsDayDto>(
period.days.map((date) => [
date,
{ date, created: 0, transitions: emptyStatusCounts(), byStatus: emptyStatusCounts() },
]),
);
for (const event of events) {
const day = days.get(event.date);
if (!day) continue;
if (event.kind === 'created') {
day.created += event.count;
} else if (isKnownStatus(event.status)) {
day.transitions[event.status] += event.count;
}
}
for (const entry of statuses) {
const day = days.get(entry.date);
if (day && isKnownStatus(entry.status)) {
day.byStatus[entry.status] += entry.count;
}
}
return { from: period.from, to: period.to, timeZone: period.timeZone, days: [...days.values()] };
}
}
function emptyStatusCounts(): IssueStatusCounts {
return Object.fromEntries(ISSUE_STATUSES.map((status) => [status.alias, 0])) as IssueStatusCounts;
}
function isKnownStatus(status: string): status is IssueStatusValue {
return ISSUE_STATUSES.some((known) => known.alias === status);
}