16 次程式碼提交

作者 SHA1 備註 提交日期
  poohr 35148cd224 [add] 3 週之前
  poohr 5b03d4549b [add] 3 週之前
  poohr 31189db006 [add] 3 週之前
  poohr 74b00088d0 [add] 3 週之前
  poohr 744493a34d [update] 3 週之前
  poohr 8de47bc716 [update] 3 週之前
  poohr f58e713e7c [update] 3 週之前
  poohr 20c440e5e3 [add] 3 週之前
  poohr ebf476a8b9 [update] 3 週之前
  poohr 14e0c18901 [add] 3 週之前
  poohr c102b31eb9 [update] 3 週之前
  poohr 800776fc24 [add] 3 週之前
  poohr b6e4d3c313 [add] 3 週之前
  poohr 00235056a4 [add] 3 週之前
  poohr 590cdfb382 [add] 3 週之前
  poohr 480c5b836e [add] 3 週之前

+ 9
- 0
src/app/app.routes.ts 查看文件

@@ -7,12 +7,17 @@ import { FamilyEdit } from './pages/family-edit/family-edit';
7 7
 import { KakochoEdit } from './pages/kakocho-edit/kakocho-edit';
8 8
 import { MemorialList } from './pages/memorial-list/memorial-list';
9 9
 import { Search } from './pages/search/search';
10
+import { FamilyTree } from './pages/family-tree/family-tree';
10 11
 
11 12
 export const routes: Routes = [
12 13
   {
13 14
     path: '',
14 15
     component: Dashboard,
15 16
   },
17
+  {
18
+    path: 'dashboard',
19
+    component: Dashboard,
20
+  },
16 21
   {
17 22
     path: 'danka-list',
18 23
     component: DankaList,
@@ -53,4 +58,8 @@ export const routes: Routes = [
53 58
     path: 'search',
54 59
     component: Search,
55 60
   },
61
+  {
62
+    path: 'family-tree',
63
+    component: FamilyTree,
64
+  },
56 65
 ];

+ 4
- 1
src/app/models/family.ts 查看文件

@@ -7,5 +7,8 @@ export interface Family {
7 7
   relationship: string;
8 8
   birthDate: string;
9 9
   note: string;
10
+  fatherId: string;
11
+  motherId: string;
12
+  spouseId: string;
13
+  gender: string;
10 14
 }
11
-

+ 10
- 0
src/app/models/marriage-relation.ts 查看文件

@@ -0,0 +1,10 @@
1
+export interface MarriageRelation {
2
+  id: string;
3
+  dankaId: string;
4
+  person1Id: string;
5
+  person2Id: string;
6
+  status: 'current' | 'divorced' | 'widowed' | 'unknown';
7
+  startDate: string;
8
+  endDate: string;
9
+  note: string;
10
+}

+ 261
- 51
src/app/pages/danka-detail/danka-detail.html 查看文件

@@ -67,62 +67,63 @@
67 67
             </nav>
68 68
           </div>
69 69
           @if (selectedTab === 'basic') {
70
-          <button
71
-            type="button"
72
-            class="edit-button"
73
-            [routerLink]="['/danka-edit', danka.id]"
74
-          >
75
-            編集
76
-          </button>
70
+            <button
71
+              type="button"
72
+              class="edit-button"
73
+              [routerLink]="['/danka-edit', danka.id]"
74
+            >
75
+              編集
76
+            </button>
77 77
           }
78 78
 
79 79
           @if (selectedTab === 'kakocho') {
80
-          <button
81
-            type="button"
82
-            class="add-button"
83
-            [routerLink]="['/kakocho-edit', danka.id]"
84
-          >
85
-            故人を登録
86
-          </button>
87
-
88
-          <button
89
-            type="button"
90
-            class="edit-button"
91
-            [routerLink]="['/kakocho-edit', danka.id, kakocholist[0].id]"
92
-          >
93
-            編集
94
-          </button>
80
+            <button
81
+              type="button"
82
+              class="add-button"
83
+              [routerLink]="['/kakocho-edit', danka.id]"
84
+            >
85
+              故人を登録
86
+            </button>
87
+
88
+            <button
89
+              type="button"
90
+              class="edit-button"
91
+              [routerLink]="['/kakocho-edit', danka.id, kakocholist[0].id]"
92
+            >
93
+              編集
94
+            </button>
95 95
           }
96
-
97 96
         </div>
98 97
 
99
-        <section class="family-summary">
100
-          @if (selectedTab === 'basic') {
101
-          <div class="family-name-area">
102
-            <p class="family-name">{{ danka.householdName }}</p>
103
-            <p class="family-head">世帯主: {{ danka.householder }}</p>
104
-          </div>
98
+        @if (selectedTab === 'basic' || selectedTab === 'kakocho') {
99
+          <section class="family-summary">
100
+            @if (selectedTab === 'basic') {
101
+              <div class="family-name-area">
102
+                <p class="family-name">{{ danka.householdName }}</p>
103
+                <p class="family-head">世帯主: {{ danka.householder }}</p>
104
+              </div>
105 105
 
106
-          <div class="family-address">
107
-            <p>郵便番号 〒{{ danka.postalCode }}</p>
108
-            <p>住所 {{ danka.address }}</p>
109
-          </div>
106
+              <div class="family-address">
107
+                <p>郵便番号 〒{{ danka.postalCode }}</p>
108
+                <p>住所 {{ danka.address }}</p>
109
+              </div>
110 110
 
111
-          <button type="button" class="memo-button">
112
-            回忌を見る
113
-          </button>
114
-          }
111
+              <button type="button" class="memo-button">
112
+                回忌を見る
113
+              </button>
114
+            }
115 115
 
116
-          @if (selectedTab === 'kakocho') {
117
-          <div class="family-name-area">
118
-            <p class="family-name">{{ danka.householdName }}の過去帳{{ kakocholist.length }} 名</p>
119
-          </div>
116
+            @if (selectedTab === 'kakocho') {
117
+              <div class="family-name-area">
118
+                <p class="family-name">{{ danka.householdName }}の過去帳{{ kakocholist.length }} 名</p>
119
+              </div>
120 120
 
121
-          <button type="button" class="memo-button">
122
-            年次法要を見る
123
-          </button>
124
-          }
125
-        </section>
121
+              <button type="button" class="memo-button">
122
+                年次法要を見る
123
+              </button>
124
+            }
125
+          </section>
126
+        }
126 127
 
127 128
         @if (selectedTab === 'basic') {
128 129
           <div class="detail-content">
@@ -355,20 +356,229 @@
355 356
                 <div class="empty-family-message">
356 357
                   登録されている家族情報はありません。
357 358
                 </div>
358
-              }              
359
+              }
359 360
             </p>
