first commit

This commit is contained in:
2025-03-07 00:29:07 -06:00
commit b21a11c2ee
564 changed files with 94041 additions and 0 deletions

View 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();
}
}
}
}

View 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");
}
}

View 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();
}
}
}

View 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)];
}
}

View 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'),
]);
}
}

View 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);
}
}

View 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
View 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']);
}
}
}

View 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);
}
}

View 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);
}
}