43 次程式碼提交

作者 SHA1 備註 提交日期
  poohr 262595cdb4 Merge remote-tracking branch 'origin/master' 2 週之前
  kuni 2aff82c0e0 added 2 週之前
  kuni aeccc2b2d4 added 2 週之前
  poohr fb2fd0e081 [fix] 2 週之前
  poohr 2e0b09ebbb [update] 2 週之前
  poohr 4af603bcb3 Merge remote-tracking branch 'origin/master' 3 週之前
  kuni 213395bf35 added 3 週之前
  poohr 902303ebf0 Merge remote-tracking branch 'origin/master' 3 週之前
  kuni 7950ed12d5 added 3 週之前
  poohr b1ef54657b [update] 3 週之前
  kuni 4a7211b4a2 added 3 週之前
  poohr e829c2b791 Merge remote-tracking branch 'origin/master' 3 週之前
  poohr 876b7da25e [update] 3 週之前
  kuni d2e7d4d6c9 added 3 週之前
  poohr 19fb420f5c [update] 3 週之前
  kuni e094dd1506 added 3 週之前
  poohr 55c8e48e20 Merge remote-tracking branch 'origin/master' 3 週之前
  poohr 72b3f06044 Merge remote-tracking branch 'origin/master' 3 週之前
  kuni 411056add2 added 3 週之前
  kuni ce5ff462e4 added 3 週之前
  poohr 8c5b2e22e5 [update] 3 週之前
  kuni b0224195a9 Merge branch 'master' of https://gitea.softopia.gku.ac.jp/nyoraiji/kaimyo-management 3 週之前
  kuni 157806e70f added 3 週之前
  poohr 4b23f24d7c Merge remote-tracking branch 'origin/master' 3 週之前
  poohr 55930d3ed2 [update] 3 週之前
  kuni 57f38c65db added 3 週之前
  kuni 148dd5a47c added 3 週之前
  poohr 2f6e9bb4fb Merge remote-tracking branch 'origin/master' 3 週之前
  poohr e2e0368eef [update] 3 週之前
  kuni 5a3ba3f4e4 added 3 週之前
  poohr f82987694b [add] 3 週之前
  kuni 22575404f9 added 3 週之前
  poohr 0a44ef1385 [add] 3 週之前
  kuni 5b696f87f5 added 3 週之前
  poohr e22fc1aa37 Merge branch 'develop' 3 週之前
  poohr 8b605096a0 Merge remote-tracking branch 'origin/master' 3 週之前
  poohr fdd9746ac2 [add] 3 週之前
  kuni 40d636463b added 3 週之前
  poohr 3ed9dc93f2 Merge branch 'develop' 3 週之前
  poohr 44f033cb93 [wip] 3 週之前
  kuni 1adc0761ce added 3 週之前
  poohr 0de4a70860 [add] 3 週之前
  poohr ade8d77d9d [add] 3 週之前
共有 45 個文件被更改,包括 3403 次插入2034 次删除
  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 查看文件

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 查看文件

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 查看文件

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

+ 4
- 3
angular.json 查看文件

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

+ 16
- 0
firebase.json 查看文件

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
文件差異過大導致無法顯示
查看文件


+ 1
- 0
package.json 查看文件

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

+ 15
- 0
src/app/firebase.ts 查看文件

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 查看文件

1
 import { Family } from "./family";
1
 import { Family } from "./family";
2
+import { Kakocho } from "./kakocho";
2
 
3
 
3
 export interface FamilyTreeNode {
4
 export interface FamilyTreeNode {
4
   family: Family;
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
   parents: FamilyTreeNode[];
13
   parents: FamilyTreeNode[];
11
 }
14
 }

+ 1
- 0
src/app/models/memorial.ts 查看文件

3
   id: string;
3
   id: string;
4
   dankaId: string;
4
   dankaId: string;
5
   name: string;
5
   name: string;
6
+  furigana: string;
6
   kaimyo: string;
7
   kaimyo: string;
7
   relationship: string;
8
   relationship: string;
8
   householdName: string;
9
   householdName: string;

+ 606
- 594
src/app/pages/danka-detail/danka-detail.html
文件差異過大導致無法顯示
查看文件


+ 203
- 67
src/app/pages/danka-detail/danka-detail.scss 查看文件

2
   position: relative;
2
   position: relative;
3
   display: block;
3
   display: block;
4
   min-height: 100vh;
4
   min-height: 100vh;
5
-  background: #f4eee4;
5
+  background: #f6f0e7;
6
   color: #2f2720;
6
   color: #2f2720;
7
 }
7
 }
8
 
8
 
9
 .danka-detail-page {
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
 .danka-detail-main {
17
 .danka-detail-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19
   box-sizing: border-box;
19
   box-sizing: border-box;
20
 }
20
 }
21
 
21
 
22
 .detail-panel {
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
   border: 2px solid #d8caba;
26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28
   box-sizing: border-box;
28
   box-sizing: border-box;
29
 }
29
 }
30
 
30
 
32
   display: flex;
32
   display: flex;
33
   justify-content: space-between;
33
   justify-content: space-between;
34
   align-items: flex-start;
34
   align-items: flex-start;
35
-  margin-bottom: 20px;
35
+  gap: 24px;
36
+  margin-bottom: 22px;
36
 }
37
 }
37
 
38
 
38
 .page-title-row h1 {
39
 .page-title-row h1 {
39
-  margin: 0 0 8px;
40
+  margin: 0 0 18px;
40
   color: #2f2720;
41
   color: #2f2720;
41
-  font-size: 32px;
42
-  line-height: 1.2;
42
+  font-size: 34px;
43
+  line-height: 1.1;
43
   font-weight: 800;
44
   font-weight: 800;
44
-  letter-spacing: 0.02em;
45
+  letter-spacing: 0;
45
 }
46
 }
46
 
47
 
