update
This commit is contained in:
@@ -125,7 +125,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{!isOwnMessage && (
|
||||
{!isOwnMessage && !isOneToOne && (
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block' }}>
|
||||
{message.actorDisplayName}
|
||||
</Typography>
|
||||
|
||||
@@ -137,6 +137,7 @@ const ChatMessageView: React.FC = () => {
|
||||
}, [selectedRoomToken, chatPanelOpen, queryClient, fetchReactions]);
|
||||
|
||||
const room = rooms.find((r) => r.token === selectedRoomToken);
|
||||
const isOneToOne = room?.type === 1;
|
||||
|
||||
const sendMutation = useMutation({
|
||||
mutationFn: ({ message, replyTo }: { message: string; replyTo?: number }) =>
|
||||
@@ -271,6 +272,7 @@ const ChatMessageView: React.FC = () => {
|
||||
key={msg.id}
|
||||
message={msg}
|
||||
isOwnMessage={msg.actorType === 'users' && msg.actorId === loginName}
|
||||
isOneToOne={isOneToOne}
|
||||
onReplyClick={setReplyToMessage}
|
||||
onReactionToggled={handleReactionToggled}
|
||||
reactionsOverride={reactionsMap.get(msg.id)}
|
||||
|
||||
@@ -189,7 +189,58 @@ const ChatPanelInner: React.FC = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
) : 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 />
|
||||
)}
|
||||
|
||||
@@ -238,7 +238,8 @@ function FahrzeugBuchungen() {
|
||||
.checkAvailability(
|
||||
form.fahrzeugId,
|
||||
new Date(form.beginn),
|
||||
new Date(form.ende)
|
||||
new Date(form.ende),
|
||||
editingBooking?.id
|
||||
)
|
||||
.then((result) => {
|
||||
if (!cancelled) setAvailability(result);
|
||||
@@ -249,7 +250,7 @@ function FahrzeugBuchungen() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [form.fahrzeugId, form.beginn, form.ende]);
|
||||
}, [form.fahrzeugId, form.beginn, form.ende, editingBooking?.id]);
|
||||
|
||||
const openCreateDialog = () => {
|
||||
setEditingBooking(null);
|
||||
@@ -693,15 +694,17 @@ function FahrzeugBuchungen() {
|
||||
Von: {detailBooking.gebucht_von_name}
|
||||
</Typography>
|
||||
)}
|
||||
{canWrite && (
|
||||
{(canWrite || detailBooking.gebucht_von === user?.id) && (
|
||||
<Box sx={{ mt: 1.5, display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Edit />}
|
||||
onClick={handleOpenEdit}
|
||||
>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
{canWrite && (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Edit />}
|
||||
onClick={handleOpenEdit}
|
||||
>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
|
||||
@@ -70,7 +70,7 @@ import {
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
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 { useNotification } from '../contexts/NotificationContext';
|
||||
import { trainingApi } from '../services/training';
|
||||
@@ -213,6 +213,24 @@ function fromDatetimeLocal(value: string): string {
|
||||
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
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -910,7 +928,11 @@ function parseCsvRow(line: string, lineNo: number): CsvRow {
|
||||
if (parts.length < 4) {
|
||||
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 convertDate = (raw: string): string => {
|
||||
@@ -1383,19 +1405,19 @@ function VeranstaltungFormDialog({
|
||||
const vonDate = new Date(form.datum_von);
|
||||
const bisDate = new Date(form.datum_bis);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (bisDate < vonDate) {
|
||||
notification.showError('Datum Bis muss nach Datum Von liegen');
|
||||
return;
|
||||
}
|
||||
if (wiederholungAktiv && wiederholungBis && !isValidGermanDate(wiederholungBis)) {
|
||||
notification.showError('Ungültiges Datum für Wiederholung Bis (Format: 01.03.2025)');
|
||||
if (wiederholungAktiv && wiederholungBis && isNaN(new Date(wiederholungBis).getTime())) {
|
||||
notification.showError('Ungültiges Datum für Wiederholung Bis');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1406,7 +1428,7 @@ function VeranstaltungFormDialog({
|
||||
wiederholung: (!editingEvent && wiederholungAktiv && wiederholungBis)
|
||||
? {
|
||||
typ: wiederholungTyp,
|
||||
bis: fromGermanDate(wiederholungBis) || wiederholungBis,
|
||||
bis: wiederholungBis,
|
||||
intervall: wiederholungTyp === 'wöchentlich' ? wiederholungIntervall : undefined,
|
||||
wochentag: (wiederholungTyp === 'monatlich_erster_wochentag' || wiederholungTyp === 'monatlich_letzter_wochentag')
|
||||
? wiederholungWochentag
|
||||
@@ -1488,39 +1510,39 @@ function VeranstaltungFormDialog({
|
||||
/>
|
||||
<TextField
|
||||
label="Von"
|
||||
placeholder={form.ganztaegig ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
|
||||
type={form.ganztaegig ? 'date' : 'datetime-local'}
|
||||
value={
|
||||
form.ganztaegig
|
||||
? toGermanDate(form.datum_von)
|
||||
: toGermanDateTime(form.datum_von)
|
||||
? toDateInputValue(form.datum_von)
|
||||
: toDatetimeLocalValue(form.datum_von)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
if (!raw) return;
|
||||
const iso = form.ganztaegig
|
||||
? fromDatetimeLocal(raw ? `${fromGermanDate(raw)} 00:00` : '')
|
||||
: fromDatetimeLocal(raw);
|
||||
? new Date(raw + 'T00:00:00').toISOString()
|
||||
: new Date(raw).toISOString();
|
||||
handleChange('datum_von', iso);
|
||||
}}
|
||||
helperText={form.ganztaegig ? 'Format: 01.03.2025' : 'Format: 01.03.2025 18:00'}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Bis"
|
||||
placeholder={form.ganztaegig ? 'TT.MM.JJJJ' : 'TT.MM.JJJJ HH:MM'}
|
||||
type={form.ganztaegig ? 'date' : 'datetime-local'}
|
||||
value={
|
||||
form.ganztaegig
|
||||
? toGermanDate(form.datum_bis)
|
||||
: toGermanDateTime(form.datum_bis)
|
||||
? toDateInputValue(form.datum_bis)
|
||||
: toDatetimeLocalValue(form.datum_bis)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
if (!raw) return;
|
||||
const iso = form.ganztaegig
|
||||
? fromDatetimeLocal(raw ? `${fromGermanDate(raw)} 23:59` : '')
|
||||
: fromDatetimeLocal(raw);
|
||||
? new Date(raw + 'T23:59:00').toISOString()
|
||||
: new Date(raw).toISOString();
|
||||
handleChange('datum_bis', iso);
|
||||
}}
|
||||
helperText={form.ganztaegig ? 'Format: 01.03.2025' : 'Format: 01.03.2025 18:00'}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
/>
|
||||
@@ -1649,7 +1671,7 @@ function VeranstaltungFormDialog({
|
||||
<TextField
|
||||
label="Wiederholungen bis"
|
||||
size="small"
|
||||
placeholder="TT.MM.JJJJ"
|
||||
type="date"
|
||||
value={wiederholungBis}
|
||||
onChange={(e) => setWiederholungBis(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
|
||||
@@ -57,7 +57,8 @@ export const bookingApi = {
|
||||
checkAvailability(
|
||||
fahrzeugId: string,
|
||||
from: Date,
|
||||
to: Date
|
||||
to: Date,
|
||||
excludeId?: string
|
||||
): Promise<{ available: boolean; reason?: string; ausserDienstVon?: string; ausserDienstBis?: string }> {
|
||||
return api
|
||||
.get<ApiResponse<{ available: boolean; reason?: string; ausserDienstVon?: string; ausserDienstBis?: string }>>('/api/bookings/availability', {
|
||||
@@ -65,6 +66,7 @@ export const bookingApi = {
|
||||
fahrzeugId,
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
...(excludeId ? { excludeId } : {}),
|
||||
},
|
||||
})
|
||||
.then((r) => r.data.data);
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface FahrzeugBuchungListItem {
|
||||
beginn: string; // ISO
|
||||
ende: string; // ISO
|
||||
abgesagt: boolean;
|
||||
gebucht_von: string;
|
||||
gebucht_von_name?: string | null;
|
||||
}
|
||||
|
||||
@@ -35,7 +36,6 @@ export interface FahrzeugBuchung extends FahrzeugBuchungListItem {
|
||||
beschreibung?: string | null;
|
||||
kontakt_person?: string | null;
|
||||
kontakt_telefon?: string | null;
|
||||
gebucht_von: string;
|
||||
abgesagt_grund?: string | null;
|
||||
erstellt_am: string;
|
||||
aktualisiert_am: string;
|
||||
|
||||
Reference in New Issue
Block a user