PDF-Viewer-Modal fuer Bankkarte-/Ausweis-Dokument
Neue Komponente PdfViewerModal (Modal + iframe auf viewUrl(documentPath), inkl. Neuer-Tab/Download-Links). Buttons: - Vertragsansicht (ContractDetail): im Card-Header von Bankkarte/Ausweis - Vertrag bearbeiten (ContractForm): neben dem Label Bankkarte/Ausweis Button erscheint NUR wenn ein documentPath hinterlegt/gewaehlt ist. Nutzt den bestehenden /api/files/download-Endpoint (Per-File-Ownership-Check) - kein neuer Zugriffspfad. iframe (nicht embed/object wegen object-src 'none'); same-origin via CSP default-src 'self' erlaubt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,18 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **📄 PDF-Viewer-Modal für Bankkarte-/Ausweis-Dokument** (2026-08-13)
|
||||||
|
- Neue wiederverwendbare Komponente `PdfViewerModal` (Modal + `<iframe>` auf
|
||||||
|
`viewUrl(documentPath)`, inkl. „In neuem Tab" + „Herunterladen"). Nutzt den
|
||||||
|
bestehenden `/api/files/download`-Endpoint (Per-File-Ownership-Check) – kein
|
||||||
|
neuer Zugriffspfad. `<iframe>` erlaubt via CSP `default-src 'self'`
|
||||||
|
(`object-src 'none'` bleibt, daher iframe statt embed/object).
|
||||||
|
- **Vertragsansicht** (ContractDetail): Button (FileText-Icon) im Card-Header
|
||||||
|
von „Bankkarte"/„Ausweis" – **nur wenn `documentPath` gesetzt**.
|
||||||
|
- **Vertrag bearbeiten** (ContractForm): Button neben dem Label „Bankkarte"/
|
||||||
|
„Ausweis" – **nur wenn die aktuell gewählte Karte/Ausweis ein `documentPath`
|
||||||
|
hat** (`e.preventDefault/stopPropagation`, da im `<label>`).
|
||||||
|
|
||||||
- [x] **📇 Vertragslisten: Karteninhaber + Mobilfunknetz + Kündigung (rot)** (2026-08-13)
|
- [x] **📇 Vertragslisten: Karteninhaber + Mobilfunknetz + Kündigung (rot)** (2026-08-13)
|
||||||
- In **beiden** Vertragslisten (Kundenakte-Baum + Hauptmenü `/contracts`): bei
|
- In **beiden** Vertragslisten (Kundenakte-Baum + Hauptmenü `/contracts`): bei
|
||||||
Mobilfunkverträgen mit Rufnummer zusätzlich **Karteninhaber** (`SimCard.cardUser`
|
Mobilfunkverträgen mit Rufnummer zusätzlich **Karteninhaber** (`SimCard.cardUser`
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import Modal from './Modal';
|
||||||
|
import { viewUrl, fileUrl } from '../../utils/fileUrl';
|
||||||
|
import { ExternalLink, Download } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PdfViewerModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title: string;
|
||||||
|
/** Relativer Upload-Pfad des Dokuments (documentPath). */
|
||||||
|
path: string | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt ein hochgeladenes PDF (Bankkarte/Ausweis) inline in einem Modal.
|
||||||
|
*
|
||||||
|
* Die Datei wird über `viewUrl(path)` geladen → `/api/files/download` mit
|
||||||
|
* `disposition=inline` und Token als Query-Param (iframe/GET, kein
|
||||||
|
* Authorization-Header möglich). Der Backend-Controller macht weiterhin den
|
||||||
|
* Per-File-Ownership-Check – hier wird kein neuer Zugriffspfad geöffnet.
|
||||||
|
*/
|
||||||
|
export default function PdfViewerModal({ isOpen, onClose, title, path }: PdfViewerModalProps) {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} title={title} size="xl">
|
||||||
|
{path ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-end gap-4 text-sm">
|
||||||
|
<a
|
||||||
|
href={viewUrl(path)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" /> In neuem Tab öffnen
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={fileUrl(path)}
|
||||||
|
className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" /> Herunterladen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<iframe
|
||||||
|
src={viewUrl(path)}
|
||||||
|
title={title}
|
||||||
|
className="w-full h-[75vh] border border-gray-200 rounded bg-gray-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500">Kein Dokument hinterlegt.</p>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import Button from '../../components/ui/Button';
|
|||||||
import Badge from '../../components/ui/Badge';
|
import Badge from '../../components/ui/Badge';
|
||||||
import Input from '../../components/ui/Input';
|
import Input from '../../components/ui/Input';
|
||||||
import Modal from '../../components/ui/Modal';
|
import Modal from '../../components/ui/Modal';
|
||||||
|
import PdfViewerModal from '../../components/ui/PdfViewerModal';
|
||||||
import FileUpload from '../../components/ui/FileUpload';
|
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, Images } 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 toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -1551,6 +1552,9 @@ export default function ContractDetail() {
|
|||||||
// Kunden-Schnellansicht-Modal
|
// Kunden-Schnellansicht-Modal
|
||||||
const [showCustomerInfo, setShowCustomerInfo] = useState(false);
|
const [showCustomerInfo, setShowCustomerInfo] = useState(false);
|
||||||
|
|
||||||
|
// PDF-Viewer-Modal (Bankkarte-/Ausweis-Dokument)
|
||||||
|
const [pdfModal, setPdfModal] = useState<{ title: string; path: string } | null>(null);
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['contract', id],
|
queryKey: ['contract', id],
|
||||||
queryFn: () => contractApi.getById(contractId),
|
queryFn: () => contractApi.getById(contractId),
|
||||||
@@ -2658,7 +2662,20 @@ export default function ContractDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{c.bankCard && (
|
{c.bankCard && (
|
||||||
<Card title="Bankkarte">
|
<Card
|
||||||
|
title="Bankkarte"
|
||||||
|
actions={c.bankCard.documentPath ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPdfModal({ title: 'Bankkarte – Dokument', path: c.bankCard!.documentPath! })}
|
||||||
|
className="text-gray-400 hover:text-blue-600 p-1 rounded"
|
||||||
|
title="Hinterlegtes Dokument anzeigen (PDF)"
|
||||||
|
aria-label="Bankkarten-Dokument anzeigen"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
) : undefined}
|
||||||
|
>
|
||||||
<p className="font-medium">{c.bankCard.accountHolder}</p>
|
<p className="font-medium">{c.bankCard.accountHolder}</p>
|
||||||
<p className="font-mono flex items-center gap-1">
|
<p className="font-mono flex items-center gap-1">
|
||||||
{c.bankCard.iban}
|
{c.bankCard.iban}
|
||||||
@@ -2673,7 +2690,20 @@ export default function ContractDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{c.identityDocument && (
|
{c.identityDocument && (
|
||||||
<Card title="Ausweis">
|
<Card
|
||||||
|
title="Ausweis"
|
||||||
|
actions={c.identityDocument.documentPath ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPdfModal({ title: 'Ausweis – Dokument', path: c.identityDocument!.documentPath! })}
|
||||||
|
className="text-gray-400 hover:text-blue-600 p-1 rounded"
|
||||||
|
title="Hinterlegtes Dokument anzeigen (PDF)"
|
||||||
|
aria-label="Ausweis-Dokument anzeigen"
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
) : undefined}
|
||||||
|
>
|
||||||
<p className="font-mono flex items-center gap-1">
|
<p className="font-mono flex items-center gap-1">
|
||||||
{c.identityDocument.documentNumber}
|
{c.identityDocument.documentNumber}
|
||||||
<CopyButton value={c.identityDocument.documentNumber} />
|
<CopyButton value={c.identityDocument.documentNumber} />
|
||||||
@@ -3372,6 +3402,14 @@ export default function ContractDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* PDF-Viewer (Bankkarte-/Ausweis-Dokument) */}
|
||||||
|
<PdfViewerModal
|
||||||
|
isOpen={!!pdfModal}
|
||||||
|
onClose={() => setPdfModal(null)}
|
||||||
|
title={pdfModal?.title || 'Dokument'}
|
||||||
|
path={pdfModal?.path}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Vorgängervertrag Modal */}
|
{/* Vorgängervertrag Modal */}
|
||||||
{showPredecessorModal && c.previousContract && (
|
{showPredecessorModal && c.previousContract && (
|
||||||
<ContractDetailModal
|
<ContractDetailModal
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { buildContractLabelParts } from '../../utils/contractLabel';
|
|||||||
import type { ContractType } from '../../types';
|
import type { ContractType } from '../../types';
|
||||||
import { formatDate } from '../../utils/dateFormat';
|
import { formatDate } from '../../utils/dateFormat';
|
||||||
import { useProviderSettings } from '../../hooks/useProviderSettings';
|
import { useProviderSettings } from '../../hooks/useProviderSettings';
|
||||||
import { Plus, Trash2, Eye, EyeOff, Info, X, ArrowLeft, ExternalLink, Mail, Copy } from 'lucide-react';
|
import { Plus, Trash2, Eye, EyeOff, Info, X, ArrowLeft, ExternalLink, Mail, Copy, FileText } from 'lucide-react';
|
||||||
|
import PdfViewerModal from '../../components/ui/PdfViewerModal';
|
||||||
|
|
||||||
// Kleine Helper-Komponente: Label + Mini-Link im neuen Tab. Wird neben
|
// Kleine Helper-Komponente: Label + Mini-Link im neuen Tab. Wird neben
|
||||||
// Select-Feldern eingesetzt, deren Stammdaten an anderer Stelle gepflegt
|
// Select-Feldern eingesetzt, deren Stammdaten an anderer Stelle gepflegt
|
||||||
@@ -244,6 +245,8 @@ export default function ContractForm() {
|
|||||||
// Passwort-Sichtbarkeit
|
// Passwort-Sichtbarkeit
|
||||||
const [showPortalPassword, setShowPortalPassword] = useState(false);
|
const [showPortalPassword, setShowPortalPassword] = useState(false);
|
||||||
const [showCustomerInfo, setShowCustomerInfo] = useState(false);
|
const [showCustomerInfo, setShowCustomerInfo] = useState(false);
|
||||||
|
// PDF-Viewer-Modal (Bankkarte-/Ausweis-Dokument der aktuell gewählten Karte)
|
||||||
|
const [pdfModal, setPdfModal] = useState<{ title: string; path: string } | null>(null);
|
||||||
const [showInternetPassword, setShowInternetPassword] = useState(false);
|
const [showInternetPassword, setShowInternetPassword] = useState(false);
|
||||||
const [showSipPasswords, setShowSipPasswords] = useState<Record<number, boolean>>({});
|
const [showSipPasswords, setShowSipPasswords] = useState<Record<number, boolean>>({});
|
||||||
const [showSimPins, setShowSimPins] = useState<Record<number, boolean>>({});
|
const [showSimPins, setShowSimPins] = useState<Record<number, boolean>>({});
|
||||||
@@ -1044,6 +1047,17 @@ export default function ContractForm() {
|
|||||||
<LabelWithLink to={`/customers/${customerId}?tab=bankcards`} title="Bankkarten des Kunden in neuem Tab öffnen">
|
<LabelWithLink to={`/customers/${customerId}?tab=bankcards`} title="Bankkarten des Kunden in neuem Tab öffnen">
|
||||||
Bankkarte
|
Bankkarte
|
||||||
</LabelWithLink>
|
</LabelWithLink>
|
||||||
|
{selectedBankCard?.documentPath && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setPdfModal({ title: 'Bankkarte – Dokument', path: selectedBankCard.documentPath! }); }}
|
||||||
|
className="text-gray-400 hover:text-blue-600 p-0.5 rounded"
|
||||||
|
title="Hinterlegtes Dokument anzeigen (PDF)"
|
||||||
|
aria-label="Bankkarten-Dokument anzeigen"
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{selectedBankCard && (
|
{selectedBankCard && (
|
||||||
<CopyButton value={selectedBankCard.iban} title="IBAN kopieren" />
|
<CopyButton value={selectedBankCard.iban} title="IBAN kopieren" />
|
||||||
)}
|
)}
|
||||||
@@ -1070,6 +1084,17 @@ export default function ContractForm() {
|
|||||||
<LabelWithLink to={`/customers/${customerId}?tab=documents`} title="Ausweise des Kunden in neuem Tab öffnen">
|
<LabelWithLink to={`/customers/${customerId}?tab=documents`} title="Ausweise des Kunden in neuem Tab öffnen">
|
||||||
Ausweis
|
Ausweis
|
||||||
</LabelWithLink>
|
</LabelWithLink>
|
||||||
|
{selectedDocument?.documentPath && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setPdfModal({ title: 'Ausweis – Dokument', path: selectedDocument.documentPath! }); }}
|
||||||
|
className="text-gray-400 hover:text-blue-600 p-0.5 rounded"
|
||||||
|
title="Hinterlegtes Dokument anzeigen (PDF)"
|
||||||
|
aria-label="Ausweis-Dokument anzeigen"
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{selectedDocument && (
|
{selectedDocument && (
|
||||||
<CopyButton value={selectedDocument.documentNumber} title="Ausweisnummer kopieren" />
|
<CopyButton value={selectedDocument.documentNumber} title="Ausweisnummer kopieren" />
|
||||||
)}
|
)}
|
||||||
@@ -2007,6 +2032,14 @@ export default function ContractForm() {
|
|||||||
contractStressfreiEmailId={selectedStressfreiEmailId ? parseInt(selectedStressfreiEmailId) : null}
|
contractStressfreiEmailId={selectedStressfreiEmailId ? parseInt(selectedStressfreiEmailId) : null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* PDF-Viewer (Bankkarte-/Ausweis-Dokument der gewählten Karte) */}
|
||||||
|
<PdfViewerModal
|
||||||
|
isOpen={!!pdfModal}
|
||||||
|
onClose={() => setPdfModal(null)}
|
||||||
|
title={pdfModal?.title || 'Dokument'}
|
||||||
|
path={pdfModal?.path}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user