add attendance

This commit is contained in:
Chris Chen
2026-06-20 19:33:04 -07:00
parent 2af169fa60
commit 87425b3276
24 changed files with 1357 additions and 5 deletions
@@ -64,8 +64,56 @@
</div>
</div>
<!-- Recent Transactions -->
<!-- Sunday Attendance Trend -->
<div class="section">
<div class="section-header">
<h2>Sunday Attendance · 主日出席人數</h2>
</div>
<div class="attendance-chart">
<kendo-chart *ngIf="hasAttendanceData" [style.height.px]="340">
<kendo-chart-legend position="bottom"></kendo-chart-legend>
<kendo-chart-category-axis>
<kendo-chart-category-axis-item [categories]="attendanceDates">
</kendo-chart-category-axis-item>
</kendo-chart-category-axis>
<kendo-chart-series>
<kendo-chart-series-item type="column" [stack]="true" [data]="attendanceKid" name="Kid · 兒童"
color="#42a5f5">
<kendo-chart-series-item-labels [visible]="true" position="center" color="#ffffff"
background="transparent" font="bold 13px sans-serif" [content]="segmentLabelContent">
</kendo-chart-series-item-labels>
</kendo-chart-series-item>
<kendo-chart-series-item type="column" [stack]="true" [data]="attendanceYouth" name="Youth · 青少年"
color="#66bb6a">
<kendo-chart-series-item-labels [visible]="true" position="center" color="#ffffff"
background="transparent" font="bold 13px sans-serif" [content]="segmentLabelContent">
</kendo-chart-series-item-labels>
</kendo-chart-series-item>
<kendo-chart-series-item type="column" [stack]="true" [data]="attendanceAdult" name="Adult · 成人"
color="#ef5350">
<kendo-chart-series-item-labels [visible]="true" position="center" color="#ffffff"
background="transparent" font="bold 13px sans-serif" [content]="segmentLabelContent">
</kendo-chart-series-item-labels>
</kendo-chart-series-item>
<!-- Invisible line series whose only job is to carry the total label on top of each bar. -->
<kendo-chart-series-item type="line" [data]="attendanceTotal" [visibleInLegend]="false"
color="transparent" [width]="0" [markers]="{ visible: false }"
[highlight]="{ visible: false }" [tooltip]="{ visible: false }">
<kendo-chart-series-item-labels [visible]="true" position="above" color="#374151"
font="bold 13px sans-serif" [content]="totalLabelContent">
</kendo-chart-series-item-labels>
</kendo-chart-series-item>
</kendo-chart-series>
<kendo-chart-tooltip [shared]="true" background="#1f2937" color="#ffffff">
</kendo-chart-tooltip>
</kendo-chart>
<p *ngIf="!hasAttendanceData" class="attendance-empty">
No attendance recorded yet · 尚無出席紀錄
</p>
</div>
</div>
<!-- Quick Actions -->
<div class="section">
@@ -534,3 +534,17 @@
}
}
}
.attendance-chart {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 1rem;
}
.attendance-empty {
text-align: center;
color: #94a3b8;
padding: 2rem 0;
margin: 0;
}
@@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChartsModule } from '@progress/kendo-angular-charts';
import { AuthService, UserInfo } from '../../../../shared/services/auth.service';
import { MealAttendanceApiService } from '../../../../features/meal-attendance/services/meal-attendance-api.service';
interface Transaction {
id: string;
@@ -15,13 +17,21 @@ interface Transaction {
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule],
imports: [CommonModule, ChartsModule],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit {
currentUser: UserInfo | null = null;
// Sunday attendance trend (last 8 weeks) for the chart.
attendanceDates: string[] = [];
attendanceAdult: number[] = [];
attendanceYouth: number[] = [];
attendanceKid: number[] = [];
attendanceTotal: number[] = [];
hasAttendanceData = false;
activeTransactions = 5;
pendingTasks = 12;
completedTransactions = 23;
@@ -57,15 +67,60 @@ export class DashboardComponent implements OnInit {
}
];
constructor(private authService: AuthService) { }
constructor(
private authService: AuthService,
private attendanceApi: MealAttendanceApiService,
) { }
ngOnInit(): void {
this.authService.currentUser$.subscribe(user => {
this.currentUser = user;
});
this.loadAttendance();
}
getDisplayName(): string {
return this.currentUser?.email || '';
}
// Per-segment label: show the count inside its colored block, but hide zeros to avoid clutter.
segmentLabelContent = (args: { value: number }): string => {
return args.value > 0 ? String(args.value) : '';
};
// Total label sitting on top of each stacked bar.
totalLabelContent = (args: { value: number }): string => {
return args.value > 0 ? String(args.value) : '';
};
private loadAttendance(): void {
const today = new Date();
const from = new Date();
from.setDate(today.getDate() - 56); // last ~8 weeks
this.attendanceApi.getRange(this.toLocalIso(from), this.toLocalIso(today)).subscribe({
next: rows => {
this.attendanceDates = rows.map(r => this.toShortLabel(r.date));
this.attendanceAdult = rows.map(r => r.adult);
this.attendanceYouth = rows.map(r => r.youth);
this.attendanceKid = rows.map(r => r.kid);
this.attendanceTotal = rows.map(r => r.adult + r.youth + r.kid);
this.hasAttendanceData = rows.length > 0;
},
error: () => { this.hasAttendanceData = false; },
});
}
// Local yyyy-MM-dd (never toISOString — it shifts the day by timezone).
private toLocalIso(d: Date): string {
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${d.getFullYear()}-${month}-${day}`;
}
// 'yyyy-MM-dd' → 'M/d' for the axis label (split to avoid timezone parsing).
private toShortLabel(isoDate: string): string {
const [, month, day] = isoDate.split('-');
return `${Number(month)}/${Number(day)}`;
}
}