360 361
           </section>
361 362
         }
362 363
 
363 364
         @if (selectedTab === 'familyTree') {
364
-          <section class="coming-soon-section">
365
+          <section class="family-tree-tab-content">
365 366
             <div class="section-heading">
366 367
               <h2>家系図</h2>
368
+              <p>
369
+                家族情報に登録された親子・配偶者の関係をもとに表示します。
370
+              </p>
367 371
             </div>
368 372
 
369
-            <p class="empty-family-message">
370
-              家系図はこれから実装します。
371
-            </p>
373
+            @if (families.length > 0) {
374
+              <div class="family-tree-layout">
375
+                <section class="family-tree-area">
376
+                  <div class="family-tree-toolbar">
377
+                    <div>
378
+                      <p class="family-tree-title">
379
+                        {{ danka.householdName }}の家系図
380
+                      </p>
381
+                      <p class="family-tree-caption">
382
+                        人物カードを選択すると、右側に関係情報が表示されます。
383
+                      </p>
384
+                    </div>
385
+
386
+                    <div class="family-tree-action-list">
387
+                      <button type="button" class="tree-action-button"
388
+                              [routerLink]="['/danka', danka.id, 'family-new']"
389
+                              [queryParams]="{relationMode: 'child', baseFamilyId: selectedFamily?.id}">
390
+                        親子を追加
391
+                      </button>
392
+
393
+                      <button type="button" class="tree-action-button"
394
+                              [routerLink]="['/danka', danka.id, 'family-new']"
395
+                              [queryParams]="{relationMode: 'spouse', baseFamilyId: selectedFamily?.id}">
396
+                        配偶者を追加
397
+                      </button>
398
+                    </div>
399
+                  </div>
400
+
401
+                  @if (selectedFamily) {
402
+                    <div class="family-tree-diagram">
403
+
404
+                      <div class="tree-generation parent-generation">
405
+                        @if (getFather(selectedFamily)) {
406
+                          <button
407
+                            type="button"
408
+                            class="family-tree-card small"
409
+                            (click)="selectFamily(getFather(selectedFamily)!)">
410
+                            <span class="tree-card-name">{{ getFather(selectedFamily)?.name }}</span>
411
+                            <span class="tree-card-relationship">父</span>
412
+                          </button>
413
+                        }
414
+                        @if (getMother(selectedFamily)) {
415
+                          <button type="button" class="family-tree-card small"
416
+                                  (click)="selectFamily(getMother(selectedFamily)!)">
417
+                            <span class="tree-card-name">{{ getMother(selectedFamily)?.name }}</span>
418
+                            <span class="tree-card-relationship">母</span>
419
+                          </button>
420
+                        }
421
+                      </div>
422
+
423
+                      <div class="tree-generation main-generation">
424
+                        <button type="button"
425
+                                class="family-tree-card active main-person">
426
+                          <span class="tree-card-name">
427
+                            {{ selectedFamily.name }}
428
+                          </span>
429
+                          <span class="tree-card-relationship">
430
+                            {{ selectedFamily.relationship || '関係未登録' }}
431
+                          </span>
432
+                          <span class="tree-card-birth">
433
+                            {{ selectedFamily.birthDate || '生年月日未登録' }}
434
+                          </span>
435
+                        </button>
436
+
437
+                        @if (getSpouse(selectedFamily)) {
438
+                          <button
439
+                            type="button"
440
+                            class="family-tree-card spouse-person"
441
+                            (click)="selectFamily(getSpouse(selectedFamily)!)">
442
+                            <span class="tree-card-name">
443
+                              {{ getSpouse(selectedFamily)?.name }}
444
+                            </span>
445
+                            <span class="tree-card-relationship">配偶者</span>
446
+                            <span class="tree-card-birth">
447
+                              {{ getSpouse(selectedFamily)?.birthDate || '生年月日未登録' }}
448
+                            </span>
449
+                          </button>
450
+                        }
451
+                      </div>
452
+
453
+                      <div class="tree-generation child-generation">
454
+                        @if (getChildren(selectedFamily).length > 0) {
455
+                          @for (child of getChildren(selectedFamily); track child.id) {
456
+                            <button
457
+                              type="button"
458
+                              class="family-tree-card small"
459
+                              (click)="selectFamily(child)">
460
+                              <span class="tree-card-name">
461
+                                {{ child.name }}
462
+                              </span>
463
+                              <span class="tree-card-relationship">
464
+                                {{ child.relationship || '子' }}
465
+                              </span>
466
+                            </button>
467
+                          }
468
+                        } @else {
469
+                          <p class="tree-empty-text">
470
+                            子どもの登録はありません。
471
+                          </p>
472
+                        }
473
+                      </div>
474
+                    </div>
475
+                    <div class="family-tree-member-list">
476
+                      <p class="member-list-title">人物一覧</p>
477
+
478
+                      <div class="member-chip-list">
479
+                        @for (family of families; track family.id) {
480
+                          <button
481
+                            type="button"
482
+                            class="member-chip"
483
+                            [class.active]="selectedFamily?.id === family.id"
484
+                            (click)="selectFamily(family)"
485
+                          >
486
+                            {{ family.name }}
487
+                          </button>
488
+                        }
489
+                      </div>
490
+                    </div>
491
+                  } @else {
492
+                    <p class="empty-family-message">
493
+                      人物カードを選択してください。
494
+                    </p>
495
+                  }
496
+                </section>
497
+
498
+                <aside class="family-tree-side-panel">
499
+                  <h3>選択中の人物情報</h3>
500
+
501
+                  @if (selectedFamily) {
502
+                    <div class="selected-person-card">
503
+                      <p class="selected-person-name">
504
+                        {{ selectedFamily.name }}
505
+                      </p>
506
+
507
+                      <p class="selected-person-sub">
508
+                        {{ selectedFamily.furigana || 'ふりがな未登録' }}
509
+                      </p>
510
+                    </div>
511
+
512
+                    <div class="selected-info-list">
513
+                      <div class="selected-info-row">
514
+                        <span>関係</span>
515
+                        <strong>{{ selectedFamily.relationship || '未登録' }}</strong>
516
+                      </div>
517
+
518
+                      <div class="selected-info-row">
519
+                        <span>生年月日</span>
520
+                        <strong>{{ selectedFamily.birthDate || '未登録' }}</strong>
521
+                      </div>
522
+
523
+                      <div class="selected-info-row">
524
+                        <span>父</span>
525
+                        <strong>{{ getFather(selectedFamily)?.name || '未登録' }}</strong>
526
+                      </div>
527
+
528
+                      <div class="selected-info-row">
529
+                        <span>母</span>
530
+                        <strong>{{ getMother(selectedFamily)?.name || '未登録' }}</strong>
531
+                      </div>
532
+
533
+                      <div class="selected-info-row">
534
+                        <span>配偶者</span>
535
+                        <strong>{{ getSpouse(selectedFamily)?.name || '未登録' }}</strong>
536
+                      </div>
537
+
538
+                      <div class="selected-info-row children-row">
539
+                        <span>子</span>
540
+                        <strong>
541
+                          @if (getChildren(selectedFamily).length > 0) {
542
+                            @for (child of getChildren(selectedFamily); track child.id) {
543
+                              <span class="child-name">
544
+                        {{ child.name }}
545
+                      </span>
546
+                            }
547
+                          } @else {
548
+                            未登録
549
+                          }
550
+                        </strong>
551
+                      </div>
552
+                    </div>
553
+
554
+                    <div class="selected-person-actions">
555
+                      <a
556
+                        class="selected-person-button"
557
+                        [routerLink]="['/danka', danka.id, 'family', selectedFamily.id, 'edit']"
558
+                      >
559
+                        個人情報を編集
560
+                      </a>
561
+
562
+                      <button
563
+                        type="button"
564
+                        class="selected-person-button secondary"
565
+                        (click)="selectedTab = 'kakocho'"
566
+                      >
567
+                        過去帳を確認
568
+                      </button>
569
+                    </div>
570
+                  } @else {
571
+                    <p class="empty-family-message">
572
+                      人物カードを選択してください。
573
+                    </p>
574
+                  }
575
+                </aside>
576
+              </div>
577
+            } @else {
578
+              <div class="empty-family-message">
579
+                登録されている家族情報はありません。
580
+              </div>
581
+            }
372 582
           </section>
373 583
         }
