Budget: - Modelle + Migration 0003: budget_template_lines, planned_income_lines, budget_periods, budget_period_lines. - services/budget: Perioden-Grenzen (einstellbarer Starttag, Label nach Endmonat), Standard-Vorlage vs. Monats-Override (Snapshot-on-Customize), Soll/Ist inkl. budgetierter Sparen-Transfers, geplante Einnahmen (Soll/Ist). - routers/budgets: Vorlage, geplante Einnahmen (CRUD), Monats-Soll/Ist, Monats-Override, Zurücksetzen. pytest: Perioden, Soll/Ist, Override/Reset, Einnahmen. - Frontend: Budget-Seite (Monat + Standard-Budget), Fortschrittsbalken grün/rot, Monats-Navigation, editierbare Soll-Beträge; Sparen-Option im Transfer-Formular. Deployment-Fix (Nutzer-Anforderung: http://IP:Port muss funktionieren): - Dienst bindet an APP_BIND_HOST=0.0.0.0 (direkt im LAN erreichbar). - APP_COOKIE_SECURE=false als Standard (sonst kein Login über reines HTTP); für reinen HTTPS-Betrieb umstellbar. Doku + install.sh entsprechend angepasst. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
2.4 KiB
JavaScript
68 lines
2.4 KiB
JavaScript
// Formatierung von Beträgen, Zahlen und Daten – währungs-/gebietsspezifisch.
|
||
// EUR im deutschen Format (1.234,56 €), CHF im Schweizer Format (CHF 1'234.56).
|
||
// Datum als TT.MM.JJJJ. Beträge liegen intern als Ganzzahl in Cent/Rappen vor.
|
||
|
||
export function moneyLocale(currency) {
|
||
return currency === "CHF" ? "de-CH" : "de-DE";
|
||
}
|
||
|
||
/** Formatiert einen Cent-Betrag als Währungstext (z. B. 123456 -> "1.234,56 €"). */
|
||
export function formatMoney(cents, currency = "EUR") {
|
||
const value = (Number(cents) || 0) / 100;
|
||
return new Intl.NumberFormat(moneyLocale(currency), {
|
||
style: "currency",
|
||
currency,
|
||
}).format(value);
|
||
}
|
||
|
||
/** Formatiert einen Cent-Betrag ohne Währungssymbol (für Eingaben/Tabellen). */
|
||
export function formatAmount(cents, currency = "EUR") {
|
||
const value = (Number(cents) || 0) / 100;
|
||
return new Intl.NumberFormat(moneyLocale(currency), {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
}).format(value);
|
||
}
|
||
|
||
/** ISO-Datum (YYYY-MM-DD oder ISO-Zeitstempel) als TT.MM.JJJJ. */
|
||
export function formatDate(iso, locale = "de") {
|
||
if (!iso) return "";
|
||
const d = new Date(String(iso).length <= 10 ? `${iso}T00:00:00` : iso);
|
||
if (Number.isNaN(d.getTime())) return String(iso);
|
||
return new Intl.DateTimeFormat(locale === "en" ? "en-GB" : "de-DE").format(d);
|
||
}
|
||
|
||
/** Eingabetext ("12,34" oder "12.34") in Ganzzahl-Cent; null bei leer/ungültig. */
|
||
export function parseAmountToCents(str) {
|
||
if (str == null) return null;
|
||
const cleaned = String(str).trim().replace(/\s/g, "").replace(",", ".");
|
||
if (cleaned === "") return null;
|
||
const value = Number.parseFloat(cleaned);
|
||
if (Number.isNaN(value)) return null;
|
||
return Math.round(value * 100);
|
||
}
|
||
|
||
/** Cent-Betrag als bearbeitbarer Eingabetext ("1234" -> "12,34"). */
|
||
export function centsToInput(cents) {
|
||
if (cents == null) return "";
|
||
return (cents / 100).toFixed(2).replace(".", ",");
|
||
}
|
||
|
||
/** Heutiges Datum als YYYY-MM-DD (lokal). */
|
||
export function todayIso() {
|
||
const d = new Date();
|
||
const z = (n) => String(n).padStart(2, "0");
|
||
return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}`;
|
||
}
|
||
|
||
/** Monats-Label "2026-09" -> "September 2026". */
|
||
export function formatMonthLabel(label, locale = "de") {
|
||
if (!label) return "";
|
||
const [y, m] = label.split("-").map(Number);
|
||
const d = new Date(y, (m || 1) - 1, 1);
|
||
return new Intl.DateTimeFormat(locale === "en" ? "en-GB" : "de-DE", {
|
||
month: "long",
|
||
year: "numeric",
|
||
}).format(d);
|
||
}
|