Files
Goa-gel-fullstack/frontend/src/app/features/approvals/approval-history/approval-history.component.ts

233 lines
5.7 KiB
TypeScript
Raw Normal View History

import { Component, Input, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { StatusBadgeComponent } from '../../../shared/components/status-badge/status-badge.component';
import { EmptyStateComponent } from '../../../shared/components/empty-state/empty-state.component';
import { ApprovalService } from '../services/approval.service';
import { ApprovalResponseDto } from '../../../api/models';
@Component({
selector: 'app-approval-history',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatProgressSpinnerModule,
StatusBadgeComponent,
EmptyStateComponent,
],
template: `
<div class="approval-history">
<h3>Approval History</h3>
@if (loading()) {
<div class="loading-container">
<mat-spinner diameter="32"></mat-spinner>
</div>
} @else if (approvals().length === 0) {
<app-empty-state
icon="history"
title="No approval history"
message="No approval actions have been taken yet."
/>
} @else {
<div class="timeline">
@for (approval of approvals(); track approval.id) {
<div class="timeline-item">
<div class="timeline-marker" [class]="getMarkerClass(approval.status)">
<mat-icon>{{ getStatusIcon(approval.status) }}</mat-icon>
</div>
<mat-card class="timeline-content">
<div class="timeline-header">
<span class="department">{{ approval.departmentName }}</span>
<app-status-badge [status]="approval.status" />
</div>
@if (approval.remarks) {
<p class="remarks">{{ approval.remarks }}</p>
}
@if (approval.rejectionReason) {
<p class="rejection-reason">
<strong>Reason:</strong> {{ formatReason(approval.rejectionReason) }}
</p>
}
<div class="timeline-meta">
<span>{{ approval.updatedAt | date: 'medium' }}</span>
</div>
</mat-card>
</div>
}
</div>
}
</div>
`,
styles: [
`
.approval-history {
margin-top: 24px;
h3 {
margin: 0 0 16px;
font-size: 1.125rem;
font-weight: 500;
}
}
.loading-container {
display: flex;
justify-content: center;
padding: 32px;
}
.timeline {
position: relative;
padding-left: 32px;
&::before {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 2px;
background-color: #e0e0e0;
}
}
.timeline-item {
position: relative;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.timeline-marker {
position: absolute;
left: -32px;
top: 16px;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #e0e0e0;
z-index: 1;
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
color: white;
}
&.approved {
background-color: #4caf50;
}
&.rejected {
background-color: #f44336;
}
&.pending {
background-color: #ff9800;
}
&.changes {
background-color: #2196f3;
}
}
.timeline-content {
padding: 16px;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.department {
font-weight: 500;
}
.remarks {
margin: 8px 0;
color: rgba(0, 0, 0, 0.7);
font-size: 0.875rem;
}
.rejection-reason {
margin: 8px 0;
color: #f44336;
font-size: 0.875rem;
}
.timeline-meta {
font-size: 0.75rem;
color: rgba(0, 0, 0, 0.54);
}
`,
],
})
export class ApprovalHistoryComponent implements OnInit {
@Input({ required: true }) requestId!: string;
private readonly approvalService = inject(ApprovalService);
readonly loading = signal(true);
readonly approvals = signal<ApprovalResponseDto[]>([]);
ngOnInit(): void {
this.loadHistory();
}
private loadHistory(): void {
this.approvalService.getApprovalHistory(this.requestId).subscribe({
next: (data) => {
this.approvals.set(data);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
getStatusIcon(status: string): string {
switch (status) {
case 'APPROVED':
return 'check';
case 'REJECTED':
return 'close';
case 'CHANGES_REQUESTED':
return 'edit';
default:
return 'hourglass_empty';
}
}
getMarkerClass(status: string): string {
switch (status) {
case 'APPROVED':
return 'approved';
case 'REJECTED':
return 'rejected';
case 'CHANGES_REQUESTED':
return 'changes';
default:
return 'pending';
}
}
formatReason(reason: string): string {
return reason.replace(/_/g, ' ').toLowerCase().replace(/^\w/, (c) => c.toUpperCase());
}
}