374 584
       } @else {

+ 445
- 1
src/app/pages/danka-detail/danka-detail.scss 查看文件

@@ -566,6 +566,395 @@
566 566
   margin-top: 24px;
567 567
 }
568 568
 
569
+/* =========================
570
+   家系図タブ
571
+========================= */
572
+
573
+.family-tree-tab-content {
574
+  margin-top: 24px;
575
+}
576
+
577
+.family-tree-tab-content .section-heading {
578
+  margin-bottom: 16px;
579
+}
580
+
581
+.family-tree-layout {
582
+  display: grid;
583
+  grid-template-columns: minmax(0, 1fr) 360px;
584
+  gap: 24px;
585
+  align-items: start;
586
+}
587
+
588
+.family-tree-area {
589
+  min-height: 460px;
590
+  padding: 22px;
591
+  background: #fffaf3;
592
+  border: 2px solid #d8c2aa;
593
+  border-radius: 18px;
594
+  box-sizing: border-box;
595
+}
596
+
597
+.family-tree-toolbar {
598
+  display: flex;
599
+  justify-content: space-between;
600
+  align-items: flex-start;
601
+  gap: 18px;
602
+  margin-bottom: 22px;
603
+}
604
+
605
+.family-tree-title {
606
+  margin: 0;
607
+  color: #2f2720;
608
+  font-size: 20px;
609
+  font-weight: 800;
610
+}
611
+
612
+.family-tree-caption {
613
+  margin: 4px 0 0;
614
+  color: #7b6b5c;
615
+  font-size: 14px;
616
+  line-height: 1.6;
617
+}
618
+
619
+.family-tree-action-list {
620
+  display: flex;
621
+  gap: 10px;
622
+  flex-shrink: 0;
623
+}
624
+
625
+.tree-action-button {
626
+  min-width: 112px;
627
+  height: 38px;
628
+  padding: 0 14px;
629
+  border: 2px solid #d8caba;
630
+  border-radius: 6px;
631
+  background: #ffffff;
632
+  color: #2f2720;
633
+  font-size: 14px;
634
+  font-weight: 800;
635
+  cursor: pointer;
636
+  box-sizing: border-box;
637
+}
638
+
639
+.tree-action-button:hover {
640
+  background: #f6efe6;
641
+}
642
+
643
+.family-tree-card-list {
644
+  display: grid;
645
+  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
646
+  gap: 14px;
647
+}
648
+
649
+.family-tree-card {
650
+  min-height: 112px;
651
+  padding: 16px 18px;
652
+  border: 2px solid #d8c2aa;
653
+  border-radius: 14px;
654
+  background: #ffffff;
655
+  color: #2f2720;
656
+  text-align: left;
657
+  cursor: pointer;
658
+  box-sizing: border-box;
659
+  display: flex;
660
+  flex-direction: column;
661
+  gap: 6px;
662
+}
663
+
664
+.family-tree-card:hover {
665
+  background: #f6efe6;
666
+}
667
+
668
+.family-tree-card.active {
669
+  border-color: #8a6543;
670
+  background: #eadfce;
671
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.16);
672
+}
673
+
674
+.tree-card-name {
675
+  color: #5d3b24;
676
+  font-size: 18px;
677
+  font-weight: 800;
678
+}
679
+
680
+.tree-card-relationship {
681
+  color: #2f2720;
682
+  font-size: 14px;
683
+  font-weight: 700;
684
+}
685
+
686
+.tree-card-birth {
687
+  color: #7b6b5c;
688
+  font-size: 13px;
689
+}
690
+
691
+.family-tree-side-panel {
692
+  min-height: 460px;
693
+  padding: 24px 22px;
694
+  background: #fffdf9;
695
+  border: 2px solid #d8caba;
696
+  border-radius: 36px;
697
+  box-sizing: border-box;
698
+}
699
+
700
+.family-tree-side-panel h3 {
701
+  margin: 0 0 16px;
702
+  color: #2f2720;
703
+  font-size: 20px;
704
+  font-weight: 800;
705
+}
706
+
707
+.selected-person-card {
708
+  margin-bottom: 18px;
709
+  padding: 16px 18px;
710
+  background: #eadfce;
711
+  border: 2px solid #d8caba;
712
+  border-radius: 14px;
713
+  box-sizing: border-box;
714
+}
715
+
716
+.selected-person-name {
717
+  margin: 0;
718
+  color: #2f2720;
719
+  font-size: 22px;
720
+  font-weight: 800;
721
+  line-height: 1.2;
722
+}
723
+
724
+.selected-person-sub {
725
+  margin: 4px 0 0;
726
+  color: #7b6b5c;
727
+  font-size: 14px;
728
+}
729
+
730
+.selected-info-list {
731
+  display: grid;
732
+  gap: 10px;
733
+}
734
+
735
+.selected-info-row {
736
+  display: grid;
737
+  grid-template-columns: 82px 1fr;
738
+  gap: 10px;
739
+  align-items: start;
740
+  padding: 10px 12px;
741
+  background: #ffffff;
742
+  border: 2px solid #d8caba;
743
+  border-radius: 8px;
744
+  box-sizing: border-box;
745
+}
746
+
747
+.selected-info-row span {
748
+  color: #7b6b5c;
749
+  font-size: 14px;
750
+  font-weight: 700;
751
+}
752
+
753
+.selected-info-row strong {
754
+  color: #2f2720;
755
+  font-size: 15px;
756
+  font-weight: 800;
757
+  line-height: 1.6;
758
+}
759
+
760
+.children-row strong {
761
+  display: flex;
762
+  flex-wrap: wrap;
763
+  gap: 6px;
764
+}
765
+
766
+.child-name {
767
+  display: inline-flex;
768
+  align-items: center;
769
+  min-height: 26px;
770
+  padding: 2px 8px;
771
+  border-radius: 999px;
772
+  background: #f1e7d8;
773
+  color: #5d3b24;
774
+  font-size: 13px;
775
+  font-weight: 800;
776
+}
777
+
778
+.selected-person-actions {
779
+  display: grid;
780
+  gap: 10px;
781
+  margin-top: 18px;
782
+}
783
+
784
+.selected-person-button {
785
+  min-height: 42px;
786
+  border: 2px solid #8a6543;
787
+  border-radius: 6px;
788
+  background: #8a6543;
789
+  color: #ffffff;
790
+  font-size: 15px;
791
+  font-weight: 800;
792
+  text-decoration: none;
793
+  cursor: pointer;
794
+  display: flex;
795
+  align-items: center;
796
+  justify-content: center;
797
+  box-sizing: border-box;
798
+}
799
+
800
+.selected-person-button:hover {
801
+  background: #765639;
802
+}
803
+
804
+.selected-person-button.secondary {
805
+  background: #ffffff;
806
+  color: #8a6543;
807
+}
808
+
809
+.selected-person-button.secondary:hover {
810
+  background: #f6efe6;
811
+}
812
+
813
+/* =========================
814
+   家系図 図式レイアウト
815
+========================= */
816
+
817
+.family-tree-diagram {
818
+  min-height: 330px;
819
+  padding: 28px 18px;
820
+  background: #fffdf9;
821
+  border: 2px dashed #d8c2aa;
822
+  border-radius: 18px;
823
+  box-sizing: border-box;
824
+  display: grid;
825
+  gap: 34px;
826
+}
827
+
828
+.tree-generation {
829
+  display: flex;
830
+  justify-content: center;
831
+  align-items: center;
832
+  gap: 18px;
833
+  position: relative;
834
+}
835
+
836
+.parent-generation {
837
+  padding-bottom: 8px;
838
+}
839
+
840
+.main-generation {
841
+  padding: 8px 0;
842
+}
843
+
844
+.child-generation {
845
+  padding-top: 8px;
846
+  flex-wrap: wrap;
847
+}
848
+
849
+.family-tree-card.small {
850
+  min-height: 86px;
851
+  max-width: 180px;
852
+}
853
+
854
+.family-tree-card.main-person {
855
+  min-width: 210px;
856
+}
857
+
858
+.family-tree-card.spouse-person {
859
+  min-width: 190px;
860
+}
861
+
862
+/* 本人と配偶者の間の線 */
863
+.main-generation .main-person::after {
864
+  content: "";
865
+  position: absolute;
866
+  top: 50%;
867
+  right: -20px;
868
+  width: 20px;
869
+  height: 2px;
870
+  background: #8a6543;
871
+}
872
+
873
+/* カードを線の基準にする */
874
+.main-generation .family-tree-card {
875
+  position: relative;
876
+}
877
+
878
+/* 親世代から本人世代への縦線 */
879
+.parent-generation::after {
880
+  content: "";
881
+  position: absolute;
882
+  bottom: -28px;
883
+  left: 50%;
884
+  width: 2px;
885
+  height: 28px;
886
+  background: #d8c2aa;
887
+  transform: translateX(-50%);
888
+}
889
+
890
+/* 本人世代から子世代への縦線 */
891
+.main-generation::after {
892
+  content: "";
893
+  position: absolute;
894
+  bottom: -30px;
895
+  left: 50%;
896
+  width: 2px;
897
+  height: 30px;
898
+  background: #d8c2aa;
899
+  transform: translateX(-50%);
900
+}
901
+
902
+/* 子どもがいない場合の文言 */
903
+.tree-empty-text {
904
+  margin: 0;
905
+  padding: 12px 18px;
906
+  background: #ffffff;
907
+  border: 2px solid #d8c2aa;
908
+  border-radius: 10px;
909
+  color: #7b6b5c;
910
+  font-size: 14px;
911
+  font-weight: 700;
912
+}
913
+
914
+.family-tree-member-list {
915
+  margin-top: 18px;
916
+  padding: 16px 18px;
917
+  background: #ffffff;
918
+  border: 2px solid #d8c2aa;
919
+  border-radius: 14px;
920
+  box-sizing: border-box;
921
+}
922
+
923
+.member-list-title {
924
+  margin: 0 0 10px;
925
+  color: #5d3b24;
926
+  font-size: 15px;
927
+  font-weight: 800;
928
+}
929
+
930
+.member-chip-list {
931
+  display: flex;
932
+  flex-wrap: wrap;
933
+  gap: 8px;
934
+}
935
+
936
+.member-chip {
937
+  min-height: 34px;
938
+  padding: 6px 12px;
939
+  border: 2px solid #d8caba;
940
+  border-radius: 999px;
941
+  background: #fffdf9;
942
+  color: #2f2720;
943
+  font-size: 14px;
944
+  font-weight: 700;
945
+  cursor: pointer;
946
+}
947
+
948
+.member-chip:hover {
949
+  background: #f6efe6;
950
+}
951
+
952
+.member-chip.active {
953
+  border-color: #8a6543;
954
+  background: #8a6543;
955
+  color: #ffffff;
956
+}
957
+
569 958
 /* 家族表が狭い画面では横スクロール */
