@jjlmoya/utils-civic 1.6.0 → 1.8.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 +3 -1
- package/src/entries.ts +4 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tests/translation_copy.test.ts +1 -1
- package/src/tool/parliamentary-voting-analyzer/bibliography.astro +14 -0
- package/src/tool/parliamentary-voting-analyzer/bibliography.ts +6 -0
- package/src/tool/parliamentary-voting-analyzer/component.astro +63 -0
- package/src/tool/parliamentary-voting-analyzer/contract.test.ts +16 -0
- package/src/tool/parliamentary-voting-analyzer/controller.ts +65 -0
- package/src/tool/parliamentary-voting-analyzer/dom-views.ts +63 -0
- package/src/tool/parliamentary-voting-analyzer/entry.ts +27 -0
- package/src/tool/parliamentary-voting-analyzer/evaluator.ts +12 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/de.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/en.ts +79 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/es.ts +49 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/fr.ts +43 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/id.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/it.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/ja.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/ko.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/nl.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/pl.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/pt.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/ru.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/sv.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/tr.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/i18n/zh.ts +45 -0
- package/src/tool/parliamentary-voting-analyzer/index.ts +11 -0
- package/src/tool/parliamentary-voting-analyzer/logic.test.ts +52 -0
- package/src/tool/parliamentary-voting-analyzer/logic.ts +279 -0
- package/src/tool/parliamentary-voting-analyzer/parliamentary-voting-analyzer.css +491 -0
- package/src/tool/parliamentary-voting-analyzer/seo.astro +14 -0
- package/src/tool/parliamentary-voting-analyzer/storage.ts +25 -0
- package/src/tool/parliamentary-voting-analyzer/ui.ts +98 -0
- 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 +4 -0
|
@@ -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;
|
|
@@ -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: 'Suelta aquí un CSV o JSON de presupuesto',
|
|
8
|
+
uploadHelp: 'Campos obligatorios: categoría e importe. Opcionales: población, periodo, priorAmount, moneda y categoría padre.',
|
|
9
|
+
loadExample: 'Cargar presupuesto de ejemplo',
|
|
10
|
+
supportedFormat: 'CSV o JSON, procesado solo en este navegador',
|
|
11
|
+
tableHeading: 'Filas de origen y totales por categoría',
|
|
12
|
+
categoryLabel: 'Categoría',
|
|
13
|
+
amountLabel: 'Importe',
|
|
14
|
+
populationLabel: 'Población',
|
|
15
|
+
sourceRowsLabel: 'Filas de origen',
|
|
16
|
+
analyzeEmpty: 'Elige un archivo o carga el ejemplo para revisar un presupuesto.',
|
|
17
|
+
resultHeading: 'Paisaje presupuestario',
|
|
18
|
+
totalLabel: 'Gasto total',
|
|
19
|
+
perCapitaLabel: 'Gasto por habitante',
|
|
20
|
+
variationLabel: 'Cambio frente al periodo anterior',
|
|
21
|
+
topCategoryLabel: 'Categoría mayoritaria',
|
|
22
|
+
shareLabel: 'Parte del total',
|
|
23
|
+
sourceLabel: 'Filas de origen',
|
|
24
|
+
noData: 'Todavía no hay filas presupuestarias válidas disponibles.',
|
|
25
|
+
warningsHeading: 'Advertencias que revisar',
|
|
26
|
+
errorsHeading: 'Filas no incluidas',
|
|
27
|
+
methodHeading: 'Método aplicado',
|
|
28
|
+
methodText: 'La herramienta valida los campos obligatorios, conserva cada fila de origen, agrupa las rutas exactas de categoría y calcula los totales a partir de importes normalizados. La parte de cada categoría es su importe dividido por el total cuando este es positivo. El gasto por habitante divide el total por el primer denominador de población positivo y consistente. El cambio del periodo es la diferencia dividida por el importe anterior absoluto.',
|
|
29
|
+
limitsHeading: 'Lo que esta herramienta no hace',
|
|
30
|
+
limitsText: 'No demuestra que un presupuesto sea completo, comparable, legal, eficiente, justo, imparcial o esté bien clasificado. No audita la fuente, no evalúa la calidad de una política, no convierte monedas ni identifica si una fila es una autorización, obligación, pago o previsión.',
|
|
31
|
+
edgeCasesHeading: 'Casos límite y advertencias de datos',
|
|
32
|
+
edgeCasesText: 'Las monedas mezcladas, poblaciones cambiantes, categorías duplicadas, ajustes negativos, filas padre junto a filas hijas, formatos numéricos locales y los importes anteriores iguales a cero requieren revisión humana. El navegador conserva el número de fila original para rastrear cada agregado hasta su entrada.',
|
|
33
|
+
downloadCsv: 'Descargar tabla revisada',
|
|
34
|
+
printAction: 'Imprimir análisis',
|
|
35
|
+
rowsLabel: 'filas válidas',
|
|
36
|
+
categoriesLabel: 'categorías',
|
|
37
|
+
positiveTotalLabel: 'importes positivos visualizados',
|
|
38
|
+
previousTotalLabel: 'Total anterior',
|
|
39
|
+
invalidFile: 'El archivo no se ha podido entender como una tabla CSV o JSON no vacía.',
|
|
40
|
+
fileReadError: 'El archivo no se ha podido leer en este navegador.',
|
|
41
|
+
exampleName: 'Ejemplo de presupuesto municipal',
|
|
42
|
+
noPreviousData: 'No se ha indicado un importe anterior comparable.',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const softwareApplication: SoftwareApplication = {
|
|
46
|
+
'@type': 'SoftwareApplication',
|
|
47
|
+
name: 'Analizador de presupuesto público',
|
|
48
|
+
applicationCategory: 'EducationalApplication',
|
|
49
|
+
operatingSystem: 'Any',
|
|
50
|
+
description: 'Revisa localmente gastos públicos, partes por categoría, importes por habitante y cambios entre periodos.',
|
|
51
|
+
url: 'https://gamebob.dev/es/analizador-presupuesto-publico-gasto',
|
|
52
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const faqPage: FAQPage = {
|
|
56
|
+
'@type': 'FAQPage',
|
|
57
|
+
mainEntity: [
|
|
58
|
+
{ '@type': 'Question', name: '¿Qué columnas necesita el analizador?', acceptedAnswer: { '@type': 'Answer', text: 'Cada fila necesita una categoría y un importe numérico. Población, periodo, priorAmount, moneda y categoría padre son opcionales.' } },
|
|
59
|
+
{ '@type': 'Question', name: '¿Cómo se calcula el gasto por habitante?', acceptedAnswer: { '@type': 'Answer', text: 'El importe total se divide por el primer valor de población positivo y consistente. Si los denominadores cambian, la herramienta mantiene el cálculo y muestra una advertencia.' } },
|
|
60
|
+
{ '@type': 'Question', name: '¿Qué ocurre con las categorías duplicadas?', acceptedAnswer: { '@type': 'Answer', text: 'Las filas con la misma ruta de categoría se suman y conservan visibles sus números de fila de origen. Así se puede distinguir una división real de una doble contabilización.' } },
|
|
61
|
+
{ '@type': 'Question', name: '¿Puede demostrar que un presupuesto es bueno o justo?', acceptedAnswer: { '@type': 'Answer', text: 'No. Describe los números recibidos, pero no establece su integridad, legalidad, eficiencia, justicia, clasificación ni sus resultados.' } },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const howTo: HowTo = {
|
|
66
|
+
'@type': 'HowTo',
|
|
67
|
+
name: 'Analizar una tabla de presupuesto público',
|
|
68
|
+
step: [
|
|
69
|
+
{ '@type': 'HowToStep', name: 'Preparar una tabla', text: 'Crea una tabla CSV o JSON con una categoría y un importe por fila. Mantén la misma moneda y base contable.' },
|
|
70
|
+
{ '@type': 'HowToStep', name: 'Añadir contexto', text: 'Añade población, periodo, priorAmount, moneda o categoría padre solo cuando su significado sea consistente.' },
|
|
71
|
+
{ '@type': 'HowToStep', name: 'Cargar el archivo', text: 'Suelta el archivo en el analizador o carga el ejemplo para ver la estructura esperada y la validación local.' },
|
|
72
|
+
{ '@type': 'HowToStep', name: 'Leer el paisaje', text: 'Usa las barras proporcionales para la composición y la tabla accesible para importes, cambios y filas de origen exactos.' },
|
|
73
|
+
{ '@type': 'HowToStep', name: 'Revisar advertencias', text: 'Aclara monedas mezcladas, rutas duplicadas, ajustes negativos, denominadores cero y filas padre antes de concluir.' },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const content: PublicBudgetLocaleContent = {
|
|
78
|
+
slug: 'analizador-presupuesto-publico-gasto',
|
|
79
|
+
title: 'Analizador de presupuesto público',
|
|
80
|
+
description: 'Revisa localmente una tabla de presupuesto público para ver gasto total, partes por categoría, gasto por habitante, cambios y las filas de origen detrás de cada agregado.',
|
|
81
|
+
ui,
|
|
82
|
+
seo: [
|
|
83
|
+
{ type: 'title', text: 'Convierte una tabla de presupuesto público en una vista auditable', level: 2 },
|
|
84
|
+
{ type: 'paragraph', html: 'Los archivos de presupuesto público suelen mezclar etiquetas de categoría, periodos contables, denominadores de población y ajustes en una tabla difícil de leer de un vistazo. Este analizador conserva las filas de origen, valida los campos necesarios y convierte los mismos datos normalizados en totales, proporciones, gasto por habitante y cambios entre periodos.' },
|
|
85
|
+
{ type: 'paragraph', html: 'El resultado es descriptivo, no un juicio político. Una categoría grande puede reflejar una competencia legal, una inversión excepcional, una transferencia o un límite contable distinto. La herramienta hace visibles esos patrones para que puedas volver a la fuente con una pregunta mejor definida.' },
|
|
86
|
+
{ type: 'title', text: 'Qué significan los cálculos', level: 2 },
|
|
87
|
+
{ type: 'paragraph', html: 'El total suma todos los importes numéricos válidos, incluidos los ajustes negativos. La parte de una categoría es su importe agrupado dividido por el total cuando este es positivo. Los importes positivos reciben barras proporcionales; los negativos permanecen en la tabla y se señalan porque representar su área como positiva cambiaría su significado.' },
|
|
88
|
+
{ type: 'table', headers: ['Resultado', 'Cálculo', 'Pregunta de revisión'], rows: [['Gasto total', 'Suma de importes normalizados', '¿Todas las filas usan la misma base contable?'], ['Parte de categoría', 'Importe de categoría dividido por el total positivo', '¿Las categorías son excluyentes o una categoría padre contiene a sus hijas?'], ['Gasto por habitante', 'Total dividido por un denominador de población positivo', '¿La población describe el mismo lugar y periodo?'], ['Cambio del periodo', 'Importe actual menos anterior dividido por el valor anterior absoluto', '¿Los periodos son comparables y se evita una base cero?']] },
|
|
89
|
+
{ type: 'title', text: 'Preparar datos comparables', level: 2 },
|
|
90
|
+
{ type: 'paragraph', html: 'Usa una sola moneda y una definición explícita del importe en cada fila. Una autorización presupuestaria, una obligación, un pago y una previsión pueden ser cifras válidas y responder a preguntas distintas. Anota ese significado junto a la fuente porque el analizador no lo deduce del nombre de una columna.' },
|
|
91
|
+
{ type: 'list', items: ['Usa una fila por categoría o indica una categoría padre para crear una jerarquía deliberada.', 'Mantén coherentes los valores de población y no mezcles residentes, electores, hogares o usuarios del servicio.', 'Incluye priorAmount solo cuando el periodo anterior use las mismas categorías y base contable.', 'Comprueba las rutas duplicadas y las filas padre para evitar dobles conteos accidentales.', 'Conserva la tabla revisada junto con el archivo original y las hipótesis usadas.'] },
|
|
92
|
+
{ type: 'tip', title: 'Una proporción no es una prioridad', html: 'La categoría con mayor importe no es automáticamente la más importante, derrochadora o exitosa. Usa la vista para localizar escala y cambios, y lee después las notas de la fuente y los resultados del servicio.' },
|
|
93
|
+
{ type: 'title', text: 'Leer las advertencias antes de concluir', level: 2 },
|
|
94
|
+
{ type: 'paragraph', html: 'Las rutas duplicadas se agrupan para que el agregado no quede infravalorado sin avisar, pero la lista de filas de origen permite comprobar si la agrupación era intencionada. Las monedas mezcladas nunca se convierten. Una población cero elimina el gasto por habitante y un importe anterior cero elimina el porcentaje de cambio porque dividir entre cero no tiene una interpretación útil.' },
|
|
95
|
+
{ type: 'paragraph', html: 'Las categorías anidadas requieren especial cuidado. Si una tabla contiene Educación y Educación > Escuelas como importes, sumarlas puede contar dos veces el mismo gasto. La herramienta avisa de las rutas anidadas, pero solo la metodología de la fuente puede decir si la categoría padre es un subtotal o una partida independiente.' },
|
|
96
|
+
{ type: 'title', text: 'Qué hacer después del primer análisis', level: 2 },
|
|
97
|
+
{ type: 'paragraph', html: 'Usa los números de fila para volver al archivo oficial y documentar cada corrección o interpretación. Compara datos equivalentes entre periodos, conserva los ajustes negativos como ajustes y vuelve a ejecutar el análisis después de eliminar subtotales cuando la fuente confirme que ya están incluidos.' },
|
|
98
|
+
{ type: 'tip', title: 'Mantén visible el límite de los datos', html: 'Esto es un análisis descriptivo local. No verifica que el archivo esté completo, no clasifica el gasto según una norma oficial, no reconcilia monedas ni demuestra una relación causal entre gasto y resultados.' },
|
|
99
|
+
],
|
|
100
|
+
faq: [
|
|
101
|
+
{ question: '¿Qué columnas necesita el analizador?', answer: 'Cada fila necesita categoría e importe numérico. Población, periodo, priorAmount, moneda y categoría padre son opcionales.' },
|
|
102
|
+
{ question: '¿Cómo se calcula el gasto por habitante?', answer: 'El total se divide por el primer valor de población positivo y consistente. Los denominadores distintos generan una advertencia.' },
|
|
103
|
+
{ question: '¿Qué ocurre con las categorías duplicadas?', answer: 'Las mismas rutas de categoría se suman y sus filas de origen quedan visibles para revisarlas.' },
|
|
104
|
+
{ question: '¿Puede demostrar que un presupuesto es bueno o justo?', answer: 'No. Describe los números suministrados y no puede establecer integridad, legalidad, eficiencia, justicia ni resultados.' },
|
|
105
|
+
],
|
|
106
|
+
bibliography,
|
|
107
|
+
howTo: [
|
|
108
|
+
{ name: 'Preparar una tabla', text: 'Crea una tabla CSV o JSON con categoría e importe por fila, usando la misma moneda y base contable.' },
|
|
109
|
+
{ name: 'Añadir contexto', text: 'Añade población, periodo, priorAmount, moneda o categoría padre solo cuando mantengan un significado consistente.' },
|
|
110
|
+
{ name: 'Cargar el archivo', text: 'Suelta el archivo o carga el ejemplo para ver la estructura esperada.' },
|
|
111
|
+
{ name: 'Leer el paisaje', text: 'Usa las barras proporcionales para la composición y la tabla para valores, cambios y filas de origen.' },
|
|
112
|
+
{ name: 'Revisar advertencias', text: 'Aclara monedas mezcladas, duplicados, ajustes negativos, denominadores cero y filas padre antes de concluir.' },
|
|
113
|
+
],
|
|
114
|
+
schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[],
|
|
115
|
+
};
|