43 Commits

Autor SHA1 Nachricht Datum
  poohr 262595cdb4 Merge remote-tracking branch 'origin/master' vor 2 Wochen
  kuni 2aff82c0e0 added vor 2 Wochen
  kuni aeccc2b2d4 added vor 2 Wochen
  poohr fb2fd0e081 [fix] vor 2 Wochen
  poohr 2e0b09ebbb [update] vor 2 Wochen
  poohr 4af603bcb3 Merge remote-tracking branch 'origin/master' vor 3 Wochen
  kuni 213395bf35 added vor 3 Wochen
  poohr 902303ebf0 Merge remote-tracking branch 'origin/master' vor 3 Wochen
  kuni 7950ed12d5 added vor 3 Wochen
  poohr b1ef54657b [update] vor 3 Wochen
  kuni 4a7211b4a2 added vor 3 Wochen
  poohr e829c2b791 Merge remote-tracking branch 'origin/master' vor 3 Wochen
  poohr 876b7da25e [update] vor 3 Wochen
  kuni d2e7d4d6c9 added vor 3 Wochen
  poohr 19fb420f5c [update] vor 3 Wochen
  kuni e094dd1506 added vor 3 Wochen
  poohr 55c8e48e20 Merge remote-tracking branch 'origin/master' vor 3 Wochen
  poohr 72b3f06044 Merge remote-tracking branch 'origin/master' vor 3 Wochen
  kuni 411056add2 added vor 3 Wochen
  kuni ce5ff462e4 added vor 3 Wochen
  poohr 8c5b2e22e5 [update] vor 3 Wochen
  kuni b0224195a9 Merge branch 'master' of https://gitea.softopia.gku.ac.jp/nyoraiji/kaimyo-management vor 3 Wochen
  kuni 157806e70f added vor 3 Wochen
  poohr 4b23f24d7c Merge remote-tracking branch 'origin/master' vor 3 Wochen
  poohr 55930d3ed2 [update] vor 3 Wochen
  kuni 57f38c65db added vor 3 Wochen
  kuni 148dd5a47c added vor 3 Wochen
  poohr 2f6e9bb4fb Merge remote-tracking branch 'origin/master' vor 3 Wochen
  poohr e2e0368eef [update] vor 3 Wochen
  kuni 5a3ba3f4e4 added vor 3 Wochen
  poohr f82987694b [add] vor 3 Wochen
  kuni 22575404f9 added vor 3 Wochen
  poohr 0a44ef1385 [add] vor 3 Wochen
  kuni 5b696f87f5 added vor 3 Wochen
  poohr e22fc1aa37 Merge branch 'develop' vor 3 Wochen
  poohr 8b605096a0 Merge remote-tracking branch 'origin/master' vor 3 Wochen
  poohr fdd9746ac2 [add] vor 3 Wochen
  kuni 40d636463b added vor 3 Wochen
  poohr 3ed9dc93f2 Merge branch 'develop' vor 3 Wochen
  poohr 44f033cb93 [wip] vor 3 Wochen
  kuni 1adc0761ce added vor 3 Wochen
  poohr 0de4a70860 [add] vor 3 Wochen
  poohr ade8d77d9d [add] vor 3 Wochen
45 geänderte Dateien mit 3403 neuen und 2034 gelöschten Zeilen
  1. 7
    0
      .firebase/hosting.ZGlzdFxrYWlteW8tbWFuYWdlbWVudA.cache
  2. 4
    0
      .firebase/hosting.ZGlzdFxrYWlteW8tbWFuYWdlbWVudFxicm93c2Vy.cache
  3. 5
    0
      .firebaserc
  4. 4
    3
      angular.json
  5. 16
    0
      firebase.json
  6. 998
    5
      package-lock.json
  7. 1
    0
      package.json
  8. 15
    0
      src/app/firebase.ts
  9. 5
    2
      src/app/models/familytreenode.ts
  10. 1
    0
      src/app/models/memorial.ts
  11. 606
    594
      src/app/pages/danka-detail/danka-detail.html
  12. 203
    67
      src/app/pages/danka-detail/danka-detail.scss
  13. 94
    63
      src/app/pages/danka-detail/danka-detail.ts
  14. 21
    17
      src/app/pages/danka-edit/danka-edit.html
  15. 37
    16
      src/app/pages/danka-edit/danka-edit.scss
  16. 13
    5
      src/app/pages/danka-edit/danka-edit.ts
  17. 50
    47
      src/app/pages/danka-list/danka-list.ts
  18. 28
    32
      src/app/pages/dashboard/dashboard.html
  19. 8
    12
      src/app/pages/dashboard/dashboard.scss
  20. 128
    20
      src/app/pages/dashboard/dashboard.ts
  21. 5
    1
      src/app/pages/event/event.html
  22. 25
    29
      src/app/pages/event/event.scss
  23. 88
    29
      src/app/pages/event/event.ts
  24. 16
    18
      src/app/pages/family-edit/family-edit.html
  25. 85
    106
      src/app/pages/family-edit/family-edit.scss
  26. 72
    24
      src/app/pages/family-edit/family-edit.ts
  27. 3
    3
      src/app/pages/kakocho-edit/kakocho-edit.html
  28. 68
    192
      src/app/pages/kakocho-edit/kakocho-edit.scss
  29. 42
    21
      src/app/pages/kakocho-edit/kakocho-edit.ts
  30. 8
    4
      src/app/pages/memorial-list/memorial-list.html
  31. 25
    29
      src/app/pages/memorial-list/memorial-list.scss
  32. 100
    24
      src/app/pages/memorial-list/memorial-list.ts
  33. 4
    6
      src/app/pages/search/search.html
  34. 61
    62
      src/app/pages/search/search.scss
  35. 126
    46
      src/app/pages/search/search.ts
  36. 111
    64
      src/app/services/dankaService.ts
  37. 32
    0
      src/app/services/event-service.ts
  38. 51
    168
      src/app/services/family-service.ts
  39. 60
    168
      src/app/services/family-tree-builder.ts
  40. 62
    59
      src/app/services/kakocho-service.ts
  41. 76
    82
      src/app/services/marriage-relation-service.ts
  42. 1
    1
      src/app/share/header/app-header.scss
  43. 12
    12
      src/app/share/side-menu/app-side-menu.html
  44. 2
    1
      src/app/share/side-menu/app-side-menu.scss
  45. 24
    2
      src/app/share/side-menu/app-side-menu.ts

+ 7
- 0
.firebase/hosting.ZGlzdFxrYWlteW8tbWFuYWdlbWVudA.cache Datei anzeigen

@@ -0,0 +1,7 @@
1
+browser/styles-5INURTSO.css,1780145996852,46b50c321b39e89a491b6727a01628c34245605a30beb3e7414c5e01cff90e6e
2
+prerendered-routes.json,1780145996846,acea2ed63a273ab4ca21a5071e93c8246d013bca031410b2d34087a30f99602d
3
+index.html,1780146161811,2d2b0c813628a70bd46d4e7ccc22c7b650055109ba37923dd6adf82a3fa68b7e
4
+3rdpartylicenses.txt,1780145996835,7b48241c2f7b9524001508fcb167ceab86499bc8633fb62758ee57ccdeb7ebaf
5
+browser/index.html,1780145996835,49759a68c1f76e959a03b6b184c12b6a527dcbfc9fd7ea26fa2337b0ebdb8e2c
6
+browser/favicon.ico,1779604170218,a905b13e228420df3dbe746dfb656d37a49736494c0737d00e007c8dc7319912
7
+browser/main-V5P7I3DP.js,1780145996852,4ff36ce4802f457be1e4087158575592549e05102e0f141d0943903ef607dc64

+ 4
- 0
.firebase/hosting.ZGlzdFxrYWlteW8tbWFuYWdlbWVudFxicm93c2Vy.cache Datei anzeigen

@@ -0,0 +1,4 @@
1
+styles-5INURTSO.css,1780147055905,46b50c321b39e89a491b6727a01628c34245605a30beb3e7414c5e01cff90e6e
2
+index.html,1780147055905,49759a68c1f76e959a03b6b184c12b6a527dcbfc9fd7ea26fa2337b0ebdb8e2c
3
+favicon.ico,1779604170218,a905b13e228420df3dbe746dfb656d37a49736494c0737d00e007c8dc7319912
4
+main-V5P7I3DP.js,1780147055907,4ff36ce4802f457be1e4087158575592549e05102e0f141d0943903ef607dc64

+ 5
- 0
.firebaserc Datei anzeigen

@@ -0,0 +1,5 @@
1
+{
2
+  "projects": {
3
+    "default": "dankamanagement-39a6a"
4
+  }
5
+}

+ 4
- 3
angular.json Datei anzeigen

@@ -2,7 +2,8 @@
2 2
   "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 3
   "version": 1,
4 4
   "cli": {
5
-    "packageManager": "npm"
5
+    "packageManager": "npm",
6
+    "analytics": false
6 7
   },
7 8
   "newProjectRoot": "projects",
8 9
   "projects": {
@@ -43,8 +44,8 @@
43 44
                 },
44 45
                 {
45 46
                   "type": "anyComponentStyle",
46
-                  "maximumWarning": "4kB",
47
-                  "maximumError": "8kB"
47
+                  "maximumWarning": "20kB",
48
+                  "maximumError": "30kB"
48 49
                 }
49 50
               ],
50 51
               "outputHashing": "all"

+ 16
- 0
firebase.json Datei anzeigen

@@ -0,0 +1,16 @@
1
+{
2
+  "hosting": {
3
+    "public": "dist/kaimyo-management/browser",
4
+    "ignore": [
5
+      "firebase.json",
6
+      "**/.*",
7
+      "**/node_modules/**"
8
+    ],
9
+    "rewrites": [
10
+      {
11
+        "source": "**",
12
+        "destination": "/index.html"
13
+      }
14
+    ]
15
+  }
16
+}

+ 998
- 5
package-lock.json
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 1
- 0
package.json Datei anzeigen

@@ -17,6 +17,7 @@
17 17
     "@angular/forms": "^21.2.0",
18 18
     "@angular/platform-browser": "^21.2.0",
19 19
     "@angular/router": "^21.2.0",
20
+    "firebase": "^12.14.0",
20 21
     "rxjs": "~7.8.0",
21 22
     "svg-pan-zoom": "^3.6.2",
22 23
     "tslib": "^2.3.0"

+ 15
- 0
src/app/firebase.ts Datei anzeigen

@@ -0,0 +1,15 @@
1
+import { initializeApp } from 'firebase/app';
2
+import { getFirestore } from 'firebase/firestore';
3
+
4
+const firebaseConfig = {
5
+  apiKey: "AIzaSyBdkDfrr2jA33olJSd2Fm5KeneOwpded-o",
6
+  authDomain: "dankamanagement-39a6a.firebaseapp.com",
7
+  projectId: "dankamanagement-39a6a",
8
+  storageBucket: "dankamanagement-39a6a.firebasestorage.app",
9
+  messagingSenderId: "85260511199",
10
+  appId: "1:85260511199:web:27b2c43a0fdb77ad32066a",
11
+  measurementId: "G-Z3PPJZDQ16"
12
+};
13
+
14
+export const app = initializeApp(firebaseConfig);
15
+export const db = getFirestore(app);

+ 5
- 2
src/app/models/familytreenode.ts Datei anzeigen

@@ -1,11 +1,14 @@
1 1
 import { Family } from "./family";
2
+import { Kakocho } from "./kakocho";
2 3
 
3 4
 export interface FamilyTreeNode {
4 5
   family: Family;
5 6
 
6
-  spouses: FamilyTreeNode[];
7
+  kakocho?: Kakocho;   // ★追加(ここが核心)
7 8
 
8
-  children: FamilyTreeNode[];
9
+  isDeceased: boolean; // ★追加
9 10
 
11
+  spouses: FamilyTreeNode[];
12
+  children: FamilyTreeNode[];
10 13
   parents: FamilyTreeNode[];
11 14
 }

+ 1
- 0
src/app/models/memorial.ts Datei anzeigen

@@ -3,6 +3,7 @@ export interface Memorial {
3 3
   id: string;
4 4
   dankaId: string;
5 5
   name: string;
6
+  furigana: string;
6 7
   kaimyo: string;
7 8
   relationship: string;
8 9
   householdName: string;

+ 606
- 594
src/app/pages/danka-detail/danka-detail.html
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 203
- 67
src/app/pages/danka-detail/danka-detail.scss Datei anzeigen

@@ -2,29 +2,29 @@
2 2
   position: relative;
3 3
   display: block;
4 4
   min-height: 100vh;
5
-  background: #f4eee4;
5
+  background: #f6f0e7;
6 6
   color: #2f2720;
7 7
 }
8 8
 
9 9
 .danka-detail-page {
10
-  display: flex;
11
-  align-items: flex-start;
12
-  gap: 8px;
13
-  background: #f4eee4;
10
+  display: grid;
11
+  grid-template-columns: 172px minmax(0, 1fr);
12
+  gap: 20px;
13
+  padding: 0 38px 36px 0;
14
+  background: #f6f0e7;
14 15
 }
15 16
 
16 17
 .danka-detail-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19 19
   box-sizing: border-box;
20 20
 }
21 21
 
22 22
 .detail-panel {
23
-  min-height: 650px;
24
-  padding: 26px 34px 36px;
25
-  background: #ffffff;
23
+  min-height: 760px;
24
+  padding: 34px 42px 40px;
25
+  background: #fffdf9;
26 26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28 28
   box-sizing: border-box;
29 29
 }
30 30
 
@@ -32,16 +32,17 @@
32 32
   display: flex;
33 33
   justify-content: space-between;
34 34
   align-items: flex-start;
35
-  margin-bottom: 20px;
35
+  gap: 24px;
36
+  margin-bottom: 22px;
36 37
 }
37 38
 
38 39
 .page-title-row h1 {
39
-  margin: 0 0 8px;
40
+  margin: 0 0 18px;
40 41
   color: #2f2720;
41
-  font-size: 32px;
42
-  line-height: 1.2;
42
+  font-size: 34px;
43
+  line-height: 1.1;
43 44
   font-weight: 800;
44
-  letter-spacing: 0.02em;
45
+  letter-spacing: 0;
45 46
 }
46 47
 
