diff --git a/.gitignore b/.gitignore index d07bec2..8e1291c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /.phpunit.result.cache /.zed /.idea +composer.lock \ No newline at end of file diff --git a/Http/Controllers/AdminController.php b/Http/Controllers/AdminController.php deleted file mode 100644 index edd14e7..0000000 --- a/Http/Controllers/AdminController.php +++ /dev/null @@ -1,62 +0,0 @@ -expectsJson(), 403, __('errors.ajax_only')); - - $VuexyAdminService = app(VuexyAdminService::class); - - return response()->json($VuexyAdminService->getVuexySearchData()); - } - - public function quickLinksUpdate(Request $request) - { - abort_if(!request()->expectsJson(), 403, __('errors.ajax_only')); - - $validated = $request->validate([ - 'action' => 'required|in:update,remove', - 'route' => 'required|string', - ]); - - $quickLinks = Setting::where('user_id', Auth::user()->id) - ->where('key', 'quicklinks') - ->first(); - - $quickLinks = $quickLinks ? json_decode($quickLinks->value, true) : []; - - if ($validated['action'] === 'update') { - // Verificar si ya existe - if (!in_array($validated['route'], $quickLinks)) - $quickLinks[] = $validated['route']; - } elseif ($validated['action'] === 'remove') { - // Eliminar la ruta si existe - $quickLinks = array_filter($quickLinks, function ($route) use ($validated) { - return $route !== $validated['route']; - }); - } - - Setting::updateOrCreate(['user_id' => Auth::user()->id, 'key' => 'quicklinks'], ['value' => json_encode($quickLinks)]); - - VuexyAdminService::clearQuickLinksCache(); - } - - public function generalSettings() - { - return view('vuexy-admin::admin-settings.webapp-general-settings'); - } - - public function smtpSettings() - { - return view('vuexy-admin::admin-settings.smtp-settings'); - } -} diff --git a/Http/Controllers/CacheController.php b/Http/Controllers/CacheController.php index fd365d6..aa04c6f 100644 --- a/Http/Controllers/CacheController.php +++ b/Http/Controllers/CacheController.php @@ -8,6 +8,13 @@ use Koneko\VuexyAdmin\Services\CacheConfigService; class CacheController extends Controller { + public function index(CacheConfigService $cacheConfigService) + { + $configCache = $cacheConfigService->getConfig(); + + return view('vuexy-admin::cache-manager.index', compact('configCache')); + } + public function generateConfigCache() { try { @@ -27,15 +34,9 @@ class CacheController extends Controller Artisan::call('route:cache'); return response()->json(['success' => true, 'message' => 'Cache de rutas generado correctamente.']); + } catch (\Exception $e) { return response()->json(['success' => false, 'message' => 'Error al generar el cache de rutas.', 'error' => $e->getMessage()], 500); } } - - public function cacheManager(CacheConfigService $cacheConfigService) - { - $configCache = $cacheConfigService->getConfig(); - - return view('vuexy-admin::cache-manager.index', compact('configCache')); - } } diff --git a/Http/Controllers/GlobalSettingsController.php b/Http/Controllers/GlobalSettingsController.php new file mode 100644 index 0000000..584d4e4 --- /dev/null +++ b/Http/Controllers/GlobalSettingsController.php @@ -0,0 +1,75 @@ +ajax()) { + $bootstrapTableIndexConfig = [ + 'table' => 'settings', + 'columns' => [ + 'settings.id', + 'settings.key', + 'settings.category', + 'settings.user_id', + DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS user_fullname"), + 'settings.value_string', + 'settings.value_integer', + 'settings.value_boolean', + 'settings.value_float', + DB::raw("IF(LENGTH(settings.value_text) > 60, CONCAT(LEFT(settings.value_text, 60), '..'), settings.value_text) AS value_text"), + DB::raw("IF(settings.value_binary, '-BINARY-', '') AS value_binary"), + 'settings.mime_type', + 'settings.file_name', + 'settings.created_at', + 'settings.updated_at', + 'settings.updated_by', + DB::raw("CONCAT_WS(' ', creator.name, creator.last_name) AS creator_name"), + ], + 'joins' => [ + [ + 'table' => 'users', + 'first' => 'settings.user_id', + 'second' => 'users.id', + 'type' => 'leftJoin', + ], + [ + 'table' => 'users', + 'first' => 'settings.updated_by', + 'second' => 'creator.id', + 'type' => 'leftJoin', + 'alias' => 'creator', + ], + ], + 'filters' => [ + 'search' => [ + 'settings.key', + 'settings.category', + 'users.name', + 'users.last_name', + 'creator.name', + 'creator.last_name', + ], + ], + 'sort_column' => 'settings.key', + 'default_sort_order' => 'asc', + ]; + + return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson(); + } + + return view('vuexy-admin::global-settings.index'); + } +} diff --git a/Http/Controllers/PermissionController.php b/Http/Controllers/PermissionController.php index 87fa3ca..4d36c78 100644 --- a/Http/Controllers/PermissionController.php +++ b/Http/Controllers/PermissionController.php @@ -3,11 +3,8 @@ namespace Koneko\VuexyAdmin\Http\Controllers; use Illuminate\Http\Request; -use Illuminate\Support\Arr; -use Spatie\Permission\Models\Permission; -use Yajra\DataTables\Facades\DataTables; - use App\Http\Controllers\Controller; +use Koneko\VuexyAdmin\Queries\GenericQueryBuilder; class PermissionController extends Controller { @@ -19,17 +16,26 @@ class PermissionController extends Controller public function index(Request $request) { if ($request->ajax()) { - $permissions = Permission::latest()->get(); + $bootstrapTableIndexConfig = [ + 'table' => 'permissions', + 'columns' => [ + 'permissions.id', + 'permissions.name', + 'permissions.group_name', + 'permissions.sub_group_name', + 'permissions.action', + 'permissions.guard_name', + 'permissions.created_at', + 'permissions.updated_at', + ], + 'filters' => [ + 'search' => ['permissions.name', 'permissions.group_name', 'permissions.sub_group_name', 'permissions.action'], + ], + 'sort_column' => 'permissions.name', + 'default_sort_order' => 'asc', + ]; - return DataTables::of($permissions) - ->addIndexColumn() - ->addColumn('assigned_to', function ($row) { - return (Arr::pluck($row->roles, ['name'])); - }) - ->editColumn('created_at', function ($request) { - return $request->created_at->format('Y-m-d h:i:s a'); - }) - ->make(true); + return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson(); } return view('vuexy-admin::permissions.index'); diff --git a/Http/Controllers/UserController copy.php b/Http/Controllers/UserController copy.php deleted file mode 100644 index 117d910..0000000 --- a/Http/Controllers/UserController copy.php +++ /dev/null @@ -1,188 +0,0 @@ -ajax()) { - $users = User::when(!Auth::user()->hasRole('SuperAdmin'), function ($query) { - $query->where('id', '>', 1); - }) - ->latest() - ->get(); - - return DataTables::of($users) - ->only(['id', 'name', 'email', 'avatar', 'roles', 'status', 'created_at']) - ->addIndexColumn() - ->addColumn('avatar', function ($user) { - return $user->profile_photo_url; - }) - ->addColumn('roles', function ($user) { - return (Arr::pluck($user->roles, ['name'])); - }) - /* - ->addColumn('stores', function ($user) { - return (Arr::pluck($user->stores, ['nombre'])); - }) - y*/ - ->editColumn('created_at', function ($user) { - return $user->created_at->format('Y-m-d'); - }) - ->make(true); - } - - return view('vuexy-admin::users.index'); - } - - /** - * Store a newly created resource in storage. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - public function store(Request $request) - { - $validator = Validator::make($request->all(), [ - 'name' => 'required|max:255', - 'email' => 'required|max:255|unique:users', - 'photo' => 'nullable|mimes:jpg,jpeg,png|max:1024', - 'password' => 'required', - ]); - - if ($validator->fails()) - return response()->json(['errors' => $validator->errors()->all()]); - - // Preparamos los datos - $user_request = array_merge_recursive($request->all(), [ - 'remember_token' => Str::random(10), - 'created_by' => Auth::user()->id, - ]); - - $user_request['password'] = bcrypt($request->password); - - // Guardamos el nuevo usuario - $user = User::create($user_request); - - // Asignmos los permisos - $user->assignRole($request->roles); - - // Asignamos Sucursals - //$user->stores()->attach($request->stores); - - if ($request->file('photo')){ - $avatarImageService = new AvatarImageService(); - - $avatarImageService->updateProfilePhoto($user, $request->file('photo')); - } - - return response()->json(['success' => 'Se agrego correctamente el usuario']); - } - - /** - * Display the specified resource. - * - * @param int User $user - * @return \Illuminate\Http\Response - */ - public function show(User $user) - { - return view('vuexy-admin::users.show', compact('user')); - } - - /** - * Update the specified resource in storage. - * - * @param \Illuminate\Http\Request $request - * @param int User $user - * @return \Illuminate\Http\Response - */ - public function updateAjax(Request $request, User $user) - { - // Validamos los datos - $validator = Validator::make($request->all(), [ - 'name' => 'required|max:191', - 'email' => "required|max:191|unique:users,email," . $user->id, - 'photo' => 'nullable|mimes:jpg,jpeg,png|max:2048' - ]); - - if ($validator->fails()) - return response()->json(['errors' => $validator->errors()->all()]); - - // Preparamos los datos - $user_request = $request->all(); - - if ($request->password) { - $user_request['password'] = bcrypt($request->password); - } else { - unset($user_request['password']); - } - - // Guardamos los cambios - $user->update($user_request); - - // Sincronizamos Roles - $user->syncRoles($request->roles); - - // Sincronizamos Sucursals - //$user->stores()->sync($request->stores); - - // Actualizamos foto de perfil - if ($request->file('photo')) - $avatarImageService = new AvatarImageService(); - - $avatarImageService->updateProfilePhoto($user, $request->file('photo')); - - return response()->json(['success' => 'Se guardo correctamente los cambios.']); - } - - - public function userSettings(User $user) - { - return view('vuexy-admin::users.user-settings', compact('user')); - } - - public function generateAvatar(Request $request) - { - // Validación de entrada - $request->validate([ - 'name' => 'nullable|string', - 'color' => 'nullable|string|size:6', - 'background' => 'nullable|string|size:6', - 'size' => 'nullable|integer|min:20|max:1024' - ]); - - $name = $request->get('name', 'NA'); - $color = $request->get('color', '7F9CF5'); - $background = $request->get('background', 'EBF4FF'); - $size = $request->get('size', 100); - - return User::getAvatarImage($name, $color, $background, $size); - - try { - } catch (\Exception $e) { - // String base64 de una imagen PNG transparente de 1x1 píxel - $transparentBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=='; - - return response()->make(base64_decode($transparentBase64), 200, [ - 'Content-Type' => 'image/png' - ]); - } - } -} diff --git a/Http/Controllers/UserController.php b/Http/Controllers/UserController.php index f5af305..47bcfb7 100644 --- a/Http/Controllers/UserController.php +++ b/Http/Controllers/UserController.php @@ -5,7 +5,7 @@ namespace Koneko\VuexyAdmin\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Str; -use Illuminate\Support\Facades\{Auth,DB,Validator}; +use Illuminate\Support\Facades\{Auth,Validator}; use Koneko\VuexyAdmin\Models\User; use Koneko\VuexyAdmin\Services\AvatarImageService; use Koneko\VuexyAdmin\Queries\GenericQueryBuilder; @@ -24,97 +24,19 @@ class UserController extends Controller 'table' => 'users', 'columns' => [ 'users.id', - 'users.code', - DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS full_name"), + 'users.name AS full_name', 'users.email', - 'users.birth_date', - 'users.hire_date', - 'users.curp', - 'users.nss', - 'users.job_title', + 'users.email_verified_at', 'users.profile_photo_path', - DB::raw("(SELECT GROUP_CONCAT(roles.name SEPARATOR ';') as roles FROM model_has_roles INNER JOIN roles ON (model_has_roles.role_id = roles.id) WHERE model_has_roles.model_id = 1) as roles"), - 'users.is_partner', - 'users.is_employee', - 'users.is_prospect', - 'users.is_customer', - 'users.is_provider', - 'users.is_user', 'users.status', - DB::raw("CONCAT_WS(' ', created.name, created.last_name) AS creator"), - 'created.email AS creator_email', + 'users.created_by', 'users.created_at', 'users.updated_at', ], - 'joins' => [ - [ - 'table' => 'users as parent', - 'first' => 'users.parent_id', - 'second' => 'parent.id', - 'type' => 'leftJoin', - ], - [ - 'table' => 'users as agent', - 'first' => 'users.agent_id', - 'second' => 'agent.id', - 'type' => 'leftJoin', - ], - [ - 'table' => 'users as created', - 'first' => 'users.created_by', - 'second' => 'created.id', - 'type' => 'leftJoin', - ], - [ - 'table' => 'sat_codigo_postal', - 'first' => 'users.domicilio_fiscal', - 'second' => 'sat_codigo_postal.c_codigo_postal', - 'type' => 'leftJoin', - ], - [ - 'table' => 'sat_estado', - 'first' => 'sat_codigo_postal.c_estado', - 'second' => 'sat_estado.c_estado', - 'type' => 'leftJoin', - 'and' => [ - 'sat_estado.c_pais = "MEX"', - ], - ], - [ - 'table' => 'sat_localidad', - 'first' => 'sat_codigo_postal.c_localidad', - 'second' => 'sat_localidad.c_localidad', - 'type' => 'leftJoin', - 'and' => [ - 'sat_codigo_postal.c_estado = sat_localidad.c_estado', - ], - ], - [ - 'table' => 'sat_municipio', - 'first' => 'sat_codigo_postal.c_municipio', - 'second' => 'sat_municipio.c_municipio', - 'type' => 'leftJoin', - 'and' => [ - 'sat_codigo_postal.c_estado = sat_municipio.c_estado', - ], - ], - [ - 'table' => 'sat_regimen_fiscal', - 'first' => 'users.c_regimen_fiscal', - 'second' => 'sat_regimen_fiscal.c_regimen_fiscal', - 'type' => 'leftJoin', - ], - [ - 'table' => 'sat_uso_cfdi', - 'first' => 'users.c_uso_cfdi', - 'second' => 'sat_uso_cfdi.c_uso_cfdi', - 'type' => 'leftJoin', - ], - ], 'filters' => [ - 'search' => ['users.name', 'users.email', 'users.code', 'parent.name', 'created.name'], + 'search' => ['users.code', 'users.full_name', 'users.email', 'parent_name'], ], - 'sort_column' => 'users.name', + 'sort_column' => 'users.full_name', 'default_sort_order' => 'asc', ]; @@ -160,9 +82,7 @@ class UserController extends Controller //$user->stores()->attach($request->stores); if ($request->file('photo')){ - $avatarImageService = new AvatarImageService(); - - $avatarImageService->updateProfilePhoto($user, $request->file('photo')); + app(AvatarImageService::class)->updateProfilePhoto($user, $request->file('photo')); } return response()->json(['success' => 'Se agrego correctamente el usuario']); @@ -217,10 +137,9 @@ class UserController extends Controller //$user->stores()->sync($request->stores); // Actualizamos foto de perfil - if ($request->file('photo')) - $avatarImageService = new AvatarImageService(); - - $avatarImageService->updateProfilePhoto($user, $request->file('photo')); + if ($request->file('photo')){ + app(AvatarImageService::class)->updateProfilePhoto($user, $request->file('photo')); + } return response()->json(['success' => 'Se guardo correctamente los cambios.']); } diff --git a/Http/Controllers/UserProfileController.php b/Http/Controllers/UserProfileController.php index ac36c68..23a697a 100644 --- a/Http/Controllers/UserProfileController.php +++ b/Http/Controllers/UserProfileController.php @@ -5,7 +5,6 @@ namespace Koneko\VuexyAdmin\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Koneko\VuexyAdmin\Services\AvatarInitialsService; -use Koneko\VuexyAdmin\Models\User; class UserProfileController extends Controller { @@ -35,10 +34,8 @@ class UserProfileController extends Controller $background = $request->get('background', 'EBF4FF'); $size = $request->get('size', 100); - $avatarService = new AvatarInitialsService(); - try { - return $avatarService->getAvatarImage($name, $color, $background, $size); + return app(AvatarInitialsService::class)->getAvatarImage($name, $color, $background, $size); } catch (\Exception $e) { // String base64 de una imagen PNG transparente de 1x1 píxel diff --git a/Http/Controllers/VuexyAdminController.php b/Http/Controllers/VuexyAdminController.php new file mode 100644 index 0000000..7b0ad3a --- /dev/null +++ b/Http/Controllers/VuexyAdminController.php @@ -0,0 +1,100 @@ +expectsJson(), 403, __('errors.ajax_only')); + + return response()->json(app(VuexyAdminService::class)->getVuexySearchData()); + } + + /** + * Actualiza los enlaces rápidos del usuario + * + * @param Request $request Datos de la solicitud + * @return void + * @throws \Illuminate\Http\Exceptions\HttpResponseException + * @throws \Illuminate\Validation\ValidationException + */ + public function quickLinksUpdate(Request $request) + { + abort_if(!request()->expectsJson(), 403, __('errors.ajax_only')); + + $validated = $request->validate([ + 'action' => 'required|in:update,remove', + 'route' => 'required|string', + ]); + + $quickLinks = Setting::where('key', 'quicklinks') + ->where('user_id', Auth::user()->id) + ->first(); + + $quickLinks = $quickLinks ? json_decode($quickLinks->value, true) : []; + + if ($validated['action'] === 'update') { + // Verificar si ya existe + if (!in_array($validated['route'], $quickLinks)) { + $quickLinks[] = $validated['route']; + } + + } elseif ($validated['action'] === 'remove') { + // Eliminar la ruta si existe + $quickLinks = array_filter($quickLinks, function ($route) use ($validated) { + return $route !== $validated['route']; + }); + } + + app(SettingsService::class)->set('quicklinks', json_encode($quickLinks), Auth::user()->id, 'vuexy-admin'); + + VuexyAdminService::clearQuickLinksCache(); + } +} diff --git a/Livewire/AdminSettings/ApplicationSettings.php b/Livewire/AdminSettings/ApplicationSettings.php deleted file mode 100644 index 4c5e131..0000000 --- a/Livewire/AdminSettings/ApplicationSettings.php +++ /dev/null @@ -1,83 +0,0 @@ -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'); - } -} diff --git a/Livewire/AdminSettings/InterfaceSettings.php b/Livewire/AdminSettings/InterfaceSettings.php deleted file mode 100644 index 33ea5b1..0000000 --- a/Livewire/AdminSettings/InterfaceSettings.php +++ /dev/null @@ -1,118 +0,0 @@ -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'); - } -} diff --git a/Livewire/AdminSettings/MailSenderResponseSettings.php b/Livewire/AdminSettings/MailSenderResponseSettings.php deleted file mode 100644 index a6a1d35..0000000 --- a/Livewire/AdminSettings/MailSenderResponseSettings.php +++ /dev/null @@ -1,106 +0,0 @@ - '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'); - } -} diff --git a/Livewire/Form/AbstractFormOffCanvasComponent.php b/Livewire/Form/AbstractFormOffCanvasComponent.php index 8512a26..59631f6 100644 --- a/Livewire/Form/AbstractFormOffCanvasComponent.php +++ b/Livewire/Form/AbstractFormOffCanvasComponent.php @@ -128,27 +128,6 @@ abstract class AbstractFormOffCanvasComponent extends Component */ abstract protected function model(): string; - /** - * Define los campos del formulario. - * - * @return array - */ - abstract protected function fields(): array; - - /** - * Retorna los valores por defecto para los campos del formulario. - * - * @return array 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. * @@ -157,13 +136,6 @@ abstract class AbstractFormOffCanvasComponent extends Component */ abstract protected function dynamicRules(string $mode): array; - /** - * Devuelve las opciones que se mostrarán en los selectores del formulario. - * - * @return array Opciones para los campos del formulario. - */ - abstract protected function options(): array; - /** * Retorna la ruta de la vista asociada al formulario. * @@ -171,6 +143,50 @@ abstract class AbstractFormOffCanvasComponent extends Component */ abstract protected function viewPath(): string; + // ===================== CONFIGURACIÓN ===================== + + /** + * Define los campos del formulario. + * + * @return array + */ + protected function fields(): array + { + return (new ($this->model()))->getFillable(); + } + + /** + * Retorna los valores por defecto para los campos del formulario. + * + * @return array Valores predeterminados. + */ + protected function defaults(): array + { + return []; + } + + /** + * Campo que se debe enfocar cuando se abra el formulario. + * + * @return string + */ + protected function focusOnOpen(): string + { + return ''; + } + + // ===================== OPCIONES ===================== + + /** + * Devuelve las opciones que se mostrarán en los selectores del formulario. + * + * @return array Opciones para los campos del formulario. + */ + protected function options(): array + { + return []; + } + // ===================== VALIDACIONES ===================== protected function attributes(): array @@ -198,7 +214,7 @@ abstract class AbstractFormOffCanvasComponent extends Component $model = new ($this->model()); - $this->tagName = $model->tagName; + $this->tagName = Str::camel($model->tagName); $this->columnNameLabel = $model->columnNameLabel; $this->singularName = $model->singularName; $this->offcanvasId = 'offcanvas' . ucfirst(Str::camel($model->tagName)); @@ -288,6 +304,9 @@ abstract class AbstractFormOffCanvasComponent extends Component $model = $this->model()::find($id); if ($model) { + +dd($this->fields()); + $data = $model->only(['id', ...$this->fields()]); $this->applyCasts($data); diff --git a/Livewire/Permissions/PermissionIndex.php b/Livewire/Permissions/PermissionIndex.php deleted file mode 100644 index 2aa71da..0000000 --- a/Livewire/Permissions/PermissionIndex.php +++ /dev/null @@ -1,28 +0,0 @@ -roles_html_select = ""; - - return view('vuexy-admin::livewire.permissions.index'); - } -} diff --git a/Livewire/Permissions/PermissionOffCanvasForm.php b/Livewire/Permissions/PermissionOffCanvasForm.php new file mode 100644 index 0000000..f12db71 --- /dev/null +++ b/Livewire/Permissions/PermissionOffCanvasForm.php @@ -0,0 +1,160 @@ + 'loadFormModel', + 'confirmDeletionPermission' => 'loadFormModelForDeletion', + ]; + + /** + * Define el modelo Eloquent asociado con el formulario. + * + * @return string + */ + protected function model(): string + { + return Permission::class; + } + + /** + * Valores por defecto para el formulario. + * + * @return array + */ + protected function defaults(): array + { + return [ + 'guard_name' => 'web', + ]; + } + + /** + * 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 [ + 'name' => ['required', 'string', Rule::unique('permissions', 'name')->ignore($this->id)], + 'group_name' => ['nullable', 'string'], + 'sub_group_name' => ['nullable', 'string'], + 'action' => ['nullable', 'string'], + 'guard_name' => ['required', 'string'], + ]; + + case 'delete': + return ['confirmDeletion' => 'accepted']; + + default: + return []; + } + } + + /** + * Define los atributos personalizados para los errores de validación. + * + * @return array + */ + protected function attributes(): array + { + return [ + 'name' => 'nombre del permiso', + ]; + } + + /** + * Define los mensajes de error personalizados para la validación. + * + * @return array + */ + protected function messages(): array + { + return [ + 'name.required' => 'El nombre del permiso es obligatorio.', + 'name.unique' => 'Este permiso ya existe.', + ]; + } + + /** + * Carga el formulario con datos de un permiso específico. + * + * @param int $id + */ + public function loadFormModel($id): void + { + parent::loadFormModel($id); + } + + /** + * Carga el formulario para eliminar un permiso específico. + * + * @param int $id + */ + public function loadFormModelForDeletion($id): void + { + parent::loadFormModelForDeletion($id); + } + + /** + * Define las opciones de los selectores desplegables. + * + * @return array + */ + protected function options(): array + { + return []; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-admin::livewire.permissions.offcanvas-form'; + } +} diff --git a/Livewire/Permissions/Permissions.php b/Livewire/Permissions/Permissions.php deleted file mode 100644 index 661bc7f..0000000 --- a/Livewire/Permissions/Permissions.php +++ /dev/null @@ -1,35 +0,0 @@ -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() - ]); - } -} diff --git a/Livewire/Permissions/PermissionsIndex.php b/Livewire/Permissions/PermissionsIndex.php new file mode 100644 index 0000000..e5de5e1 --- /dev/null +++ b/Livewire/Permissions/PermissionsIndex.php @@ -0,0 +1,98 @@ + 'Acciones', + 'name' => 'Nombre del Permiso', + 'group_name' => 'Grupo', + 'sub_group_name' => 'Subgrupo', + 'action' => 'Acción', + 'guard_name' => 'Guard', + 'created_at' => 'Creado', + 'updated_at' => 'Modificado', + ]; + } + + /** + * Retorna el formato para cada columna. + * + * @return array + */ + protected function format(): array + { + return [ + 'action' => [ + 'formatter' => 'storeActionFormatter', + 'onlyFormatter' => true, + ], + 'name' => [ + 'switchable' => false, + ], + 'created_at' => [ + 'formatter' => 'whitespaceNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + 'updated_at' => [ + 'formatter' => 'whitespaceNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + ]; + } + + /** + * Sobrescribe la configuración base de la tabla. + * + * @return array + */ + protected function bootstraptableConfig(): array + { + return array_merge(parent::bootstraptableConfig(), [ + 'sortName' => 'name', + 'exportFileName' => 'Permisos', + 'showFullscreen' => false, + 'showPaginationSwitch'=> false, + 'showRefresh' => false, + 'pagination' => false, + ]); + } + + /** + * Retorna la vista a renderizar por este componente. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-admin::livewire.permissions.index'; + } +} diff --git a/Livewire/Profile/DeleteUserForm.php b/Livewire/Profile/DeleteUserForm.php new file mode 100644 index 0000000..f0961dd --- /dev/null +++ b/Livewire/Profile/DeleteUserForm.php @@ -0,0 +1,118 @@ +state = array_merge([ + 'email' => $user->email, + ], $user->withoutRelations()->toArray()); + } + + /** + * Update the user's profile information. + * + * @param \Laravel\Fortify\Contracts\UpdatesUserProfileInformation $updater + * @return \Illuminate\Http\RedirectResponse|null + */ + public function updateProfileInformation(UpdatesUserProfileInformation $updater) + { + $this->resetErrorBag(); + + $updater->update( + Auth::user(), + $this->photo + ? array_merge($this->state, ['photo' => $this->photo]) + : $this->state + ); + + if (isset($this->photo)) { + return redirect()->route('profile.show'); + } + + $this->dispatch('saved'); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Delete user's profile photo. + * + * @return void + */ + public function deleteProfilePhoto() + { + Auth::user()->deleteProfilePhoto(); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Sent the email verification. + * + * @return void + */ + public function sendEmailVerification() + { + Auth::user()->sendEmailVerificationNotification(); + + $this->verificationLinkSent = true; + } + + /** + * Get the current user of the application. + * + * @return mixed + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Render the component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('vuexy-admin::livewire.profile.update-profile-information-form'); + } +} diff --git a/Livewire/Profile/LogoutOtherBrowser.php b/Livewire/Profile/LogoutOtherBrowser.php new file mode 100644 index 0000000..f8efe49 --- /dev/null +++ b/Livewire/Profile/LogoutOtherBrowser.php @@ -0,0 +1,118 @@ +state = array_merge([ + 'email' => $user->email, + ], $user->withoutRelations()->toArray()); + } + + /** + * Update the user's profile information. + * + * @param \Laravel\Fortify\Contracts\UpdatesUserProfileInformation $updater + * @return \Illuminate\Http\RedirectResponse|null + */ + public function updateProfileInformation(UpdatesUserProfileInformation $updater) + { + $this->resetErrorBag(); + + $updater->update( + Auth::user(), + $this->photo + ? array_merge($this->state, ['photo' => $this->photo]) + : $this->state + ); + + if (isset($this->photo)) { + return redirect()->route('profile.show'); + } + + $this->dispatch('saved'); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Delete user's profile photo. + * + * @return void + */ + public function deleteProfilePhoto() + { + Auth::user()->deleteProfilePhoto(); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Sent the email verification. + * + * @return void + */ + public function sendEmailVerification() + { + Auth::user()->sendEmailVerificationNotification(); + + $this->verificationLinkSent = true; + } + + /** + * Get the current user of the application. + * + * @return mixed + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Render the component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('vuexy-admin::livewire.profile.update-profile-information-form'); + } +} diff --git a/Livewire/Profile/TwoFactorAuthenticationForm.php b/Livewire/Profile/TwoFactorAuthenticationForm.php new file mode 100644 index 0000000..7e9bd10 --- /dev/null +++ b/Livewire/Profile/TwoFactorAuthenticationForm.php @@ -0,0 +1,118 @@ +state = array_merge([ + 'email' => $user->email, + ], $user->withoutRelations()->toArray()); + } + + /** + * Update the user's profile information. + * + * @param \Laravel\Fortify\Contracts\UpdatesUserProfileInformation $updater + * @return \Illuminate\Http\RedirectResponse|null + */ + public function updateProfileInformation(UpdatesUserProfileInformation $updater) + { + $this->resetErrorBag(); + + $updater->update( + Auth::user(), + $this->photo + ? array_merge($this->state, ['photo' => $this->photo]) + : $this->state + ); + + if (isset($this->photo)) { + return redirect()->route('profile.show'); + } + + $this->dispatch('saved'); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Delete user's profile photo. + * + * @return void + */ + public function deleteProfilePhoto() + { + Auth::user()->deleteProfilePhoto(); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Sent the email verification. + * + * @return void + */ + public function sendEmailVerification() + { + Auth::user()->sendEmailVerificationNotification(); + + $this->verificationLinkSent = true; + } + + /** + * Get the current user of the application. + * + * @return mixed + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Render the component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('vuexy-admin::livewire.profile.update-profile-information-form'); + } +} diff --git a/Livewire/Profile/UpdatePasswordForm.php b/Livewire/Profile/UpdatePasswordForm.php new file mode 100644 index 0000000..df3a2ea --- /dev/null +++ b/Livewire/Profile/UpdatePasswordForm.php @@ -0,0 +1,118 @@ +state = array_merge([ + 'email' => $user->email, + ], $user->withoutRelations()->toArray()); + } + + /** + * Update the user's profile information. + * + * @param \Laravel\Fortify\Contracts\UpdatesUserProfileInformation $updater + * @return \Illuminate\Http\RedirectResponse|null + */ + public function updateProfileInformation(UpdatesUserProfileInformation $updater) + { + $this->resetErrorBag(); + + $updater->update( + Auth::user(), + $this->photo + ? array_merge($this->state, ['photo' => $this->photo]) + : $this->state + ); + + if (isset($this->photo)) { + return redirect()->route('profile.show'); + } + + $this->dispatch('saved'); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Delete user's profile photo. + * + * @return void + */ + public function deleteProfilePhoto() + { + Auth::user()->deleteProfilePhoto(); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Sent the email verification. + * + * @return void + */ + public function sendEmailVerification() + { + Auth::user()->sendEmailVerificationNotification(); + + $this->verificationLinkSent = true; + } + + /** + * Get the current user of the application. + * + * @return mixed + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Render the component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('vuexy-admin::livewire.profile.update-profile-information-form'); + } +} diff --git a/Livewire/Profile/UpdateProfileInformationForm.php b/Livewire/Profile/UpdateProfileInformationForm.php new file mode 100644 index 0000000..70c35ae --- /dev/null +++ b/Livewire/Profile/UpdateProfileInformationForm.php @@ -0,0 +1,118 @@ +state = array_merge([ + 'email' => $user->email, + ], $user->withoutRelations()->toArray()); + } + + /** + * Update the user's profile information. + * + * @param \Laravel\Fortify\Contracts\UpdatesUserProfileInformation $updater + * @return \Illuminate\Http\RedirectResponse|null + */ + public function updateProfileInformation(UpdatesUserProfileInformation $updater) + { + $this->resetErrorBag(); + + $updater->update( + Auth::user(), + $this->photo + ? array_merge($this->state, ['photo' => $this->photo]) + : $this->state + ); + + if (isset($this->photo)) { + return redirect()->route('profile.show'); + } + + $this->dispatch('saved'); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Delete user's profile photo. + * + * @return void + */ + public function deleteProfilePhoto() + { + Auth::user()->deleteProfilePhoto(); + + $this->dispatch('refresh-navigation-menu'); + } + + /** + * Sent the email verification. + * + * @return void + */ + public function sendEmailVerification() + { + Auth::user()->sendEmailVerificationNotification(); + + $this->verificationLinkSent = true; + } + + /** + * Get the current user of the application. + * + * @return mixed + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Render the component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('vuexy-admin::livewire.profile.update-profile-information-form'); + } +} diff --git a/Livewire/Roles/RoleIndex.php b/Livewire/Roles/RolesIndex.php similarity index 90% rename from Livewire/Roles/RoleIndex.php rename to Livewire/Roles/RolesIndex.php index 10a168d..8f467a6 100644 --- a/Livewire/Roles/RoleIndex.php +++ b/Livewire/Roles/RolesIndex.php @@ -7,7 +7,7 @@ use Livewire\WithPagination; use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission; -class RoleIndex extends Component +class RolesIndex extends Component { use WithPagination; @@ -54,8 +54,8 @@ class RoleIndex extends Component public function render() { - return view('livewire.roles', [ - 'index' => Role::paginate(10) + return view('vuexy-admin::livewire.roles.index', [ + 'roles' => Role::paginate(10) ]); } } diff --git a/Livewire/Users/UserIndex.copy.php b/Livewire/Users/UserIndex.copy.php deleted file mode 100644 index 9eb2963..0000000 --- a/Livewire/Users/UserIndex.copy.php +++ /dev/null @@ -1,115 +0,0 @@ -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 = ""; - - $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 = ''; - - $this->dispatch('refreshUserCount'); - $this->dispatch('afterDelete'); - } else { - $this->indexAlert = ''; - } - } - - public function render() - { - return view('vuexy-admin::livewire.users.index', [ - 'users' => User::paginate(10), - ]); - } -} diff --git a/Livewire/Users/UserOffCanvasForm.php b/Livewire/Users/UserOffCanvasForm.php index d65c74d..2daa607 100644 --- a/Livewire/Users/UserOffCanvasForm.php +++ b/Livewire/Users/UserOffCanvasForm.php @@ -2,14 +2,8 @@ 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 @@ -22,40 +16,15 @@ use Livewire\WithFileUploads; */ 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. * @@ -85,28 +54,6 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent return User::class; } - /** - * Define los campos del formulario. - * - * @return array - */ - 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. * @@ -117,6 +64,8 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent return 'name'; } + // ===================== VALIDACIONES ===================== + /** * Define reglas de validación dinámicas basadas en el modo actual. * @@ -129,10 +78,8 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent case 'create': case 'edit': return [ - 'code' => ['required', 'string', 'max:16', Rule::unique('contact', 'code')->ignore($this->id)], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], 'name' => ['required', 'string', 'max:96'], - 'notes' => ['nullable', 'string', 'max:1024'], - 'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], ]; case 'delete': @@ -145,8 +92,6 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent } } - // ===================== VALIDACIONES ===================== - /** * Get custom attributes for validator errors. * @@ -168,121 +113,10 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent 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. * diff --git a/Livewire/Users/UserShow.php b/Livewire/Users/UserShow.php index de9cd95..35b6516 100644 --- a/Livewire/Users/UserShow.php +++ b/Livewire/Users/UserShow.php @@ -40,7 +40,6 @@ class UserShow extends Component $is_prospect, $is_customer, $is_provider, - $is_user, $status; public $deleteUserImage; public $cuentaUsuarioAlert, @@ -55,7 +54,6 @@ class UserShow extends Component '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', @@ -102,7 +100,6 @@ class UserShow extends Component $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; @@ -140,7 +137,6 @@ class UserShow extends Component $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; @@ -150,7 +146,6 @@ class UserShow extends Component $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; diff --git a/Livewire/Users/UserCount.php b/Livewire/Users/UsersCount.php similarity index 84% rename from Livewire/Users/UserCount.php rename to Livewire/Users/UsersCount.php index 0e3eb6b..7249ebb 100644 --- a/Livewire/Users/UserCount.php +++ b/Livewire/Users/UsersCount.php @@ -6,11 +6,11 @@ use Koneko\VuexyAdmin\Models\User; use Livewire\Component; -class UserCount extends Component +class UsersCount extends Component { public $total, $enabled, $disabled; - protected $listeners = ['refreshUserCount' => 'updateCounts']; + protected $listeners = ['refreshUsersCount' => 'updateCounts']; public function mount() { diff --git a/Livewire/Users/UserIndex.php b/Livewire/Users/UsersIndex.php similarity index 61% rename from Livewire/Users/UserIndex.php rename to Livewire/Users/UsersIndex.php index 49e0e3f..69f02be 100644 --- a/Livewire/Users/UserIndex.php +++ b/Livewire/Users/UsersIndex.php @@ -2,17 +2,11 @@ namespace Koneko\VuexyAdmin\Livewire\Users; -use Livewire\WithFileUploads; use Koneko\VuexyAdmin\Models\User; use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent; -class UserIndex extends AbstractIndexComponent +class UsersIndex extends AbstractIndexComponent { - use WithFileUploads; - - public $doc_file; - public $dropzoneVisible = true; - /** * Almacena rutas útiles para la funcionalidad de edición o eliminación. */ @@ -32,32 +26,14 @@ class UserIndex extends AbstractIndexComponent 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', + 'action' => 'Acciones', + 'full_name' => 'Nombre completo', + 'email' => 'Correo electrónico', + 'email_verified_at' => 'Correo verificado', + 'created_by' => 'Creado Por', + 'status' => 'Estatus', + 'created_at' => 'Fecha Creación', + 'updated_at' => 'Última Modificación', ]; } @@ -86,14 +62,27 @@ class UserIndex extends AbstractIndexComponent 'formatter' => 'emailFormatter', 'visible' => false, ], - 'parent_name' => [ - 'formatter' => 'contactParentFormatter', + 'email_verified_at' => [ 'visible' => false, ], - 'agent_name' => [ - 'formatter' => 'agentFormatter', + 'parent_id' => [ + 'formatter' => 'parentProfileFormatter', 'visible' => false, ], + 'agent_id' => [ + 'formatter' => 'agentProfileFormatter', + 'visible' => false, + ], + 'phone' => [ + 'formatter' => 'telFormatter', + 'visible' => false, + ], + 'mobile' => [ + 'formatter' => 'telFormatter', + ], + 'whatsapp' => [ + 'formatter' => 'whatsappFormatter', + ], 'company' => [ 'formatter' => 'textNowrapFormatter', ], @@ -103,10 +92,16 @@ class UserIndex extends AbstractIndexComponent 'nss' => [ 'visible' => false, ], - 'job_title' => [ + 'license_number' => [ + 'visible' => false, + ], + 'job_position' => [ 'formatter' => 'textNowrapFormatter', 'visible' => false, ], + 'pais' => [ + 'visible' => false, + ], 'rfc' => [ 'visible' => false, ], @@ -114,6 +109,7 @@ class UserIndex extends AbstractIndexComponent 'formatter' => 'textNowrapFormatter', 'visible' => false, ], + 'domicilio_fiscal' => [ 'visible' => false, ], @@ -176,14 +172,14 @@ class UserIndex extends AbstractIndexComponent ], 'align' => 'center', ], - 'is_provider' => [ + 'is_supplier' => [ 'formatter' => [ 'name' => 'dynamicBooleanFormatter', 'params' => ['tag' => 'checkSI'], ], 'align' => 'center', ], - 'is_user' => [ + 'is_carrier' => [ 'formatter' => [ 'name' => 'dynamicBooleanFormatter', 'params' => ['tag' => 'checkSI'], @@ -211,69 +207,6 @@ class UserIndex extends AbstractIndexComponent ]; } - /** - * 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. */ diff --git a/Livewire/VuexyAdmin/AppDescriptionSettings.php b/Livewire/VuexyAdmin/AppDescriptionSettings.php new file mode 100644 index 0000000..0e1ace8 --- /dev/null +++ b/Livewire/VuexyAdmin/AppDescriptionSettings.php @@ -0,0 +1,66 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'app_name' => 'required|string|max:255', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:255', + ]); + + // Guardar título del sitio en configuraciones + $SettingsService = app(SettingsService::class); + + $SettingsService->set('admin.app_name', $this->app_name, null, 'vuexy-admin'); + $SettingsService->set('admin.title', $this->title, null, 'vuexy-admin'); + $SettingsService->set('admin.description', $this->description, null, 'vuexy-admin'); + + // Limpiar cache de plantilla + app(AdminTemplateService::class)->clearAdminVarsCache(); + + // Recargamos el formulario + $this->resetForm(); + + // Notificación de éxito + $this->dispatch( + 'notification', + target: $this->targetNotify, + type: 'success', + message: 'Se han guardado los cambios en las configuraciones.' + ); + } + + public function resetForm() + { + // Obtener los valores de las configuraciones de la base de datos + $settings = app(AdminTemplateService::class)->getAdminVars(); + + $this->app_name = $settings['app_name']; + $this->title = $settings['title']; + $this->description = $settings['description']; + } + + public function render() + { + return view('vuexy-admin::livewire.vuexy.app-description-settings'); + } +} diff --git a/Livewire/AdminSettings/GeneralSettings.php b/Livewire/VuexyAdmin/AppFaviconSettings.php similarity index 52% rename from Livewire/AdminSettings/GeneralSettings.php rename to Livewire/VuexyAdmin/AppFaviconSettings.php index e1a1cf1..2916423 100644 --- a/Livewire/AdminSettings/GeneralSettings.php +++ b/Livewire/VuexyAdmin/AppFaviconSettings.php @@ -1,19 +1,18 @@ 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']; + $this->resetForm(); } public function save() { $this->validate([ - 'admin_title' => 'required|string|max:255', - 'upload_image_favicon' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480', + 'upload_image_favicon' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480', ]); - $adminSettingsService = app(AdminSettingsService::class); + // Procesar favicon + app(AdminSettingsService::class)->processAndSaveFavicon($this->upload_image_favicon); - // Guardar título del sitio en configuraciones - $adminSettingsService->updateSetting('admin_title', $this->admin_title); + // Limpiar cache de plantilla + app(AdminTemplateService::class)->clearAdminVarsCache(); - // Procesar favicon si se ha cargado una imagen - if ($this->upload_image_favicon) { - $adminSettingsService->processAndSaveFavicon($this->upload_image_favicon); - } - - $this->loadSettings(true); + // Recargamos el formulario + $this->resetForm(); + // Notificación de éxito $this->dispatch( 'notification', target: $this->targetNotify, @@ -77,8 +51,22 @@ class GeneralSettings extends Component ); } + public function resetForm() + { + // Obtener los valores de las configuraciones de la base de datos + $settings = app(AdminTemplateService::class)->getAdminVars(); + + $this->upload_image_favicon = null; + $this->admin_favicon_16x16 = $settings['favicon']['16x16']; + $this->admin_favicon_76x76 = $settings['favicon']['76x76']; + $this->admin_favicon_120x120 = $settings['favicon']['120x120']; + $this->admin_favicon_152x152 = $settings['favicon']['152x152']; + $this->admin_favicon_180x180 = $settings['favicon']['180x180']; + $this->admin_favicon_192x192 = $settings['favicon']['192x192']; + } + public function render() { - return view('vuexy-admin::livewire.admin-settings.general-settings'); + return view('vuexy-admin::livewire.vuexy.app-favicon-settings'); } } diff --git a/Livewire/VuexyAdmin/GlobalSettingOffCanvasForm.php b/Livewire/VuexyAdmin/GlobalSettingOffCanvasForm.php new file mode 100644 index 0000000..b7ada18 --- /dev/null +++ b/Livewire/VuexyAdmin/GlobalSettingOffCanvasForm.php @@ -0,0 +1,110 @@ + 'boolean', + 'value_integer' => 'integer', + 'value_float' => 'float', + ]; + + protected $listeners = [ + 'editGlobalSetting' => 'loadFormModel', + 'confirmDeletionGlobalSetting' => 'loadFormModelForDeletion', + ]; + + protected function model(): string + { + return Setting::class; + } + + protected function fields(): array + { + return [ + 'key', 'category', 'user_id', + 'value_string', 'value_integer', 'value_boolean', + 'value_float', 'value_text' + ]; + } + + protected function defaults(): array + { + return [ + 'category' => 'general', + ]; + } + + protected function focusOnOpen(): string + { + return 'key'; + } + + protected function dynamicRules(string $mode): array + { + if ($mode === 'delete') { + return ['confirmDeletion' => 'accepted']; + } + + $uniqueRule = Rule::unique('settings', 'key') + ->where(fn ($q) => $q + ->where('user_id', $this->user_id) + ->where('category', $this->category) + ); + + if ($mode === 'edit') { + $uniqueRule = $uniqueRule->ignore($this->id); + } + + return [ + 'key' => ['required', 'string', $uniqueRule], + 'category' => ['nullable', 'string', 'max:96'], + 'user_id' => ['nullable', 'integer', 'exists:users,id'], + 'value_string' => ['nullable', 'string', 'max:255'], + 'value_integer' => ['nullable', 'integer'], + 'value_boolean' => ['nullable', 'boolean'], + 'value_float' => ['nullable', 'numeric'], + 'value_text' => ['nullable', 'string'], + ]; + } + + protected function attributes(): array + { + return [ + 'key' => 'clave de configuración', + 'category' => 'categoría', + ]; + } + + protected function messages(): array + { + return [ + 'key.required' => 'La clave del parámetro es obligatoria.', + 'key.unique' => 'Ya existe una configuración con esta clave en esa categoría.', + ]; + } + + protected function viewPath(): string + { + return 'vuexy-admin::livewire.global-settings.offcanvas-form'; + } +} diff --git a/Livewire/VuexyAdmin/GlobalSettingsIndex.php b/Livewire/VuexyAdmin/GlobalSettingsIndex.php new file mode 100644 index 0000000..7de58ae --- /dev/null +++ b/Livewire/VuexyAdmin/GlobalSettingsIndex.php @@ -0,0 +1,103 @@ + 'Acciones', + 'key' => 'Clave', + 'category' => 'Categoría', + 'user_fullname' => 'Usuario', + 'created_at' => 'Creado', + ]; + } + + /** + * Retorna el formato (formatter) para cada columna. + * Se aplican formatters para resaltar la información y se establecen propiedades de alineación y visibilidad. + * + * @return array + */ + protected function format(): array + { + return [ + 'action' => [ + 'formatter' => 'settingActionFormatter', + 'onlyFormatter' => true, + ], + 'key' => [ + 'formatter' => [ + 'name' => 'dynamicBadgeFormatter', + 'params' => ['color' => 'primary'], + ], + 'align' => 'center', + 'switchable' => false, + ], + 'category' => [ + 'switchable' => false, + ], + 'user_fullname' => [ + 'switchable' => false, + ], + 'created_at' => [ + 'formatter' => 'whitespaceNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + ]; + } + + /** + * Sobrescribe la configuración base de la tabla para ajustar + * la vista y funcionalidades específicas del catálogo. + * + * @return array + */ + protected function bootstraptableConfig(): array + { + return array_merge(parent::bootstraptableConfig(), [ + 'sortName' => 'key', + 'exportFileName' => 'Configuración', + 'showFullscreen' => false, + 'showPaginationSwitch' => false, + 'showRefresh' => false, + 'pagination' => false, + ]); + } + + /** + * Retorna la vista a renderizar para este componente. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-admin::livewire.global-settings.index'; + } +} diff --git a/Livewire/VuexyAdmin/LogoOnDarkBgSettings.php b/Livewire/VuexyAdmin/LogoOnDarkBgSettings.php new file mode 100644 index 0000000..e669551 --- /dev/null +++ b/Livewire/VuexyAdmin/LogoOnDarkBgSettings.php @@ -0,0 +1,61 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'upload_image_logo_dark' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480', + ]); + + // Procesar favicon si se ha cargado una imagen + app(AdminSettingsService::class)->processAndSaveImageLogo($this->upload_image_logo_dark, 'dark'); + + // Limpiar cache de plantilla + app(AdminTemplateService::class)->clearAdminVarsCache(); + + // Recargamos el formulario + $this->resetForm(); + + // Notificación de éxito + $this->dispatch( + 'notification', + target: $this->targetNotify, + type: 'success', + message: 'Se han guardado los cambios en las configuraciones.' + ); + } + + public function resetForm() + { + // Obtener los valores de las configuraciones de la base de datos + $settings = app(AdminTemplateService::class)->getAdminVars(); + + $this->upload_image_logo_dark = null; + $this->admin_image_logo_dark = $settings['image_logo']['large_dark']; + } + + public function render() + { + return view('vuexy-admin::livewire.vuexy.logo-on-dark-bg-settings'); + } +} diff --git a/Livewire/VuexyAdmin/LogoOnLightBgSettings.php b/Livewire/VuexyAdmin/LogoOnLightBgSettings.php new file mode 100644 index 0000000..aaabc9b --- /dev/null +++ b/Livewire/VuexyAdmin/LogoOnLightBgSettings.php @@ -0,0 +1,61 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'upload_image_logo' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480', + ]); + + // Procesar favicon si se ha cargado una imagen + app(AdminSettingsService::class)->processAndSaveImageLogo($this->upload_image_logo); + + // Limpiar cache de plantilla + app(AdminTemplateService::class)->clearAdminVarsCache(); + + // Recargamos el formulario + $this->resetForm(); + + // Notificación de éxito + $this->dispatch( + 'notification', + target: $this->targetNotify, + type: 'success', + message: 'Se han guardado los cambios en las configuraciones.' + ); + } + + public function resetForm() + { + // Obtener los valores de las configuraciones de la base de datos + $settings = app(AdminTemplateService::class)->getAdminVars(); + + $this->upload_image_logo = null; + $this->admin_image_logo = $settings['image_logo']['large']; + } + + public function render() + { + return view('vuexy-admin::livewire.vuexy.logo-on-light-bg-settings'); + } +} diff --git a/Livewire/VuexyAdmin/QuickAccessWidget.php b/Livewire/VuexyAdmin/QuickAccessWidget.php new file mode 100644 index 0000000..db5003e --- /dev/null +++ b/Livewire/VuexyAdmin/QuickAccessWidget.php @@ -0,0 +1,87 @@ +quickAccessItems = $this->processMenu($menuConfig); + } + + private function processMenu(array $menu): array + { + $user = Auth::user(); + $accessItems = []; + + foreach ($menu as $section => $items) { + if (!isset($items['submenu']) || !is_array($items['submenu'])) { + continue; + } + + $categoryData = [ + 'title' => $section, + 'icon' => $items['icon'] ?? 'ti ti-folder', + 'description' => $items['description'] ?? '', + 'submenu' => [] + ]; + + $this->processSubmenu($items['submenu'], $categoryData['submenu'], $user); + + if (!empty($categoryData['submenu'])) { + $accessItems[] = $categoryData; + } + } + + return $accessItems; + } + + + + private function processSubmenu(array $submenu, array &$categorySubmenu, $user) + { + foreach ($submenu as $title => $item) { + // Si el elemento NO tiene 'route' ni 'url' y SOLO contiene un submenu, no lo mostramos como acceso directo + if (!isset($item['route']) && !isset($item['url']) && isset($item['submenu'])) { + // Procesamos los submenús de este elemento sin agregarlo directamente a la lista + $this->processSubmenu($item['submenu'], $categorySubmenu, $user); + continue; + } + + // Validar si el usuario tiene permiso + $can = $item['can'] ?? null; + if (!$can || $user->can($can)) { + // Si tiene ruta y existe en Laravel, usarla; si no, usar url, y si tampoco hay, usar 'javascript:;' + $routeExists = isset($item['route']) && Route::has($item['route']); + $url = $routeExists ? route($item['route']) : ($item['url'] ?? 'javascript:;'); + + // Agregar elemento al submenu si tiene un destino válido + $categorySubmenu[] = [ + 'title' => $title, + 'icon' => $item['icon'] ?? 'ti ti-circle', + 'url' => $url, + ]; + } + + // Si el elemento tiene un submenu, también lo procesamos + if (isset($item['submenu']) && is_array($item['submenu'])) { + $this->processSubmenu($item['submenu'], $categorySubmenu, $user); + } + } + } + + + public function render() + { + return view('vuexy-admin::livewire.vuexy.quick-access-widget', [ + 'quickAccessItems' => $this->quickAccessItems, + ]); + } +} diff --git a/Livewire/AdminSettings/MailSmtpSettings.php b/Livewire/VuexyAdmin/SendmailSettings.php similarity index 92% rename from Livewire/AdminSettings/MailSmtpSettings.php rename to Livewire/VuexyAdmin/SendmailSettings.php index 3ddc256..d83e85a 100644 --- a/Livewire/AdminSettings/MailSmtpSettings.php +++ b/Livewire/VuexyAdmin/SendmailSettings.php @@ -1,6 +1,6 @@ getMailSystemConfig(); + $settings = app(GlobalSettingsService::class)->getMailSystemConfig(); $this->change_smtp_settings = false; $this->save_button_disabled = true; @@ -170,6 +167,6 @@ class MailSmtpSettings extends Component public function render() { - return view('vuexy-admin::livewire.admin-settings.mail-smtp-settings'); + return view('vuexy-admin::livewire.vuexy.sendmail-settings'); } } diff --git a/Livewire/VuexyAdmin/VuexyInterfaceSettings.php b/Livewire/VuexyAdmin/VuexyInterfaceSettings.php new file mode 100644 index 0000000..237c7e5 --- /dev/null +++ b/Livewire/VuexyAdmin/VuexyInterfaceSettings.php @@ -0,0 +1,121 @@ +uniqueId = uniqid(); + + $this->resetForm(); + } + + + public function save() + { + $this->validate([ + 'vuexy_maxQuickLinks' => 'required|integer|min:2|max:20', + ]); + + // Guardar configuraciones utilizando SettingsService + $SettingsService = app(SettingsService::class); + + $SettingsService->set('config.vuexy.custom.myLayout', $this->vuexy_myLayout, null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.myTheme', $this->vuexy_myTheme, null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.myStyle', $this->vuexy_myStyle, null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.hasCustomizer', $this->vuexy_hasCustomizer, null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.displayCustomizer', ($this->vuexy_hasCustomizer? $this->vuexy_displayCustomizer: false), null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.contentLayout', $this->vuexy_contentLayout, null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.navbarType', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_navbarType: null), null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.footerFixed', $this->vuexy_footerFixed, null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.menuFixed', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_menuFixed: null), null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.menuCollapsed', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_menuCollapsed: null), null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.headerType', ($this->vuexy_myLayout == 'horizontal' ? $this->vuexy_headerType: null), null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.showDropdownOnHover', ($this->vuexy_myLayout == 'horizontal' ? $this->vuexy_showDropdownOnHover: null), null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.authViewMode', $this->vuexy_authViewMode, null, 'vuexy-admin'); + $SettingsService->set('config.vuexy.custom.maxQuickLinks', $this->vuexy_maxQuickLinks, null, 'vuexy-admin'); + + // Elimina la Cache de Configuraciones + app(GlobalSettingsService::class)->clearSystemConfigCache(); + + // Refrescar el componente actual + $this->dispatch('clearLocalStoregeTemplateCustomizer'); + + // Notificación de éxito + $this->dispatch( + 'notification', + target: $this->targetNotify, + type: 'success', + message: 'Se han guardado los cambios en las configuraciones.', + deferReload: true + ); + } + + public function clearCustomConfig() + { + // Elimina las claves config.vuexy.* para cargar los valores por defecto + app(GlobalSettingsService::class)->clearVuexyConfig(); + + // Refrescar el componente actual + $this->dispatch('clearLocalStoregeTemplateCustomizer'); + + $this->dispatch( + 'notification', + target: $this->targetNotify, + type: 'success', + message: 'Se han guardado los cambios en las configuraciones.', + deferReload: true + ); + } + + public function resetForm() + { + // Obtener los valores de las configuraciones de la base de datos + $settings = app(AdminTemplateService::class)->getVuexyCustomizerVars(); + + $this->vuexy_myLayout = $settings['myLayout']; + $this->vuexy_myTheme = $settings['myTheme']; + $this->vuexy_myStyle = $settings['myStyle']; + $this->vuexy_hasCustomizer = $settings['hasCustomizer']; + $this->vuexy_displayCustomizer = $settings['displayCustomizer']; + $this->vuexy_contentLayout = $settings['contentLayout']; + $this->vuexy_navbarType = $settings['navbarType']; + $this->vuexy_footerFixed = $settings['footerFixed']; + $this->vuexy_menuFixed = $settings['menuFixed']; + $this->vuexy_menuCollapsed = $settings['menuCollapsed']; + $this->vuexy_headerType = $settings['headerType']; + $this->vuexy_showDropdownOnHover = $settings['showDropdownOnHover']; + $this->vuexy_authViewMode = $settings['authViewMode']; + $this->vuexy_maxQuickLinks = $settings['maxQuickLinks']; + } + + public function render() + { + return view('vuexy-admin::livewire.vuexy.interface-settings'); + } +} diff --git a/Models/MediaItem.php b/Models/MediaItem.php deleted file mode 100644 index ad44908..0000000 --- a/Models/MediaItem.php +++ /dev/null @@ -1,62 +0,0 @@ - 'Card', - self::TYPE_BANNER => 'Banner', - self::TYPE_COVER => 'Cover', - self::TYPE_GALLERY => 'Gallery', - self::TYPE_BANNER_HOME => 'Banner Home', - self::TYPE_CARD2 => 'Card 2', - self::TYPE_BANNER2 => 'Banner 2', - self::TYPE_COVER2 => 'Cover 2', - ]; - - /** - * Get the parent imageable model (user or post). - */ - public function imageable() - { - return $this->morphTo(); - } -} diff --git a/Models/Setting.php b/Models/Setting.php index 7300adf..2b61c4c 100644 --- a/Models/Setting.php +++ b/Models/Setting.php @@ -3,37 +3,91 @@ namespace Koneko\VuexyAdmin\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class Setting extends Model { - /** - * The attributes that are mass assignable. - * - * @var array - */ + use HasFactory; + + // ───────────────────────────────────────────── + // Configuración del modelo + // ───────────────────────────────────────────── + + protected $table = 'settings'; + protected $fillable = [ 'key', - 'value', + 'category', 'user_id', + 'value_string', + 'value_integer', + 'value_boolean', + 'value_float', + 'value_text', + 'value_binary', + 'mime_type', + 'file_name', + 'updated_by', ]; - public $timestamps = false; + protected $casts = [ + 'user_id' => 'integer', + 'value_integer' => 'integer', + 'value_boolean' => 'boolean', + 'value_float' => 'float', + 'updated_by' => 'integer', + ]; - // Relación con el usuario - public function user() + // ───────────────────────────────────────────── + // Metadatos personalizados para el generador de componentes + // ───────────────────────────────────────────── + + public string $tagName = 'setting'; + public string $columnNameLabel = 'key'; + public string $singularName = 'Configuración'; + public string $pluralName = 'Configuraciones'; + + // ───────────────────────────────────────────── + // Relaciones + // ───────────────────────────────────────────── + + public function user(): BelongsTo { return $this->belongsTo(User::class); } - // Scope para obtener configuraciones de un usuario específico - public function scopeForUser($query, $userId) + public function updatedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + // ───────────────────────────────────────────── + // Scopes + // ───────────────────────────────────────────── + + /** + * Configuraciones para un usuario específico. + */ + public function scopeForUser($query, int $userId) { return $query->where('user_id', $userId); } - // Configuraciones globales (sin usuario) + /** + * Configuraciones globales (sin usuario). + */ public function scopeGlobal($query) { return $query->whereNull('user_id'); } + + /** + * Incluir columna virtual `value` en la consulta. + */ + public function scopeWithVirtualValue($query) + { + return $query->select(['key', 'value']); + } + } diff --git a/Models/User copy.php b/Models/User copy.php deleted file mode 100644 index cb20a95..0000000 --- a/Models/User copy.php +++ /dev/null @@ -1,377 +0,0 @@ - 'Habilitado', - self::STATUS_DISABLED => 'Deshabilitado', - self::STATUS_REMOVED => 'Eliminado', - ]; - - /** - * List of names for each status. - * @var array - */ - public static $statusListClass = [ - self::STATUS_ENABLED => 'success', - self::STATUS_DISABLED => 'warning', - self::STATUS_REMOVED => 'danger', - ]; - - /** - * The attributes that are mass assignable. - * - * @var array - */ - protected $fillable = [ - 'name', - 'last_name', - 'email', - 'password', - 'profile_photo_path', - 'status', - 'created_by', - ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var array - */ - protected $hidden = [ - 'password', - 'remember_token', - 'two_factor_recovery_codes', - 'two_factor_secret', - ]; - - /** - * The accessors to append to the model's array form. - * - * @var array - */ - protected $appends = [ - 'profile_photo_url', - ]; - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } - - /** - * Attributes to include in the Audit. - * - * @var array - */ - protected $auditInclude = [ - 'name', - 'email', - ]; - - public function updateProfilePhoto(UploadedFile $image_avatar) - { - try { - // Verificar si el archivo existe - if (!file_exists($image_avatar->getRealPath())) - throw new \Exception('El archivo no existe en la ruta especificada.'); - - if (!in_array($image_avatar->getClientOriginalExtension(), ['jpg', 'jpeg', 'png'])) - throw new \Exception('El formato del archivo debe ser JPG o PNG.'); - - // Directorio donde se guardarán los avatares - $avatarDisk = self::AVATAR_DISK; - $avatarPath = self::PROFILE_PHOTO_DIR; - $avatarName = uniqid('avatar_') . '.png'; // Nombre único para el avatar - - // Crear la instancia de ImageManager - $driver = config('image.driver', 'gd'); - $manager = new ImageManager($driver); - - // Crear el directorio si no existe - if (!Storage::disk($avatarDisk)->exists($avatarPath)) - Storage::disk($avatarDisk)->makeDirectory($avatarPath); - - // Leer la imagen - $image = $manager->read($image_avatar->getRealPath()); - - // crop the best fitting 5:3 (600x360) ratio and resize to 600x360 pixel - $image->cover(self::AVATAR_WIDTH, self::AVATAR_HEIGHT); - - // Guardar la imagen en el disco de almacenamiento gestionado por Laravel - Storage::disk($avatarDisk)->put($avatarPath . '/' . $avatarName, $image->toPng(indexed: true)); - - // Elimina el avatar existente si hay uno - $this->deleteProfilePhoto(); - - // Update the user's profile photo path - $this->forceFill([ - 'profile_photo_path' => $avatarName, - ])->save(); - } catch (\Exception $e) { - throw new \Exception('Ocurrió un error al actualizar el avatar. ' . $e->getMessage()); - } - } - - public function deleteProfilePhoto() - { - if (!empty($this->profile_photo_path)) { - $avatarDisk = self::AVATAR_DISK; - - Storage::disk($avatarDisk)->delete($this->profile_photo_path); - - $this->forceFill([ - 'profile_photo_path' => null, - ])->save(); - } - } - - public function getAvatarColor() - { - // Selecciona un color basado en el id del usuario - return self::AVATAR_COLORS[$this->id % count(self::AVATAR_COLORS)]; - } - - public static function getAvatarImage($name, $color, $background, $size) - { - $avatarDisk = self::AVATAR_DISK; - $directory = self::INITIAL_AVATAR_DIR; - $initials = self::getInitials($name); - - $cacheKey = "avatar-{$initials}-{$color}-{$background}-{$size}"; - $path = "{$directory}/{$cacheKey}.png"; - $storagePath = storage_path("app/public/{$path}"); - - // Verificar si el avatar ya está en caché - if (Storage::disk($avatarDisk)->exists($path)) - return response()->file($storagePath); - - // Crear el avatar - $image = self::createAvatarImage($name, $color, $background, $size); - - // Guardar en el directorio de iniciales - Storage::disk($avatarDisk)->put($path, $image->toPng(indexed: true)); - - // Retornar la imagen directamente - return response()->file($storagePath); - } - - private static function createAvatarImage($name, $color, $background, $size) - { - // Usar la configuración del driver de imagen - $driver = config('image.driver', 'gd'); - $manager = new ImageManager($driver); - - $initials = self::getInitials($name); - - // Obtener la ruta correcta de la fuente dentro del paquete - $fontPath = __DIR__ . '/../storage/fonts/OpenSans-Bold.ttf'; - - // Crear la imagen con fondo - $image = $manager->create($size, $size) - ->fill($background); - - // Escribir texto en la imagen - $image->text( - $initials, - $size / 2, // Centrar horizontalmente - $size / 2, // Centrar verticalmente - function (FontFactory $font) use ($color, $size, $fontPath) { - $font->file($fontPath); - $font->size($size * 0.4); - $font->color($color); - $font->align('center'); - $font->valign('middle'); - } - ); - - return $image; - } - - public static function getInitials($name) - { - // Manejar casos de nombres vacíos o nulos - if (empty($name)) - return 'NA'; - - // Usar array_map para mayor eficiencia - $initials = implode('', array_map(function ($word) { - return mb_substr($word, 0, 1); - }, explode(' ', $name))); - - $initials = substr($initials, 0, self::INITIAL_MAX_LENGTH); - - return strtoupper($initials); - } - - public function getProfilePhotoUrlAttribute() - { - if ($this->profile_photo_path) - return Storage::url(self::PROFILE_PHOTO_DIR . '/' . $this->profile_photo_path); - - // Generar URL del avatar por iniciales - $name = urlencode($this->fullname); - $color = ltrim($this->getAvatarColor(), '#'); - $background = ltrim(self::AVATAR_BACKGROUND, '#'); - $size = (self::AVATAR_WIDTH + self::AVATAR_HEIGHT) / 2; - - return url("/admin/usuario/avatar?name={$name}&color={$color}&background={$background}&size={$size}"); - } - - public function getFullnameAttribute() - { - return trim($this->name . ' ' . $this->last_name); - } - - public function getInitialsAttribute() - { - return self::getInitials($this->fullname); - } - - /** - * Envía la notificación de restablecimiento de contraseña. - * - * @param string $token - */ - public function sendPasswordResetNotification($token) - { - // Usar la notificación personalizada - $this->notify(new CustomResetPasswordNotification($token)); - } - - - /** - * Obtener usuarios activos con una excepción para incluir un usuario específico desactivado. - * - * @param array $filters Filtros opcionales como ['type' => 'user', 'status' => 1] - * @param int|null $includeUserId ID de usuario específico a incluir aunque esté inactivo - * @return array - */ - public static function getUsersListWithInactive(int $includeUserId = null, array $filters = []): array - { - $query = self::query(); - - // Filtro por tipo de usuario - if (isset($filters['type'])) { - switch ($filters['type']) { - case 'partner': - $query->where('is_partner', 1); - break; - case 'employee': - $query->where('is_employee', 1); - break; - case 'prospect': - $query->where('is_prospect', 1); - break; - case 'customer': - $query->where('is_customer', 1); - break; - case 'provider': - $query->where('is_provider', 1); - break; - case 'user': - $query->where('is_user', 1); - break; - } - } - - // Incluir usuarios activos o el usuario desactivado seleccionado - $query->where(function ($q) use ($filters, $includeUserId) { - if (isset($filters['status'])) { - $q->where('status', $filters['status']); - } - - if ($includeUserId) { - $q->orWhere('id', $includeUserId); - } - }); - - // Formatear los datos como id => "Nombre Apellido" - return $query->pluck(\DB::raw("CONCAT(name, ' ', IFNULL(last_name, ''))"), 'id')->toArray(); - } - - - /** - * Relations - */ - - // User who created this user - public function creator() - { - return $this->belongsTo(self::class, 'created_by'); - } - - public function isActive() - { - return $this->status === self::STATUS_ENABLED; - } - -} diff --git a/Models/User.php b/Models/User.php index b1644d8..1ef63ed 100644 --- a/Models/User.php +++ b/Models/User.php @@ -23,23 +23,7 @@ class User extends Authenticatable implements MustVerifyEmail, AuditableContract const STATUS_DISABLED = 1; const STATUS_REMOVED = 0; - const AVATAR_DISK = 'public'; - const PROFILE_PHOTO_DIR = 'profile-photos'; - const INITIAL_AVATAR_DIR = 'initial-avatars'; - const INITIAL_MAX_LENGTH = 4; - - const AVATAR_WIDTH = 512; - const AVATAR_HEIGHT = 512; - const AVATAR_BACKGROUND = '#EBF4FF'; // Fondo por defecto - const AVATAR_COLORS = [ - '#7367f0', - '#808390', - '#28c76f', - '#ff4c51', - '#ff9f43', - '#00bad1', - '#4b4b4b', - ]; + const INITIAL_MAX_LENGTH = 3; /** * List of names for each status. @@ -148,6 +132,50 @@ class User extends Authenticatable implements MustVerifyEmail, AuditableContract 'email', ]; + /** + * Get the URL for the user's profile photo. + * + * @return string + */ + public function getProfilePhotoUrlAttribute() + { + if ($this->profile_photo_path) { + return asset('storage/profile-photos/' . $this->profile_photo_path); + } + + return $this->defaultProfilePhotoUrl(); + } + + /** + * Get the default profile photo URL if no profile photo has been uploaded. + * + * @return string + */ + protected function defaultProfilePhotoUrl() + { + return route('admin.core.user-profile.avatar', ['name' => $this->fullname]); + } + + /** + * Calcula las iniciales a partir del nombre. + * + * @param string $name Nombre completo. + * + * @return string Iniciales en mayúsculas. + */ + public static function getInitials($name) + { + if (empty($name)) { + return 'NA'; + } + + $initials = implode('', array_map(function ($word) { + return mb_substr($word, 0, 1); + }, explode(' ', $name))); + + return strtoupper(substr($initials, 0, self::INITIAL_MAX_LENGTH)); + } + /** * Get the full name of the user. * @@ -179,45 +207,6 @@ class User extends Authenticatable implements MustVerifyEmail, AuditableContract $this->notify(new CustomResetPasswordNotification($token)); } - /** - * Obtener usuarios activos con una excepción para incluir un usuario específico desactivado. - * - * @param array $filters Filtros opcionales como ['type' => 'user', 'status' => 1] - * @param int|null $includeUserId ID de usuario específico a incluir aunque esté inactivo - * @return array - */ - public static function getUsersListWithInactive($includeUserId = null, array $filters = []): array - { - $query = self::query(); - - // Filtro por tipo de usuario dinámico - $tipoUsuarios = [ - 'partner' => 'is_partner', - 'employee' => 'is_employee', - 'prospect' => 'is_prospect', - 'customer' => 'is_customer', - 'provider' => 'is_provider', - 'user' => 'is_user', - ]; - - if (isset($filters['type']) && isset($tipoUsuarios[$filters['type']])) { - $query->where($tipoUsuarios[$filters['type']], 1); - } - - // Filtrar por estado o incluir usuario inactivo - $query->where(function ($q) use ($filters, $includeUserId) { - if (isset($filters['status'])) { - $q->where('status', $filters['status']); - } - - if ($includeUserId) { - $q->orWhere('id', $includeUserId); - } - }); - - return $query->pluck(\DB::raw("CONCAT(name, ' ', IFNULL(last_name, ''))"), 'id')->toArray(); - } - /** * User who created this user */ @@ -233,5 +222,4 @@ class User extends Authenticatable implements MustVerifyEmail, AuditableContract { return $this->status === self::STATUS_ENABLED; } - } diff --git a/Notifications/CustomResetPasswordNotification.php b/Notifications/CustomResetPasswordNotification.php index a6e70cc..c58c1c6 100644 --- a/Notifications/CustomResetPasswordNotification.php +++ b/Notifications/CustomResetPasswordNotification.php @@ -47,7 +47,7 @@ class CustomResetPasswordNotification extends Notification 'email' => $notifiable->getEmailForPasswordReset() ], false)); - $appTitle = Setting::global()->where('key', 'website_title')->first()->value ?? Config::get('koneko.appTitle'); + $appTitle = Setting::withVirtualValue()->where('key', 'website_title')->first()->value ?? Config::get('koneko.appTitle'); $imageBase64 = 'data:image/png;base64,' . base64_encode(file_get_contents(public_path('/assets/img/logo/koneko-04.png'))); $expireMinutes = Config::get('auth.passwords.' . Config::get('auth.defaults.passwords') . '.expire', 60); @@ -90,6 +90,7 @@ class CustomResetPasswordNotification extends Notification { try { $smtpConfig = Setting::where('key', 'LIKE', 'mail_%') + ->withVirtualValue() ->pluck('value', 'key'); if ($smtpConfig->isEmpty()) { diff --git a/Providers/ConfigServiceProvider.php b/Providers/ConfigServiceProvider.php index d165392..de278bf 100644 --- a/Providers/ConfigServiceProvider.php +++ b/Providers/ConfigServiceProvider.php @@ -21,10 +21,6 @@ class ConfigServiceProvider extends ServiceProvider */ public function boot(): void { - // Cargar configuración del sistema - $globalSettingsService = app(GlobalSettingsService::class); - $globalSettingsService->loadSystemConfig(); - // Cargar configuración del sistema a través del servicio app(GlobalSettingsService::class)->loadSystemConfig(); } diff --git a/Providers/VuexyAdminServiceProvider.php b/Providers/VuexyAdminServiceProvider.php index 993409a..9bbdfbd 100644 --- a/Providers/VuexyAdminServiceProvider.php +++ b/Providers/VuexyAdminServiceProvider.php @@ -2,20 +2,28 @@ namespace Koneko\VuexyAdmin\Providers; -use Koneko\VuexyAdmin\Http\Middleware\AdminTemplateMiddleware; -use Koneko\VuexyAdmin\Listeners\{ClearUserCache,HandleUserLogin}; -use Koneko\VuexyAdmin\Livewire\Users\{UserIndex,UserShow,UserForm,UserOffCanvasForm}; -use Koneko\VuexyAdmin\Livewire\Roles\RoleIndex; -use Koneko\VuexyAdmin\Livewire\Permissions\PermissionIndex; -use Koneko\VuexyAdmin\Livewire\Cache\{CacheFunctions,CacheStats,SessionStats,MemcachedStats,RedisStats}; -use Koneko\VuexyAdmin\Livewire\AdminSettings\{ApplicationSettings,GeneralSettings,InterfaceSettings,MailSmtpSettings,MailSenderResponseSettings}; use Koneko\VuexyAdmin\Console\Commands\CleanInitialAvatars; use Koneko\VuexyAdmin\Helpers\VuexyHelper; -use Koneko\VuexyAdmin\Models\User; +use Koneko\VuexyAdmin\Http\Middleware\AdminTemplateMiddleware; +use Illuminate\Auth\Events\{Login,Logout}; +use Illuminate\Foundation\AliasLoader; use Illuminate\Support\Facades\{URL,Event,Blade}; use Illuminate\Support\ServiceProvider; -use Illuminate\Foundation\AliasLoader; -use Illuminate\Auth\Events\{Login,Logout}; +use Koneko\VuexyAdmin\Listeners\{ClearUserCache,HandleUserLogin}; + +use Koneko\VuexyAdmin\Livewire\Cache\{CacheFunctions,CacheStats,SessionStats,MemcachedStats,RedisStats}; +use Koneko\VuexyAdmin\Livewire\Permissions\{PermissionsIndex,PermissionOffCanvasForm}; +use Koneko\VuexyAdmin\Livewire\Profile\{UpdateProfileInformationForm,UpdatePasswordForm,TwoFactorAuthenticationForm,LogoutOtherBrowser,DeleteUserForm}; +use Koneko\VuexyAdmin\Livewire\Roles\{RolesIndex,RoleCards}; +use Koneko\VuexyAdmin\Livewire\Users\{UsersIndex,UsersCount,UserForm,UserOffCanvasForm}; + +use Koneko\VuexyAdmin\Livewire\VuexyAdmin\{LogoOnLightBgSettings,LogoOnDarkBgSettings,AppDescriptionSettings,AppFaviconSettings}; +use Koneko\VuexyAdmin\Livewire\VuexyAdmin\SendmailSettings; +use Koneko\VuexyAdmin\Livewire\VuexyAdmin\{VuexyInterfaceSettings}; +use Koneko\VuexyAdmin\Livewire\VuexyAdmin\{GlobalSettingsIndex,GlobalSettingOffCanvasForm}; +use Koneko\VuexyAdmin\Livewire\VuexyAdmin\QuickAccessWidget; + +use Koneko\VuexyAdmin\Models\User; use Livewire\Livewire; use OwenIt\Auditing\AuditableObserver; use Spatie\Permission\PermissionServiceProvider; @@ -48,14 +56,17 @@ class VuexyAdminServiceProvider extends ServiceProvider URL::forceScheme('https'); } + // Registrar alias del middleware $this->app['router']->aliasMiddleware('admin', AdminTemplateMiddleware::class); + // Sobrescribir ruta de traducciones para asegurar que se usen las del paquete $this->app->bind('path.lang', function () { return __DIR__ . '/../resources/lang'; }); + // Register the module's routes $this->loadRoutesFrom(__DIR__.'/../routes/admin.php'); @@ -63,6 +74,7 @@ class VuexyAdminServiceProvider extends ServiceProvider // Cargar vistas del paquete $this->loadViewsFrom(__DIR__.'/../resources/views', 'vuexy-admin'); + // Registrar Componentes Blade Blade::componentNamespace('VuexyAdmin\\View\\Components', 'vuexy-admin'); @@ -100,32 +112,60 @@ class VuexyAdminServiceProvider extends ServiceProvider ]); } + // Registrar Livewire Components $components = [ - 'user-index' => UserIndex::class, - 'user-show' => UserShow::class, - 'user-form' => UserForm::class, - 'user-offcanvas-form' => UserOffCanvasForm::class, - 'role-index' => RoleIndex::class, - 'permission-index' => PermissionIndex::class, + // Usuarios + 'vuexy-admin::users-index' => UsersIndex::class, + 'vuexy-admin::users-count' => UsersCount::class, + 'vuexy-admin::user-form' => UserForm::class, + 'vuexy-admin::user-offcanvas-form' => UserOffCanvasForm::class, + // Perfil del usuario + 'vuexy-admin::update-profile-information-form' => UpdateProfileInformationForm::class, + 'vuexy-admin::update-password-form' => UpdatePasswordForm::class, + 'vuexy-admin::two-factor-authentication' => TwoFactorAuthenticationForm::class, + 'vuexy-admin::logout-other-browser' => LogoutOtherBrowser::class, + 'vuexy-admin::delete-user-form' => DeleteUserForm::class, - 'general-settings' => GeneralSettings::class, - 'application-settings' => ApplicationSettings::class, - 'interface-settings' => InterfaceSettings::class, - 'mail-smtp-settings' => MailSmtpSettings::class, - 'mail-sender-response-settings' => MailSenderResponseSettings::class, - 'cache-stats' => CacheStats::class, - 'session-stats' => SessionStats::class, - 'redis-stats' => RedisStats::class, - 'memcached-stats' => MemcachedStats::class, - 'cache-functions' => CacheFunctions::class, + // Roles y Permisos + 'vuexy-admin::roles-index' => RolesIndex::class, + 'vuexy-admin::role-cards' => RoleCards::class, + 'vuexy-admin::permissions-index' => PermissionsIndex::class, + 'vuexy-admin::permission-offcanvas-form' => PermissionOffCanvasForm::class, + + // Identidad de aplicación + 'vuexy-admin::app-description-settings' => AppDescriptionSettings::class, + 'vuexy-admin::app-favicon-settings' => AppFaviconSettings::class, + 'vuexy-admin::logo-on-light-bg-settings' => LogoOnLightBgSettings::class, + 'vuexy-admin::logo-on-dark-bg-settings' => LogoOnDarkBgSettings::class, + + // Ajustes de interfaz + 'vuexy-admin::interface-settings' => VuexyInterfaceSettings::class, + + // Cache + 'vuexy-admin::cache-stats' => CacheStats::class, + 'vuexy-admin::session-stats' => SessionStats::class, + 'vuexy-admin::redis-stats' => RedisStats::class, + 'vuexy-admin::memcached-stats' => MemcachedStats::class, + 'vuexy-admin::cache-functions' => CacheFunctions::class, + + // Configuración de correo saliente + 'vuexy-admin::sendmail-settings' => SendmailSettings::class, + + // Configuraciones globales + 'vuexy-admin::global-settings-index' => GlobalSettingsIndex::class, + 'vuexy-admin::global-setting-offcanvas-form' => GlobalSettingOffCanvasForm::class, + + // Accesos rápidos de la barra de menú + 'vuexy-admin::quick-access-widget' => QuickAccessWidget::class, ]; foreach ($components as $alias => $component) { Livewire::component($alias, $component); } + // Registrar auditoría en usuarios User::observe(AuditableObserver::class); } diff --git a/Queries/BootstrapTableQueryBuilder.php b/Queries/BootstrapTableQueryBuilder.php index 00984cb..fa0fb80 100644 --- a/Queries/BootstrapTableQueryBuilder.php +++ b/Queries/BootstrapTableQueryBuilder.php @@ -27,19 +27,24 @@ abstract class BootstrapTableQueryBuilder foreach ($this->config['joins'] as $join) { $type = $join['type'] ?? 'join'; - $this->query->{$type}($join['table'], function($joinObj) use ($join) { - $joinObj->on($join['first'], '=', $join['second']); + // Soporte para alias + $table = $join['table']; + $alias = $join['alias'] ?? null; + $tableWithAlias = $alias ? DB::raw("{$table} as {$alias}") : $table; - // Soporte para AND en ON, si está definidio + $this->query->{$type}($tableWithAlias, function ($joinObj) use ($join, $alias) { + $first = $join['first']; + $second = $join['second']; + + $joinObj->on($first, '=', $second); + + // Soporte para condiciones adicionales tipo AND if (!empty($join['and'])) { foreach ((array) $join['and'] as $andCondition) { - // 'sat_codigo_postal.c_estado = sat_localidad.c_estado' $parts = explode('=', $andCondition); - if (count($parts) === 2) { $left = trim($parts[0]); $right = trim($parts[1]); - $joinObj->whereRaw("$left = $right"); } } diff --git a/Services/AdminSettingsService.php b/Services/AdminSettingsService.php index f8fe76f..219b31b 100644 --- a/Services/AdminSettingsService.php +++ b/Services/AdminSettingsService.php @@ -6,13 +6,29 @@ use Illuminate\Support\Facades\Storage; use Intervention\Image\ImageManager; use Koneko\VuexyAdmin\Models\Setting; +/** + * Servicio para gestionar la configuración administrativa de VuexyAdmin + * + * Este servicio maneja el procesamiento y almacenamiento de imágenes del favicon + * y logos del panel administrativo, incluyendo diferentes versiones y tamaños. + * + * @package Koneko\VuexyAdmin\Services + */ class AdminSettingsService { + /** @var string Driver de procesamiento de imágenes */ private $driver; - private $imageDisk = 'public'; - private $favicon_basePath = 'favicon/'; + + /** @var string Disco de almacenamiento para imágenes */ + private $imageDisk = 'public'; + + /** @var string Ruta base para favicons */ + private $favicon_basePath = 'favicon/'; + + /** @var string Ruta base para logos */ private $image_logo_basePath = 'images/logo/'; + /** @var array> Tamaños predefinidos para favicons */ private $faviconsSizes = [ '180x180' => [180, 180], '192x192' => [192, 192], @@ -22,28 +38,40 @@ class AdminSettingsService '16x16' => [16, 16], ]; - private $imageLogoMaxPixels1 = 22500; // Primera versión (px^2) - private $imageLogoMaxPixels2 = 75625; // Segunda versión (px^2) - private $imageLogoMaxPixels3 = 262144; // Tercera versión (px^2) - private $imageLogoMaxPixels4 = 230400; // Tercera versión (px^2) en Base64 + /** @var int Área máxima en píxeles para la primera versión del logo */ + private $imageLogoMaxPixels1 = 22500; - protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos + /** @var int Área máxima en píxeles para la segunda versión del logo */ + private $imageLogoMaxPixels2 = 75625; + /** @var int Área máxima en píxeles para la tercera versión del logo */ + private $imageLogoMaxPixels3 = 262144; + + /** @var int Área máxima en píxeles para la versión base64 del logo */ + private $imageLogoMaxPixels4 = 230400; + + /** @var int Tiempo de vida en caché en minutos */ + protected $cacheTTL = 60 * 24 * 30; + + /** + * Constructor del servicio + * + * Inicializa el driver de procesamiento de imágenes desde la configuración + */ public function __construct() { $this->driver = config('image.driver', 'gd'); } - public function updateSetting(string $key, string $value): bool - { - $setting = Setting::updateOrCreate( - ['key' => $key], - ['value' => trim($value)] - ); - - return $setting->save(); - } - + /** + * Procesa y guarda un nuevo favicon + * + * Genera múltiples versiones del favicon en diferentes tamaños predefinidos, + * elimina las versiones anteriores y actualiza la configuración. + * + * @param \Illuminate\Http\UploadedFile $image Archivo de imagen subido + * @return void + */ public function processAndSaveFavicon($image): void { Storage::makeDirectory($this->imageDisk . '/' . $this->favicon_basePath); @@ -66,13 +94,20 @@ class AdminSettingsService Storage::disk($this->imageDisk)->put($resizedPath, $image->toPng(indexed: true)); } - $this->updateSetting('admin_favicon_ns', $this->favicon_basePath . $imageName); + // Actualizar configuración utilizando SettingService + $SettingsService = app(SettingsService::class); + $SettingsService->set('admin.favicon_ns', $this->favicon_basePath . $imageName, null, 'vuexy-admin'); } + /** + * Elimina los favicons antiguos del almacenamiento + * + * @return void + */ protected function deleteOldFavicons(): void { // Obtener el favicon actual desde la base de datos - $currentFavicon = Setting::where('key', 'admin_favicon_ns')->value('value'); + $currentFavicon = Setting::where('key', 'admin.favicon_ns')->value('value'); if ($currentFavicon) { $filePaths = [ @@ -93,6 +128,16 @@ class AdminSettingsService } } + /** + * Procesa y guarda un nuevo logo + * + * Genera múltiples versiones del logo con diferentes tamaños máximos, + * incluyendo una versión en base64, y actualiza la configuración. + * + * @param \Illuminate\Http\UploadedFile $image Archivo de imagen subido + * @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal) + * @return void + */ public function processAndSaveImageLogo($image, string $type = ''): void { // Crear directorio si no existe @@ -112,6 +157,15 @@ class AdminSettingsService $this->generateAndSaveImageAsBase64($image, $type, $this->imageLogoMaxPixels4); // Versión 3 } + /** + * Genera y guarda una versión del logo + * + * @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a procesar + * @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal) + * @param int $maxPixels Área máxima en píxeles + * @param string $suffix Sufijo para el nombre del archivo + * @return void + */ private function generateAndSaveImage($image, string $type, int $maxPixels, string $suffix = ''): void { $imageClone = clone $image; @@ -120,6 +174,7 @@ class AdminSettingsService $this->resizeImageToMaxPixels($imageClone, $maxPixels); $imageName = 'admin_image_logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : ''); + $keyValue = 'admin.image.logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : ''); // Generar nombre y ruta $imageNameUid = uniqid($imageName . '_', ".png"); @@ -129,9 +184,17 @@ class AdminSettingsService Storage::disk($this->imageDisk)->put($resizedPath, $imageClone->toPng(indexed: true)); // Actualizar configuración - $this->updateSetting($imageName, $resizedPath); + $SettingsService = app(SettingsService::class); + $SettingsService->set($keyValue, $resizedPath, null, 'vuexy-admin'); } + /** + * Redimensiona una imagen manteniendo su proporción + * + * @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a redimensionar + * @param int $maxPixels Área máxima en píxeles + * @return \Intervention\Image\Interfaces\ImageInterface + */ private function resizeImageToMaxPixels($image, int $maxPixels) { // Obtener dimensiones originales de la imagen @@ -163,7 +226,14 @@ class AdminSettingsService return $image; } - + /** + * Genera y guarda una versión del logo en formato base64 + * + * @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a procesar + * @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal) + * @param int $maxPixels Área máxima en píxeles + * @return void + */ private function generateAndSaveImageAsBase64($image, string $type, int $maxPixels): void { $imageClone = clone $image; @@ -175,12 +245,16 @@ class AdminSettingsService $base64Image = (string) $imageClone->toJpg(40)->toDataUri(); // Guardar como configuración - $this->updateSetting( - "admin_image_logo_base64" . ($type === 'dark' ? '_dark' : ''), - $base64Image // Ya incluye "data:image/png;base64," - ); + $SettingsService = app(SettingsService::class); + $SettingsService->set("admin.image.logo_base64" . ($type === 'dark' ? '_dark' : ''), $base64Image, null, 'vuexy-admin'); } + /** + * Elimina las imágenes antiguas del logo + * + * @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal) + * @return void + */ protected function deleteOldImageWebapp(string $type = ''): void { // Determinar prefijo según el tipo (normal o dark) @@ -188,9 +262,9 @@ class AdminSettingsService // Claves relacionadas con las imágenes que queremos limpiar $imageKeys = [ - "admin_image_logo{$suffix}", - "admin_image_logo_small{$suffix}", - "admin_image_logo_medium{$suffix}", + "admin.image_logo{$suffix}", + "admin.image_logo_small{$suffix}", + "admin.image_logo_medium{$suffix}", ]; // Recuperar las imágenes actuales en una sola consulta diff --git a/Services/AdminTemplateService.php b/Services/AdminTemplateService.php index b1d177f..8ed9925 100644 --- a/Services/AdminTemplateService.php +++ b/Services/AdminTemplateService.php @@ -7,48 +7,57 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Schema; use Koneko\VuexyAdmin\Models\Setting; +/** + * Servicio para gestionar la configuración y personalización del template administrativo. + * + * Esta clase maneja las configuraciones del template VuexyAdmin, incluyendo variables + * de personalización, logos, favicons y otras configuraciones de la interfaz. + * Implementa un sistema de caché para optimizar el rendimiento. + */ class AdminTemplateService { - protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos + /** @var int Tiempo de vida del caché en minutos (60 * 24 * 30 = 30 días) */ + protected $cacheTTL = 60 * 24 * 30; - public function updateSetting(string $key, string $value): bool - { - $setting = Setting::updateOrCreate( - ['key' => $key], - ['value' => trim($value)] - ); - - return $setting->save(); - } - - public function getAdminVars($adminSetting = false): array + /** + * Obtiene las variables de configuración del admin. + * + * @param string $setting Clave específica de configuración a obtener + * @return array Configuraciones del admin o valor específico si se proporciona $setting + */ + public function getAdminVars(string $setting = ''): array { try { // Verificar si el sistema está inicializado (la tabla `migrations` existe) if (!Schema::hasTable('migrations')) { - return $this->getDefaultAdminVars($adminSetting); + return $this->getDefaultAdminVars($setting); } // Cargar desde el caché o la base de datos si está disponible - return Cache::remember('admin_settings', $this->cacheTTL, function () use ($adminSetting) { - $settings = Setting::global() - ->where('key', 'LIKE', 'admin_%') + $adminVars = Cache::remember('admin_settings', $this->cacheTTL, function () { + $settings = Setting::withVirtualValue() + ->where('key', 'LIKE', 'admin.%') ->pluck('value', 'key') ->toArray(); - $adminSettings = $this->buildAdminVarsArray($settings); - - return $adminSetting - ? $adminSettings[$adminSetting] - : $adminSettings; + return $this->buildAdminVarsArray($settings); }); + + return $setting ? ($adminVars[$setting] ?? []) : $adminVars; + } catch (\Exception $e) { // En caso de error, devolver valores predeterminados - return $this->getDefaultAdminVars($adminSetting); + return $this->getDefaultAdminVars($setting); } } - private function getDefaultAdminVars($adminSetting = false): array + /** + * Obtiene las variables predeterminadas del admin. + * + * @param string $setting Clave específica de configuración a obtener + * @return array Configuraciones predeterminadas o valor específico si se proporciona $setting + */ + private function getDefaultAdminVars(string $setting = ''): array { $defaultSettings = [ 'title' => config('koneko.appTitle', 'Default Title'), @@ -59,27 +68,41 @@ class AdminTemplateService 'image_logo' => $this->getImageLogoPaths([]), ]; - return $adminSetting - ? $defaultSettings[$adminSetting] ?? null + return $setting + ? $defaultSettings[$setting] ?? null : $defaultSettings; } + /** + * Construye el array de variables del admin a partir de las configuraciones. + * + * @param array $settings Array asociativo de configuraciones + * @return array Array estructurado con las variables del admin + */ private function buildAdminVarsArray(array $settings): array { return [ - 'title' => $settings['admin_title'] ?? config('koneko.appTitle'), + 'title' => $settings['admin.title'] ?? config('koneko.appTitle'), 'author' => config('koneko.author'), - 'description' => config('koneko.description'), + 'description' => $settings['admin.description'] ?? config('koneko.description'), 'favicon' => $this->getFaviconPaths($settings), - 'app_name' => $settings['admin_app_name'] ?? config('koneko.appName'), + 'app_name' => $settings['admin.app_name'] ?? config('koneko.appName'), 'image_logo' => $this->getImageLogoPaths($settings), ]; } + /** + * Obtiene las variables de personalización de Vuexy. + * + * Combina las configuraciones predeterminadas con las almacenadas en la base de datos, + * aplicando las transformaciones necesarias para tipos específicos como booleanos. + * + * @return array Array asociativo con las variables de personalización + */ public function getVuexyCustomizerVars() { // Obtener valores de la base de datos - $settings = Setting::global() + $settings = Setting::withVirtualValue() ->where('key', 'LIKE', 'vuexy_%') ->pluck('value', 'key') ->toArray(); @@ -96,7 +119,7 @@ class AdminTemplateService $value = $settings[$vuexyKey] ?? $defaultValue; // Forzar booleanos para claves específicas - if (in_array($key, ['displayCustomizer', 'footerFixed', 'menuFixed', 'menuCollapsed', 'showDropdownOnHover'])) { + if (in_array($key, ['hasCustomizer', 'displayCustomizer', 'footerFixed', 'menuFixed', 'menuCollapsed', 'showDropdownOnHover'])) { $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); } @@ -106,12 +129,15 @@ class AdminTemplateService } /** - * Obtiene los paths de favicon en distintos tamaños. + * Genera las rutas para los diferentes tamaños de favicon. + * + * @param array $settings Array asociativo de configuraciones + * @return array Array con las rutas de los favicons en diferentes tamaños */ private function getFaviconPaths(array $settings): array { $defaultFavicon = config('koneko.appFavicon'); - $namespace = $settings['admin_favicon_ns'] ?? null; + $namespace = $settings['admin.favicon_ns'] ?? null; return [ 'namespace' => $namespace, @@ -125,30 +151,43 @@ class AdminTemplateService } /** - * Obtiene los paths de los logos en distintos tamaños. + * Genera las rutas para los diferentes tamaños y versiones del logo. + * + * @param array $settings Array asociativo de configuraciones + * @return array Array con las rutas de los logos en diferentes tamaños y modos */ private function getImageLogoPaths(array $settings): array { $defaultLogo = config('koneko.appLogo'); return [ - 'small' => $this->getImagePath($settings, 'admin_image_logo_small', $defaultLogo), - 'medium' => $this->getImagePath($settings, 'admin_image_logo_medium', $defaultLogo), - 'large' => $this->getImagePath($settings, 'admin_image_logo', $defaultLogo), - 'small_dark' => $this->getImagePath($settings, 'admin_image_logo_small_dark', $defaultLogo), - 'medium_dark' => $this->getImagePath($settings, 'admin_image_logo_medium_dark', $defaultLogo), - 'large_dark' => $this->getImagePath($settings, 'admin_image_logo_dark', $defaultLogo), + 'small' => $this->getImagePath($settings, 'admin.image.logo_small', $defaultLogo), + 'medium' => $this->getImagePath($settings, 'admin.image.logo_medium', $defaultLogo), + 'large' => $this->getImagePath($settings, 'admin.image.logo', $defaultLogo), + 'small_dark' => $this->getImagePath($settings, 'admin.image.logo_small_dark', $defaultLogo), + 'medium_dark' => $this->getImagePath($settings, 'admin.image.logo_medium_dark', $defaultLogo), + 'large_dark' => $this->getImagePath($settings, 'admin.image.logo_dark', $defaultLogo), ]; } /** - * Obtiene un path de imagen o retorna un valor predeterminado. + * Obtiene la ruta de una imagen específica desde las configuraciones. + * + * @param array $settings Array asociativo de configuraciones + * @param string $key Clave de la configuración + * @param string $default Valor predeterminado si no se encuentra la configuración + * @return string Ruta de la imagen */ private function getImagePath(array $settings, string $key, string $default): string { return $settings[$key] ?? $default; } + /** + * Limpia el caché de las variables del admin. + * + * @return void + */ public static function clearAdminVarsCache() { Cache::forget("admin_settings"); diff --git a/Services/AvatarInitialsService.php b/Services/AvatarInitialsService.php index df045eb..5895473 100644 --- a/Services/AvatarInitialsService.php +++ b/Services/AvatarInitialsService.php @@ -15,7 +15,7 @@ class AvatarInitialsService protected const INITIAL_MAX_LENGTH = 3; protected const AVATAR_BACKGROUND = '#EBF4FF'; protected const AVATAR_COLORS = [ - '#7367f0', + '#3b82f6', '#808390', '#28c76f', '#ff4c51', diff --git a/Services/CacheConfigService.php b/Services/CacheConfigService.php index 0d16a91..5e57aae 100644 --- a/Services/CacheConfigService.php +++ b/Services/CacheConfigService.php @@ -6,8 +6,20 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis; +/** + * Servicio para gestionar y obtener información de configuración del sistema de caché. + * + * Esta clase proporciona métodos para obtener información detallada sobre las configuraciones + * de caché, sesión, base de datos y drivers del sistema. Permite consultar versiones, + * estados y configuraciones de diferentes servicios como Redis, Memcached y bases de datos. + */ class CacheConfigService { + /** + * Obtiene la configuración completa del sistema de caché y servicios relacionados. + * + * @return array Configuración completa que incluye caché, sesión, base de datos y drivers + */ public function getConfig(): array { return [ @@ -20,7 +32,11 @@ class CacheConfigService ]; } - + /** + * Obtiene la configuración específica del sistema de caché. + * + * @return array Configuración del caché incluyendo driver, host y base de datos + */ private function getCacheConfig(): array { $cacheConfig = Config::get('cache'); @@ -59,6 +75,11 @@ class CacheConfigService return $cacheConfig; } + /** + * Obtiene la configuración del sistema de sesiones. + * + * @return array Configuración de sesiones incluyendo driver, host y base de datos + */ private function getSessionConfig(): array { $sessionConfig = Config::get('session'); @@ -97,6 +118,11 @@ class CacheConfigService return $sessionConfig; } + /** + * Obtiene la configuración de la base de datos principal. + * + * @return array Configuración de la base de datos incluyendo host y nombre de la base de datos + */ private function getDatabaseConfig(): array { $databaseConfig = Config::get('database'); @@ -109,7 +135,14 @@ class CacheConfigService return $databaseConfig; } - + /** + * Obtiene información sobre las versiones de los drivers en uso. + * + * Recopila información detallada sobre las versiones de los drivers de base de datos, + * Redis y Memcached si están en uso en el sistema. + * + * @return array Información de versiones de los drivers activos + */ private function getDriverVersion(): array { $drivers = []; @@ -163,6 +196,11 @@ class CacheConfigService return $drivers; } + /** + * Obtiene la versión del servidor MySQL. + * + * @return string Versión del servidor MySQL o mensaje de error + */ private function getMySqlVersion(): string { try { @@ -173,6 +211,11 @@ class CacheConfigService } } + /** + * Obtiene la versión del servidor PostgreSQL. + * + * @return string Versión del servidor PostgreSQL o mensaje de error + */ private function getPgSqlVersion(): string { try { @@ -183,6 +226,11 @@ class CacheConfigService } } + /** + * Obtiene la versión del servidor SQL Server. + * + * @return string Versión del servidor SQL Server o mensaje de error + */ private function getSqlSrvVersion(): string { try { @@ -193,6 +241,11 @@ class CacheConfigService } } + /** + * Obtiene la versión del servidor Memcached. + * + * @return string Versión del servidor Memcached o mensaje de error + */ private function getMemcachedVersion(): string { try { @@ -213,6 +266,11 @@ class CacheConfigService } } + /** + * Obtiene la versión del servidor Redis. + * + * @return string Versión del servidor Redis o mensaje de error + */ private function getRedisVersion(): string { try { @@ -223,7 +281,14 @@ class CacheConfigService } } - + /** + * Verifica si un driver específico está en uso en el sistema. + * + * Comprueba si el driver está siendo utilizado en caché, sesiones o colas. + * + * @param string $driver Nombre del driver a verificar + * @return bool True si el driver está en uso, false en caso contrario + */ protected function isDriverInUse(string $driver): bool { return in_array($driver, [ diff --git a/Services/CacheManagerService.php b/Services/CacheManagerService.php index 6db4419..767bb26 100644 --- a/Services/CacheManagerService.php +++ b/Services/CacheManagerService.php @@ -7,19 +7,38 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\File; +/** + * Servicio para gestionar y administrar el sistema de caché. + * + * Esta clase proporciona funcionalidades para administrar diferentes drivers de caché + * (Redis, Memcached, Database, File), incluyendo operaciones como obtener estadísticas, + * limpiar la caché y monitorear el uso de recursos. + */ class CacheManagerService { + /** @var string Driver de caché actualmente seleccionado */ private string $driver; - public function __construct(string $driver = null) + /** + * Constructor del servicio de gestión de caché. + * + * @param mixed $driver Driver de caché a utilizar. Si es null, se usa el driver predeterminado + */ + public function __construct(mixed $driver = null) { $this->driver = $driver ?? config('cache.default'); } /** * Obtiene estadísticas de caché para el driver especificado. + * + * Recopila información detallada sobre el uso y rendimiento del sistema de caché, + * incluyendo uso de memoria, número de elementos y estadísticas específicas del driver. + * + * @param mixed $driver Driver de caché del cual obtener estadísticas + * @return array Estadísticas del sistema de caché */ - public function getCacheStats(string $driver = null): array + public function getCacheStats(mixed $driver = null): array { $driver = $driver ?? $this->driver; @@ -40,7 +59,13 @@ class CacheManagerService } } - public function clearCache(string $driver = null): array + /** + * Limpia la caché del driver especificado. + * + * @param mixed $driver Driver de caché a limpiar + * @return array Resultado de la operación de limpieza + */ + public function clearCache(mixed $driver = null): array { $driver = $driver ?? $this->driver; @@ -88,6 +113,11 @@ class CacheManagerService } } + /** + * Obtiene estadísticas detalladas del servidor Redis. + * + * @return array Información detallada del servidor Redis incluyendo versión, memoria, clientes y más + */ public function getRedisStats() { try { @@ -132,6 +162,11 @@ class CacheManagerService } } + /** + * Obtiene estadísticas detalladas del servidor Memcached. + * + * @return array Información detallada del servidor Memcached incluyendo versión, memoria y estadísticas de uso + */ public function getMemcachedStats() { try { @@ -176,9 +211,10 @@ class CacheManagerService } } - /** * Obtiene estadísticas para caché en base de datos. + * + * @return array Estadísticas de la caché en base de datos incluyendo cantidad de registros y uso de memoria */ private function _getDatabaseStats(): array { @@ -196,6 +232,8 @@ class CacheManagerService /** * Obtiene estadísticas para caché en archivos. + * + * @return array Estadísticas de la caché en archivos incluyendo cantidad de archivos y uso de memoria */ private function _getFilecacheStats(): array { @@ -211,6 +249,11 @@ class CacheManagerService } } + /** + * Obtiene estadísticas específicas de Redis para la caché. + * + * @return array Estadísticas de Redis incluyendo cantidad de claves y uso de memoria + */ private function _getRedisStats() { try { @@ -227,6 +270,11 @@ class CacheManagerService } } + /** + * Obtiene estadísticas específicas de Memcached para la caché. + * + * @return array Estadísticas de Memcached incluyendo cantidad de elementos y uso de memoria + */ public function _getMemcachedStats(): array { try { @@ -254,6 +302,14 @@ class CacheManagerService } } + /** + * Obtiene información sobre las bases de datos Redis en uso. + * + * Analiza y recopila información sobre las diferentes bases de datos Redis + * configuradas en el sistema (default, cache, sessions). + * + * @return array Información detallada de las bases de datos Redis + */ private function getRedisDatabases(): array { // Verificar si Redis está en uso @@ -300,7 +356,11 @@ class CacheManagerService return $result; } - + /** + * Limpia la caché almacenada en base de datos. + * + * @return bool True si se eliminaron registros, False si no había registros para eliminar + */ private function clearDatabaseCache(): bool { $count = DB::table(config('cache.stores.database.table'))->count(); @@ -313,6 +373,11 @@ class CacheManagerService return false; } + /** + * Limpia la caché almacenada en archivos. + * + * @return bool True si se eliminaron archivos, False si no había archivos para eliminar + */ private function clearFilecache(): bool { $cachePath = config('cache.stores.file.path'); @@ -326,6 +391,11 @@ class CacheManagerService return false; } + /** + * Limpia la caché almacenada en Redis. + * + * @return bool True si se eliminaron claves, False si no había claves para eliminar + */ private function clearRedisCache(): bool { $prefix = config('cache.prefix', ''); @@ -343,6 +413,11 @@ class CacheManagerService return false; } + /** + * Limpia la caché almacenada en Memcached. + * + * @return bool True si se limpió la caché, False en caso contrario + */ private function clearMemcachedCache(): bool { // Obtener el cliente Memcached directamente @@ -359,9 +434,11 @@ class CacheManagerService return false; } - /** - * Verifica si un driver es soportado. + * Verifica si un driver es soportado por el sistema. + * + * @param string $driver Nombre del driver a verificar + * @return bool True si el driver es soportado, False en caso contrario */ private function isSupportedDriver(string $driver): bool { @@ -369,7 +446,10 @@ class CacheManagerService } /** - * Convierte bytes en un formato legible. + * Convierte bytes en un formato legible por humanos. + * + * @param int|float $bytes Cantidad de bytes a formatear + * @return string Cantidad formateada con unidad (B, KB, MB, GB, TB) */ private function formatBytes($bytes) { @@ -380,7 +460,12 @@ class CacheManagerService } /** - * Genera una respuesta estandarizada. + * Genera una respuesta estandarizada para las operaciones del servicio. + * + * @param string $status Estado de la operación ('success', 'warning', 'danger', 'info') + * @param string $message Mensaje descriptivo de la operación + * @param array $data Datos adicionales de la operación + * @return array Respuesta estructurada con estado, mensaje y datos */ private function response(string $status, string $message, array $data = []): array { diff --git a/Services/GlobalSettingsService.php b/Services/GlobalSettingsService.php index 404fd66..63db900 100644 --- a/Services/GlobalSettingsService.php +++ b/Services/GlobalSettingsService.php @@ -9,28 +9,26 @@ use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Schema; use Koneko\VuexyAdmin\Models\Setting; +/** + * Servicio para gestionar la configuración global del sistema. + * + * Esta clase maneja las configuraciones globales del sistema, incluyendo servicios + * externos (Facebook, Google), configuración de Vuexy y sistema de correo. + * Implementa un sistema de caché para optimizar el rendimiento y proporciona + * valores predeterminados cuando es necesario. + */ class GlobalSettingsService { - /** - * Tiempo de vida del caché en minutos (30 días). - */ + /** @var int Tiempo de vida del caché en minutos (60 * 24 * 30 = 30 días) */ private $cacheTTL = 60 * 24 * 30; /** - * Actualiza o crea una configuración. - */ - public function updateSetting(string $key, string $value): bool - { - $setting = Setting::updateOrCreate( - ['key' => $key], - ['value' => trim($value)] - ); - - return $setting->save(); - } - - /** - * Carga y sobrescribe las configuraciones del sistema. + * Carga la configuración del sistema desde la base de datos o caché. + * + * Gestiona la carga de configuraciones para servicios externos y Vuexy. + * Si la base de datos no está inicializada, utiliza valores predeterminados. + * + * @return void */ public function loadSystemConfig(): void { @@ -41,7 +39,7 @@ class GlobalSettingsService } else { // Cargar configuración desde la caché o base de datos $config = Cache::remember('global_system_config', $this->cacheTTL, function () { - $settings = Setting::global() + $settings = Setting::withVirtualValue() ->where('key', 'LIKE', 'config.%') ->pluck('value', 'key') ->toArray(); @@ -58,6 +56,7 @@ class GlobalSettingsService Config::set('services.facebook', $config['servicesFacebook']); Config::set('services.google', $config['servicesGoogle']); Config::set('vuexy', $config['vuexy']); + } catch (\Exception $e) { // Manejo silencioso de errores para evitar interrupciones Config::set('services.facebook', config('services.facebook', [])); @@ -67,7 +66,9 @@ class GlobalSettingsService } /** - * Devuelve una configuración predeterminada si la base de datos no está inicializada. + * Obtiene la configuración predeterminada del sistema. + * + * @return array Configuración predeterminada para servicios y Vuexy */ private function getDefaultSystemConfig(): array { @@ -87,7 +88,11 @@ class GlobalSettingsService } /** - * Verifica si un bloque de configuraciones está presente. + * Verifica si existe configuración para un bloque específico. + * + * @param array $settings Array de configuraciones + * @param string $blockPrefix Prefijo del bloque a verificar + * @return bool True si existe configuración para el bloque */ protected function hasBlockConfig(array $settings, string $blockPrefix): bool { @@ -95,13 +100,17 @@ class GlobalSettingsService } /** - * Construye la configuración de un servicio (Facebook, Google, etc.). + * Construye la configuración para un servicio específico. + * + * @param array $settings Array de configuraciones + * @param string $blockPrefix Prefijo del bloque de configuración + * @param string $defaultConfigKey Clave de configuración predeterminada + * @return array Configuración del servicio */ protected function buildServiceConfig(array $settings, string $blockPrefix, string $defaultConfigKey): array { if (!$this->hasBlockConfig($settings, $blockPrefix)) { - return []; - return config($defaultConfigKey); + return config($defaultConfigKey)?? []; } return [ @@ -112,8 +121,14 @@ class GlobalSettingsService } /** - * Construye la configuración personalizada de Vuexy. - */ + * Construye la configuración de Vuexy. + * + * Combina la configuración predeterminada con los valores almacenados + * en la base de datos y normaliza los campos booleanos. + * + * @param array $settings Array de configuraciones + * @return array Configuración de Vuexy normalizada + */ protected function buildVuexyConfig(array $settings): array { // Configuración predeterminada del sistema @@ -133,7 +148,10 @@ class GlobalSettingsService } /** - * Normaliza los campos booleanos. + * Normaliza los campos booleanos en la configuración. + * + * @param array $config Configuración a normalizar + * @return array Configuración con campos booleanos normalizados */ protected function normalizeBooleanFields(array $config): array { @@ -158,7 +176,9 @@ class GlobalSettingsService } /** - * Limpia el caché de la configuración del sistema. + * Limpia la caché de configuración del sistema. + * + * @return void */ public static function clearSystemConfigCache(): void { @@ -166,21 +186,29 @@ class GlobalSettingsService } /** - * Elimina las claves config.vuexy.* y limpia global_system_config + * Limpia la configuración de Vuexy de la base de datos y caché. + * + * @return void */ public static function clearVuexyConfig(): void { Setting::where('key', 'LIKE', 'config.vuexy.%')->delete(); + Cache::forget('global_system_config'); } /** - * Obtiene y sobrescribe la configuración de correo electrónico. + * Obtiene la configuración del sistema de correo. + * + * Recupera y estructura la configuración de correo incluyendo + * configuración SMTP, direcciones de envío y respuesta. + * + * @return array Configuración completa del sistema de correo */ public function getMailSystemConfig(): array { return Cache::remember('mail_system_config', $this->cacheTTL, function () { - $settings = Setting::global() + $settings = Setting::withVirtualValue() ->where('key', 'LIKE', 'mail.%') ->pluck('value', 'key') ->toArray(); @@ -215,7 +243,9 @@ class GlobalSettingsService } /** - * Limpia el caché de la configuración de correo electrónico. + * Limpia la caché de configuración del sistema de correo. + * + * @return void */ public static function clearMailSystemConfigCache(): void { diff --git a/Services/SessionManagerService.php b/Services/SessionManagerService.php index d57d05f..fea2328 100644 --- a/Services/SessionManagerService.php +++ b/Services/SessionManagerService.php @@ -10,12 +10,12 @@ class SessionManagerService { private string $driver; - public function __construct(string $driver = null) + public function __construct(mixed $driver = null) { $this->driver = $driver ?? config('session.driver'); } - public function getSessionStats(string $driver = null): array + public function getSessionStats(mixed $driver = null): array { $driver = $driver ?? $this->driver; @@ -41,7 +41,7 @@ class SessionManagerService } } - public function clearSessions(string $driver = null): array + public function clearSessions(mixed $driver = null): array { $driver = $driver ?? $this->driver; diff --git a/Services/SettingsService.php b/Services/SettingsService.php new file mode 100644 index 0000000..5b2b859 --- /dev/null +++ b/Services/SettingsService.php @@ -0,0 +1,190 @@ +generateCacheKey($key, $userId, $category); + + if ($useCache && Cache::has($cacheKey)) { + return Cache::get($cacheKey); + } + + $value = $this->retrieveSetting($key, $userId, $category); + + if ($storeInCache && $value !== null) { + Cache::put($cacheKey, $value, now()->addMinutes($cacheTtl)); + } + + return $value; + } + + /** + * Guarda o actualiza una configuración con control de caché. + * + * @param string $key + * @param mixed $value + * @param int|null $userId + * @param string|null $category + * @param string|null $mimeType + * @param string|null $fileName + * @param bool $updateCache + * @param int|null $cacheTtl + * @return Setting|null + */ + public function set( + string $key, + mixed $value, + ?int $userId = null, + ?string $category = null, + ?string $mimeType = null, + ?string $fileName = null, + bool $updateCache = false, + ?int $cacheTtl = 120 + ): ?Setting { + $data = [ + 'user_id' => $userId, + 'category' => $category, + 'mime_type' => $mimeType, + 'file_name' => $fileName, + // Inicializar todos los campos de valor como null + 'value_string' => null, + 'value_integer' => null, + 'value_boolean' => null, + 'value_float' => null, + 'value_text' => null, + 'value_binary' => null, + ]; + + // Detectar tipo de valor + if (is_string($value)) { + // Evaluamos la longitud de la cadena + $threshold = 250; + + if (strlen($value) > $threshold) { + $data['value_text'] = $value; + } else { + $data['value_string'] = $value; + } + } elseif (is_int($value)) { + $data['value_integer'] = $value; + } elseif (is_bool($value)) { + $data['value_boolean'] = $value; + } elseif (is_float($value)) { + $data['value_float'] = $value; + } elseif (is_resource($value) || $value instanceof \SplFileInfo) { + $data['value_binary'] = is_resource($value) + ? stream_get_contents($value) + : file_get_contents($value->getRealPath()); + } elseif (is_array($value) || is_object($value)) { + $data['value_text'] = json_encode($value); + } + + // Se registra usuario que realiza la acción + if (Auth::check()) { + $data['updated_by'] = Auth::id(); + } + + $setting = Setting::updateOrCreate( + ['key' => $key, 'user_id' => $userId, 'category' => $category], + $data + ); + + if ($updateCache) { + $cacheKey = $this->generateCacheKey($key, $userId, $category); + Cache::put($cacheKey, $setting->value, now()->addMinutes($cacheTtl)); + } + + return $setting; + } + + /** + * Elimina una configuración. + * + * @param string $key + * @param int|null $userId + * @param string|null $category + * @return Setting|null La configuración eliminada o null si no existía + */ + public function delete(string $key, ?int $userId = null, ?string $category = null): ?Setting + { + $setting = Setting::where('key', $key); + + if ($userId !== null) { + $setting->where('user_id', $userId); + } + + if ($category !== null) { + $setting->where('category', $category); + } + + $setting = $setting->first(); + + if ($setting) { + $cacheKey = $this->generateCacheKey($key, $userId, $category); + Cache::forget($cacheKey); + $setting->delete(); + } + + return $setting; + } + + /** + * Recupera una configuración de la base de datos. + * + * @param string $key + * @param integer|null $userId + * @param string|null $category + * @return void + */ + protected function retrieveSetting(string $key, ?int $userId, ?string $category) + { + $query = Setting::where('key', $key); + + if ($userId !== null) { + $query->where('user_id', $userId); + } + + if ($category !== null) { + $query->where('category', $category); + } + + return $query->first()?->value; + } + + /** + * Genera una clave de caché para una configuración. + * + * @param string $key + * @param integer|null $userId + * @param string|null $category + * @return string + */ + protected function generateCacheKey(string $key, ?int $userId, ?string $category): string + { + return 'settings:' . md5($key . '|' . $userId . '|' . $category); + } +} diff --git a/Services/VuexyAdminService.php b/Services/VuexyAdminService.php index 64d3982..a2af737 100644 --- a/Services/VuexyAdminService.php +++ b/Services/VuexyAdminService.php @@ -2,10 +2,7 @@ namespace Koneko\VuexyAdmin\Services; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\{Auth,Cache,Route,Gate}; use Koneko\VuexyAdmin\Models\Setting; class VuexyAdminService @@ -26,12 +23,8 @@ class VuexyAdminService { $this->user = Auth::user(); $this->vuexySearch = Auth::user() !== null; - $this->orientation = config('vuexy.custom.myLayout'); } - /** - * Obtiene el menú según el estado del usuario (autenticado o no). - */ public function getMenu() { // Obtener el menú desde la caché @@ -45,9 +38,6 @@ class VuexyAdminService return $this->markActive($menu, $currentRoute); } - /** - * Menú para usuarios no autenticados.dump - */ private function getGuestMenu() { return Cache::remember('vuexy_menu_guest', now()->addDays(7), function () { @@ -55,12 +45,9 @@ class VuexyAdminService }); } - /** - * Menú para usuarios autenticados. - */ private function getUserMenu() { - Cache::forget("vuexy_menu_user_{$this->user->id}"); // Borrar la caché anterior para actualizarla + //Cache::forget("vuexy_menu_user_{$this->user->id}"); // Borrar la caché anterior para actualizarla return Cache::remember("vuexy_menu_user_{$this->user->id}", now()->addHours(24), function () { return $this->getMenuArray(); @@ -89,9 +76,6 @@ class VuexyAdminService return $menu; } - /** - * Invalida el cache del menú de un usuario. - */ public static function clearUserMenuCache() { $user = Auth::user(); @@ -100,9 +84,6 @@ class VuexyAdminService Cache::forget("vuexy_menu_user_{$user->id}"); } - /** - * Invalida el cache del menú de invitados. - */ public static function clearGuestMenuCache() { Cache::forget('vuexy_menu_guest'); @@ -224,7 +205,7 @@ class VuexyAdminService return $quickLinksData; } - private function collectQuickLinksFromMenu(array $menu, array &$quickLinks, string $parentTitle = null) + private function collectQuickLinksFromMenu(array $menu, array &$quickLinks, mixed $parentTitle = null) { foreach ($menu as $title => $item) { // Verificar si el elemento está en la lista de quicklinksRouteNames @@ -249,9 +230,6 @@ class VuexyAdminService } } - /** - * Verifica si la ruta actual existe en la lista de enlaces. - */ private function isCurrentPageInList(array $quickLinks, string $currentRoute): bool { foreach ($quickLinks['rows'] as $row) { @@ -291,193 +269,8 @@ class VuexyAdminService - "; } diff --git a/composer.json b/composer.json index 8a7373e..d5572e0 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,10 @@ "owen-it/laravel-auditing": "^13.6", "spatie/laravel-permission": "^6.10" }, + "prefer-stable": true, "autoload": { "psr-4": { - "Koneko\\VuexyAdmin\\": "" + "Koneko\\VuexyAdmin\\": "./" } }, "extra": { @@ -35,6 +36,5 @@ "support": { "source": "https://github.com/koneko-mx/laravel-vuexy-admin", "issues": "https://github.com/koneko-mx/laravel-vuexy-admin/issues" - }, - "prefer-stable": true + } } diff --git a/config/vuexy_menu.php b/config/vuexy_menu.php index 4fdc3bc..b4c8bf2 100644 --- a/config/vuexy_menu.php +++ b/config/vuexy_menu.php @@ -112,7 +112,7 @@ return [ ], ] ], - 'Configuración de cuenta' => [ + 'Cuenta de usuario' => [ 'route' => 'admin.core.user-profile.index', 'icon' => 'menu-icon tf-icons ti ti-user-cog', ], @@ -377,14 +377,14 @@ return [ 'can' => 'admin.inventory.product-catalogs.view', ], 'Productos y servicios' => [ - 'route' => 'admin.inventory.products.index', + 'route' => 'admin.products.products.index', 'icon' => 'menu-icon tf-icons ti ti-packages', - 'can' => 'admin.inventory.products.view', + 'can' => 'admin.products.products.view', ], 'Agregar producto o servicio' => [ - 'route' => 'admin.inventory.products.create', + 'route' => 'admin.products.products.create', 'icon' => 'menu-icon tf-icons ti ti-package', - 'can' => 'admin.inventory.products.create', + 'can' => 'admin.products.products.create', ], ] ], @@ -616,16 +616,16 @@ return [ 'can' => 'admin.inventory.suppliers.view', ], 'Órdenes de Compra' => [ - 'route' => 'admin.inventory.orders.index', - 'can' => 'admin.inventory.orders.view', + 'route' => 'admin.purchase-orders.orders.index', + 'can' => 'admin.purchase-orders.orders.view', ], 'Recepción de Productos' => [ - 'route' => 'admin.inventory.reception.index', - 'can' => 'admin.inventory.reception.view', + 'route' => 'admin.purchase-orders.reception.index', + 'can' => 'admin.purchase-orders.reception.view', ], 'Gestión de Insumos' => [ - 'route' => 'admin.inventory.materials.index', - 'can' => 'admin.inventory.materials.view', + 'route' => 'admin.purchase-orders.materials.index', + 'can' => 'admin.purchase-orders.materials.view', ], ], ], @@ -654,20 +654,20 @@ return [ 'icon' => 'menu-icon tf-icons ti ti-truck', 'submenu' => [ 'Órdenes de Envío' => [ - 'route' => 'admin.inventory.shipping-orders.index', - 'can' => 'admin.inventory.shipping-orders.view', + 'route' => 'admin.shipping.orders.index', + 'can' => 'admin.shipping.orders.view', ], 'Seguimiento de Envíos' => [ - 'route' => 'admin.inventory.shipping-tracking.index', - 'can' => 'admin.inventory.shipping-tracking.view', + 'route' => 'admin.shipping.tracking.index', + 'can' => 'admin.shipping.tracking.view', ], 'Transportistas' => [ - 'route' => 'admin.inventory.shipping-carriers.index', - 'can' => 'admin.inventory.shipping-carriers.view', + 'route' => 'admin.shipping.carriers.index', + 'can' => 'admin.shipping.carriers.view', ], 'Tarifas y Métodos de Envío' => [ - 'route' => 'admin.inventory.shipping-rates.index', - 'can' => 'admin.inventory.shipping-rates.view', + 'route' => 'admin.shipping.rates.index', + 'can' => 'admin.shipping.rates.view', ], ], ], @@ -679,16 +679,16 @@ return [ 'can' => 'admin.inventory.asset.view', ], 'Mantenimiento Preventivo' => [ - 'route' => 'admin.inventory.asset-maintenance.index', - 'can' => 'admin.inventory.asset-maintenance.view', + 'route' => 'admin.assets.maintenance.index', + 'can' => 'admin.assets.maintenance.view', ], 'Control de Vida Útil' => [ - 'route' => 'admin.inventory.asset-lifecycle.index', - 'can' => 'admin.inventory.asset-lifecycle.view', + 'route' => 'admin.assets.lifecycle.index', + 'can' => 'admin.assets.lifecycle.view', ], 'Asignación de Activos' => [ - 'route' => 'admin.inventory.asset-assignments.index', - 'can' => 'admin.inventory.asset-assignments.view', + 'route' => 'admin.assets.assignments.index', + 'can' => 'admin.assets.assignments.view', ], ], ], diff --git a/database/data/rbac-config.json b/database/data/rbac-config.json index 249d102..d2795c0 100644 --- a/database/data/rbac-config.json +++ b/database/data/rbac-config.json @@ -59,8 +59,8 @@ "admin.attendance.absences.view", "admin.inventory.product-categories.view", "admin.inventory.product-catalogs.view", - "admin.inventory.products.view", - "admin.inventory.products.create", + "admin.products.products.view", + "admin.products.products.create", "admin.sales.dashboard.allow", "admin.contacts.customers.view", "admin.sales.sales.view", @@ -99,21 +99,21 @@ "admin.billing.nomina.view", "admin.billing.verify-cfdi.allow", "admin.contacts.suppliers.view", - "admin.inventory.orders.view", - "admin.inventory.reception.view", - "admin.inventory.materials.view", + "admin.purchase-orders.orders.view", + "admin.purchase-orders.reception.view", + "admin.purchase-orders.materials.view", "admin.inventory.warehouse.view", "admin.inventory.stock.view", "admin.inventory.movements.view", "admin.inventory.transfers.view", - "admin.inventory.shipping-orders.view", - "admin.inventory.shipping-tracking.view", - "admin.inventory.shipping-carriers.view", - "admin.inventory.shipping-rates.view", - "admin.inventory.assets.view", - "admin.inventory.asset-maintenance.view", - "admin.inventory.asset-lifecycle.view", - "admin.inventory.asset-assignments.view", + "admin.shipping.orders.view", + "admin.shipping.tracking.view", + "admin.shipping.carriers.view", + "admin.shipping.rates.view", + "admin.assets.assets.view", + "admin.assets.maintenance.view", + "admin.assets.lifecycle.view", + "admin.assets.assignments.view", "admin.projects.dashboard.view", "admin.projects.view", "admin.projects.create", @@ -167,21 +167,21 @@ "admin.rrhh.organization.view", "admin.inventory.product-categories.view", "admin.inventory.product-catalogs.view", - "admin.inventory.products.view", - "admin.inventory.products.create", + "admin.products.products.view", + "admin.products.products.create", "admin.contacts.suppliers.view", "admin.contacts.suppliers.create", "admin.inventory.warehouse.view", - "admin.inventory.orders.view", - "admin.inventory.reception.view", - "admin.inventory.materials.view", + "admin.purchase-orders.orders.view", + "admin.purchase-orders.reception.view", + "admin.purchase-orders.materials.view", "admin.inventory.stock.view", "admin.inventory.movements.view", "admin.inventory.transfers.view", - "admin.inventory.assets.view", - "admin.inventory.asset-maintenance.view", - "admin.inventory.asset-lifecycle.view", - "admin.inventory.asset-assignments.view" + "admin.assets.assets.view", + "admin.assets.maintenance.view", + "admin.assets.lifecycle.view", + "admin.assets.assignments.view" ] }, "Administrador Web" : { @@ -197,8 +197,8 @@ "permissions" : [ "admin.inventory.product-categories.view", "admin.inventory.product-catalogs.view", - "admin.inventory.products.view", - "admin.inventory.products.create", + "admin.products.products.view", + "admin.products.products.create", "admin.inventory.warehouse.view", "admin.inventory.stock.view", "admin.inventory.movements.view", @@ -293,7 +293,7 @@ "admin.attendance.absences.view", "admin.inventory.product-categories.view", "admin.inventory.product-catalogs.view", - "admin.inventory.products.view", + "admin.products.products.view", "admin.contacts.customers.view", "admin.sales.sales.view", "admin.sales.quotes.view", @@ -322,21 +322,21 @@ "admin.billing.pagos.view", "admin.billing.nomina.view", "admin.contacts.suppliers.view", - "admin.inventory.orders.view", - "admin.inventory.reception.view", - "admin.inventory.materials.view", + "admin.purchase-orders.orders.view", + "admin.purchase-orders.reception.view", + "admin.purchase-orders.materials.view", "admin.inventory.warehouse.view", "admin.inventory.stock.view", "admin.inventory.movements.view", "admin.inventory.transfers.view", - "admin.inventory.shipping-orders.view", - "admin.inventory.shipping-tracking.view", - "admin.inventory.shipping-carriers.view", - "admin.inventory.shipping-rates.view", - "admin.inventory.assets.view", - "admin.inventory.asset-maintenance.view", - "admin.inventory.asset-lifecycle.view", - "admin.inventory.asset-assignments.view", + "admin.shipping.orders.view", + "admin.shipping.tracking.view", + "admin.shipping.carriers.view", + "admin.shipping.rates.view", + "admin.assets.assets.view", + "admin.assets.maintenance.view", + "admin.assets.lifecycle.view", + "admin.assets.assignments.view", "admin.projects.dashboard.view", "admin.projects.view", "admin.projects.tasks.view", @@ -421,8 +421,8 @@ "admin.attendance.absences.view", "admin.inventory.product-categories.view", "admin.inventory.product-catalogs.view", - "admin.inventory.products.view", - "admin.inventory.products.create", + "admin.products.products.view", + "admin.products.products.create", "admin.sales.dashboard.allow", "admin.contacts.customers.view", "admin.contacts.customers.create", @@ -463,21 +463,21 @@ "admin.billing.verify-cfdi.allow", "admin.contacts.suppliers.view", "admin.contacts.suppliers.create", - "admin.inventory.orders.view", - "admin.inventory.reception.view", - "admin.inventory.materials.view", + "admin.purchase-orders.orders.view", + "admin.purchase-orders.reception.view", + "admin.purchase-orders.materials.view", "admin.inventory.warehouse.view", "admin.inventory.stock.view", "admin.inventory.movements.view", "admin.inventory.transfers.view", - "admin.inventory.shipping-orders.view", - "admin.inventory.shipping-tracking.view", - "admin.inventory.shipping-carriers.view", - "admin.inventory.shipping-rates.view", - "admin.inventory.assets.view", - "admin.inventory.asset-maintenance.view", - "admin.inventory.asset-lifecycle.view", - "admin.inventory.asset-assignments.view", + "admin.shipping.orders.view", + "admin.shipping.tracking.view", + "admin.shipping.carriers.view", + "admin.shipping.rates.view", + "admin.assets.assets.view", + "admin.assets.maintenance.view", + "admin.assets.lifecycle.view", + "admin.assets.assignments.view", "admin.projects.dashboard.view", "admin.projects.view", "admin.projects.create", diff --git a/database/migrations/2024_12_14_082234_create_settings_table.php b/database/migrations/2024_12_14_082234_create_settings_table.php index db08618..8199913 100644 --- a/database/migrations/2024_12_14_082234_create_settings_table.php +++ b/database/migrations/2024_12_14_082234_create_settings_table.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Facades\DB; return new class extends Migration { @@ -12,18 +13,50 @@ return new class extends Migration public function up(): void { Schema::create('settings', function (Blueprint $table) { - $table->mediumIncrements('id'); + $table->smallIncrements('id'); + // Clave del setting $table->string('key')->index(); - $table->text('value'); + + // Categoría (opcional pero recomendable) + $table->string('category')->nullable()->index(); + + // Usuario (null para globales) $table->unsignedMediumInteger('user_id')->nullable()->index(); - // Unique constraints - $table->unique(['user_id', 'key']); + // Valores segmentados por tipo para mejor rendimiento + $table->string('value_string')->nullable(); + $table->integer('value_integer')->nullable(); + $table->boolean('value_boolean')->nullable(); + $table->float('value_float', 16, 8)->nullable(); + $table->text('value_text')->nullable(); + $table->binary('value_binary')->nullable(); + $table->string('mime_type', 50)->nullable(); + $table->string('file_name')->nullable(); + + // Auditoría + $table->timestamps(); + $table->unsignedMediumInteger('updated_by')->nullable(); + + // Unique constraint para evitar duplicados + $table->unique(['key', 'user_id', 'category']); // Relaciones - $table->foreign('user_id')->references('id')->on('users'); + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); }); + + // Agregar columna virtual unificada + DB::statement("ALTER TABLE settings ADD COLUMN value VARCHAR(255) GENERATED ALWAYS AS ( + CASE + WHEN value_string IS NOT NULL THEN value_string + WHEN value_integer IS NOT NULL THEN CAST(value_integer AS CHAR) + WHEN value_boolean IS NOT NULL THEN IF(value_boolean, 'true', 'false') + WHEN value_float IS NOT NULL THEN CAST(value_float AS CHAR) + WHEN value_text IS NOT NULL THEN LEFT(value_text, 255) + WHEN value_binary IS NOT NULL THEN '[binary_data]' + ELSE NULL + END + ) VIRTUAL"); } /** diff --git a/database/migrations/2024_12_14_083409_create_media_items_table.php b/database/migrations/2024_12_14_083409_create_media_items_table.php deleted file mode 100644 index aa1a5cb..0000000 --- a/database/migrations/2024_12_14_083409_create_media_items_table.php +++ /dev/null @@ -1,48 +0,0 @@ -mediumIncrements('id'); - - // Relación polimórfica - $table->unsignedMediumInteger('mediaable_id'); - $table->string('mediaable_type'); - - $table->unsignedTinyInteger('type')->index(); // Tipo de medio: 'image', 'video', 'file', 'youtube' - $table->unsignedTinyInteger('sub_type')->index(); // Subtipo de medio: 'thumbnail', 'main', 'additional' - - $table->string('url', 255)->nullable(); // URL del medio - $table->string('path')->nullable(); // Ruta del archivo si está almacenado localmente - - $table->string('title')->nullable()->index(); // Título del medio - $table->mediumText('description')->nullable(); // Descripción del medio - $table->unsignedTinyInteger('order')->nullable(); // Orden de presentación - - // Authoría - $table->timestamps(); - - // Índices - $table->index(['mediaable_type', 'mediaable_id']); - $table->index(['mediaable_type', 'mediaable_id', 'type']); - }); - - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('images'); - } -}; diff --git a/database/migrations/2024_12_14_092026_create_audits_table.php b/database/migrations/2024_12_14_162326_create_audits_table.php similarity index 100% rename from database/migrations/2024_12_14_092026_create_audits_table.php rename to database/migrations/2024_12_14_162326_create_audits_table.php diff --git a/database/seeders/SettingSeeder.php b/database/seeders/SettingSeeder.php deleted file mode 100644 index 75951a9..0000000 --- a/database/seeders/SettingSeeder.php +++ /dev/null @@ -1,29 +0,0 @@ - $value) { - Setting::create([ - 'key' => $key, - 'value' => $value, - ]); - }; - } -} diff --git a/resources/assets/js/bootstrap-table/bootstrapTableManager.js b/resources/assets/js/bootstrap-table/bootstrapTableManager.js index 9e272c0..b8f8c94 100644 --- a/resources/assets/js/bootstrap-table/bootstrapTableManager.js +++ b/resources/assets/js/bootstrap-table/bootstrapTableManager.js @@ -106,7 +106,8 @@ class BootstrapTableManager { * Carga los formatters dinámicamente */ async loadFormatters() { - const formattersModules = import.meta.glob('../../../../../**/resources/assets/js/bootstrap-table/*Formatters.js'); + //const formattersModules = import.meta.glob('../../../../../**/resources/assets/js/bootstrap-table/*Formatters.js'); + const formattersModules = import.meta.glob('/vendor/koneko/laravel-vuexy-admin/resources/assets/js/bootstrap-table/*Formatters.js'); const formatterPromises = Object.entries(formattersModules).map(async ([path, importer]) => { const module = await importer(); diff --git a/resources/assets/js/bootstrap-table/globalFormatters.js b/resources/assets/js/bootstrap-table/globalFormatters.js index 909c5ab..30e157f 100644 --- a/resources/assets/js/bootstrap-table/globalFormatters.js +++ b/resources/assets/js/bootstrap-table/globalFormatters.js @@ -1,5 +1,5 @@ import { booleanStatusCatalog, statusIntBadgeBgCatalogCss, statusIntBadgeBgCatalog } from './globalConfig'; -import {routes} from '../../../../../laravel-vuexy-admin/resources/assets/js/bootstrap-table/globalConfig.js'; +import {routes} from '@vuexy-admin/bootstrap-table/globalConfig.js'; export const userActionFormatter = (value, row, index) => { if (!row.id) return ''; diff --git a/resources/assets/js/config.js b/resources/assets/js/config.js index a1316d7..944d5e6 100644 --- a/resources/assets/js/config.js +++ b/resources/assets/js/config.js @@ -10,7 +10,7 @@ // JS global variables window.config = { colors: { - primary: '#7367f0', + primary: '#155dfc', secondary: '#808390', success: '#28c76f', info: '#00bad1', @@ -27,7 +27,7 @@ window.config = { borderColor: '#e6e6e8' }, colors_label: { - primary: '#7367f029', + primary: '#155dfc29', secondary: '#a8aaae29', success: '#28c76f29', info: '#00cfe829', diff --git a/resources/assets/js/forms/disabledForm.js b/resources/assets/js/forms/disabledForm.js new file mode 100644 index 0000000..39b7900 --- /dev/null +++ b/resources/assets/js/forms/disabledForm.js @@ -0,0 +1,79 @@ +/** + * Previene interacción con el elemento. + * @param {Event} event - El evento de clic. + */ +const preventInteraction = (event) => event.preventDefault(); + +/** + * Habilita o deshabilita un select con Select2. + * @param {HTMLElement} selectElement - El select afectado. + * @param {boolean} disabled - Si debe ser deshabilitado. + */ +const toggleSelect2Disabled = (selectElement, disabled) => { + selectElement.disabled = disabled; + selectElement.dispatchEvent(new Event('change', { bubbles: true })); +}; + +/** + * Aplica modo solo lectura a un select estándar o Select2. + * @param {HTMLElement} select - El select a modificar. + * @param {boolean} readonly - Si debe estar en modo solo lectura. + */ +const setSelectReadonly = (select, readonly) => { + select.setAttribute('readonly-mode', readonly); + select.style.pointerEvents = readonly ? 'none' : ''; + select.tabIndex = readonly ? -1 : ''; + + if (select.classList.contains('select2-hidden-accessible')) { + toggleSelect2Disabled(select, readonly); + } +}; + +/** + * Aplica modo solo lectura a un checkbox o radio. + * @param {HTMLElement} checkbox - El input a modificar. + * @param {boolean} readonly - Si debe ser solo lectura. + */ +const setCheckboxReadonly = (checkbox, readonly) => { + checkbox.setAttribute('readonly-mode', readonly); + checkbox.style.pointerEvents = readonly ? 'none' : ''; + checkbox[readonly ? 'addEventListener' : 'removeEventListener']('click', preventInteraction); +}; + +/** + * Deshabilita o pone en modo de solo lectura los campos del formulario. + * @param {string} formSelector - Selector del formulario a deshabilitar. + */ +export default function disableForm(formSelector) { + const form = document.querySelector(formSelector); + if (!form) { + console.warn(`Formulario no encontrado con el selector: ${formSelector}`); + return; + } + + const inputs = form.querySelectorAll('input, textarea, select'); + + inputs.forEach((el) => { + if (!el.classList.contains('data-always-enabled')) { + switch (el.tagName) { + case 'SELECT': + if (el.classList.contains('select2')) { + toggleSelect2Disabled(el, true); + } else { + setSelectReadonly(el, true); + } + break; + case 'INPUT': + if (['checkbox', 'radio'].includes(el.type)) { + setCheckboxReadonly(el, true); + } else { + el.readOnly = true; + } + break; + case 'TEXTAREA': + el.readOnly = true; + break; + } + } + }); +} diff --git a/resources/assets/js/forms/formCustomListener.js b/resources/assets/js/forms/formCustomListener.js index 644a7d3..7b65e42 100644 --- a/resources/assets/js/forms/formCustomListener.js +++ b/resources/assets/js/forms/formCustomListener.js @@ -155,7 +155,19 @@ export default class FormCustomListener { this.setButtonLoadingState(saveButton, true); // Poner en estado de carga al botón anfitrión // Enviar la solicitud de Livewire correspondiente al enviar el formulario - Livewire.dispatch(this.config.dispatchOnSubmit); + const componentEl = form.closest('[wire\\:id]'); + const componentId = componentEl?.getAttribute('wire:id'); + + if (componentId) { + const component = Livewire.find(componentId); + if (component) { + component.call(this.config.dispatchOnSubmit); + } else { + console.warn('No se encontró el componente Livewire.'); + } + } else { + console.warn('No se pudo encontrar wire:id para ejecutar el método Livewire.'); + } } /** @@ -188,7 +200,10 @@ export default class FormCustomListener { */ toggleButtonsState(buttons, isEnabled) { buttons.forEach(button => { - if (button) button.disabled = !isEnabled; + if (button){ + button.disabled = !isEnabled; + button.classList.toggle('disabled', !isEnabled); + } }); } @@ -217,24 +232,57 @@ export default class FormCustomListener { */ initializeValidation(form) { if (this.config.validationConfig) { - this.formValidationInstance = FormValidation.formValidation(form, this.config.validationConfig).on( - 'core.form.valid', - () => this.handleFormValid(form) - ); + this.formValidationInstance = FormValidation.formValidation( + form, + this.config.validationConfig + ).on('core.form.valid', () => this.handleFormValid(form)); + + // Aplicamos el fix después de un pequeño delay + setTimeout(() => { + this.fixValidationMessagePosition(form); + }, 100); // Lo suficiente para esperar a que FV inserte los mensajes } } + /** + * Mueve los mensajes de error fuera del input-group para evitar romper el diseño + */ + fixValidationMessagePosition(form) { + const groups = form.querySelectorAll('.input-group.has-validation'); + + groups.forEach(group => { + const errorContainer = group.querySelector('.fv-plugins-message-container'); + + if (errorContainer) { + // Evita duplicados + if (errorContainer.classList.contains('moved')) return; + + // Crear un contenedor si no existe + let target = group.parentElement.querySelector('.fv-message'); + if (!target) { + target = document.createElement('div'); + target.className = 'fv-message invalid-feedback'; + group.parentElement.appendChild(target); + } + + target.appendChild(errorContainer); + errorContainer.classList.add('moved'); // Marcar como ya movido + } + }); + } + reloadValidation() { const form = document.querySelector(this.config.formSelector); - // Verificar si el formulario existe y si la validación está inicializada if (form && this.formValidationInstance) { try { - // En lugar de destruir la validación, simplemente reiniciamos la validación. - this.formValidationInstance.resetForm(); // Resetear el formulario, limpiando los errores + setTimeout(() => { + this.formValidationInstance.resetForm(); // Limpiar errores + this.initializeValidation(form); // Reinicializar - // Reinicializar la validación con la configuración actual - this.initializeValidation(form); + // 🔁 Reconectar eventos de inputs y botones + this.initFormEvents(form); + }, 1); } catch (error) { console.error('Error al reiniciar la validación:', error); } @@ -242,4 +290,5 @@ export default class FormCustomListener { console.warn('Formulario no encontrado o instancia de validación no disponible.'); } } + } diff --git a/resources/assets/js/forms/slugify.js b/resources/assets/js/forms/slugify.js new file mode 100644 index 0000000..00b9b1c --- /dev/null +++ b/resources/assets/js/forms/slugify.js @@ -0,0 +1,10 @@ +window.createSlug = function(string) { + return string + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9\s-]/g, '') + .trim() + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); +}; diff --git a/resources/assets/js/livewire/registerLivewireHookOnce.js b/resources/assets/js/livewire/registerLivewireHookOnce.js new file mode 100644 index 0000000..dbcc686 --- /dev/null +++ b/resources/assets/js/livewire/registerLivewireHookOnce.js @@ -0,0 +1,27 @@ +/** + * Registra un hook Livewire solo una vez por componente. + * @param {string} hookName - Nombre del hook Livewire (por ejemplo, "morphed"). + * @param {string} componentName - Nombre exacto del componente Livewire. + * @param {function} callback - Función que se ejecutará una vez por hook+componente. + */ +export default function registerLivewireHookOnce(hookName, componentName, callback) { + if (!hookName || !componentName || typeof callback !== 'function') { + console.warn('[registerLivewireHookOnce] Parámetros inválidos.'); + return; + } + + // Clave única para este hook+componente + const safeName = componentName.replace(/[^a-zA-Z0-9]/g, '_'); + const key = `__hook_${hookName}_${safeName}`; + + if (!window[key]) { + window[key] = true; + + Livewire.hook(hookName, ({ component }) => { + if (component.name === componentName) { + // console.info(`[Livewire Hook Triggered] ${hookName} for ${component.name}`); + callback(component); + } + }); + } +} diff --git a/resources/assets/js/maps/LeafletMapHelper.js b/resources/assets/js/maps/LeafletMapHelper.js deleted file mode 100644 index 4821267..0000000 --- a/resources/assets/js/maps/LeafletMapHelper.js +++ /dev/null @@ -1,133 +0,0 @@ -import './../../vendor/libs/leaflet/leaflet' - -export const LeafletMapHelper = (() => { - let mapInstance, markerInstance; - - const DEFAULT_COORDS = [19.4326, -99.1332]; // Coordenadas de CDMX por defecto - - // Valida coordenadas - const isValidCoordinate = (lat, lng) => { - return lat && !isNaN(lat) && lat >= -90 && lat <= 90 && lat !== 0 && - lng && !isNaN(lng) && lng >= -180 && lng <= 180 && lng !== 0; - }; - - // Crea opciones del mapa según el modo - const getMapOptions = (mode) => ({ - scrollWheelZoom: mode !== 'delete', - dragging: mode !== 'delete', - doubleClickZoom: mode !== 'delete', - boxZoom: mode !== 'delete', - keyboard: mode !== 'delete', - zoomControl: mode !== 'delete', - touchZoom: mode !== 'delete' - }); - - // Destruir el mapa existente - const destroyMap = () => { - if (mapInstance) { - mapInstance.off(); - mapInstance.remove(); - mapInstance = null; - } - removeMarker(); - }; - - // Crear marcador en el mapa - const createMarker = (lat, lng, draggable = false, onDragEnd) => { - if (isValidCoordinate(lat, lng)) { - markerInstance = L.marker([lat, lng], { draggable }).addTo(mapInstance) - .bindPopup('Ubicación seleccionada').openPopup(); - - if (draggable && onDragEnd) { - markerInstance.on('dragend', (e) => { - const { lat, lng } = e.target.getLatLng(); - onDragEnd(lat, lng); - }); - } - } - }; - - // Eliminar marcador - const removeMarker = () => { - if (markerInstance) { - markerInstance.remove(); - markerInstance = null; - } - }; - - // Actualizar coordenadas en formulario - const updateCoordinates = (lat, lng, latSelector, lngSelector, livewireInstance) => { - const latInput = document.querySelector(latSelector); - const lngInput = document.querySelector(lngSelector); - - if (!latInput || !lngInput) { - console.warn(`⚠️ No se encontró el elemento del DOM para latitud (${latSelector}) o longitud (${lngSelector})`); - return; - } - - latInput.value = lat ? lat.toFixed(6) : ''; - lngInput.value = lng ? lng.toFixed(6) : ''; - - if (livewireInstance) { - livewireInstance.lat = lat ? lat.toFixed(6) : null; - livewireInstance.lng = lng ? lng.toFixed(6) : null; - } - }; - - // Inicializar el mapa - const initializeMap = (locationInputs, mode = 'create', livewireInstance = null) => { - const mapElement = document.getElementById(locationInputs.mapId); - - if (!mapElement) { - console.error(`❌ No se encontró el contenedor del mapa con ID: ${locationInputs.mapId}`); - return; - } - - let latElement = document.querySelector(locationInputs.lat); - let lngElement = document.querySelector(locationInputs.lng); - - if (!latElement || !lngElement) { - console.error(`❌ No se encontraron los campos de latitud (${locationInputs.lat}) o longitud (${locationInputs.lng})`); - return; - } - - let lat = parseFloat(latElement.value); - let lng = parseFloat(lngElement.value); - - const mapCoords = isValidCoordinate(lat, lng) ? [lat, lng] : DEFAULT_COORDS; - const zoomLevel = isValidCoordinate(lat, lng) ? 16 : 13; - - if (mapInstance) destroyMap(); - - mapInstance = L.map(locationInputs.mapId, getMapOptions(mode)).setView(mapCoords, zoomLevel); - L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png').addTo(mapInstance); - - if (mode !== 'create') createMarker(lat, lng, mode === 'edit', (lat, lng) => updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance)); - - if (mode !== 'delete') { - mapInstance.on('click', (e) => { - const { lat, lng } = e.latlng; - removeMarker(); - createMarker(lat, lng, true, (lat, lng) => updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance)); - updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance); - }); - } - - /* - const btnClearElement = document.querySelector(locationInputs.btnClear); - - if(!btnClearElement){ - console.error(`❌ No se encontró el botón de limpiar con ID: ${locationInputs.btnClear}`);return; - } - */ - }; - - return { - initializeMap, - clearCoordinates: () => { - removeMarker(); - }, - }; -})(); - -window.LeafletMapHelper = LeafletMapHelper; diff --git a/resources/assets/js/maps/LocationIQSearchHelper.js b/resources/assets/js/maps/LocationIQSearchHelper.js deleted file mode 100644 index 18941d3..0000000 --- a/resources/assets/js/maps/LocationIQSearchHelper.js +++ /dev/null @@ -1,12 +0,0 @@ -export class LocationIQSearchHelper { - constructor(apiKey) { - this.apiKey = apiKey; - this.baseUrl = 'https://us1.locationiq.com/v1/search.php'; - } - - async searchAddress(query) { - const response = await fetch(`${this.baseUrl}?key=${this.apiKey}&q=${query}&format=json`); - if (!response.ok) throw new Error('Error al buscar la dirección'); - return await response.json(); - } -} diff --git a/resources/assets/vendor/scss/_bootstrap-extended/_variables-dark.scss b/resources/assets/vendor/scss/_bootstrap-extended/_variables-dark.scss index 079a183..687cdb4 100644 --- a/resources/assets/vendor/scss/_bootstrap-extended/_variables-dark.scss +++ b/resources/assets/vendor/scss/_bootstrap-extended/_variables-dark.scss @@ -39,7 +39,7 @@ $grays: ( // scss-docs-start color-variables $blue: #007bff !default; $indigo: #6610f2 !default; -$purple: #7367f0 !default; +$purple: #64748b !default; $pink: #e83e8c !default; $red: #ff4c51 !default; $orange: #fd7e14 !default; diff --git a/resources/assets/vendor/scss/_bootstrap-extended/_variables.scss b/resources/assets/vendor/scss/_bootstrap-extended/_variables.scss index 7dc629d..9b558d5 100644 --- a/resources/assets/vendor/scss/_bootstrap-extended/_variables.scss +++ b/resources/assets/vendor/scss/_bootstrap-extended/_variables.scss @@ -35,7 +35,7 @@ $grays: ( // scss-docs-start color-variables $blue: #007bff !default; $indigo: #6610f2 !default; -$purple: #7367f0 !default; +$purple: #64748b !default; $pink: #e83e8c !default; $red: #ff4c51 !default; $orange: #fd7e14 !default; diff --git a/resources/assets/vendor/scss/core-dark.scss b/resources/assets/vendor/scss/core-dark.scss index 7233d20..c3af6ac 100644 --- a/resources/assets/vendor/scss/core-dark.scss +++ b/resources/assets/vendor/scss/core-dark.scss @@ -2,3 +2,5 @@ @import 'bootstrap-extended-dark'; @import 'components-dark'; @import 'colors-dark'; + +$primary-color: #4a8c08; diff --git a/resources/assets/vendor/scss/core.scss b/resources/assets/vendor/scss/core.scss index dae0761..20adc89 100644 --- a/resources/assets/vendor/scss/core.scss +++ b/resources/assets/vendor/scss/core.scss @@ -2,3 +2,5 @@ @import 'bootstrap-extended'; @import 'components'; @import 'colors'; + +$primary-color: #4a8c08; diff --git a/resources/assets/vendor/scss/theme-bordered-dark.scss b/resources/assets/vendor/scss/theme-bordered-dark.scss index dcaa37a..05e68cc 100644 --- a/resources/assets/vendor/scss/theme-bordered-dark.scss +++ b/resources/assets/vendor/scss/theme-bordered-dark.scss @@ -4,7 +4,7 @@ @import './_theme/pages'; @import './_theme/_theme'; -$primary-color: #7367f0; +$primary-color: #155dfc; body { background: $card-bg; diff --git a/resources/assets/vendor/scss/theme-bordered.scss b/resources/assets/vendor/scss/theme-bordered.scss index 2676efc..26ec332 100644 --- a/resources/assets/vendor/scss/theme-bordered.scss +++ b/resources/assets/vendor/scss/theme-bordered.scss @@ -4,7 +4,7 @@ @import './_theme/pages'; @import './_theme/_theme'; -$primary-color: #7367f0; +$primary-color: #155dfc; $body-bg: #f8f7fa; body { diff --git a/resources/assets/vendor/scss/theme-default-dark.scss b/resources/assets/vendor/scss/theme-default-dark.scss index b2478e0..fe57900 100644 --- a/resources/assets/vendor/scss/theme-default-dark.scss +++ b/resources/assets/vendor/scss/theme-default-dark.scss @@ -4,7 +4,7 @@ @import './_theme/pages'; @import './_theme/_theme'; -$primary-color: #7367f0; +$primary-color: #155dfc; body { background: $body-bg; diff --git a/resources/assets/vendor/scss/theme-default.scss b/resources/assets/vendor/scss/theme-default.scss index 27b48d3..7c97e0f 100644 --- a/resources/assets/vendor/scss/theme-default.scss +++ b/resources/assets/vendor/scss/theme-default.scss @@ -4,7 +4,7 @@ @import './_theme/pages'; @import './_theme/_theme'; -$primary-color: #7367f0; +$primary-color: #155dfc; $body-bg: #f8f7fa; body { diff --git a/resources/assets/vendor/scss/theme-semi-dark-dark.scss b/resources/assets/vendor/scss/theme-semi-dark-dark.scss index eb51502..0ec66a8 100644 --- a/resources/assets/vendor/scss/theme-semi-dark-dark.scss +++ b/resources/assets/vendor/scss/theme-semi-dark-dark.scss @@ -4,7 +4,7 @@ @import './_theme/pages'; @import './_theme/_theme'; -$primary-color: #7367f0; +$primary-color: #155dfc; body { background: $body-bg; diff --git a/resources/assets/vendor/scss/theme-semi-dark.scss b/resources/assets/vendor/scss/theme-semi-dark.scss index e135b10..99aea38 100644 --- a/resources/assets/vendor/scss/theme-semi-dark.scss +++ b/resources/assets/vendor/scss/theme-semi-dark.scss @@ -4,7 +4,7 @@ @import './_theme/pages'; @import './_theme/_theme'; -$primary-color: #7367f0; +$primary-color: #155dfc; $body-bg: #f8f7fa; body { diff --git a/resources/js/app.js b/resources/js/app.js index 5c3ea25..e69de29 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,83 +0,0 @@ -/** - * Deshabilita o pone en modo de solo lectura los campos del formulario. - * @param {string} formSelector - Selector del formulario a deshabilitar. - */ -const disableStoreForm = (formSelector) => { - const form = document.querySelector(formSelector); - if (!form) { - console.warn(`Formulario no encontrado con el selector: ${formSelector}`); - return; - } - - /** - * Habilita o deshabilita un select con Select2. - * @param {HTMLElement} selectElement - El select afectado. - * @param {boolean} disabled - Si debe ser deshabilitado. - */ - const toggleSelect2Disabled = (selectElement, disabled) => { - selectElement.disabled = disabled; - selectElement.dispatchEvent(new Event('change', { bubbles: true })); - }; - - /** - * Aplica modo solo lectura a un select estándar o Select2. - * @param {HTMLElement} select - El select a modificar. - * @param {boolean} readonly - Si debe estar en modo solo lectura. - */ - const setSelectReadonly = (select, readonly) => { - select.setAttribute('readonly-mode', readonly); - select.style.pointerEvents = readonly ? 'none' : ''; - select.tabIndex = readonly ? -1 : ''; - - if (select.classList.contains('select2-hidden-accessible')) { - toggleSelect2Disabled(select, readonly); - } - }; - - /** - * Aplica modo solo lectura a un checkbox o radio. - * @param {HTMLElement} checkbox - El input a modificar. - * @param {boolean} readonly - Si debe ser solo lectura. - */ - const setCheckboxReadonly = (checkbox, readonly) => { - checkbox.setAttribute('readonly-mode', readonly); - checkbox.style.pointerEvents = readonly ? 'none' : ''; - checkbox[readonly ? 'addEventListener' : 'removeEventListener']('click', preventInteraction); - }; - - /** - * Previene interacción con el elemento. - * @param {Event} event - El evento de clic. - */ - const preventInteraction = (event) => event.preventDefault(); - - // Obtener todos los inputs del formulario - const inputs = form.querySelectorAll('input, textarea, select'); - - inputs.forEach((el) => { - if (!el.classList.contains('data-always-enabled')) { - switch (el.tagName) { - case 'SELECT': - if (el.classList.contains('select2')) { - toggleSelect2Disabled(el, true); - } else { - setSelectReadonly(el, true); - } - break; - case 'INPUT': - if (['checkbox', 'radio'].includes(el.type)) { - setCheckboxReadonly(el, true); - } else { - el.readOnly = true; - } - break; - case 'TEXTAREA': - el.readOnly = true; - break; - } - } - }); -}; - -// Hacer la función accesible globalmente -window.disableStoreForm = disableStoreForm; diff --git a/resources/js/pages/admin-settings-scripts.js b/resources/js/pages/admin-settings-scripts.js index 4061713..16c59e4 100644 --- a/resources/js/pages/admin-settings-scripts.js +++ b/resources/js/pages/admin-settings-scripts.js @@ -4,24 +4,3 @@ import FormCustomListener from '../../assets/js/forms/formCustomListener'; new FormCustomListener({ buttonSelectors: ['.btn-save', '.btn-cancel', '.btn-reset'] // Selectores para botones }); - -Livewire.on('clearLocalStoregeTemplateCustomizer', event => { - const _deleteCookie = name => { - document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - }; - - const pattern = 'templateCustomizer-'; - - // Iterar sobre todas las claves en localStorage - Object.keys(localStorage).forEach(key => { - if (key.startsWith(pattern)) { - localStorage.removeItem(key); - } - }); - - _deleteCookie('admin-mode'); - _deleteCookie('admin-colorPref'); - _deleteCookie('colorPref'); - _deleteCookie('theme'); - _deleteCookie('direction'); -}); diff --git a/resources/js/pages/smtp-settings-scripts.js b/resources/js/pages/smtp-settings-scripts.js index f616c46..6751610 100644 --- a/resources/js/pages/smtp-settings-scripts.js +++ b/resources/js/pages/smtp-settings-scripts.js @@ -7,7 +7,7 @@ window.senderResponseForm = new SenderResponseForm(); Livewire.hook('morphed', ({ component }) => { switch (component.name) { - case 'mail-smtp-settings': + case 'sendmail-settings': if (window.smtpSettingsForm) { window.smtpSettingsForm.reload(); // Recarga el formulario sin destruir la instancia } diff --git a/resources/js/smtp-settings/SmtpSettingsForm.js b/resources/js/smtp-settings/SmtpSettingsForm.js index 8704bf1..a2b4419 100644 --- a/resources/js/smtp-settings/SmtpSettingsForm.js +++ b/resources/js/smtp-settings/SmtpSettingsForm.js @@ -1,7 +1,7 @@ export default class SmtpSettingsForm { constructor(config = {}) { const defaultConfig = { - formSmtpSettingsSelector: '#mail-smtp-settings-card', + formSmtpSettingsSelector: '#sendmail-settings-card', changeSmtpSettingsId: 'change_smtp_settings', testSmtpConnectionButtonId: 'test_smtp_connection_button', saveSmtpConnectionButtonId: 'save_smtp_connection_button', diff --git a/resources/scss/app.scss b/resources/scss/app.scss index 9b45e49..3e51f4c 100644 --- a/resources/scss/app.scss +++ b/resources/scss/app.scss @@ -2,6 +2,10 @@ @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; +[x-cloak] { + display: none !important; +} + .menu-horizontal-wrapper > .menu-inner > .menu-item:last-child { padding-right: 70px; } diff --git a/resources/scss/pages/page-auth.scss b/resources/scss/pages/page-auth.scss index 00dc08e..d3603d8 100644 --- a/resources/scss/pages/page-auth.scss +++ b/resources/scss/pages/page-auth.scss @@ -85,7 +85,7 @@ $authentication-1-inner-max-width: 460px !default; position: absolute; top: -35px; left: -45px; - background-image: url("data:image/svg+xml,%3Csvg width='239' height='234' viewBox='0 0 239 234' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='88.5605' y='0.700195' width='149' height='149' rx='19.5' stroke='%237367F0' stroke-opacity='0.16'/%3E%3Crect x='0.621094' y='33.761' width='200' height='200' rx='10' fill='%237367F0' fill-opacity='0.08'/%3E%3C/svg%3E%0A"); + background-image: url("data:image/svg+xml,%3Csvg width='239' height='234' viewBox='0 0 239 234' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='88.5605' y='0.700195' width='149' height='149' rx='19.5' stroke='%23155dfc' stroke-opacity='0.16'/%3E%3Crect x='0.621094' y='33.761' width='200' height='200' rx='10' fill='%23155dfc' fill-opacity='0.08'/%3E%3C/svg%3E%0A"); } &:after { @include light.media-breakpoint-down(sm) { @@ -98,7 +98,7 @@ $authentication-1-inner-max-width: 460px !default; z-index: -1; bottom: -30px; right: -56px; - background-image: url("data:image/svg+xml,%3Csvg width='181' height='181' viewBox='0 0 181 181' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1.30469' y='1.44312' width='178' height='178' rx='19' stroke='%237367F0' stroke-opacity='0.16' stroke-width='2' stroke-dasharray='8 8'/%3E%3Crect x='22.8047' y='22.9431' width='135' height='135' rx='10' fill='%237367F0' fill-opacity='0.08'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml,%3Csvg width='181' height='181' viewBox='0 0 181 181' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1.30469' y='1.44312' width='178' height='178' rx='19' stroke='%23155dfc' stroke-opacity='0.16' stroke-width='2' stroke-dasharray='8 8'/%3E%3Crect x='22.8047' y='22.9431' width='135' height='135' rx='10' fill='%23155dfc' fill-opacity='0.08'/%3E%3C/svg%3E"); } } diff --git a/resources/views/admin-settings/webapp-general-settings.blade.php b/resources/views/admin-settings/webapp-general-settings.blade.php deleted file mode 100644 index 5794f2a..0000000 --- a/resources/views/admin-settings/webapp-general-settings.blade.php +++ /dev/null @@ -1,30 +0,0 @@ -@extends('vuexy-admin::layouts.vuexy.layoutMaster') - -@section('title', 'Ajustes generales') - -@push('page-script') - @vite('vendor/koneko/laravel-vuexy-admin/resources/js/pages/admin-settings-scripts.js') -@endpush - -@section('content') -
-
- -
- @livewire('application-settings') -
-
-
- -
- @livewire('general-settings') -
-
-
- -
- @livewire('interface-settings') -
-
-
-@endsection diff --git a/resources/views/cache-manager/index.blade.php b/resources/views/cache-manager/index.blade.php index 8350c85..ccf8371 100644 --- a/resources/views/cache-manager/index.blade.php +++ b/resources/views/cache-manager/index.blade.php @@ -10,28 +10,28 @@
- @livewire('cache-stats') + @livewire('vuexy-admin::cache-stats')
- @livewire('session-stats') + @livewire('vuexy-admin::session-stats')
- @livewire('cache-functions') + @livewire('vuexy-admin::cache-functions')
@if($configCache['redisInUse'])
- @livewire('redis-stats') + @livewire('vuexy-admin::redis-stats')
@endif @if($configCache['memcachedInUse'])
- @livewire('memcached-stats') + @livewire('vuexy-admin::memcached-stats')
@endif diff --git a/resources/views/components/alert/basic.blade.php b/resources/views/components/alert/basic.blade.php new file mode 100644 index 0000000..cd4ae12 --- /dev/null +++ b/resources/views/components/alert/basic.blade.php @@ -0,0 +1,24 @@ +@props([ + 'type' => 'primary', // Tipos: primary, secondary, success, danger, warning, info, dark + 'outline' => false, + 'dismissible' => false, + 'icon' => null, +]) + +@php + $alertClass = $outline ? "alert-outline-{$type}" : "alert-{$type}"; +@endphp + + diff --git a/resources/views/components/button/index-off-canvas.blade.php b/resources/views/components/button/index-offcanvas.blade.php similarity index 100% rename from resources/views/components/button/index-off-canvas.blade.php rename to resources/views/components/button/index-offcanvas.blade.php diff --git a/resources/views/components/form/checkbox-group.blade.php b/resources/views/components/form/checkbox-group.blade.php index 59840ce..21d4642 100644 --- a/resources/views/components/form/checkbox-group.blade.php +++ b/resources/views/components/form/checkbox-group.blade.php @@ -71,7 +71,7 @@ @endphp {{-- ============================ CHECKBOX CON INPUT GROUP ============================ --}} -
+
@if ($label) @endif diff --git a/resources/views/components/form/checkbox.blade.php b/resources/views/components/form/checkbox.blade.php index 059efa1..cb2593f 100644 --- a/resources/views/components/form/checkbox.blade.php +++ b/resources/views/components/form/checkbox.blade.php @@ -74,7 +74,7 @@ @if ($switch) {{-- ============================ MODO SWITCH ============================ --}} -
+