complete new audit system
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
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 Modal from '../ui/Modal';
|
||||
import Button from '../ui/Button';
|
||||
@@ -93,7 +94,7 @@ export default function InvoicesSection({
|
||||
{/* Collapsed view - show latest invoice */}
|
||||
{!isExpanded && sortedInvoices.length > 0 && (
|
||||
<div className="text-sm text-gray-600">
|
||||
Letzte: {new Date(sortedInvoices[0].invoiceDate).toLocaleDateString('de-DE')} - {invoiceTypeLabels[sortedInvoices[0].invoiceType]}
|
||||
Letzte: {formatDate(sortedInvoices[0].invoiceDate)} - {invoiceTypeLabels[sortedInvoices[0].invoiceType]}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -108,7 +109,7 @@ export default function InvoicesSection({
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
{new Date(invoice.invoiceDate).toLocaleDateString('de-DE')}
|
||||
{formatDate(invoice.invoiceDate)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{invoiceTypeLabels[invoice.invoiceType]}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Search, FileText } from 'lucide-react';
|
||||
import Modal from '../ui/Modal';
|
||||
import Button from '../ui/Button';
|
||||
import { contractApi, cachedEmailApi, CachedEmail } from '../../services/api';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
interface AssignToContractModalProps {
|
||||
@@ -69,9 +70,9 @@ export default function AssignToContractModal({
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr?: string) => {
|
||||
const formatDateOrDash = (dateStr?: string) => {
|
||||
if (!dateStr) return '-';
|
||||
return new Date(dateStr).toLocaleDateString('de-DE');
|
||||
return formatDate(dateStr);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -147,7 +148,7 @@ export default function AssignToContractModal({
|
||||
{contract.provider && ` - ${contract.provider.name}`}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Start: {formatDate(contract.startDate)}
|
||||
Start: {formatDateOrDash(contract.startDate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
ExternalLink,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import type { CockpitContract, CockpitUrgencyLevel, ContractType } from '../../types';
|
||||
|
||||
const typeIcons: Record<ContractType, typeof Zap> = {
|
||||
@@ -570,7 +571,7 @@ export default function ContractCockpit() {
|
||||
</Badge>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{new Date(alert.expiryDate).toLocaleDateString('de-DE')}
|
||||
{formatDate(alert.expiryDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -623,7 +624,7 @@ export default function ContractCockpit() {
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">
|
||||
Zähler {reading.meter.meterNumber} – <strong>{reading.value} {reading.unit}</strong> am{' '}
|
||||
{new Date(reading.readingDate).toLocaleDateString('de-DE')}
|
||||
{formatDate(reading.readingDate)}
|
||||
{reading.notes && ` – ${reading.notes}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ 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 } from 'lucide-react';
|
||||
import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations';
|
||||
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask, ContractMeter } from '../../types';
|
||||
|
||||
const typeLabels: Record<ContractType, string> = {
|
||||
@@ -273,8 +274,8 @@ function MeterReadingsSection({
|
||||
className="flex justify-between items-center text-sm group py-1 border-b border-gray-200 last:border-0"
|
||||
>
|
||||
<span className="text-gray-500 flex items-center gap-1">
|
||||
{new Date(reading.readingDate).toLocaleDateString('de-DE')}
|
||||
<CopyButton value={new Date(reading.readingDate).toLocaleDateString('de-DE')} />
|
||||
{formatDate(reading.readingDate)}
|
||||
<CopyButton value={formatDate(reading.readingDate)} />
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono flex items-center gap-1">
|
||||
@@ -315,7 +316,7 @@ function MeterReadingsSection({
|
||||
|
||||
{!isExpanded && readings.length > 0 && (
|
||||
<p className="text-sm text-gray-500">
|
||||
Letzter Stand: {sortedReadings[0].value.toLocaleString('de-DE')} {sortedReadings[0].unit} ({new Date(sortedReadings[0].readingDate).toLocaleDateString('de-DE')})
|
||||
Letzter Stand: {sortedReadings[0].value.toLocaleString('de-DE')} {sortedReadings[0].unit} ({formatDate(sortedReadings[0].readingDate)})
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1033,8 +1034,8 @@ function ContractTaskItem({
|
||||
<p className="text-xs text-gray-400">
|
||||
{subtask.createdBy && `${subtask.createdBy} • `}
|
||||
{isSubtaskCompleted
|
||||
? `Erledigt am ${subtask.completedAt ? new Date(subtask.completedAt).toLocaleDateString('de-DE') : new Date(subtask.updatedAt).toLocaleDateString('de-DE')}`
|
||||
: new Date(subtask.createdAt).toLocaleDateString('de-DE')}
|
||||
? `Erledigt am ${subtask.completedAt ? formatDate(subtask.completedAt) : formatDate(subtask.updatedAt)}`
|
||||
: formatDate(subtask.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1077,8 +1078,8 @@ function ContractTaskItem({
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{task.createdBy && `${task.createdBy} • `}
|
||||
{isCompleted
|
||||
? `Erledigt am ${task.completedAt ? new Date(task.completedAt).toLocaleDateString('de-DE') : '-'}`
|
||||
: new Date(task.createdAt).toLocaleDateString('de-DE')}
|
||||
? `Erledigt am ${task.completedAt ? formatDate(task.completedAt) : '-'}`
|
||||
: formatDate(task.createdAt)}
|
||||
</p>
|
||||
|
||||
{/* Subtasks */}
|
||||
@@ -1713,7 +1714,7 @@ export default function ContractDetail() {
|
||||
{c.nextReviewDate && new Date(c.nextReviewDate) > new Date() && (
|
||||
<div className="flex items-center gap-1 px-2 py-1 bg-amber-100 text-amber-800 rounded-full text-xs">
|
||||
<BellOff className="w-3 h-3" />
|
||||
<span>Zurückgestellt bis {new Date(c.nextReviewDate).toLocaleDateString('de-DE')}</span>
|
||||
<span>Zurückgestellt bis {formatDate(c.nextReviewDate)}</span>
|
||||
{hasPermission('contracts:update') && (
|
||||
<button
|
||||
onClick={() => setShowUnsnoozeConfirm(true)}
|
||||
@@ -2616,7 +2617,7 @@ export default function ContractDetail() {
|
||||
{c.internetDetails.installationDate && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Installation</dt>
|
||||
<dd>{new Date(c.internetDetails.installationDate).toLocaleDateString('de-DE')}</dd>
|
||||
<dd>{formatDate(c.internetDetails.installationDate)}</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.internetDetails.homeId && (
|
||||
@@ -2891,7 +2892,7 @@ export default function ContractDetail() {
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Erstzulassung</dt>
|
||||
<dd>
|
||||
{new Date(c.carInsuranceDetails.firstRegistration).toLocaleDateString('de-DE')}
|
||||
{formatDate(c.carInsuranceDetails.firstRegistration)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import Button from '../../components/ui/Button';
|
||||
import Input from '../../components/ui/Input';
|
||||
import Select from '../../components/ui/Select';
|
||||
import type { ContractType } from '../../types';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import { Plus, Trash2, Eye, EyeOff, Info, X, ArrowLeft } from 'lucide-react';
|
||||
|
||||
// Contract types are now loaded dynamically from the database
|
||||
@@ -724,7 +725,7 @@ export default function ContractForm() {
|
||||
{...register('previousContractId')}
|
||||
options={predecessorContracts.map((c) => ({
|
||||
value: c.id,
|
||||
label: `${c.contractNumber} (${c.type}${c.startDate ? ` - ${new Date(c.startDate).toLocaleDateString('de-DE')}` : ''})`,
|
||||
label: `${c.contractNumber} (${c.type}${c.startDate ? ` - ${formatDate(c.startDate)}` : ''})`,
|
||||
}))}
|
||||
placeholder="Keinen Vorgänger auswählen"
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import Badge from '../../components/ui/Badge';
|
||||
import CopyButton from '../../components/ui/CopyButton';
|
||||
import { Plus, Search, Eye, Edit, Trash2, User, Users, ChevronDown, ChevronRight, Info, X, ShieldAlert } from 'lucide-react';
|
||||
import { gdprApi } from '../../services/api';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import type { Contract, ContractType, ContractStatus } from '../../types';
|
||||
|
||||
const typeLabels: Record<ContractType, string> = {
|
||||
@@ -307,9 +308,9 @@ export default function ContractList() {
|
||||
)}
|
||||
{contract.startDate && (
|
||||
<p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||
Beginn: {new Date(contract.startDate).toLocaleDateString('de-DE')}
|
||||
Beginn: {formatDate(contract.startDate)}
|
||||
{contract.endDate &&
|
||||
` | Ende: ${new Date(contract.endDate).toLocaleDateString('de-DE')}`}
|
||||
` | Ende: ${formatDate(contract.endDate)}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -491,7 +492,7 @@ export default function ContractList() {
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{contract.startDate
|
||||
? new Date(contract.startDate).toLocaleDateString('de-DE')
|
||||
? formatDate(contract.startDate)
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
|
||||
@@ -15,6 +15,7 @@ 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 } from 'lucide-react';
|
||||
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import type { Address, BankCard, IdentityDocument, Meter, Customer, CustomerRepresentative, CustomerSummary, CustomerConsent, ConsentType, ConsentStatus, RepresentativeAuthorization } from '../../types';
|
||||
|
||||
export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?: number } = {}) {
|
||||
@@ -887,7 +888,7 @@ function BankCardsTab({
|
||||
)}
|
||||
{card.expiryDate && (
|
||||
<p className="text-sm text-gray-500">
|
||||
Gültig bis: {new Date(card.expiryDate).toLocaleDateString('de-DE')}
|
||||
Gültig bis: {formatDate(card.expiryDate)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1378,8 +1379,8 @@ function MetersTab({
|
||||
{(isExpanded ? sortedReadings : sortedReadings.slice(0, 3)).map((reading) => (
|
||||
<div key={reading.id} className="flex justify-between items-center text-sm group">
|
||||
<span className="text-gray-500 flex items-center gap-1">
|
||||
{new Date(reading.readingDate).toLocaleDateString('de-DE')}
|
||||
<CopyButton value={new Date(reading.readingDate).toLocaleDateString('de-DE')} />
|
||||
{formatDate(reading.readingDate)}
|
||||
<CopyButton value={formatDate(reading.readingDate)} />
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono flex items-center gap-1">
|
||||
@@ -1664,9 +1665,9 @@ function ContractsTab({
|
||||
)}
|
||||
{contract.startDate && (
|
||||
<p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||
Beginn: {new Date(contract.startDate).toLocaleDateString('de-DE')}
|
||||
Beginn: {formatDate(contract.startDate)}
|
||||
{contract.endDate &&
|
||||
` | Ende: ${new Date(contract.endDate).toLocaleDateString('de-DE')}`}
|
||||
` | Ende: ${formatDate(contract.endDate)}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -3630,8 +3631,8 @@ const CONSENT_TYPE_LABELS: Record<ConsentType, { label: string; description: str
|
||||
description: 'Grundlegende Verarbeitung personenbezogener Daten zur Vertragserfüllung',
|
||||
},
|
||||
MARKETING_EMAIL: {
|
||||
label: 'E-Mail-Marketing',
|
||||
description: 'Zusendung von Werbung und Angeboten per E-Mail',
|
||||
label: 'Elektronisches Marketing',
|
||||
description: 'Zusendung von Werbung und Angeboten über elektronische Kommunikationswege (E-Mail, Messenger etc.)',
|
||||
},
|
||||
MARKETING_PHONE: {
|
||||
label: 'Telefonmarketing',
|
||||
|
||||
@@ -64,17 +64,17 @@ export default function CustomerForm() {
|
||||
// Only include the fields that can be updated - exclude relations and read-only fields
|
||||
const submitData: any = {
|
||||
type: data.type,
|
||||
salutation: data.salutation || undefined,
|
||||
salutation: data.salutation || '',
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
companyName: data.companyName || undefined,
|
||||
email: data.email || undefined,
|
||||
phone: data.phone || undefined,
|
||||
mobile: data.mobile || undefined,
|
||||
taxNumber: data.taxNumber || undefined,
|
||||
commercialRegisterNumber: data.commercialRegisterNumber || undefined,
|
||||
notes: data.notes || undefined,
|
||||
birthPlace: data.birthPlace || undefined,
|
||||
companyName: data.companyName || '',
|
||||
email: data.email || '',
|
||||
phone: data.phone || '',
|
||||
mobile: data.mobile || '',
|
||||
taxNumber: data.taxNumber || '',
|
||||
commercialRegisterNumber: data.commercialRegisterNumber || '',
|
||||
notes: data.notes || '',
|
||||
birthPlace: data.birthPlace || '',
|
||||
};
|
||||
|
||||
// Handle birthDate - convert non-empty string to ISO string, or null to clear
|
||||
|
||||
@@ -18,8 +18,8 @@ const CONSENT_TYPE_LABELS: Record<ConsentType, { label: string; description: str
|
||||
description: 'Grundlegende Verarbeitung personenbezogener Daten zur Vertragserfüllung',
|
||||
},
|
||||
MARKETING_EMAIL: {
|
||||
label: 'E-Mail-Marketing',
|
||||
description: 'Zusendung von Werbung und Angeboten per E-Mail',
|
||||
label: 'Elektronisches Marketing',
|
||||
description: 'Zusendung von Werbung und Angeboten über elektronische Kommunikationswege (E-Mail, Messenger etc.)',
|
||||
},
|
||||
MARKETING_PHONE: {
|
||||
label: 'Telefonmarketing',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { publicApi } from '../../services/api';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import { Shield, CheckCircle2, FileDown, Loader2 } from 'lucide-react';
|
||||
|
||||
export default function ConsentPage() {
|
||||
@@ -108,7 +109,7 @@ export default function ConsentPage() {
|
||||
<span>{c.label}</span>
|
||||
{c.grantedAt && (
|
||||
<span className="text-green-500">
|
||||
(am {new Date(c.grantedAt).toLocaleDateString('de-DE')})
|
||||
(am {formatDate(c.grantedAt)})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { auditLogApi, AuditLogSearchParams } from '../../services/api';
|
||||
import type { AuditLog, AuditAction, AuditSensitivity } from '../../types';
|
||||
@@ -7,7 +7,7 @@ import Card from '../../components/ui/Card';
|
||||
import Button from '../../components/ui/Button';
|
||||
import Input from '../../components/ui/Input';
|
||||
import Select from '../../components/ui/Select';
|
||||
import { ArrowLeft, Download, Eye, Shield, ShieldCheck, ShieldAlert, RefreshCw, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
||||
import { ArrowLeft, Download, Eye, Shield, ShieldAlert, RefreshCw, ChevronLeft, ChevronRight, X } from 'lucide-react';
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ value: '', label: 'Alle Aktionen' },
|
||||
@@ -82,6 +82,82 @@ function getSensitivityIcon(sensitivity: AuditSensitivity) {
|
||||
}
|
||||
}
|
||||
|
||||
const RESOURCE_TYPE_LABELS: Record<string, string> = {
|
||||
Customer: 'Kunde',
|
||||
Contract: 'Vertrag',
|
||||
BankCard: 'Bankverbindung',
|
||||
IdentityDocument: 'Ausweis',
|
||||
Address: 'Adresse',
|
||||
Meter: 'Zähler',
|
||||
MeterReading: 'Zählerstand',
|
||||
User: 'Benutzer',
|
||||
CustomerConsent: 'Einwilligung',
|
||||
RepresentativeAuthorization: 'Vollmacht',
|
||||
ContractTask: 'Aufgabe',
|
||||
EnergyContractDetails: 'Strom/Gas-Details',
|
||||
EmailProviderConfig: 'E-Mail-Provider',
|
||||
AppSetting: 'Einstellung',
|
||||
GDPR: 'Datenschutz',
|
||||
Authentication: 'Anmeldung',
|
||||
};
|
||||
|
||||
const FIELD_LABELS: Record<string, string> = {
|
||||
firstName: 'Vorname', lastName: 'Nachname', email: 'E-Mail', phone: 'Telefon', mobile: 'Mobil',
|
||||
salutation: 'Anrede', companyName: 'Firma', type: 'Typ', notes: 'Notizen',
|
||||
street: 'Straße', houseNumber: 'Hausnummer', postalCode: 'PLZ', city: 'Stadt', country: 'Land',
|
||||
meterNumber: 'Zählernummer', location: 'Standort', tariffModel: 'Tarifmodell',
|
||||
isActive: 'Aktiv', isDefault: 'Standard',
|
||||
status: 'Status', source: 'Quelle',
|
||||
contractNumber: 'Vertragsnummer', startDate: 'Vertragsbeginn', endDate: 'Vertragsende',
|
||||
portalUsername: 'Portal-Benutzername', portalEnabled: 'Portal aktiv',
|
||||
basePrice: 'Grundpreis', unitPrice: 'Arbeitspreis', unitPriceNt: 'NT-Arbeitspreis', bonus: 'Bonus',
|
||||
name: 'Name', domain: 'Domain', apiUrl: 'API-URL',
|
||||
documentNumber: 'Dokumentennummer', expiryDate: 'Ablaufdatum',
|
||||
iban: 'IBAN', bic: 'BIC', bankName: 'Bank', accountHolder: 'Kontoinhaber',
|
||||
isGranted: 'Vollmacht erteilt', documentPath: 'Dokument-Pfad',
|
||||
value: 'Zählerstand', valueNt: 'NT-Zählerstand', readingDate: 'Ablesedatum',
|
||||
privacyPolicyPath: 'Datenschutz-PDF',
|
||||
};
|
||||
|
||||
function formatValue(value: unknown): string {
|
||||
if (value === null || value === undefined) return '-';
|
||||
if (typeof value === 'boolean') return value ? 'Ja' : 'Nein';
|
||||
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) {
|
||||
try { return new Date(value).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch { return value; }
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function renderChangeDiff(before: Record<string, unknown>, after: Record<string, unknown>) {
|
||||
const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
||||
const changes: { field: string; from: unknown; to: unknown }[] = [];
|
||||
|
||||
for (const key of allKeys) {
|
||||
// Technische Felder überspringen
|
||||
if (['id', 'createdAt', 'updatedAt', 'passwordEncrypted', 'portalPasswordEncrypted', 'portalPasswordHash', 'systemEmailPasswordEncrypted'].includes(key)) continue;
|
||||
const fromVal = before[key];
|
||||
const toVal = after[key];
|
||||
if (JSON.stringify(fromVal) !== JSON.stringify(toVal)) {
|
||||
changes.push({ field: key, from: fromVal, to: toVal });
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
return <p className="text-sm text-gray-500 italic">Keine sichtbaren Änderungen</p>;
|
||||
}
|
||||
|
||||
return changes.map(({ field, from, to }) => (
|
||||
<div key={field} className="flex items-start gap-3 p-2 bg-gray-50 rounded text-sm">
|
||||
<span className="font-medium text-gray-700 min-w-[150px]">
|
||||
{FIELD_LABELS[field] || field}
|
||||
</span>
|
||||
<span className="text-red-600 line-through">{formatValue(from)}</span>
|
||||
<span className="text-gray-400">→</span>
|
||||
<span className="text-green-700 font-medium">{formatValue(to)}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
interface DetailModalProps {
|
||||
log: AuditLog;
|
||||
onClose: () => void;
|
||||
@@ -126,7 +202,7 @@ function DetailModal({ log, onClose }: DetailModalProps) {
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Ressource</dt>
|
||||
<dd className="font-medium">{log.resourceType} {log.resourceId && `#${log.resourceId}`}</dd>
|
||||
<dd className="font-medium">{RESOURCE_TYPE_LABELS[log.resourceType] || log.resourceType} {log.resourceId && `#${log.resourceId}`}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Endpoint</dt>
|
||||
@@ -161,36 +237,33 @@ function DetailModal({ log, onClose }: DetailModalProps) {
|
||||
{(before || after) && !log.changesEncrypted && (
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-medium mb-3">Änderungen</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{before && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Vorher</h4>
|
||||
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64">
|
||||
{JSON.stringify(before, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{after && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Nachher</h4>
|
||||
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64">
|
||||
{JSON.stringify(after, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{before && after ? (
|
||||
<div className="space-y-2">
|
||||
{renderChangeDiff(before, after)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{before && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Vorher</h4>
|
||||
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64">
|
||||
{JSON.stringify(before, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{after && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Daten</h4>
|
||||
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64">
|
||||
{JSON.stringify(after, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.hash && (
|
||||
<div className="border-t pt-4 mt-4">
|
||||
<h3 className="font-medium mb-2">Integrität</h3>
|
||||
<div className="text-xs font-mono bg-gray-50 p-2 rounded break-all">
|
||||
<div><span className="text-gray-500">Hash:</span> {log.hash}</div>
|
||||
{log.previousHash && <div className="mt-1"><span className="text-gray-500">Vorheriger:</span> {log.previousHash}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end mt-6 pt-4 border-t">
|
||||
<Button variant="secondary" onClick={onClose}>Schließen</Button>
|
||||
@@ -215,19 +288,6 @@ export default function AuditLogs() {
|
||||
queryFn: () => auditLogApi.search({ ...filters, page }),
|
||||
});
|
||||
|
||||
const verifyMutation = useMutation({
|
||||
mutationFn: () => auditLogApi.verifyIntegrity(),
|
||||
onSuccess: (result) => {
|
||||
if (result.data?.valid) {
|
||||
alert('Hash-Kette ist intakt. Keine Manipulationen festgestellt.');
|
||||
} else {
|
||||
alert(`Integritätsfehler gefunden:\n${result.data?.errors?.join('\n')}`);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(`Fehler bei der Integritätsprüfung: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`);
|
||||
},
|
||||
});
|
||||
|
||||
const logs = logsData?.data || [];
|
||||
const pagination = logsData?.pagination;
|
||||
@@ -239,18 +299,30 @@ export default function AuditLogs() {
|
||||
|
||||
const handleExport = async (format: 'json' | 'csv') => {
|
||||
try {
|
||||
const result = await auditLogApi.export({ ...filters, format });
|
||||
const data = result.data;
|
||||
const blob = new Blob(
|
||||
[format === 'json' ? JSON.stringify(data, null, 2) : ''],
|
||||
{ type: format === 'json' ? 'application/json' : 'text/csv' }
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `audit-logs-${new Date().toISOString().split('T')[0]}.${format}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (format === 'csv') {
|
||||
// CSV direkt als Download
|
||||
const token = localStorage.getItem('token');
|
||||
const params = new URLSearchParams();
|
||||
params.set('format', 'csv');
|
||||
if (filters.action) params.set('action', filters.action);
|
||||
if (filters.sensitivity) params.set('sensitivity', filters.sensitivity);
|
||||
if (filters.resourceType) params.set('resourceType', filters.resourceType);
|
||||
if (filters.startDate) params.set('startDate', filters.startDate);
|
||||
if (filters.endDate) params.set('endDate', filters.endDate);
|
||||
window.open(`/api/audit-logs/export?${params}&token=${token}`, '_blank');
|
||||
} else {
|
||||
const result = await auditLogApi.export({ ...filters, format });
|
||||
const blob = new Blob(
|
||||
[JSON.stringify(result.data, null, 2)],
|
||||
{ type: 'application/json' }
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `audit-logs-${new Date().toISOString().split('T')[0]}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Export fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`);
|
||||
}
|
||||
@@ -312,11 +384,11 @@ export default function AuditLogs() {
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => handleExport('json')}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
JSON
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => verifyMutation.mutate()} disabled={verifyMutation.isPending}>
|
||||
<ShieldCheck className="w-4 h-4 mr-2" />
|
||||
{verifyMutation.isPending ? 'Prüfe...' : 'Integrität'}
|
||||
<Button variant="secondary" onClick={() => handleExport('csv')}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
CSV
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1263,7 +1263,11 @@ export const auditLogApi = {
|
||||
return res.data;
|
||||
},
|
||||
verifyIntegrity: async () => {
|
||||
const res = await api.post<ApiResponse<{ valid: boolean; errors: string[] }>>('/audit-logs/verify');
|
||||
const res = await api.post<ApiResponse<{ valid: boolean; checkedCount: number; invalidEntries: number[]; message: string }>>('/audit-logs/verify');
|
||||
return res.data;
|
||||
},
|
||||
rehash: async () => {
|
||||
const res = await api.post<ApiResponse<{ rehashedCount: number }>>('/audit-logs/rehash');
|
||||
return res.data;
|
||||
},
|
||||
getRetentionPolicies: async () => {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Formatiert ein Datum als dd.mm.yyyy (immer mit führenden Nullen)
|
||||
*/
|
||||
export function formatDate(date: string | Date): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert ein Datum als dd.mm.yyyy hh:mm
|
||||
*/
|
||||
export function formatDateTime(date: string | Date): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return d.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user