first commit
This commit is contained in:
215
Services/AdminSettingsService.php
Normal file
215
Services/AdminSettingsService.php
Normal file
@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
class AdminSettingsService
|
||||
{
|
||||
private $driver;
|
||||
private $imageDisk = 'public';
|
||||
private $favicon_basePath = 'favicon/';
|
||||
private $image_logo_basePath = 'images/logo/';
|
||||
|
||||
private $faviconsSizes = [
|
||||
'180x180' => [180, 180],
|
||||
'192x192' => [192, 192],
|
||||
'152x152' => [152, 152],
|
||||
'120x120' => [120, 120],
|
||||
'76x76' => [76, 76],
|
||||
'16x16' => [16, 16],
|
||||
];
|
||||
|
||||
private $imageLogoMaxPixels1 = 22500; // Primera versión (px^2)
|
||||
private $imageLogoMaxPixels2 = 75625; // Segunda versión (px^2)
|
||||
private $imageLogoMaxPixels3 = 262144; // Tercera versión (px^2)
|
||||
private $imageLogoMaxPixels4 = 230400; // Tercera versión (px^2) en Base64
|
||||
|
||||
protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->driver = config('image.driver', 'gd');
|
||||
}
|
||||
|
||||
public function updateSetting(string $key, string $value): bool
|
||||
{
|
||||
$setting = Setting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => trim($value)]
|
||||
);
|
||||
|
||||
return $setting->save();
|
||||
}
|
||||
|
||||
public function processAndSaveFavicon($image): void
|
||||
{
|
||||
Storage::makeDirectory($this->imageDisk . '/' . $this->favicon_basePath);
|
||||
|
||||
// Eliminar favicons antiguos
|
||||
$this->deleteOldFavicons();
|
||||
|
||||
// Guardar imagen original
|
||||
$imageManager = new ImageManager($this->driver);
|
||||
|
||||
$imageName = uniqid('admin_favicon_');
|
||||
|
||||
$image = $imageManager->read($image->getRealPath());
|
||||
|
||||
foreach ($this->faviconsSizes as $size => [$width, $height]) {
|
||||
$resizedPath = $this->favicon_basePath . $imageName . "_{$size}.png";
|
||||
|
||||
$image->cover($width, $height);
|
||||
|
||||
Storage::disk($this->imageDisk)->put($resizedPath, $image->toPng(indexed: true));
|
||||
}
|
||||
|
||||
$this->updateSetting('admin_favicon_ns', $this->favicon_basePath . $imageName);
|
||||
}
|
||||
|
||||
protected function deleteOldFavicons(): void
|
||||
{
|
||||
// Obtener el favicon actual desde la base de datos
|
||||
$currentFavicon = Setting::where('key', 'admin_favicon_ns')->value('value');
|
||||
|
||||
if ($currentFavicon) {
|
||||
$filePaths = [
|
||||
$this->imageDisk . '/' . $currentFavicon,
|
||||
$this->imageDisk . '/' . $currentFavicon . '_16x16.png',
|
||||
$this->imageDisk . '/' . $currentFavicon . '_76x76.png',
|
||||
$this->imageDisk . '/' . $currentFavicon . '_120x120.png',
|
||||
$this->imageDisk . '/' . $currentFavicon . '_152x152.png',
|
||||
$this->imageDisk . '/' . $currentFavicon . '_180x180.png',
|
||||
$this->imageDisk . '/' . $currentFavicon . '_192x192.png',
|
||||
];
|
||||
|
||||
foreach ($filePaths as $filePath) {
|
||||
if (Storage::exists($filePath)) {
|
||||
Storage::delete($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function processAndSaveImageLogo($image, string $type = ''): void
|
||||
{
|
||||
// Crear directorio si no existe
|
||||
Storage::makeDirectory($this->imageDisk . '/' . $this->image_logo_basePath);
|
||||
|
||||
// Eliminar imágenes antiguas
|
||||
$this->deleteOldImageWebapp($type);
|
||||
|
||||
// Leer imagen original
|
||||
$imageManager = new ImageManager($this->driver);
|
||||
$image = $imageManager->read($image->getRealPath());
|
||||
|
||||
// Generar tres versiones con diferentes áreas máximas
|
||||
$this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels1, 'small'); // Versión 1
|
||||
$this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels2, 'medium'); // Versión 2
|
||||
$this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels3); // Versión 3
|
||||
$this->generateAndSaveImageAsBase64($image, $type, $this->imageLogoMaxPixels4); // Versión 3
|
||||
}
|
||||
|
||||
private function generateAndSaveImage($image, string $type, int $maxPixels, string $suffix = ''): void
|
||||
{
|
||||
$imageClone = clone $image;
|
||||
|
||||
// Escalar imagen conservando aspecto
|
||||
$this->resizeImageToMaxPixels($imageClone, $maxPixels);
|
||||
|
||||
$imageName = 'admin_image_logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : '');
|
||||
|
||||
// Generar nombre y ruta
|
||||
$imageNameUid = uniqid($imageName . '_', ".png");
|
||||
$resizedPath = $this->image_logo_basePath . $imageNameUid;
|
||||
|
||||
// Guardar imagen en PNG
|
||||
Storage::disk($this->imageDisk)->put($resizedPath, $imageClone->toPng(indexed: true));
|
||||
|
||||
// Actualizar configuración
|
||||
$this->updateSetting($imageName, $resizedPath);
|
||||
}
|
||||
|
||||
private function resizeImageToMaxPixels($image, int $maxPixels)
|
||||
{
|
||||
// Obtener dimensiones originales de la imagen
|
||||
$originalWidth = $image->width(); // Método para obtener el ancho
|
||||
$originalHeight = $image->height(); // Método para obtener el alto
|
||||
|
||||
// Calcular el aspecto
|
||||
$aspectRatio = $originalWidth / $originalHeight;
|
||||
|
||||
// Calcular dimensiones redimensionadas conservando aspecto
|
||||
if ($aspectRatio > 1) { // Ancho es dominante
|
||||
$newWidth = sqrt($maxPixels * $aspectRatio);
|
||||
$newHeight = $newWidth / $aspectRatio;
|
||||
} else { // Alto es dominante
|
||||
$newHeight = sqrt($maxPixels / $aspectRatio);
|
||||
$newWidth = $newHeight * $aspectRatio;
|
||||
}
|
||||
|
||||
// Redimensionar la imagen
|
||||
$image->resize(
|
||||
round($newWidth), // Redondear para evitar problemas con números decimales
|
||||
round($newHeight),
|
||||
function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
}
|
||||
);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
|
||||
private function generateAndSaveImageAsBase64($image, string $type, int $maxPixels): void
|
||||
{
|
||||
$imageClone = clone $image;
|
||||
|
||||
// Redimensionar imagen conservando el aspecto
|
||||
$this->resizeImageToMaxPixels($imageClone, $maxPixels);
|
||||
|
||||
// Convertir a Base64
|
||||
$base64Image = (string) $imageClone->toJpg(40)->toDataUri();
|
||||
|
||||
// Guardar como configuración
|
||||
$this->updateSetting(
|
||||
"admin_image_logo_base64" . ($type === 'dark' ? '_dark' : ''),
|
||||
$base64Image // Ya incluye "data:image/png;base64,"
|
||||
);
|
||||
}
|
||||
|
||||
protected function deleteOldImageWebapp(string $type = ''): void
|
||||
{
|
||||
// Determinar prefijo según el tipo (normal o dark)
|
||||
$suffix = $type === 'dark' ? '_dark' : '';
|
||||
|
||||
// Claves relacionadas con las imágenes que queremos limpiar
|
||||
$imageKeys = [
|
||||
"admin_image_logo{$suffix}",
|
||||
"admin_image_logo_small{$suffix}",
|
||||
"admin_image_logo_medium{$suffix}",
|
||||
];
|
||||
|
||||
// Recuperar las imágenes actuales en una sola consulta
|
||||
$settings = Setting::whereIn('key', $imageKeys)->pluck('value', 'key');
|
||||
|
||||
foreach ($imageKeys as $key) {
|
||||
// Obtener la imagen correspondiente
|
||||
$currentImage = $settings[$key] ?? null;
|
||||
|
||||
if ($currentImage) {
|
||||
// Construir la ruta del archivo y eliminarlo si existe
|
||||
$filePath = $this->imageDisk . '/' . $currentImage;
|
||||
if (Storage::exists($filePath)) {
|
||||
Storage::delete($filePath);
|
||||
}
|
||||
|
||||
// Eliminar la configuración de la base de datos
|
||||
Setting::where('key', $key)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
156
Services/AdminTemplateService.php
Normal file
156
Services/AdminTemplateService.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
class AdminTemplateService
|
||||
{
|
||||
protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos
|
||||
|
||||
public function updateSetting(string $key, string $value): bool
|
||||
{
|
||||
$setting = Setting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => trim($value)]
|
||||
);
|
||||
|
||||
return $setting->save();
|
||||
}
|
||||
|
||||
public function getAdminVars($adminSetting = false): array
|
||||
{
|
||||
try {
|
||||
// Verificar si el sistema está inicializado (la tabla `migrations` existe)
|
||||
if (!Schema::hasTable('migrations')) {
|
||||
return $this->getDefaultAdminVars($adminSetting);
|
||||
}
|
||||
|
||||
// Cargar desde el caché o la base de datos si está disponible
|
||||
return Cache::remember('admin_settings', $this->cacheTTL, function () use ($adminSetting) {
|
||||
$settings = Setting::global()
|
||||
->where('key', 'LIKE', 'admin_%')
|
||||
->pluck('value', 'key')
|
||||
->toArray();
|
||||
|
||||
$adminSettings = $this->buildAdminVarsArray($settings);
|
||||
|
||||
return $adminSetting
|
||||
? $adminSettings[$adminSetting]
|
||||
: $adminSettings;
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
// En caso de error, devolver valores predeterminados
|
||||
return $this->getDefaultAdminVars($adminSetting);
|
||||
}
|
||||
}
|
||||
|
||||
private function getDefaultAdminVars($adminSetting = false): array
|
||||
{
|
||||
$defaultSettings = [
|
||||
'title' => config('koneko.appTitle', 'Default Title'),
|
||||
'author' => config('koneko.author', 'Default Author'),
|
||||
'description' => config('koneko.description', 'Default Description'),
|
||||
'favicon' => $this->getFaviconPaths([]),
|
||||
'app_name' => config('koneko.appName', 'Default App Name'),
|
||||
'image_logo' => $this->getImageLogoPaths([]),
|
||||
];
|
||||
|
||||
return $adminSetting
|
||||
? $defaultSettings[$adminSetting] ?? null
|
||||
: $defaultSettings;
|
||||
}
|
||||
|
||||
private function buildAdminVarsArray(array $settings): array
|
||||
{
|
||||
return [
|
||||
'title' => $settings['admin_title'] ?? config('koneko.appTitle'),
|
||||
'author' => config('koneko.author'),
|
||||
'description' => config('koneko.description'),
|
||||
'favicon' => $this->getFaviconPaths($settings),
|
||||
'app_name' => $settings['admin_app_name'] ?? config('koneko.appName'),
|
||||
'image_logo' => $this->getImageLogoPaths($settings),
|
||||
];
|
||||
}
|
||||
|
||||
public function getVuexyCustomizerVars()
|
||||
{
|
||||
// Obtener valores de la base de datos
|
||||
$settings = Setting::global()
|
||||
->where('key', 'LIKE', 'vuexy_%')
|
||||
->pluck('value', 'key')
|
||||
->toArray();
|
||||
|
||||
// Obtener configuraciones predeterminadas
|
||||
$defaultConfig = Config::get('vuexy.custom', []);
|
||||
|
||||
// Mezclar las configuraciones predeterminadas con las de la base de datos
|
||||
return collect($defaultConfig)
|
||||
->mapWithKeys(function ($defaultValue, $key) use ($settings) {
|
||||
$vuexyKey = 'vuexy_' . $key; // Convertir clave al formato de la base de datos
|
||||
|
||||
// Obtener valor desde la base de datos o usar el predeterminado
|
||||
$value = $settings[$vuexyKey] ?? $defaultValue;
|
||||
|
||||
// Forzar booleanos para claves específicas
|
||||
if (in_array($key, ['displayCustomizer', 'footerFixed', 'menuFixed', 'menuCollapsed', 'showDropdownOnHover'])) {
|
||||
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
return [$key => $value];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene los paths de favicon en distintos tamaños.
|
||||
*/
|
||||
private function getFaviconPaths(array $settings): array
|
||||
{
|
||||
$defaultFavicon = config('koneko.appFavicon');
|
||||
$namespace = $settings['admin_favicon_ns'] ?? null;
|
||||
|
||||
return [
|
||||
'namespace' => $namespace,
|
||||
'16x16' => $namespace ? "{$namespace}_16x16.png" : $defaultFavicon,
|
||||
'76x76' => $namespace ? "{$namespace}_76x76.png" : $defaultFavicon,
|
||||
'120x120' => $namespace ? "{$namespace}_120x120.png" : $defaultFavicon,
|
||||
'152x152' => $namespace ? "{$namespace}_152x152.png" : $defaultFavicon,
|
||||
'180x180' => $namespace ? "{$namespace}_180x180.png" : $defaultFavicon,
|
||||
'192x192' => $namespace ? "{$namespace}_192x192.png" : $defaultFavicon,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene los paths de los logos en distintos tamaños.
|
||||
*/
|
||||
private function getImageLogoPaths(array $settings): array
|
||||
{
|
||||
$defaultLogo = config('koneko.appLogo');
|
||||
|
||||
return [
|
||||
'small' => $this->getImagePath($settings, 'admin_image_logo_small', $defaultLogo),
|
||||
'medium' => $this->getImagePath($settings, 'admin_image_logo_medium', $defaultLogo),
|
||||
'large' => $this->getImagePath($settings, 'admin_image_logo', $defaultLogo),
|
||||
'small_dark' => $this->getImagePath($settings, 'admin_image_logo_small_dark', $defaultLogo),
|
||||
'medium_dark' => $this->getImagePath($settings, 'admin_image_logo_medium_dark', $defaultLogo),
|
||||
'large_dark' => $this->getImagePath($settings, 'admin_image_logo_dark', $defaultLogo),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un path de imagen o retorna un valor predeterminado.
|
||||
*/
|
||||
private function getImagePath(array $settings, string $key, string $default): string
|
||||
{
|
||||
return $settings[$key] ?? $default;
|
||||
}
|
||||
|
||||
public static function clearAdminVarsCache()
|
||||
{
|
||||
Cache::forget("admin_settings");
|
||||
}
|
||||
}
|
76
Services/AvatarImageService.php
Normal file
76
Services/AvatarImageService.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
|
||||
class AvatarImageService
|
||||
{
|
||||
protected $avatarDisk = 'public';
|
||||
protected $profilePhotoDir = 'profile-photos';
|
||||
protected $avatarWidth = 512;
|
||||
protected $avatarHeight = 512;
|
||||
|
||||
/**
|
||||
* Actualiza la foto de perfil procesando la imagen subida.
|
||||
*
|
||||
* @param mixed $user Objeto usuario que se va a actualizar.
|
||||
* @param UploadedFile $image_avatar Archivo de imagen subido.
|
||||
*
|
||||
* @throws \Exception Si el archivo no existe o tiene un formato inválido.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateProfilePhoto(User $user, UploadedFile $image_avatar)
|
||||
{
|
||||
if (!file_exists($image_avatar->getRealPath())) {
|
||||
throw new \Exception('El archivo no existe en la ruta especificada.');
|
||||
}
|
||||
|
||||
if (!in_array($image_avatar->getClientOriginalExtension(), ['jpg', 'jpeg', 'png'])) {
|
||||
throw new \Exception('El formato del archivo debe ser JPG o PNG.');
|
||||
}
|
||||
|
||||
$avatarName = uniqid('avatar_') . '.png';
|
||||
$driver = config('image.driver', 'gd');
|
||||
|
||||
$manager = new ImageManager($driver);
|
||||
|
||||
if (!Storage::disk($this->avatarDisk)->exists($this->profilePhotoDir)) {
|
||||
Storage::disk($this->avatarDisk)->makeDirectory($this->profilePhotoDir);
|
||||
}
|
||||
|
||||
$image = $manager->read($image_avatar->getRealPath());
|
||||
$image->cover($this->avatarWidth, $this->avatarHeight);
|
||||
Storage::disk($this->avatarDisk)->put($this->profilePhotoDir . '/' . $avatarName, $image->toPng(indexed: true));
|
||||
|
||||
// Eliminar avatar existente
|
||||
$this->deleteProfilePhoto($user);
|
||||
|
||||
$user->forceFill([
|
||||
'profile_photo_path' => $avatarName,
|
||||
])->save();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Elimina la foto de perfil actual del usuario.
|
||||
*
|
||||
* @param mixed $user Objeto usuario.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteProfilePhoto($user)
|
||||
{
|
||||
if (!empty($user->profile_photo_path)) {
|
||||
Storage::disk($this->avatarDisk)->delete($user->profile_photo_path);
|
||||
|
||||
$user->forceFill([
|
||||
'profile_photo_path' => null,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
}
|
124
Services/AvatarInitialsService.php
Normal file
124
Services/AvatarInitialsService.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Intervention\Image\Typography\FontFactory;
|
||||
|
||||
class AvatarInitialsService
|
||||
{
|
||||
protected $avatarDisk = 'public';
|
||||
protected $initialAvatarDir = 'initial-avatars';
|
||||
protected $avatarWidth = 512;
|
||||
protected $avatarHeight = 512;
|
||||
protected const INITIAL_MAX_LENGTH = 3;
|
||||
protected const AVATAR_BACKGROUND = '#EBF4FF';
|
||||
protected const AVATAR_COLORS = [
|
||||
'#7367f0',
|
||||
'#808390',
|
||||
'#28c76f',
|
||||
'#ff4c51',
|
||||
'#ff9f43',
|
||||
'#00bad1',
|
||||
'#4b4b4b',
|
||||
];
|
||||
|
||||
/**
|
||||
* Genera o retorna el avatar basado en las iniciales.
|
||||
*
|
||||
* @param string $name Nombre completo del usuario.
|
||||
*
|
||||
* @return \Illuminate\Http\Response Respuesta con la imagen generada.
|
||||
*/
|
||||
public function getAvatarImage($name)
|
||||
{
|
||||
$color = $this->getAvatarColor($name);
|
||||
$background = ltrim(self::AVATAR_BACKGROUND, '#');
|
||||
$size = ($this->avatarWidth + $this->avatarHeight) / 2;
|
||||
$initials = self::getInitials($name);
|
||||
$cacheKey = "avatar-{$initials}-{$color}-{$background}-{$size}";
|
||||
$path = "{$this->initialAvatarDir}/{$cacheKey}.png";
|
||||
$storagePath = storage_path("app/public/{$path}");
|
||||
|
||||
if (Storage::disk($this->avatarDisk)->exists($path)) {
|
||||
return response()->file($storagePath);
|
||||
}
|
||||
|
||||
$image = $this->createAvatarImage($name, $color, self::AVATAR_BACKGROUND, $size);
|
||||
Storage::disk($this->avatarDisk)->put($path, $image->toPng(indexed: true));
|
||||
|
||||
return response()->file($storagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea la imagen del avatar con las iniciales.
|
||||
*
|
||||
* @param string $name Nombre completo.
|
||||
* @param string $color Color del texto.
|
||||
* @param string $background Color de fondo.
|
||||
* @param int $size Tamaño de la imagen.
|
||||
*
|
||||
* @return \Intervention\Image\Image La imagen generada.
|
||||
*/
|
||||
protected function createAvatarImage($name, $color, $background, $size)
|
||||
{
|
||||
$driver = config('image.driver', 'gd');
|
||||
$manager = new ImageManager($driver);
|
||||
$initials = self::getInitials($name);
|
||||
$fontPath = __DIR__ . '/../storage/fonts/OpenSans-Bold.ttf';
|
||||
|
||||
$image = $manager->create($size, $size)
|
||||
->fill($background);
|
||||
|
||||
$image->text(
|
||||
$initials,
|
||||
$size / 2,
|
||||
$size / 2,
|
||||
function (FontFactory $font) use ($color, $size, $fontPath) {
|
||||
$font->file($fontPath);
|
||||
$font->size($size * 0.4);
|
||||
$font->color($color);
|
||||
$font->align('center');
|
||||
$font->valign('middle');
|
||||
}
|
||||
);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula las iniciales a partir del nombre.
|
||||
*
|
||||
* @param string $name Nombre completo.
|
||||
*
|
||||
* @return string Iniciales en mayúsculas.
|
||||
*/
|
||||
public static function getInitials($name)
|
||||
{
|
||||
if (empty($name)) {
|
||||
return 'NA';
|
||||
}
|
||||
|
||||
$initials = implode('', array_map(function ($word) {
|
||||
return mb_substr($word, 0, 1);
|
||||
}, explode(' ', $name)));
|
||||
|
||||
return strtoupper(substr($initials, 0, self::INITIAL_MAX_LENGTH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selecciona un color basado en el nombre.
|
||||
*
|
||||
* @param string $name Nombre del usuario.
|
||||
*
|
||||
* @return string Color seleccionado.
|
||||
*/
|
||||
public function getAvatarColor($name)
|
||||
{
|
||||
// Por ejemplo, se puede basar en la suma de los códigos ASCII de las letras del nombre
|
||||
$hash = array_sum(array_map('ord', str_split($name)));
|
||||
|
||||
return self::AVATAR_COLORS[$hash % count(self::AVATAR_COLORS)];
|
||||
}
|
||||
}
|
235
Services/CacheConfigService.php
Normal file
235
Services/CacheConfigService.php
Normal file
@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CacheConfigService
|
||||
{
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
'cache' => $this->getCacheConfig(),
|
||||
'session' => $this->getSessionConfig(),
|
||||
'database' => $this->getDatabaseConfig(),
|
||||
'driver' => $this->getDriverVersion(),
|
||||
'memcachedInUse' => $this->isDriverInUse('memcached'),
|
||||
'redisInUse' => $this->isDriverInUse('redis'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function getCacheConfig(): array
|
||||
{
|
||||
$cacheConfig = Config::get('cache');
|
||||
$driver = $cacheConfig['default'];
|
||||
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
$connection = config('database.redis.cache');
|
||||
$cacheConfig['host'] = $connection['host'] ?? 'localhost';
|
||||
$cacheConfig['database'] = $connection['database'] ?? 'N/A';
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
$connection = config('database.connections.' . config('cache.stores.database.connection'));
|
||||
$cacheConfig['host'] = $connection['host'] ?? 'localhost';
|
||||
$cacheConfig['database'] = $connection['database'] ?? 'N/A';
|
||||
break;
|
||||
|
||||
case 'memcached':
|
||||
$servers = config('cache.stores.memcached.servers');
|
||||
$cacheConfig['host'] = $servers[0]['host'] ?? 'localhost';
|
||||
$cacheConfig['database'] = 'N/A';
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$cacheConfig['host'] = storage_path('framework/cache/data');
|
||||
$cacheConfig['database'] = 'N/A';
|
||||
break;
|
||||
|
||||
default:
|
||||
$cacheConfig['host'] = 'N/A';
|
||||
$cacheConfig['database'] = 'N/A';
|
||||
break;
|
||||
}
|
||||
|
||||
return $cacheConfig;
|
||||
}
|
||||
|
||||
private function getSessionConfig(): array
|
||||
{
|
||||
$sessionConfig = Config::get('session');
|
||||
$driver = $sessionConfig['driver'];
|
||||
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
$connection = config('database.redis.sessions');
|
||||
$sessionConfig['host'] = $connection['host'] ?? 'localhost';
|
||||
$sessionConfig['database'] = $connection['database'] ?? 'N/A';
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
$connection = config('database.connections.' . $sessionConfig['connection']);
|
||||
$sessionConfig['host'] = $connection['host'] ?? 'localhost';
|
||||
$sessionConfig['database'] = $connection['database'] ?? 'N/A';
|
||||
break;
|
||||
|
||||
case 'memcached':
|
||||
$servers = config('cache.stores.memcached.servers');
|
||||
$sessionConfig['host'] = $servers[0]['host'] ?? 'localhost';
|
||||
$sessionConfig['database'] = 'N/A';
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$sessionConfig['host'] = storage_path('framework/sessions');
|
||||
$sessionConfig['database'] = 'N/A';
|
||||
break;
|
||||
|
||||
default:
|
||||
$sessionConfig['host'] = 'N/A';
|
||||
$sessionConfig['database'] = 'N/A';
|
||||
break;
|
||||
}
|
||||
|
||||
return $sessionConfig;
|
||||
}
|
||||
|
||||
private function getDatabaseConfig(): array
|
||||
{
|
||||
$databaseConfig = Config::get('database');
|
||||
$connection = $databaseConfig['default'];
|
||||
|
||||
$connectionConfig = config('database.connections.' . $connection);
|
||||
$databaseConfig['host'] = $connectionConfig['host'] ?? 'localhost';
|
||||
$databaseConfig['database'] = $connectionConfig['database'] ?? 'N/A';
|
||||
|
||||
return $databaseConfig;
|
||||
}
|
||||
|
||||
|
||||
private function getDriverVersion(): array
|
||||
{
|
||||
$drivers = [];
|
||||
$defaultDatabaseDriver = config('database.default'); // Obtén el driver predeterminado
|
||||
|
||||
switch ($defaultDatabaseDriver) {
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
$drivers['mysql'] = [
|
||||
'version' => $this->getMySqlVersion(),
|
||||
'details' => config("database.connections.$defaultDatabaseDriver"),
|
||||
];
|
||||
|
||||
$drivers['mariadb'] = $drivers['mysql'];
|
||||
|
||||
case 'pgsql':
|
||||
$drivers['pgsql'] = [
|
||||
'version' => $this->getPgSqlVersion(),
|
||||
'details' => config("database.connections.pgsql"),
|
||||
];
|
||||
break;
|
||||
|
||||
case 'sqlsrv':
|
||||
$drivers['sqlsrv'] = [
|
||||
'version' => $this->getSqlSrvVersion(),
|
||||
'details' => config("database.connections.sqlsrv"),
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
$drivers['unknown'] = [
|
||||
'version' => 'No disponible',
|
||||
'details' => 'Driver no identificado',
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
// Opcional: Agrega detalles de Redis y Memcached si están en uso
|
||||
if ($this->isDriverInUse('redis')) {
|
||||
$drivers['redis'] = [
|
||||
'version' => $this->getRedisVersion(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->isDriverInUse('memcached')) {
|
||||
$drivers['memcached'] = [
|
||||
'version' => $this->getMemcachedVersion(),
|
||||
];
|
||||
}
|
||||
|
||||
return $drivers;
|
||||
}
|
||||
|
||||
private function getMySqlVersion(): string
|
||||
{
|
||||
try {
|
||||
$version = DB::selectOne('SELECT VERSION() as version');
|
||||
return $version->version ?? 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private function getPgSqlVersion(): string
|
||||
{
|
||||
try {
|
||||
$version = DB::selectOne("SHOW server_version");
|
||||
return $version->server_version ?? 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private function getSqlSrvVersion(): string
|
||||
{
|
||||
try {
|
||||
$version = DB::selectOne("SELECT @@VERSION as version");
|
||||
return $version->version ?? 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private function getMemcachedVersion(): string
|
||||
{
|
||||
try {
|
||||
$memcached = new \Memcached();
|
||||
$memcached->addServer(
|
||||
Config::get('cache.stores.memcached.servers.0.host'),
|
||||
Config::get('cache.stores.memcached.servers.0.port')
|
||||
);
|
||||
|
||||
$stats = $memcached->getStats();
|
||||
foreach ($stats as $serverStats) {
|
||||
return $serverStats['version'] ?? 'No disponible';
|
||||
}
|
||||
|
||||
return 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private function getRedisVersion(): string
|
||||
{
|
||||
try {
|
||||
$info = Redis::info();
|
||||
return $info['redis_version'] ?? 'No disponible';
|
||||
} catch (\Exception $e) {
|
||||
return 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function isDriverInUse(string $driver): bool
|
||||
{
|
||||
return in_array($driver, [
|
||||
Config::get('cache.default'),
|
||||
Config::get('session.driver'),
|
||||
Config::get('queue.default'),
|
||||
]);
|
||||
}
|
||||
}
|
389
Services/CacheManagerService.php
Normal file
389
Services/CacheManagerService.php
Normal file
@ -0,0 +1,389 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class CacheManagerService
|
||||
{
|
||||
private string $driver;
|
||||
|
||||
public function __construct(string $driver = null)
|
||||
{
|
||||
$this->driver = $driver ?? config('cache.default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas de caché para el driver especificado.
|
||||
*/
|
||||
public function getCacheStats(string $driver = null): array
|
||||
{
|
||||
$driver = $driver ?? $this->driver;
|
||||
|
||||
if (!$this->isSupportedDriver($driver)) {
|
||||
return $this->response('warning', 'Driver no soportado o no configurado.');
|
||||
}
|
||||
|
||||
try {
|
||||
return match ($driver) {
|
||||
'database' => $this->_getDatabaseStats(),
|
||||
'file' => $this->_getFilecacheStats(),
|
||||
'redis' => $this->_getRedisStats(),
|
||||
'memcached' => $this->_getMemcachedStats(),
|
||||
default => $this->response('info', 'No hay estadísticas disponibles para este driver.'),
|
||||
};
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function clearCache(string $driver = null): array
|
||||
{
|
||||
$driver = $driver ?? $this->driver;
|
||||
|
||||
if (!$this->isSupportedDriver($driver)) {
|
||||
return $this->response('warning', 'Driver no soportado o no configurado.');
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
$keysCleared = $this->clearRedisCache();
|
||||
|
||||
return $keysCleared
|
||||
? $this->response('warning', 'Se ha purgado toda la caché de Redis.')
|
||||
: $this->response('info', 'No se encontraron claves en Redis para eliminar.');
|
||||
|
||||
case 'memcached':
|
||||
$keysCleared = $this->clearMemcachedCache();
|
||||
|
||||
return $keysCleared
|
||||
? $this->response('warning', 'Se ha purgado toda la caché de Memcached.')
|
||||
: $this->response('info', 'No se encontraron claves en Memcached para eliminar.');
|
||||
|
||||
case 'database':
|
||||
$rowsDeleted = $this->clearDatabaseCache();
|
||||
|
||||
return $rowsDeleted
|
||||
? $this->response('warning', 'Se ha purgado toda la caché almacenada en la base de datos.')
|
||||
: $this->response('info', 'No se encontraron registros en la caché de la base de datos.');
|
||||
|
||||
case 'file':
|
||||
$filesDeleted = $this->clearFilecache();
|
||||
|
||||
return $filesDeleted
|
||||
? $this->response('warning', 'Se ha purgado toda la caché de archivos.')
|
||||
: $this->response('info', 'No se encontraron archivos en la caché para eliminar.');
|
||||
|
||||
default:
|
||||
Cache::flush();
|
||||
|
||||
return $this->response('warning', 'Caché purgada.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al limpiar la caché: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function getRedisStats()
|
||||
{
|
||||
try {
|
||||
if (!Redis::ping()) {
|
||||
return $this->response('warning', 'No se puede conectar con el servidor Redis.');
|
||||
}
|
||||
|
||||
$info = Redis::info();
|
||||
|
||||
$databases = $this->getRedisDatabases();
|
||||
|
||||
$redisInfo = [
|
||||
'server' => config('database.redis.default.host'),
|
||||
'redis_version' => $info['redis_version'] ?? 'N/A',
|
||||
'os' => $info['os'] ?? 'N/A',
|
||||
'tcp_port' => $info['tcp_port'] ?? 'N/A',
|
||||
'connected_clients' => $info['connected_clients'] ?? 'N/A',
|
||||
'blocked_clients' => $info['blocked_clients'] ?? 'N/A',
|
||||
'maxmemory' => $info['maxmemory'] ?? 0,
|
||||
'used_memory_human' => $info['used_memory_human'] ?? 'N/A',
|
||||
'used_memory_peak' => $info['used_memory_peak'] ?? 'N/A',
|
||||
'used_memory_peak_human' => $info['used_memory_peak_human'] ?? 'N/A',
|
||||
'total_system_memory' => $info['total_system_memory'] ?? 0,
|
||||
'total_system_memory_human' => $info['total_system_memory_human'] ?? 'N/A',
|
||||
'maxmemory_human' => $info['maxmemory_human'] !== '0B' ? $info['maxmemory_human'] : 'Sin Límite',
|
||||
'total_connections_received' => number_format($info['total_connections_received']) ?? 'N/A',
|
||||
'total_commands_processed' => number_format($info['total_commands_processed']) ?? 'N/A',
|
||||
'maxmemory_policy' => $info['maxmemory_policy'] ?? 'N/A',
|
||||
'role' => $info['role'] ?? 'N/A',
|
||||
'cache_database' => '',
|
||||
'sessions_database' => '',
|
||||
'general_database' => ',',
|
||||
'keys' => $databases['total_keys'],
|
||||
'used_memory' => $info['used_memory'] ?? 0,
|
||||
'uptime' => gmdate('H\h i\m s\s', $info['uptime_in_seconds'] ?? 0),
|
||||
'databases' => $databases,
|
||||
];
|
||||
|
||||
return $this->response('success', 'Se a recargado las estadísticas de Redis.', ['info' => $redisInfo]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al conectar con el servidor Redis: ' . Redis::getLastError());
|
||||
}
|
||||
}
|
||||
|
||||
public function getMemcachedStats()
|
||||
{
|
||||
try {
|
||||
$memcachedStats = [];
|
||||
|
||||
// Crear instancia del cliente Memcached
|
||||
$memcached = new \Memcached();
|
||||
$memcached->addServer(config('memcached.host'), config('memcached.port'));
|
||||
|
||||
// Obtener estadísticas del servidor
|
||||
$stats = $memcached->getStats();
|
||||
|
||||
foreach ($stats as $server => $data) {
|
||||
$server = explode(':', $server);
|
||||
|
||||
$memcachedStats[] = [
|
||||
'server' => $server[0],
|
||||
'tcp_port' => $server[1],
|
||||
'uptime' => $data['uptime'] ?? 'N/A',
|
||||
'version' => $data['version'] ?? 'N/A',
|
||||
'libevent' => $data['libevent'] ?? 'N/A',
|
||||
'max_connections' => $data['max_connections'] ?? 0,
|
||||
'total_connections' => $data['total_connections'] ?? 0,
|
||||
'rejected_connections' => $data['rejected_connections'] ?? 0,
|
||||
'curr_items' => $data['curr_items'] ?? 0, // Claves almacenadas
|
||||
'bytes' => $data['bytes'] ?? 0, // Memoria usada
|
||||
'limit_maxbytes' => $data['limit_maxbytes'] ?? 0, // Memoria máxima
|
||||
'cmd_get' => $data['cmd_get'] ?? 0, // Comandos GET ejecutados
|
||||
'cmd_set' => $data['cmd_set'] ?? 0, // Comandos SET ejecutados
|
||||
'get_hits' => $data['get_hits'] ?? 0, // GET exitosos
|
||||
'get_misses' => $data['get_misses'] ?? 0, // GET fallidos
|
||||
'evictions' => $data['evictions'] ?? 0, // Claves expulsadas
|
||||
'bytes_read' => $data['bytes_read'] ?? 0, // Bytes leídos
|
||||
'bytes_written' => $data['bytes_written'] ?? 0, // Bytes escritos
|
||||
'total_items' => $data['total_items'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->response('success', 'Se a recargado las estadísticas de Memcached.', ['info' => $memcachedStats]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al conectar con el servidor Memcached: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas para caché en base de datos.
|
||||
*/
|
||||
private function _getDatabaseStats(): array
|
||||
{
|
||||
try {
|
||||
$recordCount = DB::table('cache')->count();
|
||||
$tableInfo = DB::select("SHOW TABLE STATUS WHERE Name = 'cache'");
|
||||
|
||||
$memory_usage = isset($tableInfo[0]) ? $this->formatBytes($tableInfo[0]->Data_length + $tableInfo[0]->Index_length) : 'N/A';
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la caché de base de datos.', ['item_count' => $recordCount, 'memory_usage' => $memory_usage]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas de la base de datos: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene estadísticas para caché en archivos.
|
||||
*/
|
||||
private function _getFilecacheStats(): array
|
||||
{
|
||||
try {
|
||||
$cachePath = config('cache.stores.file.path');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
$memory_usage = $this->formatBytes(array_sum(array_map('filesize', $files)));
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la caché de archivos.', ['item_count' => count($files), 'memory_usage' => $memory_usage]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas de archivos: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function _getRedisStats()
|
||||
{
|
||||
try {
|
||||
$prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
|
||||
|
||||
$info = Redis::info();
|
||||
$keys = Redis::connection('cache')->keys($prefix . '*');
|
||||
|
||||
$memory_usage = $this->formatBytes($info['used_memory'] ?? 0);
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la caché de Redis.', ['item_count' => count($keys), 'memory_usage' => $memory_usage]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas de Redis: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function _getMemcachedStats(): array
|
||||
{
|
||||
try {
|
||||
// Obtener estadísticas generales del servidor
|
||||
$stats = Cache::getStore()->getMemcached()->getStats();
|
||||
|
||||
if (empty($stats)) {
|
||||
return $this->response('error', 'No se pudieron obtener las estadísticas del servidor Memcached.', ['item_count' => 0, 'memory_usage' => 0]);
|
||||
}
|
||||
|
||||
// Usar el primer servidor configurado (en la mayoría de los casos hay uno)
|
||||
$serverStats = array_shift($stats);
|
||||
|
||||
return $this->response(
|
||||
'success',
|
||||
'Estadísticas del servidor Memcached obtenidas correctamente.',
|
||||
[
|
||||
'item_count' => $serverStats['curr_items'] ?? 0, // Número total de claves
|
||||
'memory_usage' => $this->formatBytes($serverStats['bytes'] ?? 0), // Memoria usada
|
||||
'max_memory' => $this->formatBytes($serverStats['limit_maxbytes'] ?? 0), // Memoria máxima asignada
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas de Memcached: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function getRedisDatabases(): array
|
||||
{
|
||||
// Verificar si Redis está en uso
|
||||
$isRedisUsed = collect([
|
||||
config('cache.default'),
|
||||
config('session.driver'),
|
||||
config('queue.default'),
|
||||
])->contains('redis');
|
||||
|
||||
if (!$isRedisUsed) {
|
||||
return []; // Si Redis no está en uso, devolver un arreglo vacío
|
||||
}
|
||||
|
||||
// Configuraciones de bases de datos de Redis según su uso
|
||||
$databases = [
|
||||
'default' => config('database.redis.default.database', 0), // REDIS_DB
|
||||
'cache' => config('database.redis.cache.database', 0), // REDIS_CACHE_DB
|
||||
'sessions' => config('database.redis.sessions.database', 0), // REDIS_SESSION_DB
|
||||
];
|
||||
|
||||
$result = [];
|
||||
$totalKeys = 0;
|
||||
|
||||
// Recorrer solo las bases configuradas y activas
|
||||
foreach ($databases as $type => $db) {
|
||||
Redis::select($db); // Seleccionar la base de datos
|
||||
|
||||
$keys = Redis::dbsize(); // Contar las claves en la base
|
||||
|
||||
if ($keys > 0) {
|
||||
$result[$type] = [
|
||||
'database' => $db,
|
||||
'keys' => $keys,
|
||||
];
|
||||
|
||||
$totalKeys += $keys;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($result)) {
|
||||
$result['total_keys'] = $totalKeys;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
private function clearDatabaseCache(): bool
|
||||
{
|
||||
$count = DB::table(config('cache.stores.database.table'))->count();
|
||||
|
||||
if ($count > 0) {
|
||||
DB::table(config('cache.stores.database.table'))->truncate();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function clearFilecache(): bool
|
||||
{
|
||||
$cachePath = config('cache.stores.file.path');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
if (!empty($files)) {
|
||||
File::deleteDirectory($cachePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function clearRedisCache(): bool
|
||||
{
|
||||
$prefix = config('cache.prefix', '');
|
||||
$keys = Redis::connection('cache')->keys($prefix . '*');
|
||||
|
||||
if (!empty($keys)) {
|
||||
Redis::connection('cache')->flushdb();
|
||||
|
||||
// Simulate cache clearing delay
|
||||
sleep(1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function clearMemcachedCache(): bool
|
||||
{
|
||||
// Obtener el cliente Memcached directamente
|
||||
$memcached = Cache::store('memcached')->getStore()->getMemcached();
|
||||
|
||||
// Ejecutar flush para eliminar todo
|
||||
if ($memcached->flush()) {
|
||||
// Simulate cache clearing delay
|
||||
sleep(1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verifica si un driver es soportado.
|
||||
*/
|
||||
private function isSupportedDriver(string $driver): bool
|
||||
{
|
||||
return in_array($driver, ['redis', 'memcached', 'database', 'file']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte bytes en un formato legible.
|
||||
*/
|
||||
private function formatBytes($bytes)
|
||||
{
|
||||
$sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
|
||||
return sprintf('%.2f', $bytes / pow(1024, $factor)) . ' ' . $sizes[$factor];
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera una respuesta estandarizada.
|
||||
*/
|
||||
private function response(string $status, string $message, array $data = []): array
|
||||
{
|
||||
return array_merge(compact('status', 'message'), $data);
|
||||
}
|
||||
}
|
225
Services/GlobalSettingsService.php
Normal file
225
Services/GlobalSettingsService.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
class GlobalSettingsService
|
||||
{
|
||||
/**
|
||||
* Tiempo de vida del caché en minutos (30 días).
|
||||
*/
|
||||
private $cacheTTL = 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* Actualiza o crea una configuración.
|
||||
*/
|
||||
public function updateSetting(string $key, string $value): bool
|
||||
{
|
||||
$setting = Setting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => trim($value)]
|
||||
);
|
||||
|
||||
return $setting->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga y sobrescribe las configuraciones del sistema.
|
||||
*/
|
||||
public function loadSystemConfig(): void
|
||||
{
|
||||
try {
|
||||
if (!Schema::hasTable('migrations')) {
|
||||
// Base de datos no inicializada: usar valores predeterminados
|
||||
$config = $this->getDefaultSystemConfig();
|
||||
} else {
|
||||
// Cargar configuración desde la caché o base de datos
|
||||
$config = Cache::remember('global_system_config', $this->cacheTTL, function () {
|
||||
$settings = Setting::global()
|
||||
->where('key', 'LIKE', 'config.%')
|
||||
->pluck('value', 'key')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'servicesFacebook' => $this->buildServiceConfig($settings, 'config.services.facebook.', 'services.facebook'),
|
||||
'servicesGoogle' => $this->buildServiceConfig($settings, 'config.services.google.', 'services.google'),
|
||||
'vuexy' => $this->buildVuexyConfig($settings),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
// Aplicar configuración al sistema
|
||||
Config::set('services.facebook', $config['servicesFacebook']);
|
||||
Config::set('services.google', $config['servicesGoogle']);
|
||||
Config::set('vuexy', $config['vuexy']);
|
||||
} catch (\Exception $e) {
|
||||
// Manejo silencioso de errores para evitar interrupciones
|
||||
Config::set('services.facebook', config('services.facebook', []));
|
||||
Config::set('services.google', config('services.google', []));
|
||||
Config::set('vuexy', config('vuexy', []));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve una configuración predeterminada si la base de datos no está inicializada.
|
||||
*/
|
||||
private function getDefaultSystemConfig(): array
|
||||
{
|
||||
return [
|
||||
'servicesFacebook' => config('services.facebook', [
|
||||
'client_id' => '',
|
||||
'client_secret' => '',
|
||||
'redirect' => '',
|
||||
]),
|
||||
'servicesGoogle' => config('services.google', [
|
||||
'client_id' => '',
|
||||
'client_secret' => '',
|
||||
'redirect' => '',
|
||||
]),
|
||||
'vuexy' => config('vuexy', []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un bloque de configuraciones está presente.
|
||||
*/
|
||||
protected function hasBlockConfig(array $settings, string $blockPrefix): bool
|
||||
{
|
||||
return array_key_exists($blockPrefix, array_filter($settings, fn($key) => str_starts_with($key, $blockPrefix), ARRAY_FILTER_USE_KEY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye la configuración de un servicio (Facebook, Google, etc.).
|
||||
*/
|
||||
protected function buildServiceConfig(array $settings, string $blockPrefix, string $defaultConfigKey): array
|
||||
{
|
||||
if (!$this->hasBlockConfig($settings, $blockPrefix)) {
|
||||
return [];
|
||||
return config($defaultConfigKey);
|
||||
}
|
||||
|
||||
return [
|
||||
'client_id' => $settings["{$blockPrefix}client_id"] ?? '',
|
||||
'client_secret' => $settings["{$blockPrefix}client_secret"] ?? '',
|
||||
'redirect' => $settings["{$blockPrefix}redirect"] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye la configuración personalizada de Vuexy.
|
||||
*/
|
||||
protected function buildVuexyConfig(array $settings): array
|
||||
{
|
||||
// Configuración predeterminada del sistema
|
||||
$defaultVuexyConfig = config('vuexy', []);
|
||||
|
||||
// Convertimos las claves planas a un array multidimensional
|
||||
$settingsNested = Arr::undot($settings);
|
||||
|
||||
// Navegamos hasta la parte relevante del array desanidado
|
||||
$vuexySettings = $settingsNested['config']['vuexy'] ?? [];
|
||||
|
||||
// Fusionamos la configuración predeterminada con los valores del sistema
|
||||
$mergedConfig = array_replace_recursive($defaultVuexyConfig, $vuexySettings);
|
||||
|
||||
// Normalizamos los valores booleanos
|
||||
return $this->normalizeBooleanFields($mergedConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza los campos booleanos.
|
||||
*/
|
||||
protected function normalizeBooleanFields(array $config): array
|
||||
{
|
||||
$booleanFields = [
|
||||
'myRTLSupport',
|
||||
'myRTLMode',
|
||||
'hasCustomizer',
|
||||
'displayCustomizer',
|
||||
'footerFixed',
|
||||
'menuFixed',
|
||||
'menuCollapsed',
|
||||
'showDropdownOnHover',
|
||||
];
|
||||
|
||||
foreach ($booleanFields as $field) {
|
||||
if (isset($config['vuexy'][$field])) {
|
||||
$config['vuexy'][$field] = (bool) $config['vuexy'][$field];
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia el caché de la configuración del sistema.
|
||||
*/
|
||||
public static function clearSystemConfigCache(): void
|
||||
{
|
||||
Cache::forget('global_system_config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina las claves config.vuexy.* y limpia global_system_config
|
||||
*/
|
||||
public static function clearVuexyConfig(): void
|
||||
{
|
||||
Setting::where('key', 'LIKE', 'config.vuexy.%')->delete();
|
||||
Cache::forget('global_system_config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene y sobrescribe la configuración de correo electrónico.
|
||||
*/
|
||||
public function getMailSystemConfig(): array
|
||||
{
|
||||
return Cache::remember('mail_system_config', $this->cacheTTL, function () {
|
||||
$settings = Setting::global()
|
||||
->where('key', 'LIKE', 'mail.%')
|
||||
->pluck('value', 'key')
|
||||
->toArray();
|
||||
|
||||
$defaultMailersSmtpVars = config('mail.mailers.smtp');
|
||||
|
||||
return [
|
||||
'mailers' => [
|
||||
'smtp' => array_merge($defaultMailersSmtpVars, [
|
||||
'url' => $settings['mail.mailers.smtp.url'] ?? $defaultMailersSmtpVars['url'],
|
||||
'host' => $settings['mail.mailers.smtp.host'] ?? $defaultMailersSmtpVars['host'],
|
||||
'port' => $settings['mail.mailers.smtp.port'] ?? $defaultMailersSmtpVars['port'],
|
||||
'encryption' => $settings['mail.mailers.smtp.encryption'] ?? 'TLS',
|
||||
'username' => $settings['mail.mailers.smtp.username'] ?? $defaultMailersSmtpVars['username'],
|
||||
'password' => isset($settings['mail.mailers.smtp.password']) && !empty($settings['mail.mailers.smtp.password'])
|
||||
? Crypt::decryptString($settings['mail.mailers.smtp.password'])
|
||||
: $defaultMailersSmtpVars['password'],
|
||||
'timeout' => $settings['mail.mailers.smtp.timeout'] ?? $defaultMailersSmtpVars['timeout'],
|
||||
]),
|
||||
],
|
||||
'from' => [
|
||||
'address' => $settings['mail.from.address'] ?? config('mail.from.address'),
|
||||
'name' => $settings['mail.from.name'] ?? config('mail.from.name'),
|
||||
],
|
||||
'reply_to' => [
|
||||
'method' => $settings['mail.reply_to.method'] ?? config('mail.reply_to.method'),
|
||||
'email' => $settings['mail.reply_to.email'] ?? config('mail.reply_to.email'),
|
||||
'name' => $settings['mail.reply_to.name'] ?? config('mail.reply_to.name'),
|
||||
],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia el caché de la configuración de correo electrónico.
|
||||
*/
|
||||
public static function clearMailSystemConfigCache(): void
|
||||
{
|
||||
Cache::forget('mail_system_config');
|
||||
}
|
||||
|
||||
}
|
28
Services/RBACService.php
Normal file
28
Services/RBACService.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class RBACService
|
||||
{
|
||||
public static function loadRolesAndPermissions()
|
||||
{
|
||||
$filePath = database_path('data/rbac-config.json');
|
||||
if (!File::exists($filePath)) {
|
||||
throw new \Exception("Archivo de configuración RBAC no encontrado.");
|
||||
}
|
||||
|
||||
$rbacData = json_decode(File::get($filePath), true);
|
||||
foreach ($rbacData['permissions'] as $perm) {
|
||||
Permission::updateOrCreate(['name' => $perm]);
|
||||
}
|
||||
|
||||
foreach ($rbacData['roles'] as $name => $role) {
|
||||
$roleInstance = Role::updateOrCreate(['name' => $name, 'style' => $role['style']]);
|
||||
$roleInstance->syncPermissions($role['permissions']);
|
||||
}
|
||||
}
|
||||
}
|
153
Services/SessionManagerService.php
Normal file
153
Services/SessionManagerService.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class SessionManagerService
|
||||
{
|
||||
private string $driver;
|
||||
|
||||
public function __construct(string $driver = null)
|
||||
{
|
||||
$this->driver = $driver ?? config('session.driver');
|
||||
}
|
||||
|
||||
public function getSessionStats(string $driver = null): array
|
||||
{
|
||||
$driver = $driver ?? $this->driver;
|
||||
|
||||
if (!$this->isSupportedDriver($driver))
|
||||
return $this->response('warning', 'Driver no soportado o no configurado.', ['session_count' => 0]);
|
||||
|
||||
try {
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
return $this->getRedisStats();
|
||||
|
||||
case 'database':
|
||||
return $this->getDatabaseStats();
|
||||
|
||||
case 'file':
|
||||
return $this->getFileStats();
|
||||
|
||||
default:
|
||||
return $this->response('warning', 'Driver no reconocido.', ['session_count' => 0]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al obtener estadísticas: ' . $e->getMessage(), ['session_count' => 0]);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearSessions(string $driver = null): array
|
||||
{
|
||||
$driver = $driver ?? $this->driver;
|
||||
|
||||
if (!$this->isSupportedDriver($driver)) {
|
||||
return $this->response('warning', 'Driver no soportado o no configurado.');
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($driver) {
|
||||
case 'redis':
|
||||
return $this->clearRedisSessions();
|
||||
|
||||
case 'memcached':
|
||||
Cache::getStore()->flush();
|
||||
return $this->response('success', 'Se eliminó la memoria caché de sesiones en Memcached.');
|
||||
|
||||
case 'database':
|
||||
DB::table('sessions')->truncate();
|
||||
return $this->response('success', 'Se eliminó la memoria caché de sesiones en la base de datos.');
|
||||
|
||||
case 'file':
|
||||
return $this->clearFileSessions();
|
||||
|
||||
default:
|
||||
return $this->response('warning', 'Driver no reconocido.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->response('danger', 'Error al limpiar las sesiones: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function getRedisStats()
|
||||
{
|
||||
$prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
|
||||
$keys = Redis::connection('sessions')->keys($prefix . '*');
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la caché de Redis.', ['session_count' => count($keys)]);
|
||||
}
|
||||
|
||||
private function getDatabaseStats(): array
|
||||
{
|
||||
$sessionCount = DB::table('sessions')->count();
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de la base de datos.', ['session_count' => $sessionCount]);
|
||||
}
|
||||
|
||||
private function getFileStats(): array
|
||||
{
|
||||
$cachePath = config('session.files');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
return $this->response('success', 'Se ha recargado la información de sesiones de archivos.', ['session_count' => count($files)]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Limpia sesiones en Redis.
|
||||
*/
|
||||
private function clearRedisSessions(): array
|
||||
{
|
||||
$prefix = config('cache.prefix', '');
|
||||
$keys = Redis::connection('sessions')->keys($prefix . '*');
|
||||
|
||||
if (!empty($keys)) {
|
||||
Redis::connection('sessions')->flushdb();
|
||||
|
||||
// Simulate cache clearing delay
|
||||
sleep(1);
|
||||
|
||||
return $this->response('success', 'Se eliminó la memoria caché de sesiones en Redis.');
|
||||
}
|
||||
|
||||
return $this->response('info', 'No se encontraron claves para eliminar en Redis.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia sesiones en archivos.
|
||||
*/
|
||||
private function clearFileSessions(): array
|
||||
{
|
||||
$cachePath = config('session.files');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
if (!empty($files)) {
|
||||
foreach ($files as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
return $this->response('success', 'Se eliminó la memoria caché de sesiones en archivos.');
|
||||
}
|
||||
|
||||
return $this->response('info', 'No se encontraron sesiones en archivos para eliminar.');
|
||||
}
|
||||
|
||||
|
||||
private function isSupportedDriver(string $driver): bool
|
||||
{
|
||||
return in_array($driver, ['redis', 'memcached', 'database', 'file']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera una respuesta estandarizada.
|
||||
*/
|
||||
private function response(string $status, string $message, array $data = []): array
|
||||
{
|
||||
return array_merge(compact('status', 'message'), $data);
|
||||
}
|
||||
}
|
623
Services/VuexyAdminService.php
Normal file
623
Services/VuexyAdminService.php
Normal file
@ -0,0 +1,623 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
class VuexyAdminService
|
||||
{
|
||||
private $vuexySearch;
|
||||
private $quicklinksRouteNames = [];
|
||||
|
||||
protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos
|
||||
|
||||
private $homeRoute = [
|
||||
'name' => 'Inicio',
|
||||
'route' => 'admin.core.home.index',
|
||||
];
|
||||
|
||||
private $user;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->user = Auth::user();
|
||||
$this->vuexySearch = Auth::user() !== null;
|
||||
$this->orientation = config('vuexy.custom.myLayout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el menú según el estado del usuario (autenticado o no).
|
||||
*/
|
||||
public function getMenu()
|
||||
{
|
||||
// Obtener el menú desde la caché
|
||||
$menu = $this->user === null
|
||||
? $this->getGuestMenu()
|
||||
: $this->getUserMenu();
|
||||
|
||||
// Marcar la ruta actual como activa
|
||||
$currentRoute = Route::currentRouteName();
|
||||
|
||||
return $this->markActive($menu, $currentRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menú para usuarios no autenticados.dump
|
||||
*/
|
||||
private function getGuestMenu()
|
||||
{
|
||||
return Cache::remember('vuexy_menu_guest', now()->addDays(7), function () {
|
||||
return $this->getMenuArray();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Menú para usuarios autenticados.
|
||||
*/
|
||||
private function getUserMenu()
|
||||
{
|
||||
Cache::forget("vuexy_menu_user_{$this->user->id}"); // Borrar la caché anterior para actualizarla
|
||||
|
||||
return Cache::remember("vuexy_menu_user_{$this->user->id}", now()->addHours(24), function () {
|
||||
return $this->getMenuArray();
|
||||
});
|
||||
}
|
||||
|
||||
private function markActive($menu, $currentRoute)
|
||||
{
|
||||
foreach ($menu as &$item) {
|
||||
$item['active'] = false;
|
||||
|
||||
// Check if the route matches
|
||||
if (isset($item['route']) && $item['route'] === $currentRoute)
|
||||
$item['active'] = true;
|
||||
|
||||
// Process submenus recursively
|
||||
if (isset($item['submenu']) && !empty($item['submenu'])) {
|
||||
$item['submenu'] = $this->markActive($item['submenu'], $currentRoute);
|
||||
|
||||
// If any submenu is active, mark the parent as active
|
||||
if (collect($item['submenu'])->contains('active', true))
|
||||
$item['active'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalida el cache del menú de un usuario.
|
||||
*/
|
||||
public static function clearUserMenuCache()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user !== null)
|
||||
Cache::forget("vuexy_menu_user_{$user->id}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalida el cache del menú de invitados.
|
||||
*/
|
||||
public static function clearGuestMenuCache()
|
||||
{
|
||||
Cache::forget('vuexy_menu_guest');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getSearch()
|
||||
{
|
||||
return $this->vuexySearch;
|
||||
}
|
||||
|
||||
public function getVuexySearchData()
|
||||
{
|
||||
if ($this->user === null)
|
||||
return null;
|
||||
|
||||
$pages = Cache::remember("vuexy_search_user_{$this->user->id}", now()->addDays(7), function () {
|
||||
return $this->cacheVuexySearchData();
|
||||
});
|
||||
|
||||
// Formatear como JSON esperado
|
||||
return [
|
||||
'pages' => $pages,
|
||||
];
|
||||
}
|
||||
|
||||
private function cacheVuexySearchData()
|
||||
{
|
||||
$originalMenu = $this->getUserMenu();
|
||||
|
||||
return $this->getPagesSearchMenu($originalMenu);
|
||||
}
|
||||
|
||||
private function getPagesSearchMenu(array $menu, string $parentPath = '')
|
||||
{
|
||||
$formattedMenu = [];
|
||||
|
||||
foreach ($menu as $name => $item) {
|
||||
// Construir la ruta jerárquica (menu / submenu / submenu)
|
||||
$currentPath = $parentPath ? $parentPath . ' / ' . $name : $name;
|
||||
|
||||
// Verificar si el elemento tiene una URL o una ruta
|
||||
$url = $item['url'] ?? (isset($item['route']) && route::has($item['route']) ? route($item['route']) : null);
|
||||
|
||||
// Agregar el elemento al menú formateado
|
||||
if ($url) {
|
||||
$formattedMenu[] = [
|
||||
'name' => $currentPath, // Usar la ruta completa
|
||||
'icon' => $item['icon'] ?? 'ti ti-point',
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
|
||||
// Si hay un submenú, procesarlo recursivamente
|
||||
if (isset($item['submenu']) && is_array($item['submenu'])) {
|
||||
$formattedMenu = array_merge(
|
||||
$formattedMenu,
|
||||
$this->getPagesSearchMenu($item['submenu'], $currentPath) // Pasar el path acumulado
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $formattedMenu;
|
||||
}
|
||||
|
||||
public static function clearSearchMenuCache()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user !== null)
|
||||
Cache::forget("vuexy_search_user_{$user->id}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getQuickLinks()
|
||||
{
|
||||
if ($this->user === null)
|
||||
return null;
|
||||
|
||||
// Recuperar enlaces desde la caché
|
||||
$quickLinks = Cache::remember("vuexy_quick_links_user_{$this->user->id}", now()->addDays(7), function () {
|
||||
return $this->cacheQuickLinks();
|
||||
});
|
||||
|
||||
// Verificar si la ruta actual está en la lista
|
||||
$currentRoute = Route::currentRouteName();
|
||||
$currentPageInList = $this->isCurrentPageInList($quickLinks, $currentRoute);
|
||||
|
||||
// Agregar la verificación al resultado
|
||||
$quickLinks['current_page_in_list'] = $currentPageInList;
|
||||
|
||||
return $quickLinks;
|
||||
}
|
||||
|
||||
private function cacheQuickLinks()
|
||||
{
|
||||
$originalMenu = $this->getUserMenu();
|
||||
|
||||
$quickLinks = [];
|
||||
|
||||
$quicklinks = Setting::where('user_id', Auth::user()->id)
|
||||
->where('key', 'quicklinks')
|
||||
->first();
|
||||
|
||||
$this->quicklinksRouteNames = $quicklinks ? json_decode($quicklinks->value, true) : [];
|
||||
|
||||
// Ordenar y generar los quickLinks según el orden del menú
|
||||
$this->collectQuickLinksFromMenu($originalMenu, $quickLinks);
|
||||
|
||||
$quickLinksData = [
|
||||
'totalLinks' => count($quickLinks),
|
||||
'rows' => array_chunk($quickLinks, 2), // Agrupar los atajos en filas de dos
|
||||
];
|
||||
|
||||
return $quickLinksData;
|
||||
}
|
||||
|
||||
private function collectQuickLinksFromMenu(array $menu, array &$quickLinks, string $parentTitle = null)
|
||||
{
|
||||
foreach ($menu as $title => $item) {
|
||||
// Verificar si el elemento está en la lista de quicklinksRouteNames
|
||||
if (isset($item['route']) && in_array($item['route'], $this->quicklinksRouteNames)) {
|
||||
$quickLinks[] = [
|
||||
'title' => $title,
|
||||
'subtitle' => $parentTitle ?? env('APP_NAME'),
|
||||
'icon' => $item['icon'] ?? 'ti ti-point',
|
||||
'url' => isset($item['route']) ? route($item['route']) : ($item['url'] ?? '#'),
|
||||
'route' => $item['route'],
|
||||
];
|
||||
}
|
||||
|
||||
// Si tiene submenú, procesarlo recursivamente
|
||||
if (isset($item['submenu']) && is_array($item['submenu'])) {
|
||||
$this->collectQuickLinksFromMenu(
|
||||
$item['submenu'],
|
||||
$quickLinks,
|
||||
$title // Pasar el título actual como subtítulo
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si la ruta actual existe en la lista de enlaces.
|
||||
*/
|
||||
private function isCurrentPageInList(array $quickLinks, string $currentRoute): bool
|
||||
{
|
||||
foreach ($quickLinks['rows'] as $row) {
|
||||
foreach ($row as $link) {
|
||||
if (isset($link['route']) && $link['route'] === $currentRoute) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function clearQuickLinksCache()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user !== null)
|
||||
Cache::forget("vuexy_quick_links_user_{$user->id}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getNotifications()
|
||||
{
|
||||
if ($this->user === null)
|
||||
return null;
|
||||
|
||||
return Cache::remember("vuexy_notifications_user_{$this->user->id}", now()->addHours(4), function () {
|
||||
return $this->cacheNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
private function cacheNotifications()
|
||||
{
|
||||
return "<li class='nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2'>
|
||||
<a class='nav-link btn btn-text-secondary btn-icon rounded-pill dropdown-toggle hide-arrow' href='javascript:void(0);' data-bs-toggle='dropdown' data-bs-auto-close='outside' aria-expanded='false'>
|
||||
<span class='position-relative'>
|
||||
<i class='ti ti-bell ti-md'></i>
|
||||
<span class='badge rounded-pill bg-danger badge-dot badge-notifications border'></span>
|
||||
</span>
|
||||
</a>
|
||||
<ul class='dropdown-menu dropdown-menu-end p-0'>
|
||||
<li class='dropdown-menu-header border-bottom'>
|
||||
<div class='dropdown-header d-flex align-items-center py-3'>
|
||||
<h6 class='mb-0 me-auto'>Notification</h6>
|
||||
<div class='d-flex align-items-center h6 mb-0'>
|
||||
<span class='badge bg-label-primary me-2'>8 New</span>
|
||||
<a href='javascript:void(0)' class='btn btn-text-secondary rounded-pill btn-icon dropdown-notifications-all' data-bs-toggle='tooltip' data-bs-placement='top' title='Mark all as read'><i class='ti ti-mail-opened text-heading'></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='dropdown-notifications-list scrollable-container'>
|
||||
<ul class='list-group list-group-flush'>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<img src='' . asset('assets/admin/img/avatars/1.png') . '' alt class='rounded-circle'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='small mb-1'>Congratulation Lettie 🎉</h6>
|
||||
<small class='mb-1 d-block text-body'>Won the monthly best seller gold badge</small>
|
||||
<small class='text-muted'>1h ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<span class='avatar-initial rounded-circle bg-label-danger'>CF</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='mb-1 small'>Charles Franklin</h6>
|
||||
<small class='mb-1 d-block text-body'>Accepted your connection</small>
|
||||
<small class='text-muted'>12hr ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<img src='' . asset('assets/admin/img/avatars/2.png') . '' alt class='rounded-circle'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='mb-1 small'>New Message ✉️</h6>
|
||||
<small class='mb-1 d-block text-body'>You have new message from Natalie</small>
|
||||
<small class='text-muted'>1h ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<span class='avatar-initial rounded-circle bg-label-success'><i class='ti ti-shopping-cart'></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='mb-1 small'>Whoo! You have new order 🛒 </h6>
|
||||
<small class='mb-1 d-block text-body'>ACME Inc. made new order $1,154</small>
|
||||
<small class='text-muted'>1 day ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<img src='' . asset('assets/admin/img/avatars/9.png') . '' alt class='rounded-circle'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='mb-1 small'>Application has been approved 🚀 </h6>
|
||||
<small class='mb-1 d-block text-body'>Your ABC project application has been approved.</small>
|
||||
<small class='text-muted'>2 days ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<span class='avatar-initial rounded-circle bg-label-success'><i class='ti ti-chart-pie'></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='mb-1 small'>Monthly report is generated</h6>
|
||||
<small class='mb-1 d-block text-body'>July monthly financial report is generated </small>
|
||||
<small class='text-muted'>3 days ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<img src='' . asset('assets/admin/img/avatars/5.png') . '' alt class='rounded-circle'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='mb-1 small'>Send connection request</h6>
|
||||
<small class='mb-1 d-block text-body'>Peter sent you connection request</small>
|
||||
<small class='text-muted'>4 days ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<img src='' . asset('assets/admin/img/avatars/6.png') . '' alt class='rounded-circle'>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='mb-1 small'>New message from Jane</h6>
|
||||
<small class='mb-1 d-block text-body'>Your have new message from Jane</small>
|
||||
<small class='text-muted'>5 days ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
|
||||
<div class='d-flex'>
|
||||
<div class='flex-shrink-0 me-3'>
|
||||
<div class='avatar'>
|
||||
<span class='avatar-initial rounded-circle bg-label-warning'><i class='ti ti-alert-triangle'></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-grow-1'>
|
||||
<h6 class='mb-1 small'>CPU is running high</h6>
|
||||
<small class='mb-1 d-block text-body'>CPU Utilization Percent is currently at 88.63%,</small>
|
||||
<small class='text-muted'>5 days ago</small>
|
||||
</div>
|
||||
<div class='flex-shrink-0 dropdown-notifications-actions'>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
|
||||
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class='border-top'>
|
||||
<div class='d-grid p-4'>
|
||||
<a class='btn btn-primary btn-sm d-flex' href='javascript:void(0);'>
|
||||
<small class='align-middle'>View all notifications</small>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>";
|
||||
}
|
||||
|
||||
public static function clearNotificationsCache()
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if ($user !== null)
|
||||
Cache::forget("vuexy_notifications_user_{$user->id}");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getBreadcrumbs()
|
||||
{
|
||||
$originalMenu = $this->user === null
|
||||
? $this->getGuestMenu()
|
||||
: $this->getUserMenu();
|
||||
|
||||
// Lógica para construir los breadcrumbs
|
||||
$breadcrumbs = $this->findBreadcrumbTrail($originalMenu);
|
||||
|
||||
// Asegurar que el primer elemento siempre sea "Inicio"
|
||||
array_unshift($breadcrumbs, $this->homeRoute);
|
||||
|
||||
return $breadcrumbs;
|
||||
}
|
||||
|
||||
private function findBreadcrumbTrail(array $menu, array $breadcrumbs = []): array
|
||||
{
|
||||
foreach ($menu as $title => $item) {
|
||||
$skipBreadcrumb = isset($item['breadcrumbs']) && $item['breadcrumbs'] === false;
|
||||
|
||||
$itemRoute = isset($item['route']) ? implode('.', array_slice(explode('.', $item['route']), 0, -1)): '';
|
||||
$currentRoute = implode('.', array_slice(explode('.', Route::currentRouteName()), 0, -1));
|
||||
|
||||
if ($itemRoute === $currentRoute) {
|
||||
if (!$skipBreadcrumb) {
|
||||
$breadcrumbs[] = [
|
||||
'name' => $title,
|
||||
'active' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return $breadcrumbs;
|
||||
}
|
||||
|
||||
if (isset($item['submenu']) && is_array($item['submenu'])) {
|
||||
$newBreadcrumbs = $breadcrumbs;
|
||||
|
||||
if (!$skipBreadcrumb)
|
||||
$newBreadcrumbs[] = [
|
||||
'name' => $title,
|
||||
'route' => $item['route'] ?? null,
|
||||
];
|
||||
|
||||
$found = $this->findBreadcrumbTrail($item['submenu'], $newBreadcrumbs);
|
||||
|
||||
if ($found)
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private function getMenuArray()
|
||||
{
|
||||
$configMenu = config('vuexy_menu');
|
||||
|
||||
return $this->filterMenu($configMenu);
|
||||
}
|
||||
|
||||
private function filterMenu(array $menu)
|
||||
{
|
||||
$filteredMenu = [];
|
||||
|
||||
foreach ($menu as $key => $item) {
|
||||
// Evaluar permisos con Spatie y eliminar elementos no autorizados
|
||||
if (isset($item['can']) && !$this->userCan($item['can'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($item['canNot']) && $this->userCannot($item['canNot'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Si tiene un submenú, filtrarlo recursivamente
|
||||
if (isset($item['submenu'])) {
|
||||
$item['submenu'] = $this->filterMenu($item['submenu']);
|
||||
|
||||
// Si el submenú queda vacío, eliminar el menú
|
||||
if (empty($item['submenu'])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Removemos los atributos 'can' y 'canNot' del resultado final
|
||||
unset($item['can'], $item['canNot']);
|
||||
|
||||
if(isset($item['route']) && route::has($item['route'])){
|
||||
$item['url'] = route($item['route'])?? '';
|
||||
}
|
||||
|
||||
// Agregar elemento filtrado al menú resultante
|
||||
$filteredMenu[$key] = $item;
|
||||
}
|
||||
|
||||
return $filteredMenu;
|
||||
}
|
||||
|
||||
private function userCan($permissions)
|
||||
{
|
||||
if (is_array($permissions)) {
|
||||
foreach ($permissions as $permission) {
|
||||
if (Gate::allows($permission)) {
|
||||
return true; // Si tiene al menos un permiso, lo mostramos
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return Gate::allows($permissions);
|
||||
}
|
||||
|
||||
private function userCannot($permissions)
|
||||
{
|
||||
if (is_array($permissions)) {
|
||||
foreach ($permissions as $permission) {
|
||||
if (Gate::denies($permission)) {
|
||||
return true; // Si se le ha denegado al menos un permiso, lo ocultamos
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return Gate::denies($permissions);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user