233 lines
5.7 KiB
TypeScript
233 lines
5.7 KiB
TypeScript
|
|
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());
|
||
|
|
}
|
||
|
|
}
|