1095 lines
63 KiB
Plaintext
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 & 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 & 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();
|
|
}
|
|
}
|