Prepare modules

This commit is contained in:
Arturo Corro 2025-03-22 12:41:56 -06:00
parent 04f5e973d7
commit 3916c62935
74 changed files with 4431 additions and 194 deletions

View File

@ -0,0 +1,37 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Koneko\VuexyWebsiteAdmin\Models\SitemapUrl;
class SitemapGenerate extends Command
{
protected $signature = 'sitemap:generate';
protected $description = 'Genera un sitemap.xml con rutas dinámicas del sistema';
public function handle()
{
$urls = SitemapUrl::where('is_active', true)->get();
$sitemap = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
$sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . PHP_EOL;
foreach ($urls as $url) {
$sitemap .= " <url><loc>{$url->url}</loc>" . PHP_EOL;
$sitemap .= " <changefreq>{$url->changefreq}</changefreq>" . PHP_EOL;
$sitemap .= " <priority>{$url->priority}</priority>" . PHP_EOL;
if ($url->lastmod) {
$sitemap .= " <lastmod>{$url->lastmod->toDateString()}</lastmod>" . PHP_EOL;
}
$sitemap .= " </url>" . PHP_EOL;
}
$sitemap .= '</urlset>';
Storage::disk('public')->put('sitemap.xml', $sitemap);
$this->info('✅ Sitemap generado en storage/app/public/sitemap.xml');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class ChatController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::chat.index');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class ContactFormController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::contact-form.index');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class ContactInfoController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::contact-info.index');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class FaqController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::faq.index');
}
}

View File

@ -1,186 +0,0 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;;
use Illuminate\Http\Request;
use Yajra\DataTables\Facades\DataTables;
use App\Models\Catalog\DropdownList;
use App\Http\Controllers\Controller;
class GeneralController extends Controller
{
public function webApp()
{
$breadcrumbs = [
['route' => '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]);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class GoogleAnalyticsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::google-analytics.index');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class ImagesController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::images.index');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class LegalNoticesController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::legal-notices.index');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class SitemapController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::sitemap-manager.index');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class SocialMediaController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::social-media.index');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
class VuexyWebsiteAdminController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('vuexy-website-admin::general-settings.index');
}
}

103
Livewire/Faq/FaqIndex.php Normal file
View File

