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
@@ -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>
);
}