feat: «Mi día», the few tickets I pick for today

A home panel with the tickets picked for today (10 at most), crossed out
as they get done and counted («1 de 3 terminados»), my open tickets to
add with one click, and what was left unfinished on my last day to carry
over. «☀ Mi día» on a ticket adds it or takes it out. The day is the
person's own (YYYY-MM-DD from the browser); private, no history (Mongo
`my_days`).

API: myDay(day), addToMyDay, removeFromMyDay; GET /api/me/day,
PUT/DELETE /api/me/day/{issue}. MCP: my_day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:18:10 +02:00
parent b88296c22b
commit dfb5f973f4
31 changed files with 2183 additions and 50 deletions

View File

@@ -46,6 +46,9 @@ export * from './lib/tree-follows.service';
export * from './lib/reminders/issue-reminders';
export * from './lib/ports/issue-reminders.repository';
export * from './lib/issue-reminders.service';
export * from './lib/my-day/my-day';
export * from './lib/ports/my-days.repository';
export * from './lib/my-day.service';
export * from './lib/tracker-application.module';
export * from './lib/ports/presence.store';
export * from './lib/presence.service';

View File

@@ -0,0 +1,32 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import {
AddToMyDayCommand,
GetMyDayQuery,
MyDay,
RemoveFromMyDayCommand,
} from './my-day/my-day';
/** «Mi día»: the tickets I picked for today. */
@Injectable()
export class MyDayService {
constructor(
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
) {}
get(userId: string, day: string): Promise<MyDay> {
return this.queryBus.execute(new GetMyDayQuery(userId, day));
}
add(userId: string, day: string, issueIds: string[]): Promise<void> {
return this.commands.execute(new AddToMyDayCommand(userId, day, issueIds));
}
remove(userId: string, day: string, issueId: string): Promise<void> {
return this.commands.execute(
new RemoveFromMyDayCommand(userId, day, issueId),
);
}
}

View File

