myCRM/src/Service/PermissionModuleSync.php
olli a787019a3b feat: Implement PDF upload functionality in PDFUploadForm.vue
- Added a file input for PDF uploads with validation for file type and size.
- Implemented file size formatting and user feedback for selected files.
- Created upload and cancel methods with placeholder for future API integration.

feat: Create PaymentForm.vue for handling payments

- Developed a form for entering payment details including date, amount, method, and notes.
- Integrated currency formatting and dropdown for payment methods.
- Implemented save and cancel actions with API call for saving payment data.

docs: Add documentation for dynamic plugin menus and permissions

- Provided guidelines for defining menu items and permissions in plugins.
- Explained the process for synchronizing permissions and integrating menus in the frontend.
- Included examples and best practices for plugin development.

feat: Add database migrations for invoices, invoice items, and payments

- Created migration scripts to define the database schema for invoices, invoice items, and payments.
- Established foreign key relationships between invoices and related entities.

feat: Implement command for synchronizing plugin permissions

- Developed a console command to synchronize plugin permissions with the database.
- Added options for dry-run and force synchronization for unlicensed modules.

feat: Create API controller for plugin menu items

- Implemented API endpoints to retrieve plugin menu items in both flat and grouped formats.
- Ensured access control with role-based permissions for API access.

feat: Develop service for managing plugin menu items

- Created a service to collect and manage menu items from installed plugins.
- Implemented methods for retrieving flat and grouped menu items for frontend use.

feat: Add service for synchronizing plugin permissions

- Developed a service to handle the synchronization of plugin permissions with the database.
- Included logic for creating and updating permission modules based on plugin definitions.
2025-12-05 11:13:41 +01:00

188 lines
5.6 KiB
PHP

<?php
namespace App\Service;
use App\Entity\Module;
use App\Plugin\ModuleRegistry;
use App\Repository\ModuleRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
/**
* Service zur Synchronisation von Plugin-Permissions mit der Datenbank
*
* Liest alle Permission-Module von Plugins und legt sie in der DB an,
* falls sie noch nicht existieren.
*/
class PermissionModuleSync
{
public function __construct(
private readonly ModuleRegistry $moduleRegistry,
private readonly ModuleRepository $moduleRepository,
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger
) {
}
/**
* Synchronisiert alle Plugin-Module mit der Datenbank
*
* @param bool $force Synchronisiert auch unlizenzierte Module (für Development)
* @return array{created: int, updated: int, skipped: int, errors: array<string>}
*/
public function syncAll(bool $force = false): array
{
$stats = [
'created' => 0,
'updated' => 0,
'skipped' => 0,
'errors' => [],
];
foreach ($this->moduleRegistry->getAllModules() as $plugin) {
try {
$result = $this->syncPlugin($plugin->getIdentifier(), $force);
$stats['created'] += $result['created'];
$stats['updated'] += $result['updated'];
$stats['skipped'] += $result['skipped'];
} catch (\Throwable $e) {
$error = sprintf(
'Plugin "%s": %s',
$plugin->getIdentifier(),
$e->getMessage()
);
$stats['errors'][] = $error;
$this->logger->error($error);
}
}
return $stats;
}
/**
* Synchronisiert die Module eines bestimmten Plugins
*
* @param string $pluginIdentifier
* @param bool $force Synchronisiert auch unlizenzierte Module (für Development)
* @return array{created: int, updated: int, skipped: int}
*/
public function syncPlugin(string $pluginIdentifier, bool $force = false): array
{
$stats = ['created' => 0, 'updated' => 0, 'skipped' => 0];
$plugin = $this->moduleRegistry->getModule($pluginIdentifier);
if (!$plugin) {
throw new \RuntimeException(sprintf('Plugin "%s" nicht gefunden', $pluginIdentifier));
}
// Nur lizenzierte Module synchronisieren (außer force)
if (!$force && !$plugin->isLicensed()) {
$this->logger->info(sprintf(
'Plugin "%s" übersprungen: Nicht lizenziert',
$pluginIdentifier
));
return $stats;
}
$permissionModules = $plugin->getPermissionModules();
foreach ($permissionModules as $moduleCode) {
$result = $this->syncModule(
$moduleCode,
$plugin->getDisplayName(),
$plugin->getDescription()
);
$stats[$result]++;
}
$this->em->flush();
return $stats;
}
/**
* Synchronisiert ein einzelnes Modul
*
* @param string $code Modul-Code (z.B. 'billing', 'invoicing')
* @param string $displayName Display-Name des Plugins
* @param string $description Plugin-Beschreibung
* @return string 'created', 'updated' oder 'skipped'
*/
private function syncModule(string $code, string $displayName, string $description): string
{
$existingModule = $this->moduleRepository->findOneBy(['code' => $code]);
if ($existingModule) {
// Modul existiert bereits - Update nur wenn nötig
$updated = false;
if ($existingModule->getDescription() !== $description) {
$existingModule->setDescription($description);
$updated = true;
}
if ($updated) {
$this->logger->info(sprintf('Modul "%s" aktualisiert', $code));
return 'updated';
}
return 'skipped';
}
// Neues Modul anlegen
$module = new Module();
$module->setCode($code);
// Name: Plugin-Name + Code (falls unterschiedlich)
$moduleName = $displayName;
if (strcasecmp($code, $displayName) !== 0) {
$moduleName = sprintf('%s (%s)', $displayName, ucfirst($code));
}
$module->setName($moduleName);
$module->setDescription($description);
$module->setIsActive(true);
$module->setSortOrder($this->getNextSortOrder());
// Icon aus Code ableiten (optional)
$icon = $this->guessIcon($code);
if ($icon) {
$module->setIcon($icon);
}
$this->em->persist($module);
$this->logger->info(sprintf('Modul "%s" erstellt', $code));
return 'created';
}
/**
* Ermittelt die nächste Sort-Order
*/
private function getNextSortOrder(): int
{
$maxSortOrder = $this->em->createQuery(
'SELECT MAX(m.sortOrder) FROM App\Entity\Module m'
)->getSingleScalarResult();
return ($maxSortOrder ?? 0) + 10;
}
/**
* Versucht, ein passendes Icon zu raten
*/
private function guessIcon(string $code): ?string
{
$iconMap = [
'billing' => 'pi-file-pdf',
'invoicing' => 'pi-receipt',
'inventory' => 'pi-box',
'reporting' => 'pi-chart-bar',
'crm' => 'pi-users',
'test' => 'pi-code',
];
return $iconMap[$code] ?? 'pi-circle';
}
}