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

View File

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

View File

@@ -55,6 +55,7 @@ import {
Edit as EditIcon, Edit as EditIcon,
Event as EventIcon, Event as EventIcon,
HelpOutline as UnknownIcon, HelpOutline as UnknownIcon,
IosShare,
Star as StarIcon, Star as StarIcon,
Today as TodayIcon, Today as TodayIcon,
Tune, Tune,
@@ -1085,6 +1086,12 @@ export default function Kalender() {
const [cancelBookingGrund, setCancelBookingGrund] = useState(''); const [cancelBookingGrund, setCancelBookingGrund] = useState('');
const [cancelBookingLoading, setCancelBookingLoading] = useState(false); 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 ───────────────────────────────────────────────────────────── // ── Data loading ─────────────────────────────────────────────────────────────
const loadCalendarData = useCallback(async () => { 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 ─────────────────────────────────────────────────────────────────── // ── Render ───────────────────────────────────────────────────────────────────
return ( return (
@@ -1451,6 +1480,16 @@ export default function Kalender() {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
{/* iCal subscribe */}
<Button
startIcon={<IosShare />}
onClick={handleIcalEventOpen}
variant="outlined"
size="small"
>
Kalender
</Button>
</Box> </Box>
{/* Month navigation */} {/* Month navigation */}
@@ -1594,6 +1633,37 @@ export default function Kalender() {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </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> </Box>
)} )}
@@ -1649,6 +1719,16 @@ export default function Kalender() {
Neue Buchung Neue Buchung
</Button> </Button>
)} )}
{/* iCal subscribe */}
<Button
startIcon={<IosShare />}
onClick={handleIcalBookingOpen}
variant="outlined"
size="small"
>
Kalender
</Button>
</Box> </Box>
{bookingsLoading && ( {bookingsLoading && (
@@ -1696,11 +1776,11 @@ export default function Kalender() {
<TableRow key={vehicle.id} hover> <TableRow key={vehicle.id} hover>
<TableCell> <TableCell>
<Typography variant="body2" fontWeight={600}> <Typography variant="body2" fontWeight={600}>
{vehicle.name} {vehicle.bezeichnung}
</Typography> </Typography>
{vehicle.kennzeichen && ( {vehicle.amtliches_kennzeichen && (
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
{vehicle.kennzeichen} {vehicle.amtliches_kennzeichen}
</Typography> </Typography>
)} )}
</TableCell> </TableCell>
@@ -1900,7 +1980,7 @@ export default function Kalender() {
> >
{vehicles.map((v) => ( {vehicles.map((v) => (
<MenuItem key={v.id} value={v.id}> <MenuItem key={v.id} value={v.id}>
{v.name}{v.kennzeichen ? ` (${v.kennzeichen})` : ''} {v.bezeichnung}{v.amtliches_kennzeichen ? ` (${v.amtliches_kennzeichen})` : ''}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
@@ -2041,6 +2121,37 @@ export default function Kalender() {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </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> </Box>
)} )}

View File

@@ -113,5 +113,5 @@ export const bookingApi = {
export function fetchVehicles(): Promise<Fahrzeug[]> { export function fetchVehicles(): Promise<Fahrzeug[]> {
return api return api
.get<ApiResponse<Fahrzeug[]>>('/api/vehicles') .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 { export interface Fahrzeug {
id: string; id: string;
name: string; bezeichnung: string;
kennzeichen?: string | null; kurzname: string | null;
typ: string; amtliches_kennzeichen: string | null;
is_active: boolean; status: string;
archived_at?: string | null;
} }
export interface CreateBuchungInput { export interface CreateBuchungInput {