Prepare modules

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

1
.gitignore vendored
View File

@ -8,3 +8,4 @@
/.phpunit.result.cache /.phpunit.result.cache
/.zed /.zed
/.idea /.idea
composer.lock

View File

@ -1,62 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Models\Setting;
use Koneko\VuexyAdmin\Services\VuexyAdminService;
class AdminController extends Controller
{
public function searchNavbar()
{
abort_if(!request()->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');
}
}

View File

@ -8,6 +8,13 @@ use Koneko\VuexyAdmin\Services\CacheConfigService;
class CacheController extends Controller class CacheController extends Controller
{ {
public function index(CacheConfigService $cacheConfigService)
{
$configCache = $cacheConfigService->getConfig();
return view('vuexy-admin::cache-manager.index', compact('configCache'));
}
public function generateConfigCache() public function generateConfigCache()
{ {
try { try {
@ -27,15 +34,9 @@ class CacheController extends Controller
Artisan::call('route:cache'); Artisan::call('route:cache');
return response()->json(['success' => true, 'message' => 'Cache de rutas generado correctamente.']); return response()->json(['success' => true, 'message' => 'Cache de rutas generado correctamente.']);
} catch (\Exception $e) { } catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Error al generar el cache de rutas.', 'error' => $e->getMessage()], 500); 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'));
}
} }

View File

@ -0,0 +1,75 @@
<?php
namespace Koneko\VuexyAdmin\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
use Koneko\VuexyAdmin\Queries\GenericQueryBuilder;
class GlobalSettingsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if ($request->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');
}
}

View File

@ -3,11 +3,8 @@
namespace Koneko\VuexyAdmin\Http\Controllers; namespace Koneko\VuexyAdmin\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Spatie\Permission\Models\Permission;
use Yajra\DataTables\Facades\DataTables;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Koneko\VuexyAdmin\Queries\GenericQueryBuilder;
class PermissionController extends Controller class PermissionController extends Controller
{ {
@ -19,17 +16,26 @@ class PermissionController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
if ($request->ajax()) { 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) return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson();
->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 view('vuexy-admin::permissions.index'); return view('vuexy-admin::permissions.index');

View File

@ -1,188 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Auth;
use Yajra\DataTables\Facades\DataTables;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Services\AvatarImageService;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if ($request->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'
]);
}
}
}

View File

@ -5,7 +5,7 @@ namespace Koneko\VuexyAdmin\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; 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\Models\User;
use Koneko\VuexyAdmin\Services\AvatarImageService; use Koneko\VuexyAdmin\Services\AvatarImageService;
use Koneko\VuexyAdmin\Queries\GenericQueryBuilder; use Koneko\VuexyAdmin\Queries\GenericQueryBuilder;
@ -24,97 +24,19 @@ class UserController extends Controller
'table' => 'users', 'table' => 'users',
'columns' => [ 'columns' => [
'users.id', 'users.id',
'users.code', 'users.name AS full_name',
DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS full_name"),
'users.email', 'users.email',
'users.birth_date', 'users.email_verified_at',
'users.hire_date',
'users.curp',
'users.nss',
'users.job_title',
'users.profile_photo_path', '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', 'users.status',
DB::raw("CONCAT_WS(' ', created.name, created.last_name) AS creator"), 'users.created_by',
'created.email AS creator_email',
'users.created_at', 'users.created_at',
'users.updated_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' => [ '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', 'default_sort_order' => 'asc',
]; ];
@ -160,9 +82,7 @@ class UserController extends Controller
//$user->stores()->attach($request->stores); //$user->stores()->attach($request->stores);
if ($request->file('photo')){ if ($request->file('photo')){
$avatarImageService = new AvatarImageService(); app(AvatarImageService::class)->updateProfilePhoto($user, $request->file('photo'));
$avatarImageService->updateProfilePhoto($user, $request->file('photo'));
} }
return response()->json(['success' => 'Se agrego correctamente el usuario']); return response()->json(['success' => 'Se agrego correctamente el usuario']);
@ -217,10 +137,9 @@ class UserController extends Controller
//$user->stores()->sync($request->stores); //$user->stores()->sync($request->stores);
// Actualizamos foto de perfil // Actualizamos foto de perfil
if ($request->file('photo')) if ($request->file('photo')){
$avatarImageService = new AvatarImageService(); app(AvatarImageService::class)->updateProfilePhoto($user, $request->file('photo'));
}
$avatarImageService->updateProfilePhoto($user, $request->file('photo'));
return response()->json(['success' => 'Se guardo correctamente los cambios.']); return response()->json(['success' => 'Se guardo correctamente los cambios.']);
} }

View File