570 959
 @media (max-width: 800px) {
571 960
   .family-list-summary {
@@ -585,4 +974,59 @@
585 974
   .family-support-area {
586 975
     grid-template-columns: 1fr;
587 976
   }
588
-}
977
+
978
+  //家系図タブ
979
+  .family-tree-layout {
980
+    grid-template-columns: 1fr;
981
+  }
982
+
983
+  .family-tree-toolbar {
984
+    flex-direction: column;
985
+  }
986
+
987
+  .family-tree-action-list {
988
+    width: 100%;
989
+    flex-direction: column;
990
+  }
991
+
992
+  .tree-action-button {
993
+    width: 100%;
994
+  }
995
+
996
+  .family-tree-side-panel {
997
+    border-radius: 28px;
998
+  }
999
+
1000
+  .selected-info-row {
1001
+    grid-template-columns: 1fr;
1002
+    gap: 4px;
1003
+  }
1004
+
1005
+  .family-tree-diagram {
1006
+    padding: 22px 12px;
1007
+    gap: 26px;
1008
+  }
1009
+
1010
+  .tree-generation {
1011
+    flex-direction: column;
1012
+    gap: 12px;
1013
+  }
1014
+
1015
+  .family-tree-card.small,
1016
+  .family-tree-card.main-person,
1017
+  .family-tree-card.spouse-person {
1018
+    width: 100%;
1019
+    max-width: none;
1020
+    min-width: 0;
1021
+  }
1022
+
1023
+  .main-generation .main-person::after {
1024
+    display: none;
1025
+  }
1026
+
1027
+  .parent-generation::after,
1028
+  .main-generation::after {
1029
+    height: 20px;
1030
+    bottom: -22px;
1031
+  }
1032
+}

