This commit is contained in:
Matthias Hochmeister
2026-03-26 12:12:18 +01:00
parent d351ea2647
commit 507111e8e8
12 changed files with 202 additions and 131 deletions

View File

@@ -25,6 +25,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import GermanDateField from '../shared/GermanDateField';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { bannerApi } from '../../services/banners'; import { bannerApi } from '../../services/banners';
import { useNotification } from '../../contexts/NotificationContext'; import { useNotification } from '../../contexts/NotificationContext';
@@ -228,14 +229,13 @@ function BannerManagementTab() {
<MenuItem value="widget">Widget</MenuItem> <MenuItem value="widget">Widget</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
<TextField <GermanDateField
mode="datetime"
margin="dense" margin="dense"
label="Ablaufdatum (optional)" label="Ablaufdatum (optional)"
type="datetime-local"
fullWidth fullWidth
value={newEndsAt} value={newEndsAt}
onChange={(e) => setNewEndsAt(e.target.value)} onChange={(v) => setNewEndsAt(v)}
InputLabelProps={{ shrink: true }}
helperText="Leer lassen für kein Ablaufdatum" helperText="Leer lassen für kein Ablaufdatum"
/> />
</DialogContent> </DialogContent>

View File

@@ -4,6 +4,7 @@ import {
TextField, Button, Alert, CircularProgress, Chip, TextField, Button, Alert, CircularProgress, Chip,
} from '@mui/material'; } from '@mui/material';
import BuildIcon from '@mui/icons-material/Build'; import BuildIcon from '@mui/icons-material/Build';
import GermanDateField from '../shared/GermanDateField';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { settingsApi } from '../../services/settings'; import { settingsApi } from '../../services/settings';
import { permissionsApi } from '../../services/permissions'; import { permissionsApi } from '../../services/permissions';
@@ -113,15 +114,14 @@ export default function ServiceModeTab() {
helperText="Diese Nachricht sehen Benutzer auf der Wartungsseite." helperText="Diese Nachricht sehen Benutzer auf der Wartungsseite."
/> />
<TextField <GermanDateField
mode="datetime"
fullWidth fullWidth
label="Automatisch deaktivieren am" label="Automatisch deaktivieren am"
type="datetime-local"
value={endsAt} value={endsAt}
onChange={(e) => setEndsAt(e.target.value)} onChange={(v) => setEndsAt(v)}
InputLabelProps={{ shrink: true }}
helperText="Optional: Wartungsmodus wird automatisch zu diesem Zeitpunkt deaktiviert." helperText="Optional: Wartungsmodus wird automatisch zu diesem Zeitpunkt deaktiviert."
sx={{ mb: 3, '& input': { color: 'text.primary' } }} sx={{ mb: 3 }}
/> />
<Button <Button

View File

@@ -14,6 +14,7 @@ import {
SelectChangeEvent, SelectChangeEvent,
} from '@mui/material'; } from '@mui/material';
import { DirectionsCar } from '@mui/icons-material'; import { DirectionsCar } from '@mui/icons-material';
import GermanDateField from '../shared/GermanDateField';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { bookingApi, fetchVehicles } from '../../services/bookings'; import { bookingApi, fetchVehicles } from '../../services/bookings';
import type { CreateBuchungInput } from '../../types/booking.types'; import type { CreateBuchungInput } from '../../types/booking.types';
@@ -141,28 +142,24 @@ const VehicleBookingQuickAddWidget: React.FC = () => {
inputProps={{ maxLength: 250 }} inputProps={{ maxLength: 250 }}
/> />
<TextField <GermanDateField
mode="datetime"
fullWidth fullWidth
size="small" size="small"
label="Beginn" label="Beginn"
type="datetime-local"
value={beginn} value={beginn}
onChange={(e) => setBeginn(e.target.value)} onChange={(v) => setBeginn(v)}
required required
InputLabelProps={{ shrink: true }}
sx={{ '& input': { color: 'text.primary' } }}
/> />
<TextField <GermanDateField
mode="datetime"
fullWidth fullWidth
size="small" size="small"
label="Ende" label="Ende"
type="datetime-local"
value={ende} value={ende}
onChange={(e) => setEnde(e.target.value)} onChange={(v) => setEnde(v)}
required required
InputLabelProps={{ shrink: true }}
sx={{ '& input': { color: 'text.primary' } }}
/> />
<TextField <TextField

View File

@@ -14,6 +14,7 @@ import {
SelectChangeEvent, SelectChangeEvent,
} from '@mui/material'; } from '@mui/material';
import { AddTask } from '@mui/icons-material'; import { AddTask } from '@mui/icons-material';
import GermanDateField from '../shared/GermanDateField';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { vikunjaApi } from '../../services/vikunja'; import { vikunjaApi } from '../../services/vikunja';
import { useNotification } from '../../contexts/NotificationContext'; import { useNotification } from '../../contexts/NotificationContext';
@@ -126,14 +127,13 @@ const VikunjaQuickAddWidget: React.FC = () => {
inputProps={{ maxLength: 250 }} inputProps={{ maxLength: 250 }}
/> />
<TextField <GermanDateField
mode="date"
fullWidth fullWidth
size="small" size="small"
label="Fälligkeitsdatum (optional)" label="Fälligkeitsdatum (optional)"
type="date"
value={dueDate} value={dueDate}
onChange={(e) => setDueDate(e.target.value)} onChange={(v) => setDueDate(v)}
InputLabelProps={{ shrink: true }}
/> />
<Button <Button

View File

@@ -0,0 +1,118 @@
import { useState, useRef } from 'react';
import TextField, { type TextFieldProps } from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import IconButton from '@mui/material/IconButton';
import Box from '@mui/material/Box';
import CalendarMonth from '@mui/icons-material/CalendarMonth';
import {
toGermanDate,
toGermanDateTime,
fromGermanDate,
fromGermanDateTime,
} from '../../utils/dateInput';
export type GermanDateMode = 'date' | 'datetime';
export interface GermanDateFieldProps extends Omit<TextFieldProps, 'value' | 'onChange' | 'type'> {
/** ISO date (YYYY-MM-DD) or datetime (YYYY-MM-DDTHH:MM) string */
value?: string | null;
/** Called with ISO date/datetime string whenever value changes to a valid date */
onChange?: (isoValue: string) => void;
mode?: GermanDateMode;
}
/**
* A MUI TextField that:
* - Displays dates in German format (TT.MM.JJJJ / TT.MM.JJJJ HH:MM)
* - Accepts free-text entry in that format
* - Has a calendar icon that opens the native browser date picker as a popup
* - onChange fires with an ISO string whenever the entered value is valid
*/
export default function GermanDateField({
value,
onChange,
mode = 'date',
sx,
...rest
}: GermanDateFieldProps) {
// localValue overrides the displayed text while the user is typing
const [localValue, setLocalValue] = useState<string | null>(null);
const hiddenRef = useRef<HTMLInputElement>(null);
const displayed =
localValue ?? (mode === 'date' ? toGermanDate(value) : toGermanDateTime(value));
function handleTextChange(raw: string) {
setLocalValue(raw);
const iso = mode === 'date' ? fromGermanDate(raw) : fromGermanDateTime(raw);
if (iso) {
onChange?.(iso);
setLocalValue(null);
}
}
function handleBlur() {
// Revert to the last valid formatted value if the text is incomplete
setLocalValue(null);
}
function handlePickerChange(nativeVal: string) {
if (!nativeVal) return;
if (mode === 'date') {
onChange?.(nativeVal);
} else {
// Preserve current time portion if available, default to 08:00
const time = value?.substring(11, 16) || '08:00';
onChange?.(`${nativeVal}T${time}`);
}
}
const nativeValue =
mode === 'date' ? (value?.substring(0, 10) || '') : (value?.substring(0, 16) || '');
return (
<Box sx={{ position: 'relative', display: 'inline-flex', width: rest.fullWidth ? '100%' : undefined, ...sx }}>
<TextField
{...rest}
sx={{ width: '100%' }}
value={displayed}
onChange={(e) => handleTextChange(e.target.value)}
onBlur={handleBlur}
placeholder={mode === 'date' ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
InputLabelProps={{ shrink: true, ...rest.InputLabelProps }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
size="small"
tabIndex={-1}
edge="end"
onClick={() => hiddenRef.current?.showPicker?.()}
>
<CalendarMonth fontSize="small" />
</IconButton>
</InputAdornment>
),
...rest.InputProps,
}}
/>
{/* Hidden native input used solely to open the browser's date picker popup */}
<input
ref={hiddenRef}
type={mode === 'date' ? 'date' : 'datetime-local'}
value={nativeValue}
onChange={(e) => handlePickerChange(e.target.value)}
tabIndex={-1}
style={{
position: 'absolute',
bottom: 0,
left: 0,
opacity: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
}}
/>
</Box>
);
}