@ -0,0 +1,103 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\Faq;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
use Koneko\VuexyWebsiteAdmin\Models\Faq;
class FaqIndex extends AbstractIndexComponent
{
/**
* Retorna la clase del modelo asociado.
*
* @return string
*/
protected function model(): string
{
return Faq::class;
}
/**
* Configura el encabezado (header) de la tabla (las columnas).
*
* @return array
*/
protected function columns(): array
{
return [
'action' => '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();
}
}

View File

@ -0,0 +1,217 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\Faq;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
use Koneko\VuexyContacts\Services\ContactCatalogService;
use Koneko\VuexyStoreManager\Services\StoreCatalogService;
use Koneko\VuexyWarehouse\Models\Warehouse;
/**
* Class FaqOffcanvasForm
*
* Componente Livewire para gestionar almacenes.
* Extiende la clase AbstractFormOffCanvasComponent e implementa validaciones dinámicas,
* manejo de formularios, eventos y actualizaciones en tiempo real.
*
* @package Koneko\VuexyWarehouse\Livewire\Warehouses
*/
class FaqOffcanvasForm extends AbstractFormOffCanvasComponent
{
/**
* Propiedades del formulario relacionadas con el almacén.
*/
public $id, $store_id, $work_center_id, $code, $name, $description,
$manager_id, $tel, $tel2, $priority, $status, $confirmDeletion;
/**
* Listas de opciones para selects en el formulario.
*/
public $store_options = [],
$work_center_options = [],
$manager_options = [];
/**
* Eventos de escucha de Livewire.
*
* @var array
*/
protected $listeners = [
'editWarehouse' => '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<string, mixed>
*/
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<string, string>
*/
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<string, string>
*/
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';
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\Images;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
use Koneko\VuexyWebsiteAdmin\Models\Faq;
use Koneko\VuexyWebsiteAdmin\Models\Images;
class ImagesIndex extends AbstractIndexComponent
{
/**
* Retorna la clase del modelo asociado.
*
* @return string
*/
protected function model(): string
{
return Faq::class;
}
/**
* Configura el encabezado (header) de la tabla (las columnas).
*
* @return array
*/
protected function columns(): array
{
return [
'action' => '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();
}
}

View File

@ -0,0 +1,217 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\LegalNotices;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
use Koneko\VuexyContacts\Services\ContactCatalogService;
use Koneko\VuexyStoreManager\Services\StoreCatalogService;
use Koneko\VuexyWarehouse\Models\Warehouse;
/**
* Class LegalNoticeOffCanvasForm
*
* Componente Livewire para gestionar almacenes.
* Extiende la clase AbstractFormOffCanvasComponent e implementa validaciones dinámicas,
* manejo de formularios, eventos y actualizaciones en tiempo real.
*
* @package Koneko\VuexyWarehouse\Livewire\Warehouses
*/
class LegalNoticeOffCanvasForm extends AbstractFormOffCanvasComponent
{
/**
* Propiedades del formulario relacionadas con el almacén.
*/
public $id, $store_id, $work_center_id, $code, $name, $description,
$manager_id, $tel, $tel2, $priority, $status, $confirmDeletion;
/**
* Listas de opciones para selects en el formulario.
*/
public $store_options = [],
$work_center_options = [],
$manager_options = [];
/**
* Eventos de escucha de Livewire.
*
* @var array
*/
protected $listeners = [
'editWarehouse' => '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<string, mixed>
*/
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<string, string>
*/
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<string, string>
*/
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';
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\LegalNotices;
use Livewire\Component;
use Koneko\VuexyAdmin\Rules\NotEmptyHtml;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
class LegalNoticesIndex extends Component
{
private $targetNotify = "#website-legal-settings-card .notification-container";
public $legalVars = [];
public $currentSection = null;
protected $listeners = [
'saveLegalNotices' => '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');
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Modules\Admin\App\Livewire\WebsiteSettings;
use Livewire\Component;
use App\Services\WebsiteTemplateService;
use Modules\Admin\App\Rules\NotEmptyHtml;
use Modules\Admin\App\Services\WebsiteSettingsService;
class LegalSettings extends Component
{
private $targetNotify = "#website-legal-settings-card .notification-container";
public $legalVars = [];
public $currentSection = null;
protected $listeners = [
'saveLegal' => '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');
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\SitemapManager;
use Livewire\Component;
use Koneko\VuexyWebsiteAdmin\Models\SitemapUrl;
class SitemapManagerIndex extends Component
{
public $urls, $newUrl, $changefreq = 'weekly', $priority = 0.5;
public function mount()
{
$this->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]);
}}

View File

@ -0,0 +1,217 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\SitemapManager;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
use Koneko\VuexyContacts\Services\ContactCatalogService;
use Koneko\VuexyStoreManager\Services\StoreCatalogService;
use Koneko\VuexyWarehouse\Models\Warehouse;
/**
* Class SitemapUrlOffcanvasForm
*
* Componente Livewire para gestionar almacenes.
* Extiende la clase AbstractFormOffCanvasComponent e implementa validaciones dinámicas,
* manejo de formularios, eventos y actualizaciones en tiempo real.
*
* @package Koneko\VuexyWarehouse\Livewire\Warehouses
*/
class SitemapUrlOffcanvasForm extends AbstractFormOffCanvasComponent
{
/**
* Propiedades del formulario relacionadas con el almacén.
*/
public $id, $store_id, $work_center_id, $code, $name, $description,
$manager_id, $tel, $tel2, $priority, $status, $confirmDeletion;
/**
* Listas de opciones para selects en el formulario.
*/
public $store_options = [],
$work_center_options = [],
$manager_options = [];
/**
* Eventos de escucha de Livewire.
*
* @var array
*/
protected $listeners = [
'editWarehouse' => '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<string, mixed>
*/
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<string, string>
*/
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<string, string>
*/
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';
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
use Livewire\Component;
class ChatSettings extends Component
{
private $targetNotify = "#website-chat-settings-card .notification-container";
public $chat_provider,
$chat_whatsapp_number,
$chat_whatsapp_message;
public function mount()
{
$this->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');
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
use Livewire\Component;
class ContactFormSettings extends Component
{
private $targetNotify = "#website-contact-form-settings-card .notification-container";
public $to_email,
$to_email_cc,
$subject,
$submit_message;
public function mount()
{
$this->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');
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
use Livewire\Component;
class ContactInfoSettings extends Component
{
private $targetNotify = "#website-contact-info-settings-card .notification-container";
public $phone_number,
$phone_number_ext,
$phone_number_2,
$phone_number_2_ext,
$email,
$horario;
public function mount()
{
$this->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');
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
use Livewire\Component;
class GoogleAnalyticsSettings extends Component
{
private $targetNotify = "#website-analytics-settings-card .notification-container";
public $google_analytics_enabled,
$google_analytics_id;
public function mount()
{
$this->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');
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
use Livewire\Component;
class LocationSettings extends Component
{
private $targetNotify = "#website-location-settings-card .notification-container";
public $direccion,
$location_lat,
$location_lng;
public function mount()
{
$this->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');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Livewire\Component;
use Livewire\WithFileUploads;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteSettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
class LogoOnDarkBgSettings extends Component
{
use WithFileUploads;
private $targetNotify = "#logo-on-dark-bg-settings-card .notification-container";
public $website_image_logo_dark,
$upload_image_logo_dark;
public function mount()
{
$this->resetForm();
}
public function save()
{
$this->validate([
'upload_image_logo_dark' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
]);
// Procesar favicon si se ha cargado una imagen
app(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');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Livewire\Component;
use Livewire\WithFileUploads;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteSettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
class LogoOnLightBgSettings extends Component
{
use WithFileUploads;
private $targetNotify = "#logo-on-light-bg-settings-card .notification-container";
public $website_image_logo,
$upload_image_logo;
public function mount()
{
$this->resetForm();
}
public function save()
{
$this->validate([
'upload_image_logo' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
]);
// Procesar favicon si se ha cargado una imagen
app(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');
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
use Livewire\Component;
class SocialMediaSettings extends Component
{
private $targetNotify = "#website-social-settings-card .notification-container";
public $social_whatsapp,
$social_whatsapp_message,
$social_facebook,
$social_instagram,
$social_linkedin,
$social_tiktok,
$social_x_twitter,
$social_google,
$social_pinterest,
$social_youtube,
$social_vimeo;
public function mount()
{
$this->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');
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
class WebsiteDescriptionSettings extends Component
{
private $targetNotify = "#website-description-settings-card .notification-container";
public $title,
$description;
public function mount()
{
$this->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');
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin;
use Livewire\Component;
use Livewire\WithFileUploads;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteSettingsService;
use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService;
class WebsiteFaviconSettings extends Component
{
use WithFileUploads;
private $targetNotify = "#website-favicon-settings-card .notification-container";
public $website_favicon_16x16,
$website_favicon_76x76,
$website_favicon_120x120,
$website_favicon_152x152,
$website_favicon_180x180,
$website_favicon_192x192;
public $upload_image_favicon;
public function mount()
{
$this->resetForm();
}
public function save()
{
$this->validate([
'upload_image_favicon' => 'required|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
]);
// Procesar favicon
app(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');
}
}

33
Models/Faq.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Faq extends Model
{
use HasFactory;
protected $fillable = [
'category_id',
'question',
'answer',
'order',
'is_active',
];
protected $casts = [
'order' => 'integer',
'is_active' => 'boolean',
];
/**
* Categoría a la que pertenece esta FAQ.
*/
public function category(): BelongsTo
{
return $this->belongsTo(FaqCategory::class, 'category_id');
}
}

32
Models/FaqCategory.php Normal file
View File

@ -0,0 +1,32 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class FaqCategory extends Model
{
use HasFactory;
protected $fillable = [
'name',
'icon',
'order',
'is_active',
];
protected $casts = [
'order' => 'integer',
'is_active' => 'boolean',
];
/**
* FAQs asociadas a esta categoría.
*/
public function faqs(): HasMany
{
return $this->hasMany(Faq::class, 'category_id');
}
}

View File

19
Models/SitemapUrl.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SitemapUrl extends Model
{
use HasFactory;
protected $fillable = [
'url',
'changefreq',
'priority',
'lastmod',
'is_active',
];
}

View File

@ -4,7 +4,16 @@ namespace Koneko\VuexyWebsiteAdmin\Providers;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
use OwenIt\Auditing\AuditableObserver;
use Koneko\VuexyWebsiteAdmin\Console\Commands\SitemapGenerate;
use Koneko\VuexyWebsiteAdmin\Livewire\Faq\{FaqIndex,FaqOffCanvasForm};
use Koneko\VuexyWebsiteAdmin\Livewire\Images\ImagesIndex;
use Koneko\VuexyWebsiteAdmin\Livewire\LegalNotices\{LegalNoticesIndex,LegalNoticeOffCanvasForm};
use Koneko\VuexyWebsiteAdmin\Livewire\SitemapManager\{SitemapManagerIndex,SitemapUrlOffcanvasForm};
use Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin\{WebsiteDescriptionSettings,WebsiteFaviconSettings,LogoOnLightBgSettings,LogoOnDarkBgSettings};
use Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin\{SocialMediaSettings,ChatSettings,GoogleAnalyticsSettings};
use Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin\{ContactInfoSettings,LocationSettings,ContactFormSettings};
class VuexyWebsiteAdminServiceProvider extends ServiceProvider
{
@ -32,18 +41,57 @@ class VuexyWebsiteAdminServiceProvider extends ServiceProvider
// Register the migrations
$this->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);
}
}

View File

@ -0,0 +1,292 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Services;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Koneko\VuexyAdmin\Models\Setting;
use Koneko\VuexyAdmin\Services\SettingsService;
/**
* Servicio para gestionar la configuración del template del website.
*
* Este servicio maneja el procesamiento y almacenamiento de imágenes del favicon
* y logos del website, incluyendo diferentes versiones y tamaños.
*
* @package Koneko\VuexyWebsiteAdmin\Services
*/
class WebsiteSettingsService
{
/** @var string Driver de procesamiento de imágenes */
private $driver;
/** @var string Disco de almacenamiento para imágenes */
private $imageDisk = 'public';
/** @var string Ruta base para favicons */
private $favicon_basePath = 'favicon/';
/** @var string Ruta base para logos */
private $image_logo_basePath = 'images/logo/';
/** @var array<string,array<int>> 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();
}
}
}
}

View File

@ -0,0 +1,395 @@
<?php
namespace Koneko\VuexyWebsiteAdmin\Services;
use Illuminate\Support\Facades\{Cache,Schema};
use Koneko\VuexyAdmin\Models\Setting;
/**
* Servicio para gestionar la configuración y personalización del template del Website.
*
* Esta clase maneja las configuraciones del template del website, incluyendo variables
* de personalización, logos, favicons y otras configuraciones de la interfaz.
* Implementa un sistema de caché para optimizar el rendimiento.
*/
class WebsiteTemplateService
{
/** @var int Tiempo de vida del caché en minutos (60 * 24 * 30 = 30 días) */
protected $cacheTTL = 60 * 24 * 30;
/**
* Obtiene las variables del template del website.
*
* @param string $setting Clave de la configuración a obtener
* @return array Array con las variables del template
*/
public function getWebsiteVars(string $setting = ''): array
{
try {
// Verifica si la base de datos está inicializada
if (!Schema::hasTable('migrations')) {
return $this->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;
}
}

View File

@ -11,7 +11,7 @@
},
"autoload": {
"psr-4": {
"Koneko\\VuexyWebsiteAdmin\\": ""
"Koneko\\VuexyWebsiteAdmin\\": "./"
}
},
"extra": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')
<div class="row">
<div class="col-md-6">
@livewire('vuexy-website-admin::chat-settings')
</div>
</div>
@endsection

View File

@ -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')
<div class="row">
<div class="col-md-6">
@livewire('vuexy-website-admin::contact-form-settings')
</div>
</div>
@endsection

View File

@ -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')
<div class="row">
<div class="col-md-6">
@livewire('vuexy-website-admin::contact-info-settings')
</div>
<div class="col-md-6">
@livewire('vuexy-website-admin::location-settings')
</div>
</div>
@endsection

View File

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

View File

@ -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')
<div class="row">
<div class="col-lg-5">
@livewire('vuexy-website-admin::website-description-settings')
@livewire('vuexy-website-admin::website-favicon-settings')
</div>
<div class="col-lg-4">
@livewire('vuexy-website-admin::logo-on-light-bg-settings')
@livewire('vuexy-website-admin::logo-on-dark-bg-settings')
</div>
</div>
@endsection

View File

@ -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')
<div class="row">
<div class="col-md-6">
@livewire('vuexy-website-admin::google-analytics-settings')
</div>
</div>
@endsection

View File

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

View File

@ -0,0 +1,7 @@
@extends('vuexy-admin::layouts.vuexy.layoutMaster')
@section('title', 'Avisos Legales')
@section('content')
@livewire('vuexy-website-admin::legal-notices-index')
@endsection

View File

@ -0,0 +1,28 @@
@extends('admin::layouts.vuexy.layoutMaster')
@section('title', 'Avisos legales')
<!-- Vendor Styles -->
@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
<!-- Vendor Scripts -->
@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

View File

@ -0,0 +1,7 @@
<x-vuexy-admin::table.bootstrap.manager :tagName="$tagName" :datatableConfig="$bt_datatable">
<x-slot name="tools">
<div class="mb-4 pr-2">
<x-vuexy-admin::button.index-offcanvas :label="$singularName" :tagName="$tagName" />
</div>
</x-slot>
</x-vuexy-admin::table.bootstrap.manager>

View File

@ -0,0 +1,12 @@
<form id="css-form">
<div class="row">
<div class="col-md-6">
<div class="card">
<h5 class="card-header">Imagenes</h5>
<div class="card-body">
</div>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,70 @@
<div>
<x-vuexy-admin::form.form id="website-legal-notices-settings-card" class="form-custom-listener mb-4" wire:ignore.self>
<x-vuexy-admin::card.basic title="Avisos Legales" class="mb-2">
{{-- Selector de sección --}}
<ul class="nav nav-pills" role="tablist">
@foreach($legalVars as $key => $section)
<li class="nav-item">
<button
type="button"
class="nav-link @if($currentSection === $key) active @endif"
onclick="@this.currentSection = '{{ $key }}';"
role="tab"
data-bs-toggle="tab"
data-bs-target="#{{ $key }}-nav"
aria-controls="{{ $key }}-nav"
aria-selected="@if($currentSection === $key) true @else false @endif">
{{ $section['title'] }}
</button>
</li>
@endforeach
</ul>
<div class="tab-content">
@foreach($legalVars as $key => $section)
<div class="tab-pane fade @if($currentSection === $key) show active @endif" id="{{ $key }}-nav" role="tabpanel">
{{-- Habilitar sección --}}
<x-vuexy-admin::form.checkbox
model="legalVars.{{ $key }}.enabled"
label="Habilitar sección"
switch />
{{-- Editor de contenido --}}
<x-vuexy-admin::form.textarea
model="legalVars.{{ $key }}.content"
label="Contenido"
switch
required />
</div>
@endforeach
</div>
</x-vuexy-admin::card.basic>
{{-- Botones de acción --}}
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
type="submit"
variant="primary"
size="sm"
icon="ti ti-device-floppy"
label="Guardar cambios"
disabled
class="btn-save mt-2 mr-2"
waves
data-loading-text="Guardando..." />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
label="Cancelar"
wire:click="loadSettings"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
{{-- Contenedor para notificaciones --}}
<div class="notification-container pt-4" wire:ignore></div>
</x-vuexy-admin::form.form>
</div>

View File

@ -0,0 +1,22 @@
<div>
<h2>Gestión del Sitemap</h2>
<input type="text" wire:model="newUrl" placeholder="Nueva URL">
<select wire:model="changefreq">
<option value="daily">Diario</option>
<option value="weekly">Semanal</option>
<option value="monthly">Mensual</option>
</select>
<input type="number" step="0.1" wire:model="priority" min="0.1" max="1.0">
<button wire:click="addUrl">Agregar</button>
<ul>
@foreach($urls as $url)
<li>{{ $url->url }} ({{ $url->changefreq }}, {{ $url->priority }})
<button wire:click="deleteUrl({{ $url->id }})"></button>
</li>
@endforeach
</ul>
<button wire:click="$emit('generateSitemap')">Regenerar Sitemap</button>
</div>

View File

@ -0,0 +1,34 @@
<div x-data="{ googleanalyticsEnabled: @entangle('google_analytics_enabled') }">
<x-vuexy-admin::form.form id="website-analytics-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode>
<x-vuexy-admin::card.basic title="Google Analytics" class="mb-2">
<div class="mb-6">
<a href="https://analytics.google.com/analytics/web/">https://analytics.google.com/analytics/web/</a>
</div>
<x-vuexy-admin::form.checkbox model="google_analytics_enabled" label="Habilitar Google Analytics" switch />
<x-vuexy-admin::form.input model="google_analytics_id" label="ID de medición de Google Analytics" icon="fab fa-google" placeholder="XX-12345678901" x-bind:disabled='!googleanalyticsEnabled' />
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
type="submit"
variant="primary"
size="sm"
icon="ti ti-device-floppy"
label="Guardar cambios"
disabled
class="btn-save mt-2 mr-2 waves-effect waves-light"
waves
data-loading-text="Guardando..." />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2 waves-effect waves-light"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</x-vuexy-admin::form.form>
</div>

View File

@ -0,0 +1,58 @@
<div x-data="{ chatProvider: @entangle('chat_provider') }">
<x-vuexy-admin::form.form id="website-chat-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode>
<x-vuexy-admin::card.basic title="Configuración del Chat" class="mb-2">
{{-- Proveedor --}}
<div class="mb-4 fv-row">
<label for="chat_provider" class="form-label">Proveedor</label>
<select id="chat_provider" name="chat_provider" x-model="chatProvider" wire:model="chat_provider" class="form-select">
<option value="">Deshabilitar Chat</option>
<option value="whatsapp">WhatsApp</option>
</select>
</div>
{{-- Configuración de WhatsApp --}}
<div x-show="chatProvider === 'whatsapp'" class="mt-5">
<h5>WhatsApp</h5>
<x-vuexy-admin::form.input
model="chat_whatsapp_number"
label="Número telefónico"
placeholder="Número telefónico"
required />
<x-vuexy-admin::form.input
model="chat_whatsapp_message"
label="Mensaje de saludo"
placeholder="Mensaje de saludo"
required />
</div>
</x-vuexy-admin::card.basic>
{{-- Botones de acción --}}
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
type="submit"
variant="primary"
size="sm"
icon="ti ti-device-floppy"
label="Guardar cambios"
disabled
class="btn-save mt-2 mr-2"
waves
data-loading-text="Guardando..." />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
{{-- Contenedor para notificaciones --}}
<div class="notification-container pt-4" wire:ignore></div>
</x-vuexy-admin::form.form>
</div>

View File

@ -0,0 +1,33 @@
<div>
<x-vuexy-admin::form.form id="website-contact-form-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode>
<x-vuexy-admin::card.basic title="Configuración del Formulario" class="mb-2">
<x-vuexy-admin::form.input model="to_email" label="Correo principal" type="email" icon="ti ti-mail" placeholder="Email donde se enviarán los mensajes" required />
<x-vuexy-admin::form.input model="to_email_cc" label="Correo CC" type="email" icon="ti ti-mail-forward" placeholder="Email adicional para copia" helperText="Email adicional que recibirá una copia de los mensajes" />
<x-vuexy-admin::form.input model="subject" label="Asunto del correo" placeholder="Asunto predeterminado del email" required />
<x-vuexy-admin::form.textarea model="submit_message" label="Mensaje de Confirmación" placeholder="Mensaje que se mostrará al usuario cuando envíe el formulario" required />
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
type="submit"
variant="primary"
size="sm"
icon="ti ti-device-floppy"
label="Guardar cambios"
disabled
class="btn-save mt-2 mr-2"
waves
data-loading-text="Guardando..." />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</x-vuexy-admin::form.form>
</div>

View File

@ -0,0 +1,41 @@
<div>
<x-vuexy-admin::form.form id="website-contact-info-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode>
<x-vuexy-admin::card.basic title="Información de contacto" class="mb-2">
<div class="row">
<x-vuexy-admin::form.input model="phone_number" label="Número telefónico" icon="ti ti-phone" placeholder="Número telefónico" parentClass="col-md-8" />
<x-vuexy-admin::form.input model="phone_number_ext" label="Extención telefónica" icon="ti ti-phone-plus" placeholder="Ext. núm." parentClass="col-md-4" />
</div>
<div class="row">
<x-vuexy-admin::form.input model="phone_number_2" label="Número telefónico alternativo" icon="ti ti-phone" placeholder="Número telefónico alternativo" parentClass="col-md-8" />
<x-vuexy-admin::form.input model="phone_number_2_ext" label="Extención telefónica²" icon="ti ti-phone-plus" placeholder="Ext. núm.²" parentClass="col-md-4" />
</div>
<x-vuexy-admin::form.input model="email" label="Correo electrónico" icon="ti ti-mail" type="email" placeholder="Correo electrónico" />
<x-vuexy-admin::form.input model="horario" label="Horario" icon="ti ti-clock" placeholder="Horario" />
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
type="submit"
variant="primary"
size="sm"
icon="ti ti-device-floppy"
label="Guardar cambios"
disabled
class="btn-save mt-2 mr-2"
waves
data-loading-text="Guardando..." />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</x-vuexy-admin::form.form>
</div>

View File

@ -0,0 +1,34 @@
<div>
<x-vuexy-admin::form.form id="website-location-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode>
<x-vuexy-admin::card.basic title="Ubicación y Horarios" class="mb-2">
<x-vuexy-admin::form.input model="direccion" label="Dirección" icon="ti ti-map-pin" placeholder="Dirección" />
<div class="row">
<x-vuexy-admin::form.input type="number" step="0.00000" model="location_lat" label="Latitud" icon="ti ti-map-pin-2" placeholder="Latitud" parentClass="col-6" align="center" />
<x-vuexy-admin::form.input type="number" step="0.00000" model="location_lng" label="Longitud" icon="ti ti-map-pin-2" placeholder="Longitud" parentClass="col-6" align="center" />
</div>
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
type="submit"
variant="primary"
size="sm"
icon="ti ti-device-floppy"
label="Guardar cambios"
disabled
class="btn-save mt-2 mr-2"
waves
data-loading-text="Guardando..." />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</x-vuexy-admin::form.form>
</div>

View File

@ -0,0 +1,39 @@
<div>
<div id="logo-on-dark-bg-settings-card" class="mb-4">
<x-vuexy-admin::card.basic title="Logotipo sobre fondo oscuro" class="mb-2">
<x-vuexy-admin::form.input
type="file"
label="Logotipo sobre fondo oscuro"
model="upload_image_logo_dark"
accept="image/*" />
<div class="mb-3 text-center align-items-center">
<div class="justify-content-center align-items-center bg-slate-800 p-4">
<img src="{{ $upload_image_logo_dark ? $upload_image_logo_dark->temporaryUrl() : asset('storage/' . $website_image_logo_dark) }}">
</div>
</div>
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
variant="primary"
size="sm"
icon="ti ti-device-floppy"
disabled="{{ $upload_image_logo_dark === null }}"
label="Guardar cambios"
wire:click="save"
class="btn-save mt-2 mr-2"
waves />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
disabled="{{ $upload_image_logo_dark === null }}"
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</div>
</div>

View File

@ -0,0 +1,39 @@
<div>
<div id="logo-on-light-bg-settings-card" class="mb-4">
<x-vuexy-admin::card.basic title="Logotipo sobre fondo claro" class="mb-2">
<x-vuexy-admin::form.input
type="file"
label="Logotipo sobre fondo claro"
model="upload_image_logo"
accept="image/*" />
<div class="mb-3 text-center align-items-center">
<div class="justify-content-center align-items-center bg-slate-100 p-4">
<img src="{{ $upload_image_logo ? $upload_image_logo->temporaryUrl() : asset('storage/' . $website_image_logo) }}">
</div>
</div>
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
variant="primary"
size="sm"
icon="ti ti-device-floppy"
disabled="{{ $upload_image_logo === null }}"
label="Guardar cambios"
wire:click="save"
class="btn-save mt-2 mr-2"
waves />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
disabled="{{ $upload_image_logo === null }}"
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</div>
</div>

View File

@ -0,0 +1,47 @@
<div>
<x-vuexy-admin::form.form id="website-social-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode>
<x-vuexy-admin::card.basic title="Redes sociales" class="mb-2">
<div class="row">
<div class="col-md-6">
<x-vuexy-admin::form.input model="social_whatsapp" label="WhatsApp" icon="ti ti-brand-whatsapp" placeholder="Enlace de WhatsApp" />
<x-vuexy-admin::form.input model="social_whatsapp_message" placeholder="Mensaje de saludo WhatsApp" />
<x-vuexy-admin::form.input model="social_facebook" label="Facebook" icon="ti ti-brand-facebook" placeholder="Enlace de Facebook" />
<x-vuexy-admin::form.input model="social_instagram" label="Instagram" icon="ti ti-brand-instagram" placeholder="Enlace de Instagram" />
<x-vuexy-admin::form.input model="social_linkedin" label="LinkedIn" icon="ti ti-brand-linkedin" placeholder="Enlace de LinkedIn" />
<x-vuexy-admin::form.input model="social_tiktok" label="TikTok" icon="ti ti-brand-tiktok" placeholder="Enlace de TikTok" />
</div>
<div class="col-md-6">
<x-vuexy-admin::form.input model="social_x_twitter" label="X (Twitter)" icon="ti ti-brand-twitter" placeholder="Enlace de X (Twitter)" />
<x-vuexy-admin::form.input model="social_google" label="Google" icon="ti ti-brand-google" placeholder="Enlace de Google" />
<x-vuexy-admin::form.input model="social_pinterest" label="Pinterest" icon="ti ti-brand-pinterest" placeholder="Enlace de Pinterest" />
<x-vuexy-admin::form.input model="social_youtube" label="YouTube" icon="ti ti-brand-youtube" placeholder="Enlace de YouTube" />
<x-vuexy-admin::form.input model="social_vimeo" label="Vimeo" icon="ti ti-brand-vimeo" placeholder="Enlace de Vimeo" />
</div>
</div>
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
type="submit"
variant="primary"
size="sm"
icon="ti ti-device-floppy"
data-loading-text="Guardando..."
label="Guardar cambios"
disabled
class="btn-save mt-2 mr-2"
waves />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
disabled
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</x-vuexy-admin::form.form>
</div>

View File

@ -0,0 +1,49 @@
<div>
<form id="website-template-settings-card" novalidate="novalidate">
<div class="card">
<div class="card-body">
<h5>Porto Template 12.0.0</h5>
<div class="mb-4">
<x-form.checkbox
name='website_tpl_style_switcher'
wire:model.defer='website_tpl_style_switcher'
parent_class='form-switch'>
Mostrar personalizador de estilos
</x-form.checkbox>
</div>
<div class="mb-4 fv-row">
<label for="website_tpl_footer_text" class="form-label">Titulo de pie de página</label>
<input type="text" id="website_tpl_footer_text" name="website_tpl_footer_text" wire:model='website_tpl_footer_text' class="form-control" placeholder="Titulo de pie de página">
@error("website_tpl_footer_text")
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
</div>
</div>
<div>
{{-- Botones --}}
<div class="row my-4">
<div class="col-lg-12 text-end">
<button
type="submit"
class="btn btn-primary btn-save btn-sm mt-2 mr-2 waves-effect waves-light"
disabled
data-loading-text="Guardando...">
<i class="ti ti-device-floppy mr-2"></i>
Guardar cambios
</button>
<button
type="button"
wire:click="loadSettings"
class="btn btn-secondary btn-cancel btn-sm mt-2 mr-2 waves-effect waves-light"
disabled>
<i class="ti ti-rotate-2 mr-2"></i>
Cancelar
</button>
</div>
</div>
{{-- Notifications --}}
<div class="notification-container" wire:ignore></div>
</div>
</form>
</div>

View File

@ -0,0 +1,31 @@
<div>
<div id="website-description-settings-card" class="form-custom-listener mb-4">
<x-vuexy-admin::card.basic title="Datos de la aplicación" class="mb-2">
<x-vuexy-admin::form.input model="title" label="Titulo del sitio web" />
<x-vuexy-admin::form.textarea model="description" label="Descripción del sitio web" />
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
variant="primary"
size="sm"
icon="ti ti-device-floppy"
label="Guardar cambios"
disabled
wire:click="save"
class="btn-save mt-2 mr-2"
waves />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
disabled
label="Cancelar"
wire:click="resetForm"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</div>
</div>

View File

@ -0,0 +1,84 @@
<div>
<div id="website-favicon-settings-card" class="mb-4">
<x-vuexy-admin::card.basic title="Favicon" class="mb-2">
<x-vuexy-admin::form.input
type="file"
label="Icono de navegador"
model="upload_image_favicon"
accept="image/*" />
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div class="text-center flex flex-col items-center">
<div class="mb-3 text-center d-flex flex-column align-items-center">
<div class="image-wrapper-16x16 d-flex justify-content-center align-items-center">
<img src="{{ $upload_image_favicon ? $upload_image_favicon->temporaryUrl() : asset('storage/' . $website_favicon_16x16) }}">
</div>
<span class="text-muted mt-1">Navegadores web (16x16)</span>
</div>
</div>
<div class="text-center flex flex-col items-center">
<div class="mb-3 text-center d-flex flex-column align-items-center">
<div class="image-wrapper-76x76 d-flex justify-content-center align-items-center">
<img src="{{ $upload_image_favicon ? $upload_image_favicon->temporaryUrl() : asset('storage/' . $website_favicon_76x76) }}">
</div>
<span class="text-muted mt-1">iPad sin Retina (76x76)</span>
</div>
</div>
<div class="text-center flex flex-col items-center">
<div class="mb-3 text-center d-flex flex-column align-items-center">
<div class="image-wrapper-120x120 d-flex justify-content-center align-items-center">
<img src="{{ $upload_image_favicon ? $upload_image_favicon->temporaryUrl() : asset('storage/' . $website_favicon_120x120) }}">
</div>
<span class="text-muted mt-1">iPhone (120x120)</span>
</div>
</div>
<div class="text-center flex flex-col items-center">
<div class="mb-3 text-center d-flex flex-column align-items-center">
<div class="image-wrapper-152x152 d-flex justify-content-center align-items-center">
<img src="{{ $upload_image_favicon ? $upload_image_favicon->temporaryUrl() : asset('storage/' . $website_favicon_152x152) }}">
</div>
<span class="text-muted mt-1">iPad (152x152)</span>
</div>
</div>
<div class="text-center flex flex-col items-center">
<div class="mb-3 text-center d-flex flex-column align-items-center">
<div class="image-wrapper-180x180 d-flex justify-content-center align-items-center">
<img src="{{ $upload_image_favicon ? $upload_image_favicon->temporaryUrl() : asset('storage/' . $website_favicon_180x180) }}">
</div>
<span class="text-muted mt-1">iPhone con Retina HD (180x180)</span>
</div>
</div>
<div class="text-center flex flex-col items-center">
<div class="mb-3 text-center d-flex flex-column align-items-center">
<div class="image-wrapper-192x192 d-flex justify-content-center align-items-center">
<img src="{{ $upload_image_favicon ? $upload_image_favicon->temporaryUrl() : asset('storage/' . $website_favicon_192x192) }}">
</div>
<span class="text-muted mt-1">Android y otros dispositivos móviles (192x192)</span>
</div>
</div>
</div>
</x-vuexy-admin::card.basic>
<div class="row">
<div class="col-lg-12 text-end">
<x-vuexy-admin::button.basic
variant="primary"
size="sm"
icon="ti ti-device-floppy"
label="Guardar cambios"
wire:click="save"
:disabled="$upload_image_favicon === null"
class="btn-save mt-2 mr-2"
waves />
<x-vuexy-admin::button.basic
variant="secondary"
size="sm"
icon="ti ti-rotate-2"
label="Cancelar"
wire:click="resetForm"
:disabled="$upload_image_favicon === null"
class="btn-cancel mt-2 mr-2"
waves />
</div>
</div>
<div class="notification-container pt-4" wire:ignore></div>
</div>
</div>

View File

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

View File

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

View File

@ -1,9 +1,59 @@
<?php
use Illuminate\Support\Facades\Route;
use Koneko\VuexyAdmin\Http\Controllers\UUserController;
use Koneko\VuexyWebsiteAdmin\Http\Controllers\{LegalNoticesController,FaqController,ImagesController};
use Koneko\VuexyWebsiteAdmin\Http\Controllers\{SocialMediaController,ChatController,GoogleAnalyticsController};
use Koneko\VuexyWebsiteAdmin\Http\Controllers\{ContactInfoController,ContactFormController,VuexyWebsiteAdminController,SitemapController};
// Grupo raíz para admin con middleware y prefijos comunes
Route::prefix('admin')->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');
});
});