Keine Beschreibung
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

dashboard.ts 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import { ChangeDetectorRef, Component } from '@angular/core';
  2. import { Router, RouterLink } from '@angular/router';
  3. import { KakochoService } from '../../services/kakocho-service';
  4. import { DankaService } from '../../services/dankaService';
  5. import { Danka } from '../../models/danka';
  6. import { AppHeader } from '../../share/header/app-header';
  7. import { AppSideMenu } from '../../share/side-menu/app-side-menu';
  8. import { FormsModule } from '@angular/forms';
  9. interface UpcomingMemorial {
  10. id: string;
  11. dankaId: string;
  12. date: Date;
  13. dateLabel: string;
  14. title: string;
  15. subText: string;
  16. type: '年忌法要' | '命日';
  17. status: '準備確認' | '要確認';
  18. }
  19. interface RecentDanka {
  20. danka: Danka;
  21. nextMemorialLabel: string;
  22. updatedAtLabel: string;
  23. }
  24. @Component({
  25. selector: 'app-dashboard',
  26. imports: [AppHeader, AppSideMenu, RouterLink, FormsModule],
  27. templateUrl: './dashboard.html',
  28. styleUrl: './dashboard.scss',
  29. })
  30. export class Dashboard {
  31. searchKeyword = '';
  32. todayLabel = this.formatTodayLabel(new Date());
  33. weeklyMemorialCount = 0;
  34. todayMemorialCount = 0;
  35. upcomingWeeklyMemorialCount = 0;
  36. monthlyMemorialCount = 0;
  37. recentDankaList: RecentDanka[] = [];
  38. upcomingMemorials: UpcomingMemorial[] = [];
  39. private readonly targetYear = new Date().getFullYear();
  40. constructor(
  41. private kakochoService: KakochoService,
  42. private dankaService: DankaService,
  43. private router: Router,
  44. private cdr: ChangeDetectorRef,
  45. ) {
  46. this.setWeeklyMemorialSummary();
  47. this.setMonthlyMemorialSummary();
  48. this.setRecentDankaList();
  49. this.setUpcomingMemorials();
  50. }
  51. searchAll(): void {
  52. const keyword = this.searchKeyword.trim();
  53. this.router.navigate(['/search'], {
  54. queryParams: keyword ? { keyword } : undefined,
  55. });
  56. }
  57. private async setRecentDankaList(): Promise<void> {
  58. const dankaList = await this.dankaService.getRecentDankaList(5);
  59. this.recentDankaList = await Promise.all(
  60. dankaList.map(async (danka) => ({
  61. danka,
  62. nextMemorialLabel: await this.getNextMemorialLabel(danka.id),
  63. updatedAtLabel: this.formatUpdatedAt(danka.updatedAt),
  64. })),
  65. );
  66. this.cdr.detectChanges();
  67. }
  68. private async setWeeklyMemorialSummary(): Promise<void> {
  69. const today = this.toDateOnly(new Date());
  70. const weekEnd = this.addDays(this.getWeekStart(today), 6);
  71. const weeklyMemorials = (await this.kakochoService.getKakochoList()).filter((kakocho) => {
  72. const deathDate = this.parseDate(kakocho.deathDate);
  73. if (!deathDate) {
  74. return false;
  75. }
  76. if (!this.isMemorialTarget(deathDate)) {
  77. return false;
  78. }
  79. const memorialDate = new Date(this.targetYear, deathDate.getMonth(), deathDate.getDate());
  80. return memorialDate >= today && memorialDate <= weekEnd;
  81. });
  82. this.weeklyMemorialCount = weeklyMemorials.length;
  83. this.todayMemorialCount = weeklyMemorials.filter((kakocho) => {
  84. const deathDate = this.parseDate(kakocho.deathDate);
  85. return deathDate?.getMonth() === today.getMonth() && deathDate.getDate() === today.getDate();
  86. }).length;
  87. this.upcomingWeeklyMemorialCount = this.weeklyMemorialCount - this.todayMemorialCount;
  88. this.cdr.detectChanges();
  89. }
  90. private async setMonthlyMemorialSummary(): Promise<void> {
  91. const today = this.toDateOnly(new Date());
  92. this.monthlyMemorialCount = (await this.kakochoService.getKakochoList()).filter((kakocho) => {
  93. const deathDate = this.parseDate(kakocho.deathDate);
  94. if (!deathDate || !this.isMemorialTarget(deathDate)) {
  95. return false;
  96. }
  97. return deathDate.getMonth() === today.getMonth();
  98. }).length;
  99. this.cdr.detectChanges();
  100. }
  101. private async setUpcomingMemorials(): Promise<void> {
  102. const today = this.toDateOnly(new Date());
  103. const endDate = this.addDays(today, 30);
  104. const kakochoList = await this.kakochoService.getKakochoList();
  105. const results = await Promise.all(
  106. kakochoList.map(async (kakocho): Promise<UpcomingMemorial | null> => {
  107. const deathDate = this.parseDate(kakocho.deathDate);
  108. if (!deathDate) return null;
  109. const eventDate = new Date(this.targetYear, deathDate.getMonth(), deathDate.getDate());
  110. if (eventDate < today || eventDate > endDate) return null;
  111. const memorialType = this.getMemorialType(deathDate);
  112. const danka = await this.dankaService.getDankaById(kakocho.dankaId);
  113. const type = memorialType ? '年忌法要' : '命日';
  114. const status = memorialType ? '準備確認' : '要確認';
  115. return {
  116. id: kakocho.id,
  117. dankaId: kakocho.dankaId,
  118. date: eventDate,
  119. dateLabel: this.formatDateLabel(eventDate, today),
  120. title: `${danka?.householdName ?? '不明'} ${kakocho.name}様 ${memorialType || '祥月命日'}`,
  121. subText: `${danka?.address ?? '住所未登録'} / ${kakocho.kaimyo || '戒名未登録'}`,
  122. type,
  123. status,
  124. };
  125. }),
  126. );
  127. this.upcomingMemorials = results
  128. .filter((memorial): memorial is UpcomingMemorial => memorial !== null)
  129. .sort((a, b) => a.date.getTime() - b.date.getTime() || a.title.localeCompare(b.title, 'ja'))
  130. .slice(0, 3);
  131. this.cdr.detectChanges();
  132. }
  133. private isMemorialTarget(deathDate: Date): boolean {
  134. return this.getMemorialType(deathDate) !== '';
  135. }
  136. private getMemorialType(deathDate: Date): string {
  137. const yearDiff = this.targetYear - deathDate.getFullYear();
  138. switch (yearDiff) {
  139. case 1:
  140. return '一周忌';
  141. case 2:
  142. return '三回忌';
  143. case 6:
  144. return '七回忌';
  145. case 12:
  146. return '十三回忌';
  147. case 16:
  148. return '十七回忌';
  149. case 22:
  150. return '二十三回忌';
  151. default:
  152. return '';
  153. }
  154. }
  155. private formatDateLabel(date: Date, today: Date): string {
  156. if (date.getTime() === today.getTime()) {
  157. return '今日';
  158. }
  159. return `${date.getMonth() + 1}月${date.getDate()}日`;
  160. }
  161. private async getNextMemorialLabel(dankaId: string): Promise<string> {
  162. const today = this.toDateOnly(new Date());
  163. const nextMemorial = (await this.kakochoService.getKakochoByDankaId(dankaId))
  164. .map((kakocho) => {
  165. const deathDate = this.parseDate(kakocho.deathDate);
  166. if (!deathDate) {
  167. return null;
  168. }
  169. const memorialDate = new Date(this.targetYear, deathDate.getMonth(), deathDate.getDate());
  170. if (memorialDate < today || !this.getMemorialType(deathDate)) {
  171. return null;
  172. }
  173. return memorialDate;
  174. })
  175. .filter((date): date is Date => date !== null)
  176. .sort((a, b) => a.getTime() - b.getTime())[0];
  177. return nextMemorial ? this.formatDateLabel(nextMemorial, today) : '未設定';
  178. }
  179. private formatUpdatedAt(updatedAt: unknown): string {
  180. const updatedDate = this.parseDate(updatedAt);
  181. const today = this.toDateOnly(new Date());
  182. if (!updatedDate) {
  183. return '未登録';
  184. }
  185. const diffDays = Math.floor((today.getTime() - updatedDate.getTime()) / 86400000);
  186. if (diffDays === 0) {
  187. return '今日';
  188. }
  189. if (diffDays === 1) {
  190. return '昨日';
  191. }
  192. if (diffDays > 1 && diffDays <= 7) {
  193. return `${diffDays}日前`;
  194. }
  195. return `${updatedDate.getMonth() + 1}月${updatedDate.getDate()}日`;
  196. }
  197. private formatTodayLabel(date: Date): string {
  198. const weekdays = ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'];
  199. return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${weekdays[date.getDay()]}`;
  200. }
  201. private getWeekStart(date: Date): Date {
  202. const day = date.getDay();
  203. const diff = day === 0 ? -6 : 1 - day;
  204. return this.addDays(date, diff);
  205. }
  206. private addDays(date: Date, days: number): Date {
  207. const result = new Date(date);
  208. result.setDate(result.getDate() + days);
  209. return result;
  210. }
  211. private toDateOnly(date: Date): Date {
  212. return new Date(date.getFullYear(), date.getMonth(), date.getDate());
  213. }
  214. private parseDate(value: unknown): Date | null {
  215. if (!value) {
  216. return null;
  217. }
  218. if (value instanceof Date) {
  219. return value;
  220. }
  221. if (typeof value === 'object' && 'toDate' in value && typeof value.toDate === 'function') {
  222. return value.toDate();
  223. }
  224. if (typeof value !== 'string') {
  225. return null;
  226. }
  227. const [year, month, day] = value.split('-').map(Number);
  228. if (!year || !month || !day) {
  229. return null;
  230. }
  231. return new Date(year, month - 1, day);
  232. }
  233. }