@ -5,7 +5,6 @@ namespace Koneko\VuexyAdmin\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Koneko\VuexyAdmin\Services\AvatarInitialsService; use Koneko\VuexyAdmin\Services\AvatarInitialsService;
use Koneko\VuexyAdmin\Models\User;
class UserProfileController extends Controller class UserProfileController extends Controller
{ {
@ -35,10 +34,8 @@ class UserProfileController extends Controller
$background = $request->get('background', 'EBF4FF'); $background = $request->get('background', 'EBF4FF');
$size = $request->get('size', 100); $size = $request->get('size', 100);
$avatarService = new AvatarInitialsService();
try { try {
return $avatarService->getAvatarImage($name, $color, $background, $size); return app(AvatarInitialsService::class)->getAvatarImage($name, $color, $background, $size);
} catch (\Exception $e) { } catch (\Exception $e) {
// String base64 de una imagen PNG transparente de 1x1 píxel // String base64 de una imagen PNG transparente de 1x1 píxel

View File

@ -0,0 +1,100 @@
<?php
namespace Koneko\VuexyAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Models\Setting;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyAdmin\Services\VuexyAdminService;
/**
* Controlador para la gestión de funcionalidades administrativas de Vuexy
*/
class VuexyAdminController extends Controller
{
/**
* Muestra la vista de configuraciones generales
*
* @return \Illuminate\View\View
*/
public function GeneralSettings()
{
return view('vuexy-admin::general-settings.index');
}
/**
* Muestra la vista de configuraciones SMTP
*
* @return \Illuminate\View\View
*/
public function smtpSettings()
{
return view('vuexy-admin::sendmail-settings.index');
}
/**
* Muestra la vista de configuraciones de interfaz
*
* @return \Illuminate\View\View
*/
public function VuexyInterfaceSettings()
{
return view('vuexy-admin::interface-settings.index');
}
/**
* Realiza búsqueda en la barra de navegación
*
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Http\Exceptions\HttpResponseException
*/
public function searchNavbar()
{
abort_if(!request()->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();
}
}

View File

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

View File

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

View File

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

View File

@ -128,27 +128,6 @@ abstract class AbstractFormOffCanvasComponent extends Component
*/ */
abstract protected function model(): string; abstract protected function model(): string;
/**
* Define los campos del formulario.
*
* @return array<string, mixed>
*/
abstract protected function fields(): array;
/**
* Retorna los valores por defecto para los campos del formulario.
*
* @return array<string, mixed> Valores predeterminados.
*/
abstract protected function defaults(): array;
/**
* Campo que se debe enfocar cuando se abra el formulario.
*
* @return string
*/
abstract protected function focusOnOpen(): string;
/** /**
* Define reglas de validación dinámicas según el modo del formulario. * 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; abstract protected function dynamicRules(string $mode): array;
/**
* Devuelve las opciones que se mostrarán en los selectores del formulario.
*
* @return array<string, mixed> Opciones para los campos del formulario.
*/
abstract protected function options(): array;
/** /**
* Retorna la ruta de la vista asociada al formulario. * Retorna la ruta de la vista asociada al formulario.
* *
@ -171,6 +143,50 @@ abstract class AbstractFormOffCanvasComponent extends Component
*/ */
abstract protected function viewPath(): string; abstract protected function viewPath(): string;
// ===================== CONFIGURACIÓN =====================
/**
* Define los campos del formulario.
*
* @return array<string, mixed>
*/
protected function fields(): array
{
return (new ($this->model()))->getFillable();
}
/**
* Retorna los valores por defecto para los campos del formulario.
*
* @return array<string, mixed> 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<string, mixed> Opciones para los campos del formulario.
*/
protected function options(): array
{
return [];
}
// ===================== VALIDACIONES ===================== // ===================== VALIDACIONES =====================
protected function attributes(): array protected function attributes(): array
@ -198,7 +214,7 @@ abstract class AbstractFormOffCanvasComponent extends Component
$model = new ($this->model()); $model = new ($this->model());
$this->tagName = $model->tagName; $this->tagName = Str::camel($model->tagName);
$this->columnNameLabel = $model->columnNameLabel; $this->columnNameLabel = $model->columnNameLabel;
$this->singularName = $model->singularName; $this->singularName = $model->singularName;
$this->offcanvasId = 'offcanvas' . ucfirst(Str::camel($model->tagName)); $this->offcanvasId = 'offcanvas' . ucfirst(Str::camel($model->tagName));
@ -288,6 +304,9 @@ abstract class AbstractFormOffCanvasComponent extends Component
$model = $this->model()::find($id); $model = $this->model()::find($id);
if ($model) { if ($model) {
dd($this->fields());
$data = $model->only(['id', ...$this->fields()]); $data = $model->only(['id', ...$this->fields()]);
$this->applyCasts($data); $this->applyCasts($data);

View File

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

View File

@ -0,0 +1,160 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Permissions;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
use Illuminate\Validation\Rule;
use Spatie\Permission\Models\Permission;
/**
* Class PermissionOffcanvasForm
*
* Componente Livewire para gestionar permisos.
* Extiende AbstractFormOffCanvasComponent para proporcionar una interfaz
* eficiente para la gestión de permisos en el ERP.
*
* @package App\Http\Livewire\Forms
*/
class PermissionOffcanvasForm extends AbstractFormOffCanvasComponent
{
/**
* Propiedades del formulario.
*
* @var string|null
*/
public $id, $name, $group_name, $sub_group_name, $action, $guard_name = 'web';
/**
* Eventos de escucha de Livewire.
*
* @var array
*/
protected $listeners = [
'editPermission' => '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<string, string>
*/
protected function attributes(): array
{
return [
'name' => 'nombre del permiso',
];
}
/**
* Define los mensajes de error personalizados para la validación.
*
* @return array<string, string>
*/
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';
}
}

View File

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

View File

@ -0,0 +1,98 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Permissions;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
use Spatie\Permission\Models\Permission;
use Illuminate\Support\Facades\DB;
/**
* Listado de Permisos, extiende de la clase base AbstractIndexComponent
* para reutilizar la lógica de configuración y renderizado de tablas.
*/
class PermissionsIndex extends AbstractIndexComponent
{
/**
* Define la clase del modelo a usar en este Index.
*
* @return string
*/
protected function model(): string
{
return Permission::class;
}
/**
* Retorna las columnas de la tabla.
*
* @return array
*/
protected function columns(): array
{
return [
'action' => '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';
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Profile;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
use Livewire\Component;
use Livewire\WithFileUploads;
class DeleteUserForm extends Component
{
use WithFileUploads;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* The new avatar for the user.
*
* @var mixed
*/
public $photo;
/**
* Determine if the verification email was sent.
*
* @var bool
*/
public $verificationLinkSent = false;
/**
* Prepare the component.
*
* @return void
*/
public function mount()
{
$user = Auth::user();
$this->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');
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Profile;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
use Livewire\Component;
use Livewire\WithFileUploads;
class LogoutOtherBrowser extends Component
{
use WithFileUploads;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* The new avatar for the user.
*
* @var mixed
*/
public $photo;
/**
* Determine if the verification email was sent.
*
* @var bool
*/
public $verificationLinkSent = false;
/**
* Prepare the component.
*
* @return void
*/
public function mount()
{
$user = Auth::user();
$this->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');
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Profile;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
use Livewire\Component;
use Livewire\WithFileUploads;
class TwoFactorAuthenticationForm extends Component
{
use WithFileUploads;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* The new avatar for the user.
*
* @var mixed
*/
public $photo;
/**
* Determine if the verification email was sent.
*
* @var bool
*/
public $verificationLinkSent = false;
/**
* Prepare the component.
*
* @return void
*/
public function mount()
{
$user = Auth::user();
$this->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');
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Profile;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
use Livewire\Component;
use Livewire\WithFileUploads;
class UpdatePasswordForm extends Component
{
use WithFileUploads;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* The new avatar for the user.
*
* @var mixed
*/
public $photo;
/**
* Determine if the verification email was sent.
*
* @var bool
*/
public $verificationLinkSent = false;
/**
* Prepare the component.
*
* @return void
*/
public function mount()
{
$user = Auth::user();
$this->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');
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Profile;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
use Livewire\Component;
use Livewire\WithFileUploads;
class UpdateProfileInformationForm extends Component
{
use WithFileUploads;
/**
* The component's state.
*
* @var array
*/
public $state = [];
/**
* The new avatar for the user.
*
* @var mixed
*/
public $photo;
/**
* Determine if the verification email was sent.
*
* @var bool
*/
public $verificationLinkSent = false;
/**
* Prepare the component.
*
* @return void
*/
public function mount()
{
$user = Auth::user();
$this->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');
}
}

View File

@ -7,7 +7,7 @@ use Livewire\WithPagination;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;
class RoleIndex extends Component class RolesIndex extends Component
{ {
use WithPagination; use WithPagination;
@ -54,8 +54,8 @@ class RoleIndex extends Component
public function render() public function render()
{ {
return view('livewire.roles', [ return view('vuexy-admin::livewire.roles.index', [
'index' => Role::paginate(10) 'roles' => Role::paginate(10)
]); ]);
} }
} }

View File

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

View File

@ -2,14 +2,8 @@
namespace Koneko\VuexyAdmin\Livewire\Users; 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\Livewire\Form\AbstractFormOffCanvasComponent;
use Koneko\VuexyAdmin\Models\User; use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyContacts\Services\{ContactCatalogService,ConstanciaFiscalService,FacturaXmlService};
use Koneko\VuexyStoreManager\Services\StoreCatalogService;
use Livewire\WithFileUploads;
/** /**
* Class UserOffCanvasForm * Class UserOffCanvasForm
@ -22,40 +16,15 @@ use Livewire\WithFileUploads;
*/ */
class UserOffCanvasForm extends AbstractFormOffCanvasComponent class UserOffCanvasForm extends AbstractFormOffCanvasComponent
{ {
use WithFileUploads;
public $doc_file;
public $dropzoneVisible = true;
/** /**
* Propiedades del formulario relacionadas con el usuario. * Propiedades del formulario relacionadas con el usuario.
*/ */
public $code, public $code,
$parent_id,
$name, $name,
$last_name, $last_name,
$email, $email,
$company,
$rfc,
$nombre_fiscal,
$tipo_persona,
$c_regimen_fiscal,
$domicilio_fiscal,
$is_partner,
$is_employee,
$is_prospect,
$is_customer,
$is_provider,
$status; $status;
/**
* Listas de opciones para selects en el formulario.
*/
public $store_options = [],
$work_center_options = [],
$manager_options = [];
/** /**
* Eventos de escucha de Livewire. * Eventos de escucha de Livewire.
* *
@ -85,28 +54,6 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent
return User::class; return User::class;
} }
/**
* Define los campos del formulario.
*
* @return array<string, mixed>
*/
protected function fields(): array
{
return (new User())->getFillable();
}
/**
* Valores por defecto para el formulario.
*
* @return array
*/
protected function defaults(): array
{
return [
//
];
}
/** /**
* Campo que se debe enfocar cuando se abra el formulario. * Campo que se debe enfocar cuando se abra el formulario.
* *
@ -117,6 +64,8 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent
return 'name'; return 'name';
} }
// ===================== VALIDACIONES =====================
/** /**
* Define reglas de validación dinámicas basadas en el modo actual. * Define reglas de validación dinámicas basadas en el modo actual.
* *
@ -129,10 +78,8 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent
case 'create': case 'create':
case 'edit': case 'edit':
return [ 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'], 'name' => ['required', 'string', 'max:96'],
'notes' => ['nullable', 'string', 'max:1024'],
'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'],
]; ];
case 'delete': case 'delete':
@ -145,8 +92,6 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent
} }
} }
// ===================== VALIDACIONES =====================
/** /**
* Get custom attributes for validator errors. * Get custom attributes for validator errors.
* *
@ -168,121 +113,10 @@ class UserOffCanvasForm extends AbstractFormOffCanvasComponent
protected function messages(): array protected function messages(): array
{ {
return [ return [
'code.unique' => 'Este código ya está en uso por otro usuario.',
'name.required' => 'El nombre del usuario es obligatorio.', '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. * Ruta de la vista asociada con este formulario.
* *

View File

@ -40,7 +40,6 @@ class UserShow extends Component
$is_prospect, $is_prospect,
$is_customer, $is_customer,
$is_provider, $is_provider,
$is_user,
$status; $status;
public $deleteUserImage; public $deleteUserImage;
public $cuentaUsuarioAlert, public $cuentaUsuarioAlert,
@ -55,7 +54,6 @@ class UserShow extends Component
'is_prospect' => 'nullable|boolean', 'is_prospect' => 'nullable|boolean',
'is_customer' => 'nullable|boolean', 'is_customer' => 'nullable|boolean',
'is_provider' => 'nullable|boolean', 'is_provider' => 'nullable|boolean',
'is_user' => 'nullable|boolean',
'pricelist_id' => 'nullable|integer', 'pricelist_id' => 'nullable|integer',
'enable_credit' => 'nullable|boolean', 'enable_credit' => 'nullable|boolean',
'credit_days' => 'nullable|integer', 'credit_days' => 'nullable|integer',
@ -102,7 +100,6 @@ class UserShow extends Component
$this->is_prospect = $this->user->is_prospect? true : false; $this->is_prospect = $this->user->is_prospect? true : false;
$this->is_customer = $this->user->is_customer? true : false; $this->is_customer = $this->user->is_customer? true : false;
$this->is_provider = $this->user->is_provider? 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->pricelist_id = $this->user->pricelist_id;
$this->enable_credit = $this->user->enable_credit? true : false; $this->enable_credit = $this->user->enable_credit? true : false;
$this->credit_days = $this->user->credit_days; $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_prospect'] = $validatedData['is_prospect'] ? 1 : 0;
$validatedData['is_customer'] = $validatedData['is_customer'] ? 1 : 0; $validatedData['is_customer'] = $validatedData['is_customer'] ? 1 : 0;
$validatedData['is_provider'] = $validatedData['is_provider'] ? 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['pricelist_id'] = $validatedData['pricelist_id'] ?: null;
$validatedData['enable_credit'] = $validatedData['enable_credit'] ? 1 : 0; $validatedData['enable_credit'] = $validatedData['enable_credit'] ? 1 : 0;
$validatedData['credit_days'] = $validatedData['credit_days'] ?: null; $validatedData['credit_days'] = $validatedData['credit_days'] ?: null;
@ -150,7 +146,6 @@ class UserShow extends Component
$validatedData['cargo'] = null; $validatedData['cargo'] = null;
$validatedData['is_prospect'] = null; $validatedData['is_prospect'] = null;
$validatedData['is_provider'] = null; $validatedData['is_provider'] = null;
$validatedData['is_user'] = null;
$validatedData['enable_credit'] = null; $validatedData['enable_credit'] = null;
$validatedData['credit_days'] = null; $validatedData['credit_days'] = null;
$validatedData['credit_limit'] = null; $validatedData['credit_limit'] = null;

View File

@ -6,11 +6,11 @@ use Koneko\VuexyAdmin\Models\User;
use Livewire\Component; use Livewire\Component;
class UserCount extends Component class UsersCount extends Component
{ {
public $total, $enabled, $disabled; public $total, $enabled, $disabled;
protected $listeners = ['refreshUserCount' => 'updateCounts']; protected $listeners = ['refreshUsersCount' => 'updateCounts'];
public function mount() public function mount()
{ {

View File

@ -2,17 +2,11 @@
namespace Koneko\VuexyAdmin\Livewire\Users; namespace Koneko\VuexyAdmin\Livewire\Users;
use Livewire\WithFileUploads;
use Koneko\VuexyAdmin\Models\User; use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent; 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. * Almacena rutas útiles para la funcionalidad de edición o eliminación.
*/ */
@ -33,30 +27,12 @@ class UserIndex extends AbstractIndexComponent
{ {
return [ return [
'action' => 'Acciones', 'action' => 'Acciones',
'code' => 'Código personal', 'full_name' => 'Nombre completo',
'full_name' => 'Nombre Completo', 'email' => 'Correo electrónico',
'email' => 'Correo Electrónico', 'email_verified_at' => 'Correo verificado',
'parent_name' => 'Responsable', 'created_by' => 'Creado Por',
'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', 'status' => 'Estatus',
'creator' => 'Creado Por', 'created_at' => 'Fecha Creación',
'creator_email' => 'Correo Creador',
'created_at' => 'Fecha de Creación',
'updated_at' => 'Última Modificación', 'updated_at' => 'Última Modificación',
]; ];
} }
@ -86,14 +62,27 @@ class UserIndex extends AbstractIndexComponent
'formatter' => 'emailFormatter', 'formatter' => 'emailFormatter',
'visible' => false, 'visible' => false,
], ],
'parent_name' => [ 'email_verified_at' => [
'formatter' => 'contactParentFormatter',
'visible' => false, 'visible' => false,
], ],
'agent_name' => [ 'parent_id' => [
'formatter' => 'agentFormatter', 'formatter' => 'parentProfileFormatter',
'visible' => false, 'visible' => false,
], ],
'agent_id' => [
'formatter' => 'agentProfileFormatter',
'visible' => false,
],
'phone' => [
'formatter' => 'telFormatter',
'visible' => false,
],
'mobile' => [
'formatter' => 'telFormatter',
],
'whatsapp' => [
'formatter' => 'whatsappFormatter',
],
'company' => [ 'company' => [
'formatter' => 'textNowrapFormatter', 'formatter' => 'textNowrapFormatter',
], ],
@ -103,10 +92,16 @@ class UserIndex extends AbstractIndexComponent
'nss' => [ 'nss' => [
'visible' => false, 'visible' => false,
], ],
'job_title' => [ 'license_number' => [
'visible' => false,
],
'job_position' => [
'formatter' => 'textNowrapFormatter', 'formatter' => 'textNowrapFormatter',
'visible' => false, 'visible' => false,
], ],
'pais' => [
'visible' => false,
],
'rfc' => [ 'rfc' => [
'visible' => false, 'visible' => false,
], ],
@ -114,6 +109,7 @@ class UserIndex extends AbstractIndexComponent
'formatter' => 'textNowrapFormatter', 'formatter' => 'textNowrapFormatter',
'visible' => false, 'visible' => false,
], ],
'domicilio_fiscal' => [ 'domicilio_fiscal' => [
'visible' => false, 'visible' => false,
], ],
@ -176,14 +172,14 @@ class UserIndex extends AbstractIndexComponent
], ],
'align' => 'center', 'align' => 'center',
], ],
'is_provider' => [ 'is_supplier' => [
'formatter' => [ 'formatter' => [
'name' => 'dynamicBooleanFormatter', 'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'], 'params' => ['tag' => 'checkSI'],
], ],
'align' => 'center', 'align' => 'center',
], ],
'is_user' => [ 'is_carrier' => [
'formatter' => [ 'formatter' => [
'name' => 'dynamicBooleanFormatter', 'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'], '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. * Montamos el componente y llamamos al parent::mount() para configurar la tabla.
*/ */

View File

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

View File

@ -1,19 +1,18 @@
<?php <?php
namespace Koneko\VuexyAdmin\Livewire\AdminSettings; namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component; use Livewire\Component;
use Livewire\WithFileUploads; use Livewire\WithFileUploads;
use Koneko\VuexyAdmin\Services\AdminSettingsService; use Koneko\VuexyAdmin\Services\AdminSettingsService;
use Koneko\VuexyAdmin\Services\AdminTemplateService; use Koneko\VuexyAdmin\Services\AdminTemplateService;
class GeneralSettings extends Component class AppFaviconSettings extends Component
{ {
use WithFileUploads; use WithFileUploads;
private $targetNotify = "#general-settings-card .notification-container"; private $targetNotify = "#app-favicon-settings-card .notification-container";
public $admin_title;
public $admin_favicon_16x16, public $admin_favicon_16x16,
$admin_favicon_76x76, $admin_favicon_76x76,
$admin_favicon_120x120, $admin_favicon_120x120,
@ -25,50 +24,25 @@ class GeneralSettings extends Component
public function mount() public function mount()
{ {
$this->loadSettings(); $this->resetForm();
}
public function loadSettings($clearcache = false)
{
$this->upload_image_favicon = null;
$adminTemplateService = app(AdminTemplateService::class);
if ($clearcache) {
$adminTemplateService->clearAdminVarsCache();
}
// Obtener los valores de las configuraciones de la base de datos
$settings = $adminTemplateService->getAdminVars();
$this->admin_title = $settings['title'];
$this->admin_favicon_16x16 = $settings['favicon']['16x16'];
$this->admin_favicon_76x76 = $settings['favicon']['76x76'];
$this->admin_favicon_120x120 = $settings['favicon']['120x120'];
$this->admin_favicon_152x152 = $settings['favicon']['152x152'];
$this->admin_favicon_180x180 = $settings['favicon']['180x180'];
$this->admin_favicon_192x192 = $settings['favicon']['192x192'];
} }
public function save() public function save()
{ {
$this->validate([ $this->validate([
'admin_title' => 'required|string|max:255', 'upload_image_favicon' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
'upload_image_favicon' => 'nullable|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 // Limpiar cache de plantilla
$adminSettingsService->updateSetting('admin_title', $this->admin_title); app(AdminTemplateService::class)->clearAdminVarsCache();
// Procesar favicon si se ha cargado una imagen // Recargamos el formulario
if ($this->upload_image_favicon) { $this->resetForm();
$adminSettingsService->processAndSaveFavicon($this->upload_image_favicon);
}
$this->loadSettings(true);
// Notificación de éxito
$this->dispatch( $this->dispatch(
'notification', 'notification',
target: $this->targetNotify, 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() public function render()
{ {
return view('vuexy-admin::livewire.admin-settings.general-settings'); return view('vuexy-admin::livewire.vuexy.app-favicon-settings');
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<?php <?php
namespace Koneko\VuexyAdmin\Livewire\AdminSettings; namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
@ -11,9 +11,9 @@ use Symfony\Component\Mime\Email;
use Koneko\VuexyAdmin\Services\GlobalSettingsService; use Koneko\VuexyAdmin\Services\GlobalSettingsService;
class MailSmtpSettings extends Component class SendmailSettings extends Component
{ {
private $targetNotify = "#mail-smtp-settings-card .notification-container"; private $targetNotify = "#sendmail-settings-card .notification-container";
public $change_smtp_settings, public $change_smtp_settings,
$host, $host,
@ -68,10 +68,7 @@ class MailSmtpSettings extends Component
public function loadSettings() public function loadSettings()
{ {
$globalSettingsService = app(GlobalSettingsService::class); $settings = app(GlobalSettingsService::class)->getMailSystemConfig();
// Obtener los valores de las configuraciones de la base de datos
$settings = $globalSettingsService->getMailSystemConfig();
$this->change_smtp_settings = false; $this->change_smtp_settings = false;
$this->save_button_disabled = true; $this->save_button_disabled = true;
@ -170,6 +167,6 @@ class MailSmtpSettings extends Component
public function render() public function render()
{ {
return view('vuexy-admin::livewire.admin-settings.mail-smtp-settings'); return view('vuexy-admin::livewire.vuexy.sendmail-settings');
} }
} }

View File

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

View File

@ -1,62 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MediaItem extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'url',
'imageable_type',
'imageable_id',
'type',
'sub_type',
'url',
'path',
'title',
'description',
'order',
];
// the list of types values that can be stored in table
const TYPE_CARD = 1;
const TYPE_BANNER = 2;
const TYPE_COVER = 3;
const TYPE_GALLERY = 4;
const TYPE_BANNER_HOME = 5;
const TYPE_CARD2 = 6;
const TYPE_BANNER2 = 7;
const TYPE_COVER2 = 8;
/**
* List of names for each types.
* @var array
*/
public static $typesList = [
self::TYPE_CARD => '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();
}
}

View File

@ -3,37 +3,91 @@
namespace Koneko\VuexyAdmin\Models; namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Setting extends Model class Setting extends Model
{ {
/** use HasFactory;
* The attributes that are mass assignable.
* // ─────────────────────────────────────────────
* @var array<int, string> // Configuración del modelo
*/ // ─────────────────────────────────────────────
protected $table = 'settings';
protected $fillable = [ protected $fillable = [
'key', 'key',
'value', 'category',
'user_id', '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); return $this->belongsTo(User::class);
} }
// Scope para obtener configuraciones de un usuario específico public function updatedBy(): BelongsTo
public function scopeForUser($query, $userId) {
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); return $query->where('user_id', $userId);
} }
// Configuraciones globales (sin usuario) /**
* Configuraciones globales (sin usuario).
*/
public function scopeGlobal($query) public function scopeGlobal($query)
{ {
return $query->whereNull('user_id'); return $query->whereNull('user_id');
} }
/**
* Incluir columna virtual `value` en la consulta.
*/
public function scopeWithVirtualValue($query)
{
return $query->select(['key', 'value']);
}
} }

View File

@ -1,377 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Http\UploadedFile;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Intervention\Image\Typography\FontFactory;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
use OwenIt\Auditing\Auditable;
use Spatie\Permission\Traits\HasRoles;
use Koneko\VuexyAdmin\Notifications\CustomResetPasswordNotification;
if (trait_exists(\Koneko\VuexyContacts\Traits\HasContactsAttributes::class)) {
trait DynamicContactsAttributes {
use \Koneko\VuexyContacts\Traits\HasContactsAttributes;
}
} else {
trait DynamicContactsAttributes {}
}
class User extends Authenticatable implements MustVerifyEmail, AuditableContract
{
use HasRoles,
HasApiTokens,
HasFactory,
Notifiable,
TwoFactorAuthenticatable,
Auditable,
DynamicContactsAttributes;
// the list of status values that can be stored in table
const STATUS_ENABLED = 10;
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',
];
/**
* List of names for each status.
* @var array
*/
public static $statusList = [
self::STATUS_ENABLED => '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<int, string>
*/
protected $fillable = [
'name',
'last_name',
'email',
'password',
'profile_photo_path',
'status',
'created_by',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'two_factor_recovery_codes',
'two_factor_secret',
];
/**
* The accessors to append to the model's array form.
*
* @var array<int, string>
*/
protected $appends = [
'profile_photo_url',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
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;
}
}

View File

@ -23,23 +23,7 @@ class User extends Authenticatable implements MustVerifyEmail, AuditableContract
const STATUS_DISABLED = 1; const STATUS_DISABLED = 1;
const STATUS_REMOVED = 0; const STATUS_REMOVED = 0;
const AVATAR_DISK = 'public'; const INITIAL_MAX_LENGTH = 3;
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',
];
/** /**
* List of names for each status. * List of names for each status.
@ -148,6 +132,50 @@ class User extends Authenticatable implements MustVerifyEmail, AuditableContract
'email', '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. * Get the full name of the user.
* *
@ -179,45 +207,6 @@ class User extends Authenticatable implements MustVerifyEmail, AuditableContract
$this->notify(new CustomResetPasswordNotification($token)); $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 * User who created this user
*/ */
@ -233,5 +222,4 @@ class User extends Authenticatable implements MustVerifyEmail, AuditableContract
{ {
return $this->status === self::STATUS_ENABLED; return $this->status === self::STATUS_ENABLED;
} }
} }

View File

@ -47,7 +47,7 @@ class CustomResetPasswordNotification extends Notification
'email' => $notifiable->getEmailForPasswordReset() 'email' => $notifiable->getEmailForPasswordReset()
], false)); ], 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'))); $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); $expireMinutes = Config::get('auth.passwords.' . Config::get('auth.defaults.passwords') . '.expire', 60);
@ -90,6 +90,7 @@ class CustomResetPasswordNotification extends Notification
{ {
try { try {
$smtpConfig = Setting::where('key', 'LIKE', 'mail_%') $smtpConfig = Setting::where('key', 'LIKE', 'mail_%')
->withVirtualValue()
->pluck('value', 'key'); ->pluck('value', 'key');
if ($smtpConfig->isEmpty()) { if ($smtpConfig->isEmpty()) {

View File

@ -21,10 +21,6 @@ class ConfigServiceProvider extends ServiceProvider
*/ */
public function boot(): void 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 // Cargar configuración del sistema a través del servicio
app(GlobalSettingsService::class)->loadSystemConfig(); app(GlobalSettingsService::class)->loadSystemConfig();
} }

View File

@ -2,20 +2,28 @@
namespace Koneko\VuexyAdmin\Providers; 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\Console\Commands\CleanInitialAvatars;
use Koneko\VuexyAdmin\Helpers\VuexyHelper; 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\Facades\{URL,Event,Blade};
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Foundation\AliasLoader; use Koneko\VuexyAdmin\Listeners\{ClearUserCache,HandleUserLogin};
use Illuminate\Auth\Events\{Login,Logout};
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 Livewire\Livewire;
use OwenIt\Auditing\AuditableObserver; use OwenIt\Auditing\AuditableObserver;
use Spatie\Permission\PermissionServiceProvider; use Spatie\Permission\PermissionServiceProvider;
@ -48,14 +56,17 @@ class VuexyAdminServiceProvider extends ServiceProvider
URL::forceScheme('https'); URL::forceScheme('https');
} }
// Registrar alias del middleware // Registrar alias del middleware
$this->app['router']->aliasMiddleware('admin', AdminTemplateMiddleware::class); $this->app['router']->aliasMiddleware('admin', AdminTemplateMiddleware::class);
// Sobrescribir ruta de traducciones para asegurar que se usen las del paquete // Sobrescribir ruta de traducciones para asegurar que se usen las del paquete
$this->app->bind('path.lang', function () { $this->app->bind('path.lang', function () {
return __DIR__ . '/../resources/lang'; return __DIR__ . '/../resources/lang';
}); });
// Register the module's routes // Register the module's routes
$this->loadRoutesFrom(__DIR__.'/../routes/admin.php'); $this->loadRoutesFrom(__DIR__.'/../routes/admin.php');
@ -63,6 +74,7 @@ class VuexyAdminServiceProvider extends ServiceProvider
// Cargar vistas del paquete // Cargar vistas del paquete
$this->loadViewsFrom(__DIR__.'/../resources/views', 'vuexy-admin'); $this->loadViewsFrom(__DIR__.'/../resources/views', 'vuexy-admin');
// Registrar Componentes Blade // Registrar Componentes Blade
Blade::componentNamespace('VuexyAdmin\\View\\Components', 'vuexy-admin'); Blade::componentNamespace('VuexyAdmin\\View\\Components', 'vuexy-admin');
@ -100,32 +112,60 @@ class VuexyAdminServiceProvider extends ServiceProvider
]); ]);
} }
// Registrar Livewire Components // Registrar Livewire Components
$components = [ $components = [
'user-index' => UserIndex::class, // Usuarios
'user-show' => UserShow::class, 'vuexy-admin::users-index' => UsersIndex::class,
'user-form' => UserForm::class, 'vuexy-admin::users-count' => UsersCount::class,
'user-offcanvas-form' => UserOffCanvasForm::class, 'vuexy-admin::user-form' => UserForm::class,
'role-index' => RoleIndex::class, 'vuexy-admin::user-offcanvas-form' => UserOffCanvasForm::class,
'permission-index' => PermissionIndex::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, // Roles y Permisos
'application-settings' => ApplicationSettings::class, 'vuexy-admin::roles-index' => RolesIndex::class,
'interface-settings' => InterfaceSettings::class, 'vuexy-admin::role-cards' => RoleCards::class,
'mail-smtp-settings' => MailSmtpSettings::class, 'vuexy-admin::permissions-index' => PermissionsIndex::class,
'mail-sender-response-settings' => MailSenderResponseSettings::class, 'vuexy-admin::permission-offcanvas-form' => PermissionOffCanvasForm::class,
'cache-stats' => CacheStats::class,
'session-stats' => SessionStats::class, // Identidad de aplicación
'redis-stats' => RedisStats::class, 'vuexy-admin::app-description-settings' => AppDescriptionSettings::class,
'memcached-stats' => MemcachedStats::class, 'vuexy-admin::app-favicon-settings' => AppFaviconSettings::class,
'cache-functions' => CacheFunctions::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) { foreach ($components as $alias => $component) {
Livewire::component($alias, $component); Livewire::component($alias, $component);
} }
// Registrar auditoría en usuarios // Registrar auditoría en usuarios
User::observe(AuditableObserver::class); User::observe(AuditableObserver::class);
} }

View File

@ -27,19 +27,24 @@ abstract class BootstrapTableQueryBuilder
foreach ($this->config['joins'] as $join) { foreach ($this->config['joins'] as $join) {
$type = $join['type'] ?? 'join'; $type = $join['type'] ?? 'join';
$this->query->{$type}($join['table'], function($joinObj) use ($join) { // Soporte para alias
$joinObj->on($join['first'], '=', $join['second']); $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'])) { if (!empty($join['and'])) {
foreach ((array) $join['and'] as $andCondition) { foreach ((array) $join['and'] as $andCondition) {
// 'sat_codigo_postal.c_estado = sat_localidad.c_estado'
$parts = explode('=', $andCondition); $parts = explode('=', $andCondition);
if (count($parts) === 2) { if (count($parts) === 2) {
$left = trim($parts[0]); $left = trim($parts[0]);
$right = trim($parts[1]); $right = trim($parts[1]);
$joinObj->whereRaw("$left = $right"); $joinObj->whereRaw("$left = $right");
} }
} }

View File

@ -6,13 +6,29 @@ use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Koneko\VuexyAdmin\Models\Setting; 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 class AdminSettingsService
{ {
/** @var string Driver de procesamiento de imágenes */
private $driver; private $driver;
/** @var string Disco de almacenamiento para imágenes */
private $imageDisk = 'public'; private $imageDisk = 'public';
/** @var string Ruta base para favicons */
private $favicon_basePath = 'favicon/'; private $favicon_basePath = 'favicon/';
/** @var string Ruta base para logos */
private $image_logo_basePath = 'images/logo/'; private $image_logo_basePath = 'images/logo/';
/** @var array<string,array<int>> Tamaños predefinidos para favicons */
private $faviconsSizes = [ private $faviconsSizes = [
'180x180' => [180, 180], '180x180' => [180, 180],
'192x192' => [192, 192], '192x192' => [192, 192],
@ -22,28 +38,40 @@ class AdminSettingsService
'16x16' => [16, 16], '16x16' => [16, 16],
]; ];
private $imageLogoMaxPixels1 = 22500; // Primera versión (px^2) /** @var int Área máxima en píxeles para la primera versión del logo */
private $imageLogoMaxPixels2 = 75625; // Segunda versión (px^2) private $imageLogoMaxPixels1 = 22500;
private $imageLogoMaxPixels3 = 262144; // Tercera versión (px^2)
private $imageLogoMaxPixels4 = 230400; // Tercera versión (px^2) en Base64
protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos /** @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() public function __construct()
{ {
$this->driver = config('image.driver', 'gd'); $this->driver = config('image.driver', 'gd');
} }
public function updateSetting(string $key, string $value): bool /**
{ * Procesa y guarda un nuevo favicon
$setting = Setting::updateOrCreate( *
['key' => $key], * Genera múltiples versiones del favicon en diferentes tamaños predefinidos,
['value' => trim($value)] * elimina las versiones anteriores y actualiza la configuración.
); *
* @param \Illuminate\Http\UploadedFile $image Archivo de imagen subido
return $setting->save(); * @return void
} */
public function processAndSaveFavicon($image): void public function processAndSaveFavicon($image): void
{ {
Storage::makeDirectory($this->imageDisk . '/' . $this->favicon_basePath); Storage::makeDirectory($this->imageDisk . '/' . $this->favicon_basePath);
@ -66,13 +94,20 @@ class AdminSettingsService
Storage::disk($this->imageDisk)->put($resizedPath, $image->toPng(indexed: true)); 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 protected function deleteOldFavicons(): void
{ {
// Obtener el favicon actual desde la base de datos // 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) { if ($currentFavicon) {
$filePaths = [ $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 public function processAndSaveImageLogo($image, string $type = ''): void
{ {
// Crear directorio si no existe // Crear directorio si no existe
@ -112,6 +157,15 @@ class AdminSettingsService
$this->generateAndSaveImageAsBase64($image, $type, $this->imageLogoMaxPixels4); // Versión 3 $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 private function generateAndSaveImage($image, string $type, int $maxPixels, string $suffix = ''): void
{ {
$imageClone = clone $image; $imageClone = clone $image;
@ -120,6 +174,7 @@ class AdminSettingsService
$this->resizeImageToMaxPixels($imageClone, $maxPixels); $this->resizeImageToMaxPixels($imageClone, $maxPixels);
$imageName = 'admin_image_logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : ''); $imageName = 'admin_image_logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : '');
$keyValue = 'admin.image.logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : '');
// Generar nombre y ruta // Generar nombre y ruta
$imageNameUid = uniqid($imageName . '_', ".png"); $imageNameUid = uniqid($imageName . '_', ".png");
@ -129,9 +184,17 @@ class AdminSettingsService
Storage::disk($this->imageDisk)->put($resizedPath, $imageClone->toPng(indexed: true)); Storage::disk($this->imageDisk)->put($resizedPath, $imageClone->toPng(indexed: true));
// Actualizar configuración // 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) private function resizeImageToMaxPixels($image, int $maxPixels)
{ {
// Obtener dimensiones originales de la imagen // Obtener dimensiones originales de la imagen
@ -163,7 +226,14 @@ class AdminSettingsService
return $image; 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 private function generateAndSaveImageAsBase64($image, string $type, int $maxPixels): void
{ {
$imageClone = clone $image; $imageClone = clone $image;
@ -175,12 +245,16 @@ class AdminSettingsService
$base64Image = (string) $imageClone->toJpg(40)->toDataUri(); $base64Image = (string) $imageClone->toJpg(40)->toDataUri();
// Guardar como configuración // Guardar como configuración
$this->updateSetting( $SettingsService = app(SettingsService::class);
"admin_image_logo_base64" . ($type === 'dark' ? '_dark' : ''), $SettingsService->set("admin.image.logo_base64" . ($type === 'dark' ? '_dark' : ''), $base64Image, null, 'vuexy-admin');
$base64Image // Ya incluye "data:image/png;base64,"
);
} }
/**
* 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 protected function deleteOldImageWebapp(string $type = ''): void
{ {
// Determinar prefijo según el tipo (normal o dark) // Determinar prefijo según el tipo (normal o dark)
@ -188,9 +262,9 @@ class AdminSettingsService
// Claves relacionadas con las imágenes que queremos limpiar // Claves relacionadas con las imágenes que queremos limpiar
$imageKeys = [ $imageKeys = [
"admin_image_logo{$suffix}", "admin.image_logo{$suffix}",
"admin_image_logo_small{$suffix}", "admin.image_logo_small{$suffix}",
"admin_image_logo_medium{$suffix}", "admin.image_logo_medium{$suffix}",
]; ];
// Recuperar las imágenes actuales en una sola consulta // Recuperar las imágenes actuales en una sola consulta

View File

@ -7,48 +7,57 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Koneko\VuexyAdmin\Models\Setting; 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 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 /**
{ * Obtiene las variables de configuración del admin.
$setting = Setting::updateOrCreate( *
['key' => $key], * @param string $setting Clave específica de configuración a obtener
['value' => trim($value)] * @return array Configuraciones del admin o valor específico si se proporciona $setting
); */
public function getAdminVars(string $setting = ''): array
return $setting->save();
}
public function getAdminVars($adminSetting = false): array
{ {
try { try {
// Verificar si el sistema está inicializado (la tabla `migrations` existe) // Verificar si el sistema está inicializado (la tabla `migrations` existe)
if (!Schema::hasTable('migrations')) { if (!Schema::hasTable('migrations')) {
return $this->getDefaultAdminVars($adminSetting); return $this->getDefaultAdminVars($setting);
} }
// Cargar desde el caché o la base de datos si está disponible // Cargar desde el caché o la base de datos si está disponible
return Cache::remember('admin_settings', $this->cacheTTL, function () use ($adminSetting) { $adminVars = Cache::remember('admin_settings', $this->cacheTTL, function () {
$settings = Setting::global() $settings = Setting::withVirtualValue()
->where('key', 'LIKE', 'admin_%') ->where('key', 'LIKE', 'admin.%')
->pluck('value', 'key') ->pluck('value', 'key')
->toArray(); ->toArray();
$adminSettings = $this->buildAdminVarsArray($settings); return $this->buildAdminVarsArray($settings);
return $adminSetting
? $adminSettings[$adminSetting]
: $adminSettings;
}); });
return $setting ? ($adminVars[$setting] ?? []) : $adminVars;
} catch (\Exception $e) { } catch (\Exception $e) {
// En caso de error, devolver valores predeterminados // 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 = [ $defaultSettings = [
'title' => config('koneko.appTitle', 'Default Title'), 'title' => config('koneko.appTitle', 'Default Title'),
@ -59,27 +68,41 @@ class AdminTemplateService
'image_logo' => $this->getImageLogoPaths([]), 'image_logo' => $this->getImageLogoPaths([]),
]; ];
return $adminSetting return $setting
? $defaultSettings[$adminSetting] ?? null ? $defaultSettings[$setting] ?? null
: $defaultSettings; : $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 private function buildAdminVarsArray(array $settings): array
{ {
return [ return [
'title' => $settings['admin_title'] ?? config('koneko.appTitle'), 'title' => $settings['admin.title'] ?? config('koneko.appTitle'),
'author' => config('koneko.author'), 'author' => config('koneko.author'),
'description' => config('koneko.description'), 'description' => $settings['admin.description'] ?? config('koneko.description'),
'favicon' => $this->getFaviconPaths($settings), '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), '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() public function getVuexyCustomizerVars()
{ {
// Obtener valores de la base de datos // Obtener valores de la base de datos
$settings = Setting::global() $settings = Setting::withVirtualValue()
->where('key', 'LIKE', 'vuexy_%') ->where('key', 'LIKE', 'vuexy_%')
->pluck('value', 'key') ->pluck('value', 'key')
->toArray(); ->toArray();
@ -96,7 +119,7 @@ class AdminTemplateService
$value = $settings[$vuexyKey] ?? $defaultValue; $value = $settings[$vuexyKey] ?? $defaultValue;
// Forzar booleanos para claves específicas // 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); $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 private function getFaviconPaths(array $settings): array
{ {
$defaultFavicon = config('koneko.appFavicon'); $defaultFavicon = config('koneko.appFavicon');
$namespace = $settings['admin_favicon_ns'] ?? null; $namespace = $settings['admin.favicon_ns'] ?? null;
return [ return [
'namespace' => $namespace, '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 private function getImageLogoPaths(array $settings): array
{ {
$defaultLogo = config('koneko.appLogo'); $defaultLogo = config('koneko.appLogo');
return [ return [
'small' => $this->getImagePath($settings, 'admin_image_logo_small', $defaultLogo), 'small' => $this->getImagePath($settings, 'admin.image.logo_small', $defaultLogo),
'medium' => $this->getImagePath($settings, 'admin_image_logo_medium', $defaultLogo), 'medium' => $this->getImagePath($settings, 'admin.image.logo_medium', $defaultLogo),
'large' => $this->getImagePath($settings, 'admin_image_logo', $defaultLogo), 'large' => $this->getImagePath($settings, 'admin.image.logo', $defaultLogo),
'small_dark' => $this->getImagePath($settings, 'admin_image_logo_small_dark', $defaultLogo), 'small_dark' => $this->getImagePath($settings, 'admin.image.logo_small_dark', $defaultLogo),
'medium_dark' => $this->getImagePath($settings, 'admin_image_logo_medium_dark', $defaultLogo), 'medium_dark' => $this->getImagePath($settings, 'admin.image.logo_medium_dark', $defaultLogo),
'large_dark' => $this->getImagePath($settings, 'admin_image_logo_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 private function getImagePath(array $settings, string $key, string $default): string
{ {
return $settings[$key] ?? $default; return $settings[$key] ?? $default;
} }
/**
* Limpia el caché de las variables del admin.
*
* @return void
*/
public static function clearAdminVarsCache() public static function clearAdminVarsCache()
{ {
Cache::forget("admin_settings"); Cache::forget("admin_settings");

View File

@ -15,7 +15,7 @@ class AvatarInitialsService
protected const INITIAL_MAX_LENGTH = 3; protected const INITIAL_MAX_LENGTH = 3;
protected const AVATAR_BACKGROUND = '#EBF4FF'; protected const AVATAR_BACKGROUND = '#EBF4FF';
protected const AVATAR_COLORS = [ protected const AVATAR_COLORS = [
'#7367f0', '#3b82f6',
'#808390', '#808390',
'#28c76f', '#28c76f',
'#ff4c51', '#ff4c51',

View File

@ -6,8 +6,20 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis; 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 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 public function getConfig(): array
{ {
return [ 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 private function getCacheConfig(): array
{ {
$cacheConfig = Config::get('cache'); $cacheConfig = Config::get('cache');
@ -59,6 +75,11 @@ class CacheConfigService
return $cacheConfig; 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 private function getSessionConfig(): array
{ {
$sessionConfig = Config::get('session'); $sessionConfig = Config::get('session');
@ -97,6 +118,11 @@ class CacheConfigService
return $sessionConfig; 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 private function getDatabaseConfig(): array
{ {
$databaseConfig = Config::get('database'); $databaseConfig = Config::get('database');
@ -109,7 +135,14 @@ class CacheConfigService
return $databaseConfig; 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 private function getDriverVersion(): array
{ {
$drivers = []; $drivers = [];
@ -163,6 +196,11 @@ class CacheConfigService
return $drivers; return $drivers;
} }
/**
* Obtiene la versión del servidor MySQL.
*
* @return string Versión del servidor MySQL o mensaje de error
*/
private function getMySqlVersion(): string private function getMySqlVersion(): string
{ {
try { 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 private function getPgSqlVersion(): string
{ {
try { 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 private function getSqlSrvVersion(): string
{ {
try { 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 private function getMemcachedVersion(): string
{ {
try { 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 private function getRedisVersion(): string
{ {
try { 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 protected function isDriverInUse(string $driver): bool
{ {
return in_array($driver, [ return in_array($driver, [

View File

@ -7,19 +7,38 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\File; 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 class CacheManagerService
{ {
/** @var string Driver de caché actualmente seleccionado */
private string $driver; 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'); $this->driver = $driver ?? config('cache.default');
} }
/** /**
* Obtiene estadísticas de caché para el driver especificado. * 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; $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; $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() public function getRedisStats()
{ {
try { 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() public function getMemcachedStats()
{ {
try { try {
@ -176,9 +211,10 @@ class CacheManagerService
} }
} }
/** /**
* Obtiene estadísticas para caché en base de datos. * 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 private function _getDatabaseStats(): array
{ {
@ -196,6 +232,8 @@ class CacheManagerService
/** /**
* Obtiene estadísticas para caché en archivos. * 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 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() private function _getRedisStats()
{ {
try { 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 public function _getMemcachedStats(): array
{ {
try { 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 private function getRedisDatabases(): array
{ {
// Verificar si Redis está en uso // Verificar si Redis está en uso
@ -300,7 +356,11 @@ class CacheManagerService
return $result; 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 private function clearDatabaseCache(): bool
{ {
$count = DB::table(config('cache.stores.database.table'))->count(); $count = DB::table(config('cache.stores.database.table'))->count();
@ -313,6 +373,11 @@ class CacheManagerService
return false; 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 private function clearFilecache(): bool
{ {
$cachePath = config('cache.stores.file.path'); $cachePath = config('cache.stores.file.path');
@ -326,6 +391,11 @@ class CacheManagerService
return false; 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 private function clearRedisCache(): bool
{ {
$prefix = config('cache.prefix', ''); $prefix = config('cache.prefix', '');
@ -343,6 +413,11 @@ class CacheManagerService
return false; return false;
} }
/**
* Limpia la caché almacenada en Memcached.
*
* @return bool True si se limpió la caché, False en caso contrario
*/
private function clearMemcachedCache(): bool private function clearMemcachedCache(): bool
{ {
// Obtener el cliente Memcached directamente // Obtener el cliente Memcached directamente
@ -359,9 +434,11 @@ class CacheManagerService
return false; 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 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) 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 private function response(string $status, string $message, array $data = []): array
{ {

View File

@ -9,28 +9,26 @@ use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Koneko\VuexyAdmin\Models\Setting; 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 class GlobalSettingsService
{ {
/** /** @var int Tiempo de vida del caché en minutos (60 * 24 * 30 = 30 días) */
* Tiempo de vida del caché en minutos (30 días).
*/
private $cacheTTL = 60 * 24 * 30; private $cacheTTL = 60 * 24 * 30;
/** /**
* Actualiza o crea una configuración. * Carga la configuración del sistema desde la base de datos o caché.
*/ *
public function updateSetting(string $key, string $value): bool * Gestiona la carga de configuraciones para servicios externos y Vuexy.
{ * Si la base de datos no está inicializada, utiliza valores predeterminados.
$setting = Setting::updateOrCreate( *
['key' => $key], * @return void
['value' => trim($value)]
);
return $setting->save();
}
/**
* Carga y sobrescribe las configuraciones del sistema.
*/ */
public function loadSystemConfig(): void public function loadSystemConfig(): void
{ {
@ -41,7 +39,7 @@ class GlobalSettingsService
} else { } else {
// Cargar configuración desde la caché o base de datos // Cargar configuración desde la caché o base de datos
$config = Cache::remember('global_system_config', $this->cacheTTL, function () { $config = Cache::remember('global_system_config', $this->cacheTTL, function () {
$settings = Setting::global() $settings = Setting::withVirtualValue()
->where('key', 'LIKE', 'config.%') ->where('key', 'LIKE', 'config.%')
->pluck('value', 'key') ->pluck('value', 'key')
->toArray(); ->toArray();
@ -58,6 +56,7 @@ class GlobalSettingsService
Config::set('services.facebook', $config['servicesFacebook']); Config::set('services.facebook', $config['servicesFacebook']);
Config::set('services.google', $config['servicesGoogle']); Config::set('services.google', $config['servicesGoogle']);
Config::set('vuexy', $config['vuexy']); Config::set('vuexy', $config['vuexy']);
} catch (\Exception $e) { } catch (\Exception $e) {
// Manejo silencioso de errores para evitar interrupciones // Manejo silencioso de errores para evitar interrupciones
Config::set('services.facebook', config('services.facebook', [])); 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 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 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 protected function buildServiceConfig(array $settings, string $blockPrefix, string $defaultConfigKey): array
{ {
if (!$this->hasBlockConfig($settings, $blockPrefix)) { if (!$this->hasBlockConfig($settings, $blockPrefix)) {
return []; return config($defaultConfigKey)?? [];
return config($defaultConfigKey);
} }
return [ return [
@ -112,7 +121,13 @@ 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 protected function buildVuexyConfig(array $settings): array
{ {
@ -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 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 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 public static function clearVuexyConfig(): void
{ {
Setting::where('key', 'LIKE', 'config.vuexy.%')->delete(); Setting::where('key', 'LIKE', 'config.vuexy.%')->delete();
Cache::forget('global_system_config'); 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 public function getMailSystemConfig(): array
{ {
return Cache::remember('mail_system_config', $this->cacheTTL, function () { return Cache::remember('mail_system_config', $this->cacheTTL, function () {
$settings = Setting::global() $settings = Setting::withVirtualValue()
->where('key', 'LIKE', 'mail.%') ->where('key', 'LIKE', 'mail.%')
->pluck('value', 'key') ->pluck('value', 'key')
->toArray(); ->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 public static function clearMailSystemConfigCache(): void
{ {

View File

@ -10,12 +10,12 @@ class SessionManagerService
{ {
private string $driver; private string $driver;
public function __construct(string $driver = null) public function __construct(mixed $driver = null)
{ {
$this->driver = $driver ?? config('session.driver'); $this->driver = $driver ?? config('session.driver');
} }
public function getSessionStats(string $driver = null): array public function getSessionStats(mixed $driver = null): array
{ {
$driver = $driver ?? $this->driver; $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; $driver = $driver ?? $this->driver;

View File

@ -0,0 +1,190 @@
<?php
namespace Koneko\VuexyAdmin\Services;
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Models\Setting;
use Illuminate\Support\Facades\Cache;
class SettingsService
{
/**
* Obtiene una configuración con opciones avanzadas de caché.
*
* @param string $key
* @param int|null $userId
* @param string|null $category
* @param bool $useCache
* @param bool $storeInCache
* @param int|null $cacheTtl
* @return mixed|null
*/
public function get(
string $key,
?int $userId = null,
?string $category = null,
bool $useCache = false,
bool $storeInCache = true,
?int $cacheTtl = 120
) {
$cacheKey = $this->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);
}
}

View File

@ -2,10 +2,7 @@
namespace Koneko\VuexyAdmin\Services; namespace Koneko\VuexyAdmin\Services;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\{Auth,Cache,Route,Gate};
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Gate;
use Koneko\VuexyAdmin\Models\Setting; use Koneko\VuexyAdmin\Models\Setting;
class VuexyAdminService class VuexyAdminService
@ -26,12 +23,8 @@ class VuexyAdminService
{ {
$this->user = Auth::user(); $this->user = Auth::user();
$this->vuexySearch = Auth::user() !== null; $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() public function getMenu()
{ {
// Obtener el menú desde la caché // Obtener el menú desde la caché
@ -45,9 +38,6 @@ class VuexyAdminService
return $this->markActive($menu, $currentRoute); return $this->markActive($menu, $currentRoute);
} }
/**
* Menú para usuarios no autenticados.dump
*/
private function getGuestMenu() private function getGuestMenu()
{ {
return Cache::remember('vuexy_menu_guest', now()->addDays(7), function () { return Cache::remember('vuexy_menu_guest', now()->addDays(7), function () {
@ -55,12 +45,9 @@ class VuexyAdminService
}); });
} }
/**
* Menú para usuarios autenticados.
*/
private function getUserMenu() 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 Cache::remember("vuexy_menu_user_{$this->user->id}", now()->addHours(24), function () {
return $this->getMenuArray(); return $this->getMenuArray();
@ -89,9 +76,6 @@ class VuexyAdminService
return $menu; return $menu;
} }
/**
* Invalida el cache del menú de un usuario.
*/
public static function clearUserMenuCache() public static function clearUserMenuCache()
{ {
$user = Auth::user(); $user = Auth::user();
@ -100,9 +84,6 @@ class VuexyAdminService
Cache::forget("vuexy_menu_user_{$user->id}"); Cache::forget("vuexy_menu_user_{$user->id}");
} }
/**
* Invalida el cache del menú de invitados.
*/
public static function clearGuestMenuCache() public static function clearGuestMenuCache()
{ {
Cache::forget('vuexy_menu_guest'); Cache::forget('vuexy_menu_guest');
@ -224,7 +205,7 @@ class VuexyAdminService
return $quickLinksData; 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) { foreach ($menu as $title => $item) {
// Verificar si el elemento está en la lista de quicklinksRouteNames // 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 private function isCurrentPageInList(array $quickLinks, string $currentRoute): bool
{ {
foreach ($quickLinks['rows'] as $row) { foreach ($quickLinks['rows'] as $row) {
@ -291,193 +269,8 @@ class VuexyAdminService
<a class='nav-link btn btn-text-secondary btn-icon rounded-pill dropdown-toggle hide-arrow' href='javascript:void(0);' data-bs-toggle='dropdown' data-bs-auto-close='outside' aria-expanded='false'> <a class='nav-link btn btn-text-secondary btn-icon rounded-pill dropdown-toggle hide-arrow' href='javascript:void(0);' data-bs-toggle='dropdown' data-bs-auto-close='outside' aria-expanded='false'>
<span class='position-relative'> <span class='position-relative'>
<i class='ti ti-bell ti-md'></i> <i class='ti ti-bell ti-md'></i>
<span class='badge rounded-pill bg-danger badge-dot badge-notifications border'></span>
</span> </span>
</a> </a>
<ul class='dropdown-menu dropdown-menu-end p-0'>
<li class='dropdown-menu-header border-bottom'>
<div class='dropdown-header d-flex align-items-center py-3'>
<h6 class='mb-0 me-auto'>Notification</h6>
<div class='d-flex align-items-center h6 mb-0'>
<span class='badge bg-label-primary me-2'>8 New</span>
<a href='javascript:void(0)' class='btn btn-text-secondary rounded-pill btn-icon dropdown-notifications-all' data-bs-toggle='tooltip' data-bs-placement='top' title='Mark all as read'><i class='ti ti-mail-opened text-heading'></i></a>
</div>
</div>
</li>
<li class='dropdown-notifications-list scrollable-container'>
<ul class='list-group list-group-flush'>
<li class='list-group-item list-group-item-action dropdown-notifications-item'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<img src='' . asset('assets/admin/img/avatars/1.png') . '' alt class='rounded-circle'>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='small mb-1'>Congratulation Lettie 🎉</h6>
<small class='mb-1 d-block text-body'>Won the monthly best seller gold badge</small>
<small class='text-muted'>1h ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
<li class='list-group-item list-group-item-action dropdown-notifications-item'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<span class='avatar-initial rounded-circle bg-label-danger'>CF</span>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='mb-1 small'>Charles Franklin</h6>
<small class='mb-1 d-block text-body'>Accepted your connection</small>
<small class='text-muted'>12hr ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<img src='' . asset('assets/admin/img/avatars/2.png') . '' alt class='rounded-circle'>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='mb-1 small'>New Message ✉️</h6>
<small class='mb-1 d-block text-body'>You have new message from Natalie</small>
<small class='text-muted'>1h ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
<li class='list-group-item list-group-item-action dropdown-notifications-item'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<span class='avatar-initial rounded-circle bg-label-success'><i class='ti ti-shopping-cart'></i></span>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='mb-1 small'>Whoo! You have new order 🛒 </h6>
<small class='mb-1 d-block text-body'>ACME Inc. made new order $1,154</small>
<small class='text-muted'>1 day ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<img src='' . asset('assets/admin/img/avatars/9.png') . '' alt class='rounded-circle'>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='mb-1 small'>Application has been approved 🚀 </h6>
<small class='mb-1 d-block text-body'>Your ABC project application has been approved.</small>
<small class='text-muted'>2 days ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<span class='avatar-initial rounded-circle bg-label-success'><i class='ti ti-chart-pie'></i></span>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='mb-1 small'>Monthly report is generated</h6>
<small class='mb-1 d-block text-body'>July monthly financial report is generated </small>
<small class='text-muted'>3 days ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<img src='' . asset('assets/admin/img/avatars/5.png') . '' alt class='rounded-circle'>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='mb-1 small'>Send connection request</h6>
<small class='mb-1 d-block text-body'>Peter sent you connection request</small>
<small class='text-muted'>4 days ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
<li class='list-group-item list-group-item-action dropdown-notifications-item'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<img src='' . asset('assets/admin/img/avatars/6.png') . '' alt class='rounded-circle'>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='mb-1 small'>New message from Jane</h6>
<small class='mb-1 d-block text-body'>Your have new message from Jane</small>
<small class='text-muted'>5 days ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
<li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'>
<div class='d-flex'>
<div class='flex-shrink-0 me-3'>
<div class='avatar'>
<span class='avatar-initial rounded-circle bg-label-warning'><i class='ti ti-alert-triangle'></i></span>
</div>
</div>
<div class='flex-grow-1'>
<h6 class='mb-1 small'>CPU is running high</h6>
<small class='mb-1 d-block text-body'>CPU Utilization Percent is currently at 88.63%,</small>
<small class='text-muted'>5 days ago</small>
</div>
<div class='flex-shrink-0 dropdown-notifications-actions'>
<a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a>
<a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a>
</div>
</div>
</li>
</ul>
</li>
<li class='border-top'>
<div class='d-grid p-4'>
<a class='btn btn-primary btn-sm d-flex' href='javascript:void(0);'>
<small class='align-middle'>View all notifications</small>
</a>
</div>
</li>
</ul>
</li>"; </li>";
} }

View File

@ -14,9 +14,10 @@
"owen-it/laravel-auditing": "^13.6", "owen-it/laravel-auditing": "^13.6",
"spatie/laravel-permission": "^6.10" "spatie/laravel-permission": "^6.10"
}, },
"prefer-stable": true,
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Koneko\\VuexyAdmin\\": "" "Koneko\\VuexyAdmin\\": "./"
} }
}, },
"extra": { "extra": {
@ -35,6 +36,5 @@
"support": { "support": {
"source": "https://github.com/koneko-mx/laravel-vuexy-admin", "source": "https://github.com/koneko-mx/laravel-vuexy-admin",
"issues": "https://github.com/koneko-mx/laravel-vuexy-admin/issues" "issues": "https://github.com/koneko-mx/laravel-vuexy-admin/issues"
}, }
"prefer-stable": true
} }

View File

@ -112,7 +112,7 @@ return [
], ],
] ]
], ],
'Configuración de cuenta' => [ 'Cuenta de usuario' => [
'route' => 'admin.core.user-profile.index', 'route' => 'admin.core.user-profile.index',
'icon' => 'menu-icon tf-icons ti ti-user-cog', 'icon' => 'menu-icon tf-icons ti ti-user-cog',
], ],
@ -377,14 +377,14 @@ return [
'can' => 'admin.inventory.product-catalogs.view', 'can' => 'admin.inventory.product-catalogs.view',
], ],
'Productos y servicios' => [ 'Productos y servicios' => [
'route' => 'admin.inventory.products.index', 'route' => 'admin.products.products.index',
'icon' => 'menu-icon tf-icons ti ti-packages', 'icon' => 'menu-icon tf-icons ti ti-packages',
'can' => 'admin.inventory.products.view', 'can' => 'admin.products.products.view',
], ],
'Agregar producto o servicio' => [ 'Agregar producto o servicio' => [
'route' => 'admin.inventory.products.create', 'route' => 'admin.products.products.create',
'icon' => 'menu-icon tf-icons ti ti-package', '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', 'can' => 'admin.inventory.suppliers.view',
], ],
'Órdenes de Compra' => [ 'Órdenes de Compra' => [
'route' => 'admin.inventory.orders.index', 'route' => 'admin.purchase-orders.orders.index',
'can' => 'admin.inventory.orders.view', 'can' => 'admin.purchase-orders.orders.view',
], ],
'Recepción de Productos' => [ 'Recepción de Productos' => [
'route' => 'admin.inventory.reception.index', 'route' => 'admin.purchase-orders.reception.index',
'can' => 'admin.inventory.reception.view', 'can' => 'admin.purchase-orders.reception.view',
], ],
'Gestión de Insumos' => [ 'Gestión de Insumos' => [
'route' => 'admin.inventory.materials.index', 'route' => 'admin.purchase-orders.materials.index',
'can' => 'admin.inventory.materials.view', 'can' => 'admin.purchase-orders.materials.view',
], ],
], ],
], ],
@ -654,20 +654,20 @@ return [
'icon' => 'menu-icon tf-icons ti ti-truck', 'icon' => 'menu-icon tf-icons ti ti-truck',
'submenu' => [ 'submenu' => [
'Órdenes de Envío' => [ 'Órdenes de Envío' => [
'route' => 'admin.inventory.shipping-orders.index', 'route' => 'admin.shipping.orders.index',
'can' => 'admin.inventory.shipping-orders.view', 'can' => 'admin.shipping.orders.view',
], ],
'Seguimiento de Envíos' => [ 'Seguimiento de Envíos' => [
'route' => 'admin.inventory.shipping-tracking.index', 'route' => 'admin.shipping.tracking.index',
'can' => 'admin.inventory.shipping-tracking.view', 'can' => 'admin.shipping.tracking.view',
], ],
'Transportistas' => [ 'Transportistas' => [
'route' => 'admin.inventory.shipping-carriers.index', 'route' => 'admin.shipping.carriers.index',
'can' => 'admin.inventory.shipping-carriers.view', 'can' => 'admin.shipping.carriers.view',
], ],
'Tarifas y Métodos de Envío' => [ 'Tarifas y Métodos de Envío' => [
'route' => 'admin.inventory.shipping-rates.index', 'route' => 'admin.shipping.rates.index',
'can' => 'admin.inventory.shipping-rates.view', 'can' => 'admin.shipping.rates.view',
], ],
], ],
], ],
@ -679,16 +679,16 @@ return [
'can' => 'admin.inventory.asset.view', 'can' => 'admin.inventory.asset.view',
], ],
'Mantenimiento Preventivo' => [ 'Mantenimiento Preventivo' => [
'route' => 'admin.inventory.asset-maintenance.index', 'route' => 'admin.assets.maintenance.index',
'can' => 'admin.inventory.asset-maintenance.view', 'can' => 'admin.assets.maintenance.view',
], ],
'Control de Vida Útil' => [ 'Control de Vida Útil' => [
'route' => 'admin.inventory.asset-lifecycle.index', 'route' => 'admin.assets.lifecycle.index',
'can' => 'admin.inventory.asset-lifecycle.view', 'can' => 'admin.assets.lifecycle.view',
], ],
'Asignación de Activos' => [ 'Asignación de Activos' => [
'route' => 'admin.inventory.asset-assignments.index', 'route' => 'admin.assets.assignments.index',
'can' => 'admin.inventory.asset-assignments.view', 'can' => 'admin.assets.assignments.view',
], ],
], ],
], ],

View File

@ -59,8 +59,8 @@
"admin.attendance.absences.view", "admin.attendance.absences.view",
"admin.inventory.product-categories.view", "admin.inventory.product-categories.view",
"admin.inventory.product-catalogs.view", "admin.inventory.product-catalogs.view",
"admin.inventory.products.view", "admin.products.products.view",
"admin.inventory.products.create", "admin.products.products.create",
"admin.sales.dashboard.allow", "admin.sales.dashboard.allow",
"admin.contacts.customers.view", "admin.contacts.customers.view",
"admin.sales.sales.view", "admin.sales.sales.view",
@ -99,21 +99,21 @@
"admin.billing.nomina.view", "admin.billing.nomina.view",
"admin.billing.verify-cfdi.allow", "admin.billing.verify-cfdi.allow",
"admin.contacts.suppliers.view", "admin.contacts.suppliers.view",
"admin.inventory.orders.view", "admin.purchase-orders.orders.view",
"admin.inventory.reception.view", "admin.purchase-orders.reception.view",
"admin.inventory.materials.view", "admin.purchase-orders.materials.view",
"admin.inventory.warehouse.view", "admin.inventory.warehouse.view",
"admin.inventory.stock.view", "admin.inventory.stock.view",
"admin.inventory.movements.view", "admin.inventory.movements.view",
"admin.inventory.transfers.view", "admin.inventory.transfers.view",
"admin.inventory.shipping-orders.view", "admin.shipping.orders.view",
"admin.inventory.shipping-tracking.view", "admin.shipping.tracking.view",
"admin.inventory.shipping-carriers.view", "admin.shipping.carriers.view",
"admin.inventory.shipping-rates.view", "admin.shipping.rates.view",
"admin.inventory.assets.view", "admin.assets.assets.view",
"admin.inventory.asset-maintenance.view", "admin.assets.maintenance.view",
"admin.inventory.asset-lifecycle.view", "admin.assets.lifecycle.view",
"admin.inventory.asset-assignments.view", "admin.assets.assignments.view",
"admin.projects.dashboard.view", "admin.projects.dashboard.view",
"admin.projects.view", "admin.projects.view",
"admin.projects.create", "admin.projects.create",
@ -167,21 +167,21 @@
"admin.rrhh.organization.view", "admin.rrhh.organization.view",
"admin.inventory.product-categories.view", "admin.inventory.product-categories.view",
"admin.inventory.product-catalogs.view", "admin.inventory.product-catalogs.view",
"admin.inventory.products.view", "admin.products.products.view",
"admin.inventory.products.create", "admin.products.products.create",
"admin.contacts.suppliers.view", "admin.contacts.suppliers.view",
"admin.contacts.suppliers.create", "admin.contacts.suppliers.create",
"admin.inventory.warehouse.view", "admin.inventory.warehouse.view",
"admin.inventory.orders.view", "admin.purchase-orders.orders.view",
"admin.inventory.reception.view", "admin.purchase-orders.reception.view",
"admin.inventory.materials.view", "admin.purchase-orders.materials.view",
"admin.inventory.stock.view", "admin.inventory.stock.view",
"admin.inventory.movements.view", "admin.inventory.movements.view",
"admin.inventory.transfers.view", "admin.inventory.transfers.view",
"admin.inventory.assets.view", "admin.assets.assets.view",
"admin.inventory.asset-maintenance.view", "admin.assets.maintenance.view",
"admin.inventory.asset-lifecycle.view", "admin.assets.lifecycle.view",
"admin.inventory.asset-assignments.view" "admin.assets.assignments.view"
] ]
}, },
"Administrador Web" : { "Administrador Web" : {
@ -197,8 +197,8 @@
"permissions" : [ "permissions" : [
"admin.inventory.product-categories.view", "admin.inventory.product-categories.view",
"admin.inventory.product-catalogs.view", "admin.inventory.product-catalogs.view",
"admin.inventory.products.view", "admin.products.products.view",
"admin.inventory.products.create", "admin.products.products.create",
"admin.inventory.warehouse.view", "admin.inventory.warehouse.view",
"admin.inventory.stock.view", "admin.inventory.stock.view",
"admin.inventory.movements.view", "admin.inventory.movements.view",
@ -293,7 +293,7 @@
"admin.attendance.absences.view", "admin.attendance.absences.view",
"admin.inventory.product-categories.view", "admin.inventory.product-categories.view",
"admin.inventory.product-catalogs.view", "admin.inventory.product-catalogs.view",
"admin.inventory.products.view", "admin.products.products.view",
"admin.contacts.customers.view", "admin.contacts.customers.view",
"admin.sales.sales.view", "admin.sales.sales.view",
"admin.sales.quotes.view", "admin.sales.quotes.view",
@ -322,21 +322,21 @@
"admin.billing.pagos.view", "admin.billing.pagos.view",
"admin.billing.nomina.view", "admin.billing.nomina.view",
"admin.contacts.suppliers.view", "admin.contacts.suppliers.view",
"admin.inventory.orders.view", "admin.purchase-orders.orders.view",
"admin.inventory.reception.view", "admin.purchase-orders.reception.view",
"admin.inventory.materials.view", "admin.purchase-orders.materials.view",
"admin.inventory.warehouse.view", "admin.inventory.warehouse.view",
"admin.inventory.stock.view", "admin.inventory.stock.view",
"admin.inventory.movements.view", "admin.inventory.movements.view",
"admin.inventory.transfers.view", "admin.inventory.transfers.view",
"admin.inventory.shipping-orders.view", "admin.shipping.orders.view",
"admin.inventory.shipping-tracking.view", "admin.shipping.tracking.view",
"admin.inventory.shipping-carriers.view", "admin.shipping.carriers.view",
"admin.inventory.shipping-rates.view", "admin.shipping.rates.view",
"admin.inventory.assets.view", "admin.assets.assets.view",
"admin.inventory.asset-maintenance.view", "admin.assets.maintenance.view",
"admin.inventory.asset-lifecycle.view", "admin.assets.lifecycle.view",
"admin.inventory.asset-assignments.view", "admin.assets.assignments.view",
"admin.projects.dashboard.view", "admin.projects.dashboard.view",
"admin.projects.view", "admin.projects.view",
"admin.projects.tasks.view", "admin.projects.tasks.view",
@ -421,8 +421,8 @@
"admin.attendance.absences.view", "admin.attendance.absences.view",
"admin.inventory.product-categories.view", "admin.inventory.product-categories.view",
"admin.inventory.product-catalogs.view", "admin.inventory.product-catalogs.view",
"admin.inventory.products.view", "admin.products.products.view",
"admin.inventory.products.create", "admin.products.products.create",
"admin.sales.dashboard.allow", "admin.sales.dashboard.allow",
"admin.contacts.customers.view", "admin.contacts.customers.view",
"admin.contacts.customers.create", "admin.contacts.customers.create",
@ -463,21 +463,21 @@
"admin.billing.verify-cfdi.allow", "admin.billing.verify-cfdi.allow",
"admin.contacts.suppliers.view", "admin.contacts.suppliers.view",
"admin.contacts.suppliers.create", "admin.contacts.suppliers.create",
"admin.inventory.orders.view", "admin.purchase-orders.orders.view",
"admin.inventory.reception.view", "admin.purchase-orders.reception.view",
"admin.inventory.materials.view", "admin.purchase-orders.materials.view",
"admin.inventory.warehouse.view", "admin.inventory.warehouse.view",
"admin.inventory.stock.view", "admin.inventory.stock.view",
"admin.inventory.movements.view", "admin.inventory.movements.view",
"admin.inventory.transfers.view", "admin.inventory.transfers.view",
"admin.inventory.shipping-orders.view", "admin.shipping.orders.view",
"admin.inventory.shipping-tracking.view", "admin.shipping.tracking.view",
"admin.inventory.shipping-carriers.view", "admin.shipping.carriers.view",
"admin.inventory.shipping-rates.view", "admin.shipping.rates.view",
"admin.inventory.assets.view", "admin.assets.assets.view",
"admin.inventory.asset-maintenance.view", "admin.assets.maintenance.view",
"admin.inventory.asset-lifecycle.view", "admin.assets.lifecycle.view",
"admin.inventory.asset-assignments.view", "admin.assets.assignments.view",
"admin.projects.dashboard.view", "admin.projects.dashboard.view",
"admin.projects.view", "admin.projects.view",
"admin.projects.create", "admin.projects.create",

View File

@ -3,6 +3,7 @@
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration return new class extends Migration
{ {
@ -12,18 +13,50 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::create('settings', function (Blueprint $table) { Schema::create('settings', function (Blueprint $table) {
$table->mediumIncrements('id'); $table->smallIncrements('id');
// Clave del setting
$table->string('key')->index(); $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(); $table->unsignedMediumInteger('user_id')->nullable()->index();
// Unique constraints // Valores segmentados por tipo para mejor rendimiento
$table->unique(['user_id', 'key']); $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 // 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");
} }
/** /**

View File

@ -1,48 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('media_items', function (Blueprint $table) {
$table->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');
}
};

View File

@ -1,29 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Crypt;
use Koneko\VuexyAdmin\Models\Setting;
class SettingSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$settings_array = [
//
];
foreach ($settings_array as $key => $value) {
Setting::create([
'key' => $key,
'value' => $value,
]);
};
}
}

View File

@ -106,7 +106,8 @@ class BootstrapTableManager {
* Carga los formatters dinámicamente * Carga los formatters dinámicamente
*/ */
async loadFormatters() { 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 formatterPromises = Object.entries(formattersModules).map(async ([path, importer]) => {
const module = await importer(); const module = await importer();

View File

@ -1,5 +1,5 @@
import { booleanStatusCatalog, statusIntBadgeBgCatalogCss, statusIntBadgeBgCatalog } from './globalConfig'; 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) => { export const userActionFormatter = (value, row, index) => {
if (!row.id) return ''; if (!row.id) return '';

View File

@ -10,7 +10,7 @@
// JS global variables // JS global variables
window.config = { window.config = {
colors: { colors: {
primary: '#7367f0', primary: '#155dfc',
secondary: '#808390', secondary: '#808390',
success: '#28c76f', success: '#28c76f',
info: '#00bad1', info: '#00bad1',
@ -27,7 +27,7 @@ window.config = {
borderColor: '#e6e6e8' borderColor: '#e6e6e8'
}, },
colors_label: { colors_label: {
primary: '#7367f029', primary: '#155dfc29',
secondary: '#a8aaae29', secondary: '#a8aaae29',
success: '#28c76f29', success: '#28c76f29',
info: '#00cfe829', info: '#00cfe829',

View File

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

View File

@ -155,7 +155,19 @@ export default class FormCustomListener {
this.setButtonLoadingState(saveButton, true); // Poner en estado de carga al botón anfitrión this.setButtonLoadingState(saveButton, true); // Poner en estado de carga al botón anfitrión
// Enviar la solicitud de Livewire correspondiente al enviar el formulario // 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) { toggleButtonsState(buttons, isEnabled) {
buttons.forEach(button => { 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) { initializeValidation(form) {
if (this.config.validationConfig) { if (this.config.validationConfig) {
this.formValidationInstance = FormValidation.formValidation(form, this.config.validationConfig).on( this.formValidationInstance = FormValidation.formValidation(
'core.form.valid', form,
() => this.handleFormValid(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() { reloadValidation() {
const form = document.querySelector(this.config.formSelector); const form = document.querySelector(this.config.formSelector);
// Verificar si el formulario existe y si la validación está inicializada
if (form && this.formValidationInstance) { if (form && this.formValidationInstance) {
try { try {
// En lugar de destruir la validación, simplemente reiniciamos la validación. setTimeout(() => {
this.formValidationInstance.resetForm(); // Resetear el formulario, limpiando los errores this.formValidationInstance.resetForm(); // Limpiar errores
this.initializeValidation(form); // Reinicializar
// Reinicializar la validación con la configuración actual // 🔁 Reconectar eventos de inputs y botones
this.initializeValidation(form); this.initFormEvents(form);
}, 1);
} catch (error) { } catch (error) {
console.error('Error al reiniciar la validación:', 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.'); console.warn('Formulario no encontrado o instancia de validación no disponible.');
} }
} }
} }

View File

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

View File

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

View File

@ -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('<b>Ubicación seleccionada</b>').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;

View File

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

View File

@ -39,7 +39,7 @@ $grays: (
// scss-docs-start color-variables // scss-docs-start color-variables
$blue: #007bff !default; $blue: #007bff !default;
$indigo: #6610f2 !default; $indigo: #6610f2 !default;
$purple: #7367f0 !default; $purple: #64748b !default;
$pink: #e83e8c !default; $pink: #e83e8c !default;
$red: #ff4c51 !default; $red: #ff4c51 !default;
$orange: #fd7e14 !default; $orange: #fd7e14 !default;

View File

@ -35,7 +35,7 @@ $grays: (
// scss-docs-start color-variables // scss-docs-start color-variables
$blue: #007bff !default; $blue: #007bff !default;
$indigo: #6610f2 !default; $indigo: #6610f2 !default;
$purple: #7367f0 !default; $purple: #64748b !default;
$pink: #e83e8c !default; $pink: #e83e8c !default;
$red: #ff4c51 !default; $red: #ff4c51 !default;
$orange: #fd7e14 !default; $orange: #fd7e14 !default;

View File

@ -2,3 +2,5 @@
@import 'bootstrap-extended-dark'; @import 'bootstrap-extended-dark';
@import 'components-dark'; @import 'components-dark';
@import 'colors-dark'; @import 'colors-dark';
$primary-color: #4a8c08;

View File

@ -2,3 +2,5 @@
@import 'bootstrap-extended'; @import 'bootstrap-extended';
@import 'components'; @import 'components';
@import 'colors'; @import 'colors';
$primary-color: #4a8c08;

View File

@ -4,7 +4,7 @@
@import './_theme/pages'; @import './_theme/pages';
@import './_theme/_theme'; @import './_theme/_theme';
$primary-color: #7367f0; $primary-color: #155dfc;
body { body {
background: $card-bg; background: $card-bg;

View File

@ -4,7 +4,7 @@
@import './_theme/pages'; @import './_theme/pages';
@import './_theme/_theme'; @import './_theme/_theme';
$primary-color: #7367f0; $primary-color: #155dfc;
$body-bg: #f8f7fa; $body-bg: #f8f7fa;
body { body {

View File

@ -4,7 +4,7 @@
@import './_theme/pages'; @import './_theme/pages';
@import './_theme/_theme'; @import './_theme/_theme';
$primary-color: #7367f0; $primary-color: #155dfc;
body { body {
background: $body-bg; background: $body-bg;

View File

@ -4,7 +4,7 @@
@import './_theme/pages'; @import './_theme/pages';
@import './_theme/_theme'; @import './_theme/_theme';
$primary-color: #7367f0; $primary-color: #155dfc;
$body-bg: #f8f7fa; $body-bg: #f8f7fa;
body { body {

View File

@ -4,7 +4,7 @@
@import './_theme/pages'; @import './_theme/pages';
@import './_theme/_theme'; @import './_theme/_theme';
$primary-color: #7367f0; $primary-color: #155dfc;
body { body {
background: $body-bg; background: $body-bg;

View File

@ -4,7 +4,7 @@
@import './_theme/pages'; @import './_theme/pages';
@import './_theme/_theme'; @import './_theme/_theme';
$primary-color: #7367f0; $primary-color: #155dfc;
$body-bg: #f8f7fa; $body-bg: #f8f7fa;
body { body {

View File

@ -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;

View File

@ -4,24 +4,3 @@ import FormCustomListener from '../../assets/js/forms/formCustomListener';
new FormCustomListener({ new FormCustomListener({
buttonSelectors: ['.btn-save', '.btn-cancel', '.btn-reset'] // Selectores para botones 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');
});

View File

@ -7,7 +7,7 @@ window.senderResponseForm = new SenderResponseForm();
Livewire.hook('morphed', ({ component }) => { Livewire.hook('morphed', ({ component }) => {
switch (component.name) { switch (component.name) {
case 'mail-smtp-settings': case 'sendmail-settings':
if (window.smtpSettingsForm) { if (window.smtpSettingsForm) {
window.smtpSettingsForm.reload(); // Recarga el formulario sin destruir la instancia window.smtpSettingsForm.reload(); // Recarga el formulario sin destruir la instancia
} }

View File

@ -1,7 +1,7 @@
export default class SmtpSettingsForm { export default class SmtpSettingsForm {
constructor(config = {}) { constructor(config = {}) {
const defaultConfig = { const defaultConfig = {
formSmtpSettingsSelector: '#mail-smtp-settings-card', formSmtpSettingsSelector: '#sendmail-settings-card',
changeSmtpSettingsId: 'change_smtp_settings', changeSmtpSettingsId: 'change_smtp_settings',
testSmtpConnectionButtonId: 'test_smtp_connection_button', testSmtpConnectionButtonId: 'test_smtp_connection_button',
saveSmtpConnectionButtonId: 'save_smtp_connection_button', saveSmtpConnectionButtonId: 'save_smtp_connection_button',

View File

@ -2,6 +2,10 @@
@import 'tailwindcss/components'; @import 'tailwindcss/components';
@import 'tailwindcss/utilities'; @import 'tailwindcss/utilities';
[x-cloak] {
display: none !important;
}
.menu-horizontal-wrapper > .menu-inner > .menu-item:last-child { .menu-horizontal-wrapper > .menu-inner > .menu-item:last-child {
padding-right: 70px; padding-right: 70px;
} }

View File

@ -85,7 +85,7 @@ $authentication-1-inner-max-width: 460px !default;
position: absolute; position: absolute;
top: -35px; top: -35px;
left: -45px; 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 { &:after {
@include light.media-breakpoint-down(sm) { @include light.media-breakpoint-down(sm) {
@ -98,7 +98,7 @@ $authentication-1-inner-max-width: 460px !default;
z-index: -1; z-index: -1;
bottom: -30px; bottom: -30px;
right: -56px; 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");
} }
} }

View File

@ -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')
<div class="row">
<div class="col-lg-4">
<!-- App Settings Card -->
<div class="mb-4">
@livewire('application-settings')
</div>
</div>
<div class="col-lg-4">
<!-- General Settings Card -->
<div class="mb-4">
@livewire('general-settings')
</div>
</div>
<div class="col-lg-4">
<!-- Interface Settings Card -->
<div class="mb-4">
@livewire('interface-settings')
</div>
</div>
</div>
@endsection

View File

@ -10,28 +10,28 @@
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<div class="mb-6"> <div class="mb-6">
@livewire('cache-stats') @livewire('vuexy-admin::cache-stats')
</div> </div>
<div class="mb-6"> <div class="mb-6">
@livewire('session-stats') @livewire('vuexy-admin::session-stats')
</div> </div>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<div class="mb-6"> <div class="mb-6">
@livewire('cache-functions') @livewire('vuexy-admin::cache-functions')
</div> </div>
<div class="row"> <div class="row">
@if($configCache['redisInUse']) @if($configCache['redisInUse'])
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-6"> <div class="mb-6">
@livewire('redis-stats') @livewire('vuexy-admin::redis-stats')
</div> </div>
</div> </div>
@endif @endif
@if($configCache['memcachedInUse']) @if($configCache['memcachedInUse'])
<div class="col-md-6"> <div class="col-md-6">
<div class="mb-6"> <div class="mb-6">
@livewire('memcached-stats') @livewire('vuexy-admin::memcached-stats')
</div> </div>
</div> </div>
@endif @endif

View File

@ -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
<div class="alert {{ $alertClass }} {{ $dismissible ? 'alert-dismissible' : '' }} d-flex align-items-center" role="alert">
@if ($icon)
<span class="alert-icon rounded me-2">
<i class="{{ $icon }}"></i>
</span>
@endif
<div>
{{ $slot }}
</div>
@if ($dismissible)
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
@endif
</div>

View File

@ -71,7 +71,7 @@
@endphp @endphp
{{-- ============================ CHECKBOX CON INPUT GROUP ============================ --}} {{-- ============================ CHECKBOX CON INPUT GROUP ============================ --}}
<div class="mb-4 {{ $parentClass }}"> <div class="mb-4 {{ $parentClass }} fv-row">
@if ($label) @if ($label)
<label for="{{ $checkboxId }}" class="{{ $labelClass }}">{{ $label }}</label> <label for="{{ $checkboxId }}" class="{{ $labelClass }}">{{ $label }}</label>
@endif @endif

View File

@ -74,7 +74,7 @@
@if ($switch) @if ($switch)
{{-- ============================ MODO SWITCH ============================ --}} {{-- ============================ MODO SWITCH ============================ --}}
<div class="{{ $alignClass }} {{ $inline ? 'd-inline-block' : '' }} {{ $parentClass }} {{ $mb0 ? '' : 'mb-2' }}"> <div class="{{ $alignClass }} {{ $inline ? 'd-inline-block' : '' }} {{ $parentClass }} {{ $mb0 ? '' : 'mb-2' }} fv-row">
<label class="switch {{ $switchTypeClass }} {{ $switchColorClass }} {{ $sizeClass }} {{ $labelClass }}"> <label class="switch {{ $switchTypeClass }} {{ $switchColorClass }} {{ $sizeClass }} {{ $labelClass }}">
<input <input
{{ $livewireModel ? "wire:model=$livewireModel" : '' }} {{ $livewireModel ? "wire:model=$livewireModel" : '' }}
@ -110,7 +110,7 @@
@else @else
{{-- ============================ MODO CHECKBOX ============================ --}} {{-- ============================ MODO CHECKBOX ============================ --}}
<div class="form-check {{ $checkColorClass }} {{ $alignClass }} {{ $sizeClass }} {{ $inline ? 'form-check-inline' : '' }} {{ $parentClass }} {{ $mb0 ? '' : 'mb-4' }}"> <div class="form-check {{ $checkColorClass }} {{ $alignClass }} {{ $sizeClass }} {{ $inline ? 'form-check-inline' : '' }} {{ $parentClass }} {{ $mb0 ? '' : 'mb-4' }} fv-row">
<input <input
{{ $livewireModel ? "wire:model=$livewireModel" : '' }} {{ $livewireModel ? "wire:model=$livewireModel" : '' }}
{{ $disabled ? 'disabled' : '' }} {{ $disabled ? 'disabled' : '' }}

View File

@ -24,7 +24,7 @@
: ($image ? "<img src='{$image}' alt='{$title}' class='img-fluid rounded'>" : ''); : ($image ? "<img src='{$image}' alt='{$title}' class='img-fluid rounded'>" : '');
@endphp @endphp
<div class="mb-4 form-check custom-option custom-option-icon {{ $checked ? 'checked' : '' }}"> <div class="mb-4 form-check custom-option custom-option-icon {{ $checked ? 'checked' : '' }} fv-row">
<label class="form-check-label custom-option-content" for="{{ $inputId }}"> <label class="form-check-label custom-option-content" for="{{ $inputId }}">
<span class="custom-option-body"> <span class="custom-option-body">
{!! $visualContent !!} {!! $visualContent !!}

View File

@ -8,6 +8,8 @@
'wireSubmit' => false, // Usar wire:submit.prevent 'wireSubmit' => false, // Usar wire:submit.prevent
'class' => '', // Clases adicionales para el formulario 'class' => '', // Clases adicionales para el formulario
'actionPosition' => 'bottom', // Posición de acciones: top, bottom, both, none 'actionPosition' => 'bottom', // Posición de acciones: top, bottom, both, none
'whitOutId' => false, // Excluir el ID del formulario
'whitOutMode' => false, // Excluir el modo del formulario
]) ])
@php @php
@ -28,8 +30,12 @@
@endphp @endphp
<form {{ $attributes->merge($formAttributes) }}> <form {{ $attributes->merge($formAttributes) }}>
@if (!$whitOutId)
<x-vuexy-admin::form.input :uid="$uniqueId" type="hidden" model="id" /> <x-vuexy-admin::form.input :uid="$uniqueId" type="hidden" model="id" />
@endif
@if (!$whitOutMode)
<x-vuexy-admin::form.input :uid="$uniqueId" type="hidden" model="mode" /> <x-vuexy-admin::form.input :uid="$uniqueId" type="hidden" model="mode" />
@endif
@if ($mode !== 'delete' && in_array($actionPosition, ['top', 'both'])) @if ($mode !== 'delete' && in_array($actionPosition, ['top', 'both']))
<div class="notification-container mb-4"></div> <div class="notification-container mb-4"></div>
<div class="form-actions mb-4"> <div class="form-actions mb-4">

View File

@ -1,184 +0,0 @@
@props([
'uid' => uniqid(),
'model' => '',
'label' => '',
'labelClass' => '',
'class' => '',
'align' => 'start',
'size' => '',
'mb-0' => false,
'parentClass' => '',
'prefix' => null,
'suffix' => null,
'icon' => null,
'clickableIcon' => null,
'inline' => false,
'labelCol' => 4,
'inputCol' => 8,
'floatLabel' => false,
'helperText' => '',
'attributes' => new \Illuminate\View\ComponentAttributeBag([]), // Atributos adicionales
])
@php
// Configuración dinámica de atributos y clases CSS
$livewireModel = $attributes->get('wire:model', $model); // Permitir uso directo de wire:model en el atributo
$name = $name ?: $livewireModel; // Si no se proporciona el nombre, toma el nombre del modelo
$inputId = $id ?: ($uid ? $name . '_' . $uid : $name); // ID generado si no se proporciona uno
// Obtener los atributos actuales en un array
$attributesArray = array_merge([
'type' => $type,
'id' => $inputId,
'name' => $name,
]);
// Agregar wire:model solo si existe
if ($livewireModel) {
$attributesArray['wire:model'] = $livewireModel;
}
$attributesArray = array_merge($attributesArray, $attributes->getAttributes());
// Reconstruir el ComponentAttributeBag con los atributos modificados
$inputAttributes = new \Illuminate\View\ComponentAttributeBag($attributesArray);
dump($inputAttributes);
// Manejo de errores de validación
$errorKey = $livewireModel ?: $name;
$errorClass = $errors->has($errorKey) ? 'is-invalid' : '';
// Definir el tamaño del input basado en la clase seleccionada
$sizeClass = $size === 'small' ? 'form-control-sm' : ($size === 'large' ? 'form-control-lg' : '');
// Alineación del texto
$alignClass = match ($align) {
'center' => 'text-center',
'end' => 'text-end',
default => ''
};
// Clases combinadas para el input
$fullClass = trim("form-control $sizeClass $alignClass $errorClass $class");
// Detectar si se necesita usar input-group
$requiresInputGroup = $prefix || $suffix || $icon || $clickableIcon;
@endphp
{{-- Input oculto sin estilos --}}
@if($type === 'hidden')
<input type="hidden" id="{{ $inputId }}" name="{{ $name }}" {{ $livewireModel ? "wire:model=$livewireModel" : '' }}
/>
@elseif($floatLabel)
{{-- Input con etiqueta flotante --}}
<div class="form-floating mb-4 {{ $parentClass }}">
<input
type="{{ $type }}"
id="{{ $inputId }}"
name="{{ $name }}"
{{ $livewireModel ? "wire:model=$livewireModel" : '' }}
class="{{ $fullClass }}"
placeholder="{{ $placeholder ?: 'Ingrese ' . strtolower($label) }}"
{{ $step ? "step=$step" : '' }}
{{ $max ? "maxlength=$max" : '' }}
{{ $min ? "minlength=$min" : '' }}
{{ $pattern ? "pattern=$pattern" : '' }}
{{ $disabled ? 'disabled' : '' }}
{{ $multiple ? 'multiple' : '' }} />
<label for="{{ $inputId }}">{{ $label }}</label>
@if ($helperText)
<div class="form-text">{{ $helperText }}</div>
@endif
@if ($errors->has($errorKey))
<span class="text-danger">{{ $errors->first($errorKey) }}</span>
@endif
</div>
@else
{{-- Input con formato clásico --}}
<div class="{{ $inline ? 'row' : '' }} {{ $parentClass }} fv-row mb-4">
@isset($label)
<label for="{{ $inputId }}" class="{{ $inline ? 'col-form-label col-md-' . $labelCol : 'form-label' }} {{ $labelClass }}">{{ $label }}</label>
@endisset
<div class="{{ $inline ? 'col-md-' . $inputCol : '' }}">
@if ($requiresInputGroup)
<div class="input-group input-group-merge">
@if ($prefix)
<span class="input-group-text">{{ $prefix }}</span>
@endif
@if ($icon)
<span class="input-group-text"><i class="{{ $icon }}"></i></span>
@endif
<input
type="{{ $type }}"
id="{{ $inputId }}"
name="{{ $name }}"
{{ $livewireModel ? "wire:model=$livewireModel" : '' }}
class="{{ $fullClass }}"
placeholder="{{ $placeholder ?: 'Ingrese ' . strtolower($label) }}"
{{ $step ? "step=$step" : '' }}
{{ $max ? "maxlength=$max" : '' }}
{{ $min ? "minlength=$min" : '' }}
{{ $pattern ? "pattern=$pattern" : '' }}
{{ $disabled ? 'disabled' : '' }}
{{ $multiple ? 'multiple' : '' }} />
@if ($suffix)
<span class="input-group-text">{{ $suffix }}</span>
@endif
@if ($clickableIcon)
<button type="button" class="input-group-text cursor-pointer">
<i class="{{ $clickableIcon }}"></i>
</button>
@endif
</div>
@else
{{-- Input sin prefijo o íconos --}}
<input
type="{{ $type }}"
id="{{ $inputId }}"
name="{{ $name }}"
{{ $livewireModel ? "wire:model=$livewireModel" : '' }}
class="{{ $fullClass }}"
placeholder="{{ $placeholder ?: 'Ingrese ' . strtolower($label) }}"
{{ $step ? "step=$step" : '' }}
{{ $max ? "maxlength=$max" : '' }}
{{ $min ? "minlength=$min" : '' }}
{{ $pattern ? "pattern=$pattern" : '' }}
{{ $disabled ? 'disabled' : '' }}
{{ $multiple ? 'multiple' : '' }} />
@endif
@if ($helperText)
<small class="form-text text-muted">{{ $helperText }}</small>
@endif
@if ($errors->has($errorKey))
<span class="text-danger">{{ $errors->first($errorKey) }}</span>
@endif
</div>
</div>
@endif

View File

@ -16,13 +16,18 @@
'mb0' => false, // Remover margen inferior 'mb0' => false, // Remover margen inferior
'parentClass' => '', 'parentClass' => '',
// Elementos opcionales antes/después del input // Elementos de prefijo
'prefix' => null, 'prefix' => null,
'suffix' => null, 'prefixIcon' => null,
'icon' => null, // Alias para prefixIcon
'prefixClickable' => false,
'prefixAction' => null,
// Íconos dentro del input // Elementos de sufijo
'icon' => null, 'suffix' => null,
'clickableIcon' => null, 'suffixIcon' => null,
'suffixClickable' => false,
'suffixAction' => null,
// Configuración especial // Configuración especial
'phoneMode' => false, // "national", "international", "both" 'phoneMode' => false, // "national", "international", "both"
@ -47,6 +52,9 @@
$inputId = $attributes->get('id', $name . '_' . $uid); $inputId = $attributes->get('id', $name . '_' . $uid);
$type = $attributes->get('type', 'text'); $type = $attributes->get('type', 'text');
// Manejar el alias de icon a prefixIcon
$prefixIcon = $prefixIcon ?? $icon;
// **Definir formato de teléfono según `phoneMode`** // **Definir formato de teléfono según `phoneMode`**
if ($phoneMode) { if ($phoneMode) {
$type = 'tel'; $type = 'tel';
@ -120,37 +128,68 @@
'id' => $inputId, 'id' => $inputId,
'name' => $name, 'name' => $name,
])->class("form-control $sizeClass $alignClass $errorClass"); ])->class("form-control $sizeClass $alignClass $errorClass");
// Verificar si se necesita el input-group
$hasAddons = $prefix || $prefixIcon || $suffix || $suffixIcon;
@endphp @endphp
{{-- Estructura del Input --}} {{-- Estructura del Input --}}
<div class="{{ $mb0 ? '' : 'mb-4' }} {{ $parentClass }}"> <div class="{{ $mb0 ? '' : 'mb-4' }} {{ $parentClass }} fv-row">
{{-- Etiqueta --}} {{-- Etiqueta --}}
@if ($label) @if ($label)
<label for="{{ $inputId }}" class="form-label {{ $labelClass }}">{{ $label }}</label> <label for="{{ $inputId }}" class="form-label {{ $labelClass }}">{{ $label }}</label>
@endif @endif
{{-- Input con Prefijos, Sufijos o Íconos --}} {{-- Input con Prefijos o Sufijos --}}
@if ($prefix || $suffix || $icon || $clickableIcon) @if ($hasAddons)
<div class="input-group input-group-merge"> <div class="input-group input-group-merge">
@isset($prefix) {{-- Prefijo --}}
<span class="input-group-text">{{ $prefix }}</span> @if ($prefix || $prefixIcon)
@endisset @if ($prefixClickable)
<button type="button" class="input-group-text cursor-pointer" {{ $prefixAction ? "wire:click=$prefixAction" : '' }}>
@isset($icon) @if ($prefixIcon)
<span class="input-group-text"><i class="{{ $icon }}"></i></span> <i class="{{ $prefixIcon }}"></i>
@endisset @endif
@if ($prefix)
{{ $prefix }}
@endif
</button>
@else
<span class="input-group-text">
@if ($prefixIcon)
<i class="{{ $prefixIcon }}"></i>
@endif
@if ($prefix)
{{ $prefix }}
@endif
</span>
@endif
@endif
<input {!! $inputAttributes !!} {{ $livewireModel ? "wire:model=$livewireModel" : '' }} /> <input {!! $inputAttributes !!} {{ $livewireModel ? "wire:model=$livewireModel" : '' }} />
@isset($suffix) {{-- Sufijo --}}
<span class="input-group-text">{{ $suffix }}</span> @if ($suffix || $suffixIcon)
@endisset @if ($suffixClickable)
<button type="button" class="input-group-text cursor-pointer" {{ $suffixAction ? "wire:click=$suffixAction" : '' }}>
@isset($clickableIcon) @if ($suffixIcon)
<button type="button" class="input-group-text cursor-pointer"> <i class="{{ $suffixIcon }}"></i>
<i class="{{ $clickableIcon }}"></i> @endif
@if ($suffix)
{{ $suffix }}
@endif
</button> </button>
@endisset @else
<span class="input-group-text">
@if ($suffixIcon)
<i class="{{ $suffixIcon }}"></i>
@endif
@if ($suffix)
{{ $suffix }}
@endif
</span>
@endif
@endif
</div> </div>
@else @else
{{-- Input Simple --}} {{-- Input Simple --}}

View File

@ -72,7 +72,7 @@
@endphp @endphp
{{-- ============================ RADIO BUTTON CON INPUT GROUP ============================ --}} {{-- ============================ RADIO BUTTON CON INPUT GROUP ============================ --}}
<div class="{{ $mb0 ? '' : 'mb-4' }} {{ $parentClass }}"> <div class="{{ $mb0 ? '' : 'mb-4' }} {{ $parentClass }} fv-row">
@if ($label) @if ($label)
<label for="{{ $radioId }}" class="{{ $labelClass }}">{{ $label }}</label> <label for="{{ $radioId }}" class="{{ $labelClass }}">{{ $label }}</label>
@endif @endif
@ -86,7 +86,7 @@
type="radio" type="radio"
{{ $livewireRadio }} {{ $livewireRadio }}
{{ $disabled ? 'disabled' : '' }} {{ $disabled ? 'disabled' : '' }}
class="form-check-input mt-0" class="form-check-input fv-row mt-0"
onchange="toggleRadioInputState('{{ $radioId }}', '{{ $textInputId }}', {{ $focusOnCheck ? 'true' : 'false' }}, {{ $disableOnOffRadio ? 'true' : 'false' }})" onchange="toggleRadioInputState('{{ $radioId }}', '{{ $textInputId }}', {{ $focusOnCheck ? 'true' : 'false' }}, {{ $disableOnOffRadio ? 'true' : 'false' }})"
> >
</div> </div>

View File

@ -72,7 +72,7 @@
@if ($switch) @if ($switch)
{{-- ============================ MODO SWITCH ============================ --}} {{-- ============================ MODO SWITCH ============================ --}}
<div class="{{ $alignClass }} {{ $inline ? 'd-inline-block' : '' }} {{ $parentClass }} {{ $mb0 ? '' : 'mb-4' }}"> <div class="{{ $alignClass }} {{ $inline ? 'd-inline-block' : '' }} {{ $parentClass }} {{ $mb0 ? '' : 'mb-4' }} fv-row">
<label class="switch {{ $switchTypeClass }} {{ $switchColorClass }} {{ $sizeClass }} {{ $labelClass }}"> <label class="switch {{ $switchTypeClass }} {{ $switchColorClass }} {{ $sizeClass }} {{ $labelClass }}">
<input <input
{{ $livewireModel ? "wire:model=$livewireModel" : '' }} {{ $livewireModel ? "wire:model=$livewireModel" : '' }}
@ -102,7 +102,7 @@
@else @else
{{-- ============================ MODO RADIO ============================ --}} {{-- ============================ MODO RADIO ============================ --}}
<div class="{{ $layoutClass }} {{ $radioColorClass }} {{ $alignClass }} {{ $sizeClass }} {{ $inline ? 'form-check-inline' : '' }} {{ $parentClass }} {{ $mb0 ? '' : 'mb-4' }}"> <div class="{{ $layoutClass }} {{ $radioColorClass }} {{ $alignClass }} {{ $sizeClass }} {{ $inline ? 'form-check-inline' : '' }} {{ $parentClass }} {{ $mb0 ? '' : 'mb-4' }} fv-row">
<input <input
{{ $livewireModel ? "wire:model=$livewireModel" : '' }} {{ $livewireModel ? "wire:model=$livewireModel" : '' }}
{{ $disabled ? 'disabled' : '' }} {{ $disabled ? 'disabled' : '' }}

View File

@ -1,86 +0,0 @@
@props([
'uid' => uniqid(),
'id' => '',
'model' => '',
'name' => '',
'label' => '',
'labelClass' => 'form-label',
'placeholder' => '',
'options' => [],
'selected' => null,
'class' => '',
'parentClass' => '',
'multiple' => false,
'disabled' => false,
'prefixLabel' => null,
'suffixLabel' => null,
'buttonBefore' => null,
'buttonAfter' => null,
'inline' => false, // Si es en línea
'labelCol' => 2, // Columnas que ocupa el label (Bootstrap grid)
'inputCol' => 10, // Columnas que ocupa el input (Bootstrap grid)
'helperText' => '', // Texto de ayuda opcional
'select2' => false, // Activar Select2 automáticamente
])
@php
$name = $name ?: $model;
$inputId = $id ?: ($uid ? str_replace('.', '_', $name) . '_' . $uid : $name);
$placeholder = $placeholder ?: 'Seleccione ' . strtolower($label);
$errorClass = $errors->has($model) ? 'is-invalid' : '';
$options = is_array($options) ? collect($options) : $options;
$select2Class = $select2 ? 'select2' : ''; // Agrega la clase select2 si está habilitado
@endphp
<div class="{{ $inline ? 'row' : 'mb-4' }} {{ $parentClass }} fv-row">
@if($label != null)
<label for="{{ $inputId }}" class="{{ $inline ? 'col-md-' . $labelCol : '' }} {{ $labelClass }}">{{ $label }}</label>
@endif
<div class="{{ $inline ? 'col-md-' . $inputCol : '' }}">
<div class="input-group {{ $prefixLabel || $suffixLabel || $buttonBefore || $buttonAfter ? 'input-group-merge' : '' }}">
@if ($buttonBefore)
<button class="btn btn-outline-primary waves-effect" type="button">{{ $buttonBefore }}</button>
@endif
@if ($prefixLabel)
<label class="input-group-text" for="{{ $inputId }}">{{ $prefixLabel }}</label>
@endif
<select
id="{{ $inputId }}"
name="{{ $name }}"
class="form-select {{ $errorClass }} {{ $class }} {{ $select2Class }}"
{{ $multiple ? 'multiple' : '' }}
{{ $disabled ? 'disabled' : '' }}
{{ $model ? "wire:model=$model" : '' }}
{{ $select2 ? 'data-live-search="true"' : '' }}
>
@if (!$multiple && $placeholder)
<option value="">{{ $placeholder }}</option>
@endif
@foreach ($options as $key => $value)
<option value="{{ $key }}" {{ (string) $key === (string) $selected ? 'selected' : '' }}>
{{ $value }}
</option>
@endforeach
</select>
@if ($suffixLabel)
<label class="input-group-text" for="{{ $inputId }}">{{ $suffixLabel }}</label>
@endif
@if ($buttonAfter)
<button class="btn btn-outline-primary waves-effect" type="button">{{ $buttonAfter }}</button>
@endif
</div>
@if ($helperText)
<div class="form-text">{{ $helperText }}</div>
@endif
@if ($errors->has($model))
<span class="text-danger">{{ $errors->first($model) }}</span>
@endif
</div>
</div>

View File

@ -88,7 +88,7 @@
@endphp @endphp
{{-- ============================ TEXTAREA ============================ --}} {{-- ============================ TEXTAREA ============================ --}}
<div class="{{ $mb0 ? '' : 'mb-4' }} {{ $parentClass }} {{ $alignClass }} {{ $floatingClass }}"> <div class="{{ $mb0 ? '' : 'mb-4' }} {{ $parentClass }} {{ $alignClass }} {{ $floatingClass }} fv-row">
@if (!$floating && $label) @if (!$floating && $label)
<label for="{{ $inputId }}" class="{{ $labelClass }}">{{ $label }}</label> <label for="{{ $inputId }}" class="{{ $labelClass }}">{{ $label }}</label>
@endif @endif

Some files were not shown because too many files have changed in this diff Show More