|
@@ -0,0 +1,428 @@
|
|
|
+import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, isDevMode } from '@angular/core';
|
|
|
+import { CommonModule } from '@angular/common';
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
+import { Observable, Subject, Subscription, debounceTime } from 'rxjs';
|
|
|
+import { ProjectService } from '../../../services/project.service';
|
|
|
+import { Task } from '../../../models/project.model';
|
|
|
+import { Router } from '@angular/router';
|
|
|
+
|
|
|
+interface CalendarDay {
|
|
|
+ date: Date;
|
|
|
+ currentMonth: boolean;
|
|
|
+ tasks: Task[];
|
|
|
+}
|
|
|
+
|
|
|
+@Component({
|
|
|
+ selector: 'app-workload-calendar',
|
|
|
+ standalone: true,
|
|
|
+ imports: [CommonModule, FormsModule],
|
|
|
+ templateUrl: './workload-calendar.html',
|
|
|
+ styleUrls: ['./workload-calendar.scss'],
|
|
|
+ changeDetection: ChangeDetectionStrategy.OnPush
|
|
|
+})
|
|
|
+export class WorkloadCalendarComponent implements OnInit, OnDestroy {
|
|
|
+ view: 'day' | 'week' | 'month' = 'month';
|
|
|
+ selectedDate: Date = new Date();
|
|
|
+ today: Date = new Date();
|
|
|
+ selectedDesigner: string = 'all';
|
|
|
+ designers: string[] = [];
|
|
|
+ tasks: Task[] = [];
|
|
|
+ monthDays: CalendarDay[] = [];
|
|
|
+ showOverdueOnly: boolean = false;
|
|
|
+ expandedDays = new Set<string>();
|
|
|
+ designerStatuses: { name: string; label: string; cls: string; tasksCount: number; overdue: number }[] = [];
|
|
|
+ weekDays: CalendarDay[] = [];
|
|
|
+ // 缓存:模板直接使用,避免频繁函数调用
|
|
|
+ dayTasks: Task[] = [];
|
|
|
+ monthLabel: string = '';
|
|
|
+ // 记忆化:周期筛选缓存
|
|
|
+ private tasksVersion = 0;
|
|
|
+ private memoSignature = '';
|
|
|
+ private memoPeriodTasks: Task[] = [];
|
|
|
+ private monthTaskMap: Map<string, Task[]> = new Map();
|
|
|
+ private monthTaskMapKey: string | null = null; // 记录缓存对应的年月(YYYY-MM)
|
|
|
+ private recompute$ = new Subject<void>();
|
|
|
+ private recomputeSub?: Subscription;
|
|
|
+ private tasksSub?: Subscription;
|
|
|
+
|
|
|
+ constructor(private projectService: ProjectService, private router: Router) {}
|
|
|
+
|
|
|
+ ngOnInit(): void {
|
|
|
+ // 恢复上次的视图状态
|
|
|
+ try {
|
|
|
+ const savedRaw = localStorage.getItem('workloadCalendarState');
|
|
|
+ if (savedRaw) {
|
|
|
+ const saved = JSON.parse(savedRaw);
|
|
|
+ if (saved.view) this.view = saved.view;
|
|
|
+ if (saved.selectedDesigner) this.selectedDesigner = saved.selectedDesigner;
|
|
|
+ if (typeof saved.showOverdueOnly === 'boolean') this.showOverdueOnly = saved.showOverdueOnly;
|
|
|
+ if (saved.selectedDate) this.selectedDate = new Date(saved.selectedDate);
|
|
|
+ }
|
|
|
+ } catch {}
|
|
|
+
|
|
|
+ // 初始化变更去抖订阅:合并快速连续变更并统一重算
|
|
|
+ this.recomputeSub = this.recompute$.pipe(debounceTime(50)).subscribe(() => this.recomputeAll());
|
|
|
+
|
|
|
+ // 开发模式:允许通过 localStorage.mockTasks 注入大数据量以进行性能压测
|
|
|
+ const mockSizeRaw = isDevMode() ? localStorage.getItem('mockTasks') : null;
|
|
|
+ const mockSize = mockSizeRaw ? Number(mockSizeRaw) : NaN;
|
|
|
+ const tasks$ : Observable<Task[]> = isDevMode() && Number.isFinite(mockSize) && mockSize > 0
|
|
|
+ ? (console.info('[WorkloadCalendar] Using mock tasks for benchmarking:', mockSize), this.projectService.getTasks(mockSize))
|
|
|
+ : this.projectService.getTasks();
|
|
|
+
|
|
|
+ this.tasksSub = tasks$.subscribe((tasks: Task[]) => {
|
|
|
+ // 仅将有截止日期的任务纳入排期,并规范化 deadline 类型
|
|
|
+ this.tasks = (tasks || [])
|
|
|
+ .filter((t: Task) => !!t.deadline)
|
|
|
+ .map((t: Task) => ({ ...t, deadline: new Date(t.deadline) } as Task));
|
|
|
+ this.tasksVersion++;
|
|
|
+ this.designers = Array.from(new Set(this.tasks.map(t => t.assignee))).filter(Boolean).sort();
|
|
|
+ // 懒构建:仅为当前视图构建相应数据
|
|
|
+ if (this.view === 'month') this.buildMonthDays();
|
|
|
+ else if (this.view === 'week') this.buildWeekDays();
|
|
|
+ this.computeDesignerStatuses();
|
|
|
+ // 初始化缓存
|
|
|
+ this.monthLabel = this.formatMonthYear(this.selectedDate);
|
|
|
+ this.dayTasks = this.getTasksForDate(this.selectedDate);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ ngOnDestroy(): void {
|
|
|
+ this.recomputeSub?.unsubscribe();
|
|
|
+ this.tasksSub?.unsubscribe();
|
|
|
+ }
|
|
|
+
|
|
|
+ private computeDesignerStatuses(): void {
|
|
|
+ if (isDevMode()) console.time('computeDesignerStatuses');
|
|
|
+ const periodTasks = this.getPeriodFilteredTasks();
|
|
|
+ const agg = new Map<string, { count: number; overdue: number }>();
|
|
|
+ for (const t of periodTasks) {
|
|
|
+ const name = t.assignee || '未分配';
|
|
|
+ const cur = agg.get(name) || { count: 0, overdue: 0 };
|
|
|
+ cur.count += 1;
|
|
|
+ if (t.isOverdue) cur.overdue += 1;
|
|
|
+ agg.set(name, cur);
|
|
|
+ }
|
|
|
+ this.designerStatuses = this.designers.map(name => {
|
|
|
+ const a = agg.get(name) || { count: 0, overdue: 0 };
|
|
|
+ let label = '正常', cls = 'normal';
|
|
|
+ if (a.overdue > 0) { label = '异常'; cls = 'abnormal'; }
|
|
|
+ else if (a.count === 0) { label = '空闲'; cls = 'idle'; }
|
|
|
+ else if ((this.view === 'week' && a.count >= 6) || (this.view === 'month' && a.count >= 15) || (this.view === 'day' && a.count >= 3)) { label = '繁忙'; cls = 'busy'; }
|
|
|
+ return { name, label, cls, tasksCount: a.count, overdue: a.overdue };
|
|
|
+ });
|
|
|
+ if (isDevMode()) console.timeEnd('computeDesignerStatuses');
|
|
|
+ }
|
|
|
+
|
|
|
+ private getPeriodFilteredTasks(): Task[] {
|
|
|
+ const tasks = this.tasks.filter(t => (!this.showOverdueOnly || t.isOverdue));
|
|
|
+ const d = this.selectedDate;
|
|
|
+ if (this.view === 'day') {
|
|
|
+ return tasks.filter(t => this.isSameDay(t.deadline, d));
|
|
|
+ }
|
|
|
+ if (this.view === 'week') {
|
|
|
+ const day = d.getDay() || 7;
|
|
|
+ const start = new Date(d); start.setDate(d.getDate() - day + 1);
|
|
|
+ const end = new Date(start); end.setDate(start.getDate() + 6);
|
|
|
+ return tasks.filter(t => t.deadline >= start && t.deadline <= end);
|
|
|
+ }
|
|
|
+ const start = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
|
+ const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
|
|
|
+ return tasks.filter(t => t.deadline >= start && t.deadline <= end);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算设计师在当前视图周期下的工作状态
|
|
|
+ getDesignerStatus(name: string): { label: string; cls: string; tasksCount: number; overdue: number } {
|
|
|
+ // 兼容旧方法:直接从一次聚合的结果中取数
|
|
|
+ const all = this.getPeriodFilteredTasks().filter(t => !name || t.assignee === name);
|
|
|
+ const overdue = all.filter(t => t.isOverdue).length;
|
|
|
+ const count = all.length;
|
|
|
+ let label = '正常', cls = 'normal';
|
|
|
+ if (overdue > 0) { label = '异常'; cls = 'abnormal'; }
|
|
|
+ else if (count === 0) { label = '空闲'; cls = 'idle'; }
|
|
|
+ else if ((this.view === 'week' && count >= 6) || (this.view === 'month' && count >= 15) || (this.view === 'day' && count >= 3)) { label = '繁忙'; cls = 'busy'; }
|
|
|
+ return { label, cls, tasksCount: count, overdue };
|
|
|
+ }
|
|
|
+
|
|
|
+ private getPeriodDateRangeTasks(name?: string): Task[] {
|
|
|
+ // 保留方法签名用于兼容,但内部委托至一次性筛选结果
|
|
|
+ const all = this.getPeriodFilteredTasks();
|
|
|
+ return all.filter(t => !name || t.assignee === name);
|
|
|
+ }
|
|
|
+ private saveState(): void {
|
|
|
+ try {
|
|
|
+ localStorage.setItem('workloadCalendarState', JSON.stringify({
|
|
|
+ view: this.view,
|
|
|
+ selectedDesigner: this.selectedDesigner,
|
|
|
+ showOverdueOnly: this.showOverdueOnly,
|
|
|
+ selectedDate: this.selectedDate
|
|
|
+ }));
|
|
|
+ } catch {}
|
|
|
+ }
|
|
|
+
|
|
|
+ private priorityRank(p: any): number {
|
|
|
+ const r: Record<string, number> = { high: 3, medium: 2, low: 1 };
|
|
|
+ const key = typeof p === 'string' ? p.toLowerCase() : String(p);
|
|
|
+ return r[key] ?? 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ switchView(v: 'day' | 'week' | 'month'): void {
|
|
|
+ this.view = v;
|
|
|
+ this.scheduleRecompute();
|
|
|
+ }
|
|
|
+
|
|
|
+ navigateDate(direction: 'prev' | 'next'): void {
|
|
|
+ const d = new Date(this.selectedDate);
|
|
|
+ if (this.view === 'day') {
|
|
|
+ d.setDate(d.getDate() + (direction === 'prev' ? -1 : 1));
|
|
|
+ } else if (this.view === 'week') {
|
|
|
+ d.setDate(d.getDate() + (direction === 'prev' ? -7 : 7));
|
|
|
+ } else {
|
|
|
+ d.setMonth(d.getMonth() + (direction === 'prev' ? -1 : 1));
|
|
|
+ }
|
|
|
+ this.selectedDate = d;
|
|
|
+ this.scheduleRecompute();
|
|
|
+ }
|
|
|
+
|
|
|
+ setToday(): void {
|
|
|
+ this.selectedDate = new Date();
|
|
|
+ this.today = new Date();
|
|
|
+ this.scheduleRecompute();
|
|
|
+ }
|
|
|
+
|
|
|
+ isSameDay(a: Date, b: Date): boolean {
|
|
|
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
|
+ }
|
|
|
+
|
|
|
+ public isWeekend(d: Date): boolean {
|
|
|
+ const day = d.getDay();
|
|
|
+ return day === 0 || day === 6;
|
|
|
+ }
|
|
|
+
|
|
|
+ private matchesDesigner(t: Task): boolean {
|
|
|
+ return this.selectedDesigner === 'all' || t.assignee === this.selectedDesigner;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新增:跳转到项目详情
|
|
|
+ navigateToProject(t: Task, ev?: Event): void {
|
|
|
+ if (ev) { ev.stopPropagation(); ev.preventDefault?.(); }
|
|
|
+ if (!t || !t.projectId) return;
|
|
|
+ // 复用设计师端项目详情页面
|
|
|
+ this.router.navigate(['/designer/project-detail', t.projectId]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新增:按设计师快速筛选(保持当前日期与视图)
|
|
|
+ filterByDesigner(name: string, ev?: Event): void {
|
|
|
+ if (ev) { ev.stopPropagation(); ev.preventDefault?.(); }
|
|
|
+ if (!name) return;
|
|
|
+ this.selectedDesigner = name;
|
|
|
+ this.scheduleRecompute();
|
|
|
+ }
|
|
|
+
|
|
|
+ onDesignerChange(): void {
|
|
|
+ this.scheduleRecompute();
|
|
|
+ }
|
|
|
+
|
|
|
+ onOverdueOnlyChange(): void {
|
|
|
+ this.scheduleRecompute();
|
|
|
+ }
|
|
|
+
|
|
|
+ private scheduleRecompute(): void {
|
|
|
+ this.recompute$.next();
|
|
|
+ }
|
|
|
+
|
|
|
+ private recomputeAll(): void {
|
|
|
+ if (this.view === 'month') {
|
|
|
+ this.buildMonthDays();
|
|
|
+ } else if (this.view === 'week') {
|
|
|
+ this.buildWeekDays();
|
|
|
+ }
|
|
|
+ this.dayTasks = this.getTasksForDate(this.selectedDate);
|
|
|
+ this.monthLabel = this.formatMonthYear(this.selectedDate);
|
|
|
+ this.computeDesignerStatuses();
|
|
|
+ this.saveState();
|
|
|
+ }
|
|
|
+
|
|
|
+ getTasksForDate(date: Date): Task[] {
|
|
|
+ // 若已有当月的任务分组缓存且日期同当前所选月份,走快捷路径
|
|
|
+ const selKeyYM = `${this.selectedDate.getFullYear()}-${(this.selectedDate.getMonth() + 1).toString().padStart(2, '0')}`;
|
|
|
+ const dateKeyYM = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
|
|
|
+ if (
|
|
|
+ this.monthTaskMap.size > 0 &&
|
|
|
+ this.monthTaskMapKey === selKeyYM &&
|
|
|
+ dateKeyYM === selKeyYM
|
|
|
+ ) {
|
|
|
+ const key = this.toKey(date);
|
|
|
+ const cached = this.monthTaskMap.get(key);
|
|
|
+ if (cached) {
|
|
|
+ return cached.slice();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return this.tasks
|
|
|
+ .filter(t => this.matchesDesigner(t) && this.isSameDay(t.deadline, date))
|
|
|
+ .filter(t => !this.showOverdueOnly || t.isOverdue)
|
|
|
+ .slice()
|
|
|
+ .sort((a, b) =>
|
|
|
+ Number(b.isOverdue) - Number(a.isOverdue) ||
|
|
|
+ this.priorityRank(b.priority) - this.priorityRank(a.priority) ||
|
|
|
+ (a.deadline as any) - (b.deadline as any)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ getWeekDays(): CalendarDay[] {
|
|
|
+ const base = new Date(this.selectedDate);
|
|
|
+ const day = base.getDay() || 7; // 周日归为7
|
|
|
+ const start = new Date(base);
|
|
|
+ start.setDate(base.getDate() - day + 1); // 周一作为一周的第一天
|
|
|
+ const days: CalendarDay[] = [];
|
|
|
+ for (let i = 0; i < 7; i++) {
|
|
|
+ const d = new Date(start);
|
|
|
+ d.setDate(start.getDate() + i);
|
|
|
+ days.push({ date: d, currentMonth: true, tasks: this.getTasksForDate(d) });
|
|
|
+ }
|
|
|
+ return days;
|
|
|
+ }
|
|
|
+
|
|
|
+ buildWeekDays(): void {
|
|
|
+ if (isDevMode()) console.time('buildWeekDays');
|
|
|
+ const base = new Date(this.selectedDate);
|
|
|
+ const day = base.getDay() || 7; // 周日归为7
|
|
|
+ const start = new Date(base);
|
|
|
+ start.setDate(base.getDate() - day + 1); // 周一作为一周的第一天
|
|
|
+ const days: CalendarDay[] = [];
|
|
|
+ for (let i = 0; i < 7; i++) {
|
|
|
+ const d = new Date(start);
|
|
|
+ d.setDate(start.getDate() + i);
|
|
|
+ days.push({ date: d, currentMonth: true, tasks: this.getTasksForDate(d) });
|
|
|
+ }
|
|
|
+ this.weekDays = days;
|
|
|
+ if (isDevMode()) console.timeEnd('buildWeekDays');
|
|
|
+ }
|
|
|
+
|
|
|
+ getDayTasks(): Task[] {
|
|
|
+ // 移除重复排序:getTasksForDate 已经排好序
|
|
|
+ return this.getTasksForDate(this.selectedDate);
|
|
|
+ }
|
|
|
+
|
|
|
+ getPeriodTasks(): Task[] {
|
|
|
+ if (this.view === 'day') return this.getDayTasks();
|
|
|
+ if (this.view === 'week') {
|
|
|
+ return this.weekDays.flatMap(d => d.tasks);
|
|
|
+ }
|
|
|
+ return this.monthDays.flatMap(d => d.tasks);
|
|
|
+ }
|
|
|
+
|
|
|
+ formatMonthYear(d: Date = this.selectedDate): string {
|
|
|
+ return `${d.getFullYear()}年${(d.getMonth() + 1).toString().padStart(2, '0')}月`;
|
|
|
+ }
|
|
|
+
|
|
|
+ formatDateLabel(d: Date): string {
|
|
|
+ return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ private toKey(d: Date): string {
|
|
|
+ return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
|
|
+ }
|
|
|
+ isExpanded(d: Date): boolean { return this.expandedDays.has(this.toKey(d)); }
|
|
|
+ toggleExpand(d: Date, ev?: MouseEvent): void {
|
|
|
+ if (ev) ev.stopPropagation();
|
|
|
+ const k = this.toKey(d);
|
|
|
+ if (this.expandedDays.has(k)) this.expandedDays.delete(k); else this.expandedDays.add(k);
|
|
|
+ }
|
|
|
+
|
|
|
+ selectDate(d: Date): void {
|
|
|
+ this.selectedDate = new Date(d);
|
|
|
+ this.view = 'day';
|
|
|
+ this.scheduleRecompute();
|
|
|
+ }
|
|
|
+
|
|
|
+ buildMonthDays(): void {
|
|
|
+ if (isDevMode()) console.time('buildMonthDays');
|
|
|
+ const year = this.selectedDate.getFullYear();
|
|
|
+ const month = this.selectedDate.getMonth();
|
|
|
+ const firstDay = new Date(year, month, 1);
|
|
|
+ const lastDay = new Date(year, month + 1, 0);
|
|
|
+ // 以周一为一周第一天的偏移(周一=0,周日=6)
|
|
|
+ const firstWeekday = (firstDay.getDay() || 7) - 1;
|
|
|
+ const days: CalendarDay[] = [];
|
|
|
+
|
|
|
+ // 预过滤当月范围+筛选条件(设计师/仅看超期),并一次性按日期分组
|
|
|
+ const monthStart = new Date(year, month, 1);
|
|
|
+ const monthEnd = new Date(year, month + 1, 0);
|
|
|
+ const grouped = new Map<string, Task[]>();
|
|
|
+ const base = this.tasks.filter(t =>
|
|
|
+ this.matchesDesigner(t) && (!this.showOverdueOnly || t.isOverdue) &&
|
|
|
+ t.deadline >= monthStart && t.deadline <= monthEnd
|
|
|
+ );
|
|
|
+ for (const t of base) {
|
|
|
+ const k = this.toKey(t.deadline);
|
|
|
+ const arr = grouped.get(k) || [];
|
|
|
+ arr.push(t);
|
|
|
+ grouped.set(k, arr);
|
|
|
+ }
|
|
|
+ // 统一在分组阶段做排序,避免每格重复排序
|
|
|
+ for (const [k, arr] of grouped) {
|
|
|
+ arr.sort((a, b) =>
|
|
|
+ Number(b.isOverdue) - Number(a.isOverdue) ||
|
|
|
+ this.priorityRank(b.priority) - this.priorityRank(a.priority) ||
|
|
|
+ (a.deadline as any) - (b.deadline as any)
|
|
|
+ );
|
|
|
+ grouped.set(k, arr);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上月填充
|
|
|
+ for (let i = firstWeekday; i > 0; i--) {
|
|
|
+ const d = new Date(year, month, 1 - i);
|
|
|
+ days.push({ date: d, currentMonth: false, tasks: [] });
|
|
|
+ }
|
|
|
+ // 当月
|
|
|
+ for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
|
+ const d = new Date(year, month, i);
|
|
|
+ const key = this.toKey(d);
|
|
|
+ const tasks = grouped.get(key) || [];
|
|
|
+ days.push({ date: d, currentMonth: true, tasks });
|
|
|
+ }
|
|
|
+ // 下月填充至42格
|
|
|
+ while (days.length < 42) {
|
|
|
+ const last = days[days.length - 1].date;
|
|
|
+ const next = new Date(last);
|
|
|
+ next.setDate(last.getDate() + 1);
|
|
|
+ days.push({ date: next, currentMonth: false, tasks: [] });
|
|
|
+ }
|
|
|
+
|
|
|
+ this.monthDays = days;
|
|
|
+ // 同步月度任务映射缓存
|
|
|
+ this.monthTaskMap = grouped;
|
|
|
+ this.monthTaskMapKey = `${year}-${(month + 1).toString().padStart(2, '0')}`;
|
|
|
+ if (isDevMode()) console.timeEnd('buildMonthDays');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算设计师在当前视图周期下的工作状态
|
|
|
+ getDesignerStatusLegacy(name: string): { label: string; cls: string; tasksCount: number; overdue: number } {
|
|
|
+ const periodTasks = this.getPeriodDateRangeTasksLegacy(name);
|
|
|
+ const overdue = periodTasks.filter(t => t.isOverdue).length;
|
|
|
+ const count = periodTasks.length;
|
|
|
+ let label = '正常', cls = 'normal';
|
|
|
+ if (overdue > 0) { label = '异常'; cls = 'abnormal'; }
|
|
|
+ else if (count === 0) { label = '空闲'; cls = 'idle'; }
|
|
|
+ else if ((this.view === 'week' && count >= 6) || (this.view === 'month' && count >= 15) || (this.view === 'day' && count >= 3)) { label = '繁忙'; cls = 'busy'; }
|
|
|
+ return { label, cls, tasksCount: count, overdue };
|
|
|
+ }
|
|
|
+
|
|
|
+ private getPeriodDateRangeTasksLegacy(name?: string): Task[] {
|
|
|
+ const tasks = this.tasks.filter(t => (!name || t.assignee === name) && (!this.showOverdueOnly || t.isOverdue));
|
|
|
+ const d = new Date(this.selectedDate);
|
|
|
+ if (this.view === 'day') {
|
|
|
+ return tasks.filter(t => this.isSameDay(t.deadline, d));
|
|
|
+ }
|
|
|
+ if (this.view === 'week') {
|
|
|
+ const day = d.getDay() || 7;
|
|
|
+ const start = new Date(d); start.setDate(d.getDate() - day + 1);
|
|
|
+ const end = new Date(start); end.setDate(start.getDate() + 6);
|
|
|
+ return tasks.filter(t => t.deadline >= start && t.deadline <= end);
|
|
|
+ }
|
|
|
+ const start = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
|
+ const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
|
|
|
+ return tasks.filter(t => t.deadline >= start && t.deadline <= end);
|
|
|
+ }
|
|
|
+}
|