1223 lines
81 KiB
Plaintext
1223 lines
81 KiB
Plaintext
@page "/sla-reports"
|
|
@using System.Collections.Generic
|
|
@using System.Linq
|
|
@using System.Threading.Tasks
|
|
@rendermode InteractiveServer
|
|
|
|
<div class="bg-[#f4f7fb] min-h-screen text-[#1f2a3a] font-sans antialiased p-6 md:p-8 text-left">
|
|
|
|
@* Page Header Area *@
|
|
<div class="flex flex-col lg:flex-row lg:items-end justify-between mb-6 gap-4 border-b border-[#e4eaf2] pb-5">
|
|
<div class="text-left">
|
|
<h2 class="text-2xl font-bold tracking-tight text-[#003b71]">SLA & Monthly Reports Dashboard</h2>
|
|
<p class="text-xs text-[#5a6577] mt-1">Enterprise service level compliance, National Treasury rate-cap checking, and monthly operational reports ledger prepared for SAQA bid submission by TMT Projects</p>
|
|
</div>
|
|
|
|
@* Filters & Controls *@
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-[#8a96a8] font-bold uppercase shrink-0">Period:</span>
|
|
<select @bind="SelectedPeriod" @bind:after="OnFilterChanged"
|
|
class="px-2.5 py-1.5 bg-white text-[#1f2a3a] border border-[#c8d3e1] rounded-lg text-xs font-semibold focus:outline-none focus:border-[#0066b3]">
|
|
<option value="May 2026">May 2026 (Active)</option>
|
|
<option value="April 2026">April 2026</option>
|
|
<option value="Q1 2026">Q1 2026 (Quarterly)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-[#8a96a8] font-bold uppercase shrink-0">Cost Centre:</span>
|
|
<select @bind="SelectedCostCentre" @bind:after="OnFilterChanged"
|
|
class="px-2.5 py-1.5 bg-white text-[#1f2a3a] border border-[#c8d3e1] rounded-lg text-xs font-semibold focus:outline-none focus:border-[#0066b3]">
|
|
<option value="All">All Cost Centres</option>
|
|
<option value="Office of the CEO">Office of the CEO</option>
|
|
<option value="Qualifications and Standards">Qualifications & Standards</option>
|
|
<option value="Verifications">Verifications</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex gap-2 shrink-0">
|
|
<button @onclick="TriggerPdfDownload"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] transition shadow-sm cursor-pointer">
|
|
<!-- PDF Document SVG -->
|
|
<svg class="w-3.5 h-3.5 text-[#003b71]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
</svg>
|
|
Export SLA Pack (PDF)
|
|
</button>
|
|
<button @onclick="TriggerExcelDownload"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold bg-[#00a3a1] text-white hover:bg-[#008280] transition shadow-sm cursor-pointer">
|
|
<!-- Spreadsheet SVG -->
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v16.5M21 3v16.5M3.75 3h17.25M3.75 21h17.25M3.75 12h17.25M12 3v18" />
|
|
</svg>
|
|
Download Logs (Excel)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@* Alert / Action Feedback Banner *@
|
|
@if (!string.IsNullOrEmpty(AlertFeedback))
|
|
{
|
|
<div class="mb-6 p-4 bg-[#effaf3] border-l-4 border-[#22a06b] rounded-r-lg text-xs text-[#1b8054] flex items-center justify-between shadow-sm transition-all animate-fade-in">
|
|
<div class="flex items-center gap-2">
|
|
<!-- Check SVG -->
|
|
<svg class="w-4 h-4 text-[#22a06b] shrink-0" 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 class="font-bold">@AlertFeedback</span>
|
|
</div>
|
|
<button @onclick="() => AlertFeedback = string.Empty" class="text-[#8a96a8] hover:text-[#1f2a3a]">
|
|
<svg class="w-3.5 h-3.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>
|
|
}
|
|
|
|
@* SLA Scorecard Cards - Quick telemetry indicators *@
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-6 gap-4 mb-6">
|
|
@foreach (var metric in CoreSlaMetrics)
|
|
{
|
|
<div class="bg-white border border-[#e4eaf2] rounded-[10px] p-4 shadow-sm relative overflow-hidden before:content-[''] before:absolute before:top-0 before:left-0 before:w-1 before:h-full @metric.BarColorClass text-left">
|
|
<div class="text-[10px] text-[#5a6577] uppercase tracking-wider font-bold mb-1">@metric.Name</div>
|
|
<div class="flex items-baseline gap-1">
|
|
<span class="text-xl font-black text-[#1f2a3a]">@metric.Actual</span>
|
|
<span class="text-[10px] text-[#8a96a8]">@metric.Unit</span>
|
|
</div>
|
|
<div class="flex items-center justify-between gap-1 mt-2 text-[10px] border-t border-[#f4f7fb] pt-2">
|
|
<span class="text-gray-500">Target: <strong class="text-gray-700">@metric.Target</strong></span>
|
|
<span class="px-1.5 py-0.5 rounded font-bold text-[8px] @metric.StatusClass">@metric.StatusLabel</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@* Navigation Tabs Bar *@
|
|
<div class="flex border-b border-[#e4eaf2] mb-6 overflow-x-auto gap-1">
|
|
<button @onclick="@(() => SetActiveTab("scorecard"))"
|
|
class="inline-flex items-center gap-2 px-5 py-3 text-xs font-bold whitespace-nowrap border-b-2 transition-all cursor-pointer @(ActiveTab == "scorecard" ? "border-[#003b71] text-[#003b71]" : "border-transparent text-[#5a6577] hover:text-[#1f2a3a]")">
|
|
<!-- Analytics SVG -->
|
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
|
|
</svg>
|
|
SLA Scorecard Details
|
|
</button>
|
|
<button @onclick="@(() => SetActiveTab("library"))"
|
|
class="inline-flex items-center gap-2 px-5 py-3 text-xs font-bold whitespace-nowrap border-b-2 transition-all cursor-pointer @(ActiveTab == "library" ? "border-[#0066b3] text-[#0066b3]" : "border-transparent text-[#5a6577] hover:text-[#1f2a3a]")">
|
|
<!-- Book/Folder SVG -->
|
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.967 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
|
|
</svg>
|
|
Monthly Reports Library
|
|
<span class="bg-[#00a3a1] text-white text-[9px] font-bold px-1.5 py-0.5 rounded-full">4 Logs</span>
|
|
</button>
|
|
<button @onclick="@(() => SetActiveTab("consultants"))"
|
|
class="inline-flex items-center gap-2 px-5 py-3 text-xs font-bold whitespace-nowrap border-b-2 transition-all cursor-pointer @(ActiveTab == "consultants" ? "border-[#00a3a1] text-[#00a3a1]" : "border-transparent text-[#5a6577] hover:text-[#1f2a3a]")">
|
|
<!-- Users SVG -->
|
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.109A11.386 11.386 0 0110.089 21m5.016-1.872a11.383 11.386 0 01-5.017-1.872M10.089 21a11.375 11.375 0 01-5.017-1.872m5.017 1.872v-.008c0-1.113-.285-2.16-.786-3.07M10.089 21v-.008A11.32 11.32 0 0010 18.25m-5.017 1.872c-.497-.905-.783-1.94-.783-3.03v-.123m0 0A4.125 4.125 0 0110 14.25m-5.017 1.872V16m5.017-1.75a4.125 4.125 0 015.017 1.75m0 0H18m-5.017-1.75a11.32 11.32 0 00-.783-3.03M10 18.25a11.32 11.32 0 01-.783-3.03M10 18.25c-.29 0-.573-.02-.85-.058m0 0c.29-.06.573-.146.85-.258" />
|
|
</svg>
|
|
Consultant Performance Index
|
|
</button>
|
|
<button @onclick="@(() => SetActiveTab("disputes"))"
|
|
class="inline-flex items-center gap-2 px-5 py-3 text-xs font-bold whitespace-nowrap border-b-2 transition-all cursor-pointer @(ActiveTab == "disputes" ? "border-[#dc2626] text-[#dc2626]" : "border-transparent text-[#5a6577] hover:text-[#1f2a3a]")">
|
|
<!-- ShieldExclamation SVG -->
|
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.249-8.25-3.286zm0 13.036h.008v.008H12v-.008z" />
|
|
</svg>
|
|
Log Service Violation / Query
|
|
<span class="bg-[#dc2626] text-white text-[9px] font-bold px-1.5 py-0.5 rounded-full">@ServiceQueries.Count(q => q.Status != "Resolved") Open</span>
|
|
</button>
|
|
</div>
|
|
|
|
@* ===================== TAB 1: SCORECARD DETAILS ===================== *@
|
|
@if (ActiveTab == "scorecard")
|
|
{
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in text-left">
|
|
|
|
@* Left block: Target meters list (lg:col-span-8) *@
|
|
<div class="lg:col-span-8 bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left">
|
|
<div class="flex items-center justify-between mb-4 border-b border-[#e4eaf2] pb-3">
|
|
<div>
|
|
<h3 class="text-sm font-bold text-[#003b71]">SAQA Dedicated SLA Telemetry Matrix</h3>
|
|
<p class="text-xs text-[#8a96a8] mt-0.5">May 2026 contractual performance indexes, target deviations, and billing benchmarks</p>
|
|
</div>
|
|
<span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">97.8% SLA Aggregated Compliance</span>
|
|
</div>
|
|
|
|
<div class="space-y-4 pt-2">
|
|
|
|
@* Metric row 1 *@
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between items-center text-xs">
|
|
<div>
|
|
<span class="font-bold text-[#1f2a3a]">Booking Response turn-around (Domestic)</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Measure of time elapsed from travel authorization submission to first quote iteration</span>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="font-bold text-[#22a06b]">44 minutes</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Target: < 60 mins (98.4% met)</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-full bg-[#f4f7fb] h-2 rounded-full overflow-hidden">
|
|
<div class="bg-[#22a06b] h-2 rounded-full" style="width: 98%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
@* Metric row 2 *@
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between items-center text-xs">
|
|
<div>
|
|
<span class="font-bold text-[#1f2a3a]">Emergency After-Hours response speed</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Emergency helpline consultant response and ticket remediation threshold</span>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="font-bold text-[#22a06b]">9 minutes</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Target: < 15 mins (100% met)</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-full bg-[#f4f7fb] h-2 rounded-full overflow-hidden">
|
|
<div class="bg-[#00a3a1] h-2 rounded-full" style="width: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
@* Metric row 3 *@
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between items-center text-xs">
|
|
<div>
|
|
<span class="font-bold text-[#1f2a3a]">National Treasury Price Caps Adherence Rate</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Adherence to Treasury instructions on flight quotas, hotel matrices and shuttle spending caps</span>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="font-bold text-[#f4a300]">94.1% compliance</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Target: 100% (7 authorized deviations)</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-full bg-[#f4f7fb] h-2 rounded-full overflow-hidden">
|
|
<div class="bg-[#f4a300] h-2 rounded-full" style="width: 94%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
@* Metric row 4 *@
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between items-center text-xs">
|
|
<div>
|
|
<span class="font-bold text-[#1f2a3a]">Billing Reconciliation & Lodge Matching</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Automated lodge card clearing speed and invoice linkages matching PO guidelines</span>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="font-bold text-[#22a06b]">28 hours</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Target: < 48 hours (99.1% met)</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-full bg-[#f4f7fb] h-2 rounded-full overflow-hidden">
|
|
<div class="bg-[#22a06b] h-2 rounded-full" style="width: 99%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
@* Metric row 5 *@
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between items-center text-xs">
|
|
<div>
|
|
<span class="font-bold text-[#1f2a3a]">Ticketing Accuracy / Zero Voucher Error Rate</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Zero ticketing rejects or hotel vouchers invalid on check-in</span>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="font-bold text-[#22a06b]">99.96% accuracy</span>
|
|
<span class="text-[10px] text-[#8a96a8] block">Target: > 99.90% (0 errors)</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-full bg-[#f4f7fb] h-2 rounded-full overflow-hidden">
|
|
<div class="bg-[#22a06b] h-2 rounded-full" style="width: 99%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
@* Consolidated SLA Quality Box *@
|
|
<div class="p-3.5 bg-[#effaf3] rounded-lg border border-[#a1dfc2] flex flex-col sm:flex-row justify-between items-center gap-3 mt-4">
|
|
<div class="text-left">
|
|
<span class="block text-[11px] text-[#1b8054] font-bold uppercase">SLA Target Met Rating</span>
|
|
<span class="text-xs text-[#2b7250] mt-0.5 block">TMT Projects dedicated operations are strictly aligned with ASATA standards, assuring SAQA of seamless continuity.</span>
|
|
</div>
|
|
<div class="shrink-0 text-center px-4 py-1.5 bg-[#22a06b] text-white rounded font-bold text-xs">
|
|
97.8% Achieved
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
@* Right block: Live SLA alerts & warnings desk (lg:col-span-4) *@
|
|
<div class="lg:col-span-4 bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left flex flex-col justify-between">
|
|
<div>
|
|
<div class="mb-4 pb-2 border-b border-[#e4eaf2]">
|
|
<h3 class="text-sm font-bold text-[#003b71]">Live SLA Alert Desk</h3>
|
|
<p class="text-xs text-[#8a96a8] mt-0.5">Real-time GDS operational alerts matching May SLAs</p>
|
|
</div>
|
|
|
|
<div class="space-y-3.5">
|
|
<div class="p-3 border border-[#fbd897] bg-[#fff9ec] rounded-lg flex items-start gap-2.5">
|
|
<!-- Warning SVG -->
|
|
<svg class="w-4 h-4 text-[#f4a300] shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<div class="text-xs">
|
|
<div class="font-bold text-[#1f2a3a]">Hotel Cap Deviation Pending Review</div>
|
|
<p class="text-[#5a6577] mt-0.5">Cape Town peak-booking R 2,400/night has passed 24h limit without CFO override. Ticket pending hold.</p>
|
|
<span class="text-[9px] font-mono text-gray-400 mt-1 block">SLA limit: 4h response standard</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-3 border border-[#b4d3f5] bg-[#f3f8fd] rounded-lg flex items-start gap-2.5">
|
|
<!-- Information SVG -->
|
|
<svg class="w-4 h-4 text-[#0066b3] shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 111.063.852l-.708 2.836a.75.75 0 001.063.852l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
|
</svg>
|
|
<div class="text-xs">
|
|
<div class="font-bold text-[#1f2a3a]">After-Hours Call log triggered</div>
|
|
<p class="text-[#5a6577] mt-0.5">London flight delay rerouting handled for Dr. Sithole. Resolution verified in 12 mins.</p>
|
|
<span class="text-[9px] font-mono text-green-600 font-bold mt-1 block">Target met (9m actual)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-3 border border-[#a1dfc2] bg-[#effaf3] rounded-lg flex items-start gap-2.5">
|
|
<!-- Verification Shield SVG -->
|
|
<svg class="w-4 h-4 text-[#22a06b] shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
|
|
</svg>
|
|
<div class="text-xs">
|
|
<div class="font-bold text-[#1f2a3a]">Lodge Reconciliation auto-cleared</div>
|
|
<p class="text-[#5a6577] mt-0.5">142 May flight billing matched automatically with zero manual discrepancies logged.</p>
|
|
<span class="text-[9px] font-mono text-green-600 font-bold mt-1 block">Reconciled standard: 100%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3 border-t border-[#e4eaf2] text-[10px] text-gray-500 leading-normal">
|
|
Real-time monitoring nodes are automatically backed by failover pathways. Performance thresholds audited independently.
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
}
|
|
|
|
@* ===================== TAB 2: REPORTS LIBRARY ===================== *@
|
|
@if (ActiveTab == "library")
|
|
{
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in text-left">
|
|
|
|
@* Reports Listing column (lg:col-span-4) *@
|
|
<div class="lg:col-span-4 bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left flex flex-col justify-between">
|
|
<div>
|
|
<div class="mb-4 pb-2 border-b border-[#e4eaf2]">
|
|
<h3 class="text-sm font-bold text-[#003b71]">Standard Monthly Logs</h3>
|
|
<p class="text-xs text-[#8a96a8] mt-0.5">Select a ledger below to view the interactive live telemetry data</p>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
@foreach (var report in MonthlyReportTemplates)
|
|
{
|
|
<button @onclick="@(() => SelectReport(report.Key))"
|
|
class="w-full text-left p-3 rounded-lg border transition-all flex items-start gap-3 cursor-pointer @(SelectedReportKey == report.Key ? "bg-[#e2eefb] border-[#0066b3] text-[#003b71]" : "border-[#e4eaf2] hover:bg-[#f4f7fb] text-gray-700")">
|
|
<span class="text-xl mt-0.5 shrink-0">@report.Value.Emoji</span>
|
|
<div class="text-xs">
|
|
<div class="font-bold">@report.Value.Name</div>
|
|
<p class="text-[10px] opacity-80 mt-0.5">@report.Value.Description</p>
|
|
<span class="inline-block mt-2 font-mono text-[9px] bg-white px-2 py-0.5 rounded border border-current font-bold uppercase">
|
|
@GetReportCount(report.Key) Records
|
|
</span>
|
|
</div>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 p-3 bg-[#e0f7f6] text-[#00a3a1] rounded-lg text-xs">
|
|
<span class="font-bold block">National Treasury compliant formats</span>
|
|
<p class="text-[10px] mt-0.5 text-[#2b7250]">All ledgers export smoothly to National Treasury standard spreadsheets with pre-populated cost centers.</p>
|
|
</div>
|
|
</div>
|
|
|
|
@* Report Interactive Preview Table (lg:col-span-8) *@
|
|
<div class="lg:col-span-8 bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left flex flex-col justify-between min-h-[500px]">
|
|
<div>
|
|
@* Table Header Area *@
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-[#e4eaf2] pb-4 mb-4">
|
|
<div class="text-left">
|
|
<h3 class="text-sm font-bold text-[#003b71] flex items-center gap-1.5">
|
|
<span class="text-xl">@ActiveReportMetadata.Emoji</span>
|
|
@ActiveReportMetadata.Name
|
|
</h3>
|
|
<p class="text-xs text-[#8a96a8] mt-0.5">Active data preview for @SelectedPeriod period</p>
|
|
</div>
|
|
|
|
@* Search Filter *@
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-xs text-[#8a96a8] shrink-0 font-medium">Search:</span>
|
|
<input type="text" @bind="ReportSearchQuery" @bind:event="oninput"
|
|
placeholder="Search ledger..."
|
|
class="px-2.5 py-1.5 bg-[#f4f7fb] text-xs border border-[#c8d3e1] rounded-lg font-semibold focus:outline-none focus:border-[#0066b3] max-w-[150px]" />
|
|
</div>
|
|
</div>
|
|
|
|
@* Table Renderings based on chosen reports *@
|
|
<div class="overflow-x-auto min-h-[350px]">
|
|
@if (SelectedReportKey == "deviations")
|
|
{
|
|
<table class="w-full border-collapse text-left">
|
|
<thead>
|
|
<tr class="border-b border-[#e4eaf2]">
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Traveller</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Route</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Paid</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Justification / Reason</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Approver</th>
|
|
<th class="text-right text-[10px] text-[#8a96a8] uppercase font-bold py-2">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-xs">
|
|
@foreach (var row in FilteredDeviations)
|
|
{
|
|
<tr class="border-b border-[#e4eaf2] hover:bg-[#fafcfe] transition-colors">
|
|
<td class="py-2.5 font-bold text-[#101827]">
|
|
<div>@row.Traveller</div>
|
|
<span class="text-[9px] text-[#8a96a8] block">@row.CostCentre</span>
|
|
</td>
|
|
<td class="py-2.5 text-gray-600">
|
|
<div>@row.Route (@row.Supplier)</div>
|
|
<span class="text-[9px] text-gray-400 block">@row.Date</span>
|
|
</td>
|
|
<td class="py-2.5 font-mono">
|
|
<div class="font-bold text-[#dc2626]">R @row.PaidPrice.ToString("N0")</div>
|
|
<span class="text-[9px] text-green-600 block">Saved: R @row.Saving.ToString("N0")</span>
|
|
</td>
|
|
<td class="py-2.5 max-w-[200px]">
|
|
<div class="font-medium text-gray-800 text-[11px] truncate" title="@row.Justification">@row.Justification</div>
|
|
<span class="text-[9px] font-mono text-gray-500 uppercase block">Code: @row.ReasonCode</span>
|
|
</td>
|
|
<td class="py-2.5 font-semibold text-[#0066b3]">@row.ApprovedBy</td>
|
|
<td class="py-2.5 text-right">
|
|
<span class="inline-block px-2 py-0.5 rounded text-[9px] font-bold bg-[#e3f6ed] text-[#22a06b]">@row.Status</span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
@if (!FilteredDeviations.Any())
|
|
{
|
|
<tr>
|
|
<td colspan="6" class="text-center py-8 text-gray-400 text-xs">No matching records found.</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
else if (SelectedReportKey == "afterhours")
|
|
{
|
|
<table class="w-full border-collapse text-left">
|
|
<thead>
|
|
<tr class="border-b border-[#e4eaf2]">
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Timestamp</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Traveller</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Category / Reason</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Resolution Process</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">SLA Speed</th>
|
|
<th class="text-right text-[10px] text-[#8a96a8] uppercase font-bold py-2">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-xs">
|
|
@foreach (var row in FilteredAfterHours)
|
|
{
|
|
<tr class="border-b border-[#e4eaf2] hover:bg-[#fafcfe] transition-colors">
|
|
<td class="py-2.5 text-gray-600 font-mono">
|
|
<div>@row.Date</div>
|
|
<span class="text-[9px] text-[#8a96a8] block">@row.Time</span>
|
|
</td>
|
|
<td class="py-2.5 font-bold text-[#101827]">@row.Traveller</td>
|
|
<td class="py-2.5">
|
|
<div class="font-bold text-gray-800 text-[11px]">@row.Category</div>
|
|
<span class="text-[9px] text-gray-500 block">@row.Reason</span>
|
|
</td>
|
|
<td class="py-2.5 max-w-[200px] text-gray-600 italic leading-snug">@row.Resolution</td>
|
|
<td class="py-2.5">
|
|
<span class="font-bold @(row.ResponseTimeMinutes <= 15 ? "text-[#22a06b]" : "text-[#dc2626]")">
|
|
@row.ResponseTimeMinutes mins
|
|
</span>
|
|
<span class="text-[9px] text-gray-400 block">Target: <15m</span>
|
|
</td>
|
|
<td class="py-2.5 text-right">
|
|
<span class="inline-block px-2 py-0.5 rounded text-[9px] font-bold bg-[#e3f6ed] text-[#22a06b]">@row.Status</span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
@if (!FilteredAfterHours.Any())
|
|
{
|
|
<tr>
|
|
<td colspan="6" class="text-center py-8 text-gray-400 text-xs">No matching records found.</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
else if (SelectedReportKey == "refunds")
|
|
{
|
|
<table class="w-full border-collapse text-left">
|
|
<thead>
|
|
<tr class="border-b border-[#e4eaf2]">
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Traveller / Ticket #</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Airline & Route</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Face Value</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Issue / Expiry Date</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Claim Status</th>
|
|
<th class="text-right text-[10px] text-[#8a96a8] uppercase font-bold py-2">Action Required</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-xs">
|
|
@foreach (var row in FilteredRefunds)
|
|
{
|
|
<tr class="border-b border-[#e4eaf2] hover:bg-[#fafcfe] transition-colors">
|
|
<td class="py-2.5 font-bold text-[#101827]">
|
|
<div>@row.Traveller</div>
|
|
<span class="text-[9px] text-[#8a96a8] font-mono block">Ticket: @row.TicketNumber</span>
|
|
</td>
|
|
<td class="py-2.5 text-gray-600">
|
|
<div class="font-semibold text-gray-800">@row.Airline</div>
|
|
<span class="text-[9px] text-gray-400 block">@row.Route</span>
|
|
</td>
|
|
<td class="py-2.5 font-bold font-mono text-[#003b71]">R @row.FaceValue.ToString("N0")</td>
|
|
<td class="py-2.5 text-xs text-gray-500">
|
|
<div>Issued: @row.IssueDate</div>
|
|
<span class="text-[9px] text-red-500 font-medium block">Expires: @row.ExpiryDate</span>
|
|
</td>
|
|
<td class="py-2.5 font-bold">
|
|
<span class="px-2 py-0.5 rounded text-[9px] @(row.RefundStatus == "Refunded" ? "bg-[#e3f6ed] text-[#22a06b]" : row.RefundStatus == "Credit Logged" ? "bg-[#e2eefb] text-[#0066b3]" : "bg-[#fff4dd] text-[#b06b00]")">
|
|
@row.RefundStatus
|
|
</span>
|
|
</td>
|
|
<td class="py-2.5 text-right">
|
|
<button @onclick="() => TriggerVoucherAction(row)"
|
|
class="px-2 py-1 bg-white border border-[#e4eaf2] text-[#1f2a3a] hover:bg-[#fafcfe] rounded text-[10px] font-bold shadow-xs cursor-pointer">
|
|
@row.ActionPending
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
@if (!FilteredRefunds.Any())
|
|
{
|
|
<tr>
|
|
<td colspan="6" class="text-center py-8 text-gray-400 text-xs">No matching records found.</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
else if (SelectedReportKey == "carbon")
|
|
{
|
|
<div class="space-y-6 text-left">
|
|
@{
|
|
decimal totalCarbon = FilteredCarbon.Sum(c => c.CarbonFootprint);
|
|
decimal greenAccommodationRatio = FilteredCarbon.Any(c => c.Category == "Lodging" && c.EcoPerformanceTag.Contains("Eco")) ? 42.8M : 0M;
|
|
int treesOffset = (int)(totalCarbon * 41.5M);
|
|
}
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 bg-[#f4f7fb] p-4 rounded-xl border border-[#e4eaf2]">
|
|
<div>
|
|
<span class="block text-[10px] text-[#8a96a8] uppercase font-bold mb-1">Total Carbon Emitted</span>
|
|
<span class="text-lg font-black text-[#1f2a3a]">@totalCarbon.ToString("F1") Tons CO₂</span>
|
|
<span class="block text-[9px] text-gray-400 mt-0.5">@((totalCarbon * -0.24M).ToString("F1"))% vs baseline</span>
|
|
</div>
|
|
<div>
|
|
<span class="block text-[10px] text-[#00a3a1] uppercase font-bold mb-1">Green Accommodation Ratio</span>
|
|
<span class="text-lg font-black text-[#00a3a1]">@(greenAccommodationRatio > 0 ? "42.8%" : "0.0%") Bookings</span>
|
|
<span class="block text-[9px] text-[#00a3a1] mt-0.5">Target: 30.0% standard</span>
|
|
</div>
|
|
<div>
|
|
<span class="block text-[10px] text-[#22a06b] uppercase font-bold mb-1">Tree-Offset Equivalent</span>
|
|
<span class="text-lg font-black text-[#22a06b]">@treesOffset.ToString("N0") Trees/Year</span>
|
|
<span class="block text-[9px] text-[#22a06b] mt-0.5">SLA Sustainability Match</span>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="w-full border-collapse text-left">
|
|
<thead>
|
|
<tr class="border-b border-[#e4eaf2]">
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Category</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Activity Metric</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Carbon Footprint</th>
|
|
<th class="text-left text-[10px] text-[#8a96a8] uppercase font-bold py-2">Eco Performance Tag</th>
|
|
<th class="text-right text-[10px] text-[#8a96a8] uppercase font-bold py-2">Green Recommendations</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-xs">
|
|
@foreach (var row in FilteredCarbon)
|
|
{
|
|
<tr class="border-b border-[#e4eaf2] hover:bg-[#fafcfe] transition-colors">
|
|
<td class="py-2.5 font-bold text-[#101827]">@row.Category</td>
|
|
<td class="py-2.5 text-gray-600">@row.ActivityMetric</td>
|
|
<td class="py-2.5 font-bold @(row.CarbonFootprint >= 10M ? "text-[#dc2626]" : row.CarbonFootprint >= 5M ? "text-[#0066b3]" : "text-[#22a06b]")">
|
|
@row.CarbonFootprint.ToString("F1") Tons CO₂
|
|
</td>
|
|
<td class="py-2.5">
|
|
<span class="px-2 py-0.5 rounded text-[9px] font-bold @(row.EcoPerformanceTag == "Heavy Footprint" ? "bg-[#fde5e8] text-[#dc2626]" : row.EcoPerformanceTag.Contains("Eco") ? "bg-[#e3f6ed] text-[#22a06b]" : "bg-[#f3f8fd] text-[#0066b3]")">
|
|
@row.EcoPerformanceTag
|
|
</span>
|
|
</td>
|
|
<td class="py-2.5 text-right text-xs text-[#5a6577] italic">@row.GreenRecommendation</td>
|
|
</tr>
|
|
}
|
|
@if (!FilteredCarbon.Any())
|
|
{
|
|
<tr>
|
|
<td colspan="5" class="text-center py-8 text-gray-400 text-xs">No matching records found.</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3.5 border-t border-[#e4eaf2] text-xs text-[#8a96a8] flex justify-between items-center">
|
|
<span>* Showing live data queried directly from ASATA-linked accounts. Securely verified by POPIA guidelines.</span>
|
|
<button @onclick="TriggerExcelDownload"
|
|
class="px-3.5 py-1.5 bg-[#e2eefb] hover:bg-[#b4d3f5] text-[#0066b3] text-xs font-bold rounded-lg transition-colors cursor-pointer">
|
|
Export Ledger (.xlsx)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
}
|
|
|
|
@* ===================== TAB 3: CONSULTANTS PERFORMANCE ===================== *@
|
|
@if (ActiveTab == "consultants")
|
|
{
|
|
<div class="space-y-6 animate-fade-in text-left">
|
|
|
|
<div class="bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left">
|
|
<div class="flex items-center justify-between mb-4 border-b border-[#e4eaf2] pb-3">
|
|
<div>
|
|
<h3 class="text-sm font-bold text-[#003b71]">SAQA Dedicated Travel Consultants Performance</h3>
|
|
<p class="text-xs text-[#8a96a8] mt-0.5">SLA productivity tracker for TMT Projects specialists permanently assigned to South African Qualifications Authority</p>
|
|
</div>
|
|
<span class="bg-[#e3f6ed] text-[#22a06b] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">100% Availability Verified</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 pt-2">
|
|
@foreach (var consultant in ConsultantPerformances)
|
|
{
|
|
<div class="border border-[#e4eaf2] rounded-xl p-5 bg-white relative overflow-hidden text-left flex flex-col justify-between hover:shadow-md transition">
|
|
<div>
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="h-10 w-10 rounded-full bg-gradient-to-tr from-[#0066b3] to-[#00a3a1] text-white flex items-center justify-center font-bold text-xs uppercase shadow-sm">
|
|
@string.Concat(consultant.Name.Split(' ').Select(n => n[0]))
|
|
</div>
|
|
<div class="text-left">
|
|
<h4 class="font-bold text-[#1f2a3a] text-sm">@consultant.Name</h4>
|
|
<span class="text-[10px] text-[#8a96a8] font-semibold uppercase">@consultant.Role</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2 border-t border-[#f4f7fb] pt-3 text-xs">
|
|
<div class="flex justify-between py-1">
|
|
<span class="text-[#8a96a8]">Specialty Focus:</span>
|
|
<span class="font-bold text-[#1f2a3a]">@consultant.Specialty</span>
|
|
</div>
|
|
<div class="flex justify-between py-1">
|
|
<span class="text-[#8a96a8]">Bookings Handled:</span>
|
|
<span class="font-mono font-bold text-[#0066b3]">@consultant.BookingsHandled</span>
|
|
</div>
|
|
<div class="flex justify-between py-1">
|
|
<span class="text-[#8a96a8]">Avg Response Speed:</span>
|
|
<span class="font-mono font-bold text-[#22a06b]">@consultant.ResponseTimeMinutes mins</span>
|
|
</div>
|
|
<div class="flex justify-between py-1">
|
|
<span class="text-[#8a96a8]">Error Ticket Volume:</span>
|
|
<span class="font-mono font-bold @(consultant.ErrorsCount == 0 ? "text-[#22a06b]" : "text-[#dc2626]")">
|
|
@consultant.ErrorsCount
|
|
</span>
|
|
</div>
|
|
|
|
@* Performance bar *@
|
|
<div class="pt-2">
|
|
<div class="flex justify-between items-center text-[10px] mb-1">
|
|
<span class="font-semibold text-gray-600">SLA Success Score:</span>
|
|
<span class="font-bold text-[#22a06b]">@(consultant.ErrorsCount == 0 ? "100%" : "99.8%")</span>
|
|
</div>
|
|
<div class="w-full bg-gray-100 h-1.5 rounded-full overflow-hidden">
|
|
<div class="bg-[#22a06b] h-1.5 rounded-full" style="width: @(consultant.ErrorsCount == 0 ? "100%" : "99.8%");"></div>
|
|
</div>
|
|
</div>
|
|
|
|
@* CSAT Score rating representation *@
|
|
<div class="flex justify-between items-center py-1 mt-2 bg-[#f4f7fb] p-2 rounded-lg">
|
|
<span class="text-[#5a6577] text-[10px]">CSAT Rating:</span>
|
|
<div class="flex items-center gap-1">
|
|
<span class="text-xs font-bold text-[#f4a300]">★ @consultant.CsatScore.ToString("F2")</span>
|
|
<span class="text-[9px] text-gray-400">/ 5.0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button @onclick="@(() => StartConsultantChatSim(consultant.Name))"
|
|
class="mt-4 w-full py-1.5 bg-[#f4f7fb] hover:bg-[#e2eefb] text-[#0066b3] text-xs font-bold rounded transition">
|
|
Request SLA Priority Contact
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left">
|
|
<h3 class="text-sm font-bold text-[#003b71] mb-2">Dedicated SLA Service Protocols</h3>
|
|
<p class="text-xs text-[#5a6577] leading-relaxed">
|
|
TMT Projects provides a dedicated 24/7 corporate desk. For executive and VIP bookings, Thabo Khumalo is assigned as the primary liaison officer. Escalated service matters and disputes bypass the standard queue and route instantly to Lerato Nkosi for immediate SLA verification and reconciliation logs.
|
|
</p>
|
|
</div>
|
|
|
|
</div>
|
|
}
|
|
|
|
@* ===================== TAB 4: DISPUTE & SERVICE LOGS ===================== *@
|
|
@if (ActiveTab == "disputes")
|
|
{
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in text-left">
|
|
|
|
@* Service dispute logging form (lg:col-span-5) *@
|
|
<div class="lg:col-span-5 bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left">
|
|
<div class="mb-4 pb-2 border-b border-[#e4eaf2]">
|
|
<h3 class="text-sm font-bold text-[#003b71]">Log SLA Query or Service Violation</h3>
|
|
<p class="text-xs text-[#8a96a8] mt-0.5">Submit service delays or rate cap disputes for priority TMT investigation</p>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Select SLA Category</label>
|
|
<select @bind="NewQueryCategory"
|
|
class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white focus:outline-none focus:border-[#0066b3]">
|
|
<option value="Booking Response Delay">Booking Response Delay</option>
|
|
<option value="National Treasury Cap Discrepancy">National Treasury Cap Discrepancy</option>
|
|
<option value="Billing / Invoice Dispute">Billing / Invoice Dispute</option>
|
|
<option value="Consultant / Service Quality Issue">Consultant / Service Quality Issue</option>
|
|
<option value="Voucher Validation Delay">Voucher Validation Delay</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3 text-left">
|
|
<div>
|
|
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Traveller Name</label>
|
|
<input type="text" @bind="NewQueryTraveller" placeholder="e.g. J. Mokoena"
|
|
class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white focus:outline-none focus:border-[#0066b3]" />
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Urgency Priority</label>
|
|
<select @bind="NewQueryUrgency"
|
|
class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white focus:outline-none focus:border-[#0066b3]">
|
|
<option value="Low">Low - Compliance Query</option>
|
|
<option value="Medium">Medium - Standard Booking</option>
|
|
<option value="High">High - Urgent Travel Impact</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-[#5a6577] uppercase mb-1">Describe Service Violation Parameter</label>
|
|
<textarea @bind="NewQueryDescription" rows="4" placeholder="Detail any response lags or invoice matching discrepancies..."
|
|
class="w-full px-3 py-2 border border-[#c8d3e1] rounded-lg text-sm bg-white focus:outline-none focus:border-[#0066b3] resize-none"></textarea>
|
|
</div>
|
|
|
|
<button @onclick="SubmitSlaQuery"
|
|
class="w-full py-2 bg-[#dc2626] hover:bg-[#b91c1c] text-white text-xs font-bold rounded-lg shadow-sm transition">
|
|
Submit SLA Query to Response Desk
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@* Active disputes / Queries tracking ledger (lg:col-span-7) *@
|
|
<div class="lg:col-span-7 bg-white border border-[#e4eaf2] rounded-xl p-5 shadow-sm text-left flex flex-col justify-between">
|
|
<div>
|
|
<div class="flex items-center justify-between mb-4 border-b border-[#e4eaf2] pb-3">
|
|
<div>
|
|
<h3 class="text-sm font-bold text-[#003b71]">SAQA Active Service Quality Log</h3>
|
|
<p class="text-xs text-[#8a96a8] mt-0.5">Live status tracker of submitted queries in May 2026</p>
|
|
</div>
|
|
<span class="bg-[#fff4dd] text-[#b06b00] px-2.5 py-0.5 rounded-full text-[11px] font-semibold">
|
|
@ServiceQueries.Count(q => q.Status != "Resolved") Active Disputes
|
|
</span>
|
|
</div>
|
|
|
|
<div class="space-y-3.5 max-h-[400px] overflow-y-auto pr-1">
|
|
@foreach (var query in ServiceQueries)
|
|
{
|
|
<div class="p-3 border rounded-xl flex items-start gap-3 transition-all hover:bg-[#fafcfe] @(query.Urgency == "High" ? "bg-[#fdeef0] border-[#f8b4b4]" : query.Urgency == "Medium" ? "bg-[#fff9ec] border-[#fbd897]" : "bg-[#f3f8fd] border-[#b4d3f5]")">
|
|
<div class="shrink-0 mt-0.5">
|
|
@if (query.Urgency == "High")
|
|
{
|
|
<!-- Alert Shield SVG -->
|
|
<svg class="w-5 h-5 text-[#dc2626]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.249-8.25-3.286zm0 13.036h.008v.008H12v-.008z" />
|
|
</svg>
|
|
}
|
|
else
|
|
{
|
|
<!-- Warning Shield SVG -->
|
|
<svg class="w-5 h-5 text-[#f4a300]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.742 0 01-1.043 5.636 3.741 3.741 0 01-5.076.63 3.75 3.75 0 01-3.758-3.848c-.021-.013-.042-.027-.063-.04A4.5 4.5 0 113.6 8.775 5.25 5.25 0 0113.8 6.44a3 3 0 013.758 3.848A3.713 3.752 0 0118 12.75" />
|
|
</svg>
|
|
}
|
|
</div>
|
|
|
|
<div class="flex-1 text-xs">
|
|
<div class="flex justify-between items-baseline mb-1">
|
|
<h4 class="font-bold text-[#1f2a3a]">@query.Category</h4>
|
|
<span class="text-[9px] font-mono text-[#8a96a8]">@query.Date</span>
|
|
</div>
|
|
<p class="text-[#5a6577]">@query.Description</p>
|
|
<div class="mt-2.5 flex items-center justify-between border-t border-gray-200/50 pt-2 text-[10px]">
|
|
<span class="font-bold text-[#003b71]">Traveller Reference: @query.TravellerName</span>
|
|
<div class="flex gap-1.5">
|
|
<span class="px-2 py-0.5 rounded text-[9px] font-bold @(query.Status == "In Review" ? "bg-[#fff4dd] text-[#b06b00]" : query.Status == "Resolved" ? "bg-[#e3f6ed] text-[#22a06b]" : "bg-[#fde5e8] text-[#dc2626]")">
|
|
@query.Status
|
|
</span>
|
|
@if (query.Status != "Resolved")
|
|
{
|
|
<button @onclick="@(() => ResolveSlaQuerySim(query.Id))"
|
|
class="px-2 py-0.5 bg-[#22a06b] hover:bg-[#1b8054] text-white text-[9px] font-bold rounded shadow-xs cursor-pointer">
|
|
Mark Resolved
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3.5 border-t border-[#e4eaf2] text-[10px] text-gray-500 leading-normal">
|
|
Disputes logged are actioned by Lerato Nkosi within 4 hours. Escalations are automatically flagged in daily reports.
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
}
|
|
|
|
</div>
|
|
|
|
@code {
|
|
// Reporting parameters
|
|
private string SelectedPeriod { get; set; } = "May 2026";
|
|
private string SelectedCostCentre { get; set; } = "All";
|
|
|
|
// SLA state feedback messages
|
|
private string AlertFeedback { get; set; } = string.Empty;
|
|
|
|
// Primary Tab Selection
|
|
private string ActiveTab { get; set; } = "scorecard";
|
|
|
|
// Reports Library state
|
|
private string SelectedReportKey { get; set; } = "deviations";
|
|
private string ReportSearchQuery { get; set; } = string.Empty;
|
|
|
|
// SLA Query / Dispute form bind states
|
|
private string NewQueryCategory { get; set; } = "Booking Response Delay";
|
|
private string NewQueryTraveller { get; set; } = string.Empty;
|
|
private string NewQueryUrgency { get; set; } = "Medium";
|
|
private string NewQueryDescription { get; set; } = string.Empty;
|
|
|
|
// Data Structures & Models
|
|
public class SlaMetric
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
public string Target { get; set; } = string.Empty;
|
|
public string Actual { get; set; } = string.Empty;
|
|
public string Unit { get; set; } = string.Empty;
|
|
public string StatusLabel { get; set; } = string.Empty;
|
|
public string StatusClass { get; set; } = string.Empty;
|
|
public string BarColorClass { get; set; } = string.Empty;
|
|
}
|
|
|
|
public class MonthlyReportMetadata
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
public string Description { get; set; } = string.Empty;
|
|
public string Emoji { get; set; } = string.Empty;
|
|
public int RecordCount { get; set; }
|
|
}
|
|
|
|
public class DeviationReportRow
|
|
{
|
|
public string Traveller { get; set; } = string.Empty;
|
|
public string CostCentre { get; set; } = string.Empty;
|
|
public string Route { get; set; } = string.Empty;
|
|
public string Date { get; set; } = string.Empty;
|
|
public string Supplier { get; set; } = string.Empty;
|
|
public decimal OpenPrice { get; set; }
|
|
public decimal PaidPrice { get; set; }
|
|
public decimal Saving => OpenPrice - PaidPrice;
|
|
public string ReasonCode { get; set; } = string.Empty;
|
|
public string Justification { get; set; } = string.Empty;
|
|
public string ApprovedBy { get; set; } = string.Empty;
|
|
public string Status { get; set; } = "Approved";
|
|
}
|
|
|
|
public class AfterHoursLogRow
|
|
{
|
|
public string Date { get; set; } = string.Empty;
|
|
public string Time { get; set; } = string.Empty;
|
|
public string Traveller { get; set; } = string.Empty;
|
|
public string Category { get; set; } = string.Empty;
|
|
public string Reason { get; set; } = string.Empty;
|
|
public string Resolution { get; set; } = string.Empty;
|
|
public int ResponseTimeMinutes { get; set; }
|
|
public string Status { get; set; } = "Resolved";
|
|
public string CostCentre { get; set; } = string.Empty;
|
|
}
|
|
|
|
public class UnusedTicketRow
|
|
{
|
|
public string Traveller { get; set; } = string.Empty;
|
|
public string TicketNumber { get; set; } = string.Empty;
|
|
public string Airline { get; set; } = string.Empty;
|
|
public string Route { get; set; } = string.Empty;
|
|
public decimal FaceValue { get; set; }
|
|
public string IssueDate { get; set; } = string.Empty;
|
|
public string ExpiryDate { get; set; } = string.Empty;
|
|
public string RefundStatus { get; set; } = "Pending";
|
|
public string ActionPending { get; set; } = "Process Refund";
|
|
public string CostCentre { get; set; } = string.Empty;
|
|
}
|
|
|
|
public class CarbonReportRow
|
|
{
|
|
public string Category { get; set; } = string.Empty;
|
|
public string ActivityMetric { get; set; } = string.Empty;
|
|
public decimal CarbonFootprint { get; set; }
|
|
public string EcoPerformanceTag { get; set; } = string.Empty;
|
|
public string GreenRecommendation { get; set; } = string.Empty;
|
|
public string CostCentre { get; set; } = string.Empty;
|
|
public string Date { get; set; } = string.Empty;
|
|
}
|
|
|
|
public class ConsultantPerformance
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
public string Role { get; set; } = string.Empty;
|
|
public string Specialty { get; set; } = string.Empty;
|
|
public int BookingsHandled { get; set; }
|
|
public int ErrorsCount { get; set; }
|
|
public int ResponseTimeMinutes { get; set; }
|
|
public double CsatScore { get; set; }
|
|
}
|
|
|
|
public class SlaQueryDispute
|
|
{
|
|
public int Id { get; set; }
|
|
public string Date { get; set; } = string.Empty;
|
|
public string Category { get; set; } = string.Empty;
|
|
public string TravellerName { get; set; } = string.Empty;
|
|
public string Urgency { get; set; } = string.Empty;
|
|
public string Description { get; set; } = string.Empty;
|
|
public string Status { get; set; } = "In Review";
|
|
}
|
|
|
|
// Databases
|
|
private List<SlaMetric> CoreSlaMetrics { get; set; } = new();
|
|
private Dictionary<string, MonthlyReportMetadata> MonthlyReportTemplates { get; set; } = new();
|
|
private List<DeviationReportRow> DeviationRows { get; set; } = new();
|
|
private List<AfterHoursLogRow> AfterHoursRows { get; set; } = new();
|
|
private List<UnusedTicketRow> UnusedTicketRows { get; set; } = new();
|
|
private List<CarbonReportRow> CarbonRows { get; set; } = new();
|
|
private List<ConsultantPerformance> ConsultantPerformances { get; set; } = new();
|
|
private List<SlaQueryDispute> ServiceQueries { get; set; } = new();
|
|
|
|
// Active Metadata calculated helper property
|
|
private MonthlyReportMetadata ActiveReportMetadata => MonthlyReportTemplates.ContainsKey(SelectedReportKey)
|
|
? MonthlyReportTemplates[SelectedReportKey]
|
|
: new MonthlyReportMetadata { Name = "Report Log", Emoji = "📁" };
|
|
|
|
// Dynamic Record Counter
|
|
private int GetReportCount(string key)
|
|
{
|
|
return key switch
|
|
{
|
|
"deviations" => FilteredDeviations.Count(),
|
|
"afterhours" => FilteredAfterHours.Count(),
|
|
"refunds" => FilteredRefunds.Count(),
|
|
"carbon" => FilteredCarbon.Count(),
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
// Cost Centre normalizer
|
|
private bool IsCostCentreMatch(string itemCc, string selectedCc)
|
|
{
|
|
if (selectedCc == "All") return true;
|
|
|
|
string normalizedItem = itemCc.Replace("&", "and").Replace(" ", "").ToLower();
|
|
string normalizedSelected = selectedCc.Replace("&", "and").Replace(" ", "").ToLower();
|
|
|
|
return normalizedItem == normalizedSelected;
|
|
}
|
|
|
|
// Date Period helper
|
|
private bool IsDateInPeriod(string itemDate, string selectedPeriod)
|
|
{
|
|
if (string.IsNullOrEmpty(selectedPeriod) || selectedPeriod == "All") return true;
|
|
|
|
if (selectedPeriod == "May 2026")
|
|
{
|
|
return itemDate.Contains("May", System.StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else if (selectedPeriod == "April 2026")
|
|
{
|
|
return itemDate.Contains("Apr", System.StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else if (selectedPeriod == "Q1 2026")
|
|
{
|
|
return itemDate.Contains("Jan", System.StringComparison.OrdinalIgnoreCase)
|
|
|| itemDate.Contains("Feb", System.StringComparison.OrdinalIgnoreCase)
|
|
|| itemDate.Contains("Mar", System.StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Filtered Preview Data Lists
|
|
private IEnumerable<DeviationReportRow> FilteredDeviations => DeviationRows
|
|
.Where(d => IsCostCentreMatch(d.CostCentre, SelectedCostCentre)
|
|
&& IsDateInPeriod(d.Date, SelectedPeriod)
|
|
&& (string.IsNullOrEmpty(ReportSearchQuery)
|
|
|| d.Traveller.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| d.CostCentre.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| d.Supplier.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| d.Justification.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)));
|
|
|
|
private IEnumerable<AfterHoursLogRow> FilteredAfterHours => AfterHoursRows
|
|
.Where(a => IsCostCentreMatch(a.CostCentre, SelectedCostCentre)
|
|
&& IsDateInPeriod(a.Date, SelectedPeriod)
|
|
&& (string.IsNullOrEmpty(ReportSearchQuery)
|
|
|| a.Traveller.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| a.Category.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| a.Reason.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| a.Resolution.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)));
|
|
|
|
private IEnumerable<UnusedTicketRow> FilteredRefunds => UnusedTicketRows
|
|
.Where(r => IsCostCentreMatch(r.CostCentre, SelectedCostCentre)
|
|
&& IsDateInPeriod(r.IssueDate, SelectedPeriod)
|
|
&& (string.IsNullOrEmpty(ReportSearchQuery)
|
|
|| r.Traveller.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| r.Airline.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| r.Route.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)));
|
|
|
|
private IEnumerable<CarbonReportRow> FilteredCarbon => CarbonRows
|
|
.Where(c => IsCostCentreMatch(c.CostCentre, SelectedCostCentre)
|
|
&& IsDateInPeriod(c.Date, SelectedPeriod)
|
|
&& (string.IsNullOrEmpty(ReportSearchQuery)
|
|
|| c.Category.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| c.ActivityMetric.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)
|
|
|| c.GreenRecommendation.Contains(ReportSearchQuery, System.StringComparison.OrdinalIgnoreCase)));
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
LoadScorecardData();
|
|
LoadMonthlyReportsMeta();
|
|
LoadDeviationReport();
|
|
LoadAfterHoursReport();
|
|
LoadUnusedTicketsReport();
|
|
LoadCarbonReport();
|
|
LoadConsultantPerformances();
|
|
LoadServiceDisputes();
|
|
}
|
|
|
|
private void SetActiveTab(string tab)
|
|
{
|
|
ActiveTab = tab;
|
|
ReportSearchQuery = string.Empty;
|
|
}
|
|
|
|
private void SelectReport(string reportKey)
|
|
{
|
|
SelectedReportKey = reportKey;
|
|
ReportSearchQuery = string.Empty;
|
|
}
|
|
|
|
private void OnFilterChanged()
|
|
{
|
|
TriggerFeedback($"Loaded SLA and operations data for period '{SelectedPeriod}' under Cost Centre '{SelectedCostCentre}' Filter.");
|
|
}
|
|
|
|
// PDF and Excel download simulation triggers
|
|
private void TriggerPdfDownload()
|
|
{
|
|
TriggerFeedback($"Generating Monthly SLA Compliance Pack for period {SelectedPeriod}. Downloader thread initialized...");
|
|
}
|
|
|
|
private void TriggerExcelDownload()
|
|
{
|
|
TriggerFeedback($"Exporting Full Operational Ledgers ({ActiveReportMetadata.Name}) as spreadsheet output for National Treasury submission.");
|
|
}
|
|
|
|
private void TriggerFeedback(string msg)
|
|
{
|
|
AlertFeedback = msg;
|
|
_ = DismissAlertDelay();
|
|
}
|
|
|
|
private async Task DismissAlertDelay()
|
|
{
|
|
await Task.Delay(4000);
|
|
AlertFeedback = string.Empty;
|
|
StateHasChanged();
|
|
}
|
|
|
|
// Submit user SLA Issue query
|
|
private void SubmitSlaQuery()
|
|
{
|
|
if (string.IsNullOrEmpty(NewQueryTraveller) || string.IsNullOrEmpty(NewQueryDescription))
|
|
{
|
|
TriggerFeedback("ERROR: Please fill in all fields (Traveller Name & Problem description) before logging a query.");
|
|
return;
|
|
}
|
|
|
|
var newQuery = new SlaQueryDispute
|
|
{
|
|
Id = ServiceQueries.Count + 1,
|
|
Date = "Just Now",
|
|
Category = NewQueryCategory,
|
|
TravellerName = NewQueryTraveller,
|
|
Urgency = NewQueryUrgency,
|
|
Description = NewQueryDescription,
|
|
Status = "Awaiting Desk Action"
|
|
};
|
|
|
|
ServiceQueries.Insert(0, newQuery);
|
|
NewQueryTraveller = string.Empty;
|
|
NewQueryDescription = string.Empty;
|
|
|
|
TriggerFeedback($"SUCCESS: SLA Service Query registered successfully as Issue ID #2026-QS0{newQuery.Id}. Dispatched to Lerato Nkosi.");
|
|
}
|
|
|
|
// Complete dispute action simulating resolution
|
|
private void ResolveSlaQuerySim(int id)
|
|
{
|
|
var found = ServiceQueries.FirstOrDefault(q => q.Id == id);
|
|
if (found != null)
|
|
{
|
|
found.Status = "Resolved";
|
|
TriggerFeedback($"SLA Service Issue #{id} marked as RESOLVED. Audit trace logged.");
|
|
}
|
|
}
|
|
|
|
// Process voucher simulation inside registry
|
|
private void TriggerVoucherAction(UnusedTicketRow row)
|
|
{
|
|
if (row.RefundStatus == "Pending")
|
|
{
|
|
row.RefundStatus = "Refunded";
|
|
row.ActionPending = "Audit Logged";
|
|
TriggerFeedback($"Refund of R {row.FaceValue:N0} successfully processed to SAQA Lodge Card for traveller '{row.Traveller}'.");
|
|
}
|
|
else if (row.RefundStatus == "Credit Logged")
|
|
{
|
|
row.RefundStatus = "Applied to Flight";
|
|
row.ActionPending = "Synced to GDS";
|
|
TriggerFeedback($"Credit note R {row.FaceValue:N0} reallocated dynamically to upcoming domestic route.");
|
|
}
|
|
}
|
|
|
|
// Contact Consultant sim
|
|
private void StartConsultantChatSim(string name)
|
|
{
|
|
TriggerFeedback($"Escalated priority channel connected directly with {name}. Chat initialized securely via TLS.");
|
|
}
|
|
|
|
// Core Data Loaders
|
|
private void LoadScorecardData()
|
|
{
|
|
CoreSlaMetrics = new List<SlaMetric>
|
|
{
|
|
new SlaMetric { Name = "Response Time", Target = "< 60m", Actual = "44", Unit = "Mins Avg", StatusLabel = "Target Met", StatusClass = "bg-[#e3f6ed] text-[#22a06b]", BarColorClass = "before:bg-[#22a06b]" },
|
|
new SlaMetric { Name = "After Hours SLA", Target = "< 15m", Actual = "9", Unit = "Mins Avg", StatusLabel = "Target Met", StatusClass = "bg-[#e3f6ed] text-[#22a06b]", BarColorClass = "before:bg-[#00a3a1]" },
|
|
new SlaMetric { Name = "Rate Cap Adherence", Target = "100%", Actual = "94.1", Unit = "% Booked", StatusLabel = "Warning", StatusClass = "bg-[#fff4dd] text-[#b06b00]", BarColorClass = "before:bg-[#f4a300]" },
|
|
new SlaMetric { Name = "Lodge Matching Speed", Target = "< 48h", Actual = "28", Unit = "Hours Avg", StatusLabel = "Target Met", StatusClass = "bg-[#e3f6ed] text-[#22a06b]", BarColorClass = "before:bg-[#22a06b]" },
|
|
new SlaMetric { Name = "Ticketing Quality", Target = "> 99.9%", Actual = "99.96", Unit = "% Perfect", StatusLabel = "Target Met", StatusClass = "bg-[#e3f6ed] text-[#22a06b]", BarColorClass = "before:bg-[#22a06b]" },
|
|
new SlaMetric { Name = "Adoption (Mobile Portal)", Target = "> 75%", Actual = "78.1", Unit = "% Active", StatusLabel = "Target Met", StatusClass = "bg-[#e2eefb] text-[#0066b3]", BarColorClass = "before:bg-[#0066b3]" }
|
|
};
|
|
}
|
|
|
|
private void LoadMonthlyReportsMeta()
|
|
{
|
|
MonthlyReportTemplates = new Dictionary<string, MonthlyReportMetadata>
|
|
{
|
|
{ "deviations", new MonthlyReportMetadata { Name = "NT Cost Containment Deviation Ledger", Description = "Out-of-policy bookings requiring CFO exception logs and approvals", Emoji = "⚖️", RecordCount = 7 } },
|
|
{ "afterhours", new MonthlyReportMetadata { Name = "After-Hours Support Events Log", Description = "Late night crisis calls, flight delays and urgent re-routing records", Emoji = "🌙", RecordCount = 4 } },
|
|
{ "refunds", new MonthlyReportMetadata { Name = "Unused Ticket & Open Refund Registry", Description = "Credits notes tracked securely to prevent voucher value expiry", Emoji = "🎟️", RecordCount = 5 } },
|
|
{ "carbon", new MonthlyReportMetadata { Name = "CO2 Sustainability Footprint Report", Description = "Carbon offsets, bed-night emissions, and eco accommodation trends", Emoji = "🍃", RecordCount = 3 } }
|
|
};
|
|
}
|
|
|
|
private void LoadDeviationReport()
|
|
{
|
|
DeviationRows = new List<DeviationReportRow>
|
|
{
|
|
new DeviationReportRow { Traveller = "T. Maluleke", CostCentre = "Office of the CEO", Route = "JNB → LHR", Date = "14 May 2026", Supplier = "British Airways", OpenPrice = 21440M, PaidPrice = 18940M, ReasonCode = "DEV-CAB-01", Justification = "Premium Economy class upgrade justified by medical clearance certification", ApprovedBy = "CFO (Approved)", Status = "Authorized" },
|
|
new DeviationReportRow { Traveller = "J. Mokoena", CostCentre = "Office of the CEO", Route = "JNB → CPT", Date = "02 May 2026", Supplier = "SAA", OpenPrice = 3290M, PaidPrice = 2890M, ReasonCode = "CAP-AIR-01", Justification = "Late schedule confirmation due to urgent CEO board meetings; exceeded treasury R2,500 flight cap.", ApprovedBy = "CFO Office", Status = "Authorized" },
|
|
new DeviationReportRow { Traveller = "N. Ndlovu", CostCentre = "Qualifications & Standards", Route = "Cape Town stay", Date = "22 May 2026", Supplier = "Premier Hotels", OpenPrice = 2400M, PaidPrice = 1895M, ReasonCode = "CAP-HOT-02", Justification = "Cape Town peak-season rates exceed R 1,900/night limit. Bypassed with CFO support.", ApprovedBy = "CFO (Line Manager)", Status = "Authorized" },
|
|
new DeviationReportRow { Traveller = "S. Pillay", CostCentre = "Verifications", Route = "Durban engagement", Date = "14 May 2026", Supplier = "Avis Car", OpenPrice = 520M, PaidPrice = 420M, ReasonCode = "CAP-CAR-01", Justification = "Personal leisure extension added. Differential costs calculated and billed to personal card.", ApprovedBy = "Corporate Services", Status = "Authorized" }
|
|
};
|
|
}
|
|
|
|
private void LoadAfterHoursReport()
|
|
{
|
|
AfterHoursRows = new List<AfterHoursLogRow>
|
|
{
|
|
new AfterHoursLogRow { Date = "22 May 2026", Time = "11:14 PM", Traveller = "Dr. P. Sithole", Category = "Flight Delay Rerouting", Reason = "BA JNB-LHR delayed by 4 hours, missing connecting transit node", Resolution = "Rerouted to early morning shuttle flight. Lounge voucher auto-issued.", ResponseTimeMinutes = 9, Status = "Resolved", CostCentre = "Office of the CEO" },
|
|
new AfterHoursLogRow { Date = "18 May 2026", Time = "01:05 AM", Traveller = "T. Maluleke", Category = "Hotel Check-In Dispute", Reason = "Premier Hotel desk logged zero confirmation for booking #HT7811", Resolution = "Contacted hotel night supervisor, synced Sabre API voucher, check-in cleared.", ResponseTimeMinutes = 11, Status = "Resolved", CostCentre = "Office of the CEO" },
|
|
new AfterHoursLogRow { Date = "10 May 2026", Time = "05:45 AM", Traveller = "A. Dlamini", Category = "Urgent eVisa Query", Reason = "eVisa barcode rejected on Nairobi check-in transit node", Resolution = "Repulled VFS Global Kenyan eVisa PDF. SMS copy sent to boarding agent.", ResponseTimeMinutes = 6, Status = "Resolved", CostCentre = "Qualifications & Standards" }
|
|
};
|
|
}
|
|
|
|
private void LoadUnusedTicketsReport()
|
|
{
|
|
UnusedTicketRows = new List<UnusedTicketRow>
|
|
{
|
|
new UnusedTicketRow { Traveller = "J. Mokoena", TicketNumber = "083-4412092", Airline = "SAA", Route = "JNB → CPT", FaceValue = 2890M, IssueDate = "21 May 2026", ExpiryDate = "21 Jul 2026", RefundStatus = "Pending", ActionPending = "Process Refund", CostCentre = "Office of the CEO" },
|
|
new UnusedTicketRow { Traveller = "T. Maluleke", TicketNumber = "125-9921443", Airline = "British Airways", Route = "JNB → LHR", FaceValue = 18940M, IssueDate = "05 Jan 2026", ExpiryDate = "05 Sep 2026", RefundStatus = "Credit Logged", ActionPending = "Apply Note", CostCentre = "Office of the CEO" },
|
|
new UnusedTicketRow { Traveller = "N. Ndlovu", TicketNumber = "083-1144021", Airline = "FlySafair", Route = "JNB → DUR", FaceValue = 1985M, IssueDate = "18 Apr 2026", ExpiryDate = "18 Aug 2026", RefundStatus = "Pending", ActionPending = "Process Refund", CostCentre = "Qualifications & Standards" }
|
|
};
|
|
}
|
|
|
|
private void LoadCarbonReport()
|
|
{
|
|
CarbonRows = new List<CarbonReportRow>
|
|
{
|
|
new CarbonReportRow { Category = "Air Travel", ActivityMetric = "142 Flights logged", CarbonFootprint = 21.4M, EcoPerformanceTag = "Heavy Footprint", GreenRecommendation = "Consolidate multiple JNB-CPT site trips", CostCentre = "Office of the CEO", Date = "14 May 2026" },
|
|
new CarbonReportRow { Category = "Lodging", ActivityMetric = "128 Bed nights", CarbonFootprint = 9.8M, EcoPerformanceTag = "Eco Certified (42%)", GreenRecommendation = "Select Green-flag Premier Hotels", CostCentre = "Qualifications & Standards", Date = "22 May 2026" },
|
|
new CarbonReportRow { Category = "Ground & Transit", ActivityMetric = "32 Car rental cycles", CarbonFootprint = 3.0M, EcoPerformanceTag = "Optimal", GreenRecommendation = "Utilize Gautrain connections regularly", CostCentre = "Verifications", Date = "14 May 2026" },
|
|
new CarbonReportRow { Category = "Air Travel", ActivityMetric = "28 Flights logged", CarbonFootprint = 4.2M, EcoPerformanceTag = "Moderate", GreenRecommendation = "Use Microsoft Teams for routine alignments", CostCentre = "Qualifications & Standards", Date = "18 Apr 2026" },
|
|
new CarbonReportRow { Category = "Lodging", ActivityMetric = "15 Bed nights", CarbonFootprint = 1.1M, EcoPerformanceTag = "Eco Certified (100%)", GreenRecommendation = "Green accommodation target met", CostCentre = "Office of the CEO", Date = "10 Jan 2026" }
|
|
};
|
|
}
|
|
|
|
private void LoadConsultantPerformances()
|
|
{
|
|
ConsultantPerformances = new List<ConsultantPerformance>
|
|
{
|
|
new ConsultantPerformance { Name = "Thabo Khumalo", Role = "Senior Travel Lead", Specialty = "VIP & Executive Bookings", BookingsHandled = 412, ErrorsCount = 0, ResponseTimeMinutes = 44, CsatScore = 4.92 },
|
|
new ConsultantPerformance { Name = "Lerato Nkosi", Role = "Reconciliation Expert", Specialty = "SLA Compliance & Disputes", BookingsHandled = 310, ErrorsCount = 1, ResponseTimeMinutes = 22, CsatScore = 4.88 },
|
|
new ConsultantPerformance { Name = "Zama Ndlovu", Role = "Helpline Coordinator", Specialty = "After-Hours Emergency Support", BookingsHandled = 142, ErrorsCount = 0, ResponseTimeMinutes = 9, CsatScore = 4.96 }
|
|
};
|
|
}
|
|
|
|
private void LoadServiceDisputes()
|
|
{
|
|
ServiceQueries = new List<SlaQueryDispute>
|
|
{
|
|
new SlaQueryDispute { Id = 1, Date = "24 May 2026", Category = "Booking Response Delay", TravellerName = "A. Dlamini", Urgency = "High", Description = "Flight request from Durban took 74 mins to receive first quote validation. SLA limit is 60 mins.", Status = "In Review" },
|
|
new SlaQueryDispute { Id = 2, Date = "19 May 2026", Category = "Billing / Invoice Dispute", TravellerName = "N. Ndlovu", Urgency = "Medium", Description = "Double billing check-in on Durban Airport transfer invoice. Outstanding audit matched in ledger.", Status = "Resolved" }
|
|
};
|
|
}
|
|
}
|