Bläddra i källkod

[add]

行事を追加
poohr 3 veckor sedan
förälder
incheckning
a99997e321

+ 5
- 0
src/app/app.routes.ts Visa fil

@@ -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,

+ 15
- 0
src/app/models/event.ts Visa fil

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

+ 102
- 0
src/app/pages/event/event.html Visa fil

@@ -0,0 +1,102 @@
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="year-filter">
14
+              <label for="targetYear">対象年</label>
15
+              <input id="targetYear" type="number" [(ngModel)]="targetYear" />
16
+            </div>
17
+
18
+            <div class="event-filter">
19
+              <span>行事</span>
20
+              @for (filter of eventTypeFilters; track filter.value) {
21
+                <button
22
+                  type="button"
23
+                  class="filter-button"
24
+                  [class.active]="selectedEventType === filter.value"
25
+                  (click)="changeEventType(filter.value)"
26
+                >
27
+                  {{ filter.label }}
28
+                </button>
29
+              }
30
+            </div>
31
+          </div>
32
+        </div>
33
+
34
+        <button type="button" class="reload-button" (click)="createEventTargetList()">
35
+          再表示
36
+        </button>
37
+      </div>
38
+
39
+      <div class="list-header-row">
40
+        <h2>対象 {{ eventTargets.length }} 名</h2>
41
+
42
+        <p>並び順: 行事 / 年齢 / 氏名</p>
43
+      </div>
44
+
45
+      <section class="event-table-section">
46
+        <div class="event-table">
47
+          <div class="event-table-header">
48
+            <div>氏名</div>
49
+            <div>檀家名</div>
50
+            <div>続柄</div>
51
+            <div>生年月日</div>
52
+            <div>年齢</div>
53
+            <div>対象行事</div>
54
+            <div>状態</div>
55
+          </div>
56
+
57
+          @if (eventTargets.length > 0) {
58
+            @for (target of eventTargets; track target.id) {
59
+              <div class="event-table-row">
60
+                <div class="person-name">
61
+                  {{ target.name }}
62
+                </div>
63
+                <div>
64
+                  {{ target.householdName }}
65
+                </div>
66
+                <div>
67
+                  {{ target.relationship }}
68
+                </div>
69
+                <div>
70
+                  {{ target.birthDate }}
71
+                </div>
72
+                <div>
73
+                  {{ target.age }}歳
74
+                </div>
75
+                <div class="event-type">
76
+                  {{ target.eventType }}
77
+                </div>
78
+                <div>
79
+                  <select
80
+                    class="status-select"
81
+                    [class.sent]="target.status === '案内済'"
82
+                    [ngModel]="target.status"
83
+                    (ngModelChange)="changeStatus(target, $event)"
84
+                    aria-label="状態"
85
+                  >
86
+                    @for (status of eventStatuses; track status) {
87
+                      <option [value]="status">{{ status }}</option>
88
+                    }
89
+                  </select>
90
+                </div>
91
+              </div>
92
+            }
93
+          } @else {
94
+            <div class="empty-message">
95
+              対象となる行事対象者はありません。
96
+            </div>
97
+          }
98
+        </div>
99
+      </section>
100
+    </section>
101
+  </main>
102
+</div>

+ 309
- 0
src/app/pages/event/event.scss Visa fil

