109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
|
|
// ============================================================================
|
|||
|
|
// Task Progress Bar Component
|
|||
|
|
// Per CUB-44: Determinate progress bar with optional elapsed time display.
|
|||
|
|
// Uses Angular Material mat-progress-bar in determinate mode with tactical
|
|||
|
|
// dark theme styling via CSS custom properties.
|
|||
|
|
// ============================================================================
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
ChangeDetectionStrategy,
|
|||
|
|
ChangeDetectorRef,
|
|||
|
|
Component,
|
|||
|
|
Input,
|
|||
|
|
OnDestroy,
|
|||
|
|
OnInit,
|
|||
|
|
} from '@angular/core';
|
|||
|
|
import { CommonModule } from '@angular/common';
|
|||
|
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Displays a determinate progress bar with an optional elapsed time indicator.
|
|||
|
|
*
|
|||
|
|
* Usage:
|
|||
|
|
* ```html
|
|||
|
|
* <app-task-progress-bar [progress]="65" />
|
|||
|
|
* <app-task-progress-bar [progress]="42" [showElapsed]="true" />
|
|||
|
|
* ```
|
|||
|
|
*/
|
|||
|
|
@Component({
|
|||
|
|
selector: 'app-task-progress-bar',
|
|||
|
|
standalone: true,
|
|||
|
|
imports: [CommonModule, MatProgressBarModule],
|
|||
|
|
templateUrl: './task-progress-bar.component.html',
|
|||
|
|
styleUrl: './task-progress-bar.component.scss',
|
|||
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|||
|
|
})
|
|||
|
|
export class TaskProgressBarComponent implements OnInit, OnDestroy {
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Inputs
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
/** Current progress percentage (0–100). Required. */
|
|||
|
|
@Input({ required: true })
|
|||
|
|
progress!: number;
|
|||
|
|
|
|||
|
|
/** Whether to show elapsed time next to the percentage. Defaults to false. */
|
|||
|
|
@Input()
|
|||
|
|
showElapsed = false;
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Internal state
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
/** Timestamp when the component initialized — used for elapsed calculation. */
|
|||
|
|
startTime = Date.now();
|
|||
|
|
|
|||
|
|
/** Formatted elapsed time string, e.g. "2m 15s ago". */
|
|||
|
|
elapsedText = '';
|
|||
|
|
|
|||
|
|
/** Interval timer for updating the elapsed display. */
|
|||
|
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|||
|
|
|
|||
|
|
constructor(private cdr: ChangeDetectorRef) {}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Lifecycle
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
ngOnInit(): void {
|
|||
|
|
this.updateElapsed();
|
|||
|
|
|
|||
|
|
if (this.showElapsed) {
|
|||
|
|
// Update elapsed time every second
|
|||
|
|
this.timer = setInterval(() => {
|
|||
|
|
this.updateElapsed();
|
|||
|
|
this.cdr.markForCheck();
|
|||
|
|
}, 1000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ngOnDestroy(): void {
|
|||
|
|
if (this.timer) {
|
|||
|
|
clearInterval(this.timer);
|
|||
|
|
this.timer = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Helpers
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
/** Clamp progress to 0–100 for safety. */
|
|||
|
|
get clampedProgress(): number {
|
|||
|
|
return Math.max(0, Math.min(100, this.progress ?? 0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Recalculate the elapsed time string. */
|
|||
|
|
private updateElapsed(): void {
|
|||
|
|
const elapsedMs = Date.now() - this.startTime;
|
|||
|
|
const totalSeconds = Math.floor(elapsedMs / 1000);
|
|||
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|||
|
|
const seconds = totalSeconds % 60;
|
|||
|
|
|
|||
|
|
if (minutes > 0) {
|
|||
|
|
this.elapsedText = `${minutes}m ${seconds}s ago`;
|
|||
|
|
} else {
|
|||
|
|
this.elapsedText = `${seconds}s ago`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|