3 Коммиты

Автор SHA1 Сообщение Дата
  poohr e829c2b791 Merge remote-tracking branch 'origin/master' 3 недель назад
  poohr 876b7da25e [update] 3 недель назад
  poohr 19fb420f5c [update] 3 недель назад

+ 3
- 4
src/app/pages/dashboard/dashboard.ts Просмотреть файл

@@ -69,7 +69,7 @@ export class Dashboard {
69 69
         danka,
70 70
         nextMemorialLabel: await this.getNextMemorialLabel(danka.id),
71 71
         updatedAtLabel: this.formatUpdatedAt(danka.updatedAt),
72
-      }))
72
+      })),
73 73
     );
74 74
   }
75 75
 
@@ -143,7 +143,7 @@ export class Dashboard {
143 143
           type,
144 144
           status,
145 145
         };
146
-      })
146
+      }),
147 147
     );
148 148
 
149 149
     this.upcomingMemorials = results
@@ -187,8 +187,7 @@ export class Dashboard {
187 187
 
188 188
   private async getNextMemorialLabel(dankaId: string): Promise<string> {
189 189
     const today = this.toDateOnly(new Date());
190
-    const nextMemorial = (await this.kakochoService
191
-      .getKakochoByDankaId(dankaId))
190
+    const nextMemorial = (await this.kakochoService.getKakochoByDankaId(dankaId))
192 191
       .map((kakocho) => {
193 192
         const deathDate = this.parseDate(kakocho.deathDate);
194 193
         if (!deathDate) {

+ 5
- 1
src/app/pages/event/event.html Просмотреть файл

@@ -76,7 +76,11 @@
76 76
             <div>状態</div>
77 77
           </div>
78 78
 
79
-          @if (filteredEventTargets.length > 0) {
79
+          @if (isLoading) {
80
+            <div class="empty-message">
81
+              読み込み中です。
82
+            </div>
83
+          } @else if (filteredEventTargets.length > 0) {
80 84
             @for (target of filteredEventTargets; track target.id) {
81 85
               <div class="event-table-row">
82 86
                 <div>

+ 54
- 10
src/app/pages/event/event.ts Просмотреть файл

@@ -1,4 +1,4 @@
1
-import { Component, OnInit } from '@angular/core';
1
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2 2
 import { FormsModule } from '@angular/forms';
3 3
 import { DankaService } from '../../services/dankaService';
4 4
 import { FamilyService } from '../../services/family-service';
@@ -15,6 +15,7 @@ import { AppSideMenu } from '../../share/side-menu/app-side-menu';
15 15
 })
16 16
 export class EventPage implements OnInit{
17 17
   eventTargets: EventTarget[] = [];
18
+  isLoading = true;
18 19
   targetYear: number = new Date().getFullYear();
19 20
   selectedEventType: EventType | 'all' = 'all';
20 21
   selectedStatus: EventStatus | 'all' = 'all';
@@ -38,11 +39,13 @@ export class EventPage implements OnInit{
38 39
     { label: '未案内', value: '未案内' },
39 40
     { label: '案内済', value: '案内済' },
40 41
   ];
42
+  private eventRequestId = 0;
41 43
 
42 44
   constructor(
43 45
     private dankaService: DankaService,
44 46
     private familyService: FamilyService,
45 47
     private eventService: EventService,
48
+    private cdr: ChangeDetectorRef,
46 49
   ) { }
47 50
 
48 51
   ngOnInit(): void {
@@ -54,7 +57,15 @@ export class EventPage implements OnInit{
54 57
   }
55 58
 
56 59
   async createEventTargetList(): Promise<void> {
57
-    this.eventTargets = [];
60
+    const requestId = ++this.eventRequestId;
61
+    const targetYear = this.targetYear;
62
+    const selectedEventType = this.selectedEventType;
63
+    const eventTargets: EventTarget[] = [];
64
+
65
+    if (this.eventTargets.length === 0) {
66
+      this.isLoading = true;
67
+      this.cdr.detectChanges();
68
+    }
58 69
 
59 70
     const families = await this.familyService.getFamilyList();
60 71
 
@@ -62,7 +73,7 @@ export class EventPage implements OnInit{
62 73
       const birthDate = this.parseDate(family.birthDate);
63 74
       if (!birthDate) continue;
64 75
 
65
-      const age = this.targetYear - birthDate.getFullYear();
76
+      const age = targetYear - birthDate.getFullYear();
66 77
       const eventTypes = this.getEventTypes(age);
67 78
       if (eventTypes.length === 0) continue;
68 79
 
@@ -71,8 +82,8 @@ export class EventPage implements OnInit{
71 82
 
72 83
       for (const eventType of eventTypes) {
73 84
         if (
74
-          this.selectedEventType !== 'all' &&
75
-          this.selectedEventType !== eventType
85
+          selectedEventType !== 'all' &&
86
+          selectedEventType !== eventType
76 87
         ) {
77 88
           continue;
78 89
         }
@@ -82,14 +93,14 @@ export class EventPage implements OnInit{
82 93
         const defaultStatus: EventStatus =
83 94
           Number(family.id) % 2 === 0 ? '案内済' : '未案内';
84 95
 
85
-        this.eventTargets.push({
96
+        eventTargets.push({
86 97
           id,
87 98
           dankaId: family.dankaId,
88 99
           name: family.name,
89 100
           furigana: family.furigana,
90 101
           householdName: danka?.householdName ?? '不明',
91 102
           relationship: family.relationship || '未登録',
92
-          birthDate: family.birthDate,
103
+          birthDate: this.formatDateForValue(birthDate),
93 104
           age,
94 105
           eventType,
95 106
           note: family.note,
@@ -98,13 +109,19 @@ export class EventPage implements OnInit{
98 109
       }
99 110
     }
100 111
 
101
-    this.eventTargets.sort(
112
+    if (requestId !== this.eventRequestId) {
113
+      return;
114
+    }
115
+
116
+    this.eventTargets = eventTargets.sort(
102 117
       (a, b) =>
103 118
         this.getEventSortOrder(a.eventType) -
104 119
         this.getEventSortOrder(b.eventType) ||
105 120
         a.age - b.age ||
106 121
         a.name.localeCompare(b.name, 'ja'),
107 122
     );
123
+    this.isLoading = false;
124
+    this.cdr.detectChanges();
108 125
   }
109 126
 
110 127
   changeEventType(eventType: EventType | 'all'): void {
@@ -128,7 +145,7 @@ export class EventPage implements OnInit{
128 145
           target.eventType,
129 146
           target.note,
130 147
           target.status,
131
-        ].some((value) => value.includes(keyword));
148
+        ].some((value) => this.includesKeyword(value, keyword));
132 149
 
133 150
       return matchesStatus && matchesKeyword;
134 151
     });
@@ -162,11 +179,38 @@ export class EventPage implements OnInit{
162 179
     return ['稚児行列', '七五三', '成人式', '米寿'].indexOf(eventType);
163 180
   }
164 181
 
165
-  private parseDate(value: string): Date | null {
182
+  private parseDate(value: unknown): Date | null {
183
+    if (!value) {
184
+      return null;
185
+    }
186
+
187
+    if (value instanceof Date) {
188
+      return value;
189
+    }
190
+
191
+    if (typeof value === 'object' && 'toDate' in value && typeof value.toDate === 'function') {
192
+      return value.toDate();
193
+    }
194
+
195
+    if (typeof value !== 'string') {
196
+      return null;
197
+    }
198
+
166 199
     const [year, month, day] = value.split('-').map(Number);
167 200
     if (!year || !month || !day) {
168 201
       return null;
169 202
     }
170 203
     return new Date(year, month - 1, day);
171 204
   }
205
+
206
+  private includesKeyword(value: unknown, keyword: string): boolean {
207
+    return String(value ?? '').includes(keyword);
208
+  }
209
+
210
+  private formatDateForValue(date: Date): string {
211
+    const year = date.getFullYear();
212
+    const month = String(date.getMonth() + 1).padStart(2, '0');
213
+    const day = String(date.getDate()).padStart(2, '0');
214
+    return `${year}-${month}-${day}`;
215
+  }
172 216
 }

+ 5
- 1
src/app/pages/memorial-list/memorial-list.html Просмотреть файл

@@ -69,7 +69,11 @@
69 69
             <div>詳細</div>
70 70
           </div>
71 71
 
72
-          @if (filteredMemorialList.length > 0) {
72
+          @if (isLoading) {
73
+            <div class="empty-message">
74
+              読み込み中です。
75
+            </div>
76
+          } @else if (filteredMemorialList.length > 0) {
73 77
             @for (memorial of filteredMemorialList; track memorial.id) {
74 78
               <div class="memorial-table-row">
75 79
                 <div class="person-name">

+ 69
- 13
src/app/pages/memorial-list/memorial-list.ts Просмотреть файл

@@ -1,4 +1,4 @@
1
-import { Component, OnInit } from '@angular/core';
1
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2 2
 import { ActivatedRoute, Router, RouterLink } from '@angular/router';
3 3
 import { Memorial } from '../../models/memorial';
4 4
 import { DankaService } from '../../services/dankaService';
@@ -15,9 +15,11 @@ import { FormsModule } from '@angular/forms';
15 15
 })
16 16
 export class MemorialList implements OnInit{
17 17
   memorialList: Memorial[] = [];
18
+  isLoading = true;
18 19
   targetYear: number = new Date().getFullYear();
19 20
   selectedMemorialType = 'all';
20 21
   searchKeyword = '';
22
+  private memorialRequestId = 0;
21 23
   yearOptions: number[] = [
22 24
     this.targetYear - 1,
23 25
     this.targetYear,
@@ -45,6 +47,7 @@ export class MemorialList implements OnInit{
45 47
   constructor(
46 48
     private dankaService: DankaService,
47 49
     private kakochoService: KakochoService,
50
+    private cdr: ChangeDetectorRef,
48 51
   ) {
49 52
 
50 53
   }
@@ -57,20 +60,31 @@ export class MemorialList implements OnInit{
57 60
   }
58 61
 
59 62
   async createMemorialList(): Promise<void> {
60
-    this.memorialList = [];
63
+    const requestId = ++this.memorialRequestId;
64
+    const targetYear = this.targetYear;
65
+    const selectedMemorialType = this.selectedMemorialType;
66
+    const memorialList: Memorial[] = [];
67
+
68
+    if (this.memorialList.length === 0) {
69
+      this.isLoading = true;
70
+      this.cdr.detectChanges();
71
+    }
61 72
 
62 73
     const kakochoList = await this.kakochoService.getKakochoList();
63 74
 
64 75
     for (const kakocho of kakochoList) {
65
-      const deathYear = Number(kakocho.deathDate.slice(0, 4));
66
-      const yearDiff = this.targetYear - deathYear;
76
+      const deathDate = this.parseDate(kakocho.deathDate);
77
+      if (!deathDate) continue;
78
+
79
+      const deathYear = deathDate.getFullYear();
80
+      const yearDiff = targetYear - deathYear;
67 81
 
68 82
       const memorialType = this.getMemorialType(yearDiff);
69 83
       if (memorialType === '') continue;
70 84
 
71 85
       if (
72
-        this.selectedMemorialType !== 'all' &&
73
-        this.selectedMemorialType !== memorialType
86
+        selectedMemorialType !== 'all' &&
87
+        selectedMemorialType !== memorialType
74 88
       ) {
75 89
         continue;
76 90
       }
@@ -85,15 +99,19 @@ export class MemorialList implements OnInit{
85 99
         kaimyo: kakocho.kaimyo,
86 100
         relationship: kakocho.relationship,
87 101
         householdName: danka?.householdName ?? '不明',
88
-        deathDate: kakocho.deathDate,
102
+        deathDate: this.formatDateForValue(deathDate),
89 103
         memorialType,
90 104
         note: kakocho.note,
91 105
       };
92 106
 
93
-      this.memorialList.push(memorialTarget);
107
+      memorialList.push(memorialTarget);
94 108
     }
95 109
 
96
-    this.memorialList.sort((a, b) => {
110
+    if (requestId !== this.memorialRequestId) {
111
+      return;
112
+    }
113
+
114
+    this.memorialList = memorialList.sort((a, b) => {
97 115
       const deathDateA = new Date(a.deathDate).getTime();
98 116
       const deathDateB = new Date(b.deathDate).getTime();
99 117
 
@@ -103,6 +121,8 @@ export class MemorialList implements OnInit{
103 121
 
104 122
       return a.name.localeCompare(b.name, 'ja');
105 123
     });
124
+    this.isLoading = false;
125
+    this.cdr.detectChanges();
106 126
   }
107 127
 
108 128
   changeMemorialType(memorialType: string): void {
@@ -126,17 +146,21 @@ export class MemorialList implements OnInit{
126 146
         memorial.householdName,
127 147
         memorial.memorialType,
128 148
         memorial.note,
129
-      ].some((value) => value.includes(keyword)),
149
+      ].some((value) => this.includesKeyword(value, keyword)),
130 150
     );
131 151
   }
132 152
 
153
+  private includesKeyword(value: unknown, keyword: string): boolean {
154
+    return String(value ?? '').includes(keyword);
155
+  }
156
+
133 157
   formatDeathDate(deathDate: string): string {
134
-    const [, month, day] = deathDate.split('-').map(Number);
135
-    if (!month || !day) {
158
+    const date = this.parseDate(deathDate);
159
+    if (!date) {
136 160
       return '未登録';
137 161
     }
138 162
 
139
-    return `${month}月${day}日`;
163
+    return `${date.getMonth() + 1}月${date.getDate()}日`;
140 164
   }
141 165
 
142 166
   getMemorialType(yearDiff: number) {
@@ -173,4 +197,36 @@ export class MemorialList implements OnInit{
173 197
         return '';
174 198
     }
175 199
   }
200
+
201
+  private parseDate(value: unknown): Date | null {
202
+    if (!value) {
203
+      return null;
204
+    }
205
+
206
+    if (value instanceof Date) {
207
+      return value;
208
+    }
209
+
210
+    if (typeof value === 'object' && 'toDate' in value && typeof value.toDate === 'function') {
211
+      return value.toDate();
212
+    }
213
+
214
+    if (typeof value !== 'string') {
215
+      return null;
216
+    }
217
+
218
+    const [year, month, day] = value.split('-').map(Number);
219
+    if (!year || !month || !day) {
220
+      return null;
221
+    }
222
+
223
+    return new Date(year, month - 1, day);
224
+  }
225
+
226
+  private formatDateForValue(date: Date): string {
227
+    const year = date.getFullYear();
228
+    const month = String(date.getMonth() + 1).padStart(2, '0');
229
+    const day = String(date.getDate()).padStart(2, '0');
230
+    return `${year}-${month}-${day}`;
231
+  }
176 232
 }

+ 4
- 3
src/app/pages/search/search.html Просмотреть файл

@@ -13,11 +13,12 @@
13 13
         <div class="search-input-row">
14 14
           <div class="search-input-box">
15 15
             <span class="search-icon">⌕</span>
16
-            <input type="text" [(ngModel)]="searchKeyword"
16
+            <input type="text" [ngModel]="searchKeyword"
17
+                   (ngModelChange)="onSearchKeywordChange($event)"
17 18
                    placeholder="三回忌 / 〇〇歳 / 2024 /"
18
-                   (keydown.enter)="searchAll()"/>
19
+                   (keydown.enter)="submitSearch()"/>
19 20
           </div>
20
-          <button type="button" class="search-button" (click)="searchAll()">
21
+          <button type="button" class="search-button" (click)="submitSearch()">
21 22
             検索
22 23
           </button>
23 24
           <button type="button" class="filter-button-main" (click)="clearSearch()">

+ 101
- 39
src/app/pages/search/search.ts Просмотреть файл

@@ -1,6 +1,6 @@
1
-import { Component, OnInit } from '@angular/core';
2
-import { FormBuilder, FormsModule } from '@angular/forms';
3
-import { ActivatedRoute, Router, RouterLink } from '@angular/router';
1
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2
+import { FormsModule } from '@angular/forms';
3
+import { ActivatedRoute, RouterLink } from '@angular/router';
4 4
 import { Danka } from '../../models/danka';
5 5
 import { Family } from '../../models/family';
6 6
 import { Kakocho } from '../../models/kakocho';
@@ -29,12 +29,15 @@ export class Search implements OnInit{
29 29
   familyResults: Family[] = [];
30 30
   kakochoResults: Kakocho[] = [];
31 31
   totalResultCount = 0;
32
+  private searchTimer?: ReturnType<typeof setTimeout>;
33
+  private searchRequestId = 0;
32 34
 
33 35
   constructor(
34 36
     private dankaService: DankaService,
35 37
     private familyService: FamilyService,
36 38
     private kakochoService: KakochoService,
37 39
     private route: ActivatedRoute,
40
+    private cdr: ChangeDetectorRef,
38 41
   ) { }
39 42
 
40 43
   ngOnInit(): void {
@@ -42,26 +45,46 @@ export class Search implements OnInit{
42 45
 
43 46
     if (keyword) {
44 47
       this.searchKeyword = keyword;
45
-      this.searchAll();
48
+      this.submitSearch();
46 49
     }
47 50
   }
48 51
 
49 52
   // フィルタータブの選択処理
50 53
   changeSearchType(searchType: string): void {
51 54
     this.selectedSearchType = searchType;
55
+    this.submitSearch();
56
+  }
57
+
58
+  onSearchKeywordChange(keyword: string): void {
59
+    this.searchKeyword = keyword;
60
+
61
+    if (this.searchTimer) {
62
+      clearTimeout(this.searchTimer);
63
+    }
64
+
65
+    this.searchTimer = setTimeout(() => {
66
+      this.searchAll();
67
+    }, 250);
68
+  }
69
+
70
+  submitSearch(): void {
71
+    if (this.searchTimer) {
72
+      clearTimeout(this.searchTimer);
73
+    }
74
+
52 75
     this.searchAll();
53 76
   }
54 77
 
55 78
   // 全検索の処理
56 79
   async searchAll(): Promise<void> {
80
+    const requestId = ++this.searchRequestId;
57 81
     const keyword = this.searchKeyword.trim();
82
+    const searchType = this.selectedSearchType;
58 83
 
59
-    this.dankaResults = [];
60
-    this.familyResults = [];
61
-    this.kakochoResults = [];
62
-    this.totalResultCount = 0;
63
-
64
-    if (keyword === '') return;
84
+    if (keyword === '') {
85
+      this.setSearchResults([], [], [], requestId);
86
+      return;
87
+    }
65 88
 
66 89
     const [dankaList, familyList, kakochoList] = await Promise.all([
67 90
       this.dankaService.getDankaList(),
@@ -69,57 +92,96 @@ export class Search implements OnInit{
69 92
       this.kakochoService.getKakochoList(),
70 93
     ]);
71 94
 
95
+    if (requestId !== this.searchRequestId) {
96
+      return;
97
+    }
98
+
99
+    let dankaResults: Danka[] = [];
100
+    let familyResults: Family[] = [];
101
+    let kakochoResults: Kakocho[] = [];
102
+
72 103
     // 檀家検索
73
-    if (this.selectedSearchType === 'all' || this.selectedSearchType === 'danka') {
74
-      this.dankaResults = dankaList.filter((danka) =>
75
-        danka.householdName.includes(keyword) ||
76
-        danka.householder.includes(keyword) ||
77
-        danka.postalCode.includes(keyword) ||
78
-        danka.address.includes(keyword) ||
79
-        danka.phones.some(
104
+    if (searchType === 'all' || searchType === 'danka') {
105
+      dankaResults = dankaList.filter((danka) =>
106
+        this.includesKeyword(danka.householdName, keyword) ||
107
+        this.includesKeyword(danka.householder, keyword) ||
108
+        this.includesKeyword(danka.postalCode, keyword) ||
109
+        this.includesKeyword(danka.address, keyword) ||
110
+        this.getPhones(danka).some(
80 111
           (phone) =>
81
-            phone.tel.includes(keyword) ||
82
-            phone.note.includes(keyword)
112
+            this.includesKeyword(phone.tel, keyword) ||
113
+            this.includesKeyword(phone.note, keyword)
83 114
         )
84 115
       );
85 116
     }
86 117
 
87 118
     // 家族検索
88
-    if (this.selectedSearchType === 'all' || this.selectedSearchType === 'family') {
89
-      this.familyResults = familyList.filter((family) =>
90
-        family.name.includes(keyword) ||
91
-        family.furigana.includes(keyword) ||
92
-        family.relationship.includes(keyword) ||
93
-        family.birthDate.includes(keyword) ||
94
-        family.note.includes(keyword)
119
+    if (searchType === 'all' || searchType === 'family') {
120
+      familyResults = familyList.filter((family) =>
121
+        this.includesKeyword(family.name, keyword) ||
122
+        this.includesKeyword(family.furigana, keyword) ||
123
+        this.includesKeyword(family.relationship, keyword) ||
124
+        this.includesKeyword(family.birthDate, keyword) ||
125
+        this.includesKeyword(family.note, keyword)
95 126
       );
96 127
     }
97 128
 
98 129
     // 過去帳検索
99
-    if (this.selectedSearchType === 'all' || this.selectedSearchType === 'kakocho') {
100
-      this.kakochoResults = kakochoList.filter((kakocho) =>
101
-        kakocho.name.includes(keyword) ||
102
-        kakocho.furigana.includes(keyword) ||
103
-        kakocho.relationship.includes(keyword) ||
104
-        kakocho.kaimyo.includes(keyword) ||
105
-        kakocho.deathDate.includes(keyword) ||
106
-        kakocho.ageAtDeath.includes(keyword) ||
107
-        kakocho.note.includes(keyword)
130
+    if (searchType === 'all' || searchType === 'kakocho') {
131
+      kakochoResults = kakochoList.filter((kakocho) =>
132
+        this.includesKeyword(kakocho.name, keyword) ||
133
+        this.includesKeyword(kakocho.furigana, keyword) ||
134
+        this.includesKeyword(kakocho.relationship, keyword) ||
135
+        this.includesKeyword(kakocho.kaimyo, keyword) ||
136
+        this.includesKeyword(kakocho.deathDate, keyword) ||
137
+        this.includesKeyword(kakocho.ageAtDeath, keyword) ||
138
+        this.includesKeyword(kakocho.note, keyword)
108 139
       );
109 140
     }
110 141
 
111
-    this.totalResultCount =
112
-      this.dankaResults.length +
113
-      this.familyResults.length +
114
-      this.kakochoResults.length;
142
+    this.setSearchResults(dankaResults, familyResults, kakochoResults, requestId);
115 143
   }
116 144
 
117 145
   clearSearch(): void {
146
+    if (this.searchTimer) {
147
+      clearTimeout(this.searchTimer);
148
+    }
149
+
150
+    this.searchRequestId++;
118 151
     this.searchKeyword = '';
119 152
     this.selectedSearchType = 'all';
120 153
     this.dankaResults = [];
121 154
     this.familyResults = [];
122 155
     this.kakochoResults = [];
123 156
     this.totalResultCount = 0;
157
+    this.cdr.detectChanges();
158
+  }
159
+
160
+  private includesKeyword(value: unknown, keyword: string): boolean {
161
+    return String(value ?? '').includes(keyword);
162
+  }
163
+
164
+  private getPhones(danka: Danka) {
165
+    return Array.isArray(danka.phones) ? danka.phones : [];
166
+  }
167
+
168
+  private setSearchResults(
169
+    dankaResults: Danka[],
170
+    familyResults: Family[],
171
+    kakochoResults: Kakocho[],
172
+    requestId: number,
173
+  ): void {
174
+    if (requestId !== this.searchRequestId) {
175
+      return;
176
+    }
177
+
178
+    this.dankaResults = dankaResults;
179
+    this.familyResults = familyResults;
180
+    this.kakochoResults = kakochoResults;
181
+    this.totalResultCount =
182
+      dankaResults.length +
183
+      familyResults.length +
184
+      kakochoResults.length;
185
+    this.cdr.detectChanges();
124 186
   }
125 187
 }

Загрузка…
Отмена
Сохранить