Files
2026-07-09 21:41:26 +02:00

1095 lines
63 KiB
Plaintext

@page "/visa"
@using System.Collections.Generic
@using System.Linq
<div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8">
@* Page Header Area *@
<div class="flex flex-col sm:flex-row sm:items-end justify-between mb-6 gap-4">
<div class="text-left">
<h2 class="text-2xl font-bold tracking-tight text-[#003b71]">Visa Services</h2>
<p class="text-xs text-[#5a6577] mt-1">Start, track and complete visa applications for SAQA travellers</p>
</div>
<div class="flex gap-2">
<button @onclick='() => OpenLibraryTab("requirements")'
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] transition-colors shadow-sm cursor-pointer">
<!-- Globe SVG -->
<svg class="w-4 h-4 text-[#003b71]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>
Visa rules database
</button>
<button @onclick="OpenNewAppModal"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] transition-colors shadow-sm cursor-pointer">
<!-- Plus SVG -->
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
</svg>
Start new application
</button>
</div>
</div>
@if (!string.IsNullOrEmpty(AlertFeedback))
{
<div class="mb-6 p-4 bg-[#effaf3] border-l-4 border-[#22a06b] rounded-r-lg text-sm text-[#1b8054] flex items-center gap-2.5 shadow-sm animate-pulse text-left">
<!-- Check SVG -->
<svg class="w-5 h-5 shrink-0 text-[#22a06b]" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
<span>@AlertFeedback</span>
</div>
}
@* KPI Grid *@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#0066b3] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Active Applications</div>
<div class="text-2xl font-bold tracking-tight">@ActiveAppsCount</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>3 in progress, 2 awaiting consulate</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#22a06b] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Approved This Month</div>
<div class="text-2xl font-bold tracking-tight text-[#22a06b]">@ApprovedThisMonth</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#22a06b] font-semibold">
<span>100% success rate</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#00a3a1] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Average Turnaround</div>
<div class="text-2xl font-bold tracking-tight">@AverageTurnaround</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#8a96a8]">
<span>Schengen and UK</span>
</div>
</div>
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-[18px] shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full before:bg-[#dc2626] text-left">
<div class="text-[11px] text-[#5a6577] uppercase tracking-[0.08em] font-semibold mb-2">Documents Pending</div>
<div class="text-2xl font-bold tracking-tight text-[#dc2626]">@DocumentsPending</div>
<div class="flex items-center gap-1 mt-2 text-xs text-[#dc2626] font-semibold">
<span>Action required from traveller</span>
</div>
</div>
</div>
@* Grid 2: Core Workflow and Table Panels *@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
@* Visa Application Workflow Card *@
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm flex flex-col justify-between text-left">
<div>
<div class="mb-4">
<h3 class="text-[15px] font-bold">Visa Application Workflow</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Start a new application or manage selected traveller's steps</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 items-end p-4 bg-[#f4f7fb] rounded-lg border border-[#e4eaf2] mb-5">
<div>
<label class="block text-[10px] text-[#8a96a8] font-bold uppercase mb-1">Traveller</label>
<select @bind="VisaSelectedTraveller" @bind:after="OnWorkflowTravellerChanged"
class="w-full px-3 py-1.5 border border-[#c8d3e1] rounded-lg text-xs bg-white focus:outline-none focus:border-[#0066b3]">
<option value="T. Maluleke">T. Maluleke</option>
<option value="A. Dlamini">A. Dlamini</option>
<option value="J. Mokoena">J. Mokoena</option>
<option value="N. Ndlovu">N. Ndlovu</option>
<option value="S. Pillay">S. Pillay</option>
</select>
</div>
<div>
<label class="block text-[10px] text-[#8a96a8] font-bold uppercase mb-1">Destination</label>
<select @bind="VisaSelectedDestination"
class="w-full px-3 py-1.5 border border-[#c8d3e1] rounded-lg text-xs bg-white focus:outline-none focus:border-[#0066b3]">
<option value="United Kingdom">United Kingdom</option>
<option value="Schengen Area">Schengen Area</option>
<option value="Kenya">Kenya</option>
<option value="USA">USA</option>
<option value="UAE">UAE</option>
</select>
</div>
<div class="flex gap-2">
<div class="flex-1">
<label class="block text-[10px] text-[#8a96a8] font-bold uppercase mb-1">Travel Date</label>
<input type="date" @bind="VisaTravelDate" class="w-full px-3 py-1 border border-[#c8d3e1] rounded-lg text-xs bg-white focus:outline-none focus:border-[#0066b3]" />
</div>
<button @onclick="HandleStartWorkflow"
class="px-4 py-1.5 rounded-lg text-xs font-semibold bg-[#0066b3] text-white hover:bg-[#003b71] shadow-sm cursor-pointer shrink-0">
Start
</button>
</div>
</div>
@* Steps Progress Checklist *@
@if (ActiveWorkflowApp != null)
{
<div class="space-y-3.5">
<div class="flex items-center justify-between border-b border-[#e4eaf2] pb-2">
<h4 class="text-xs font-bold text-[#003b71] uppercase tracking-wide">
Workflow Status: <span class="text-[#1f2a3a] normal-case">@ActiveWorkflowApp.Traveller (@ActiveWorkflowApp.Country)</span>
</h4>
<span class="text-[11px] text-[#8a96a8] font-medium">Travel Date: @ActiveWorkflowApp.TravelDate</span>
</div>
@foreach (var step in ActiveWorkflowApp.Steps.Select((value, i) => new { value, i }))
{
<div class="flex items-start justify-between p-3 rounded-lg border border-[#e4eaf2] bg-white hover:bg-[#fafcfe] transition-all">
<div class="flex gap-3">
<div class="h-5 w-5 rounded-full bg-[#f0f4f8] text-[#003b71] flex items-center justify-center text-[11px] font-bold shrink-0 mt-0.5">
@(step.i + 1)
</div>
<div>
<div class="text-[13px] font-semibold text-[#1f2a3a]">@step.value.Label</div>
<div class="text-[11px] text-[#8a96a8] mt-0.5">@step.value.Sub</div>
</div>
</div>
<span class="px-2.5 py-0.5 rounded-full text-[11px] font-semibold text-center whitespace-nowrap @step.value.StatusClass">
@step.value.Status
</span>
</div>
}
</div>
}
</div>
</div>
@* Application Tracker Card *@
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm flex flex-col justify-between text-left">
<div>
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-[15px] font-bold">Application Tracker</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Live status across all travellers and destinations</p>
</div>
<span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">Active Tracker</span>
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse text-left">
<thead>
<tr class="border-b border-[#e4eaf2]">
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Traveller</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Country</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Type</th>
<th class="text-left text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Stage</th>
<th class="text-right text-[11px] text-[#8a96a8] uppercase tracking-wider font-semibold py-2 px-1">Action</th>
</tr>
</thead>
<tbody class="text-sm">
@foreach (var app in VisaApplications)
{
<tr class="border-b border-[#e4eaf2] last:border-0 hover:bg-[#fafcfe] transition-colors @(ActiveWorkflowId == app.Id ? "bg-[#f4f8fc]/50" : "")">
<td class="py-2.5 px-1">
<div class="flex items-center gap-2.5">
<div class="h-7 w-7 rounded-full bg-[#e2eefb] text-[#0066b3] flex items-center justify-center font-bold text-xs uppercase shrink-0">
@app.Avatar
</div>
<div>
<div class="font-bold text-[#101827]">@app.Traveller</div>
</div>
</div>
</td>
<td class="py-2.5 px-1 text-[#5a6577]">@app.Country</td>
<td class="py-2.5 px-1 text-[#8a96a8]">@app.Type</td>
<td class="py-2.5 px-1">
<span class="inline-block px-2.5 py-0.5 rounded-full text-[11px] font-semibold @GetStageBadgeClass(app.Stage)">
@app.Stage
</span>
</td>
<td class="py-2.5 px-1 text-right whitespace-nowrap">
@if (app.Stage == "Docs required")
{
<button @onclick="() => HandleOpenUploadDocs(app.Id)"
class="px-2.5 py-1 bg-[#0066b3] hover:bg-[#003b71] text-white text-xs font-semibold rounded-lg shadow-sm transition cursor-pointer">
Upload
</button>
}
else
{
<button @onclick="() => SetActiveWorkflow(app.Id)"
class="px-2.5 py-1 bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] text-[#1f2a3a] text-xs font-semibold rounded-lg shadow-sm transition cursor-pointer">
View
</button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
@* Visa Information Library Card *@
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-5 shadow-sm text-left">
<div class="flex items-center justify-between mb-5">
<div>
<h3 class="text-[15px] font-bold">Visa Information Library</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Live entry rules database for 195 countries · Updated daily</p>
</div>
<span class="bg-[#e2eefb] text-[#0066b3] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">Updated daily</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
@* Tile 1: Entry Requirements *@
<div @onclick='() => OpenLibraryTab("requirements")'
class="p-4 border border-[#e4eaf2] rounded-xl hover:border-[#0066b3] hover:bg-[#f4f8fc]/40 transition-all cursor-pointer group text-left">
<div class="h-10 w-10 rounded-lg bg-[#e2eefb] text-[#0066b3] flex items-center justify-center mb-3 group-hover:bg-[#0066b3] group-hover:text-white transition-colors">
<!-- Globe SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20M2 12h20" />
</svg>
</div>
<h4 class="text-sm font-bold text-[#1f2a3a] group-hover:text-[#0066b3] transition-colors">Entry Requirements</h4>
<p class="text-xs text-[#8a96a8] mt-1">Rules by passport nationality & destination country</p>
</div>
@* Tile 2: Processing Times *@
<div @onclick='() => OpenLibraryTab("processing")'
class="p-4 border border-[#e4eaf2] rounded-xl hover:border-[#00a3a1] hover:bg-[#f2faf9]/50 transition-all cursor-pointer group text-left">
<div class="h-10 w-10 rounded-lg bg-[#e0f7f6] text-[#00a3a1] flex items-center justify-center mb-3 group-hover:bg-[#00a3a1] group-hover:text-white transition-colors">
<!-- Clock SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
</div>
<h4 class="text-sm font-bold text-[#1f2a3a] group-hover:text-[#00a3a1] transition-colors">Processing Times</h4>
<p class="text-xs text-[#8a96a8] mt-1">Live consulate turnaround estimation tracker</p>
</div>
@* Tile 3: Fee Calculator *@
<div @onclick='() => OpenLibraryTab("fees")'
class="p-4 border border-[#e4eaf2] rounded-xl hover:border-[#22a06b] hover:bg-[#edfbf4]/50 transition-all cursor-pointer group text-left">
<div class="h-10 w-10 rounded-lg bg-[#e3f6ed] text-[#22a06b] flex items-center justify-center mb-3 group-hover:bg-[#22a06b] group-hover:text-white transition-colors">
<!-- Calculator SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<rect x="4" y="2" width="16" height="20" rx="2" ry="2" />
<line x1="12" y1="18" x2="16" y2="18" />
<line x1="8" y1="14" x2="16" y2="14" />
<line x1="8" y1="10" x2="16" y2="10" />
</svg>
</div>
<h4 class="text-sm font-bold text-[#1f2a3a] group-hover:text-[#22a06b] transition-colors">Fee Calculator</h4>
<p class="text-xs text-[#8a96a8] mt-1">Local embassy fees plus handling & VFS charges</p>
</div>
@* Tile 4: Embassy Locator *@
<div @onclick='() => OpenLibraryTab("locator")'
class="p-4 border border-[#e4eaf2] rounded-xl hover:border-[#f4a300] hover:bg-[#fffbf2]/50 transition-all cursor-pointer group text-left">
<div class="h-10 w-10 rounded-lg bg-[#fff4dd] text-[#f4a300] flex items-center justify-center mb-3 group-hover:bg-[#f4a300] group-hover:text-white transition-colors">
<!-- MapPin SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
</div>
<h4 class="text-sm font-bold text-[#1f2a3a] group-hover:text-[#f4a300] transition-colors">Embassy Locator</h4>
<p class="text-xs text-[#8a96a8] mt-1">Nearest consulates, hours, booking portal links</p>
</div>
</div>
</div>
</div>
@* ===================== MODAL DIALOGS ===================== *@
@* 1. Visa Rules Database Modal *@
@if (ShowVisaRulesModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-2xl w-full p-6 flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div class="text-left">
<h3 class="text-base font-bold text-[#003b71]">Visa Rules Database</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">SAQA official compliance & reference database</p>
</div>
<button @onclick="CloseVisaRulesModal" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<!-- Close X SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
@* Tabs bar *@
<div class="flex border-b border-[#e4eaf2] mb-4 overflow-x-auto">
<button @onclick='() => SetActiveLibraryTab("requirements")'
class="px-4 py-2 text-xs font-semibold whitespace-nowrap border-b-2 transition-all cursor-pointer @(ActiveLibraryTab == "requirements" ? "border-[#0066b3] text-[#0066b3]" : "border-transparent text-[#5a6577] hover:text-[#1f2a3a]")">
Entry Requirements
</button>
<button @onclick='() => SetActiveLibraryTab("processing")'
class="px-4 py-2 text-xs font-semibold whitespace-nowrap border-b-2 transition-all cursor-pointer @(ActiveLibraryTab == "processing" ? "border-[#00a3a1] text-[#00a3a1]" : "border-transparent text-[#5a6577] hover:text-[#1f2a3a]")">
Processing Times
</button>
<button @onclick='() => SetActiveLibraryTab("fees")'
class="px-4 py-2 text-xs font-semibold whitespace-nowrap border-b-2 transition-all cursor-pointer @(ActiveLibraryTab == "fees" ? "border-[#22a06b] text-[#22a06b]" : "border-transparent text-[#5a6577] hover:text-[#1f2a3a]")">
Fee Calculator
</button>
<button @onclick='() => SetActiveLibraryTab("locator")'
class="px-4 py-2 text-xs font-semibold whitespace-nowrap border-b-2 transition-all cursor-pointer @(ActiveLibraryTab == "locator" ? "border-[#f4a300] text-[#f4a300]" : "border-transparent text-[#5a6577] hover:text-[#1f2a3a]")">
Consulate Locator
</button>
</div>
<div class="flex-1 overflow-y-auto space-y-4 pr-1 text-sm text-[#1f2a3a]">
@if (ActiveLibraryTab == "requirements")
{
<div class="space-y-4 text-left">
<div class="p-3 bg-[#e2eefb] text-[#0066b3] text-xs font-medium rounded-lg flex gap-2">
<!-- Info SVG -->
<svg class="w-4 h-4 shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
<span>Holders of South African passports require standard visas for UK, Schengen, and USA. eVise solutions are available for Kenya and UAE.</span>
</div>
<div class="space-y-3">
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#003b71] text-xs uppercase mb-1">United Kingdom (Standard Visitor Visa)</div>
<p class="text-xs text-[#5a6577]">Required documents: Valid passport (6 months validity), detailed travel itinerary, 3 months payslips, 3 months bank statements, employment verification letter from SAQA Human Resources.</p>
</div>
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#00a3a1] text-xs uppercase mb-1">Schengen Area (Short-stay Business Visa)</div>
<p class="text-xs text-[#5a6577]">Required documents: Schengen application form, travel insurance coverage of min €30,000, flight itinerary with reserved return tickets, hotel bookings, official SAQA invitation/support letter, 3-months stamped bank statements.</p>
</div>
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#22a06b] text-xs uppercase mb-1">USA (B1/B2 Visitor Visa)</div>
<p class="text-xs text-[#5a6577]">Required: Completed online DS-160 form, payment confirmation slip, interview scheduled at US Embassy (Pretoria/Johannesburg), physical photo (5x5cm), SAQA letter affirming funding responsibility.</p>
</div>
</div>
</div>
}
@if (ActiveLibraryTab == "processing")
{
<div class="space-y-4 text-left">
<p class="text-xs text-[#5a6577]">Approximate turnaround times based on VFS Global and consulate metrics for South African applicants:</p>
<div class="space-y-3.5">
<div>
<div class="flex justify-between text-xs font-bold mb-1">
<span>United Kingdom</span>
<span class="text-[#0066b3]">10 to 15 working days</span>
</div>
<div class="w-full bg-[#f4f7fb] h-2 rounded-full">
<div class="bg-[#0066b3] h-2 rounded-full" style="width: 45%;"></div>
</div>
</div>
<div>
<div class="flex justify-between text-xs font-bold mb-1">
<span>Schengen Area (Germany / France / Italy)</span>
<span class="text-[#00a3a1]">5 to 10 working days</span>
</div>
<div class="w-full bg-[#f4f7fb] h-2 rounded-full">
<div class="bg-[#00a3a1] h-2 rounded-full" style="width: 25%;"></div>
</div>
</div>
<div>
<div class="flex justify-between text-xs font-bold mb-1">
<span>USA (B1/B2)</span>
<span class="text-[#dc2626]">30+ working days (Appointment backlog)</span>
</div>
<div class="w-full bg-[#f4f7fb] h-2 rounded-full">
<div class="bg-[#dc2626] h-2 rounded-full" style="width: 90%;"></div>
</div>
</div>
<div>
<div class="flex justify-between text-xs font-bold mb-1">
<span>Kenya (eVisa)</span>
<span class="text-[#22a06b]">1 to 3 working days</span>
</div>
<div class="w-full bg-[#f4f7fb] h-2 rounded-full">
<div class="bg-[#22a06b] h-2 rounded-full" style="width: 10%;"></div>
</div>
</div>
</div>
</div>
}
@if (ActiveLibraryTab == "fees")
{
<div class="space-y-4 text-left">
<div class="p-4 bg-[#f4f7fb] rounded-xl border border-[#e4eaf2]">
<h4 class="text-xs font-bold text-[#003b71] uppercase mb-3">Live Interactive Visa Fee Calculator</h4>
<div class="grid grid-cols-2 gap-3 mb-4 text-left">
<div>
<label class="block text-[10px] text-[#8a96a8] font-bold uppercase mb-1">Destination</label>
<select @bind="CalcCountry"
class="w-full px-3 py-1.5 border border-[#c8d3e1] rounded-lg text-xs bg-white">
<option value="United Kingdom">United Kingdom</option>
<option value="Schengen Area">Schengen Area</option>
<option value="USA">USA</option>
<option value="Kenya">Kenya</option>
<option value="UAE">UAE</option>
</select>
</div>
<div>
<label class="block text-[10px] text-[#8a96a8] font-bold uppercase mb-1">Visa Category</label>
<select @bind="CalcVisaType"
class="w-full px-3 py-1.5 border border-[#c8d3e1] rounded-lg text-xs bg-white">
<option value="Tourist">Tourist / Visit</option>
<option value="Business">Business Meeting</option>
<option value="Transit">Transit</option>
</select>
</div>
</div>
@{
var fees = GetCalculatedFees(CalcCountry, CalcVisaType);
}
<div class="space-y-2 border-t border-[#e4eaf2] pt-3 text-xs text-[#5a6577]">
<div class="flex justify-between">
<span>Embassy consulate fee (converted):</span>
<span class="font-semibold text-[#1f2a3a]">R @fees.embassyFee.ToString("N0")</span>
</div>
<div class="flex justify-between">
<span>VFS Global/TLS handling fee:</span>
<span class="font-semibold text-[#1f2a3a]">R @fees.serviceFee.ToString("N0")</span>
</div>
<div class="flex justify-between text-sm font-bold text-[#003b71] pt-2 border-t border-dashed border-[#e4eaf2]">
<span>Total Corporate Lodge Cost:</span>
<span>R @fees.total.ToString("N0")</span>
</div>
</div>
<button @onclick="() => HandleAuthorizeLodgeCardFee(fees.total)"
class="mt-4 w-full py-2 bg-[#22a06b] hover:bg-[#1b8054] text-white text-xs font-semibold rounded-lg shadow-sm transition cursor-pointer text-center">
Authorize Lodge Card Settlement
</button>
</div>
</div>
}
@if (ActiveLibraryTab == "locator")
{
<div class="space-y-4 text-left">
<div class="space-y-3">
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#1f2a3a] text-xs">VFS Global (Schengen &amp; UAE Processing)</div>
<p class="text-xs text-[#5a6577] mt-1">Address: 1st Floor, Riviera Road Office Park, 66 Riviera Rd, Johannesburg</p>
<p class="text-[11px] text-[#8a96a8] mt-0.5">Hours: Mon-Fri 08:00 - 15:00 · Appointment Required</p>
</div>
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#1f2a3a] text-xs">TLScontact (United Kingdom Visa Center)</div>
<p class="text-xs text-[#5a6577] mt-1">Address: 3rd Floor, 24 Central, 6 Fort St, Sandton, Johannesburg</p>
<p class="text-[11px] text-[#8a96a8] mt-0.5">Hours: Mon-Fri 08:30 - 16:00 · Priority service slots available</p>
</div>
<div class="p-3 border border-[#e4eaf2] rounded-lg">
<div class="font-bold text-[#1f2a3a] text-xs">United States Consulate General</div>
<p class="text-xs text-[#5a6577] mt-1">Address: 1 Sandton Dr, Sandhurst, Sandton</p>
<p class="text-[11px] text-[#8a96a8] mt-0.5">Hours: By strict scheduled appointment slip only</p>
</div>
</div>
</div>
}
</div>
<div class="flex justify-end pt-4 border-t border-[#e4eaf2]">
<button @onclick="CloseVisaRulesModal"
class="px-4 py-2 rounded-lg text-sm font-semibold text-white bg-[#003b71] hover:bg-[#00264d] transition shadow-sm cursor-pointer">
Close Database
</button>
</div>
</div>
</div>
}
@* 2. Start New Visa Application Modal *@
@if (ShowNewAppModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div class="text-left">
<h3 class="text-base font-bold text-[#003b71]">Start New Visa Application</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Initiate visa record for tracking & automated checklist support</p>
</div>
<button @onclick="CloseNewAppModal" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<!-- Close X SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4 text-left">
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Select SAQA Traveller</label>
<select @bind="NewAppTraveller"
class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white">
<option value="T. Maluleke">T. Maluleke</option>
<option value="A. Dlamini">A. Dlamini</option>
<option value="J. Mokoena">J. Mokoena</option>
<option value="N. Ndlovu">N. Ndlovu</option>
<option value="S. Pillay">S. Pillay</option>
<option value="Custom">-- Other / Add Custom Traveller --</option>
</select>
</div>
@if (NewAppTraveller == "Custom")
{
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Traveller Full Name</label>
<input type="text" placeholder="e.g. Dr. Phumzile Sithole" @bind="NewAppCustomTraveller"
class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white" />
</div>
}
<div class="grid grid-cols-2 gap-3 text-left">
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Destination</label>
<select @bind="NewAppDestination"
class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white">
<option value="United Kingdom">United Kingdom</option>
<option value="Schengen Area">Schengen Area</option>
<option value="USA">USA</option>
<option value="Kenya">Kenya</option>
<option value="UAE">UAE</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Visa Type</label>
<select @bind="NewAppType"
class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white">
<option value="Business">Business</option>
<option value="Tourist">Tourist</option>
<option value="Transit">Transit</option>
</select>
</div>
</div>
<div>
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Estimated Travel Date</label>
<input type="date" @bind="NewAppDate" class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white" />
</div>
<div class="p-3 bg-[#e2eefb] text-[#0066b3] rounded-lg text-xs space-y-1 text-left">
<div class="font-bold flex items-center gap-1">
<!-- Info SVG -->
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
SAQA Corporate Compliance
</div>
<p>Creating this record auto-populates pre-verified profile files, schedules background lodge card authorizations, and sends VFS checklists to the traveller.</p>
</div>
<div class="flex gap-2 justify-end pt-2 border-t border-[#e4eaf2]">
<button @onclick="CloseNewAppModal"
class="px-4 py-2 rounded-lg text-sm font-semibold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">
Cancel
</button>
<button @onclick="HandleCreateNewAppFromModal"
class="px-4 py-2 rounded-lg text-sm font-semibold text-white bg-[#0066b3] hover:bg-[#003b71] cursor-pointer">
Generate Application
</button>
</div>
</div>
</div>
</div>
}
@* 3. Document Submission Portal Modal *@
@if (ShowUploadDocsModal)
{
<div class="fixed inset-0 bg-[#1f2a3a]/40 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl border border-[#e4eaf2] shadow-xl max-w-md w-full p-6">
<div class="flex items-center justify-between mb-4 pb-2 border-b border-[#e4eaf2]">
<div class="text-left">
<h3 class="text-base font-bold text-[#003b71]">Document Submission Portal</h3>
<p class="text-xs text-[#8a96a8] mt-0.5">Submit missing requirements to proceed with application</p>
</div>
<button @onclick="CloseUploadDocsModal" class="text-[#8a96a8] hover:text-[#1f2a3a] cursor-pointer">
<!-- Close X SVG -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4 text-left">
<div>
<label class="block text-[11px] font-bold text-[#5a6577] uppercase mb-1">Upload Checklists for Traveller</label>
<div class="p-3 bg-[#fff4dd] text-[#b06b00] rounded-lg text-xs flex gap-1.5 font-medium mb-3 text-left">
<!-- Info SVG -->
<svg class="w-4 h-4 shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
<span>Awaiting: Signed visa declaration form &amp; 3 months stamped bank statements.</span>
</div>
</div>
@* Mock Drag and drop zone *@
<div @onclick="TriggerMockFileUpload"
class="border-2 border-dashed border-[#c8d3e1] hover:border-[#0066b3] rounded-xl p-6 text-center bg-[#f4f7fb]/50 hover:bg-[#f4f8fc] transition-all cursor-pointer relative">
<!-- Upload SVG -->
<svg class="w-8 h-8 text-[#8a96a8] mx-auto mb-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" />
</svg>
<div class="text-sm font-bold text-[#003b71]">Click here to simulate file selection</div>
<div class="text-xs text-[#8a96a8] mt-1">Simulates uploading PDF, JPG, PNG (Max 10MB per file)</div>
</div>
@if (UploadSelectedFiles.Count > 0)
{
<div class="space-y-1.5 text-left">
<div class="text-xs font-bold text-[#5a6577] uppercase">Selected Files (@UploadSelectedFiles.Count):</div>
@foreach (var file in UploadSelectedFiles)
{
<div class="flex items-center justify-between p-2 rounded bg-[#e2eefb] text-xs text-[#0066b3] font-medium">
<span class="truncate">@file</span>
<span class="text-[10px] bg-[#22a06b] text-white px-1.5 py-0.5 rounded">Ready</span>
</div>
}
</div>
}
<div class="flex gap-2 justify-end pt-2 border-t border-[#e4eaf2]">
<button @onclick="CloseUploadDocsModal"
class="px-4 py-2 rounded-lg text-sm font-semibold text-[#1f2a3a] bg-white hover:bg-[#f4f7fb] border border-[#e4eaf2] cursor-pointer">
Cancel
</button>
<button @onclick="HandleConfirmUploadDocs"
disabled="@(UploadSelectedFiles.Count == 0)"
class="px-4 py-2 rounded-lg text-sm font-semibold text-white transition @(UploadSelectedFiles.Count > 0 ? "bg-[#22a06b] hover:bg-[#1b8054] cursor-pointer" : "bg-[#8a96a8]/50 cursor-not-allowed")">
Submit Documents
</button>
</div>
</div>
</div>
</div>
}
@code {
// Model Definitions
public class VisaStep
{
public string Label { get; set; } = string.Empty;
public string Sub { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string StatusClass { get; set; } = string.Empty;
}
public class VisaApplication
{
public int Id { get; set; }
public string Traveller { get; set; } = string.Empty;
public string Avatar { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Stage { get; set; } = string.Empty;
public string TravelDate { get; set; } = string.Empty;
public List<VisaStep> Steps { get; set; } = new();
}
// Core States
private int ActiveAppsCount { get; set; } = 5;
private int ApprovedThisMonth { get; set; } = 8;
private string AverageTurnaround { get; set; } = "9 days";
private int DocumentsPending { get; set; } = 3;
private string AlertFeedback { get; set; } = string.Empty;
// Interactive Selectors
private string VisaSelectedTraveller { get; set; } = "T. Maluleke";
private string VisaSelectedDestination { get; set; } = "United Kingdom";
private DateTime VisaTravelDate { get; set; } = new DateTime(2026, 6, 14);
private int ActiveWorkflowId { get; set; } = 1;
// Library interactive fee calculator state
private string CalcCountry { get; set; } = "United Kingdom";
private string CalcVisaType { get; set; } = "Business";
// Modals visibility toggles
private bool ShowVisaRulesModal { get; set; } = false;
private bool ShowNewAppModal { get; set; } = false;
private bool ShowUploadDocsModal { get; set; } = false;
private string ActiveLibraryTab { get; set; } = "requirements";
// Modals data input forms
private string NewAppTraveller { get; set; } = "T. Maluleke";
private string NewAppCustomTraveller { get; set; } = string.Empty;
private string NewAppDestination { get; set; } = "Schengen Area";
private string NewAppType { get; set; } = "Business";
private DateTime NewAppDate { get; set; } = new DateTime(2026, 7, 15);
// Document submissions portal tracker
private int? SelectedUploadAppId { get; set; } = null;
private List<string> UploadSelectedFiles { get; set; } = new();
// Data Source List
private List<VisaApplication> VisaApplications { get; set; } = new();
// Derived active app in workflow detail panel
private VisaApplication? ActiveWorkflowApp =>
VisaApplications.FirstOrDefault(a => a.Id == ActiveWorkflowId) ?? VisaApplications.FirstOrDefault();
protected override void OnInitialized()
{
// Populate standard mock data matching React App's seed dataset
VisaApplications = new List<VisaApplication>
{
new VisaApplication
{
Id = 1,
Traveller = "T. Maluleke",
Avatar = "TM",
Country = "United Kingdom",
Type = "Business",
Stage = "At consulate",
TravelDate = "2026-06-14",
Steps = new List<VisaStep>
{
new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 2, document checklist", Sub = "Passport, photo, invitation letter, bank statement", Status = "4 of 4 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 2,840", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 4, consulate appointment", Sub = "VFS Sandton, 28 May, 10:30", Status = "Booked", StatusClass = "bg-[#e2eefb] text-[#0066b3] border border-[#b4d3f5]" },
new VisaStep { Label = "Step 5, decision", Sub = "Visa sticker delivery to office", Status = "Awaiting", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" }
}
},
new VisaApplication
{
Id = 2,
Traveller = "A. Dlamini",
Avatar = "AD",
Country = "Kenya",
Type = "eVisa",
Stage = "Approved",
TravelDate = "2026-06-20",
Steps = new List<VisaStep>
{
new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step-2, document checklist", Sub = "Passport copy, digital photo, itinerary", Status = "2 of 2 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 1,250", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 4, consulate appointment", Sub = "Not Required (eVisa system)", Status = "Not Required", StatusClass = "bg-[#f4f7fb] text-[#5a6577] border border-[#e4eaf2]" },
new VisaStep { Label = "Step 5, decision", Sub = "Approved eVisa copy sent to traveller email", Status = "Approved", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }
}
},
new VisaApplication
{
Id = 3,
Traveller = "J. Mokoena",
Avatar = "JM",
Country = "Schengen Area",
Type = "Business",
Stage = "Docs uploaded",
TravelDate = "2026-07-01",
Steps = new List<VisaStep>
{
new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 2, document checklist", Sub = "Passport, photo, Schengen insurance, flights", Status = "4 of 4 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 2,200", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 4, consulate appointment", Sub = "VFS Global Cherry Lane, 12 June, 09:15", Status = "Booked", StatusClass = "bg-[#e2eefb] text-[#0066b3] border border-[#b4d3f5]" },
new VisaStep { Label = "Step 5, decision", Sub = "Passport pending collection", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" }
}
},
new VisaApplication
{
Id = 4,
Traveller = "N. Ndlovu",
Avatar = "NN",
Country = "USA",
Type = "B1 / B2",
Stage = "Docs required",
TravelDate = "2026-08-15",
Steps = new List<VisaStep>
{
new VisaStep { Label = "Step 1, DS-160 application form", Sub = "Partially filled by traveller", Status = "In progress", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" },
new VisaStep { Label = "Step 2, document checklist", Sub = "Passport, US-spec photos, bank statements required", Status = "2 of 4 uploaded", StatusClass = "bg-[#fde5e8] text-[#dc2626] border border-[#f8b4b4]" },
new VisaStep { Label = "Step 3, fee payment", Sub = "Pending approval from travel authority", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" },
new VisaStep { Label = "Step 4, consulate appointment", Sub = "Awaiting DS-160 completion & payment", Status = "Not booked", StatusClass = "bg-[#f4f7fb] text-[#5a6577] border border-[#e4eaf2]" },
new VisaStep { Label = "Step 5, decision", Sub = "US Embassy Pretoria interview", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" }
}
},
new VisaApplication
{
Id = 5,
Traveller = "S. Pillay",
Avatar = "SP",
Country = "UAE",
Type = "eVisa",
Stage = "Approved",
TravelDate = "2026-06-05",
Steps = new List<VisaStep>
{
new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 2, document checklist", Sub = "Passport copy, photo, hotel booking", Status = "2 of 2 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 1,850", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 4, consulate appointment", Sub = "Not Required (eVisa system)", Status = "Not Required", StatusClass = "bg-[#f4f7fb] text-[#5a6577] border border-[#e4eaf2]" },
new VisaStep { Label = "Step 5, decision", Sub = "Approved eVisa copy sent to traveller email", Status = "Approved", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" }
}
}
};
}
// Workflows and Tracker actions
private void OnWorkflowTravellerChanged()
{
var matched = VisaApplications.FirstOrDefault(a => a.Traveller.Equals(VisaSelectedTraveller, System.StringComparison.OrdinalIgnoreCase));
if (matched != null)
{
ActiveWorkflowId = matched.Id;
}
}
private void SetActiveWorkflow(int id)
{
ActiveWorkflowId = id;
var app = VisaApplications.FirstOrDefault(a => a.Id == id);
if (app != null)
{
VisaSelectedTraveller = app.Traveller;
VisaSelectedDestination = app.Country;
}
}
private void HandleStartWorkflow()
{
var existing = VisaApplications.FirstOrDefault(app => app.Traveller.Equals(VisaSelectedTraveller, System.StringComparison.OrdinalIgnoreCase));
if (existing != null)
{
ActiveWorkflowId = existing.Id;
TriggerFeedback($"Application for {VisaSelectedTraveller} already exists. Showing current progress below.");
}
else
{
var initials = string.Concat(VisaSelectedTraveller.Split(' ').Select(n => n.Length > 0 ? n[0].ToString() : string.Empty)).ToUpper();
if (string.IsNullOrEmpty(initials)) initials = "TR";
var newId = (VisaApplications.Any() ? VisaApplications.Max(a => a.Id) : 0) + 1;
var newApp = new VisaApplication
{
Id = newId,
Traveller = VisaSelectedTraveller,
Avatar = initials,
Country = VisaSelectedDestination,
Type = "Business",
Stage = "Docs uploaded",
TravelDate = VisaTravelDate.ToString("yyyy-MM-dd"),
Steps = new List<VisaStep>
{
new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 2, document checklist", Sub = "Passport copy, digital photos, supporting letters", Status = "4 of 4 uploaded", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 3, fee payment", Sub = "Settled via SAQA lodge card", Status = "Paid R 2,150", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep { Label = "Step 4, consulate appointment", Sub = "Awaiting appointment confirmation", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" },
new VisaStep { Label = "Step 5, decision", Sub = "Embassy review in progress", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" }
}
};
VisaApplications.Insert(0, newApp);
ActiveWorkflowId = newId;
ActiveAppsCount++;
TriggerFeedback($"Successfully initialized a new {VisaSelectedDestination} visa application workflow for {VisaSelectedTraveller}.");
}
}
private void HandleCreateNewAppFromModal()
{
var name = NewAppTraveller == "Custom" ? NewAppCustomTraveller : NewAppTraveller;
if (string.IsNullOrWhiteSpace(name))
{
TriggerFeedback("Please enter a traveller name.");
return;
}
var initials = string.Concat(name.Split(' ').Select(n => n.Length > 0 ? n[0].ToString() : string.Empty)).ToUpper();
if (string.IsNullOrEmpty(initials)) initials = "TR";
var newId = (VisaApplications.Any() ? VisaApplications.Max(a => a.Id) : 0) + 1;
bool isUS = NewAppDestination == "USA";
var newApp = new VisaApplication
{
Id = newId,
Traveller = name,
Avatar = initials,
Country = NewAppDestination,
Type = NewAppType,
Stage = isUS ? "Docs required" : "Docs uploaded",
TravelDate = NewAppDate.ToString("yyyy-MM-dd"),
Steps = new List<VisaStep>
{
new VisaStep { Label = "Step 1, application form", Sub = "Auto populated from traveller profile", Status = "Auto filled", StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]" },
new VisaStep
{
Label = "Step 2, document checklist",
Sub = isUS ? "Passport, photo, US visa fee, 3-months bank statement" : "Passport copy, photos, travel insurance, hotel bookings",
Status = isUS ? "2 of 4 uploaded" : "4 of 4 uploaded",
StatusClass = isUS ? "bg-[#fde5e8] text-[#dc2626] border border-[#f8b4b4]" : "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]"
},
new VisaStep
{
Label = "Step 3, fee payment",
Sub = "Settled via SAQA lodge card",
Status = isUS ? "Pending" : "Paid R 2,250",
StatusClass = isUS ? "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" : "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]"
},
new VisaStep
{
Label = "Step 4, consulate appointment",
Sub = isUS ? "Awaiting DS-160 and payment" : "VFS Global appointment confirmed",
Status = isUS ? "Not booked" : "Booked",
StatusClass = isUS ? "bg-[#f4f7fb] text-[#5a6577] border border-[#e4eaf2]" : "bg-[#e2eefb] text-[#0066b3] border border-[#b4d3f5]"
},
new VisaStep { Label = "Step 5, decision", Sub = "Visa sticker or eVisa approval pending review", Status = "Pending", StatusClass = "bg-[#fff4dd] text-[#b06b00] border border-[#fbd897]" }
}
};
VisaApplications.Insert(0, newApp);
ActiveWorkflowId = newId;
ActiveAppsCount++;
if (isUS)
{
DocumentsPending++;
}
TriggerFeedback($"New application created successfully for {name} ({NewAppDestination}).");
ShowNewAppModal = false;
NewAppCustomTraveller = string.Empty;
}
private void HandleOpenUploadDocs(int appId)
{
SelectedUploadAppId = appId;
UploadSelectedFiles.Clear();
ShowUploadDocsModal = true;
}
private void TriggerMockFileUpload()
{
// Simulates double-clicking or uploading typical checklist files in Blazor page
UploadSelectedFiles = new List<string> { "signed_visa_form.pdf", "stamped_bank_statements_3_months.pdf" };
StateHasChanged();
}
private void HandleConfirmUploadDocs()
{
if (SelectedUploadAppId == null) return;
var targetId = SelectedUploadAppId.Value;
var app = VisaApplications.FirstOrDefault(a => a.Id == targetId);
if (app != null)
{
app.Stage = "Docs uploaded";
if (app.Steps.Count >= 3)
{
app.Steps[1].Status = "4 of 4 uploaded";
app.Steps[1].StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]";
app.Steps[2].Status = "Paid R 2,840";
app.Steps[2].StatusClass = "bg-[#e3f6ed] text-[#22a06b] border border-[#a1dfc2]";
}
DocumentsPending = System.Math.Max(0, DocumentsPending - 1);
TriggerFeedback($"Documents uploaded successfully for {app.Traveller}. Application is now complete for submission!");
}
ShowUploadDocsModal = false;
SelectedUploadAppId = null;
}
// Modal Helpers
private void OpenLibraryTab(string tabName)
{
ActiveLibraryTab = tabName;
ShowVisaRulesModal = true;
}
private void SetActiveLibraryTab(string tabName)
{
ActiveLibraryTab = tabName;
}
private void CloseVisaRulesModal()
{
ShowVisaRulesModal = false;
}
private void OpenNewAppModal()
{
ShowNewAppModal = true;
}
private void CloseNewAppModal()
{
ShowNewAppModal = false;
}
private void CloseUploadDocsModal()
{
ShowUploadDocsModal = false;
}
// Fee calculations
private (int embassyFee, int serviceFee, int total) GetCalculatedFees(string country, string type)
{
int embassyFee = 2450;
int serviceFee = 450;
if (country == "United Kingdom")
{
embassyFee = type == "Transit" ? 950 : type == "Tourist" ? 2150 : 2840;
}
else if (country == "Schengen Area")
{
embassyFee = type == "Transit" ? 820 : type == "Tourist" ? 1820 : 2200;
}
else if (country == "USA")
{
embassyFee = type == "Transit" ? 1950 : type == "Tourist" ? 3420 : 3650;
}
else if (country == "Kenya")
{
embassyFee = 540;
serviceFee = 150;
}
else if (country == "UAE")
{
embassyFee = type == "Transit" ? 1150 : type == "Tourist" ? 1650 : 1850;
}
return (embassyFee, serviceFee, embassyFee + serviceFee);
}
private void HandleAuthorizeLodgeCardFee(int totalFee)
{
TriggerFeedback($"Authorized R {totalFee.ToString("N0")} on Lodge Card for {CalcCountry} ({CalcVisaType} Visa).");
}
// Badge CSS Mapper helper
private string GetStageBadgeClass(string stage)
{
return stage switch
{
"Approved" => "bg-[#e3f6ed] text-[#22a06b]",
"Docs required" => "bg-[#fde5e8] text-[#dc2626]",
"Docs uploaded" => "bg-[#e2eefb] text-[#0066b3]",
_ => "bg-[#fff4dd] text-[#b06b00]"
};
}
// Toast/Feedback Trigger
private void TriggerFeedback(string message)
{
AlertFeedback = message;
// Simulate simple asynchronous auto-dismiss in C# Blazor
_ = DismissAlertAfterDelay();
}
private async System.Threading.Tasks.Task DismissAlertAfterDelay()
{
await System.Threading.Tasks.Task.Delay(5000);
AlertFeedback = string.Empty;
StateHasChanged();
}
}