Files
Goa-gel-fullstack/frontend/src/app/features/dashboard/admin-dashboard/admin-dashboard.component.ts

604 lines
16 KiB
TypeScript
Raw Normal View History

import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { StatusBadgeComponent } from '../../../shared/components/status-badge/status-badge.component';
import { BlockchainExplorerMiniComponent } from '../../../shared/components/blockchain-explorer-mini/blockchain-explorer-mini.component';
import { ApiService } from '../../../core/services/api.service';
import { AdminStatsDto } from '../../../api/models';
@Component({
selector: 'app-admin-dashboard',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatProgressSpinnerModule,
StatusBadgeComponent,
BlockchainExplorerMiniComponent,
],
template: `
<div class="page-container">
<!-- Welcome Section -->
<section class="welcome-section">
<div class="welcome-content">
<div class="welcome-text">
<span class="greeting">Admin Dashboard</span>
<h1>Platform Overview</h1>
<p class="subtitle">Monitor and manage the License Authority Platform</p>
</div>
<div class="quick-actions">
<button mat-raised-button class="action-btn primary" routerLink="/admin">
<mat-icon>admin_panel_settings</mat-icon>
Admin Portal
</button>
<button mat-stroked-button class="action-btn" routerLink="/requests">
<mat-icon>list_alt</mat-icon>
All Requests
</button>
</div>
</div>
</section>
@if (loading()) {
<div class="loading-container">
<mat-spinner diameter="48"></mat-spinner>
<p>Loading dashboard...</p>
</div>
} @else if (stats()) {
<!-- Stats Cards -->
<section class="stats-section">
<div class="stats-grid">
<mat-card class="stat-card requests" routerLink="/requests">
<div class="stat-icon-wrapper">
<mat-icon>description</mat-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats()!.totalRequests }}</div>
<div class="stat-label">Total Requests</div>
</div>
<div class="stat-decoration"></div>
</mat-card>
<mat-card class="stat-card approvals">
<div class="stat-icon-wrapper">
<mat-icon>check_circle</mat-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats()!.totalApprovals }}</div>
<div class="stat-label">Approvals</div>
</div>
<div class="stat-decoration"></div>
</mat-card>
<mat-card class="stat-card documents">
<div class="stat-icon-wrapper">
<mat-icon>folder_open</mat-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats()!.totalDocuments }}</div>
<div class="stat-label">Documents</div>
</div>
<div class="stat-decoration"></div>
</mat-card>
<mat-card class="stat-card departments" routerLink="/departments">
<div class="stat-icon-wrapper">
<mat-icon>business</mat-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats()!.totalDepartments }}</div>
<div class="stat-label">Departments</div>
</div>
<div class="stat-decoration"></div>
</mat-card>
<mat-card class="stat-card applicants">
<div class="stat-icon-wrapper">
<mat-icon>people</mat-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats()!.totalApplicants }}</div>
<div class="stat-label">Applicants</div>
</div>
<div class="stat-decoration"></div>
</mat-card>
<mat-card class="stat-card blockchain">
<div class="stat-icon-wrapper">
<mat-icon>link</mat-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stats()!.totalBlockchainTransactions }}</div>
<div class="stat-label">Blockchain Tx</div>
</div>
<div class="stat-decoration"></div>
</mat-card>
</div>
</section>
<!-- Main Content Grid -->
<div class="content-grid">
<!-- Left Column -->
<div class="content-main">
<!-- Requests by Status -->
<mat-card class="section-card">
<div class="card-header">
<div class="header-left">
<mat-icon>pie_chart</mat-icon>
<h2>Requests by Status</h2>
</div>
<button mat-button color="primary" routerLink="/requests">
View All
<mat-icon>arrow_forward</mat-icon>
</button>
</div>
<div class="card-content">
<div class="status-grid">
@for (item of (stats()?.requestsByStatus || []); track item.status) {
<div class="status-item" [routerLink]="['/requests']" [queryParams]="{ status: item.status }">
<app-status-badge [status]="item.status" />
<span class="count">{{ item.count }}</span>
</div>
}
</div>
</div>
</mat-card>
<!-- Quick Actions -->
<mat-card class="section-card">
<div class="card-header">
<div class="header-left">
<mat-icon>flash_on</mat-icon>
<h2>Quick Actions</h2>
</div>
</div>
<div class="card-content">
<div class="actions-grid">
<div class="action-item" routerLink="/departments">
<div class="action-icon departments">
<mat-icon>business</mat-icon>
</div>
<span>Manage Departments</span>
</div>
<div class="action-item" routerLink="/workflows">
<div class="action-icon workflows">
<mat-icon>account_tree</mat-icon>
</div>
<span>Manage Workflows</span>
</div>
<div class="action-item" routerLink="/audit">
<div class="action-icon audit">
<mat-icon>history</mat-icon>
</div>
<span>View Audit Logs</span>
</div>
<div class="action-item" routerLink="/webhooks">
<div class="action-icon webhooks">
<mat-icon>webhook</mat-icon>
</div>
<span>Webhooks</span>
</div>
</div>
</div>
</mat-card>
</div>
<!-- Right Column: Blockchain Activity -->
<div class="content-sidebar">
<app-blockchain-explorer-mini [showViewAll]="true" [refreshInterval]="15000"></app-blockchain-explorer-mini>
</div>
</div>
}
</div>
`,
styles: [`
.page-container {
padding: 0;
}
/* Welcome Section */
.welcome-section {
background: linear-gradient(135deg, #1D0A69 0%, #2563EB 100%) !important;
color: white !important;
padding: 32px;
margin: 0 0 24px 0;
border-radius: 16px;
position: relative;
overflow: hidden;
}
.welcome-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
position: relative;
z-index: 1;
}
.welcome-text .greeting {
font-size: 0.9rem;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255, 255, 255, 0.9) !important;
display: block;
}
.welcome-text h1 {
margin: 8px 0;
font-size: 2rem;
font-weight: 700;
color: white !important;
}
.welcome-text .subtitle {
margin: 0;
opacity: 0.9;
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.9) !important;
}
.quick-actions {
display: flex;
gap: 12px;
}
.action-btn.primary {
background: white !important;
color: #1D0A69 !important;
}
.action-btn:not(.primary) {
color: white;
border-color: rgba(255, 255, 255, 0.5);
}
.action-btn:not(.primary):hover {
background: rgba(255, 255, 255, 0.1);
}
.action-btn mat-icon {
margin-right: 8px;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px;
gap: 16px;
}
.loading-container p {
color: #8E8E8E;
}
/* Stats Section */
.stats-section {
margin-bottom: 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
}
.stat-card {
display: flex;
align-items: center;
gap: 16px;
padding: 24px;
border-radius: 16px !important;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
background: white !important;
color: #150202;
border: 1px solid #EBEAEA;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.stat-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
background: rgba(29, 10, 105, 0.08);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #1D0A69;
}
.stat-icon-wrapper mat-icon {
font-size: 24px;
width: 24px;
height: 24px;
}
.stat-card.approvals .stat-icon-wrapper {
background: rgba(5, 150, 105, 0.1);
color: #059669;
}
.stat-card.documents .stat-icon-wrapper {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.stat-card.departments .stat-icon-wrapper {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
.stat-card.applicants .stat-icon-wrapper {
background: rgba(8, 145, 178, 0.1);
color: #0891b2;
}
.stat-card.blockchain .stat-icon-wrapper {
background: rgba(71, 85, 105, 0.1);
color: #475569;
}
.stat-content {
flex: 1;
z-index: 1;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
line-height: 1.2;
color: #150202;
}
.stat-label {
font-size: 0.8rem;
color: #8E8E8E;
margin-top: 4px;
}
.stat-decoration {
display: none;
}
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: 1fr 400px;
gap: 24px;
}
.content-main {
display: flex;
flex-direction: column;
gap: 24px;
}
.section-card {
border-radius: 16px !important;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #EBEAEA;
}
.card-header .header-left {
display: flex;
align-items: center;
gap: 12px;
}
.card-header .header-left mat-icon {
color: #2563EB;
}
.card-header .header-left h2 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #150202;
}
.card-header button mat-icon {
margin-left: 4px;
font-size: 18px;
width: 18px;
height: 18px;
}
.card-content {
padding: 20px 24px;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
background: #F5F5F5;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
}
.status-item:hover {
background: rgba(0, 0, 0, 0.08);
}
.status-item .count {
font-size: 1.25rem;
font-weight: 700;
color: #150202;
}
/* Quick Actions */
.actions-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.action-item:hover {
background: #F5F5F5;
}
.action-item span {
font-size: 0.85rem;
color: #150202;
font-weight: 500;
}
.action-icon {
width: 56px;
height: 56px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.action-icon mat-icon {
font-size: 26px;
width: 26px;
height: 26px;
}
.action-icon.departments {
background: linear-gradient(135deg, #7c3aed, #a78bfa);
color: white;
}
.action-icon.workflows {
background: linear-gradient(135deg, #1D0A69, #2563EB);
color: white;
}
.action-icon.audit {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
color: white;
}
.action-icon.webhooks {
background: linear-gradient(135deg, #059669, #10b981);
color: white;
}
.content-sidebar {
min-width: 0;
}
/* Responsive adjustments */
@media (max-width: 1200px) {
.content-grid {
grid-template-columns: 1fr;
}
.content-sidebar {
order: -1;
}
}
@media (max-width: 768px) {
.welcome-content {
flex-direction: column;
align-items: flex-start;
}
.actions-grid {
grid-template-columns: repeat(2, 1fr);
}
}
`],
})
export class AdminDashboardComponent implements OnInit {
private readonly api = inject(ApiService);
readonly loading = signal(true);
readonly stats = signal<AdminStatsDto | null>(null);
ngOnInit(): void {
this.loadStats();
}
private loadStats(): void {
this.api.get<AdminStatsDto>('/admin/stats').subscribe({
next: (data) => {
this.stats.set(data);
this.loading.set(false);
},
error: () => {
// Use mock data for demo when API is unavailable
this.loadMockStats();
this.loading.set(false);
},
});
}
private loadMockStats(): void {
const mockStats: AdminStatsDto = {
totalRequests: 156,
totalApprovals: 89,
totalDocuments: 423,
totalDepartments: 12,
totalApplicants: 67,
totalBlockchainTransactions: 234,
averageProcessingTime: 4.5,
requestsByStatus: [
{ status: 'DRAFT', count: 12 },
{ status: 'SUBMITTED', count: 23 },
{ status: 'IN_REVIEW', count: 18 },
{ status: 'APPROVED', count: 89 },
{ status: 'REJECTED', count: 8 },
{ status: 'COMPLETED', count: 6 },
],
requestsByType: [
{ type: 'NEW_LICENSE', count: 98 },
{ type: 'RENEWAL', count: 42 },
{ type: 'AMENDMENT', count: 16 },
],
departmentStats: [],
lastUpdated: new Date().toISOString(),
};
this.stats.set(mockStats);
}
}