JPGs → PDF: neuer Button überall bei PDF-Upload

- Neue Komponente JpgToPdfModal (jsPDF clientseitig, kein Backend-Roundtrip).
- Bilder hinzufügen per Klick, Drag&Drop oder Strg+V (Clipboard).
- Reihenfolge per Drag&Drop sortierbar; pro Bild 90°/180°-Drehung +
  horizontal/vertikal-Spiegelung.
- Jedes Bild = eine A4-Seite, Orientation automatisch nach Bild,
  JPEG-Qualität 100%.
- FileUpload-Komponente zeigt den Sekundär-Button automatisch, sobald
  accept PDF einschließt (Datenschutz, Vollmacht, Bankkarten, Ausweise,
  Gewerbeanmeldung, Handelsregister, Kündigungsschreiben/-bestätigung
  + jeweilige Optionen).
- Direktinputs ebenfalls erweitert: Vertragsdokumente (ContractDetail),
  Vollmacht-Tab (CustomerDetail), Rechnungen (InvoicesSection).
- PdfTemplates bewusst ausgenommen – braucht AcroForm-Felder.
This commit is contained in:
2026-06-03 12:27:37 +02:00
parent 358688db9e
commit 30f528596c
8 changed files with 810 additions and 54 deletions
@@ -1,7 +1,8 @@
import { useState, useRef } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { formatDate } from '../../utils/dateFormat';
import { Plus, Edit, Trash2, ChevronDown, ChevronUp, FileText, Download, AlertTriangle, Check, Eye } from 'lucide-react';
import { Plus, Edit, Trash2, ChevronDown, ChevronUp, FileText, Download, AlertTriangle, Check, Eye, Images } from 'lucide-react';
import JpgToPdfModal from '../ui/JpgToPdfModal';
import Modal from '../ui/Modal';
import Button from '../ui/Button';
import Input from '../ui/Input';
@@ -218,6 +219,7 @@ function InvoiceModal({
});
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [error, setError] = useState<string | null>(null);
const [isJpgModalOpen, setIsJpgModalOpen] = useState(false);
const addInvoiceFn = async (data: { invoiceDate: string; invoiceType: string; notes?: string }) => {
if (ecdId) {
@@ -386,15 +388,31 @@ function InvoiceModal({
onChange={handleFileSelect}
className="hidden"
/>
<Button
type="button"
variant="secondary"
onClick={() => fileInputRef.current?.click()}
>
{invoice?.documentPath || selectedFile ? 'Ersetzen' : 'PDF hochladen'}
</Button>
<div className="flex gap-2 flex-wrap">
<Button
type="button"
variant="secondary"
onClick={() => fileInputRef.current?.click()}
>
{invoice?.documentPath || selectedFile ? 'Ersetzen' : 'PDF hochladen'}
</Button>
<Button
type="button"
variant="ghost"
onClick={() => setIsJpgModalOpen(true)}
title="Mehrere JPGs zu einer PDF kombinieren"
>
<Images className="w-4 h-4 mr-1" /> JPGs PDF
</Button>
</div>
</div>
)}
<JpgToPdfModal
isOpen={isJpgModalOpen}
onClose={() => setIsJpgModalOpen(false)}
onComplete={(file) => setSelectedFile(file)}
fileNameHint="rechnung"
/>
{formData.invoiceType === 'NOT_AVAILABLE' && (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-800 text-sm">
+78 -31
View File
@@ -1,6 +1,7 @@
import { useRef, useState } from 'react';
import { Upload } from 'lucide-react';
import { Upload, Images } from 'lucide-react';
import Button from './Button';
import JpgToPdfModal from './JpgToPdfModal';
interface FileUploadProps {
onUpload: (file: File) => Promise<void>;
@@ -8,6 +9,10 @@ interface FileUploadProps {
accept?: string;
label?: string;
disabled?: boolean;
/** Standard: aktiv, sobald `accept` PDF einschließt. Explizit auf false setzen, um den Button auszublenden. */
enableJpgToPdf?: boolean;
/** Default-Name (ohne .pdf) für die aus JPGs erzeugte PDF. */
jpgToPdfFileNameHint?: string;
}
export default function FileUpload({
@@ -16,10 +21,16 @@ export default function FileUpload({
accept = '.pdf,.jpg,.jpeg,.png',
label = 'Dokument hochladen',
disabled = false,
enableJpgToPdf,
jpgToPdfFileNameHint,
}: FileUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false);
const [dragOver, setDragOver] = useState(false);
const [isJpgModalOpen, setIsJpgModalOpen] = useState(false);
const acceptsPdf = /pdf/i.test(accept);
const showJpgButton = (enableJpgToPdf ?? acceptsPdf) && !disabled;
const handleFileSelect = async (file: File) => {
if (!file) return;
@@ -64,40 +75,67 @@ export default function FileUpload({
<div className="space-y-2">
{existingFile ? (
!disabled && (
<Button
variant="secondary"
size="sm"
onClick={() => inputRef.current?.click()}
disabled={isUploading}
>
{isUploading ? 'Wird hochgeladen...' : 'Ersetzen'}
</Button>
<div className="flex flex-wrap gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => inputRef.current?.click()}
disabled={isUploading}
>
{isUploading ? 'Wird hochgeladen...' : 'Ersetzen'}
</Button>
{showJpgButton && (
<Button
variant="ghost"
size="sm"
onClick={() => setIsJpgModalOpen(true)}
disabled={isUploading}
title="Mehrere JPGs zu einer PDF kombinieren"
>
<Images className="w-4 h-4 mr-1" /> JPGs PDF
</Button>
)}
</div>
)
) : (
<div
className={`border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors ${
dragOver
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={() => !disabled && inputRef.current?.click()}
onDrop={!disabled ? handleDrop : undefined}
onDragOver={!disabled ? handleDragOver : undefined}
onDragLeave={!disabled ? handleDragLeave : undefined}
>
{isUploading ? (
<div className="text-gray-500">
<div className="animate-spin w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-2" />
Wird hochgeladen...
<>
<div
className={`border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors ${
dragOver
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={() => !disabled && inputRef.current?.click()}
onDrop={!disabled ? handleDrop : undefined}
onDragOver={!disabled ? handleDragOver : undefined}
onDragLeave={!disabled ? handleDragLeave : undefined}
>
{isUploading ? (
<div className="text-gray-500">
<div className="animate-spin w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-2" />
Wird hochgeladen...
</div>
) : (
<>
<Upload className="w-6 h-6 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600">{label}</p>
<p className="text-xs text-gray-400 mt-1">PDF, JPG oder PNG (max. 10MB)</p>
</>
)}
</div>
{showJpgButton && !isUploading && (
<div className="flex justify-center">
<Button
variant="ghost"
size="sm"
onClick={() => setIsJpgModalOpen(true)}
title="Mehrere JPGs zu einer PDF kombinieren"
>
<Images className="w-4 h-4 mr-1" /> JPGs PDF
</Button>
</div>
) : (
<>
<Upload className="w-6 h-6 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600">{label}</p>
<p className="text-xs text-gray-400 mt-1">PDF, JPG oder PNG (max. 10MB)</p>
</>
)}
</div>
</>
)}
<input
@@ -108,6 +146,15 @@ export default function FileUpload({
className="hidden"
disabled={disabled || isUploading}
/>
<JpgToPdfModal
isOpen={isJpgModalOpen}
onClose={() => setIsJpgModalOpen(false)}
onComplete={(file) => {
handleFileSelect(file);
}}
fileNameHint={jpgToPdfFileNameHint}
/>
</div>
);
}
@@ -0,0 +1,443 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { jsPDF } from 'jspdf';
import {
Upload,
RotateCcw,
RotateCw,
FlipHorizontal,
FlipVertical,
Trash2,
FileText,
Repeat,
} from 'lucide-react';
import Modal from './Modal';
import Button from './Button';
interface ImageItem {
id: string;
dataUrl: string;
naturalWidth: number;
naturalHeight: number;
rotation: 0 | 90 | 180 | 270;
flipH: boolean;
flipV: boolean;
fileName: string;
}
interface JpgToPdfModalProps {
isOpen: boolean;
onClose: () => void;
onComplete: (pdfFile: File) => void;
fileNameHint?: string;
}
function makeId() {
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
function readFileAsDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
function loadImage(dataUrl: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Bild-Decode fehlgeschlagen'));
img.src = dataUrl;
});
}
function renderImageToCanvas(image: HTMLImageElement, item: ImageItem): HTMLCanvasElement {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Canvas-Kontext konnte nicht erstellt werden');
const w = image.naturalWidth;
const h = image.naturalHeight;
const rotated = item.rotation === 90 || item.rotation === 270;
canvas.width = rotated ? h : w;
canvas.height = rotated ? w : h;
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate((item.rotation * Math.PI) / 180);
ctx.scale(item.flipH ? -1 : 1, item.flipV ? -1 : 1);
ctx.drawImage(image, -w / 2, -h / 2);
ctx.restore();
return canvas;
}
export default function JpgToPdfModal({
isOpen,
onClose,
onComplete,
fileNameHint,
}: JpgToPdfModalProps) {
const [images, setImages] = useState<ImageItem[]>([]);
const [dragSrcIdx, setDragSrcIdx] = useState<number | null>(null);
const [isOverModal, setIsOverModal] = useState(false);
const [isBuilding, setIsBuilding] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!isOpen) {
setImages([]);
setDragSrcIdx(null);
setIsOverModal(false);
setIsBuilding(false);
setError(null);
}
}, [isOpen]);
const addFiles = useCallback(async (files: FileList | File[]) => {
const list = Array.from(files).filter((f) => f.type.startsWith('image/'));
if (list.length === 0) {
setError('Nur Bilddateien erlaubt (JPG/PNG).');
return;
}
setError(null);
const added: ImageItem[] = [];
for (const file of list) {
try {
const dataUrl = await readFileAsDataUrl(file);
const img = await loadImage(dataUrl);
added.push({
id: makeId(),
dataUrl,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
rotation: 0,
flipH: false,
flipV: false,
fileName: file.name || 'clipboard.png',
});
} catch {
setError(`Bild konnte nicht geladen werden: ${file.name || 'unbenannt'}`);
}
}
if (added.length > 0) {
setImages((prev) => [...prev, ...added]);
}
}, []);
useEffect(() => {
if (!isOpen) return;
const handler = (e: ClipboardEvent) => {
const items = e.clipboardData?.items;
if (!items) return;
const files: File[] = [];
for (let i = 0; i < items.length; i++) {
const it = items[i];
if (it.kind === 'file' && it.type.startsWith('image/')) {
const f = it.getAsFile();
if (f) files.push(f);
}
}
if (files.length > 0) {
e.preventDefault();
addFiles(files);
}
};
document.addEventListener('paste', handler);
return () => document.removeEventListener('paste', handler);
}, [isOpen, addFiles]);
const rotate = (id: string, delta: 90 | -90 | 180) => {
setImages((prev) =>
prev.map((it) =>
it.id === id
? {
...it,
rotation: ((((it.rotation + delta) % 360) + 360) % 360) as ImageItem['rotation'],
}
: it
)
);
};
const flip = (id: string, axis: 'h' | 'v') => {
setImages((prev) =>
prev.map((it) =>
it.id === id
? { ...it, ...(axis === 'h' ? { flipH: !it.flipH } : { flipV: !it.flipV }) }
: it
)
);
};
const remove = (id: string) => {
setImages((prev) => prev.filter((it) => it.id !== id));
};
const handleDragStart = (idx: number) => (e: React.DragEvent) => {
setDragSrcIdx(idx);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(idx));
};
const handleItemDragOver = (e: React.DragEvent) => {
if (dragSrcIdx === null) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleDropOnItem = (idx: number) => (e: React.DragEvent) => {
if (dragSrcIdx === null) return;
e.preventDefault();
e.stopPropagation();
if (dragSrcIdx === idx) {
setDragSrcIdx(null);
return;
}
setImages((prev) => {
const next = [...prev];
const [moved] = next.splice(dragSrcIdx, 1);
next.splice(idx, 0, moved);
return next;
});
setDragSrcIdx(null);
};
const handleModalDragOver = (e: React.DragEvent) => {
if (e.dataTransfer.types.includes('Files')) {
e.preventDefault();
setIsOverModal(true);
}
};
const handleModalDragLeave = (e: React.DragEvent) => {
if (e.target === e.currentTarget) setIsOverModal(false);
};
const handleModalDrop = (e: React.DragEvent) => {
if (e.dataTransfer.files.length > 0) {
e.preventDefault();
setIsOverModal(false);
addFiles(e.dataTransfer.files);
}
};
const buildPdf = async () => {
if (images.length === 0) return;
setIsBuilding(true);
setError(null);
try {
const a4w = 210;
const a4h = 297;
let pdf: jsPDF | null = null;
for (let i = 0; i < images.length; i++) {
const item = images[i];
const img = await loadImage(item.dataUrl);
const canvas = renderImageToCanvas(img, item);
const jpegDataUrl = canvas.toDataURL('image/jpeg', 1.0);
const orientation: 'portrait' | 'landscape' =
canvas.width > canvas.height ? 'landscape' : 'portrait';
const pageW = orientation === 'landscape' ? a4h : a4w;
const pageH = orientation === 'landscape' ? a4w : a4h;
if (!pdf) {
pdf = new jsPDF({ orientation, unit: 'mm', format: 'a4' });
} else {
pdf.addPage('a4', orientation);
}
const margin = 5;
const maxW = pageW - 2 * margin;
const maxH = pageH - 2 * margin;
const ratio = Math.min(maxW / canvas.width, maxH / canvas.height);
const w = canvas.width * ratio;
const h = canvas.height * ratio;
const x = (pageW - w) / 2;
const y = (pageH - h) / 2;
pdf.addImage(jpegDataUrl, 'JPEG', x, y, w, h, undefined, 'SLOW');
}
const blob = pdf!.output('blob');
const base = (fileNameHint || 'bilder').replace(/[^\w.-]+/g, '_').slice(0, 80) || 'bilder';
const file = new File([blob], `${base}.pdf`, { type: 'application/pdf' });
onComplete(file);
onClose();
} catch (e) {
console.error('PDF-Erstellung fehlgeschlagen:', e);
setError('PDF konnte nicht erstellt werden.');
} finally {
setIsBuilding(false);
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="JPGs zu PDF" size="xl">
<div
onDragOver={handleModalDragOver}
onDragLeave={handleModalDragLeave}
onDrop={handleModalDrop}
className={`space-y-4 ${isOverModal ? 'ring-2 ring-blue-400 rounded-lg' : ''}`}
>
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm text-gray-600">
Bilder per Klick wählen, hier reinziehen, oder mit{' '}
<kbd className="px-1.5 py-0.5 border rounded text-xs">Strg</kbd>+
<kbd className="px-1.5 py-0.5 border rounded text-xs">V</kbd> aus der Zwischenablage
einfügen.
</p>
<Button
variant="secondary"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={isBuilding}
>
<Upload className="w-4 h-4 mr-1" /> Bilder hinzufügen
</Button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) addFiles(e.target.files);
e.target.value = '';
}}
/>
</div>
{error && (
<div className="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
{error}
</div>
)}
{images.length === 0 ? (
<div
onClick={() => fileInputRef.current?.click()}
className="border-2 border-dashed rounded-lg p-12 text-center cursor-pointer border-gray-300 hover:border-gray-400 hover:bg-gray-50"
>
<Upload className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600">
Bilder hier hineinziehen oder zum Auswählen klicken
</p>
<p className="text-xs text-gray-400 mt-1">
JPG / PNG · Reihenfolge per Drag &amp; Drop, einzeln drehen/spiegeln
</p>
</div>
) : (
<>
<div className="text-xs text-gray-500">
{images.length} {images.length === 1 ? 'Bild' : 'Bilder'} · Reihenfolge per
Drag&amp;Drop, einzeln drehen/spiegeln. Jedes Bild = eine Seite.
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 max-h-[60vh] overflow-y-auto p-1">
{images.map((item, idx) => {
const transforms = [
`rotate(${item.rotation}deg)`,
`scaleX(${item.flipH ? -1 : 1})`,
`scaleY(${item.flipV ? -1 : 1})`,
].join(' ');
return (
<div
key={item.id}
draggable
onDragStart={handleDragStart(idx)}
onDragOver={handleItemDragOver}
onDrop={handleDropOnItem(idx)}
className={`border rounded-lg p-2 bg-white shadow-sm cursor-move ${
dragSrcIdx === idx ? 'opacity-40' : ''
}`}
title="Zum Sortieren ziehen"
>
<div className="flex items-center justify-between mb-1.5 text-xs">
<span className="font-semibold text-gray-700">#{idx + 1}</span>
<button
type="button"
onClick={() => remove(item.id)}
className="text-red-500 hover:text-red-700"
title="Entfernen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<div className="aspect-square bg-gray-100 rounded flex items-center justify-center overflow-hidden">
<img
src={item.dataUrl}
alt={item.fileName}
style={{ transform: transforms }}
className="max-w-full max-h-full object-contain transition-transform"
draggable={false}
/>
</div>
<div className="mt-1.5 flex justify-center gap-0.5">
<button
type="button"
onClick={() => rotate(item.id, -90)}
className="p-1 hover:bg-gray-100 rounded"
title="90° gegen Uhrzeigersinn"
>
<RotateCcw className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => rotate(item.id, 90)}
className="p-1 hover:bg-gray-100 rounded"
title="90° im Uhrzeigersinn"
>
<RotateCw className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => rotate(item.id, 180)}
className="p-1 hover:bg-gray-100 rounded"
title="180° drehen"
>
<Repeat className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => flip(item.id, 'h')}
className={`p-1 rounded ${
item.flipH ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100'
}`}
title="Horizontal spiegeln"
>
<FlipHorizontal className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => flip(item.id, 'v')}
className={`p-1 rounded ${
item.flipV ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100'
}`}
title="Vertikal spiegeln"
>
<FlipVertical className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
})}
</div>
</>
)}
<div className="flex justify-end gap-2 pt-2 border-t">
<Button variant="ghost" onClick={onClose} disabled={isBuilding}>
Abbrechen
</Button>
<Button onClick={buildPdf} disabled={images.length === 0 || isBuilding}>
<FileText className="w-4 h-4 mr-1" />
{isBuilding ? 'Erstelle PDF...' : 'PDF erstellen & hochladen'}
</Button>
</div>
</div>
</Modal>
);
}
@@ -14,7 +14,8 @@ import Badge from '../../components/ui/Badge';
import Input from '../../components/ui/Input';
import Modal from '../../components/ui/Modal';
import FileUpload from '../../components/ui/FileUpload';
import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator, Info, X, BellOff, Lock, Shield, FileText } from 'lucide-react';
import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator, Info, X, BellOff, Lock, Shield, FileText, Images } from 'lucide-react';
import JpgToPdfModal from '../../components/ui/JpgToPdfModal';
import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations';
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
import { formatDate } from '../../utils/dateFormat';
@@ -3484,6 +3485,7 @@ function ContractDocumentsSection({
const [uploadDeliveryDate, setUploadDeliveryDate] = useState<string>(
() => new Date().toISOString().split('T')[0],
);
const [isJpgModalOpen, setIsJpgModalOpen] = useState(false);
const { data: docsData } = useQuery({
queryKey: ['contract-documents', contractId],
@@ -3581,15 +3583,31 @@ function ContractDocumentsSection({
</p>
</div>
)}
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 flex-wrap">
<label className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer text-sm">
<Plus className="w-4 h-4" />
Datei wählen (PDF, JPG, PNG)
<input type="file" accept=".pdf,.jpg,.jpeg,.png" className="hidden" onChange={handleFileSelect} />
</label>
<Button variant="ghost" size="sm" onClick={() => setIsJpgModalOpen(true)} title="Mehrere JPGs zu einer PDF kombinieren">
<Images className="w-4 h-4 mr-1" /> JPGs PDF
</Button>
<Button variant="secondary" size="sm" onClick={() => setShowUpload(false)}>Abbrechen</Button>
{uploadMutation.isPending && <span className="text-sm text-gray-500">Hochladen...</span>}
</div>
<JpgToPdfModal
isOpen={isJpgModalOpen}
onClose={() => setIsJpgModalOpen(false)}
onComplete={(file) => {
uploadMutation.mutate({
file,
documentType: uploadType,
notes: uploadNotes || undefined,
deliveryDate: isDelivery ? uploadDeliveryDate : undefined,
});
}}
fileNameHint={uploadType}
/>
{uploadMutation.isError && (
<p className="text-xs text-red-600 mt-2">Fehler beim Hochladen</p>
)}
+34 -11
View File
@@ -14,7 +14,8 @@ import Modal from '../../components/ui/Modal';
import Input from '../../components/ui/Input';
import Select from '../../components/ui/Select';
import FileUpload from '../../components/ui/FileUpload';
import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight, Info, Shield, ShieldCheck, ShieldX, ShieldAlert, Lock, ArrowLeft, Cake, RefreshCw, ExternalLink } from 'lucide-react';
import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight, Info, Shield, ShieldCheck, ShieldX, ShieldAlert, Lock, ArrowLeft, Cake, RefreshCw, ExternalLink, Images } from 'lucide-react';
import JpgToPdfModal from '../../components/ui/JpgToPdfModal';
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
import BirthdayManagementModal from '../../components/BirthdayManagementModal';
import { formatDate } from '../../utils/dateFormat';
@@ -4560,6 +4561,7 @@ function AuthorizationsSection({ customerId, customerEmail }: { customerId: numb
const queryClient = useQueryClient();
const { user } = useAuth();
const [sendDropdownFor, setSendDropdownFor] = useState<number | null>(null);
const [jpgModalFor, setJpgModalFor] = useState<number | null>(null);
const { data: authData, isLoading } = useQuery({
queryKey: ['authorizations', customerId],
@@ -4730,21 +4732,42 @@ function AuthorizationsSection({ customerId, customerEmail }: { customerId: numb
</button>
</>
) : (
<label className="text-xs text-blue-600 hover:underline cursor-pointer flex items-center gap-1">
<Plus className="w-3 h-3" />
Vollmacht-PDF hochladen
<input
type="file"
accept=".pdf"
className="hidden"
onChange={(e) => handleFileUpload(auth.representativeId, e)}
/>
</label>
<>
<label className="text-xs text-blue-600 hover:underline cursor-pointer flex items-center gap-1">
<Plus className="w-3 h-3" />
Vollmacht-PDF hochladen
<input
type="file"
accept=".pdf"
className="hidden"
onChange={(e) => handleFileUpload(auth.representativeId, e)}
/>
</label>
<button
type="button"
onClick={() => setJpgModalFor(auth.representativeId)}
className="text-xs text-blue-600 hover:underline cursor-pointer flex items-center gap-1"
title="Mehrere JPGs zu einer PDF kombinieren"
>
<Images className="w-3 h-3" />
JPGs → PDF
</button>
</>
)}
</div>
</div>
))}
</div>
<JpgToPdfModal
isOpen={jpgModalFor !== null}
onClose={() => setJpgModalFor(null)}
onComplete={(file) => {
if (jpgModalFor !== null) {
uploadMutation.mutate({ representativeId: jpgModalFor, file });
}
}}
fileNameHint="vollmacht"
/>
</div>
);
}