featuer change for calendar

This commit is contained in:
Matthias Hochmeister
2026-03-02 17:26:01 +01:00
parent 314b6c3bed
commit 9a6b9511c8
5 changed files with 129 additions and 19 deletions

View File

@@ -78,7 +78,7 @@ class BookingService {
SELECT
b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
b.beginn, b.ende, b.abgesagt,
f.name AS fahrzeug_name, f.kennzeichen AS fahrzeug_kennzeichen,
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
u.name AS gebucht_von_name
FROM fahrzeug_buchungen b
JOIN fahrzeuge f ON f.id = b.fahrzeug_id
@@ -103,7 +103,7 @@ class BookingService {
SELECT
b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
b.beginn, b.ende, b.abgesagt,
f.name AS fahrzeug_name, f.kennzeichen AS fahrzeug_kennzeichen,
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
u.name AS gebucht_von_name
FROM fahrzeug_buchungen b
JOIN fahrzeuge f ON f.id = b.fahrzeug_id
@@ -128,7 +128,7 @@ class BookingService {
b.gebucht_von, b.kontakt_person, b.kontakt_telefon,
b.abgesagt, b.abgesagt_grund,
b.erstellt_am, b.aktualisiert_am,
f.name AS fahrzeug_name, f.kennzeichen AS fahrzeug_kennzeichen,
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
u.name AS gebucht_von_name
FROM fahrzeug_buchungen b
JOIN fahrzeuge f ON f.id = b.fahrzeug_id
@@ -345,7 +345,7 @@ class BookingService {
SELECT
b.id, b.titel, b.beschreibung, b.buchungs_art::text AS buchungs_art,
b.beginn, b.ende,
f.name AS fahrzeug_name
f.bezeichnung AS fahrzeug_name
FROM fahrzeug_buchungen b
JOIN fahrzeuge f ON f.id = b.fahrzeug_id
WHERE b.abgesagt = FALSE

View File

@@ -423,11 +423,11 @@ function FahrzeugBuchungen() {
<TableRow key={vehicle.id} hover>
<TableCell>
<Typography variant="body2" fontWeight={600}>
{vehicle.name}
{vehicle.bezeichnung}
</Typography>
{vehicle.kennzeichen && (
{vehicle.amtliches_kennzeichen && (
<Typography variant="caption" color="text.secondary">
{vehicle.kennzeichen}
{vehicle.amtliches_kennzeichen}
</Typography>
)}
</TableCell>
@@ -632,8 +632,8 @@ function FahrzeugBuchungen() {
>
{vehicles.map((v) => (
<MenuItem key={v.id} value={v.id}>
{v.name}
{v.kennzeichen ? ` (${v.kennzeichen})` : ''}
{v.bezeichnung}
{v.amtliches_kennzeichen ? ` (${v.amtliches_kennzeichen})` : ''}
</MenuItem>
))}
</Select>

View File

@@ -55,6 +55,7 @@ import {
Edit as EditIcon,
Event as EventIcon,
HelpOutline as UnknownIcon,
IosShare,
Star as StarIcon,
Today as TodayIcon,
Tune,
@@ -1085,6 +1086,12 @@ export default function Kalender() {
const [cancelBookingGrund, setCancelBookingGrund] = useState('');
const [cancelBookingLoading, setCancelBookingLoading] = useState(false);
// iCal subscription
const [icalEventOpen, setIcalEventOpen] = useState(false);
const [icalEventUrl, setIcalEventUrl] = useState('');
const [icalBookingOpen, setIcalBookingOpen] = useState(false);
const [icalBookingUrl, setIcalBookingUrl] = useState('');
// ── Data loading ─────────────────────────────────────────────────────────────
const loadCalendarData = useCallback(async () => {
@@ -1354,6 +1361,28 @@ export default function Kalender() {
}
};
const handleIcalEventOpen = async () => {
try {
const { subscribeUrl } = await eventsApi.getCalendarToken();
setIcalEventUrl(subscribeUrl);
setIcalEventOpen(true);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Fehler beim Laden des Tokens';
notification.showError(msg);
}
};
const handleIcalBookingOpen = async () => {
try {
const { subscribeUrl } = await bookingApi.getCalendarToken();
setIcalBookingUrl(subscribeUrl);
setIcalBookingOpen(true);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Fehler beim Laden des Tokens';
notification.showError(msg);
}
};
// ── Render ───────────────────────────────────────────────────────────────────
return (
@@ -1451,6 +1480,16 @@ export default function Kalender() {
</IconButton>
</Tooltip>
)}
{/* iCal subscribe */}
<Button
startIcon={<IosShare />}
onClick={handleIcalEventOpen}
variant="outlined"
size="small"
>
Kalender
</Button>
</Box>
{/* Month navigation */}
@@ -1594,6 +1633,37 @@ export default function Kalender() {
</Button>
</DialogActions>
</Dialog>
{/* iCal Event subscription dialog */}
<Dialog open={icalEventOpen} onClose={() => setIcalEventOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Kalender abonnieren</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Abonniere den Dienste- & Veranstaltungskalender in deiner Kalenderanwendung. Kopiere die URL und füge sie als neuen
Kalender (per URL) in Apple Kalender, Google Kalender oder Outlook ein.
</Typography>
<TextField
fullWidth
value={icalEventUrl}
InputProps={{
readOnly: true,
endAdornment: (
<IconButton
onClick={() => {
navigator.clipboard.writeText(icalEventUrl);
notification.showSuccess('URL kopiert!');
}}
>
<CopyIcon />
</IconButton>
),
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setIcalEventOpen(false)}>Schließen</Button>
</DialogActions>
</Dialog>
</Box>
)}
@@ -1649,6 +1719,16 @@ export default function Kalender() {
Neue Buchung
</Button>
)}
{/* iCal subscribe */}
<Button
startIcon={<IosShare />}
onClick={handleIcalBookingOpen}
variant="outlined"
size="small"
>
Kalender
</Button>
</Box>
{bookingsLoading && (
@@ -1696,11 +1776,11 @@ export default function Kalender() {
<TableRow key={vehicle.id} hover>
<TableCell>
<Typography variant="body2" fontWeight={600}>
{vehicle.name}
{vehicle.bezeichnung}
</Typography>
{vehicle.kennzeichen && (
{vehicle.amtliches_kennzeichen && (
<Typography variant="caption" color="text.secondary">
{vehicle.kennzeichen}
{vehicle.amtliches_kennzeichen}
</Typography>
)}
</TableCell>
@@ -1900,7 +1980,7 @@ export default function Kalender() {
>
{vehicles.map((v) => (
<MenuItem key={v.id} value={v.id}>
{v.name}{v.kennzeichen ? ` (${v.kennzeichen})` : ''}
{v.bezeichnung}{v.amtliches_kennzeichen ? ` (${v.amtliches_kennzeichen})` : ''}
</MenuItem>
))}
</Select>
@@ -2041,6 +2121,37 @@ export default function Kalender() {
</Button>
</DialogActions>
</Dialog>
{/* iCal Booking subscription dialog */}
<Dialog open={icalBookingOpen} onClose={() => setIcalBookingOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Kalender abonnieren</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Abonniere den Fahrzeugbuchungskalender in deiner Kalenderanwendung. Kopiere die URL und füge sie als neuen
Kalender (per URL) in Apple Kalender, Google Kalender oder Outlook ein.
</Typography>
<TextField
fullWidth
value={icalBookingUrl}
InputProps={{
readOnly: true,
endAdornment: (
<IconButton
onClick={() => {
navigator.clipboard.writeText(icalBookingUrl);
notification.showSuccess('URL kopiert!');
}}
>
<CopyIcon />
</IconButton>
),
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setIcalBookingOpen(false)}>Schließen</Button>
</DialogActions>
</Dialog>
</Box>
)}

View File

@@ -113,5 +113,5 @@ export const bookingApi = {
export function fetchVehicles(): Promise<Fahrzeug[]> {
return api
.get<ApiResponse<Fahrzeug[]>>('/api/vehicles')
.then((r) => r.data.data.filter((v: Fahrzeug) => !v.archived_at));
.then((r) => r.data.data);
}

View File

@@ -41,11 +41,10 @@ export interface FahrzeugBuchung extends FahrzeugBuchungListItem {
export interface Fahrzeug {
id: string;
name: string;
kennzeichen?: string | null;
typ: string;
is_active: boolean;
archived_at?: string | null;
bezeichnung: string;
kurzname: string | null;
amtliches_kennzeichen: string | null;
status: string;
}
export interface CreateBuchungInput {