47
 .tab-list {
48
 .tab-list {
83
 .edit-button {
84
 .edit-button {
84
   width: 140px;
85
   width: 140px;
85
   height: 46px;
86
   height: 46px;
86
-  margin-top: 36px;
87
+  margin-top: 54px;
87
   border: 2px solid #8a6543;
88
   border: 2px solid #8a6543;
88
   border-radius: 6px;
89
   border-radius: 6px;
89
   background: #ffffff;
90
   background: #ffffff;
100
 
101
 
101
 .family-summary {
102
 .family-summary {
102
   min-height: 64px;
103
   min-height: 64px;
103
-  margin-bottom: 28px;
104
-  padding: 12px 22px;
104
+  margin-bottom: 22px;
105
+  padding: 14px 24px;
105
   border: 2px solid #d8caba;
106
   border: 2px solid #d8caba;
106
-  border-radius: 14px;
107
-  background: #eadfce;
107
+  border-radius: 8px;
108
+  background: #fbf7f0;
108
   display: flex;
109
   display: flex;
109
   align-items: center;
110
   align-items: center;
110
   box-sizing: border-box;
111
   box-sizing: border-box;
112
 
113
 
113
 .family-name-area {
114
 .family-name-area {
114
   display: flex;
115
   display: flex;
115
-  align-items: baseline;
116
+  align-items: center;
116
   gap: 18px;
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
 .family-name {
133
 .family-name {
120
   margin: 0;
134
   margin: 0;
121
   color: #2f2720;
135
   color: #2f2720;
156
 .add-button {
170
 .add-button {
157
   width: 140px;
171
   width: 140px;
158
   height: 46px;
172
   height: 46px;
159
-  margin-top: 36px;
173
+  margin-top: 54px;
160
   border: 2px solid #8a6543;
174
   border: 2px solid #8a6543;
161
   border-radius: 6px;
175
   border-radius: 6px;
162
   background: #ffffff;
176
   background: #ffffff;
174
 .family-page-add-button {
188
 .family-page-add-button {
175
   width: 140px;
189
   width: 140px;
176
   height: 46px;
190
   height: 46px;
177
-  margin-top: 36px;
191
+  margin-top: 54px;
178
   border: 2px solid #8a6543;
192
   border: 2px solid #8a6543;
179
   border-radius: 6px;
193
   border-radius: 6px;
180
   background: #ffffff;
194
   background: #ffffff;
195
 
209
 
196
 .detail-content {
210
 .detail-content {
197
   display: grid;
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
   align-items: start;
214
   align-items: start;
201
 }
215
 }
202
 
216
 
203
 .basic-info-section {
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
 .section-heading {
225
 .section-heading {
208
-  margin-bottom: 8px;
226
+  margin-bottom: 14px;
209
 }
227
 }
210
 
228
 
211
 .section-heading h2 {
229
 .section-heading h2 {
213
   color: #2f2720;
231
   color: #2f2720;
214
   font-size: 22px;
232
   font-size: 22px;
215
   font-weight: 800;
233
   font-weight: 800;
234
+  line-height: 1.3;
216
 }
235
 }
217
 
236
 
218
 .section-heading p {
237
 .section-heading p {
226
 }
245
 }
227
 
246
 
228
 .info-pair-row {
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
   display: grid;
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
 .info-row {
261
 .info-row {
236
   display: grid;
262
   display: grid;
237
-  grid-template-columns: 96px 1fr;
263
+  grid-template-columns: 154px 1fr;
238
   align-items: center;
264
   align-items: center;
239
   margin-top: 0;
265
   margin-top: 0;
266
+  min-height: 62px;
267
+  padding: 6px 0;
268
+  box-sizing: border-box;
240
 }
269
 }
241
 
270
 
242
 .info-form > .info-row {
271
 .info-form > .info-row {
244
 }
273
 }
245
 
274
 
246
 .info-label {
275
 .info-label {
276
+  min-height: 42px;
277
+  padding: 0 14px;
278
+  border-radius: 6px;
279
+  background: #f3eee8;
247
   color: #4b3c31;
280
   color: #4b3c31;
248
-  font-size: 17px;
281
+  font-size: 16px;
249
   font-weight: 800;
282
   font-weight: 800;
283
+  display: flex;
284
+  align-items: center;
285
+  box-sizing: border-box;
250
 }
286
 }
251
 
287
 
252
 .info-value {
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
   color: #2f2720;
294
   color: #2f2720;
259
-  font-size: 17px;
295
+  font-size: 16px;
296
+  font-weight: 700;
260
   box-sizing: border-box;
297
   box-sizing: border-box;
261
   display: flex;
298
   display: flex;
262
   align-items: center;
299
   align-items: center;
264
 
301
 
265
 .phone-row {
302
 .phone-row {
266
   align-items: start;
303
   align-items: start;
304
+  min-height: 104px;
267
 }
305
 }
268
 
306
 
269
 .phone-row .info-label {
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
 .phone-table {
313
 .phone-table {
274
   width: 100%;
314
   width: 100%;
315
+  padding: 8px 18px;
316
+  box-sizing: border-box;
275
 }
317
 }
276
 
318
 
277
 .phone-header,
319
 .phone-header,
294
 }
336
 }
295
 
337
 
296
 .phone-item {
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
   color: #2f2720;
345
   color: #2f2720;
304
   font-size: 16px;
346
   font-size: 16px;
305
   box-sizing: border-box;
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
 .status-panel {
382
 .status-panel {
309
-  min-height: 382px;
310
-  padding: 24px 22px 22px;
383
+  min-height: 100%;
384
+  padding: 26px 20px 24px;
311
   border: 2px solid #d8caba;
385
   border: 2px solid #d8caba;
312
-  border-radius: 62px;
386
+  border-radius: 12px;
313
   background: #fffdf9;
387
   background: #fffdf9;
314
   box-sizing: border-box;
388
   box-sizing: border-box;
315
 }
389
 }
316
 
390
 
317
 .status-panel h2 {
391
 .status-panel h2 {
318
-  margin: 0 0 18px;
392
+  margin: 0 0 24px;
319
   color: #2f2720;
393
   color: #2f2720;
320
   font-size: 22px;
394
   font-size: 22px;
321
   font-weight: 800;
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
 .status-card-list {
407
 .status-card-list {
325
   display: grid;
408
   display: grid;
326
   grid-template-columns: 1fr 1fr;
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
 .status-card {
414
 .status-card {
332
-  min-height: 104px;
333
-  padding: 14px 18px;
415
+  min-height: 126px;
416
+  padding: 18px 16px;
334
   border: 2px solid #d8caba;
417
   border: 2px solid #d8caba;
335
-  border-radius: 14px;
418
+  border-radius: 8px;
336
   background: #ffffff;
419
   background: #ffffff;
337
   color: #2f2720;
420
   color: #2f2720;
338
   text-decoration: none;
421
   text-decoration: none;
347
   margin: 0;
430
   margin: 0;
348
   color: #7b6b5c;
431
   color: #7b6b5c;
349
   font-size: 16px;
432
   font-size: 16px;
433
+  text-align: center;
350
 }
434
 }
351
 
435
 
352
 .status-count {
436
 .status-count {
353
-  margin: 2px 0 0;
437
+  margin: 10px 0 0;
354
   color: #2f2720;
438
   color: #2f2720;
355
   font-size: 32px;
439
   font-size: 32px;
356
   font-weight: 800;
440
   font-weight: 800;
357
   line-height: 1.1;
441
   line-height: 1.1;
442
+  text-align: center;
358
 }
443
 }
359
 
444
 
360
 .status-link {
445
 .status-link {
361
-  margin: 4px 0 0;
446
+  margin: 10px 0 0;
362
   color: #7b6b5c;
447
   color: #7b6b5c;
363
   font-size: 14px;
448
   font-size: 14px;
449
+  text-align: center;
364
 }
450
 }
365
 
451
 
366
 .next-memorial {
452
 .next-memorial {
368
 }
454
 }
369
 
455
 
370
 .next-memorial h3 {
456
 .next-memorial h3 {
371
-  margin: 0 0 8px;
457
+  margin: 0 0 16px;
372
   color: #4b3c31;
458
   color: #4b3c31;
373
-  font-size: 17px;
459
+  font-size: 18px;
374
   font-weight: 800;
460
   font-weight: 800;
461
+  display: flex;
462
+  align-items: center;
463
+  gap: 12px;
375
 }
464
 }
376
 
465
 
377
 .memorial-card {
466
 .memorial-card {
378
-  min-height: 72px;
379
-  padding: 14px 20px;
467
+  min-height: 116px;
468
+  padding: 24px 18px;
380
   border: 2px solid #d8caba;
469
   border: 2px solid #d8caba;
381
-  border-radius: 10px;
470
+  border-radius: 8px;
382
   background: #ffffff;
471
   background: #ffffff;
383
   box-sizing: border-box;
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
 .memorial-title {
479
 .memorial-title {
446
 
539
 
447
   h2 {
540
   h2 {
448
     margin: 0;
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
 
813
 
720
   h2 {
814
   h2 {
721
     margin: 0;
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
   color: #ffffff;
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
 @media (max-width: 800px) {
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
   .family-list-summary {
1325
   .family-list-summary {
1190
     align-items: flex-start;
1326
     align-items: flex-start;
1191
     flex-direction: column;
1327
     flex-direction: column;
1303
 .family-text {
1439
 .family-text {
1304
   writing-mode: vertical-rl;
1440
   writing-mode: vertical-rl;
1305
   text-orientation: upright;
1441
   text-orientation: upright;
1306
-}
1442
+}

+ 94
- 63
src/app/pages/danka-detail/danka-detail.ts 查看文件

1
-import { Component } from '@angular/core';
1
+import { ChangeDetectorRef, Component } from '@angular/core';
2
 import {
2
 import {
3
   ElementRef,
3
   ElementRef,
4
   ViewChild,
4
   ViewChild,
5
   AfterViewInit
5
   AfterViewInit
6
 } from '@angular/core';
6
 } from '@angular/core';
7
+import { OnInit } from '@angular/core';
7
 import { ActivatedRoute, RouterLink } from '@angular/router';
8
 import { ActivatedRoute, RouterLink } from '@angular/router';
8
 import { DankaService } from '../../services/dankaService';
9
 import { DankaService } from '../../services/dankaService';
9
 import { FamilyService } from '../../services/family-service';
10
 import { FamilyService } from '../../services/family-service';
17
 import { MarriageRelationService } from '../../services/marriage-relation-service';
18
 import { MarriageRelationService } from '../../services/marriage-relation-service';
18
 import { FormsModule } from '@angular/forms';
19
 import { FormsModule } from '@angular/forms';
19
 import { EventStatus, EventTarget, EventType } from '../../models/event';
20
 import { EventStatus, EventTarget, EventType } from '../../models/event';
21
+import { EventService } from '../../services/event-service';
20
 import {
22
 import {
21
   FamilyTreeBuilderService,
23
   FamilyTreeBuilderService,
22
   FamilyTreeNode
24
   FamilyTreeNode
33
 import { FamilyUnitLayoutService } from '../../services/family-unit-layout';
35
 import { FamilyUnitLayoutService } from '../../services/family-unit-layout';
34
 
36
 
35
 
37
 
38
+
36
 interface NextMemorial {
39
 interface NextMemorial {
37
   name: string;
40
   name: string;
38
   memorialType: string;
41
   memorialType: string;
46
   templateUrl: './danka-detail.html',
49
   templateUrl: './danka-detail.html',
47
   styleUrl: './danka-detail.scss',
50
   styleUrl: './danka-detail.scss',
48
 })
51
 })
49
-export class DankaDetail implements AfterViewInit {
52
+export class DankaDetail implements OnInit, AfterViewInit {
50
   danka: Danka | undefined;
53
   danka: Danka | undefined;
51
   families: Family[] = [];
54
   families: Family[] = [];
52
   kakocholist: Kakocho[] = [];
55
   kakocholist: Kakocho[] = [];
56
   familySearchKeyword = '';
59
   familySearchKeyword = '';
57
   eventSearchKeyword = '';
60
   eventSearchKeyword = '';
58
   eventStatuses: EventStatus[] = ['未案内', '案内済'];
61
   eventStatuses: EventStatus[] = ['未案内', '案内済'];
59
-  private eventStatusByTargetId: Record<string, EventStatus> = {};
60
   treeNodes: FamilyTreeNode[] = [];
62
   treeNodes: FamilyTreeNode[] = [];
61
   layoutNodes: LayoutNode[] = [];
63
   layoutNodes: LayoutNode[] = [];
62
   layoutNodeMap = new Map<string, LayoutNode>();
64
   layoutNodeMap = new Map<string, LayoutNode>();
70
 
72
 
71
   @ViewChild('familyTreeSvg')
73
   @ViewChild('familyTreeSvg')
72
   familyTreeSvg?: ElementRef<SVGSVGElement>;
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
   private panZoomInstance: any;
82
   private panZoomInstance: any;
74
 
83
 
75
   readonly PERSON_WIDTH = 90;
84
   readonly PERSON_WIDTH = 90;
90
     private familyTreeBuilder: FamilyTreeBuilderService,
99
     private familyTreeBuilder: FamilyTreeBuilderService,
91
     private familyTreeLayout: FamilyTreeLayoutService,
100
     private familyTreeLayout: FamilyTreeLayoutService,
92
     private familyUnitLayout: FamilyUnitLayoutService,
101
     private familyUnitLayout: FamilyUnitLayoutService,
102
+    private eventService: EventService,
103
+    private cdr: ChangeDetectorRef,
93
   ) {
104
   ) {
105
+
94
     const tab = this.route.snapshot.queryParams['tab'];
106
     const tab = this.route.snapshot.queryParams['tab'];
95
     if (tab === 'family') {
107
     if (tab === 'family') {
96
       this.selectedTab = 'family';
108
       this.selectedTab = 'family';
102
       this.selectedTab = 'familyTree';
114
       this.selectedTab = 'familyTree';
103
     }
115
     }
104
 
116
 
117
+  }
118
+  ngOnInit(): void {
119
+    this.init();
120
+  }
121
+  async init(): Promise<void> {
105
     const id = this.route.snapshot.params['id'];
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
   ngAfterViewInit(): void {
209
   ngAfterViewInit(): void {
288
         const age = this.currentYear - birthDate.getFullYear();
319
         const age = this.currentYear - birthDate.getFullYear();
289
         return this.getEventTypes(age).map((eventType) => {
320
         return this.getEventTypes(age).map((eventType) => {
290
           const id = `${family.id}-${eventType}`;
321
           const id = `${family.id}-${eventType}`;
322
+          const defaultStatus: EventStatus = Number(family.id) % 2 === 0 ? '案内済' : '未案内';
291
           return {
323
           return {
292
             id,
324
             id,
293
             dankaId: family.dankaId,
325
             dankaId: family.dankaId,
299
             age,
331
             age,
300
             eventType,
332
             eventType,
301
             note: family.note,
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
 
366
 
336
   changeEventStatus(target: EventTarget, status: EventStatus): void {
367
   changeEventStatus(target: EventTarget, status: EventStatus): void {
337
     target.status = status;
368
     target.status = status;
338
-    this.eventStatusByTargetId[target.id] = status;
369
+    this.eventService.saveEventStatus(target.id, status);
339
   }
370
   }
340
 
371
 
341
   getKaiki(deathDate: string): number {
372
   getKaiki(deathDate: string): number {

+ 21
- 17
src/app/pages/danka-edit/danka-edit.html 查看文件

100
           <section class="phone-edit-section">
100
           <section class="phone-edit-section">
101
             <div class="section-heading">
101
             <div class="section-heading">
102
               <h2>電話番号(複数登録)</h2>
102
               <h2>電話番号(複数登録)</h2>
103
-              <p>番号と備考を複数登録できます。</p>
104
             </div>
103
             </div>
105
 
104
 
106
             <div formArrayName="phones" class="phone-table">
105
             <div formArrayName="phones" class="phone-table">
148
         </div>
147
         </div>
149
 
148
 
150
         <div class="bottom-actions">
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
         </div>
171
         </div>
168
       </form>
172
       </form>
169
     </section>
173
     </section>

+ 37
- 16
src/app/pages/danka-edit/danka-edit.scss 查看文件

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

+ 13
- 5
src/app/pages/danka-edit/danka-edit.ts 查看文件

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

+ 50
- 47
src/app/pages/danka-list/danka-list.ts 查看文件

1
-import { Component } from '@angular/core';
1
+import { Component, OnInit } from '@angular/core';
2
 import { RouterLink } from '@angular/router';
2
 import { RouterLink } from '@angular/router';
3
 import { DankaService } from '../../services/dankaService';
3
 import { DankaService } from '../../services/dankaService';
4
 import { FamilyService } from '../../services/family-service';
4
 import { FamilyService } from '../../services/family-service';
26
   templateUrl: './danka-list.html',
26
   templateUrl: './danka-list.html',
27
   styleUrl: './danka-list.scss',
27
   styleUrl: './danka-list.scss',
28
 })
28
 })
29
-export class DankaList {
29
+export class DankaList implements OnInit {
30
   dankaList: Danka[] = [];
30
   dankaList: Danka[] = [];
31
   filterDankaList: Danka[] = [];
31
   filterDankaList: Danka[] = [];
32
   searchKeyword: string = '';
32
   searchKeyword: string = '';
33
   dankaDisplay: number = 0;
33
   dankaDisplay: number = 0;
34
   selectedFilter = 'all';
34
   selectedFilter = 'all';
35
   selectedKanaRow: KanaRowValue = 'all';
35
   selectedKanaRow: KanaRowValue = 'all';
36
+
36
   kanaRows: { label: string; value: KanaRowValue }[] = [
37
   kanaRows: { label: string; value: KanaRowValue }[] = [
37
     { label: '全件', value: 'all' },
38
     { label: '全件', value: 'all' },
38
     { label: 'あ行', value: 'a' },
39
     { label: 'あ行', value: 'a' },
48
   ];
49
   ];
49
 
50
 
50
   private readonly kanaRowMap: Record<Exclude<KanaRowValue, 'all'>, string[]> = {
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
   constructor(
64
   constructor(
64
     private dankaService: DankaService,
65
     private dankaService: DankaService,
65
     private familyService: FamilyService,
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
     this.showAllDanka();
76
     this.showAllDanka();
69
   }
77
   }
70
 
78
 
71
-  //全件タグで絞り込み
72
   showAllDanka() {
79
   showAllDanka() {
73
     this.selectedFilter = 'all';
80
     this.selectedFilter = 'all';
74
     this.selectedKanaRow = 'all';
81
     this.selectedKanaRow = 'all';
77
     this.dankaDisplay = this.filterDankaList.length;
84
     this.dankaDisplay = this.filterDankaList.length;
78
   }
85
   }
79
 
86
 
80
-  //電話番号タグで絞り込み
81
   filterPhoneAvailable() {
87
   filterPhoneAvailable() {
82
     this.selectedFilter = 'phone';
88
     this.selectedFilter = 'phone';
83
     this.selectedKanaRow = 'all';
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
     this.dankaDisplay = this.filterDankaList.length;
95
     this.dankaDisplay = this.filterDankaList.length;
89
   }
96
   }
90
 
97
 
91
-  //検索処理
92
   searchDanka() {
98
   searchDanka() {
93
     this.selectedFilter = 'search';
99
     this.selectedFilter = 'search';
94
     this.selectedKanaRow = 'all';
100
     this.selectedKanaRow = 'all';
95
-    //検索欄に入力された値から余白を削除
101
+
96
     const keyword = this.searchKeyword.trim();
102
     const keyword = this.searchKeyword.trim();
97
-    //検索欄が空の場合は檀家の一覧を表示
103
+
98
     if (keyword === '') {
104
     if (keyword === '') {
99
-      this.filterDankaList = this.dankaList;
100
-      this.dankaDisplay = this.filterDankaList.length;
105
+      this.showAllDanka();
101
       return;
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
     this.dankaDisplay = this.filterDankaList.length;
119
     this.dankaDisplay = this.filterDankaList.length;
117
-    console.log(this.dankaDisplay);
118
   }
120
   }
119
 
121
 
120
   filterByKanaRow(row: KanaRowValue): void {
122
   filterByKanaRow(row: KanaRowValue): void {
123
     this.searchKeyword = '';
125
     this.searchKeyword = '';
124
 
126
 
125
     if (row === 'all') {
127
     if (row === 'all') {
126
-      this.filterDankaList = this.dankaList;
127
-      this.dankaDisplay = this.filterDankaList.length;
128
+      this.showAllDanka();
128
       return;
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
       return this.kanaRowMap[row].includes(firstKana);
134
       return this.kanaRowMap[row].includes(firstKana);
134
     });
135
     });
136
+
135
     this.dankaDisplay = this.filterDankaList.length;
137
     this.dankaDisplay = this.filterDankaList.length;
136
   }
138
   }
137
 
139
 
143
     this.dankaDisplay = 0;
145
     this.dankaDisplay = 0;
144
   }
146
   }
145
 
147
 
146
-  private getDankaSortText(danka: Danka): string {
148
+  private async getDankaSortText(danka: Danka): Promise<string> {
147
     const householderName = this.normalizeName(danka.householder);
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
     return (householderFamily?.furigana || danka.householder).trim();
155
     return (householderFamily?.furigana || danka.householder).trim();
153
   }
156
   }
155
   private normalizeName(name: string): string {
158
   private normalizeName(name: string): string {
156
     return name.replace(/\s/g, '');
159
     return name.replace(/\s/g, '');
157
   }
160
   }
158
-}
161
+}

+ 28
- 32
src/app/pages/dashboard/dashboard.html 查看文件

9
         <div>
9
         <div>
10
           <h1>ホーム</h1>
10
           <h1>ホーム</h1>
11
         </div>
11
         </div>
12
-        <div class="date-pill">2026年5月28日 木曜日</div>
12
+        <div class="date-pill">{{ todayLabel }}</div>
13
       </div>
13
       </div>
14
 
14
 
15
       <section class="overview" aria-label="概要">
15
       <section class="overview" aria-label="概要">
16
-        <a class="card primary" href="#">
16
+        <a class="card" href="#">
17
           <div class="card-label">今週の法要</div>
17
           <div class="card-label">今週の法要</div>
18
           <div class="metric"><strong>{{ weeklyMemorialCount }}</strong><span>件</span></div>
18
           <div class="metric"><strong>{{ weeklyMemorialCount }}</strong><span>件</span></div>
19
           <p class="card-text">
19
           <p class="card-text">
29
         <div class="search-card">
29
         <div class="search-card">
30
           <div class="search-head">
30
           <div class="search-head">
31
             <div class="search-label">まとめて検索</div>
31
             <div class="search-label">まとめて検索</div>
32
-            <div class="search-title">檀家・家族・故人を探す</div>
32
+            <div class="search-title"><!--檀家・-->家族・故人を探す</div>
33
           </div>
33
           </div>
34
           <input
34
           <input
35
             class="search-input"
35
             class="search-input"
36
             type="search"
36
             type="search"
37
+            [(ngModel)]="searchKeyword"
37
             placeholder="氏名、ふりがな、住所、戒名で検索"
38
             placeholder="氏名、ふりがな、住所、戒名で検索"
38
             aria-label="まとめて検索"
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
         </div>
43
         </div>
42
       </section>
44
       </section>
43
 
45
 
44
       <section class="section">
46
       <section class="section">
45
         <div class="section-head">
47
         <div class="section-head">
46
           <div>
48
           <div>
47
-            <h2>最近開いた檀家・世帯</h2>
49
+            <h2>最近開いた檀家</h2>
48
           </div>
50
           </div>
49
-          <a class="text-link" href="#">一覧へ</a>
50
         </div>
51
         </div>
51
 
52
 
52
         <div class="recent-table" role="table" aria-label="最近開いた檀家">
53
         <div class="recent-table" role="table" aria-label="最近開いた檀家">
53
           <div class="recent-row recent-row-head" role="row">
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
             <div class="cell" role="columnheader">住所</div>
56
             <div class="cell" role="columnheader">住所</div>
57
             <div class="cell" role="columnheader">次の法要</div>
57
             <div class="cell" role="columnheader">次の法要</div>
58
             <div class="cell" role="columnheader">最終更新</div>
58
             <div class="cell" role="columnheader">最終更新</div>
59
           </div>
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
         </div>
81
         </div>
85
       </section>
82
       </section>
86
 
83
 
89
           <div>
86
           <div>
90
             <h2>近日の法要・命日</h2>
87
             <h2>近日の法要・命日</h2>
91
           </div>
88
           </div>
92
-          <a class="text-link" href="#">年次法要一覧へ</a>
93
         </div>
89
         </div>
94
 
90
 
95
         <div class="upcoming-list">
91
         <div class="upcoming-list">

+ 8
- 12
src/app/pages/dashboard/dashboard.scss 查看文件

104
   background: #fff8ed;
104
   background: #fff8ed;
105
 }
105
 }
106
 
106
 
107
-.card.primary {
108
-  background: var(--focus);
109
-  border-color: var(--accent);
110
-}
111
-
112
 .card-label,
107
 .card-label,
113
 .search-label {
108
 .search-label {
114
   color: var(--muted);
109
   color: var(--muted);
203
   font-size: 14px;
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
 .recent-table {
201
 .recent-table {
213
   overflow: hidden;
202
   overflow: hidden;
214
   border: 2px solid var(--line);
203
   border: 2px solid var(--line);
218
 
207
 
219
 .recent-row {
208
 .recent-row {
220
   display: grid;
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
   min-height: 52px;
211
   min-height: 52px;
223
   border-top: 1px solid var(--line);
212
   border-top: 1px solid var(--line);
224
   align-items: center;
213
   align-items: center;
242
   color: var(--muted);
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
 .upcoming-list {
241
 .upcoming-list {
246
   display: grid;
242
   display: grid;
247
   gap: 10px;
243
   gap: 10px;

+ 128
- 20
src/app/pages/dashboard/dashboard.ts 查看文件

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
 import { KakochoService } from '../../services/kakocho-service';
3
 import { KakochoService } from '../../services/kakocho-service';
4
 import { DankaService } from '../../services/dankaService';
4
 import { DankaService } from '../../services/dankaService';
5
+import { Danka } from '../../models/danka';
5
 import { AppHeader } from '../../share/header/app-header';
6
 import { AppHeader } from '../../share/header/app-header';
6
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
7
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
8
+import { FormsModule } from '@angular/forms';
7
 
9
 
8
 interface UpcomingMemorial {
10
 interface UpcomingMemorial {
9
   id: string;
11
   id: string;
16
   status: '準備確認' | '要確認';
18
   status: '準備確認' | '要確認';
17
 }
19
 }
18
 
20
 
21
+interface RecentDanka {
22
+  danka: Danka;
23
+  nextMemorialLabel: string;
24
+  updatedAtLabel: string;
25
+}
26
+
19
 @Component({
27
 @Component({
20
   selector: 'app-dashboard',
28
   selector: 'app-dashboard',
21
-  imports: [AppHeader, AppSideMenu, RouterLink],
29
+  imports: [AppHeader, AppSideMenu, RouterLink, FormsModule],
22
   templateUrl: './dashboard.html',
30
   templateUrl: './dashboard.html',
23
   styleUrl: './dashboard.scss',
31
   styleUrl: './dashboard.scss',
24
 })
32
 })
25
 export class Dashboard {
33
 export class Dashboard {
34
+  searchKeyword = '';
35
+  todayLabel = this.formatTodayLabel(new Date());
26
   weeklyMemorialCount = 0;
36
   weeklyMemorialCount = 0;
27
   todayMemorialCount = 0;
37
   todayMemorialCount = 0;
28
   upcomingWeeklyMemorialCount = 0;
38
   upcomingWeeklyMemorialCount = 0;
29
   monthlyMemorialCount = 0;
39
   monthlyMemorialCount = 0;
40
+  recentDankaList: RecentDanka[] = [];
30
   upcomingMemorials: UpcomingMemorial[] = [];
41
   upcomingMemorials: UpcomingMemorial[] = [];
31
 
42
 
32
   private readonly targetYear = new Date().getFullYear();
43
   private readonly targetYear = new Date().getFullYear();
34
   constructor(
45
   constructor(
35
     private kakochoService: KakochoService,
46
     private kakochoService: KakochoService,
36
     private dankaService: DankaService,
47
     private dankaService: DankaService,
48
+    private router: Router,
49
+    private cdr: ChangeDetectorRef,
37
   ) {
50
   ) {
38
     this.setWeeklyMemorialSummary();
51
     this.setWeeklyMemorialSummary();
39
     this.setMonthlyMemorialSummary();
52
     this.setMonthlyMemorialSummary();
53
+    this.setRecentDankaList();
40
     this.setUpcomingMemorials();
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
     const today = this.toDateOnly(new Date());
79
     const today = this.toDateOnly(new Date());
45
     const weekEnd = this.addDays(this.getWeekStart(today), 6);
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
       const deathDate = this.parseDate(kakocho.deathDate);
83
       const deathDate = this.parseDate(kakocho.deathDate);
49
       if (!deathDate) {
84
       if (!deathDate) {
50
         return false;
85
         return false;
64
       return deathDate?.getMonth() === today.getMonth() && deathDate.getDate() === today.getDate();
99
       return deathDate?.getMonth() === today.getMonth() && deathDate.getDate() === today.getDate();
65
     }).length;
100
     }).length;
66
     this.upcomingWeeklyMemorialCount = this.weeklyMemorialCount - this.todayMemorialCount;
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
     const today = this.toDateOnly(new Date());
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
       const deathDate = this.parseDate(kakocho.deathDate);
109
       const deathDate = this.parseDate(kakocho.deathDate);
74
       if (!deathDate || !this.isMemorialTarget(deathDate)) {
110
       if (!deathDate || !this.isMemorialTarget(deathDate)) {
75
         return false;
111
         return false;
77
 
113
 
78
       return deathDate.getMonth() === today.getMonth();
114
       return deathDate.getMonth() === today.getMonth();
79
     }).length;
115
     }).length;
116
+    this.cdr.detectChanges();
80
   }
117
   }
81
 
118
 
82
-  private setUpcomingMemorials(): void {
119
+  private async setUpcomingMemorials(): Promise<void> {
83
     const today = this.toDateOnly(new Date());
120
     const today = this.toDateOnly(new Date());
84
     const endDate = this.addDays(today, 30);
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
         const deathDate = this.parseDate(kakocho.deathDate);
127
         const deathDate = this.parseDate(kakocho.deathDate);
90
-        if (!deathDate) {
91
-          return null;
92
-        }
128
+        if (!deathDate) return null;
93
 
129
 
94
         const eventDate = new Date(this.targetYear, deathDate.getMonth(), deathDate.getDate());
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
         const memorialType = this.getMemorialType(deathDate);
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
         const type = memorialType ? '年忌法要' : '命日';
137
         const type = memorialType ? '年忌法要' : '命日';
102
         const status = memorialType ? '準備確認' : '要確認';
138
         const status = memorialType ? '準備確認' : '要確認';
103
 
139
 
111
           type,
147
           type,
112
           status,
148
           status,
113
         };
149
         };
114
-      })
150
+      }),
151
+    );
152
+
153
+    this.upcomingMemorials = results
115
       .filter((memorial): memorial is UpcomingMemorial => memorial !== null)
154
       .filter((memorial): memorial is UpcomingMemorial => memorial !== null)
116
       .sort((a, b) => a.date.getTime() - b.date.getTime() || a.title.localeCompare(b.title, 'ja'))
155
       .sort((a, b) => a.date.getTime() - b.date.getTime() || a.title.localeCompare(b.title, 'ja'))
117
       .slice(0, 3);
156
       .slice(0, 3);
157
+    this.cdr.detectChanges();
118
   }
158
   }
119
 
159
 
120
   private isMemorialTarget(deathDate: Date): boolean {
160
   private isMemorialTarget(deathDate: Date): boolean {
150
     return `${date.getMonth() + 1}月${date.getDate()}日`;
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
   private getWeekStart(date: Date): Date {
245
   private getWeekStart(date: Date): Date {
154
     const day = date.getDay();
246
     const day = date.getDay();
155
     const diff = day === 0 ? -6 : 1 - day;
247
     const diff = day === 0 ? -6 : 1 - day;
166
     return new Date(date.getFullYear(), date.getMonth(), date.getDate());
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
     const [year, month, day] = value.split('-').map(Number);
278
     const [year, month, day] = value.split('-').map(Number);
171
     if (!year || !month || !day) {
279
     if (!year || !month || !day) {
172
       return null;
280
       return null;

+ 5
- 1
src/app/pages/event/event.html 查看文件

76
             <div>状態</div>
76
             <div>状態</div>
77
           </div>
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
             @for (target of filteredEventTargets; track target.id) {
84
             @for (target of filteredEventTargets; track target.id) {
81
               <div class="event-table-row">
85
               <div class="event-table-row">
82
                 <div>
86
                 <div>

+ 25
- 29
src/app/pages/event/event.scss 查看文件

7
 }
7
 }
8
 
8
 
9
 .event-page {
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
   background: #f4eee4;
14
   background: #f4eee4;
14
 }
15
 }
15
 
16
 
16
 .event-main {
17
 .event-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19
   box-sizing: border-box;
19
   box-sizing: border-box;
20
 }
20
 }
21
 
21
 
22
 .event-panel {
22
 .event-panel {
23
-  min-height: 650px;
24
-  padding: 26px 34px 36px;
23
+  min-height: 760px;
24
+  padding: 34px 42px 40px;
25
   background: #ffffff;
25
   background: #ffffff;
26
   border: 2px solid #d8caba;
26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28
   box-sizing: border-box;
28
   box-sizing: border-box;
29
 }
29
 }
30
 
30
 
45
 .page-title-row h1 {
45
 .page-title-row h1 {
46
   margin: 0;
46
   margin: 0;
47
   color: #2f2720;
47
   color: #2f2720;
48
-  font-size: 32px;
49
-  line-height: 1.2;
48
+  font-size: 34px;
49
+  line-height: 1.1;
50
   font-weight: 800;
50
   font-weight: 800;
51
-  letter-spacing: 0.02em;
52
 }
51
 }
53
 
52
 
54
 .filter-row {
53
 .filter-row {
74
 
73
 
75
 .filter-field select,
74
 .filter-field select,
76
 .search-field input {
75
 .search-field input {
77
-  height: 38px;
76
+  height: 46px;
78
   padding: 0 14px;
77
   padding: 0 14px;
79
   border: 2px solid #d8caba;
78
   border: 2px solid #d8caba;
80
   border-radius: 8px;
79
   border-radius: 8px;
116
 .list-header-row h2 {
115
 .list-header-row h2 {
117
   margin: 0;
116
   margin: 0;
118
   color: #2f2720;
117
   color: #2f2720;
119
-  font-size: 26px;
118
+  font-size: 22px;
120
   font-weight: 800;
119
   font-weight: 800;
121
 }
120
 }
122
 
121
 
147
 }
146
 }
148
 
147
 
149
 .event-table-header {
148
 .event-table-header {
150
-  min-height: 46px;
149
+  min-height: 40px;
151
   padding: 0 14px;
150
   padding: 0 14px;
152
   background: #efe4d6;
151
   background: #efe4d6;
153
   color: #111111;
152
   color: #111111;
157
 }
156
 }
158
 
157
 
159
 .event-table-row {
158
 .event-table-row {
160
-  min-height: 78px;
159
+  min-height: 62px;
161
   padding: 10px 14px;
160
   padding: 10px 14px;
162
   border-top: 1px solid #d8c2aa;
161
   border-top: 1px solid #d8c2aa;
163
   background: #fffdf9;
162
   background: #fffdf9;
234
 }
233
 }
235
 
234
 
236
 @media (max-width: 1100px) {
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
   .page-title-row {
247
   .page-title-row {
238
     flex-direction: column;
248
     flex-direction: column;
239
   }
249
   }
249
 }
259
 }
250
 
260
 
251
 @media (max-width: 800px) {
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
   .page-title-row h1 {
262
   .page-title-row h1 {
267
     font-size: 26px;
263
     font-size: 26px;
268
   }
264
   }

+ 88
- 29
src/app/pages/event/event.ts 查看文件

1
-import { Component } from '@angular/core';
1
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2
 import { FormsModule } from '@angular/forms';
2
 import { FormsModule } from '@angular/forms';
3
 import { DankaService } from '../../services/dankaService';
3
 import { DankaService } from '../../services/dankaService';
4
 import { FamilyService } from '../../services/family-service';
4
 import { FamilyService } from '../../services/family-service';
5
+import { EventService } from '../../services/event-service';
5
 import { EventStatus, EventTarget, EventType } from '../../models/event';
6
 import { EventStatus, EventTarget, EventType } from '../../models/event';
6
 import { AppHeader } from '../../share/header/app-header';
7
 import { AppHeader } from '../../share/header/app-header';
7
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
8
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
12
   templateUrl: './event.html',
13
   templateUrl: './event.html',
13
   styleUrl: './event.scss',
14
   styleUrl: './event.scss',
14
 })
15
 })
15
-export class EventPage {
16
+export class EventPage implements OnInit{
16
   eventTargets: EventTarget[] = [];
17
   eventTargets: EventTarget[] = [];
18
+  isLoading = true;
17
   targetYear: number = new Date().getFullYear();
19
   targetYear: number = new Date().getFullYear();
18
   selectedEventType: EventType | 'all' = 'all';
20
   selectedEventType: EventType | 'all' = 'all';
19
   selectedStatus: EventStatus | 'all' = 'all';
21
   selectedStatus: EventStatus | 'all' = 'all';
37
     { label: '未案内', value: '未案内' },
39
     { label: '未案内', value: '未案内' },
38
     { label: '案内済', value: '案内済' },
40
     { label: '案内済', value: '案内済' },
39
   ];
41
   ];
40
-  private statusByTargetId: Record<string, EventStatus> = {};
42
+  private eventRequestId = 0;
41
 
43
 
42
   constructor(
44
   constructor(
43
     private dankaService: DankaService,
45
     private dankaService: DankaService,
44
     private familyService: FamilyService,
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
       const birthDate = this.parseDate(family.birthDate);
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
       const eventTypes = this.getEventTypes(age);
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
         const id = `${family.id}-${eventType}`;
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
           id,
97
           id,
73
           dankaId: family.dankaId,
98
           dankaId: family.dankaId,
74
           name: family.name,
99
           name: family.name,
75
           furigana: family.furigana,
100
           furigana: family.furigana,
76
           householdName: danka?.householdName ?? '不明',
101
           householdName: danka?.householdName ?? '不明',
77
           relationship: family.relationship || '未登録',
102
           relationship: family.relationship || '未登録',
78
-          birthDate: family.birthDate,
103
+          birthDate: this.formatDateForValue(birthDate),
79
           age,
104
           age,
80
           eventType,
105
           eventType,
81
           note: family.note,
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
       (a, b) =>
117
       (a, b) =>
89
-        this.getEventSortOrder(a.eventType) - this.getEventSortOrder(b.eventType) ||
118
+        this.getEventSortOrder(a.eventType) -
119
+        this.getEventSortOrder(b.eventType) ||
90
         a.age - b.age ||
120
         a.age - b.age ||
91
         a.name.localeCompare(b.name, 'ja'),
121
         a.name.localeCompare(b.name, 'ja'),
92
     );
122
     );
123
+    this.isLoading = false;
124
+    this.cdr.detectChanges();
93
   }
125
   }
94
 
126
 
95
   changeEventType(eventType: EventType | 'all'): void {
127
   changeEventType(eventType: EventType | 'all'): void {
113
           target.eventType,
145
           target.eventType,
114
           target.note,
146
           target.note,
115
           target.status,
147
           target.status,
116
-        ].some((value) => value.includes(keyword));
148
+        ].some((value) => this.includesKeyword(value, keyword));
117
 
149
 
118
       return matchesStatus && matchesKeyword;
150
       return matchesStatus && matchesKeyword;
119
     });
151
     });
121
 
153
 
122
   changeStatus(target: EventTarget, status: EventStatus): void {
154
   changeStatus(target: EventTarget, status: EventStatus): void {
123
     target.status = status;
155
     target.status = status;
124
-    this.statusByTargetId[target.id] = status;
156
+    this.eventService.saveEventStatus(target.id, status);
125
   }
157
   }
126
 
158
 
127
   private getEventTypes(age: number): EventType[] {
159
   private getEventTypes(age: number): EventType[] {
147
     return ['稚児行列', '七五三', '成人式', '米寿'].indexOf(eventType);
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
     const [year, month, day] = value.split('-').map(Number);
199
     const [year, month, day] = value.split('-').map(Number);
152
     if (!year || !month || !day) {
200
     if (!year || !month || !day) {
153
       return null;
201
       return null;
154
     }
202
     }
155
     return new Date(year, month - 1, day);
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 查看文件

86
                 </div>
86
                 </div>
87
               </div>
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
                 <div class="form-field">
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
                 </div>
97
                 </div>
97
               </div>
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
               <div class="form-row">
109
               <div class="form-row">
100
                 <label for="fatherId">父</label>
110
                 <label for="fatherId">父</label>
101
                 <div class="form-field">
111
                 <div class="form-field">
167
 
177
 
168
             </div>
178
             </div>
169
           </section>
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
         </div>
180
         </div>
183
 
181
 
184
         <div class="bottom-actions">
182
         <div class="bottom-actions">

+ 85
- 106
src/app/pages/family-edit/family-edit.scss 查看文件

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

+ 72
- 24
src/app/pages/family-edit/family-edit.ts 查看文件

6
   ReactiveFormsModule,
6
   ReactiveFormsModule,
7
   Validators,
7
   Validators,
8
 } from '@angular/forms';
8
 } from '@angular/forms';
9
+import { OnInit } from '@angular/core';
9
 import { ActivatedRoute, Router, RouterLink } from '@angular/router';
10
 import { ActivatedRoute, Router, RouterLink } from '@angular/router';
10
 import { AppHeader } from '../../share/header/app-header';
11
 import { AppHeader } from '../../share/header/app-header';
11
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
12
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
13
+import { DankaService } from '../../services/dankaService';
12
 import { FamilyService } from '../../services/family-service';
14
 import { FamilyService } from '../../services/family-service';
13
 import { MarriageRelationService } from '../../services/marriage-relation-service';
15
 import { MarriageRelationService } from '../../services/marriage-relation-service';
14
 import { Danka } from '../../models/danka';
16
 import { Danka } from '../../models/danka';
21
   templateUrl: './family-edit.html',
23
   templateUrl: './family-edit.html',
22
   styleUrl: './family-edit.scss',
24
   styleUrl: './family-edit.scss',
23
 })
25
 })
24
-export class FamilyEdit {
26
+export class FamilyEdit implements OnInit {
25
   danka: Danka | undefined;
27
   danka: Danka | undefined;
26
   family: Family | undefined;
28
   family: Family | undefined;
27
   families: Family[] = [];
29
   families: Family[] = [];
30
   relationMode: string = '';
32
   relationMode: string = '';
31
   baseFamilyId: string = '';
33
   baseFamilyId: string = '';
32
   marriageErrorMessages: string[] = [];
34
   marriageErrorMessages: string[] = [];
35
+  private setHouseholderOnSave = false;
33
 
36
 
34
   constructor(
37
   constructor(
38
+    private dankaService: DankaService,
35
     private familyService: FamilyService,
39
     private familyService: FamilyService,
36
     private marriageRelationService: MarriageRelationService,
40
     private marriageRelationService: MarriageRelationService,
37
     private route: ActivatedRoute,
41
     private route: ActivatedRoute,
38
     private router: Router,
42
     private router: Router,
39
   ) {
43
   ) {
44
+  }
45
+
46
+  ngOnInit(): void {
47
+    this.init();
48
+  }
49
+
50
+  async init(): Promise<void> {
40
     this.dankaId = this.route.snapshot.params['dankaId'];
51
     this.dankaId = this.route.snapshot.params['dankaId'];
41
     this.familyId = this.route.snapshot.params['familyId'];
52
     this.familyId = this.route.snapshot.params['familyId'];
42
-    this.families = this.familyService.getFamiliesByDankaId(this.dankaId);
53
+
43
     this.relationMode = this.route.snapshot.queryParamMap.get('relationMode') ?? '';
54
     this.relationMode = this.route.snapshot.queryParamMap.get('relationMode') ?? '';
44
     this.baseFamilyId = this.route.snapshot.queryParamMap.get('baseFamilyId') ?? '';
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
     if (this.familyId) {
61
     if (this.familyId) {
46
-      this.family = this.familyService.getFamilyById(this.familyId);
62
+      this.family = await this.familyService.getFamilyById(this.familyId);
63
+
47
       if (this.family) {
64
       if (this.family) {
48
         this.familyForm.patchValue({
65
         this.familyForm.patchValue({
49
           furigana: this.family.furigana,
66
           furigana: this.family.furigana,
56
           spouseId: this.family.spouseId,
73
           spouseId: this.family.spouseId,
57
           gender: this.family.gender,
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
     }
86
     }
69
 
87
 
70
     if (!this.familyId && this.relationMode === 'child') {
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
       if (baseFamily?.gender === 'male') {
91
       if (baseFamily?.gender === 'male') {
74
         this.familyForm.patchValue({
92
         this.familyForm.patchValue({
117
     return this.families.filter((family) => family.id !== this.familyId);
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
     const relation =
141
     const relation =
123
       relations.find((marriageRelation) => marriageRelation.status === 'current') ?? relations[0];
142
       relations.find((marriageRelation) => marriageRelation.status === 'current') ?? relations[0];
124
 
143
 
125
-    if (!relation) {
126
-      return;
127
-    }
144
+    if (!relation) return;
128
 
145
 
129
     this.familyForm.patchValue({
146
     this.familyForm.patchValue({
130
       spouseId: relation.person1Id === familyId ? relation.person2Id : relation.person1Id,
147
       spouseId: relation.person1Id === familyId ? relation.person2Id : relation.person1Id,
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
     if (this.familyForm.invalid) {
167
     if (this.familyForm.invalid) {
147
       return;
168
       return;
148
     }
169
     }
169
     };
190
     };
170
 
191
 
171
     if (spouseId) {
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
         dankaId,
197
         dankaId,
176
         person1Id: familyId,
198
         person1Id: familyId,
177
         person2Id: spouseId,
199
         person2Id: spouseId,
186
         return;
208
         return;
187
       }
209
       }
188
     } else {
210
     } else {
189
-      const currentMarriage = this.marriageRelationService.getCurrentMarriageByFamilyId(familyId);
211
+      const currentMarriage = await this.marriageRelationService.getCurrentMarriageByFamilyId(familyId);
190
       if (currentMarriage) {
212
       if (currentMarriage) {
191
         this.marriageRelationService.deleteMarriageRelation(currentMarriage.id);
213
         this.marriageRelationService.deleteMarriageRelation(currentMarriage.id);
192
       }
214
       }
193
     }
215
     }
194
 
216
 
195
     this.familyService.saveFamily(updatedFamily);
217
     this.familyService.saveFamily(updatedFamily);
218
+    this.saveHouseholderIfSelected(updatedFamily);
196
     this.router.navigate(['/danka-detail', dankaId], { queryParams: { tab: 'family' } });
219
     this.router.navigate(['/danka-detail', dankaId], { queryParams: { tab: 'family' } });
197
   }
220
   }
198
 
221
 
210
     this.router.navigate(['/danka-detail', dankaId], { queryParams: { tab: 'family' } });
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 查看文件

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

+ 68
- 192
src/app/pages/kakocho-edit/kakocho-edit.scss 查看文件

2
   position: relative;
2
   position: relative;
3
   display: block;
3
   display: block;
4
   min-height: 100vh;
4
   min-height: 100vh;
5
-  background: #f4eee4;
5
+  background: #f6f0e7;
6
   color: #2f2720;
6
   color: #2f2720;
7
 }
7
 }
8
 
8
 
9
 .danka-edit-page {
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
 .danka-edit-main {
17
 .danka-edit-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19
   box-sizing: border-box;
19
   box-sizing: border-box;
20
 }
20
 }
21
 
21
 
22
 .edit-panel {
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
   border: 2px solid #d8caba;
26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28
   box-sizing: border-box;
28
   box-sizing: border-box;
29
 }
29
 }
30
 
30
 
31
 .page-title-area {
31
 .page-title-area {
32
-  margin-bottom: 8px;
32
+  margin-bottom: 22px;
33
 }
33
 }
34
 
34
 
35
 .page-title-area h1 {
35
 .page-title-area h1 {
36
   margin: 0;
36
   margin: 0;
37
   color: #2f2720;
37
   color: #2f2720;
38
-  font-size: 32px;
39
-  line-height: 1.2;
38
+  font-size: 34px;
39
+  line-height: 1.1;
40
   font-weight: 800;
40
   font-weight: 800;
41
-  letter-spacing: 0.02em;
42
 }
41
 }
43
 
42
 
44
 .edit-form {
43
 .edit-form {
49
 }
48
 }
50
 
49
 
51
 .edit-form input,
50
 .edit-form input,
51
+.edit-form textarea,
52
 .edit-form button {
52
 .edit-form button {
53
   font-family: inherit;
53
   font-family: inherit;
54
 }
54
 }
55
 
55
 
56
 .edit-content {
56
 .edit-content {
57
   display: grid;
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
   align-items: start;
60
   align-items: start;
61
 }
61
 }
62
 
62
 
63
 .basic-edit-section,
63
 .basic-edit-section,
64
 .phone-edit-section {
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
 .basic-edit-section h2,
73
 .basic-edit-section h2,
69
 .phone-edit-section h2,
74
 .phone-edit-section h2,
70
 .support-box h2 {
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
   color: #2f2720;
80
   color: #2f2720;
73
   font-size: 22px;
81
   font-size: 22px;
82
+  line-height: 1.3;
74
   font-weight: 800;
83
   font-weight: 800;
75
 }
84
 }
76
 
85
 
77
 .section-heading p {
86
 .section-heading p {
78
-  margin: 6px 0 14px;
87
+  margin: 4px 0 12px;
79
   color: #7b6b5c;
88
   color: #7b6b5c;
80
-  font-size: 15px;
89
+  font-size: 14px;
81
 }
90
 }
82
 
91
 
83
-/* 基本情報 */
92
+/* 蝓コ譛ャ諠・ア */
84
 .form-list {
93
 .form-list {
85
-  margin-top: 14px;
94
+  width: min(100%, 980px);
95
+  margin-top: 0;
86
 }
96
 }
87
 
97
 
88
 .form-field {
98
 .form-field {
93
 
103
 
94
 .form-row {
104
 .form-row {
95
   display: grid;
105
   display: grid;
96
-  grid-template-columns: 120px 1fr;
106
+  grid-template-columns: 140px 1fr;
97
   align-items: center;
107
   align-items: center;
98
-  gap: 16px;
99
-  margin-bottom: 14px;
108
+  gap: 14px;
109
+  margin-bottom: 12px;
100
 }
110
 }
101
 
111
 
102
 .form-row label {
112
 .form-row label {
103
   color: #4b3c31;
113
   color: #4b3c31;
104
-  font-size: 17px;
114
+  font-size: 15px;
105
   font-weight: 800;
115
   font-weight: 800;
106
 }
116
 }
107
 
117
 
108
 .form-row input,
118
 .form-row input,
109
 .form-row textarea {
119
 .form-row textarea {
110
   width: 100%;
120
   width: 100%;
111
-  padding: 0 14px;
112
   border: 2px solid #d8caba;
121
   border: 2px solid #d8caba;
113
   border-radius: 8px;
122
   border-radius: 8px;
114
   background: #fffdf9;
123
   background: #fffdf9;
115
   color: #2f2720;
124
   color: #2f2720;
116
-  font-size: 18px;
125
+  font-size: 16px;
117
   font-weight: 600;
126
   font-weight: 600;
118
   box-sizing: border-box;
127
   box-sizing: border-box;
119
   outline: none;
128
   outline: none;
120
 }
129
 }
121
 
130
 
122
 .form-row input {
131
 .form-row input {
123
-  height: 54px;
132
+  height: 46px;
133
+  padding: 0 14px;
124
 }
134
 }
125
 
135
 
126
 .form-row textarea {
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
   resize: vertical;
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
 .bottom-actions {
158
 .bottom-actions {
269
   display: flex;
159
   display: flex;
270
   justify-content: flex-end;
160
   justify-content: flex-end;
271
   align-items: center;
161
   align-items: center;
272
   gap: 12px;
162
   gap: 12px;
273
-  margin-top: 22px;
163
+  margin-top: 26px;
274
 }
164
 }
275
 
165
 
276
 .delete-button,
166
 .delete-button,
277
 .cancel-button,
167
 .cancel-button,
278
 .save-button {
168
 .save-button {
279
   min-width: 116px;
169
   min-width: 116px;
280
-  height: 52px;
170
+  height: 46px;
281
   border: 2px solid #d8caba;
171
   border: 2px solid #d8caba;
282
   border-radius: 8px;
172
   border-radius: 8px;
283
-  font-size: 17px;
173
+  font-size: 16px;
284
   font-weight: 800;
174
   font-weight: 800;
285
   cursor: pointer;
175
   cursor: pointer;
286
   box-sizing: border-box;
176
   box-sizing: border-box;
307
   background: #765639;
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
 .error-message {
200
 .error-message {
323
   margin: 6px 0 0;
201
   margin: 6px 0 0;
324
   color: #b33a2f;
202
   color: #b33a2f;
336
   opacity: 0.7;
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 查看文件

1
-import { Component } from '@angular/core';
1
+import { Component, OnInit } from '@angular/core';
2
 import {
2
 import {
3
   FormBuilder,
3
   FormBuilder,
4
   FormGroup,
4
   FormGroup,
16
 
16
 
17
 import { DankaService } from '../../services/dankaService';
17
 import { DankaService } from '../../services/dankaService';
18
 import { KakochoService } from '../../services/kakocho-service';
18
 import { KakochoService } from '../../services/kakocho-service';
19
+import { FamilyService } from '../../services/family-service';
19
 
20
 
20
 import { Danka } from '../../models/danka';
21
 import { Danka } from '../../models/danka';
21
 import { Kakocho } from '../../models/kakocho';
22
 import { Kakocho } from '../../models/kakocho';
23
+import { Family } from '../../models/family';
22
 
24
 
23
 @Component({
25
 @Component({
24
   selector: 'app-kakocho-edit',
26
   selector: 'app-kakocho-edit',
30
   templateUrl: './kakocho-edit.html',
32
   templateUrl: './kakocho-edit.html',
31
   styleUrl: './kakocho-edit.scss',
33
   styleUrl: './kakocho-edit.scss',
32
 })
34
 })
33
-export class KakochoEdit {
35
+export class KakochoEdit implements OnInit {
34
   danka?: Danka;
36
   danka?: Danka;
35
   kakocho?: Kakocho;
37
   kakocho?: Kakocho;
38
+  sourceFamily?: Family;
36
   kakochoForm: FormGroup;
39
   kakochoForm: FormGroup;
37
-  dankaId: string;
40
+  dankaId: string | undefined;
41
+  returnTab: 'family' | 'kakocho' = 'kakocho';
38
 
42
 
39
   constructor(
43
   constructor(
40
     private fb: FormBuilder,
44
     private fb: FormBuilder,
41
     private dankaService: DankaService,
45
     private dankaService: DankaService,
42
     private kakochoService: KakochoService,
46
     private kakochoService: KakochoService,
47
+    private familyService: FamilyService,
43
     private route: ActivatedRoute,
48
     private route: ActivatedRoute,
44
     private router: Router,
49
     private router: Router,
45
   ) {
50
   ) {
46
-
47
-    // フォーム初期化
48
     this.kakochoForm = this.fb.group({
51
     this.kakochoForm = this.fb.group({
49
       name: ['', Validators.required],
52
       name: ['', Validators.required],
50
       furigana: [''],
53
       furigana: [''],
54
       ageAtDeath: [''],
57
       ageAtDeath: [''],
55
       note: [''],
58
       note: [''],
56
     });
59
     });
60
+  }
61
+
62
+  ngOnInit(): void {
63
+    this.init();
64
+  }
57
 
65
 
58
-    // 檀家ID
66
+  async init(): Promise<void> {
59
     const dankaId = this.route.snapshot.params['dankaId'];
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
     if (dankaId) {
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
     if (kakochoId) {
83
     if (kakochoId) {
73
-
74
-      this.kakocho =
75
-        this.kakochoService.getKakochoById(kakochoId);
84
+      this.kakocho = await this.kakochoService.getKakochoById(kakochoId);
76
 
85
 
77
       if (this.kakocho) {
86
       if (this.kakocho) {
78
-
79
         this.kakochoForm.patchValue({
87
         this.kakochoForm.patchValue({
80
           name: this.kakocho.name,
88
           name: this.kakocho.name,
81
           furigana: this.kakocho.furigana,
89
           furigana: this.kakocho.furigana,
85
           ageAtDeath: this.kakocho.ageAtDeath,
93
           ageAtDeath: this.kakocho.ageAtDeath,
86
           note: this.kakocho.note,
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
     // form値取得
111
     // form値取得
96
     const formValue = this.kakochoForm.value;
112
     const formValue = this.kakochoForm.value;
103
         ...formValue,
119
         ...formValue,
104
       };
120
       };
105
 
121
 
106
-      this.kakochoService.updateKakocho(
122
+      await this.kakochoService.updateKakocho(
107
         updatedKakocho
123
         updatedKakocho
108
       );
124
       );
109
 
125
 
115
 
131
 
116
         dankaId: this.danka?.id ?? '',
132
         dankaId: this.danka?.id ?? '',
117
 
133
 
118
-        familyId: '',
134
+        familyId: this.sourceFamily?.id ?? '',
119
 
135
 
120
         name: formValue.name,
136
         name: formValue.name,
121
         furigana: formValue.furigana,
137
         furigana: formValue.furigana,
129
       this.kakochoService.addKakocho(
145
       this.kakochoService.addKakocho(
130
         newKakocho
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
   }
169
   }
150
 
170
 
151
   cancelKakochoEdit() {
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 查看文件

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

+ 25
- 29
src/app/pages/memorial-list/memorial-list.scss 查看文件

7
 }
7
 }
8
 
8
 
9
 .memorial-list-page {
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
   background: #f4eee4;
14
   background: #f4eee4;
14
 }
15
 }
15
 
16
 
16
 .memorial-list-main {
17
 .memorial-list-main {
17
-  flex: 1;
18
-  padding-right: 34px;
18
+  min-width: 0;
19
   box-sizing: border-box;
19
   box-sizing: border-box;
20
 }
20
 }
21
 
21
 
22
 .memorial-panel {
22
 .memorial-panel {
23
-  min-height: 650px;
24
-  padding: 26px 34px 36px;
23
+  min-height: 760px;
24
+  padding: 34px 42px 40px;
25
   background: #ffffff;
25
   background: #ffffff;
26
   border: 2px solid #d8caba;
26
   border: 2px solid #d8caba;
27
-  border-radius: 76px;
27
+  border-radius: 64px;
28
   box-sizing: border-box;
28
   box-sizing: border-box;
29
 }
29
 }
30
 
30
 
45
 .page-title-row h1 {
45
 .page-title-row h1 {
46
   margin: 0;
46
   margin: 0;
47
   color: #2f2720;
47
   color: #2f2720;
48
-  font-size: 32px;
49
-  line-height: 1.2;
48
+  font-size: 34px;
49
+  line-height: 1.1;
50
   font-weight: 800;
50
   font-weight: 800;
51
-  letter-spacing: 0.02em;
52
 }
51
 }
53
 
52
 
54
 .filter-row {
53
 .filter-row {
74
 
73
 
75
 .filter-field select,
74
 .filter-field select,
76
 .search-field input {
75
 .search-field input {
77
-  height: 38px;
76
+  height: 46px;
78
   padding: 0 14px;
77
   padding: 0 14px;
79
   border: 2px solid #d8caba;
78
   border: 2px solid #d8caba;
80
   border-radius: 8px;
79
   border-radius: 8px;
120
 .list-header-row h2 {
119
 .list-header-row h2 {
121
   margin: 0;
120
   margin: 0;
122
   color: #2f2720;
121
   color: #2f2720;
123
-  font-size: 26px;
122
+  font-size: 22px;
124
   font-weight: 800;
123
   font-weight: 800;
125
 }
124
 }
126
 
125
 
151
 }
150
 }
152
 
151
 
153
 .memorial-table-header {
152
 .memorial-table-header {
154
-  min-height: 46px;
153
+  min-height: 40px;
155
   padding: 0 14px;
154
   padding: 0 14px;
156
   background: #efe4d6;
155
   background: #efe4d6;
157
   color: #111111;
156
   color: #111111;
161
 }
160
 }
162
 
161
 
163
 .memorial-table-row {
162
 .memorial-table-row {
164
-  min-height: 78px;
163
+  min-height: 62px;
165
   padding: 10px 14px;
164
   padding: 10px 14px;
166
   border-top: 1px solid #d8c2aa;
165
   border-top: 1px solid #d8c2aa;
167
   background: #fffdf9;
166
   background: #fffdf9;
223
 }
222
 }
224
 
223
 
225
 @media (max-width: 1100px) {
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
   .page-title-row {
236
   .page-title-row {
227
     flex-direction: column;
237
     flex-direction: column;
228
   }
238
   }
238
 }
248
 }
239
 
249
 
240
 @media (max-width: 800px) {
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
   .page-title-row h1 {
251
   .page-title-row h1 {
256
     font-size: 26px;
252
     font-size: 26px;
257
   }
253
   }

+ 100
- 24
src/app/pages/memorial-list/memorial-list.ts 查看文件

1
-import { Component } from '@angular/core';
1
+import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2
 import { ActivatedRoute, Router, RouterLink } from '@angular/router';
2
 import { ActivatedRoute, Router, RouterLink } from '@angular/router';
3
 import { Memorial } from '../../models/memorial';
3
 import { Memorial } from '../../models/memorial';
4
 import { DankaService } from '../../services/dankaService';
4
 import { DankaService } from '../../services/dankaService';
13
   templateUrl: './memorial-list.html',
13
   templateUrl: './memorial-list.html',
14
   styleUrl: './memorial-list.scss',
14
   styleUrl: './memorial-list.scss',
15
 })
15
 })
16
-export class MemorialList {
16
+export class MemorialList implements OnInit{
17
   memorialList: Memorial[] = [];
17
   memorialList: Memorial[] = [];
18
+  isLoading = true;
18
   targetYear: number = new Date().getFullYear();
19
   targetYear: number = new Date().getFullYear();
19
   selectedMemorialType = 'all';
20
   selectedMemorialType = 'all';
20
   searchKeyword = '';
21
   searchKeyword = '';
22
+  private memorialRequestId = 0;
21
   yearOptions: number[] = [
23
   yearOptions: number[] = [
22
     this.targetYear - 1,
24
     this.targetYear - 1,
23
     this.targetYear,
25
     this.targetYear,
45
   constructor(
47
   constructor(
46
     private dankaService: DankaService,
48
     private dankaService: DankaService,
47
     private kakochoService: KakochoService,
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
       const memorialType = this.getMemorialType(yearDiff);
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
       const memorialTarget: Memorial = {
94
       const memorialTarget: Memorial = {
67
         id: kakocho.id,
95
         id: kakocho.id,
68
         dankaId: kakocho.dankaId,
96
         dankaId: kakocho.dankaId,
69
         name: kakocho.name,
97
         name: kakocho.name,
98
+        furigana: kakocho.furigana,
70
         kaimyo: kakocho.kaimyo,
99
         kaimyo: kakocho.kaimyo,
71
         relationship: kakocho.relationship,
100
         relationship: kakocho.relationship,
72
         householdName: danka?.householdName ?? '不明',
101
         householdName: danka?.householdName ?? '不明',
73
-        deathDate: kakocho.deathDate,
74
-        memorialType: memorialType,
102
+        deathDate: this.formatDateForValue(deathDate),
103
+        memorialType,
75
         note: kakocho.note,
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
       const deathDateA = new Date(a.deathDate).getTime();
115
       const deathDateA = new Date(a.deathDate).getTime();
81
       const deathDateB = new Date(b.deathDate).getTime();
116
       const deathDateB = new Date(b.deathDate).getTime();
117
+
82
       if (deathDateA !== deathDateB) {
118
       if (deathDateA !== deathDateB) {
83
         return deathDateA - deathDateB;
119
         return deathDateA - deathDateB;
84
       }
120
       }
121
+
85
       return a.name.localeCompare(b.name, 'ja');
122
       return a.name.localeCompare(b.name, 'ja');
86
     });
123
     });
124
+    this.isLoading = false;
125
+    this.cdr.detectChanges();
87
   }
126
   }
88
 
127
 
89
   changeMemorialType(memorialType: string): void {
128
   changeMemorialType(memorialType: string): void {
101
       [
140
       [
102
         memorial.kaimyo,
141
         memorial.kaimyo,
103
         memorial.name,
142
         memorial.name,
143
+        memorial.furigana,
104
         memorial.deathDate,
144
         memorial.deathDate,
105
         memorial.relationship,
145
         memorial.relationship,
106
         memorial.householdName,
146
         memorial.householdName,
107
         memorial.memorialType,
147
         memorial.memorialType,
108
         memorial.note,
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
   formatDeathDate(deathDate: string): string {
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
       return '未登録';
160
       return '未登録';
117
     }
161
     }
118
 
162
 
119
-    return `${month}月${day}日`;
163
+    return `${date.getMonth() + 1}月${date.getDate()}日`;
120
   }
164
   }
121
 
165
 
122
   getMemorialType(yearDiff: number) {
166
   getMemorialType(yearDiff: number) {
153
         return '';
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 查看文件

13
         <div class="search-input-row">
13
         <div class="search-input-row">
14
           <div class="search-input-box">
14
           <div class="search-input-box">
15
             <span class="search-icon">⌕</span>
15
             <span class="search-icon">⌕</span>
16
-            <input type="text" [(ngModel)]="searchKeyword"
16
+            <input type="text" [ngModel]="searchKeyword"
17
+                   (ngModelChange)="onSearchKeywordChange($event)"
17
                    placeholder="三回忌 / 〇〇歳 / 2024 /"
18
                    placeholder="三回忌 / 〇〇歳 / 2024 /"
18
-                   (keydown.enter)="searchAll()"/>
19
+                   (keydown.enter)="submitSearch()"/>
19
           </div>
20
           </div>
20
-          <button type="button" class="search-button" (click)="searchAll()">
21
+          <button type="button" class="search-button" (click)="submitSearch()">
21
             検索
22
             検索
22
           </button>
23
           </button>
23
           <button type="button" class="filter-button-main" (click)="clearSearch()">
24
           <button type="button" class="filter-button-main" (click)="clearSearch()">
58
                   <div>
59
                   <div>
59
                     住所: {{ danka.address }}
60
                     住所: {{ danka.address }}
60
                   </div>
61
                   </div>
61
-                  <div class="result-link-text">
62
-                    開く
63
-                  </div>
64
                 </a>
62
                 </a>
65
               }
63
               }
66
             </section>
64
             </section>

+ 61
- 62
src/app/pages/search/search.scss 查看文件

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

+ 126
- 46
src/app/pages/search/search.ts 查看文件

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
 import { Danka } from '../../models/danka';
4
 import { Danka } from '../../models/danka';
5
 import { Family } from '../../models/family';
5
 import { Family } from '../../models/family';
6
 import { Kakocho } from '../../models/kakocho';
6
 import { Kakocho } from '../../models/kakocho';
16
   templateUrl: './search.html',
16
   templateUrl: './search.html',
17
   styleUrl: './search.scss',
17
   styleUrl: './search.scss',
18
 })
18
 })
19
-export class Search {
19
+export class Search implements OnInit{
20
   searchKeyword = '';
20
   searchKeyword = '';
21
   selectedSearchType = 'all';
21
   selectedSearchType = 'all';
22
   searchTypeFilters = [
22
   searchTypeFilters = [
29
   familyResults: Family[] = [];
29
   familyResults: Family[] = [];
30
   kakochoResults: Kakocho[] = [];
30
   kakochoResults: Kakocho[] = [];
31
   totalResultCount = 0;
31
   totalResultCount = 0;
32
+  private searchTimer?: ReturnType<typeof setTimeout>;
33
+  private searchRequestId = 0;
32
 
34
 
33
   constructor(
35
   constructor(
34
     private dankaService: DankaService,
36
     private dankaService: DankaService,
35
     private familyService: FamilyService,
37
     private familyService: FamilyService,
36
     private kakochoService: KakochoService,
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
   changeSearchType(searchType: string): void {
53
   changeSearchType(searchType: string): void {
41
     this.selectedSearchType = searchType;
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
     this.searchAll();
75
     this.searchAll();
43
   }
76
   }
44
 
77
 
45
   // 全検索の処理
78
   // 全検索の処理
46
-  searchAll() {
79
+  async searchAll(): Promise<void> {
80
+    const requestId = ++this.searchRequestId;
47
     const keyword = this.searchKeyword.trim();
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
     if (keyword === '') {
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
       return;
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
   clearSearch(): void {
145
   clearSearch(): void {
146
+    if (this.searchTimer) {
147
+      clearTimeout(this.searchTimer);
148
+    }
149
+
150
+    this.searchRequestId++;
100
     this.searchKeyword = '';
151
     this.searchKeyword = '';
101
     this.selectedSearchType = 'all';
152
     this.selectedSearchType = 'all';
102
     this.dankaResults = [];
153
     this.dankaResults = [];
103
     this.familyResults = [];
154
     this.familyResults = [];
104
     this.kakochoResults = [];
155
     this.kakochoResults = [];
105
     this.totalResultCount = 0;
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 查看文件

1
 import { Injectable } from '@angular/core';
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
 import { Danka } from '../models/danka';
13
 import { Danka } from '../models/danka';
3
 
14
 
4
 @Injectable({
15
 @Injectable({
5
   providedIn: 'root',
16
   providedIn: 'root',
6
 })
17
 })
7
 export class DankaService {
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 查看文件

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 查看文件

1
 import { Injectable } from '@angular/core';
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
 import { Family } from '../models/family';
14
 import { Family } from '../models/family';
3
 
15
 
4
 @Injectable({
16
 @Injectable({
5
   providedIn: 'root',
17
   providedIn: 'root',
6
 })
18
 })
7
 export class FamilyService {
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 查看文件

1
 import { Injectable } from '@angular/core';
1
 import { Injectable } from '@angular/core';
2
-
3
 import { Family } from '../models/family';
2
 import { Family } from '../models/family';
4
 import { MarriageRelation } from '../models/marriage-relation';
3
 import { MarriageRelation } from '../models/marriage-relation';
4
+import { Kakocho } from '../models/kakocho';
5
 import { FamilyUnit } from '../models/family-unit';
5
 import { FamilyUnit } from '../models/family-unit';
6
 import { FamilyUnitNode } from '../models/family-unit-node';
6
 import { FamilyUnitNode } from '../models/family-unit-node';
7
 
7
 
8
+
8
 export interface FamilyTreeNode {
9
 export interface FamilyTreeNode {
9
   family: Family;
10
   family: Family;
10
-
11
-  parents: FamilyTreeNode[];
12
-
13
-  children: FamilyTreeNode[];
11
+  kakocho?: Kakocho;
12
+  isDeceased: boolean;
14
 
13
 
15
   spouses: FamilyTreeNode[];
14
   spouses: FamilyTreeNode[];
15
+  children: FamilyTreeNode[];
16
+  parents: FamilyTreeNode[];
16
 }
17
 }
17
 
18
 
18
 @Injectable({
19
 @Injectable({
23
   build(
24
   build(
24
     families: Family[],
25
     families: Family[],
25
     marriages: MarriageRelation[],
26
     marriages: MarriageRelation[],
27
+    kakocholist: Kakocho[]
26
   ): FamilyTreeNode[] {
28
   ): FamilyTreeNode[] {
27
 
29
 
28
     const nodeMap = new Map<string, FamilyTreeNode>();
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
     families.forEach((family) => {
39
     families.forEach((family) => {
34
 
40
 
41
+      const kakocho = kakochoMap.get(family.id);
42
+
35
       nodeMap.set(family.id, {
43
       nodeMap.set(family.id, {
36
         family,
44
         family,
45
+        kakocho,
46
+        isDeceased: !!kakocho,
47
+
37
         parents: [],
48
         parents: [],
38
         children: [],
49
         children: [],
39
         spouses: [],
50
         spouses: [],
41
 
52
 
42
     });
53
     });
43
 
54
 
44
-    //
45
-    // 親子関係生成
46
-    //
55
+    // -----------------------------
56
+    // 親子関係
57
+    // -----------------------------
47
     families.forEach((family) => {
58
     families.forEach((family) => {
48
 
59
 
49
       const childNode = nodeMap.get(family.id);
60
       const childNode = nodeMap.get(family.id);
61
+      if (!childNode) return;
50
 
62
 
51
-      if (!childNode) {
52
-        return;
53
-      }
54
-
55
-      //
56
-      // 父
57
-      //
58
       if (family.fatherId) {
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
       if (family.motherId) {
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
     marriages
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
     return [...nodeMap.values()];
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
     return nodes.filter(node => {
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
       return true;
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
   buildFamilyUnits(
138
   buildFamilyUnits(
238
 
146
 
239
     for (const node of nodes) {
147
     for (const node of nodes) {
240
 
148
 
241
-      //
242
-      // 夫婦あり
243
-      //
244
       if (node.spouses.length > 0) {
149
       if (node.spouses.length > 0) {
245
 
150
 
246
         const spouse =
151
         const spouse =
262
 
167
 
263
         processed.add(key);
168
         processed.add(key);
264
 
169
 
265
-        //
266
-        // この夫婦の子供を取得
267
-        //
268
         const children =
170
         const children =
269
           nodes
171
           nodes
270
             .filter(child => {
172
             .filter(child => {
310
 
212
 
311
       } else {
213
       } else {
312
 
214
 
313
-        //
314
-        // 独身者
315
-        //
316
         units.push({
215
         units.push({
317
 
216
 
318
           id:
217
           id:
346
     const nodeMap =
245
     const nodeMap =
347
       new Map<string, FamilyUnitNode>();
246
       new Map<string, FamilyUnitNode>();
348
 
247
 
349
-    //
350
-    // ノード生成
351
-    //
352
     units.forEach(unit => {
248
     units.forEach(unit => {
353
 
249
 
354
       nodeMap.set(unit.id, {
250
       nodeMap.set(unit.id, {
363
 
259
 
364
     });
260
     });
365
 
261
 
366
-    //
367
-    // 親子リンク
368
-    //
369
     units.forEach(parentUnit => {
262
     units.forEach(parentUnit => {
370
 
263
 
371
       parentUnit.children.forEach(child => {
264
       parentUnit.children.forEach(child => {
417
     );
310
     );
418
 
311
 
419
   }
312
   }
420
-
421
 }
313
 }

+ 62
- 59
src/app/services/kakocho-service.ts 查看文件

1
 import { Injectable } from '@angular/core';
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
 import { Kakocho } from '../models/kakocho';
14
 import { Kakocho } from '../models/kakocho';
3
 
15
 
4
 @Injectable({
16
 @Injectable({
5
   providedIn: 'root',
17
   providedIn: 'root',
6
 })
18
 })
7
 export class KakochoService {
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
   // 1件取得
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 查看文件

1
 import { Injectable } from '@angular/core';
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
 import { MarriageRelation } from '../models/marriage-relation';
14
 import { MarriageRelation } from '../models/marriage-relation';
3
 
15
 
4
 @Injectable({
16
 @Injectable({
5
   providedIn: 'root',
17
   providedIn: 'root',
6
 })
18
 })
7
 export class MarriageRelationService {
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
   validateMarriageRelation(data: MarriageRelation): string[] {
85
   validateMarriageRelation(data: MarriageRelation): string[] {
62
     const errors: string[] = [];
86
     const errors: string[] = [];
63
 
87
 
66
     }
90
     }
67
 
91
 
68
     if (data.person1Id === data.person2Id) {
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
     return errors;
99
     return errors;
102
   }
100
   }
103
 
101
 
104
-  saveMarriageRelation(data: MarriageRelation): string[] {
102
+  async saveMarriageRelation(data: MarriageRelation): Promise<string[]> {
105
     const errors = this.validateMarriageRelation(data);
103
     const errors = this.validateMarriageRelation(data);
106
 
104
 
107
     if (errors.length > 0) {
105
     if (errors.length > 0) {
108
       return errors;
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
     return [];
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 查看文件

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

+ 12
- 12
src/app/share/side-menu/app-side-menu.html 查看文件

3
     <p class="menu-title">メニュー</p>
3
     <p class="menu-title">メニュー</p>
4
 
4
 
5
     <nav class="menu-list">
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
       </a>
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
       </a>
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
       </a>
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
       </a>
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
       </a>
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
       </a>
32
       </a>
29
 
33
 
30
-<!--  ユーザー数が増えた際に実装    -->
31
-<!--      <a routerLink="/user-setting" routerLinkActive="active" class="menu-button">-->
32
-<!--        利用者設定-->
33
-<!--      </a>-->
34
     </nav>
34
     </nav>
35
   </div>
35
   </div>
36
-</aside>
36
+</aside>

+ 2
- 1
src/app/share/side-menu/app-side-menu.scss 查看文件

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

+ 24
- 2
src/app/share/side-menu/app-side-menu.ts 查看文件

1
 import { Component } from '@angular/core';
1
 import { Component } from '@angular/core';
2
-import { RouterLink, RouterLinkActive } from '@angular/router';
2
+import { Router, RouterLink, RouterLinkActive } from '@angular/router';
3
 
3
 
4
 @Component({
4
 @Component({
5
   selector: 'app-side-menu',
5
   selector: 'app-side-menu',
6
+  standalone: true,
6
   imports: [RouterLink, RouterLinkActive],
7
   imports: [RouterLink, RouterLinkActive],
7
   templateUrl: './app-side-menu.html',
8
   templateUrl: './app-side-menu.html',
8
   styleUrl: './app-side-menu.scss',
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
+}

Loading…
取消
儲存