(this.endpoint, { params });
+ }
+}
diff --git a/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.html b/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.html
index 98022d8..cd6e99c 100644
--- a/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.html
+++ b/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.html
@@ -64,8 +64,56 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No attendance recorded yet · 尚無出席紀錄
+
+
+
diff --git a/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.scss b/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.scss
index eb7af1d..12850cc 100644
--- a/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.scss
+++ b/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.scss
@@ -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;
+}
diff --git a/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.ts b/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.ts
index d5d27d8..39b4138 100644
--- a/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.ts
+++ b/APP/src/app/portals/user-portal/pages/dashboard/dashboard.component.ts
@@ -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)}`;
+ }
}