save email as pdf likae attachment version 2

This commit is contained in:
2026-02-04 19:49:09 +01:00
parent d98c97a81f
commit 2d052c76d9
21 changed files with 1143 additions and 151 deletions
@@ -0,0 +1,194 @@
import { useQuery } from '@tanstack/react-query';
import { contractApi } from '../../services/api';
import Modal from '../ui/Modal';
import Badge from '../ui/Badge';
import Card from '../ui/Card';
import CopyButton from '../ui/CopyButton';
import type { ContractType, ContractStatus } from '../../types';
const typeLabels: Record<ContractType, string> = {
ELECTRICITY: 'Strom',
GAS: 'Gas',
DSL: 'DSL',
CABLE: 'Kabelinternet',
FIBER: 'Glasfaser',
MOBILE: 'Mobilfunk',
TV: 'TV',
CAR_INSURANCE: 'KFZ-Versicherung',
};
const statusLabels: Record<ContractStatus, string> = {
DRAFT: 'Entwurf',
PENDING: 'Ausstehend',
ACTIVE: 'Aktiv',
CANCELLED: 'Gekündigt',
EXPIRED: 'Abgelaufen',
DEACTIVATED: 'Deaktiviert',
};
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
ACTIVE: 'success',
PENDING: 'warning',
CANCELLED: 'danger',
EXPIRED: 'danger',
DRAFT: 'default',
DEACTIVATED: 'default',
};
interface ContractDetailModalProps {
contractId: number;
isOpen: boolean;
onClose: () => void;
}
export default function ContractDetailModal({ contractId, isOpen, onClose }: ContractDetailModalProps) {
const { data, isLoading, error } = useQuery({
queryKey: ['contract', contractId],
queryFn: () => contractApi.getById(contractId),
enabled: isOpen,
});
const c = data?.data;
return (
<Modal isOpen={isOpen} onClose={onClose} title="Vertragsdetails" size="xl">
{isLoading && (
<div className="text-center py-8 text-gray-500">Laden...</div>
)}
{error && (
<div className="text-center py-8 text-red-600">Fehler beim Laden des Vertrags</div>
)}
{c && (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center gap-3 pb-4 border-b">
<span className="text-xl font-bold font-mono flex items-center gap-2">
{c.contractNumber}
<CopyButton value={c.contractNumber} />
</span>
<Badge>{typeLabels[c.type as ContractType] || c.type}</Badge>
<Badge variant={statusVariants[c.status as ContractStatus] || 'default'}>
{statusLabels[c.status as ContractStatus] || c.status}
</Badge>
</div>
{/* Anbieter & Tarif */}
{(c.providerName || c.provider?.name || c.tariffName || c.tariff?.name) && (
<Card title="Anbieter & Tarif">
<div className="grid grid-cols-2 gap-4">
{(c.providerName || c.provider?.name) && (
<div>
<dt className="text-sm text-gray-500">Anbieter</dt>
<dd className="flex items-center gap-1">
{c.providerName || c.provider?.name}
<CopyButton value={c.providerName || c.provider?.name || ''} />
</dd>
</div>
)}
{(c.tariffName || c.tariff?.name) && (
<div>
<dt className="text-sm text-gray-500">Tarif</dt>
<dd className="flex items-center gap-1">
{c.tariffName || c.tariff?.name}
<CopyButton value={c.tariffName || c.tariff?.name || ''} />
</dd>
</div>
)}
{c.customerNumberAtProvider && (
<div>
<dt className="text-sm text-gray-500">Kundennummer beim Anbieter</dt>
<dd className="font-mono flex items-center gap-1">
{c.customerNumberAtProvider}
<CopyButton value={c.customerNumberAtProvider} />
</dd>
</div>
)}
</div>
</Card>
)}
{/* Laufzeit */}
<Card title="Laufzeit">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{c.startDate && (
<div>
<dt className="text-sm text-gray-500">Vertragsbeginn</dt>
<dd>{new Date(c.startDate).toLocaleDateString('de-DE')}</dd>
</div>
)}
{c.endDate && (
<div>
<dt className="text-sm text-gray-500">Vertragsende</dt>
<dd>{new Date(c.endDate).toLocaleDateString('de-DE')}</dd>
</div>
)}
{c.contractDuration && (
<div>
<dt className="text-sm text-gray-500">Laufzeit</dt>
<dd>{c.contractDuration.description}</dd>
</div>
)}
{c.cancellationPeriod && (
<div>
<dt className="text-sm text-gray-500">Kündigungsfrist</dt>
<dd>{c.cancellationPeriod.description}</dd>
</div>
)}
</div>
</Card>
{/* Portal-Zugangsdaten */}
{(c.portalUsername || c.provider?.portalUrl) && (
<Card title="Portal-Zugangsdaten">
<div className="grid grid-cols-2 gap-4">
{c.provider?.portalUrl && (
<div>
<dt className="text-sm text-gray-500">Portal-URL</dt>
<dd>
<a
href={c.provider.portalUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{c.provider.portalUrl}
</a>
</dd>
</div>
)}
{c.portalUsername && (
<div>
<dt className="text-sm text-gray-500">Benutzername</dt>
<dd className="font-mono flex items-center gap-1">
{c.portalUsername}
<CopyButton value={c.portalUsername} />
</dd>
</div>
)}
</div>
</Card>
)}
{/* Adresse */}
{c.address && (
<Card title="Lieferadresse">
<p>
{c.address.street} {c.address.houseNumber}
</p>
<p>{c.address.postalCode} {c.address.city}</p>
</Card>
)}
{/* Notizen */}
{c.notes && (
<Card title="Notizen">
<p className="whitespace-pre-wrap text-gray-700">{c.notes}</p>
</Card>
)}
</div>
)}
</Modal>
);
}
@@ -0,0 +1 @@
export { default as ContractDetailModal } from './ContractDetailModal';
+21 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Reply, Star, Paperclip, Link2, X, Download, ExternalLink, Trash2, Undo2, Save } from 'lucide-react';
import { Reply, Star, Paperclip, Link2, X, Download, ExternalLink, Trash2, Undo2, Save, FileDown } from 'lucide-react';
import { CachedEmail, cachedEmailApi } from '../../services/api';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import Button from '../ui/Button';
@@ -7,6 +7,7 @@ import { Link } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import toast from 'react-hot-toast';
import SaveAttachmentModal from './SaveAttachmentModal';
import SaveEmailAsPdfModal from './SaveEmailAsPdfModal';
interface EmailDetailProps {
email: CachedEmail;
@@ -37,6 +38,7 @@ export default function EmailDetail({
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false);
const [showPermanentDeleteConfirm, setShowPermanentDeleteConfirm] = useState(false);
const [saveAttachmentFilename, setSaveAttachmentFilename] = useState<string | null>(null);
const [showSaveAsPdfModal, setShowSaveAsPdfModal] = useState(false);
const queryClient = useQueryClient();
const { hasPermission } = useAuth();
@@ -219,6 +221,15 @@ export default function EmailDetail({
<Reply className="w-4 h-4 mr-1" />
Antworten
</Button>
{/* E-Mail als PDF speichern */}
<Button
variant="ghost"
size="sm"
onClick={() => setShowSaveAsPdfModal(true)}
title="E-Mail als PDF speichern"
>
<FileDown className="w-4 h-4" />
</Button>
{/* Löschen-Button nur für User mit emails:delete Permission */}
{hasPermission('emails:delete') && (
<Button
@@ -481,6 +492,15 @@ export default function EmailDetail({
attachmentFilename={saveAttachmentFilename}
/>
)}
{/* E-Mail als PDF speichern Modal */}
{showSaveAsPdfModal && (
<SaveEmailAsPdfModal
isOpen={true}
onClose={() => setShowSaveAsPdfModal(false)}
emailId={email.id}
/>
)}
</div>
);
}
+1
View File
@@ -4,3 +4,4 @@ export { default as ComposeEmailModal } from './ComposeEmailModal';
export { default as AssignToContractModal } from './AssignToContractModal';
export { default as EmailClientTab } from './EmailClientTab';
export { default as ContractEmailsSection } from './ContractEmailsSection';
export { default as SaveEmailAsPdfModal } from './SaveEmailAsPdfModal';