myCRM/assets/js/views/ModuleManagement.vue
olli 42e7bc7e10 feat(billing-module): Implementieren der Modulstruktur und Lizenzverwaltung
- Hinzufügen der DependencyInjection-Konfiguration für das Billing-Modul.
- Erstellen der Invoice-Entity mit API-Ressourcen und Berechtigungen.
- Konfigurieren der Services in services.yaml für das Billing-Modul.
- Implementieren von CLI-Commands zur Verwaltung von Modul-Lizenzen und zur Auflistung installierter Module.
- Erstellen eines API-Controllers zur Verwaltung von Modulen und Lizenzen.
- Hinzufügen eines EventListeners für das Booten von Modulen.
- Definieren von Interfaces für Lizenzvalidierung und Modul-Plugins.
- Implementieren der ModuleRegistry zur Verwaltung und Booten von Modulen.
- Erstellen eines LicenseValidator-Services zur Validierung und Registrierung von Lizenzen.
2025-12-03 15:14:07 +01:00

411 lines
13 KiB
Vue

<template>
<div class="module-management">
<div class="card">
<div class="card-header flex justify-between items-center">
<h3 class="text-2xl font-semibold">Modul-Verwaltung</h3>
<Button
icon="pi pi-refresh"
label="Aktualisieren"
@click="loadModules"
:loading="loading"
outlined
/>
</div>
<DataTable
:value="modules"
:loading="loading"
dataKey="identifier"
:paginator="true"
:rows="10"
stripedRows
>
<Column field="displayName" header="Modul">
<template #body="{ data }">
<div>
<div class="font-semibold">{{ data.displayName }}</div>
<div class="text-sm text-gray-600">{{ data.identifier }}</div>
</div>
</template>
</Column>
<Column field="version" header="Version" />
<Column field="description" header="Beschreibung">
<template #body="{ data }">
<div class="max-w-md truncate" :title="data.description">
{{ data.description }}
</div>
</template>
</Column>
<Column header="Status" bodyClass="text-center">
<template #body="{ data }">
<div class="flex flex-col gap-1">
<Tag
:value="data.licensed ? 'Lizenziert' : 'Nicht lizenziert'"
:severity="data.licensed ? 'success' : 'danger'"
/>
<Tag
:value="data.active ? 'Aktiv' : 'Inaktiv'"
:severity="data.active ? 'success' : 'warning'"
/>
</div>
</template>
</Column>
<Column header="Lizenz-Info">
<template #body="{ data }">
<div v-if="data.licensed" class="text-sm">
<div v-if="data.licenseInfo.licensedTo">
<strong>Lizenziert an:</strong> {{ data.licenseInfo.licensedTo }}
</div>
<div v-if="data.licenseInfo.expiresAt">
<strong>Läuft ab:</strong>
{{ formatDate(data.licenseInfo.expiresAt) }}
</div>
<div v-if="data.licenseInfo.features?.length">
<strong>Features:</strong>
{{ data.licenseInfo.features.join(', ') }}
</div>
</div>
<div v-else class="text-sm text-red-600">
{{ data.licenseInfo.message || 'Keine Lizenz' }}
</div>
</template>
</Column>
<Column header="Aktionen" bodyClass="text-center">
<template #body="{ data }">
<div class="flex gap-2 justify-center">
<Button
v-if="!data.licensed"
icon="pi pi-key"
label="Lizenz eingeben"
@click="showLicenseDialog(data)"
size="small"
outlined
/>
<Button
v-if="data.licensed"
icon="pi pi-refresh"
label="Validieren"
@click="validateLicense(data)"
size="small"
outlined
/>
<Button
v-if="data.licensed"
icon="pi pi-times"
label="Widerrufen"
@click="confirmRevoke(data)"
size="small"
severity="danger"
outlined
/>
</div>
</template>
</Column>
</DataTable>
</div>
<!-- Lizenz-Eingabe Dialog -->
<Dialog
v-model:visible="licenseDialogVisible"
header="Lizenz registrieren"
:modal="true"
:closable="true"
:style="{ width: '500px' }"
>
<div v-if="selectedModule" class="flex flex-col gap-4">
<div>
<p class="font-semibold">Modul: {{ selectedModule.displayName }}</p>
<p class="text-sm text-gray-600">{{ selectedModule.identifier }}</p>
</div>
<div class="field">
<label for="licenseKey" class="block mb-2">Lizenzschlüssel</label>
<Textarea
id="licenseKey"
v-model="licenseKey"
rows="4"
class="w-full"
placeholder="Fügen Sie hier Ihren Lizenzschlüssel ein..."
/>
</div>
<Message v-if="licenseError" severity="error" :closable="false">
{{ licenseError }}
</Message>
</div>
<template #footer>
<Button
label="Abbrechen"
@click="licenseDialogVisible = false"
outlined
/>
<Button
label="Registrieren"
@click="registerLicense"
:loading="registeringLicense"
:disabled="!licenseKey"
/>
</template>
</Dialog>
<!-- Bestätigungsdialog für Widerruf -->
<Dialog
v-model:visible="revokeDialogVisible"
header="Lizenz widerrufen"
:modal="true"
:closable="true"
:style="{ width: '400px' }"
>
<div v-if="selectedModule">
<p>
Möchten Sie die Lizenz für das Modul
<strong>{{ selectedModule.displayName }}</strong>
wirklich widerrufen?
</p>
<p class="text-sm text-gray-600 mt-2">
Das Modul wird deaktiviert und kann nicht mehr verwendet werden.
</p>
</div>
<template #footer>
<Button
label="Abbrechen"
@click="revokeDialogVisible = false"
outlined
/>
<Button
label="Widerrufen"
@click="revokeLicense"
:loading="revokingLicense"
severity="danger"
/>
</template>
</Dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useToast } from 'primevue/usetoast'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import Tag from 'primevue/tag'
import Textarea from 'primevue/textarea'
import Message from 'primevue/message'
const toast = useToast()
// State
const modules = ref([])
const loading = ref(false)
const licenseDialogVisible = ref(false)
const revokeDialogVisible = ref(false)
const selectedModule = ref(null)
const licenseKey = ref('')
const licenseError = ref('')
const registeringLicense = ref(false)
const revokingLicense = ref(false)
// Module laden
const loadModules = async () => {
loading.value = true
try {
const response = await fetch('/api/modules', {
headers: {
'Accept': 'application/json',
},
})
if (!response.ok) {
throw new Error('Fehler beim Laden der Module')
}
const data = await response.json()
modules.value = data.modules || []
} catch (error) {
console.error('Fehler beim Laden der Module:', error)
toast.add({
severity: 'error',
summary: 'Fehler',
detail: 'Module konnten nicht geladen werden',
life: 3000,
})
} finally {
loading.value = false
}
}
// Lizenz-Dialog öffnen
const showLicenseDialog = (module) => {
selectedModule.value = module
licenseKey.value = ''
licenseError.value = ''
licenseDialogVisible.value = true
}
// Lizenz registrieren
const registerLicense = async () => {
if (!licenseKey.value || !selectedModule.value) return
registeringLicense.value = true
licenseError.value = ''
try {
const response = await fetch(`/api/modules/${selectedModule.value.identifier}/license`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
license_key: licenseKey.value,
}),
})
const data = await response.json()
if (!response.ok) {
licenseError.value = data.error || 'Fehler bei der Registrierung'
return
}
toast.add({
severity: 'success',
summary: 'Erfolg',
detail: 'Lizenz erfolgreich registriert',
life: 3000,
})
licenseDialogVisible.value = false
await loadModules()
} catch (error) {
console.error('Fehler bei Lizenzregistrierung:', error)
licenseError.value = 'Netzwerkfehler bei der Registrierung'
} finally {
registeringLicense.value = false
}
}
// Lizenz validieren
const validateLicense = async (module) => {
try {
const response = await fetch(`/api/modules/${module.identifier}/validate`, {
method: 'POST',
headers: {
'Accept': 'application/json',
},
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Validierung fehlgeschlagen')
}
toast.add({
severity: data.licenseInfo.valid ? 'success' : 'warn',
summary: data.licenseInfo.valid ? 'Lizenz gültig' : 'Lizenz ungültig',
detail: data.licenseInfo.message || '',
life: 3000,
})
await loadModules()
} catch (error) {
console.error('Fehler bei Lizenzvalidierung:', error)
toast.add({
severity: 'error',
summary: 'Fehler',
detail: error.message || 'Validierung fehlgeschlagen',
life: 3000,
})
}
}
// Widerruf bestätigen
const confirmRevoke = (module) => {
selectedModule.value = module
revokeDialogVisible.value = true
}
// Lizenz widerrufen
const revokeLicense = async () => {
if (!selectedModule.value) return
revokingLicense.value = true
try {
const response = await fetch(`/api/modules/${selectedModule.value.identifier}/license`, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
},
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || 'Widerruf fehlgeschlagen')
}
toast.add({
severity: 'success',
summary: 'Erfolg',
detail: 'Lizenz erfolgreich widerrufen',
life: 3000,
})
revokeDialogVisible.value = false
await loadModules()
} catch (error) {
console.error('Fehler beim Widerruf:', error)
toast.add({
severity: 'error',
summary: 'Fehler',
detail: error.message || 'Widerruf fehlgeschlagen',
life: 3000,
})
} finally {
revokingLicense.value = false
}
}
// Datum formatieren
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
// Initial laden
onMounted(() => {
loadModules()
})
</script>
<style scoped>
.module-management {
padding: 1rem;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
</style>