@@ -0,0 +1,145 @@
import {
CommandBus,
CqrsModule,
IQueryHandler,
QueryBus,
QueryHandler,
} from '@nestjs/cqrs';
import { Test } from '@nestjs/testing';
import {
BusinessRuleError,
NotFoundError,
ValidationError,
} from '@task-stream/api/shared/domain';
import {
IssueReadModel,
StatusCategory,
} from '@task-stream/api/tracker/domain';
import { MyDayRecord, MyDaysRepository } from '../ports/my-days.repository';
import { GetIssuesByIdsQuery } from '../queries/get-issues-by-ids.query';
import {
AddToMyDayCommand,
GetMyDayQuery,
MAX_MY_DAY,
MY_DAY,
MyDay,
RemoveFromMyDayCommand,
} from './my-day';
class InMemoryDays extends MyDaysRepository {
rows: MyDayRecord[] = [];
async find(userId: string, day: string) {
return this.rows.find((r) => r.userId === userId && r.day === day) ?? null;
}
async latestBefore(userId: string, day: string) {
return (
this.rows
.filter((r) => r.userId === userId && r.day < day)
.sort((a, b) => b.day.localeCompare(a.day))[0] ?? null
);
}
async save(record: MyDayRecord) {
this.rows = [
...this.rows.filter(
(r) => !(r.userId === record.userId && r.day === record.day),
),
record,
];
}
}
/** Ana reads i0…i11; i3 is done. */
const issues = new Map<string, IssueReadModel>();
@QueryHandler(GetIssuesByIdsQuery)
class FakeIssuesByIds implements IQueryHandler<GetIssuesByIdsQuery> {
async execute(query: GetIssuesByIdsQuery) {
return query.viewerId === 'ana'
? query.ids.flatMap((id) => issues.get(id) ?? [])
: [];
}
}
const day = (offset: number) =>
new Date(Date.now() + offset * 86_400_000).toISOString().slice(0, 10);
async function setup() {
issues.clear();
for (let i = 0; i < 12; i++)
issues.set(`i${i}`, {
id: `i${i}`,
key: `WEB-${i}`,
statusCategory: i === 3 ? StatusCategory.DONE : StatusCategory.TODO,
trashedAt: null,
} as IssueReadModel);
const rows = new InMemoryDays();
const moduleRef = await Test.createTestingModule({
imports: [CqrsModule.forRoot()],
providers: [
...MY_DAY,
FakeIssuesByIds,
{ provide: MyDaysRepository, useValue: rows },
],
}).compile();
await moduleRef.init();
const commands = moduleRef.get(CommandBus);
const queries = moduleRef.get(QueryBus);
return {
rows,
commands,
keys: async (d: string) => {
const myDay: MyDay = await queries.execute(new GetMyDayQuery('ana', d));
return {
issues: myDay.issues.map((i) => i.key),
leftover: myDay.leftover?.issues.map((i) => i.key) ?? null,
};
},
};
}
describe('Mi día', () => {
it('keeps the tickets picked for today, in order, once each', async () => {
const { commands, keys } = await setup();
await commands.execute(new AddToMyDayCommand('ana', day(0), ['i1', 'i2']));
await commands.execute(new AddToMyDayCommand('ana', day(0), ['i2', 'i3']));
await commands.execute(new RemoveFromMyDayCommand('ana', day(0), 'i1'));
expect((await keys(day(0))).issues).toEqual(['WEB-2', 'WEB-3']);
});
it(`takes ${MAX_MY_DAY} at most, only readable tickets, only around today`, async () => {
const { commands } = await setup();
const many = Array.from({ length: MAX_MY_DAY + 1 }, (_, i) => `i${i}`);
await expect(
commands.execute(new AddToMyDayCommand('ana', day(0), many)),
).rejects.toBeInstanceOf(BusinessRuleError);
await expect(
commands.execute(new AddToMyDayCommand('ana', day(0), ['nope'])),
).rejects.toBeInstanceOf(NotFoundError);
await expect(
commands.execute(new AddToMyDayCommand('ana', day(5), ['i1'])),
).rejects.toBeInstanceOf(ValidationError);
await expect(
commands.execute(new AddToMyDayCommand('ana', '20-10-2026', ['i1'])),
).rejects.toBeInstanceOf(ValidationError);
});
it('offers what was left unfinished on the last day, but not the done ones', async () => {
const { commands, rows, keys } = await setup();
rows.rows.push({
userId: 'ana',
day: day(-1),
issueIds: ['i1', 'i3', 'i4'],
updatedAt: '',
});
expect(await keys(day(0))).toEqual({
issues: [],
leftover: ['WEB-1', 'WEB-4'],
});
// Carried over: no longer offered.
await commands.execute(new AddToMyDayCommand('ana', day(0), ['i1', 'i4']));
expect(await keys(day(0))).toEqual({
issues: ['WEB-1', 'WEB-4'],
leftover: null,
});
});
});

View File