View File

@@ -47,6 +47,7 @@ import {
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import GermanDateField from '../components/shared/GermanDateField';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext'; import { usePermissionContext } from '../contexts/PermissionContext';
import { bestellungApi } from '../services/bestellung'; import { bestellungApi } from '../services/bestellung';
@@ -417,11 +418,11 @@ export default function BestellungDetail() {
curY += 3; curY += 3;
} }
// ── Besteller block ── // ── Kontaktperson (Besteller) block ──
if (bestellung.besteller_name) { if (bestellung.besteller_name) {
doc.setFontSize(10); doc.setFontSize(10);
doc.setFont('helvetica', 'bold'); doc.setFont('helvetica', 'bold');
doc.text('Besteller', 10, curY); doc.text('Kontaktperson', 10, curY);
curY += 5; curY += 5;
const nameWithRank = bestellung.besteller_dienstgrad const nameWithRank = bestellung.besteller_dienstgrad
? `${kurzDienstgrad(bestellung.besteller_dienstgrad)} ${bestellung.besteller_name}` ? `${kurzDienstgrad(bestellung.besteller_dienstgrad)} ${bestellung.besteller_name}`
@@ -1012,13 +1013,11 @@ export default function BestellungDetail() {
{/* Inline Add Reminder Form */} {/* Inline Add Reminder Form */}
{reminderFormOpen && ( {reminderFormOpen && (
<Box sx={{ display: 'flex', gap: 1, mt: 2, alignItems: 'flex-end' }}> <Box sx={{ display: 'flex', gap: 1, mt: 2, alignItems: 'flex-end' }}>
<TextField <GermanDateField
size="small" size="small"
type="date"
label="Fällig am" label="Fällig am"
InputLabelProps={{ shrink: true }}
value={reminderForm.faellig_am} value={reminderForm.faellig_am}
onChange={(e) => setReminderForm((f) => ({ ...f, faellig_am: e.target.value }))} onChange={(iso) => setReminderForm((f) => ({ ...f, faellig_am: iso }))}
/> />
<TextField <TextField
size="small" size="small"

View File

@@ -31,6 +31,7 @@ import { useQuery } from '@tanstack/react-query';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import ServiceModePage from '../components/shared/ServiceModePage'; import ServiceModePage from '../components/shared/ServiceModePage';
import GermanDateField from '../components/shared/GermanDateField';
import { usePermissionContext } from '../contexts/PermissionContext'; import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
import { bookingApi, fetchVehicles, kategorieApi } from '../services/bookings'; import { bookingApi, fetchVehicles, kategorieApi } from '../services/bookings';
@@ -286,51 +287,49 @@ function BookingFormPage() {
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <GermanDateField
fullWidth fullWidth
size="small" size="small"
label="Beginn" label="Beginn"
type={form.ganztaegig ? 'date' : 'datetime-local'}
required required
mode={form.ganztaegig ? 'date' : 'datetime'}
value={ value={
form.ganztaegig form.ganztaegig
? form.beginn?.split('T')[0] || '' ? form.beginn?.split('T')[0] || ''
: form.beginn : form.beginn
} }
onChange={(e) => { onChange={(iso) => {
if (form.ganztaegig) { if (form.ganztaegig) {
setForm((f) => ({ setForm((f) => ({
...f, ...f,
beginn: `${e.target.value}T00:00`, beginn: `${iso}T00:00`,
})); }));
} else { } else {
setForm((f) => ({ ...f, beginn: e.target.value })); setForm((f) => ({ ...f, beginn: iso }));
} }
}} }}
InputLabelProps={{ shrink: true }}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <GermanDateField
fullWidth fullWidth
size="small" size="small"
label="Ende" label="Ende"
type={form.ganztaegig ? 'date' : 'datetime-local'}
required required
mode={form.ganztaegig ? 'date' : 'datetime'}
value={ value={
form.ganztaegig ? form.ende?.split('T')[0] || '' : form.ende form.ganztaegig ? form.ende?.split('T')[0] || '' : form.ende
} }
onChange={(e) => { onChange={(iso) => {
if (form.ganztaegig) { if (form.ganztaegig) {
setForm((f) => ({ setForm((f) => ({
...f, ...f,
ende: `${e.target.value}T23:59`, ende: `${iso}T23:59`,
})); }));
} else { } else {
setForm((f) => ({ ...f, ende: e.target.value })); setForm((f) => ({ ...f, ende: iso }));
} }
}} }}
InputLabelProps={{ shrink: true }}
/> />
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -48,6 +48,7 @@ import { useNavigate } from 'react-router-dom';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab'; import ChatAwareFab from '../components/shared/ChatAwareFab';
import GermanDateField from '../components/shared/GermanDateField';
import ServiceModePage from '../components/shared/ServiceModePage'; import ServiceModePage from '../components/shared/ServiceModePage';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext'; import { usePermissionContext } from '../contexts/PermissionContext';
@@ -262,22 +263,18 @@ function FahrzeugBuchungen() {
{/* Filters */} {/* Filters */}
<Paper sx={{ p: 2, mb: 2 }}> <Paper sx={{ p: 2, mb: 2 }}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems="center"> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems="center">
<TextField <GermanDateField
size="small" size="small"
label="Von" label="Von"
type="date"
value={filterFrom} value={filterFrom}
onChange={(e) => setFilterFrom(e.target.value)} onChange={(iso) => setFilterFrom(iso)}
InputLabelProps={{ shrink: true }}
sx={{ minWidth: 160 }} sx={{ minWidth: 160 }}
/> />
<TextField <GermanDateField
size="small" size="small"
label="Bis" label="Bis"
type="date"
value={filterTo} value={filterTo}
onChange={(e) => setFilterTo(e.target.value)} onChange={(iso) => setFilterTo(iso)}
InputLabelProps={{ shrink: true }}
sx={{ minWidth: 160 }} sx={{ minWidth: 160 }}
/> />
<FormControl size="small" sx={{ minWidth: 180 }}> <FormControl size="small" sx={{ minWidth: 180 }}>

View File

@@ -57,6 +57,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab'; import ChatAwareFab from '../components/shared/ChatAwareFab';
import { vehiclesApi } from '../services/vehicles'; import { vehiclesApi } from '../services/vehicles';
import GermanDateField from '../components/shared/GermanDateField';
import { fromGermanDate } from '../utils/dateInput'; import { fromGermanDate } from '../utils/dateInput';
import { equipmentApi } from '../services/equipment'; import { equipmentApi } from '../services/equipment';
import { import {
@@ -404,23 +405,21 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
{isAusserDienst(newStatus) && ( {isAusserDienst(newStatus) && (
<> <>
<TextField <GermanDateField
label="Außer Dienst von *" label="Außer Dienst von *"
type="datetime-local" mode="datetime"
fullWidth fullWidth
sx={{ mb: 2 }} sx={{ mb: 2 }}
value={ausserDienstVon} value={ausserDienstVon}
onChange={(e) => setAusserDienstVon(e.target.value)} onChange={(iso) => setAusserDienstVon(iso)}
InputLabelProps={{ shrink: true }}
/> />
<TextField <GermanDateField
label="Geschätztes Ende *" label="Geschätztes Ende *"
type="datetime-local" mode="datetime"
fullWidth fullWidth
sx={{ mb: 1 }} sx={{ mb: 1 }}
value={ausserDienstBis} value={ausserDienstBis}
onChange={(e) => setAusserDienstBis(e.target.value)} onChange={(iso) => setAusserDienstBis(iso)}
InputLabelProps={{ shrink: true }}
/> />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2 }}> <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
Zeitangabe ist eine Schätzung Zeitangabe ist eine Schätzung
@@ -717,13 +716,12 @@ const WartungTab: React.FC<WartungTabProps> = ({ fahrzeugId, wartungslog, onAdde
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <GermanDateField
label="Nächste Fälligkeit" label="Nächste Fälligkeit"
type="date" mode="date"
fullWidth fullWidth
value={form.naechste_faelligkeit ?? ''} value={form.naechste_faelligkeit || null}
onChange={(e) => setForm((f) => ({ ...f, naechste_faelligkeit: e.target.value }))} onChange={(iso) => setForm((f) => ({ ...f, naechste_faelligkeit: iso }))}
InputLabelProps={{ shrink: true }}
/> />
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -13,6 +13,7 @@ import {
import { ArrowBack, Save } from '@mui/icons-material'; import { ArrowBack, Save } from '@mui/icons-material';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import GermanDateField from '../components/shared/GermanDateField';
import { vehiclesApi } from '../services/vehicles'; import { vehiclesApi } from '../services/vehicles';
import { import {
CreateFahrzeugPayload, CreateFahrzeugPayload,
@@ -272,24 +273,20 @@ function FahrzeugForm() {
<Typography variant="h6" gutterBottom sx={{ mt: 3 }}>Prüf- und Wartungsfristen</Typography> <Typography variant="h6" gutterBottom sx={{ mt: 3 }}>Prüf- und Wartungsfristen</Typography>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <GermanDateField
label="§57a fällig am" label="§57a fällig am"
fullWidth fullWidth
type="date"
value={form.paragraph57a_faellig_am} value={form.paragraph57a_faellig_am}
onChange={(e) => setForm((prev) => ({ ...prev, paragraph57a_faellig_am: e.target.value }))} onChange={(iso) => setForm((prev) => ({ ...prev, paragraph57a_faellig_am: iso }))}
InputLabelProps={{ shrink: true }}
helperText="Periodische Begutachtung (§57a StVO)" helperText="Periodische Begutachtung (§57a StVO)"
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <GermanDateField
label="Nächste Wartung am" label="Nächste Wartung am"
fullWidth fullWidth
type="date"
value={form.naechste_wartung_am} value={form.naechste_wartung_am}
onChange={(e) => setForm((prev) => ({ ...prev, naechste_wartung_am: e.target.value }))} onChange={(iso) => setForm((prev) => ({ ...prev, naechste_wartung_am: iso }))}
InputLabelProps={{ shrink: true }}
helperText="Nächster geplanter Servicetermin" helperText="Nächster geplanter Servicetermin"
/> />
</Grid> </Grid>

View File

@@ -70,6 +70,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import ServiceModePage from '../components/shared/ServiceModePage'; import ServiceModePage from '../components/shared/ServiceModePage';
import ChatAwareFab from '../components/shared/ChatAwareFab'; import ChatAwareFab from '../components/shared/ChatAwareFab';
import GermanDateField from '../components/shared/GermanDateField';
import { toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput'; import { toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput';
import { usePermissionContext } from '../contexts/PermissionContext'; import { usePermissionContext } from '../contexts/PermissionContext';
@@ -185,24 +186,6 @@ function formatDateLong(d: Date): string {
return `${days[d.getDay()]}, ${d.getDate()}. ${MONTH_LABELS[d.getMonth()]} ${d.getFullYear()}`; return `${days[d.getDay()]}, ${d.getDate()}. ${MONTH_LABELS[d.getMonth()]} ${d.getFullYear()}`;
} }
/** ISO string → YYYY-MM-DDTHH:MM (for type="datetime-local") */
function toDatetimeLocalValue(iso: string | null | undefined): string {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return '';
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
/** ISO string → YYYY-MM-DD (for type="date") */
function toDateInputValue(iso: string | null | undefined): string {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return '';
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
// Types for unified calendar // Types for unified calendar
// ────────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────────
@@ -1343,44 +1326,30 @@ function VeranstaltungFormDialog({
} }
label="Ganztägig" label="Ganztägig"
/> />
<TextField <GermanDateField
label="Von" label="Von"
type={form.ganztaegig ? 'date' : 'datetime-local'} mode={form.ganztaegig ? 'date' : 'datetime'}
value={ value={form.datum_von}
form.ganztaegig onChange={(iso) => {
? toDateInputValue(form.datum_von)
: toDatetimeLocalValue(form.datum_von)
}
onChange={(e) => {
const raw = e.target.value;
if (!raw) return;
const d = form.ganztaegig const d = form.ganztaegig
? new Date(raw + 'T00:00:00') ? new Date(iso + 'T00:00:00')
: new Date(raw + ':00'); : new Date(iso);
if (isNaN(d.getTime())) return; if (isNaN(d.getTime())) return;
handleChange('datum_von', d.toISOString()); handleChange('datum_von', d.toISOString());
}} }}
InputLabelProps={{ shrink: true }}
fullWidth fullWidth
/> />
<TextField <GermanDateField
label="Bis" label="Bis"
type={form.ganztaegig ? 'date' : 'datetime-local'} mode={form.ganztaegig ? 'date' : 'datetime'}
value={ value={form.datum_bis}
form.ganztaegig onChange={(iso) => {
? toDateInputValue(form.datum_bis)
: toDatetimeLocalValue(form.datum_bis)
}
onChange={(e) => {
const raw = e.target.value;
if (!raw) return;
const d = form.ganztaegig const d = form.ganztaegig
? new Date(raw + 'T23:59:00') ? new Date(iso + 'T23:59:00')
: new Date(raw + ':00'); : new Date(iso);
if (isNaN(d.getTime())) return; if (isNaN(d.getTime())) return;
handleChange('datum_bis', d.toISOString()); handleChange('datum_bis', d.toISOString());
}} }}
InputLabelProps={{ shrink: true }}
fullWidth fullWidth
/> />
<TextField <TextField
@@ -1525,13 +1494,12 @@ function VeranstaltungFormDialog({
</FormControl> </FormControl>
)} )}
<TextField <GermanDateField
label="Wiederholungen bis" label="Wiederholungen bis"
size="small" size="small"
type="date" mode="date"
value={wiederholungBis} value={wiederholungBis}
onChange={(e) => setWiederholungBis(e.target.value)} onChange={(iso) => setWiederholungBis(iso)}
InputLabelProps={{ shrink: true }}
fullWidth fullWidth
disabled={!!editingEvent?.wiederholung_parent_id} disabled={!!editingEvent?.wiederholung_parent_id}
helperText="Letztes Datum für Wiederholungen" helperText="Letztes Datum für Wiederholungen"
@@ -2333,22 +2301,20 @@ export default function Kalender() {
<> <>
{/* Date range inputs for list view */} {/* Date range inputs for list view */}
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}> <Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
<TextField <GermanDateField
type="date"
label="Von" label="Von"
size="small" size="small"
mode="date"
value={listFrom} value={listFrom}
onChange={(e) => setListFrom(e.target.value)} onChange={(iso) => setListFrom(iso)}
InputLabelProps={{ shrink: true }}
sx={{ width: 170 }} sx={{ width: 170 }}
/> />
<TextField <GermanDateField
type="date"
label="Bis" label="Bis"
size="small" size="small"
mode="date"
value={listTo} value={listTo}
onChange={(e) => setListTo(e.target.value)} onChange={(iso) => setListTo(iso)}
InputLabelProps={{ shrink: true }}
sx={{ width: 170 }} sx={{ width: 170 }}
/> />
</Box> </Box>

View File

@@ -52,6 +52,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout'; import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab'; import ChatAwareFab from '../components/shared/ChatAwareFab';
import GermanDateField from '../components/shared/GermanDateField';
import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput'; import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput';
import { usePermissionContext } from '../contexts/PermissionContext'; import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
@@ -1016,16 +1017,15 @@ function EventFormDialog({
</FormControl> </FormControl>
)} )}
<TextField <GermanDateField
label="Wiederholen bis" label="Wiederholen bis"
type="date"
size="small" size="small"
mode="date"
value={form.wiederholung.bis} value={form.wiederholung.bis}
onChange={(e) => { onChange={(iso) => {
const w = { ...form.wiederholung!, bis: e.target.value }; const w = { ...form.wiederholung!, bis: iso };
handleChange('wiederholung', w); handleChange('wiederholung', w);
}} }}
InputLabelProps={{ shrink: true }}
fullWidth fullWidth
helperText="Enddatum der Wiederholungsserie" helperText="Enddatum der Wiederholungsserie"
/> />