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:
@@ -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>
|
||||
Reference in New Issue
Block a user