@@ -0,0 +1,194 @@
import {
Command,
CommandHandler,
ICommandHandler,
IQueryHandler,
Query,
QueryBus,
QueryHandler,
} from '@nestjs/cqrs';
import {
BusinessRuleError,
NotFoundError,
assertThat,
} from '@task-stream/api/shared/domain';
import {
IssueReadModel,
StatusCategory,
} from '@task-stream/api/tracker/domain';
import { MyDayRecord, MyDaysRepository } from '../ports/my-days.repository';
import { GetIssuesByIdsQuery } from '../queries/get-issues-by-ids.query';
/*
* «Mi día»: the few tickets someone picks each morning for today, and at
* the end what got done. The day is the person's own (`YYYY-MM-DD` from
* their browser). Private, no history.
*/
export const MAX_MY_DAY = 10;
/** How far back what was left counts as «from before». */
const CARRY_OVER_DAYS = 7;
const DAY_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
const DAY_MS = 86_400_000;
const finished = (issue: IssueReadModel) =>
issue.statusCategory === StatusCategory.DONE ||
issue.statusCategory === StatusCategory.CANCELLED;
/** A day of the person's: well formed and near today (time zones apart). */
export function assertDay(day: string, now = Date.now()): void {
const at = Date.parse(`${day}T12:00:00Z`);
assertThat(DAY_FORMAT.test(day) && !Number.isNaN(at), 'A day is YYYY-MM-DD');
assertThat(
Math.abs(at - now) <= 2 * DAY_MS,
'Only today can be planned (in your time zone)',
);
}
export interface MyDay {
day: string;
/** Picked for the day, in order (done ones included). */
issues: IssueReadModel[];
/** What was left unfinished on the latest earlier day, if recent. */
leftover: { day: string; issues: IssueReadModel[] } | null;
}
export class AddToMyDayCommand extends Command<void> {
constructor(
public readonly userId: string,
public readonly day: string,
/** Several at once: carrying over what was left. */
public readonly issueIds: string[],
) {
super();
}
}
export class RemoveFromMyDayCommand extends Command<void> {
constructor(
public readonly userId: string,
public readonly day: string,
public readonly issueId: string,
) {
super();
}
}
export class GetMyDayQuery extends Query<MyDay> {
constructor(
public readonly userId: string,
public readonly day: string,
) {
super();
}
}
@CommandHandler(AddToMyDayCommand)
export class AddToMyDayCommandHandler
implements ICommandHandler<AddToMyDayCommand>
{
constructor(
private readonly days: MyDaysRepository,
private readonly queryBus: QueryBus,
) {}
async execute(command: AddToMyDayCommand): Promise<void> {
assertDay(command.day);
const wanted = [...new Set(command.issueIds)];
const readable: IssueReadModel[] = await this.queryBus.execute(
new GetIssuesByIdsQuery(command.userId, wanted),
);
const ok = new Set(readable.filter((i) => !i.trashedAt).map((i) => i.id));
// One I cannot read does not exist, as far as I know.
const missing = wanted.find((id) => !ok.has(id));
if (missing) throw new NotFoundError('Issue', missing);
const current = await this.days.find(command.userId, command.day);
const issueIds = [
...(current?.issueIds ?? []),
...wanted.filter((id) => !current?.issueIds.includes(id)),
];
if (issueIds.length > MAX_MY_DAY)
throw new BusinessRuleError(
`Mi día takes ${MAX_MY_DAY} tickets at most: finish or take some out first`,
);
await this.days.save({
userId: command.userId,
day: command.day,
issueIds,
updatedAt: new Date().toISOString(),
});
}
}
@CommandHandler(RemoveFromMyDayCommand)
export class RemoveFromMyDayCommandHandler
implements ICommandHandler<RemoveFromMyDayCommand>
{
constructor(private readonly days: MyDaysRepository) {}
async execute(command: RemoveFromMyDayCommand): Promise<void> {
assertDay(command.day);
const current = await this.days.find(command.userId, command.day);
if (!current?.issueIds.includes(command.issueId)) return;
await this.days.save({
...current,
issueIds: current.issueIds.filter((id) => id !== command.issueId),
updatedAt: new Date().toISOString(),
});
}
}
@QueryHandler(GetMyDayQuery)
export class GetMyDayQueryHandler
implements IQueryHandler<GetMyDayQuery, MyDay>
{
constructor(
private readonly days: MyDaysRepository,
private readonly queryBus: QueryBus,
) {}
async execute(query: GetMyDayQuery): Promise<MyDay> {
assertDay(query.day);
const [today, before] = await Promise.all([
this.days.find(query.userId, query.day),
this.days.latestBefore(query.userId, query.day),
]);
const recent =
before &&
Date.parse(`${query.day}T00:00:00Z`) -
Date.parse(`${before.day}T00:00:00Z`) <=
CARRY_OVER_DAYS * DAY_MS
? before
: null;
const ids = [
...new Set([...(today?.issueIds ?? []), ...(recent?.issueIds ?? [])]),
];
const found: IssueReadModel[] = ids.length
? await this.queryBus.execute(new GetIssuesByIdsQuery(query.userId, ids))
: [];
const byId = new Map(
found.filter((i) => !i.trashedAt).map((i) => [i.id, i]),
);
const pick = (record: MyDayRecord | null) =>
(record?.issueIds ?? []).flatMap((id) => byId.get(id) ?? []);
const issues = pick(today);
const leftover = pick(recent).filter(
(i) => !finished(i) && !today?.issueIds.includes(i.id),
);
return {
day: query.day,
issues,
leftover:
recent && leftover.length
? { day: recent.day, issues: leftover }
: null,
};
}
}
export const MY_DAY = [
AddToMyDayCommandHandler,
RemoveFromMyDayCommandHandler,
GetMyDayQueryHandler,
];

View File

