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,83 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
use Livewire\Component;
use Livewire\WithFileUploads;
use Koneko\VuexyAdmin\Services\AdminSettingsService;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
class ApplicationSettings extends Component
{
use WithFileUploads;
private $targetNotify = "#application-settings-card .notification-container";
public $admin_app_name,
$admin_image_logo,
$admin_image_logo_dark;
public $upload_image_logo,
$upload_image_logo_dark;
public function mount()
{
$this->loadSettings();
}
public function loadSettings($clearcache = false)
{
$this->upload_image_logo = null;
$this->upload_image_logo_dark = null;
$adminTemplateService = app(AdminTemplateService::class);
if ($clearcache) {
$adminTemplateService->clearAdminVarsCache();
}
// Obtener los valores de las configuraciones de la base de datos
$settings = $adminTemplateService->getAdminVars();
$this->admin_app_name = $settings['app_name'];
$this->admin_image_logo = $settings['image_logo']['large'];
$this->admin_image_logo_dark = $settings['image_logo']['large_dark'];
}
public function save()
{
$this->validate([
'admin_app_name' => 'required|string|max:255',
'upload_image_logo' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
'upload_image_logo_dark' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
]);
$adminSettingsService = app(AdminSettingsService::class);
// Guardar título del App en configuraciones
$adminSettingsService->updateSetting('admin_app_name', $this->admin_app_name);
// Procesar favicon si se ha cargado una imagen
if ($this->upload_image_logo) {
$adminSettingsService->processAndSaveImageLogo($this->upload_image_logo);
}
if ($this->upload_image_logo_dark) {
$adminSettingsService->processAndSaveImageLogo($this->upload_image_logo_dark, 'dark');
}
$this->loadSettings(true);
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.'
);
}
public function render()
{
return view('vuexy-admin::livewire.admin-settings.application-settings');
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
use Livewire\Component;
use Livewire\WithFileUploads;
use Koneko\VuexyAdmin\Services\AdminSettingsService;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
class GeneralSettings extends Component
{
use WithFileUploads;
private $targetNotify = "#general-settings-card .notification-container";
public $admin_title;
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->loadSettings();
}
public function loadSettings($clearcache = false)
{
$this->upload_image_favicon = null;
$adminTemplateService = app(AdminTemplateService::class);
if ($clearcache) {
$adminTemplateService->clearAdminVarsCache();
}
// Obtener los valores de las configuraciones de la base de datos
$settings = $adminTemplateService->getAdminVars();
$this->admin_title = $settings['title'];
$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 save()
{
$this->validate([
'admin_title' => 'required|string|max:255',
'upload_image_favicon' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
]);
$adminSettingsService = app(AdminSettingsService::class);
// Guardar título del sitio en configuraciones
$adminSettingsService->updateSetting('admin_title', $this->admin_title);
// Procesar favicon si se ha cargado una imagen
if ($this->upload_image_favicon) {
$adminSettingsService->processAndSaveFavicon($this->upload_image_favicon);
}
$this->loadSettings(true);
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.'
);
}
public function render()
{
return view('vuexy-admin::livewire.admin-settings.general-settings');
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
class InterfaceSettings extends Component
{
private $targetNotify = "#interface-settings-card .notification-container";
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->loadSettings();
}
public function loadSettings()
{
$adminTemplateService = app(AdminTemplateService::class);
// Obtener los valores de las configuraciones de la base de datos
$settings = $adminTemplateService->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 save()
{
$this->validate([
'vuexy_maxQuickLinks' => 'required|integer|min:2|max:20',
]);
$globalSettingsService = app(GlobalSettingsService::class);
// Guardar configuraciones
$globalSettingsService->updateSetting('config.vuexy.custom.myLayout', $this->vuexy_myLayout);
$globalSettingsService->updateSetting('config.vuexy.custom.myTheme', $this->vuexy_myTheme);
$globalSettingsService->updateSetting('config.vuexy.custom.myStyle', $this->vuexy_myStyle);
$globalSettingsService->updateSetting('config.vuexy.custom.hasCustomizer', $this->vuexy_hasCustomizer);
$globalSettingsService->updateSetting('config.vuexy.custom.displayCustomizer', $this->vuexy_displayCustomizer);
$globalSettingsService->updateSetting('config.vuexy.custom.contentLayout', $this->vuexy_contentLayout);
$globalSettingsService->updateSetting('config.vuexy.custom.navbarType', $this->vuexy_navbarType);
$globalSettingsService->updateSetting('config.vuexy.custom.footerFixed', $this->vuexy_footerFixed);
$globalSettingsService->updateSetting('config.vuexy.custom.menuFixed', $this->vuexy_menuFixed);
$globalSettingsService->updateSetting('config.vuexy.custom.menuCollapsed', $this->vuexy_menuCollapsed);
$globalSettingsService->updateSetting('config.vuexy.custom.headerType', $this->vuexy_headerType);
$globalSettingsService->updateSetting('config.vuexy.custom.showDropdownOnHover', $this->vuexy_showDropdownOnHover);
$globalSettingsService->updateSetting('config.vuexy.custom.authViewMode', $this->vuexy_authViewMode);
$globalSettingsService->updateSetting('config.vuexy.custom.maxQuickLinks', $this->vuexy_maxQuickLinks);
$globalSettingsService->clearSystemConfigCache();
// 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 clearCustomConfig()
{
$globalSettingsService = app(GlobalSettingsService::class);
$globalSettingsService->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 render()
{
return view('vuexy-admin::livewire.admin-settings.interface-settings');
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
class MailSenderResponseSettings extends Component
{
private $targetNotify = "#mail-sender-response-settings-card .notification-container";
public $from_address,
$from_name,
$reply_to_method,
$reply_to_email,
$reply_to_name;
protected $listeners = ['saveMailSenderResponseSettings' => 'save'];
const REPLY_EMAIL_CREATOR = 1;
const REPLY_EMAIL_SENDER = 2;
const REPLY_EMAIL_CUSTOM = 3;
public $reply_email_options = [
self::REPLY_EMAIL_CREATOR => 'Responder al creador del documento',
self::REPLY_EMAIL_SENDER => 'Responder a quien envía el documento',
self::REPLY_EMAIL_CUSTOM => 'Definir dirección de correo electrónico',
];
public function mount()
{
$this->loadSettings();
}
public function loadSettings()
{
$globalSettingsService = app(GlobalSettingsService::class);
// Obtener los valores de las configuraciones de la base de datos
$settings = $globalSettingsService->getMailSystemConfig();
$this->from_address = $settings['from']['address'];
$this->from_name = $settings['from']['name'];
$this->reply_to_method = $settings['reply_to']['method'];
$this->reply_to_email = $settings['reply_to']['email'];
$this->reply_to_name = $settings['reply_to']['name'];
}
public function save()
{
$this->validate([
'from_address' => 'required|email',
'from_name' => 'required|string|max:255',
'reply_to_method' => 'required|string|max:255',
], [
'from_address.required' => 'El campo de correo electrónico es obligatorio.',
'from_address.email' => 'El formato del correo electrónico no es válido.',
'from_name.required' => 'El nombre es obligatorio.',
'from_name.string' => 'El nombre debe ser una cadena de texto.',
'from_name.max' => 'El nombre no puede tener más de 255 caracteres.',
'reply_to_method.required' => 'El método de respuesta es obligatorio.',
'reply_to_method.string' => 'El método de respuesta debe ser una cadena de texto.',
'reply_to_method.max' => 'El método de respuesta no puede tener más de 255 caracteres.',
]);
if ($this->reply_to_method == self::REPLY_EMAIL_CUSTOM) {
$this->validate([
'reply_to_email' => ['required', 'email'],
'reply_to_name' => ['required', 'string', 'max:255'],
], [
'reply_to_email.required' => 'El correo de respuesta es obligatorio.',
'reply_to_email.email' => 'El formato del correo de respuesta no es válido.',
'reply_to_name.required' => 'El nombre de respuesta es obligatorio.',
'reply_to_name.string' => 'El nombre de respuesta debe ser una cadena de texto.',
'reply_to_name.max' => 'El nombre de respuesta no puede tener más de 255 caracteres.',
]);
}
$globalSettingsService = app(GlobalSettingsService::class);
// Guardar título del App en configuraciones
$globalSettingsService->updateSetting('mail.from.address', $this->from_address);
$globalSettingsService->updateSetting('mail.from.name', $this->from_name);
$globalSettingsService->updateSetting('mail.reply_to.method', $this->reply_to_method);
$globalSettingsService->updateSetting('mail.reply_to.email', $this->reply_to_method == self::REPLY_EMAIL_CUSTOM ? $this->reply_to_email : '');
$globalSettingsService->updateSetting('mail.reply_to.name', $this->reply_to_method == self::REPLY_EMAIL_CUSTOM ? $this->reply_to_name : '');
$globalSettingsService->clearMailSystemConfigCache();
$this->loadSettings();
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.',
);
}
public function render()
{
return view('vuexy-admin::livewire.admin-settings.mail-sender-response-settings');
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
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 MailSmtpSettings extends Component
{
private $targetNotify = "#mail-smtp-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()
{
$globalSettingsService = app(GlobalSettingsService::class);
// Obtener los valores de las configuraciones de la base de datos
$settings = $globalSettingsService->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.admin-settings.mail-smtp-settings');
}
}

View File

@ -0,0 +1,212 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Cache;
use Livewire\Component;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
class CacheFunctions extends Component
{
private $targetNotify = "#cache-functions-card .notification-container";
public $cacheCounts = [
'general' => 0,
'config' => 0,
'routes' => 0,
'views' => 0,
'events' => 0,
];
protected $listeners = [
'reloadCacheFunctionsStatsEvent' => 'reloadCacheStats',
];
public function mount()
{
$this->reloadCacheStats(false);
}
public function reloadCacheStats($notify = true)
{
$cacheDriver = config('cache.default'); // Obtiene el driver configurado para caché
// Caché General
switch ($cacheDriver) {
case 'memcached':
try {
$cacheStore = Cache::getStore()->getMemcached();
$stats = $cacheStore->getStats();
$this->cacheCounts['general'] = array_sum(array_column($stats, 'curr_items')); // Total de claves en Memcached
} catch (\Exception $e) {
$this->cacheCounts['general'] = 'Error obteniendo datos de Memcached';
}
break;
case 'redis':
try {
$prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
$keys = Redis::connection('cache')->keys($prefix . '*');
$this->cacheCounts['general'] = count($keys); // Total de claves en Redis
} catch (\Exception $e) {
$this->cacheCounts['general'] = 'Error obteniendo datos de Redis';
}
break;
case 'database':
try {
$this->cacheCounts['general'] = DB::table('cache')->count(); // Total de registros en la tabla de caché
} catch (\Exception $e) {
$this->cacheCounts['general'] = 'Error obteniendo datos de la base de datos';
}
break;
case 'file':
try {
$cachePath = config('cache.stores.file.path');
$files = glob($cachePath . '/*');
$this->cacheCounts['general'] = count($files);
} catch (\Exception $e) {
$this->cacheCounts['general'] = 'Error obteniendo datos de archivos';
}
break;
default:
$this->cacheCounts['general'] = 'Driver de caché no soportado';
}
// Configuración
$this->cacheCounts['config'] = file_exists(base_path('bootstrap/cache/config.php')) ? 1 : 0;
// Rutas
$this->cacheCounts['routes'] = count(glob(base_path('bootstrap/cache/routes-*.php'))) > 0 ? 1 : 0;
// Vistas
$this->cacheCounts['views'] = count(glob(storage_path('framework/views/*')));
// Configuración
$this->cacheCounts['events'] = file_exists(base_path('bootstrap/cache/events.php')) ? 1 : 0;
if ($notify) {
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han recargado los estadísticos de caché.'
);
}
}
public function clearLaravelCache()
{
Artisan::call('cache:clear');
sleep(1);
$this->response('Se han limpiado las cachés de la aplicación.', 'warning');
}
public function clearConfigCache()
{
Artisan::call('config:clear');
$this->response('Se ha limpiado la cache de la configuración de Laravel.', 'warning');
}
public function configCache()
{
Artisan::call('config:cache');
}
public function clearRouteCache()
{
Artisan::call('route:clear');
$this->response('Se han limpiado las rutas de Laravel.', 'warning');
}
public function cacheRoutes()
{
Artisan::call('route:cache');
}
public function clearViewCache()
{
Artisan::call('view:clear');
$this->response('Se han limpiado las vistas de Laravel.', 'warning');
}
public function cacheViews()
{
Artisan::call('view:cache');
$this->response('Se han cacheado las vistas de Laravel.');
}
public function clearEventCache()
{
Artisan::call('event:clear');
$this->response('Se han limpiado los eventos de Laravel.', 'warning');
}
public function cacheEvents()
{
Artisan::call('event:cache');
$this->response('Se han cacheado los eventos de Laravel.');
}
public function optimizeClear()
{
Artisan::call('optimize:clear');
$this->response('Se han optimizado todos los cachés de Laravel.');
}
public function resetPermissionCache()
{
Artisan::call('permission:cache-reset');
$this->response('Se han limpiado los cachés de permisos.', 'warning');
}
public function clearResetTokens()
{
Artisan::call('auth:clear-resets');
$this->response('Se han limpiado los tokens de reseteo de contraseña.', 'warning');
}
/**
* Genera una respuesta estandarizada.
*/
private function response(string $message, string $type = 'success'): void
{
$this->reloadCacheStats(false);
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $type,
message: $message,
);
$this->dispatch('reloadCacheStatsEvent', notify: false);
$this->dispatch('reloadSessionStatsEvent', notify: false);
$this->dispatch('reloadRedisStatsEvent', notify: false);
$this->dispatch('reloadMemcachedStatsEvent', notify: false);
}
public function render()
{
return view('vuexy-admin::livewire.cache.cache-functions');
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Cache;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\CacheConfigService;
use Koneko\VuexyAdmin\Services\CacheManagerService;
class CacheStats extends Component
{
private $targetNotify = "#cache-stats-card .notification-container";
public $cacheConfig = [];
public $cacheStats = [];
protected $listeners = ['reloadCacheStatsEvent' => 'reloadCacheStats'];
public function mount(CacheConfigService $cacheConfigService)
{
$this->cacheConfig = $cacheConfigService->getConfig();
$this->reloadCacheStats(false);
}
public function reloadCacheStats($notify = true)
{
$cacheManagerService = new CacheManagerService();
$this->cacheStats = $cacheManagerService->getCacheStats();
if ($notify) {
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $this->cacheStats['status'],
message: $this->cacheStats['message']
);
}
}
public function clearCache()
{
$cacheManagerService = new CacheManagerService();
$message = $cacheManagerService->clearCache();
$this->reloadCacheStats(false);
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $message['status'],
message: $message['message'],
);
$this->dispatch('reloadRedisStatsEvent', notify: false);
$this->dispatch('reloadMemcachedStatsEvent', notify: false);
$this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
}
public function render()
{
return view('vuexy-admin::livewire.cache.cache-stats');
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Cache;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\CacheManagerService;
class MemcachedStats extends Component
{
private $driver = 'memcached';
private $targetNotify = "#memcached-stats-card .notification-container";
public $memcachedStats = [];
protected $listeners = ['reloadMemcachedStatsEvent' => 'reloadCacheStats'];
public function mount()
{
$this->reloadCacheStats(false);
}
public function reloadCacheStats($notify = true)
{
$cacheManagerService = new CacheManagerService($this->driver);
$memcachedStats = $cacheManagerService->getMemcachedStats();
$this->memcachedStats = $memcachedStats['info'];
if ($notify) {
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $memcachedStats['status'],
message: $memcachedStats['message']
);
}
}
public function clearCache()
{
$cacheManagerService = new CacheManagerService($this->driver);
$message = $cacheManagerService->clearCache();
$this->reloadCacheStats(false);
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $message['status'],
message: $message['message'],
);
$this->dispatch('reloadCacheStatsEvent', notify: false);
$this->dispatch('reloadSessionStatsEvent', notify: false);
$this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
}
public function render()
{
return view('vuexy-admin::livewire.cache.memcached-stats');
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Cache;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\CacheManagerService;
class RedisStats extends Component
{
private $driver = 'redis';
private $targetNotify = "#redis-stats-card .notification-container";
public $redisStats = [];
protected $listeners = ['reloadRedisStatsEvent' => 'reloadCacheStats'];
public function mount()
{
$this->reloadCacheStats(false);
}
public function reloadCacheStats($notify = true)
{
$cacheManagerService = new CacheManagerService($this->driver);
$redisStats = $cacheManagerService->getRedisStats();
$this->redisStats = $redisStats['info'];
if ($notify) {
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $redisStats['status'],
message: $redisStats['message']
);
}
}
public function clearCache()
{
$cacheManagerService = new CacheManagerService($this->driver);
$message = $cacheManagerService->clearCache();
$this->reloadCacheStats(false);
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $message['status'],
message: $message['message'],
);
$this->dispatch('reloadCacheStatsEvent', notify: false);
$this->dispatch('reloadSessionStatsEvent', notify: false);
$this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
}
public function render()
{
return view('vuexy-admin::livewire.cache.redis-stats');
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Cache;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\CacheConfigService;
use Koneko\VuexyAdmin\Services\SessionManagerService;
class SessionStats extends Component
{
private $targetNotify = "#session-stats-card .notification-container";
public $cacheConfig = [];
public $sessionStats = [];
protected $listeners = ['reloadSessionStatsEvent' => 'reloadSessionStats'];
public function mount(CacheConfigService $cacheConfigService)
{
$this->cacheConfig = $cacheConfigService->getConfig();
$this->reloadSessionStats(false);
}
public function reloadSessionStats($notify = true)
{
$sessionManagerService = new SessionManagerService();
$this->sessionStats = $sessionManagerService->getSessionStats();
if ($notify) {
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $this->sessionStats['status'],
message: $this->sessionStats['message']
);
}
}
public function clearSessions()
{
$sessionManagerService = new SessionManagerService();
$message = $sessionManagerService->clearSessions();
$this->reloadSessionStats(false);
$this->dispatch(
'notification',
target: $this->targetNotify,
type: $message['status'],
message: $message['message'],
);
$this->dispatch('reloadRedisStatsEvent', notify: false);
$this->dispatch('reloadMemcachedStatsEvent', notify: false);
}
public function render()
{
return view('vuexy-admin::livewire.cache.session-stats');
}
}

View File

@ -0,0 +1,515 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Form;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\QueryException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use Livewire\Component;
/**
* Class AbstractFormComponent
*
* Clase base y abstracta para la creación de formularios con Livewire.
* Proporciona métodos y un flujo general para manejar operaciones CRUD
* (creación, edición y eliminación), validaciones, notificaciones y
* administración de errores en un entorno transaccional.
*
* @package Koneko\VuexyAdmin\Livewire\Form
*/
abstract class AbstractFormComponent extends Component
{
/**
* Identificador único del formulario, útil para distinguir múltiples instancias.
*
* @var string
*/
public $uniqueId;
/**
* Modo actual del formulario: puede ser 'create', 'edit' o 'delete'.
*
* @var string
*/
public $mode;
/**
* Texto que se mostrará en el botón de envío. Se adapta
* automáticamente en función del modo actual (crear, editar o eliminar).
*
* @var string
*/
public $btnSubmitText;
/**
* ID del registro que se está editando o eliminando.
* Si el formulario está en modo 'create', puede ser null.
*
* @var int|null
*/
public $id;
/**
* Nombre de la etiqueta para generar Componentes
*
* @var string
*/
public $tagName;
/**
* Nombre de la columna que contiene el nombre del registro.
*
* @var string
*/
public $columnNameLabel;
/**
* Nombre singular del modelo
*
* @var string
*/
public $singularName;
/*
* Nombre del identificador del Canvas
*
* @var string
*/
public $offcanvasId;
/*
* Nombre del identificador del Form
*
* @var string
*/
public $formId;
// ======================================================================
// MÉTODOS ABSTRACTOS
// ======================================================================
/**
* Retorna la clase (namespace) del modelo Eloquent asociado al formulario.
*
* @return string
*/
abstract protected function model(): string;
/**
* Retorna las reglas de validación de forma dinámica, dependiendo del modo del formulario.
*
* @param string $mode El modo actual del formulario (por ejemplo, 'create', 'edit' o 'delete').
* @return array Reglas de validación (similares a las usadas en un Request de Laravel).
*/
abstract protected function dynamicRules(string $mode): array;
/**
* Inicializa los datos del formulario con base en el registro (si existe)
* y en el modo actual. Útil para prellenar campos en modo 'edit'.
*
* @param mixed $record El registro encontrado, o null si se crea uno nuevo.
* @param string $mode El modo actual del formulario.
* @return void
*/
abstract protected function initializeFormData(mixed $record, string $mode): void;
/**
* Prepara los datos ya validados para ser guardados en base de datos.
* Permite, por ejemplo, castear valores o limpiar ciertos campos.
*
* @param array $validatedData Datos que ya pasaron la validación.
* @return array Datos listos para el almacenamiento (por ejemplo, en create o update).
*/
abstract protected function prepareData(array $validatedData): array;
/**
* Define los contenedores de destino para las notificaciones.
*
* Retorna un array con keys como 'form', 'index', etc., y sus
* valores deben ser selectores o identificadores en la vista, donde
* se inyectarán las notificaciones.
*
* @return array
*/
abstract protected function targetNotifies(): array;
/**
* Retorna la ruta de la vista Blade correspondiente a este formulario.
*
* Por ejemplo: 'package::livewire.some-form'.
*
* @return string
*/
abstract protected function viewPath(): string;
// ======================================================================
// MÉTODOS DE VALIDACIÓN
// ======================================================================
/**
* Retorna un array que define nombres de atributos personalizados para los mensajes de validación.
*
* @return array
*/
protected function attributes(): array
{
return [];
}
/**
* Retorna un array con mensajes de validación personalizados.
*
* @return array
*/
protected function messages(): array
{
return [];
}
// ======================================================================
// INICIALIZACIÓN Y CICLO DE VIDA
// ======================================================================
/**
* Método que se ejecuta al montar (instanciar) el componente Livewire.
* Inicializa propiedades clave como el $mode, $id, $uniqueId, el texto
* del botón de envío, y carga datos del registro si no es un 'create'.
*
* @param string $mode Modo del formulario: 'create', 'edit' o 'delete'.
* @param int|null $id ID del registro a editar/eliminar (o null para crear).
* @return void
*/
public function mount(string $mode = 'create', mixed $id = null): void
{
$this->uniqueId = uniqid();
$this->mode = $mode;
$this->id = $id;
$model = new ($this->model());
$this->tagName = $model->tagName;
$this->columnNameLabel = $model->columnNameLabel;
$this->singularName = $model->singularName;
$this->formId = Str::camel($model->tagName) .'Form';
$this->setBtnSubmitText();
if ($this->mode !== 'create' && $this->id) {
// Si no es modo 'create', cargamos el registro desde la BD
$record = $this->model()::findOrFail($this->id);
$this->initializeFormData($record, $mode);
} else {
// Modo 'create', o sin ID: iniciamos datos vacíos
$this->initializeFormData(null, $mode);
}
}
/**
* Configura el texto del botón principal de envío, basado en la propiedad $mode.
*
* @return void
*/
private function setBtnSubmitText(): void
{
$this->btnSubmitText = match ($this->mode) {
'create' => 'Crear ' . $this->singularName(),
'edit' => 'Guardar cambios',
'delete' => 'Eliminar ' . $this->singularName(),
default => 'Enviar'
};
}
/**
* Retorna el "singularName" definido en el modelo asociado.
* Permite también decidir si se devuelve con la primera letra en mayúscula
* o en minúscula.
*
* @param string $type Puede ser 'uppercase' o 'lowercase'. Por defecto, 'lowercase'.
* @return string Nombre en singular del modelo, formateado.
*/
private function singularName($type = 'lowercase'): string
{
/** @var Model $model */
$model = new ($this->model());
return $type === 'uppercase'
? ucfirst($model->singularName)
: lcfirst($model->singularName);
}
/**
* Método del ciclo de vida de Livewire que se llama en cada hidratación.
* Puedes disparar eventos o manejar lógica que suceda en cada request
* una vez que Livewire 'rehidrate' el componente en el servidor.
*
* @return void
*/
public function hydrate(): void
{
$this->dispatch($this->dispatches()['on-hydrate']);
}
// ======================================================================
// OPERACIONES CRUD
// ======================================================================
/**
* Método principal de envío del formulario (submit). Gestiona los flujos
* de crear, editar o eliminar un registro dentro de una transacción de BD.
*
* @return void
*/
public function onSubmit(): void
{
DB::beginTransaction();
try {
if ($this->mode === 'delete') {
$this->delete();
} else {
$this->save();
}
DB::commit();
} catch (ValidationException $e) {
DB::rollBack();
$this->handleValidationException($e);
} catch (QueryException $e) {
DB::rollBack();
$this->handleDatabaseException($e);
} catch (ModelNotFoundException $e) {
DB::rollBack();
$this->handleException('danger', 'Registro no encontrado.');
} catch (Exception $e) {
DB::rollBack();
$this->handleException('danger', 'Error al eliminar el registro: ' . $e->getMessage());
}
}
/**
* Crea o actualiza un registro en la base de datos,
* aplicando validaciones y llamadas a hooks antes y después de guardar.
*
* @return void
* @throws ValidationException
*/
protected function save(): void
{
// Validamos los datos, con posibles atributos y mensajes personalizados
$validatedData = $this->validate(
$this->dynamicRules($this->mode),
$this->messages(),
$this->attributes()
);
// Hook previo (por referencia)
$this->beforeSave($validatedData);
// Ajustamos/convertimos los datos finales
$data = $this->prepareData($validatedData);
$record = $this->model()::updateOrCreate(['id' => $this->id], $data);
// Hook posterior
$this->afterSave($record);
// Notificamos éxito
$this->handleSuccess('success', $this->singularName('uppercase') . " guardado correctamente.");
}
/**
* Elimina un registro de la base de datos (modo 'delete'),
* aplicando validaciones y hooks antes y después de la eliminación.
*
* @return void
* @throws ValidationException
*/
protected function delete(): void
{
$this->validate($this->dynamicRules('delete', $this->messages(), $this->attributes()));
$record = $this->model()::findOrFail($this->id);
// Hook antes de la eliminación
$this->beforeDelete($record);
$record->delete();
// Hook después de la eliminación
$this->afterDelete($record);
$this->handleSuccess('warning', $this->singularName('uppercase') . " eliminado.");
}
// ======================================================================
// HOOKS DE ACCIONES
// ======================================================================
/**
* Hook que se ejecuta antes de guardar o actualizar un registro.
* Puede usarse para ajustar o limpiar datos antes de la operación en base de datos.
*
* @param array $data Datos validados que se van a guardar.
* Se pasa por referencia para permitir cambios.
* @return void
*/
protected function beforeSave(array &$data): void {}
/**
* Hook que se ejecuta después de guardar o actualizar un registro.
* Puede usarse para acciones como disparar eventos, notificaciones a otros sistemas, etc.
*
* @param mixed $record Instancia del modelo recién creado o actualizado.
* @return void
*/
protected function afterSave($record): void {}
/**
* Hook que se ejecuta antes de eliminar un registro.
* Puede emplearse para validaciones adicionales o limpieza de datos relacionados.
*
* @param mixed $record Instancia del modelo que se eliminará.
* @return void
*/
protected function beforeDelete($record): void {}
/**
* Hook que se ejecuta después de eliminar un registro.
* Útil para operaciones finales, como remover archivos relacionados o
* disparar un evento de "elemento eliminado".
*
* @param mixed $record Instancia del modelo que se acaba de eliminar.
* @return void
*/
protected function afterDelete($record): void {}
// ======================================================================
// MANEJO DE VALIDACIONES Y ERRORES
// ======================================================================
/**
* Maneja las excepciones de validación (ValidationException).
* Asigna los errores al error bag de Livewire y muestra notificaciones.
*
* @param ValidationException $e Excepción de validación.
* @return void
*/
protected function handleValidationException(ValidationException $e): void
{
$this->setErrorBag($e->validator->errors());
$this->handleException('danger', 'Error en la validación de los datos.');
$this->dispatch($this->dispatches()['on-failed-validation']);
}
/**
* Maneja las excepciones de base de datos (QueryException).
* Incluye casos especiales para claves foráneas y duplicadas.
*
* @param QueryException $e Excepción de consulta a la base de datos.
* @return void
*/
protected function handleDatabaseException(QueryException $e): void
{
$errorMessage = match ($e->errorInfo[1]) {
1452 => "Una clave foránea no es válida.",
1062 => $this->extractDuplicateField($e->getMessage()),
1451 => "No se puede eliminar el registro porque está en uso.",
default => env('APP_DEBUG') ? $e->getMessage() : "Error inesperado en la base de datos.",
};
$this->handleException('danger', $errorMessage, 'form', 120000);
}
/**
* Maneja excepciones o errores generales, mostrando una notificación al usuario.
*
* @param string $type Tipo de notificación (por ejemplo, 'success', 'warning', 'danger').
* @param string $message Mensaje que se mostrará en la notificación.
* @param string $target Objetivo/área donde se mostrará la notificación ('form', 'index', etc.).
* @param int $delay Tiempo en milisegundos que la notificación permanecerá visible.
* @return void
*/
protected function handleException($type, $message, $target = 'form', $delay = 9000): void
{
$this->dispatchNotification($type, $message, $target, $delay);
}
/**
* Extrae el campo duplicado de un mensaje de error MySQL, para mostrar un mensaje amigable.
*
* @param string $errorMessage Mensaje de error completo de la base de datos.
* @return string Mensaje simplificado indicando cuál campo está duplicado.
*/
private function extractDuplicateField($errorMessage): string
{
preg_match("/for key 'unique_(.*?)'/", $errorMessage, $matches);
return isset($matches[1])
? "El valor ingresado para '" . str_replace('_', ' ', $matches[1]) . "' ya está en uso."
: "Ya existe un registro con este valor.";
}
// ======================================================================
// NOTIFICACIONES Y REDIRECCIONAMIENTOS
// ======================================================================
/**
* Maneja el flujo de notificación y redirección cuando una operación
* (guardar, eliminar) finaliza satisfactoriamente.
*
* @param string $type Tipo de notificación ('success', 'warning', etc.).
* @param string $message Mensaje a mostrar.
* @return void
*/
protected function handleSuccess($type, $message): void
{
$this->dispatchNotification($type, $message, 'index');
$this->redirectRoute($this->getRedirectRoute());
}
/**
* Envía una notificación al navegador (mediante eventos de Livewire)
* indicando el tipo, el mensaje y el destino donde debe visualizarse.
*
* @param string $type Tipo de notificación (success, danger, etc.).
* @param string $message Mensaje de la notificación.
* @param string $target Destino para mostrarla ('form', 'index', etc.).
* @param int $delay Duración de la notificación en milisegundos.
* @return void
*/
protected function dispatchNotification($type, $message, $target = 'form', $delay = 9000): void
{
$this->dispatch(
$target == 'index' ? 'store-notification' : 'notification',
target: $target === 'index' ? $this->targetNotifies()['index'] : $this->targetNotifies()['form'],
type: $type,
message: $message,
delay: $delay
);
}
// ======================================================================
// RENDERIZACIÓN
// ======================================================================
/**
* Renderiza la vista Blade asociada a este componente.
* Retorna un objeto Illuminate\View\View.
*
* @return View
*/
public function render(): View
{
return view($this->viewPath());
}
}

View File

@ -0,0 +1,667 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Form;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use Livewire\Component;
/**
* Clase base abstracta para manejar formularios de tipo Off-Canvas con Livewire.
*
* Esta clase proporciona métodos reutilizables para operaciones CRUD, validaciones dinámicas,
* manejo de transacciones en base de datos y eventos de Livewire.
*
* @package Koneko\VuexyAdmin\Livewire\Form
*/
abstract class AbstractFormOffCanvasComponent extends Component
{
/**
* Identificador único del formulario, usado para evitar conflictos en instancias múltiples.
*
* @var string
*/
public $uniqueId;
/**
* Modo actual del formulario: puede ser 'create', 'edit' o 'delete'.
*
* @var string
*/
public $mode;
/**
* ID del registro que se está editando o eliminando.
*
* @var int|null
*/
public $id;
/**
* Valores por defecto para los campos del formulario,
*
* @var array
*/
public $defaultValues;
/**
* Nombre de la etiqueta para generar Componentes
*
* @var string
*/
public $tagName;
/**
* Nombre de la columna que contiene el nombre del registro.
*
* @var string
*/
public $columnNameLabel;
/**
* Nombre singular del modelo
*
* @var string
*/
public $singularName;
/*
* Nombre del identificador del Canvas
*
* @var string
*/
public $offcanvasId;
/*
* Nombre del identificador del Form
*
* @var string
*/
public $formId;
/**
* Campo que se debe enfocar cuando se abra el formulario.
*
* @var string
*/
public $focusOnOpen;
/**
* Indica si se desea confirmar la eliminación del registro.
*
* @var bool
*/
public $confirmDeletion = false;
/**
* Indica si se ha producido un error de validación.
*
* @var bool
*/
public $validationError = false;
/*
* Indica si se ha procesado correctamente el formulario.
*
* @var bool
*/
public $successProcess = false;
/**
* Campos que deben ser casteados a tipos específicos.
*
* @var array<string, string>
*/
protected $casts = [];
// ===================== MÉTODOS ABSTRACTOS =====================
/**
* Define el modelo Eloquent asociado con el formulario.
*
* @return string
*/
abstract protected function model(): string;
/**
* Define los campos del formulario.
*
* @return array<string, mixed>
*/
abstract protected function fields(): array;
/**
* Retorna los valores por defecto para los campos del formulario.
*
* @return array<string, mixed> Valores predeterminados.
*/
abstract protected function defaults(): array;
/**
* Campo que se debe enfocar cuando se abra el formulario.
*
* @return string
*/
abstract protected function focusOnOpen(): string;
/**
* Define reglas de validación dinámicas según el modo del formulario.
*
* @param string $mode Modo actual del formulario ('create', 'edit', 'delete').
* @return array<string, mixed> Reglas de validación.
*/
abstract protected function dynamicRules(string $mode): array;
/**
* Devuelve las opciones que se mostrarán en los selectores del formulario.
*
* @return array<string, mixed> Opciones para los campos del formulario.
*/
abstract protected function options(): array;
/**
* Retorna la ruta de la vista asociada al formulario.
*
* @return string Ruta de la vista Blade.
*/
abstract protected function viewPath(): string;
// ===================== VALIDACIONES =====================
protected function attributes(): array
{
return [];
}
protected function messages(): array
{
return [];
}
// ===================== INICIALIZACIÓN DEL COMPONENTE =====================
/**
* Se ejecuta cuando el componente se monta por primera vez.
*
* Inicializa propiedades y carga datos iniciales.
*
* @return void
*/
public function mount(): void
{
$this->uniqueId = uniqid();
$model = new ($this->model());
$this->tagName = $model->tagName;
$this->columnNameLabel = $model->columnNameLabel;
$this->singularName = $model->singularName;
$this->offcanvasId = 'offcanvas' . ucfirst(Str::camel($model->tagName));
$this->formId = Str::camel($model->tagName) .'Form';
$this->focusOnOpen = "{$this->focusOnOpen()}_{$this->uniqueId}";
$this->loadDefaults();
$this->loadOptions();
}
// ===================== INICIALIZACIÓN Y CONFIGURACIÓN =====================
/**
* Devuelve los valores por defecto para los campos del formulario.
*
* @return array<string, mixed> Valores por defecto.
*/
private function loadDefaults(): void
{
$this->defaultValues = $this->defaults();
}
/**
* Carga las opciones necesarias para los campos del formulario.
*
* @return void
*/
private function loadOptions(): void
{
foreach ($this->options() as $key => $value) {
$this->$key = $value;
}
}
/**
* Carga los datos de un modelo específico en el formulario para su edición.
*
* @param int $id ID del registro a editar.
* @return void
*/
public function loadFormModel(int $id): void
{
if ($this->loadData($id)) {
$this->mode = 'edit';
$this->dispatch($this->getDispatche('refresh-offcanvas'));
}
}
/**
* Carga el modelo para confirmar su eliminación.
*
* @param int $id ID del registro a eliminar.
* @return void
*/
public function loadFormModelForDeletion(int $id): void
{
if ($this->loadData($id)) {
$this->mode = 'delete';
$this->confirmDeletion = false;
$this->dispatch($this->getDispatche('refresh-offcanvas'));
}
}
private function getDispatche(string $name): string
{
$model = new ($this->model());
$dispatches = [
'refresh-offcanvas' => 'refresh-' . Str::kebab($model->tagName) . '-offcanvas',
'reload-table' => 'reload-bt-' . Str::kebab($model->tagName) . 's',
];
return $dispatches[$name] ?? null;
}
/**
* Carga los datos del modelo según el ID proporcionado.
*
* @param int $id ID del modelo.
* @return bool True si los datos fueron cargados correctamente.
*/
protected function loadData(int $id): bool
{
$model = $this->model()::find($id);
if ($model) {
$data = $model->only(['id', ...$this->fields()]);
$this->applyCasts($data);
$this->fill($data);
return true;
}
return false;
}
// ===================== OPERACIONES CRUD =====================
/**
* Método principal para enviar el formulario.
*
* @return void
*/
public function onSubmit(): void
{
$this->successProcess = false;
$this->validationError = false;
if(!$this->mode)
$this->mode = 'create';
DB::beginTransaction(); // Iniciar transacción
try {
if($this->mode === 'delete'){
$this->delete();
}else{
$this->save();
}
DB::commit();
} catch (ValidationException $e) {
DB::rollBack();
$this->handleValidationException($e);
} catch (QueryException $e) {
DB::rollBack();
$this->handleDatabaseException($e);
} catch (ModelNotFoundException $e) {
DB::rollBack();
$this->handleException('danger', 'Registro no encontrado.');
} catch (Exception $e) {
DB::rollBack(); // Revertir la transacción si ocurre un error
$this->handleException('danger', 'Error al eliminar el registro: ' . $e->getMessage());
}
}
/**
* Guarda o actualiza un registro en la base de datos.
*
* @return void
* @throws ValidationException
*/
protected function save(): void
{
// Valida incluyendo atributos personalizados
$validatedData = $this->validate(
$this->dynamicRules($this->mode),
$this->messages(),
$this->attributes()
);
$this->convertEmptyValuesToNull($validatedData);
$this->applyCasts($validatedData);
$this->beforeSave($validatedData);
$record = $this->model()::updateOrCreate(['id' => $this->id], $validatedData);
$this->afterSave($record);
$this->handleSuccess('success', ucfirst($this->singularName) . " guardado correctamente.");
}
/**
* Elimina un registro en la base de datos.
*
* @return void
*/
protected function delete(): void
{
$this->validate($this->dynamicRules(
'delete',
$this->messages(),
$this->attributes()
));
$record = $this->model()::findOrFail($this->id);
$this->beforeDelete($record);
$record->delete();
$this->afterDelete($record);
$this->handleSuccess('warning', ucfirst($this->singularName) . " eliminado.");
}
// ===================== HOOKS DE ACCIONES CRUD =====================
/**
* Hook que se ejecuta antes de guardar datos en la base de datos.
*
* Este método permite realizar modificaciones o preparar los datos antes de ser validados
* y almacenados. Es útil para formatear datos, agregar valores calculados o realizar
* operaciones previas a la persistencia.
*
* @param array $data Datos validados que se almacenarán. Se pasan por referencia,
* por lo que cualquier cambio aquí afectará directamente los datos guardados.
*
* @return void
*/
protected function beforeSave(array &$data): void {}
/**
* Hook que se ejecuta después de guardar o actualizar un registro en la base de datos.
*
* Ideal para ejecutar tareas posteriores al guardado, como enviar notificaciones,
* registrar auditorías o realizar acciones en otros modelos relacionados.
*
* @param \Illuminate\Database\Eloquent\Model $record El modelo que fue guardado, conteniendo
* los datos actualizados.
*
* @return void
*/
protected function afterSave($record): void {}
/**
* Hook que se ejecuta antes de eliminar un registro de la base de datos.
*
* Permite validar si el registro puede ser eliminado o realizar tareas previas
* como desasociar relaciones, eliminar archivos relacionados o verificar restricciones.
*
* @param \Illuminate\Database\Eloquent\Model $record El modelo que está por ser eliminado.
*
* @return void
*/
protected function beforeDelete($record): void {}
/**
* Hook que se ejecuta después de eliminar un registro de la base de datos.
*
* Útil para realizar acciones adicionales tras la eliminación, como limpiar datos relacionados,
* eliminar archivos vinculados o registrar eventos de auditoría.
*
* @param \Illuminate\Database\Eloquent\Model $record El modelo eliminado. Aunque ya no existe en la base de datos,
* se conserva la información del registro en memoria.
*
* @return void
*/
protected function afterDelete($record): void {}
// ===================== MANEJO DE VALIDACIONES Y EXCEPCIONES =====================
/**
* Maneja las excepciones de validación.
*
* Este método captura los errores de validación, los agrega al error bag de Livewire
* y dispara un evento para manejar el fallo de validación, útil en formularios modales.
*
* @param ValidationException $e Excepción de validación capturada.
* @return void
*/
protected function handleValidationException(ValidationException $e): void
{
$this->setErrorBag($e->validator->errors());
// Notifica al usuario que ocurrió un error de validación
$this->handleException('danger', 'Error en la validación de los datos.');
}
/**
* Maneja las excepciones relacionadas con la base de datos.
*
* Analiza el código de error de la base de datos y genera un mensaje de error específico
* para la situación. También se encarga de enviar una notificación de error.
*
* @param QueryException $e Excepción capturada durante la ejecución de una consulta.
* @return void
*/
protected function handleDatabaseException(QueryException $e): void
{
$errorMessage = match ($e->errorInfo[1]) {
1452 => "Una clave foránea no es válida.",
1062 => $this->extractDuplicateField($e->getMessage()),
1451 => "No se puede eliminar el registro porque está en uso.",
default => env('APP_DEBUG') ? $e->getMessage() : "Error inesperado en la base de datos.",
};
$this->handleException('danger', $errorMessage, 'form', 120000);
}
/**
* Maneja cualquier tipo de excepción general y envía una notificación al usuario.
*
* @param string $type El tipo de notificación (success, danger, warning).
* @param string $message El mensaje que se mostrará al usuario.
* @param string $target El contenedor donde se mostrará la notificación (por defecto 'form').
* @param int $delay Tiempo en milisegundos que durará la notificación en pantalla.
* @return void
*/
protected function handleException($type, $message, $target = 'form', $delay = 9000): void
{
$this->validationError = true;
$this->dispatch($this->getDispatche('refresh-offcanvas'));
$this->dispatchNotification($type, $message, $target, $delay);
}
/**
* Extrae el nombre del campo duplicado de un error de base de datos MySQL.
*
* Esta función se utiliza para identificar el campo específico que causó un error
* de duplicación de clave única, y genera un mensaje personalizado para el usuario.
*
* @param string $errorMessage El mensaje de error completo proporcionado por MySQL.
* @return string Mensaje de error amigable para el usuario.
*/
private function extractDuplicateField($errorMessage): string
{
preg_match("/for key 'unique_(.*?)'/", $errorMessage, $matches);
return isset($matches[1])
? "El valor ingresado para '" . str_replace('_', ' ', $matches[1]) . "' ya está en uso."
: "Ya existe un registro con este valor.";
}
// ===================== NOTIFICACIONES Y ÉXITO =====================
/**
* Despacha una notificación tras el éxito de una operación.
*
* @param string $type Tipo de notificación (success, warning, danger)
* @param string $message Mensaje a mostrar.
* @return void
*/
protected function handleSuccess(string $type, string $message): void
{
$this->successProcess = true;
$this->dispatch($this->getDispatche('refresh-offcanvas'));
$this->dispatch($this->getDispatche('reload-table'));
$this->dispatchNotification($type, $message, 'index');
}
/**
* Envía una notificación al navegador.
*
* @param string $type Tipo de notificación (success, danger, etc.)
* @param string $message Mensaje de la notificación
* @param string $target Destino (form, index)
* @param int $delay Duración de la notificación en milisegundos
*/
protected function dispatchNotification($type, $message, $target = 'form', $delay = 9000): void
{
$model = new ($this->model());
$this->tagName = $model->tagName;
$this->columnNameLabel = $model->columnNameLabel;
$this->singularName = $model->singularName;
$tagOffcanvas = ucfirst(Str::camel($model->tagName));
$targetNotifies = [
"index" => '#bt-' . Str::kebab($model->tagName) . 's .notification-container',
"form" => "#offcanvas{$tagOffcanvas} .notification-container",
];
$this->dispatch(
'notification',
target: $target === 'index' ? $targetNotifies['index'] : $targetNotifies['form'],
type: $type,
message: $message,
delay: $delay
);
}
// ===================== FORMULARIO Y CONVERSIÓN DE DATOS =====================
/**
* Convierte los valores vacíos a `null` en los campos que son configurados como `nullable`.
*
* Esta función verifica las reglas de validación actuales y transforma todos los campos vacíos
* en valores `null` si las reglas permiten valores nulos. Es útil para evitar insertar cadenas vacías
* en la base de datos donde se espera un valor nulo.
*
* @param array $data Los datos del formulario que se deben procesar.
* @return void
*/
protected function convertEmptyValuesToNull(array &$data): void
{
$nullableFields = array_keys(array_filter($this->dynamicRules($this->mode), function ($rules) {
return in_array('nullable', (array) $rules);
}));
foreach ($nullableFields as $field) {
if (isset($data[$field]) && $data[$field] === '') {
$data[$field] = null;
}
}
}
/**
* Aplica tipos de datos definidos en `$casts` a los campos del formulario.
*
* Esta función toma los datos de entrada y los transforma en el tipo de datos esperado según
* lo definido en la propiedad `$casts`. Es útil para asegurar que los datos se almacenen en
* el formato correcto, como convertir cadenas a números enteros o booleanos.
*
* @param array $data Los datos del formulario que necesitan ser casteados.
* @return void
*/
protected function applyCasts(array &$data): void
{
foreach ($this->casts as $field => $type) {
if (array_key_exists($field, $data)) {
$data[$field] = $this->castValue($type, $data[$field]);
}
}
}
/**
* Castea un valor a su tipo de dato correspondiente.
*
* Convierte un valor dado al tipo especificado, manejando adecuadamente los valores vacíos
* o nulos. También asegura que valores como `0` o `''` sean tratados correctamente
* para evitar errores al almacenarlos en la base de datos.
*
* @param string $type El tipo de dato al que se debe convertir (`boolean`, `integer`, `float`, `string`, `array`).
* @param mixed $value El valor que se debe castear.
* @return mixed El valor convertido al tipo especificado.
*/
protected function castValue($type, $value): mixed
{
// Convertir valores vacíos o cero a null si corresponde
if (is_null($value) || $value === '' || $value === '0' || $value === 0.0) {
return match ($type) {
'boolean' => false, // No permitir null en booleanos
'integer' => 0, // Valor por defecto para enteros
'float', 'double' => 0.0, // Valor por defecto para decimales
'string' => "", // Convertir cadena vacía en null
'array' => [], // Evitar null en arrays
default => null, // Valor por defecto para otros tipos
};
}
// Castear el valor si no es null ni vacío
return match ($type) {
'boolean' => (bool) $value,
'integer' => (int) $value,
'float', 'double' => (float) $value,
'string' => (string) $value,
'array' => (array) $value,
default => $value,
};
}
// ===================== RENDERIZACIÓN DE VISTA =====================
/**
* Renderiza la vista del formulario.
*
* @return \Illuminate\View\View
*/
public function render(): View
{
return view($this->viewPath());
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Permissions;
use Spatie\Permission\Models\Role;
use Livewire\Component;
class PermissionIndex extends Component
{
public $roles_html_select;
public $rows_roles;
public function render()
{
// Generamos Select y estilos HTML de roles
$this->roles_html_select = "<select id=\"UserRole\" class=\"form-select text-capitalize\"><option value=\"\"> Selecciona un rol </option>";
foreach (Role::all() as $role) {
$this->rows_roles[$role->name] = "<span class=\"badge bg-label-{$role->style} m-1\">{$role->name}</span>";
$this->roles_html_select .= "<option value=\"{$role->name}\" class=\"text-capitalize\">{$role->name}</option>";
}
$this->roles_html_select .= "</select>";
return view('vuexy-admin::livewire.permissions.index');
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Permissions;
use Livewire\Component;
use Spatie\Permission\Models\Permission;
class Permissions extends Component
{
public $permissionName;
public function createPermission()
{
$this->validate([
'permissionName' => 'required|unique:permissions,name'
]);
Permission::create(['name' => $this->permissionName]);
session()->flash('message', 'Permiso creado con éxito.');
$this->reset('permissionName');
}
public function deletePermission($id)
{
Permission::find($id)->delete();
session()->flash('message', 'Permiso eliminado.');
}
public function render()
{
return view('livewire.permissions', [
'permissions' => Permission::all()
]);
}
}

View File

@ -0,0 +1,182 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Roles;
use Illuminate\Support\Facades\Auth;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Livewire\Component;
class RoleCards extends Component
{
public $roles = [];
public $permissions = [];
public $roleId;
public $name;
public $style;
public $title;
public $btn_submit_text;
public $permissionsInputs = [];
public $destroyRoleId;
protected $listeners = ['saveRole', 'deleteRole'];
public function mount()
{
$this->loadRolesAndPermissions();
$this->dispatch('reloadForm');
}
private function loadRolesAndPermissions()
{
$this->roles = Auth::user()->hasRole('SuperAdmin') ?
Role::all() :
Role::where('name', '!=', 'SuperAdmin')->get();
// Obtener todos los permisos
$permissions = Permission::all()->map(function ($permission) {
$name = $permission->name;
$action = substr($name, strrpos($name, '.') + 1);
return [
'group_name' => $permission->group_name,
'sub_group_name' => $permission->sub_group_name,
$action => $name // Agregar la acción directamente al array
];
})->groupBy('group_name'); // Agrupar los permisos por grupo
// Procesar los permisos agrupados para cargarlos en el componente
$permissionsInputs = [];
$this->permissions = $permissions->map(function ($groupPermissions) use (&$permissionsInputs) {
$permission = [
'group_name' => $groupPermissions[0]['group_name'], // Tomar el grupo del primer permiso del grupo
'sub_group_name' => $groupPermissions[0]['sub_group_name'], // Tomar la descripción del primer permiso del grupo
];
// Agregar todas las acciones al permissionsInputs y al permission
foreach ($groupPermissions as $permissionData) {
foreach ($permissionData as $key => $value) {
if ($key !== 'sub_group_name' && $key !== 'group_name') {
$permissionsInputs[str_replace('.', '_', $value)] = false;
$permission[$key] = $value;
}
}
}
return $permission;
});
$this->permissionsInputs = $permissionsInputs;
}
public function loadRoleData($action, $roleId = false)
{
$this->resetForm();
$this->title = 'Agregar un nuevo rol';
$this->btn_submit_text = 'Crear nuevo rol';
if ($roleId) {
$role = Role::findOrFail($roleId);
switch ($action) {
case 'view':
$this->title = $role->name;
$this->name = $role->name;
$this->style = $role->style;
$this->dispatch('deshabilitarFormulario');
break;
case 'update':
$this->title = 'Editar rol';
$this->btn_submit_text = 'Guardar cambios';
$this->roleId = $roleId;
$this->name = $role->name;
$this->style = $role->style;
$this->dispatch('habilitarFormulario');
break;
case 'clone':
$this->style = $role->style;
$this->dispatch('habilitarFormulario');
break;
default:
break;
}
foreach ($role->permissions as $permission) {
$this->permissionsInputs[str_replace('.', '_', $permission->name)] = true;
}
}
$this->dispatch('reloadForm');
}
public function loadDestroyRoleData() {}
public function saveRole()
{
$permissions = [];
foreach ($this->permissionsInputs as $permission => $value) {
if ($value === true)
$permissions[] = str_replace('_', '.', $permission);
}
if ($this->roleId) {
$role = Role::find($this->roleId);
$role->name = $this->name;
$role->style = $this->style;
$role->save();
$role->syncPermissions($permissions);
} else {
$role = Role::create([
'name' => $this->name,
'style' => $this->style,
]);
$role->syncPermissions($permissions);
}
$this->loadRolesAndPermissions();
$this->dispatch('modalHide');
$this->dispatch('reloadForm');
}
public function deleteRole()
{
$role = Role::find($this->destroyRoleId);
if ($role)
$role->delete();
$this->loadRolesAndPermissions();
$this->dispatch('modalDeleteHide');
$this->dispatch('reloadForm');
}
private function resetForm()
{
$this->roleId = '';
$this->name = '';
$this->style = '';
foreach ($this->permissionsInputs as $key => $permission) {
$this->permissionsInputs[$key] = false;
}
}
public function render()
{
return view('vuexy-admin::livewire.roles.cards');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Roles;
use Livewire\Component;
use Livewire\WithPagination;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
class RoleIndex extends Component
{
use WithPagination;
public $roleName;
public $selectedRole;
public $permissions = [];
public $availablePermissions;
public function mount()
{
$this->availablePermissions = Permission::all();
}
public function createRole()
{
$this->validate([
'roleName' => 'required|unique:roles,name'
]);
$role = Role::create(['name' => $this->roleName]);
$this->reset(['roleName']);
session()->flash('message', 'Rol creado con éxito.');
}
public function selectRole($roleId)
{
$this->selectedRole = Role::find($roleId);
$this->permissions = $this->selectedRole->permissions->pluck('id')->toArray();
}
public function updateRolePermissions()
{
if ($this->selectedRole) {
$this->selectedRole->syncPermissions($this->permissions);
session()->flash('message', 'Permisos actualizados correctamente.');
}
}
public function deleteRole($roleId)
{
Role::find($roleId)->delete();
session()->flash('message', 'Rol eliminado.');
}
public function render()
{
return view('livewire.roles', [
'index' => Role::paginate(10)
]);
}
}

View File

@ -0,0 +1,174 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Livewire\Component;
/**
* Clase base abstracta para la creación de componentes tipo "Index" con Livewire.
*
* Provee una estructura general para:
* - Configurar y renderizar tablas con Bootstrap Table.
* - Definir columnas y formatos de manera estándar.
* - Manejar búsquedas, filtros, o catálogos necesarios.
* - Centralizar la lógica de montaje (mount).
*
* @package Koneko\VuexyAdmin\Livewire\Table
*/
abstract class AbstractIndexComponent extends Component
{
/**
* Configuración principal para la tabla con Bootstrap Table.
*
* @var array
*/
public $bt_datatable = [];
/**
* Tag identificador del componente, derivado del modelo.
*
* @var string
*/
public $tagName;
/**
* Nombre singular del modelo (para mensajes, etiquetado, etc.).
*
* @var string
*/
public $singularName;
/**
* Identificador único del formulario (vinculado al Offcanvas o Modal).
*
* @var string
*/
public $formId;
/**
* Método para obtener la instancia del modelo asociado.
*
* Debe retornarse una instancia (o la clase) del modelo Eloquent que maneja este Index.
*
* @return Model|string
*/
abstract protected function model(): string;
/**
* Define las columnas (header) de la tabla. Este array se fusionará
* o se inyectará en la configuración principal $bt_datatable.
*
* @return array
*/
abstract protected function columns(): array;
/**
* Define el formato (formatter) de las columnas.
*
* @return array
*/
abstract protected function format(): array;
/**
* Retorna la ruta de la vista Blade que renderizará el componente.
*
* @return string
*/
abstract protected function viewPath(): string;
/**
* Método que define la configuración base del DataTable.
* Aquí puedes poner ajustes comunes (exportFileName, paginación, etc.).
*
* @return array
*/
protected function bootstraptableConfig(): array
{
return [
'sortName' => 'id', // Campo por defecto para ordenar
'exportFileName' => 'Listado', // Nombre de archivo para exportar
'showFullscreen' => false,
'showPaginationSwitch'=> false,
'showRefresh' => false,
'pagination' => false,
// Agrega aquí cualquier otra configuración por defecto que uses
];
}
/**
* Se ejecuta al montar el componente Livewire.
* Configura $tagName, $singularName, $formId y $bt_datatable.
*
* @return void
*/
public function mount(): void
{
// Obtenemos el modelo
$model = $this->model();
if (is_string($model)) {
// Si se retornó la clase en abstract protected function model(),
// instanciamos manualmente
$model = new $model;
}
// Usamos las propiedades definidas en el modelo
// (tagName, singularName, etc.), si existen en el modelo.
// Ajusta nombres según tu convención.
$this->tagName = $model->tagName ?? Str::snake(class_basename($model));
$this->singularName = $model->singularName ?? class_basename($model);
$this->formId = Str::kebab($this->tagName) . '-form';
// Inicia la configuración principal de la tabla
$this->setupDataTable();
}
/**
* Combina la configuración base de la tabla con las columnas y formatos
* definidos en las clases hijas.
*
* @return void
*/
protected function setupDataTable(): void
{
$baseConfig = $this->bootstraptableConfig();
$this->bt_datatable = array_merge($baseConfig, [
'header' => $this->columns(),
'format' => $this->format(),
]);
}
/**
* Renderiza la vista definida en viewPath().
*
* @return \Illuminate\View\View
*/
public function render()
{
return view($this->viewPath());
}
/**
* Ejemplo de método para la lógica de filtrado que podrías sobreescribir en la clase hija.
*
* @param array $criteria
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function applyFilters($criteria = [])
{
// Aplica tu lógica de filtros, búsquedas, etc.
// La clase hija podría sobrescribir este método o llamarlo desde su propia lógica.
$query = $this->model()::query();
// Por ejemplo:
/*
if (!empty($criteria['store_id'])) {
$query->where('store_id', $criteria['store_id']);
}
*/
return $query;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Users;
use Koneko\VuexyAdmin\Models\User;
use Livewire\Component;
class UserCount extends Component
{
public $total, $enabled, $disabled;
protected $listeners = ['refreshUserCount' => 'updateCounts'];
public function mount()
{
$this->updateCounts();
}
public function updateCounts()
{
$this->total = User::count();
$this->enabled = User::where('status', User::STATUS_ENABLED)->count();
$this->disabled = User::where('status', User::STATUS_DISABLED)->count();
}
public function render()
{
return view('vuexy-admin::livewire.users.count');
}
}

306
Livewire/Users/UserForm.php Normal file
View File

@ -0,0 +1,306 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Users;
use Illuminate\Validation\Rule;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormComponent;
use Koneko\SatCatalogs\Models\{Colonia, Estado, Localidad, Municipio, Pais, RegimenFiscal};
use Koneko\VuexyAdmin\Models\Store;
/**
* Class UserForm
*
* Componente Livewire para manejar el formulario CRUD de sucursales en el sistema ERP.
* Implementa la creación, edición y eliminación de sucursales con validaciones dinámicas.
*/
class UserForm extends AbstractFormComponent
{
/**
* Campos específicos del formulario.
*/
public $code, $name, $description, $manager_id, $rfc, $nombre_fiscal, $c_regimen_fiscal,
$domicilio_fiscal, $serie_ingresos, $serie_egresos, $serie_pagos, $c_codigo_postal,
$c_pais, $c_estado, $c_localidad, $c_municipio, $c_colonia, $direccion, $num_ext,
$num_int, $email, $tel, $tel2, $lat, $lng, $show_on_website, $enable_ecommerce, $status;
public $confirmDeletion = false;
/**
* Listas de opciones para selects en el formulario.
*/
public $manager_id_options = [],
$c_regimen_fiscal_options = [],
$c_pais_options = [],
$c_estado_options = [],
$c_localidad_options = [],
$c_municipio_options = [],
$c_colonia_options = [];
/**
* Montar el formulario e inicializar datos específicos.
*
* @param string $mode Modo del formulario: create, edit, delete.
* @param Store|null $store El modelo Store si está en modo edición o eliminación.
*/
public function mount(string $mode = 'create', mixed $store = null): void
{
parent::mount($mode, $store->id ?? null);
}
/**
* Cargar opciones de formularios según el modo actual.
*
* @param string $mode
*/
private function loadOptions(string $mode): void
{
$this->manager_id_options = User::getUsersListWithInactive($this->manager_id, ['type' => 'user', 'status' => 1]);
$this->c_regimen_fiscal_options = RegimenFiscal::selectList();
$this->c_pais_options = Pais::selectList();
$this->c_estado_options = Estado::selectList($this->c_pais)->toArray();
if ($mode !== 'create') {
$this->c_localidad_options = Localidad::selectList($this->c_estado)->toArray();
$this->c_municipio_options = Municipio::selectList($this->c_estado, $this->c_municipio)->toArray();
$this->c_colonia_options = Colonia::selectList($this->c_codigo_postal, $this->c_colonia)->toArray();
}
}
// ===================== MÉTODOS OBLIGATORIOS =====================
/**
* Devuelve el modelo Eloquent asociado.
*
* @return string
*/
protected function model(): string
{
return Store::class;
}
/**
* Reglas de validación dinámicas según el modo actual.
*
* @param string $mode
* @return array
*/
protected function dynamicRules(string $mode): array
{
switch ($mode) {
case 'create':
case 'edit':
return [
'code' => [
'required', 'string', 'alpha_num', 'max:16',
Rule::unique('stores', 'code')->ignore($this->id)
],
'name' => 'required|string|max:96',
'description' => 'nullable|string|max:1024',
'manager_id' => 'nullable|exists:users,id',
// Información fiscal
'rfc' => ['nullable', 'string', 'regex:/^([A-ZÑ&]{3,4})(\d{6})([A-Z\d]{3})$/i', 'max:13'],
'nombre_fiscal' => 'nullable|string|max:255',
'c_regimen_fiscal' => 'nullable|exists:sat_regimen_fiscal,c_regimen_fiscal',
'domicilio_fiscal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal',
// Ubicación
'c_pais' => 'nullable|exists:sat_pais,c_pais|string|size:3',
'c_estado' => 'nullable|exists:sat_estado,c_estado|string|min:2|max:3',
'c_municipio' => 'nullable|exists:sat_municipio,c_municipio|integer',
'c_localidad' => 'nullable|integer',
'c_codigo_postal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal|integer',
'c_colonia' => 'nullable|exists:sat_colonia,c_colonia|integer',
'direccion' => 'nullable|string|max:255',
'num_ext' => 'nullable|string|max:50',
'num_int' => 'nullable|string|max:50',
'lat' => 'nullable|numeric|between:-90,90',
'lng' => 'nullable|numeric|between:-180,180',
// Contacto
'email' => ['nullable', 'email', 'required_if:enable_ecommerce,true'],
'tel' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'],
'tel2' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'],
// Configuración web y estado
'show_on_website' => 'nullable|boolean',
'enable_ecommerce' => 'nullable|boolean',
'status' => 'nullable|boolean',
];
case 'delete':
return [
'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación
];
default:
return [];
}
}
/**
* Inicializa los datos del formulario en función del modo.
*
* @param Store|null $store
* @param string $mode
*/
protected function initializeFormData(mixed $store, string $mode): void
{
if ($store) {
$this->code = $store->code;
$this->name = $store->name;
$this->description = $store->description;
$this->manager_id = $store->manager_id;
$this->rfc = $store->rfc;
$this->nombre_fiscal = $store->nombre_fiscal;
$this->c_regimen_fiscal = $store->c_regimen_fiscal;
$this->domicilio_fiscal = $store->domicilio_fiscal;
$this->c_pais = $store->c_pais;
$this->c_estado = $store->c_estado;
$this->c_municipio = $store->c_municipio;
$this->c_localidad = $store->c_localidad;
$this->c_codigo_postal = $store->c_codigo_postal;
$this->c_colonia = $store->c_colonia;
$this->direccion = $store->direccion;
$this->num_ext = $store->num_ext;
$this->num_int = $store->num_int;
$this->lat = $store->lat;
$this->lng = $store->lng;
$this->email = $store->email;
$this->tel = $store->tel;
$this->tel2 = $store->tel2;
$this->show_on_website = (bool) $store->show_on_website;
$this->enable_ecommerce = (bool) $store->enable_ecommerce;
$this->status = (bool) $store->status;
} else {
$this->c_pais = 'MEX';
$this->status = true;
$this->show_on_website = false;
$this->enable_ecommerce = false;
}
$this->loadOptions($mode);
}
/**
* Prepara los datos validados para su almacenamiento.
*
* @param array $validatedData
* @return array
*/
protected function prepareData(array $validatedData): array
{
return [
'code' => $validatedData['code'],
'name' => $validatedData['name'],
'description' => strip_tags($validatedData['description']),
'manager_id' => $validatedData['manager_id'],
'rfc' => $validatedData['rfc'],
'nombre_fiscal' => $validatedData['nombre_fiscal'],
'c_regimen_fiscal' => $validatedData['c_regimen_fiscal'],
'domicilio_fiscal' => $validatedData['domicilio_fiscal'],
'c_codigo_postal' => $validatedData['c_codigo_postal'],
'c_pais' => $validatedData['c_pais'],
'c_estado' => $validatedData['c_estado'],
'c_localidad' => $validatedData['c_localidad'],
'c_municipio' => $validatedData['c_municipio'],
'c_colonia' => $validatedData['c_colonia'],
'direccion' => $validatedData['direccion'],
'num_ext' => $validatedData['num_ext'],
'num_int' => $validatedData['num_int'],
'email' => $validatedData['email'],
'tel' => $validatedData['tel'],
'tel2' => $validatedData['tel2'],
'lat' => $validatedData['lat'],
'lng' => $validatedData['lng'],
'status' => $validatedData['status'],
'show_on_website' => $validatedData['show_on_website'],
'enable_ecommerce' => $validatedData['enable_ecommerce'],
];
}
/**
* Definición de los contenedores de notificación.
*
* @return array
*/
protected function targetNotifies(): array
{
return [
"index" => "#bt-stores .notification-container",
"form" => "#store-form .notification-container",
];
}
/**
* Ruta de vista asociada al formulario.
*
* @return \Illuminate\Contracts\View\View
*/
protected function viewPath(): string
{
return 'vuexy-store-manager::livewire.stores.form';
}
// ===================== VALIDACIONES =====================
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'code' => 'código de sucursal',
'name' => 'nombre de la sucursal',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'code.required' => 'El código de la sucursal es obligatorio.',
'code.unique' => 'Este código ya está en uso por otra sucursal.',
'name.required' => 'El nombre de la sucursal es obligatorio.',
];
}
// ===================== PREPARACIÓN DE DATOS =====================
// ===================== NOTIFICACIONES Y EVENTOS =====================
/**
* Definición de los eventos del componente.
*
* @return array
*/
protected function dispatches(): array
{
return [
'on-failed-validation' => 'on-failed-validation-store',
'on-hydrate' => 'on-hydrate-store-modal',
];
}
// ===================== REDIRECCIÓN =====================
/**
* Define la ruta de redirección tras guardar o eliminar.
*
* @return string
*/
protected function getRedirectRoute(): string
{
return 'admin.core.user.index';
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Users;
use Spatie\Permission\Models\Role;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Koneko\VuexyAdmin\Models\User;
use Livewire\Component;
class UserIndex extends Component
{
public $statuses;
public $status_options;
public $rows_roles = [];
public $roles_options = [];
public $roles_html_select;
public $total, $enabled, $disabled;
public $indexAlert;
public $userId, $name, $email, $password, $roles, $status, $photo, $src_photo;
public $modalTitle;
public $btnSubmitTxt;
public function mount()
{
$this->modalTitle = 'Crear usuario nuevo';
$this->btnSubmitTxt = 'Crear usuario';
$this->statuses = [
User::STATUS_ENABLED => ['title' => 'Activo', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_ENABLED]],
User::STATUS_DISABLED => ['title' => 'Deshabilitado', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_DISABLED]],
User::STATUS_REMOVED => ['title' => 'Eliminado', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_REMOVED]],
];
$roles = Role::whereNotIn('name', ['Patient', 'Doctor'])->get();
$this->roles_html_select = "<select id=\"UserRole\" class=\"form-select text-capitalize\"><option value=\"\"> Selecciona un rol </option>";
foreach ($roles as $role) {
$this->rows_roles[$role->name] = "<span class=\"badge bg-label-" . $role->style . " mx-1\">" . $role->name . "</span>";
if (Auth::user()->hasRole('SuperAdmin') || $role->name != 'SuperAdmin') {
$this->roles_html_select .= "<option value=\"" . $role->name . "\" class=\"text-capitalize\">" . $role->name . "</option>";
$this->roles_options[$role->name] = $role->name;
}
}
$this->roles_html_select .= "</select>";
$this->status_options = [
User::STATUS_ENABLED => User::$statusList[User::STATUS_ENABLED],
User::STATUS_DISABLED => User::$statusList[User::STATUS_DISABLED],
];
}
public function countUsers()
{
$this->total = User::count();
$this->enabled = User::where('status', User::STATUS_ENABLED)->count();
$this->disabled = User::where('status', User::STATUS_DISABLED)->count();
}
public function edit($id)
{
$user = User::findOrFail($id);
$this->indexAlert = '';
$this->modalTitle = 'Editar usuario: ' . $id;
$this->btnSubmitTxt = 'Guardar cambios';
$this->userId = $user->id;
$this->name = $user->name;
$this->email = $user->email;
$this->password = '';
$this->roles = $user->roles->pluck('name')->toArray();
$this->src_photo = $user->profile_photo_url;
$this->status = $user->status;
$this->dispatch('openModal');
}
public function delete($id)
{
$user = User::find($id);
if ($user) {
// Eliminar la imagen de perfil si existe
if ($user->profile_photo_path)
Storage::disk('public')->delete($user->profile_photo_path);
// Eliminar el usuario
$user->delete();
$this->indexAlert = '<div class="alert alert-warning alert-dismissible" role="alert">Se eliminó correctamente el usuario.<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
$this->dispatch('refreshUserCount');
$this->dispatch('afterDelete');
} else {
$this->indexAlert = '<div class="alert alert-danger alert-dismissible" role="alert">Usuario no encontrado.<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
}
}
public function render()
{
return view('vuexy-admin::livewire.users.index', [
'users' => User::paginate(10),
]);
}
}

View File

@ -0,0 +1,299 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Users;
use Livewire\WithFileUploads;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
class UserIndex extends AbstractIndexComponent
{
use WithFileUploads;
public $doc_file;
public $dropzoneVisible = true;
/**
* Almacena rutas útiles para la funcionalidad de edición o eliminación.
*/
public $routes = [];
/**
* Método que define la clase o instancia del modelo a usar en este Index.
*/
protected function model(): string
{
return User::class;
}
/**
* Retorna las columnas (header) de la tabla.
*/
protected function columns(): array
{
return [
'action' => 'Acciones',
'code' => 'Código personal',
'full_name' => 'Nombre Completo',
'email' => 'Correo Electrónico',
'parent_name' => 'Responsable',
'parent_email' => 'Correo Responsable',
'company' => 'Empresa',
'birth_date' => 'Fecha de Nacimiento',
'hire_date' => 'Fecha de Contratación',
'curp' => 'CURP',
'nss' => 'NSS',
'job_title' => 'Puesto',
'rfc' => 'RFC',
'nombre_fiscal' => 'Nombre Fiscal',
'profile_photo_path' => 'Foto de Perfil',
'is_partner' => 'Socio',
'is_employee' => 'Empleado',
'is_prospect' => 'Prospecto',
'is_customer' => 'Cliente',
'is_provider' => 'Proveedor',
'is_user' => 'Usuario',
'status' => 'Estatus',
'creator' => 'Creado Por',
'creator_email' => 'Correo Creador',
'created_at' => 'Fecha de Creación',
'updated_at' => 'Última Modificación',
];
}
/**
* Retorna el formato (formatter) para cada columna.
*/
protected function format(): array
{
return [
'action' => [
'formatter' => 'userActionFormatter',
'onlyFormatter' => true,
],
'code' => [
'formatter' => [
'name' => 'dynamicBadgeFormatter',
'params' => ['color' => 'secondary'],
],
'align' => 'center',
'switchable' => false,
],
'full_name' => [
'formatter' => 'userProfileFormatter',
],
'email' => [
'formatter' => 'emailFormatter',
'visible' => false,
],
'parent_name' => [
'formatter' => 'contactParentFormatter',
'visible' => false,
],
'agent_name' => [
'formatter' => 'agentFormatter',
'visible' => false,
],
'company' => [
'formatter' => 'textNowrapFormatter',
],
'curp' => [
'visible' => false,
],
'nss' => [
'visible' => false,
],
'job_title' => [
'formatter' => 'textNowrapFormatter',
'visible' => false,
],
'rfc' => [
'visible' => false,
],
'nombre_fiscal' => [
'formatter' => 'textNowrapFormatter',
'visible' => false,
],
'domicilio_fiscal' => [
'visible' => false,
],
'c_uso_cfdi' => [
'formatter' => 'usoCfdiFormatter',
'visible' => false,
],
'tipo_persona' => [
'formatter' => 'dynamicBadgeFormatter',
'align' => 'center',
'visible' => false,
],
'c_regimen_fiscal' => [
'formatter' => 'regimenFiscalFormatter',
'visible' => false,
],
'birth_date' => [
'align' => 'center',
'visible' => false,
],
'hire_date' => [
'align' => 'center',
'visible' => false,
],
'estado' => [
'formatter' => 'textNowrapFormatter',
],
'municipio' => [
'formatter' => 'textNowrapFormatter',
],
'localidad' => [
'formatter' => 'textNowrapFormatter',
'visible' => false,
],
'is_partner' => [
'formatter' => [
'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'],
],
'align' => 'center',
],
'is_employee' => [
'formatter' => [
'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'],
],
'align' => 'center',
],
'is_prospect' => [
'formatter' => [
'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'],
],
'align' => 'center',
],
'is_customer' => [
'formatter' => [
'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'],
],
'align' => 'center',
],
'is_provider' => [
'formatter' => [
'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'],
],
'align' => 'center',
],
'is_user' => [
'formatter' => [
'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'],
],
'align' => 'center',
],
'status' => [
'formatter' => 'statusIntBadgeBgFormatter',
'align' => 'center',
],
'creator' => [
'formatter' => 'creatorFormatter',
'visible' => false,
],
'created_at' => [
'formatter' => 'textNowrapFormatter',
'align' => 'center',
'visible' => false,
],
'updated_at' => [
'formatter' => 'textNowrapFormatter',
'align' => 'center',
'visible' => false,
],
];
}
/**
* Procesa el documento recibido (CFDI XML o Constancia PDF).
*/
public function processDocument()
{
// Verificamos si el archivo es válido
if (!$this->doc_file instanceof UploadedFile) {
return $this->addError('doc_file', 'No se pudo recibir el archivo.');
}
try {
// Validar tipo de archivo
$this->validate([
'doc_file' => 'required|mimes:pdf,xml|max:2048'
]);
// **Detectar el tipo de documento**
$extension = strtolower($this->doc_file->getClientOriginalExtension());
// **Procesar según el tipo de archivo**
switch ($extension) {
case 'xml':
$service = new FacturaXmlService();
$data = $service->processUploadedFile($this->doc_file);
break;
case 'pdf':
$service = new ConstanciaFiscalService();
$data = $service->extractData($this->doc_file);
break;
default:
throw new Exception("Formato de archivo no soportado.");
}
dd($data);
// **Asignar los valores extraídos al formulario**
$this->rfc = $data['rfc'] ?? null;
$this->name = $data['name'] ?? null;
$this->email = $data['email'] ?? null;
$this->tel = $data['telefono'] ?? null;
//$this->direccion = $data['domicilio_fiscal'] ?? null;
// Ocultar el Dropzone después de procesar
$this->dropzoneVisible = false;
} catch (ValidationException $e) {
$this->handleValidationException($e);
} catch (QueryException $e) {
$this->handleDatabaseException($e);
} catch (ModelNotFoundException $e) {
$this->handleException('danger', 'Registro no encontrado.');
} catch (Exception $e) {
$this->handleException('danger', 'Error al procesar el archivo: ' . $e->getMessage());
}
}
/**
* Montamos el componente y llamamos al parent::mount() para configurar la tabla.
*/
public function mount(): void
{
parent::mount();
// Definimos las rutas específicas de este componente
$this->routes = [
'admin.user.show' => route('admin.core.users.show', ['user' => ':id']),
'admin.user.edit' => route('admin.core.users.edit', ['user' => ':id']),
'admin.user.delete' => route('admin.core.users.delete', ['user' => ':id']),
];
}
/**
* Retorna la vista a renderizar por este componente.
*/
protected function viewPath(): string
{
return 'vuexy-admin::livewire.users.index';
}
}

View File

@ -0,0 +1,295 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Users;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Illuminate\Http\UploadedFile;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyContacts\Services\{ContactCatalogService,ConstanciaFiscalService,FacturaXmlService};
use Koneko\VuexyStoreManager\Services\StoreCatalogService;
use Livewire\WithFileUploads;
/**
* Class UserOffCanvasForm
*
* Componente Livewire para gestionar almacenes.
* Extiende la clase AbstractFormOffCanvasComponent e implementa validaciones dinámicas,
* manejo de formularios, eventos y actualizaciones en tiempo real.
*
* @package Koneko\VuexyAdmin\Livewire\Users
*/
class UserOffCanvasForm extends AbstractFormOffCanvasComponent
{
use WithFileUploads;
public $doc_file;
public $dropzoneVisible = true;
/**
* Propiedades del formulario relacionadas con el usuario.
*/
public $code,
$parent_id,
$name,
$last_name,
$email,
$company,
$rfc,
$nombre_fiscal,
$tipo_persona,
$c_regimen_fiscal,
$domicilio_fiscal,
$is_partner,
$is_employee,
$is_prospect,
$is_customer,
$is_provider,
$status;
/**
* Listas de opciones para selects en el formulario.
*/
public $store_options = [],
$work_center_options = [],
$manager_options = [];
/**
* Eventos de escucha de Livewire.
*
* @var array
*/
protected $listeners = [
'editUsers' => 'loadFormModel',
'confirmDeletionUsers' => 'loadFormModelForDeletion',
];
/**
* Definición de tipos de datos que se deben castear.
*
* @var array
*/
protected $casts = [
'status' => 'boolean',
];
/**
* Define el modelo Eloquent asociado con el formulario.
*
* @return string
*/
protected function model(): string
{
return User::class;
}
/**
* Define los campos del formulario.
*
* @return array<string, mixed>
*/
protected function fields(): array
{
return (new User())->getFillable();
}
/**
* Valores por defecto para el formulario.
*
* @return array
*/
protected function defaults(): array
{
return [
//
];
}
/**
* Campo que se debe enfocar cuando se abra el formulario.
*
* @return string
*/
protected function focusOnOpen(): string
{
return 'name';
}
/**
* Define reglas de validación dinámicas basadas en el modo actual.
*
* @param string $mode El modo actual del formulario ('create', 'edit', 'delete').
* @return array
*/
protected function dynamicRules(string $mode): array
{
switch ($mode) {
case 'create':
case 'edit':
return [
'code' => ['required', 'string', 'max:16', Rule::unique('contact', 'code')->ignore($this->id)],
'name' => ['required', 'string', 'max:96'],
'notes' => ['nullable', 'string', 'max:1024'],
'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'],
];
case 'delete':
return [
'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación
];
default:
return [];
}
}
// ===================== VALIDACIONES =====================
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
protected function attributes(): array
{
return [
'code' => 'código de usuario',
'name' => 'nombre del usuario',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
protected function messages(): array
{
return [
'code.unique' => 'Este código ya está en uso por otro usuario.',
'name.required' => 'El nombre del usuario es obligatorio.',
];
}
/**
* Carga el formulario con datos del usuario y actualiza las opciones dinámicas.
*
* @param int $id
*/
public function loadFormModel($id): void
{
parent::loadFormModel($id);
$this->work_center_options = $this->store_id
? DB::table('store_work_centers')
->where('store_id', $this->store_id)
->pluck('name', 'id')
->toArray()
: [];
}
/**
* Carga el formulario para eliminar un usuario, actualizando las opciones necesarias.
*
* @param int $id
*/
public function loadFormModelForDeletion($id): void
{
parent::loadFormModelForDeletion($id);
$this->work_center_options = DB::table('store_work_centers')
->where('store_id', $this->store_id)
->pluck('name', 'id')
->toArray();
}
/**
* Define las opciones de los selectores desplegables.
*
* @return array
*/
protected function options(): array
{
$storeCatalogService = app(StoreCatalogService::class);
$contactCatalogService = app(ContactCatalogService::class);
return [
'store_options' => $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]),
'manager_options' => $contactCatalogService->searchCatalog('users', '', ['limit' => -1]),
];
}
/**
* Procesa el documento recibido (CFDI XML o Constancia PDF).
*/
public function processDocument()
{
// Verificamos si el archivo es válido
if (!$this->doc_file instanceof UploadedFile) {
return $this->addError('doc_file', 'No se pudo recibir el archivo.');
}
try {
// Validar tipo de archivo
$this->validate([
'doc_file' => 'required|mimes:pdf,xml|max:2048'
]);
// **Detectar el tipo de documento**
$extension = strtolower($this->doc_file->getClientOriginalExtension());
// **Procesar según el tipo de archivo**
switch ($extension) {
case 'xml':
$service = new FacturaXmlService();
$data = $service->processUploadedFile($this->doc_file);
break;
case 'pdf':
$service = new ConstanciaFiscalService();
$data = $service->extractData($this->doc_file);
break;
default:
throw new Exception("Formato de archivo no soportado.");
}
dd($data);
// **Asignar los valores extraídos al formulario**
$this->rfc = $data['rfc'] ?? null;
$this->name = $data['name'] ?? null;
$this->email = $data['email'] ?? null;
$this->tel = $data['telefono'] ?? null;
//$this->direccion = $data['domicilio_fiscal'] ?? null;
// Ocultar el Dropzone después de procesar
$this->dropzoneVisible = false;
} catch (ValidationException $e) {
$this->handleValidationException($e);
} catch (QueryException $e) {
$this->handleDatabaseException($e);
} catch (ModelNotFoundException $e) {
$this->handleException('danger', 'Registro no encontrado.');
} catch (Exception $e) {
$this->handleException('danger', 'Error al procesar el archivo: ' . $e->getMessage());
}
}
/**
* Ruta de la vista asociada con este formulario.
*
* @return string
*/
protected function viewPath(): string
{
return 'vuexy-admin::livewire.users.offcanvas-form';
}
}

283
Livewire/Users/UserShow.php Normal file
View File

@ -0,0 +1,283 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Users;
use App\Models\User;
use App\Models\Catalog\DropdownList;
use Koneko\SatCatalogs\Models\UsoCfdi;
use Koneko\SatCatalogs\Models\RegimenFiscal;
use Intervention\Image\ImageManager;
use Livewire\WithFileUploads;
use Livewire\Component;
class UserShow extends Component
{
use WithFileUploads;
public $image;
public User $user;
public $status_options, $pricelists_options;
public $regimen_fiscal_options, $uso_cfdi_options;
public $userId,
$name,
$cargo,
$profile_photo,
$profile_photo_path,
$email,
$password,
$password_confirmation,
$tipo_persona,
$rfc,
$nombre_fiscal,
$c_regimen_fiscal,
$domicilio_fiscal,
$c_uso_cfdi,
$pricelist_id,
$enable_credit,
$credit_days,
$credit_limit,
$is_prospect,
$is_customer,
$is_provider,
$is_user,
$status;
public $deleteUserImage;
public $cuentaUsuarioAlert,
$accesosAlert,
$facturacionElectronicaAlert;
// Reglas de validación para la cuenta de usuario
protected $rulesUser = [
'tipo_persona' => 'nullable|integer',
'name' => 'required|string|min:3|max:255',
'cargo' => 'nullable|string|min:3|max:255',
'is_prospect' => 'nullable|boolean',
'is_customer' => 'nullable|boolean',
'is_provider' => 'nullable|boolean',
'is_user' => 'nullable|boolean',
'pricelist_id' => 'nullable|integer',
'enable_credit' => 'nullable|boolean',
'credit_days' => 'nullable|integer',
'credit_limit' => 'nullable|numeric|min:0|max:9999999.99|regex:/^\d{1,7}(\.\d{1,2})?$/',
'image' => 'nullable|mimes:jpg,png|image|max:20480', // 20MB Max
];
// Reglas de validación para los campos fiscales
protected $rulesFacturacion = [
'rfc' => 'nullable|string|max:13',
'domicilio_fiscal' => [
'nullable',
'regex:/^[0-9]{5}$/',
'exists:sat_codigo_postal,c_codigo_postal'
],
'nombre_fiscal' => 'nullable|string|max:255',
'c_regimen_fiscal' => 'nullable|integer',
'c_uso_cfdi' => 'nullable|string',
];
public function mount($userId)
{
$this->user = User::findOrFail($userId);
$this->reloadUserData();
$this->pricelists_options = DropdownList::selectList(DropdownList::POS_PRICELIST);
$this->status_options = [
User::STATUS_ENABLED => User::$statusList[User::STATUS_ENABLED],
User::STATUS_DISABLED => User::$statusList[User::STATUS_DISABLED],
];
$this->regimen_fiscal_options = RegimenFiscal::selectList();
$this->uso_cfdi_options = UsoCfdi::selectList();
}
public function reloadUserData()
{
$this->tipo_persona = $this->user->tipo_persona;
$this->name = $this->user->name;
$this->cargo = $this->user->cargo;
$this->is_prospect = $this->user->is_prospect? true : false;
$this->is_customer = $this->user->is_customer? true : false;
$this->is_provider = $this->user->is_provider? true : false;
$this->is_user = $this->user->is_user? true : false;
$this->pricelist_id = $this->user->pricelist_id;
$this->enable_credit = $this->user->enable_credit? true : false;
$this->credit_days = $this->user->credit_days;
$this->credit_limit = $this->user->credit_limit;
$this->profile_photo = $this->user->profile_photo_url;
$this->profile_photo_path = $this->user->profile_photo_path;
$this->image = null;
$this->deleteUserImage = false;
$this->status = $this->user->status;
$this->email = $this->user->email;
$this->password = null;
$this->password_confirmation = null;
$this->rfc = $this->user->rfc;
$this->domicilio_fiscal = $this->user->domicilio_fiscal;
$this->nombre_fiscal = $this->user->nombre_fiscal;
$this->c_regimen_fiscal = $this->user->c_regimen_fiscal;
$this->c_uso_cfdi = $this->user->c_uso_cfdi;
$this->cuentaUsuarioAlert = null;
$this->accesosAlert = null;
$this->facturacionElectronicaAlert = null;
}
public function saveCuentaUsuario()
{
try {
// Validar Información de usuario
$validatedData = $this->validate($this->rulesUser);
$validatedData['name'] = trim($validatedData['name']);
$validatedData['cargo'] = $validatedData['cargo']? trim($validatedData['cargo']): null;
$validatedData['is_prospect'] = $validatedData['is_prospect'] ? 1 : 0;
$validatedData['is_customer'] = $validatedData['is_customer'] ? 1 : 0;
$validatedData['is_provider'] = $validatedData['is_provider'] ? 1 : 0;
$validatedData['is_user'] = $validatedData['is_user'] ? 1 : 0;
$validatedData['pricelist_id'] = $validatedData['pricelist_id'] ?: null;
$validatedData['enable_credit'] = $validatedData['enable_credit'] ? 1 : 0;
$validatedData['credit_days'] = $validatedData['credit_days'] ?: null;
$validatedData['credit_limit'] = $validatedData['credit_limit'] ?: null;
if($this->tipo_persona == User::TIPO_RFC_PUBLICO){
$validatedData['cargo'] = null;
$validatedData['is_prospect'] = null;
$validatedData['is_provider'] = null;
$validatedData['is_user'] = null;
$validatedData['enable_credit'] = null;
$validatedData['credit_days'] = null;
$validatedData['credit_limit'] = null;
}
if(!$this->user->is_prospect && !$this->user->is_customer){
$validatedData['pricelist_id'] = null;
}
if(!$this->user->is_customer){
$validatedData['enable_credit'] = null;
$validatedData['credit_days'] = null;
$validatedData['credit_limit'] = null;
}
$this->user->update($validatedData);
if($this->deleteUserImage && $this->user->profile_photo_path){
$this->user->deleteProfilePhoto();
// Reiniciar variables después de la eliminación
$this->deleteUserImage = false;
$this->profile_photo_path = null;
$this->profile_photo = $this->user->profile_photo_url;
}else if ($this->image) {
$image = ImageManager::imagick()->read($this->image->getRealPath());
$image = $image->scale(520, 520);
$imageName = $this->image->hashName(); // Genera un nombre único
$image->save(storage_path('app/public/profile-photos/' . $imageName));
$this->user->deleteProfilePhoto();
$this->profile_photo_path = $this->user->profile_photo_path = 'profile-photos/' . $imageName;
$this->profile_photo = $this->user->profile_photo_url;
$this->user->save();
unlink($this->image->getRealPath());
$this->reset('image');
}
// Puedes también devolver un mensaje de éxito si lo deseas
$this->setAlert('Se guardó los cambios exitosamente.', 'cuentaUsuarioAlert');
} catch (\Illuminate\Validation\ValidationException $e) {
// Si hay errores de validación, los puedes capturar y manejar aquí
$this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'cuentaUsuarioAlert', 'danger');
}
}
public function saveAccesos()
{
try {
$validatedData = $this->validate([
'status' => 'integer',
'email' => ['required', 'email', 'unique:users,email,' . $this->user->id],
'password' => ['nullable', 'string', 'min:6', 'max:32', 'confirmed'], // La regla 'confirmed' valida que ambas contraseñas coincidan
], [
'email.required' => 'El correo electrónico es obligatorio.',
'email.email' => 'Debes ingresar un correo electrónico válido.',
'email.unique' => 'Este correo ya está en uso.',
'password.min' => 'La contraseña debe tener al menos 5 caracteres.',
'password.max' => 'La contraseña no puede tener más de 32 caracteres.',
'password.confirmed' => 'Las contraseñas no coinciden.',
]);
// Si la validación es exitosa, continuar con el procesamiento
$validatedData['email'] = trim($this->email);
if ($this->password)
$validatedData['password'] = bcrypt($this->password);
else
unset($validatedData['password']);
$this->user->update($validatedData);
$this->password = null;
$this->password_confirmation = null;
// Puedes también devolver un mensaje de éxito si lo deseas
$this->setAlert('Se guardó los cambios exitosamente.', 'accesosAlert');
} catch (\Illuminate\Validation\ValidationException $e) {
// Si hay errores de validación, los puedes capturar y manejar aquí
$this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'accesosAlert', 'danger');
}
}
public function saveFacturacionElectronica()
{
try {
// Validar Información fiscal
$validatedData = $this->validate($this->rulesFacturacion);
$validatedData['rfc'] = strtoupper(trim($validatedData['rfc'])) ?: null;
$validatedData['domicilio_fiscal'] = $validatedData['domicilio_fiscal'] ?: null;
$validatedData['nombre_fiscal'] = strtoupper(trim($validatedData['nombre_fiscal'])) ?: null;
$validatedData['c_regimen_fiscal'] = $validatedData['c_regimen_fiscal'] ?: null;
$validatedData['c_uso_cfdi'] = $validatedData['c_uso_cfdi'] ?: null;
$this->user->update($validatedData);
// Puedes también devolver un mensaje de éxito si lo deseas
$this->setAlert('Se guardó los cambios exitosamente.', 'facturacionElectronicaAlert');
} catch (\Illuminate\Validation\ValidationException $e) {
// Si hay errores de validación, los puedes capturar y manejar aquí
$this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'facturacionElectronicaAlert', 'danger');
}
}
private function setAlert($message, $alertName, $type = 'success')
{
$this->$alertName = [
'message' => $message,
'type' => $type
];
}
public function render()
{
return view('livewire.admin.crm.contact-view');
}
}