@jjlmoya/utils-civic 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +2 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/public-budget-analyzer/bibliography.astro +15 -0
- package/src/tool/public-budget-analyzer/bibliography.ts +7 -0
- package/src/tool/public-budget-analyzer/component.astro +52 -0
- package/src/tool/public-budget-analyzer/controller.ts +84 -0
- package/src/tool/public-budget-analyzer/dom-views.ts +52 -0
- package/src/tool/public-budget-analyzer/entry.ts +27 -0
- package/src/tool/public-budget-analyzer/evaluator.ts +15 -0
- package/src/tool/public-budget-analyzer/i18n/de.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/en.ts +78 -0
- package/src/tool/public-budget-analyzer/i18n/es.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/fr.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/id.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/it.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/ja.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/ko.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/nl.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/pl.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/pt.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/ru.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/sv.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/tr.ts +115 -0
- package/src/tool/public-budget-analyzer/i18n/zh.ts +115 -0
- package/src/tool/public-budget-analyzer/index.ts +11 -0
- package/src/tool/public-budget-analyzer/logic.test.ts +81 -0
- package/src/tool/public-budget-analyzer/logic.ts +249 -0
- package/src/tool/public-budget-analyzer/public-budget-analyzer.css +461 -0
- package/src/tool/public-budget-analyzer/seo.astro +15 -0
- package/src/tool/public-budget-analyzer/storage.ts +28 -0
- package/src/tool/public-budget-analyzer/ui.ts +78 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -4,11 +4,12 @@ import { electionSeatApportionmentCalculator } from '../tool/election-seat-appor
|
|
|
4
4
|
import { referendumThresholdCalculator } from '../tool/referendum-threshold-calculator/entry';
|
|
5
5
|
import { voteSeatDisproportionalityAnalyzer } from '../tool/vote-seat-disproportionality-analyzer/entry';
|
|
6
6
|
import { participatoryBudgetAllocator } from '../tool/participatory-budget-allocator/entry';
|
|
7
|
+
import { publicBudgetAnalyzer } from '../tool/public-budget-analyzer/entry';
|
|
7
8
|
import type { CategoryLocaleContent, KnownLocale } from '../types';
|
|
8
9
|
|
|
9
10
|
export const civicCategory = {
|
|
10
11
|
icon: 'mdi:bank-outline',
|
|
11
|
-
tools: [civicPriorityMatrix, coalitionMajorityCalculator, electionSeatApportionmentCalculator, referendumThresholdCalculator, voteSeatDisproportionalityAnalyzer, participatoryBudgetAllocator],
|
|
12
|
+
tools: [civicPriorityMatrix, coalitionMajorityCalculator, electionSeatApportionmentCalculator, referendumThresholdCalculator, voteSeatDisproportionalityAnalyzer, participatoryBudgetAllocator, publicBudgetAnalyzer],
|
|
12
13
|
i18n: {
|
|
13
14
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
14
15
|
de: () => import('./i18n/de').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { electionSeatApportionmentCalculator } from './tool/election-seat-apport
|
|
|
12
12
|
import { referendumThresholdCalculator } from './tool/referendum-threshold-calculator/entry';
|
|
13
13
|
import { voteSeatDisproportionalityAnalyzer } from './tool/vote-seat-disproportionality-analyzer/entry';
|
|
14
14
|
import { participatoryBudgetAllocator } from './tool/participatory-budget-allocator/entry';
|
|
15
|
+
import { publicBudgetAnalyzer } from './tool/public-budget-analyzer/entry';
|
|
15
16
|
|
|
16
17
|
export const ALL_ENTRIES = [
|
|
17
18
|
coalitionMajorityCalculator,
|
|
@@ -20,4 +21,5 @@ export const ALL_ENTRIES = [
|
|
|
20
21
|
referendumThresholdCalculator,
|
|
21
22
|
voteSeatDisproportionalityAnalyzer,
|
|
22
23
|
participatoryBudgetAllocator,
|
|
24
|
+
publicBudgetAnalyzer,
|
|
23
25
|
];
|
|
@@ -5,7 +5,7 @@ import { civicCategory } from '../data';
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
7
|
it('should have tools in ALL_TOOLS', () => {
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(7);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('civicCategory should be defined', () => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { publicBudgetAnalyzer } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale } = Astro.props as Props;
|
|
11
|
+
const loader = publicBudgetAnalyzer.i18n[locale] ?? publicBudgetAnalyzer.i18n.en;
|
|
12
|
+
const content = loader ? await loader() : undefined;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content ? <Bibliography links={content.bibliography} /> : null}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{ name: 'International Monetary Fund, Government Finance Statistics Manual 2014', url: 'https://www.imf.org/external/pubs/ft/gfs/manual/2014/gfsfinal.pdf' },
|
|
5
|
+
{ name: 'Intervencion General de la Administracion del Estado, Clasificacion funcional del gasto de las Administraciones Publicas COFOG', url: 'https://www.igae.pap.hacienda.gob.es/sitios/igae/es-ES/Contabilidad/ContabilidadNacional/Publicaciones/paginas/iacogof.aspx' },
|
|
6
|
+
{ name: 'OECD, Budget Transparency Toolkit', url: 'https://www.oecd.org/en/publications/oecd-budget-transparency-toolkit_9789264282070-en.html' },
|
|
7
|
+
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { PublicBudgetUI } from './ui';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: PublicBudgetUI;
|
|
6
|
+
locale?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props as Props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="public-budget-analyzer" data-public-budget-analyzer>
|
|
13
|
+
<div class="budget-command-bar">
|
|
14
|
+
<div class="budget-dropzone" data-budget-dropzone>
|
|
15
|
+
<input id="budget-file-input" data-budget-file type="file" accept=".csv,.json,text/csv,application/json" aria-label={ui.uploadLabel} />
|
|
16
|
+
<label for="budget-file-input">{ui.uploadLabel}</label>
|
|
17
|
+
<p>{ui.uploadHelp}</p>
|
|
18
|
+
<small>{ui.supportedFormat}</small>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="budget-actions">
|
|
21
|
+
<button type="button" data-budget-example aria-label={`${ui.loadExample}: ${ui.exampleName}`}>{ui.loadExample}</button>
|
|
22
|
+
<button type="button" data-budget-download>{ui.downloadCsv}</button>
|
|
23
|
+
<button type="button" data-budget-print>{ui.printAction}</button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="budget-result" data-budget-result aria-live="polite">
|
|
27
|
+
<p class="budget-empty-note">{ui.analyzeEmpty}</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="budget-guidance">
|
|
30
|
+
<section>
|
|
31
|
+
<h2>{ui.methodHeading}</h2>
|
|
32
|
+
<p>{ui.methodText}</p>
|
|
33
|
+
</section>
|
|
34
|
+
<section>
|
|
35
|
+
<h2>{ui.limitsHeading}</h2>
|
|
36
|
+
<p>{ui.limitsText}</p>
|
|
37
|
+
</section>
|
|
38
|
+
<section>
|
|
39
|
+
<h2>{ui.edgeCasesHeading}</h2>
|
|
40
|
+
<p>{ui.edgeCasesText}</p>
|
|
41
|
+
</section>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script is:inline type="application/json" data-budget-ui set:html={JSON.stringify(ui)}></script>
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
import { mountPublicBudgetAnalyzer } from './controller';
|
|
49
|
+
const root = document.querySelector<HTMLElement>('[data-public-budget-analyzer]');
|
|
50
|
+
const data = document.querySelector<HTMLScriptElement>('[data-budget-ui]');
|
|
51
|
+
if (root && data) mountPublicBudgetAnalyzer(root, JSON.parse(data.textContent || '{}'));
|
|
52
|
+
</script>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { analyzeBudgetRecords, exampleBudgetRecords, parseBudgetText, type BudgetAnalysis } from './logic';
|
|
2
|
+
import { loadSavedBudget, saveBudgetState } from './storage';
|
|
3
|
+
import { renderAnalysis, reviewedCsv } from './dom-views';
|
|
4
|
+
import type { PublicBudgetUI } from './ui';
|
|
5
|
+
|
|
6
|
+
function getElement<T extends HTMLElement>(root: HTMLElement, selector: string): T | undefined {
|
|
7
|
+
return root.querySelector<T>(selector) ?? undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function fileFormat(file: File): 'csv' | 'json' {
|
|
11
|
+
return file.name.toLowerCase().endsWith('.json') ? 'json' : 'csv';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readFile(file: File): Promise<string> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const reader = new FileReader();
|
|
17
|
+
reader.onload = () => resolve(String(reader.result ?? ''));
|
|
18
|
+
reader.onerror = () => reject(new Error('read failed'));
|
|
19
|
+
reader.readAsText(file);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function downloadCsv(analysis: BudgetAnalysis): void {
|
|
24
|
+
const blob = new Blob([reviewedCsv(analysis)], { type: 'text/csv;charset=utf-8' });
|
|
25
|
+
const url = URL.createObjectURL(blob);
|
|
26
|
+
const anchor = document.createElement('a');
|
|
27
|
+
anchor.href = url;
|
|
28
|
+
anchor.download = 'public-budget-reviewed.csv';
|
|
29
|
+
anchor.click();
|
|
30
|
+
URL.revokeObjectURL(url);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function showMessage(target: HTMLElement, message: string, tone: 'error' | 'info'): void {
|
|
34
|
+
target.innerHTML = `<p class="budget-inline-message budget-inline-${tone}" role="status">${message}</p>`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function analyzeText(text: string, format: 'csv' | 'json', target: HTMLElement, ui: PublicBudgetUI): BudgetAnalysis | undefined {
|
|
38
|
+
const parsed = parseBudgetText(text, format);
|
|
39
|
+
if (parsed.errors.length) {
|
|
40
|
+
showMessage(target, `${ui.invalidFile} ${parsed.errors.join(' ')}`, 'error');
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const analysis = analyzeBudgetRecords(parsed.records);
|
|
44
|
+
renderAnalysis(target, analysis, ui);
|
|
45
|
+
saveBudgetState({ format, text });
|
|
46
|
+
return analysis;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderExample(target: HTMLElement, ui: PublicBudgetUI): BudgetAnalysis {
|
|
50
|
+
const analysis = analyzeBudgetRecords(exampleBudgetRecords());
|
|
51
|
+
renderAnalysis(target, analysis, ui);
|
|
52
|
+
return analysis;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mountPublicBudgetAnalyzer(root: HTMLElement, ui: PublicBudgetUI): void {
|
|
56
|
+
const fileInput = getElement<HTMLInputElement>(root, '[data-budget-file]');
|
|
57
|
+
const result = getElement<HTMLElement>(root, '[data-budget-result]');
|
|
58
|
+
const example = getElement<HTMLButtonElement>(root, '[data-budget-example]');
|
|
59
|
+
const print = getElement<HTMLButtonElement>(root, '[data-budget-print]');
|
|
60
|
+
const download = getElement<HTMLButtonElement>(root, '[data-budget-download]');
|
|
61
|
+
const dropzone = getElement<HTMLElement>(root, '[data-budget-dropzone]');
|
|
62
|
+
if (!fileInput || !result || !example || !print || !download || !dropzone) return;
|
|
63
|
+
let latest: BudgetAnalysis | undefined;
|
|
64
|
+
const processFile = async (file: File): Promise<void> => {
|
|
65
|
+
try {
|
|
66
|
+
latest = analyzeText(await readFile(file), fileFormat(file), result, ui);
|
|
67
|
+
} catch {
|
|
68
|
+
showMessage(result, ui.fileReadError, 'error');
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
fileInput.addEventListener('change', () => { const file = fileInput.files?.[0]; if (file) void processFile(file); });
|
|
72
|
+
dropzone.addEventListener('dragover', (event) => { event.preventDefault(); dropzone.classList.add('is-dragging'); });
|
|
73
|
+
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('is-dragging'));
|
|
74
|
+
dropzone.addEventListener('drop', (event) => { event.preventDefault(); dropzone.classList.remove('is-dragging'); const file = event.dataTransfer?.files[0]; if (file) void processFile(file); });
|
|
75
|
+
example.addEventListener('click', () => { latest = renderExample(result, ui); });
|
|
76
|
+
print.addEventListener('click', () => window.print());
|
|
77
|
+
download.addEventListener('click', () => { if (latest) downloadCsv(latest); });
|
|
78
|
+
const saved = loadSavedBudget();
|
|
79
|
+
if (saved) {
|
|
80
|
+
latest = analyzeText(saved.text, saved.format, result, ui);
|
|
81
|
+
} else {
|
|
82
|
+
latest = renderExample(result, ui);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { PublicBudgetUI } from './ui';
|
|
2
|
+
import type { BudgetAnalysis } from './logic';
|
|
3
|
+
import { evaluateBudget, statusLabel } from './evaluator';
|
|
4
|
+
|
|
5
|
+
function escapeHtml(value: string): string {
|
|
6
|
+
return value.replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character] || character));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatAmount(value: number, currency?: string): string {
|
|
10
|
+
const code = currency && /^[A-Za-z]{3}$/.test(currency) ? currency.toUpperCase() : undefined;
|
|
11
|
+
return new Intl.NumberFormat('en-US', { style: code ? 'currency' : 'decimal', currency: code || 'USD', maximumFractionDigits: 2 }).format(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatPercent(value: number | undefined): string {
|
|
15
|
+
return value === undefined ? 'Not available' : `${new Intl.NumberFormat('en-US', { maximumFractionDigits: 1, signDisplay: 'exceptZero' }).format(value * 100)}%`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function metric(label: string, value: string): string {
|
|
19
|
+
return `<div class="budget-metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function categoryBars(analysis: BudgetAnalysis, ui: PublicBudgetUI): string {
|
|
23
|
+
const positive = analysis.categories.filter((category) => category.amount > 0).slice(0, 8);
|
|
24
|
+
if (!positive.length) return `<p class="budget-empty-note">${escapeHtml(ui.noData)}</p>`;
|
|
25
|
+
return positive.map((category, index) => {
|
|
26
|
+
const width = analysis.positiveTotal ? Math.max(2, category.amount / analysis.positiveTotal * 100) : 0;
|
|
27
|
+
const shade = index % 2 === 0 ? 'budget-segment-a' : 'budget-segment-b';
|
|
28
|
+
return `<div class="budget-category-row"><div class="budget-category-label"><span>${escapeHtml(category.path.join(' / '))}</span><strong>${formatAmount(category.amount, analysis.currency)}</strong></div><div class="budget-bar"><span class="${shade}" style="--segment-width:${width}%"></span></div><small>${escapeHtml(ui.shareLabel)} ${formatPercent(category.share)} · ${escapeHtml(ui.sourceLabel)} ${category.sourceRows.join(', ')}</small></div>`;
|
|
29
|
+
}).join('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function tableRows(analysis: BudgetAnalysis): string {
|
|
33
|
+
return analysis.categories.map((category) => `<tr><th scope="row">${escapeHtml(category.path.join(' / '))}</th><td>${formatAmount(category.amount, analysis.currency)}</td><td>${formatPercent(category.share)}</td><td>${formatPercent(category.variation)}</td><td>${category.sourceRows.join(', ')}</td></tr>`).join('');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function notices(analysis: BudgetAnalysis, ui: PublicBudgetUI): string {
|
|
37
|
+
const warnings = analysis.warnings.length ? `<section class="budget-notice budget-notice-warning"><h3>${escapeHtml(ui.warningsHeading)}</h3><ul>${analysis.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul></section>` : '';
|
|
38
|
+
const errors = analysis.errors.length ? `<section class="budget-notice budget-notice-error"><h3>${escapeHtml(ui.errorsHeading)}</h3><ul>${analysis.errors.map((error) => `<li>${escapeHtml(error)}</li>`).join('')}</ul></section>` : '';
|
|
39
|
+
return `${errors}${warnings}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function renderAnalysis(target: HTMLElement, analysis: BudgetAnalysis, ui: PublicBudgetUI): void {
|
|
43
|
+
const status = evaluateBudget(analysis);
|
|
44
|
+
const topCategory = analysis.categories[0]?.path.join(' / ') || ui.noData;
|
|
45
|
+
target.innerHTML = `<div class="budget-result-head"><div><span class="budget-status budget-status-${status}">${escapeHtml(statusLabel(status))}</span><h2>${escapeHtml(ui.resultHeading)}</h2></div><p>${analysis.period ? escapeHtml(analysis.period) : ''}</p></div>${analysis.rows.length ? `<div class="budget-metrics">${metric(ui.totalLabel, formatAmount(analysis.total, analysis.currency))}${metric(ui.perCapitaLabel, analysis.perCapita === undefined ? 'Not available' : formatAmount(analysis.perCapita, analysis.currency))}${metric(ui.variationLabel, formatPercent(analysis.variation))}${metric(ui.topCategoryLabel, topCategory)}</div><div class="budget-landscape" role="img" aria-label="Budget categories sized by their positive amounts"><div class="budget-landscape-axis"><span>0</span><span>${formatAmount(analysis.positiveTotal, analysis.currency)}</span></div>${categoryBars(analysis, ui)}</div><div class="budget-detail"><div class="budget-detail-heading"><h3>${escapeHtml(ui.tableHeading)}</h3><span>${analysis.rows.length} ${escapeHtml(ui.rowsLabel)} / ${analysis.categories.length} ${escapeHtml(ui.categoriesLabel)}</span></div><div class="budget-table-wrap"><table><thead><tr><th scope="col">${escapeHtml(ui.categoryLabel)}</th><th scope="col">${escapeHtml(ui.amountLabel)}</th><th scope="col">${escapeHtml(ui.shareLabel)}</th><th scope="col">${escapeHtml(ui.variationLabel)}</th><th scope="col">${escapeHtml(ui.sourceRowsLabel)}</th></tr></thead><tbody>${tableRows(analysis)}</tbody></table></div></div>${notices(analysis, ui)}` : `<p class="budget-empty-note">${escapeHtml(ui.noData)}</p>`}<div class="budget-facts"><div><span>${escapeHtml(ui.positiveTotalLabel)}</span><strong>${formatAmount(analysis.positiveTotal, analysis.currency)}</strong></div><div><span>${escapeHtml(ui.previousTotalLabel)}</span><strong>${analysis.previousTotal === undefined ? escapeHtml(ui.noPreviousData) : formatAmount(analysis.previousTotal, analysis.currency)}</strong></div><div><span>${escapeHtml(ui.populationLabel)}</span><strong>${analysis.population === undefined ? 'Not available' : new Intl.NumberFormat('en-US').format(analysis.population)}</strong></div></div>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function reviewedCsv(analysis: BudgetAnalysis): string {
|
|
49
|
+
const header = ['category', 'amount', 'share', 'variation', 'sourceRows'];
|
|
50
|
+
const rows = analysis.categories.map((category) => [category.path.join(' > '), String(category.amount), category.share === undefined ? '' : String(category.share), category.variation === undefined ? '' : String(category.variation), category.sourceRows.join('|')]);
|
|
51
|
+
return [header, ...rows].map((row) => row.map((value) => `"${value.replace(/"/g, '""')}"`).join(',')).join('\n');
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CivicToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { PublicBudgetUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type PublicBudgetLocaleContent = ToolLocaleContent<PublicBudgetUI>;
|
|
5
|
+
|
|
6
|
+
export const publicBudgetAnalyzer: CivicToolEntry<PublicBudgetUI> = {
|
|
7
|
+
id: 'public-budget-analyzer',
|
|
8
|
+
phase: 'localized',
|
|
9
|
+
icons: { bg: 'mdi:bank-outline', fg: 'mdi:chart-pie' },
|
|
10
|
+
i18n: {
|
|
11
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
12
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
13
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
14
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
15
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
16
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
17
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
18
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
19
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
20
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
21
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
22
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
23
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
24
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
25
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BudgetAnalysis } from './logic';
|
|
2
|
+
|
|
3
|
+
export type BudgetStatus = 'ready' | 'review' | 'empty';
|
|
4
|
+
|
|
5
|
+
export function evaluateBudget(analysis: BudgetAnalysis): BudgetStatus {
|
|
6
|
+
if (!analysis.rows.length) return 'empty';
|
|
7
|
+
if (analysis.errors.length || analysis.warnings.length) return 'review';
|
|
8
|
+
return 'ready';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function statusLabel(status: BudgetStatus): string {
|
|
12
|
+
if (status === 'ready') return 'Ready to review';
|
|
13
|
+
if (status === 'review') return 'Review data notes';
|
|
14
|
+
return 'Waiting for data';
|
|
15
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import { bibliography } from '../bibliography';
|
|
3
|
+
import type { PublicBudgetLocaleContent } from '../entry';
|
|
4
|
+
import type { PublicBudgetUI } from '../ui';
|
|
5
|
+
|
|
6
|
+
const ui: PublicBudgetUI = {
|
|
7
|
+
uploadLabel: 'Budget CSV oder JSON hier ablegen',
|
|
8
|
+
uploadHelp: 'Pflichtfelder: Kategorie und Betrag. Optional: Bevölkerung, Zeitraum, priorAmount, Währung und übergeordnete Kategorie.',
|
|
9
|
+
loadExample: 'Beispielbudget laden',
|
|
10
|
+
supportedFormat: 'CSV oder JSON, nur in diesem Browser verarbeitet',
|
|
11
|
+
tableHeading: 'Quellzeilen und Kategoriesummen',
|
|
12
|
+
categoryLabel: 'Kategorie',
|
|
13
|
+
amountLabel: 'Betrag',
|
|
14
|
+
populationLabel: 'Bevölkerung',
|
|
15
|
+
sourceRowsLabel: 'Quellzeilen',
|
|
16
|
+
analyzeEmpty: 'Wähle eine Datei oder lade das Beispiel, um ein Budget zu prüfen.',
|
|
17
|
+
resultHeading: 'Budgetlandschaft',
|
|
18
|
+
totalLabel: 'Gesamtausgaben',
|
|
19
|
+
perCapitaLabel: 'Ausgaben pro Person',
|
|
20
|
+
variationLabel: 'Änderung gegenüber dem Vorzeitraum',
|
|
21
|
+
topCategoryLabel: 'Größte Kategorie',
|
|
22
|
+
shareLabel: 'Anteil an der Summe',
|
|
23
|
+
sourceLabel: 'Quellzeilen',
|
|
24
|
+
noData: 'Noch keine gültigen Budgetzeilen verfügbar.',
|
|
25
|
+
warningsHeading: 'Zu prüfende Warnungen',
|
|
26
|
+
errorsHeading: 'Nicht einbezogene Zeilen',
|
|
27
|
+
methodHeading: 'Angewandte Methode',
|
|
28
|
+
methodText: 'Das Werkzeug prüft Pflichtfelder, bewahrt jede Quellzeile, gruppiert exakte Kategoriepfade und berechnet Summen aus normalisierten Beträgen. Der Kategorieanteil ist der Betrag geteilt durch die positive Gesamtsumme. Die Pro Kopf Ausgabe teilt die Summe durch den ersten einheitlichen positiven Bevölkerungswert. Die Zeitänderung ist die Differenz geteilt durch den absoluten Vorbetrag.',
|
|
29
|
+
limitsHeading: 'Was dieses Werkzeug nicht leistet',
|
|
30
|
+
limitsText: 'Es beweist nicht, dass ein Budget vollständig, vergleichbar, rechtmäßig, effizient, fair, unparteiisch oder korrekt klassifiziert ist. Es prüft die Quelle nicht, bewertet keine Politik, rechnet Währungen nicht um und erkennt nicht, ob eine Zeile eine Bewilligung, Verpflichtung, Zahlung oder Prognose ist.',
|
|
31
|
+
edgeCasesHeading: 'Sonderfälle und Datenwarnungen',
|
|
32
|
+
edgeCasesText: 'Gemischte Währungen, wechselnde Bevölkerungen, doppelte Kategorien, negative Anpassungen, Elternzeilen neben Kindzeilen, lokale Zahlenformate und ein Vorbetrag von null brauchen menschliche Prüfung. Die ursprüngliche Zeilennummer bleibt sichtbar, damit jede Summe zur Eingabe zurückverfolgt werden kann.',
|
|
33
|
+
downloadCsv: 'Geprüfte Tabelle herunterladen',
|
|
34
|
+
printAction: 'Analyse drucken',
|
|
35
|
+
rowsLabel: 'gültige Zeilen',
|
|
36
|
+
categoriesLabel: 'Kategorien',
|
|
37
|
+
positiveTotalLabel: 'Visualisierte positive Beträge',
|
|
38
|
+
previousTotalLabel: 'Vorherige Summe',
|
|
39
|
+
invalidFile: 'Die Datei konnte nicht als gefüllte CSV oder JSON Tabelle gelesen werden.',
|
|
40
|
+
fileReadError: 'Die Datei konnte in diesem Browser nicht gelesen werden.',
|
|
41
|
+
exampleName: 'Beispiel eines kommunalen Budgets',
|
|
42
|
+
noPreviousData: 'Kein vergleichbarer Vorzeitraum angegeben.',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const softwareApplication: SoftwareApplication = {
|
|
46
|
+
'@type': 'SoftwareApplication',
|
|
47
|
+
name: 'Analyse öffentlicher Haushalte',
|
|
48
|
+
applicationCategory: 'EducationalApplication',
|
|
49
|
+
operatingSystem: 'Any',
|
|
50
|
+
description: 'Öffentliche Budgettabellen lokal nach Ausgaben, Kategorien, Pro Kopf Werten und Zeitänderungen prüfen.',
|
|
51
|
+
url: 'https://gamebob.dev/de/oeffentliche-haushaltsanalyse-ausgaben',
|
|
52
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const faqPage: FAQPage = {
|
|
56
|
+
'@type': 'FAQPage',
|
|
57
|
+
mainEntity: [
|
|
58
|
+
{ '@type': 'Question', name: 'Welche Spalten braucht die Analyse?', acceptedAnswer: { '@type': 'Answer', text: 'Jede Zeile braucht eine Kategorie und einen numerischen Betrag. Bevölkerung, Zeitraum, priorAmount, Währung und übergeordnete Kategorie sind optional.' } },
|
|
59
|
+
{ '@type': 'Question', name: 'Wie werden Ausgaben pro Person berechnet?', acceptedAnswer: { '@type': 'Answer', text: 'Die Gesamtsumme wird durch den ersten einheitlichen positiven Bevölkerungswert geteilt. Abweichende Nenner bleiben sichtbar und erzeugen eine Warnung.' } },
|
|
60
|
+
{ '@type': 'Question', name: 'Was passiert mit doppelten Kategorien?', acceptedAnswer: { '@type': 'Answer', text: 'Gleiche Kategoriepfade werden addiert. Die zugehörigen Quellzeilen bleiben sichtbar, damit eine echte Aufteilung von einer möglichen Doppelzählung unterschieden werden kann.' } },
|
|
61
|
+
{ '@type': 'Question', name: 'Beweist die Analyse, dass ein Budget gut oder fair ist?', acceptedAnswer: { '@type': 'Answer', text: 'Nein. Sie beschreibt nur die gelieferten Zahlen und bewertet weder Vollständigkeit noch Rechtmäßigkeit, Effizienz, Fairness oder Ergebnisse.' } },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const howTo: HowTo = {
|
|
66
|
+
'@type': 'HowTo',
|
|
67
|
+
name: 'Eine öffentliche Budgettabelle analysieren',
|
|
68
|
+
step: [
|
|
69
|
+
{ '@type': 'HowToStep', name: 'Tabelle vorbereiten', text: 'Erstelle eine CSV oder JSON Tabelle mit einer Kategorie und einem Betrag je Zeile. Halte Währung und Rechnungsgrundlage einheitlich.' },
|
|
70
|
+
{ '@type': 'HowToStep', name: 'Kontext ergänzen', text: 'Füge Bevölkerung, Zeitraum, priorAmount, Währung oder übergeordnete Kategorie nur bei konsistenter Bedeutung hinzu.' },
|
|
71
|
+
{ '@type': 'HowToStep', name: 'Datei laden', text: 'Lege die Datei ab oder lade das Beispiel, um Aufbau und lokale Prüfung zu sehen.' },
|
|
72
|
+
{ '@type': 'HowToStep', name: 'Landschaft lesen', text: 'Nutze die proportionalen Balken für die Zusammensetzung und die Tabelle für genaue Beträge, Änderungen und Quellzeilen.' },
|
|
73
|
+
{ '@type': 'HowToStep', name: 'Warnungen prüfen', text: 'Klär gemischte Währungen, doppelte Pfade, negative Anpassungen, Null Nenner und Elternzeilen, bevor du Schlussfolgerungen ziehst.' },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const content: PublicBudgetLocaleContent = {
|
|
78
|
+
slug: 'oeffentliche-haushaltsanalyse-ausgaben',
|
|
79
|
+
title: 'Analyse öffentlicher Haushalte',
|
|
80
|
+
description: 'Prüfe eine öffentliche Budgettabelle lokal und sieh Gesamtausgaben, Kategorieanteile, Pro Kopf Werte, Änderungen und die Quellzeilen hinter jeder Summe.',
|
|
81
|
+
ui,
|
|
82
|
+
seo: [
|
|
83
|
+
{ type: 'title', text: 'Eine öffentliche Budgettabelle prüfbar machen', level: 2 },
|
|
84
|
+
{ type: 'paragraph', html: 'Öffentliche Budgetdateien vermischen oft Kategorien, Rechnungszeiträume, Bevölkerungswerte und Anpassungen. Diese Analyse bewahrt die Quellzeilen, prüft Pflichtfelder und macht aus denselben normalisierten Daten Summen, Anteile, Ausgaben pro Person und Zeitänderungen.' },
|
|
85
|
+
{ type: 'paragraph', html: 'Das Ergebnis beschreibt Zahlen und spricht kein politisches Urteil. Eine große Kategorie kann eine gesetzliche Aufgabe, eine einmalige Investition, einen Transfer oder eine andere Abgrenzung widerspiegeln. Die Ansicht zeigt diese Muster, damit die Originalquelle gezielter geprüft werden kann.' },
|
|
86
|
+
{ type: 'title', text: 'Was die Berechnungen bedeuten', level: 2 },
|
|
87
|
+
{ type: 'paragraph', html: 'Die Gesamtsumme addiert alle gültigen numerischen Beträge einschließlich negativer Anpassungen. Ein Kategorieanteil ist der gruppierte Betrag geteilt durch die positive Summe. Positive Beträge werden proportional dargestellt; negative Beträge bleiben in der Tabelle, weil eine Flächendarstellung ihre Bedeutung verfälschen würde.' },
|
|
88
|
+
{ type: 'table', headers: ['Ausgabe', 'Berechnung', 'Prüffrage'], rows: [['Gesamtausgaben', 'Summe der normalisierten Beträge', 'Haben alle Zeilen dieselbe Rechnungsgrundlage?'], ['Kategorieanteil', 'Kategoriebetrag geteilt durch positive Summe', 'Überlappen Elternkategorien und Kinder?'], ['Ausgaben pro Person', 'Summe geteilt durch positiven Bevölkerungsnenner', 'Beschreibt die Bevölkerung denselben Ort und Zeitraum?'], ['Zeitänderung', 'Aktueller Betrag minus Vorbetrag geteilt durch absoluten Vorbetrag', 'Sind die Zeiträume vergleichbar und ist der Ausgangswert ungleich null?']] },
|
|
89
|
+
{ type: 'title', text: 'Vergleichbare Daten vorbereiten', level: 2 },
|
|
90
|
+
{ type: 'paragraph', html: 'Verwende eine Währung und eine klare Bedeutung des Betrags für jede Zeile. Bewilligung, Verpflichtung, Zahlung und Prognose können gültige Zahlen sein, beantworten aber unterschiedliche Fragen. Halte diese Bedeutung neben der Quelle fest, denn das Werkzeug erkennt sie nicht aus dem Spaltennamen.' },
|
|
91
|
+
{ type: 'list', items: ['Nutze eine Zeile je Kategorie oder gib für eine bewusste Hierarchie eine übergeordnete Kategorie an.', 'Halte Bevölkerungswerte konsistent und mische keine Einwohner, Wahlberechtigten, Haushalte oder Nutzer.', 'Verwende priorAmount nur bei gleicher Kategorie und Rechnungsgrundlage im Vorzeitraum.', 'Prüfe doppelte Kategoriepfade und Elternzeilen auf unbeabsichtigte Doppelzählung.', 'Bewahre die geprüfte Tabelle zusammen mit Quelle und Annahmen auf.'] },
|
|
92
|
+
{ type: 'tip', title: 'Ein Anteil ist keine Prioritätswertung', html: 'Die größte Kategorie ist nicht automatisch die wichtigste, verschwenderischste oder erfolgreichste. Nutze die Ansicht für Größenordnung und Änderungen und lies danach Quellen und Ergebnisse des Dienstes.' },
|
|
93
|
+
{ type: 'title', text: 'Warnungen vor Schlussfolgerungen lesen', level: 2 },
|
|
94
|
+
{ type: 'paragraph', html: 'Doppelte Pfade werden gruppiert, damit die Summe nicht still zu niedrig ausfällt. Die Quellzeilen zeigen jedoch, ob diese Gruppierung beabsichtigt ist. Gemischte Währungen werden nicht umgerechnet. Eine Bevölkerung von null entfernt den Pro Kopf Wert, ein Vorbetrag von null die Prozentänderung.' },
|
|
95
|
+
{ type: 'paragraph', html: 'Verschachtelte Kategorien brauchen besondere Vorsicht. Enthält eine Tabelle sowohl Bildung als auch Bildung > Schulen, kann eine Addition dieselben Ausgaben doppelt zählen. Die Analyse warnt vor solchen Pfaden, aber nur die Methodik der Quelle kann klären, ob die Elternzeile eine Zwischensumme oder eine eigenständige Position ist.' },
|
|
96
|
+
{ type: 'title', text: 'Nach dem ersten Durchlauf handeln', level: 2 },
|
|
97
|
+
{ type: 'paragraph', html: 'Nutze die Quellzeilen, um zur offiziellen Datei zurückzugehen und jede Korrektur oder Auslegung zu dokumentieren. Vergleiche nur gleichartige Zeiträume, bewahre negative Anpassungen als Anpassungen und entferne Zwischensummen erst dann, wenn die Quelle bestätigt, dass sie bereits enthalten sind.' },
|
|
98
|
+
{ type: 'tip', title: 'Die Datengrenze sichtbar halten', html: 'Dies ist eine lokale beschreibende Analyse. Sie prüft weder Vollständigkeit noch eine amtliche Klassifikation, rechnet Währungen um oder beweist einen Ursache Wirkung Zusammenhang zwischen Ausgaben und Ergebnissen.' },
|
|
99
|
+
],
|
|
100
|
+
faq: [
|
|
101
|
+
{ question: 'Welche Spalten braucht die Analyse?', answer: 'Jede Zeile braucht Kategorie und numerischen Betrag. Bevölkerung, Zeitraum, priorAmount, Währung und übergeordnete Kategorie sind optional.' },
|
|
102
|
+
{ question: 'Wie werden Ausgaben pro Person berechnet?', answer: 'Die Gesamtsumme wird durch den ersten konsistenten positiven Bevölkerungswert geteilt. Uneinheitliche Nenner erzeugen eine Warnung.' },
|
|
103
|
+
{ question: 'Was passiert mit doppelten Kategorien?', answer: 'Gleiche Kategoriepfade werden addiert und ihre Quellzeilen bleiben zur Prüfung sichtbar.' },
|
|
104
|
+
{ question: 'Beweist die Analyse, dass ein Budget gut oder fair ist?', answer: 'Nein. Sie beschreibt Zahlen und kann Vollständigkeit, Rechtmäßigkeit, Effizienz, Fairness oder Ergebnisse nicht feststellen.' },
|
|
105
|
+
],
|
|
106
|
+
bibliography,
|
|
107
|
+
howTo: [
|
|
108
|
+
{ name: 'Tabelle vorbereiten', text: 'Erstelle eine CSV oder JSON Tabelle mit Kategorie und Betrag je Zeile sowie gleicher Währung und Rechnungsgrundlage.' },
|
|
109
|
+
{ name: 'Kontext ergänzen', text: 'Füge Bevölkerung, Zeitraum, priorAmount, Währung oder übergeordnete Kategorie nur bei konsistenter Bedeutung hinzu.' },
|
|
110
|
+
{ name: 'Datei laden', text: 'Lege die Datei ab oder lade das Beispiel, um den erwarteten Aufbau zu sehen.' },
|
|
111
|
+
{ name: 'Landschaft lesen', text: 'Nutze proportionale Balken für die Zusammensetzung und die Tabelle für genaue Werte, Änderungen und Quellzeilen.' },
|
|
112
|
+
{ name: 'Warnungen prüfen', text: 'Klär gemischte Währungen, doppelte Pfade, negative Anpassungen, Null Nenner und Elternzeilen vor einer Schlussfolgerung.' },
|
|
113
|
+
],
|
|
114
|
+
schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[],
|
|
115
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import { bibliography } from '../bibliography';
|
|
3
|
+
import type { PublicBudgetLocaleContent } from '../entry';
|
|
4
|
+
import { ui } from '../ui';
|
|
5
|
+
|
|
6
|
+
const softwareApplication: SoftwareApplication = {
|
|
7
|
+
'@type': 'SoftwareApplication',
|
|
8
|
+
name: 'Public Budget Analyzer',
|
|
9
|
+
applicationCategory: 'EducationalApplication',
|
|
10
|
+
operatingSystem: 'Any',
|
|
11
|
+
description: 'Inspect spending totals, category shares, per capita values, and period changes from a local public budget table.',
|
|
12
|
+
url: 'https://gamebob.dev/en/public-budget-analyzer',
|
|
13
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const faqPage: FAQPage = {
|
|
17
|
+
'@type': 'FAQPage',
|
|
18
|
+
mainEntity: [
|
|
19
|
+
{ '@type': 'Question', name: 'What columns does the analyzer need?', acceptedAnswer: { '@type': 'Answer', text: 'Each row needs a category and a numeric amount. Population, period, priorAmount, currency, and parentCategory are optional, so the analyzer can still show totals and category composition when those fields are missing.' } },
|
|
20
|
+
{ '@type': 'Question', name: 'How is spending per person calculated?', acceptedAnswer: { '@type': 'Answer', text: 'The total amount is divided by the first consistent positive population value found in the table. If population values differ, the tool keeps the calculation but displays a warning so the denominator can be checked.' } },
|
|
21
|
+
{ '@type': 'Question', name: 'What happens to duplicate categories?', acceptedAnswer: { '@type': 'Answer', text: 'Rows with the same category path are added together and their source row numbers remain visible. Grouping is transparent, but duplicate rows may be a real split in the source or an accidental double count.' } },
|
|
22
|
+
{ '@type': 'Question', name: 'Can this prove that a budget is good or fair?', acceptedAnswer: { '@type': 'Answer', text: 'No. The analyzer describes the numbers supplied to it. It cannot establish completeness, legality, efficiency, fairness, classification quality, or whether the spending produced a desired outcome.' } },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const howTo: HowTo = {
|
|
27
|
+
'@type': 'HowTo',
|
|
28
|
+
name: 'Analyze a public budget table',
|
|
29
|
+
step: [
|
|
30
|
+
{ '@type': 'HowToStep', name: 'Prepare a table', text: 'Create a CSV or JSON table with one category and amount per row. Keep amounts on the same currency and accounting basis.' },
|
|
31
|
+
{ '@type': 'HowToStep', name: 'Add context fields', text: 'Add population, period, priorAmount, currency, or parentCategory only when their denominator and meaning are consistent across the rows.' },
|
|
32
|
+
{ '@type': 'HowToStep', name: 'Load the file', text: 'Drop the file into the analyzer or load the example to see the expected shape and the local validation result.' },
|
|
33
|
+
{ '@type': 'HowToStep', name: 'Read the landscape', text: 'Use the proportional category bars for composition, then inspect the accessible table for exact totals, changes, and source row numbers.' },
|
|
34
|
+
{ '@type': 'HowToStep', name: 'Review warnings', text: 'Resolve mixed currencies, duplicate paths, negative adjustments, zero denominators, and parent rows before drawing a policy conclusion.' },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const content: PublicBudgetLocaleContent = {
|
|
39
|
+
slug: 'public-budget-analyzer',
|
|
40
|
+
title: 'Public Budget Analyzer',
|
|
41
|
+
description: 'Inspect a public budget table locally to see total spending, category shares, per capita values, changes, and the source rows behind every aggregate.',
|
|
42
|
+
ui,
|
|
43
|
+
seo: [
|
|
44
|
+
{ type: 'title', text: 'Turn a Public Budget Table Into an Auditable View', level: 2 },
|
|
45
|
+
{ type: 'paragraph', html: 'Public budget files often mix category labels, accounting periods, population denominators, and adjustments in a table that is difficult to read at a glance. This analyzer keeps the source rows in the browser, validates the required fields, and turns the same normalized data into totals, category proportions, per capita spending, and period changes.' },
|
|
46
|
+
{ type: 'paragraph', html: 'The result is descriptive rather than judgmental. A large category may reflect a statutory responsibility, a one-off investment, a transfer, or a different accounting boundary. The tool makes those patterns visible so that a reviewer can ask a better question of the source document.' },
|
|
47
|
+
{ type: 'title', text: 'What the Calculations Mean', level: 2 },
|
|
48
|
+
{ type: 'paragraph', html: 'The total is the sum of valid numeric amounts, including negative adjustments. A category share is its grouped amount divided by the total when the total is positive. Positive amounts receive proportional bars; negative amounts remain in the table and are flagged because a treemap cannot represent them as positive area without changing their meaning.' },
|
|
49
|
+
{ type: 'table', headers: ['Output', 'Calculation', 'Review question'], rows: [['Total spending', 'Sum of normalized amounts', 'Are all rows on the same accounting basis?'], ['Category share', 'Category amount divided by positive total', 'Are categories mutually exclusive or do parent rows overlap children?'], ['Spending per person', 'Total divided by a positive population denominator', 'Does the population describe the same place and period?'], ['Period change', '(Current amount minus prior amount) divided by absolute prior amount', 'Are the two periods comparable and is a zero baseline being avoided?']] },
|
|
50
|
+
{ type: 'title', text: 'Prepare Data That Can Be Compared', level: 2 },
|
|
51
|
+
{ type: 'paragraph', html: 'Use a single currency and an explicit amount meaning for every row. A budget authorization, an obligation, a payment, and a forecast can all be valid numbers while answering different questions. Record that meaning beside the source file because the analyzer does not infer it from a column name.' },
|
|
52
|
+
{ type: 'list', items: ['Use one row per category or provide a parentCategory field for a deliberate hierarchy.', 'Keep population values consistent and do not mix residents, eligible voters, households, or service users.', 'Include priorAmount only when the prior period uses the same categories and amount basis.', 'Check duplicate category paths and parent rows for accidental double counting.', 'Keep the exported reviewed table with the original source and the assumptions used to interpret it.'] },
|
|
53
|
+
{ type: 'tip', title: 'A proportion is not a priority score', html: 'A category with the largest amount is not automatically the most important, wasteful, or successful. Use the view to locate scale and changes, then read the source notes and service outcomes before evaluating a policy.' },
|
|
54
|
+
{ type: 'title', text: 'Read Warnings Before Drawing Conclusions', level: 2 },
|
|
55
|
+
{ type: 'paragraph', html: 'Duplicate paths are grouped so that the aggregate is not silently understated, but the source row list lets you investigate whether that grouping is intentional. Mixed currencies are never converted. A population of zero removes the per capita result, and a zero prior amount removes the percentage change because division by zero has no meaningful interpretation.' },
|
|
56
|
+
{ type: 'paragraph', html: 'Nested categories need particular care. If a table contains both Education and Education > Schools as amounts, adding both may count the same spending twice. The tool warns when paths are nested, but only the source methodology can tell you whether the parent is a subtotal or an independent line.' },
|
|
57
|
+
{ type: 'title', text: 'What to Do After the First Pass', level: 2 },
|
|
58
|
+
{ type: 'paragraph', html: 'Use the source row numbers to return to the official file and document each correction or interpretation. Compare like with like across periods, preserve negative adjustments as adjustments, and rerun the analysis after removing subtotal rows when the source says they are already included.' },
|
|
59
|
+
{ type: 'tip', title: 'Keep the data boundary visible', html: 'This is a local descriptive analysis. It does not verify that the file is complete, classify expenditure according to an official standard, reconcile currencies, or prove a causal link between spending and results.' },
|
|
60
|
+
],
|
|
61
|
+
faq: [
|
|
62
|
+
{ question: 'What columns does the analyzer need?', answer: 'Each row needs a category and a numeric amount. Population, period, priorAmount, currency, and parentCategory are optional.' },
|
|
63
|
+
{ question: 'How is spending per person calculated?', answer: 'The total amount is divided by the first consistent positive population value. Inconsistent denominators produce a warning.' },
|
|
64
|
+
{ question: 'What happens to duplicate categories?', answer: 'Same category paths are added together and their source row numbers remain visible for review.' },
|
|
65
|
+
{ question: 'Can this prove that a budget is good or fair?', answer: 'No. It describes supplied numbers and cannot establish completeness, legality, efficiency, fairness, or outcomes.' },
|
|
66
|
+
],
|
|
67
|
+
bibliography,
|
|
68
|
+
howTo: [
|
|
69
|
+
{ name: 'Prepare a table', text: 'Create a CSV or JSON table with one category and amount per row on the same currency and accounting basis.' },
|
|
70
|
+
{ name: 'Add context fields', text: 'Add population, period, priorAmount, currency, or parentCategory only when their meaning is consistent.' },
|
|
71
|
+
{ name: 'Load the file', text: 'Drop the file into the analyzer or load the example to see the expected shape.' },
|
|
72
|
+
{ name: 'Read the landscape', text: 'Use the proportional bars for composition and the table for exact values, changes, and source row numbers.' },
|
|
73
|
+
{ name: 'Review warnings', text: 'Resolve mixed currencies, duplicates, negative adjustments, zero denominators, and parent rows before drawing conclusions.' },
|
|
74
|
+
],
|
|
75
|
+
schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const englishContent = content;
|