@@ -0,0 +1,309 @@
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-start;
57
+  gap: 16px 28px;
58
+  flex-wrap: wrap;
59
+}
60
+
61
+.year-filter {
62
+  display: flex;
63
+  align-items: center;
64
+  gap: 12px;
65
+}
66
+
67
+.year-filter label,
68
+.event-filter span {
69
+  color: #4b3c31;
70
+  font-size: 16px;
71
+  font-weight: 800;
72
+}
73
+
74
+.year-filter input {
75
+  width: 96px;
76
+  height: 48px;
77
+  padding: 0 14px;
78
+  border: 2px solid #d8caba;
79
+  border-radius: 8px;
80
+  background: #fffdf9;
81
+  color: #2f2720;
82
+  font-size: 17px;
83
+  font-weight: 700;
84
+  box-sizing: border-box;
85
+}
86
+
87
+.event-filter {
88
+  display: flex;
89
+  align-items: flex-start;
90
+  flex-wrap: wrap;
91
+  gap: 8px;
92
+}
93
+
94
+.event-filter span {
95
+  min-height: 36px;
96
+  display: flex;
97
+  align-items: center;
98
+}
99
+
100
+.filter-button {
101
+  min-width: 86px;
102
+  height: 36px;
103
+  padding: 0 12px;
104
+  border: 2px solid #d8caba;
105
+  border-radius: 6px;
106
+  background: #f1e7d8;
107
+  color: #2f2720;
108
+  font-size: 14px;
109
+  font-weight: 700;
110
+  white-space: nowrap;
111
+  cursor: pointer;
112
+  box-sizing: border-box;
113
+}
114
+
115
+.filter-button:hover {
116
+  background: #eadfce;
117
+}
118
+
119
+.filter-button.active {
120
+  background: #8a6543;
121
+  border-color: #8a6543;
122
+  color: #ffffff;
123
+}
124
+
125
+.reload-button {
126
+  flex: 0 0 auto;
127
+  width: 120px;
128
+  height: 48px;
129
+  margin-top: 34px;
130
+  border: 2px solid #8a6543;
131
+  border-radius: 6px;
132
+  background: #8a6543;
133
+  color: #ffffff;
134
+  font-size: 17px;
135
+  font-weight: 800;
136
+  cursor: pointer;
137
+  box-sizing: border-box;
138
+}
139
+
140
+.reload-button:hover {
141
+  background: #765639;
142
+}
143
+
144
+.list-header-row {
145
+  display: flex;
146
+  justify-content: space-between;
147
+  align-items: flex-end;
148
+  margin: 18px 0 10px;
149
+}
150
+
151
+.list-header-row h2 {
152
+  margin: 0;
153
+  color: #2f2720;
154
+  font-size: 26px;
155
+  font-weight: 800;
156
+}
157
+
158
+.list-header-row p {
159
+  margin: 0;
160
+  color: #7b6b5c;
161
+  font-size: 14px;
162
+}
163
+
164
+.event-table-section {
165
+  margin-top: 8px;
166
+}
167
+
168
+.event-table {
169
+  display: grid;
170
+  gap: 4px;
171
+}
172
+
173
+.event-table-header,
174
+.event-table-row {
175
+  display: grid;
176
+  grid-template-columns: 1.2fr 1.1fr 0.8fr 1fr 0.55fr 0.9fr 0.75fr;
177
+  align-items: center;
178
+  column-gap: 12px;
179
+}
180
+
181
+.event-table-header {
182
+  min-height: 38px;
183
+  padding: 0 12px;
184
+  border: 2px solid #d8caba;
185
+  border-radius: 8px;
186
+  background: #eadfce;
187
+  color: #5a4a3c;
188
+  font-size: 15px;
189
+  font-weight: 800;
190
+  box-sizing: border-box;
191
+}
192
+
193
+.event-table-row {
194
+  min-height: 58px;
195
+  padding: 0 12px;
196
+  border: 2px solid #d8caba;
197
+  border-radius: 10px;
198
+  background: #fffdf9;
199
+  color: #2f2720;
200
+  font-size: 16px;
201
+  box-sizing: border-box;
202
+}
203
+
204
+.event-table-row:hover {
205
+  background: #f6efe6;
206
+}
207
+
208
+.person-name,
209
+.event-type {
210
+  font-weight: 800;
211
+}
212
+
213
+.event-type {
214
+  color: #8a6543;
215
+}
216
+
217
+.status-select {
218
+  width: 100%;
219
+  max-width: 112px;
220
+  height: 34px;
221
+  padding: 0 10px;
222
+  border: 2px solid #d8caba;
223
+  border-radius: 8px;
224
+  background: #ffffff;
225
+  color: #7b6b5c;
226
+  font-size: 14px;
227
+  font-weight: 800;
228
+  box-sizing: border-box;
229
+  cursor: pointer;
230
+  outline: none;
231
+}
232
+
233
+.status-select.sent {
234
+  border-color: #8a6543;
235
+  background: #f1e7d8;
236
+  color: #8a6543;
237
+}
238
+
239
+.status-select:focus {
240
+  border-color: #8a6543;
241
+  box-shadow: 0 0 0 3px rgba(138, 101, 67, 0.15);
242
+}
243
+
244
+.empty-message {
245
+  min-height: 58px;
246
+  padding: 18px 20px;
247
+  border: 2px solid #d8caba;
248
+  border-radius: 10px;
249
+  background: #fffdf9;
250
+  color: #7b6b5c;
251
+  font-size: 15px;
252
+  font-weight: 700;
253
+  box-sizing: border-box;
254
+}
255
+
256
+@media (max-width: 1100px) {
257
+  .page-title-row {
258
+    flex-direction: column;
259
+  }
260
+
261
+  .reload-button {
262
+    margin-top: 0;
263
+  }
264
+
265
+  .event-table {
266
+    overflow-x: auto;
267
+  }
268
+
269
+  .event-table-header,
270
+  .event-table-row {
271
+    min-width: 980px;
272
+  }
273
+}
274
+
275
+@media (max-width: 800px) {
276
+  .event-page {
277
+    flex-direction: column;
278
+  }
279
+
280
+  .event-main {
281
+    width: 100%;
282
+    padding: 16px 20px 32px;
283
+  }
284
+
285
+  .event-panel {
286
+    padding: 24px 20px 30px;
287
+    border-radius: 32px;
288
+  }
289
+
290
+  .page-title-row h1 {
291
+    font-size: 26px;
292
+  }
293
+
294
+  .filter-row {
295
+    align-items: flex-start;
296
+    flex-direction: column;
297
+    gap: 14px;
298
+  }
299
+
300
+  .event-filter {
301
+    flex-wrap: wrap;
302
+  }
303
+
304
+  .list-header-row {
305
+    align-items: flex-start;
306
+    flex-direction: column;
307
+    gap: 8px;
308
+  }
309
+}