@@ -0,0 +1,19 @@
/** The tickets someone picked for one day of theirs. */
export interface MyDayRecord {
userId: string;
/** `YYYY-MM-DD` in the person's own time zone. */
day: string;
/** In the order they were picked. */
issueIds: string[];
updatedAt: string;
}
export abstract class MyDaysRepository {
abstract find(userId: string, day: string): Promise<MyDayRecord | null>;
/** The latest day before `day`, if any (to carry over what was left). */
abstract latestBefore(
userId: string,
day: string,
): Promise<MyDayRecord | null>;
abstract save(record: MyDayRecord): Promise<void>;
}

View File

@@ -39,6 +39,8 @@ import { PINS } from './saved-filters/pins';
import { ALERT_HANDLERS } from './alerts/alerts';
import { TREE_FOLLOWS } from './follows/tree-follows';
import { ISSUE_REMINDERS } from './reminders/issue-reminders';
import { MY_DAY } from './my-day/my-day';
import { MyDayService } from './my-day.service';
import { IssueRemindersService } from './issue-reminders.service';
import { TreeFollowsService } from './tree-follows.service';
import { AlertsService } from './alerts.service';
@@ -90,6 +92,7 @@ const SERVICES = [
AlertsService,
TreeFollowsService,
IssueRemindersService,
MyDayService,
SprintsService,
GitService,
WebhooksService,
@@ -124,6 +127,7 @@ const SERVICES = [
...ALERT_HANDLERS,
...TREE_FOLLOWS,
...ISSUE_REMINDERS,
...MY_DAY,
...GIT_HANDLERS,
...WEBHOOK_HANDLERS,
...TRANSFER_HANDLERS,

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import {
MyDayRecord,
MyDaysRepository,
} from '@task-stream/api/tracker/application';
import { Model } from 'mongoose';
import { ReadModels } from './read-model.schemas';
type Stored = MyDayRecord & { _id?: unknown };
const strip = ({ _id: _ignored, ...record }: Stored): MyDayRecord => ({
...record,
// Mongo leaves an emptied list out.
issueIds: record.issueIds ?? [],
});
@Injectable()
export class MongoMyDaysRepository extends MyDaysRepository {
constructor(
@InjectModel(ReadModels.MyDay) private readonly days: Model<Stored>,
) {
super();
}
async find(userId: string, day: string): Promise<MyDayRecord | null> {
const doc = await this.days.findOne({ userId, day }).lean<Stored>();
return doc ? strip(doc) : null;
}
async latestBefore(userId: string, day: string): Promise<MyDayRecord | null> {
const doc = await this.days
.findOne({ userId, day: { $lt: day } })
.sort({ day: -1 })
.lean<Stored>();
return doc ? strip(doc) : null;
}
async save(record: MyDayRecord): Promise<void> {
await this.days.updateOne(
{ userId: record.userId, day: record.day },
{ $set: record },
{ upsert: true },
);
}
}

View File

@@ -111,6 +111,7 @@ export const ReadModels = {
AlertTrace: 'AlertTrace',
TreeFollow: 'TreeFollow',
IssueReminder: 'IssueReminder',
MyDay: 'MyDay',
Sprint: 'SprintReadModel',
GitIntegration: 'GitIntegration',
OutgoingWebhook: 'OutgoingWebhook',
@@ -385,6 +386,15 @@ export const IssueReminderSchema = SchemaFactory.createForClass(
IssueReminderSchema.index({ sentAt: 1, at: 1 });
IssueReminderSchema.index({ userId: 1, issueId: 1 });
@Schema({ ...LOOSE, collection: 'my_days' })
class MyDayDocument {
@Prop({ type: String }) userId!: string;
@Prop({ type: String }) day!: string;
@Prop({ type: [String] }) issueIds!: string[];
}
export const MyDaySchema = SchemaFactory.createForClass(MyDayDocument);
MyDaySchema.index({ userId: 1, day: -1 }, { unique: true });
@Schema({ ...LOOSE, collection: 'git_integrations' })
class GitIntegrationDocument {
/** The project's id. */
@@ -492,6 +502,7 @@ export const READ_MODEL_SCHEMAS = [
{ name: ReadModels.AlertTrace, schema: AlertTraceSchema },
{ name: ReadModels.TreeFollow, schema: TreeFollowSchema },
{ name: ReadModels.IssueReminder, schema: IssueReminderSchema },
{ name: ReadModels.MyDay, schema: MyDaySchema },
{ name: ReadModels.CalendarFeed, schema: CalendarFeedSchema },
{ name: ReadModels.DashboardShare, schema: DashboardShareSchema },
{ name: ReadModels.PushSubscription, schema: PushSubscriptionSchema },

View File

@@ -17,6 +17,7 @@ import {
AlertIntegrationsRepository,
TreeFollowsRepository,
IssueRemindersRepository,
MyDaysRepository,
GitIntegrationsRepository,
OutgoingWebhooksRepository,
AutomationsRepository,
@@ -86,6 +87,7 @@ import { MongoPinsRepository } from './mongo/pins.repository';
import { MongoAlertIntegrationsRepository } from './mongo/alert-integrations.repository';
import { MongoTreeFollowsRepository } from './mongo/tree-follows.repository';
import { MongoIssueRemindersRepository } from './mongo/issue-reminders.repository';
import { MongoMyDaysRepository } from './mongo/my-days.repository';
import { MongoGitIntegrationsRepository } from './mongo/git-integrations.repository';
import { MongoOutgoingWebhooksRepository } from './mongo/outgoing-webhooks.repository';
import { HttpWebhookSender } from './webhooks/http-webhook.sender';
@@ -172,6 +174,7 @@ const PORTS = [
provide: IssueRemindersRepository,
useClass: MongoIssueRemindersRepository,
},
{ provide: MyDaysRepository, useClass: MongoMyDaysRepository },
{
provide: GitIntegrationsRepository,
useClass: MongoGitIntegrationsRepository,

View File

@@ -20,6 +20,7 @@ import {
PinsService,
TreeFollowsService,
IssueRemindersService,
MyDayService,
PresenceService,
WorkflowsService,
} from '@task-stream/api/tracker/application';
@@ -54,6 +55,7 @@ import {
IssuePageGql,
IssueParentGql,
IssueReminderGql,
MyDayGql,
ReminderOnIssueGql,
PresenceGql,
} from './types';
@@ -69,6 +71,7 @@ export class IssueResolver {
private readonly pins: PinsService,
private readonly treeFollows: TreeFollowsService,
private readonly reminders: IssueRemindersService,
private readonly myDayService: MyDayService,
) {}
@Mutation(() => [PresenceGql], {
@@ -442,6 +445,40 @@ export class IssueResolver {
return true;
}
@Query(() => MyDayGql, {
description:
'«Mi día»: the tickets I picked for today, and what was left on my last day',
})
myDay(
@CurrentUser() me: User,
@Args('day', { description: 'Today, YYYY-MM-DD in my time zone' })
day: string,
) {
return this.myDayService.get(me.id, day);
}
@Mutation(() => Boolean, {
description: 'Add tickets to «Mi día» (10 at most)',
})
async addToMyDay(
@CurrentUser() me: User,
@Args('day') day: string,
@Args('issueIds', { type: () => [ID] }) issueIds: string[],
): Promise<boolean> {
await this.myDayService.add(me.id, day, issueIds);
return true;
}
@Mutation(() => Boolean, { description: 'Take a ticket out of «Mi día»' })
async removeFromMyDay(
@CurrentUser() me: User,
@Args('day') day: string,
@Args('issueId', { type: () => ID }) issueId: string,
): Promise<boolean> {
await this.myDayService.remove(me.id, day, issueId);
return true;
}
@Query(() => [ReminderOnIssueGql], {
description: 'My waiting reminders on tickets, soonest first',
})

View File

@@ -1393,3 +1393,22 @@ export class ReminderOnIssueGql {
@Field(() => IssueReminderGql) reminder!: IssueReminderGql;
@Field(() => IssueGql) issue!: IssueGql;
}
@ObjectType('MyDayLeftover', {
description: 'What was left unfinished on the latest earlier day',
})
export class MyDayLeftoverGql {
@Field() day!: string;
@Field(() => [IssueGql]) issues!: IssueGql[];
}
@ObjectType('MyDay', {
description: '«Mi día»: the tickets I picked for today',
})
export class MyDayGql {
@Field({ description: 'YYYY-MM-DD, in my time zone' }) day!: string;
@Field(() => [IssueGql], { description: 'In order, done ones included' })
issues!: IssueGql[];
@Field(() => MyDayLeftoverGql, { nullable: true })
leftover!: MyDayLeftoverGql | null;
}

View File

@@ -1552,6 +1552,12 @@ export class PushEndpointDto {
endpoint!: string;
}
export class MyDayQueryDto {
@ApiProperty({ example: '2026-10-20', description: 'Today, in my time zone' })
@Matches(/^\d{4}-\d{2}-\d{2}$/)
day!: string;
}
export class SetReminderDto {
@ApiProperty({
format: 'date-time',

View File

@@ -1427,3 +1427,15 @@ export class ReminderOnIssueDto {
@ApiProperty({ type: IssueReminderDto }) reminder!: IssueReminderDto;
@ApiProperty({ type: () => IssueDto }) issue!: IssueDto;
}
export class MyDayLeftoverDto {
@ApiProperty() day!: string;
@ApiProperty({ type: () => [IssueDto] }) issues!: IssueDto[];
}
export class MyDayDto {
@ApiProperty({ description: 'YYYY-MM-DD, in my time zone' }) day!: string;
@ApiProperty({ type: () => [IssueDto] }) issues!: IssueDto[];
@ApiProperty({ type: MyDayLeftoverDto, nullable: true })
leftover!: MyDayLeftoverDto | null;
}

View File

@@ -35,6 +35,7 @@ import {
PinsService,
TreeFollowsService,
IssueRemindersService,
MyDayService,
PresenceService,
ProjectsService,
} from '@task-stream/api/tracker/application';
@@ -49,6 +50,7 @@ import {
CreateIssueDto,
DecisionsQueryDto,
SetReminderDto,
MyDayQueryDto,
IssueSearchQueryDto,
PresenceBodyDto,
LinkIssueDto,
@@ -69,6 +71,7 @@ import {
CommentPageDto,
DecisionDto,
IssueReminderDto,
MyDayDto,
ReminderOnIssueDto,
EpicDto,
IssueDto,
@@ -106,6 +109,7 @@ export class IssuesController {
private readonly pins: PinsService,
private readonly treeFollows: TreeFollowsService,
private readonly reminders: IssueRemindersService,
private readonly myDayService: MyDayService,
) {}
@Get('me/home')
@@ -210,6 +214,59 @@ export class IssuesController {
await this.pins.pin(me.id, await this.refs.issueId(me.id, issue), false);
}
@Get('me/day')
@ApiOperation({
summary: '«Mi día»: the tickets I picked for today',
description: 'And what was left unfinished on my latest earlier day.',
})
@ApiOkResponse({ type: MyDayDto })
@ApiReadErrors()
async myDay(@CurrentUser() me: User, @Query() query: MyDayQueryDto) {
const myDay = await this.myDayService.get(me.id, query.day);
return {
day: myDay.day,
issues: await this.refs.issueDtos(myDay.issues, viewerOf(me)),
leftover: myDay.leftover && {
day: myDay.leftover.day,
issues: await this.refs.issueDtos(myDay.leftover.issues, viewerOf(me)),
},
};
}
@Put('me/day/:issue')
@HttpCode(204)
@ISSUE
@ApiOperation({ summary: 'Add a ticket to «Mi día» (10 at most)' })
@ApiNoContentResponse()
@ApiCommandErrors()
async addToMyDay(
@CurrentUser() me: User,
@Param('issue') issue: string,
@Query() query: MyDayQueryDto,
) {
await this.myDayService.add(me.id, query.day, [
await this.refs.issueId(me.id, issue),
]);
}
@Delete('me/day/:issue')
@HttpCode(204)
@ISSUE
@ApiOperation({ summary: 'Take a ticket out of «Mi día»' })
@ApiNoContentResponse()
@ApiCommandErrors()
async removeFromMyDay(
@CurrentUser() me: User,
@Param('issue') issue: string,
@Query() query: MyDayQueryDto,
) {
await this.myDayService.remove(
me.id,
query.day,
await this.refs.issueId(me.id, issue),
);
}
@Get('me/reminders')
@ApiOperation({ summary: 'My waiting reminders on tickets, soonest first' })
@ApiOkResponse({ type: [ReminderOnIssueDto] })