add attendance
This commit is contained in:
@@ -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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user