+ 22
- 0
src/app/pages/event/event.spec.ts Visa fil

@@ -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
+});

+ 120
- 0
src/app/pages/event/event.ts Visa fil

@@ -0,0 +1,120 @@
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
+  eventStatuses: EventStatus[] = ['未案内', '案内済'];
20
+  eventTypeFilters: { label: string; value: EventType | 'all' }[] = [
21
+    { label: 'すべて', value: 'all' },
22
+    { label: '稚児行列', value: '稚児行列' },
23
+    { label: '七五三', value: '七五三' },
24
+    { label: '成人式', value: '成人式' },
25
+    { label: '米寿', value: '米寿' },
26
+  ];
27
+  private statusByTargetId: Record<string, EventStatus> = {};
28
+
29
+  constructor(
30
+    private dankaService: DankaService,
31
+    private familyService: FamilyService,
32
+  ) {
33
+    this.createEventTargetList();
34
+  }
35
+
36
+  createEventTargetList(): void {
37
+    this.eventTargets = [];
38
+
39
+    this.familyService.getFamilyList().forEach((family) => {
40
+      const birthDate = this.parseDate(family.birthDate);
41
+      if (!birthDate) {
42
+        return;
43
+      }
44
+
45
+      const age = this.targetYear - birthDate.getFullYear();
46
+      const eventTypes = this.getEventTypes(age);
47
+      if (eventTypes.length === 0) {
48
+        return;
49
+      }
50
+
51
+      const danka = this.dankaService.getDankaById(family.dankaId);
52
+      eventTypes.forEach((eventType) => {
53
+        if (this.selectedEventType !== 'all' && this.selectedEventType !== eventType) {
54
+          return;
55
+        }
56
+
57
+        const id = `${family.id}-${eventType}`;
58
+        this.eventTargets.push({
59
+          id,
60
+          dankaId: family.dankaId,
61
+          name: family.name,
62
+          householdName: danka?.householdName ?? '不明',
63
+          relationship: family.relationship || '未登録',
64
+          birthDate: family.birthDate,
65
+          age,
66
+          eventType,
67
+          status: this.statusByTargetId[id] ?? (Number(family.id) % 2 === 0 ? '案内済' : '未案内'),
68
+        });
69
+      });
70
+    });
71
+
72
+    this.eventTargets.sort(
73
+      (a, b) =>
74
+        this.getEventSortOrder(a.eventType) - this.getEventSortOrder(b.eventType) ||
75
+        a.age - b.age ||
76
+        a.name.localeCompare(b.name, 'ja'),
77
+    );
78
+  }
79
+
80
+  changeEventType(eventType: EventType | 'all'): void {
81
+    this.selectedEventType = eventType;
82
+    this.createEventTargetList();
83
+  }
84
+
85
+  changeStatus(target: EventTarget, status: EventStatus): void {
86
+    target.status = status;
87
+    this.statusByTargetId[target.id] = status;
88
+  }
89
+
90
+  private getEventTypes(age: number): EventType[] {
91
+    const eventTypes: EventType[] = [];
92
+
93
+    if (age >= 3 && age <= 12) {
94
+      eventTypes.push('稚児行列');
95
+    }
96
+    if ([3, 5, 7].includes(age)) {
97
+      eventTypes.push('七五三');
98
+    }
99
+    if (age === 20) {
100
+      eventTypes.push('成人式');
101
+    }
102
+    if (age === 88) {
103
+      eventTypes.push('米寿');
104
+    }
105
+
106
+    return eventTypes;
107
+  }
108
+
109
+  private getEventSortOrder(eventType: EventType): number {
110
+    return ['稚児行列', '七五三', '成人式', '米寿'].indexOf(eventType);
111
+  }
112
+
113
+  private parseDate(value: string): Date | null {
114
+    const [year, month, day] = value.split('-').map(Number);
115
+    if (!year || !month || !day) {
116
+      return null;
117
+    }
118
+    return new Date(year, month - 1, day);
119
+  }
120
+}

+ 4
- 0
src/app/share/side-menu/app-side-menu.html Visa fil

@@ -19,6 +19,10 @@
19 19
         年次法要
20 20
       </a>
21 21
 
22
+      <a routerLink="/event" routerLinkActive="active" class="menu-button">
23
+        行事
24
+      </a>
25
+
22 26
       <a routerLink="/search" routerLinkActive="active" class="menu-button">
23 27
         まとめて検索
24 28
       </a>

Laddar…
Avbryt
Spara