Przeglądaj źródła

[add]

檀家詳細ページを追加
poohr 3 tygodni temu
rodzic
commit
f990256a40

+ 649
- 530
src/app/pages/danka-detail/danka-detail.html
Plik diff jest za duży
Wyświetl plik


+ 0
- 40
src/app/pages/danka-detail/danka-detail.scss Wyświetl plik

1264
     bottom: -22px;
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 Wyświetl plik

1
 import { Component } from '@angular/core';
1
 import { Component } from '@angular/core';
2
-import {
3
-  ElementRef,
4
-  ViewChild,
5
-  AfterViewInit
6
-} from '@angular/core';
7
 import { ActivatedRoute, RouterLink } from '@angular/router';
2
 import { ActivatedRoute, RouterLink } from '@angular/router';
8
 import { DankaService } from '../../services/dankaService';
3
 import { DankaService } from '../../services/dankaService';
9
 import { FamilyService } from '../../services/family-service';
4
 import { FamilyService } from '../../services/family-service';
15
 import { AppHeader } from '../../share/header/app-header';
10
 import { AppHeader } from '../../share/header/app-header';
16
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
11
 import { AppSideMenu } from '../../share/side-menu/app-side-menu';
17
 import { MarriageRelationService } from '../../services/marriage-relation-service';
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
 interface NextMemorial {
16
 interface NextMemorial {
34
   name: string;
17
   name: string;
39
 
22
 
40
 @Component({
23
 @Component({
41
   selector: 'app-danka-detail',
24
   selector: 'app-danka-detail',
42
-  imports: [AppHeader, AppSideMenu, RouterLink],
25
+  imports: [AppHeader, AppSideMenu, RouterLink, FormsModule],
43
   templateUrl: './danka-detail.html',
26
   templateUrl: './danka-detail.html',
44
   styleUrl: './danka-detail.scss',
27
   styleUrl: './danka-detail.scss',
45
 })
28
 })
46
-export class DankaDetail implements AfterViewInit {
29
+export class DankaDetail {
47
   danka: Danka | undefined;
30
   danka: Danka | undefined;
48
   families: Family[] = [];
31
   families: Family[] = [];
49
   kakocholist: Kakocho[] = [];
32
   kakocholist: Kakocho[] = [];
50
   marriageRelations: MarriageRelation[] = [];
33
   marriageRelations: MarriageRelation[] = [];
51
   nextMemorial: NextMemorial | undefined;
34
   nextMemorial: NextMemorial | undefined;
52
   currentYear = new Date().getFullYear();
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
   constructor(
44
   constructor(
77
     private dankaService: DankaService,
45
     private dankaService: DankaService,
79
     private kakochoService: KakochoService,
47
     private kakochoService: KakochoService,
80
     private marriageRelationService: MarriageRelationService,
48
     private marriageRelationService: MarriageRelationService,
81
     private route: ActivatedRoute,
49
     private route: ActivatedRoute,
82
-    private familyTreeBuilder: FamilyTreeBuilderService,
83
-    private familyTreeLayout: FamilyTreeLayoutService,
84
-    private familyUnitLayout: FamilyUnitLayoutService,
85
   ) {
50
   ) {
86
     const tab = this.route.snapshot.queryParams['tab'];
51
     const tab = this.route.snapshot.queryParams['tab'];
87
     if (tab === 'family') {
52
     if (tab === 'family') {
88
       this.selectedTab = 'family';
53
       this.selectedTab = 'family';
89
     } else if (tab === 'kakocho') {
54
     } else if (tab === 'kakocho') {
90
       this.selectedTab = 'kakocho';
55
       this.selectedTab = 'kakocho';
56
+    } else if (tab === 'event') {
57
+      this.selectedTab = 'event';
91
     } else if (tab === 'familyTree') {
58
     } else if (tab === 'familyTree') {
92
       this.selectedTab = 'familyTree';
59
       this.selectedTab = 'familyTree';
93
     }
60
     }
100
       this.selectedFamily = this.families[0];
67
       this.selectedFamily = this.families[0];
101
       this.kakocholist = this.kakochoService.getKakochoByDankaId(id);
68
       this.kakocholist = this.kakochoService.getKakochoByDankaId(id);
102
       this.nextMemorial = this.getNextMemorial();
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
   getKaiki(deathDate: string): number {
184
   getKaiki(deathDate: string): number {
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
   private getNextMemorial(): NextMemorial | undefined {
251
   private getNextMemorial(): NextMemorial | undefined {
295
     const today = this.toDateOnly(new Date());
252
     const today = this.toDateOnly(new Date());
296
 
253
 
431
     }
388
     }
432
     return '不明';
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
 }

Ładowanie…
Anuluj
Zapisz