| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790 |
- import { Component } from '@angular/core';
- import {
- ElementRef,
- ViewChild,
- AfterViewInit
- } from '@angular/core';
- import { OnInit } from '@angular/core';
- import { ActivatedRoute, RouterLink } from '@angular/router';
- import { DankaService } from '../../services/dankaService';
- import { FamilyService } from '../../services/family-service';
- import { KakochoService } from '../../services/kakocho-service';
- import { Danka } from '../../models/danka';
- import { Family } from '../../models/family';
- import { Kakocho } from '../../models/kakocho';
- import { MarriageRelation } from '../../models/marriage-relation';
- import { AppHeader } from '../../share/header/app-header';
- import { AppSideMenu } from '../../share/side-menu/app-side-menu';
- import { MarriageRelationService } from '../../services/marriage-relation-service';
- import { FormsModule } from '@angular/forms';
- import { EventStatus, EventTarget, EventType } from '../../models/event';
- import { EventService } from '../../services/event-service';
- import {
- FamilyTreeBuilderService,
- FamilyTreeNode
- } from '../../services/family-tree-builder';
- import {
- FamilyTreeLayoutService
- } from '../../services/family-tree-layout';
-
- import {
- LayoutNode
- } from '../../models/layoutnode';
- import svgPanZoom from 'svg-pan-zoom';
- import { FamilyUnitLayout } from '../../models/family-unit-layout';
- import { FamilyUnitLayoutService } from '../../services/family-unit-layout';
-
-
-
- interface NextMemorial {
- name: string;
- memorialType: string;
- targetYear: number;
- deathDate: string;
- }
-
- @Component({
- selector: 'app-danka-detail',
- imports: [AppHeader, AppSideMenu, RouterLink, FormsModule],
- templateUrl: './danka-detail.html',
- styleUrl: './danka-detail.scss',
- })
- export class DankaDetail implements OnInit, AfterViewInit {
- danka: Danka | undefined;
- families: Family[] = [];
- kakocholist: Kakocho[] = [];
- marriageRelations: MarriageRelation[] = [];
- nextMemorial: NextMemorial | undefined;
- currentYear = new Date().getFullYear();
- familySearchKeyword = '';
- eventSearchKeyword = '';
- eventStatuses: EventStatus[] = ['未案内', '案内済'];
- treeNodes: FamilyTreeNode[] = [];
- layoutNodes: LayoutNode[] = [];
- layoutNodeMap = new Map<string, LayoutNode>();
- unitLayouts: FamilyUnitLayout[] = [];
- unitLayoutMap = new Map<string, FamilyUnitLayout>();
- deathDateMap = new Map<string, Kakocho>();
- private kakochoByNameMap = new Map<string, Kakocho>();
-
- selectedTab: 'basic' | 'family' | 'kakocho' | 'event' | 'familyTree' = 'basic';
- selectedFamily: Family | undefined = undefined;
-
- @ViewChild('familyTreeSvg')
- familyTreeSvg?: ElementRef<SVGSVGElement>;
- private panZoomInstance: any;
-
- readonly PERSON_WIDTH = 90;
- readonly PERSON_HEIGHT = 140;
- readonly SPOUSE_GAP = 30;
-
- viewBox = '0 0 6000 6000';
- readonly DEATH_FONT_SIZE = 10;
- readonly DEATH_LINE_HEIGHT = 10; // 少し余白込み
-
-
- constructor(
- private dankaService: DankaService,
- private familyService: FamilyService,
- private kakochoService: KakochoService,
- private marriageRelationService: MarriageRelationService,
- private route: ActivatedRoute,
- private familyTreeBuilder: FamilyTreeBuilderService,
- private familyTreeLayout: FamilyTreeLayoutService,
- private familyUnitLayout: FamilyUnitLayoutService,
- private eventService: EventService,
- ) {
-
- const tab = this.route.snapshot.queryParams['tab'];
- if (tab === 'family') {
- this.selectedTab = 'family';
- } else if (tab === 'kakocho') {
- this.selectedTab = 'kakocho';
- } else if (tab === 'event') {
- this.selectedTab = 'event';
- } else if (tab === 'familyTree') {
- this.selectedTab = 'familyTree';
- }
-
- }
- ngOnInit(): void {
- this.init();
- }
- async init(): Promise<void> {
- const id = this.route.snapshot.params['id'];
- if (!id) return;
-
- this.danka = (await this.dankaService.getDankaById(id)) ?? undefined;
- if (!this.danka) return;
-
- this.marriageRelations = await this.marriageRelationService.getMarriageRelationsByDankaId(id);
- this.families = this.sortFamiliesByHouseholder(
- await this.familyService.getFamiliesByDankaId(id)
- );
-
- this.selectedFamily = this.families[0];
- this.kakocholist = await this.kakochoService.getKakochoByDankaId(id);
-
- this.nextMemorial = this.getNextMemorial();
-
- this.treeNodes =
- this.familyTreeBuilder.build(
- this.families,
- this.marriageRelations
- );
-
- const units =
- this.familyTreeBuilder.buildFamilyUnits(
- this.treeNodes
- );
-
- const unitTree =
- this.familyTreeBuilder.buildFamilyUnitTree(
- units
- );
-
- const unitRoots =
- this.familyTreeBuilder.getUnitRoots(
- unitTree
- );
-
- this.unitLayouts =
- this.familyUnitLayout.buildLayout(
- unitRoots
- );
-
- const roots =
- this.familyTreeBuilder.getRoots(
- this.treeNodes
- );
-
- this.layoutNodes =
- this.familyTreeLayout.buildLayout(
- roots
- );
-
- this.rebuildLayoutNodeMap();
-
- this.unitLayouts =
- this.familyUnitLayout.buildLayout(
- unitRoots
- );
-
- this.rebuildUnitLayoutMap();
-
- this.calculateViewBox();
-
- this.kakocholist.forEach(kakocho => {
- if (kakocho.familyId) {
- this.deathDateMap.set(
- kakocho.familyId,
- kakocho
- );
- }
- });
-
- this.kakocholist.forEach(k => {
- const key = this.normalizeName(k.name) + '_' + k.dankaId;
- this.kakochoByNameMap.set(key, k);
- });
-
- }
-
- ngAfterViewInit(): void {
- if (!this.familyTreeSvg) {
- return;
- }
-
- this.panZoomInstance = svgPanZoom(
- this.familyTreeSvg.nativeElement,
- {
- fit: true,
- center: true,
- zoomEnabled: true,
- panEnabled: true,
- minZoom: 0.1,
- maxZoom: 50
- }
- );
- }
-
- initializePanZoom(): void {
-
- if (!this.familyTreeSvg) {
- console.log('SVGなし');
- return;
- }
-
- console.log('SVGあり');
-
- if (!this.familyTreeSvg) {
- return;
- }
-
- if (this.panZoomInstance) {
- return;
- }
-
- this.panZoomInstance = svgPanZoom(
- this.familyTreeSvg.nativeElement,
- {
- zoomEnabled: true,
- panEnabled: true,
- fit: true,
- center: true,
- }
- );
- }
- openFamilyTreeTab(): void {
- this.selectedTab = 'familyTree';
- setTimeout(() => {
- this.initializePanZoom();
- });
- }
-
- getAge(birthDate: string): string {
- if (birthDate === '') {
- return '-';
- }
-
- const birth = new Date(birthDate);
- const today = new Date();
- const thisYearBirthday = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
-
- let age = today.getFullYear() - birth.getFullYear();
-
- if (thisYearBirthday > today) {
- age--;
- }
-
- return age.toString();
- }
-
- get filteredFamilies(): Family[] {
- const keyword = this.familySearchKeyword.trim();
- if (!keyword) {
- return this.families;
- }
-
- return this.families.filter((family) =>
- [
- family.name,
- family.furigana,
- family.birthDate,
- family.relationship,
- family.note,
- ].some((value) => value.includes(keyword)),
- );
- }
-
- getFamilyEventLabels(family: Family): string {
- const birthDate = this.parseDate(family.birthDate);
- if (!birthDate) {
- return '-';
- }
-
- const age = this.currentYear - birthDate.getFullYear();
- const eventTypes = this.getEventTypes(age);
- return eventTypes.length > 0 ? eventTypes.join('、') : '-';
- }
-
- get eventTargets(): EventTarget[] {
- if (!this.danka) {
- return [];
- }
-
- return this.families
- .flatMap((family) => {
- const birthDate = this.parseDate(family.birthDate);
- if (!birthDate) {
- return [];
- }
-
- const age = this.currentYear - birthDate.getFullYear();
- return this.getEventTypes(age).map((eventType) => {
- const id = `${family.id}-${eventType}`;
- const defaultStatus: EventStatus = Number(family.id) % 2 === 0 ? '案内済' : '未案内';
- return {
- id,
- dankaId: family.dankaId,
- name: family.name,
- furigana: family.furigana,
- householdName: this.danka?.householdName ?? '不明',
- relationship: family.relationship || '未登録',
- birthDate: family.birthDate,
- age,
- eventType,
- note: family.note,
- status: this.eventService.getEventStatus(id, defaultStatus),
- };
- });
- })
- .sort(
- (a, b) =>
- this.getEventSortOrder(a.eventType) - this.getEventSortOrder(b.eventType) ||
- a.age - b.age ||
- a.name.localeCompare(b.name, 'ja'),
- );
- }
-
- get filteredEventTargets(): EventTarget[] {
- const keyword = this.eventSearchKeyword.trim();
- if (!keyword) {
- return this.eventTargets;
- }
-
- return this.eventTargets.filter((target) =>
- [
- target.name,
- target.furigana,
- target.householdName,
- target.relationship,
- target.birthDate,
- target.age.toString(),
- target.eventType,
- target.note,
- target.status,
- ].some((value) => value.includes(keyword)),
- );
- }
-
- changeEventStatus(target: EventTarget, status: EventStatus): void {
- target.status = status;
- this.eventService.saveEventStatus(target.id, status);
- }
-
- getKaiki(deathDate: string): number {
- return this.currentYear - new Date(deathDate).getFullYear() + 1;
- }
-
- formatUpdatedAt(updatedAt: string): string {
- const date = this.parseDate(updatedAt);
- if (!date) {
- return '未登録';
- }
-
- return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
- }
-
- formatDeathDateWithYear(deathDate: string): string {
- const date = this.parseDate(deathDate);
- if (!date) {
- return '未登録';
- }
-
- return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
- }
-
- getMemorialType(deathDate: string): string {
- const deathYear = Number(deathDate.slice(0, 4));
- const yearDiff = this.currentYear - deathYear;
-
- switch (yearDiff) {
- case 1:
- return '一周忌';
- case 2:
- return '三回忌';
- case 6:
- return '七回忌';
- case 12:
- return '十三回忌';
- case 16:
- return '十七回忌';
- case 22:
- return '二十三回忌';
- default:
- return '';
- }
- }
-
- private getEventTypes(age: number): EventType[] {
- const eventTypes: EventType[] = [];
-
- if (age >= 3 && age <= 12) {
- eventTypes.push('稚児行列');
- }
- if ([3, 5, 7].includes(age)) {
- eventTypes.push('七五三');
- }
- if (age === 20) {
- eventTypes.push('成人式');
- }
- if (age === 88) {
- eventTypes.push('米寿');
- }
-
- return eventTypes;
- }
-
- private getEventSortOrder(eventType: EventType): number {
- return ['稚児行列', '七五三', '成人式', '米寿'].indexOf(eventType);
- }
-
- private getNextMemorial(): NextMemorial | undefined {
- const today = this.toDateOnly(new Date());
-
- return this.kakocholist
- .map((kakocho) => {
- const memorialType = this.getMemorialType(kakocho.deathDate);
- const deathDate = this.parseDate(kakocho.deathDate);
-
- if (!memorialType || !deathDate) {
- return undefined;
- }
-
- const memorialDate = new Date(
- this.currentYear,
- deathDate.getMonth(),
- deathDate.getDate(),
- );
-
- if (memorialDate < today) {
- return undefined;
- }
-
- return {
- memorialDate,
- memorial: {
- name: kakocho.name,
- memorialType,
- targetYear: this.currentYear,
- deathDate: kakocho.deathDate,
- },
- };
- })
- .filter(
- (item): item is { memorialDate: Date; memorial: NextMemorial } => item !== undefined,
- )
- .sort(
- (a, b) =>
- a.memorialDate.getTime() - b.memorialDate.getTime() ||
- a.memorial.name.localeCompare(b.memorial.name, 'ja'),
- )[0]?.memorial;
- }
-
- private toDateOnly(date: Date): Date {
- return new Date(date.getFullYear(), date.getMonth(), date.getDate());
- }
-
- private parseDate(value: string): Date | null {
- const [year, month, day] = value.split('-').map(Number);
- if (!year || !month || !day) {
- return null;
- }
- return new Date(year, month - 1, day);
- }
-
- selectFamily(family: Family): void {
- this.selectedFamily = family;
- }
-
- private sortFamiliesByHouseholder(families: Family[]): Family[] {
- if (!this.danka) {
- return families;
- }
-
- const householderName = this.normalizeName(this.danka.householder);
-
- return [...families].sort((a, b) => {
- const aIsHouseholder = this.normalizeName(a.name) === householderName;
- const bIsHouseholder = this.normalizeName(b.name) === householderName;
-
- if (aIsHouseholder === bIsHouseholder) {
- return 0;
- }
-
- return aIsHouseholder ? -1 : 1;
- });
- }
-
- private normalizeName(name: string): string {
- return name.replace(/\s/g, '');
- }
-
- getFamilyById(id: string): Family | undefined {
- if (!id) {
- return undefined;
- }
-
- return this.families.find((family) => family.id === id);
- }
-
- getFather(family: Family): Family | undefined {
- return this.getFamilyById(family.fatherId);
- }
-
- getMother(family: Family): Family | undefined {
- return this.getFamilyById(family.motherId);
- }
-
- getSpouse(family: Family): Family | undefined {
- return this.getFamilyById(family.spouseId);
- }
-
- getChildren(family: Family): Family[] {
- return this.families.filter(
- (child) => child.fatherId === family.id || child.motherId === family.id,
- );
- }
-
- getCurrentMarriage(family: Family): MarriageRelation | undefined {
- return this.marriageRelations.find(
- (relation) =>
- relation.status === 'current' &&
- (relation.person1Id === family.id || relation.person2Id === family.id),
- );
- }
-
- getPastMarriages(family: Family): MarriageRelation[] {
- return this.marriageRelations.filter(
- (relation) =>
- relation.status !== 'current' &&
- (relation.person1Id === family.id || relation.person2Id === family.id),
- );
- }
-
- getMarriagePartner(relation: MarriageRelation, family: Family): Family | undefined {
- const partnerId = relation.person1Id === family.id ? relation.person2Id : relation.person1Id;
- return this.getFamilyById(partnerId);
- }
-
- getMarriageStatusLabel(status: string): string {
- if (status === 'current') {
- return '現在の配偶者';
- }
- if (status === 'divorced') {
- return '離婚';
- }
- if (status === 'widowed') {
- return '死別';
- }
- return '不明';
- }
-
- getLayoutNode(
- familyId: string
- ): LayoutNode | undefined {
- return this.layoutNodeMap.get(
- familyId
- );
-
- }
- getCurrentMarriageLines() {
- return this.marriageRelations.filter(
- x => x.status === 'current'
- );
-
- }
-
- private rebuildLayoutNodeMap(): void {
- this.layoutNodeMap.clear();
- this.layoutNodes.forEach(node => {
- this.layoutNodeMap.set(
- node.familyId,
- node
- );
- });
- }
-
- private rebuildUnitLayoutMap(): void {
- this.unitLayoutMap.clear();
- this.unitLayouts.forEach(layout => {
- this.unitLayoutMap.set(
- layout.node.unit.id,
- layout
- );
- });
- }
-
- getUnitLayout(
- unitId: string
- ): FamilyUnitLayout | undefined {
- return this.unitLayoutMap.get(
- unitId
- );
-
- }
-
- getCenterX(layout: FamilyUnitLayout): number {
- return (
- layout.x +
- this.PERSON_WIDTH +
- this.SPOUSE_GAP / 2
- );
-
- }
-
- getTopCenterY(
- layout: FamilyUnitLayout
- ): number {
- return layout.y;
- }
-
- getBottomCenterY(
- layout: FamilyUnitLayout
- ): number {
- return (
- layout.y +
- this.PERSON_HEIGHT
- );
- }
-
- getMiddleY(
- parent: FamilyUnitLayout,
- child: FamilyUnitLayout
- ): number {
-
- return (
- this.getBottomCenterY(parent)
- +
- child.y
- ) / 2;
-
- }
-
- getHusbandX(
- layout: FamilyUnitLayout
- ): number {
-
- if (!layout.node.unit.wife) {
- return (
- layout.x +
- (this.PERSON_WIDTH + this.SPOUSE_GAP) / 2
- );
- }
-
- return layout.x;
-
- }
-
- getWifeX(
- layout: FamilyUnitLayout
- ): number {
-
- if (!layout.node.unit.husband) {
- return (
- layout.x +
- (this.PERSON_WIDTH + this.SPOUSE_GAP) / 2
- );
- }
-
- return (
- layout.x +
- this.PERSON_WIDTH +
- this.SPOUSE_GAP
- );
-
- }
-
- getHusbandTextX(
- layout: FamilyUnitLayout
- ): number {
- return (
- this.getHusbandX(layout)
- + this.PERSON_WIDTH / 2
- );
-
- }
-
- getWifeTextX(
- layout: FamilyUnitLayout
- ): number {
- return (
- this.getWifeX(layout)
- + this.PERSON_WIDTH / 2
- );
- }
-
- private calculateViewBox(): void {
-
- if (this.unitLayouts.length === 0) {
- return;
- }
-
- const minX = Math.min(
- ...this.unitLayouts.map(x => x.x)
- );
-
- const minY = Math.min(
- ...this.unitLayouts.map(x => x.y)
- );
-
- const maxX = Math.max(
- ...this.unitLayouts.map(
- x => x.x + this.PERSON_WIDTH * 2 + this.SPOUSE_GAP
- )
- );
-
- const maxY = Math.max(
- ...this.unitLayouts.map(
- x => x.y + this.PERSON_HEIGHT
- )
- );
-
- const padding = 100;
-
- this.viewBox =
- `${minX - padding}
- ${minY - padding}
- ${maxX - minX + padding * 2}
- ${maxY - minY + padding * 2}`;
- }
-
- getKakochoByFamilyId(
- familyId: string
- ): Kakocho | undefined {
- return this.deathDateMap.get(
- familyId
- );
- }
-
- getKakochoByFamily(family: Family): Kakocho | undefined {
- const key =
- this.normalizeName(family.name) + '_' + family.dankaId;
- return this.kakochoByNameMap.get(key);
- }
-
- getDeathWareki(family: Family): string {
- const kakocho = this.getKakochoByFamily(family);
- if (!kakocho?.deathDate) return '';
- const date = new Date(kakocho.deathDate);
- const wareki = new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
- era: 'long',
- year: 'numeric',
- }).format(date);
-
- return `${wareki}${date.getMonth() + 1}月${date.getDate()}日没`;
- }
-
- getAgeAtDeathText(family: Family): string {
- const kakocho = this.getKakochoByFamily(family);
- if (!kakocho?.ageAtDeath) return '';
- return `享年${kakocho.ageAtDeath}歳`;
- }
-
- getDeathDate(family: Family): string {
- const kakocho = this.getKakochoByFamily(family);
- return kakocho?.deathDate ?? '';
- }
-
- /**
- * 縦書きテキストの高さを概算する
- */
- getVerticalTextHeight(text: string | null | undefined): number {
- if (!text) return 0;
- // 縦書きは基本「1文字=1行」
- return text.length * this.DEATH_LINE_HEIGHT;
- }
-
- /**
- * テキストをカード下に揃えるためのY座標
- */
- getDeathTextY(layout: FamilyUnitLayout, text: string | null | undefined): number {
- const height = this.getVerticalTextHeight(text);
- return (
- layout.y +
- this.PERSON_HEIGHT - 5 - height
- );
- }
-
- }
|