poohr 3 주 전
부모
커밋
e476d2aa58

+ 5
- 0
src/app/app.routes.ts 파일 보기

@@ -8,6 +8,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 10
 import { FamilyTree } from './pages/family-tree/family-tree';
11
+import { EventPage } from './pages/event/event';
11 12
 
12 13
 export const routes: Routes = [
13 14
   {
@@ -54,6 +55,10 @@ export const routes: Routes = [
54 55
     path: 'memorial-list',
55 56
     component: MemorialList,
56 57
   },
58
+  {
59
+    path: 'event',
60
+    component: EventPage,
61
+  },
57 62
   {
58 63
     path: 'search',
59 64
     component: Search,

+ 3
- 0
src/app/models/danka.ts 파일 보기

@@ -2,9 +2,12 @@
2 2
 export interface Danka {
3 3
   id: string;
4 4
   householdName: string;
5
+  householdFurigana: string;
5 6
   householder: string;
7
+  householderFurigana: string;
6 8
   postalCode: string;
7 9
   address: string;
10
+  note: string;
8 11
   phones: Phone[];
9 12
   updatedAt: string;
10 13
 }

+ 17
- 0
src/app/models/event.ts 파일 보기

@@ -0,0 +1,17 @@
1
+export type EventType = '稚児行列' | '七五三' | '成人式' | '米寿';
2
+
3
+export type EventStatus = '案内済' | '未案内';
4
+
5
+export interface EventTarget {
6
+  id: string;
7
+  dankaId: string;
8
+  name: string;
9
+  furigana: string;
10
+  householdName: string;
11
+  relationship: string;
12
+  birthDate: string;
13
+  age: number;
14
+  eventType: EventType;
15
+  note: string;
16
+  status: EventStatus;
17
+}

+ 1
- 0
src/app/models/memorial.ts 파일 보기

@@ -8,4 +8,5 @@ export interface Memorial {
8 8
   householdName: string;
9 9
   deathDate: string;
10 10
   memorialType: string;
11
+  note: string;
11 12
 }

+ 205
- 32
src/app/pages/danka-detail/danka-detail.scss 파일 보기

@@ -86,8 +86,8 @@
86 86
   margin-top: 36px;
87 87
   border: 2px solid #8a6543;
88 88
   border-radius: 6px;
89
-  background: #8a6543;
90
-  color: #ffffff;
89
+  background: #ffffff;
90
+  color: #8a6543;
91 91
   font-size: 18px;
92 92
   font-weight: 800;
93 93
   cursor: pointer;
@@ -95,23 +95,27 @@
95 95
 }
96 96
 
97 97
 .edit-button:hover {
98
-  background: #765639;
98
+  background: #f6efe6;
99 99
 }
100 100
 
101 101
 .family-summary {
102
-  min-height: 82px;
102
+  min-height: 64px;
103 103
   margin-bottom: 28px;
104
-  padding: 14px 26px;
104
+  padding: 12px 22px;
105 105
   border: 2px solid #d8caba;
106 106
   border-radius: 14px;
107 107
   background: #eadfce;
108
-  display: grid;
109
-  grid-template-columns: 1.2fr 1.6fr auto;
108
+  display: flex;
110 109
   align-items: center;
111
-  column-gap: 24px;
112 110
   box-sizing: border-box;
113 111
 }
114 112
 
113
+.family-name-area {
114
+  display: flex;
115
+  align-items: baseline;
116
+  gap: 18px;
117
+}
118
+
115 119
 .family-name {
116 120
   margin: 0;
117 121
   color: #2f2720;
@@ -121,7 +125,7 @@
121 125
 }
122 126
 
123 127
 .family-head {
124
-  margin: 2px 0 0;
128
+  margin: 0;
125 129
   color: #6f6257;
126 130
   font-size: 16px;
127 131
 }
@@ -153,11 +157,11 @@
153 157
   width: 140px;
154 158
   height: 46px;
155 159
   margin-top: 36px;
156
-  border: 2px solid #d8caba;
160
+  border: 2px solid #8a6543;
157 161
   border-radius: 6px;
158 162
   background: #ffffff;
159
-  color: #2f2720;
160
-  font-size: 16px;
163
+  color: #8a6543;
164
+  font-size: 18px;
161 165
   font-weight: 800;
162 166
   cursor: pointer;
163 167
   box-sizing: border-box;
@@ -167,16 +171,37 @@
167 171
   background: #f6efe6;
168 172
 }
169 173
 
174
+.family-page-add-button {
175
+  width: 140px;
176
+  height: 46px;
177
+  margin-top: 36px;
178
+  border: 2px solid #8a6543;
179
+  border-radius: 6px;
180
+  background: #ffffff;
181
+  color: #8a6543;
182
+  font-size: 18px;
183
+  font-weight: 800;
184
+  text-decoration: none;
185
+  display: flex;
186
+  align-items: center;
187
+  justify-content: center;
188
+  box-sizing: border-box;
189
+}
190
+
191
+.family-page-add-button:hover {
192
+  background: #f6efe6;
193
+}
194
+
170 195
 
171 196
 .detail-content {
172 197
   display: grid;
173
-  grid-template-columns: minmax(0, 1fr) 500px;
174
-  gap: 48px;
198
+  grid-template-columns: minmax(0, 1fr) 460px;
199
+  gap: 28px;
175 200
   align-items: start;
176 201
 }
177 202
 
178 203
 .basic-info-section {
179
-  padding-left: 8px;
204
+  padding-left: 0;
180 205
 }
181 206
 
182 207
 .section-heading {
@@ -197,13 +222,24 @@
197 222
 }
198 223
 
199 224
 .info-form {
200
-  width: 650px;
225
+  width: 100%;
226
+}
227
+
228
+.info-pair-row {
229
+  display: grid;
230
+  grid-template-columns: 1fr 1fr;
231
+  gap: 14px;
232
+  margin-top: 10px;
201 233
 }
202 234
 
203 235
 .info-row {
204 236
   display: grid;
205
-  grid-template-columns: 120px 1fr;
237
+  grid-template-columns: 96px 1fr;
206 238
   align-items: center;
239
+  margin-top: 0;
240
+}
241
+
242
+.info-form > .info-row {
207 243
   margin-top: 10px;
208 244
 }
209 245
 
@@ -228,7 +264,6 @@
228 264
 
229 265
 .phone-row {
230 266
   align-items: start;
231
-  margin-top: 18px;
232 267
 }
233 268
 
234 269
 .phone-row .info-label {
@@ -242,7 +277,7 @@
242 277
 .phone-header,
243 278
 .phone-item {
244 279
   display: grid;
245
-  grid-template-columns: 1fr 1.4fr 1.2fr;
280
+  grid-template-columns: 1fr 1fr;
246 281
   align-items: center;
247 282
 }
248 283
 
@@ -272,7 +307,7 @@
272 307
 
273 308
 .status-panel {
274 309
   min-height: 382px;
275
-  padding: 30px 24px 22px;
310
+  padding: 24px 22px 22px;
276 311
   border: 2px solid #d8caba;
277 312
   border-radius: 62px;
278 313
   background: #fffdf9;
@@ -421,23 +456,39 @@
421 456
   justify-content: space-between;
422 457
   align-items: center;
423 458
   gap: 16px;
424
-  padding: 16px 20px;
459
+  padding: 14px 18px;
425 460
   margin-bottom: 20px;
426 461
   background: #efe4d6;
427 462
   border: 2px solid #d8c2aa;
428 463
   border-radius: 12px;
429 464
 }
430 465
 
431
-.family-list-title {
432
-  margin: 0;
433
-  font-size: 20px;
434
-  font-weight: 700;
466
+.family-search-box {
467
+  flex: 1;
468
+  min-width: 0;
469
+  display: grid;
470
+  grid-template-columns: 96px minmax(0, 1fr);
471
+  gap: 12px;
472
+  align-items: center;
435 473
 }
436 474
 
437
-.family-list-head {
438
-  margin: 0;
439
-  font-size: 14px;
440
-  color: #6f6256;
475
+.family-search-label {
476
+  color: #4b3c31;
477
+  font-size: 16px;
478
+  font-weight: 800;
479
+}
480
+
481
+.family-search-box input {
482
+  width: 100%;
483
+  height: 42px;
484
+  padding: 0 14px;
485
+  border: 2px solid #d8c2aa;
486
+  border-radius: 8px;
487
+  background: #fffdf9;
488
+  color: #2f2720;
489
+  font-size: 15px;
490
+  font-weight: 700;
491
+  box-sizing: border-box;
441 492
 }
442 493
 
443 494
 .family-table-section {
@@ -483,6 +534,32 @@
483 534
   background: #f6efe6;
484 535
 }
485 536
 
537
+.event-status-select {
538
+  width: 84px;
539
+  height: 34px;
540
+  padding: 0 8px;
541
+  border: 2px solid #d8c2aa;
542
+  border-radius: 6px;
543
+  background: #ffffff;
544
+  color: #6f6256;
545
+  font-size: 13px;
546
+  font-weight: 800;
547
+  cursor: pointer;
548
+  box-sizing: border-box;
549
+  outline: none;
550
+}
551
+
552
+.event-status-select.sent {
553
+  border-color: #8a6543;
554
+  background: #f1e7d8;
555
+  color: #8a6543;
556
+}
557
+
558
+.event-status-select:focus {
559
+  border-color: #8a6543;
560
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.15);
561
+}
562
+
486 563
 .family-table-header {
487 564
   padding: 12px 14px;
488 565
   background: #efe4d6;
@@ -490,7 +567,7 @@
490 567
   border-radius: 10px;
491 568
   font-size: 14px;
492 569
   font-weight: 700;
493
-  color: #5d3b24;
570
+  color: #111111;
494 571
 }
495 572
 
496 573
 .family-table-row {
@@ -500,7 +577,7 @@
500 577
   border: 2px solid #d8c2aa;
501 578
   border-radius: 10px;
502 579
   font-size: 15px;
503
-  color: #2f2923;
580
+  color: #111111;
504 581
 }
505 582
 
506 583
 .family-table-row:hover {
@@ -512,7 +589,88 @@
512 589
   margin: 0;
513 590
   font-size: 15px;
514 591
   font-weight: 700;
515
-  color: #5d3b24;
592
+  color: #111111;
593
+}
594
+
595
+.family-member-table {
596
+  gap: 0;
597
+  border: 2px solid #d8c2aa;
598
+  border-radius: 8px;
599
+  overflow: hidden;
600
+}
601
+
602
+.family-member-table .family-table-header,
603
+.family-member-table .family-table-row {
604
+  grid-template-columns: 1.35fr 0.75fr 1.05fr 1fr 1.2fr 168px;
605
+  gap: 12px;
606
+  border: 0;
607
+  border-radius: 0;
608
+}
609
+
610
+.family-member-table .family-table-header {
611
+  min-height: 46px;
612
+  padding: 0 14px;
613
+  background: #efe4d6;
614
+}
615
+
616
+.family-member-table .family-table-row {
617
+  min-height: 78px;
618
+  padding: 10px 14px;
619
+  background: #fffdf9;
620
+  border-top: 1px solid #d8c2aa;
621
+}
622
+
623
+.family-member-table .family-table-row:hover {
624
+  background: #fff8ee;
625
+}
626
+
627
+.family-person-sub,
628
+.family-person-date {
629
+  margin: 0;
630
+}
631
+
632
+.family-person-sub {
633
+  margin-top: 4px;
634
+  color: #111111;
635
+  font-size: 13px;
636
+  line-height: 1.35;
637
+}
638
+
639
+.family-person-date {
640
+  color: #111111;
641
+  font-size: 15px;
642
+}
643
+
644
+.family-member-table .family-table-action {
645
+  justify-content: center;
646
+  gap: 8px;
647
+}
648
+
649
+.family-member-table .family-table-header > div:last-child {
650
+  text-align: center;
651
+}
652
+
653
+.family-member-table .family-edit-link {
654
+  width: 74px;
655
+}
656
+
657
+.kakocho-member-table .family-table-header,
658
+.kakocho-member-table .family-table-row {
659
+  grid-template-columns: 1.25fr 1.15fr 1.05fr 0.65fr 0.6fr 1.1fr 82px;
660
+}
661
+
662
+.event-member-table .family-table-header,
663
+.event-member-table .family-table-row {
664
+  grid-template-columns: 1.35fr 1fr 0.75fr 1.05fr 0.95fr 96px;
665
+}
666
+
667
+.event-member-table .event-status-select {
668
+  width: 90px;
669
+}
670
+
671
+.event-member-table .family-table-header > div:last-child,
672
+.event-member-table .family-table-row > div:last-child {
673
+  text-align: center;
516 674
 }
517 675
 
518 676
 .empty-family-message {
@@ -556,6 +714,16 @@
556 714
   margin-top: 24px;
557 715
 }
558 716
 
717
+.coming-soon-section .section-heading {
718
+  margin-bottom: 16px;
719
+
720
+  h2 {
721
+    margin: 0;
722
+    font-size: 20px;
723
+    font-weight: 700;
724
+  }
725
+}
726
+
559 727
 /* =========================
560 728
    家系図タブ
561 729
 ========================= */
@@ -1023,6 +1191,11 @@
1023 1191
     flex-direction: column;
1024 1192
   }
1025 1193
 
1194
+  .family-search-box {
1195
+    width: 100%;
1196
+    grid-template-columns: 1fr;
1197
+  }
1198
+
1026 1199
   .family-table {
1027 1200
     overflow-x: auto;
1028 1201
   }

+ 32
- 4
src/app/pages/danka-edit/danka-edit.html 파일 보기

@@ -17,7 +17,7 @@
17 17
 
18 18
             <div class="form-list">
19 19
               <div class="form-row">
20
-                <label for="householdName">世帯名</label>
20
+                <label for="householdName">檀家名</label>
21 21
                 <div class="form-field">
22 22
                   <input
23 23
                     id="householdName"
@@ -25,13 +25,22 @@
25 25
                     formControlName="householdName"
26 26
                   />
27 27
                   @if (dankaForm.get('householdName')?.invalid && dankaForm.get('householdName')?.touched) {
28
-                    <p class="error-message">世帯名を入力してください。</p>
28
+                    <p class="error-message">檀家名を入力してください。</p>
29 29
                   }
30 30
                 </div>
31 31
               </div>
32 32
 
33 33
               <div class="form-row">
34
-                <label for="householder">世帯主</label>
34
+                <label for="householdFurigana">檀家名ふりがな</label>
35
+                <input
36
+                  id="householdFurigana"
37
+                  type="text"
38
+                  formControlName="householdFurigana"
39
+                />
40
+              </div>
41
+
42
+              <div class="form-row">
43
+                <label for="householder">施主名</label>
35 44
                 <div class="form-field">
36 45
                   <input
37 46
                     id="householder"
@@ -39,11 +48,20 @@
39 48
                     formControlName="householder"
40 49
                   />
41 50
                   @if (dankaForm.get('householder')?.invalid && dankaForm.get('householder')?.touched) {
42
-                    <p class="error-message">世帯主を入力してください。</p>
51
+                    <p class="error-message">施主名を入力してください。</p>
43 52
                   }
44 53
                 </div>
45 54
               </div>
46 55
 
56
+              <div class="form-row">
57
+                <label for="householderFurigana">施主名ふりがな</label>
58
+                <input
59
+                  id="householderFurigana"
60
+                  type="text"
61
+                  formControlName="householderFurigana"
62
+                />
63
+              </div>
64
+
47 65
               <div class="form-row">
48 66
                 <label for="postalCode">郵便番号</label>
49 67
                 <div class="form-field">
@@ -66,6 +84,16 @@
66 84
                   formControlName="address"
67 85
                 />
68 86
               </div>
87
+
88
+              <div class="form-row note-row">
89
+                <label for="note">備考</label>
90
+                <textarea
91
+                  id="note"
92
+                  formControlName="note"
93
+                  rows="4"
94
+                  placeholder="檀家に関する連絡事項や注意点を入力"
95
+                ></textarea>
96
+              </div>
69 97
             </div>
70 98
           </section>
71 99
 

+ 26
- 5
src/app/pages/danka-edit/danka-edit.scss 파일 보기

@@ -49,6 +49,7 @@
49 49
 }
50 50
 
51 51
 .edit-form input,
52
+.edit-form textarea,
52 53
 .edit-form button {
53 54
   font-family: inherit;
54 55
 }
@@ -93,7 +94,7 @@
93 94
 
94 95
 .form-row {
95 96
   display: grid;
96
-  grid-template-columns: 120px 1fr;
97
+  grid-template-columns: 160px 1fr;
97 98
   align-items: center;
98 99
   gap: 16px;
99 100
   margin-bottom: 14px;
@@ -105,10 +106,9 @@
105 106
   font-weight: 800;
106 107
 }
107 108
 
108
-.form-row input {
109
+.form-row input,
110
+.form-row textarea {
109 111
   width: 100%;
110
-  height: 54px;
111
-  padding: 0 14px;
112 112
   border: 2px solid #d8caba;
113 113
   border-radius: 8px;
114 114
   background: #fffdf9;
@@ -119,7 +119,28 @@
119 119
   outline: none;
120 120
 }
121 121
 
122
-.form-row input:focus {
122
+.form-row input {
123
+  height: 54px;
124
+  padding: 0 14px;
125
+}
126
+
127
+.form-row textarea {
128
+  min-height: 112px;
129
+  padding: 12px 14px;
130
+  line-height: 1.6;
131
+  resize: vertical;
132
+}
133
+
134
+.note-row {
135
+  align-items: start;
136
+}
137
+
138
+.note-row label {
139
+  padding-top: 12px;
140
+}
141
+
142
+.form-row input:focus,
143
+.form-row textarea:focus {
123 144
   border-color: #8a6543;
124 145
   box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.15);
125 146
 }

+ 9
- 0
src/app/pages/danka-edit/danka-edit.ts 파일 보기

@@ -24,9 +24,12 @@ export class DankaEdit {
24 24
 
25 25
   dankaForm = new FormGroup({
26 26
     householdName: new FormControl('', [Validators.required]),
27
+    householdFurigana: new FormControl(''),
27 28
     householder: new FormControl('', [Validators.required]),
29
+    householderFurigana: new FormControl(''),
28 30
     postalCode: new FormControl('', Validators.pattern(/^\d{3}-\d{4}$/)),
29 31
     address: new FormControl(''),
32
+    note: new FormControl(''),
30 33
     phones: new FormArray([this.createPhoneForm('', '')]),
31 34
   });
32 35
 
@@ -42,9 +45,12 @@ export class DankaEdit {
42 45
       if (this.danka) {
43 46
         this.dankaForm.patchValue({
44 47
           householdName: this.danka.householdName,
48
+          householdFurigana: this.danka.householdFurigana,
45 49
           householder: this.danka.householder,
50
+          householderFurigana: this.danka.householderFurigana,
46 51
           postalCode: this.danka.postalCode,
47 52
           address: this.danka.address,
53
+          note: this.danka.note,
48 54
         });
49 55
 
50 56
         this.phones.clear();
@@ -90,9 +96,12 @@ export class DankaEdit {
90 96
     const updatedDanka: Danka = {
91 97
       id: dankaId,
92 98
       householdName: formValue.householdName?.trim() ?? '',
99
+      householdFurigana: formValue.householdFurigana?.trim() ?? '',
93 100
       householder: formValue.householder?.trim() ?? '',
101
+      householderFurigana: formValue.householderFurigana?.trim() ?? '',
94 102
       postalCode: formValue.postalCode?.trim() ?? '',
95 103
       address: formValue.address?.trim() ?? '',
104
+      note: formValue.note?.trim() ?? '',
96 105
       updatedAt: this.formatDateForSave(new Date()),
97 106
       phones: (formValue.phones ?? [])
98 107
         .map((phone) => ({

+ 10
- 6
src/app/pages/danka-list/danka-list.html 파일 보기

@@ -51,11 +51,10 @@
51 51
       <div class="list-content">
52 52
         <div class="danka-table">
53 53
           <div class="danka-table-header">
54
-            <div>世帯主</div>
55
-            <div>世帯名</div>
54
+            <div>檀家名・ふりがな</div>
55
+            <div>施主名・ふりがな</div>
56 56
             <div>住所</div>
57 57
             <div>電話</div>
58
-            <div>家族</div>
59 58
           </div>
60 59
 
61 60
           @if (filterDankaList.length === 0) {
@@ -64,11 +63,16 @@
64 63
 
65 64
           @for (danka of filterDankaList; track danka.id) {
66 65
             <a class="danka-table-row" [routerLink]="['/danka-detail', danka.id]">
67
-              <div class="strong">{{ danka.householder }}</div>
68
-              <div>{{ danka.householdName }}</div>
66
+              <div>
67
+                <p class="danka-name">{{ danka.householdName }}</p>
68
+                <p class="danka-sub">{{ danka.householdFurigana || 'ふりがな未登録' }}</p>
69
+              </div>
70
+              <div>
71
+                <p class="danka-name">{{ danka.householder }}</p>
72
+                <p class="danka-sub">{{ danka.householderFurigana || 'ふりがな未登録' }}</p>
73
+              </div>
69 74
               <div>{{ danka.address }}</div>
70 75
               <div>{{ danka.phones[0]?.tel }}</div>
71
-              <div>{{ danka.phones.length }}件</div>
72 76
             </a>
73 77
           }
74 78
         </div>

+ 36
- 41
src/app/pages/danka-list/danka-list.scss 파일 보기

@@ -43,7 +43,7 @@
43 43
 
44 44
 .search-area {
45 45
   display: grid;
46
-  grid-template-columns: 1fr 140px 190px 110px;
46
+  grid-template-columns: 1fr 140px 140px;
47 47
   gap: 18px;
48 48
   align-items: center;
49 49
   margin-bottom: 18px;
@@ -90,33 +90,21 @@
90 90
 .new-button,
91 91
 .condition-button {
92 92
   height: 58px;
93
-  border: 2px solid #d8caba;
93
+  border: 2px solid #8a6543;
94 94
   border-radius: 8px;
95
-  color: #2f2720;
95
+  background: #ffffff;
96
+  color: #8a6543;
96 97
   font-size: 18px;
97 98
   font-weight: 800;
98 99
   cursor: pointer;
99 100
   box-sizing: border-box;
100 101
 }
101 102
 
102
-.search-button {
103
-  background: #8a6543;
104
-  border-color: #8a6543;
105
-  color: #ffffff;
106
-}
107
-
108
-.new-button {
109
-  background: #e6d8c4;
110
-}
111
-
112 103
 .condition-button {
113 104
   background: #ffffff;
114 105
 }
115 106
 
116
-.search-button:hover {
117
-  background: #765639;
118
-}
119
-
107
+.search-button:hover,
120 108
 .new-button:hover,
121 109
 .condition-button:hover {
122 110
   background: #f6efe6;
@@ -178,59 +166,66 @@
178 166
 
179 167
 .list-content {
180 168
   position: relative;
181
-  padding-right: 172px;
182 169
 }
183 170
 
184 171
 .danka-table {
185 172
   width: 100%;
173
+  display: grid;
174
+  gap: 0;
175
+  border: 2px solid #d8c2aa;
176
+  border-radius: 8px;
177
+  overflow: hidden;
186 178
 }
187 179
 
188 180
 .danka-table-header,
189 181
 .danka-table-row {
190 182
   display: grid;
191
-  grid-template-columns: 1.5fr 1.1fr 2.8fr 1.25fr 0.65fr;
183
+  grid-template-columns: 1.2fr 1.2fr 2.2fr 1fr;
192 184
   align-items: center;
193
-  column-gap: 16px;
185
+  justify-items: start;
186
+  gap: 12px;
187
+  text-align: left;
194 188
 }
195 189
 
196 190
 .danka-table-header {
197
-  min-height: 40px;
198
-  padding: 0 12px;
199
-  background: #eadfce;
200
-  border: 2px solid #d8caba;
201
-  border-radius: 6px;
191
+  min-height: 46px;
192
+  padding: 0 14px;
193
+  background: #efe4d6;
202 194
   box-sizing: border-box;
203
-  color: #5a4a3c;
204
-  font-size: 15px;
205
-  font-weight: 700;
195
+  color: #111111;
196
+  font-size: 14px;
197
+  font-weight: 800;
206 198
 }
207 199
 
208 200
 .danka-table-row {
209
-  min-height: 66px;
210
-  margin-top: 4px;
211
-  padding: 0 12px;
212
-  background: #fbf7f1;
213
-  border: 2px solid #d8caba;
214
-  border-radius: 8px;
201
+  min-height: 78px;
202
+  padding: 10px 14px;
203
+  background: #fffdf9;
204
+  border-top: 1px solid #d8c2aa;
215 205
   box-sizing: border-box;
216
-  color: #2f2720;
217
-  font-size: 16px;
206
+  color: #111111;
207
+  font-size: 15px;
218 208
   text-decoration: none;
219 209
 }
220 210
 
221 211
 .danka-table-row:hover {
222
-  background: #f3eadc;
212
+  background: #fff8ee;
223 213
 }
224 214
 
225
-.danka-table-row .strong {
215
+.danka-name {
216
+  margin: 0;
226 217
   font-weight: 800;
227 218
 }
228 219
 
220
+.danka-sub {
221
+  margin: 4px 0 0;
222
+  color: #111111;
223
+  font-size: 13px;
224
+  line-height: 1.35;
225
+}
226
+
229 227
 .empty-message {
230
-  margin-top: 16px;
231 228
   padding: 24px;
232
-  border: 2px dashed #d8caba;
233
-  border-radius: 12px;
234 229
   background: #fffdf9;
235 230
   color: #7b6b5c;
236 231
   font-size: 16px;

+ 2
- 0
src/app/pages/danka-list/danka-list.ts 파일 보기

@@ -105,7 +105,9 @@ export class DankaList {
105 105
     this.filterDankaList = this.dankaList.filter((danka) => {
106 106
       return (
107 107
         danka.householdName.includes(keyword) ||
108
+        danka.householdFurigana.includes(keyword) ||
108 109
         danka.householder.includes(keyword) ||
110
+        danka.householderFurigana.includes(keyword) ||
109 111
         danka.postalCode.includes(keyword) ||
110 112
         danka.address.includes(keyword) ||
111 113
         danka.phones.some((phone) => phone.tel.includes(keyword) || phone.note.includes(keyword))

+ 1
- 1
src/app/pages/dashboard/dashboard.html 파일 보기

@@ -51,7 +51,7 @@
51 51
 
52 52
         <div class="recent-table" role="table" aria-label="最近開いた檀家">
53 53
           <div class="recent-row recent-row-head" role="row">
54
-            <div class="cell" role="columnheader">世帯主</div>
54
+            <div class="cell" role="columnheader">施主名</div>
55 55
             <div class="cell" role="columnheader">ふりがな</div>
56 56
             <div class="cell" role="columnheader">住所</div>
57 57
             <div class="cell" role="columnheader">次の法要</div>

+ 130
- 0
src/app/pages/event/event.html 파일 보기

@@ -0,0 +1,130 @@
1
+<app-header></app-header>
2
+
3
+<div class="event-page">
4
+  <app-side-menu></app-side-menu>
5
+
6
+  <main class="event-main">
7
+    <section class="event-panel">
8
+      <div class="page-title-row">
9
+        <div class="title-filter-area">
10
+          <h1>行事対象者一覧</h1>
11
+
12
+          <div class="filter-row">
13
+            <div class="filter-field">
14
+              <label for="targetYear">年度</label>
15
+              <select
16
+                id="targetYear"
17
+                [(ngModel)]="targetYear"
18
+                (ngModelChange)="createEventTargetList()"
19
+              >
20
+                @for (year of yearOptions; track year) {
21
+                  <option [ngValue]="year">{{ year }}年度</option>
22
+                }
23
+              </select>
24
+            </div>
25
+
26
+            <div class="filter-field">
27
+              <label for="eventType">行事</label>
28
+              <select
29
+                id="eventType"
30
+                [(ngModel)]="selectedEventType"
31
+                (ngModelChange)="changeEventType($event)"
32
+              >
33
+                @for (filter of eventTypeFilters; track filter.value) {
34
+                  <option [ngValue]="filter.value">{{ filter.label }}</option>
35
+                }
36
+              </select>
37
+            </div>
38
+
39
+            <div class="filter-field">
40
+              <label for="eventStatus">状態</label>
41
+              <select id="eventStatus" [(ngModel)]="selectedStatus">
42
+                @for (filter of statusFilters; track filter.value) {
43
+                  <option [ngValue]="filter.value">{{ filter.label }}</option>
44
+                }
45
+              </select>
46
+            </div>
47
+
48
+            <div class="search-field">
49
+              <label for="eventSearch">検索</label>
50
+              <input
51
+                id="eventSearch"
52
+                type="text"
53
+                [(ngModel)]="searchKeyword"
54
+                placeholder="氏名・ふりがな・檀家名で検索"
55
+              />
56
+            </div>
57
+          </div>
58
+        </div>
59
+      </div>
60
+
61
+      <div class="list-header-row">
62
+        <h2>対象 {{ filteredEventTargets.length }} 名</h2>
63
+
64
+        <p>並び順: 行事 / 年齢 / 氏名</p>
65
+      </div>
66
+
67
+      <section class="event-table-section">
68
+        <div class="event-table">
69
+          <div class="event-table-header">
70
+            <div>氏名・ふりがな</div>
71
+            <div>檀家名</div>
72
+            <div>続柄</div>
73
+            <div>生年月日・年齢</div>
74
+            <div>対象行事</div>
75
+            <div>備考</div>
76
+            <div>状態</div>
77
+          </div>
78
+
79
+          @if (filteredEventTargets.length > 0) {
80
+            @for (target of filteredEventTargets; track target.id) {
81
+              <div class="event-table-row">
82
+                <div>
83
+                  <p class="person-name">{{ target.name }}</p>
84
+                  <p class="person-sub">{{ target.furigana || 'ふりがな未登録' }}</p>
85
+                </div>
86
+                <div>
87
+                  {{ target.householdName }}
88
+                </div>
89
+                <div>
90
+                  {{ target.relationship }}
91
+                </div>
92
+                <div>
93
+                  <p class="person-date">{{ target.birthDate }}</p>
94
+                  <p class="person-sub">{{ target.age }}歳</p>
95
+                </div>
96
+                <div class="event-type">
97
+                  {{ target.eventType }}
98
+                </div>
99
+                <div>
100
+                  {{ target.note || '' }}
101
+                </div>
102
+                <div>
103
+                  <select
104
+                    class="status-select"
105
+                    [class.sent]="target.status === '案内済'"
106
+                    [ngModel]="target.status"
107
+                    (ngModelChange)="changeStatus(target, $event)"
108
+                    aria-label="状態"
109
+                  >
110
+                    @for (status of eventStatuses; track status) {
111
+                      <option [value]="status">{{ status }}</option>
112
+                    }
113
+                  </select>
114
+                </div>
115
+              </div>
116
+            }
117
+          } @else {
118
+            <div class="empty-message">
119
+              @if (eventTargets.length > 0) {
120
+                検索条件に一致する行事対象者はありません。
121
+              } @else {
122
+                対象となる行事対象者はありません。
123
+              }
124
+            </div>
125
+          }
126
+        </div>
127
+      </section>
128
+    </section>
129
+  </main>
130
+</div>

+ 287
- 0
src/app/pages/event/event.scss 파일 보기

@@ -0,0 +1,287 @@
1
+:host {
2
+  position: relative;
3
+  display: block;
4
+  min-height: 100vh;
5
+  background: #f4eee4;
6
+  color: #2f2720;
7
+}
8
+
9
+.event-page {
10
+  display: flex;
11
+  align-items: flex-start;
12
+  gap: 8px;
13
+  background: #f4eee4;
14
+}
15
+
16
+.event-main {
17
+  flex: 1;
18
+  padding-right: 34px;
19
+  box-sizing: border-box;
20
+}
21
+
22
+.event-panel {
23
+  min-height: 650px;
24
+  padding: 26px 34px 36px;
25
+  background: #ffffff;
26
+  border: 2px solid #d8caba;
27
+  border-radius: 76px;
28
+  box-sizing: border-box;
29
+}
30
+
31
+.page-title-row {
32
+  display: flex;
33
+  justify-content: space-between;
34
+  align-items: flex-start;
35
+  gap: 24px;
36
+  margin-bottom: 22px;
37
+}
38
+
39
+.title-filter-area {
40
+  display: grid;
41
+  gap: 12px;
42
+  min-width: 0;
43
+}
44
+
45
+.page-title-row h1 {
46
+  margin: 0;
47
+  color: #2f2720;
48
+  font-size: 32px;
49
+  line-height: 1.2;
50
+  font-weight: 800;
51
+  letter-spacing: 0.02em;
52
+}
53
+
54
+.filter-row {
55
+  display: flex;
56
+  align-items: flex-end;
57
+  gap: 14px 18px;
58
+  flex-wrap: wrap;
59
+  width: 100%;
60
+}
61
+
62
+.filter-field,
63
+.search-field {
64
+  display: grid;
65
+  gap: 6px;
66
+}
67
+
68
+.filter-field label,
69
+.search-field label {
70
+  color: #4b3c31;
71
+  font-size: 14px;
72
+  font-weight: 800;
73
+}
74
+
75
+.filter-field select,
76
+.search-field input {
77
+  height: 38px;
78
+  padding: 0 14px;
79
+  border: 2px solid #d8caba;
80
+  border-radius: 8px;
81
+  background: #fffdf9;
82
+  color: #2f2720;
83
+  font-size: 15px;
84
+  font-weight: 700;
85
+  box-sizing: border-box;
86
+  outline: none;
87
+}
88
+
89
+.filter-field select {
90
+  width: 148px;
91
+  cursor: pointer;
92
+}
93
+
94
+.search-field {
95
+  flex: 1 1 260px;
96
+  min-width: 260px;
97
+}
98
+
99
+.search-field input {
100
+  width: 100%;
101
+}
102
+
103
+.filter-field select:focus,
104
+.search-field input:focus {
105
+  border-color: #8a6543;
106
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.12);
107
+}
108
+
109
+.list-header-row {
110
+  display: flex;
111
+  justify-content: space-between;
112
+  align-items: flex-end;
113
+  margin: 18px 0 10px;
114
+}
115
+
116
+.list-header-row h2 {
117
+  margin: 0;
118
+  color: #2f2720;
119
+  font-size: 26px;
120
+  font-weight: 800;
121
+}
122
+
123
+.list-header-row p {
124
+  margin: 0;
125
+  color: #7b6b5c;
126
+  font-size: 14px;
127
+}
128
+
129
+.event-table-section {
130
+  margin-top: 8px;
131
+}
132
+
133
+.event-table {
134
+  display: grid;
135
+  gap: 0;
136
+  border: 2px solid #d8c2aa;
137
+  border-radius: 8px;
138
+  overflow: hidden;
139
+}
140
+
141
+.event-table-header,
142
+.event-table-row {
143
+  display: grid;
144
+  grid-template-columns: 1.25fr 0.95fr 0.68fr 0.95fr 0.82fr 1fr 96px;
145
+  align-items: center;
146
+  gap: 12px;
147
+}
148
+
149
+.event-table-header {
150
+  min-height: 46px;
151
+  padding: 0 14px;
152
+  background: #efe4d6;
153
+  color: #111111;
154
+  font-size: 14px;
155
+  font-weight: 800;
156
+  box-sizing: border-box;
157
+}
158
+
159
+.event-table-row {
160
+  min-height: 78px;
161
+  padding: 10px 14px;
162
+  border-top: 1px solid #d8c2aa;
163
+  background: #fffdf9;
164
+  color: #111111;
165
+  font-size: 15px;
166
+  box-sizing: border-box;
167
+}
168
+
169
+.event-table-row:hover {
170
+  background: #fff8ee;
171
+}
172
+
173
+.person-name,
174
+.event-type {
175
+  margin: 0;
176
+  font-weight: 800;
177
+}
178
+
179
+.person-sub,
180
+.person-date {
181
+  margin: 0;
182
+}
183
+
184
+.person-sub {
185
+  margin-top: 4px;
186
+  color: #111111;
187
+  font-size: 13px;
188
+  line-height: 1.35;
189
+}
190
+
191
+.person-date {
192
+  color: #111111;
193
+  font-size: 15px;
194
+}
195
+
196
+.event-type {
197
+  color: #111111;
198
+}
199
+
200
+.status-select {
201
+  width: 90px;
202
+  height: 34px;
203
+  padding: 0 10px;
204
+  border: 2px solid #d8caba;
205
+  border-radius: 8px;
206
+  background: #ffffff;
207
+  color: #111111;
208
+  font-size: 14px;
209
+  font-weight: 800;
210
+  box-sizing: border-box;
211
+  cursor: pointer;
212
+  outline: none;
213
+}
214
+
215
+.status-select.sent {
216
+  border-color: #8a6543;
217
+  background: #f1e7d8;
218
+  color: #111111;
219
+}
220
+
221
+.status-select:focus {
222
+  border-color: #8a6543;
223
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.15);
224
+}
225
+
226
+.empty-message {
227
+  min-height: 58px;
228
+  padding: 18px 20px;
229
+  background: #fffdf9;
230
+  color: #7b6b5c;
231
+  font-size: 15px;
232
+  font-weight: 700;
233
+  box-sizing: border-box;
234
+}
235
+
236
+@media (max-width: 1100px) {
237
+  .page-title-row {
238
+    flex-direction: column;
239
+  }
240
+
241
+  .event-table {
242
+    overflow-x: auto;
243
+  }
244
+
245
+  .event-table-header,
246
+  .event-table-row {
247
+    min-width: 980px;
248
+  }
249
+}
250
+
251
+@media (max-width: 800px) {
252
+  .event-page {
253
+    flex-direction: column;
254
+  }
255
+
256
+  .event-main {
257
+    width: 100%;
258
+    padding: 16px 20px 32px;
259
+  }
260
+
261
+  .event-panel {
262
+    padding: 24px 20px 30px;
263
+    border-radius: 32px;
264
+  }
265
+
266
+  .page-title-row h1 {
267
+    font-size: 26px;
268
+  }
269
+
270
+  .filter-row {
271
+    align-items: flex-start;
272
+    flex-direction: column;
273
+    gap: 14px;
274
+  }
275
+
276
+  .filter-field,
277
+  .filter-field select,
278
+  .search-field {
279
+    width: 100%;
280
+  }
281
+
282
+  .list-header-row {
283
+    align-items: flex-start;
284
+    flex-direction: column;
285
+    gap: 8px;
286
+  }
287
+}

+ 22
- 0
src/app/pages/event/event.spec.ts 파일 보기

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

+ 157
- 0
src/app/pages/event/event.ts 파일 보기

@@ -0,0 +1,157 @@
1
+import { Component } from '@angular/core';
2
+import { FormsModule } from '@angular/forms';
3
+import { DankaService } from '../../services/dankaService';
4
+import { FamilyService } from '../../services/family-service';
5
+import { EventStatus, EventTarget, EventType } from '../../models/event';
6
+import { AppHeader } from '../../share/header/app-header';
7
+import { AppSideMenu } from '../../share/side-menu/app-side-menu';
8
+
9
+@Component({
10
+  selector: 'app-event',
11
+  imports: [AppHeader, AppSideMenu, FormsModule],
12
+  templateUrl: './event.html',
13
+  styleUrl: './event.scss',
14
+})
15
+export class EventPage {
16
+  eventTargets: EventTarget[] = [];
17
+  targetYear: number = new Date().getFullYear();
18
+  selectedEventType: EventType | 'all' = 'all';
19
+  selectedStatus: EventStatus | 'all' = 'all';
20
+  searchKeyword = '';
21
+  eventStatuses: EventStatus[] = ['未案内', '案内済'];
22
+  yearOptions: number[] = [
23
+    this.targetYear - 1,
24
+    this.targetYear,
25
+    this.targetYear + 1,
26
+    this.targetYear + 2,
27
+  ];
28
+  eventTypeFilters: { label: string; value: EventType | 'all' }[] = [
29
+    { label: 'すべて', value: 'all' },
30
+    { label: '稚児行列', value: '稚児行列' },
31
+    { label: '七五三', value: '七五三' },
32
+    { label: '成人式', value: '成人式' },
33
+    { label: '米寿', value: '米寿' },
34
+  ];
35
+  statusFilters: { label: string; value: EventStatus | 'all' }[] = [
36
+    { label: 'すべて', value: 'all' },
37
+    { label: '未案内', value: '未案内' },
38
+    { label: '案内済', value: '案内済' },
39
+  ];
40
+  private statusByTargetId: Record<string, EventStatus> = {};
41
+
42
+  constructor(
43
+    private dankaService: DankaService,
44
+    private familyService: FamilyService,
45
+  ) {
46
+    this.createEventTargetList();
47
+  }
48
+
49
+  createEventTargetList(): void {
50
+    this.eventTargets = [];
51
+
52
+    this.familyService.getFamilyList().forEach((family) => {
53
+      const birthDate = this.parseDate(family.birthDate);
54
+      if (!birthDate) {
55
+        return;
56
+      }
57
+
58
+      const age = this.targetYear - birthDate.getFullYear();
59
+      const eventTypes = this.getEventTypes(age);
60
+      if (eventTypes.length === 0) {
61
+        return;
62
+      }
63
+
64
+      const danka = this.dankaService.getDankaById(family.dankaId);
65
+      eventTypes.forEach((eventType) => {
66
+        if (this.selectedEventType !== 'all' && this.selectedEventType !== eventType) {
67
+          return;
68
+        }
69
+
70
+        const id = `${family.id}-${eventType}`;
71
+        this.eventTargets.push({
72
+          id,
73
+          dankaId: family.dankaId,
74
+          name: family.name,
75
+          furigana: family.furigana,
76
+          householdName: danka?.householdName ?? '不明',
77
+          relationship: family.relationship || '未登録',
78
+          birthDate: family.birthDate,
79
+          age,
80
+          eventType,
81
+          note: family.note,
82
+          status: this.statusByTargetId[id] ?? (Number(family.id) % 2 === 0 ? '案内済' : '未案内'),
83
+        });
84
+      });
85
+    });
86
+
87
+    this.eventTargets.sort(
88
+      (a, b) =>
89
+        this.getEventSortOrder(a.eventType) - this.getEventSortOrder(b.eventType) ||
90
+        a.age - b.age ||
91
+        a.name.localeCompare(b.name, 'ja'),
92
+    );
93
+  }
94
+
95
+  changeEventType(eventType: EventType | 'all'): void {
96
+    this.selectedEventType = eventType;
97
+    this.createEventTargetList();
98
+  }
99
+
100
+  get filteredEventTargets(): EventTarget[] {
101
+    const keyword = this.searchKeyword.trim();
102
+
103
+    return this.eventTargets.filter((target) => {
104
+      const matchesStatus = this.selectedStatus === 'all' || target.status === this.selectedStatus;
105
+      const matchesKeyword =
106
+        !keyword ||
107
+        [
108
+          target.name,
109
+          target.furigana,
110
+          target.householdName,
111
+          target.relationship,
112
+          target.birthDate,
113
+          target.eventType,
114
+          target.note,
115
+          target.status,
116
+        ].some((value) => value.includes(keyword));
117
+
118
+      return matchesStatus && matchesKeyword;
119
+    });
120
+  }
121
+
122
+  changeStatus(target: EventTarget, status: EventStatus): void {
123
+    target.status = status;
124
+    this.statusByTargetId[target.id] = status;
125
+  }
126
+
127
+  private getEventTypes(age: number): EventType[] {
128
+    const eventTypes: EventType[] = [];
129
+
130
+    if (age >= 3 && age <= 12) {
131
+      eventTypes.push('稚児行列');
132
+    }
133
+    if ([3, 5, 7].includes(age)) {
134
+      eventTypes.push('七五三');
135
+    }
136
+    if (age === 20) {
137
+      eventTypes.push('成人式');
138
+    }
139
+    if (age === 88) {
140
+      eventTypes.push('米寿');
141
+    }
142
+
143
+    return eventTypes;
144
+  }
145
+
146
+  private getEventSortOrder(eventType: EventType): number {
147
+    return ['稚児行列', '七五三', '成人式', '米寿'].indexOf(eventType);
148
+  }
149
+
150
+  private parseDate(value: string): Date | null {
151
+    const [year, month, day] = value.split('-').map(Number);
152
+    if (!year || !month || !day) {
153
+      return null;
154
+    }
155
+    return new Date(year, month - 1, day);
156
+  }
157
+}

+ 4
- 4
src/app/pages/family-edit/family-edit.html 파일 보기

@@ -7,7 +7,7 @@
7 7
   <main class="danka-edit-main">
8 8
     <section class="edit-panel">
9 9
       <div class="page-title-area">
10
-        <h1>家族(個人)編集</h1>
10
+        <h1>{{ familyId ? '家族編集' : '家族追加' }}</h1>
11 11
       </div>
12 12
 
13 13
       <form [formGroup]="familyForm" class="edit-form">
@@ -44,7 +44,7 @@
44 44
                   <select id="relationship"
45 45
                           formControlName="relationship">
46 46
                     <option value="">選択してください</option>
47
-                    <option value="世帯主">世帯主</option>
47
+                    <option value="施主">施主</option>
48 48
                     <option value="配偶者">配偶者</option>
49 49
                     <option value="父">父</option>
50 50
                     <option value="母">母</option>
@@ -170,12 +170,12 @@
170 170
 
171 171
           <section class="phone-edit-section">
172 172
             <div class="householder-area">
173
-              <h3>この方を世帯主にする</h3>
173
+              <h3>この方を主にする</h3>
174 174
 
175 175
               <button type="button"
176 176
                       class="set-householder-button"
177 177
                       (click)="setAsHouseholder()">
178
-                世帯主に設定
178
+                主に設定
179 179
               </button>
180 180
             </div>
181 181
           </section>

+ 4
- 5
src/app/pages/kakocho-edit/kakocho-edit.html 파일 보기

@@ -10,9 +10,9 @@
10 10
       <div class="page-title-area">
11 11
         <h1>
12 12
           @if (kakocho) {
13
-            故人編集
13
+            故人編集
14 14
           } @else {
15
-            故人追加
15
+            故人追加
16 16
           }
17 17
         </h1>
18 18
       </div>
@@ -130,8 +130,7 @@
130 130
           <button
131 131
             type="button"
132 132
             class="cancel-button"
133
-            [routerLink]="['/kakocho-list']"
134
-          >
133
+            (click)="cancelKakochoEdit()">
135 134
             キャンセル
136 135
           </button>
137 136
 
@@ -150,4 +149,4 @@
150 149
 
151 150
     </section>
152 151
   </main>
153
-</div>
152
+</div>

+ 14
- 3
src/app/pages/kakocho-edit/kakocho-edit.scss 파일 보기

@@ -105,9 +105,9 @@
105 105
   font-weight: 800;
106 106
 }
107 107
 
108
-.form-row input {
108
+.form-row input,
109
+.form-row textarea {
109 110
   width: 100%;
110
-  height: 54px;
111 111
   padding: 0 14px;
112 112
   border: 2px solid #d8caba;
113 113
   border-radius: 8px;
@@ -119,7 +119,18 @@
119 119
   outline: none;
120 120
 }
121 121
 
122
-.form-row input:focus {
122
+.form-row input {
123
+  height: 54px;
124
+}
125
+
126
+.form-row textarea {
127
+  min-height: 108px;
128
+  padding-top: 14px;
129
+  resize: vertical;
130
+}
131
+
132
+.form-row input:focus,
133
+.form-row textarea:focus {
123 134
   border-color: #8a6543;
124 135
   box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.15);
125 136
 }

+ 9
- 14
src/app/pages/kakocho-edit/kakocho-edit.ts 파일 보기

@@ -9,7 +9,6 @@ import {
9 9
 import {
10 10
   ActivatedRoute,
11 11
   Router,
12
-  RouterLink,
13 12
 } from '@angular/router';
14 13
 
15 14
 import { AppHeader } from '../../share/header/app-header';
@@ -27,17 +26,15 @@ import { Kakocho } from '../../models/kakocho';
27 26
     AppHeader,
28 27
     AppSideMenu,
29 28
     ReactiveFormsModule,
30
-    RouterLink,
31 29
   ],
32 30
   templateUrl: './kakocho-edit.html',
33 31
   styleUrl: './kakocho-edit.scss',
34 32
 })
35 33
 export class KakochoEdit {
36
-
37 34
   danka?: Danka;
38 35
   kakocho?: Kakocho;
39
-
40 36
   kakochoForm: FormGroup;
37
+  dankaId: string;
41 38
 
42 39
   constructor(
43 40
     private fb: FormBuilder,
@@ -60,6 +57,7 @@ export class KakochoEdit {
60 57
 
61 58
     // 檀家ID
62 59
     const dankaId = this.route.snapshot.params['dankaId'];
60
+    this.dankaId = this.route.snapshot.params['dankaId'];
63 61
 
64 62
     if (dankaId) {
65 63
       this.danka =
@@ -137,22 +135,19 @@ export class KakochoEdit {
137 135
     this.router.navigate([
138 136
       '/danka-detail',
139 137
       this.danka?.id,
140
-    ]);
138
+    ], { queryParams: { tab: 'kakocho' } });
141 139
   }
142 140
 
143 141
   deleteKakocho(): void {
144
-
145 142
     if (!this.kakocho) {
146 143
       return;
147 144
     }
148 145
 
149
-    this.kakochoService.deleteKakocho(
150
-      this.kakocho.id
151
-    );
146
+    this.kakochoService.deleteKakocho(this.kakocho.id);
152 147
 
153
-    this.router.navigate([
154
-      '/danka-detail',
155
-      this.danka?.id,
156
-    ]);
148
+    this.router.navigate(['/danka-detail', this.danka?.id], { queryParams: { tab: 'kakocho' } });
157 149
   }
158
-}
150
+
151
+  cancelKakochoEdit() {
152
+    this.router.navigate(['/danka-detail', this.danka?.id], { queryParams: { tab: 'kakocho' } });  }
153
+}

+ 45
- 23
src/app/pages/memorial-list/memorial-list.html 파일 보기

@@ -11,34 +11,47 @@
11 11
           <h1>年次法要(回忌)一覧</h1>
12 12
 
13 13
           <div class="filter-row">
14
-            <div class="year-filter">
14
+            <div class="filter-field">
15 15
               <label for="targetYear">対象年</label>
16
-              <input id="targetYear" type="number" [(ngModel)]="targetYear"/>
16
+              <select
17
+                id="targetYear"
18
+                [(ngModel)]="targetYear"
19
+                (ngModelChange)="createMemorialList()"
20
+              >
21
+                @for (year of yearOptions; track year) {
22
+                  <option [ngValue]="year">{{ year }}年度</option>
23
+                }
24
+              </select>
17 25
             </div>
18 26
 
19
-            <div class="memorial-filter">
20
-              <span>回忌</span>
21
-              @for (filter of memorialTypeFilters; track filter.value) {
22
-                <button
23
-                  type="button"
24
-                  class="filter-button"
25
-                  [class.active]="selectedMemorialType === filter.value"
26
-                  (click)="changeMemorialType(filter.value)"
27
-                >
28
-                  {{ filter.label }}
29
-                </button>
30
-              }
27
+            <div class="filter-field">
28
+              <label for="memorialType">回忌</label>
29
+              <select
30
+                id="memorialType"
31
+                [(ngModel)]="selectedMemorialType"
32
+                (ngModelChange)="changeMemorialType($event)"
33
+              >
34
+                @for (filter of memorialTypeFilters; track filter.value) {
35
+                  <option [ngValue]="filter.value">{{ filter.label }}</option>
36
+                }
37
+              </select>
38
+            </div>
39
+
40
+            <div class="search-field">
41
+              <label for="memorialSearch">検索</label>
42
+              <input
43
+                id="memorialSearch"
44
+                type="text"
45
+                [(ngModel)]="searchKeyword"
46
+                placeholder="戒名・俗名・檀家名で検索"
47
+              />
31 48
             </div>
32 49
           </div>
33 50
         </div>
34
-
35
-        <button type="button" class="reload-button" (click)="createMemorialList()">
36
-          再表示
37
-        </button>
38 51
       </div>
39 52
 
40 53
       <div class="list-header-row">
41
-        <h2>対象 {{ memorialList.length }} 名</h2>
54
+        <h2>対象 {{ filteredMemorialList.length }} 名</h2>
42 55
 
43 56
         <p>並び順: 没年月日 / 氏名</p>
44 57
       </div>
@@ -52,17 +65,19 @@
52 65
             <div>関係</div>
53 66
             <div>檀家(世帯)</div>
54 67
             <div>回忌</div>
68
+            <div>備考</div>
55 69
             <div>詳細</div>
56 70
           </div>
57 71
 
58
-          @if (memorialList.length > 0) {
59
-            @for (memorial of memorialList; track memorial.id) {
72
+          @if (filteredMemorialList.length > 0) {
73
+            @for (memorial of filteredMemorialList; track memorial.id) {
60 74
               <div class="memorial-table-row">
61 75
                 <div class="person-name">
62 76
                   {{ memorial.kaimyo }}
63 77
                 </div>
64 78
                 <div>
65
-                  {{ memorial.name }}
79
+                  <p class="person-name">{{ memorial.name }}</p>
80
+                  <p class="person-sub">俗名</p>
66 81
                 </div>
67 82
                 <div>
68 83
                   {{ formatDeathDate(memorial.deathDate) }}
@@ -76,6 +91,9 @@
76 91
                 <div class="memorial-type">
77 92
                   {{ memorial.memorialType }}
78 93
                 </div>
94
+                <div>
95
+                  {{ memorial.note || '' }}
96
+                </div>
79 97
                 <div>
80 98
                   <a class="detail-link" [routerLink]="['/danka-detail', memorial.dankaId]" [queryParams]="{tab: 'kakocho'}">
81 99
                     開く
@@ -85,7 +103,11 @@
85 103
             }
86 104
           } @else {
87 105
             <div class="empty-message">
88
-              対象となる法要はありません。
106
+              @if (memorialList.length > 0) {
107
+                検索条件に一致する法要はありません。
108
+              } @else {
109
+                対象となる法要はありません。
110
+              }
89 111
             </div>
90 112
           }
91 113
         </div>

+ 73
- 111
src/app/pages/memorial-list/memorial-list.scss 파일 보기

@@ -38,7 +38,8 @@
38 38
 
39 39
 .title-filter-area {
40 40
   display: grid;
41
-  gap: 4px;
41
+  gap: 12px;
42
+  min-width: 0;
42 43
 }
43 44
 
44 45
 .page-title-row h1 {
@@ -52,82 +53,61 @@
52 53
 
53 54
 .filter-row {
54 55
   display: flex;
55
-  align-items: center;
56
-  gap: 28px;
56
+  align-items: flex-end;
57
+  gap: 14px 18px;
57 58
   flex-wrap: wrap;
59
+  width: 100%;
58 60
 }
59 61
 
60
-.year-filter {
61
-  display: flex;
62
-  align-items: center;
63
-  gap: 12px;
62
+.filter-field,
63
+.search-field {
64
+  display: grid;
65
+  gap: 6px;
64 66
 }
65 67
 
66
-.year-filter label,
67
-.memorial-filter span {
68
+.filter-field label,
69
+.search-field label {
68 70
   color: #4b3c31;
69
-  font-size: 16px;
71
+  font-size: 14px;
70 72
   font-weight: 800;
71 73
 }
72 74
 
73
-.year-filter input {
74
-  width: 96px;
75
-  height: 48px;
75
+.filter-field select,
76
+.search-field input {
77
+  height: 38px;
76 78
   padding: 0 14px;
77 79
   border: 2px solid #d8caba;
78 80
   border-radius: 8px;
79 81
   background: #fffdf9;
80 82
   color: #2f2720;
81
-  font-size: 17px;
83
+  font-size: 15px;
82 84
   font-weight: 700;
83 85
   box-sizing: border-box;
86
+  outline: none;
84 87
 }
85 88
 
86
-.memorial-filter {
87
-  display: flex;
88
-  align-items: center;
89
-  gap: 8px;
90
-}
91
-
92
-.filter-button {
93
-  width: 108px;
94
-  height: 40px;
95
-  border: 2px solid #d8caba;
96
-  border-radius: 6px;
97
-  background: #f1e7d8;
98
-  color: #2f2720;
99
-  font-size: 15px;
100
-  font-weight: 700;
89
+.filter-field select {
90
+  width: 148px;
101 91
   cursor: pointer;
102
-  box-sizing: border-box;
103 92
 }
104 93
 
105
-.filter-button:hover {
106
-  background: #eadfce;
94
+.search-field {
95
+  flex: 1 1 260px;
96
+  min-width: 260px;
107 97
 }
108 98
 
109
-.filter-button.active {
110
-  background: #8a6543;
111
-  border-color: #8a6543;
112
-  color: #ffffff;
99
+.search-field input {
100
+  width: 100%;
113 101
 }
114 102
 
115
-.reload-button {
116
-  width: 120px;
117
-  height: 48px;
118
-  margin-top: 30px;
119
-  border: 2px solid #8a6543;
120
-  border-radius: 6px;
121
-  background: #8a6543;
122
-  color: #ffffff;
123
-  font-size: 17px;
124
-  font-weight: 800;
125
-  cursor: pointer;
126
-  box-sizing: border-box;
103
+.filter-field select:focus {
104
+  border-color: #8a6543;
105
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.12);
127 106
 }
128 107
 
129
-.reload-button:hover {
130
-  background: #765639;
108
+.search-field input:focus {
109
+  border-color: #8a6543;
110
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.12);
131 111
 }
132 112
 
133 113
 .list-header-row {
@@ -156,69 +136,85 @@
156 136
 
157 137
 .memorial-table {
158 138
   display: grid;
159
-  gap: 4px;
139
+  gap: 0;
140
+  border: 2px solid #d8c2aa;
141
+  border-radius: 8px;
142
+  overflow: hidden;
160 143
 }
161 144
 
162 145
 .memorial-table-header,
163 146
 .memorial-table-row {
164 147
   display: grid;
165
-  grid-template-columns: 1.4fr 1.2fr 0.9fr 0.8fr 1.2fr 0.8fr 0.7fr;
148
+  grid-template-columns: 1.2fr 0.95fr 0.8fr 0.6fr 0.95fr 0.65fr 0.95fr 64px;
166 149
   align-items: center;
167
-  column-gap: 12px;
150
+  gap: 12px;
168 151
 }
169 152
 
170 153
 .memorial-table-header {
171
-  min-height: 38px;
172
-  padding: 0 12px;
173
-  border: 2px solid #d8caba;
174
-  border-radius: 8px;
175
-  background: #eadfce;
176
-  color: #5a4a3c;
177
-  font-size: 15px;
154
+  min-height: 46px;
155
+  padding: 0 14px;
156
+  background: #efe4d6;
157
+  color: #111111;
158
+  font-size: 14px;
178 159
   font-weight: 800;
179 160
   box-sizing: border-box;
180 161
 }
181 162
 
182 163
 .memorial-table-row {
183
-  min-height: 58px;
184
-  padding: 0 12px;
185
-  border: 2px solid #d8caba;
186
-  border-radius: 10px;
164
+  min-height: 78px;
165
+  padding: 10px 14px;
166
+  border-top: 1px solid #d8c2aa;
187 167
   background: #fffdf9;
188
-  color: #2f2720;
189
-  font-size: 16px;
168
+  color: #111111;
169
+  font-size: 15px;
190 170
   box-sizing: border-box;
191 171
 }
192 172
 
193 173
 .memorial-table-row:hover {
194
-  background: #f6efe6;
174
+  background: #fff8ee;
195 175
 }
196 176
 
197 177
 .person-name {
178
+  margin: 0;
198 179
   font-weight: 800;
199 180
 }
200 181
 
182
+.person-sub {
183
+  margin: 4px 0 0;
184
+  color: #111111;
185
+  font-size: 13px;
186
+  line-height: 1.35;
187
+}
188
+
201 189
 .memorial-type {
202
-  color: #8a6543;
190
+  color: #111111;
203 191
   font-weight: 800;
204 192
 }
205 193
 
206 194
 .detail-link {
207
-  color: #3f6f45;
208
-  font-size: 15px;
195
+  width: 64px;
196
+  height: 34px;
197
+  border: 2px solid #8a6543;
198
+  border-radius: 6px;
199
+  background: #ffffff;
200
+  color: #8a6543;
201
+  font-size: 14px;
209 202
   font-weight: 800;
210 203
   text-decoration: none;
204
+  display: flex;
205
+  align-items: center;
206
+  justify-content: center;
207
+  box-sizing: border-box;
211 208
 }
212 209
 
213 210
 .detail-link:hover {
214
-  text-decoration: underline;
211
+  background: #f6efe6;
212
+  text-decoration: none;
215 213
 }
216 214
 
217 215
 .empty-message {
218 216
   min-height: 58px;
219 217
   padding: 18px 20px;
220
-  border: 2px solid #d8caba;
221
-  border-radius: 10px;
222 218
   background: #fffdf9;
223 219
   color: #7b6b5c;
224 220
   font-size: 15px;
@@ -226,43 +222,11 @@
226 222
   box-sizing: border-box;
227 223
 }
228 224
 
229
-.notice-box {
230
-  margin-top: 26px;
231
-  padding: 18px 26px;
232
-  border: 2px solid #d8caba;
233
-  border-radius: 10px;
234
-  background: #fffdf9;
235
-  box-sizing: border-box;
236
-}
237
-
238
-.notice-box p {
239
-  margin: 0;
240
-  color: #7b6b5c;
241
-  font-size: 15px;
242
-  line-height: 1.7;
243
-}
244
-
245
-.bottom-note {
246
-  width: 700px;
247
-  margin: 18px 0 22px 36px;
248
-  padding: 4px 12px;
249
-  border: 2px solid #d8caba;
250
-  border-radius: 4px;
251
-  background: #eadfce;
252
-  color: #7b6b5c;
253
-  font-size: 14px;
254
-  box-sizing: border-box;
255
-}
256
-
257 225
 @media (max-width: 1100px) {
258 226
   .page-title-row {
259 227
     flex-direction: column;
260 228
   }
261 229
 
262
-  .reload-button {
263
-    margin-top: 0;
264
-  }
265
-
266 230
   .memorial-table {
267 231
     overflow-x: auto;
268 232
   }
@@ -298,8 +262,10 @@
298 262
     gap: 14px;
299 263
   }
300 264
 
301
-  .memorial-filter {
302
-    flex-wrap: wrap;
265
+  .filter-field,
266
+  .filter-field select,
267
+  .search-field {
268
+    width: 100%;
303 269
   }
304 270
 
305 271
   .list-header-row {
@@ -308,8 +274,4 @@
308 274
     gap: 8px;
309 275
   }
310 276
 
311
-  .bottom-note {
312
-    width: auto;
313
-    margin: 10px 20px 22px;
314
-  }
315 277
 }

+ 51
- 0
src/app/pages/memorial-list/memorial-list.ts 파일 보기

@@ -17,6 +17,13 @@ export class MemorialList {
17 17
   memorialList: Memorial[] = [];
18 18
   targetYear: number = new Date().getFullYear();
19 19
   selectedMemorialType = 'all';
20
+  searchKeyword = '';
21
+  yearOptions: number[] = [
22
+    this.targetYear - 1,
23
+    this.targetYear,
24
+    this.targetYear + 1,
25
+    this.targetYear + 2,
26
+  ];
20 27
   memorialTypeFilters = [
21 28
     { label: 'すべて', value: 'all' },
22 29
     { label: '一周忌', value: '一周忌' },
@@ -25,6 +32,14 @@ export class MemorialList {
25 32
     { label: '十三回忌', value: '十三回忌' },
26 33
     { label: '十七回忌', value: '十七回忌' },
27 34
     { label: '二十三回忌', value: '二十三回忌' },
35
+    { label: '二十五回忌', value: '二十五回忌' },
36
+    { label: '二十七回忌', value: '二十七回忌' },
37
+    { label: '三十三回忌', value: '三十三回忌' },
38
+    { label: '三十七回忌', value: '三十七回忌' },
39
+    { label: '四十三回忌', value: '四十三回忌' },
40
+    { label: '四十七回忌', value: '四十七回忌' },
41
+    { label: '五十回忌', value: '五十回忌' },
42
+    { label: '百回忌', value: '百回忌' },
28 43
   ];
29 44
 
30 45
   constructor(
@@ -57,6 +72,7 @@ export class MemorialList {
57 72
         householdName: danka?.householdName ?? '不明',
58 73
         deathDate: kakocho.deathDate,
59 74
         memorialType: memorialType,
75
+        note: kakocho.note,
60 76
       };
61 77
       this.memorialList.push(memorialTarget);
62 78
     });
@@ -75,6 +91,25 @@ export class MemorialList {
75 91
     this.createMemorialList();
76 92
   }
77 93
 
94
+  get filteredMemorialList(): Memorial[] {
95
+    const keyword = this.searchKeyword.trim();
96
+    if (!keyword) {
97
+      return this.memorialList;
98
+    }
99
+
100
+    return this.memorialList.filter((memorial) =>
101
+      [
102
+        memorial.kaimyo,
103
+        memorial.name,
104
+        memorial.deathDate,
105
+        memorial.relationship,
106
+        memorial.householdName,
107
+        memorial.memorialType,
108
+        memorial.note,
109
+      ].some((value) => value.includes(keyword)),
110
+    );
111
+  }
112
+
78 113
   formatDeathDate(deathDate: string): string {
79 114
     const [, month, day] = deathDate.split('-').map(Number);
80 115
     if (!month || !day) {
@@ -98,6 +133,22 @@ export class MemorialList {
98 133
         return '十七回忌';
99 134
       case 22:
100 135
         return '二十三回忌';
136
+      case 24:
137
+        return '二十五回忌';
138
+      case 26:
139
+        return '二十七回忌';
140
+      case 32:
141
+        return '三十三回忌';
142
+      case 36:
143
+        return '三十七回忌';
144
+      case 42:
145
+        return '四十三回忌';
146
+      case 46:
147
+        return '四十七回忌';
148
+      case 49:
149
+        return '五十回忌';
150
+      case 99:
151
+        return '百回忌';
101 152
       default:
102 153
         return '';
103 154
     }

+ 1
- 1
src/app/pages/search/search.html 파일 보기

@@ -53,7 +53,7 @@
53 53
                     {{ danka.householdName }}
54 54
                   </div>
55 55
                   <div>
56
-                    世帯主: {{ danka.householder }}
56
+                    施主名: {{ danka.householder }}
57 57
                   </div>
58 58
                   <div>
59 59
                     住所: {{ danka.address }}

+ 8
- 2
src/app/services/dankaService.ts 파일 보기

@@ -9,9 +9,12 @@ export class DankaService {
9 9
     {
10 10
       id: '1',
11 11
       householdName: '鈴木家',
12
+      householdFurigana: 'すずきけ',
12 13
       householder: '鈴木 太郎',
14
+      householderFurigana: 'すずき たろう',
13 15
       postalCode: '123-4567',
14 16
       address: '市内 1-2-3',
17
+      note: '寺報送付あり。年忌法要の案内は施主へ連絡。',
15 18
       updatedAt: '2026-05-28',
16 19
       phones: [
17 20
         {
@@ -20,16 +23,19 @@ export class DankaService {
20 23
         },
21 24
         {
22 25
           tel: '090-1234-5678',
23
-          note: '世帯主',
26
+          note: '主',
24 27
         },
25 28
       ],
26 29
     },
27 30
     {
28 31
       id: '2',
29 32
       householdName: '古田家',
33
+      householdFurigana: 'ふるたけ',
30 34
       householder: '古田 太郎',
35
+      householderFurigana: 'ふるた たろう',
31 36
       postalCode: '234-4567',
32 37
       address: '市内 1-2-3',
38
+      note: '電話連絡を優先。',
33 39
       updatedAt: '2026-05-28',
34 40
       phones: [
35 41
         {
@@ -38,7 +44,7 @@ export class DankaService {
38 44
         },
39 45
         {
40 46
           tel: '080-7890-4567',
41
-          note: '世帯主',
47
+          note: '主',
42 48
         },
43 49
       ],
44 50
     }

+ 1
- 1
src/app/services/family-service.ts 파일 보기

@@ -13,7 +13,7 @@ export class FamilyService {
13 13
       name: '鈴木 花子',
14 14
       relationship: '母',
15 15
       birthDate: '1975-01-01',
16
-      note: '次の世帯主',
16
+      note: '次の主',
17 17
       fatherId: '5',
18 18
       motherId: '6',
19 19
       spouseId: '3',

Loading…
취소
저장