+ 74
- 4
src/app/pages/danka-detail/danka-detail.ts 查看文件

@@ -6,8 +6,10 @@ import { KakochoService } from '../../services/kakocho-service';
6 6
 import { Danka } from '../../models/danka';
7 7
 import { Family } from '../../models/family';
8 8
 import { Kakocho } from '../../models/kakocho';
9
+import { MarriageRelation } from '../../models/marriage-relation';
9 10
 import { AppHeader } from '../../share/header/app-header';
10 11
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
12
+import { MarriageRelationService } from '../../services/marriage-relation-service';
11 13
 
12 14
 @Component({
13 15
   selector: 'app-danka-detail',
@@ -19,14 +21,17 @@ export class DankaDetail {
19 21
   danka: Danka | undefined;
20 22
   families: Family[] = [];
21 23
   kakocholist: Kakocho[] = [];
24
+  marriageRelations: MarriageRelation[] = [];
22 25
   currentYear = new Date().getFullYear();
23 26
 
24 27
   selectedTab: 'basic' | 'family' | 'kakocho' | 'familyTree' = 'basic';
28
+  selectedFamily: Family | undefined = undefined;
25 29
 
26 30
   constructor(
27 31
     private dankaService: DankaService,
28 32
     private familyService: FamilyService,
29 33
     private kakochoService: KakochoService,
34
+    private marriageRelationService: MarriageRelationService,
30 35
     private route: ActivatedRoute,
31 36
   ) {
32 37
     //遷移先からタブ情報を取得
@@ -42,7 +47,9 @@ export class DankaDetail {
42 47
     const id = this.route.snapshot.params['id'];
43 48
     if (id) {
44 49
       this.danka = this.dankaService.getDankaById(id);
50
+      this.marriageRelations = this.marriageRelationService.getMarriageRelationsByDankaId(id);
45 51
       this.families = this.familyService.getFamiliesByDankaId(id);
52
+      this.selectedFamily = this.families[0];
46 53
       this.kakocholist = this.kakochoService.getKakochoByDankaId(id);
47 54
     }
48 55
     console.log(this.danka);
@@ -50,6 +57,7 @@ export class DankaDetail {
50 57
     console.log(tab);
51 58
   }
52 59
 
60
+  //年齢計算の処理
53 61
   getAge(birthDate: string) {
54 62
     if (birthDate === '') {
55 63
       return '-';
@@ -68,11 +76,73 @@ export class DankaDetail {
68 76
     console.log(this.kakocholist);
69 77
   }
70 78
 
79
+  //回忌計算の処理
71 80
   getKaiki(deathDate: string): number {
72
-    return (
73
-      this.currentYear -
74
-      new Date(deathDate).getFullYear() +
75
-      1
81
+    return this.currentYear - new Date(deathDate).getFullYear() + 1;
82
+  }
83
+
84
+  //家系図の処理
85
+  selectFamily(family: Family): void {
86
+    this.selectedFamily = family;
87
+  }
88
+
89
+  getFamilyById(id: string): Family | undefined {
90
+    if (!id) {
91
+      return undefined;
92
+    }
93
+    return this.families.find((family) => family.id === id);
94
+  }
95
+
96
+  getFather(family: Family): Family | undefined {
97
+    return this.getFamilyById(family.fatherId);
98
+  }
99
+
100
+  getMother(family: Family): Family | undefined {
101
+    return this.getFamilyById(family.motherId);
102
+  }
103
+
104
+  getSpouse(family: Family): Family | undefined {
105
+    return this.getFamilyById(family.spouseId);
106
+  }
107
+
108
+  getChildren(family: Family): Family[] {
109
+    return this.families.filter(
110
+      (child) => child.fatherId === family.id || child.motherId === family.id,
76 111
     );
77 112
   }
113
+
114
+  getCurrentMarriage(family: Family): MarriageRelation | undefined {
115
+    return this.marriageRelations.find(
116
+      (relation) =>
117
+        relation.status === 'current' &&
118
+        (relation.person1Id === family.id || relation.person2Id === family.id),
119
+    );
120
+  }
121
+
122
+  getPastMarriages(family: Family): MarriageRelation[] {
123
+    return this.marriageRelations.filter(
124
+      (relation) =>
125
+        relation.status !== 'current' &&
126
+        (relation.person1Id === family.id || relation.person2Id === family.id),
127
+    );
128
+
129
+  }
130
+
131
+  getMarriagePartner(relation: MarriageRelation, family: Family): Family | undefined {
132
+    const partnerId = relation.person1Id === family.id ? relation.person2Id : relation.person1Id;
133
+    return this.getFamilyById(partnerId);
134
+  }
135
+
136
+  getMarriageStatusLabel(status: string): string {
137
+    if (status === 'current') {
138
+      return '現在の配偶者';
139
+    }
140
+    if (status === 'divorced') {
141
+      return '離婚';
142
+    }
143
+    if (status === 'widowed') {
144
+      return '死別';
145
+    }
146
+    return '不明';
147
+  }
78 148
 }

+ 80
- 46
src/app/pages/family-edit/family-edit.html 查看文件

@@ -22,11 +22,8 @@
22 22
               <div class="form-row">
23 23
                 <label for="name">氏名</label>
24 24
                 <div class="form-field">
25
-                  <input
26
-                    id="name"
27
-                    type="text"
28
-                    formControlName="name"
29
-                  />
25
+                  <input id="name" type="text"
26
+                         formControlName="name"/>
30 27
                   @if (familyForm.get('name')?.invalid && familyForm.get('name')?.touched) {
31 28
                     <p class="error-message">氏名を入力してください。</p>
32 29
                   }
@@ -36,11 +33,8 @@
36 33
               <div class="form-row">
37 34
                 <label for="furigana">ふりがな</label>
38 35
                 <div class="form-field">
39
-                  <input
40
-                    id="furigana"
41
-                    type="text"
42
-                    formControlName="furigana"
43
-                  />
36
+                  <input id="furigana" type="text"
37
+                         formControlName="furigana"/>
44 38
                   @if (familyForm.get('furigana')?.invalid && familyForm.get('furigana')?.touched) {
45 39
                     <p class="error-message">ふりがなを入力してください。</p>
46 40
                   }
@@ -50,10 +44,8 @@
50 44
               <div class="form-row">
51 45
                 <label for="relationship">世帯主との関係</label>
52 46
                 <div class="form-field">
53
-                  <select
54
-                    id="relationship"
55
-                    formControlName="relationship"
56
-                  >
47
+                  <select id="relationship"
48
+                          formControlName="relationship">
57 49
                     <option value="">選択してください</option>
58 50
                     <option value="世帯主">世帯主</option>
59 51
                     <option value="配偶者">配偶者</option>
@@ -65,46 +57,97 @@
65 57
                     <option value="次女">次女</option>
66 58
                     <option value="その他">その他</option>
67 59
                   </select>
68
-
69 60
                   @if (familyForm.get('relationship')?.invalid && familyForm.get('relationship')?.touched) {
70 61
                     <p class="error-message">世帯主との関係を選択してください。</p>
71 62
                   }
72 63
                 </div>
73 64
               </div>
74 65
 
66
+              <div class="form-row">
67
+                <label for="gender">性別</label>
68
+                <div class="form-field">
69
+                  <select id="gender" formControlName="gender">
70
+                    <option value="unknown">未設定</option>
71
+                    <option value="male">男性</option>
72
+                    <option value="female">女性</option>
73
+                  </select>
74
+                </div>
75
+              </div>
76
+
75 77
               <div class="form-row">
76 78
                 <label for="birthDate">生年月日</label>
77 79
                 <div class="form-field">
78
-                  <input
79
-                    id="birthDate"
80
-                    type="date"
81
-                    formControlName="birthDate"
82
-                  />
80
+                  <input id="birthDate" type="date"
81
+                         formControlName="birthDate"/>
83 82
                 </div>
84 83
               </div>
85 84
 
86 85
               <div class="form-row">
87 86
                 <label for="age">年齢</label>
88 87
                 <div class="form-field">
89
-                  <input
90
-                    id="age"
91
-                    type="text"
92
-                    [value]="getAgeText()"
93
-                    readonly
94
-                  />
88
+                  <input id="age" type="text" [value]="getAgeText()" readonly/>
89
+                </div>
90
+              </div>
91
+
92
+              <div class="form-row form-row-heading">
93
+                <label></label>
94
+                <div class="form-field">
95
+                  <h3 class="sub-section-title">家系図情報</h3>
96
+                  <p class="sub-section-description">
97
+                    家系図に反映する親子・配偶者の関係を設定します。
98
+                  </p>
99
+                </div>
100
+              </div>
101
+
102
+              <div class="form-row">
103
+                <label for="fatherId">父</label>
104
+                <div class="form-field">
105
+                  <select id="fatherId" formControlName="fatherId">
106
+                    <option value="">選択なし</option>
107
+                    @for (family of getFamilyOptions(); track family.id) {
108
+                      <option [value]="family.id">
109
+                        {{ family.name }}({{ family.relationship }})
110
+                      </option>
111
+                    }
112
+                  </select>
113
+                </div>
114
+              </div>
115
+
116
+              <div class="form-row">
117
+                <label for="motherId">母</label>
118
+                <div class="form-field">
119
+                  <select id="motherId" formControlName="motherId">
120
+                    <option value="">選択なし</option>
121
+                    @for (family of getFamilyOptions(); track family.id) {
122
+                      <option [value]="family.id">
123
+                        {{ family.name }}({{ family.relationship }})
124
+                      </option>
125
+                    }
126
+                  </select>
127
+                </div>
128
+              </div>
129
+
130
+              <div class="form-row">
131
+                <label for="spouseId">配偶者</label>
132
+                <div class="form-field">
133
+                  <select id="spouseId" formControlName="spouseId">
134
+                    <option value="">選択なし</option>
135
+                    @for (family of getFamilyOptions(); track family.id) {
136
+                      <option [value]="family.id">
137
+                        {{ family.name }}({{ family.relationship }})
138
+                      </option>
139
+                    }
140
+                  </select>
95 141
                 </div>
96 142
               </div>
97 143
 
98 144
               <div class="form-row">
99 145
                 <label for="note">備考</label>
100 146
                 <div class="form-field">
101
-                  <textarea
102
-                    id="note"
103
-                    formControlName="note"
104
-                    rows="4"
105
-                  ></textarea>
147
+                  <textarea id="note" formControlName="note" rows="4"></textarea>
106 148
                 </div>
107 149
               </div>
150
+
108 151
             </div>
109 152
           </section>
110 153
 
@@ -112,11 +155,9 @@
112 155
             <div class="householder-area">
113 156
               <h3>この方を世帯主にする</h3>
114 157
 
115
-              <button
116
-                type="button"
117
-                class="set-householder-button"
118
-                (click)="setAsHouseholder()"
119
-              >
158
+              <button type="button"
159
+                      class="set-householder-button"
160
+                      (click)="setAsHouseholder()">
120 161
                 世帯主に設定
121 162
               </button>
122 163
             </div>
@@ -124,10 +165,7 @@
124 165
         </div>
125 166
 
126 167
         <div class="bottom-actions">
127
-          <button
128
-            type="button"
129
-            class="delete-button"
130
-            (click)="deleteFamily()">
168
+          <button type="button" class="delete-button" (click)="deleteFamily()">
131 169
             削除
132 170
           </button>
133 171
 
@@ -135,12 +173,8 @@
135 173
             キャンセル
136 174
           </button>
137 175
 
138
-          <button
139
-            type="button"
140
-            class="save-button"
141
-            [disabled]="familyForm.invalid"
142
-            (click)="saveFamily()"
143
-          >
176
+          <button type="button" class="save-button"
177
+                  [disabled]="familyForm.invalid" (click)="saveFamily()">
144 178
             保存
145 179
           </button>
146 180
         </div>

+ 22
- 0
src/app/pages/family-edit/family-edit.scss 查看文件

@@ -148,6 +148,28 @@
148 148
   font-weight: 700;
149 149
 }
150 150
 
151
+.form-row-heading {
152
+  margin-top: 30px;
153
+}
154
+
155
+.form-row-heading label {
156
+  padding-top: 0;
157
+}
158
+
159
+.sub-section-title {
160
+  margin: 0;
161
+  color: #2f2720;
162
+  font-size: 18px;
163
+  font-weight: 800;
164
+}
165
+
166
+.sub-section-description {
167
+  margin: 6px 0 0;
168
+  color: #7b6b5c;
169
+  font-size: 14px;
170
+  line-height: 1.6;
171
+}
172
+
151 173
 .phone-edit-section {
152 174
   min-height: 382px;
153 175
   padding: 30px 24px 22px;

+ 47
- 3
src/app/pages/family-edit/family-edit.ts 查看文件

@@ -22,9 +22,11 @@ import { Family } from '../../models/family';
22 22
 export class FamilyEdit {
23 23
   danka: Danka | undefined;
24 24
   family: Family | undefined;
25
+  families: Family[] = [];
25 26
   dankaId: string = '';
26 27
   familyId: string | null = null;
27
-  isEditing = false;
28
+  relationMode: string = '';
29
+  baseFamilyId: string = '';
28 30
 
29 31
   constructor(
30 32
     private familyService: FamilyService,
@@ -33,6 +35,9 @@ export class FamilyEdit {
33 35
   ) {
34 36
     this.dankaId = this.route.snapshot.params['dankaId'];
35 37
     this.familyId = this.route.snapshot.params['familyId'];
38
+    this.families = this.familyService.getFamiliesByDankaId(this.dankaId);
39
+    this.relationMode = this.route.snapshot.queryParamMap.get('relationMode') ?? '';
40
+    this.baseFamilyId = this.route.snapshot.queryParamMap.get('baseFamilyId') ?? '';
36 41
     if (this.familyId) {
37 42
       this.family = this.familyService.getFamilyById(this.familyId);
38 43
       if (this.family) {
@@ -42,6 +47,32 @@ export class FamilyEdit {
42 47
           relationship: this.family.relationship,
43 48
           birthDate: this.family.birthDate,
44 49
           note: this.family.note,
50
+          fatherId: this.family.fatherId,
51
+          motherId: this.family.motherId,
52
+          spouseId: this.family.spouseId,
53
+          gender: this.family.gender,
54
+        });
55
+      }
56
+    }
57
+
58
+    if (!this.familyId && this.relationMode === 'spouse') {
59
+      this.familyForm.patchValue({
60
+        spouseId: this.baseFamilyId,
61
+      });
62
+    }
63
+
64
+    if (!this.familyId && this.relationMode === 'child') {
65
+      const baseFamily = this.familyService.getFamilyById(this.baseFamilyId);
66
+
67
+      if (baseFamily?.gender === 'male') {
68
+        this.familyForm.patchValue({
69
+          fatherId: this.baseFamilyId,
70
+        });
71
+      }
72
+
73
+      if (baseFamily?.gender === 'female') {
74
+        this.familyForm.patchValue({
75
+          motherId: this.baseFamilyId,
45 76
         });
46 77
       }
47 78
     }
@@ -53,12 +84,16 @@ export class FamilyEdit {
53 84
     relationship: new FormControl(''),
54 85
     birthDate: new FormControl('', Validators.required),
55 86
     note: new FormControl(''),
87
+    fatherId: new FormControl(''),
88
+    motherId: new FormControl(''),
89
+    spouseId: new FormControl(''),
90
+    gender: new FormControl('unknown'),
56 91
   });
57 92
 
58 93
   getAgeText() {
59 94
     const birthDate = this.familyForm.get('birthDate')?.value;
60 95
     if (!birthDate) {
61
-      return;
96
+      return '生年月日を入力すると自動計算されます';
62 97
     }
63 98
     const birth = new Date(birthDate);
64 99
     const today = new Date();
@@ -71,6 +106,10 @@ export class FamilyEdit {
71 106
     return `${age}歳`;
72 107
   }
73 108
 
109
+  getFamilyOptions(): Family[] {
110
+    return this.families.filter((family) => family.id !== this.familyId);
111
+  }
112
+
74 113
   saveFamily() {
75 114
     if (this.familyForm.invalid) {
76 115
       return;
@@ -87,9 +126,14 @@ export class FamilyEdit {
87 126
       relationship: formValue.relationship?.trim() ?? '',
88 127
       birthDate: formValue.birthDate?.trim() ?? '',
89 128
       note: formValue.note?.trim() ?? '',
129
+      fatherId: this.familyForm.value.fatherId ?? '',
130
+      motherId: this.familyForm.value.motherId ?? '',
131
+      spouseId: this.familyForm.value.spouseId ?? '',
132
+      gender: this.familyForm.value.gender ?? 'unknown',
90 133
     };
91 134
     this.familyService.saveFamily(updatedFamily);
92
-    this.router.navigate(['/danka-detail', dankaId], { queryParams: { tab: 'family' } });  }
135
+    this.router.navigate(['/danka-detail', dankaId], { queryParams: { tab: 'family' } });
136
+  }
93 137
 
94 138
   cancelFamilyEdit(): void {
95 139
     const dankaId = this.dankaId;

+ 14
- 0
src/app/pages/family-tree/family-tree.html 查看文件

@@ -0,0 +1,14 @@
1
+<app-header></app-header>
2
+
3
+<div class="breadcrumb">
4
+  ホーム &gt; 家系図
5
+</div>
6
+
7
+<div class="search-page">
8
+  <app-side-menu></app-side-menu>
9
+
10
+  <main class="search-main">
11
+
12
+  </main>
13
+</div>
14
+

+ 0
- 0
src/app/pages/family-tree/family-tree.scss 查看文件


+ 22
- 0
src/app/pages/family-tree/family-tree.spec.ts 查看文件

@@ -0,0 +1,22 @@
1
+import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+import { FamilyTree } from './family-tree';
4
+
5
+describe('FamilyTree', () => {
6
+  let component: FamilyTree;
7
+  let fixture: ComponentFixture<FamilyTree>;
8
+
9
+  beforeEach(async () => {
10
+    await TestBed.configureTestingModule({
11
+      imports: [FamilyTree],
12
+    }).compileComponents();
13
+
14
+    fixture = TestBed.createComponent(FamilyTree);
15
+    component = fixture.componentInstance;
16
+    await fixture.whenStable();
17
+  });
18
+
19
+  it('should create', () => {
20
+    expect(component).toBeTruthy();
21
+  });
22
+});

+ 13
- 0
src/app/pages/family-tree/family-tree.ts 查看文件

@@ -0,0 +1,13 @@
1
+import { Component } from '@angular/core';
2
+import { FormsModule } from '@angular/forms';
3
+import { RouterLink } from '@angular/router';
4
+import { AppHeader } from '../../share/header/app-header';
5
+import { AppSideMenu } from '../../share/side-menu/app-side-menu';
6
+
7
+@Component({
8
+  selector: 'app-family-tree',
9
+  imports: [AppHeader, AppSideMenu, FormsModule, RouterLink],
10
+  templateUrl: './family-tree.html',
11
+  styleUrl: './family-tree.scss',
12
+})
13
+export class FamilyTree {}

+ 120
- 6
src/app/services/family-service.ts 查看文件

@@ -14,6 +14,10 @@ export class FamilyService {
14 14
       relationship: '母',
15 15
       birthDate: '1975-01-01',
16 16
       note: '次の世帯主',
17
+      fatherId: '5',
18
+      motherId: '6',
19
+      spouseId: '3',
20
+      gender: 'female',
17 21
     },
18 22
     {
19 23
       id: '2',
@@ -23,6 +27,88 @@ export class FamilyService {
23 27
       relationship: '長男',
24 28
       birthDate: '2005-12-31',
25 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',
26 112
     },
27 113
   ];
28 114
 
@@ -37,14 +123,42 @@ export class FamilyService {
37 123
   }
38 124
 
39 125
   //家族の情報を更新
40
-  saveFamily(updateFamily: Family) {
41
-    const index = this.families.findIndex((families) => families.id === updateFamily.id);
42
-    if (index === -1) {
43
-      this.families.push(updateFamily);
44
-      return;
126
+  saveFamily(updatedFamily: Family): void {
127
+    const oldFamily = this.families.find((family) => family.id === updatedFamily.id);
128
+
129
+    const oldSpouseId = oldFamily?.spouseId ?? '';
130
+    const newSpouseId = updatedFamily.spouseId ?? '';
131
+
132
+    const familyIndex = this.families.findIndex((family) => family.id === updatedFamily.id);
133
+
134
+    if (familyIndex !== -1) {
135
+      this.families[familyIndex] = updatedFamily;
136
+    } else {
137
+      this.families.push(updatedFamily);
138
+    }
139
+
140
+    if (oldSpouseId && oldSpouseId !== newSpouseId) {
141
+      const oldSpouseIndex = this.families.findIndex((family) => family.id === oldSpouseId);
142
+      if (oldSpouseIndex !== -1 && this.families[oldSpouseIndex].spouseId === updatedFamily.id) {
143
+        this.families[oldSpouseIndex] = {
144
+          ...this.families[oldSpouseIndex],
145
+          spouseId: '',
146
+        };
147
+      }
148
+    }
149
+
150
+    if (newSpouseId) {
151
+      const newSpouseIndex = this.families.findIndex((family) => family.id === newSpouseId);
152
+
153
+      if (newSpouseIndex !== -1) {
154
+        this.families[newSpouseIndex] = {
155
+          ...this.families[newSpouseIndex],
156
+          spouseId: updatedFamily.id,
157
+        };
158
+      }
45 159
     }
46
-    this.families[index] = updateFamily;
47 160
   }
161
+
48 162
   //家族の情報を削除
49 163
   deleteFamily(id: string | undefined) {
50 164
     const index = this.families.findIndex((family) => family.id === id);

+ 16
- 0
src/app/services/marriage-relation-service.spec.ts 查看文件

@@ -0,0 +1,16 @@
1
+import { TestBed } from '@angular/core/testing';
2
+
3
+import { MarriageRelationService } from './marriage-relation-service';
4
+
5
+describe('MarriageRelationService', () => {
6
+  let service: MarriageRelationService;
7
+
8
+  beforeEach(() => {
9
+    TestBed.configureTestingModule({});
10
+    service = TestBed.inject(MarriageRelationService);
11
+  });
12
+
13
+  it('should be created', () => {
14
+    expect(service).toBeTruthy();
15
+  });
16
+});

+ 125
- 0
src/app/services/marriage-relation-service.ts 查看文件

@@ -0,0 +1,125 @@
1
+import { Injectable } from '@angular/core';
2
+import { MarriageRelation } from '../models/marriage-relation';
3
+
4
+@Injectable({
5
+  providedIn: 'root',
6
+})
7
+export class MarriageRelationService {
8
+  private marriageRelations: MarriageRelation[] = [
9
+    {
10
+      id: '1',
11
+      dankaId: '1',
12
+      person1Id: '2', // 太郎
13
+      person2Id: '8', // 花子
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
+
35
+  getMarriageRelationsByFamilyId(familyId: string): MarriageRelation[] {
36
+    return this.marriageRelations.filter(
37
+      (relation) => relation.person1Id === familyId || relation.person2Id === familyId,
38
+    );
39
+  }
40
+
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
+    );
47
+  }
48
+
49
+  getPastMarriagesByFamilyId(familyId: string): MarriageRelation[] {
50
+    return this.marriageRelations.filter(
51
+      (relation) =>
52
+        relation.status !== 'current' &&
53
+        (relation.person1Id === familyId || relation.person2Id === familyId),
54
+    );
55
+  }
56
+
57
+  getMarriageRelationById(id: string): MarriageRelation | undefined {
58
+    return this.marriageRelations.find((relation) => relation.id === id);
59
+  }
60
+
61
+  validateMarriageRelation(data: MarriageRelation): string[] {
62
+    const errors: string[] = [];
63
+
64
+    if (!data.person1Id || !data.person2Id) {
65
+      errors.push('配偶関係の人物が選択されていません。');
66
+    }
67
+
68
+    if (data.person1Id === data.person2Id) {
69
+      errors.push('同じ人物同士を配偶関係に設定することはできません。');
70
+    }
71
+
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
+    }
100
+
101
+    return errors;
102
+  }
103
+
104
+  saveMarriageRelation(data: MarriageRelation): string[] {
105
+    const errors = this.validateMarriageRelation(data);
106
+
107
+    if (errors.length > 0) {
108
+      return errors;
109
+    }
110
+
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
+    }
118
+
119
+    return [];
120
+  }
121
+
122
+  deleteMarriageRelation(id: string): void {
123
+    this.marriageRelations = this.marriageRelations.filter((relation) => relation.id !== id);
124
+  }
125
+}

Loading…
取消
儲存