v1.32: deutsches Datumsformat in allen Modulen (fmtDatum, ISO-Normalisierung)

This commit is contained in:
2026-10-04 18:51:00 +02:00
parent 18a35dbac9
commit 7616af73cf
11 changed files with 55 additions and 16 deletions
+7 -2
View File
@@ -211,6 +211,11 @@ const mapBeleg = (b) => ({
const deutschesDatum = () => new Date().toLocaleDateString('de-DE')
const deutschesDatumZeit = () => new Date().toLocaleString('de-DE')
/** ISO-Eingaben (YYYY-MM-DD, z. B. aus date-Feldern) in das deutsche Format wandeln. */
const datumDeutsch = (s) => {
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s || '').trim())
return iso ? `${iso[3]}.${iso[2]}.${iso[1]}` : String(s || '').trim()
}
// ── Health ───────────────────────────────────────────────────
app.get('/api/healthz', async (_req, res) => {
@@ -566,7 +571,7 @@ app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
if (!ZAEHLER_ARTEN.includes(art)) return res.status(400).json({ fehler: 'Ungültige Zählerart.' })
const standZahl = Number(stand)
if (!Number.isFinite(standZahl) || standZahl < 0) return res.status(400).json({ fehler: 'Ungültiger Zählerstand.' })
const datum = String(standAm || '').trim() || deutschesDatum()
const datum = datumDeutsch(standAm) || deutschesDatum()
if (!zaehlerVerlauf.datumGueltig(datum)) return res.status(400).json({ fehler: 'Ungültiges Ablesedatum.' })
// Wohnung angegeben → Haus aus der Wohnung ableiten; sonst muss das Haus im Body stehen
let einheitId = unitId ? String(unitId) : null
@@ -665,7 +670,7 @@ app.post('/api/meters/:id/readings', auth, async (req, res, next) => {
const { stand, standAm } = req.body || {}
const standZahl = Number(stand)
if (!Number.isFinite(standZahl) || standZahl < 0) return res.status(400).json({ fehler: 'Ungültiger Zählerstand.' })
const datum = String(standAm || '').trim() || deutschesDatum()
const datum = datumDeutsch(standAm) || deutschesDatum()
if (!zaehlerVerlauf.datumGueltig(datum)) return res.status(400).json({ fehler: 'Ungültiges Ablesedatum.' })
const { eintrag, anomalie, grund } = await zaehlerVerlauf.standEintragen({
meter: m, stand: standZahl, standAm: datum,
+2 -1
View File
@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useStore } from '../store/store'
import { fmtDatum } from '../lib/format'
import type { DocumentItem } from '../types'
/** PDF-Lightbox (v1.31): Inline-Vorschau erzeugter/gespeicherter PDFs mit
@@ -68,7 +69,7 @@ export default function PdfLightbox({ dokument, trigger }: {
<div className="min-w-0">
<DialogTitle className="truncate text-base">{dokument.title}</DialogTitle>
<p className="truncate text-xs text-slate-500 dark:text-slate-400">
{dokument.category} · {dokument.date}
{dokument.category} · {fmtDatum(dokument.date)}
{dokument.fileName && ` · ${dokument.fileName}`}
</p>
</div>
+25
View File
@@ -0,0 +1,25 @@
/** Zentrale Datumsformate (v1.32): Anzeige immer deutsch (TT.MM.JJJJ).
* Ältere Einträge können ISO-Formate (YYYY-MM-DD) enthalten – diese werden
* robust umgerechnet, deutsche Werte unverändert durchgereicht. */
const ISO_DATUM = /^(\d{4})-(\d{2})-(\d{2})$/
const ISO_DATUMZEIT = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?/
/** „2026-10-04" → „04.10.2026"; „04.10.2026" bleibt; unbekannt → unverändert. */
export function fmtDatum(wert: string | null | undefined): string {
if (!wert) return '–'
const iso = ISO_DATUM.exec(wert.trim())
if (iso) return `${iso[3]}.${iso[2]}.${iso[1]}`
return wert
}
/** Wie fmtDatum, aber mit Uhrzeit: ISO-Datetime → „04.10.2026, 15:23". */
export function fmtDatumZeit(wert: string | null | undefined): string {
if (!wert) return '–'
const iso = ISO_DATUMZEIT.exec(wert.trim())
if (iso) {
const sek = iso[6] ? `:${iso[6]}` : ''
return `${iso[3]}.${iso[2]}.${iso[1]}, ${iso[4]}:${iso[5]}${sek}`
}
return fmtDatum(wert)
}
+2 -1
View File
@@ -14,6 +14,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { useStore } from '../store/store'
import { fmtDatumZeit } from '../lib/format'
import type { Benutzer, BenutzerRolle } from '../types'
function Chip({ label, value, tone = 'slate' }: { label: string; value: number | string; tone?: 'slate' | 'amber' | 'teal' | 'blue' }) {
@@ -419,7 +420,7 @@ export default function BenutzerVerwaltung() {
<div className="truncate text-xs text-slate-500">
@{b.benutzername}
{einheit && ` · ${einheit.building}, Whg. ${einheit.number}`}
{b.letzterLogin && ` · zuletzt: ${b.letzterLogin}`}
{b.letzterLogin && ` · zuletzt: ${fmtDatumZeit(b.letzterLogin)}`}
</div>
<div className="mt-1 flex items-center gap-1.5">
<Mail className="h-3.5 w-3.5 shrink-0 text-slate-400" />
+3 -2
View File
@@ -23,6 +23,7 @@ import {
} from 'lucide-react'
import { Link } from 'react-router'
import { useStore, fmtEUR } from '../store/store'
import { fmtDatum } from '../lib/format'
const monthShort = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
@@ -146,7 +147,7 @@ function TenantDashboard() {
<span className="text-xs font-semibold uppercase tracking-wide text-teal-700">
{a.tag}
</span>
<span className="text-xs text-slate-400">{a.date}</span>
<span className="text-xs text-slate-400">{fmtDatum(a.date)}</span>
</div>
<div className="mt-1 text-sm font-medium">{a.title}</div>
<div className="mt-0.5 line-clamp-2 text-xs text-slate-500">{a.text}</div>
@@ -174,7 +175,7 @@ function TenantDashboard() {
<li key={t.id} className="flex items-center justify-between rounded-xl bg-slate-50 px-4 py-3">
<div>
<div className="text-sm font-medium">{t.title}</div>
<div className="text-xs text-slate-400">{t.category} · {t.createdAt}</div>
<div className="text-xs text-slate-400">{t.category} · {fmtDatum(t.createdAt)}</div>
</div>
<span className={`rounded-full px-2.5 py-1 text-xs font-semibold ${
t.status === 'offen' ? 'bg-amber-100 text-amber-800' : 'bg-blue-100 text-blue-800'
+2 -1
View File
@@ -28,6 +28,7 @@ import {
TableRow,
} from '@/components/ui/table'
import { useStore } from '../store/store'
import { fmtDatum } from '../lib/format'
import PdfLightbox from '../components/PdfLightbox'
import { apiUpload, ApiFehler } from '../lib/api'
import type { DocumentItem, Unit } from '../types'
@@ -343,7 +344,7 @@ export default function Documents() {
{d.category}
</Badge>
</TableCell>
<TableCell>{d.date}</TableCell>
<TableCell>{fmtDatum(d.date)}</TableCell>
<TableCell>{d.size}</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
+2 -1
View File
@@ -28,6 +28,7 @@ import {
TableRow,
} from '@/components/ui/table'
import { useStore, fmtEUR } from '../store/store'
import { fmtDatum } from '../lib/format'
import type { Beleg, DocumentItem, KostenItem } from '../types'
import PdfLightbox from '../components/PdfLightbox'
@@ -408,7 +409,7 @@ function BelegeListe() {
{b.notiz && <div className="text-xs font-normal text-slate-400">{b.notiz}</div>}
</TableCell>
<TableCell className="text-slate-500">
{b.datum || '–'}
{b.datum ? fmtDatum(b.datum) : '–'}
{b.jahr && <span className="text-xs text-slate-400"> · {b.jahr}</span>}
</TableCell>
<TableCell className="text-slate-500">{b.kategorie || '–'}</TableCell>
+2 -1
View File
@@ -14,6 +14,7 @@ import {
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { useStore } from '../store/store'
import { fmtDatumZeit } from '../lib/format'
function NewMessageDialog({ onCreated }: { onCreated: (id: string) => void }) {
const { newThread, role, meineEinheit } = useStore()
@@ -185,7 +186,7 @@ export default function Messages() {
)}
<div className="whitespace-pre-wrap">{m.text}</div>
<div className={`mt-1 text-[10px] ${mine ? 'text-teal-100/80' : 'text-slate-400'}`}>
{m.at}
{fmtDatumZeit(m.at)}
</div>
</div>
</div>
+2 -1
View File
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useStore } from '../store/store'
import { fmtDatum } from '../lib/format'
export default function Profil() {
const { me, role, meineEinheit, units, aktualisiereProfil } = useStore()
@@ -88,7 +89,7 @@ export default function Profil() {
</div>
<div>
<dt className="text-xs uppercase tracking-wide text-slate-400">Mitglied seit</dt>
<dd className="font-medium">{me.erstelltAm}</dd>
<dd className="font-medium">{fmtDatum(me.erstelltAm)}</dd>
</div>
</dl>
</div>
+3 -2
View File
@@ -22,6 +22,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { useStore } from '../store/store'
import { fmtDatum, fmtDatumZeit } from '../lib/format'
import type { TicketPriority, TicketStatus } from '../types'
const statusStyle: Record<TicketStatus, string> = {
@@ -214,7 +215,7 @@ export default function Tickets() {
{role === 'admin' && ` · ${t.tenant}`}
</span>
<span className="inline-flex items-center gap-1">
<CalendarDays className="h-3.5 w-3.5" /> {t.createdAt}
<CalendarDays className="h-3.5 w-3.5" /> {fmtDatum(t.createdAt)}
</span>
<span>Kategorie: {t.category}</span>
</div>
@@ -265,7 +266,7 @@ export default function Tickets() {
<div className="mt-2 text-xs text-slate-600">
<span className="font-semibold">{letzte.author}:</span>{' '}
<span className="line-clamp-2">{letzte.text}</span>
<span className="mt-0.5 block text-slate-400">{letzte.at}</span>
<span className="mt-0.5 block text-slate-400">{fmtDatumZeit(letzte.at)}</span>
</div>
)}
</div>
+5 -4
View File
@@ -20,6 +20,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { useStore } from '../store/store'
import { fmtDatum } from '../lib/format'
import type { Meter, MeterReading, ZaehlerArt } from '../types'
const ARTEN: ZaehlerArt[] = ['Strom', 'Wasser', 'Gas', 'Wärme', 'Sonstiges']
@@ -282,7 +283,7 @@ function VerlaufDialog({ meter }: { meter: Meter }) {
</span>
</div>
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400">
Ablesedatum: {r.standAm}
Ablesedatum: {fmtDatum(r.standAm)}
{r.erfasstVon && <span> · erfasst von {r.erfasstVon}</span>}
</div>
{r.anomalie && r.anomalieGrund && (
@@ -317,7 +318,7 @@ function StandEintragenDialog({ meter }: { meter: Meter }) {
setSende(true)
setFehler(null)
setWarnung(null)
const r = await zaehlerstandEintragen(meter.id, standZahl, datum)
const r = await zaehlerstandEintragen(meter.id, standZahl, datum.split('-').reverse().join('.'))
setSende(false)
if (r.fehler) return setFehler(r.fehler)
if (r.anomalie) {
@@ -528,7 +529,7 @@ export default function Zaehler() {
</div>
<div className="flex items-center gap-1.5">
<CalendarDays className="h-3.5 w-3.5 shrink-0" />
Stand: {m.standAm}
Stand: {fmtDatum(m.standAm)}
<span className="text-slate-400 dark:text-slate-500">(gepflegt: {m.geaendertAm})</span>
</div>
</div>
@@ -572,7 +573,7 @@ export default function Zaehler() {
<td className="px-3 py-2.5 text-right font-semibold tabular-nums text-slate-800 dark:text-slate-100">
{fmtStand(m.stand)}
</td>
<td className="hidden px-3 py-2.5 text-slate-500 dark:text-slate-400 lg:table-cell">{m.standAm}</td>
<td className="hidden px-3 py-2.5 text-slate-500 dark:text-slate-400 lg:table-cell">{fmtDatum(m.standAm)}</td>
<td className="px-3 py-2.5">
{m.letzteAnomalie && (
<TriangleAlert