The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
543 lines
22 KiB
TypeScript
543 lines
22 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* Löschfristen - Data Retention Management
|
||
*
|
||
* Art. 17 DSGVO - Recht auf Löschung
|
||
* Art. 5 Abs. 1 lit. e DSGVO - Speicherbegrenzung
|
||
*
|
||
* Migriert auf SDK API: /sdk/v1/dsgvo/retention-policies
|
||
*/
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||
|
||
interface RetentionPolicy {
|
||
id: string
|
||
tenant_id: string
|
||
namespace_id?: string
|
||
name: string
|
||
description: string
|
||
data_category: string
|
||
retention_period_days: number
|
||
retention_period_text: string
|
||
legal_basis: string
|
||
legal_reference?: string
|
||
deletion_method: string // automatic, manual, anonymization
|
||
deletion_procedure?: string
|
||
exception_criteria?: string
|
||
applicable_systems?: string[]
|
||
responsible_person: string
|
||
responsible_department: string
|
||
status: string // draft, active, archived
|
||
last_review_at?: string
|
||
next_review_at?: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export default function LoeschfristenPage() {
|
||
const [policies, setPolicies] = useState<RetentionPolicy[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||
const [newPolicy, setNewPolicy] = useState({
|
||
name: '',
|
||
description: '',
|
||
data_category: '',
|
||
retention_period_days: 365,
|
||
retention_period_text: '1 Jahr',
|
||
legal_basis: 'legal_requirement',
|
||
legal_reference: '',
|
||
deletion_method: 'automatic',
|
||
deletion_procedure: '',
|
||
responsible_person: '',
|
||
responsible_department: '',
|
||
status: 'draft'
|
||
})
|
||
|
||
useEffect(() => {
|
||
loadPolicies()
|
||
}, [])
|
||
|
||
async function loadPolicies() {
|
||
setLoading(true)
|
||
setError(null)
|
||
try {
|
||
const res = await fetch('/sdk/v1/dsgvo/retention-policies', {
|
||
headers: {
|
||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||
}
|
||
})
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}`)
|
||
}
|
||
const data = await res.json()
|
||
setPolicies(data.policies || [])
|
||
} catch (err) {
|
||
console.error('Failed to load retention policies:', err)
|
||
setError('Fehler beim Laden der Löschfristen')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
async function createPolicy() {
|
||
try {
|
||
const res = await fetch('/sdk/v1/dsgvo/retention-policies', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||
},
|
||
body: JSON.stringify(newPolicy)
|
||
})
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}`)
|
||
}
|
||
setShowCreateModal(false)
|
||
setNewPolicy({
|
||
name: '',
|
||
description: '',
|
||
data_category: '',
|
||
retention_period_days: 365,
|
||
retention_period_text: '1 Jahr',
|
||
legal_basis: 'legal_requirement',
|
||
legal_reference: '',
|
||
deletion_method: 'automatic',
|
||
deletion_procedure: '',
|
||
responsible_person: '',
|
||
responsible_department: '',
|
||
status: 'draft'
|
||
})
|
||
loadPolicies()
|
||
} catch (err) {
|
||
console.error('Failed to create policy:', err)
|
||
alert('Fehler beim Erstellen der Löschfrist')
|
||
}
|
||
}
|
||
|
||
async function exportPolicies(format: 'csv' | 'json') {
|
||
try {
|
||
const res = await fetch(`/sdk/v1/dsgvo/export/retention?format=${format}`, {
|
||
headers: {
|
||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||
}
|
||
})
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}`)
|
||
}
|
||
const blob = await res.blob()
|
||
const url = window.URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `loeschfristen-export.${format}`
|
||
a.click()
|
||
window.URL.revokeObjectURL(url)
|
||
} catch (err) {
|
||
console.error('Export failed:', err)
|
||
alert('Export fehlgeschlagen')
|
||
}
|
||
}
|
||
|
||
const getStatusBadge = (status: string) => {
|
||
switch (status) {
|
||
case 'active':
|
||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Aktiv</span>
|
||
case 'draft':
|
||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Entwurf</span>
|
||
case 'archived':
|
||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-600">Archiviert</span>
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
const getDeletionMethodBadge = (method: string) => {
|
||
switch (method) {
|
||
case 'automatic':
|
||
return <span className="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs">Auto-Löschung</span>
|
||
case 'manual':
|
||
return <span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">Manuell</span>
|
||
case 'anonymization':
|
||
return <span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs">Anonymisierung</span>
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
const getLegalBasisLabel = (basis: string) => {
|
||
const labels: Record<string, string> = {
|
||
'legal_requirement': 'Gesetzliche Pflicht',
|
||
'consent': 'Einwilligung',
|
||
'legitimate_interest': 'Berechtigtes Interesse',
|
||
'contract': 'Vertragserfüllung',
|
||
}
|
||
return labels[basis] || basis
|
||
}
|
||
|
||
// Group policies by status
|
||
const activePolicies = policies.filter(p => p.status === 'active')
|
||
const draftPolicies = policies.filter(p => p.status === 'draft')
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center h-64">
|
||
<div className="text-slate-500">Lade Löschfristen...</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<PagePurpose
|
||
title="Löschfristen & Datenaufbewahrung"
|
||
purpose="Verwaltung von Aufbewahrungsfristen und automatischen Löschungen gemäß DSGVO Art. 5 (Speicherbegrenzung) und Art. 17 (Recht auf Löschung)."
|
||
audience={['DSB', 'IT-Admin', 'Compliance Officer']}
|
||
gdprArticles={['Art. 5 Abs. 1 lit. e (Speicherbegrenzung)', 'Art. 17 (Recht auf Löschung)']}
|
||
architecture={{
|
||
services: ['AI Compliance SDK (Go)', 'PostgreSQL'],
|
||
databases: ['PostgreSQL'],
|
||
}}
|
||
relatedPages={[
|
||
{ name: 'VVT', href: '/dsgvo/vvt', description: 'Verarbeitungsverzeichnis' },
|
||
{ name: 'DSR', href: '/dsgvo/dsr', description: 'Löschanfragen' },
|
||
{ name: 'TOM', href: '/dsgvo/tom', description: 'Technische Maßnahmen' },
|
||
]}
|
||
collapsible={true}
|
||
defaultCollapsed={true}
|
||
/>
|
||
|
||
{error && (
|
||
<div className="mb-6 bg-red-50 border border-red-200 rounded-xl p-4 text-red-700">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* Header Actions */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div></div>
|
||
<div className="flex items-center gap-3">
|
||
<button
|
||
onClick={() => exportPolicies('csv')}
|
||
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||
>
|
||
CSV Export
|
||
</button>
|
||
<button
|
||
onClick={() => exportPolicies('json')}
|
||
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||
>
|
||
JSON Export
|
||
</button>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700"
|
||
>
|
||
+ Neue Löschfrist
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Stats */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||
<div className="text-2xl font-bold text-slate-900">{policies.length}</div>
|
||
<div className="text-sm text-slate-500">Löschfristen gesamt</div>
|
||
</div>
|
||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||
<div className="text-2xl font-bold text-green-600">{activePolicies.length}</div>
|
||
<div className="text-sm text-slate-500">Aktive Richtlinien</div>
|
||
</div>
|
||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||
<div className="text-2xl font-bold text-yellow-600">{draftPolicies.length}</div>
|
||
<div className="text-sm text-slate-500">Entwürfe</div>
|
||
</div>
|
||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||
<div className="text-2xl font-bold text-purple-600">
|
||
{policies.filter(p => p.deletion_method === 'automatic').length}
|
||
</div>
|
||
<div className="text-sm text-slate-500">Auto-Löschung</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Policies List */}
|
||
{policies.length === 0 ? (
|
||
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
|
||
<div className="text-slate-400 text-4xl mb-4">🗑️</div>
|
||
<h3 className="text-lg font-medium text-slate-800 mb-2">Keine Löschfristen definiert</h3>
|
||
<p className="text-slate-500 mb-4">Legen Sie Aufbewahrungsfristen für verschiedene Datenkategorien an.</p>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700"
|
||
>
|
||
Erste Löschfrist anlegen
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="bg-white rounded-xl border border-slate-200">
|
||
<div className="p-6">
|
||
<h2 className="text-lg font-semibold text-slate-900 mb-6">Aufbewahrungsfristen</h2>
|
||
|
||
<div className="space-y-4">
|
||
{policies.map((policy) => (
|
||
<div key={policy.id} className="border border-slate-200 rounded-lg p-4 hover:border-primary-300 transition-colors">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-grow">
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<h3 className="font-semibold text-slate-900">{policy.name}</h3>
|
||
{getStatusBadge(policy.status)}
|
||
{getDeletionMethodBadge(policy.deletion_method)}
|
||
</div>
|
||
|
||
{policy.description && (
|
||
<p className="text-sm text-slate-600 mb-3">{policy.description}</p>
|
||
)}
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||
<div>
|
||
<span className="text-slate-500">Datenkategorie:</span>
|
||
<span className="ml-1 font-medium text-slate-700">{policy.data_category}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-slate-500">Frist:</span>
|
||
<span className="ml-1 font-medium text-slate-700">{policy.retention_period_text}</span>
|
||
<span className="text-slate-400 ml-1">({policy.retention_period_days} Tage)</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-slate-500">Rechtsgrundlage:</span>
|
||
<span className="ml-1 text-slate-600">{getLegalBasisLabel(policy.legal_basis)}</span>
|
||
</div>
|
||
{policy.legal_reference && (
|
||
<div>
|
||
<span className="text-slate-500">Referenz:</span>
|
||
<span className="ml-1 text-slate-600 font-mono text-xs">{policy.legal_reference}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-3 flex flex-wrap gap-4 text-xs text-slate-500">
|
||
{policy.responsible_person && (
|
||
<span>Verantwortlich: {policy.responsible_person}</span>
|
||
)}
|
||
{policy.responsible_department && (
|
||
<span>Abteilung: {policy.responsible_department}</span>
|
||
)}
|
||
{policy.last_review_at && (
|
||
<span>Letzte Prüfung: {new Date(policy.last_review_at).toLocaleDateString('de-DE')}</span>
|
||
)}
|
||
{policy.next_review_at && (
|
||
<span>Nächste Prüfung: {new Date(policy.next_review_at).toLocaleDateString('de-DE')}</span>
|
||
)}
|
||
</div>
|
||
|
||
{policy.applicable_systems && policy.applicable_systems.length > 0 && (
|
||
<div className="mt-3 flex flex-wrap gap-2">
|
||
{policy.applicable_systems.map((sys, idx) => (
|
||
<span key={idx} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">
|
||
{sys}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Info */}
|
||
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||
<div className="flex gap-3">
|
||
<svg className="w-5 h-5 text-purple-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<div>
|
||
<h4 className="font-semibold text-purple-900">Speicherbegrenzung (Art. 5)</h4>
|
||
<p className="text-sm text-purple-800 mt-1">
|
||
Personenbezogene Daten dürfen nur so lange gespeichert werden, wie es für die Zwecke
|
||
erforderlich ist. Die automatische Löschung stellt die Einhaltung dieser Vorgabe sicher.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Create Modal */}
|
||
{showCreateModal && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||
<div className="p-6 border-b border-slate-200">
|
||
<h3 className="text-lg font-semibold text-slate-900">Neue Löschfrist anlegen</h3>
|
||
</div>
|
||
<div className="p-6 space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Name *</label>
|
||
<input
|
||
type="text"
|
||
value={newPolicy.name}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, name: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
placeholder="z.B. Aufbewahrung Nutzerkonten"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
|
||
<textarea
|
||
value={newPolicy.description}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, description: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2 h-20"
|
||
placeholder="Beschreibung der Löschfrist..."
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Datenkategorie *</label>
|
||
<input
|
||
type="text"
|
||
value={newPolicy.data_category}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, data_category: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
placeholder="z.B. Nutzerdaten, Logs, Rechnungen"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Status</label>
|
||
<select
|
||
value={newPolicy.status}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, status: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
>
|
||
<option value="draft">Entwurf</option>
|
||
<option value="active">Aktiv</option>
|
||
<option value="archived">Archiviert</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Aufbewahrungsfrist (Tage)</label>
|
||
<input
|
||
type="number"
|
||
value={newPolicy.retention_period_days}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, retention_period_days: parseInt(e.target.value) || 0 })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
placeholder="365"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Aufbewahrungsfrist (Text)</label>
|
||
<input
|
||
type="text"
|
||
value={newPolicy.retention_period_text}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, retention_period_text: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
placeholder="z.B. 3 Jahre, 10 Jahre nach Vertragsende"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Rechtsgrundlage</label>
|
||
<select
|
||
value={newPolicy.legal_basis}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, legal_basis: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
>
|
||
<option value="legal_requirement">Gesetzliche Pflicht</option>
|
||
<option value="consent">Einwilligung</option>
|
||
<option value="legitimate_interest">Berechtigtes Interesse</option>
|
||
<option value="contract">Vertragserfüllung</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Gesetzliche Referenz</label>
|
||
<input
|
||
type="text"
|
||
value={newPolicy.legal_reference}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, legal_reference: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
placeholder="z.B. § 147 AO, § 257 HGB"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Löschmethode</label>
|
||
<select
|
||
value={newPolicy.deletion_method}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, deletion_method: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
>
|
||
<option value="automatic">Automatische Löschung</option>
|
||
<option value="manual">Manuelle Löschung</option>
|
||
<option value="anonymization">Anonymisierung</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Löschprozedur</label>
|
||
<input
|
||
type="text"
|
||
value={newPolicy.deletion_procedure}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, deletion_procedure: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
placeholder="z.B. Cron-Job, Skript"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Verantwortliche Person</label>
|
||
<input
|
||
type="text"
|
||
value={newPolicy.responsible_person}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, responsible_person: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
placeholder="Name"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-slate-700 mb-1">Abteilung</label>
|
||
<input
|
||
type="text"
|
||
value={newPolicy.responsible_department}
|
||
onChange={(e) => setNewPolicy({ ...newPolicy, responsible_department: e.target.value })}
|
||
className="w-full border border-slate-300 rounded-lg px-3 py-2"
|
||
placeholder="z.B. IT, Datenschutz"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="p-6 border-t border-slate-200 flex justify-end gap-3">
|
||
<button
|
||
onClick={() => setShowCreateModal(false)}
|
||
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
<button
|
||
onClick={createPolicy}
|
||
disabled={!newPolicy.name || !newPolicy.data_category}
|
||
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
Löschfrist anlegen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|