update
This commit is contained in:
@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
|
|||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import bookingService from '../services/booking.service';
|
import bookingService from '../services/booking.service';
|
||||||
import vehicleService from '../services/vehicle.service';
|
import vehicleService from '../services/vehicle.service';
|
||||||
|
import { hasPermission, resolveRequestRole } from '../middleware/rbac.middleware';
|
||||||
import {
|
import {
|
||||||
CreateBuchungSchema,
|
CreateBuchungSchema,
|
||||||
UpdateBuchungSchema,
|
UpdateBuchungSchema,
|
||||||
@@ -87,7 +88,7 @@ class BookingController {
|
|||||||
*/
|
*/
|
||||||
async checkAvailability(req: Request, res: Response): Promise<void> {
|
async checkAvailability(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { fahrzeugId, from, to } = req.query;
|
const { fahrzeugId, from, to, excludeId } = req.query;
|
||||||
if (!fahrzeugId || !from || !to) {
|
if (!fahrzeugId || !from || !to) {
|
||||||
res
|
res
|
||||||
.status(400)
|
.status(400)
|
||||||
@@ -114,7 +115,7 @@ class BookingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasConflict = await bookingService.checkConflict(
|
const hasConflict = await bookingService.checkConflict(
|
||||||
fahrzeugId as string, beginn, ende
|
fahrzeugId as string, beginn, ende, excludeId as string | undefined
|
||||||
);
|
);
|
||||||
res.json({ success: true, data: { available: !hasConflict } });
|
res.json({ success: true, data: { available: !hasConflict } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -204,8 +205,9 @@ class BookingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/bookings/:id
|
* DELETE /api/bookings/:id or PATCH /api/bookings/:id/cancel
|
||||||
* Soft-cancels a booking (sets abgesagt=TRUE).
|
* Soft-cancels a booking (sets abgesagt=TRUE).
|
||||||
|
* Allowed for booking creator or users with bookings:write permission.
|
||||||
*/
|
*/
|
||||||
async cancel(req: Request, res: Response): Promise<void> {
|
async cancel(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -214,6 +216,20 @@ class BookingController {
|
|||||||
res.status(400).json({ success: false, message: 'Ungültige Buchungs-ID' });
|
res.status(400).json({ success: false, message: 'Ungültige Buchungs-ID' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check ownership: creator can always cancel their own booking
|
||||||
|
const booking = await bookingService.getById(id);
|
||||||
|
if (!booking) {
|
||||||
|
res.status(404).json({ success: false, message: 'Buchung nicht gefunden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isOwner = booking.gebucht_von === req.user!.id;
|
||||||
|
const role = resolveRequestRole(req);
|
||||||
|
if (!isOwner && !hasPermission(role, 'bookings:write')) {
|
||||||
|
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = CancelBuchungSchema.safeParse(req.body);
|
const parsed = CancelBuchungSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
handleZodError(res, parsed.error);
|
handleZodError(res, parsed.error);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const PERMISSION_ROLE_MIN: Record<string, AppRole> = {
|
|||||||
function roleFromGroups(groups: string[]): AppRole {
|
function roleFromGroups(groups: string[]): AppRole {
|
||||||
if (groups.includes('dashboard_admin')) return 'admin';
|
if (groups.includes('dashboard_admin')) return 'admin';
|
||||||
if (groups.includes('dashboard_kommando')) return 'kommandant';
|
if (groups.includes('dashboard_kommando')) return 'kommandant';
|
||||||
if (groups.includes('dashboard_gruppenfuehrer') || groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister')) return 'gruppenfuehrer';
|
if (groups.includes('dashboard_gruppenfuehrer') || groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister') || groups.includes('dashboard_chargen')) return 'gruppenfuehrer';
|
||||||
return 'mitglied';
|
return 'mitglied';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface FahrzeugBuchungListItem {
|
|||||||
beginn: Date;
|
beginn: Date;
|
||||||
ende: Date;
|
ende: Date;
|
||||||
abgesagt: boolean;
|
abgesagt: boolean;
|
||||||
|
gebucht_von: string;
|
||||||
gebucht_von_name?: string | null;
|
gebucht_von_name?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ router.get('/calendar-token', authenticate, bookingController.getCalendarToken.b
|
|||||||
router.post('/', authenticate, bookingController.create.bind(bookingController));
|
router.post('/', authenticate, bookingController.create.bind(bookingController));
|
||||||
router.patch('/:id', authenticate, requirePermission('bookings:write'), bookingController.update.bind(bookingController));
|
router.patch('/:id', authenticate, requirePermission('bookings:write'), bookingController.update.bind(bookingController));
|
||||||
|
|
||||||
// Soft-cancel (sets abgesagt=TRUE)
|
// Soft-cancel (sets abgesagt=TRUE) — creator or bookings:write
|
||||||
router.delete('/:id', authenticate, requirePermission('bookings:write'), bookingController.cancel.bind(bookingController));
|
router.delete('/:id', authenticate, bookingController.cancel.bind(bookingController));
|
||||||
router.patch('/:id/cancel', authenticate, requirePermission('bookings:write'), bookingController.cancel.bind(bookingController));
|
router.patch('/:id/cancel', authenticate, bookingController.cancel.bind(bookingController));
|
||||||
|
|
||||||
// Hard-delete (admin only)
|
// Hard-delete (admin only)
|
||||||
router.delete('/:id/force', authenticate, requirePermission('bookings:delete'), bookingController.hardDelete.bind(bookingController));
|
router.delete('/:id/force', authenticate, requirePermission('bookings:delete'), bookingController.hardDelete.bind(bookingController));
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ function rowToListItem(row: any): FahrzeugBuchungListItem {
|
|||||||
beginn: new Date(row.beginn),
|
beginn: new Date(row.beginn),
|
||||||
ende: new Date(row.ende),
|
ende: new Date(row.ende),
|
||||||
abgesagt: row.abgesagt,
|
abgesagt: row.abgesagt,
|
||||||
|
gebucht_von: row.gebucht_von,
|
||||||
gebucht_von_name: row.gebucht_von_name ?? null,
|
gebucht_von_name: row.gebucht_von_name ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,7 @@ class BookingService {
|
|||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
|
b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
|
||||||
b.beginn, b.ende, b.abgesagt,
|
b.beginn, b.ende, b.abgesagt, b.gebucht_von,
|
||||||
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
|
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
|
||||||
u.name AS gebucht_von_name
|
u.name AS gebucht_von_name
|
||||||
FROM fahrzeug_buchungen b
|
FROM fahrzeug_buchungen b
|
||||||
@@ -102,7 +103,7 @@ class BookingService {
|
|||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
|
b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
|
||||||
b.beginn, b.ende, b.abgesagt,
|
b.beginn, b.ende, b.abgesagt, b.gebucht_von,
|
||||||
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
|
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
|
||||||
u.name AS gebucht_von_name
|
u.name AS gebucht_von_name
|
||||||
FROM fahrzeug_buchungen b
|
FROM fahrzeug_buchungen b
|
||||||
|
|||||||
@@ -586,6 +586,7 @@ class EventsService {
|
|||||||
'dashboard_jugend': 'Feuerwehrjugend',
|
'dashboard_jugend': 'Feuerwehrjugend',
|
||||||
'dashboard_kommandant': 'Kommandanten',
|
'dashboard_kommandant': 'Kommandanten',
|
||||||
'dashboard_moderator': 'Moderatoren',
|
'dashboard_moderator': 'Moderatoren',
|
||||||
|
'dashboard_chargen': 'Gruppenkommandanten',
|
||||||
'feuerwehr-admin': 'Feuerwehr Admin',
|
'feuerwehr-admin': 'Feuerwehr Admin',
|
||||||
'feuerwehr-kommandant': 'Feuerwehr Kommandant',
|
'feuerwehr-kommandant': 'Feuerwehr Kommandant',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
|
|||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isOwnMessage && (
|
{!isOwnMessage && !isOneToOne && (
|
||||||
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block' }}>
|
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block' }}>
|
||||||
{message.actorDisplayName}
|
{message.actorDisplayName}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ const ChatMessageView: React.FC = () => {
|
|||||||
}, [selectedRoomToken, chatPanelOpen, queryClient, fetchReactions]);
|
}, [selectedRoomToken, chatPanelOpen, queryClient, fetchReactions]);
|
||||||
|
|
||||||
const room = rooms.find((r) => r.token === selectedRoomToken);
|
const room = rooms.find((r) => r.token === selectedRoomToken);
|
||||||
|
const isOneToOne = room?.type === 1;
|
||||||
|
|
||||||
const sendMutation = useMutation({
|
const sendMutation = useMutation({
|
||||||
mutationFn: ({ message, replyTo }: { message: string; replyTo?: number }) =>
|
mutationFn: ({ message, replyTo }: { message: string; replyTo?: number }) =>
|
||||||
@@ -271,6 +272,7 @@ const ChatMessageView: React.FC = () => {
|
|||||||
key={msg.id}
|
key={msg.id}
|
||||||
message={msg}
|
message={msg}
|
||||||
isOwnMessage={msg.actorType === 'users' && msg.actorId === loginName}
|
isOwnMessage={msg.actorType === 'users' && msg.actorId === loginName}
|
||||||
|
isOneToOne={isOneToOne}
|
||||||
onReplyClick={setReplyToMessage}
|
onReplyClick={setReplyToMessage}
|
||||||
onReactionToggled={handleReactionToggled}
|
onReactionToggled={handleReactionToggled}
|
||||||
reactionsOverride={reactionsMap.get(msg.id)}
|
reactionsOverride={reactionsMap.get(msg.id)}
|
||||||
|
|||||||
@@ -189,7 +189,58 @@ const ChatPanelInner: React.FC = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : selectedRoomToken ? (
|
) : selectedRoomToken ? (
|
||||||
<ChatMessageView />
|
<Box sx={{ display: 'flex', flex: 1, minHeight: 0 }}>
|
||||||
|
{/* Compact room sidebar — hidden on mobile */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'none', sm: 'flex' },
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: 56,
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRight: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
overflow: 'auto',
|
||||||
|
py: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rooms.map((room) => {
|
||||||
|
const isSelected = room.token === selectedRoomToken;
|
||||||
|
return (
|
||||||
|
<Tooltip key={room.token} title={room.displayName} placement="left" arrow>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => selectRoom(room.token)}
|
||||||
|
sx={{
|
||||||
|
mx: 'auto',
|
||||||
|
my: 0.25,
|
||||||
|
p: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="dot"
|
||||||
|
color="primary"
|
||||||
|
invisible={room.unreadMessages === 0}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
bgcolor: isSelected ? 'primary.main' : 'action.hover',
|
||||||
|
color: isSelected ? 'primary.contrastText' : 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{room.displayName.substring(0, 2).toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
|
</Badge>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<ChatMessageView />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<ChatRoomList />
|
<ChatRoomList />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -238,7 +238,8 @@ function FahrzeugBuchungen() {
|
|||||||
.checkAvailability(
|
.checkAvailability(
|
||||||
form.fahrzeugId,
|
form.fahrzeugId,
|
||||||
new Date(form.beginn),
|
new Date(form.beginn),
|
||||||
new Date(form.ende)
|
new Date(form.ende),
|
||||||
|
editingBooking?.id
|
||||||
)
|
)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!cancelled) setAvailability(result);
|
if (!cancelled) setAvailability(result);
|
||||||
@@ -249,7 +250,7 @@ function FahrzeugBuchungen() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [form.fahrzeugId, form.beginn, form.ende]);
|
}, [form.fahrzeugId, form.beginn, form.ende, editingBooking?.id]);
|
||||||
|
|
||||||
const openCreateDialog = () => {
|
const openCreateDialog = () => {
|
||||||
setEditingBooking(null);
|
setEditingBooking(null);
|
||||||
@@ -693,15 +694,17 @@ function FahrzeugBuchungen() {
|
|||||||
Von: {detailBooking.gebucht_von_name}
|
Von: {detailBooking.gebucht_von_name}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{canWrite && (
|
{(canWrite || detailBooking.gebucht_von === user?.id) && (
|
||||||
<Box sx={{ mt: 1.5, display: 'flex', gap: 1 }}>
|
<Box sx={{ mt: 1.5, display: 'flex', gap: 1 }}>
|
||||||
<Button
|
{canWrite && (
|
||||||
size="small"
|
<Button
|
||||||
startIcon={<Edit />}
|
size="small"
|
||||||
onClick={handleOpenEdit}
|
startIcon={<Edit />}
|
||||||
>
|
onClick={handleOpenEdit}
|
||||||
Bearbeiten
|
>
|
||||||
</Button>
|
Bearbeiten
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
color="error"
|
color="error"
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import {
|
|||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } 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 { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime, isValidGermanDate, isValidGermanDateTime } from '../utils/dateInput';
|
import { toGermanDateTime, fromGermanDate, fromGermanDateTime, isValidGermanDateTime } from '../utils/dateInput';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useNotification } from '../contexts/NotificationContext';
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
import { trainingApi } from '../services/training';
|
import { trainingApi } from '../services/training';
|
||||||
@@ -213,6 +213,24 @@ function fromDatetimeLocal(value: string): string {
|
|||||||
return new Date(value).toISOString();
|
return new Date(value).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -910,7 +928,11 @@ function parseCsvRow(line: string, lineNo: number): CsvRow {
|
|||||||
if (parts.length < 4) {
|
if (parts.length < 4) {
|
||||||
return { titel: '', datum_von: '', datum_bis: '', ganztaegig: false, ort: null, beschreibung: null, valid: false, error: `Zeile ${lineNo}: Zu wenige Spalten` };
|
return { titel: '', datum_von: '', datum_bis: '', ganztaegig: false, ort: null, beschreibung: null, valid: false, error: `Zeile ${lineNo}: Zu wenige Spalten` };
|
||||||
}
|
}
|
||||||
const [titel, rawVon, rawBis, rawGanztaegig, ort, , beschreibung] = parts;
|
const [titel, rawVon, rawBis, rawGanztaegig, ort, ...rest] = parts;
|
||||||
|
// Support both 7-column (with Kategorie) and 6-column (without) CSVs:
|
||||||
|
// 7 cols: Titel;Von;Bis;Ganztaegig;Ort;Kategorie;Beschreibung
|
||||||
|
// 6 cols: Titel;Von;Bis;Ganztaegig;Ort;Beschreibung
|
||||||
|
const beschreibung = rest.length >= 2 ? rest[1] : rest[0];
|
||||||
const ganztaegig = rawGanztaegig?.trim().toLowerCase() === 'ja';
|
const ganztaegig = rawGanztaegig?.trim().toLowerCase() === 'ja';
|
||||||
|
|
||||||
const convertDate = (raw: string): string => {
|
const convertDate = (raw: string): string => {
|
||||||
@@ -1383,19 +1405,19 @@ function VeranstaltungFormDialog({
|
|||||||
const vonDate = new Date(form.datum_von);
|
const vonDate = new Date(form.datum_von);
|
||||||
const bisDate = new Date(form.datum_bis);
|
const bisDate = new Date(form.datum_bis);
|
||||||
if (isNaN(vonDate.getTime())) {
|
if (isNaN(vonDate.getTime())) {
|
||||||
notification.showError(`Ungültiges Datum Von (Format: ${form.ganztaegig ? '01.03.2025' : '01.03.2025 18:00'})`);
|
notification.showError('Ungültiges Datum Von');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isNaN(bisDate.getTime())) {
|
if (isNaN(bisDate.getTime())) {
|
||||||
notification.showError(`Ungültiges Datum Bis (Format: ${form.ganztaegig ? '01.03.2025' : '01.03.2025 18:00'})`);
|
notification.showError('Ungültiges Datum Bis');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (bisDate < vonDate) {
|
if (bisDate < vonDate) {
|
||||||
notification.showError('Datum Bis muss nach Datum Von liegen');
|
notification.showError('Datum Bis muss nach Datum Von liegen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (wiederholungAktiv && wiederholungBis && !isValidGermanDate(wiederholungBis)) {
|
if (wiederholungAktiv && wiederholungBis && isNaN(new Date(wiederholungBis).getTime())) {
|
||||||
notification.showError('Ungültiges Datum für Wiederholung Bis (Format: 01.03.2025)');
|
notification.showError('Ungültiges Datum für Wiederholung Bis');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1406,7 +1428,7 @@ function VeranstaltungFormDialog({
|
|||||||
wiederholung: (!editingEvent && wiederholungAktiv && wiederholungBis)
|
wiederholung: (!editingEvent && wiederholungAktiv && wiederholungBis)
|
||||||
? {
|
? {
|
||||||
typ: wiederholungTyp,
|
typ: wiederholungTyp,
|
||||||
bis: fromGermanDate(wiederholungBis) || wiederholungBis,
|
bis: wiederholungBis,
|
||||||
intervall: wiederholungTyp === 'wöchentlich' ? wiederholungIntervall : undefined,
|
intervall: wiederholungTyp === 'wöchentlich' ? wiederholungIntervall : undefined,
|
||||||
wochentag: (wiederholungTyp === 'monatlich_erster_wochentag' || wiederholungTyp === 'monatlich_letzter_wochentag')
|
wochentag: (wiederholungTyp === 'monatlich_erster_wochentag' || wiederholungTyp === 'monatlich_letzter_wochentag')
|
||||||
? wiederholungWochentag
|
? wiederholungWochentag
|
||||||
@@ -1488,39 +1510,39 @@ function VeranstaltungFormDialog({
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Von"
|
label="Von"
|
||||||
placeholder={form.ganztaegig ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
|
type={form.ganztaegig ? 'date' : 'datetime-local'}
|
||||||
value={
|
value={
|
||||||
form.ganztaegig
|
form.ganztaegig
|
||||||
? toGermanDate(form.datum_von)
|
? toDateInputValue(form.datum_von)
|
||||||
: toGermanDateTime(form.datum_von)
|
: toDatetimeLocalValue(form.datum_von)
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const raw = e.target.value;
|
const raw = e.target.value;
|
||||||
|
if (!raw) return;
|
||||||
const iso = form.ganztaegig
|
const iso = form.ganztaegig
|
||||||
? fromDatetimeLocal(raw ? `${fromGermanDate(raw)} 00:00` : '')
|
? new Date(raw + 'T00:00:00').toISOString()
|
||||||
: fromDatetimeLocal(raw);
|
: new Date(raw).toISOString();
|
||||||
handleChange('datum_von', iso);
|
handleChange('datum_von', iso);
|
||||||
}}
|
}}
|
||||||
helperText={form.ganztaegig ? 'Format: 01.03.2025' : 'Format: 01.03.2025 18:00'}
|
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Bis"
|
label="Bis"
|
||||||
placeholder={form.ganztaegig ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
|
type={form.ganztaegig ? 'date' : 'datetime-local'}
|
||||||
value={
|
value={
|
||||||
form.ganztaegig
|
form.ganztaegig
|
||||||
? toGermanDate(form.datum_bis)
|
? toDateInputValue(form.datum_bis)
|
||||||
: toGermanDateTime(form.datum_bis)
|
: toDatetimeLocalValue(form.datum_bis)
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const raw = e.target.value;
|
const raw = e.target.value;
|
||||||
|
if (!raw) return;
|
||||||
const iso = form.ganztaegig
|
const iso = form.ganztaegig
|
||||||
? fromDatetimeLocal(raw ? `${fromGermanDate(raw)} 23:59` : '')
|
? new Date(raw + 'T23:59:00').toISOString()
|
||||||
: fromDatetimeLocal(raw);
|
: new Date(raw).toISOString();
|
||||||
handleChange('datum_bis', iso);
|
handleChange('datum_bis', iso);
|
||||||
}}
|
}}
|
||||||
helperText={form.ganztaegig ? 'Format: 01.03.2025' : 'Format: 01.03.2025 18:00'}
|
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
@@ -1649,7 +1671,7 @@ function VeranstaltungFormDialog({
|
|||||||
<TextField
|
<TextField
|
||||||
label="Wiederholungen bis"
|
label="Wiederholungen bis"
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="TT.MM.JJJJ"
|
type="date"
|
||||||
value={wiederholungBis}
|
value={wiederholungBis}
|
||||||
onChange={(e) => setWiederholungBis(e.target.value)}
|
onChange={(e) => setWiederholungBis(e.target.value)}
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export const bookingApi = {
|
|||||||
checkAvailability(
|
checkAvailability(
|
||||||
fahrzeugId: string,
|
fahrzeugId: string,
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date
|
to: Date,
|
||||||
|
excludeId?: string
|
||||||
): Promise<{ available: boolean; reason?: string; ausserDienstVon?: string; ausserDienstBis?: string }> {
|
): Promise<{ available: boolean; reason?: string; ausserDienstVon?: string; ausserDienstBis?: string }> {
|
||||||
return api
|
return api
|
||||||
.get<ApiResponse<{ available: boolean; reason?: string; ausserDienstVon?: string; ausserDienstBis?: string }>>('/api/bookings/availability', {
|
.get<ApiResponse<{ available: boolean; reason?: string; ausserDienstVon?: string; ausserDienstBis?: string }>>('/api/bookings/availability', {
|
||||||
@@ -65,6 +66,7 @@ export const bookingApi = {
|
|||||||
fahrzeugId,
|
fahrzeugId,
|
||||||
from: from.toISOString(),
|
from: from.toISOString(),
|
||||||
to: to.toISOString(),
|
to: to.toISOString(),
|
||||||
|
...(excludeId ? { excludeId } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((r) => r.data.data);
|
.then((r) => r.data.data);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface FahrzeugBuchungListItem {
|
|||||||
beginn: string; // ISO
|
beginn: string; // ISO
|
||||||
ende: string; // ISO
|
ende: string; // ISO
|
||||||
abgesagt: boolean;
|
abgesagt: boolean;
|
||||||
|
gebucht_von: string;
|
||||||
gebucht_von_name?: string | null;
|
gebucht_von_name?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +36,6 @@ export interface FahrzeugBuchung extends FahrzeugBuchungListItem {
|
|||||||
beschreibung?: string | null;
|
beschreibung?: string | null;
|
||||||
kontakt_person?: string | null;
|
kontakt_person?: string | null;
|
||||||
kontakt_telefon?: string | null;
|
kontakt_telefon?: string | null;
|
||||||
gebucht_von: string;
|
|
||||||
abgesagt_grund?: string | null;
|
abgesagt_grund?: string | null;
|
||||||
erstellt_am: string;
|
erstellt_am: string;
|
||||||
aktualisiert_am: string;
|
aktualisiert_am: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user