From 3916c62935684044118a56717375b2c3c71c0dd0 Mon Sep 17 00:00:00 2001 From: Arturo Corro Date: Sat, 22 Mar 2025 12:41:56 -0600 Subject: [PATCH] Prepare modules --- Console/Commands/SitemapGenerate.php | 37 ++ Http/Controllers/ChatController.php | 19 + Http/Controllers/ContactFormController.php | 19 + Http/Controllers/ContactInfoController.php | 19 + Http/Controllers/FaqController.php | 19 + Http/Controllers/GeneralController.php | 186 --------- .../Controllers/GoogleAnalyticsController.php | 19 + Http/Controllers/ImagesController.php | 19 + Http/Controllers/LegalNoticesController.php | 19 + Http/Controllers/SitemapController.php | 19 + Http/Controllers/SocialMediaController.php | 19 + .../VuexyWebsiteAdminController.php | 19 + Livewire/Faq/FaqIndex.php | 103 +++++ Livewire/Faq/FaqOffcanvasForm.php | 217 ++++++++++ Livewire/Images/ImagesIndex.php | 104 +++++ .../LegalNotices/LegalNoticeOffCanvasForm.php | 217 ++++++++++ Livewire/LegalNotices/LegalNoticesIndex.php | 109 +++++ Livewire/LegalNotices/LegalSettings.php | 108 +++++ .../SitemapManager/SitemapManagerIndex.php | 38 ++ .../SitemapUrlOffcanvasForm.php | 217 ++++++++++ Livewire/VuexyWebsiteAdmin/ChatSettings.php | 67 +++ .../VuexyWebsiteAdmin/ContactFormSettings.php | 70 ++++ .../VuexyWebsiteAdmin/ContactInfoSettings.php | 78 ++++ .../GoogleAnalyticsSettings.php | 63 +++ .../VuexyWebsiteAdmin/LocationSettings.php | 69 +++ .../LogoOnDarkBgSettings.php | 61 +++ .../LogoOnLightBgSettings.php | 61 +++ .../VuexyWebsiteAdmin/SocialMediaSettings.php | 98 +++++ .../WebsiteDescriptionSettings.php | 62 +++ .../WebsiteFaviconSettings.php | 72 ++++ Models/Faq.php | 33 ++ Models/FaqCategory.php | 32 ++ Models/SitemapConfiguration.php | 0 Models/SitemapUrl.php | 19 + .../VuexyWebsiteAdminServiceProvider.php | 58 ++- Services/WebsiteSettingsService.php | 292 +++++++++++++ Services/WebsiteTemplateService.php | 395 ++++++++++++++++++ composer.json | 2 +- ..._29_081812_create_faq_categories_table.php | 34 ++ .../2024_12_29_081815_create_faqs_table.php | 38 ++ .../create_sitemap_configurations_table.php | 23 + .../migrations/create_sitemap_urls_table.php | 32 ++ resources/js/chat-settings-card.js | 73 ++++ resources/js/contact-form-settings-card.js | 86 ++++ resources/js/contact-info-settings-card.js | 213 ++++++++++ .../js/google-analytics-settings-card.js | 41 ++ resources/js/website-settings-card.js | 133 ++++++ resources/views/chat/index.blade.php | 29 ++ resources/views/contact-form/index.blade.php | 29 ++ resources/views/contact-info/index.blade.php | 32 ++ resources/views/faq/index.blade.php | 23 + .../views/general-settings/index.blade.php | 20 + .../views/google-analytics/index.blade.php | 29 ++ resources/views/images/index.blade.php | 11 + resources/views/legal-notices/index.blade.php | 7 + .../views/legal-notices/legal-index.blade.php | 28 ++ resources/views/livewire/faq/index.blade.php | 7 + .../views/livewire/images/index.blade.php | 12 + .../livewire/legal-notices/index.blade.php | 70 ++++ .../livewire/sitemap-manager/index.blade.php | 22 + .../vuexy/analytics-settings.blade.php | 34 ++ .../livewire/vuexy/chat-settings.blade.php | 58 +++ .../vuexy/contact-form-settings.blade.php | 33 ++ .../vuexy/contact-info-settings.blade.php | 41 ++ .../vuexy/location-settings.blade.php | 34 ++ .../vuexy/logo-on-dark-bg-settings.blade.php | 39 ++ .../vuexy/logo-on-light-bg-settings.blade.php | 39 ++ .../vuexy/social-media-settings.blade.php | 47 +++ .../vuexy/template-settings.blade.php | 49 +++ .../website-description-settings.blade.php | 31 ++ .../vuexy/website-favicon-settings.blade.php | 84 ++++ .../views/sitemap-manager/index.blade.php | 7 + resources/views/social-media/index.blade.php | 25 ++ routes/admin.php | 54 ++- 74 files changed, 4431 insertions(+), 194 deletions(-) create mode 100644 Console/Commands/SitemapGenerate.php create mode 100644 Http/Controllers/ChatController.php create mode 100644 Http/Controllers/ContactFormController.php create mode 100644 Http/Controllers/ContactInfoController.php create mode 100644 Http/Controllers/FaqController.php delete mode 100644 Http/Controllers/GeneralController.php create mode 100644 Http/Controllers/GoogleAnalyticsController.php create mode 100644 Http/Controllers/ImagesController.php create mode 100644 Http/Controllers/LegalNoticesController.php create mode 100644 Http/Controllers/SitemapController.php create mode 100644 Http/Controllers/SocialMediaController.php create mode 100644 Http/Controllers/VuexyWebsiteAdminController.php create mode 100644 Livewire/Faq/FaqIndex.php create mode 100644 Livewire/Faq/FaqOffcanvasForm.php create mode 100644 Livewire/Images/ImagesIndex.php create mode 100644 Livewire/LegalNotices/LegalNoticeOffCanvasForm.php create mode 100644 Livewire/LegalNotices/LegalNoticesIndex.php create mode 100644 Livewire/LegalNotices/LegalSettings.php create mode 100644 Livewire/SitemapManager/SitemapManagerIndex.php create mode 100644 Livewire/SitemapManager/SitemapUrlOffcanvasForm.php create mode 100644 Livewire/VuexyWebsiteAdmin/ChatSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/ContactFormSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/ContactInfoSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/GoogleAnalyticsSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/LocationSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/LogoOnDarkBgSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/LogoOnLightBgSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/SocialMediaSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/WebsiteDescriptionSettings.php create mode 100644 Livewire/VuexyWebsiteAdmin/WebsiteFaviconSettings.php create mode 100644 Models/Faq.php create mode 100644 Models/FaqCategory.php create mode 100644 Models/SitemapConfiguration.php create mode 100644 Models/SitemapUrl.php create mode 100644 Services/WebsiteSettingsService.php create mode 100644 Services/WebsiteTemplateService.php create mode 100644 database/migrations/2024_12_29_081812_create_faq_categories_table.php create mode 100644 database/migrations/2024_12_29_081815_create_faqs_table.php create mode 100644 database/migrations/create_sitemap_configurations_table.php create mode 100644 database/migrations/create_sitemap_urls_table.php create mode 100644 resources/js/chat-settings-card.js create mode 100644 resources/js/contact-form-settings-card.js create mode 100644 resources/js/contact-info-settings-card.js create mode 100644 resources/js/google-analytics-settings-card.js create mode 100644 resources/js/website-settings-card.js create mode 100644 resources/views/chat/index.blade.php create mode 100644 resources/views/contact-form/index.blade.php create mode 100644 resources/views/contact-info/index.blade.php create mode 100644 resources/views/faq/index.blade.php create mode 100644 resources/views/general-settings/index.blade.php create mode 100644 resources/views/google-analytics/index.blade.php create mode 100644 resources/views/images/index.blade.php create mode 100644 resources/views/legal-notices/index.blade.php create mode 100644 resources/views/legal-notices/legal-index.blade.php create mode 100644 resources/views/livewire/faq/index.blade.php create mode 100644 resources/views/livewire/images/index.blade.php create mode 100644 resources/views/livewire/legal-notices/index.blade.php create mode 100644 resources/views/livewire/sitemap-manager/index.blade.php create mode 100644 resources/views/livewire/vuexy/analytics-settings.blade.php create mode 100644 resources/views/livewire/vuexy/chat-settings.blade.php create mode 100644 resources/views/livewire/vuexy/contact-form-settings.blade.php create mode 100644 resources/views/livewire/vuexy/contact-info-settings.blade.php create mode 100644 resources/views/livewire/vuexy/location-settings.blade.php create mode 100644 resources/views/livewire/vuexy/logo-on-dark-bg-settings.blade.php create mode 100644 resources/views/livewire/vuexy/logo-on-light-bg-settings.blade.php create mode 100644 resources/views/livewire/vuexy/social-media-settings.blade.php create mode 100644 resources/views/livewire/vuexy/template-settings.blade.php create mode 100644 resources/views/livewire/vuexy/website-description-settings.blade.php create mode 100644 resources/views/livewire/vuexy/website-favicon-settings.blade.php create mode 100644 resources/views/sitemap-manager/index.blade.php create mode 100644 resources/views/social-media/index.blade.php diff --git a/Console/Commands/SitemapGenerate.php b/Console/Commands/SitemapGenerate.php new file mode 100644 index 0000000..5a2fd67 --- /dev/null +++ b/Console/Commands/SitemapGenerate.php @@ -0,0 +1,37 @@ +get(); + + $sitemap = '' . PHP_EOL; + $sitemap .= '' . PHP_EOL; + + foreach ($urls as $url) { + $sitemap .= " {$url->url}" . PHP_EOL; + $sitemap .= " {$url->changefreq}" . PHP_EOL; + $sitemap .= " {$url->priority}" . PHP_EOL; + if ($url->lastmod) { + $sitemap .= " {$url->lastmod->toDateString()}" . PHP_EOL; + } + $sitemap .= " " . PHP_EOL; + } + + $sitemap .= ''; + + Storage::disk('public')->put('sitemap.xml', $sitemap); + + $this->info('✅ Sitemap generado en storage/app/public/sitemap.xml'); + } +} \ No newline at end of file diff --git a/Http/Controllers/ChatController.php b/Http/Controllers/ChatController.php new file mode 100644 index 0000000..c00f22d --- /dev/null +++ b/Http/Controllers/ChatController.php @@ -0,0 +1,19 @@ + 'admin.home', 'name' => "Inicio"], - ['name' => "Ajustes"], - ['name' => "General"], - ['name' => "Aplicación web", 'active' => true], - ]; - - return view('admin.settings.general.webapp-index', compact('breadcrumbs')); - } - - public function store() - { - $breadcrumbs = [ - ['route' => 'admin.home', 'name' => "Inicio"], - ['name' => "Ajustes"], - ['name' => "General"], - ['name' => "Empresa", 'active' => true], - ]; - - return view('admin.settings.general.store-index', compact('breadcrumbs')); - } - - public function divisas(Request $request) - { - if ($request->ajax()) { - $query = DropdownList::select( - 'dropdown_lists.id', - 'dropdown_lists.single', - 'sat_moneda.descripcion', - 'dropdown_lists.param1', - 'dropdown_lists.param2', - 'dropdown_lists.param3', - 'dropdown_lists.param4', - 'dropdown_lists.created_at' - ) - ->Join('sat_moneda', 'dropdown_lists.single', '=', 'sat_moneda.c_moneda') - ->where('dropdown_lists.label', DropdownList::SYS_DIVISA); - - // Manejar el ordenamiento del lado del servidor basado en las columnas que DataTables solicita - if ($request->has('order')) { - $columns = [2 => 'single', 'descripcion', 'param1', 'param2', 'param3', 'param4', 'created_at']; - - foreach ($request->get('order') as $order) { - $query->orderBy($columns[$order['column']], $order['dir']); - } - } - - $warehouses = $query->get(); - - return DataTables::of($warehouses) - ->only(['id', 'single', 'descripcion', 'param1', 'param2', 'param3', 'param4', 'created_at']) - ->addIndexColumn() - ->editColumn('created_at', function ($user) { - return $user->created_at->format('Y-m-d'); - }) - ->make(true); - } - - $breadcrumbs = [ - ['route' => 'admin.home', 'name' => "Inicio"], - ['name' => "Ajustes"], - ['name' => "General"], - ['name' => "Divisas", 'active' => true], - ]; - - return view('admin.settings.general.divisas-index', compact('breadcrumbs')); - } - - public function warehouses(Request $request) - { - if ($request->ajax()) { - $query = DropdownList::select( - 'dropdown_lists.id', - 'dropdown_lists.single', - 'dropdown_lists.param1', - 'dropdown_lists.status', - 'dropdown_lists.created_at' - ) - ->where('dropdown_lists.label', DropdownList::SYS_WAREHOUSE); - - // Manejar el ordenamiento del lado del servidor basado en las columnas que DataTables solicita - if ($request->has('order')) { - $columns = [2 => 'single', 'param1', 'status', 'created_at']; - - foreach ($request->get('order') as $order) { - $query->orderBy($columns[$order['column']], $order['dir']); - } - } - - $warehouses = $query->get(); - - return DataTables::of($warehouses) - ->only(['id', 'single', 'param1', 'status', 'created_at']) - ->addIndexColumn() - ->editColumn('created_at', function ($user) { - return $user->created_at->format('Y-m-d'); - }) - ->make(true); - } - - $breadcrumbs = [ - ['route' => 'admin.home', 'name' => "Inicio"], - ['name' => "Ajustes"], - ['name' => "General"], - ['name' => "Almacenes", 'active' => true], - ]; - - return view('admin.settings.general.warehouses-index', compact('breadcrumbs')); - } - - public function formasPago2() - { - $breadcrumbs = [ - ['route' => 'admin.home', 'name' => "Inicio"], - ['name' => "Ajustes"], - ['name' => "General"], - ['name' => "Formas de pago ²", 'active' => true], - ]; - - return view('admin.settings.general.formas-pago-2-index', compact('breadcrumbs')); - } - - public function apiBanxico() - { - $breadcrumbs = [ - ['route' => 'admin.home', 'name' => "Inicio"], - ['name' => "Ajustes"], - ['name' => "General"], - ['name' => "API BANXICO", 'active' => true], - ]; - - return view('admin.settings.general.api-banxico-index', compact('breadcrumbs')); - } - - public function smtp() - { - $breadcrumbs = [ - ['route' => 'admin.home', 'name' => "Inicio"], - ['name' => "Ajustes"], - ['name' => "General"], - ['name' => "Servidor de correo SMTP", 'active' => true], - ]; - - return view('admin.settings.general.smtp-index', compact('breadcrumbs')); - } - - public function checkUniqueWarehouse(Request $request) - { - $id = $request->input('id'); - $single = $request->input('single'); - - $exists = DropdownList::where('single', $single) - ->where('label', DropdownList::SYS_WAREHOUSE) - ->where('id', '!=', $id) // Excluir el registro actual - ->exists(); - - return response()->json(['isUnique' => !$exists]); - } - - public function checkUniqueDivisa(Request $request) - { - $id = $request->input('id'); - $single = $request->input('single'); - - $exists = DropdownList::where('single', $single) - ->where('label', DropdownList::SYS_DIVISA) - ->where('id', '!=', $id) // Excluir el registro actual - ->exists(); - - return response()->json(['isUnique' => !$exists]); - } - -} diff --git a/Http/Controllers/GoogleAnalyticsController.php b/Http/Controllers/GoogleAnalyticsController.php new file mode 100644 index 0000000..efdfb15 --- /dev/null +++ b/Http/Controllers/GoogleAnalyticsController.php @@ -0,0 +1,19 @@ + 'Acciones', + 'status' => 'Estatus', + 'created_at' => 'Fecha de Creación', + 'updated_at' => 'Última Actualización', + ]; + } + + /** + * Define los formatos de cada columna (se inyectará en $bt_datatable['format']). + * + * @return array + */ + protected function format(): array + { + return [ + 'action' => [ + 'formatter' => 'FaqActionFormatter', + 'onlyFormatter' => true, + ], + + 'status' => [ + 'formatter' => [ + 'name' => 'dynamicBooleanFormatter', + 'params' => ['tag' => 'activo'] + ], + 'align' => 'center', + ], + 'created_at' => [ + 'formatter' => 'textNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + 'updated_at' => [ + 'formatter' => 'textNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + ]; + } + + /** + * Retorna la configuración base (común) para la tabla Bootstrap Table. + * + * @return array + */ + protected function bootstraptableConfig(): array + { + return [ + 'sortName' => 'code', + 'exportFileName' => 'Almacenes', + 'showFullscreen' => false, + 'showPaginationSwitch' => false, + 'showRefresh' => false, + 'pagination' => false, + ]; + } + + /** + * Retorna la ruta de la vista Blade. + * + * @return string + */ + protected function viewPath(): string + { + // La vista que ya tienes creada para FaqIndex + return 'vuexy-website-admin::livewire.faq.index'; + } + + /** + * Métodos que necesites sobreescribir o extender. + */ + public function mount(): void + { + parent::mount(); + } +} diff --git a/Livewire/Faq/FaqOffcanvasForm.php b/Livewire/Faq/FaqOffcanvasForm.php new file mode 100644 index 0000000..9057a72 --- /dev/null +++ b/Livewire/Faq/FaqOffcanvasForm.php @@ -0,0 +1,217 @@ + 'loadFormModel', + 'confirmDeletionWarehouse' => 'loadFormModelForDeletion', + ]; + + /** + * Definición de tipos de datos que se deben castear. + * + * @var array + */ + protected $casts = [ + 'status' => 'boolean', + ]; + + /** + * Define el modelo Eloquent asociado con el formulario. + * + * @return string + */ + protected function model(): string + { + return Warehouse::class; + } + + /** + * Define los campos del formulario. + * + * @return array + */ + protected function fields(): array + { + return (new Warehouse())->getFillable(); + } + + /** + * Valores por defecto para el formulario. + * + * @return array + */ + protected function defaults(): array + { + return [ + 'priority' => 0, + 'status' => true, + ]; + } + + /** + * Campo que se debe enfocar cuando se abra el formulario. + * + * @return string + */ + protected function focusOnOpen(): string + { + return 'code'; + } + + /** + * 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 [ + 'store_id' => ['required', 'integer', 'exists:stores,id'], + 'work_center_id' => ['nullable', 'integer', 'exists:store_work_centers,id'], + 'code' => ['required', 'string', 'max:16', Rule::unique('warehouses', 'code')->ignore($this->id)], + 'name' => ['required', 'string', 'max:96'], + 'description' => ['nullable', 'string', 'max:1024'], + 'manager_id' => ['nullable', 'integer', 'exists:users,id'], + 'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], + 'tel2' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], + 'priority' => ['nullable', 'numeric', 'between:0,99'], + 'status' => ['nullable', 'boolean'], + ]; + + case 'delete': + return [ + 'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación + ]; + + default: + return []; + } + } + + // ===================== VALIDACIONES ===================== + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + protected function attributes(): array + { + return [ + 'code' => 'código de almacén', + 'name' => 'nombre del almacén', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + protected function messages(): array + { + return [ + 'store_id.required' => 'El almacén debe estar asociado a un negocio.', + 'code.required' => 'El código del almacén es obligatorio.', + 'code.unique' => 'Este código ya está en uso por otro almacén.', + 'name.required' => 'El nombre del almacén es obligatorio.', + ]; + } + + /** + * Carga el formulario con datos del almacén y actualiza las opciones dinámicas. + * + * @param int $id + */ + public function loadFormModel($id): void + { + parent::loadFormModel($id); + + $this->work_center_options = DB::table('store_work_centers') + ->where('store_id', $this->store_id) + ->pluck('name', 'id') + ->toArray(); + } + + /** + * Carga el formulario para eliminar un almacén, 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]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.faq.offcanvas-form'; + } +} diff --git a/Livewire/Images/ImagesIndex.php b/Livewire/Images/ImagesIndex.php new file mode 100644 index 0000000..9b30650 --- /dev/null +++ b/Livewire/Images/ImagesIndex.php @@ -0,0 +1,104 @@ + 'Acciones', + 'status' => 'Estatus', + 'created_at' => 'Fecha de Creación', + 'updated_at' => 'Última Actualización', + ]; + } + + /** + * Define los formatos de cada columna (se inyectará en $bt_datatable['format']). + * + * @return array + */ + protected function format(): array + { + return [ + 'action' => [ + 'formatter' => 'ImagesActionFormatter', + 'onlyFormatter' => true, + ], + + 'status' => [ + 'formatter' => [ + 'name' => 'dynamicBooleanFormatter', + 'params' => ['tag' => 'activo'] + ], + 'align' => 'center', + ], + 'created_at' => [ + 'formatter' => 'textNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + 'updated_at' => [ + 'formatter' => 'textNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + ]; + } + + /** + * Retorna la configuración base (común) para la tabla Bootstrap Table. + * + * @return array + */ + protected function bootstraptableConfig(): array + { + return [ + 'sortName' => 'code', + 'exportFileName' => 'Almacenes', + 'showFullscreen' => false, + 'showPaginationSwitch' => false, + 'showRefresh' => false, + 'pagination' => false, + ]; + } + + /** + * Retorna la ruta de la vista Blade. + * + * @return string + */ + protected function viewPath(): string + { + // La vista que ya tienes creada para ImagesIndex + return 'vuexy-website-admin::livewire.images.index'; + } + + /** + * Métodos que necesites sobreescribir o extender. + */ + public function mount(): void + { + parent::mount(); + } +} diff --git a/Livewire/LegalNotices/LegalNoticeOffCanvasForm.php b/Livewire/LegalNotices/LegalNoticeOffCanvasForm.php new file mode 100644 index 0000000..c727eca --- /dev/null +++ b/Livewire/LegalNotices/LegalNoticeOffCanvasForm.php @@ -0,0 +1,217 @@ + 'loadFormModel', + 'confirmDeletionWarehouse' => 'loadFormModelForDeletion', + ]; + + /** + * Definición de tipos de datos que se deben castear. + * + * @var array + */ + protected $casts = [ + 'status' => 'boolean', + ]; + + /** + * Define el modelo Eloquent asociado con el formulario. + * + * @return string + */ + protected function model(): string + { + return Warehouse::class; + } + + /** + * Define los campos del formulario. + * + * @return array + */ + protected function fields(): array + { + return (new Warehouse())->getFillable(); + } + + /** + * Valores por defecto para el formulario. + * + * @return array + */ + protected function defaults(): array + { + return [ + 'priority' => 0, + 'status' => true, + ]; + } + + /** + * Campo que se debe enfocar cuando se abra el formulario. + * + * @return string + */ + protected function focusOnOpen(): string + { + return 'code'; + } + + /** + * 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 [ + 'store_id' => ['required', 'integer', 'exists:stores,id'], + 'work_center_id' => ['nullable', 'integer', 'exists:store_work_centers,id'], + 'code' => ['required', 'string', 'max:16', Rule::unique('warehouses', 'code')->ignore($this->id)], + 'name' => ['required', 'string', 'max:96'], + 'description' => ['nullable', 'string', 'max:1024'], + 'manager_id' => ['nullable', 'integer', 'exists:users,id'], + 'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], + 'tel2' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], + 'priority' => ['nullable', 'numeric', 'between:0,99'], + 'status' => ['nullable', 'boolean'], + ]; + + case 'delete': + return [ + 'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación + ]; + + default: + return []; + } + } + + // ===================== VALIDACIONES ===================== + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + protected function attributes(): array + { + return [ + 'code' => 'código de almacén', + 'name' => 'nombre del almacén', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + protected function messages(): array + { + return [ + 'store_id.required' => 'El almacén debe estar asociado a un negocio.', + 'code.required' => 'El código del almacén es obligatorio.', + 'code.unique' => 'Este código ya está en uso por otro almacén.', + 'name.required' => 'El nombre del almacén es obligatorio.', + ]; + } + + /** + * Carga el formulario con datos del almacén y actualiza las opciones dinámicas. + * + * @param int $id + */ + public function loadFormModel($id): void + { + parent::loadFormModel($id); + + $this->work_center_options = DB::table('store_work_centers') + ->where('store_id', $this->store_id) + ->pluck('name', 'id') + ->toArray(); + } + + /** + * Carga el formulario para eliminar un almacén, 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]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.legal-notices.offcanvas-form'; + } +} diff --git a/Livewire/LegalNotices/LegalNoticesIndex.php b/Livewire/LegalNotices/LegalNoticesIndex.php new file mode 100644 index 0000000..3a72353 --- /dev/null +++ b/Livewire/LegalNotices/LegalNoticesIndex.php @@ -0,0 +1,109 @@ + 'save', + ]; + + public function mount() + { + $this->loadSettings(); + + // Seleccionar la primera sección por defecto + $this->currentSection = array_key_first($this->legalVars); + } + + function loadSettings() + { + $websiteTemplateService = app(WebsiteTemplateService::class); + + switch ($this->currentSection) { + case 'legal_terminos_y_condiciones': + $this->legalVars['legal_terminos_y_condiciones'] = $websiteTemplateService->getLegalVars('legal_terminos_y_condiciones'); + break; + + case 'legal_aviso_de_privacidad': + $this->legalVars['legal_aviso_de_privacidad'] = $websiteTemplateService->getLegalVars('legal_aviso_de_privacidad'); + break; + + case 'legal_politica_de_devoluciones': + $this->legalVars['legal_politica_de_devoluciones'] = $websiteTemplateService->getLegalVars('legal_politica_de_devoluciones'); + break; + + case 'legal_politica_de_envios': + $this->legalVars['legal_politica_de_envios'] = $websiteTemplateService->getLegalVars('legal_politica_de_envios'); + break; + + case 'legal_politica_de_cookies': + $this->legalVars['legal_politica_de_cookies'] = $websiteTemplateService->getLegalVars('legal_politica_de_cookies'); + break; + + case 'legal_autorizaciones_y_licencias': + $this->legalVars['legal_autorizaciones_y_licencias'] = $websiteTemplateService->getLegalVars('legal_autorizaciones_y_licencias'); + break; + + case 'legal_informacion_comercial': + $this->legalVars['legal_informacion_comercial'] = $websiteTemplateService->getLegalVars('legal_informacion_comercial'); + break; + + case 'legal_consentimiento_para_el_login_de_terceros': + $this->legalVars['legal_consentimiento_para_el_login_de_terceros'] = $websiteTemplateService->getLegalVars('legal_consentimiento_para_el_login_de_terceros'); + break; + + case 'legal_leyendas_de_responsabilidad': + $this->legalVars['legal_leyendas_de_responsabilidad'] = $websiteTemplateService->getLegalVars('legal_leyendas_de_responsabilidad'); + break; + + default: + $this->legalVars = $websiteTemplateService->getLegalVars(); + } + } + + public function rules() + { + $rules = []; + + if ($this->legalVars[$this->currentSection]['enabled']) { + $rules["legalVars.{$this->currentSection}.content"] = ['required', 'string', new NotEmptyHtml]; + } + + $rules["legalVars.{$this->currentSection}.enabled"] = 'boolean'; + + return $rules; + } + + public function save() + { + $this->validate($this->rules()); + + $SettingsService = app(SettingsService::class); + + $SettingsService->set($this->currentSection . '_enabled', $this->legalVars[$this->currentSection]['enabled'], null, 'vuexy-website-admin'); + $SettingsService->set($this->currentSection . '_content', $this->legalVars[$this->currentSection]['content'], null, 'vuexy-website-admin'); + + $this->dispatch( + 'notification', + target: $this->targetNotify, + type: 'success', + message: 'Se han guardado los cambios en las configuraciones.' + ); + } + + public function render() + { + return view('vuexy-website-admin::livewire.legal-notices.index'); + } +} diff --git a/Livewire/LegalNotices/LegalSettings.php b/Livewire/LegalNotices/LegalSettings.php new file mode 100644 index 0000000..0f82f39 --- /dev/null +++ b/Livewire/LegalNotices/LegalSettings.php @@ -0,0 +1,108 @@ + 'save', + ]; + + public function mount() + { + $this->loadSettings(); + + // Seleccionar la primera sección por defecto + $this->currentSection = array_key_first($this->legalVars); + } + + function loadSettings() + { + $websiteTemplateService = app(WebsiteTemplateService::class); + + switch ($this->currentSection) { + case 'legal_terminos_y_condiciones': + $this->legalVars['legal_terminos_y_condiciones'] = $websiteTemplateService->getLegalVars('legal_terminos_y_condiciones'); + break; + + case 'legal_aviso_de_privacidad': + $this->legalVars['legal_aviso_de_privacidad'] = $websiteTemplateService->getLegalVars('legal_aviso_de_privacidad'); + break; + + case 'legal_politica_de_devoluciones': + $this->legalVars['legal_politica_de_devoluciones'] = $websiteTemplateService->getLegalVars('legal_politica_de_devoluciones'); + break; + + case 'legal_politica_de_envios': + $this->legalVars['legal_politica_de_envios'] = $websiteTemplateService->getLegalVars('legal_politica_de_envios'); + break; + + case 'legal_politica_de_cookies': + $this->legalVars['legal_politica_de_cookies'] = $websiteTemplateService->getLegalVars('legal_politica_de_cookies'); + break; + + case 'legal_autorizaciones_y_licencias': + $this->legalVars['legal_autorizaciones_y_licencias'] = $websiteTemplateService->getLegalVars('legal_autorizaciones_y_licencias'); + break; + + case 'legal_informacion_comercial': + $this->legalVars['legal_informacion_comercial'] = $websiteTemplateService->getLegalVars('legal_informacion_comercial'); + break; + + case 'legal_consentimiento_para_el_login_de_terceros': + $this->legalVars['legal_consentimiento_para_el_login_de_terceros'] = $websiteTemplateService->getLegalVars('legal_consentimiento_para_el_login_de_terceros'); + break; + + case 'legal_leyendas_de_responsabilidad': + $this->legalVars['legal_leyendas_de_responsabilidad'] = $websiteTemplateService->getLegalVars('legal_leyendas_de_responsabilidad'); + break; + + default: + $this->legalVars = $websiteTemplateService->getLegalVars(); + } + } + + public function rules() + { + $rules = []; + + if ($this->legalVars[$this->currentSection]['enabled']) { + $rules["legalVars.{$this->currentSection}.content"] = ['required', 'string', new NotEmptyHtml]; + } + + $rules["legalVars.{$this->currentSection}.enabled"] = 'boolean'; + + return $rules; + } + + public function save() + { + $this->validate($this->rules()); + + $websiteSettingsService = app(WebsiteSettingsService::class); + $websiteSettingsService->updateSetting($this->currentSection . '_enabled', $this->legalVars[$this->currentSection]['enabled']); + $websiteSettingsService->updateSetting($this->currentSection . '_content', $this->legalVars[$this->currentSection]['content']); + + $this->dispatch( + 'notification', + target: $this->targetNotify, + type: 'success', + message: 'Se han guardado los cambios en las configuraciones.' + ); + } + + public function render() + { + return view('admin::livewire.website-settings.legal-settings'); + } +} diff --git a/Livewire/SitemapManager/SitemapManagerIndex.php b/Livewire/SitemapManager/SitemapManagerIndex.php new file mode 100644 index 0000000..836d8e8 --- /dev/null +++ b/Livewire/SitemapManager/SitemapManagerIndex.php @@ -0,0 +1,38 @@ +urls = SitemapUrl::all(); + } + + public function addUrl() + { + SitemapUrl::create([ + 'url' => $this->newUrl, + 'changefreq' => $this->changefreq, + 'priority' => $this->priority, + 'lastmod' => now() + ]); + $this->reset(['newUrl', 'changefreq', 'priority']); + $this->mount(); + } + + public function deleteUrl($id) + { + SitemapUrl::find($id)->delete(); + $this->mount(); + } + + public function render() + { + return view('vuexy-website-admin::livewire.sitemap-manager.index', ['urls' => $this->urls]); + }} diff --git a/Livewire/SitemapManager/SitemapUrlOffcanvasForm.php b/Livewire/SitemapManager/SitemapUrlOffcanvasForm.php new file mode 100644 index 0000000..8959574 --- /dev/null +++ b/Livewire/SitemapManager/SitemapUrlOffcanvasForm.php @@ -0,0 +1,217 @@ + 'loadFormModel', + 'confirmDeletionWarehouse' => 'loadFormModelForDeletion', + ]; + + /** + * Definición de tipos de datos que se deben castear. + * + * @var array + */ + protected $casts = [ + 'status' => 'boolean', + ]; + + /** + * Define el modelo Eloquent asociado con el formulario. + * + * @return string + */ + protected function model(): string + { + return Warehouse::class; + } + + /** + * Define los campos del formulario. + * + * @return array + */ + protected function fields(): array + { + return (new Warehouse())->getFillable(); + } + + /** + * Valores por defecto para el formulario. + * + * @return array + */ + protected function defaults(): array + { + return [ + 'priority' => 0, + 'status' => true, + ]; + } + + /** + * Campo que se debe enfocar cuando se abra el formulario. + * + * @return string + */ + protected function focusOnOpen(): string + { + return 'code'; + } + + /** + * 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 [ + 'store_id' => ['required', 'integer', 'exists:stores,id'], + 'work_center_id' => ['nullable', 'integer', 'exists:store_work_centers,id'], + 'code' => ['required', 'string', 'max:16', Rule::unique('warehouses', 'code')->ignore($this->id)], + 'name' => ['required', 'string', 'max:96'], + 'description' => ['nullable', 'string', 'max:1024'], + 'manager_id' => ['nullable', 'integer', 'exists:users,id'], + 'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], + 'tel2' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], + 'priority' => ['nullable', 'numeric', 'between:0,99'], + 'status' => ['nullable', 'boolean'], + ]; + + case 'delete': + return [ + 'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación + ]; + + default: + return []; + } + } + + // ===================== VALIDACIONES ===================== + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + protected function attributes(): array + { + return [ + 'code' => 'código de almacén', + 'name' => 'nombre del almacén', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + protected function messages(): array + { + return [ + 'store_id.required' => 'El almacén debe estar asociado a un negocio.', + 'code.required' => 'El código del almacén es obligatorio.', + 'code.unique' => 'Este código ya está en uso por otro almacén.', + 'name.required' => 'El nombre del almacén es obligatorio.', + ]; + } + + /** + * Carga el formulario con datos del almacén y actualiza las opciones dinámicas. + * + * @param int $id + */ + public function loadFormModel($id): void + { + parent::loadFormModel($id); + + $this->work_center_options = DB::table('store_work_centers') + ->where('store_id', $this->store_id) + ->pluck('name', 'id') + ->toArray(); + } + + /** + * Carga el formulario para eliminar un almacén, 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]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.sitemap-manager.offcanvas-form'; + } +} diff --git a/Livewire/VuexyWebsiteAdmin/ChatSettings.php b/Livewire/VuexyWebsiteAdmin/ChatSettings.php new file mode 100644 index 0000000..dc756ed --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/ChatSettings.php @@ -0,0 +1,67 @@ +resetForm(); + } + + public function save() + { + if ($this->chat_provider == 'whatsapp') { + $this->validate([ + 'chat_whatsapp_number' => 'required|string|max:20', + 'chat_whatsapp_message' => 'required|string|max:255', + ]); + } + + // Guardar título del sitio en configuraciones + $SettingsService = app(SettingsService::class); + + $SettingsService->set('chat.provider', $this->chat_provider, null, 'vuexy-website-admin'); + $SettingsService->set('chat.whatsapp_number', $this->chat_whatsapp_number, null, 'vuexy-website-admin'); + $SettingsService->set('chat.whatsapp_message', $this->chat_whatsapp_message, null, 'vuexy-website-admin'); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars('chat'); + + $this->chat_provider = $settings['provider']; + $this->chat_whatsapp_number = $settings['whatsapp_number']; + $this->chat_whatsapp_message = $settings['whatsapp_message']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.chat-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/ContactFormSettings.php b/Livewire/VuexyWebsiteAdmin/ContactFormSettings.php new file mode 100644 index 0000000..4ac685b --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/ContactFormSettings.php @@ -0,0 +1,70 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'to_email' => 'required|email', + 'to_email_cc' => 'nullable|email', + 'subject' => 'required|string', + 'submit_message' => 'required|string' + ]); + + // Guardar título del sitio en configuraciones + $SettingsService = app(SettingsService::class); + + $SettingsService->set('contact.form.to_email', $this->to_email, null, 'vuexy-website-admin'); + $SettingsService->set('contact.form.to_email_cc', $this->to_email_cc, null, 'vuexy-website-admin'); + $SettingsService->set('contact.form.subject', $this->subject, null, 'vuexy-website-admin'); + $SettingsService->set('contact.form.submit_message', $this->submit_message, null, 'vuexy-website-admin'); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars('contact'); + + $this->to_email = $settings['form']['to_email']; + $this->to_email_cc = $settings['form']['to_email_cc']; + $this->subject = $settings['form']['subject']; + $this->submit_message = $settings['form']['submit_message']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.contact-form-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/ContactInfoSettings.php b/Livewire/VuexyWebsiteAdmin/ContactInfoSettings.php new file mode 100644 index 0000000..671c62c --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/ContactInfoSettings.php @@ -0,0 +1,78 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'phone_number' => 'nullable|string', + 'phone_number_ext' => 'nullable|string', + 'phone_number_2' => 'nullable|string', + 'phone_number_2_ext' => 'nullable|string', + 'email' => 'nullable|email', + 'horario' => 'nullable|string', + ]); + + // Guardar título del sitio en configuraciones + $SettingsService = app(SettingsService::class); + + $SettingsService->set('contact.phone_number', $this->phone_number, null, 'vuexy-website-admin'); + $SettingsService->set('contact.phone_number_ext', $this->phone_number_ext, null, 'vuexy-website-admin'); + $SettingsService->set('contact.phone_number_2', $this->phone_number_2, null, 'vuexy-website-admin'); + $SettingsService->set('contact.phone_number_2_ext', $this->phone_number_2_ext, null, 'vuexy-website-admin'); + $SettingsService->set('contact.email', $this->email, null, 'vuexy-website-admin'); + $SettingsService->set('contact.horario', $this->horario, null, 'vuexy-website-admin'); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars('contact'); + + $this->phone_number = $settings['phone_number']; + $this->phone_number_ext = $settings['phone_number_ext']; + $this->phone_number_2 = $settings['phone_number_2']; + $this->phone_number_2_ext = $settings['phone_number_2_ext']; + $this->email = $settings['email']; + $this->horario = $settings['horario']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.contact-info-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/GoogleAnalyticsSettings.php b/Livewire/VuexyWebsiteAdmin/GoogleAnalyticsSettings.php new file mode 100644 index 0000000..78f9d8a --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/GoogleAnalyticsSettings.php @@ -0,0 +1,63 @@ +resetForm(); + } + + public function save() + { + if ($this->google_analytics_enabled) { + $this->validate([ + 'google_analytics_id' => 'required|string|min:12|max:30', + ]); + } + + // Guardar título del sitio en configuraciones + $SettingsService = app(SettingsService::class); + + $SettingsService->set('google.analytics_enabled', $this->google_analytics_enabled, null, 'vuexy-website-admin'); + $SettingsService->set('google.analytics_id', $this->google_analytics_id, null, 'vuexy-website-admin'); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars('google'); + + $this->google_analytics_enabled = $settings['analytics']['enabled']; + $this->google_analytics_id = $settings['analytics']['id']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.analytics-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/LocationSettings.php b/Livewire/VuexyWebsiteAdmin/LocationSettings.php new file mode 100644 index 0000000..1134b63 --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/LocationSettings.php @@ -0,0 +1,69 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'direccion' => ['nullable', 'string', 'max:255'], + 'location_lat' => ['nullable', 'numeric'], + 'location_lng' => ['nullable', 'numeric'], + ]); + + // Guardar título del sitio en configuraciones + $SettingsService = app(SettingsService::class); + + $location_lat = $this->location_lat? (float) $this->location_lat: null; + $location_lng = $this->location_lng? (float) $this->location_lng: null; + + $SettingsService->set('contact.direccion', $this->direccion, null, 'vuexy-website-admin'); + $SettingsService->set('contact.location.lat', $location_lat, null, 'vuexy-website-admin'); + $SettingsService->set('contact.location.lng', $location_lng, null, 'vuexy-website-admin'); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars('contact'); + + $this->direccion = $settings['direccion']; + $this->location_lat = $settings['location']['lat']; + $this->location_lng = $settings['location']['lng']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.location-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/LogoOnDarkBgSettings.php b/Livewire/VuexyWebsiteAdmin/LogoOnDarkBgSettings.php new file mode 100644 index 0000000..3ef70ea --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/LogoOnDarkBgSettings.php @@ -0,0 +1,61 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'upload_image_logo_dark' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480', + ]); + + // Procesar favicon si se ha cargado una imagen + app(WebsiteSettingsService::class)->processAndSaveImageLogo($this->upload_image_logo_dark, 'dark'); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars(); + + $this->upload_image_logo_dark = null; + $this->website_image_logo_dark = $settings['image_logo']['large_dark']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.logo-on-dark-bg-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/LogoOnLightBgSettings.php b/Livewire/VuexyWebsiteAdmin/LogoOnLightBgSettings.php new file mode 100644 index 0000000..2b47e4b --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/LogoOnLightBgSettings.php @@ -0,0 +1,61 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'upload_image_logo' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480', + ]); + + // Procesar favicon si se ha cargado una imagen + app(WebsiteSettingsService::class)->processAndSaveImageLogo($this->upload_image_logo); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars(); + + $this->upload_image_logo = null; + $this->website_image_logo = $settings['image_logo']['large']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.logo-on-light-bg-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/SocialMediaSettings.php b/Livewire/VuexyWebsiteAdmin/SocialMediaSettings.php new file mode 100644 index 0000000..12cbd29 --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/SocialMediaSettings.php @@ -0,0 +1,98 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'social_whatsapp' => 'string|max:20', + 'social_whatsapp_message' => 'string|max:255', + 'social_facebook' => 'url', + 'social_instagram' => 'url', + 'social_linkedin' => 'url', + 'social_tiktok' => 'url', + 'social_x_twitter' => 'url', + 'social_google' => 'url', + 'social_pinterest' => 'url', + 'social_youtube' => 'url', + 'social_vimeo' => 'url', + ]); + + // Guardar título del sitio en configuraciones + $SettingsService = app(SettingsService::class); + + $SettingsService->set('social.whatsapp', $this->social_whatsapp, null, 'vuexy-website-admin'); + $SettingsService->set('social.whatsapp_message', $this->social_whatsapp_message, null, 'vuexy-website-admin'); + $SettingsService->set('social.facebook', $this->social_facebook, null, 'vuexy-website-admin'); + $SettingsService->set('social.instagram', $this->social_instagram, null, 'vuexy-website-admin'); + $SettingsService->set('social.linkedin', $this->social_linkedin, null, 'vuexy-website-admin'); + $SettingsService->set('social.tiktok', $this->social_tiktok, null, 'vuexy-website-admin'); + $SettingsService->set('social.x_twitter', $this->social_x_twitter, null, 'vuexy-website-admin'); + $SettingsService->set('social.google', $this->social_google, null, 'vuexy-website-admin'); + $SettingsService->set('social.pinterest', $this->social_pinterest, null, 'vuexy-website-admin'); + $SettingsService->set('social.youtube', $this->social_youtube, null, 'vuexy-website-admin'); + $SettingsService->set('social.vimeo', $this->social_vimeo, null, 'vuexy-website-admin'); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getSocialVars(); + + $this->social_whatsapp = $settings['whatsapp']; + $this->social_whatsapp_message = $settings['whatsapp_message']; + $this->social_facebook = $settings['facebook']; + $this->social_instagram = $settings['instagram']; + $this->social_linkedin = $settings['linkedin']; + $this->social_tiktok = $settings['tiktok']; + $this->social_x_twitter = $settings['x_twitter']; + $this->social_google = $settings['google']; + $this->social_pinterest = $settings['pinterest']; + $this->social_youtube = $settings['youtube']; + $this->social_vimeo = $settings['vimeo']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.social-media-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/WebsiteDescriptionSettings.php b/Livewire/VuexyWebsiteAdmin/WebsiteDescriptionSettings.php new file mode 100644 index 0000000..2603bab --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/WebsiteDescriptionSettings.php @@ -0,0 +1,62 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:255', + ]); + + // Guardar título del sitio en configuraciones + $SettingsService = app(SettingsService::class); + + $SettingsService->set('website.title', $this->title, null, 'vuexy-website-admin'); + $SettingsService->set('website.description', $this->description, null, 'vuexy-website-admin'); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars(); + + $this->title = $settings['title']; + $this->description = $settings['description']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.website-description-settings'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/WebsiteFaviconSettings.php b/Livewire/VuexyWebsiteAdmin/WebsiteFaviconSettings.php new file mode 100644 index 0000000..2666e24 --- /dev/null +++ b/Livewire/VuexyWebsiteAdmin/WebsiteFaviconSettings.php @@ -0,0 +1,72 @@ +resetForm(); + } + + public function save() + { + $this->validate([ + 'upload_image_favicon' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480', + ]); + + // Procesar favicon + app(WebsiteSettingsService::class)->processAndSaveFavicon($this->upload_image_favicon); + + // Limpiar cache de plantilla + app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); + + // 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(WebsiteTemplateService::class)->getWebsiteVars(); + + $this->upload_image_favicon = null; + $this->website_favicon_16x16 = $settings['favicon']['16x16']; + $this->website_favicon_76x76 = $settings['favicon']['76x76']; + $this->website_favicon_120x120 = $settings['favicon']['120x120']; + $this->website_favicon_152x152 = $settings['favicon']['152x152']; + $this->website_favicon_180x180 = $settings['favicon']['180x180']; + $this->website_favicon_192x192 = $settings['favicon']['192x192']; + } + + public function render() + { + return view('vuexy-website-admin::livewire.vuexy.website-favicon-settings'); + } +} diff --git a/Models/Faq.php b/Models/Faq.php new file mode 100644 index 0000000..90af6ab --- /dev/null +++ b/Models/Faq.php @@ -0,0 +1,33 @@ + 'integer', + 'is_active' => 'boolean', + ]; + + /** + * Categoría a la que pertenece esta FAQ. + */ + public function category(): BelongsTo + { + return $this->belongsTo(FaqCategory::class, 'category_id'); + } +} diff --git a/Models/FaqCategory.php b/Models/FaqCategory.php new file mode 100644 index 0000000..c1feb8f --- /dev/null +++ b/Models/FaqCategory.php @@ -0,0 +1,32 @@ + 'integer', + 'is_active' => 'boolean', + ]; + + /** + * FAQs asociadas a esta categoría. + */ + public function faqs(): HasMany + { + return $this->hasMany(Faq::class, 'category_id'); + } +} diff --git a/Models/SitemapConfiguration.php b/Models/SitemapConfiguration.php new file mode 100644 index 0000000..e69de29 diff --git a/Models/SitemapUrl.php b/Models/SitemapUrl.php new file mode 100644 index 0000000..dffb3b0 --- /dev/null +++ b/Models/SitemapUrl.php @@ -0,0 +1,19 @@ +loadMigrationsFrom(__DIR__ . '/../database/migrations'); + // Registrar comandos de consola + if ($this->app->runningInConsole()) { + $this->commands([ + SitemapGenerate::class, + ]); + } + // Registrar Livewire Components $components = [ - //'user-count' => UserCount::class, + // ajustes generales + 'vuexy-website-admin::website-description-settings' => WebsiteDescriptionSettings::class, + 'vuexy-website-admin::website-favicon-settings' => WebsiteFaviconSettings::class, + 'vuexy-website-admin::logo-on-light-bg-settings' => LogoOnLightBgSettings::class, + 'vuexy-website-admin::logo-on-dark-bg-settings' => LogoOnDarkBgSettings::class, + + // Avisos legales + 'vuexy-website-admin::legal-notices-index' => LegalNoticesIndex::class, + 'vuexy-website-admin::legal-notice-offcanvas-form' => LegalNoticeOffCanvasForm::class, + + // Preguntas frecuentes + 'vuexy-website-admin::faq-index' => FaqIndex::class, + 'vuexy-website-admin::faq-offcanvas-form' => FaqOffCanvasForm::class, + + // Redes sociales + 'vuexy-website-admin::social-media-settings' => SocialMediaSettings::class, + + // Chat + 'vuexy-website-admin::chat-settings' => ChatSettings::class, + + // Galería de imágenes + 'vuexy-website-admin::images-index' => ImagesIndex::class, + + // Google Analytics + 'vuexy-website-admin::google-analytics-settings' => GoogleAnalyticsSettings::class, + + // Información de contacto + 'vuexy-website-admin::contact-info-settings' => ContactInfoSettings::class, + 'vuexy-website-admin::location-settings' => LocationSettings::class, + + // Formulario de contacto + 'vuexy-website-admin::contact-form-settings' => ContactFormSettings::class, + + // Mapa del sitio + 'vuexy-website-admin::sitemap-manager-index' => SitemapManagerIndex::class, + 'vuexy-website-admin::sitemap-manager-offcanvas-form' => SitemapUrlOffcanvasForm::class, ]; foreach ($components as $alias => $component) { Livewire::component($alias, $component); } - - // Registrar auditoría en usuarios - //User::observe(AuditableObserver::class); } } diff --git a/Services/WebsiteSettingsService.php b/Services/WebsiteSettingsService.php new file mode 100644 index 0000000..bbe1c5e --- /dev/null +++ b/Services/WebsiteSettingsService.php @@ -0,0 +1,292 @@ +> Tamaños predefinidos para favicons */ + private $faviconsSizes = [ + '180x180' => [180, 180], + '192x192' => [192, 192], + '152x152' => [152, 152], + '120x120' => [120, 120], + '76x76' => [76, 76], + '16x16' => [16, 16], + ]; + + /** @var int Área máxima en píxeles para la primera versión del logo */ + private $imageLogoMaxPixels1 = 22500; + + /** @var int Área máxima en píxeles para la segunda versión del logo */ + private $imageLogoMaxPixels2 = 75625; + + /** @var int Área máxima en píxeles para la tercera versión del logo */ + private $imageLogoMaxPixels3 = 262144; + + /** @var int Área máxima en píxeles para la versión base64 del logo */ + private $imageLogoMaxPixels4 = 230400; + + /** @var int Tiempo de vida en caché en minutos */ + protected $cacheTTL = 60 * 24 * 30; + + /** + * Constructor del servicio + * + * Inicializa el driver de procesamiento de imágenes desde la configuración + */ + public function __construct() + { + $this->driver = config('image.driver', 'gd'); + } + + /** + * Procesa y guarda un nuevo favicon + * + * Genera múltiples versiones del favicon en diferentes tamaños predefinidos, + * elimina las versiones anteriores y actualiza la configuración. + * + * @param \Illuminate\Http\UploadedFile $image Archivo de imagen subido + * @return void + */ + public function processAndSaveFavicon($image): void + { + Storage::makeDirectory($this->imageDisk . '/' . $this->favicon_basePath); + + // Eliminar favicons antiguos + $this->deleteOldFavicons(); + + // Guardar imagen original + $imageManager = new ImageManager($this->driver); + + $imageName = uniqid('website_favicon_'); + + $image = $imageManager->read($image->getRealPath()); + + foreach ($this->faviconsSizes as $size => [$width, $height]) { + $resizedPath = $this->favicon_basePath . $imageName . "_{$size}.png"; + + $image->cover($width, $height); + + Storage::disk($this->imageDisk)->put($resizedPath, $image->toPng(indexed: true)); + } + + // Actualizar configuración utilizando SettingService + $SettingsService = app(SettingsService::class); + $SettingsService->set('website.favicon_ns', $this->favicon_basePath . $imageName, null, 'vuexy-website-admin'); + } + + /** + * Elimina los favicons antiguos del almacenamiento + * + * @return void + */ + protected function deleteOldFavicons(): void + { + // Obtener el favicon actual desde la base de datos + $currentFavicon = Setting::where('key', 'website.favicon_ns')->value('value'); + + if ($currentFavicon) { + $filePaths = [ + $this->imageDisk . '/' . $currentFavicon, + $this->imageDisk . '/' . $currentFavicon . '_16x16.png', + $this->imageDisk . '/' . $currentFavicon . '_76x76.png', + $this->imageDisk . '/' . $currentFavicon . '_120x120.png', + $this->imageDisk . '/' . $currentFavicon . '_152x152.png', + $this->imageDisk . '/' . $currentFavicon . '_180x180.png', + $this->imageDisk . '/' . $currentFavicon . '_192x192.png', + ]; + + foreach ($filePaths as $filePath) { + if (Storage::exists($filePath)) { + Storage::delete($filePath); + } + } + } + } + + /** + * Procesa y guarda un nuevo logo + * + * Genera múltiples versiones del logo con diferentes tamaños máximos, + * incluyendo una versión en base64, y actualiza la configuración. + * + * @param \Illuminate\Http\UploadedFile $image Archivo de imagen subido + * @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal) + * @return void + */ + public function processAndSaveImageLogo($image, string $type = ''): void + { + // Crear directorio si no existe + Storage::makeDirectory($this->imageDisk . '/' . $this->image_logo_basePath); + + // Eliminar imágenes antiguas + $this->deleteOldImageWebapp($type); + + // Leer imagen original + $imageManager = new ImageManager($this->driver); + $image = $imageManager->read($image->getRealPath()); + + // Generar tres versiones con diferentes áreas máximas + $this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels1, 'small'); // Versión 1 + $this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels2, 'medium'); // Versión 2 + $this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels3); // Versión 3 + $this->generateAndSaveImageAsBase64($image, $type, $this->imageLogoMaxPixels4); // Versión 3 + } + + /** + * Genera y guarda una versión del logo + * + * @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a procesar + * @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal) + * @param int $maxPixels Área máxima en píxeles + * @param string $suffix Sufijo para el nombre del archivo + * @return void + */ + private function generateAndSaveImage($image, string $type, int $maxPixels, string $suffix = ''): void + { + $imageClone = clone $image; + + // Escalar imagen conservando aspecto + $this->resizeImageToMaxPixels($imageClone, $maxPixels); + + $imageName = 'website_image_logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : ''); + $keyValue = 'website.image.logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : ''); + + // Generar nombre y ruta + $imageNameUid = uniqid($imageName . '_', ".png"); + $resizedPath = $this->image_logo_basePath . $imageNameUid; + + // Guardar imagen en PNG + Storage::disk($this->imageDisk)->put($resizedPath, $imageClone->toPng(indexed: true)); + + // Actualizar configuración + $SettingsService = app(SettingsService::class); + $SettingsService->set($keyValue, $resizedPath, null, 'vuexy-website-admin'); + } + + /** + * Redimensiona una imagen manteniendo su proporción + * + * @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a redimensionar + * @param int $maxPixels Área máxima en píxeles + * @return \Intervention\Image\Interfaces\ImageInterface + */ + private function resizeImageToMaxPixels($image, int $maxPixels) + { + // Obtener dimensiones originales de la imagen + $originalWidth = $image->width(); // Método para obtener el ancho + $originalHeight = $image->height(); // Método para obtener el alto + + // Calcular el aspecto + $aspectRatio = $originalWidth / $originalHeight; + + // Calcular dimensiones redimensionadas conservando aspecto + if ($aspectRatio > 1) { // Ancho es dominante + $newWidth = sqrt($maxPixels * $aspectRatio); + $newHeight = $newWidth / $aspectRatio; + + } else { // Alto es dominante + $newHeight = sqrt($maxPixels / $aspectRatio); + $newWidth = $newHeight * $aspectRatio; + } + + // Redimensionar la imagen + $image->resize( + round($newWidth), // Redondear para evitar problemas con números decimales + round($newHeight), + function ($constraint) { + $constraint->aspectRatio(); + $constraint->upsize(); + } + ); + + return $image; + } + + /** + * Genera y guarda una versión del logo en formato base64 + * + * @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a procesar + * @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal) + * @param int $maxPixels Área máxima en píxeles + * @return void + */ + private function generateAndSaveImageAsBase64($image, string $type, int $maxPixels): void + { + $imageClone = clone $image; + + // Redimensionar imagen conservando el aspecto + $this->resizeImageToMaxPixels($imageClone, $maxPixels); + + // Convertir a Base64 + $base64Image = (string) $imageClone->toJpg(40)->toDataUri(); + + // Guardar como configuración + $SettingsService = app(SettingsService::class); + $SettingsService->set("website.image.logo_base64" . ($type === 'dark' ? '_dark' : ''), $base64Image, null, 'vuexy-website-admin'); + } + + /** + * Elimina las imágenes antiguas del logo + * + * @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal) + * @return void + */ + protected function deleteOldImageWebapp(string $type = ''): void + { + // Determinar prefijo según el tipo (normal o dark) + $suffix = $type === 'dark' ? '_dark' : ''; + + // Claves relacionadas con las imágenes que queremos limpiar + $imageKeys = [ + "website.image_logo{$suffix}", + "website.image_logo_small{$suffix}", + "website.image_logo_medium{$suffix}", + ]; + + // Recuperar las imágenes actuales en una sola consulta + $settings = Setting::whereIn('key', $imageKeys)->pluck('value', 'key'); + + foreach ($imageKeys as $key) { + // Obtener la imagen correspondiente + $currentImage = $settings[$key] ?? null; + + if ($currentImage) { + // Construir la ruta del archivo y eliminarlo si existe + $filePath = $this->imageDisk . '/' . $currentImage; + + if (Storage::exists($filePath)) { + Storage::delete($filePath); + } + + // Eliminar la configuración de la base de datos + Setting::where('key', $key)->delete(); + } + } + } +} diff --git a/Services/WebsiteTemplateService.php b/Services/WebsiteTemplateService.php new file mode 100644 index 0000000..107b876 --- /dev/null +++ b/Services/WebsiteTemplateService.php @@ -0,0 +1,395 @@ +getDefaultWebsiteVars($setting); + } + + $webVars = Cache::remember('website_settings', $this->cacheTTL, function () { + $settings = Setting::withVirtualValue() + ->where(function ($query) { + $query->where('key', 'LIKE', 'website.%') + ->orWhere('key', 'LIKE', 'google.%') + ->orWhere('key', 'LIKE', 'chat.%'); + }) + ->pluck('value', 'key') + ->toArray(); + + return $this->buildWebsiteVars($settings); + }); + + return $setting ? ($webVars[$setting] ?? []) : $webVars; + + } catch (\Exception $e) { + // Manejo de excepciones: devolver valores predeterminados + return $this->getDefaultWebsiteVars($setting); + } + } + + /** + * Construye las variables del template del website. + * + * @param array $settings Array asociativo de configuraciones + * @return array Array con las variables del template + */ + private function buildWebsiteVars(array $settings): array + { + return [ + 'title' => $settings['website.title'] ?? config('_var.appTitle'), + 'author' => config('_var.author'), + 'description' => $settings['website.description'] ?? config('_var.appDescription'), + 'favicon' => $this->getFaviconPaths($settings), + 'app_name' => $settings['website.app_name'] ?? config('_var.appName'), + 'image_logo' => $this->getImageLogoPaths($settings), + //'template' => $this->getTemplateVars($settings), + 'google' => $this->getGoogleVars($settings), + 'chat' => $this->getChatVars($settings), + 'contact' => $this->getContactVars(), + 'social' => $this->getSocialVars(), + ]; + } + + /** + * Obtiene las variables del template del website por defecto. + * + * @param string $setting Clave de la configuración a obtener + * @return array Array con las variables del template + */ + private function getDefaultWebsiteVars(string $setting = ''): array + { + $defaultVars = [ + 'title' => config('_var.appTitle', 'Default Title'), + 'author' => config('_var.author', 'Default Author'), + 'description' => config('_var.appDescription', 'Default Description'), + 'favicon' => $this->getFaviconPaths([]), + 'image_logo' => $this->getImageLogoPaths([]), + //'template' => $this->getTemplateVars([]), + 'google' => $this->getGoogleVars([]), + 'chat' => $this->getChatVars([]), + 'contact' => [], + 'social' => [], + ]; + + return $setting ? ($defaultVars[$setting] ?? []) : $defaultVars; + } + + /** + * Genera las rutas para los diferentes tamaños de favicon. + * + * @param array $settings Array asociativo de configuraciones + * @return array Array con las rutas de los favicons en diferentes tamaños + */ + private function getFaviconPaths(array $settings): array + { + $defaultFavicon = config('koneko.appFavicon'); + $namespace = $settings['website.favicon_ns'] ?? null; + + return [ + 'namespace' => $namespace, + '16x16' => $namespace ? "{$namespace}_16x16.png" : $defaultFavicon, + '76x76' => $namespace ? "{$namespace}_76x76.png" : $defaultFavicon, + '120x120' => $namespace ? "{$namespace}_120x120.png" : $defaultFavicon, + '152x152' => $namespace ? "{$namespace}_152x152.png" : $defaultFavicon, + '180x180' => $namespace ? "{$namespace}_180x180.png" : $defaultFavicon, + '192x192' => $namespace ? "{$namespace}_192x192.png" : $defaultFavicon, + ]; + } + + /** + * Genera las rutas para los diferentes tamaños y versiones del logo. + * + * @param array $settings Array asociativo de configuraciones + * @return array Array con las rutas de los logos en diferentes tamaños y modos + */ + private function getImageLogoPaths(array $settings): array + { + $defaultLogo = config('koneko.appLogo'); + + return [ + 'small' => $this->getImagePath($settings, 'website.image.logo_small', $defaultLogo), + 'medium' => $this->getImagePath($settings, 'website.image.logo_medium', $defaultLogo), + 'large' => $this->getImagePath($settings, 'website.image.logo', $defaultLogo), + 'small_dark' => $this->getImagePath($settings, 'website.image.logo_small_dark', $defaultLogo), + 'medium_dark' => $this->getImagePath($settings, 'website.image.logo_medium_dark', $defaultLogo), + 'large_dark' => $this->getImagePath($settings, 'website.image.logo_dark', $defaultLogo), + ]; + } + + /** + * Obtiene la ruta de una imagen específica desde las configuraciones. + * + * @param array $settings Array asociativo de configuraciones + * @param string $key Clave de la configuración + * @param string $default Valor predeterminado si no se encuentra la configuración + * @return string Ruta de la imagen + */ + private function getImagePath(array $settings, string $key, string $default): string + { + return $settings[$key] ?? $default; + } + + /* + private function getTemplateVars(array $settings): array + { + return [ + 'style_switcher' => (bool)($settings['website.tpl_style_switcher'] ?? false), + 'footer_text' => $settings['website.tpl_footer_text'] ?? '', + ]; + } + */ + + /** + * Obtiene las variables de Google Analytics. + * + * @param array $settings Array asociativo de configuraciones + * @return array Array con las variables de Google Analytics + */ + private function getGoogleVars(array $settings): array + { + return [ + 'analytics' => [ + 'enabled' => (bool)($settings['google.analytics_enabled'] ?? false), + 'id' => $settings['google.analytics_id'] ?? '', + ] + ]; + } + + /** + * Obtiene las variables de chat. + * + * @param array $settings Array asociativo de configuraciones + * @return array Array con las variables de chat + */ + private function getChatVars(array $settings): array + { + return [ + 'provider' => $settings['chat.provider'] ?? '', + 'whatsapp_number' => $settings['chat.whatsapp_number'] ?? '', + 'whatsapp_message' => $settings['chat.whatsapp_message'] ?? '', + ]; + } + + /** + * Obtiene las variables de contacto. + * + * @return array Array con las variables de contacto + */ + public function getContactVars(): array + { + $settings = Setting::withVirtualValue() + ->where('key', 'LIKE', 'contact.%') + ->pluck('value', 'key') + ->toArray(); + + return [ + 'phone_number' => isset($settings['contact.phone_number']) + ? preg_replace('/\D/', '', $settings['contact.phone_number']) // Elimina todo lo que no sea un número + : '', + 'phone_number_text' => $settings['contact.phone_number'] ?? '', + 'phone_number_ext' => $settings['contact.phone_number_ext'] ?? '', + 'phone_number_2' => isset($settings['contact.phone_number_2']) + ? preg_replace('/\D/', '', $settings['contact.phone_number_2']) // Elimina todo lo que no sea un número + : '', + 'phone_number_2_text' => $settings['contact.phone_number_2'] ?? '', + 'phone_number_2_ext' => $settings['contact.phone_number_2_ext'] ?? '', + 'email' => $settings['contact.email'] ?? '', + 'direccion' => $settings['contact.direccion'] ?? '', + 'horario' => $settings['contact.horario'] ?? '', + 'location' => [ + 'lat' => $settings['contact.location.lat'] ?? '', + 'lng' => $settings['contact.location.lng'] ?? '', + ], + 'form' => [ + 'to_email' => $settings['contact.form.to_email'] ?? '', + 'to_email_cc' => $settings['contact.form.to_email_cc'] ?? '', + 'subject' => $settings['contact.form.subject'] ?? '', + 'submit_message' => $settings['contact.form.submit_message'] ?? '', + ], + ]; + } + + /** + * Obtiene las variables de redes sociales. + * + * @return array Array con las variables de redes sociales + */ + public function getSocialVars(): array + { + $social = Setting::withVirtualValue() + ->where('key', 'LIKE', 'social.%') + ->pluck('value', 'key') + ->toArray(); + + return [ + 'whatsapp' => $social['social.whatsapp'] ?? '', + 'whatsapp_message' => $social['social.whatsapp_message'] ?? '', + 'facebook' => $social['social.facebook'] ?? '', + 'instagram' => $social['social.instagram'] ?? '', + 'linkedin' => $social['social.linkedin'] ?? '', + 'tiktok' => $social['social.tiktok'] ?? '', + 'x_twitter' => $social['social.x_twitter'] ?? '', + 'google' => $social['social.google'] ?? '', + 'pinterest' => $social['social.pinterest'] ?? '', + 'youtube' => $social['social.youtube'] ?? '', + 'vimeo' => $social['social.vimeo'] ?? '', + ]; + } + + + /** + * Limpia el caché de las variables del website. + * + * @return void + */ + public static function clearWebsiteVarsCache() + { + Cache::forget("website_settings"); + } + + /** + * Obtiene las variables de legal notice. + * + * @param string $legalDocument Documento legal a obtener + * @return array Array con las variables de legal notice + */ + public function getLegalVars($legalDocument = false) + { + $legal = Setting::withVirtualValue() + ->where('key', 'LIKE', 'legal_notice.%') + ->pluck('value', 'key') + ->toArray(); + + $legalDocuments = [ + 'legal_notice.terminos_y_condiciones' => [ + 'title' => 'Términos y condiciones', + 'enabled' => (bool)($legal['legal_notice.terminos_y_condiciones_enabled'] ?? false), + 'content' => $legal['legal_notice.terminos_y_condiciones_content'] ?? '', + ], + 'legal_notice.aviso_de_privacidad' => [ + 'title' => 'Aviso de privacidad', + 'enabled' => (bool)($legal['legal_notice.aviso_de_privacidad_enabled'] ?? false), + 'content' => $legal['legal_notice.aviso_de_privacidad_content'] ?? '', + ], + 'legal_notice.politica_de_devoluciones' => [ + 'title' => 'Política de devoluciones y reembolsos', + 'enabled' => (bool)($legal['legal_notice.politica_de_devoluciones_enabled'] ?? false), + 'content' => $legal['legal_notice.politica_de_devoluciones_content'] ?? '', + ], + 'legal_notice.politica_de_envios' => [ + 'title' => 'Política de envíos', + 'enabled' => (bool)($legal['legal_notice.politica_de_envios_enabled'] ?? false), + 'content' => $legal['legal_notice.politica_de_envios_content'] ?? '', + ], + 'legal_notice.politica_de_cookies' => [ + 'title' => 'Política de cookies', + 'enabled' => (bool)($legal['legal_notice.politica_de_cookies_enabled'] ?? false), + 'content' => $legal['legal_notice.politica_de_cookies_content'] ?? '', + ], + 'legal_notice.autorizaciones_y_licencias' => [ + 'title' => 'Autorizaciones y licencias', + 'enabled' => (bool)($legal['legal_notice.autorizaciones_y_licencias_enabled'] ?? false), + 'content' => $legal['legal_notice.autorizaciones_y_licencias_content'] ?? '', + ], + 'legal_notice.informacion_comercial' => [ + 'title' => 'Información comercial', + 'enabled' => (bool)($legal['legal_notice.informacion_comercial_enabled'] ?? false), + 'content' => $legal['legal_notice.informacion_comercial_content'] ?? '', + ], + 'legal_notice.consentimiento_para_el_login_de_terceros' => [ + 'title' => 'Consentimiento para el login de terceros', + 'enabled' => (bool)($legal['legal_notice.consentimiento_para_el_login_de_terceros_enabled'] ?? false), + 'content' => $legal['legal_notice.consentimiento_para_el_login_de_terceros_content'] ?? '', + ], + 'legal_notice.leyendas_de_responsabilidad' => [ + 'title' => 'Leyendas de responsabilidad', + 'enabled' => (bool)($legal['legal_notice.leyendas_de_responsabilidad_enabled'] ?? false), + 'content' => $legal['legal_notice.leyendas_de_responsabilidad_content'] ?? '', + ], + ]; + + return $legalDocument + ? $legalDocuments[$legalDocument] + : $legalDocuments; + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +} diff --git a/composer.json b/composer.json index a61e098..0253bf3 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ }, "autoload": { "psr-4": { - "Koneko\\VuexyWebsiteAdmin\\": "" + "Koneko\\VuexyWebsiteAdmin\\": "./" } }, "extra": { diff --git a/database/migrations/2024_12_29_081812_create_faq_categories_table.php b/database/migrations/2024_12_29_081812_create_faq_categories_table.php new file mode 100644 index 0000000..c2877d8 --- /dev/null +++ b/database/migrations/2024_12_29_081812_create_faq_categories_table.php @@ -0,0 +1,34 @@ +smallIncrements('id'); + + $table->string('name')->unique(); + $table->string('icon')->nullable(); + $table->unsignedInteger('order')->default(0)->index(); + $table->boolean('is_active')->default(true)->index(); + + // Auditoria + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('faq_categories'); + } +}; diff --git a/database/migrations/2024_12_29_081815_create_faqs_table.php b/database/migrations/2024_12_29_081815_create_faqs_table.php new file mode 100644 index 0000000..22ebe1f --- /dev/null +++ b/database/migrations/2024_12_29_081815_create_faqs_table.php @@ -0,0 +1,38 @@ +id(); + + $table->unsignedSmallInteger('category_id')->nullable()->index(); + $table->string('question'); + $table->text('answer'); + $table->unsignedInteger('order')->default(0)->index(); + $table->boolean('is_active')->default(true)->index(); + + // Auditoria + $table->timestamps(); + + // Relaciones + $table->foreign('category_id')->references('id')->on('faq_categories')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('faqs'); + } +}; diff --git a/database/migrations/create_sitemap_configurations_table.php b/database/migrations/create_sitemap_configurations_table.php new file mode 100644 index 0000000..9c53366 --- /dev/null +++ b/database/migrations/create_sitemap_configurations_table.php @@ -0,0 +1,23 @@ +id(); + $table->string('route'); + $table->boolean('include')->default(true); + $table->decimal('priority', 2, 1)->default(0.5); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sitemap_configurations'); + } +}; \ No newline at end of file diff --git a/database/migrations/create_sitemap_urls_table.php b/database/migrations/create_sitemap_urls_table.php new file mode 100644 index 0000000..56a1541 --- /dev/null +++ b/database/migrations/create_sitemap_urls_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('url')->unique(); + $table->enum('changefreq', ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'])->default('weekly'); + $table->decimal('priority', 2, 1)->default(0.5); + $table->timestamp('lastmod')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sitemap_urls'); + } +}; diff --git a/resources/js/chat-settings-card.js b/resources/js/chat-settings-card.js new file mode 100644 index 0000000..5d5bb40 --- /dev/null +++ b/resources/js/chat-settings-card.js @@ -0,0 +1,73 @@ +import '@vuexy-admin/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; +import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; + +// Inicializar formularios de ajustes de chat +window.ChatSettingsForm = new FormCustomListener({ + formSelector: '#website-chat-settings-card', + buttonSelectors: ['.btn-save', '.btn-cancel'], + callbacks: [() => {}], + dispatchOnSubmit: 'save', + validationConfig: { + fields: { + chat_whatsapp_number: { + validators: { + callback: { + message: 'Por favor, introduce un número de teléfono válido para México.', + callback: function (input) { + // Obtener el proveedor directamente dentro de la validación + const provider = document.querySelector('#chat_provider')?.value; + + // Validar solo si el proveedor es WhatsApp + if (provider !== 'whatsapp') return true; + + const cleanValue = input.value.replace(/\D/g, ''); + const regex = /^[1-9]\d{9}$/; // Exactamente 10 dígitos + + return regex.test(cleanValue); + } + }, + notEmpty: { + message: 'El número de teléfono es obligatorio.', + enabled: () => { + // Obtener el proveedor directamente dentro de la validación + const provider = document.querySelector('#chat_provider')?.value; + + return provider === 'whatsapp'; // Habilita solo si es WhatsApp + } + } + } + }, + chat_whatsapp_message: { + validators: { + stringLength: { + max: 500, + message: 'El mensaje no puede exceder los 500 caracteres.' + }, + notEmpty: { + message: 'El mensaje es obligatorio.', + enabled: () => { + // Obtener el proveedor directamente dentro de la validación + const provider = document.querySelector('#chat_provider')?.value; + + return provider === 'whatsapp'; // Habilita solo si es WhatsApp + } + } + } + } + }, + plugins: { + trigger: new FormValidation.plugins.Trigger(), + bootstrap5: new FormValidation.plugins.Bootstrap5({ + eleValidClass: '', + rowSelector: '.fv-row' + }), + submitButton: new FormValidation.plugins.SubmitButton(), + autoFocus: new FormValidation.plugins.AutoFocus() + } + } +}); + +registerLivewireHookOnce('morphed', 'vuexy-website-admin::chat-settings', (component) => { + ChatSettingsForm.reloadValidation(); +}); diff --git a/resources/js/contact-form-settings-card.js b/resources/js/contact-form-settings-card.js new file mode 100644 index 0000000..f3b69f0 --- /dev/null +++ b/resources/js/contact-form-settings-card.js @@ -0,0 +1,86 @@ +import '@vuexy-admin/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; +import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; + +// Inicializar formularios de ajustes de Formularios de contacto +window.ContactFormSettingsForm = new FormCustomListener({ + formSelector: '#website-contact-form-settings-card', + buttonSelectors: ['.btn-save', '.btn-cancel'], + callbacks: [() => {}], + dispatchOnSubmit: 'save', + validationConfig: { + fields: { + // Validación para correo electrónico de recepción + to_email: { + validators: { + emailAddress: { + message: 'Por favor, introduce un correo electrónico válido.' + }, + notEmpty: { + message: 'El correo electrónico es obligatorio.' + } + } + }, + // Validación para correo electrónico con copia + to_email_cc: { + validators: { + emailAddress: { + message: 'Por favor, introduce un correo electrónico válido.' + }, + // Validación personalizada para comparar ambos correos electrónicos + callback: { + message: 'Los correos electrónicos deben ser diferentes.', + callback: function (input) { + const email = document.querySelector('[name="to_email"]').value.trim(); + const emailCC = input.value.trim(); + + // Si ambos correos son iguales, la validación falla + if (email === emailCC) { + return false; // Los correos son iguales, por lo que la validación falla + } + + return true; // Si son diferentes, la validación pasa + } + } + } + }, + // Validación para el asunto del formulario de contacto + subject: { + validators: { + stringLength: { + max: 60, + message: 'El título del correo no puede exceder los 60 caracteres.' + }, + notEmpty: { + message: 'El título del correo es obligatorio.' + } + } + }, + // Validación para el mensaje de envío + submit_message: { + validators: { + stringLength: { + max: 250, + message: 'El mensaje no puede exceder los 250 caracteres.' + }, + notEmpty: { + message: 'El mensaje de envío es obligatorio.' + } + } + } + }, + plugins: { + trigger: new FormValidation.plugins.Trigger(), + bootstrap5: new FormValidation.plugins.Bootstrap5({ + eleValidClass: '', + rowSelector: '.fv-row' + }), + submitButton: new FormValidation.plugins.SubmitButton(), + autoFocus: new FormValidation.plugins.AutoFocus() + } + } +}); + +registerLivewireHookOnce('morphed', 'vuexy-website-admin::contact-form-settings', (component) => { + ContactFormSettingsForm.reloadValidation(); +}); \ No newline at end of file diff --git a/resources/js/contact-info-settings-card.js b/resources/js/contact-info-settings-card.js new file mode 100644 index 0000000..9c8b8e2 --- /dev/null +++ b/resources/js/contact-info-settings-card.js @@ -0,0 +1,213 @@ +import '@vuexy-admin/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; +import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; + +// Inicializar formularios de ajustes de información de contacto +window.ContactInfoSettingsForm = new FormCustomListener({ + formSelector: '#website-contact-info-settings-card', + buttonSelectors: ['.btn-save', '.btn-cancel'], + callbacks: [() => {}], + dispatchOnSubmit: 'save', + validationConfig: { + fields: { + // Validación para número telefónico + phone_number: { + validators: { + callback: { + message: 'Por favor, introduce un número de teléfono válido para México.', + callback: function (input) { + // Si el campo está vacío, no hacemos validación + if (input.value.trim() === '') { + return true; // Permitir vacío + } + + // Si no está vacío, validamos el formato del número + const cleanValue = input.value.replace(/\D/g, ''); + const regex = /^[1-9]\d{9}$/; // Exactamente 10 dígitos + + return regex.test(cleanValue); // Valida solo si hay un número + } + } + } + }, + // Validación para extensión telefónica (opcional, pero solo si phone_number tiene valor) + phone_number_ext: { + validators: { + stringLength: { + max: 10, + message: 'La extensión no debe exceder los 10 caracteres.' + }, + callback: { + message: 'La extensión requiere de ingresar un número telefónico.', + callback: function (input) { + // Obtener el valor de 'phone_number' + const phoneNumber = document.querySelector('[name="phone_number"]')?.value.trim(); + + // Si el número telefónico tiene valor, entonces la extensión es obligatoria + if (phoneNumber !== '') { + // Si la extensión está vacía, la validación falla + return true; // Permitir vacío + } + + // Si no se ha ingresado un número telefónico, la extensión no debe tener valor + return input.value.trim() === ''; + } + } + } + }, + // Validación para número telefónico + phone_number_2: { + validators: { + callback: { + message: 'Por favor, introduce un número de teléfono válido para México.', + callback: function (input) { + // Si el campo está vacío, no hacemos validación + if (input.value.trim() === '') { + return true; // Permitir vacío + } + + // Si no está vacío, validamos el formato del número + const cleanValue = input.value.replace(/\D/g, ''); + const regex = /^[1-9]\d{9}$/; // Exactamente 10 dígitos + + return regex.test(cleanValue); // Valida solo si hay un número + } + } + } + }, + // Validación para extensión telefónica (opcional, pero solo si phone_number tiene valor) + phone_number_2_ext: { + validators: { + stringLength: { + max: 10, + message: 'La extensión no debe exceder los 10 caracteres.' + }, + callback: { + message: 'La extensión requiere de ingresar un número telefónico.', + callback: function (input) { + // Obtener el valor de 'phone_number' + const phoneNumber = document.querySelector('[name="phone_number_2"]')?.value.trim(); + + // Si el número telefónico tiene valor, entonces la extensión es obligatoria + if (phoneNumber !== '') { + // Si la extensión está vacía, la validación falla + return true; // Permitir vacío + } + + // Si no se ha ingresado un número telefónico, la extensión no debe tener valor + return input.value.trim() === ''; + } + } + } + }, + // Validación para correo electrónico de contacto (opcional) + email: { + validators: { + emailAddress: { + message: 'Por favor, introduce un correo electrónico válido.' + } + } + }, + // Validación para horario (No obligatorio, máximo 160 caracteres) + horario: { + validators: { + stringLength: { + max: 160, + message: 'El horario no puede exceder los 160 caracteres.' + } + } + }, + }, + plugins: { + trigger: new FormValidation.plugins.Trigger(), + bootstrap5: new FormValidation.plugins.Bootstrap5({ + eleValidClass: '', + rowSelector: '.fv-row' + }), + submitButton: new FormValidation.plugins.SubmitButton(), + autoFocus: new FormValidation.plugins.AutoFocus() + } + } +}); + +registerLivewireHookOnce('morphed', 'vuexy-website-admin::contact-info-settings', (component) => { + ContactInfoSettingsForm.reloadValidation(); +}); + +// Inicializar formularios de ajustes de ubicación +window.LocationSettingsForm = new FormCustomListener({ + formSelector: '#website-location-settings-card', + buttonSelectors: ['.btn-save', '.btn-cancel'], + callbacks: [() => {}], + dispatchOnSubmit: 'save', + validationConfig: { + fields: { + // Validación para dirección (No obligatorio, máximo 160 caracteres) + direccion: { + validators: { + stringLength: { + max: 160, + message: 'La dirección no puede exceder los 160 caracteres.' + } + } + }, + // Validación para latitud (No obligatorio, pero debe ser un número si se ingresa) + location_lat: { + validators: { + numeric: { + message: 'La latitud debe ser un número.' + }, + callback: { + message: 'La latitud es obligatoria si se ingresa longitud.', + callback: function (input) { + // Obtener el valor de longitud + const longitude = document.querySelector('[name="location_lng"]')?.value.trim(); + + // Si longitud tiene un valor, entonces latitud es obligatorio + if (longitude !== '') { + return input.value.trim() !== ''; // La latitud no puede estar vacía + } + + return true; // Si longitud está vacío, no se valida latitud + } + } + } + }, + // Validación para longitud (No obligatorio, pero debe ser un número si se ingresa) + location_lng: { + validators: { + numeric: { + message: 'La longitud debe ser un número.' + }, + callback: { + message: 'La longitud es obligatoria si se ingresa latitud.', + callback: function (input) { + // Obtener el valor de latitud + const latitude = document.querySelector('[name="location_lat"]')?.value.trim(); + + // Si latitud tiene un valor, entonces longitud es obligatorio + if (latitude !== '') { + return input.value.trim() !== ''; // La longitud no puede estar vacía + } + + return true; // Si latitud está vacío, no se valida longitud + } + } + } + } + }, + plugins: { + trigger: new FormValidation.plugins.Trigger(), + bootstrap5: new FormValidation.plugins.Bootstrap5({ + eleValidClass: '', + rowSelector: '.fv-row' + }), + submitButton: new FormValidation.plugins.SubmitButton(), + autoFocus: new FormValidation.plugins.AutoFocus() + } + } +}); + +registerLivewireHookOnce('morphed', 'vuexy-website-admin::location-settings', (component) => { + LocationSettingsForm.reloadValidation(); +}); diff --git a/resources/js/google-analytics-settings-card.js b/resources/js/google-analytics-settings-card.js new file mode 100644 index 0000000..80ef540 --- /dev/null +++ b/resources/js/google-analytics-settings-card.js @@ -0,0 +1,41 @@ +import '@vuexy-admin/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; +import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; + +// Inicializar formularios de ajustes de análisis de datos +window.AnalyticsSettingsForm = new FormCustomListener({ + formSelector: '#website-analytics-settings-card', + buttonSelectors: ['.btn-save', '.btn-cancel'], + callbacks: [() => {}], + dispatchOnSubmit: 'save', + validationConfig: { + fields: { + google_analytics_id: { + validators: { + callback: { + message: 'ID de medición de Google Analytics no tienen un formato válido.', + callback: function (input) { + if (document.getElementById('google_analytics_enabled').checked) { + return input.value.trim() !== ''; + } + return true; + } + } + } + } + }, + plugins: { + trigger: new FormValidation.plugins.Trigger(), + bootstrap5: new FormValidation.plugins.Bootstrap5({ + eleValidClass: '', + rowSelector: '.fv-row' + }), + submitButton: new FormValidation.plugins.SubmitButton(), + autoFocus: new FormValidation.plugins.AutoFocus() + } + } +}); + +registerLivewireHookOnce('morphed', 'vuexy-website-admin::analytics-index', (component) => { + AnalyticsSettingsForm.reloadValidation(); +}); \ No newline at end of file diff --git a/resources/js/website-settings-card.js b/resources/js/website-settings-card.js new file mode 100644 index 0000000..166336e --- /dev/null +++ b/resources/js/website-settings-card.js @@ -0,0 +1,133 @@ +import '@vuexy-admin/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; +import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; + +// Inicializar formularios de ajustes de social media +window.SocialSettingsForm = new FormCustomListener({ + formSelector: '#website-social-settings-card', + buttonSelectors: ['.btn-save', '.btn-cancel'], + callbacks: [() => {}], + dispatchOnSubmit: 'save', + validationConfig: { + fields: { + social_whatsapp: { + validators: { + callback: { + message: 'Por favor, introduce un número de teléfono válido para México.', + callback: function (input) { + // Si el campo está vacío, no hacemos validación + if (input.value.trim() === '') { + return true; // Permitir vacío + } + + // Si no está vacío, validamos el formato del número + const cleanValue = input.value.replace(/\D/g, ''); + const regex = /^[1-9]\d{9}$/; // Exactamente 10 dígitos + + return regex.test(cleanValue); // Valida solo si hay un número + } + } + } + }, + social_whatsapp_message: { + validators: { + stringLength: { + max: 500, + message: 'El mensaje no puede exceder los 500 caracteres.' + }, + callback: { + message: 'El mensaje es obligatorio.', + callback: function (input) { + // Obtener el valor de 'social_whatsapp' + const whatsappNumber = document.querySelector('#social_whatsapp').value.trim(); + + // Si 'social_whatsapp' tiene un valor, entonces el mensaje es obligatorio + if (whatsappNumber !== '') { + return input.value.trim() !== ''; // El mensaje no puede estar vacío + } + + return true; // Si 'social_whatsapp' está vacío, no validamos 'social_whatsapp_message' + } + } + } + }, + social_facebook: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + }, + social_instagram: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + }, + social_linkedin: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + }, + social_tiktok: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + }, + social_x_twitter: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + }, + social_google: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + }, + social_pinterest: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + }, + social_youtube: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + }, + social_vimeo: { + validators: { + uri: { + message: 'Por favor, introduce una URL válida.' + } + } + } + }, + plugins: { + trigger: new FormValidation.plugins.Trigger(), + bootstrap5: new FormValidation.plugins.Bootstrap5({ + eleValidClass: '', + rowSelector: '.fv-row', + messageContainer: '.fv-message' + }), + submitButton: new FormValidation.plugins.SubmitButton(), + autoFocus: new FormValidation.plugins.AutoFocus() + } + } +}); + +registerLivewireHookOnce('morphed', 'vuexy-website-admin::social-media-settings', (component) => { + SocialSettingsForm.reloadValidation(); +}); diff --git a/resources/views/chat/index.blade.php b/resources/views/chat/index.blade.php new file mode 100644 index 0000000..4fd72a0 --- /dev/null +++ b/resources/views/chat/index.blade.php @@ -0,0 +1,29 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Chat') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/form-validation.scss' + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/popular.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/bootstrap5.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/auto-focus.js', + ]) +@endsection + +@push('page-script') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/chat-settings-card.js') +@endpush + +@section('content') +
+
+ @livewire('vuexy-website-admin::chat-settings') +
+
+@endsection diff --git a/resources/views/contact-form/index.blade.php b/resources/views/contact-form/index.blade.php new file mode 100644 index 0000000..20bb15f --- /dev/null +++ b/resources/views/contact-form/index.blade.php @@ -0,0 +1,29 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Formulario de Contacto') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/form-validation.scss' + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/popular.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/bootstrap5.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/auto-focus.js', + ]) +@endsection + +@push('page-script') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/contact-form-settings-card.js') +@endpush + +@section('content') +
+
+ @livewire('vuexy-website-admin::contact-form-settings') +
+
+@endsection diff --git a/resources/views/contact-info/index.blade.php b/resources/views/contact-info/index.blade.php new file mode 100644 index 0000000..c927cc2 --- /dev/null +++ b/resources/views/contact-info/index.blade.php @@ -0,0 +1,32 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Información de Contacto') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/form-validation.scss' + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/popular.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/bootstrap5.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/auto-focus.js', + ]) +@endsection + +@push('page-script') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/contact-info-settings-card.js') +@endpush + +@section('content') +
+
+ @livewire('vuexy-website-admin::contact-info-settings') +
+
+ @livewire('vuexy-website-admin::location-settings') +
+
+@endsection diff --git a/resources/views/faq/index.blade.php b/resources/views/faq/index.blade.php new file mode 100644 index 0000000..7c161fb --- /dev/null +++ b/resources/views/faq/index.blade.php @@ -0,0 +1,23 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Preguntas Frecuentes') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.scss', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/bootstrap-table/bootstrap-table.scss', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/fonts/bootstrap-icons.scss', + ]) +@endsection + +@push('page-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/bootstrap-table/bootstrapTableManager.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/forms/formConvasHelper.js', + ]) +@endpush + +@section('content') + @livewire('vuexy-website-admin::faq-index') +@endsection diff --git a/resources/views/general-settings/index.blade.php b/resources/views/general-settings/index.blade.php new file mode 100644 index 0000000..f843973 --- /dev/null +++ b/resources/views/general-settings/index.blade.php @@ -0,0 +1,20 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Ajustes Generales') + +@push('page-script') + @vite('vendor/koneko/laravel-vuexy-admin/resources/js/pages/admin-settings-scripts.js') +@endpush + +@section('content') +
+
+ @livewire('vuexy-website-admin::website-description-settings') + @livewire('vuexy-website-admin::website-favicon-settings') +
+
+ @livewire('vuexy-website-admin::logo-on-light-bg-settings') + @livewire('vuexy-website-admin::logo-on-dark-bg-settings') +
+
+@endsection diff --git a/resources/views/google-analytics/index.blade.php b/resources/views/google-analytics/index.blade.php new file mode 100644 index 0000000..5010e3e --- /dev/null +++ b/resources/views/google-analytics/index.blade.php @@ -0,0 +1,29 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Google Analytics') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/form-validation.scss' + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/popular.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/bootstrap5.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/auto-focus.js', + ]) +@endsection + +@push('page-script') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/google-analytics-settings-card.js') +@endpush + +@section('content') +
+
+ @livewire('vuexy-website-admin::google-analytics-settings') +
+
+@endsection diff --git a/resources/views/images/index.blade.php b/resources/views/images/index.blade.php new file mode 100644 index 0000000..4a81c63 --- /dev/null +++ b/resources/views/images/index.blade.php @@ -0,0 +1,11 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Galería de Imágenes') + +@push('page-script') + @vite('vendor/koneko/laravel-vuexy-admin/resources/js/pages/admin-settings-scripts.js') +@endpush + +@section('content') + @livewire('vuexy-website-admin::images-index') +@endsection diff --git a/resources/views/legal-notices/index.blade.php b/resources/views/legal-notices/index.blade.php new file mode 100644 index 0000000..7cadcbe --- /dev/null +++ b/resources/views/legal-notices/index.blade.php @@ -0,0 +1,7 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Avisos Legales') + +@section('content') + @livewire('vuexy-website-admin::legal-notices-index') +@endsection diff --git a/resources/views/legal-notices/legal-index.blade.php b/resources/views/legal-notices/legal-index.blade.php new file mode 100644 index 0000000..ceeef63 --- /dev/null +++ b/resources/views/legal-notices/legal-index.blade.php @@ -0,0 +1,28 @@ +@extends('admin::layouts.vuexy.layoutMaster') + +@section('title', 'Avisos legales') + + +@section('vendor-style') + @vite([ + 'modules/Admin/Resources/assets/vendor/libs/quill/typography.scss', + //'modules/Admin/Resources/assets/vendor/libs/quill/katex.scss', + 'modules/Admin/Resources/assets/vendor/libs/quill/editor.scss' + ]) +@endsection + + +@section('vendor-script') + @vite([ + //'modules/Admin/Resources/assets/vendor/libs/quill/katex.js', + 'modules/Admin/Resources/assets/vendor/libs/quill/quill.js' + ]) +@endsection + +@section('page-script') + @vite('modules/Admin/Resources/js/website-settings/legal-settings-scripts.js') +@endsection + +@section('content') + @livewire('website-legal-settings') +@endsection diff --git a/resources/views/livewire/faq/index.blade.php b/resources/views/livewire/faq/index.blade.php new file mode 100644 index 0000000..12a6a21 --- /dev/null +++ b/resources/views/livewire/faq/index.blade.php @@ -0,0 +1,7 @@ + + +
+ +
+
+
diff --git a/resources/views/livewire/images/index.blade.php b/resources/views/livewire/images/index.blade.php new file mode 100644 index 0000000..c540f4f --- /dev/null +++ b/resources/views/livewire/images/index.blade.php @@ -0,0 +1,12 @@ +
+
+
+
+
Imagenes
+
+ +
+
+
+
+
diff --git a/resources/views/livewire/legal-notices/index.blade.php b/resources/views/livewire/legal-notices/index.blade.php new file mode 100644 index 0000000..9016033 --- /dev/null +++ b/resources/views/livewire/legal-notices/index.blade.php @@ -0,0 +1,70 @@ +
+ + + {{-- Selector de sección --}} + +
+ @foreach($legalVars as $key => $section) +
+ {{-- Habilitar sección --}} + + + {{-- Editor de contenido --}} + +
+ @endforeach +
+
+ + {{-- Botones de acción --}} +
+
+ + + +
+
+ + {{-- Contenedor para notificaciones --}} +
+
+
diff --git a/resources/views/livewire/sitemap-manager/index.blade.php b/resources/views/livewire/sitemap-manager/index.blade.php new file mode 100644 index 0000000..69e3e8c --- /dev/null +++ b/resources/views/livewire/sitemap-manager/index.blade.php @@ -0,0 +1,22 @@ +
+

Gestión del Sitemap

+ + + + + + +
    + @foreach($urls as $url) +
  • {{ $url->url }} ({{ $url->changefreq }}, {{ $url->priority }}) + +
  • + @endforeach +
+ + +
diff --git a/resources/views/livewire/vuexy/analytics-settings.blade.php b/resources/views/livewire/vuexy/analytics-settings.blade.php new file mode 100644 index 0000000..f9e2067 --- /dev/null +++ b/resources/views/livewire/vuexy/analytics-settings.blade.php @@ -0,0 +1,34 @@ +
+ + + + + + +
+
+ + +
+
+
+
+
diff --git a/resources/views/livewire/vuexy/chat-settings.blade.php b/resources/views/livewire/vuexy/chat-settings.blade.php new file mode 100644 index 0000000..d202a7d --- /dev/null +++ b/resources/views/livewire/vuexy/chat-settings.blade.php @@ -0,0 +1,58 @@ +
+ + + {{-- Proveedor --}} +
+ + +
+ + {{-- Configuración de WhatsApp --}} +
+
WhatsApp
+ + + +
+
+ + {{-- Botones de acción --}} +
+
+ + + +
+
+ + {{-- Contenedor para notificaciones --}} +
+
+
diff --git a/resources/views/livewire/vuexy/contact-form-settings.blade.php b/resources/views/livewire/vuexy/contact-form-settings.blade.php new file mode 100644 index 0000000..9a3edf4 --- /dev/null +++ b/resources/views/livewire/vuexy/contact-form-settings.blade.php @@ -0,0 +1,33 @@ +
+ + + + + + + +
+
+ + +
+
+
+
+
diff --git a/resources/views/livewire/vuexy/contact-info-settings.blade.php b/resources/views/livewire/vuexy/contact-info-settings.blade.php new file mode 100644 index 0000000..25a6cc6 --- /dev/null +++ b/resources/views/livewire/vuexy/contact-info-settings.blade.php @@ -0,0 +1,41 @@ +
+ + +
+ + +
+
+ + +
+ + +
+ +
+
+ + + +
+
+
+
+
diff --git a/resources/views/livewire/vuexy/location-settings.blade.php b/resources/views/livewire/vuexy/location-settings.blade.php new file mode 100644 index 0000000..6aeb34d --- /dev/null +++ b/resources/views/livewire/vuexy/location-settings.blade.php @@ -0,0 +1,34 @@ +
+ + + +
+ + +
+
+
+
+ + +
+
+
+
+
diff --git a/resources/views/livewire/vuexy/logo-on-dark-bg-settings.blade.php b/resources/views/livewire/vuexy/logo-on-dark-bg-settings.blade.php new file mode 100644 index 0000000..dd08120 --- /dev/null +++ b/resources/views/livewire/vuexy/logo-on-dark-bg-settings.blade.php @@ -0,0 +1,39 @@ +
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+
+
diff --git a/resources/views/livewire/vuexy/logo-on-light-bg-settings.blade.php b/resources/views/livewire/vuexy/logo-on-light-bg-settings.blade.php new file mode 100644 index 0000000..75cf53d --- /dev/null +++ b/resources/views/livewire/vuexy/logo-on-light-bg-settings.blade.php @@ -0,0 +1,39 @@ +
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+
+
diff --git a/resources/views/livewire/vuexy/social-media-settings.blade.php b/resources/views/livewire/vuexy/social-media-settings.blade.php new file mode 100644 index 0000000..010efd4 --- /dev/null +++ b/resources/views/livewire/vuexy/social-media-settings.blade.php @@ -0,0 +1,47 @@ +
+ + +
+
+ + + + + + +
+
+ + + + + +
+
+
+
+
+ + +
+
+
+
+
diff --git a/resources/views/livewire/vuexy/template-settings.blade.php b/resources/views/livewire/vuexy/template-settings.blade.php new file mode 100644 index 0000000..c97d37e --- /dev/null +++ b/resources/views/livewire/vuexy/template-settings.blade.php @@ -0,0 +1,49 @@ +
+
+
+
+
Porto Template 12.0.0
+
+ + Mostrar personalizador de estilos + +
+
+ + + @error("website_tpl_footer_text") + {{ $message }} + @enderror +
+
+
+
+ {{-- Botones --}} +
+
+ + +
+
+ {{-- Notifications --}} +
+
+
+
diff --git a/resources/views/livewire/vuexy/website-description-settings.blade.php b/resources/views/livewire/vuexy/website-description-settings.blade.php new file mode 100644 index 0000000..dde900c --- /dev/null +++ b/resources/views/livewire/vuexy/website-description-settings.blade.php @@ -0,0 +1,31 @@ +
+
+ + + + +
+
+ + +
+
+
+
+
diff --git a/resources/views/livewire/vuexy/website-favicon-settings.blade.php b/resources/views/livewire/vuexy/website-favicon-settings.blade.php new file mode 100644 index 0000000..3705dd5 --- /dev/null +++ b/resources/views/livewire/vuexy/website-favicon-settings.blade.php @@ -0,0 +1,84 @@ +
+
+ + +
+
+
+
+ +
+ Navegadores web (16x16) +
+
+
+
+
+ +
+ iPad sin Retina (76x76) +
+
+
+
+
+ +
+ iPhone (120x120) +
+
+
+
+
+ +
+ iPad (152x152) +
+
+
+
+
+ +
+ iPhone con Retina HD (180x180) +
+
+
+
+
+ +
+ Android y otros dispositivos móviles (192x192) +
+
+
+
+
+
+ + +
+
+
+
+
diff --git a/resources/views/sitemap-manager/index.blade.php b/resources/views/sitemap-manager/index.blade.php new file mode 100644 index 0000000..0ed2b83 --- /dev/null +++ b/resources/views/sitemap-manager/index.blade.php @@ -0,0 +1,7 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Mapa del Sitio') + +@section('content') + @livewire('vuexy-website-admin::sitemap-manager-index') +@endsection diff --git a/resources/views/social-media/index.blade.php b/resources/views/social-media/index.blade.php new file mode 100644 index 0000000..de1ffcf --- /dev/null +++ b/resources/views/social-media/index.blade.php @@ -0,0 +1,25 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Redes Sociales') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/form-validation.scss' + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/popular.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/bootstrap5.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/@form-validation/auto-focus.js', + ]) +@endsection + +@push('page-script') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/website-settings-card.js') +@endpush + +@section('content') + @livewire('vuexy-website-admin::social-media-settings') +@endsection diff --git a/routes/admin.php b/routes/admin.php index 3049bbd..739664a 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -1,9 +1,59 @@ name('admin.')->middleware(['web', 'auth', 'admin.settings'])->group(function () { +Route::prefix('admin/sitio-web')->name('admin.website-admin.')->middleware(['web', 'auth', 'admin'])->group(function () { + // ajustes generales + Route::controller(VuexyWebsiteAdminController::class)->prefix('ajustes-generales')->group(function () { + Route::get('ajustes-generales', 'index')->name('general-settings.index'); + }); + // Avisos legales + Route::controller(LegalNoticesController::class)->prefix('avisos-legales')->group(function () { + Route::get('/', 'index')->name('legal-notices.index'); + }); + + // Preguntas frecuentes + Route::controller(FaqController::class)->prefix('preguntas-frecuentes')->group(function () { + Route::get('/', 'index')->name('faq.index'); + }); + + // Redes sociales + Route::controller(SocialMediaController::class)->prefix('redes-sociales')->group(function () { + Route::get('/', 'index')->name('social-media.index'); + }); + + // Chat + Route::controller(ChatController::class)->prefix('chat')->group(function () { + Route::get('/', 'index')->name('chat.index'); + }); + + // Galería de imágenes + Route::controller(ImagesController::class)->prefix('galeria-de-imagenes')->group(function () { + Route::get('/', 'index')->name('images.index'); + }); + + // Google Analytics + Route::controller(GoogleAnalyticsController::class)->prefix('google-analytics')->group(function () { + Route::get('/', 'index')->name('google-analytics.index'); + }); + + // Información de contacto + Route::controller(ContactInfoController::class)->prefix('informacion-de-contacto')->group(function () { + Route::get('/', 'index')->name('contact-info.index'); + }); + + // Formulario de contacto + Route::controller(ContactFormController::class)->prefix('formulario-de-contacto')->group(function () { + Route::get('/', 'index')->name('contact-form.index'); + }); + + // Mapa del sitio + Route::controller(SitemapController::class)->prefix('mapa-del-sitio')->group(function () { + Route::get('/', 'index')->name('sitemap.index'); + }); });