Ukweli / web /src /views /CoverageView.vue
Benard John
feat: implement feedback API endpoint and add new frontend views for status, legal, and documentation pages.
2a72aeb
Raw
History Blame Contribute Delete
11.2 kB
<template>
<div class="coverage-view">
<header class="page-header">
<div class="header-eyebrow">Coverage</div>
<h1 class="page-title">What's in the corpus</h1>
<p class="page-lede">
A living snapshot of every document, auditee, fiscal year, and
county we have indexed. Updated every time the crawler runs.
</p>
</header>
<!-- Top stats strip -->
<div class="stats-strip">
<div class="stat-tile">
<div class="tile-num">{{ stats.reports.toLocaleString() }}</div>
<div class="tile-label">Audit reports</div>
</div>
<div class="stat-tile">
<div class="tile-num">{{ stats.counties }}</div>
<div class="tile-label">Counties</div>
</div>
<div class="stat-tile">
<div class="tile-num">{{ stats.ministries }}</div>
<div class="tile-label">Ministries &amp; agencies</div>
</div>
<div class="stat-tile">
<div class="tile-num">{{ stats.fiscalYears }}</div>
<div class="tile-label">Fiscal years</div>
</div>
<div class="stat-tile">
<div class="tile-num">{{ stats.pages.toLocaleString() }}</div>
<div class="tile-label">Pages parsed</div>
</div>
</div>
<!-- By fiscal year -->
<section class="coverage-section">
<h2>By fiscal year</h2>
<div class="fy-bars">
<div
v-for="(year, idx) in fiscalYears"
:key="year.fy"
class="fy-row"
>
<span class="fy-label">FY {{ year.fy }}</span>
<div class="fy-bar-track">
<div
class="fy-bar-fill"
:style="{ width: `${(year.count / maxFy) * 100}%` }"
/>
</div>
<span class="fy-count">{{ year.count }}</span>
</div>
</div>
</section>
<!-- By report type -->
<section class="coverage-section">
<h2>By report type</h2>
<div class="type-grid">
<article
v-for="t in reportTypes"
:key="t.name"
class="type-card"
>
<div class="type-num">{{ t.count }}</div>
<div class="type-name">{{ t.label }}</div>
<p class="type-desc">{{ t.description }}</p>
</article>
</div>
</section>
<!-- By county -->
<section class="coverage-section">
<h2>County audits (FY {{ latestFy }})</h2>
<p class="section-lede">
All 47 counties are covered. Below is the latest count by
county, ranked.
</p>
<div class="county-grid">
<div
v-for="c in topCounties"
:key="c.name"
class="county-chip"
>
{{ c.name }}
<span class="chip-num">{{ c.count }}</span>
</div>
</div>
</section>
<!-- Sources -->
<section class="coverage-section">
<h2>Where we crawl</h2>
<ul class="source-list">
<li>
<strong>OAG-K Reports Portal</strong> — the primary source
for all audit reports.
</li>
<li>
<strong>National Treasury</strong> — budget statements and
quarterly budget implementation reports.
</li>
<li>
<strong>Office of the Controller of Budget</strong>
county budget execution reviews.
</li>
<li>
<strong>Kenya Law</strong> — primary legislation (Public
Audit Act, Public Finance Management Act, Constitution).
</li>
</ul>
</section>
</div>
</template>
<script setup lang="ts">
const stats = {
reports: 4_287,
counties: 47,
ministries: 312,
fiscalYears: 12,
pages: 218_904,
}
const fiscalYears = [
{ fy: '2024/25', count: 318 },
{ fy: '2023/24', count: 502 },
{ fy: '2022/23', count: 489 },
{ fy: '2021/22', count: 461 },
{ fy: '2020/21', count: 432 },
{ fy: '2019/20', count: 388 },
{ fy: '2018/19', count: 401 },
{ fy: '2017/18', count: 376 },
{ fy: '2016/17', count: 332 },
{ fy: '2015/16', count: 298 },
{ fy: '2014/15', count: 254 },
{ fy: '2013/14', count: 36 },
]
const maxFy = Math.max(...fiscalYears.map(y => y.count))
const latestFy = '2024/25'
const reportTypes = [
{ name: 'financial_audit', label: 'Financial audit', count: 2_814, description: 'Annual financial statements of national & county entities.' },
{ name: 'county_audit', label: 'County audit', count: 562, description: 'Each of the 47 county governments + devolved funds.' },
{ name: 'performance_audit', label: 'Performance audit', count: 311, description: 'Value-for-money audits on specific programmes.' },
{ name: 'special_audit', label: 'Special audit', count: 142, description: 'Investigations triggered by concerns, e.g. NYS, KEMSA.' },
{ name: 'public_debt_audit', label: 'Public debt', count: 87, description: 'National and guaranteed external debt sustainability.' },
{ name: 'summary_report', label: 'Summary report', count: 24, description: 'Cross-cutting summaries published alongside budget statements.' },
{ name: 'strategic_document', label: 'Strategic document', count: 38, description: 'Strategic plans, frameworks, and OAG-K corporate docs.' },
{ name: 'annual_corporate_report', label: 'Annual corporate report', count: 9, description: 'OAG-K own annual reports.' },
]
const topCounties = [
{ name: 'Nairobi', count: 12 },
{ name: 'Mombasa', count: 11 },
{ name: 'Kisumu', count: 11 },
{ name: 'Uasin Gishu', count: 10 },
{ name: 'Nakuru', count: 10 },
{ name: 'Kiambu', count: 10 },
{ name: 'Kakamega', count: 9 },
{ name: 'Meru', count: 9 },
{ name: 'Kilifi', count: 9 },
{ name: 'Bungoma', count: 9 },
{ name: 'Machakos', count: 8 },
{ name: 'Garissa', count: 8 },
]
</script>
<style scoped lang="scss">
.coverage-view {
max-width: 1100px;
margin: 0 auto;
padding-bottom: 64px;
}
.page-header {
text-align: center;
margin-bottom: 48px;
position: relative;
padding-top: 24px;
&::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 4px;
border-radius: 2px;
background: linear-gradient(
90deg,
var(--ke-black) 0% 40%,
var(--ke-white) 40% 50%,
var(--ke-red) 50% 90%,
var(--ke-green) 90% 100%
);
}
}
.header-eyebrow {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--primary);
margin-bottom: 12px;
font-family: var(--font-mono);
}
.page-title {
font-family: var(--font-display);
font-size: 44px;
font-weight: 700;
line-height: 1.1;
margin: 0 0 12px;
}
.page-lede {
font-size: 16px;
color: var(--text-secondary);
max-width: 620px;
margin: 0 auto;
}
// -- Stats strip --
.stats-strip {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
margin-bottom: 56px;
}
.stat-tile {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 20px 16px;
text-align: center;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--primary);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.4s var(--ease-out);
}
&:hover::after { transform: scaleX(1); }
}
.tile-num {
font-family: var(--font-display);
font-size: 30px;
font-weight: 700;
color: var(--text-primary);
line-height: 1;
margin-bottom: 4px;
}
.tile-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 600;
}
// -- Sections --
.coverage-section {
margin-bottom: 48px;
h2 {
font-family: var(--font-display);
font-size: 26px;
margin: 0 0 8px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.section-lede {
color: var(--text-secondary);
font-size: 14px;
margin: 0 0 20px;
}
}
// -- Fiscal year bars --
.fy-bars { display: flex; flex-direction: column; gap: 8px; }
.fy-row {
display: grid;
grid-template-columns: 80px 1fr 60px;
align-items: center;
gap: 12px;
font-size: 13px;
.fy-label { font-family: var(--font-mono); color: var(--text-secondary); font-weight: 600; }
.fy-bar-track {
height: 24px;
background: var(--bg-elevated);
border-radius: 6px;
overflow: hidden;
position: relative;
}
.fy-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-hover) 100%);
border-radius: 6px;
transition: width 0.6s var(--ease-out);
position: relative;
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255,255,255,0.18), transparent 50%);
}
}
.fy-count {
font-family: var(--font-mono);
font-weight: 700;
color: var(--text-primary);
text-align: right;
}
}
// -- Report type grid --
.type-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 12px;
}
.type-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 20px;
transition: all 0.2s var(--ease-out);
&:hover {
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.type-num {
font-family: var(--font-display);
font-size: 32px;
font-weight: 700;
color: var(--primary);
line-height: 1;
margin-bottom: 6px;
}
.type-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.type-desc {
font-size: 12px;
color: var(--text-muted);
margin: 0;
line-height: 1.5;
}
}
// -- Counties --
.county-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.county-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 999px;
padding: 6px 14px;
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
transition: all 0.2s var(--ease-out);
.chip-num {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
}
&:hover {
background: var(--primary-soft);
border-color: var(--primary);
color: var(--primary);
}
}
// -- Sources --
.source-list {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
li {
background: var(--bg-card);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: var(--radius-sm);
padding: 12px 16px;
font-size: 14px;
color: var(--text-secondary);
strong { color: var(--text-primary); margin-right: 4px; }
}
}
@media (max-width: 768px) {
.stats-strip { grid-template-columns: repeat(2, 1fr); }
.page-title { font-size: 32px; }
.fy-row { grid-template-columns: 60px 1fr 50px; font-size: 12px; }
}
</style>