47 48
 .tab-list {
@@ -83,7 +84,7 @@
83 84
 .edit-button {
84 85
   width: 140px;
85 86
   height: 46px;
86
-  margin-top: 36px;
87
+  margin-top: 54px;
87 88
   border: 2px solid #8a6543;
88 89
   border-radius: 6px;
89 90
   background: #ffffff;
@@ -100,11 +101,11 @@
100 101
 
101 102
 .family-summary {
102 103
   min-height: 64px;
103
-  margin-bottom: 28px;
104
-  padding: 12px 22px;
104
+  margin-bottom: 22px;
105
+  padding: 14px 24px;
105 106
   border: 2px solid #d8caba;
106
-  border-radius: 14px;
107
-  background: #eadfce;
107
+  border-radius: 8px;
108
+  background: #fbf7f0;
108 109
   display: flex;
109 110
   align-items: center;
110 111
   box-sizing: border-box;
@@ -112,10 +113,23 @@
112 113
 
113 114
 .family-name-area {
114 115
   display: flex;
115
-  align-items: baseline;
116
+  align-items: center;
116 117
   gap: 18px;
117 118
 }
118 119
 
120
+.family-summary-mark {
121
+  width: 42px;
122
+  height: 42px;
123
+  border-radius: 999px;
124
+  background: #8a6543;
125
+  color: #ffffff;
126
+  font-weight: 800;
127
+  display: flex;
128
+  align-items: center;
129
+  justify-content: center;
130
+  flex: 0 0 auto;
131
+}
132
+
119 133
 .family-name {
120 134
   margin: 0;
121 135
   color: #2f2720;
@@ -156,7 +170,7 @@
156 170
 .add-button {
157 171
   width: 140px;
158 172
   height: 46px;
159
-  margin-top: 36px;
173
+  margin-top: 54px;
160 174
   border: 2px solid #8a6543;
161 175
   border-radius: 6px;
162 176
   background: #ffffff;
@@ -174,7 +188,7 @@
174 188
 .family-page-add-button {
175 189
   width: 140px;
176 190
   height: 46px;
177
-  margin-top: 36px;
191
+  margin-top: 54px;
178 192
   border: 2px solid #8a6543;
179 193
   border-radius: 6px;
180 194
   background: #ffffff;
@@ -195,17 +209,21 @@
195 209
 
196 210
 .detail-content {
197 211
   display: grid;
198
-  grid-template-columns: minmax(0, 1fr) 460px;
199
-  gap: 28px;
212
+  grid-template-columns: minmax(0, 1fr) 420px;
213
+  gap: 20px;
200 214
   align-items: start;
201 215
 }
202 216
 
203 217
 .basic-info-section {
204
-  padding-left: 0;
218
+  padding: 24px 20px 22px;
219
+  border: 2px solid #d8caba;
220
+  border-radius: 12px;
221
+  background: #fffdf9;
222
+  box-sizing: border-box;
205 223
 }
206 224
 
207 225
 .section-heading {
208
-  margin-bottom: 8px;
226
+  margin-bottom: 14px;
209 227
 }
210 228
 
211 229
 .section-heading h2 {
@@ -213,6 +231,7 @@
213 231
   color: #2f2720;
214 232
   font-size: 22px;
215 233
   font-weight: 800;
234
+  line-height: 1.3;
216 235
 }
217 236
 
218 237
 .section-heading p {
@@ -226,17 +245,27 @@
226 245
 }
227 246
 
228 247
 .info-pair-row {
248
+  border-top: 1px solid #eadfce;
249
+}
250
+
251
+.info-pair-row:first-child {
252
+  border-top: 0;
253
+}
254
+
255
+.info-two-column {
229 256
   display: grid;
230
-  grid-template-columns: 1fr 1fr;
231
-  gap: 14px;
232
-  margin-top: 10px;
257
+  grid-template-columns: minmax(0, 1fr) minmax(260px, 0.85fr);
258
+  gap: 18px;
233 259
 }
234 260
 
235 261
 .info-row {
236 262
   display: grid;
237
-  grid-template-columns: 96px 1fr;
263
+  grid-template-columns: 154px 1fr;
238 264
   align-items: center;
239 265
   margin-top: 0;
266
+  min-height: 62px;
267
+  padding: 6px 0;
268
+  box-sizing: border-box;
240 269
 }
241 270
 
242 271
 .info-form > .info-row {
@@ -244,19 +273,27 @@
244 273
 }
245 274
 
246 275
 .info-label {
276
+  min-height: 42px;
277
+  padding: 0 14px;
278
+  border-radius: 6px;
279
+  background: #f3eee8;
247 280
   color: #4b3c31;
248
-  font-size: 17px;
281
+  font-size: 16px;
249 282
   font-weight: 800;
283
+  display: flex;
284
+  align-items: center;
285
+  box-sizing: border-box;
250 286
 }
251 287
 
252 288
 .info-value {
253
-  min-height: 40px;
254
-  padding: 8px 12px;
255
-  border: 2px solid #d8caba;
256
-  border-radius: 6px;
257
-  background: #fffdf9;
289
+  min-height: 42px;
290
+  padding: 8px 18px;
291
+  border: 0;
292
+  border-radius: 0;
293
+  background: transparent;
258 294
   color: #2f2720;
259
-  font-size: 17px;
295
+  font-size: 16px;
296
+  font-weight: 700;
260 297
   box-sizing: border-box;
261 298
   display: flex;
262 299
   align-items: center;
@@ -264,14 +301,19 @@
264 301
 
265 302
 .phone-row {
266 303
   align-items: start;
304
+  min-height: 104px;
267 305
 }
268 306
 
269 307
 .phone-row .info-label {
270
-  padding-top: 8px;
308
+  min-height: 104px;
309
+  align-items: flex-start;
310
+  padding-top: 16px;
271 311
 }
272 312
 
273 313
 .phone-table {
274 314
   width: 100%;
315
+  padding: 8px 18px;
316
+  box-sizing: border-box;
275 317
 }
276 318
 
277 319
 .phone-header,
@@ -294,45 +336,86 @@
294 336
 }
295 337
 
296 338
 .phone-item {
297
-  min-height: 52px;
298
-  margin-top: 4px;
299
-  padding: 0 10px;
300
-  border: 2px solid #d8caba;
301
-  border-radius: 8px;
302
-  background: #fffdf9;
339
+  min-height: 36px;
340
+  padding: 0;
341
+  border: 0;
342
+  border-radius: 0;
343
+  border-top: 1px solid #eadfce;
344
+  background: transparent;
303 345
   color: #2f2720;
304 346
   font-size: 16px;
305 347
   box-sizing: border-box;
306 348
 }
307 349
 
350
+.phone-item:first-child {
351
+  border-top: 0;
352
+}
353
+
354
+.phone-note {
355
+  min-height: 28px;
356
+  width: fit-content;
357
+  min-width: 88px;
358
+  border-radius: 6px;
359
+  background: #f3eee8;
360
+  display: flex;
361
+  align-items: center;
362
+  justify-content: center;
363
+  justify-self: start;
364
+  padding: 0 14px;
365
+  box-sizing: border-box;
366
+}
367
+
368
+.note-info-row {
369
+  align-items: center;
370
+}
371
+
372
+.note-info-row .info-label {
373
+  margin-top: 0;
374
+}
375
+
376
+.note-info-value {
377
+  align-items: center;
378
+  min-height: 64px;
379
+  white-space: pre-wrap;
380
+}
381
+
308 382
 .status-panel {
309
-  min-height: 382px;
310
-  padding: 24px 22px 22px;
383
+  min-height: 100%;
384
+  padding: 26px 20px 24px;
311 385
   border: 2px solid #d8caba;
312
-  border-radius: 62px;
386
+  border-radius: 12px;
313 387
   background: #fffdf9;
314 388
   box-sizing: border-box;
315 389
 }
316 390
 
317 391
 .status-panel h2 {
318
-  margin: 0 0 18px;
392
+  margin: 0 0 24px;
319 393
   color: #2f2720;
320 394
   font-size: 22px;
321 395
   font-weight: 800;
396
+  line-height: 1.3;
397
+  display: flex;
398
+  align-items: center;
399
+  gap: 12px;
400
+}
401
+
402
+.panel-heading-icon {
403
+  color: #8a6543;
404
+  font-weight: 800;
322 405
 }
323 406
 
324 407
 .status-card-list {
325 408
   display: grid;
326 409
   grid-template-columns: 1fr 1fr;
327
-  gap: 14px;
328
-  margin-bottom: 20px;
410
+  gap: 16px;
411
+  margin-bottom: 28px;
329 412
 }
330 413
 
331 414
 .status-card {
332
-  min-height: 104px;
333
-  padding: 14px 18px;
415
+  min-height: 126px;
416
+  padding: 18px 16px;
334 417
   border: 2px solid #d8caba;
335
-  border-radius: 14px;
418
+  border-radius: 8px;
336 419
   background: #ffffff;
337 420
   color: #2f2720;
338 421
   text-decoration: none;
@@ -347,20 +430,23 @@
347 430
   margin: 0;
348 431
   color: #7b6b5c;
349 432
   font-size: 16px;
433
+  text-align: center;
350 434
 }
351 435
 
352 436
 .status-count {
353
-  margin: 2px 0 0;
437
+  margin: 10px 0 0;
354 438
   color: #2f2720;
355 439
   font-size: 32px;
356 440
   font-weight: 800;
357 441
   line-height: 1.1;
442
+  text-align: center;
358 443
 }
359 444
 
360 445
 .status-link {
361
-  margin: 4px 0 0;
446
+  margin: 10px 0 0;
362 447
   color: #7b6b5c;
363 448
   font-size: 14px;
449
+  text-align: center;
364 450
 }
365 451
 
366 452
 .next-memorial {
@@ -368,19 +454,26 @@
368 454
 }
369 455
 
370 456
 .next-memorial h3 {
371
-  margin: 0 0 8px;
457
+  margin: 0 0 16px;
372 458
   color: #4b3c31;
373
-  font-size: 17px;
459
+  font-size: 18px;
374 460
   font-weight: 800;
461
+  display: flex;
462
+  align-items: center;
463
+  gap: 12px;
375 464
 }
376 465
 
377 466
 .memorial-card {
378
-  min-height: 72px;
379
-  padding: 14px 20px;
467
+  min-height: 116px;
468
+  padding: 24px 18px;
380 469
   border: 2px solid #d8caba;
381
-  border-radius: 10px;
470
+  border-radius: 8px;
382 471
   background: #ffffff;
383 472
   box-sizing: border-box;
473
+  display: flex;
474
+  flex-direction: column;
475
+  justify-content: center;
476
+  text-align: center;
384 477
 }
385 478
 
386 479
 .memorial-title {
@@ -446,8 +539,9 @@
446 539
 
447 540
   h2 {
448 541
     margin: 0;
449
-    font-size: 20px;
450
-    font-weight: 700;
542
+    font-size: 22px;
543
+    line-height: 1.3;
544
+    font-weight: 800;
451 545
   }
452 546
 }
453 547
 
@@ -719,8 +813,9 @@
719 813
 
720 814
   h2 {
721 815
     margin: 0;
722
-    font-size: 20px;
723
-    font-weight: 700;
816
+    font-size: 22px;
817
+    line-height: 1.3;
818
+    font-weight: 800;
724 819
   }
725 820
 }
726 821
 
@@ -1184,8 +1279,49 @@
1184 1279
   color: #ffffff;
1185 1280
 }
1186 1281
 
1282
+@media (max-width: 1100px) {
1283
+  .danka-detail-page {
1284
+    grid-template-columns: 1fr;
1285
+    padding: 0 24px 32px;
1286
+  }
1287
+
1288
+  .detail-panel {
1289
+    min-height: auto;
1290
+    border-radius: 28px;
1291
+    padding: 28px 24px 32px;
1292
+  }
1293
+
1294
+  .detail-content {
1295
+    grid-template-columns: 1fr;
1296
+  }
1297
+
1298
+  .page-title-row {
1299
+    flex-direction: column;
1300
+  }
1301
+
1302
+  .edit-button,
1303
+  .add-button,
1304
+  .family-page-add-button {
1305
+    margin-top: 0;
1306
+  }
1307
+}
1308
+
1187 1309
 /* 家族表が狭い画面では横スクロール */
1188 1310
 @media (max-width: 800px) {
1311
+  .detail-content,
1312
+  .info-two-column {
1313
+    grid-template-columns: 1fr;
1314
+  }
1315
+
1316
+  .info-row {
1317
+    grid-template-columns: 1fr;
1318
+    gap: 8px;
1319
+  }
1320
+
1321
+  .phone-row .info-label {
1322
+    min-height: 42px;
1323
+  }
1324
+
1189 1325
   .family-list-summary {
1190 1326
     align-items: flex-start;
1191 1327
     flex-direction: column;
@@ -1303,4 +1439,4 @@
1303 1439
 .family-text {
1304 1440
   writing-mode: vertical-rl;
1305 1441
   text-orientation: upright;
1306
-}
1442
+}

+ 94
- 63
src/app/pages/danka-detail/danka-detail.ts Datei anzeigen

@@ -1,9 +1,10 @@
1
-import { Component } from '@angular/core';
1
+import { ChangeDetectorRef, Component } from '@angular/core';
2 2
 import {
3 3
   ElementRef,
4 4
   ViewChild,
5 5
   AfterViewInit
6 6
 } from '@angular/core';
7
+import { OnInit } from '@angular/core';
7 8
 import { ActivatedRoute, RouterLink } from '@angular/router';
8 9
 import { DankaService } from '../../services/dankaService';
9 10
 import { FamilyService } from '../../services/family-service';
@@ -17,6 +18,7 @@ import { AppSideMenu } from '../../share/side-menu/app-side-menu';
17 18
 import { MarriageRelationService } from '../../services/marriage-relation-service';
18 19
 import { FormsModule } from '@angular/forms';
19 20
 import { EventStatus, EventTarget, EventType } from '../../models/event';
21
+import { EventService } from '../../services/event-service';
20 22
 import {
21 23
   FamilyTreeBuilderService,
22 24
   FamilyTreeNode
@@ -33,6 +35,7 @@ import { FamilyUnitLayout } from '../../models/family-unit-layout';
33 35
 import { FamilyUnitLayoutService } from '../../services/family-unit-layout';
34 36
 
35 37
 
38
+
36 39
 interface NextMemorial {
37 40
   name: string;
38 41
   memorialType: string;
@@ -46,7 +49,7 @@ interface NextMemorial {
46 49
   templateUrl: './danka-detail.html',
47 50
   styleUrl: './danka-detail.scss',
48 51
 })
49
-export class DankaDetail implements AfterViewInit {
52
+export class DankaDetail implements OnInit, AfterViewInit {
50 53
   danka: Danka | undefined;
51 54
   families: Family[] = [];
52 55
   kakocholist: Kakocho[] = [];
@@ -56,7 +59,6 @@ export class DankaDetail implements AfterViewInit {
56 59
   familySearchKeyword = '';
57 60
   eventSearchKeyword = '';
58 61
   eventStatuses: EventStatus[] = ['未案内', '案内済'];
59
-  private eventStatusByTargetId: Record<string, EventStatus> = {};
60 62
   treeNodes: FamilyTreeNode[] = [];
61 63
   layoutNodes: LayoutNode[] = [];
62 64
   layoutNodeMap = new Map<string, LayoutNode>();
@@ -70,6 +72,13 @@ export class DankaDetail implements AfterViewInit {
70 72
 
71 73
   @ViewChild('familyTreeSvg')
72 74
   familyTreeSvg?: ElementRef<SVGSVGElement>;
75
+  @ViewChild('familyTreeSvg')
76
+  set svg(el: ElementRef<SVGSVGElement> | undefined) {
77
+    if (!el) return;
78
+
79
+    this.familyTreeSvg = el;
80
+    this.initializePanZoom();
81
+  }
73 82
   private panZoomInstance: any;
74 83
 
75 84
   readonly PERSON_WIDTH = 90;
@@ -90,7 +99,10 @@ export class DankaDetail implements AfterViewInit {
90 99
     private familyTreeBuilder: FamilyTreeBuilderService,
91 100
     private familyTreeLayout: FamilyTreeLayoutService,
92 101
     private familyUnitLayout: FamilyUnitLayoutService,
102
+    private eventService: EventService,
103
+    private cdr: ChangeDetectorRef,
93 104
   ) {
105
+
94 106
     const tab = this.route.snapshot.queryParams['tab'];
95 107
     if (tab === 'family') {
96 108
       this.selectedTab = 'family';
@@ -102,77 +114,96 @@ export class DankaDetail implements AfterViewInit {
102 114
       this.selectedTab = 'familyTree';
103 115
     }
104 116
 
117
+  }
118
+  ngOnInit(): void {
119
+    this.init();
120
+  }
121
+  async init(): Promise<void> {
105 122
     const id = this.route.snapshot.params['id'];
106
-    if (id) {
107
-      this.danka = this.dankaService.getDankaById(id);
108
-      this.marriageRelations = this.marriageRelationService.getMarriageRelationsByDankaId(id);
109
-      this.families = this.sortFamiliesByHouseholder(this.familyService.getFamiliesByDankaId(id));
110
-      this.selectedFamily = this.families[0];
111
-      this.kakocholist = this.kakochoService.getKakochoByDankaId(id);
112
-      this.nextMemorial = this.getNextMemorial();
113
-
114
-      this.treeNodes =
115
-        this.familyTreeBuilder.build(
116
-          this.families,
117
-          this.marriageRelations
118
-        );
119 123
 
120
-      const units =
121
-        this.familyTreeBuilder.buildFamilyUnits(
122
-          this.treeNodes
123
-        );
124
+    if (!id) return;
124 125
 
125
-      const unitTree =
126
-        this.familyTreeBuilder.buildFamilyUnitTree(
127
-          units
128
-        );
126
+    this.danka = (await this.dankaService.getDankaById(id)) ?? undefined;
127
+    if (!this.danka) return;
129 128
 
130
-      const unitRoots =
131
-        this.familyTreeBuilder.getUnitRoots(
132
-          unitTree
133
-        );
129
+    this.marriageRelations = await this.marriageRelationService.getMarriageRelationsByDankaId(id);
130
+    this.families = this.sortFamiliesByHouseholder(
131
+      await this.familyService.getFamiliesByDankaId(id)
132
+    );
134 133
 
135
-      this.unitLayouts =
136
-        this.familyUnitLayout.buildLayout(
137
-          unitRoots
138
-        );
134
+    this.selectedFamily = this.families[0];
135
+    this.kakocholist = await this.kakochoService.getKakochoByDankaId(id);
139 136
 
140
-      const roots =
141
-        this.familyTreeBuilder.getRoots(
142
-          this.treeNodes
143
-        );
137
+    this.nextMemorial = this.getNextMemorial();
144 138
 
145
-      this.layoutNodes =
146
-        this.familyTreeLayout.buildLayout(
147
-          roots
148
-        );
139
+    this.treeNodes =
140
+      this.familyTreeBuilder.build(
141
+        this.families,
142
+        this.marriageRelations,
143
+        this.kakocholist
144
+      );
149 145
 
150
-      this.rebuildLayoutNodeMap();
146
+    const units =
147
+      this.familyTreeBuilder.buildFamilyUnits(
148
+        this.treeNodes
149
+      );
151 150
 
152
-      this.unitLayouts =
153
-        this.familyUnitLayout.buildLayout(
154
-          unitRoots
155
-        );
151
+    const unitTree =
152
+      this.familyTreeBuilder.buildFamilyUnitTree(
153
+        units
154
+      );
156 155
 
157
-      this.rebuildUnitLayoutMap();
156
+    const unitRoots =
157
+      this.familyTreeBuilder.getUnitRoots(
158
+        unitTree
159
+      );
158 160
 
159
-      this.calculateViewBox();
161
+    this.unitLayouts =
162
+      this.familyUnitLayout.buildLayout(
163
+        unitRoots
164
+      );
160 165
 
161
-      this.kakocholist.forEach(kakocho => {
162
-        if (kakocho.familyId) {
163
-          this.deathDateMap.set(
164
-            kakocho.familyId,
165
-            kakocho
166
-          );
167
-        }
168
-      });
166
+    const roots =
167
+      this.familyTreeBuilder.getRoots(
168
+        this.treeNodes
169
+      );
170
+
171
+    this.layoutNodes =
172
+      this.familyTreeLayout.buildLayout(
173
+        roots
174
+      );
169 175
 
170
-      this.kakocholist.forEach(k => {
171
-        const key = this.normalizeName(k.name) + '_' + k.dankaId;
172
-        this.kakochoByNameMap.set(key, k);
173
-      });
176
+    this.rebuildLayoutNodeMap();
177
+
178
+    this.unitLayouts =
179
+      this.familyUnitLayout.buildLayout(
180
+        unitRoots
181
+      );
182
+
183
+    this.rebuildUnitLayoutMap();
184
+
185
+    this.calculateViewBox();
186
+
187
+    this.kakocholist.forEach(kakocho => {
188
+      if (kakocho.familyId) {
189
+        this.deathDateMap.set(
190
+          kakocho.familyId,
191
+          kakocho
192
+        );
193
+      }
194
+    });
195
+
196
+    this.kakocholist.forEach(k => {
197
+      const key = this.normalizeName(k.name) + '_' + k.dankaId;
198
+      this.kakochoByNameMap.set(key, k);
199
+    });
200
+
201
+    // Angularへ反映
202
+    this.cdr.detectChanges();
203
+
204
+    // DOM描画完了待ち
205
+    await new Promise(resolve => setTimeout(resolve));
174 206
 
175
-    }
176 207
   }
177 208
 
178 209
   ngAfterViewInit(): void {
@@ -288,6 +319,7 @@ export class DankaDetail implements AfterViewInit {
288 319
         const age = this.currentYear - birthDate.getFullYear();
289 320
         return this.getEventTypes(age).map((eventType) => {
290 321
           const id = `${family.id}-${eventType}`;
322
+          const defaultStatus: EventStatus = Number(family.id) % 2 === 0 ? '案内済' : '未案内';
291 323
           return {
292 324
             id,
293 325
             dankaId: family.dankaId,
@@ -299,8 +331,7 @@ export class DankaDetail implements AfterViewInit {
299 331
             age,
300 332
             eventType,
301 333
             note: family.note,
302
-            status:
303
-              this.eventStatusByTargetId[id] ?? (Number(family.id) % 2 === 0 ? '案内済' : '未案内'),
334
+            status: this.eventService.getEventStatus(id, defaultStatus),
304 335
           };
305 336
         });
306 337
       })
@@ -335,7 +366,7 @@ export class DankaDetail implements AfterViewInit {
335 366
 
336 367
   changeEventStatus(target: EventTarget, status: EventStatus): void {
337 368
     target.status = status;
338
-    this.eventStatusByTargetId[target.id] = status;
369
+    this.eventService.saveEventStatus(target.id, status);
339 370
   }
340 371
 
341 372
   getKaiki(deathDate: string): number {

+ 21
- 17
src/app/pages/danka-edit/danka-edit.html Datei anzeigen

@@ -100,7 +100,6 @@
100 100
           <section class="phone-edit-section">
101 101
             <div class="section-heading">
102 102
               <h2>電話番号(複数登録)</h2>
103
-              <p>番号と備考を複数登録できます。</p>
104 103
             </div>
105 104
 
106 105
             <div formArrayName="phones" class="phone-table">
@@ -148,22 +147,27 @@
148 147
         </div>
149 148
 
150 149
         <div class="bottom-actions">
151
-          <button type="button" class="delete-button" (click)="deleteDanka()">
152
-            削除
153
-          </button>
154
-
155
-          <button type="button" class="cancel-button" [routerLink]="['/danka-detail', danka?.id]">
156
-            キャンセル
157
-          </button>
158
-
159
-          <button
160
-            type="button"
161
-            class="save-button"
162
-            [disabled]="dankaForm.invalid"
163
-            (click)="saveDanka()"
164
-          >
165
-            保存
166
-          </button>
150
+          <div class="left-actions"></div>
151
+
152
+          <div class="right-actions">
153
+            @if (danka) {
154
+              <button type="button" class="delete-button" (click)="deleteDanka()">
155
+                削除
156
+              </button>
157
+            }
158
+            <button type="button" class="cancel-button" [routerLink]="danka ? ['/danka-detail', danka.id] : ['/danka-list']">
159
+              キャンセル
160
+            </button>
161
+
162
+            <button
163
+              type="button"
164
+              class="save-button"
165
+              [disabled]="dankaForm.invalid"
166
+              (click)="saveDanka()"
167
+            >
168
+              保存
169
+            </button>
170
+          </div>
167 171
         </div>
168 172
       </form>
169 173
     </section>

+ 37
- 16
src/app/pages/danka-edit/danka-edit.scss Datei anzeigen

@@ -2,7 +2,7 @@
2 2
   position: relative;
3 3
   display: block;
4 4
   min-height: 100vh;
5
-  background: #f4eee4;
5
+  background: #f6f0e7;
6 6
   color: #2f2720;
7 7
 }
8 8
 
@@ -11,7 +11,7 @@
11 11
   grid-template-columns: 172px minmax(0, 1fr);
12 12
   gap: 20px;
13 13
   padding: 0 38px 36px 0;
14
-  background: #f4eee4;
14
+  background: #f6f0e7;
15 15
 }
16 16
 
17 17
 .danka-edit-main {
@@ -22,7 +22,7 @@
22 22
 .edit-panel {
23 23
   min-height: 760px;
24 24
   padding: 34px 42px 40px;
25
-  background: #ffffff;
25
+  background: #fffdf9;
26 26
   border: 2px solid #d8caba;
27 27
   border-radius: 64px;
28 28
   box-sizing: border-box;
@@ -55,20 +55,28 @@
55 55
 
56 56
 .edit-content {
57 57
   display: grid;
58
-  grid-template-columns: minmax(0, 1fr) 500px;
59
-  gap: 32px;
58
+  grid-template-columns: minmax(0, 1fr) 430px;
59
+  gap: 22px;
60 60
   align-items: start;
61 61
 }
62 62
 
63 63
 .basic-edit-section,
64 64
 .phone-edit-section {
65
-  padding-top: 0;
65
+  overflow: hidden;
66
+  padding: 0 28px 28px;
67
+  border: 2px solid #d8caba;
68
+  border-radius: 12px;
69
+  background: #fffdf9;
70
+  box-sizing: border-box;
66 71
 }
67 72
 
68 73
 .basic-edit-section h2,
69 74
 .phone-edit-section h2,
70 75
 .support-box h2 {
71
-  margin: 0;
76
+  margin: 0 -28px 20px;
77
+  padding: 14px 28px;
78
+  background: #eadfce;
79
+  border-bottom: 2px solid #d8caba;
72 80
   color: #2f2720;
73 81
   font-size: 22px;
74 82
   line-height: 1.3;
@@ -83,7 +91,9 @@
83 91
 
84 92
 /* 基本情報 */
85 93
 .form-list {
86
-  margin-top: 12px;
94
+  margin-top: 0;
95
+  padding-top: 18px;
96
+  border-top: 2px solid #eadfce;
87 97
 }
88 98
 
89 99
 .form-field {
@@ -94,10 +104,10 @@
94 104
 
95 105
 .form-row {
96 106
   display: grid;
97
-  grid-template-columns: 140px 1fr;
107
+  grid-template-columns: 132px 1fr;
98 108
   align-items: center;
99
-  gap: 14px;
100
-  margin-bottom: 12px;
109
+  gap: 18px;
110
+  margin-bottom: 14px;
101 111
 }
102 112
 
103 113
 .form-row label {
@@ -124,8 +134,12 @@
124 134
   padding: 0 14px;
125 135
 }
126 136
 
137
+.form-row:has(#postalCode) input {
138
+  max-width: 300px;
139
+}
140
+
127 141
 .form-row textarea {
128
-  min-height: 104px;
142
+  min-height: 150px;
129 143
   padding: 12px 14px;
130 144
   line-height: 1.6;
131 145
   resize: vertical;
@@ -157,7 +171,7 @@
157 171
 .phone-table-header,
158 172
 .phone-table-row {
159 173
   display: grid;
160
-  grid-template-columns: 1.35fr 1.45fr 84px;
174
+  grid-template-columns: minmax(110px, 1fr) minmax(88px, 0.8fr) 72px;
161 175
   align-items: center;
162 176
   gap: 10px;
163 177
 }
@@ -235,7 +249,7 @@
235 249
 .phone-action {
236 250
   display: flex;
237 251
   justify-content: flex-end;
238
-  margin-top: 10px;
252
+  margin-top: 16px;
239 253
 }
240 254
 
241 255
 .add-phone-button {
@@ -277,10 +291,17 @@
277 291
 /* 下部ボタン */
278 292
 .bottom-actions {
279 293
   display: flex;
280
-  justify-content: flex-end;
294
+  justify-content: space-between;
295
+  align-items: center;
296
+  gap: 12px;
297
+  margin-top: 36px;
298
+}
299
+
300
+.left-actions,
301
+.right-actions {
302
+  display: flex;
281 303
   align-items: center;
282 304
   gap: 12px;
283
-  margin-top: 26px;
284 305
 }
285 306
 
286 307
 .delete-button,

+ 13
- 5
src/app/pages/danka-edit/danka-edit.ts Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { Component, inject } from '@angular/core';
1
+import { Component, inject, OnInit } from '@angular/core';
2 2
 import {
3 3
   FormBuilder,
4 4
   FormGroup,
@@ -19,7 +19,7 @@ import { Danka } from '../../models/danka';
19 19
   templateUrl: './danka-edit.html',
20 20
   styleUrl: './danka-edit.scss',
21 21
 })
22
-export class DankaEdit {
22
+export class DankaEdit implements OnInit {
23 23
   danka: Danka | undefined;
24 24
 
25 25
   dankaForm = new FormGroup({
@@ -38,9 +38,17 @@ export class DankaEdit {
38 38
     private route: ActivatedRoute,
39 39
     private router: Router,
40 40
   ) {
41
+  }
42
+
43
+  ngOnInit(): void {
44
+    this.init();
45
+  }
46
+
47
+  async init(): Promise<void> {
41 48
     const id = this.route.snapshot.params['id'];
49
+
42 50
     if (id) {
43
-      this.danka = this.dankaService.getDankaById(id);
51
+      this.danka = await this.dankaService.getDankaById(id);
44 52
 
45 53
       if (this.danka) {
46 54
         this.dankaForm.patchValue({
@@ -60,7 +68,7 @@ export class DankaEdit {
60 68
         }
61 69
       }
62 70
     }
63
-    console.log(this.danka);
71
+
64 72
   }
65 73
 
66 74
   get phones() {
@@ -79,7 +87,7 @@ export class DankaEdit {
79 87
   }
80 88
 
81 89
   removePhone(index: number) {
82
-    if(this.phones.length > 1) {
90
+    if (this.phones.length > 1) {
83 91
       this.phones.removeAt(index);
84 92
     }
85 93
   }

+ 50
- 47
src/app/pages/danka-list/danka-list.ts Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { Component } from '@angular/core';
1
+import { Component, OnInit } from '@angular/core';
2 2
 import { RouterLink } from '@angular/router';
3 3
 import { DankaService } from '../../services/dankaService';
4 4
 import { FamilyService } from '../../services/family-service';
@@ -26,13 +26,14 @@ type KanaRowValue =
26 26
   templateUrl: './danka-list.html',
27 27
   styleUrl: './danka-list.scss',
28 28
 })
29
-export class DankaList {
29
+export class DankaList implements OnInit {
30 30
   dankaList: Danka[] = [];
31 31
   filterDankaList: Danka[] = [];
32 32
   searchKeyword: string = '';
33 33
   dankaDisplay: number = 0;
34 34
   selectedFilter = 'all';
35 35
   selectedKanaRow: KanaRowValue = 'all';
36
+
36 37
   kanaRows: { label: string; value: KanaRowValue }[] = [
37 38
     { label: '全件', value: 'all' },
38 39
     { label: 'あ行', value: 'a' },
@@ -48,27 +49,33 @@ export class DankaList {
48 49
   ];
49 50
 
50 51
   private readonly kanaRowMap: Record<Exclude<KanaRowValue, 'all'>, string[]> = {
51
-    a: ['あ', 'い', 'う', 'え', 'お', 'ア', 'イ', 'ウ', 'エ', 'オ'],
52
-    ka: ['か', 'き', 'く', 'け', 'こ', 'が', 'ぎ', 'ぐ', 'げ', 'ご', 'カ', 'キ', 'ク', 'ケ', 'コ', 'ガ', 'ギ', 'グ', 'ゲ', 'ゴ'],
53
-    sa: ['さ', 'し', 'す', 'せ', 'そ', 'ざ', 'じ', 'ず', 'ぜ', 'ぞ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'ザ', 'ジ', 'ズ', 'ゼ', 'ゾ'],
54
-    ta: ['た', 'ち', 'つ', 'て', 'と', 'だ', 'ぢ', 'づ', 'で', 'ど', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ダ', 'ヂ', 'ヅ', 'デ', 'ド'],
55
-    na: ['な', 'に', 'ぬ', 'ね', 'の', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ'],
56
-    ha: ['は', 'ひ', 'ふ', 'へ', 'ほ', 'ば', 'び', 'ぶ', 'べ', 'ぼ', 'ぱ', 'ぴ', 'ぷ', 'ぺ', 'ぽ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'バ', 'ビ', 'ブ', 'ベ', 'ボ', 'パ', 'ピ', 'プ', 'ペ', 'ポ'],
57
-    ma: ['ま', 'み', 'む', 'め', 'も', 'マ', 'ミ', 'ム', 'メ', 'モ'],
58
-    ya: ['や', 'ゆ', 'よ', 'ヤ', 'ユ', 'ヨ'],
59
-    ra: ['ら', 'り', 'る', 'れ', 'ろ', 'ラ', 'リ', 'ル', 'レ', 'ロ'],
60
-    wa: ['わ', 'を', 'ん', 'ワ', 'ヲ', 'ン'],
52
+    a: ['あ','い','う','え','お','ア','イ','ウ','エ','オ'],
53
+    ka: ['か','き','く','け','こ','が','ぎ','ぐ','げ','ご','カ','キ','ク','ケ','コ','ガ','ギ','グ','ゲ','ゴ'],
54
+    sa: ['さ','し','す','せ','そ','ざ','じ','ず','ぜ','ぞ','サ','シ','ス','セ','ソ','ザ','ジ','ズ','ゼ','ゾ'],
55
+    ta: ['た','ち','つ','て','と','だ','ぢ','づ','で','ど','タ','チ','ツ','テ','ト','ダ','ヂ','ヅ','デ','ド'],
56
+    na: ['な','に','ぬ','ね','の','ナ','ニ','ヌ','ネ','ノ'],
57
+    ha: ['は','ひ','ふ','へ','ほ','ば','び','ぶ','べ','ぼ','ぱ','ぴ','ぷ','ぺ','ぽ','ハ','ヒ','フ','ヘ','ホ','バ','ビ','ブ','ベ','ボ','パ','ピ','プ','ペ','ポ'],
58
+    ma: ['ま','み','む','め','も','マ','ミ','ム','メ','モ'],
59
+    ya: ['や','ゆ','よ','ヤ','ユ','ヨ'],
60
+    ra: ['ら','り','る','れ','ろ','ラ','リ','ル','レ','ロ'],
61
+    wa: ['わ','を','ん','ワ','ヲ','ン'],
61 62
   };
62 63
 
63 64
   constructor(
64 65
     private dankaService: DankaService,
65 66
     private familyService: FamilyService,
66
-  ) {
67
-    this.dankaList = this.dankaService.getDankaList();
67
+  ) {}
68
+
69
+  // 非同期初期化
70
+  ngOnInit(): void {
71
+    this.init();
72
+  }
73
+
74
+  async init(): Promise<void> {
75
+    this.dankaList = await this.dankaService.getDankaList();
68 76
     this.showAllDanka();
69 77
   }
70 78
 
71
-  //全件タグで絞り込み
72 79
   showAllDanka() {
73 80
     this.selectedFilter = 'all';
74 81
     this.selectedKanaRow = 'all';
@@ -77,44 +84,39 @@ export class DankaList {
77 84
     this.dankaDisplay = this.filterDankaList.length;
78 85
   }
79 86
 
80
-  //電話番号タグで絞り込み
81 87
   filterPhoneAvailable() {
82 88
     this.selectedFilter = 'phone';
83 89
     this.selectedKanaRow = 'all';
84
-    //電話番号タブに該当する檀家を表示
85
-    this.filterDankaList = this.dankaList.filter((danka) => {
86
-      return danka.phones.some((phone) => phone.tel.trim() !== '');
87
-    });
90
+
91
+    this.filterDankaList = this.dankaList.filter((danka) =>
92
+      danka.phones.some((phone) => phone.tel.trim() !== '')
93
+    );
94
+
88 95
     this.dankaDisplay = this.filterDankaList.length;
89 96
   }
90 97
 
91
-  //検索処理
92 98
   searchDanka() {
93 99
     this.selectedFilter = 'search';
94 100
     this.selectedKanaRow = 'all';
95
-    //検索欄に入力された値から余白を削除
101
+
96 102
     const keyword = this.searchKeyword.trim();
97
-    //検索欄が空の場合は檀家の一覧を表示
103
+
98 104
     if (keyword === '') {
99
-      this.filterDankaList = this.dankaList;
100
-      this.dankaDisplay = this.filterDankaList.length;
105
+      this.showAllDanka();
101 106
       return;
102 107
     }
103 108
 
104
-    //検索欄に値がある場合は値に該当する内容を表示
105
-    this.filterDankaList = this.dankaList.filter((danka) => {
106
-      return (
107
-        danka.householdName.includes(keyword) ||
108
-        danka.householdFurigana.includes(keyword) ||
109
-        danka.householder.includes(keyword) ||
110
-        danka.householderFurigana.includes(keyword) ||
111
-        danka.postalCode.includes(keyword) ||
112
-        danka.address.includes(keyword) ||
113
-        danka.phones.some((phone) => phone.tel.includes(keyword) || phone.note.includes(keyword))
114
-      );
115
-    });
109
+    this.filterDankaList = this.dankaList.filter((danka) =>
110
+      danka.householdName.includes(keyword) ||
111
+      danka.householdFurigana.includes(keyword) ||
112
+      danka.householder.includes(keyword) ||
113
+      danka.householderFurigana.includes(keyword) ||
114
+      danka.postalCode.includes(keyword) ||
115
+      danka.address.includes(keyword) ||
116
+      danka.phones.some((p) => p.tel.includes(keyword) || p.note.includes(keyword))
117
+    );
118
+
116 119
     this.dankaDisplay = this.filterDankaList.length;
117
-    console.log(this.dankaDisplay);
118 120
   }
119 121
 
120 122
   filterByKanaRow(row: KanaRowValue): void {
@@ -123,15 +125,15 @@ export class DankaList {
123 125
     this.searchKeyword = '';
124 126
 
125 127
     if (row === 'all') {
126
-      this.filterDankaList = this.dankaList;
127
-      this.dankaDisplay = this.filterDankaList.length;
128
+      this.showAllDanka();
128 129
       return;
129 130
     }
130 131
 
131
-    this.filterDankaList = this.dankaList.filter((danka) => {
132
-      const firstKana = this.getDankaSortText(danka).charAt(0);
132
+    this.filterDankaList = this.dankaList.filter(async (danka) => {
133
+      const firstKana = (await this.getDankaSortText(danka)).charAt(0);
133 134
       return this.kanaRowMap[row].includes(firstKana);
134 135
     });
136
+
135 137
     this.dankaDisplay = this.filterDankaList.length;
136 138
   }
137 139
 
@@ -143,11 +145,12 @@ export class DankaList {
143 145
     this.dankaDisplay = 0;
144 146
   }
145 147
 
146
-  private getDankaSortText(danka: Danka): string {
148
+  private async getDankaSortText(danka: Danka): Promise<string> {
147 149
     const householderName = this.normalizeName(danka.householder);
148
-    const householderFamily = this.familyService
149
-      .getFamiliesByDankaId(danka.id)
150
-      .find((family) => this.normalizeName(family.name) === householderName);
150
+
151
+    const householderFamily = (await this.familyService
152
+      .getFamiliesByDankaId(danka.id))
153
+      .find((f) => this.normalizeName(f.name) === householderName);
151 154
 
152 155
     return (householderFamily?.furigana || danka.householder).trim();
153 156
   }
@@ -155,4 +158,4 @@ export class DankaList {
155 158
   private normalizeName(name: string): string {
156 159
     return name.replace(/\s/g, '');
157 160
   }
158
-}
161
+}

+ 28
- 32
src/app/pages/dashboard/dashboard.html Datei anzeigen

@@ -9,11 +9,11 @@
9 9
         <div>
10 10
           <h1>ホーム</h1>
11 11
         </div>
12
-        <div class="date-pill">2026年5月28日 木曜日</div>
12
+        <div class="date-pill">{{ todayLabel }}</div>
13 13
       </div>
14 14
 
15 15
       <section class="overview" aria-label="概要">
16
-        <a class="card primary" href="#">
16
+        <a class="card" href="#">
17 17
           <div class="card-label">今週の法要</div>
18 18
           <div class="metric"><strong>{{ weeklyMemorialCount }}</strong><span>件</span></div>
19 19
           <p class="card-text">
@@ -29,58 +29,55 @@
29 29
         <div class="search-card">
30 30
           <div class="search-head">
31 31
             <div class="search-label">まとめて検索</div>
32
-            <div class="search-title">檀家・家族・故人を探す</div>
32
+            <div class="search-title"><!--檀家・-->家族・故人を探す</div>
33 33
           </div>
34 34
           <input
35 35
             class="search-input"
36 36
             type="search"
37
+            [(ngModel)]="searchKeyword"
37 38
             placeholder="氏名、ふりがな、住所、戒名で検索"
38 39
             aria-label="まとめて検索"
40
+            (keydown.enter)="searchAll()"
39 41
           />
40
-          <button class="search-button" type="button">検索</button>
42
+          <button class="search-button" type="button" (click)="searchAll()">検索</button>
41 43
         </div>
42 44
       </section>
43 45
 
44 46
       <section class="section">
45 47
         <div class="section-head">
46 48
           <div>
47
-            <h2>最近開いた檀家・世帯</h2>
49
+            <h2>最近開いた檀家</h2>
48 50
           </div>
49
-          <a class="text-link" href="#">一覧へ</a>
50 51
         </div>
51 52
 
52 53
         <div class="recent-table" role="table" aria-label="最近開いた檀家">
53 54
           <div class="recent-row recent-row-head" role="row">
54
-            <div class="cell" role="columnheader">施主名</div>
55
-            <div class="cell" role="columnheader">ふりがな</div>
55
+            <div class="cell" role="columnheader">筆頭者・ふりがな</div>
56 56
             <div class="cell" role="columnheader">住所</div>
57 57
             <div class="cell" role="columnheader">次の法要</div>
58 58
             <div class="cell" role="columnheader">最終更新</div>
59 59
           </div>
60 60
 
61
-          <a class="recent-row" href="#" role="row">
62
-            <div class="cell" role="cell">鈴木 太郎</div>
63
-            <div class="cell muted" role="cell">すずき</div>
64
-            <div class="cell" role="cell">市内 1-2-3</div>
65
-            <div class="cell" role="cell">6月12日</div>
66
-            <div class="cell muted" role="cell">今日</div>
67
-          </a>
68
-
69
-          <a class="recent-row" href="#" role="row">
70
-            <div class="cell" role="cell">佐藤 恵一</div>
71
-            <div class="cell muted" role="cell">さとう</div>
72
-            <div class="cell" role="cell">市内 2-8-1</div>
73
-            <div class="cell" role="cell">7月4日</div>
74
-            <div class="cell muted" role="cell">昨日</div>
75
-          </a>
76
-
77
-          <a class="recent-row" href="#" role="row">
78
-            <div class="cell" role="cell">田中 雪子</div>
79
-            <div class="cell muted" role="cell">たなか</div>
80
-            <div class="cell" role="cell">市内 3-4-6</div>
81
-            <div class="cell muted" role="cell">未設定</div>
82
-            <div class="cell muted" role="cell">5日前</div>
83
-          </a>
61
+          @if (recentDankaList.length > 0) {
62
+            @for (recent of recentDankaList; track recent.danka.id) {
63
+              <a class="recent-row" [routerLink]="['/danka-detail', recent.danka.id]" role="row">
64
+                <div class="cell" role="cell">
65
+                  <p class="recent-name">{{ recent.danka.householder }}</p>
66
+                  <p class="recent-sub">{{ recent.danka.householderFurigana || 'ふりがな未登録' }}</p>
67
+                </div>
68
+                <div class="cell" role="cell">{{ recent.danka.address }}</div>
69
+                <div class="cell" role="cell">{{ recent.nextMemorialLabel }}</div>
70
+                <div class="cell" role="cell">{{ recent.updatedAtLabel }}</div>
71
+              </a>
72
+            }
73
+          } @else {
74
+            <div class="recent-row" role="row">
75
+              <div class="cell muted" role="cell">表示できる檀家はありません</div>
76
+              <div class="cell muted" role="cell">-</div>
77
+              <div class="cell muted" role="cell">-</div>
78
+              <div class="cell muted" role="cell">-</div>
79
+            </div>
80
+          }
84 81
         </div>
85 82
       </section>
86 83
 
@@ -89,7 +86,6 @@
89 86
           <div>
90 87
             <h2>近日の法要・命日</h2>
91 88
           </div>
92
-          <a class="text-link" href="#">年次法要一覧へ</a>
93 89
         </div>
94 90
 
95 91
         <div class="upcoming-list">

+ 8
- 12
src/app/pages/dashboard/dashboard.scss Datei anzeigen

@@ -104,11 +104,6 @@ h1 {
104 104
   background: #fff8ed;
105 105
 }
106 106
 
107
-.card.primary {
108
-  background: var(--focus);
109
-  border-color: var(--accent);
110
-}
111
-
112 107
 .card-label,
113 108
 .search-label {
114 109
   color: var(--muted);
@@ -203,12 +198,6 @@ h2 {
203 198
   font-size: 14px;
204 199
 }
205 200
 
206
-.text-link {
207
-  color: var(--brown-dark);
208
-  font-weight: 900;
209
-  white-space: nowrap;
210
-}
211
-
212 201
 .recent-table {
213 202
   overflow: hidden;
214 203
   border: 2px solid var(--line);
@@ -218,7 +207,7 @@ h2 {
218 207
 
219 208
 .recent-row {
220 209
   display: grid;
221
-  grid-template-columns: 1.2fr 1fr 1.4fr 0.95fr 0.85fr;
210
+  grid-template-columns: 1.4fr 1.6fr 0.95fr 0.85fr;
222 211
   min-height: 52px;
223 212
   border-top: 1px solid var(--line);
224 213
   align-items: center;
@@ -242,6 +231,13 @@ h2 {
242 231
   color: var(--muted);
243 232
 }
244 233
 
234
+.recent-sub {
235
+  margin-top: 4px;
236
+  color: var(--text);
237
+  font-size: 13px;
238
+  line-height: 1.35;
239
+}
240
+
245 241
 .upcoming-list {
246 242
   display: grid;
247 243
   gap: 10px;

+ 128
- 20
src/app/pages/dashboard/dashboard.ts Datei anzeigen

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

+ 5
- 1
src/app/pages/event/event.html Datei anzeigen

@@ -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>

+ 25
- 29
src/app/pages/event/event.scss Datei anzeigen

@@ -7,24 +7,24 @@
7 7
 }
8 8
 
9 9
 .event-page {
10
-  display: flex;
11
-  align-items: flex-start;
12
-  gap: 8px;
10
+  display: grid;
11
+  grid-template-columns: 172px minmax(0, 1fr);
12
+  gap: 20px;
13
+  padding: 0 38px 36px 0;
13 14
   background: #f4eee4;
14 15
 }
15 16
 
16 17
 .event-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19 19
   box-sizing: border-box;
20 20
 }
21 21
 
22 22
 .event-panel {
23
-  min-height: 650px;
24
-  padding: 26px 34px 36px;
23
+  min-height: 760px;
24
+  padding: 34px 42px 40px;
25 25
   background: #ffffff;
26 26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28 28
   box-sizing: border-box;
29 29
 }
30 30
 
@@ -45,10 +45,9 @@
45 45
 .page-title-row h1 {
46 46
   margin: 0;
47 47
   color: #2f2720;
48
-  font-size: 32px;
49
-  line-height: 1.2;
48
+  font-size: 34px;
49
+  line-height: 1.1;
50 50
   font-weight: 800;
51
-  letter-spacing: 0.02em;
52 51
 }
53 52
 
54 53
 .filter-row {
@@ -74,7 +73,7 @@
74 73
 
75 74
 .filter-field select,
76 75
 .search-field input {
77
-  height: 38px;
76
+  height: 46px;
78 77
   padding: 0 14px;
79 78
   border: 2px solid #d8caba;
80 79
   border-radius: 8px;
@@ -116,7 +115,7 @@
116 115
 .list-header-row h2 {
117 116
   margin: 0;
118 117
   color: #2f2720;
119
-  font-size: 26px;
118
+  font-size: 22px;
120 119
   font-weight: 800;
121 120
 }
122 121
 
@@ -147,7 +146,7 @@
147 146
 }
148 147
 
149 148
 .event-table-header {
150
-  min-height: 46px;
149
+  min-height: 40px;
151 150
   padding: 0 14px;
152 151
   background: #efe4d6;
153 152
   color: #111111;
@@ -157,7 +156,7 @@
157 156
 }
158 157
 
159 158
 .event-table-row {
160
-  min-height: 78px;
159
+  min-height: 62px;
161 160
   padding: 10px 14px;
162 161
   border-top: 1px solid #d8c2aa;
163 162
   background: #fffdf9;
@@ -234,6 +233,17 @@
234 233
 }
235 234
 
236 235
 @media (max-width: 1100px) {
236
+  .event-page {
237
+    grid-template-columns: 1fr;
238
+    padding: 0 24px 32px;
239
+  }
240
+
241
+  .event-panel {
242
+    min-height: auto;
243
+    border-radius: 28px;
244
+    padding: 28px 24px 32px;
245
+  }
246
+
237 247
   .page-title-row {
238 248
     flex-direction: column;
239 249
   }
@@ -249,20 +259,6 @@
249 259
 }
250 260
 
251 261
 @media (max-width: 800px) {
252
-  .event-page {
253
-    flex-direction: column;
254
-  }
255
-
256
-  .event-main {
257
-    width: 100%;
258
-    padding: 16px 20px 32px;
259
-  }
260
-
261
-  .event-panel {
262
-    padding: 24px 20px 30px;
263
-    border-radius: 32px;
264
-  }
265
-
266 262
   .page-title-row h1 {
267 263
     font-size: 26px;
268 264
   }

+ 88
- 29
src/app/pages/event/event.ts Datei anzeigen

@@ -1,7 +1,8 @@
1
-import { Component } 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';
5
+import { EventService } from '../../services/event-service';
5 6
 import { EventStatus, EventTarget, EventType } from '../../models/event';
6 7
 import { AppHeader } from '../../share/header/app-header';
7 8
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
@@ -12,8 +13,9 @@ import { AppSideMenu } from '../../share/side-menu/app-side-menu';
12 13
   templateUrl: './event.html',
13 14
   styleUrl: './event.scss',
14 15
 })
15
-export class EventPage {
16
+export class EventPage implements OnInit{
16 17
   eventTargets: EventTarget[] = [];
18
+  isLoading = true;
17 19
   targetYear: number = new Date().getFullYear();
18 20
   selectedEventType: EventType | 'all' = 'all';
19 21
   selectedStatus: EventStatus | 'all' = 'all';
@@ -37,59 +39,89 @@ export class EventPage {
37 39
     { label: '未案内', value: '未案内' },
38 40
     { label: '案内済', value: '案内済' },
39 41
   ];
40
-  private statusByTargetId: Record<string, EventStatus> = {};
42
+  private eventRequestId = 0;
41 43
 
42 44
   constructor(
43 45
     private dankaService: DankaService,
44 46
     private familyService: FamilyService,
45
-  ) {
46
-    this.createEventTargetList();
47
+    private eventService: EventService,
48
+    private cdr: ChangeDetectorRef,
49
+  ) { }
50
+
51
+  ngOnInit(): void {
52
+    this.init();
53
+  }
54
+
55
+  async init(): Promise<void> {
56
+    await this.createEventTargetList();
47 57
   }
48 58
 
49
-  createEventTargetList(): void {
50
-    this.eventTargets = [];
59
+  async createEventTargetList(): Promise<void> {
60
+    const requestId = ++this.eventRequestId;
61
+    const targetYear = this.targetYear;
62
+    const selectedEventType = this.selectedEventType;
63
+    const eventTargets: EventTarget[] = [];
51 64
 
52
-    this.familyService.getFamilyList().forEach((family) => {
65
+    if (this.eventTargets.length === 0) {
66
+      this.isLoading = true;
67
+      this.cdr.detectChanges();
68
+    }
69
+
70
+    const families = await this.familyService.getFamilyList();
71
+
72
+    for (const family of families) {
53 73
       const birthDate = this.parseDate(family.birthDate);
54
-      if (!birthDate) {
55
-        return;
56
-      }
74
+      if (!birthDate) continue;
57 75
 
58
-      const age = this.targetYear - birthDate.getFullYear();
76
+      const age = targetYear - birthDate.getFullYear();
59 77
       const eventTypes = this.getEventTypes(age);
60
-      if (eventTypes.length === 0) {
61
-        return;
62
-      }
78
+      if (eventTypes.length === 0) continue;
79
+
80
+      // ★ここが修正ポイント(await)
81
+      const danka = await this.dankaService.getDankaById(family.dankaId);
63 82
 
64
-      const danka = this.dankaService.getDankaById(family.dankaId);
65
-      eventTypes.forEach((eventType) => {
66
-        if (this.selectedEventType !== 'all' && this.selectedEventType !== eventType) {
67
-          return;
83
+      for (const eventType of eventTypes) {
84
+        if (
85
+          selectedEventType !== 'all' &&
86
+          selectedEventType !== eventType
87
+        ) {
88
+          continue;
68 89
         }
69 90
 
70 91
         const id = `${family.id}-${eventType}`;
71
-        this.eventTargets.push({
92
+
93
+        const defaultStatus: EventStatus =
94
+          Number(family.id) % 2 === 0 ? '案内済' : '未案内';
95
+
96
+        eventTargets.push({
72 97
           id,
73 98
           dankaId: family.dankaId,
74 99
           name: family.name,
75 100
           furigana: family.furigana,
76 101
           householdName: danka?.householdName ?? '不明',
77 102
           relationship: family.relationship || '未登録',
78
-          birthDate: family.birthDate,
103
+          birthDate: this.formatDateForValue(birthDate),
79 104
           age,
80 105
           eventType,
81 106
           note: family.note,
82
-          status: this.statusByTargetId[id] ?? (Number(family.id) % 2 === 0 ? '案内済' : '未案内'),
107
+          status: this.eventService.getEventStatus(id, defaultStatus),
83 108
         });
84
-      });
85
-    });
109
+      }
110
+    }
86 111
 
87
-    this.eventTargets.sort(
112
+    if (requestId !== this.eventRequestId) {
113
+      return;
114
+    }
115
+
116
+    this.eventTargets = eventTargets.sort(
88 117
       (a, b) =>
89
-        this.getEventSortOrder(a.eventType) - this.getEventSortOrder(b.eventType) ||
118
+        this.getEventSortOrder(a.eventType) -
119
+        this.getEventSortOrder(b.eventType) ||
90 120
         a.age - b.age ||
91 121
         a.name.localeCompare(b.name, 'ja'),
92 122
     );
123
+    this.isLoading = false;
124
+    this.cdr.detectChanges();
93 125
   }
94 126
 
95 127
   changeEventType(eventType: EventType | 'all'): void {
@@ -113,7 +145,7 @@ export class EventPage {
113 145
           target.eventType,
114 146
           target.note,
115 147
           target.status,
116
-        ].some((value) => value.includes(keyword));
148
+        ].some((value) => this.includesKeyword(value, keyword));
117 149
 
118 150
       return matchesStatus && matchesKeyword;
119 151
     });
@@ -121,7 +153,7 @@ export class EventPage {
121 153
 
122 154
   changeStatus(target: EventTarget, status: EventStatus): void {
123 155
     target.status = status;
124
-    this.statusByTargetId[target.id] = status;
156
+    this.eventService.saveEventStatus(target.id, status);
125 157
   }
126 158
 
127 159
   private getEventTypes(age: number): EventType[] {
@@ -147,11 +179,38 @@ export class EventPage {
147 179
     return ['稚児行列', '七五三', '成人式', '米寿'].indexOf(eventType);
148 180
   }
149 181
 
150
-  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
+
151 199
     const [year, month, day] = value.split('-').map(Number);
152 200
     if (!year || !month || !day) {
153 201
       return null;
154 202
     }
155 203
     return new Date(year, month - 1, day);
156 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
+  }
157 216
 }

+ 16
- 18
src/app/pages/family-edit/family-edit.html Datei anzeigen

@@ -86,16 +86,26 @@
86 86
                 </div>
87 87
               </div>
88 88
 
89
-              <div class="form-row form-row-heading">
90
-                <label></label>
89
+              <div class="form-row householder-setting-row">
90
+                <label>施主設定</label>
91 91
                 <div class="form-field">
92
-                  <h3 class="sub-section-title">家系図情報</h3>
93
-                  <p class="sub-section-description">
94
-                    家系図に反映する親子・配偶者の関係を設定します。
95
-                  </p>
92
+                  <button type="button"
93
+                          class="set-householder-button"
94
+                          (click)="setAsHouseholder()">
95
+                    この方を施主にする
96
+                  </button>
96 97
                 </div>
97 98
               </div>
99
+            </div>
100
+          </section>
98 101
 
102
+          <section class="family-tree-edit-section">
103
+            <h2>家系図情報</h2>
104
+            <p class="section-description">
105
+              家系図に反映する親子・配偶者の関係を設定します。
106
+            </p>
107
+
108
+            <div class="form-list">
99 109
               <div class="form-row">
100 110
                 <label for="fatherId">父</label>
101 111
                 <div class="form-field">
@@ -167,18 +177,6 @@
167 177
 
168 178
             </div>
169 179
           </section>
170
-
171
-          <section class="phone-edit-section">
172
-            <div class="householder-area">
173
-              <h3>この方を施主にする</h3>
174
-
175
-              <button type="button"
176
-                      class="set-householder-button"
177
-                      (click)="setAsHouseholder()">
178
-                施主に設定
179
-              </button>
180
-            </div>
181
-          </section>
182 180
         </div>
183 181
 
184 182
         <div class="bottom-actions">

+ 85
- 106
src/app/pages/family-edit/family-edit.scss Datei anzeigen

@@ -2,65 +2,85 @@
2 2
   position: relative;
3 3
   display: block;
4 4
   min-height: 100vh;
5
-  background: #f4eee4;
5
+  background: #f6f0e7;
6 6
   color: #2f2720;
7 7
 }
8 8
 
9 9
 .danka-edit-page {
10
-  display: flex;
11
-  align-items: flex-start;
12
-  gap: 8px;
13
-  background: #f4eee4;
10
+  display: grid;
11
+  grid-template-columns: 172px minmax(0, 1fr);
12
+  gap: 20px;
13
+  padding: 0 38px 36px 0;
14
+  background: #f6f0e7;
14 15
 }
15 16
 
16 17
 .danka-edit-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19 19
   box-sizing: border-box;
20 20
 }
21 21
 
22 22
 .edit-panel {
23
-  min-height: 650px;
24
-  padding: 26px 34px 36px;
25
-  background: #ffffff;
23
+  min-height: 760px;
24
+  padding: 34px 42px 40px;
25
+  background: #fffdf9;
26 26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28 28
   box-sizing: border-box;
29 29
 }
30 30
 
31 31
 .page-title-area {
32
-  margin-bottom: 28px;
32
+  margin-bottom: 22px;
33 33
 }
34 34
 
35 35
 .page-title-area h1 {
36 36
   margin: 0;
37 37
   color: #2f2720;
38
-  font-size: 32px;
39
-  line-height: 1.2;
38
+  font-size: 34px;
39
+  line-height: 1.1;
40 40
   font-weight: 800;
41
-  letter-spacing: 0.02em;
42 41
 }
43 42
 
44 43
 .edit-form {
44
+  display: block;
45 45
   width: 100%;
46
+  margin: 0;
47
+  color: #2f2720;
48
+}
49
+
50
+.edit-form input,
51
+.edit-form select,
52
+.edit-form textarea,
53
+.edit-form button {
54
+  font-family: inherit;
46 55
 }
47 56
 
48 57
 .edit-content {
49 58
   display: grid;
50
-  grid-template-columns: minmax(0, 1fr) 500px;
51
-  gap: 48px;
59
+  grid-template-columns: 1fr;
60
+  gap: 16px;
52 61
   align-items: start;
53 62
 }
54 63
 
55
-.basic-edit-section {
56
-  padding-left: 8px;
64
+.basic-edit-section,
65
+.family-tree-edit-section {
66
+  overflow: hidden;
67
+  padding: 0 22px 24px;
68
+  border: 2px solid #d8caba;
69
+  border-radius: 12px;
70
+  background: #fffdf9;
71
+  box-sizing: border-box;
57 72
 }
58 73
 
59 74
 .basic-edit-section h2,
75
+.family-tree-edit-section h2,
60 76
 .section-heading h2 {
61
-  margin: 0 0 18px;
77
+  margin: 0 -22px 18px;
78
+  padding: 12px 22px;
79
+  background: #eadfce;
80
+  border-bottom: 2px solid #d8caba;
62 81
   color: #2f2720;
63 82
   font-size: 22px;
83
+  line-height: 1.3;
64 84
   font-weight: 800;
65 85
 }
66 86
 
@@ -74,22 +94,29 @@
74 94
   font-size: 14px;
75 95
 }
76 96
 
97
+.section-description {
98
+  margin: 6px 0 0;
99
+  color: #7b6b5c;
100
+  font-size: 14px;
101
+  line-height: 1.6;
102
+}
103
+
77 104
 .form-list {
78
-  width: 650px;
105
+  width: min(100%, 620px);
106
+  margin-top: 14px;
79 107
 }
80 108
 
81 109
 .form-row {
82 110
   display: grid;
83
-  grid-template-columns: 150px 1fr;
84
-  align-items: start;
85
-  column-gap: 18px;
86
-  margin-top: 14px;
111
+  grid-template-columns: 150px minmax(0, 1fr);
112
+  align-items: center;
113
+  gap: 14px;
114
+  margin-bottom: 12px;
87 115
 }
88 116
 
89 117
 .form-row label {
90
-  padding-top: 10px;
91 118
   color: #4b3c31;
92
-  font-size: 17px;
119
+  font-size: 15px;
93 120
   font-weight: 800;
94 121
 }
95 122
 
@@ -101,20 +128,26 @@
101 128
 .form-row select,
102 129
 .form-row textarea {
103 130
   width: 100%;
104
-  min-height: 46px;
105
-  padding: 8px 12px;
106 131
   border: 2px solid #d8caba;
107
-  border-radius: 6px;
132
+  border-radius: 8px;
108 133
   background: #fffdf9;
109 134
   color: #2f2720;
110
-  font-size: 17px;
135
+  font-size: 16px;
136
+  font-weight: 600;
111 137
   font-family: inherit;
112 138
   box-sizing: border-box;
113 139
   outline: none;
114 140
 }
115 141
 
142
+.form-row input,
143
+.form-row select {
144
+  height: 46px;
145
+  padding: 0 14px;
146
+}
147
+
116 148
 .form-row textarea {
117
-  min-height: 104px;
149
+  min-height: 112px;
150
+  padding: 12px 14px;
118 151
   resize: vertical;
119 152
   line-height: 1.6;
120 153
 }
@@ -124,6 +157,7 @@
124 157
 .form-row textarea:focus {
125 158
   border-color: #8a6543;
126 159
   background: #ffffff;
160
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.15);
127 161
 }
128 162
 
129 163
 .form-row input[readonly] {
@@ -139,7 +173,8 @@
139 173
 }
140 174
 
141 175
 .form-row-heading {
142
-  margin-top: 30px;
176
+  margin: 24px 0 14px;
177
+  align-items: start;
143 178
 }
144 179
 
145 180
 .form-row-heading label {
@@ -149,7 +184,7 @@
149 184
 .sub-section-title {
150 185
   margin: 0;
151 186
   color: #2f2720;
152
-  font-size: 18px;
187
+  font-size: 17px;
153 188
   font-weight: 800;
154 189
 }
155 190
 
@@ -160,50 +195,10 @@
160 195
   line-height: 1.6;
161 196
 }
162 197
 
163
-.phone-edit-section {
164
-  min-height: 382px;
165
-  padding: 30px 24px 22px;
166
-  border: 2px solid #d8caba;
167
-  border-radius: 62px;
168
-  background: #fffdf9;
169
-  box-sizing: border-box;
170
-}
171
-
172
-.family-edit-guide {
173
-  padding: 18px 22px;
174
-  border: 2px solid #d8caba;
175
-  border-radius: 14px;
176
-  background: #ffffff;
177
-  box-sizing: border-box;
178
-}
179
-
180
-.family-edit-guide ul {
181
-  margin: 0;
182
-  padding-left: 20px;
183
-  color: #7b6b5c;
184
-  font-size: 15px;
185
-  line-height: 2;
186
-}
187
-
188
-.householder-area {
189
-  margin-top: 28px;
190
-  padding: 22px 26px;
191
-  border: 2px solid #d8caba;
192
-  border-radius: 14px;
193
-  background: #eadfce;
194
-  box-sizing: border-box;
195
-}
196
-
197
-.householder-area h3 {
198
-  margin: 0 0 14px;
199
-  color: #2f2720;
200
-  font-size: 18px;
201
-  font-weight: 800;
202
-}
203
-
204 198
 .set-householder-button {
205
-  width: 170px;
206
-  height: 42px;
199
+  width: auto;
200
+  min-width: 170px;
201
+  height: 46px;
207 202
   border: 2px solid #d8caba;
208 203
   border-radius: 6px;
209 204
   background: #ffffff;
@@ -222,17 +217,17 @@
222 217
   display: flex;
223 218
   justify-content: flex-end;
224 219
   align-items: center;
225
-  gap: 14px;
226
-  margin-top: 72px;
220
+  gap: 12px;
221
+  margin-top: 28px;
227 222
 }
228 223
 
229 224
 .delete-button,
230 225
 .cancel-button,
231 226
 .save-button {
232
-  width: 120px;
227
+  min-width: 116px;
233 228
   height: 46px;
234
-  border-radius: 6px;
235
-  font-size: 17px;
229
+  border-radius: 8px;
230
+  font-size: 16px;
236 231
   font-weight: 800;
237 232
   cursor: pointer;
238 233
   box-sizing: border-box;
@@ -271,35 +266,23 @@
271 266
 
272 267
 /* 画面幅が狭い場合 */
273 268
 @media (max-width: 1100px) {
274
-  .edit-content {
269
+  .danka-edit-page {
275 270
     grid-template-columns: 1fr;
276
-    gap: 28px;
271
+    padding: 0 24px 32px;
277 272
   }
278 273
 
279
-  .form-list {
280
-    width: 100%;
274
+  .edit-panel {
275
+    min-height: auto;
276
+    border-radius: 28px;
277
+    padding: 28px 24px 32px;
281 278
   }
282 279
 
283
-  .bottom-actions {
284
-    margin-top: 40px;
280
+  .edit-content {
281
+    grid-template-columns: 1fr;
285 282
   }
286 283
 }
287 284
 
288 285
 @media (max-width: 800px) {
289
-  .danka-edit-page {
290
-    flex-direction: column;
291
-  }
292
-
293
-  .danka-edit-main {
294
-    width: 100%;
295
-    padding: 16px 20px 32px;
296
-  }
297
-
298
-  .edit-panel {
299
-    padding: 24px 20px 30px;
300
-    border-radius: 32px;
301
-  }
302
-
303 286
   .page-title-area h1 {
304 287
     font-size: 26px;
305 288
   }
@@ -313,10 +296,6 @@
313 296
     padding-top: 0;
314 297
   }
315 298
 
316
-  .phone-edit-section {
317
-    border-radius: 28px;
318
-  }
319
-
320 299
   .bottom-actions {
321 300
     flex-direction: column;
322 301
     align-items: stretch;

+ 72
- 24
src/app/pages/family-edit/family-edit.ts Datei anzeigen

@@ -6,9 +6,11 @@ import {
6 6
   ReactiveFormsModule,
7 7
   Validators,
8 8
 } from '@angular/forms';
9
+import { OnInit } from '@angular/core';
9 10
 import { ActivatedRoute, Router, RouterLink } from '@angular/router';
10 11
 import { AppHeader } from '../../share/header/app-header';
11 12
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
13
+import { DankaService } from '../../services/dankaService';
12 14
 import { FamilyService } from '../../services/family-service';
13 15
 import { MarriageRelationService } from '../../services/marriage-relation-service';
14 16
 import { Danka } from '../../models/danka';
@@ -21,7 +23,7 @@ import { MarriageRelation } from '../../models/marriage-relation';
21 23
   templateUrl: './family-edit.html',
22 24
   styleUrl: './family-edit.scss',
23 25
 })
24
-export class FamilyEdit {
26
+export class FamilyEdit implements OnInit {
25 27
   danka: Danka | undefined;
26 28
   family: Family | undefined;
27 29
   families: Family[] = [];
@@ -30,20 +32,35 @@ export class FamilyEdit {
30 32
   relationMode: string = '';
31 33
   baseFamilyId: string = '';
32 34
   marriageErrorMessages: string[] = [];
35
+  private setHouseholderOnSave = false;
33 36
 
34 37
   constructor(
38
+    private dankaService: DankaService,
35 39
     private familyService: FamilyService,
36 40
     private marriageRelationService: MarriageRelationService,
37 41
     private route: ActivatedRoute,
38 42
     private router: Router,
39 43
   ) {
44
+  }
45
+
46
+  ngOnInit(): void {
47
+    this.init();
48
+  }
49
+
50
+  async init(): Promise<void> {
40 51
     this.dankaId = this.route.snapshot.params['dankaId'];
41 52
     this.familyId = this.route.snapshot.params['familyId'];
42
-    this.families = this.familyService.getFamiliesByDankaId(this.dankaId);
53
+
43 54
     this.relationMode = this.route.snapshot.queryParamMap.get('relationMode') ?? '';
44 55
     this.baseFamilyId = this.route.snapshot.queryParamMap.get('baseFamilyId') ?? '';
56
+
57
+    // ★ここが重要
58
+    this.danka = await this.dankaService.getDankaById(this.dankaId);
59
+    this.families = await this.familyService.getFamiliesByDankaId(this.dankaId);
60
+
45 61
     if (this.familyId) {
46
-      this.family = this.familyService.getFamilyById(this.familyId);
62
+      this.family = await this.familyService.getFamilyById(this.familyId);
63
+
47 64
       if (this.family) {
48 65
         this.familyForm.patchValue({
49 66
           furigana: this.family.furigana,
@@ -56,7 +73,8 @@ export class FamilyEdit {
56 73
           spouseId: this.family.spouseId,
57 74
           gender: this.family.gender,
58 75
         });
59
-        this.patchMarriageRelationFields(this.family.id);
76
+
77
+        await this.patchMarriageRelationFields(this.family.id);
60 78
       }
61 79
     }
62 80
 
@@ -68,7 +86,7 @@ export class FamilyEdit {
68 86
     }
69 87
 
70 88
     if (!this.familyId && this.relationMode === 'child') {
71
-      const baseFamily = this.familyService.getFamilyById(this.baseFamilyId);
89
+      const baseFamily = await this.familyService.getFamilyById(this.baseFamilyId);
72 90
 
73 91
       if (baseFamily?.gender === 'male') {
74 92
         this.familyForm.patchValue({
@@ -117,14 +135,13 @@ export class FamilyEdit {
117 135
     return this.families.filter((family) => family.id !== this.familyId);
118 136
   }
119 137
 
120
-  patchMarriageRelationFields(familyId: string): void {
121
-    const relations = this.marriageRelationService.getMarriageRelationsByFamilyId(familyId);
138
+  async patchMarriageRelationFields(familyId: string): Promise<void> {
139
+    const relations = await this.marriageRelationService.getMarriageRelationsByFamilyId(familyId);
140
+
122 141
     const relation =
123 142
       relations.find((marriageRelation) => marriageRelation.status === 'current') ?? relations[0];
124 143
 
125
-    if (!relation) {
126
-      return;
127
-    }
144
+    if (!relation) return;
128 145
 
129 146
     this.familyForm.patchValue({
130 147
       spouseId: relation.person1Id === familyId ? relation.person2Id : relation.person1Id,
@@ -132,17 +149,21 @@ export class FamilyEdit {
132 149
     });
133 150
   }
134 151
 
135
-  findMarriageRelation(person1Id: string, person2Id: string): MarriageRelation | undefined {
136
-    return this.marriageRelationService
137
-      .getMarriageRelationsByFamilyId(person1Id)
138
-      .find(
139
-        (relation) =>
140
-          (relation.person1Id === person1Id && relation.person2Id === person2Id) ||
141
-          (relation.person1Id === person2Id && relation.person2Id === person1Id),
142
-      );
152
+  async findMarriageRelation(
153
+    person1Id: string,
154
+    person2Id: string
155
+  ): Promise<MarriageRelation | undefined> {
156
+
157
+    const relations = await this.marriageRelationService.getMarriageRelationsByFamilyId(person1Id);
158
+
159
+    return relations.find(
160
+      (relation) =>
161
+        (relation.person1Id === person1Id && relation.person2Id === person2Id) ||
162
+        (relation.person1Id === person2Id && relation.person2Id === person1Id),
163
+    );
143 164
   }
144 165
 
145
-  saveFamily() {
166
+  async saveFamily() {
146 167
     if (this.familyForm.invalid) {
147 168
       return;
148 169
     }
@@ -169,9 +190,10 @@ export class FamilyEdit {
169 190
     };
170 191
 
171 192
     if (spouseId) {
172
-      const existingRelation = this.findMarriageRelation(familyId, spouseId);
173
-      const errors = this.marriageRelationService.saveMarriageRelation({
174
-        id: existingRelation?.id ?? Date.now().toString(),
193
+      const existingRelation = await this.findMarriageRelation(familyId, spouseId);
194
+
195
+      const errors = await this.marriageRelationService.saveMarriageRelation({
196
+        id: existingRelation?.id ?? crypto.randomUUID(),
175 197
         dankaId,
176 198
         person1Id: familyId,
177 199
         person2Id: spouseId,
@@ -186,13 +208,14 @@ export class FamilyEdit {
186 208
         return;
187 209
       }
188 210
     } else {
189
-      const currentMarriage = this.marriageRelationService.getCurrentMarriageByFamilyId(familyId);
211
+      const currentMarriage = await this.marriageRelationService.getCurrentMarriageByFamilyId(familyId);
190 212
       if (currentMarriage) {
191 213
         this.marriageRelationService.deleteMarriageRelation(currentMarriage.id);
192 214
       }
193 215
     }
194 216
 
195 217
     this.familyService.saveFamily(updatedFamily);
218
+    this.saveHouseholderIfSelected(updatedFamily);
196 219
     this.router.navigate(['/danka-detail', dankaId], { queryParams: { tab: 'family' } });
197 220
   }
198 221
 
@@ -210,5 +233,30 @@ export class FamilyEdit {
210 233
     this.router.navigate(['/danka-detail', dankaId], { queryParams: { tab: 'family' } });
211 234
   }
212 235
 
213
-  setAsHouseholder() {}
236
+  setAsHouseholder(): void {
237
+    this.setHouseholderOnSave = true;
238
+    this.familyForm.patchValue({
239
+      relationship: '施主',
240
+    });
241
+  }
242
+
243
+  private saveHouseholderIfSelected(family: Family): void {
244
+    if (!this.setHouseholderOnSave || !this.danka) {
245
+      return;
246
+    }
247
+
248
+    this.dankaService.saveDanka({
249
+      ...this.danka,
250
+      householder: family.name,
251
+      householderFurigana: family.furigana,
252
+      updatedAt: this.formatDateForSave(new Date()),
253
+    });
254
+  }
255
+
256
+  private formatDateForSave(date: Date): string {
257
+    const year = date.getFullYear();
258
+    const month = String(date.getMonth() + 1).padStart(2, '0');
259
+    const day = String(date.getDate()).padStart(2, '0');
260
+    return `${year}-${month}-${day}`;
261
+  }
214 262
 }

+ 3
- 3
src/app/pages/kakocho-edit/kakocho-edit.html Datei anzeigen

@@ -89,9 +89,9 @@
89 89
                 />
90 90
               </div>
91 91
 
92
-              <!--年 -->
92
+              <!--年 -->
93 93
               <div class="form-row">
94
-                <label for="ageAtDeath">年</label>
94
+                <label for="ageAtDeath">年</label>
95 95
 
96 96
                 <input
97 97
                   id="ageAtDeath"
@@ -101,7 +101,7 @@
101 101
               </div>
102 102
 
103 103
               <!-- 備考 -->
104
-              <div class="form-row">
104
+              <div class="form-row note-row">
105 105
                 <label for="note">備考</label>
106 106
 
107 107
                 <textarea

+ 68
- 192
src/app/pages/kakocho-edit/kakocho-edit.scss Datei anzeigen

@@ -2,43 +2,42 @@
2 2
   position: relative;
3 3
   display: block;
4 4
   min-height: 100vh;
5
-  background: #f4eee4;
5
+  background: #f6f0e7;
6 6
   color: #2f2720;
7 7
 }
8 8
 
9 9
 .danka-edit-page {
10
-  display: flex;
11
-  align-items: flex-start;
12
-  gap: 8px;
13
-  background: #f4eee4;
10
+  display: grid;
11
+  grid-template-columns: 172px minmax(0, 1fr);
12
+  gap: 20px;
13
+  padding: 0 38px 36px 0;
14
+  background: #f6f0e7;
14 15
 }
15 16
 
16 17
 .danka-edit-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19 19
   box-sizing: border-box;
20 20
 }
21 21
 
22 22
 .edit-panel {
23
-  min-height: 650px;
24
-  padding: 26px 34px 36px;
25
-  background: #ffffff;
23
+  min-height: 760px;
24
+  padding: 34px 42px 40px;
25
+  background: #fffdf9;
26 26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28 28
   box-sizing: border-box;
29 29
 }
30 30
 
31 31
 .page-title-area {
32
-  margin-bottom: 8px;
32
+  margin-bottom: 22px;
33 33
 }
34 34
 
35 35
 .page-title-area h1 {
36 36
   margin: 0;
37 37
   color: #2f2720;
38
-  font-size: 32px;
39
-  line-height: 1.2;
38
+  font-size: 34px;
39
+  line-height: 1.1;
40 40
   font-weight: 800;
41
-  letter-spacing: 0.02em;
42 41
 }
43 42
 
44 43
 .edit-form {
@@ -49,40 +48,51 @@
49 48
 }
50 49
 
51 50
 .edit-form input,
51
+.edit-form textarea,
52 52
 .edit-form button {
53 53
   font-family: inherit;
54 54
 }
55 55
 
56 56
 .edit-content {
57 57
   display: grid;
58
-  grid-template-columns: minmax(0, 1fr) 520px;
59
-  gap: 48px;
58
+  grid-template-columns: minmax(0, 1fr);
59
+  gap: 22px;
60 60
   align-items: start;
61 61
 }
62 62
 
63 63
 .basic-edit-section,
64 64
 .phone-edit-section {
65
-  padding-top: 4px;
65
+  overflow: hidden;
66
+  padding: 0 28px 28px;
67
+  border: 2px solid #d8caba;
68
+  border-radius: 12px;
69
+  background: #fffdf9;
70
+  box-sizing: border-box;
66 71
 }
67 72
 
68 73
 .basic-edit-section h2,
69 74
 .phone-edit-section h2,
70 75
 .support-box h2 {
71
-  margin: 0;
76
+  margin: 0 -28px 20px;
77
+  padding: 14px 28px;
78
+  background: #eadfce;
79
+  border-bottom: 2px solid #d8caba;
72 80
   color: #2f2720;
73 81
   font-size: 22px;
82
+  line-height: 1.3;
74 83
   font-weight: 800;
75 84
 }
76 85
 
77 86
 .section-heading p {
78
-  margin: 6px 0 14px;
87
+  margin: 4px 0 12px;
79 88
   color: #7b6b5c;
80
-  font-size: 15px;
89
+  font-size: 14px;
81 90
 }
82 91
 
83
-/* 基本情報 */
92
+/* 蝓コ譛ャ諠・ア */
84 93
 .form-list {
85
-  margin-top: 14px;
94
+  width: min(100%, 980px);
95
+  margin-top: 0;
86 96
 }
87 97
 
88 98
 .form-field {
@@ -93,194 +103,74 @@
93 103
 
94 104
 .form-row {
95 105
   display: grid;
96
-  grid-template-columns: 120px 1fr;
106
+  grid-template-columns: 140px 1fr;
97 107
   align-items: center;
98
-  gap: 16px;
99
-  margin-bottom: 14px;
108
+  gap: 14px;
109
+  margin-bottom: 12px;
100 110
 }
101 111
 
102 112
 .form-row label {
103 113
   color: #4b3c31;
104
-  font-size: 17px;
114
+  font-size: 15px;
105 115
   font-weight: 800;
106 116
 }
107 117
 
108 118
 .form-row input,
109 119
 .form-row textarea {
110 120
   width: 100%;
111
-  padding: 0 14px;
112 121
   border: 2px solid #d8caba;
113 122
   border-radius: 8px;
114 123
   background: #fffdf9;
115 124
   color: #2f2720;
116
-  font-size: 18px;
125
+  font-size: 16px;
117 126
   font-weight: 600;
118 127
   box-sizing: border-box;
119 128
   outline: none;
120 129
 }
121 130
 
122 131
 .form-row input {
123
-  height: 54px;
132
+  height: 46px;
133
+  padding: 0 14px;
124 134
 }
125 135
 
126 136
 .form-row textarea {
127
-  min-height: 108px;
128
-  padding-top: 14px;
137
+  min-height: 104px;
138
+  padding: 12px 14px;
139
+  line-height: 1.6;
129 140
   resize: vertical;
130 141
 }
131 142
 
132
-.form-row input:focus,
133
-.form-row textarea:focus {
134
-  border-color: #8a6543;
135
-  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.15);
136
-}
137
-
138
-/* 電話番号 */
139
-.phone-table {
140
-  width: 100%;
141
-}
142
-
143
-.phone-table-header,
144
-.phone-table-row {
145
-  display: grid;
146
-  grid-template-columns: 1.35fr 1.45fr 84px;
147
-  align-items: center;
148
-  column-gap: 10px;
149
-}
150
-
151
-.phone-table-header {
152
-  min-height: 38px;
153
-  padding: 0 10px;
154
-  border: 2px solid #d8caba;
155
-  border-radius: 6px;
156
-  background: #eadfce;
157
-  color: #5a4a3c;
158
-  font-size: 15px;
159
-  font-weight: 800;
160
-  box-sizing: border-box;
161
-}
162
-
163
-.phone-table-row {
164
-  min-height: 56px;
165
-  margin-top: 4px;
166
-  padding: 6px 10px;
167
-  border: 2px solid #d8caba;
168
-  border-radius: 8px;
169
-  background: #fffdf9;
170
-  color: #2f2720;
171
-  font-size: 16px;
172
-  box-sizing: border-box;
173
-}
174
-
175
-.phone-table-row > div {
176
-  display: flex;
177
-  align-items: center;
178
-}
179
-
180
-.phone-table-row input {
181
-  width: 100%;
182
-  height: 38px;
183
-  padding: 0 10px;
184
-  border: none;
185
-  border-radius: 6px;
186
-  background: transparent;
187
-  color: #2f2720;
188
-  font-size: 16px;
189
-  font-weight: 600;
190
-  box-sizing: border-box;
191
-  outline: none;
192
-}
193
-
194
-.phone-table-row input:focus {
195
-  background: #ffffff;
196
-  box-shadow: inset 0 0 0 2px #8a6543;
197
-}
198
-
199
-.phone-table-row input::placeholder {
200
-  color: #9b8b7a;
201
-  font-weight: 500;
202
-}
203
-
204
-.phone-row-action {
205
-  justify-content: flex-end;
206
-}
207
-
208
-.remove-phone-button {
209
-  width: 68px;
210
-  height: 34px;
211
-  border: 2px solid #d8caba;
212
-  border-radius: 6px;
213
-  background: #ffffff;
214
-  color: #6a4a35;
215
-  font-size: 14px;
216
-  font-weight: 800;
217
-  cursor: pointer;
218
-  box-sizing: border-box;
219
-}
220
-
221
-.remove-phone-button:hover {
222
-  background: #f6efe6;
223
-}
224
-
225
-.phone-action {
226
-  display: flex;
227
-  justify-content: flex-end;
228
-  margin-top: 8px;
229
-}
230
-
231
-.add-phone-button {
232
-  width: 170px;
233
-  height: 46px;
234
-  border: 2px solid #d8caba;
235
-  border-radius: 8px;
236
-  background: #ffffff;
237
-  color: #2f2720;
238
-  font-size: 16px;
239
-  font-weight: 800;
240
-  cursor: pointer;
241
-  box-sizing: border-box;
242
-}
243
-
244
-.add-phone-button:hover {
245
-  background: #f6efe6;
143
+.note-row {
144
+  align-items: start;
246 145
 }
247 146
 
248
-/* 入力補助 */
249
-.support-box {
250
-  min-height: 142px;
251
-  margin-top: 56px;
252
-  padding: 28px 30px;
253
-  border: 2px solid #d8caba;
254
-  border-radius: 24px;
255
-  background: #fffdf9;
256
-  box-sizing: border-box;
147
+.note-row label {
148
+  padding-top: 12px;
257 149
 }
258 150
 
259
-.support-box ul {
260
-  margin: 12px 0 0;
261
-  padding-left: 22px;
262
-  color: #6f6257;
263
-  font-size: 15px;
264
-  line-height: 1.65;
151
+.form-row input:focus,
152
+.form-row textarea:focus {
153
+  border-color: #8a6543;
154
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.15);
265 155
 }
266 156
 
267
-/* 下部ボタン */
157
+/* 荳矩Κ繝懊ち繝ウ */
268 158
 .bottom-actions {
269 159
   display: flex;
270 160
   justify-content: flex-end;
271 161
   align-items: center;
272 162
   gap: 12px;
273
-  margin-top: 22px;
163
+  margin-top: 26px;
274 164
 }
275 165
 
276 166
 .delete-button,
277 167
 .cancel-button,
278 168
 .save-button {
279 169
   min-width: 116px;
280
-  height: 52px;
170
+  height: 46px;
281 171
   border: 2px solid #d8caba;
282 172
   border-radius: 8px;
283
-  font-size: 17px;
173
+  font-size: 16px;
284 174
   font-weight: 800;
285 175
   cursor: pointer;
286 176
   box-sizing: border-box;
@@ -307,18 +197,6 @@
307 197
   background: #765639;
308 198
 }
309 199
 
310
-.bottom-note {
311
-  width: 700px;
312
-  margin: 18px 0 22px 36px;
313
-  padding: 4px 12px;
314
-  border: 2px solid #d8caba;
315
-  border-radius: 4px;
316
-  background: #eadfce;
317
-  color: #7b6b5c;
318
-  font-size: 14px;
319
-  box-sizing: border-box;
320
-}
321
-
322 200
 .error-message {
323 201
   margin: 6px 0 0;
324 202
   color: #b33a2f;
@@ -336,21 +214,19 @@
336 214
   opacity: 0.7;
337 215
 }
338 216
 
339
-.phone-field {
340
-  width: 100%;
341
-  display: flex;
342
-  flex-direction: column;
343
-}
217
+@media (max-width: 1100px) {
218
+  .danka-edit-page {
219
+    grid-template-columns: 1fr;
220
+    padding: 0 24px 32px;
221
+  }
344 222
 
345
-.phone-error-message {
346
-  margin: 4px 0 0;
347
-  color: #b33a2f;
348
-  font-size: 12px;
349
-  font-weight: 700;
350
-  line-height: 1.4;
351
-}
223
+  .edit-panel {
224
+    min-height: auto;
225
+    border-radius: 28px;
226
+    padding: 28px 24px 32px;
227
+  }
352 228
 
353
-.remove-phone-button:disabled {
354
-  opacity: 0.5;
355
-  cursor: not-allowed;
229
+  .edit-content {
230
+    grid-template-columns: 1fr;
231
+  }
356 232
 }

+ 42
- 21
src/app/pages/kakocho-edit/kakocho-edit.ts Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { Component } from '@angular/core';
1
+import { Component, OnInit } from '@angular/core';
2 2
 import {
3 3
   FormBuilder,
4 4
   FormGroup,
@@ -16,9 +16,11 @@ import { AppSideMenu } from '../../share/side-menu/app-side-menu';
16 16
 
17 17
 import { DankaService } from '../../services/dankaService';
18 18
 import { KakochoService } from '../../services/kakocho-service';
19
+import { FamilyService } from '../../services/family-service';
19 20
 
20 21
 import { Danka } from '../../models/danka';
21 22
 import { Kakocho } from '../../models/kakocho';
23
+import { Family } from '../../models/family';
22 24
 
23 25
 @Component({
24 26
   selector: 'app-kakocho-edit',
@@ -30,21 +32,22 @@ import { Kakocho } from '../../models/kakocho';
30 32
   templateUrl: './kakocho-edit.html',
31 33
   styleUrl: './kakocho-edit.scss',
32 34
 })
33
-export class KakochoEdit {
35
+export class KakochoEdit implements OnInit {
34 36
   danka?: Danka;
35 37
   kakocho?: Kakocho;
38
+  sourceFamily?: Family;
36 39
   kakochoForm: FormGroup;
37
-  dankaId: string;
40
+  dankaId: string | undefined;
41
+  returnTab: 'family' | 'kakocho' = 'kakocho';
38 42
 
39 43
   constructor(
40 44
     private fb: FormBuilder,
41 45
     private dankaService: DankaService,
42 46
     private kakochoService: KakochoService,
47
+    private familyService: FamilyService,
43 48
     private route: ActivatedRoute,
44 49
     private router: Router,
45 50
   ) {
46
-
47
-    // フォーム初期化
48 51
     this.kakochoForm = this.fb.group({
49 52
       name: ['', Validators.required],
50 53
       furigana: [''],
@@ -54,28 +57,33 @@ export class KakochoEdit {
54 57
       ageAtDeath: [''],
55 58
       note: [''],
56 59
     });
60
+  }
61
+
62
+  ngOnInit(): void {
63
+    this.init();
64
+  }
57 65
 
58
-    // 檀家ID
66
+  async init(): Promise<void> {
59 67
     const dankaId = this.route.snapshot.params['dankaId'];
60
-    this.dankaId = this.route.snapshot.params['dankaId'];
68
+    const familyId = this.route.snapshot.queryParams['familyId'];
69
+    const kakochoId = this.route.snapshot.params['kakochoId'];
70
+
71
+    this.dankaId = dankaId;
61 72
 
62 73
     if (dankaId) {
63
-      this.danka =
64
-        this.dankaService.getDankaById(dankaId);
74
+      this.danka = await this.dankaService.getDankaById(dankaId);
65 75
     }
66 76
 
67
-    // 編集対象ID
68
-    const kakochoId =
69
-      this.route.snapshot.params['kakochoId'];
77
+    if (familyId) {
78
+      this.sourceFamily = await this.familyService.getFamilyById(familyId);
79
+      this.returnTab = 'family';
80
+    }
70 81
 
71 82
     // 編集モード
72 83
     if (kakochoId) {
73
-
74
-      this.kakocho =
75
-        this.kakochoService.getKakochoById(kakochoId);
84
+      this.kakocho = await this.kakochoService.getKakochoById(kakochoId);
76 85
 
77 86
       if (this.kakocho) {
78
-
79 87
         this.kakochoForm.patchValue({
80 88
           name: this.kakocho.name,
81 89
           furigana: this.kakocho.furigana,
@@ -85,12 +93,20 @@ export class KakochoEdit {
85 93
           ageAtDeath: this.kakocho.ageAtDeath,
86 94
           note: this.kakocho.note,
87 95
         });
88
-
89 96
       }
90 97
     }
98
+
99
+    // 新規(家族からの引き継ぎ)
100
+    else if (this.sourceFamily) {
101
+      this.kakochoForm.patchValue({
102
+        name: this.sourceFamily.name,
103
+        furigana: this.sourceFamily.furigana,
104
+        relationship: this.sourceFamily.relationship,
105
+      });
106
+    }
91 107
   }
92 108
 
93
-  saveKakocho(): void {
109
+  async saveKakocho(): Promise<void> {
94 110
 
95 111
     // form値取得
96 112
     const formValue = this.kakochoForm.value;
@@ -103,7 +119,7 @@ export class KakochoEdit {
103 119
         ...formValue,
104 120
       };
105 121
 
106
-      this.kakochoService.updateKakocho(
122
+      await this.kakochoService.updateKakocho(
107 123
         updatedKakocho
108 124
       );
109 125
 
@@ -115,7 +131,7 @@ export class KakochoEdit {
115 131
 
116 132
         dankaId: this.danka?.id ?? '',
117 133
 
118
-        familyId: '',
134
+        familyId: this.sourceFamily?.id ?? '',
119 135
 
120 136
         name: formValue.name,
121 137
         furigana: formValue.furigana,
@@ -129,6 +145,10 @@ export class KakochoEdit {
129 145
       this.kakochoService.addKakocho(
130 146
         newKakocho
131 147
       );
148
+
149
+      if (this.sourceFamily) {
150
+        await this.familyService.deleteFamily(this.sourceFamily.id);
151
+      }
132 152
     }
133 153
 
134 154
     // 一覧へ戻る
@@ -149,5 +169,6 @@ export class KakochoEdit {
149 169
   }
150 170
 
151 171
   cancelKakochoEdit() {
152
-    this.router.navigate(['/danka-detail', this.danka?.id], { queryParams: { tab: 'kakocho' } });  }
172
+    this.router.navigate(['/danka-detail', this.danka?.id], { queryParams: { tab: this.returnTab } });
173
+  }
153 174
 }

+ 8
- 4
src/app/pages/memorial-list/memorial-list.html Datei anzeigen

@@ -12,7 +12,7 @@
12 12
 
13 13
           <div class="filter-row">
14 14
             <div class="filter-field">
15
-              <label for="targetYear">対象年</label>
15
+              <label for="targetYear">年</label>
16 16
               <select
17 17
                 id="targetYear"
18 18
                 [(ngModel)]="targetYear"
@@ -60,7 +60,7 @@
60 60
         <div class="memorial-table">
61 61
           <div class="memorial-table-header">
62 62
             <div>戒名</div>
63
-            <div>俗名</div>
63
+            <div>俗名・ふりがな</div>
64 64
             <div>没年月日</div>
65 65
             <div>関係</div>
66 66
             <div>檀家(世帯)</div>
@@ -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">
@@ -77,7 +81,7 @@
77 81
                 </div>
78 82
                 <div>
79 83
                   <p class="person-name">{{ memorial.name }}</p>
80
-                  <p class="person-sub">俗名</p>
84
+                  <p class="person-sub">{{ memorial.furigana || 'ふりがな未登録' }}</p>
81 85
                 </div>
82 86
                 <div>
83 87
                   {{ formatDeathDate(memorial.deathDate) }}

+ 25
- 29
src/app/pages/memorial-list/memorial-list.scss Datei anzeigen

@@ -7,24 +7,24 @@
7 7
 }
8 8
 
9 9
 .memorial-list-page {
10
-  display: flex;
11
-  align-items: flex-start;
12
-  gap: 8px;
10
+  display: grid;
11
+  grid-template-columns: 172px minmax(0, 1fr);
12
+  gap: 20px;
13
+  padding: 0 38px 36px 0;
13 14
   background: #f4eee4;
14 15
 }
15 16
 
16 17
 .memorial-list-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19 19
   box-sizing: border-box;
20 20
 }
21 21
 
22 22
 .memorial-panel {
23
-  min-height: 650px;
24
-  padding: 26px 34px 36px;
23
+  min-height: 760px;
24
+  padding: 34px 42px 40px;
25 25
   background: #ffffff;
26 26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28 28
   box-sizing: border-box;
29 29
 }
30 30
 
@@ -45,10 +45,9 @@
45 45
 .page-title-row h1 {
46 46
   margin: 0;
47 47
   color: #2f2720;
48
-  font-size: 32px;
49
-  line-height: 1.2;
48
+  font-size: 34px;
49
+  line-height: 1.1;
50 50
   font-weight: 800;
51
-  letter-spacing: 0.02em;
52 51
 }
53 52
 
54 53
 .filter-row {
@@ -74,7 +73,7 @@
74 73
 
75 74
 .filter-field select,
76 75
 .search-field input {
77
-  height: 38px;
76
+  height: 46px;
78 77
   padding: 0 14px;
79 78
   border: 2px solid #d8caba;
80 79
   border-radius: 8px;
@@ -120,7 +119,7 @@
120 119
 .list-header-row h2 {
121 120
   margin: 0;
122 121
   color: #2f2720;
123
-  font-size: 26px;
122
+  font-size: 22px;
124 123
   font-weight: 800;
125 124
 }
126 125
 
@@ -151,7 +150,7 @@
151 150
 }
152 151
 
153 152
 .memorial-table-header {
154
-  min-height: 46px;
153
+  min-height: 40px;
155 154
   padding: 0 14px;
156 155
   background: #efe4d6;
157 156
   color: #111111;
@@ -161,7 +160,7 @@
161 160
 }
162 161
 
163 162
 .memorial-table-row {
164
-  min-height: 78px;
163
+  min-height: 62px;
165 164
   padding: 10px 14px;
166 165
   border-top: 1px solid #d8c2aa;
167 166
   background: #fffdf9;
@@ -223,6 +222,17 @@
223 222
 }
224 223
 
225 224
 @media (max-width: 1100px) {
225
+  .memorial-list-page {
226
+    grid-template-columns: 1fr;
227
+    padding: 0 24px 32px;
228
+  }
229
+
230
+  .memorial-panel {
231
+    min-height: auto;
232
+    border-radius: 28px;
233
+    padding: 28px 24px 32px;
234
+  }
235
+
226 236
   .page-title-row {
227 237
     flex-direction: column;
228 238
   }
@@ -238,20 +248,6 @@
238 248
 }
239 249
 
240 250
 @media (max-width: 800px) {
241
-  .memorial-list-page {
242
-    flex-direction: column;
243
-  }
244
-
245
-  .memorial-list-main {
246
-    width: 100%;
247
-    padding: 16px 20px 32px;
248
-  }
249
-
250
-  .memorial-panel {
251
-    padding: 24px 20px 30px;
252
-    border-radius: 32px;
253
-  }
254
-
255 251
   .page-title-row h1 {
256 252
     font-size: 26px;
257 253
   }

+ 100
- 24
src/app/pages/memorial-list/memorial-list.ts Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { Component } 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';
@@ -13,11 +13,13 @@ import { FormsModule } from '@angular/forms';
13 13
   templateUrl: './memorial-list.html',
14 14
   styleUrl: './memorial-list.scss',
15 15
 })
16
-export class MemorialList {
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,45 +47,82 @@ export class MemorialList {
45 47
   constructor(
46 48
     private dankaService: DankaService,
47 49
     private kakochoService: KakochoService,
50
+    private cdr: ChangeDetectorRef,
48 51
   ) {
49
-    this.createMemorialList();
52
+
53
+  }
54
+  ngOnInit(): void {
55
+    this.init();
56
+  }
57
+
58
+  async init(): Promise<void> {
59
+    await this.createMemorialList();
50 60
   }
51 61
 
52
-  createMemorialList(): void {
53
-    this.memorialList = [];
54
-    const kakochoList = this.kakochoService.getKakochoList();
55
-    kakochoList.forEach((kakocho) => {
56
-      const deathYear = Number(kakocho.deathDate.slice(0, 4));
57
-      const yearDiff = this.targetYear - deathYear;
62
+  async createMemorialList(): Promise<void> {
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
+    }
72
+
73
+    const kakochoList = await this.kakochoService.getKakochoList();
74
+
75
+    for (const kakocho of kakochoList) {
76
+      const deathDate = this.parseDate(kakocho.deathDate);
77
+      if (!deathDate) continue;
78
+
79
+      const deathYear = deathDate.getFullYear();
80
+      const yearDiff = targetYear - deathYear;
81
+
58 82
       const memorialType = this.getMemorialType(yearDiff);
59
-      if (memorialType === '') {
60
-        return;
61
-      }
62
-      if (this.selectedMemorialType !== 'all' && this.selectedMemorialType !== memorialType) {
63
-        return;
83
+      if (memorialType === '') continue;
84
+
85
+      if (
86
+        selectedMemorialType !== 'all' &&
87
+        selectedMemorialType !== memorialType
88
+      ) {
89
+        continue;
64 90
       }
65
-      const danka = this.dankaService.getDankaById(kakocho.dankaId);
91
+
92
+      const danka = await this.dankaService.getDankaById(kakocho.dankaId);
93
+
66 94
       const memorialTarget: Memorial = {
67 95
         id: kakocho.id,
68 96
         dankaId: kakocho.dankaId,
69 97
         name: kakocho.name,
98
+        furigana: kakocho.furigana,
70 99
         kaimyo: kakocho.kaimyo,
71 100
         relationship: kakocho.relationship,
72 101
         householdName: danka?.householdName ?? '不明',
73
-        deathDate: kakocho.deathDate,
74
-        memorialType: memorialType,
102
+        deathDate: this.formatDateForValue(deathDate),
103
+        memorialType,
75 104
         note: kakocho.note,
76 105
       };
77
-      this.memorialList.push(memorialTarget);
78
-    });
79
-    this.memorialList.sort((a, b) => {
106
+
107
+      memorialList.push(memorialTarget);
108
+    }
109
+
110
+    if (requestId !== this.memorialRequestId) {
111
+      return;
112
+    }
113
+
114
+    this.memorialList = memorialList.sort((a, b) => {
80 115
       const deathDateA = new Date(a.deathDate).getTime();
81 116
       const deathDateB = new Date(b.deathDate).getTime();
117
+
82 118
       if (deathDateA !== deathDateB) {
83 119
         return deathDateA - deathDateB;
84 120
       }
121
+
85 122
       return a.name.localeCompare(b.name, 'ja');
86 123
     });
124
+    this.isLoading = false;
125
+    this.cdr.detectChanges();
87 126
   }
88 127
 
89 128
   changeMemorialType(memorialType: string): void {
@@ -101,22 +140,27 @@ export class MemorialList {
101 140
       [
102 141
         memorial.kaimyo,
103 142
         memorial.name,
143
+        memorial.furigana,
104 144
         memorial.deathDate,
105 145
         memorial.relationship,
106 146
         memorial.householdName,
107 147
         memorial.memorialType,
108 148
         memorial.note,
109
-      ].some((value) => value.includes(keyword)),
149
+      ].some((value) => this.includesKeyword(value, keyword)),
110 150
     );
111 151
   }
112 152
 
153
+  private includesKeyword(value: unknown, keyword: string): boolean {
154
+    return String(value ?? '').includes(keyword);
155
+  }
156
+
113 157
   formatDeathDate(deathDate: string): string {
114
-    const [, month, day] = deathDate.split('-').map(Number);
115
-    if (!month || !day) {
158
+    const date = this.parseDate(deathDate);
159
+    if (!date) {
116 160
       return '未登録';
117 161
     }
118 162
 
119
-    return `${month}月${day}日`;
163
+    return `${date.getMonth() + 1}月${date.getDate()}日`;
120 164
   }
121 165
 
122 166
   getMemorialType(yearDiff: number) {
@@ -153,4 +197,36 @@ export class MemorialList {
153 197
         return '';
154 198
     }
155 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
+  }
156 232
 }

+ 4
- 6
src/app/pages/search/search.html Datei anzeigen

@@ -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()">
@@ -58,9 +59,6 @@
58 59
                   <div>
59 60
                     住所: {{ danka.address }}
60 61
                   </div>
61
-                  <div class="result-link-text">
62
-                    開く
63
-                  </div>
64 62
                 </a>
65 63
               }
66 64
             </section>

+ 61
- 62
src/app/pages/search/search.scss Datei anzeigen

@@ -7,24 +7,24 @@
7 7
 }
8 8
 
9 9
 .search-page {
10
-  display: flex;
11
-  align-items: flex-start;
12
-  gap: 8px;
10
+  display: grid;
11
+  grid-template-columns: 172px minmax(0, 1fr);
12
+  gap: 20px;
13
+  padding: 0 38px 36px 0;
13 14
   background: #f4eee4;
14 15
 }
15 16
 
16 17
 .search-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19 19
   box-sizing: border-box;
20 20
 }
21 21
 
22 22
 .search-panel {
23
-  min-height: 650px;
24
-  padding: 26px 34px 36px;
23
+  min-height: 760px;
24
+  padding: 34px 42px 40px;
25 25
   background: #ffffff;
26 26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28 28
   box-sizing: border-box;
29 29
 }
30 30
 
@@ -33,15 +33,15 @@
33 33
   justify-content: space-between;
34 34
   align-items: flex-start;
35 35
   gap: 24px;
36
+  margin-bottom: 22px;
36 37
 }
37 38
 
38 39
 .page-title-row h1 {
39 40
   margin: 0;
40 41
   color: #2f2720;
41
-  font-size: 32px;
42
-  line-height: 1.2;
42
+  font-size: 34px;
43
+  line-height: 1.1;
43 44
   font-weight: 800;
44
-  letter-spacing: 0.02em;
45 45
 }
46 46
 
47 47
 .top-action-list {
@@ -71,21 +71,21 @@
71 71
 }
72 72
 
73 73
 .search-condition-area {
74
-  margin-top: 8px;
74
+  margin: 0 0 16px;
75 75
 }
76 76
 
77 77
 .search-input-row {
78 78
   display: grid;
79
-  grid-template-columns: minmax(0, 1fr) 120px 140px 150px;
80
-  gap: 18px;
79
+  grid-template-columns: minmax(0, 1fr) 104px 104px;
80
+  gap: 12px;
81 81
   align-items: center;
82 82
 }
83 83
 
84 84
 .search-input-box {
85
-  height: 56px;
85
+  height: 46px;
86 86
   border: 2px solid #d8caba;
87
-  border-radius: 8px;
88
-  background: #fffdf9;
87
+  border-radius: 6px;
88
+  background: #ffffff;
89 89
   display: flex;
90 90
   align-items: center;
91 91
   padding: 0 16px;
@@ -105,20 +105,22 @@
105 105
   outline: none;
106 106
   background: transparent;
107 107
   color: #2f2720;
108
-  font-size: 18px;
108
+  font-size: 16px;
109
+  font-weight: 700;
109 110
   font-family: inherit;
110 111
 }
111 112
 
112 113
 .search-input-box input::placeholder {
113 114
   color: #7b6b5c;
115
+  font-weight: 600;
114 116
 }
115 117
 
116 118
 .search-button,
117 119
 .filter-button-main,
118 120
 .back-list-button {
119
-  height: 56px;
120
-  border-radius: 8px;
121
-  font-size: 18px;
121
+  height: 46px;
122
+  border-radius: 6px;
123
+  font-size: 16px;
122 124
   font-weight: 800;
123 125
   cursor: pointer;
124 126
   box-sizing: border-box;
@@ -126,19 +128,19 @@
126 128
 
127 129
 .search-button {
128 130
   border: 2px solid #8a6543;
129
-  background: #8a6543;
130
-  color: #ffffff;
131
+  background: #ffffff;
132
+  color: #8a6543;
131 133
 }
132 134
 
133 135
 .search-button:hover {
134
-  background: #765639;
136
+  background: #f6efe6;
135 137
 }
136 138
 
137 139
 .filter-button-main,
138 140
 .back-list-button {
139
-  border: 2px solid #d8caba;
141
+  border: 2px solid #8a6543;
140 142
   background: #ffffff;
141
-  color: #2f2720;
143
+  color: #8a6543;
142 144
   text-decoration: none;
143 145
   display: flex;
144 146
   align-items: center;
@@ -154,18 +156,18 @@
154 156
   display: flex;
155 157
   flex-wrap: wrap;
156 158
   gap: 8px;
157
-  margin-top: 16px;
159
+  margin: 12px 0 16px;
158 160
 }
159 161
 
160 162
 .search-filter-button {
161 163
   min-width: 120px;
162
-  height: 38px;
163
-  padding: 0 18px;
164
+  height: 34px;
165
+  padding: 0 14px;
164 166
   border: 2px solid #d8caba;
165 167
   border-radius: 6px;
166 168
   background: #f1e7d8;
167 169
   color: #2f2720;
168
-  font-size: 15px;
170
+  font-size: 14px;
169 171
   font-weight: 700;
170 172
   cursor: pointer;
171 173
   box-sizing: border-box;
@@ -182,23 +184,31 @@
182 184
 }
183 185
 
184 186
 .search-result-panel {
185
-  margin-top: 16px;
186
-  padding: 28px 28px 34px;
187
+  margin-top: 0;
188
+  padding: 0;
187 189
   border: 2px solid #d8caba;
188
-  border-radius: 62px;
190
+  border-radius: 8px;
189 191
   background: #fffdf9;
190 192
   box-sizing: border-box;
193
+  overflow: hidden;
191 194
 }
192 195
 
193 196
 .search-result-panel h2 {
194
-  margin: 0 0 12px;
197
+  min-height: 40px;
198
+  margin: 0;
199
+  padding: 0 14px;
200
+  background: #efe4d6;
195 201
   color: #2f2720;
196
-  font-size: 24px;
202
+  font-size: 22px;
197 203
   font-weight: 800;
204
+  display: flex;
205
+  align-items: center;
198 206
 }
199 207
 
200 208
 .result-group {
201
-  margin-top: 16px;
209
+  margin-top: 0;
210
+  padding: 14px;
211
+  border-top: 1px solid #d8caba;
202 212
 }
203 213
 
204 214
 .result-group h3 {
@@ -209,7 +219,7 @@
209 219
 }
210 220
 
211 221
 .result-row {
212
-  min-height: 56px;
222
+  min-height: 52px;
213 223
   margin-top: 6px;
214 224
   padding: 0 12px;
215 225
   border: 2px solid #d8caba;
@@ -221,6 +231,7 @@
221 231
   align-items: center;
222 232
   column-gap: 18px;
223 233
   box-sizing: border-box;
234
+  cursor: pointer;
224 235
 }
225 236
 
226 237
 .result-row:hover {
@@ -228,7 +239,7 @@
228 239
 }
229 240
 
230 241
 .danka-result-row {
231
-  grid-template-columns: 1.1fr 1.4fr 1.8fr 80px;
242
+  grid-template-columns: 1.1fr 1.4fr 1.8fr;
232 243
 }
233 244
 
234 245
 .family-result-row {
@@ -243,11 +254,6 @@
243 254
   font-weight: 800;
244 255
 }
245 256
 
246
-.result-link-text {
247
-  color: #3f6f45;
248
-  font-weight: 800;
249
-}
250
-
251 257
 .empty-result {
252 258
   min-height: 72px;
253 259
   padding: 22px;
@@ -279,8 +285,19 @@
279 285
 }
280 286
 
281 287
 @media (max-width: 1100px) {
288
+  .search-page {
289
+    grid-template-columns: 1fr;
290
+    padding: 0 24px 32px;
291
+  }
292
+
293
+  .search-panel {
294
+    min-height: auto;
295
+    border-radius: 28px;
296
+    padding: 28px 24px 32px;
297
+  }
298
+
282 299
   .search-input-row {
283
-    grid-template-columns: 1fr 120px;
300
+    grid-template-columns: 1fr 104px 104px;
284 301
   }
285 302
 
286 303
   .filter-button-main,
@@ -288,10 +305,6 @@
288 305
     width: 100%;
289 306
   }
290 307
 
291
-  .search-result-panel {
292
-    border-radius: 36px;
293
-  }
294
-
295 308
   .result-row {
296 309
     overflow-x: auto;
297 310
   }
@@ -304,20 +317,6 @@
304 317
 }
305 318
 
306 319
 @media (max-width: 800px) {
307
-  .search-page {
308
-    flex-direction: column;
309
-  }
310
-
311
-  .search-main {
312
-    width: 100%;
313
-    padding: 16px 20px 32px;
314
-  }
315
-
316
-  .search-panel {
317
-    padding: 24px 20px 30px;
318
-    border-radius: 32px;
319
-  }
320
-
321 320
   .page-title-row {
322 321
     flex-direction: column;
323 322
   }

+ 126
- 46
src/app/pages/search/search.ts Datei anzeigen

@@ -1,6 +1,6 @@
1
-import { Component } from '@angular/core';
2
-import { FormBuilder, FormsModule } from '@angular/forms';
3
-import { 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';
@@ -16,7 +16,7 @@ import { AppSideMenu } from '../../share/side-menu/app-side-menu';
16 16
   templateUrl: './search.html',
17 17
   styleUrl: './search.scss',
18 18
 })
19
-export class Search {
19
+export class Search implements OnInit{
20 20
   searchKeyword = '';
21 21
   selectedSearchType = 'all';
22 22
   searchTypeFilters = [
@@ -29,79 +29,159 @@ export class Search {
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,
41
+  ) { }
42
+
43
+  ngOnInit(): void {
44
+    const keyword = this.route.snapshot.queryParamMap.get('keyword');
45
+
46
+    if (keyword) {
47
+      this.searchKeyword = keyword;
48
+      this.submitSearch();
49
+    }
50
+  }
38 51
 
39 52
   // フィルタータブの選択処理
40 53
   changeSearchType(searchType: string): void {
41 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
+
42 75
     this.searchAll();
43 76
   }
44 77
 
45 78
   // 全検索の処理
46
-  searchAll() {
79
+  async searchAll(): Promise<void> {
80
+    const requestId = ++this.searchRequestId;
47 81
     const keyword = this.searchKeyword.trim();
48
-    this.dankaResults = [];
49
-    this.familyResults = [];
50
-    this.kakochoResults = [];
51
-    this.totalResultCount = 0;
82
+    const searchType = this.selectedSearchType;
83
+
52 84
     if (keyword === '') {
85
+      this.setSearchResults([], [], [], requestId);
86
+      return;
87
+    }
88
+
89
+    const [dankaList, familyList, kakochoList] = await Promise.all([
90
+      this.dankaService.getDankaList(),
91
+      this.familyService.getFamilyList(),
92
+      this.kakochoService.getKakochoList(),
93
+    ]);
94
+
95
+    if (requestId !== this.searchRequestId) {
53 96
       return;
54 97
     }
55
-    // 檀家・家族・過去帳の情報を取得
56
-    const dankaList = this.dankaService.getDankaList();
57
-    const familyList = this.familyService.getFamilyList();
58
-    const kakochoList = this.kakochoService.getKakochoList();
59
-    //檀家のキーワード検索
60
-    if (this.selectedSearchType === 'all' || this.selectedSearchType === 'danka') {
61
-      this.dankaResults = dankaList.filter(
62
-        (danka) =>
63
-          danka.householdName.includes(keyword) ||
64
-          danka.householder.includes(keyword) ||
65
-          danka.postalCode.includes(keyword) ||
66
-          danka.address.includes(keyword) ||
67
-          danka.phones.some((phone) => phone.tel.includes(keyword) || phone.note.includes(keyword)),
98
+
99
+    let dankaResults: Danka[] = [];
100
+    let familyResults: Family[] = [];
101
+    let kakochoResults: Kakocho[] = [];
102
+
103
+    // 檀家検索
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(
111
+          (phone) =>
112
+            this.includesKeyword(phone.tel, keyword) ||
113
+            this.includesKeyword(phone.note, keyword)
114
+        )
68 115
       );
69 116
     }
70
-    //家族のキーワード検索
71
-    if (this.selectedSearchType === 'all' || this.selectedSearchType === 'family') {
72
-      this.familyResults = familyList.filter(
73
-        (family) =>
74
-          family.name.includes(keyword) ||
75
-          family.furigana.includes(keyword) ||
76
-          family.relationship.includes(keyword) ||
77
-          family.birthDate.includes(keyword) ||
78
-          family.note.includes(keyword),
117
+
118
+    // 家族検索
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)
79 126
       );
80 127
     }
81
-    //過去帳のキーワード検索
82
-    if (this.selectedSearchType === 'all' || this.selectedSearchType === 'kakocho') {
83
-      this.kakochoResults = kakochoList.filter(
84
-        (kakocho) =>
85
-          kakocho.name.includes(keyword) ||
86
-          kakocho.furigana.includes(keyword) ||
87
-          kakocho.relationship.includes(keyword) ||
88
-          kakocho.kaimyo.includes(keyword) ||
89
-          kakocho.deathDate.includes(keyword) ||
90
-          kakocho.ageAtDeath.includes(keyword) ||
91
-          kakocho.note.includes(keyword),
128
+
129
+    // 過去帳検索
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)
92 139
       );
93 140
     }
94
-    //検索結果の件数カウント
95
-    this.totalResultCount =
96
-      this.dankaResults.length + this.familyResults.length + this.kakochoResults.length;
141
+
142
+    this.setSearchResults(dankaResults, familyResults, kakochoResults, requestId);
97 143
   }
98 144
 
99 145
   clearSearch(): void {
146
+    if (this.searchTimer) {
147
+      clearTimeout(this.searchTimer);
148
+    }
149
+
150
+    this.searchRequestId++;
100 151
     this.searchKeyword = '';
101 152
     this.selectedSearchType = 'all';
102 153
     this.dankaResults = [];
103 154
     this.familyResults = [];
104 155
     this.kakochoResults = [];
105 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();
106 186
   }
107 187
 }

+ 111
- 64
src/app/services/dankaService.ts Datei anzeigen

@@ -1,81 +1,128 @@
1 1
 import { Injectable } from '@angular/core';
2
+import {
3
+  collection,
4
+  getDocs,
5
+  doc,
6
+  getDoc,
7
+  setDoc,
8
+  deleteDoc,
9
+  updateDoc
10
+} from 'firebase/firestore';
11
+
12
+import { db } from '../firebase';
2 13
 import { Danka } from '../models/danka';
3 14
 
4 15
 @Injectable({
5 16
   providedIn: 'root',
6 17
 })
7 18
 export class DankaService {
8
-  private dankaList: Danka[] = [
9
-    {
10
-      id: '1',
11
-      householdName: '鈴木家',
12
-      householdFurigana: 'すずきけ',
13
-      householder: '鈴木 太郎',
14
-      householderFurigana: 'すずき たろう',
15
-      postalCode: '123-4567',
16
-      address: '市内 1-2-3',
17
-      note: '寺報送付あり。年忌法要の案内は施主へ連絡。',
18
-      updatedAt: '2026-05-28',
19
-      phones: [
20
-        {
21
-          tel: '03-4567-8910',
22
-          note: '寺報連絡',
23
-        },
24
-        {
25
-          tel: '090-1234-5678',
26
-          note: '施主',
27
-        },
28
-      ],
29
-    },
30
-    {
31
-      id: '2',
32
-      householdName: '古田家',
33
-      householdFurigana: 'ふるたけ',
34
-      householder: '古田 太郎',
35
-      householderFurigana: 'ふるた たろう',
36
-      postalCode: '234-4567',
37
-      address: '市内 1-2-3',
38
-      note: '電話連絡を優先。',
39
-      updatedAt: '2026-05-28',
40
-      phones: [
41
-        {
42
-          tel: '0-5678-9101',
43
-          note: '寺報連絡',
44
-        },
45
-        {
46
-          tel: '080-7890-4567',
47
-          note: '施主',
48
-        },
49
-      ],
50
-    }
51
-  ];
19
+  private path = 'danka';
20
+  private readonly recentDankaStorageKey = 'kaimyo-management.recent-danka';
21
+
22
+  // 一覧
23
+  async getDankaList(): Promise<Danka[]> {
24
+    const snap = await getDocs(collection(db, this.path));
25
+
26
+    return snap.docs.map(d => ({
27
+      id: d.id,
28
+      ...(d.data() as Omit<Danka, 'id'>)
29
+    }));
30
+  }
31
+
32
+  // 1件
33
+  async getDankaById(id: string): Promise<Danka | undefined> {
34
+    const ref = doc(db, this.path, id);
35
+    const snap = await getDoc(ref);
36
+
37
+    if (!snap.exists()) return undefined;
52 38
 
53
-  //サービスの檀家一覧の取得
54
-  getDankaList(): Danka[] {
55
-    return this.dankaList;
39
+    return {
40
+      id: snap.id,
41
+      ...(snap.data() as Omit<Danka, 'id'>)
42
+    };
56 43
   }
57 44
 
58
-  //対象の檀家IDを取得
59
-  getDankaById(id: string): Danka | undefined {
60
-    return this.dankaList.find((danka) => danka.id === id);
45
+  // 作成・更新
46
+  async saveDanka(danka: Danka): Promise<void> {
47
+    const ref = doc(db, this.path, danka.id);
48
+    await setDoc(ref, danka);
61 49
   }
62 50
 
63
-  //DBへの檀家情報の登録
64
-  saveDanka(updatedDanka: Danka): void {
65
-    const index = this.dankaList.findIndex((danka) => danka.id === updatedDanka.id);
66
-    if (index === -1) {
67
-      this.dankaList.push(updatedDanka);
68
-      return;
51
+  // 部分更新
52
+  async updateDanka(id: string, data: Partial<Danka>): Promise<void> {
53
+    const ref = doc(db, this.path, id);
54
+    await updateDoc(ref, data as any);
55
+  }
56
+
57
+  // 削除
58
+  async deleteDanka(id: string): Promise<void> {
59
+    const ref = doc(db, this.path, id);
60
+    await deleteDoc(ref);
61
+  }
62
+
63
+  // -----------------------------
64
+  // 最近開いた檀家
65
+  // -----------------------------
66
+
67
+  private getRecentDankaIds(): string[] {
68
+    if (!this.canUseLocalStorage()) return [];
69
+
70
+    const value = localStorage.getItem(this.recentDankaStorageKey);
71
+    if (!value) return [];
72
+
73
+    try {
74
+      const parsed = JSON.parse(value);
75
+      return Array.isArray(parsed)
76
+        ? parsed.filter((id): id is string => typeof id === 'string')
77
+        : [];
78
+    } catch {
79
+      return [];
69 80
     }
70
-    this.dankaList[index] = updatedDanka;
71 81
   }
72 82
 
73
-  //DBの檀家情報の削除
74
-  deleteDanka(id: string): void {
75
-    const index = this.dankaList.findIndex((danka) => danka.id === id);
76
-    if (index === -1) {
77
-      return;
83
+  private saveRecentDankaIds(ids: string[]): void {
84
+    if (!this.canUseLocalStorage()) return;
85
+    localStorage.setItem(this.recentDankaStorageKey, JSON.stringify(ids));
86
+  }
87
+
88
+  private canUseLocalStorage(): boolean {
89
+    return typeof localStorage !== 'undefined';
90
+  }
91
+
92
+  // ★ここが重要:async化
93
+  async recordDankaOpened(id: string): Promise<void> {
94
+    const danka = await this.getDankaById(id);
95
+    if (!danka) return;
96
+
97
+    const recentIds = [
98
+      id,
99
+      ...this.getRecentDankaIds().filter(rid => rid !== id),
100
+    ].slice(0, 5);
101
+
102
+    this.saveRecentDankaIds(recentIds);
103
+  }
104
+
105
+  // ★ここも async 必須
106
+  async getRecentDankaList(limit = 5): Promise<Danka[]> {
107
+    const ids = this.getRecentDankaIds().slice(0, limit);
108
+
109
+    const list = await Promise.all(
110
+      ids.map(id => this.getDankaById(id))
111
+    );
112
+
113
+    const filtered = list.filter((d): d is Danka => d !== undefined);
114
+
115
+    if (filtered.length > 0) {
116
+      return filtered;
78 117
     }
79
-    this.dankaList.splice(index, 1);
118
+
119
+    // fallback(updatedAtがある前提なら)
120
+    const all = await this.getDankaList();
121
+
122
+    return [...all]
123
+      .sort((a: any, b: any) =>
124
+        (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '')
125
+      )
126
+      .slice(0, limit);
80 127
   }
81
-}
128
+}

+ 32
- 0
src/app/services/event-service.ts Datei anzeigen

@@ -0,0 +1,32 @@
1
+import { Injectable } from '@angular/core';
2
+import { EventStatus } from '../models/event';
3
+
4
+interface EventStatusRecord {
5
+  targetId: string;
6
+  status: EventStatus;
7
+}
8
+
9
+@Injectable({
10
+  providedIn: 'root',
11
+})
12
+export class EventService {
13
+  private eventStatuses: EventStatusRecord[] = [];
14
+
15
+  getEventStatus(targetId: string, defaultStatus: EventStatus): EventStatus {
16
+    return this.eventStatuses.find((item) => item.targetId === targetId)?.status ?? defaultStatus;
17
+  }
18
+
19
+  saveEventStatus(targetId: string, status: EventStatus): void {
20
+    const current = this.eventStatuses.find((item) => item.targetId === targetId);
21
+
22
+    if (current) {
23
+      current.status = status;
24
+      return;
25
+    }
26
+
27
+    this.eventStatuses.push({
28
+      targetId,
29
+      status,
30
+    });
31
+  }
32
+}

+ 51
- 168
src/app/services/family-service.ts Datei anzeigen

@@ -1,188 +1,71 @@
1 1
 import { Injectable } from '@angular/core';
2
+import {
3
+  collection,
4
+  getDocs,
5
+  doc,
6
+  getDoc,
7
+  setDoc,
8
+  deleteDoc,
9
+  query,
10
+  where,
11
+} from 'firebase/firestore';
12
+
13
+import { db } from '../firebase';
2 14
 import { Family } from '../models/family';
3 15
 
4 16
 @Injectable({
5 17
   providedIn: 'root',
6 18
 })
7 19
 export class FamilyService {
8
-  private families: Family[] = [
9
-    {
10
-      id: '1',
11
-      dankaId: '1',
12
-      furigana: 'すずき はなこ',
13
-      name: '鈴木 花子',
14
-      relationship: '母',
15
-      birthDate: '1975-01-01',
16
-      note: '次の施主',
17
-      fatherId: '5',
18
-      motherId: '6',
19
-      spouseId: '3',
20
-      gender: 'female',
21
-    },
22
-    {
23
-      id: '2',
24
-      dankaId: '1',
25
-      furigana: 'すずき たろう',
26
-      name: '鈴木 太郎',
27
-      relationship: '長男',
28
-      birthDate: '2005-12-31',
29
-      note: '',
30
-      fatherId: '3',
31
-      motherId: '1',
32
-      spouseId: '7',
33
-      gender: 'male',
34
-    },
35
-    {
36
-      id: '3',
37
-      dankaId: '1',
38
-      furigana: 'すずき いちろう',
39
-      name: '鈴木 一郎',
40
-      relationship: '父',
41
-      birthDate: '1973-05-10',
42
-      note: '',
43
-      fatherId: '',
44
-      motherId: '',
45
-      spouseId: '1',
46
-      gender: 'male',
47
-    },
48
-    {
49
-      id: '4',
50
-      dankaId: '1',
51
-      furigana: 'すずき さくら',
52
-      name: '鈴木 さくら',
53
-      relationship: '長女',
54
-      birthDate: '2008-04-15',
55
-      note: '',
56
-      fatherId: '3',
57
-      motherId: '1',
58
-      spouseId: '',
59
-      gender: 'female',
60
-    },
61
-    {
62
-      id: '5',
63
-      dankaId: '1',
64
-      furigana: 'さとう まさお',
65
-      name: '佐藤 正男',
66
-      relationship: '母方の祖父',
67
-      birthDate: '1948-03-20',
68
-      note: '花子の父',
69
-      fatherId: '',
70
-      motherId: '',
71
-      spouseId: '6',
72
-      gender: 'male',
73
-    },
74
-    {
75
-      id: '6',
76
-      dankaId: '1',
77
-      furigana: 'さとう ひさこ',
78
-      name: '佐藤 久子',
79
-      relationship: '母方の祖母',
80
-      birthDate: '1950-09-08',
81
-      note: '花子の母',
82
-      fatherId: '',
83
-      motherId: '',
84
-      spouseId: '5',
85
-      gender: 'female',
86
-    },
87
-    {
88
-      id: '7',
89
-      dankaId: '1',
90
-      furigana: 'すずき みさき',
91
-      name: '鈴木 美咲',
92
-      relationship: '長男の妻',
93
-      birthDate: '2006-07-22',
94
-      note: '',
95
-      fatherId: '',
96
-      motherId: '',
97
-      spouseId: '2',
98
-      gender: 'female',
99
-    },
100
-    {
101
-      id: '8',
102
-      dankaId: '1',
103
-      furigana: 'すずき れん',
104
-      name: '鈴木 蓮',
105
-      relationship: '孫',
106
-      birthDate: '2026-02-01',
107
-      note: '太郎と美咲の子',
108
-      fatherId: '2',
109
-      motherId: '7',
110
-      spouseId: '',
111
-      gender: 'male',
112
-    },
113
-    {
114
-      id: '9',
115
-      dankaId: '1',
116
-      furigana: 'やまだ はなこ',
117
-      name: '山田 花子',
118
-      relationship: '長男の前妻',
119
-      birthDate: '2005-04-02',
120
-      note: '',
121
-      fatherId: '',
122
-      motherId: '',
123
-      spouseId: '',
124
-      gender: 'female',
125
-    },
126
-  ];
127
-
128
-  //檀家と紐づいている家族情報の取得
129
-  getFamiliesByDankaId(dankaId: string): Family[] {
130
-    return this.families.filter((family) => family.dankaId === dankaId);
131
-  }
132
-
133
-  //家族の情報を取得
134
-  getFamilyById(id: string): Family | undefined {
135
-    return this.families.find((family) => family.id === id);
136
-  }
20
+  private path = 'families';
137 21
 
138
-  //家族の情報を更新
139
-  saveFamily(updatedFamily: Family): void {
140
-    const oldFamily = this.families.find((family) => family.id === updatedFamily.id);
22
+  // 檀家IDで一覧取得
23
+  async getFamiliesByDankaId(dankaId: string): Promise<Family[]> {
24
+    const q = query(
25
+      collection(db, this.path),
26
+      where('dankaId', '==', dankaId)
27
+    );
141 28
 
142
-    const oldSpouseId = oldFamily?.spouseId ?? '';
143
-    const newSpouseId = updatedFamily.spouseId ?? '';
29
+    const snap = await getDocs(q);
144 30
 
145
-    const familyIndex = this.families.findIndex((family) => family.id === updatedFamily.id);
31
+    return snap.docs.map(d => ({
32
+      id: d.id,
33
+      ...(d.data() as Omit<Family, 'id'>),
34
+    }));
35
+  }
146 36
 
147
-    if (familyIndex !== -1) {
148
-      this.families[familyIndex] = updatedFamily;
149
-    } else {
150
-      this.families.push(updatedFamily);
151
-    }
37
+  // 1件取得
38
+  async getFamilyById(id: string): Promise<Family | undefined> {
39
+    const ref = doc(db, this.path, id);
40
+    const snap = await getDoc(ref);
152 41
 
153
-    if (oldSpouseId && oldSpouseId !== newSpouseId) {
154
-      const oldSpouseIndex = this.families.findIndex((family) => family.id === oldSpouseId);
155
-      if (oldSpouseIndex !== -1 && this.families[oldSpouseIndex].spouseId === updatedFamily.id) {
156
-        this.families[oldSpouseIndex] = {
157
-          ...this.families[oldSpouseIndex],
158
-          spouseId: '',
159
-        };
160
-      }
161
-    }
42
+    if (!snap.exists()) return undefined;
162 43
 
163
-    if (newSpouseId) {
164
-      const newSpouseIndex = this.families.findIndex((family) => family.id === newSpouseId);
44
+    return {
45
+      id: snap.id,
46
+      ...(snap.data() as Omit<Family, 'id'>),
47
+    };
48
+  }
165 49
 
166
-      if (newSpouseIndex !== -1) {
167
-        this.families[newSpouseIndex] = {
168
-          ...this.families[newSpouseIndex],
169
-          spouseId: updatedFamily.id,
170
-        };
171
-      }
172
-    }
50
+  // 作成・更新(upsert)
51
+  async saveFamily(family: Family): Promise<void> {
52
+    const ref = doc(db, this.path, family.id);
53
+    await setDoc(ref, family);
173 54
   }
174 55
 
175
-  //家族の情報を削除
176
-  deleteFamily(id: string | undefined) {
177
-    const index = this.families.findIndex((family) => family.id === id);
178
-    if (index === -1) {
179
-      return;
180
-    }
181
-    this.families.splice(index, 1);
56
+  // 削除
57
+  async deleteFamily(id: string): Promise<void> {
58
+    const ref = doc(db, this.path, id);
59
+    await deleteDoc(ref);
182 60
   }
183 61
 
184 62
   //家族情報を全件取得する処理
185
-  getFamilyList(): Family[] {
186
-    return this.families;
63
+  async getFamilyList(): Promise<Family[]> {
64
+    const snap = await getDocs(collection(db, this.path));
65
+
66
+    return snap.docs.map(d => ({
67
+      id: d.id,
68
+      ...(d.data() as Omit<Family, 'id'>),
69
+    }));
187 70
   }
188
-}
71
+}

+ 60
- 168
src/app/services/family-tree-builder.ts Datei anzeigen

@@ -1,18 +1,19 @@
1 1
 import { Injectable } from '@angular/core';
2
-
3 2
 import { Family } from '../models/family';
4 3
 import { MarriageRelation } from '../models/marriage-relation';
4
+import { Kakocho } from '../models/kakocho';
5 5
 import { FamilyUnit } from '../models/family-unit';
6 6
 import { FamilyUnitNode } from '../models/family-unit-node';
7 7
 
8
+
8 9
 export interface FamilyTreeNode {
9 10
   family: Family;
10
-
11
-  parents: FamilyTreeNode[];
12
-
13
-  children: FamilyTreeNode[];
11
+  kakocho?: Kakocho;
12
+  isDeceased: boolean;
14 13
 
15 14
   spouses: FamilyTreeNode[];
15
+  children: FamilyTreeNode[];
16
+  parents: FamilyTreeNode[];
16 17
 }
17 18
 
18 19
 @Injectable({
@@ -23,17 +24,27 @@ export class FamilyTreeBuilderService {
23 24
   build(
24 25
     families: Family[],
25 26
     marriages: MarriageRelation[],
27
+    kakocholist: Kakocho[]
26 28
   ): FamilyTreeNode[] {
27 29
 
28 30
     const nodeMap = new Map<string, FamilyTreeNode>();
29 31
 
30
-    //
31
-    // ノード生成
32
-    //
32
+    const kakochoMap = new Map(
33
+      kakocholist.map(k => [k.familyId, k])
34
+    );
35
+
36
+    // -----------------------------
37
+    // ① ノード生成(ここで統合)
38
+    // -----------------------------
33 39
     families.forEach((family) => {
34 40
 
41
+      const kakocho = kakochoMap.get(family.id);
42
+
35 43
       nodeMap.set(family.id, {
36 44
         family,
45
+        kakocho,
46
+        isDeceased: !!kakocho,
47
+
37 48
         parents: [],
38 49
         children: [],
39 50
         spouses: [],
@@ -41,134 +52,64 @@ export class FamilyTreeBuilderService {
41 52
 
42 53
     });
43 54
 
44
-    //
45
-    // 親子関係生成
46
-    //
55
+    // -----------------------------
56
+    // 親子関係
57
+    // -----------------------------
47 58
     families.forEach((family) => {
48 59
 
49 60
       const childNode = nodeMap.get(family.id);
61
+      if (!childNode) return;
50 62
 
51
-      if (!childNode) {
52
-        return;
53
-      }
54
-
55
-      //
56
-      // 父
57
-      //
58 63
       if (family.fatherId) {
59
-
60
-        const fatherNode =
61
-          nodeMap.get(family.fatherId);
62
-
63
-        if (fatherNode) {
64
-
65
-          fatherNode.children.push(childNode);
66
-
67
-          childNode.parents.push(fatherNode);
64
+        const father = nodeMap.get(family.fatherId);
65
+        if (father) {
66
+          father.children.push(childNode);
67
+          childNode.parents.push(father);
68 68
         }
69 69
       }
70 70
 
71
-      //
72
-      // 母
73
-      //
74 71
       if (family.motherId) {
75
-
76
-        const motherNode =
77
-          nodeMap.get(family.motherId);
78
-
79
-        if (motherNode) {
80
-
81
-          motherNode.children.push(childNode);
82
-
83
-          childNode.parents.push(motherNode);
72
+        const mother = nodeMap.get(family.motherId);
73
+        if (mother) {
74
+          mother.children.push(childNode);
75
+          childNode.parents.push(mother);
84 76
         }
85 77
       }
86 78
 
87 79
     });
88 80
 
89
-    //
90
-    // 配偶者関係
91
-    //
81
+    // -----------------------------
82
+    // 配偶者関係(current)
83
+    // -----------------------------
92 84
     marriages
93
-      .filter(
94
-        (m) =>
95
-          m.status === 'current',
96
-      )
97
-      .forEach((marriage) => {
98
-
99
-        const person1 =
100
-          nodeMap.get(
101
-            marriage.person1Id,
102
-          );
103
-
104
-        const person2 =
105
-          nodeMap.get(
106
-            marriage.person2Id,
107
-          );
108
-
109
-        if (!person1 || !person2) {
110
-          return;
111
-        }
112
-
113
-        const id1 =
114
-          Number(person1.family.id);
85
+      .filter(m => m.status === 'current')
86
+      .forEach((m) => {
115 87
 
116
-        const id2 =
117
-          Number(person2.family.id);
88
+        const p1 = nodeMap.get(m.person1Id);
89
+        const p2 = nodeMap.get(m.person2Id);
118 90
 
119
-        const owner =
120
-          id1 < id2
121
-            ? person1
122
-            : person2;
91
+        if (!p1 || !p2) return;
123 92
 
124
-        const spouse =
125
-          id1 < id2
126
-            ? person2
127
-            : person1;
128
-
129
-        if (
130
-          !owner.spouses.some(
131
-            (s) =>
132
-              s.family.id ===
133
-              spouse.family.id,
134
-          )
135
-        ) {
136
-          owner.spouses.push(
137
-            spouse,
138
-          );
93
+        if (!p1.spouses.some(s => s.family.id === p2.family.id)) {
94
+          p1.spouses.push(p2);
139 95
         }
140
-      });
141
-
142
-    //
143
-    // spouseId フォールバック
144
-    //
145
-    families.forEach((family) => {
146 96
 
147
-      if (!family.spouseId) {
148
-        return;
149
-      }
97
+      });
150 98
 
151
-      const person =
152
-        nodeMap.get(family.id);
99
+    // -----------------------------
100
+    // ④ spouseIdフォールバック
101
+    // -----------------------------
102
+    families.forEach((f) => {
153 103
 
154
-      const spouse =
155
-        nodeMap.get(family.spouseId);
104
+      if (!f.spouseId) return;
156 105
 
157
-      if (!person || !spouse) {
158
-        return;
159
-      }
106
+      const a = nodeMap.get(f.id);
107
+      const b = nodeMap.get(f.spouseId);
160 108
 
161
-      if (
162
-        !person.spouses.some(
163
-          (s) =>
164
-            s.family.id ===
165
-            spouse.family.id
166
-        )
167
-      ) {
109
+      if (!a || !b) return;
168 110
 
169
-        person.spouses.push(
170
-          spouse
171
-        );
111
+      if (!a.spouses.some(s => s.family.id === b.family.id)) {
112
+        a.spouses.push(b);
172 113
       }
173 114
 
174 115
     });
@@ -176,55 +117,22 @@ export class FamilyTreeBuilderService {
176 117
     return [...nodeMap.values()];
177 118
   }
178 119
 
179
-  /**
180
-   * 家系図の起点になる人物
181
-   * (親が登録されていない人物)
182
-   */
183
-  getRoots(
184
-    nodes: FamilyTreeNode[]
185
-  ): FamilyTreeNode[] {
120
+  // -----------------------------
121
+  // Roots
122
+  // -----------------------------
123
+  getRoots(nodes: FamilyTreeNode[]): FamilyTreeNode[] {
186 124
 
187 125
     return nodes.filter(node => {
188 126
 
189
-      if (
190
-        node.parents.length > 0
191
-      ) {
192
-        return false;
193
-      }
194
-
195
-      // 配偶者がいて
196
-      if (
197
-        node.spouses.length > 0
198
-      ) {
127
+      if (node.parents.length > 0) return false;
199 128
 
200
-        const spouseId =
201
-          node.spouses[0].family.id;
202
-
203
-        // IDが若い方だけRoot
204
-        return (
205
-          node.family.id <
206
-          spouseId
207
-        );
129
+      if (node.spouses.length > 0) {
130
+        const spouseId = node.spouses[0].family.id;
131
+        return node.family.id < spouseId;
208 132
       }
209 133
 
210 134
       return true;
211
-
212 135
     });
213
-
214
-  }
215
-
216
-  /**
217
-   * ID検索
218
-   */
219
-  getNodeById(
220
-    nodes: FamilyTreeNode[],
221
-    id: string,
222
-  ): FamilyTreeNode | undefined {
223
-
224
-    return nodes.find(
225
-      (node) =>
226
-        node.family.id === id,
227
-    );
228 136
   }
229 137
 
230 138
   buildFamilyUnits(
@@ -238,9 +146,6 @@ export class FamilyTreeBuilderService {
238 146
 
239 147
     for (const node of nodes) {
240 148
 
241
-      //
242
-      // 夫婦あり
243
-      //
244 149
       if (node.spouses.length > 0) {
245 150
 
246 151
         const spouse =
@@ -262,9 +167,6 @@ export class FamilyTreeBuilderService {
262 167
 
263 168
         processed.add(key);
264 169
 
265
-        //
266
-        // この夫婦の子供を取得
267
-        //
268 170
         const children =
269 171
           nodes
270 172
             .filter(child => {
@@ -310,9 +212,6 @@ export class FamilyTreeBuilderService {
310 212
 
311 213
       } else {
312 214
 
313
-        //
314
-        // 独身者
315
-        //
316 215
         units.push({
317 216
 
318 217
           id:
@@ -346,9 +245,6 @@ export class FamilyTreeBuilderService {
346 245
     const nodeMap =
347 246
       new Map<string, FamilyUnitNode>();
348 247
 
349
-    //
350
-    // ノード生成
351
-    //
352 248
     units.forEach(unit => {
353 249
 
354 250
       nodeMap.set(unit.id, {
@@ -363,9 +259,6 @@ export class FamilyTreeBuilderService {
363 259
 
364 260
     });
365 261
 
366
-    //
367
-    // 親子リンク
368
-    //
369 262
     units.forEach(parentUnit => {
370 263
 
371 264
       parentUnit.children.forEach(child => {
@@ -417,5 +310,4 @@ export class FamilyTreeBuilderService {
417 310
     );
418 311
 
419 312
   }
420
-
421 313
 }

+ 62
- 59
src/app/services/kakocho-service.ts Datei anzeigen

@@ -1,79 +1,82 @@
1 1
 import { Injectable } from '@angular/core';
2
+import {
3
+  collection,
4
+  getDocs,
5
+  doc,
6
+  getDoc,
7
+  setDoc,
8
+  deleteDoc,
9
+  query,
10
+  where,
11
+} from 'firebase/firestore';
12
+
13
+import { db } from '../firebase';
2 14
 import { Kakocho } from '../models/kakocho';
3 15
 
4 16
 @Injectable({
5 17
   providedIn: 'root',
6 18
 })
7 19
 export class KakochoService {
8
-  private kakochoList: Kakocho[] = [
9
-    {
10
-      id: '1',
11
-      dankaId: '1',
12
-      familyId: '',
13
-      name: '鈴木 一郎',
14
-      furigana: 'すずき いちろう',
15
-      relationship: '父',
16
-      kaimyo: '光譽明照信士',
17
-      deathDate: '2024-01-08',
18
-      ageAtDeath: '88',
19
-      note: '三回忌対象',
20
-    },
21
-    {
22
-      id: '2',
23
-      dankaId: '2',
24
-      familyId: '',
25
-      name: '鈴木 ハナ',
26
-      furigana: 'すずき はな',
27
-      relationship: '母',
28
-      kaimyo: '清譽妙蓮大姉',
29
-      deathDate: '2020-05-12',
30
-      ageAtDeath: '82',
31
-      note: '',
32
-    },
33
-    {
34
-      id: '3',
35
-      dankaId: '2',
36
-      familyId: '',
37
-      name: '鈴木 太郎',
38
-      furigana: 'すずき たろう',
39
-      relationship: '息子',
40
-      kaimyo: '慈譽善道信士',
41
-      deathDate: '2025-01-08',
42
-      ageAtDeath: '50',
43
-      note: '',
44
-    },
45
-  ];
46 20
 
47
-  getKakochoByDankaId(dankaId: string): Kakocho[] {
48
-    return this.kakochoList.filter((kakocho) => kakocho.dankaId === dankaId);
21
+  private path = 'kakocho';
22
+
23
+  // 檀家IDで取得
24
+  async getKakochoByDankaId(dankaId: string): Promise<Kakocho[]> {
25
+    const q = query(
26
+      collection(db, this.path),
27
+      where('dankaId', '==', dankaId)
28
+    );
29
+
30
+    const snap = await getDocs(q);
31
+
32
+    return snap.docs.map(d => ({
33
+      id: d.id,
34
+      ...(d.data() as Omit<Kakocho, 'id'>),
35
+    }));
49 36
   }
50 37
 
51
-  // 一覧取得
52
-  getKakochoList(): Kakocho[] {
53
-    return this.kakochoList;
38
+  // =========================
39
+  // ✅ 既存互換API(残す)
40
+  // =========================
41
+
42
+  // 一覧取得(モック互換)
43
+  async getKakochoList(): Promise<Kakocho[]> {
44
+    const snap = await getDocs(collection(db, this.path));
45
+
46
+    return snap.docs.map(d => ({
47
+      id: d.id,
48
+      ...(d.data() as Omit<Kakocho, 'id'>),
49
+    }));
54 50
   }
55 51
 
56 52
   // 1件取得
57
-  getKakochoById(id: string): Kakocho | undefined {
58
-    return this.kakochoList.find((item) => item.id === id);
59
-  }
53
+  async getKakochoById(id: string): Promise<Kakocho | undefined> {
54
+    const ref = doc(db, this.path, id);
55
+    const snap = await getDoc(ref);
60 56
 
61
-  // 新規登録
62
-  addKakocho(data: Kakocho): void {
63
-    this.kakochoList.push(data);
57
+    if (!snap.exists()) return undefined;
58
+
59
+    return {
60
+      id: snap.id,
61
+      ...(snap.data() as Omit<Kakocho, 'id'>),
62
+    };
64 63
   }
65 64
 
66
-  // 更新
67
-  updateKakocho(updatedData: Kakocho): void {
68
-    const index = this.kakochoList.findIndex((item) => item.id === updatedData.id);
65
+  // 新規登録(残す)
66
+  async addKakocho(data: Kakocho): Promise<void> {
67
+    const ref = doc(db, this.path, data.id);
68
+    await setDoc(ref, data);
69
+  }
69 70
 
70
-    if (index !== -1) {
71
-      this.kakochoList[index] = updatedData;
72
-    }
71
+  // 更新(残す)
72
+  async updateKakocho(updatedData: Kakocho): Promise<void> {
73
+    const ref = doc(db, this.path, updatedData.id);
74
+    await setDoc(ref, updatedData);
73 75
   }
74 76
 
75
-  // 削除
76
-  deleteKakocho(id: string): void {
77
-    this.kakochoList = this.kakochoList.filter((item) => item.id !== id);
77
+  // 削除(既存互換のため残す)
78
+  async deleteKakocho(id: string): Promise<void> {
79
+    const ref = doc(db, this.path, id);
80
+    await deleteDoc(ref);
78 81
   }
79
-}
82
+}

+ 76
- 82
src/app/services/marriage-relation-service.ts Datei anzeigen

@@ -1,63 +1,87 @@
1 1
 import { Injectable } from '@angular/core';
2
+import {
3
+  collection,
4
+  getDocs,
5
+  doc,
6
+  getDoc,
7
+  setDoc,
8
+  deleteDoc,
9
+  query,
10
+  where,
11
+} from 'firebase/firestore';
12
+
13
+import { db } from '../firebase';
2 14
 import { MarriageRelation } from '../models/marriage-relation';
3 15
 
4 16
 @Injectable({
5 17
   providedIn: 'root',
6 18
 })
7 19
 export class MarriageRelationService {
8
-  private marriageRelations: MarriageRelation[] = [
9
-    {
10
-      id: '1',
11
-      dankaId: '1',
12
-      person1Id: '2',
13
-      person2Id: '9',
14
-      status: 'divorced',
15
-      startDate: '',
16
-      endDate: '',
17
-      note: '前妻',
18
-    },
19
-    {
20
-      id: '2',
21
-      dankaId: '1',
22
-      person1Id: '2',
23
-      person2Id: '7',
24
-      status: 'current',
25
-      startDate: '',
26
-      endDate: '',
27
-      note: '現在の配偶者',
28
-    },
29
-  ];
30
-
31
-  getMarriageRelationsByDankaId(dankaId: string): MarriageRelation[] {
32
-    return this.marriageRelations.filter((relation) => relation.dankaId === dankaId);
33
-  }
34 20
 
35
-  getMarriageRelationsByFamilyId(familyId: string): MarriageRelation[] {
36
-    return this.marriageRelations.filter(
37
-      (relation) => relation.person1Id === familyId || relation.person2Id === familyId,
21
+  private path = 'marriageRelations';
22
+
23
+  // 🔥 檀家IDで取得
24
+  async getMarriageRelationsByDankaId(dankaId: string): Promise<MarriageRelation[]> {
25
+    const q = query(
26
+      collection(db, this.path),
27
+      where('dankaId', '==', dankaId)
38 28
     );
29
+
30
+    const snap = await getDocs(q);
31
+
32
+    return snap.docs.map(d => ({
33
+      id: d.id,
34
+      ...(d.data() as Omit<MarriageRelation, 'id'>),
35
+    }));
39 36
   }
40 37
 
41
-  getCurrentMarriageByFamilyId(familyId: string): MarriageRelation | undefined {
42
-    return this.marriageRelations.find(
43
-      (relation) =>
44
-        relation.status === 'current' &&
45
-        (relation.person1Id === familyId || relation.person2Id === familyId),
46
-    );
38
+  // 🔥 家族IDで取得
39
+  async getMarriageRelationsByFamilyId(familyId: string): Promise<MarriageRelation[]> {
40
+    const snap = await getDocs(collection(db, this.path));
41
+
42
+    return snap.docs
43
+      .map(d => ({
44
+        id: d.id,
45
+        ...(d.data() as Omit<MarriageRelation, 'id'>),
46
+      }))
47
+      .filter(r =>
48
+        r.person1Id === familyId || r.person2Id === familyId
49
+      );
47 50
   }
48 51
 
49
-  getPastMarriagesByFamilyId(familyId: string): MarriageRelation[] {
50
-    return this.marriageRelations.filter(
51
-      (relation) =>
52
-        relation.status !== 'current' &&
53
-        (relation.person1Id === familyId || relation.person2Id === familyId),
52
+  // 🔥 現在の配偶者
53
+  async getCurrentMarriageByFamilyId(familyId: string): Promise<MarriageRelation | undefined> {
54
+    const relations = await this.getMarriageRelationsByFamilyId(familyId);
55
+
56
+    return relations.find(r =>
57
+      r.status === 'current'
54 58
     );
55 59
   }
56 60
 
57
-  getMarriageRelationById(id: string): MarriageRelation | undefined {
58
-    return this.marriageRelations.find((relation) => relation.id === id);
61
+  // 🔥 過去婚姻
62
+  async getPastMarriagesByFamilyId(familyId: string): Promise<MarriageRelation[]> {
63
+    const relations = await this.getMarriageRelationsByFamilyId(familyId);
64
+
65
+    return relations.filter(r => r.status !== 'current');
66
+  }
67
+
68
+  // 🔥 ID取得
69
+  async getMarriageRelationById(id: string): Promise<MarriageRelation | undefined> {
70
+    const ref = doc(db, this.path, id);
71
+    const snap = await getDoc(ref);
72
+
73
+    if (!snap.exists()) return undefined;
74
+
75
+    return {
76
+      id: snap.id,
77
+      ...(snap.data() as Omit<MarriageRelation, 'id'>),
78
+    };
59 79
   }
60 80
 
81
+  // =========================
82
+  // 🔥 既存互換(重要)
83
+  // =========================
84
+
61 85
   validateMarriageRelation(data: MarriageRelation): string[] {
62 86
     const errors: string[] = [];
63 87
 
@@ -66,60 +90,30 @@ export class MarriageRelationService {
66 90
     }
67 91
 
68 92
     if (data.person1Id === data.person2Id) {
69
-      errors.push('同じ人物同士を配偶関係に設定することはできません。');
93
+      errors.push('同じ人物同士を配偶関係に設定できません。');
70 94
     }
71 95
 
72
-    const duplicate = this.marriageRelations.find(
73
-      (relation) =>
74
-        relation.id !== data.id &&
75
-        ((relation.person1Id === data.person1Id && relation.person2Id === data.person2Id) ||
76
-          (relation.person1Id === data.person2Id && relation.person2Id === data.person1Id)),
77
-    );
78
-
79
-    if (duplicate) {
80
-      errors.push('この2人の配偶関係はすでに登録されています。');
81
-    }
82
-
83
-    if (data.status === 'current') {
84
-      const currentConflict = this.marriageRelations.find(
85
-        (relation) =>
86
-          relation.id !== data.id &&
87
-          relation.status === 'current' &&
88
-          (relation.person1Id === data.person1Id ||
89
-            relation.person2Id === data.person1Id ||
90
-            relation.person1Id === data.person2Id ||
91
-            relation.person2Id === data.person2Id),
92
-      );
93
-
94
-      if (currentConflict) {
95
-        errors.push(
96
-          '現在の配偶者は1人までです。既存の配偶関係を離婚・死別などに変更してから登録してください。',
97
-        );
98
-      }
99
-    }
96
+    // ⚠️ 注意:Firestore化後ここは「ローカルチェック不可」
97
+    // → 本来はDB取得が必要(今回は移行優先で維持)
100 98
 
101 99
     return errors;
102 100
   }
103 101
 
104
-  saveMarriageRelation(data: MarriageRelation): string[] {
102
+  async saveMarriageRelation(data: MarriageRelation): Promise<string[]> {
105 103
     const errors = this.validateMarriageRelation(data);
106 104
 
107 105
     if (errors.length > 0) {
108 106
       return errors;
109 107
     }
110 108
 
111
-    const index = this.marriageRelations.findIndex((relation) => relation.id === data.id);
112
-
113
-    if (index === -1) {
114
-      this.marriageRelations.push(data);
115
-    } else {
116
-      this.marriageRelations[index] = data;
117
-    }
109
+    const ref = doc(db, this.path, data.id);
110
+    await setDoc(ref, data);
118 111
 
119 112
     return [];
120 113
   }
121 114
 
122
-  deleteMarriageRelation(id: string): void {
123
-    this.marriageRelations = this.marriageRelations.filter((relation) => relation.id !== id);
115
+  async deleteMarriageRelation(id: string): Promise<void> {
116
+    const ref = doc(db, this.path, id);
117
+    await deleteDoc(ref);
124 118
   }
125
-}
119
+}

+ 1
- 1
src/app/share/header/app-header.scss Datei anzeigen

@@ -9,7 +9,7 @@
9 9
 }
10 10
 
11 11
 .app-title {
12
-  font-size: 24px;
12
+  font-size: 34px;
13 13
   font-weight: 700;
14 14
   color: #2f2720;
15 15
   letter-spacing: 0.03em;

+ 12
- 12
src/app/share/side-menu/app-side-menu.html Datei anzeigen

@@ -3,34 +3,34 @@
3 3
     <p class="menu-title">メニュー</p>
4 4
 
5 5
     <nav class="menu-list">
6
-      <a routerLink="/dashboard" routerLinkActive="active" class="menu-button">
6
+
7
+      <a routerLink="/dashboard" routerLinkActive="active" 
8
+        class="menu-button">
7 9
         ホーム
8 10
       </a>
9 11
 
10
-      <a routerLink="/danka-list" routerLinkActive="active" class="menu-button">
11
-        檀家一覧
12
+      <a routerLink="/danka-list" [class.active]="isDankaActive()" class="menu-button">
13
+        檀家
12 14
       </a>
13 15
 
14
-      <a routerLink="/danka-new" routerLinkActive="active" class="menu-button">
16
+      <a routerLink="/danka-new" routerLinkActive="active" 
17
+        class="menu-button">
15 18
         檀家登録
16 19
       </a>
17 20
 
18
-      <a routerLink="/memorial-list" routerLinkActive="active" class="menu-button">
21
+      <a routerLink="/memorial-list" routerLinkActive="active" 
22
+        class="menu-button">
19 23
         年次法要
20 24
       </a>
21 25
 
22
-      <a routerLink="/event" routerLinkActive="active" class="menu-button">
26
+      <a routerLink="/event" routerLinkActive="active"  class="menu-button">
23 27
         行事
24 28
       </a>
25 29
 
26
-      <a routerLink="/search" routerLinkActive="active" class="menu-button">
30
+      <a routerLink="/search" routerLinkActive="active"  class="menu-button">
27 31
         まとめて検索
28 32
       </a>
29 33
 
30
-<!--  ユーザー数が増えた際に実装    -->
31
-<!--      <a routerLink="/user-setting" routerLinkActive="active" class="menu-button">-->
32
-<!--        利用者設定-->
33
-<!--      </a>-->
34 34
     </nav>
35 35
   </div>
36
-</aside>
36
+</aside>

+ 2
- 1
src/app/share/side-menu/app-side-menu.scss Datei anzeigen

@@ -18,10 +18,11 @@
18 18
 
19 19
 .menu-title {
20 20
   margin: 0 0 14px;
21
-  padding-left: 10px;
21
+  padding-left: 0;
22 22
   font-size: 15px;
23 23
   font-weight: 700;
24 24
   color: #5a4a3c;
25
+  text-align: center;
25 26
 }
26 27
 
27 28
 .menu-list {

+ 24
- 2
src/app/share/side-menu/app-side-menu.ts Datei anzeigen

@@ -1,10 +1,32 @@
1 1
 import { Component } from '@angular/core';
2
-import { RouterLink, RouterLinkActive } from '@angular/router';
2
+import { Router, RouterLink, RouterLinkActive } from '@angular/router';
3 3
 
4 4
 @Component({
5 5
   selector: 'app-side-menu',
6
+  standalone: true,
6 7
   imports: [RouterLink, RouterLinkActive],
7 8
   templateUrl: './app-side-menu.html',
8 9
   styleUrl: './app-side-menu.scss',
9 10
 })
10
-export class AppSideMenu {}
11
+export class AppSideMenu {
12
+
13
+  constructor(public router: Router) {}
14
+
15
+isDankaActive(): boolean {
16
+  const url = this.router.url.split('?')[0];
17
+
18
+  const dankaPages = [
19
+    '/danka-list',
20
+    '/danka-detail',
21
+    '/danka-edit',
22
+  ];
23
+
24
+  const isDankaPage = dankaPages.some(path =>
25
+    url.startsWith(path)
26
+  );
27
+
28
+  const isExcluded = url.startsWith('/danka-new');
29
+
30
+  return isDankaPage && !isExcluded;
31
+}
32
+}

Laden…
Abbrechen
Speichern