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:
Mahi
2026-02-07 10:23:29 -04:00
commit 80566bf0a2
441 changed files with 102418 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'app-blockchain-info',
standalone: true,
imports: [CommonModule, MatCardModule, MatIconModule, MatButtonModule, MatTooltipModule],
template: `
@if (tokenId || txHash) {
<div class="blockchain-info" [class.compact]="compact">
<div class="header">
<mat-icon class="chain-icon">token</mat-icon>
<span class="title">Blockchain Record</span>
<span class="verified-badge">
<mat-icon>verified</mat-icon>
Verified
</span>
</div>
@if (tokenId) {
<div class="info-row">
<span class="label">License NFT Token ID</span>
<span class="value token-id">#{{ tokenId }}</span>
</div>
}
@if (txHash) {
<div class="info-row">
<span class="label">Transaction Hash</span>
<div class="tx-hash-container">
<code class="tx-hash">{{ truncateHash(txHash) }}</code>
<button
mat-icon-button
matTooltip="Copy full hash"
(click)="copyToClipboard(txHash)"
class="copy-btn"
>
<mat-icon>content_copy</mat-icon>
</button>
</div>
</div>
}
@if (showExplorer && txHash) {
<div class="explorer-link">
<a mat-button color="primary" [href]="getExplorerUrl()" target="_blank">
<mat-icon>open_in_new</mat-icon>
View on Block Explorer
</a>
</div>
}
</div>
}
`,
styles: [
`
.blockchain-info {
background: linear-gradient(135deg, #e8f5e9 0%, #e3f2fd 100%);
border: 1px solid #c8e6c9;
border-radius: 12px;
padding: 16px;
margin: 16px 0;
}
.blockchain-info.compact {
padding: 12px;
margin: 8px 0;
}
.header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.chain-icon {
color: #2e7d32;
font-size: 20px;
width: 20px;
height: 20px;
}
.title {
font-weight: 500;
color: #1b5e20;
flex: 1;
}
.verified-badge {
display: flex;
align-items: center;
gap: 4px;
background-color: #4caf50;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
}
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
&:last-of-type {
border-bottom: none;
}
}
.label {
color: rgba(0, 0, 0, 0.6);
font-size: 0.875rem;
}
.value {
font-weight: 500;
}
.token-id {
font-size: 1.125rem;
color: #1565c0;
font-family: monospace;
}
.tx-hash-container {
display: flex;
align-items: center;
gap: 4px;
}
.tx-hash {
font-family: monospace;
font-size: 0.75rem;
background-color: rgba(0, 0, 0, 0.06);
padding: 4px 8px;
border-radius: 4px;
color: #455a64;
}
.copy-btn {
width: 28px;
height: 28px;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
}
.explorer-link {
margin-top: 12px;
text-align: right;
}
.compact .header {
margin-bottom: 8px;
}
.compact .info-row {
padding: 4px 0;
}
`,
],
})
export class BlockchainInfoComponent {
@Input() tokenId?: string | number;
@Input() txHash?: string;
@Input() compact = false;
@Input() showExplorer = false;
@Input() explorerBaseUrl = 'http://localhost:25000';
truncateHash(hash: string): string {
if (hash.length <= 20) return hash;
return `${hash.substring(0, 10)}...${hash.substring(hash.length - 8)}`;
}
copyToClipboard(text: string): void {
navigator.clipboard.writeText(text);
}
getExplorerUrl(): string {
return `${this.explorerBaseUrl}/tx/${this.txHash}`;
}
}