resolve issues with new features

This commit is contained in:
Matthias Hochmeister
2026-03-12 17:20:32 +01:00
parent 68586b01dc
commit 34ca007f9b
8 changed files with 232 additions and 49 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import {
Container,
Typography,
@@ -76,9 +76,11 @@ function AdminSettings() {
adminServices: 15,
});
// State for PDF header/footer
// State for PDF header/footer/logo/org
const [pdfHeader, setPdfHeader] = useState('');
const [pdfFooter, setPdfFooter] = useState('');
const [pdfLogo, setPdfLogo] = useState('');
const [pdfOrgName, setPdfOrgName] = useState('');
// Fetch all settings
const { data: settings, isLoading } = useQuery({
@@ -113,6 +115,10 @@ function AdminSettings() {
if (pdfHeaderSetting?.value != null) setPdfHeader(pdfHeaderSetting.value);
const pdfFooterSetting = settings.find((s) => s.key === 'pdf_footer');
if (pdfFooterSetting?.value != null) setPdfFooter(pdfFooterSetting.value);
const pdfLogoSetting = settings.find((s) => s.key === 'pdf_logo');
if (pdfLogoSetting?.value != null) setPdfLogo(pdfLogoSetting.value);
const pdfOrgNameSetting = settings.find((s) => s.key === 'pdf_org_name');
if (pdfOrgNameSetting?.value != null) setPdfOrgName(pdfOrgNameSetting.value);
}
}, [settings]);
@@ -156,17 +162,40 @@ function AdminSettings() {
queryClient.invalidateQueries({ queryKey: ['pdf-settings'] });
},
});
const pdfLogoMutation = useMutation({
mutationFn: (value: string) => settingsApi.update('pdf_logo', value),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
queryClient.invalidateQueries({ queryKey: ['pdf-settings'] });
},
});
const pdfOrgNameMutation = useMutation({
mutationFn: (value: string) => settingsApi.update('pdf_org_name', value),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
queryClient.invalidateQueries({ queryKey: ['pdf-settings'] });
},
});
const handleSavePdfSettings = async () => {
try {
await Promise.all([
pdfHeaderMutation.mutateAsync(pdfHeader),
pdfFooterMutation.mutateAsync(pdfFooter),
pdfLogoMutation.mutateAsync(pdfLogo),
pdfOrgNameMutation.mutateAsync(pdfOrgName),
]);
showSuccess('PDF-Einstellungen gespeichert');
} catch {
showError('Fehler beim Speichern der PDF-Einstellungen');
}
};
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => setPdfLogo(ev.target?.result as string);
reader.readAsDataURL(file);
};
if (!isAdmin) {
return <Navigate to="/dashboard" replace />;
@@ -423,7 +452,44 @@ function AdminSettings() {
</Typography>
<Stack spacing={2}>
<TextField
label="PDF Kopfzeile"
label="Organisationsname (rechts im PDF-Header)"
value={pdfOrgName}
onChange={(e) => setPdfOrgName(e.target.value)}
fullWidth
size="small"
placeholder="FREIWILLIGE FEUERWEHR REMS"
/>
<Box>
<Typography variant="body2" color="text.secondary" gutterBottom>
Logo (erscheint rechts im PDF-Header, neben dem Organisationsnamen)
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Button component="label" variant="outlined" size="small">
Logo hochladen
<input
type="file"
hidden
accept="image/*"
onChange={handleLogoUpload}
/>
</Button>
{pdfLogo && (
<>
<Box
component="img"
src={pdfLogo}
alt="Logo Vorschau"
sx={{ height: 40, maxWidth: 120, objectFit: 'contain', borderRadius: 1 }}
/>
<IconButton size="small" onClick={() => setPdfLogo('')} title="Logo entfernen">
<Delete fontSize="small" />
</IconButton>
</>
)}
</Box>
</Box>
<TextField
label="PDF Kopfzeile (links, unter dem Header-Banner)"
value={pdfHeader}
onChange={(e) => setPdfHeader(e.target.value)}
multiline
@@ -447,7 +513,12 @@ function AdminSettings() {
onClick={handleSavePdfSettings}
variant="contained"
size="small"
disabled={pdfHeaderMutation.isPending || pdfFooterMutation.isPending}
disabled={
pdfHeaderMutation.isPending ||
pdfFooterMutation.isPending ||
pdfLogoMutation.isPending ||
pdfOrgNameMutation.isPending
}
>
Speichern
</Button>

View File

@@ -398,7 +398,7 @@ function FahrzeugBuchungen() {
<TableContainer component={Paper} elevation={1}>
<Table size="small" sx={{ tableLayout: 'fixed' }}>
<TableHead>
<TableRow sx={{ bgcolor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.100' }}>
<TableRow sx={{ bgcolor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100' }}>
<TableCell sx={{ width: 160, fontWeight: 700 }}>
Fahrzeug
</TableCell>

View File

@@ -675,7 +675,7 @@ async function fetchPdfSettings(): Promise<PdfSettings> {
_pdfSettingsCacheTime = Date.now();
return _pdfSettingsCache;
} catch {
return { pdf_header: '', pdf_footer: '' };
return { pdf_header: '', pdf_footer: '', pdf_logo: '', pdf_org_name: '' };
}
}
@@ -701,9 +701,28 @@ async function generatePdf(
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text(`Kalender — ${monthLabel} ${year}`, 10, 12);
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.text('Feuerwehr Rems', 250, 12);
// Right side: logo and/or org name
const logoSize = 14;
const logoX = 297 - 4 - logoSize; // 4mm right margin
if (pdfSettings.pdf_logo) {
try {
const fmt = pdfSettings.pdf_logo.match(/^data:image\/(\w+);/)?.[1]?.toUpperCase() ?? 'PNG';
doc.addImage(pdfSettings.pdf_logo, fmt === 'JPG' ? 'JPEG' : fmt, logoX, 2, logoSize, logoSize);
} catch { /* ignore invalid image */ }
}
if (pdfSettings.pdf_org_name) {
doc.setFontSize(9);
doc.setFont('helvetica', 'bold');
doc.setTextColor(255, 255, 255);
const nameW = doc.getTextWidth(pdfSettings.pdf_org_name);
const nameX = (pdfSettings.pdf_logo ? logoX - 3 : 297 - 4) - nameW;
doc.text(pdfSettings.pdf_org_name, nameX, 12);
} else if (!pdfSettings.pdf_logo) {
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.text('Feuerwehr Rems', 250, 12);
}
// Custom header text
let tableStartY = 22;
@@ -796,9 +815,28 @@ async function generateBookingsPdf(
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text(`Fahrzeugbuchungen — ${kwLabel} · ${startLabel} ${endLabel}`, 10, 12);
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.text('Feuerwehr Rems', 250, 12);
// Right side: logo and/or org name
const logoSize = 14;
const logoX = 297 - 4 - logoSize;
if (pdfSettings.pdf_logo) {
try {
const fmt = pdfSettings.pdf_logo.match(/^data:image\/(\w+);/)?.[1]?.toUpperCase() ?? 'PNG';
doc.addImage(pdfSettings.pdf_logo, fmt === 'JPG' ? 'JPEG' : fmt, logoX, 2, logoSize, logoSize);
} catch { /* ignore invalid image */ }
}
if (pdfSettings.pdf_org_name) {
doc.setFontSize(9);
doc.setFont('helvetica', 'bold');
doc.setTextColor(255, 255, 255);
const nameW = doc.getTextWidth(pdfSettings.pdf_org_name);
const nameX = (pdfSettings.pdf_logo ? logoX - 3 : 297 - 4) - nameW;
doc.text(pdfSettings.pdf_org_name, nameX, 12);
} else if (!pdfSettings.pdf_logo) {
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.text('Feuerwehr Rems', 250, 12);
}
// Custom header text
let tableStartY = 22;
@@ -2747,7 +2785,7 @@ export default function Kalender() {
<TableContainer component={Paper} elevation={1}>
<Table size="small" sx={{ tableLayout: 'fixed' }}>
<TableHead>
<TableRow sx={{ bgcolor: 'grey.100' }}>
<TableRow sx={{ bgcolor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100' }}>
<TableCell sx={{ width: 160, fontWeight: 700 }}>
Fahrzeug
</TableCell>
@@ -2758,7 +2796,7 @@ export default function Kalender() {
sx={{
fontWeight: fnsIsToday(day) ? 700 : 400,
color: fnsIsToday(day) ? 'primary.main' : 'text.primary',
bgcolor: fnsIsToday(day) ? 'primary.50' : undefined,
bgcolor: fnsIsToday(day) ? (theme) => theme.palette.mode === 'dark' ? 'primary.900' : 'primary.50' : undefined,
}}
>
<Typography variant="caption" display="block">