import { ChangeDetectorRef, Component } from '@angular/core'; import { Router, RouterLink } from '@angular/router'; import { KakochoService } from '../../services/kakocho-service'; import { DankaService } from '../../services/dankaService'; import { Danka } from '../../models/danka'; import { AppHeader } from '../../share/header/app-header'; import { AppSideMenu } from '../../share/side-menu/app-side-menu'; import { FormsModule } from '@angular/forms'; interface UpcomingMemorial { id: string; dankaId: string; date: Date; dateLabel: string; title: string; subText: string; type: '年忌法要' | '命日'; status: '準備確認' | '要確認'; } interface RecentDanka { danka: Danka; nextMemorialLabel: string; updatedAtLabel: string; } @Component({ selector: 'app-dashboard', imports: [AppHeader, AppSideMenu, RouterLink, FormsModule], templateUrl: './dashboard.html', styleUrl: './dashboard.scss', }) export class Dashboard { searchKeyword = ''; todayLabel = this.formatTodayLabel(new Date()); weeklyMemorialCount = 0; todayMemorialCount = 0; upcomingWeeklyMemorialCount = 0; monthlyMemorialCount = 0; recentDankaList: RecentDanka[] = []; upcomingMemorials: UpcomingMemorial[] = []; private readonly targetYear = new Date().getFullYear(); constructor( private kakochoService: KakochoService, private dankaService: DankaService, private router: Router, private cdr: ChangeDetectorRef, ) { this.setWeeklyMemorialSummary(); this.setMonthlyMemorialSummary(); this.setRecentDankaList(); this.setUpcomingMemorials(); } searchAll(): void { const keyword = this.searchKeyword.trim(); this.router.navigate(['/search'], { queryParams: keyword ? { keyword } : undefined, }); } private async setRecentDankaList(): Promise { const dankaList = await this.dankaService.getRecentDankaList(5); this.recentDankaList = await Promise.all( dankaList.map(async (danka) => ({ danka, nextMemorialLabel: await this.getNextMemorialLabel(danka.id), updatedAtLabel: this.formatUpdatedAt(danka.updatedAt), })), ); this.cdr.detectChanges(); } private async setWeeklyMemorialSummary(): Promise { const today = this.toDateOnly(new Date()); const weekEnd = this.addDays(this.getWeekStart(today), 6); const weeklyMemorials = (await this.kakochoService.getKakochoList()).filter((kakocho) => { const deathDate = this.parseDate(kakocho.deathDate); if (!deathDate) { return false; } if (!this.isMemorialTarget(deathDate)) { return false; } const memorialDate = new Date(this.targetYear, deathDate.getMonth(), deathDate.getDate()); return memorialDate >= today && memorialDate <= weekEnd; }); this.weeklyMemorialCount = weeklyMemorials.length; this.todayMemorialCount = weeklyMemorials.filter((kakocho) => { const deathDate = this.parseDate(kakocho.deathDate); return deathDate?.getMonth() === today.getMonth() && deathDate.getDate() === today.getDate(); }).length; this.upcomingWeeklyMemorialCount = this.weeklyMemorialCount - this.todayMemorialCount; this.cdr.detectChanges(); } private async setMonthlyMemorialSummary(): Promise { const today = this.toDateOnly(new Date()); this.monthlyMemorialCount = (await this.kakochoService.getKakochoList()).filter((kakocho) => { const deathDate = this.parseDate(kakocho.deathDate); if (!deathDate || !this.isMemorialTarget(deathDate)) { return false; } return deathDate.getMonth() === today.getMonth(); }).length; this.cdr.detectChanges(); } private async setUpcomingMemorials(): Promise { const today = this.toDateOnly(new Date()); const endDate = this.addDays(today, 30); const kakochoList = await this.kakochoService.getKakochoList(); const results = await Promise.all( kakochoList.map(async (kakocho): Promise => { const deathDate = this.parseDate(kakocho.deathDate); if (!deathDate) return null; const eventDate = new Date(this.targetYear, deathDate.getMonth(), deathDate.getDate()); if (eventDate < today || eventDate > endDate) return null; const memorialType = this.getMemorialType(deathDate); const danka = await this.dankaService.getDankaById(kakocho.dankaId); const type = memorialType ? '年忌法要' : '命日'; const status = memorialType ? '準備確認' : '要確認'; return { id: kakocho.id, dankaId: kakocho.dankaId, date: eventDate, dateLabel: this.formatDateLabel(eventDate, today), title: `${danka?.householdName ?? '不明'} ${kakocho.name}様 ${memorialType || '祥月命日'}`, subText: `${danka?.address ?? '住所未登録'} / ${kakocho.kaimyo || '戒名未登録'}`, type, status, }; }), ); this.upcomingMemorials = results .filter((memorial): memorial is UpcomingMemorial => memorial !== null) .sort((a, b) => a.date.getTime() - b.date.getTime() || a.title.localeCompare(b.title, 'ja')) .slice(0, 3); this.cdr.detectChanges(); } private isMemorialTarget(deathDate: Date): boolean { return this.getMemorialType(deathDate) !== ''; } private getMemorialType(deathDate: Date): string { const yearDiff = this.targetYear - deathDate.getFullYear(); switch (yearDiff) { case 1: return '一周忌'; case 2: return '三回忌'; case 6: return '七回忌'; case 12: return '十三回忌'; case 16: return '十七回忌'; case 22: return '二十三回忌'; default: return ''; } } private formatDateLabel(date: Date, today: Date): string { if (date.getTime() === today.getTime()) { return '今日'; } return `${date.getMonth() + 1}月${date.getDate()}日`; } private async getNextMemorialLabel(dankaId: string): Promise { const today = this.toDateOnly(new Date()); const nextMemorial = (await this.kakochoService.getKakochoByDankaId(dankaId)) .map((kakocho) => { const deathDate = this.parseDate(kakocho.deathDate); if (!deathDate) { return null; } const memorialDate = new Date(this.targetYear, deathDate.getMonth(), deathDate.getDate()); if (memorialDate < today || !this.getMemorialType(deathDate)) { return null; } return memorialDate; }) .filter((date): date is Date => date !== null) .sort((a, b) => a.getTime() - b.getTime())[0]; return nextMemorial ? this.formatDateLabel(nextMemorial, today) : '未設定'; } private formatUpdatedAt(updatedAt: unknown): string { const updatedDate = this.parseDate(updatedAt); const today = this.toDateOnly(new Date()); if (!updatedDate) { return '未登録'; } const diffDays = Math.floor((today.getTime() - updatedDate.getTime()) / 86400000); if (diffDays === 0) { return '今日'; } if (diffDays === 1) { return '昨日'; } if (diffDays > 1 && diffDays <= 7) { return `${diffDays}日前`; } return `${updatedDate.getMonth() + 1}月${updatedDate.getDate()}日`; } private formatTodayLabel(date: Date): string { const weekdays = ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日']; return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${weekdays[date.getDay()]}`; } private getWeekStart(date: Date): Date { const day = date.getDay(); const diff = day === 0 ? -6 : 1 - day; return this.addDays(date, diff); } private addDays(date: Date, days: number): Date { const result = new Date(date); result.setDate(result.getDate() + days); return result; } private toDateOnly(date: Date): Date { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } private parseDate(value: unknown): Date | null { if (!value) { return null; } if (value instanceof Date) { return value; } if (typeof value === 'object' && 'toDate' in value && typeof value.toDate === 'function') { return value.toDate(); } if (typeof value !== 'string') { return null; } const [year, month, day] = value.split('-').map(Number); if (!year || !month || !day) { return null; } return new Date(year, month - 1, day); } }