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

@@ -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] })