Prepare modules

This commit is contained in:
2025-03-22 12:44:30 -06:00
parent 099267ee07
commit 7d8566350d
137 changed files with 3723 additions and 4325 deletions

View File

@ -0,0 +1,66 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
class AppDescriptionSettings extends Component
{
private $targetNotify = "#app-description-settings-card .notification-container";
public $app_name,
$title,
$description;
public function mount()
{
$this->resetForm();
}
public function save()
{
$this->validate([
'app_name' => 'required|string|max:255',
'title' => 'required|string|max:255',
'description' => 'nullable|string|max:255',
]);
// Guardar título del sitio en configuraciones
$SettingsService = app(SettingsService::class);
$SettingsService->set('admin.app_name', $this->app_name, null, 'vuexy-admin');
$SettingsService->set('admin.title', $this->title, null, 'vuexy-admin');
$SettingsService->set('admin.description', $this->description, null, 'vuexy-admin');
// Limpiar cache de plantilla
app(AdminTemplateService::class)->clearAdminVarsCache();
// Recargamos el formulario
$this->resetForm();
// Notificación de éxito
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.'
);
}
public function resetForm()
{
// Obtener los valores de las configuraciones de la base de datos
$settings = app(AdminTemplateService::class)->getAdminVars();
$this->app_name = $settings['app_name'];
$this->title = $settings['title'];
$this->description = $settings['description'];
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.app-description-settings');
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Livewire\WithFileUploads;
use Koneko\VuexyAdmin\Services\AdminSettingsService;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
class AppFaviconSettings extends Component
{
use WithFileUploads;
private $targetNotify = "#app-favicon-settings-card .notification-container";
public $admin_favicon_16x16,
$admin_favicon_76x76,
$admin_favicon_120x120,
$admin_favicon_152x152,
$admin_favicon_180x180,
$admin_favicon_192x192;
public $upload_image_favicon;
public function mount()
{
$this->resetForm();
}
public function save()
{
$this->validate([
'upload_image_favicon' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
]);
// Procesar favicon
app(AdminSettingsService::class)->processAndSaveFavicon($this->upload_image_favicon);
// Limpiar cache de plantilla
app(AdminTemplateService::class)->clearAdminVarsCache();
// Recargamos el formulario
$this->resetForm();
// Notificación de éxito
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.'
);
}
public function resetForm()
{
// Obtener los valores de las configuraciones de la base de datos
$settings = app(AdminTemplateService::class)->getAdminVars();
$this->upload_image_favicon = null;
$this->admin_favicon_16x16 = $settings['favicon']['16x16'];
$this->admin_favicon_76x76 = $settings['favicon']['76x76'];
$this->admin_favicon_120x120 = $settings['favicon']['120x120'];
$this->admin_favicon_152x152 = $settings['favicon']['152x152'];
$this->admin_favicon_180x180 = $settings['favicon']['180x180'];
$this->admin_favicon_192x192 = $settings['favicon']['192x192'];
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.app-favicon-settings');
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Illuminate\Validation\Rule;
use Koneko\VuexyAdmin\Models\Setting;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
/**
* Class GlobalSettingOffCanvasForm
*
* Componente Livewire para gestionar parámetros globales del sistema.
* Permite almacenar configuraciones personalizadas y valores múltiples tipos.
*
* @package Koneko\VuexyAdmin\Livewire\Settings
*/
class GlobalSettingOffCanvasForm extends AbstractFormOffCanvasComponent
{
public $id, $key, $category, $user_id,
$value_string, $value_integer, $value_boolean,
$value_float, $value_text;
public $confirmDeletion;
protected $casts = [
'value_boolean' => 'boolean',
'value_integer' => 'integer',
'value_float' => 'float',
];
protected $listeners = [
'editGlobalSetting' => 'loadFormModel',
'confirmDeletionGlobalSetting' => 'loadFormModelForDeletion',
];
protected function model(): string
{
return Setting::class;
}
protected function fields(): array
{
return [
'key', 'category', 'user_id',
'value_string', 'value_integer', 'value_boolean',
'value_float', 'value_text'
];
}
protected function defaults(): array
{
return [
'category' => 'general',
];
}
protected function focusOnOpen(): string
{
return 'key';
}
protected function dynamicRules(string $mode): array
{
if ($mode === 'delete') {
return ['confirmDeletion' => 'accepted'];
}
$uniqueRule = Rule::unique('settings', 'key')
->where(fn ($q) => $q
->where('user_id', $this->user_id)
->where('category', $this->category)
);
if ($mode === 'edit') {
$uniqueRule = $uniqueRule->ignore($this->id);
}
return [
'key' => ['required', 'string', $uniqueRule],
'category' => ['nullable', 'string', 'max:96'],
'user_id' => ['nullable', 'integer', 'exists:users,id'],
'value_string' => ['nullable', 'string', 'max:255'],
'value_integer' => ['nullable', 'integer'],
'value_boolean' => ['nullable', 'boolean'],
'value_float' => ['nullable', 'numeric'],
'value_text' => ['nullable', 'string'],
];
}
protected function attributes(): array
{
return [
'key' => 'clave de configuración',
'category' => 'categoría',
];
}
protected function messages(): array
{
return [
'key.required' => 'La clave del parámetro es obligatoria.',
'key.unique' => 'Ya existe una configuración con esta clave en esa categoría.',
];
}
protected function viewPath(): string
{
return 'vuexy-admin::livewire.global-settings.offcanvas-form';
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
use Koneko\VuexyAdmin\Models\Setting;
/**
* Listado de Configuraciones (settings), extiende la clase base AbstractIndexComponent
* para reutilizar la lógica de configuración y renderizado de tablas.
*/
class GlobalSettingsIndex extends AbstractIndexComponent
{
/**
* Define la clase o instancia del modelo a usar.
*
* @return string
*/
protected function model(): string
{
return Setting::class;
}
/**
* Retorna las columnas (header) de la tabla.
* Se eligen las columnas más relevantes para mantener una interfaz mobile-first.
*
* @return array
*/
protected function columns(): array
{
return [
'action' => 'Acciones',
'key' => 'Clave',
'category' => 'Categoría',
'user_fullname' => 'Usuario',
'created_at' => 'Creado',
];
}
/**
* Retorna el formato (formatter) para cada columna.
* Se aplican formatters para resaltar la información y se establecen propiedades de alineación y visibilidad.
*
* @return array
*/
protected function format(): array
{
return [
'action' => [
'formatter' => 'settingActionFormatter',
'onlyFormatter' => true,
],
'key' => [
'formatter' => [
'name' => 'dynamicBadgeFormatter',
'params' => ['color' => 'primary'],
],
'align' => 'center',
'switchable' => false,
],
'category' => [
'switchable' => false,
],
'user_fullname' => [
'switchable' => false,
],
'created_at' => [
'formatter' => 'whitespaceNowrapFormatter',
'align' => 'center',
'visible' => false,
],
];
}
/**
* Sobrescribe la configuración base de la tabla para ajustar
* la vista y funcionalidades específicas del catálogo.
*
* @return array
*/
protected function bootstraptableConfig(): array
{
return array_merge(parent::bootstraptableConfig(), [
'sortName' => 'key',
'exportFileName' => 'Configuración',
'showFullscreen' => false,
'showPaginationSwitch' => false,
'showRefresh' => false,
'pagination' => false,
]);
}
/**
* Retorna la vista a renderizar para este componente.
*
* @return string
*/
protected function viewPath(): string
{
return 'vuexy-admin::livewire.global-settings.index';
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Livewire\WithFileUploads;
use Koneko\VuexyAdmin\Services\AdminSettingsService;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
class LogoOnDarkBgSettings extends Component
{
use WithFileUploads;
private $targetNotify = "#logo-on-dark-bg-settings-card .notification-container";
public $admin_image_logo_dark,
$upload_image_logo_dark;
public function mount()
{
$this->resetForm();
}
public function save()
{
$this->validate([
'upload_image_logo_dark' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
]);
// Procesar favicon si se ha cargado una imagen
app(AdminSettingsService::class)->processAndSaveImageLogo($this->upload_image_logo_dark, 'dark');
// Limpiar cache de plantilla
app(AdminTemplateService::class)->clearAdminVarsCache();
// Recargamos el formulario
$this->resetForm();
// Notificación de éxito
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.'
);
}
public function resetForm()
{
// Obtener los valores de las configuraciones de la base de datos
$settings = app(AdminTemplateService::class)->getAdminVars();
$this->upload_image_logo_dark = null;
$this->admin_image_logo_dark = $settings['image_logo']['large_dark'];
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.logo-on-dark-bg-settings');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Livewire\WithFileUploads;
use Koneko\VuexyAdmin\Services\AdminSettingsService;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
class LogoOnLightBgSettings extends Component
{
use WithFileUploads;
private $targetNotify = "#logo-on-light-bg-settings-card .notification-container";
public $admin_image_logo,
$upload_image_logo;
public function mount()
{
$this->resetForm();
}
public function save()
{
$this->validate([
'upload_image_logo' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
]);
// Procesar favicon si se ha cargado una imagen
app(AdminSettingsService::class)->processAndSaveImageLogo($this->upload_image_logo);
// Limpiar cache de plantilla
app(AdminTemplateService::class)->clearAdminVarsCache();
// Recargamos el formulario
$this->resetForm();
// Notificación de éxito
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.'
);
}
public function resetForm()
{
// Obtener los valores de las configuraciones de la base de datos
$settings = app(AdminTemplateService::class)->getAdminVars();
$this->upload_image_logo = null;
$this->admin_image_logo = $settings['image_logo']['large'];
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.logo-on-light-bg-settings');
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
class QuickAccessWidget extends Component
{
public $quickAccessItems = [];
public function mount()
{
$menuConfig = config('vuexy_menu');
$this->quickAccessItems = $this->processMenu($menuConfig);
}
private function processMenu(array $menu): array
{
$user = Auth::user();
$accessItems = [];
foreach ($menu as $section => $items) {
if (!isset($items['submenu']) || !is_array($items['submenu'])) {
continue;
}
$categoryData = [
'title' => $section,
'icon' => $items['icon'] ?? 'ti ti-folder',
'description' => $items['description'] ?? '',
'submenu' => []
];
$this->processSubmenu($items['submenu'], $categoryData['submenu'], $user);
if (!empty($categoryData['submenu'])) {
$accessItems[] = $categoryData;
}
}
return $accessItems;
}
private function processSubmenu(array $submenu, array &$categorySubmenu, $user)
{
foreach ($submenu as $title => $item) {
// Si el elemento NO tiene 'route' ni 'url' y SOLO contiene un submenu, no lo mostramos como acceso directo
if (!isset($item['route']) && !isset($item['url']) && isset($item['submenu'])) {
// Procesamos los submenús de este elemento sin agregarlo directamente a la lista
$this->processSubmenu($item['submenu'], $categorySubmenu, $user);
continue;
}
// Validar si el usuario tiene permiso
$can = $item['can'] ?? null;
if (!$can || $user->can($can)) {
// Si tiene ruta y existe en Laravel, usarla; si no, usar url, y si tampoco hay, usar 'javascript:;'
$routeExists = isset($item['route']) && Route::has($item['route']);
$url = $routeExists ? route($item['route']) : ($item['url'] ?? 'javascript:;');
// Agregar elemento al submenu si tiene un destino válido
$categorySubmenu[] = [
'title' => $title,
'icon' => $item['icon'] ?? 'ti ti-circle',
'url' => $url,
];
}
// Si el elemento tiene un submenu, también lo procesamos
if (isset($item['submenu']) && is_array($item['submenu'])) {
$this->processSubmenu($item['submenu'], $categorySubmenu, $user);
}
}
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.quick-access-widget', [
'quickAccessItems' => $this->quickAccessItems,
]);
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Crypt;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mime\Email;
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
class SendmailSettings extends Component
{
private $targetNotify = "#sendmail-settings-card .notification-container";
public $change_smtp_settings,
$host,
$port,
$encryption,
$username,
$password;
public $save_button_disabled;
protected $listeners = [
'loadSettings',
'testSmtpConnection',
];
// the list of smtp_encryption values that can be stored in table
const SMTP_ENCRYPTION_SSL = 'SSL';
const SMTP_ENCRYPTION_TLS = 'TLS';
const SMTP_ENCRYPTION_NONE = 'none';
public $encryption_options = [
self::SMTP_ENCRYPTION_SSL => 'SSL (Secure Sockets Layer)',
self::SMTP_ENCRYPTION_TLS => 'TLS (Transport Layer Security)',
self::SMTP_ENCRYPTION_NONE => 'Sin encriptación (No recomendado)',
];
public $rules = [
[
'host' => 'nullable|string|max:255',
'port' => 'nullable|integer',
'encryption' => 'nullable|string',
'username' => 'nullable|string|max:255',
'password' => 'nullable|string|max:255',
],
[
'host.string' => 'El servidor SMTP debe ser una cadena de texto.',
'host.max' => 'El servidor SMTP no puede exceder los 255 caracteres.',
'port.integer' => 'El puerto SMTP debe ser un número entero.',
'encryption.string' => 'El tipo de encriptación SMTP debe ser una cadena de texto.',
'username.string' => 'El nombre de usuario SMTP debe ser una cadena de texto.',
'username.max' => 'El nombre de usuario SMTP no puede exceder los 255 caracteres.',
'password.string' => 'La contraseña SMTP debe ser una cadena de texto.',
'password.max' => 'La contraseña SMTP no puede exceder los 255 caracteres.',
]
];
public function mount()
{
$this->loadSettings();
}
public function loadSettings()
{
$settings = app(GlobalSettingsService::class)->getMailSystemConfig();
$this->change_smtp_settings = false;
$this->save_button_disabled = true;
$this->host = $settings['mailers']['smtp']['host'];
$this->port = $settings['mailers']['smtp']['port'];
$this->encryption = $settings['mailers']['smtp']['encryption'];
$this->username = $settings['mailers']['smtp']['username'];
$this->password = null;
}
public function save()
{
$this->validate($this->rules[0]);
$globalSettingsService = app(GlobalSettingsService::class);
// Guardar título del App en configuraciones
$globalSettingsService->updateSetting('mail.mailers.smtp.host', $this->host);
$globalSettingsService->updateSetting('mail.mailers.smtp.port', $this->port);
$globalSettingsService->updateSetting('mail.mailers.smtp.encryption', $this->encryption);
$globalSettingsService->updateSetting('mail.mailers.smtp.username', $this->username);
$globalSettingsService->updateSetting('mail.mailers.smtp.password', Crypt::encryptString($this->password));
$globalSettingsService->clearMailSystemConfigCache();
$this->loadSettings();
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.'
);
}
public function testSmtpConnection()
{
// Validar los datos del formulario
$this->validate($this->rules[0]);
try {
// Verificar la conexión SMTP
if ($this->validateSMTPConnection()) {
$this->save_button_disabled = false;
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Conexión SMTP exitosa, se guardó los cambios exitosamente.',
);
}
} catch (\Exception $e) {
// Captura y maneja errores de conexión SMTP
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'danger',
message: 'Error en la conexión SMTP: ' . $e->getMessage(),
delay: 15000 // Timeout personalizado
);
}
}
private function validateSMTPConnection()
{
$dsn = sprintf(
'smtp://%s:%s@%s:%s?encryption=%s',
urlencode($this->username), // Codificar nombre de usuario
urlencode($this->password), // Codificar contraseña
$this->host, // Host SMTP
$this->port, // Puerto SMTP
$this->encryption // Encriptación (tls o ssl)
);
// Crear el transportador usando el DSN
$transport = Transport::fromDsn($dsn);
// Crear el mailer con el transportador personalizado
$mailer = new Mailer($transport);
// Enviar un correo de prueba
$email = (new Email())
->from($this->username) // Dirección de correo del remitente
->to(env('MAIL_SANDBOX')) // Dirección de correo de destino
->subject(Config::get('app.name') . ' - Correo de prueba')
->text('Este es un correo de prueba para verificar la conexión SMTP.');
// Enviar el correo
$mailer->send($email);
return true;
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.sendmail-settings');
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
use Koneko\VuexyAdmin\Services\SettingsService;
class VuexyInterfaceSettings extends Component
{
private $targetNotify = "#interface-settings-card .notification-container";
public $uniqueId;
public $vuexy_myLayout,
$vuexy_myTheme,
$vuexy_myStyle,
$vuexy_hasCustomizer,
$vuexy_displayCustomizer,
$vuexy_contentLayout,
$vuexy_navbarType,
$vuexy_footerFixed,
$vuexy_menuFixed,
$vuexy_menuCollapsed,
$vuexy_headerType,
$vuexy_showDropdownOnHover,
$vuexy_authViewMode,
$vuexy_maxQuickLinks;
public function mount()
{
$this->uniqueId = uniqid();
$this->resetForm();
}
public function save()
{
$this->validate([
'vuexy_maxQuickLinks' => 'required|integer|min:2|max:20',
]);
// Guardar configuraciones utilizando SettingsService
$SettingsService = app(SettingsService::class);
$SettingsService->set('config.vuexy.custom.myLayout', $this->vuexy_myLayout, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.myTheme', $this->vuexy_myTheme, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.myStyle', $this->vuexy_myStyle, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.hasCustomizer', $this->vuexy_hasCustomizer, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.displayCustomizer', ($this->vuexy_hasCustomizer? $this->vuexy_displayCustomizer: false), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.contentLayout', $this->vuexy_contentLayout, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.navbarType', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_navbarType: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.footerFixed', $this->vuexy_footerFixed, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.menuFixed', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_menuFixed: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.menuCollapsed', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_menuCollapsed: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.headerType', ($this->vuexy_myLayout == 'horizontal' ? $this->vuexy_headerType: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.showDropdownOnHover', ($this->vuexy_myLayout == 'horizontal' ? $this->vuexy_showDropdownOnHover: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.authViewMode', $this->vuexy_authViewMode, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.maxQuickLinks', $this->vuexy_maxQuickLinks, null, 'vuexy-admin');
// Elimina la Cache de Configuraciones
app(GlobalSettingsService::class)->clearSystemConfigCache();
// Refrescar el componente actual
$this->dispatch('clearLocalStoregeTemplateCustomizer');
// Notificación de éxito
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.',
deferReload: true
);
}
public function clearCustomConfig()
{
// Elimina las claves config.vuexy.* para cargar los valores por defecto
app(GlobalSettingsService::class)->clearVuexyConfig();
// Refrescar el componente actual
$this->dispatch('clearLocalStoregeTemplateCustomizer');
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.',
deferReload: true
);
}
public function resetForm()
{
// Obtener los valores de las configuraciones de la base de datos
$settings = app(AdminTemplateService::class)->getVuexyCustomizerVars();
$this->vuexy_myLayout = $settings['myLayout'];
$this->vuexy_myTheme = $settings['myTheme'];
$this->vuexy_myStyle = $settings['myStyle'];
$this->vuexy_hasCustomizer = $settings['hasCustomizer'];
$this->vuexy_displayCustomizer = $settings['displayCustomizer'];
$this->vuexy_contentLayout = $settings['contentLayout'];
$this->vuexy_navbarType = $settings['navbarType'];
$this->vuexy_footerFixed = $settings['footerFixed'];
$this->vuexy_menuFixed = $settings['menuFixed'];
$this->vuexy_menuCollapsed = $settings['menuCollapsed'];
$this->vuexy_headerType = $settings['headerType'];
$this->vuexy_showDropdownOnHover = $settings['showDropdownOnHover'];
$this->vuexy_authViewMode = $settings['authViewMode'];
$this->vuexy_maxQuickLinks = $settings['maxQuickLinks'];
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.interface-settings');
}
}