resolve issues with new features

This commit is contained in:
Matthias Hochmeister
2026-03-12 16:42:21 +01:00
parent 5aa309b97a
commit 68586b01dc
19 changed files with 526 additions and 109 deletions

View File

@@ -68,7 +68,7 @@ import {
ViewWeek as ViewWeekIcon,
Warning,
} from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime, isValidGermanDate, isValidGermanDateTime } from '../utils/dateInput';
import { useAuth } from '../contexts/AuthContext';
@@ -76,6 +76,7 @@ import { useNotification } from '../contexts/NotificationContext';
import { trainingApi } from '../services/training';
import { eventsApi } from '../services/events';
import { bookingApi, fetchVehicles } from '../services/bookings';
import { configApi, type PdfSettings } from '../services/config';
import type {
UebungListItem,
UebungTyp,
@@ -622,6 +623,62 @@ function DayPopover({
// PDF Export helper
// ──────────────────────────────────────────────────────────────────────────────
/**
* Render text with basic markdown (**bold**) and line breaks into a jsPDF doc.
* Returns the final Y position after rendering.
*/
function renderMarkdownText(
doc: import('jspdf').jsPDF,
text: string,
x: number,
y: number,
options?: { fontSize?: number; maxWidth?: number },
): number {
const fontSize = options?.fontSize ?? 9;
const lineHeight = fontSize * 0.5; // ~mm per line
doc.setFontSize(fontSize);
doc.setTextColor(0, 0, 0);
const lines = text.split('\n');
let curY = y;
for (const line of lines) {
// Split by ** to alternate normal/bold
const segments = line.split('**');
let curX = x;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (!seg) continue;
const isBold = i % 2 === 1;
doc.setFont('helvetica', isBold ? 'bold' : 'normal');
doc.text(seg, curX, curY);
curX += doc.getTextWidth(seg);
}
curY += lineHeight;
}
// Reset font
doc.setFont('helvetica', 'normal');
return curY;
}
let _pdfSettingsCache: PdfSettings | null = null;
let _pdfSettingsCacheTime = 0;
async function fetchPdfSettings(): Promise<PdfSettings> {
// Cache for 30 seconds to avoid fetching on every export click
if (_pdfSettingsCache && Date.now() - _pdfSettingsCacheTime < 30_000) {
return _pdfSettingsCache;
}
try {
_pdfSettingsCache = await configApi.getPdfSettings();
_pdfSettingsCacheTime = Date.now();
return _pdfSettingsCache;
} catch {
return { pdf_header: '', pdf_footer: '' };
}
}
async function generatePdf(
year: number,
month: number,
@@ -635,6 +692,8 @@ async function generatePdf(
const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
const monthLabel = MONTH_LABELS[month];
const pdfSettings = await fetchPdfSettings();
// Header bar
doc.setFillColor(183, 28, 28); // fire-red
doc.rect(0, 0, 297, 18, 'F');
@@ -646,6 +705,12 @@ async function generatePdf(
doc.setFont('helvetica', 'normal');
doc.text('Feuerwehr Rems', 250, 12);
// Custom header text
let tableStartY = 22;
if (pdfSettings.pdf_header) {
tableStartY = renderMarkdownText(doc, pdfSettings.pdf_header, 10, 24, { fontSize: 9 }) + 2;
}
// Build combined list (same logic as CombinedListView)
type ListEntry =
| { kind: 'training'; item: UebungListItem }
@@ -681,7 +746,7 @@ async function generatePdf(
autoTable(doc, {
head: [['Datum', 'Uhrzeit', 'Titel', 'Kategorie/Typ', 'Ort']],
body: rows,
startY: 22,
startY: tableStartY,
headStyles: { fillColor: [183, 28, 28], textColor: 255, fontStyle: 'bold' },
alternateRowStyles: { fillColor: [250, 235, 235] },
margin: { left: 10, right: 10 },
@@ -693,6 +758,11 @@ async function generatePdf(
3: { cellWidth: 40 },
4: { cellWidth: 60 },
},
didDrawPage: pdfSettings.pdf_footer
? () => {
renderMarkdownText(doc, pdfSettings.pdf_footer, 10, doc.internal.pageSize.height - 12, { fontSize: 8 });
}
: undefined,
});
const filename = `kalender_${year}_${String(month + 1).padStart(2, '0')}.pdf`;
@@ -717,6 +787,8 @@ async function generateBookingsPdf(
const endLabel = fnsFormat(weekEnd, 'dd.MM.yyyy');
const kwLabel = `KW ${fnsFormat(weekStart, 'w')}`;
const pdfSettings = await fetchPdfSettings();
// Header bar
doc.setFillColor(183, 28, 28); // fire-red
doc.rect(0, 0, 297, 18, 'F');
@@ -728,6 +800,12 @@ async function generateBookingsPdf(
doc.setFont('helvetica', 'normal');
doc.text('Feuerwehr Rems', 250, 12);
// Custom header text
let tableStartY = 22;
if (pdfSettings.pdf_header) {
tableStartY = renderMarkdownText(doc, pdfSettings.pdf_header, 10, 24, { fontSize: 9 }) + 2;
}
const formatDt = (iso: string) => {
const d = new Date(iso);
return fnsFormat(d, 'dd.MM.yyyy HH:mm');
@@ -745,7 +823,7 @@ async function generateBookingsPdf(
autoTable(doc, {
head: [['Fahrzeug', 'Titel', 'Beginn', 'Ende', 'Art']],
body: rows,
startY: 22,
startY: tableStartY,
headStyles: { fillColor: [183, 28, 28], textColor: 255, fontStyle: 'bold' },
alternateRowStyles: { fillColor: [250, 235, 235] },
margin: { left: 10, right: 10 },
@@ -757,6 +835,11 @@ async function generateBookingsPdf(
3: { cellWidth: 38 },
4: { cellWidth: 35 },
},
didDrawPage: pdfSettings.pdf_footer
? () => {
renderMarkdownText(doc, pdfSettings.pdf_footer, 10, doc.internal.pageSize.height - 12, { fontSize: 8 });
}
: undefined,
});
const filename = `fahrzeugbuchungen_${fnsFormat(weekStart, 'yyyy-MM-dd')}.pdf`;
@@ -1557,6 +1640,7 @@ function VeranstaltungFormDialog({
export default function Kalender() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { user } = useAuth();
const notification = useNotification();
const theme = useTheme();
@@ -1569,7 +1653,15 @@ export default function Kalender() {
const canCreateBookings = !!user;
// ── Tab ─────────────────────────────────────────────────────────────────────
const [activeTab, setActiveTab] = useState(0);
const [activeTab, setActiveTab] = useState(() => {
const t = Number(searchParams.get('tab'));
return t >= 0 && t < 2 ? t : 0;
});
useEffect(() => {
const t = Number(searchParams.get('tab'));
if (t >= 0 && t < 2) setActiveTab(t);
}, [searchParams]);
// ── Calendar tab state ───────────────────────────────────────────────────────
const today = new Date();