PdfDragButton: PDF per Drag-and-Drop aus Bankkarte/Ausweis ziehen
Neue wiederverwendbare Komponente PdfDragButton: ziehbares Element, mit dem der hinterlegte Scan direkt aus dem Browser in ein Mail-Fenster (Anhang) oder den Datei-Explorer gezogen werden kann (Chromium- DownloadURL, Format <mime>:<name>:<absolute-url>). Bewusste Plattform-Grenze: PDF per Strg+V als Datei einfuegen geht im Browser nicht (Web-Clipboard darf keine OS-Datei-Zwischenablage befuellen) -> Drag-and-Drop. DownloadURL nur Chrome/Edge, nicht Firefox (dort Klick-Fallback: Datei im Tab oeffnen). Eingebaut an: ContractForm (neben IBAN/Ausweisnummer-Copy), Vertrags- ansicht (Bankkarte/Ausweis-Card), Kundenakte-Tabs Bankkarten+Ausweise. Nur bei vorhandenem documentPath. Nutzt bestehende fileUrl()-Download- URL (Token als Query, Per-File-Ownership-Check unveraendert) - kein neuer Endpoint, keine neue Angriffsflaeche. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,24 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🖱️ PDF-Drag-and-Drop aus Bankkarte/Ausweis heraus** (2026-07-30)
|
||||||
|
- Neues wiederverwendbares `PdfDragButton`: ziehbares „📄 PDF ziehen"-
|
||||||
|
Element, mit dem der hinterlegte Scan direkt aus dem Browser in ein
|
||||||
|
Mail-Fenster (Anhang) oder den Datei-Explorer gezogen werden kann
|
||||||
|
(Chromium-`DownloadURL`, Format `<mime>:<name>:<absolute-url>`).
|
||||||
|
- **Technik-Grenze bewusst:** „PDF per Strg+V als DATEI einfügen" geht im
|
||||||
|
Browser NICHT (Web-Clipboard darf keine OS-Datei-Zwischenablage/CF_HDROP
|
||||||
|
befüllen) → Drag-and-Drop statt Clipboard. `DownloadURL` nur Chrome/Edge,
|
||||||
|
nicht Firefox (dort Klick-Fallback: Datei im Tab öffnen).
|
||||||
|
- Eingebaut an: Vertrag-Neu/Bearbeiten (`ContractForm`, neben IBAN-/
|
||||||
|
Ausweisnummer-Copy-Button), Vertragsansicht (`ContractDetail`, Bankkarte-
|
||||||
|
+ Ausweis-Card), Kundenakte-Tabs Bankkarten + Ausweise (`CustomerDetail`,
|
||||||
|
neben Anzeigen/Download). Erscheint nur bei vorhandenem `documentPath`.
|
||||||
|
- Nutzt bestehende `fileUrl()`-Download-URL (Token als Query, Per-File-
|
||||||
|
Ownership-Check unverändert) – kein neuer Endpoint, keine neue Angriffs-
|
||||||
|
fläche. Ausweitung auf weitere PDF-Stellen (Rechnungen, Vertragsdoks,
|
||||||
|
Datenschutz) folgt bei Bedarf über dieselbe Komponente.
|
||||||
|
|
||||||
- [x] **📋 Vertragsformular: Copy-Buttons für Bankkarte (IBAN) + Ausweis** (2026-07-30)
|
- [x] **📋 Vertragsformular: Copy-Buttons für Bankkarte (IBAN) + Ausweis** (2026-07-30)
|
||||||
- In „Kundendaten verknüpfen" je ein Kopieren-Button neben dem Label,
|
- In „Kundendaten verknüpfen" je ein Kopieren-Button neben dem Label,
|
||||||
kopiert nur den reinen Wert der aktuell gewählten Option: bei Bankkarte
|
kopiert nur den reinen Wert der aktuell gewählten Option: bei Bankkarte
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { GripVertical, FileDown } from 'lucide-react';
|
||||||
|
import { fileUrl } from '../../utils/fileUrl';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ziehbares Element, mit dem eine im Backend hinterlegte Datei (i.d.R. ein
|
||||||
|
* PDF-Scan) direkt aus dem Browser heraus in eine fremde Anwendung – z.B.
|
||||||
|
* das Anhang-Feld eines Mail-Fensters oder den Datei-Explorer – gezogen
|
||||||
|
* werden kann.
|
||||||
|
*
|
||||||
|
* Technik: Beim `dragstart` setzen wir den Chromium-spezifischen
|
||||||
|
* `DownloadURL`-DataTransfer-Eintrag (`<mime>:<dateiname>:<absolute-url>`).
|
||||||
|
* Chrome/Edge laden die Datei beim Ablegen von dieser URL nach und legen
|
||||||
|
* sie am Zielort als echte Datei ab.
|
||||||
|
*
|
||||||
|
* WICHTIG – Grenzen der Browser-Plattform:
|
||||||
|
* - „PDF per Strg+V als DATEI einfügen" ist im Browser NICHT möglich
|
||||||
|
* (die Web-Zwischenablage darf die OS-Datei-Zwischenablage / CF_HDROP
|
||||||
|
* nicht befüllen). Deshalb Drag-and-Drop statt Clipboard.
|
||||||
|
* - `DownloadURL` funktioniert nur in Chromium-Browsern (Chrome/Edge),
|
||||||
|
* nicht in Firefox. Dort passiert beim Ziehen nichts – der Klick-
|
||||||
|
* Fallback (Datei im neuen Tab öffnen) bleibt aber nutzbar.
|
||||||
|
*/
|
||||||
|
interface PdfDragButtonProps {
|
||||||
|
/** Server-Pfad der Datei (`documentPath`). Ohne Pfad wird nichts gerendert. */
|
||||||
|
path: string | null | undefined;
|
||||||
|
/** Dateiname beim Ablegen. Ohne Endung wird sie aus dem Pfad ergänzt. */
|
||||||
|
filename?: string;
|
||||||
|
label?: string;
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIME_BY_EXT: Record<string, string> = {
|
||||||
|
pdf: 'application/pdf',
|
||||||
|
png: 'image/png',
|
||||||
|
jpg: 'image/jpeg',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
gif: 'image/gif',
|
||||||
|
webp: 'image/webp',
|
||||||
|
};
|
||||||
|
|
||||||
|
function extOf(path: string): string {
|
||||||
|
const clean = path.split('?')[0].split('#')[0];
|
||||||
|
const dot = clean.lastIndexOf('.');
|
||||||
|
return dot === -1 ? '' : clean.slice(dot + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PdfDragButton({
|
||||||
|
path,
|
||||||
|
filename,
|
||||||
|
label = 'PDF ziehen',
|
||||||
|
title,
|
||||||
|
className = '',
|
||||||
|
size = 'sm',
|
||||||
|
}: PdfDragButtonProps) {
|
||||||
|
if (!path) return null;
|
||||||
|
|
||||||
|
const ext = extOf(path);
|
||||||
|
const mime = MIME_BY_EXT[ext] || 'application/octet-stream';
|
||||||
|
const base = (filename && filename.trim()) || 'dokument';
|
||||||
|
// Ungültige Dateinamen-Zeichen entschärfen, Endung sicherstellen.
|
||||||
|
const safeBase = base.replace(/[\\/:*?"<>|]+/g, '_');
|
||||||
|
const name = safeBase.includes('.') ? safeBase : `${safeBase}${ext ? `.${ext}` : ''}`;
|
||||||
|
|
||||||
|
// Absolute URL, damit der Drop in fremde Apps die Datei laden kann.
|
||||||
|
// fileUrl() hängt das Auth-Token als Query-Parameter an (Download).
|
||||||
|
const absolute = `${window.location.origin}${fileUrl(path)}`;
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.DragEvent) => {
|
||||||
|
try {
|
||||||
|
e.dataTransfer.setData('DownloadURL', `${mime}:${name}:${absolute}`);
|
||||||
|
} catch {
|
||||||
|
/* manche Browser werfen bei DownloadURL – dann nur die URI-Fallbacks */
|
||||||
|
}
|
||||||
|
e.dataTransfer.setData('text/uri-list', absolute);
|
||||||
|
e.dataTransfer.setData('text/plain', absolute);
|
||||||
|
e.dataTransfer.effectAllowed = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconSize = size === 'sm' ? 'w-3.5 h-3.5' : 'w-4 h-4';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
draggable
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onClick={() => window.open(fileUrl(path, { inline: true }), '_blank', 'noopener')}
|
||||||
|
title={title || 'In Outlook/Explorer ziehen (Chrome/Edge). Klick: im Tab öffnen.'}
|
||||||
|
className={`inline-flex items-center gap-1 cursor-grab active:cursor-grabbing select-none rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-xs text-gray-600 hover:text-blue-600 hover:border-blue-300 hover:bg-blue-50 transition-colors ${className}`}
|
||||||
|
>
|
||||||
|
<GripVertical className={`${iconSize} text-gray-400`} />
|
||||||
|
<FileDown className={iconSize} />
|
||||||
|
<span>{label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import toast from 'react-hot-toast';
|
|||||||
import JpgToPdfModal from '../../components/ui/JpgToPdfModal';
|
import JpgToPdfModal from '../../components/ui/JpgToPdfModal';
|
||||||
import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations';
|
import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations';
|
||||||
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||||
|
import PdfDragButton from '../../components/ui/PdfDragButton';
|
||||||
import AutosaveDateInput from '../../components/ui/AutosaveDateInput';
|
import AutosaveDateInput from '../../components/ui/AutosaveDateInput';
|
||||||
import { formatDate } from '../../utils/dateFormat';
|
import { formatDate } from '../../utils/dateFormat';
|
||||||
import { useProviderSettings } from '../../hooks/useProviderSettings';
|
import { useProviderSettings } from '../../hooks/useProviderSettings';
|
||||||
@@ -2651,6 +2652,14 @@ export default function ContractDetail() {
|
|||||||
{c.bankCard.iban}
|
{c.bankCard.iban}
|
||||||
<CopyButton value={c.bankCard.iban} />
|
<CopyButton value={c.bankCard.iban} />
|
||||||
</p>
|
</p>
|
||||||
|
{c.bankCard.documentPath && (
|
||||||
|
<div className="mt-1">
|
||||||
|
<PdfDragButton
|
||||||
|
path={c.bankCard.documentPath}
|
||||||
|
filename={`Bankkarte-${c.bankCard.iban}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{c.bankCard.bankName && <p className="text-gray-500">{c.bankCard.bankName}</p>}
|
{c.bankCard.bankName && <p className="text-gray-500">{c.bankCard.bankName}</p>}
|
||||||
{c.bankCard.description && (
|
{c.bankCard.description && (
|
||||||
<p className="text-sm text-gray-600 mt-1 italic whitespace-pre-line">
|
<p className="text-sm text-gray-600 mt-1 italic whitespace-pre-line">
|
||||||
@@ -2666,6 +2675,14 @@ export default function ContractDetail() {
|
|||||||
<CopyButton value={c.identityDocument.documentNumber} />
|
<CopyButton value={c.identityDocument.documentNumber} />
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500">{c.identityDocument.type}</p>
|
<p className="text-gray-500">{c.identityDocument.type}</p>
|
||||||
|
{c.identityDocument.documentPath && (
|
||||||
|
<div className="mt-1">
|
||||||
|
<PdfDragButton
|
||||||
|
path={c.identityDocument.documentPath}
|
||||||
|
filename={`Ausweis-${c.identityDocument.documentNumber}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from '../../components/ui/Button';
|
|||||||
import Input from '../../components/ui/Input';
|
import Input from '../../components/ui/Input';
|
||||||
import Select from '../../components/ui/Select';
|
import Select from '../../components/ui/Select';
|
||||||
import CopyButton from '../../components/ui/CopyButton';
|
import CopyButton from '../../components/ui/CopyButton';
|
||||||
|
import PdfDragButton from '../../components/ui/PdfDragButton';
|
||||||
import CustomerInfoModal from '../../components/contracts/CustomerInfoModal';
|
import CustomerInfoModal from '../../components/contracts/CustomerInfoModal';
|
||||||
import { buildContractLabelParts } from '../../utils/contractLabel';
|
import { buildContractLabelParts } from '../../utils/contractLabel';
|
||||||
import type { ContractType } from '../../types';
|
import type { ContractType } from '../../types';
|
||||||
@@ -978,6 +979,12 @@ export default function ContractForm() {
|
|||||||
{selectedBankCard && (
|
{selectedBankCard && (
|
||||||
<CopyButton value={selectedBankCard.iban} title="IBAN kopieren" />
|
<CopyButton value={selectedBankCard.iban} title="IBAN kopieren" />
|
||||||
)}
|
)}
|
||||||
|
{selectedBankCard?.documentPath && (
|
||||||
|
<PdfDragButton
|
||||||
|
path={selectedBankCard.documentPath}
|
||||||
|
filename={`Bankkarte-${selectedBankCard.iban}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
{...register('bankCardId')}
|
{...register('bankCardId')}
|
||||||
@@ -1004,6 +1011,12 @@ export default function ContractForm() {
|
|||||||
{selectedDocument && (
|
{selectedDocument && (
|
||||||
<CopyButton value={selectedDocument.documentNumber} title="Ausweisnummer kopieren" />
|
<CopyButton value={selectedDocument.documentNumber} title="Ausweisnummer kopieren" />
|
||||||
)}
|
)}
|
||||||
|
{selectedDocument?.documentPath && (
|
||||||
|
<PdfDragButton
|
||||||
|
path={selectedDocument.documentPath}
|
||||||
|
filename={`Ausweis-${selectedDocument.documentNumber}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
{...register('identityDocumentId')}
|
{...register('identityDocumentId')}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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, Images } 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 JpgToPdfModal from '../../components/ui/JpgToPdfModal';
|
||||||
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||||
|
import PdfDragButton from '../../components/ui/PdfDragButton';
|
||||||
import BirthdayManagementModal from '../../components/BirthdayManagementModal';
|
import BirthdayManagementModal from '../../components/BirthdayManagementModal';
|
||||||
import { formatDate } from '../../utils/dateFormat';
|
import { formatDate } from '../../utils/dateFormat';
|
||||||
import { getContractTypeInfo } from '../../utils/contractInfo';
|
import { getContractTypeInfo } from '../../utils/contractInfo';
|
||||||
@@ -1002,6 +1003,10 @@ function BankCardsTab({
|
|||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
|
<PdfDragButton
|
||||||
|
path={card.documentPath}
|
||||||
|
filename={`Bankkarte-${card.iban}`}
|
||||||
|
/>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<>
|
<>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
@@ -1238,6 +1243,10 @@ function DocumentsTab({
|
|||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
|
<PdfDragButton
|
||||||
|
path={doc.documentPath}
|
||||||
|
filename={`Ausweis-${doc.documentNumber}`}
|
||||||
|
/>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<>
|
<>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
|
|||||||
Reference in New Issue
Block a user