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,207 @@
<div class="page-container">
<app-page-header title="New License Request" subtitle="Submit a new license application">
<button mat-button routerLink="/requests">
<mat-icon>arrow_back</mat-icon>
Back to Requests
</button>
</app-page-header>
<mat-card class="form-card">
<!-- Card Header -->
<div class="form-header">
<div class="header-icon">
<mat-icon>assignment_add</mat-icon>
</div>
<h2>License Application</h2>
<p>Complete the form below to submit your license application</p>
</div>
<div class="form-content">
<mat-stepper linear #stepper>
<!-- Step 1: Request Type -->
<mat-step [stepControl]="basicForm" label="Request Type">
<div class="step-content">
<div class="step-header">
<h3>Select Request Type</h3>
<p>Choose the type of license request you want to submit</p>
</div>
<form [formGroup]="basicForm">
<!-- Request Type Selection -->
<div class="type-selection">
@for (type of requestTypes; track type.value) {
<div
class="type-option"
[class.selected]="basicForm.controls.requestType.value === type.value"
(click)="basicForm.controls.requestType.setValue(type.value)"
>
<div class="type-icon">
<mat-icon>{{ getTypeIcon(type.value) }}</mat-icon>
</div>
<span class="type-label">{{ type.label }}</span>
</div>
}
</div>
<!-- Workflow Selection -->
<div class="step-header" style="margin-top: 32px">
<h3>Select Workflow</h3>
<p>Choose the approval workflow for your application</p>
</div>
@if (loading()) {
<div style="display: flex; justify-content: center; padding: 32px">
<mat-spinner diameter="32"></mat-spinner>
</div>
} @else if (workflows().length === 0) {
<div style="text-align: center; padding: 32px; color: var(--dbim-grey-2)">
<mat-icon style="font-size: 48px; width: 48px; height: 48px; opacity: 0.5">warning</mat-icon>
<p>No active workflows available</p>
</div>
} @else {
<div class="workflow-selection">
@for (workflow of workflows(); track workflow.id) {
<div
class="workflow-option"
[class.selected]="basicForm.controls.workflowId.value === workflow.id"
(click)="basicForm.controls.workflowId.setValue(workflow.id)"
>
<div class="workflow-name">{{ workflow.name }}</div>
<div class="workflow-desc">{{ workflow.description || 'Standard approval workflow' }}</div>
</div>
}
</div>
}
</form>
<div class="form-actions">
<div class="actions-left">
<button mat-button routerLink="/requests">Cancel</button>
</div>
<div class="actions-right">
<button mat-raised-button color="primary" matStepperNext [disabled]="basicForm.invalid">
Continue
<mat-icon>arrow_forward</mat-icon>
</button>
</div>
</div>
</div>
</mat-step>
<!-- Step 2: Business Details -->
<mat-step [stepControl]="metadataForm" label="Business Details">
<div class="step-content">
<div class="step-header">
<h3>Business Information</h3>
<p>Provide details about your business for the license application</p>
</div>
<form [formGroup]="metadataForm">
<div class="metadata-fields">
<div class="field-group">
<mat-form-field appearance="outline">
<mat-label>Business Name</mat-label>
<input matInput formControlName="businessName" placeholder="Enter your business name" />
<mat-icon matPrefix>business</mat-icon>
@if (metadataForm.controls.businessName.hasError('required')) {
<mat-error>Business name is required</mat-error>
}
@if (metadataForm.controls.businessName.hasError('minlength')) {
<mat-error>Minimum 3 characters required</mat-error>
}
</mat-form-field>
</div>
<div class="field-group">
<mat-form-field appearance="outline">
<mat-label>Business Address</mat-label>
<input matInput formControlName="businessAddress" placeholder="Full business address" />
<mat-icon matPrefix>location_on</mat-icon>
@if (metadataForm.controls.businessAddress.hasError('required')) {
<mat-error>Business address is required</mat-error>
}
</mat-form-field>
</div>
<div class="field-group">
<mat-form-field appearance="outline">
<mat-label>Owner / Applicant Name</mat-label>
<input matInput formControlName="ownerName" placeholder="Full name of owner" />
<mat-icon matPrefix>person</mat-icon>
@if (metadataForm.controls.ownerName.hasError('required')) {
<mat-error>Owner name is required</mat-error>
}
</mat-form-field>
</div>
<div class="field-group">
<mat-form-field appearance="outline">
<mat-label>Contact Phone</mat-label>
<input matInput formControlName="ownerPhone" placeholder="+91 XXXXX XXXXX" type="tel" />
<mat-icon matPrefix>phone</mat-icon>
@if (metadataForm.controls.ownerPhone.hasError('required')) {
<mat-error>Phone number is required</mat-error>
}
</mat-form-field>
</div>
<div class="field-group">
<mat-form-field appearance="outline">
<mat-label>Email Address</mat-label>
<input matInput formControlName="ownerEmail" placeholder="email@example.com" type="email" />
<mat-icon matPrefix>email</mat-icon>
@if (metadataForm.controls.ownerEmail.hasError('email')) {
<mat-error>Please enter a valid email address</mat-error>
}
</mat-form-field>
</div>
<div class="field-group" style="grid-column: 1 / -1">
<mat-form-field appearance="outline">
<mat-label>Business Description</mat-label>
<textarea
matInput
formControlName="description"
placeholder="Brief description of your business activities"
rows="4"
></textarea>
<mat-icon matPrefix>notes</mat-icon>
<mat-hint>Optional: Provide additional details about your business</mat-hint>
</mat-form-field>
</div>
</div>
</form>
<div class="form-actions">
<div class="actions-left">
<button mat-button matStepperPrevious>
<mat-icon>arrow_back</mat-icon>
Back
</button>
</div>
<div class="actions-right">
<button
mat-raised-button
color="primary"
class="submit-btn"
(click)="onSubmit()"
[disabled]="submitting() || metadataForm.invalid"
>
@if (submitting()) {
<mat-spinner diameter="20"></mat-spinner>
Submitting...
} @else {
<ng-container>
<mat-icon>send</mat-icon>
Create Request
</ng-container>
}
</button>
</div>
</div>
</div>
</mat-step>
</mat-stepper>
</div>
</mat-card>
</div>