Переглянути джерело

[add]

檀家詳細ページを追加
poohr 3 тижднів тому
джерело
коміт
f990256a40

+ 649
- 530
src/app/pages/danka-detail/danka-detail.html
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 0
- 40
src/app/pages/danka-detail/danka-detail.scss Переглянути файл

@@ -1264,43 +1264,3 @@
1264 1264
     bottom: -22px;
1265 1265
   }
1266 1266
 }
1267
-
1268
-.family-tree-svg-container {
1269
-  width: 100%;
1270
-  height: 700px;
1271
-
1272
-  overflow: auto;
1273
-
1274
-  border: 1px solid #ddd;
1275
-}
1276
-.family-tree-svg-container {
1277
-
1278
-  width: 100%;
1279
-
1280
-  height: 700px;
1281
-
1282
-  overflow: hidden;
1283
-
1284
-  border: 1px solid #ddd;
1285
-}
1286
-.family-node {
1287
-
1288
-  cursor: pointer;
1289
-
1290
-}
1291
-
1292
-.family-text {
1293
-
1294
-  cursor: pointer;
1295
-
1296
-}
1297
-
1298
-.family-tree-svg-container svg {
1299
-  width: 100%;
1300
-  height: 100%;
1301
-}
1302
-
1303
-.family-text {
1304
-  writing-mode: vertical-rl;
1305
-  text-orientation: upright;
1306
-}

+ 124
- 432
src/app/pages/danka-detail/danka-detail.ts Переглянути файл

@@ -1,9 +1,4 @@
1 1
 import { Component } from '@angular/core';
2
-import {
3
-  ElementRef,
4
-  ViewChild,
5
-  AfterViewInit
6
-} from '@angular/core';
7 2
 import { ActivatedRoute, RouterLink } from '@angular/router';
8 3
 import { DankaService } from '../../services/dankaService';
9 4
 import { FamilyService } from '../../services/family-service';
@@ -15,20 +10,8 @@ import { MarriageRelation } from '../../models/marriage-relation';
15 10
 import { AppHeader } from '../../share/header/app-header';
16 11
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
17 12
 import { MarriageRelationService } from '../../services/marriage-relation-service';
18
-import {
19
-  FamilyTreeBuilderService,
20
-  FamilyTreeNode
21
-} from '../../services/family-tree-builder';
22
-import {
23
-  FamilyTreeLayoutService
24
-} from '../../services/family-tree-layout';
25
-
26
-import {
27
-  LayoutNode
28
-} from '../../models/layoutnode';
29
-import svgPanZoom from 'svg-pan-zoom';
30
-import { FamilyUnitLayout } from '../../models/family-unit-layout';
31
-import { FamilyUnitLayoutService } from '../../services/family-unit-layout';
13
+import { FormsModule } from '@angular/forms';
14
+import { EventStatus, EventTarget, EventType } from '../../models/event';
32 15
 
