update
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
118
frontend/src/components/shared/GermanDateField.tsx
Normal file
118
frontend/src/components/shared/GermanDateField.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user