feat: Goa GEL Blockchain e-Licensing Platform - Full Stack Implementation
Complete implementation of the Goa Government e-Licensing platform with: Backend: - NestJS API with JWT authentication - PostgreSQL database with Knex ORM - Redis caching and session management - MinIO document storage - Hyperledger Besu blockchain integration - Multi-department workflow system - Comprehensive API tests (266/282 passing) Frontend: - Angular 21 with standalone components - Angular Material + TailwindCSS UI - Visual workflow builder - Document upload with progress tracking - Blockchain explorer integration - Role-based dashboards (Admin, Department, Citizen) - E2E tests with Playwright (37 tests) Infrastructure: - Docker Compose orchestration - Blockscout blockchain explorer - Development and production configurations
This commit is contained in:
119
frontend/src/app/layouts/auth-layout/auth-layout.component.html
Normal file
119
frontend/src/app/layouts/auth-layout/auth-layout.component.html
Normal file
@@ -0,0 +1,119 @@
|
||||
<!-- Skip to main content - GIGW 3.0 Accessibility -->
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<div class="auth-layout">
|
||||
<!-- Animated Background -->
|
||||
<div class="animated-background">
|
||||
<!-- Floating Blockchain Nodes -->
|
||||
<div class="node node-1"></div>
|
||||
<div class="node node-2"></div>
|
||||
<div class="node node-3"></div>
|
||||
<div class="node node-4"></div>
|
||||
<div class="node node-5"></div>
|
||||
<div class="node node-6"></div>
|
||||
|
||||
<!-- Connection Lines -->
|
||||
<svg class="connections" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<line class="connection" x1="20" y1="30" x2="50" y2="50" />
|
||||
<line class="connection" x1="50" y1="50" x2="80" y2="25" />
|
||||
<line class="connection" x1="80" y1="25" x2="70" y2="70" />
|
||||
<line class="connection" x1="70" y1="70" x2="30" y2="80" />
|
||||
<line class="connection" x1="30" y1="80" x2="20" y2="30" />
|
||||
<line class="connection" x1="50" y1="50" x2="70" y2="70" />
|
||||
<line class="connection" x1="50" y1="50" x2="30" y2="80" />
|
||||
</svg>
|
||||
|
||||
<!-- Gradient Overlay -->
|
||||
<div class="gradient-overlay"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Container -->
|
||||
<div class="auth-container">
|
||||
<!-- Left Side - Branding -->
|
||||
<div class="auth-branding">
|
||||
<div class="branding-content">
|
||||
<div class="emblem-wrapper">
|
||||
<img
|
||||
src="assets/images/goa-emblem.svg"
|
||||
alt="Government of Goa Emblem"
|
||||
class="goa-emblem"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h1 class="brand-title">
|
||||
<span class="title-line">Government of Goa</span>
|
||||
<span class="title-highlight">Blockchain e-Licensing</span>
|
||||
</h1>
|
||||
|
||||
<p class="brand-tagline">
|
||||
Secure, Transparent, Immutable
|
||||
</p>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="feature-text">
|
||||
<span class="feature-title">Blockchain Secured</span>
|
||||
<span class="feature-desc">Tamper-proof license records</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="feature-text">
|
||||
<span class="feature-title">Instant Verification</span>
|
||||
<span class="feature-desc">Real-time license validity</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
||||
<path d="M9 11H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2zm2-7h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="feature-text">
|
||||
<span class="feature-title">Multi-Dept Workflow</span>
|
||||
<span class="feature-desc">Streamlined approvals</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Status -->
|
||||
<div class="network-status">
|
||||
<div class="status-dot"></div>
|
||||
<span class="status-text">Hyperledger Besu Network</span>
|
||||
<span class="status-badge">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side - Login Form -->
|
||||
<div class="auth-content" id="main-content" role="main">
|
||||
<div class="auth-card">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="auth-footer" role="contentinfo">
|
||||
<p class="copyright">© 2024 Government of Goa. All rights reserved.</p>
|
||||
<div class="footer-links">
|
||||
<a href="#">Privacy Policy</a>
|
||||
<span class="divider">|</span>
|
||||
<a href="#">Terms of Service</a>
|
||||
<span class="divider">|</span>
|
||||
<a href="#">Help</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
476
frontend/src/app/layouts/auth-layout/auth-layout.component.scss
Normal file
476
frontend/src/app/layouts/auth-layout/auth-layout.component.scss
Normal file
@@ -0,0 +1,476 @@
|
||||
// =============================================================================
|
||||
// AUTH LAYOUT - World-Class Blockchain Login Experience
|
||||
// DBIM v3.0 Compliant | GIGW 3.0 Accessible
|
||||
// =============================================================================
|
||||
|
||||
// Skip Link (GIGW 3.0)
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: var(--dbim-blue-dark);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
z-index: 1000;
|
||||
font-size: 14px;
|
||||
|
||||
&:focus {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LAYOUT
|
||||
// =============================================================================
|
||||
.auth-layout {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0a0520 0%, #1D0A69 50%, #130640 100%);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANIMATED BACKGROUND
|
||||
// =============================================================================
|
||||
.animated-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
// Floating Blockchain Nodes
|
||||
.node {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 20px rgba(99, 102, 241, 0.5), 0 0 40px rgba(99, 102, 241, 0.3);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 50%;
|
||||
animation: pulse-ring 2s ease-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.node-1 {
|
||||
top: 20%;
|
||||
left: 15%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.node-2 {
|
||||
top: 30%;
|
||||
left: 45%;
|
||||
animation-delay: 1s;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.node-3 {
|
||||
top: 15%;
|
||||
right: 20%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.node-4 {
|
||||
bottom: 30%;
|
||||
right: 25%;
|
||||
animation-delay: 1.5s;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.node-5 {
|
||||
bottom: 25%;
|
||||
left: 25%;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.node-6 {
|
||||
top: 50%;
|
||||
left: 35%;
|
||||
animation-delay: 2.5s;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
// Connection Lines
|
||||
.connections {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.connection {
|
||||
stroke: rgba(99, 102, 241, 0.4);
|
||||
stroke-width: 0.1;
|
||||
stroke-dasharray: 2 2;
|
||||
animation: dash 20s linear infinite;
|
||||
}
|
||||
|
||||
// Gradient Overlay
|
||||
.gradient-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 30%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 70%, rgba(139, 92, 246, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN CONTAINER
|
||||
// =============================================================================
|
||||
.auth-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LEFT SIDE - BRANDING
|
||||
// =============================================================================
|
||||
.auth-branding {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.branding-content {
|
||||
max-width: 480px;
|
||||
color: white;
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.emblem-wrapper {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.goa-emblem {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
margin: 0 0 16px;
|
||||
|
||||
.title-line {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title-highlight {
|
||||
display: block;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #FFFFFF 0%, #A5B4FC 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 0 48px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
// Features Grid
|
||||
.features-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(5px);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #A5B4FC;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.feature-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Network Status
|
||||
.network-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-radius: 50px;
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #10B981;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px #10B981;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 2px 8px;
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #10B981;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RIGHT SIDE - AUTH CONTENT
|
||||
// =============================================================================
|
||||
.auth-content {
|
||||
width: 480px;
|
||||
min-width: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
background: white;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
max-width: 480px;
|
||||
margin: auto;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
margin: 16px;
|
||||
width: calc(100% - 32px);
|
||||
padding: 32px 24px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FOOTER
|
||||
// =============================================================================
|
||||
.auth-footer {
|
||||
margin-top: auto;
|
||||
padding-top: 24px;
|
||||
text-align: center;
|
||||
|
||||
.copyright {
|
||||
font-size: 12px;
|
||||
color: var(--dbim-grey-2);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
a {
|
||||
font-size: 12px;
|
||||
color: var(--dbim-grey-3);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--dbim-blue-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: var(--dbim-grey-1);
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANIMATIONS
|
||||
// =============================================================================
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-20px) translateX(10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0) translateX(20px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(20px) translateX(10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: 100;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RESPONSIVE
|
||||
// =============================================================================
|
||||
@media (max-width: 1024px) {
|
||||
.auth-layout {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
min-height: auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth-layout',
|
||||
standalone: true,
|
||||
imports: [RouterModule],
|
||||
templateUrl: './auth-layout.component.html',
|
||||
styleUrl: './auth-layout.component.scss',
|
||||
})
|
||||
export class AuthLayoutComponent {}
|
||||
3
frontend/src/app/layouts/main-layout/CLAUDE.md
Normal file
3
frontend/src/app/layouts/main-layout/CLAUDE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
<claude-mem-context>
|
||||
|
||||
</claude-mem-context>
|
||||
260
frontend/src/app/layouts/main-layout/main-layout.component.html
Normal file
260
frontend/src/app/layouts/main-layout/main-layout.component.html
Normal file
@@ -0,0 +1,260 @@
|
||||
<!-- Skip to main content - GIGW 3.0 Accessibility -->
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
|
||||
<div class="app-shell">
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="sidebar"
|
||||
[class.sidebar-collapsed]="!sidenavOpened()"
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<!-- Logo Section -->
|
||||
<div class="sidebar-header">
|
||||
<div class="logo-section">
|
||||
<div class="emblem-container">
|
||||
<img
|
||||
src="assets/images/goa-emblem.svg"
|
||||
alt="Government of Goa Emblem"
|
||||
class="goa-emblem"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
<div class="emblem-fallback" *ngIf="!emblemLoaded">
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
@if (sidenavOpened()) {
|
||||
<div class="logo-text">
|
||||
<span class="govt-text">Government of Goa</span>
|
||||
<span class="platform-text">Blockchain e-Licensing</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
@if (sidenavOpened()) {
|
||||
<span class="nav-section-title">Main Menu</span>
|
||||
}
|
||||
@for (item of visibleNavItems(); track item.route) {
|
||||
<a
|
||||
class="nav-item"
|
||||
[routerLink]="item.route"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: item.route === '/dashboard' }"
|
||||
[attr.aria-label]="item.label"
|
||||
[matTooltip]="!sidenavOpened() ? item.label : ''"
|
||||
matTooltipPosition="right"
|
||||
>
|
||||
<div class="nav-icon">
|
||||
<mat-icon>{{ item.icon }}</mat-icon>
|
||||
</div>
|
||||
@if (sidenavOpened()) {
|
||||
<span class="nav-label">{{ item.label }}</span>
|
||||
}
|
||||
@if (item.badge && item.badge() > 0) {
|
||||
<span class="nav-badge" [class.pulse]="item.badge() > 0">
|
||||
{{ item.badge() }}
|
||||
</span>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (userType() === 'ADMIN') {
|
||||
<div class="nav-section">
|
||||
@if (sidenavOpened()) {
|
||||
<span class="nav-section-title">Administration</span>
|
||||
}
|
||||
<a
|
||||
class="nav-item"
|
||||
routerLink="/admin"
|
||||
routerLinkActive="active"
|
||||
[matTooltip]="!sidenavOpened() ? 'Admin Portal' : ''"
|
||||
matTooltipPosition="right"
|
||||
>
|
||||
<div class="nav-icon">
|
||||
<mat-icon>admin_panel_settings</mat-icon>
|
||||
</div>
|
||||
@if (sidenavOpened()) {
|
||||
<span class="nav-label">Admin Portal</span>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<!-- Sidebar Footer -->
|
||||
<div class="sidebar-footer">
|
||||
@if (sidenavOpened()) {
|
||||
<div class="blockchain-status">
|
||||
<div class="status-indicator online"></div>
|
||||
<div class="status-text">
|
||||
<span class="status-label">Blockchain</span>
|
||||
<span class="status-value">Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="blockchain-status-compact">
|
||||
<div class="status-indicator online"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="main-wrapper">
|
||||
<!-- Top Header -->
|
||||
<header class="top-header" role="banner">
|
||||
<div class="header-left">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="toggleSidenav()"
|
||||
aria-label="Toggle navigation menu"
|
||||
class="menu-toggle"
|
||||
>
|
||||
<mat-icon>{{ sidenavOpened() ? 'menu_open' : 'menu' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Breadcrumb (optional) -->
|
||||
<nav class="breadcrumb hide-mobile" aria-label="Breadcrumb">
|
||||
<span class="breadcrumb-item">Dashboard</span>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- Search (optional) -->
|
||||
<button mat-icon-button class="header-action hide-mobile" aria-label="Search">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Notifications -->
|
||||
<button
|
||||
mat-icon-button
|
||||
class="header-action"
|
||||
[matMenuTriggerFor]="notificationMenu"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
<mat-icon [matBadge]="unreadNotifications()" matBadgeColor="warn" matBadgeSize="small">
|
||||
notifications
|
||||
</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #notificationMenu="matMenu" class="notification-menu">
|
||||
<div class="notification-header">
|
||||
<span class="notification-title">Notifications</span>
|
||||
<button mat-button color="primary" class="mark-read-btn">Mark all read</button>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="notification-list">
|
||||
<div class="notification-item unread">
|
||||
<div class="notification-icon success">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<span class="notification-text">Request #1234 approved</span>
|
||||
<span class="notification-time">2 minutes ago</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-item">
|
||||
<div class="notification-icon info">
|
||||
<mat-icon>info</mat-icon>
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<span class="notification-text">New document uploaded</span>
|
||||
<span class="notification-time">1 hour ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class="view-all-btn">
|
||||
View all notifications
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<!-- User Profile -->
|
||||
<div class="user-profile">
|
||||
<button
|
||||
mat-button
|
||||
[matMenuTriggerFor]="userMenu"
|
||||
class="user-button"
|
||||
aria-label="User menu"
|
||||
>
|
||||
<div class="user-avatar" [style.background]="getAvatarColor()">
|
||||
{{ getUserInitials() }}
|
||||
</div>
|
||||
@if (currentUser | async; as user) {
|
||||
<div class="user-info hide-mobile">
|
||||
<span class="user-name">{{ user.name }}</span>
|
||||
<span class="user-role">{{ formatRole(user.type) }}</span>
|
||||
</div>
|
||||
}
|
||||
<mat-icon class="dropdown-arrow hide-mobile">expand_more</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #userMenu="matMenu" class="user-menu">
|
||||
@if (currentUser | async; as user) {
|
||||
<div class="user-menu-header">
|
||||
<div class="user-avatar-large" [style.background]="getAvatarColor()">
|
||||
{{ getUserInitials() }}
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<span class="user-name">{{ user.name }}</span>
|
||||
<span class="user-email">{{ user.email || (user.departmentCode ? user.departmentCode + '@goa.gov.in' : 'user@goa.gov.in') }}</span>
|
||||
<span class="user-badge">{{ formatRole(user.type) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
}
|
||||
<button mat-menu-item>
|
||||
<mat-icon>person</mat-icon>
|
||||
<span>My Profile</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
@if (userType() === 'DEPARTMENT') {
|
||||
<button mat-menu-item routerLink="/department/wallet">
|
||||
<mat-icon>account_balance_wallet</mat-icon>
|
||||
<span>My Wallet</span>
|
||||
</button>
|
||||
}
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="logout()" class="logout-btn">
|
||||
<mat-icon>logout</mat-icon>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main id="main-content" class="main-content" role="main">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<!-- Footer - DBIM Compliant -->
|
||||
<footer class="main-footer" role="contentinfo">
|
||||
<div class="footer-content">
|
||||
<div class="footer-left">
|
||||
<span class="footer-text">
|
||||
This platform belongs to Government of Goa, India
|
||||
</span>
|
||||
<span class="footer-divider hide-mobile">|</span>
|
||||
<span class="footer-text hide-mobile">
|
||||
Last Updated: {{ lastUpdated }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<a href="#" class="footer-link">Website Policies</a>
|
||||
<a href="#" class="footer-link">Help</a>
|
||||
<a href="#" class="footer-link">Feedback</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
751
frontend/src/app/layouts/main-layout/main-layout.component.scss
Normal file
751
frontend/src/app/layouts/main-layout/main-layout.component.scss
Normal file
@@ -0,0 +1,751 @@
|
||||
// =============================================================================
|
||||
// MAIN LAYOUT - World-Class Government Blockchain Platform
|
||||
// DBIM v3.0 Compliant | GIGW 3.0 Accessible
|
||||
// =============================================================================
|
||||
|
||||
// Variables
|
||||
$sidebar-width: 280px;
|
||||
$sidebar-collapsed-width: 72px;
|
||||
$header-height: 64px;
|
||||
$footer-height: 48px;
|
||||
$transition-speed: 250ms;
|
||||
|
||||
// =============================================================================
|
||||
// APP SHELL
|
||||
// =============================================================================
|
||||
.app-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background-color: var(--dbim-white);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SIDEBAR
|
||||
// =============================================================================
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: $sidebar-width;
|
||||
background: linear-gradient(180deg, var(--dbim-blue-dark) 0%, #130640 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 100;
|
||||
transition: width $transition-speed ease;
|
||||
box-shadow: 4px 0 24px rgba(29, 10, 105, 0.15);
|
||||
|
||||
&.sidebar-collapsed {
|
||||
width: $sidebar-collapsed-width;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.emblem-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 12px;
|
||||
justify-content: center;
|
||||
|
||||
.nav-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar Header
|
||||
.sidebar-header {
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.emblem-container {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all $transition-speed ease;
|
||||
|
||||
.goa-emblem {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.emblem-fallback {
|
||||
mat-icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.govt-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.platform-text {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar Navigation
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 16px 12px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
transition: all 150ms ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: white;
|
||||
|
||||
.nav-icon {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.3) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.25);
|
||||
|
||||
.nav-icon {
|
||||
background: linear-gradient(135deg, var(--crypto-indigo) 0%, var(--crypto-purple) 100%);
|
||||
|
||||
mat-icon {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
transition: all 150ms ease;
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 10px;
|
||||
background: var(--dbim-error);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar Footer
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.blockchain-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.blockchain-status-compact {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.online {
|
||||
background: var(--dbim-success);
|
||||
box-shadow: 0 0 8px var(--dbim-success);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background: var(--dbim-error);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.status-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN WRAPPER
|
||||
// =============================================================================
|
||||
.main-wrapper {
|
||||
flex: 1;
|
||||
margin-left: $sidebar-width;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
transition: margin-left $transition-speed ease;
|
||||
|
||||
.sidebar-collapsed + & {
|
||||
margin-left: $sidebar-collapsed-width;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOP HEADER
|
||||
// =============================================================================
|
||||
.top-header {
|
||||
height: $header-height;
|
||||
background: var(--dbim-white);
|
||||
border-bottom: 1px solid rgba(29, 10, 105, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
color: var(--dbim-grey-3);
|
||||
|
||||
&:hover {
|
||||
color: var(--dbim-blue-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--dbim-grey-2);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
color: var(--dbim-grey-2);
|
||||
|
||||
&:hover {
|
||||
color: var(--dbim-blue-dark);
|
||||
background: rgba(29, 10, 105, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
// User Profile
|
||||
.user-profile {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.user-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 6px 12px 6px 6px;
|
||||
border-radius: 50px;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(29, 10, 105, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar-large {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--dbim-brown);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 11px;
|
||||
color: var(--dbim-grey-2);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
font-size: 18px;
|
||||
color: var(--dbim-grey-2);
|
||||
}
|
||||
|
||||
// User Menu
|
||||
.user-menu-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.user-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--dbim-brown);
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 12px;
|
||||
color: var(--dbim-grey-2);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
margin-top: 6px;
|
||||
background: rgba(29, 10, 105, 0.1);
|
||||
color: var(--dbim-blue-dark);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
color: var(--dbim-error) !important;
|
||||
}
|
||||
|
||||
// Notification Menu
|
||||
.notification-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--dbim-brown);
|
||||
}
|
||||
|
||||
.mark-read-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(29, 10, 105, 0.03);
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background: rgba(13, 110, 253, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.success {
|
||||
background: rgba(25, 135, 84, 0.1);
|
||||
color: var(--dbim-success);
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: rgba(13, 110, 253, 0.1);
|
||||
color: var(--dbim-info);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
color: #B8860B;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
color: var(--dbim-error);
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.notification-text {
|
||||
font-size: 13px;
|
||||
color: var(--dbim-brown);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 11px;
|
||||
color: var(--dbim-grey-2);
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-all-btn {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--dbim-info) !important;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN CONTENT
|
||||
// =============================================================================
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
background: #f8f9fc;
|
||||
min-height: calc(100vh - #{$header-height} - #{$footer-height});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FOOTER - DBIM Compliant
|
||||
// =============================================================================
|
||||
.main-footer {
|
||||
height: $footer-height;
|
||||
background: var(--dbim-blue-dark);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.footer-divider {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: none;
|
||||
transition: color 150ms ease;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANIMATIONS
|
||||
// =============================================================================
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RESPONSIVE
|
||||
// =============================================================================
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar {
|
||||
width: $sidebar-collapsed-width;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.emblem-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.logo-text,
|
||||
.nav-label,
|
||||
.nav-section-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 12px;
|
||||
justify-content: center;
|
||||
|
||||
.nav-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.blockchain-status {
|
||||
padding: 12px;
|
||||
justify-content: center;
|
||||
|
||||
.status-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin-left: $sidebar-collapsed-width;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
width: $sidebar-width;
|
||||
|
||||
&:not(.sidebar-collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.logo-text,
|
||||
.nav-label,
|
||||
.nav-section-title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 12px 16px;
|
||||
justify-content: flex-start;
|
||||
|
||||
.nav-icon {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
124
frontend/src/app/layouts/main-layout/main-layout.component.ts
Normal file
124
frontend/src/app/layouts/main-layout/main-layout.component.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Component, inject, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Router } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
icon: string;
|
||||
route: string;
|
||||
roles?: ('APPLICANT' | 'DEPARTMENT' | 'ADMIN')[];
|
||||
badge?: () => number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-layout',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatDividerModule,
|
||||
MatBadgeModule,
|
||||
MatTooltipModule,
|
||||
],
|
||||
templateUrl: './main-layout.component.html',
|
||||
styleUrl: './main-layout.component.scss',
|
||||
})
|
||||
export class MainLayoutComponent {
|
||||
private readonly authService = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
readonly sidenavOpened = signal(true);
|
||||
readonly currentUser = this.authService.currentUser$;
|
||||
readonly userType = this.authService.userType;
|
||||
readonly emblemLoaded = signal(true);
|
||||
readonly unreadNotifications = signal(3);
|
||||
readonly lastUpdated = new Date().toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
// Pending approvals count for department badge
|
||||
private readonly pendingCount = signal(5);
|
||||
|
||||
readonly navItems: NavItem[] = [
|
||||
{ label: 'Dashboard', icon: 'dashboard', route: '/dashboard' },
|
||||
{ label: 'My Requests', icon: 'description', route: '/requests', roles: ['APPLICANT'] },
|
||||
{
|
||||
label: 'Pending Approvals',
|
||||
icon: 'pending_actions',
|
||||
route: '/approvals',
|
||||
roles: ['DEPARTMENT'],
|
||||
badge: () => this.pendingCount(),
|
||||
},
|
||||
{ label: 'All Requests', icon: 'list_alt', route: '/requests', roles: ['DEPARTMENT', 'ADMIN'] },
|
||||
{ label: 'Departments', icon: 'business', route: '/departments', roles: ['ADMIN'] },
|
||||
{ label: 'Workflows', icon: 'account_tree', route: '/workflows', roles: ['ADMIN'] },
|
||||
{ label: 'Webhooks', icon: 'webhook', route: '/webhooks', roles: ['DEPARTMENT', 'ADMIN'] },
|
||||
{ label: 'Audit Logs', icon: 'history', route: '/audit', roles: ['ADMIN'] },
|
||||
];
|
||||
|
||||
readonly visibleNavItems = computed(() => {
|
||||
const type = this.userType();
|
||||
return this.navItems.filter((item) => {
|
||||
if (!item.roles) return true;
|
||||
return type && item.roles.includes(type);
|
||||
});
|
||||
});
|
||||
|
||||
toggleSidenav(): void {
|
||||
this.sidenavOpened.update((v) => !v);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
getUserInitials(): string {
|
||||
const user = this.authService.getCurrentUser();
|
||||
if (!user?.name) return '?';
|
||||
const parts = user.name.split(' ');
|
||||
return parts
|
||||
.map((p) => p[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
}
|
||||
|
||||
getAvatarColor(): string {
|
||||
const type = this.userType();
|
||||
switch (type) {
|
||||
case 'ADMIN':
|
||||
return 'linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%)';
|
||||
case 'DEPARTMENT':
|
||||
return 'linear-gradient(135deg, #1D0A69 0%, #2563EB 100%)';
|
||||
case 'APPLICANT':
|
||||
return 'linear-gradient(135deg, #10B981 0%, #06B6D4 100%)';
|
||||
default:
|
||||
return 'linear-gradient(135deg, #6B7280 0%, #9CA3AF 100%)';
|
||||
}
|
||||
}
|
||||
|
||||
formatRole(role: string): string {
|
||||
switch (role) {
|
||||
case 'ADMIN':
|
||||
return 'Administrator';
|
||||
case 'DEPARTMENT':
|
||||
return 'Department Officer';
|
||||
case 'APPLICANT':
|
||||
return 'Citizen';
|
||||
default:
|
||||
return role;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user