33 16
 interface NextMemorial {
34 17
   name: string;
@@ -39,39 +22,24 @@ interface NextMemorial {
39 22
 
40 23
 @Component({
41 24
   selector: 'app-danka-detail',
42
-  imports: [AppHeader, AppSideMenu, RouterLink],
25
+  imports: [AppHeader, AppSideMenu, RouterLink, FormsModule],
43 26
   templateUrl: './danka-detail.html',
44 27
   styleUrl: './danka-detail.scss',
45 28
 })
46
-export class DankaDetail implements AfterViewInit {
29
+export class DankaDetail {
47 30
   danka: Danka | undefined;
48 31
   families: Family[] = [];
49 32
   kakocholist: Kakocho[] = [];
50 33
   marriageRelations: MarriageRelation[] = [];
51 34
   nextMemorial: NextMemorial | undefined;
52 35
   currentYear = new Date().getFullYear();
53
-  treeNodes: FamilyTreeNode[] = [];
54
-  layoutNodes: LayoutNode[] = [];
55
-  layoutNodeMap = new Map<string, LayoutNode>();
56
-  unitLayouts: FamilyUnitLayout[] = [];
57
-  unitLayoutMap = new Map<string, FamilyUnitLayout>();
58
-  deathDateMap = new Map<string, Kakocho>();
59
-  private kakochoByNameMap = new Map<string, Kakocho>();
60
-
61
-  selectedTab: 'basic' | 'family' | 'kakocho' | 'familyTree' = 'basic';
62
-  selectedFamily: Family | undefined = undefined;
63
-
64
-  @ViewChild('familyTreeSvg')
65
-  familyTreeSvg?: ElementRef<SVGSVGElement>;
66
-  private panZoomInstance: any;
36
+  familySearchKeyword = '';
37
+  eventSearchKeyword = '';
38
+  eventStatuses: EventStatus[] = ['未案内', '案内済'];
39
+  private eventStatusByTargetId: Record<string, EventStatus> = {};
67 40
 
68
-  readonly PERSON_WIDTH = 90;
69
-  readonly PERSON_HEIGHT = 140;
70
-  readonly SPOUSE_GAP = 30;
71
-
72
-  viewBox = '0 0 6000 6000';
73
-  readonly DEATH_FONT_SIZE = 10;
74
-  readonly DEATH_LINE_HEIGHT = 10; // 少し余白込み
41
+  selectedTab: 'basic' | 'family' | 'kakocho' | 'event' | 'familyTree' = 'basic';
42
+  selectedFamily: Family | undefined = undefined;
75 43
 
76 44
   constructor(
77 45
     private dankaService: DankaService,
@@ -79,15 +47,14 @@ export class DankaDetail implements AfterViewInit {
79 47
     private kakochoService: KakochoService,
80 48
     private marriageRelationService: MarriageRelationService,
81 49
     private route: ActivatedRoute,
82
-    private familyTreeBuilder: FamilyTreeBuilderService,
83
-    private familyTreeLayout: FamilyTreeLayoutService,
84
-    private familyUnitLayout: FamilyUnitLayoutService,
85 50
   ) {
86 51
     const tab = this.route.snapshot.queryParams['tab'];
87 52
     if (tab === 'family') {
88 53
       this.selectedTab = 'family';
89 54
     } else if (tab === 'kakocho') {
90 55
       this.selectedTab = 'kakocho';
56
+    } else if (tab === 'event') {
57
+      this.selectedTab = 'event';
91 58
     } else if (tab === 'familyTree') {
92 59
       this.selectedTab = 'familyTree';
93 60
     }
@@ -100,151 +67,118 @@ export class DankaDetail implements AfterViewInit {
100 67
       this.selectedFamily = this.families[0];
101 68
       this.kakocholist = this.kakochoService.getKakochoByDankaId(id);
102 69
       this.nextMemorial = this.getNextMemorial();
103
-
104
-      this.treeNodes =
105
-        this.familyTreeBuilder.build(
106
-          this.families,
107
-          this.marriageRelations
108
-        );
109
-
110
-      const units =
111
-        this.familyTreeBuilder.buildFamilyUnits(
112
-          this.treeNodes
113
-        );
114
-
115
-      const unitTree =
116
-        this.familyTreeBuilder.buildFamilyUnitTree(
117
-          units
118
-        );
119
-
120
-      const unitRoots =
121
-        this.familyTreeBuilder.getUnitRoots(
122
-          unitTree
123
-        );
124
-
125
-      this.unitLayouts =
126
-        this.familyUnitLayout.buildLayout(
127
-          unitRoots
128
-        );
129
-
130
-      const roots =
131
-        this.familyTreeBuilder.getRoots(
132
-          this.treeNodes
133
-        );
134
-
135
-      this.layoutNodes =
136
-        this.familyTreeLayout.buildLayout(
137
-          roots
138
-        );
139
-
140
-      this.rebuildLayoutNodeMap();
141
-
142
-      this.unitLayouts =
143
-        this.familyUnitLayout.buildLayout(
144
-          unitRoots
145
-        );
146
-
147
-      this.rebuildUnitLayoutMap();
148
-
149
-      this.calculateViewBox();
150
-
151
-      this.kakocholist.forEach(kakocho => {
152
-
153
-        if (kakocho.familyId) {
154
-
155
-          this.deathDateMap.set(
156
-            kakocho.familyId,
157
-            kakocho
158
-          );
159
-
160
-        }
161
-
162
-      });
163
-
164
-      this.kakocholist.forEach(k => {
165
-
166
-        const key = this.normalizeName(k.name) + '_' + k.dankaId;
167
-
168
-        this.kakochoByNameMap.set(key, k);
169
-
170
-      });
171
-
172 70
     }
173 71
   }
174
-  ngAfterViewInit(): void {
175
-    if (!this.familyTreeSvg) {
176
-      return;
177
-    }
178
-
179
-    this.panZoomInstance = svgPanZoom(
180
-      this.familyTreeSvg.nativeElement,
181
-      {
182
-        fit: true,
183
-        center: true,
184
-        zoomEnabled: true,
185
-        panEnabled: true,
186
-
187
-        minZoom: 0.1,
188
-        maxZoom: 50
189
-      }
190
-    );
191
-  }
192
-
193
-  initializePanZoom(): void {
194 72
 
195
-    if (!this.familyTreeSvg) {
196
-      console.log('SVGなし');
197
-      return;
73
+  getAge(birthDate: string): string {
74
+    if (birthDate === '') {
75
+      return '-';
198 76
     }
199 77
 
200
-    console.log('SVGあり');
78
+    const birth = new Date(birthDate);
79
+    const today = new Date();
80
+    const thisYearBirthday = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
201 81
 
82
+    let age = today.getFullYear() - birth.getFullYear();
202 83
 
203
-    if (!this.familyTreeSvg) {
204
-      return;
84
+    if (thisYearBirthday > today) {
85
+      age--;
205 86
     }
206 87
 
207
-    if (this.panZoomInstance) {
208
-      return;
88
+    return age.toString();
89
+  }
90
+
91
+  get filteredFamilies(): Family[] {
92
+    const keyword = this.familySearchKeyword.trim();
93
+    if (!keyword) {
94
+      return this.families;
209 95
     }
210 96
 
211
-    this.panZoomInstance = svgPanZoom(
212
-      this.familyTreeSvg.nativeElement,
213
-      {
214
-        zoomEnabled: true,
215
-        panEnabled: true,
216
-        fit: true,
217
-        center: true,
218
-      }
97
+    return this.families.filter((family) =>
98
+      [
99
+        family.name,
100
+        family.furigana,
101
+        family.birthDate,
102
+        family.relationship,
103
+        family.note,
104
+      ].some((value) => value.includes(keyword)),
219 105
     );
220
-
221 106
   }
222
-  openFamilyTreeTab(): void {
223
-
224
-    this.selectedTab = 'familyTree';
225 107
 
226
-    setTimeout(() => {
227
-      this.initializePanZoom();
228
-    });
108
+  getFamilyEventLabels(family: Family): string {
109
+    const birthDate = this.parseDate(family.birthDate);
110
+    if (!birthDate) {
111
+      return '-';
112
+    }
229 113
 
114
+    const age = this.currentYear - birthDate.getFullYear();
115
+    const eventTypes = this.getEventTypes(age);
116
+    return eventTypes.length > 0 ? eventTypes.join('、') : '-';
230 117
   }
231 118
 
232
-  getAge(birthDate: string): string {
233
-    if (birthDate === '') {
234
-      return '-';
119
+  get eventTargets(): EventTarget[] {
120
+    if (!this.danka) {
121
+      return [];
235 122
     }
236 123
 
237
-    const birth = new Date(birthDate);
238
-    const today = new Date();
239
-    const thisYearBirthday = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
124
+    return this.families
125
+      .flatMap((family) => {
126
+        const birthDate = this.parseDate(family.birthDate);
127
+        if (!birthDate) {
128
+          return [];
129
+        }
240 130
 
241
-    let age = today.getFullYear() - birth.getFullYear();
131
+        const age = this.currentYear - birthDate.getFullYear();
132
+        return this.getEventTypes(age).map((eventType) => {
133
+          const id = `${family.id}-${eventType}`;
134
+          return {
135
+            id,
136
+            dankaId: family.dankaId,
137
+            name: family.name,
138
+            furigana: family.furigana,
139
+            householdName: this.danka?.householdName ?? '不明',
140
+            relationship: family.relationship || '未登録',
141
+            birthDate: family.birthDate,
142
+            age,
143
+            eventType,
144
+            note: family.note,
145
+            status:
146
+              this.eventStatusByTargetId[id] ?? (Number(family.id) % 2 === 0 ? '案内済' : '未案内'),
147
+          };
148
+        });
149
+      })
150
+      .sort(
151
+        (a, b) =>
152
+          this.getEventSortOrder(a.eventType) - this.getEventSortOrder(b.eventType) ||
153
+          a.age - b.age ||
154
+          a.name.localeCompare(b.name, 'ja'),
155
+      );
156
+  }
242 157
 
243
-    if (thisYearBirthday > today) {
244
-      age--;
158
+  get filteredEventTargets(): EventTarget[] {
159
+    const keyword = this.eventSearchKeyword.trim();
160
+    if (!keyword) {
161
+      return this.eventTargets;
245 162
     }
246 163
 
247
-    return age.toString();
164
+    return this.eventTargets.filter((target) =>
165
+      [
166
+        target.name,
167
+        target.furigana,
168
+        target.householdName,
169
+        target.relationship,
170
+        target.birthDate,
171
+        target.age.toString(),
172
+        target.eventType,
173
+        target.note,
174
+        target.status,
175
+      ].some((value) => value.includes(keyword)),
176
+    );
177
+  }
178
+
179
+  changeEventStatus(target: EventTarget, status: EventStatus): void {
180
+    target.status = status;
181
+    this.eventStatusByTargetId[target.id] = status;
248 182
   }
249 183
 
250 184
   getKaiki(deathDate: string): number {
@@ -291,6 +225,29 @@ export class DankaDetail implements AfterViewInit {
291 225
     }
292 226
   }
293 227
 
228
+  private getEventTypes(age: number): EventType[] {
229
+    const eventTypes: EventType[] = [];
230
+
231
+    if (age >= 3 && age <= 12) {
232
+      eventTypes.push('稚児行列');
233
+    }
234
+    if ([3, 5, 7].includes(age)) {
235
+      eventTypes.push('七五三');
236
+    }
237
+    if (age === 20) {
238
+      eventTypes.push('成人式');
239
+    }
240
+    if (age === 88) {
241
+      eventTypes.push('米寿');
242
+    }
243
+
244
+    return eventTypes;
245
+  }
246
+
247
+  private getEventSortOrder(eventType: EventType): number {
248
+    return ['稚児行列', '七五三', '成人式', '米寿'].indexOf(eventType);
249
+  }
250
+
294 251
   private getNextMemorial(): NextMemorial | undefined {
295 252
     const today = this.toDateOnly(new Date());
296 253
 
@@ -431,269 +388,4 @@ export class DankaDetail implements AfterViewInit {
431 388
     }
432 389
     return '不明';
433 390
   }
434
-
435
-  getLayoutNode(
436
-    familyId: string
437
-  ): LayoutNode | undefined {
438
-
439
-    return this.layoutNodeMap.get(
440
-      familyId
441
-    );
442
-
443
-  }
444
-  getCurrentMarriageLines() {
445
-
446
-    return this.marriageRelations.filter(
447
-      x => x.status === 'current'
448
-    );
449
-
450
-  }
451
-
452
-  private rebuildLayoutNodeMap(): void {
453
-
454
-    this.layoutNodeMap.clear();
455
-
456
-    this.layoutNodes.forEach(node => {
457
-
458
-      this.layoutNodeMap.set(
459
-        node.familyId,
460
-        node
461
-      );
462
-
463
-    });
464
-
465
-  }
466
-
467
-  private rebuildUnitLayoutMap(): void {
468
-
469
-    this.unitLayoutMap.clear();
470
-
471
-    this.unitLayouts.forEach(layout => {
472
-
473
-      this.unitLayoutMap.set(
474
-        layout.node.unit.id,
475
-        layout
476
-      );
477
-
478
-    });
479
-
480
-  }
481
-
482
-  getUnitLayout(
483
-    unitId: string
484
-  ): FamilyUnitLayout | undefined {
485
-
486
-    return this.unitLayoutMap.get(
487
-      unitId
488
-    );
489
-
490
-  }
491
-
492
-  getCenterX(layout: FamilyUnitLayout): number {
493
-
494
-    return (
495
-      layout.x +
496
-      this.PERSON_WIDTH +
497
-      this.SPOUSE_GAP / 2
498
-    );
499
-
500
-  }
501
-
502
-  getTopCenterY(
503
-    layout: FamilyUnitLayout
504
-  ): number {
505
-
506
-    return layout.y;
507
-
508
-  }
509
-
510
-  getBottomCenterY(
511
-    layout: FamilyUnitLayout
512
-  ): number {
513
-
514
-    return (
515
-      layout.y +
516
-      this.PERSON_HEIGHT
517
-    );
518
-
519
-  }
520
-
521
-  getMiddleY(
522
-    parent: FamilyUnitLayout,
523
-    child: FamilyUnitLayout
524
-  ): number {
525
-
526
-    return (
527
-      this.getBottomCenterY(parent)
528
-      +
529
-      child.y
530
-    ) / 2;
531
-
532
-  }
533
-
534
-  getHusbandX(
535
-    layout: FamilyUnitLayout
536
-  ): number {
537
-
538
-    if (!layout.node.unit.wife) {
539
-      return (
540
-        layout.x +
541
-        (this.PERSON_WIDTH + this.SPOUSE_GAP) / 2
542
-      );
543
-    }
544
-
545
-    return layout.x;
546
-
547
-  }
548
-
549
-  getWifeX(
550
-    layout: FamilyUnitLayout
551
-  ): number {
552
-
553
-    if (!layout.node.unit.husband) {
554
-      return (
555
-        layout.x +
556
-        (this.PERSON_WIDTH + this.SPOUSE_GAP) / 2
557
-      );
558
-    }
559
-
560
-    return (
561
-      layout.x +
562
-      this.PERSON_WIDTH +
563
-      this.SPOUSE_GAP
564
-    );
565
-
566
-  }
567
-
568
-  getHusbandTextX(
569
-    layout: FamilyUnitLayout
570
-  ): number {
571
-
572
-    return (
573
-      this.getHusbandX(layout)
574
-      + this.PERSON_WIDTH / 2
575
-    );
576
-
577
-  }
578
-
579
-  getWifeTextX(
580
-    layout: FamilyUnitLayout
581
-  ): number {
582
-
583
-    return (
584
-      this.getWifeX(layout)
585
-      + this.PERSON_WIDTH / 2
586
-    );
587
-  }
588
-
589
-  private calculateViewBox(): void {
590
-
591
-    if (this.unitLayouts.length === 0) {
592
-      return;
593
-    }
594
-
595
-    const minX = Math.min(
596
-      ...this.unitLayouts.map(x => x.x)
597
-    );
598
-
599
-    const minY = Math.min(
600
-      ...this.unitLayouts.map(x => x.y)
601
-    );
602
-
603
-    const maxX = Math.max(
604
-      ...this.unitLayouts.map(
605
-        x => x.x + this.PERSON_WIDTH * 2 + this.SPOUSE_GAP
606
-      )
607
-    );
608
-
609
-    const maxY = Math.max(
610
-      ...this.unitLayouts.map(
611
-        x => x.y + this.PERSON_HEIGHT
612
-      )
613
-    );
614
-
615
-    const padding = 100;
616
-
617
-    this.viewBox =
618
-      `${minX - padding}
619
-     ${minY - padding}
620
-     ${maxX - minX + padding * 2}
621
-     ${maxY - minY + padding * 2}`;
622
-  }
623
-
624
-  getKakochoByFamilyId(
625
-    familyId: string
626
-  ): Kakocho | undefined {
627
-
628
-    return this.deathDateMap.get(
629
-      familyId
630
-    );
631
-
632
-  }
633
-
634
-  getKakochoByFamily(family: Family): Kakocho | undefined {
635
-
636
-    const key =
637
-      this.normalizeName(family.name) + '_' + family.dankaId;
638
-
639
-    return this.kakochoByNameMap.get(key);
640
-
641
-  }
642
-
643
-  getDeathWareki(family: Family): string {
644
-
645
-    const kakocho = this.getKakochoByFamily(family);
646
-
647
-    if (!kakocho?.deathDate) return '';
648
-
649
-    const date = new Date(kakocho.deathDate);
650
-
651
-    const wareki = new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
652
-      era: 'long',
653
-      year: 'numeric',
654
-    }).format(date);
655
-
656
-    return `${wareki}${date.getMonth() + 1}月${date.getDate()}日没`;
657
-  }
658
-
659
-  getAgeAtDeathText(family: Family): string {
660
-
661
-    const kakocho = this.getKakochoByFamily(family);
662
-
663
-    if (!kakocho?.ageAtDeath) return '';
664
-
665
-    return `享年${kakocho.ageAtDeath}歳`;
666
-  }
667
-
668
-  getDeathDate(family: Family): string {
669
-
670
-    const kakocho = this.getKakochoByFamily(family);
671
-
672
-    return kakocho?.deathDate ?? '';
673
-
674
-  }
675
-
676
-  /**
677
-   * 縦書きテキストの高さを概算する
678
-   */
679
-  getVerticalTextHeight(text: string | null | undefined): number {
680
-    if (!text) return 0;
681
-
682
-    // 縦書きは基本「1文字=1行」
683
-    return text.length * this.DEATH_LINE_HEIGHT;
684
-  }
685
-
686
-  /**
687
-   * テキストをカード下に揃えるためのY座標
688
-   */
689
-  getDeathTextY(layout: FamilyUnitLayout, text: string | null | undefined): number {
690
-
691
-    const height = this.getVerticalTextHeight(text);
692
-
693
-    return (
694
-      layout.y +
695
-      this.PERSON_HEIGHT - 5 - height
696
-    );
697
-  }
698
-
699 391
 }

Завантаження…
Відмінити
Зберегти