diff --git a/.editorconfig b/.editorconfig index 8f0de65..078441f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,8 +3,8 @@ root = true [*] charset = utf-8 end_of_line = lf -indent_size = 4 indent_style = space +indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true @@ -14,5 +14,8 @@ trim_trailing_whitespace = false [*.{yml,yaml}] indent_size = 2 +[*.{js,json,ts,vue}] +indent_size = 2 + [docker-compose.yml] indent_size = 4 diff --git a/.gitattributes b/.gitattributes index 90a4e33..1805660 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,24 +1,40 @@ -# Normaliza los saltos de línea en diferentes SO +# Normaliza los saltos de línea para todos los sistemas operativos * text=auto eol=lf -# Reglas para archivos específicos +# Reglas de diferencia por tipo de archivo *.blade.php diff=html -*.css diff=css -*.html diff=html -*.md diff=markdown -*.php diff=php +*.css diff=css +*.html diff=html +*.js diff=javascript +*.ts diff=typescript +*.vue diff=html +*.md diff=markdown +*.php diff=php +*.json diff=json +*.yml diff=yaml +*.yaml diff=yaml +*.stub diff=php -# Evitar que estos archivos se exporten con Composer create-project -/.github export-ignore -/.gitignore export-ignore -/.git export-ignore -.gitattributes export-ignore -.editorconfig export-ignore -.prettierrc.json export-ignore -.prettierignore export-ignore -.eslintrc.json export-ignore -CHANGELOG.md export-ignore -CONTRIBUTING.md export-ignore -README.md export-ignore -composer.lock export-ignore -package-lock.json export-ignore +# Archivos que NO deben exportarse con Composer create-project +/.github export-ignore +/.gitignore export-ignore +/.git export-ignore +.gitattributes export-ignore +.editorconfig export-ignore +.prettierrc.json export-ignore +.prettierignore export-ignore +.eslintrc.json export-ignore +CHANGELOG.md export-ignore +CONTRIBUTING.md export-ignore +README.md export-ignore +phpunit.xml export-ignore +phpunit.xml.dist export-ignore +composer.lock export-ignore +package-lock.json export-ignore +vite.config.js export-ignore +tailwind.config.js export-ignore +webpack.mix.js export-ignore +tests/ export-ignore +resources/assets/ export-ignore +resources/sass/ export-ignore +node_modules export-ignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b15c0d1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,25 @@ +# CODEOWNERS para koneko/laravel-vuexy-admin + +# Asignar todos los archivos a Arturo Corro Pacheco (mantenedor principal) +* @koneko-mx + +# Archivos legales y de documentación +/LICENSE* @koneko-mx +/NOTICE.md @koneko-mx +/README* @koneko-mx +/CHANGELOG* @koneko-mx +/.github/SECURITY.md @koneko-mx +/.github/CODE_OF_CONDUCT.md @koneko-mx +/.github/ISSUE_TEMPLATE/ @koneko-mx +/.github/PULL_REQUEST_TEMPLATE.md @koneko-mx + +# Código principal +/src/ @koneko-mx +/config/ @koneko-mx +/resources/ @koneko-mx +/routes/ @koneko-mx +/tests/ @koneko-mx + +# Archivos del paquete +/composer.json @koneko-mx +/package.json @koneko-mx diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e00e004 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,24 @@ +# Código de Conducta de Contribuyentes + +Como participantes y colaboradores de este proyecto, nos comprometemos a fomentar una comunidad abierta, inclusiva, y respetuosa. + +## Comportamiento Esperado + +- Usar un lenguaje amable e inclusivo. +- Respetar diferentes puntos de vista y experiencias. +- Aceptar con gracia las críticas constructivas. +- Enfocarse en lo que es mejor para la comunidad. + +## Comportamiento Inaceptable + +- Uso de lenguaje ofensivo o insultante. +- Conducta de acoso en público o privado. +- Comentarios despectivos relacionados con género, raza, orientación, religión, discapacidad o condición médica. + +## Aplicación + +Las violaciones a este código pueden ser reportadas a: **arturo@koneko.mx** + +Nos reservamos el derecho de advertir, suspender o expulsar a cualquier contribuyente que no respete este código. + +Inspirado en el [Contributor Covenant](https://www.contributor-covenant.org/) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e8ed90c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,10 @@ +--- +name: "FUNDING.yml" +about: "Opciones de patrocinio del proyecto" +title: "Patrocinio" +labels: [funding] +assignees: [] + +--- + +Opciones de patrocinio del proyecto \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..115f54f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: "🐛 Bug Report" +about: Reporta un error, fallo o comportamiento inesperado. +title: "[BUG] " +labels: [bug] +assignees: [] + +--- + +## Descripción +Describe claramente el problema. + +## Pasos para reproducir +1. Ir a '...' +2. Hacer clic en '...' +3. Ver error '...' + +## Comportamiento esperado +Una descripción clara y concisa de lo que debería pasar. + +## Capturas de pantalla +(Si aplica, añade imágenes para ayudar a explicar tu problema.) + +## Entorno +- Versión de Laravel: +- Versión del componente: +- Navegador / Sistema operativo: + +## Información adicional +Agrega cualquier otro dato que consideres relevante. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..570f677 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,51 @@ +--- +name: "🧩 Feature Request" +about: Proponer una nueva funcionalidad o mejora para el sistema +title: "[Feature] " +labels: ["feature", "enhancement"] +assignees: [] + +--- + +## 📌 Descripción + +Por favor, proporciona una descripción clara y concisa de la funcionalidad que deseas agregar. + +--- + +## 🤔 Motivación + +¿Cuál es el problema que esta nueva funcionalidad resolverá o qué mejora ofrecerá? + +--- + +## 🧩 Solución Propuesta + +Describe cómo debería funcionar la nueva característica. Si tienes una idea de la implementación técnica, inclúyela. + +--- + +## 🛠️ Requisitos Técnicos (opcional) + +- ¿Requiere cambios en base de datos? +- ¿Afecta al rendimiento? +- ¿Involucra cambios en la UI o API? +- ¿Es compatible con la versión actual del sistema? + +--- + +## 💡 Alternativas consideradas + +Si evaluaste otras soluciones, por favor menciónalas y explica por qué las descartaste. + +--- + +## 📎 Recursos adicionales + +Agrega enlaces, capturas de pantalla o documentación que respalden tu solicitud. + +--- + +## 👤 Autor(a) + +- GitHub: @<tu-usuario> diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4fb229d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,55 @@ +# 📝 Pull Request + +Gracias por tu contribución a **Koneko Laravel Vuexy Admin**. + +Por favor, completa la siguiente información para ayudarnos a revisar tu PR. + +--- + +## 📋 Descripción + +<!-- Explica brevemente qué problema resuelve este PR o qué funcionalidad agrega --> + +--- + +## 🚀 Cambios realizados + +- [ ] Bugfix 🐞 +- [ ] Nueva característica ✨ +- [ ] Mejora/refactorización 🔧 +- [ ] Documentación 📚 +- [ ] Tests 🧪 +- [ ] Otro (especificar): + +--- + +## ✅ Checklist + +- [ ] He probado los cambios localmente +- [ ] He ejecutado las migraciones correctamente (si aplica) +- [ ] No he incluido datos sensibles ni secretos +- [ ] Los tests existentes no fallan +- [ ] He actualizado documentación relevante (si aplica) +- [ ] Incluye etiquetas útiles (`good first issue`, `bug`, `enhancement`, etc.) + +--- + +## 📎 Referencias + +<!-- Indica si este PR está relacionado a algún issue o tarea --> +Closes # + +--- + +## 🌍 Idioma base + +- [ ] Español 🇲🇽 +- [ ] Inglés 🌐 +- [ ] Ambos + +--- + +## 👤 Autor(a) + +- GitHub: @<tu-usuario> +- Nombre opcional: diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..9e3ac22 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,11 @@ +# Reporte de Vulnerabilidades de Seguridad + +Agradecemos el interés por ayudarnos a mantener este proyecto seguro. + +Si encuentras una vulnerabilidad de seguridad, por favor repórtala de manera responsable enviando un correo a: + +**arturo@koneko.mx** + +No abras un issue público hasta que hayamos tenido tiempo de analizar el reporte y preparar una solución. + +Gracias por apoyar la seguridad del ecosistema Koneko ERP. diff --git a/.gitignore b/.gitignore index d07bec2..a0f224d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,60 @@ -/node_modules -/vendor -/.vscode -/.nova -/.fleet -/.phpactor.json +# ⚙️ Laravel Package Defaults +/vendor/ +composer.lock + +# 🧪 PHPUnit +.phpunit.result.cache /.phpunit.cache -/.phpunit.result.cache -/.zed -/.idea +phpunit.xml +phpunit.xml.dist + +# 🧹 Cache y logs +/.cache +/storage/*.key +/storage/pail +*.log +*.dump +*.bak +*.tmp +*.swp + +# 🔐 Entornos y configuraciones +.env +.env.* +auth.json +.phpactor.json +.php-cs-fixer.cache +phpstan.neon.local +homestead.yaml +Homestead.json + +# 🧱 Compilación frontend (Vite, Mix, Webpack, Tailwind) +/node_modules/ +node_modules +public/build/ +public/hot/ +public/storage/ +.vite + +# 🧪 Tests y mocks (solo si generas temporalmente) +/coverage/ +*.test.* +*.spec.* + +# 🛠️ IDEs y herramientas de desarrollo +/.idea/ +/.vscode/ +/.nova/ +/.zed/ +/.fleet/ +*.sublime-workspace +*.sublime-project + +# 📦 Archivos del sistema +.DS_Store +Thumbs.db + +# 🚀 Entornos de staging / producción +*.local.* +*.production.* +*.staging.* diff --git a/CHANGELOG.md b/CHANGELOG.md index 379710b..6a626eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,39 +1,39 @@ -# 📜 CHANGELOG - Laravel Vuexy Website Admin +# 📦 CHANGELOG -Este documento sigue el formato [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - -## [0.1.0] - ALPHA - 2024-03-05 - -### ✨ Added (Agregado) -- 🚀 Primera versión alpha de la librería. -- 🔹 Implementación inicial de [funcionalidad clave 1]. -- 🔹 Integración con [dependencia o servicio principal]. -- 🔹 Soporte para [Laravel/Vuexy Admin, si aplica]. - -### 🛠 Changed (Modificado) -- 🔄 Optimización de [código o estructura interna]. - -### 🐛 Fixed (Correcciones) -- 🐞 Correcciones iniciales en [migraciones, modelos, servicios, etc.]. +Todos los cambios importantes de este proyecto se documentarán en este archivo. +Este proyecto sigue [Semantic Versioning](https://semver.org/lang/es/). --- -## 📅 Próximos Cambios Planeados -- 📊 **Mejoras en [feature futuro]**. -- 🏪 **Compatibilidad con [Laravel 11, Vuexy, etc.]**. -- 📍 **Integración con [API o funcionalidad esperada]**. +## 🧪 Versión en desarrollo (`dev-develop`) + +Este módulo está en desarrollo activo. No se ha publicado una versión estable ni beta formal. + +**Añadido** + +- Registro de contenidos dinámicos en `website_contents` +- Sistema de perfiles SEO (`website_seo_profiles`) con soporte para JSON-LD, Open Graph y Twitter +- Menús públicos dinámicos (`website_menus`) +- Control de visibilidad por rol, flag, permisos y autenticación +- Soporte para múltiples sitios (`multi-site`) y variantes de página +- Soporte para `hreflang`, `manifest.json`, y sitemap +- Integración con módulos `vuexy-admin` y layouts frontales vía `template`, `variant`, `render_mode` + +**Notas** + +- Esta versión no es estable y está sujeta a cambios mayores. +- Compatibilidad mínima: Laravel 11.31, PHP 8.2 +- Requiere `laravel-vuexy-admin` (`dev-develop`) --- -**📌 Nota:** Esta es una versión **ALPHA**, aún en desarrollo. +## 🚀 Planeación de versiones + +- `1.0.0-beta`: Publicación de la versión beta inicial con funcionalidades completas del CMS +- `1.0.0`: Versión estable para producción --- -## 🔄 Sincronización de Cambios -Este `CHANGELOG.md` se actualiza primero en nuestro repositorio principal en **[Tea - Koneko Git](https://git.koneko.mx/koneko/laravel-vuexy-website-admin)** y luego se refleja en GitHub. -Los cambios recientes pueden verse antes en **Tea** que en **GitHub** debido a la sincronización automática. - ---- - -📅 Última actualización: **2024-03-05**. +## 📁 Histórico +*Ninguna versión publicada aún.* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3932692..ce2666c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,77 @@ -## 🔐 Acceso al Repositorio Privado +# 🤝 Guía para Contribuidores -Nuestro servidor Git en **Tea** tiene un registro cerrado. Para contribuir: +¡Gracias por tu interés en colaborar con **Koneko ERP**! +Este proyecto busca ofrecer una suite profesional de administración web y ERP orientado al mercado mexicano y LATAM, bajo licencia **Business Source License 1.1** con transición futura a MIT. -1. Abre un **Issue** en [GitHub](https://github.com/koneko-mx/laravel-vuexy-website-admin/issues) indicando tu interés en contribuir. -2. Alternativamente, envía un correo a **contacto@koneko.mx** solicitando acceso. -3. Una vez aprobado, recibirás una invitación para registrarte y clonar el repositorio. +--- -Si solo necesitas acceso de lectura, puedes clonar la versión pública en **GitHub**. +## 📌 Requisitos básicos + +* Conocimiento de **PHP 8.2+** y **Laravel 11** +* Respeto por las buenas prácticas, código limpio y trabajo en equipo +* Preferentemente, experiencia en: + + * Livewire + * Spatie Permissions & Roles + * Laravel Fortify y Sanctum + * Desarrollo modular con Composer + +--- + +## 🌱 Primeros pasos + +1. **Forkea** este repositorio y clónalo localmente. + +2. Instala las dependencias con Composer: + + ```bash + composer install + ``` + +3. Copia el archivo `.env.example` a `.env` y ajusta tus credenciales locales: + + ```bash + cp .env.example .env + php artisan key:generate + ``` + +4. Ejecuta las migraciones y seeders (si aplica): + + ```bash + php artisan migrate --seed + ``` + +5. Inicia el servidor: + + ```bash + php artisan serve + ``` + +--- + +## 📀 Convenciones del Proyecto + +Antes de contribuir, asegúrate de leer nuestras +📁 [Convenciones de Componentes](CONVENTIONS.md), +donde definimos estructura de carpetas, estilo de código y otras buenas prácticas clave para mantener la consistencia del ecosistema **Koneko ERP**. + +--- + +## 📝 Código de Conducta + +Consulta nuestro [Código de Conducta](CODE_OF_CONDUCT.md) para conocer las expectativas y reglas que garantizan un ambiente colaborativo, profesional y respetuoso. + +--- + +## 🚀 ¡Listo para colaborar! + +Una vez que tengas tu entorno configurado: + +* Busca issues etiquetados como `good first issue` o `help wanted` +* Lee el archivo [`CONTRIBUTING.md`](CONTRIBUTING.md) del módulo específico si estás trabajando en un subpaquete +* No olvides seguir las convenciones de estilo PSR-12 y usar `php-cs-fixer` si lo tienes disponible + +--- + +Gracias por ser parte de **Koneko ERP** 💙 +¡Esperamos tu contribución! diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100644 index 0000000..909b433 --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,105 @@ +#  Convenciones de Estructura de Componentes + +📅 *Última actualización:* 2025-04-03 +🔧 *Aplicable a todos los módulos Composer de Koneko ERP* + +--- + +## 📁 Estructura General de un Componente + +```plaintext +component-root/ +├── config/ ← Configuraciones del módulo +├── Database/ +│ ├── data/ ← Archivos JSON, CSV, XLSX +│ ├── factories/ ← Factories para testing y seeders +│ ├── migrations/ ← Migraciones del esquema del módulo +│ └── Seeders/ ← Seeders base y de datos fake +├── Enums/ ← Enums (PSR-4) usados por el módulo +├── Events/ ← Eventos del módulo +├── Http/ +│ ├── Controllers/ ← Controladores +│ └── Middleware/ ← Middlewares específicos del módulo +├── Livewire/ ← Componentes Livewire organizados por dominio +├── Models/ ← Modelos Eloquent +├── Notifications/ ← Notificaciones personalizadas +├── Providers/ ← Service Providers del módulo +├── Services/ ← Servicios (lógica de negocio) +├── Support/ +│ ├── Base/ ← Clases base abstractas +│ ├── Builders/ ← Configuradores de vistas tipo índice +│ ├── Macros/ ← Macros de Str, Collection, etc. +│ ├── Queries/ ← Query Builders avanzados +│ ├── Registries/ ← Registro dinámico de configuración +│ └── Validation/ ← Validaciones personalizadas +├── Traits/ +│ ├── Audit/ ← Traits para auditoría y tracking +│ ├── Metadata/ ← Traits para metadatos del modelo +│ ├── Users/ ← Traits relacionados con usuarios +│ └── Indexing/ ← Traits usados por configuradores de índices +├── resources/ +│ ├── assets/ ← JS, SCSS, íconos o fuentes específicos +│ ├── faker-images/ ← Imágenes utilizadas en datos de prueba +│ ├── lang/ ← Archivos de traducción +│ └── views/ ← Vistas Blade +├── routes/ +│ └── admin.php ← Rutas internas del módulo +├── storage/ ← Recursos adicionales (ej. fuentes) +└── README.md ← Documentación del componente +``` + +--- + +## 🧠 Convenciones Generales + +- Todos los módulos deben seguir PSR-4. +- Los archivos deben nombrarse en *PascalCase* excepto `config/*.php` y rutas. +- Los `Seeder` deben ser agrupados por módulo si el componente los agrupa (ej. `vuexyAdmin`, `vuexyWarehouse`). +- Los `Factory` deben ser compatibles con `SeederWithFakeImages`. + +--- + +## 🖼️ Imágenes Faker + +- Carpeta: `resources/faker-images/<dominio>` +- Subcarpetas válidas: `users/`, `stores/`, `products/`, etc. +- Las imágenes se usan exclusivamente para entornos de testing/demostración. +- Nunca se publican al frontend ni se exponen directamente. + +--- + +## 🧪 Factories + +- Todas las `factories` deben estar en `Database/factories/`. +- Si se extiende un modelo (`Koneko\VuexyAdmin\Models\User`), usar `new (User::class)` dinámico. +- Compatible con `SeederOrchestrator` y `config/seeder.php`. + +--- + +## 📊 Configuradores de Índice + +- Los index deben implementar `BaseModelIndexConfig` o su extensión. +- Pueden usar Traits como `HandlesFactory`, `HandlesIndexColumns`, `HandlesQueryBuilder`, etc. +- Se recomienda usar `Support/Builders/` para los configuradores y `Support/Registries/` si son extendibles. + +--- + +## 📚 Traducciones + +- Usar `resources/lang/es/` con archivos separados por dominio (`auth.php`, `validation.php`, etc.). +- `es_MX.json` puede usarse para traducciones inline. + +--- + +## 📌 Tips + +- Si un componente tiene `Service`, `Seeder`, `Factory` y `Livewire`, deben estar todos organizados en sus carpetas respectivas. +- La estructura del componente debe ser lo suficientemente clara para no depender de documentación externa. + +--- + +## 🤝 ¿Dudas o sugerencias? + +Este documento está en constante mejora. Si tienes sugerencias o deseas proponer mejoras, por favor abre un [issue de tipo mejora](.github/ISSUE_TEMPLATE/feature_request.md) o contribuye con un Pull Request. + +--- diff --git a/Http/Controllers/ChatController.php b/Http/Controllers/ChatController.php deleted file mode 100644 index c00f22d..0000000 --- a/Http/Controllers/ChatController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/ContactFormController.php b/Http/Controllers/ContactFormController.php deleted file mode 100644 index 0951896..0000000 --- a/Http/Controllers/ContactFormController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/ContactInfoController.php b/Http/Controllers/ContactInfoController.php deleted file mode 100644 index ccdd8ca..0000000 --- a/Http/Controllers/ContactInfoController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/FaqController.php b/Http/Controllers/FaqController.php deleted file mode 100644 index 41041e4..0000000 --- a/Http/Controllers/FaqController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/GoogleAnalyticsController.php b/Http/Controllers/GoogleAnalyticsController.php deleted file mode 100644 index efdfb15..0000000 --- a/Http/Controllers/GoogleAnalyticsController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/ImagesController.php b/Http/Controllers/ImagesController.php deleted file mode 100644 index 0428a2e..0000000 --- a/Http/Controllers/ImagesController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/LegalNoticesController.php b/Http/Controllers/LegalNoticesController.php deleted file mode 100644 index 2b87312..0000000 --- a/Http/Controllers/LegalNoticesController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/SitemapController.php b/Http/Controllers/SitemapController.php deleted file mode 100644 index 2b5defb..0000000 --- a/Http/Controllers/SitemapController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/SocialMediaController.php b/Http/Controllers/SocialMediaController.php deleted file mode 100644 index 72524a1..0000000 --- a/Http/Controllers/SocialMediaController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/Http/Controllers/VuexyWebsiteAdminController.php b/Http/Controllers/VuexyWebsiteAdminController.php deleted file mode 100644 index a471cb5..0000000 --- a/Http/Controllers/VuexyWebsiteAdminController.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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'); - } - -} diff --git a/LICENSE b/LICENSE index 486d340..7df177c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,74 @@ +Business Source License 1.1 (Koneko Custom Edition) + +Licensor: Koneko Soluciones Tecnológicas +Licensed Work: laravel-vuexy-admin +License Effective Date: 2025-05-29 +Change Date: 2028-05-29 (3 years after the Effective Date) + +--- + +## 1. Definitions + +**"Licensor"** refers to Koneko Soluciones Tecnológicas, represented legally by Arturo Corro Pacheco (RFC: COPA810622V65). + +**"Licensed Work"** refers to the source code of the repository `koneko/laravel-vuexy-admin` hosted at [https://github.com/koneko-mx](https://github.com/koneko-mx) and [https://git.koneko.mx](https://git.koneko.mx). + +**"Change Date"** is the date on which this license automatically transitions to the MIT license. + +**"Use"**, **"Commercial Use"**, and other terms are interpreted according to common software licensing practices. + +--- + +## 2. Usage Permissions + +This software is provided under the Business Source License 1.1 with the following additional conditions: + +* **Personal, Educational, Community, and Internal Development Use**: ALLOWED without restrictions. +* **Commercial Deployment**: ALLOWED ONLY under an active commercial license or a specific agreement with the Licensor. +* **Profit-oriented Projects (clients, product sales, SaaS, services)**: REQUIRE a commercial license. +* **Redistribution as part of a commercial product or automated service**: REQUIRES a commercial license. + +--- + +## 3. Automatic Transition to MIT License + +On the **Change Date**, this license automatically transitions to the standard MIT License: + +``` MIT License +Copyright (c) 2025 Koneko Soluciones Tecnológicas +Permission is hereby granted, free of charge, to any person obtaining a copy... +``` -Copyright (c) 2025 koneko +--- -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +## 4. Contact and Commercial Licensing -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +For commercial licenses, partnerships, contributions, or specific agreements: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +**Official Email:** [opensource@koneko.mx](mailto:opensource@koneko.mx) +**Website:** [https://koneko.mx](https://koneko.mx) / [https://erp.koneko.mx](https://erp.koneko.mx) + +--- + +## 5. Important Disclaimers + +* This project integrates the **Vuexy template**, which is a paid product. A **valid Themeforest license** is required to legally use it in production. +* This project does NOT distribute the template, it only facilitates its integration. +* The licensee is responsible for complying with the terms of use of said template and other third-party dependencies. + +--- + +## 6. Disclaimer of Warranty + +This software is provided "AS IS", without any warranty. The Licensor is not liable for any direct or indirect damages resulting from the use of this software. + +--- + +## 7. Contributions + +Contributions to this repository must follow the policies defined in `CONTRIBUTING.md`. By submitting code, you agree to license it under the same terms of this license. + +--- + +Copyright (c) 2025 - Koneko Soluciones Tecnológicas diff --git a/LICENSE.es.md b/LICENSE.es.md new file mode 100644 index 0000000..a1b910d --- /dev/null +++ b/LICENSE.es.md @@ -0,0 +1,74 @@ +Licencia Business Source 1.1 (Edición Personalizada de Koneko) + +Licenciante: Koneko Soluciones Tecnológicas +Trabajo Licenciado: laravel-vuexy-admin +Fecha de Entrada en Vigencia de la Licencia: 2025-05-29 +Fecha de Cambio: 2028-05-29 (3 años después de la fecha de vigencia) + +--- + +## 1. Definiciones + +**"Licenciante"** se refiere a Koneko Soluciones Tecnológicas, representada legalmente por Arturo Corro Pacheco (RFC: COPA810622V65). + +**"Trabajo Licenciado"** se refiere al código fuente del repositorio `koneko/laravel-vuexy-admin` alojado en [https://github.com/koneko-mx](https://github.com/koneko-mx) y [https://git.koneko.mx](https://git.koneko.mx). + +**"Fecha de Cambio"** es la fecha en la que esta licencia se convierte automáticamente en la licencia MIT. + +**"Uso"**, **"Uso Comercial"** y otros términos se interpretan conforme a las prácticas comunes en licenciamiento de software. + +--- + +## 2. Permisos de Uso + +Este software se proporciona bajo la Licencia Business Source 1.1 con las siguientes condiciones adicionales: + +* **Uso Personal, Educativo, Comunitario o de Desarrollo Interno**: PERMITIDO sin restricciones. +* **Implementación Comercial**: PERMITIDO SOLO bajo una licencia comercial activa o un acuerdo específico con el Licenciante. +* **Proyectos con Fines de Lucro (clientes, ventas de productos, SaaS, servicios)**: REQUIEREN una licencia comercial. +* **Redistribución como parte de un producto comercial o servicio automatizado**: REQUIERE una licencia comercial. + +--- + +## 3. Transición Automática a Licencia MIT + +En la **Fecha de Cambio**, esta licencia se convierte automáticamente en la Licencia MIT estándar: + +``` +Licencia MIT +Copyright (c) 2025 Koneko Soluciones Tecnológicas +Se concede permiso, sin cargo, a cualquier persona que obtenga una copia... +``` + +--- + +## 4. Contacto y Licenciamiento Comercial + +Para licencias comerciales, asociaciones, contribuciones o acuerdos específicos: + +**Correo Oficial:** [opensource@koneko.mx](mailto:opensource@koneko.mx) +**Sitio Web:** [https://koneko.mx](https://koneko.mx) / [https://erp.koneko.mx](https://erp.koneko.mx) + +--- + +## 5. Avisos Importantes + +* Este proyecto integra el **template Vuexy**, que es un producto de pago. Se requiere una **licencia válida de Themeforest** para utilizarlo legalmente en producción. +* Este proyecto NO distribuye dicho template, solo facilita su integración. +* El licenciatario es responsable de cumplir con los términos de uso de dicho template y otras dependencias de terceros. + +--- + +## 6. Renuncia de Garantía + +Este software se proporciona "TAL CUAL", sin garantía de ningún tipo. El Licenciante no se hace responsable de ningún daño directo o indirecto derivado del uso de este software. + +--- + +## 7. Contribuciones + +Las contribuciones a este repositorio deben seguir las políticas definidas en `CONTRIBUTING.md`. Al enviar código, aceptas licenciarlo bajo los mismos términos de esta licencia. + +--- + +Copyright (c) 2025 - Koneko Soluciones Tecnológicas diff --git a/Livewire/Faq/FaqIndex.php b/Livewire/Faq/FaqIndex.php deleted file mode 100644 index e318214..0000000 --- a/Livewire/Faq/FaqIndex.php +++ /dev/null @@ -1,103 +0,0 @@ -<?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(); - } -} diff --git a/Livewire/Images/ImagesIndex.php b/Livewire/Images/ImagesIndex.php deleted file mode 100644 index 9b30650..0000000 --- a/Livewire/Images/ImagesIndex.php +++ /dev/null @@ -1,104 +0,0 @@ -<?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(); - } -} diff --git a/Livewire/LegalNotices/LegalSettings.php b/Livewire/LegalNotices/LegalSettings.php deleted file mode 100644 index 0f82f39..0000000 --- a/Livewire/LegalNotices/LegalSettings.php +++ /dev/null @@ -1,108 +0,0 @@ -<?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'); - } -} diff --git a/Models/Faq.php b/Models/Faq.php deleted file mode 100644 index 90af6ab..0000000 --- a/Models/Faq.php +++ /dev/null @@ -1,33 +0,0 @@ -<?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'); - } -} diff --git a/Models/SitemapUrl.php b/Models/SitemapUrl.php deleted file mode 100644 index dffb3b0..0000000 --- a/Models/SitemapUrl.php +++ /dev/null @@ -1,19 +0,0 @@ -<?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', - ]; -} \ No newline at end of file diff --git a/Providers/VuexyWebsiteAdminServiceProvider.php b/Providers/VuexyWebsiteAdminServiceProvider.php deleted file mode 100644 index ddfc856..0000000 --- a/Providers/VuexyWebsiteAdminServiceProvider.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php - -namespace Koneko\VuexyWebsiteAdmin\Providers; - -use Illuminate\Support\ServiceProvider; -use Livewire\Livewire; -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 -{ - /** - * Register any application services. - */ - public function register(): void - { - // - } - - /** - * Bootstrap any application services. - */ - public function boot(): void - { - // Register the module's routes - $this->loadRoutesFrom(__DIR__.'/../routes/admin.php'); - - - // Cargar vistas del paquete - $this->loadViewsFrom(__DIR__.'/../resources/views', 'vuexy-website-admin'); - - - // Register the migrations - $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); - - // Registrar comandos de consola - if ($this->app->runningInConsole()) { - $this->commands([ - SitemapGenerate::class, - ]); - } - - - // Registrar Livewire Components - $components = [ - // 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); - } - - } -} diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..9ed1226 --- /dev/null +++ b/README.en.md @@ -0,0 +1,118 @@ +# 🧩 Laravel Vuexy Website Admin + +<p align="center"> + <a href="https://koneko.mx" target="_blank"> + <img src="https://git.koneko.mx/Koneko-ST/koneko-st/raw/branch/main/logo-images/horizontal-05.png" width="400" alt="Koneko Soluciones Tecnológicas Logo"> + </a> +</p> + +<p align="center"> + <a href="https://koneko.mx"><img src="https://img.shields.io/badge/Website-koneko.mx-blue" alt="Website"></a> + <a href="https://packagist.org/packages/koneko/laravel-vuexy-website-admin"><img src="https://img.shields.io/packagist/v/koneko/laravel-vuexy-website-admin" alt="Latest Version"></a> + <a href="https://packagist.org/packages/koneko/laravel-vuexy-website-admin"><img src="https://img.shields.io/packagist/l/koneko/laravel-vuexy-website-admin" alt="License"></a> + <a href="https://github.com/koneko-mx/laravel-vuexy-website-admin"><img src="https://img.shields.io/github/issues/koneko-mx/laravel-vuexy-website-admin" alt="Issues"></a> +</p> + +--- + +## 📌 Description + +**Laravel Vuexy Website Admin** is a core plugin in the **Koneko ERP** ecosystem. Built on Laravel 11 and integrated with the official `laravel-vuexy-admin` system, this package provides a powerful content management interface for corporate and eCommerce websites. + +It enables complete control over site configuration, SEO optimization, domain management, multilingual support, and template rendering. The module follows modern coding standards and is compatible with Redis, PostgreSQL, and multi-site deployments. + +--- + +### 📦 Main Features + +* Multi-site and multi-domain support +* Site-level settings and branding configuration +* SEO profile management with OpenGraph, Twitter Cards, JSON-LD, and canonical settings +* Sitemap, robots.txt, and manifest.json integration +* CMS with dynamic blocks, reusable menus, and content versioning +* Full page and block-based caching system +* Template selector and preview system +* Blog module with categories, tags, articles, and comment moderation +* API integration for Google, Meta, WhatsApp, Tawk.to, and more +* Translation and localization tools (DeepL, Google Translate) + +--- + +### 🚀 Quick Installation + +```bash +composer require koneko/laravel-vuexy-website-admin +php artisan migrate --seed +php artisan vendor:publish --tag=vuexy-website-admin-config +``` + +> You must have `laravel-vuexy-admin` installed before using this plugin. + +--- + +### 📦 Included Commands + +```bash +php artisan website:seo-helper +php artisan website:menu-helper +php artisan website:content-helper +php artisan website:cache-helper +php artisan website:sitemap-generate +``` +--- + +### ⚙️ Publicación de archivos + +This package publishes: + +* Routes, views and Livewire components +* Configuration and permissions files (RBAC) +* Artisan commands and content generators +* Menu and modular system extensions + +```bash +php artisan vendor:publish --tag=vuexy-website-admin-config +``` + +--- + +### 🔧 Uso y personalización + +This module allows you to customize its structure and behavior using: + +* Middlewares for web and content context +* Multisite configuration with support for dynamic templates +* Decoupled view system compatible with Blade, Vite and partial rendering + +You can extend or override any published view, layout or Livewire component. + +--- + +### 🛠️ Requirements + +* PHP `^8.2` +* Laravel `^11.31` +* [koneko/laravel-vuexy-admin](https://github.com/koneko-mx/laravel-vuexy-admin) install and configured + +--- + +## 📄 License + +This package is licensed under the [custom Business Source License 1.1](LICENSE), transitioning to MIT after 3 years. For commercial usage, redistribution, or extended usage, please contact: + +📧 [opensource@koneko.mx](mailto:opensource@koneko.mx) + +--- + +## 📚 More Information + +* [Vuexy Admin Core](https://github.com/koneko-mx/laravel-vuexy-admin) +* [Documentation in Spanish](README.md) +* [Koneko ST Official Website](https://koneko.mx) +* [Contact Email](mailto:opensource@koneko.mx) + +--- + +<p align="center"> + Made with ❤️ in Mexico by <a href="https://koneko.mx">Koneko Soluciones Tecnológicas</a> +</p> diff --git a/README.md b/README.md index ba91b9f..26af72f 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,116 @@ -# 🎨 Laravel Vuexy Website Admin - Vuexy Admin +# 🧩 Laravel Vuexy Website Admin <p align="center"> - <a href="https://koneko.mx" target="_blank"> <img src="https://git.koneko.mx/Koneko-ST/koneko-st/raw/branch/main/logo-images/horizontal-05.png" width="400" alt="Koneko Soluciones Tecnológicas Logo"> </a> + <a href="https://koneko.mx" target="_blank"> + <img src="https://git.koneko.mx/Koneko-ST/koneko-st/raw/branch/main/logo-images/horizontal-05.png" width="400" alt="Koneko Soluciones Tecnológicas Logo"> + </a> </p> + <p align="center"> - <a href="https://koneko.mx"><img src="https://img.shields.io/badge/Website-koneko.mx-blue" alt="Sitio Web"></a> - <a href="https://packagist.org/packages/koneko/laravel-vuexy-website-admin"><img src="https://img.shields.io/packagist/v/koneko/laravel-vuexy-website-admin" alt="Latest Stable Version"></a> - <a href="https://packagist.org/packages/koneko/laravel-vuexy-website-admin"><img src="https://img.shields.io/packagist/l/koneko/laravel-vuexy-website-admin" alt="License"></a> - <a href="https://git.koneko.mx/koneko"><img src="https://img.shields.io/badge/Git%20Server-Koneko%20Git-orange" alt="Servidor Git"></a> - <a href="https://github.com/koneko-mx/laravel-vuexy-website-admin/actions/workflows/tests.yml"><img src="https://github.com/koneko-mx/laravel-vuexy-website-admin/actions/workflows/tests.yml/badge.svg" alt="Build Status"></a> - <a href="https://github.com/koneko-mx/laravel-vuexy-website-admin/issues"><img src="https://img.shields.io/github/issues/koneko/laravel-vuexy-website-admin" alt="Issues"></a> + <a href="https://koneko.mx"><img src="https://img.shields.io/badge/Sitio%20Web-koneko.mx-blue" alt="Sitio Web"></a> + <a href="https://packagist.org/packages/koneko/laravel-vuexy-website-admin"><img src="https://img.shields.io/packagist/v/koneko/laravel-vuexy-website-admin" alt="Versión estable"></a> + <a href="https://packagist.org/packages/koneko/laravel-vuexy-website-admin"><img src="https://img.shields.io/packagist/l/koneko/laravel-vuexy-website-admin" alt="Licencia"></a> + <a href="https://github.com/koneko-mx/laravel-vuexy-website-admin"><img src="https://img.shields.io/github/issues/koneko-mx/laravel-vuexy-website-admin" alt="Issues"></a> </p> --- ## 📌 Descripción -**Laravel Vuexy Website Admin** es un módulo diseñado para **Laravel Vuexy Admin**, proporcionando [breve descripción de la funcionalidad]. +**Laravel Vuexy Website Admin** es un módulo del ecosistema **Koneko ERP** desarrollado en Laravel 11, orientado a la administración de contenido web multisitio, multidioma y multitemplate. Forma parte del stack web profesional de Koneko, integrando funcionalidades CMS, SEO, blog, renderización por bloques, cache HTML y plantillas dinámicas. -### ✨ Características: -- 🔹 Integración completa con Vuexy Admin. -- 🔹 Funcionalidad clave 1. -- 🔹 Funcionalidad clave 2. +Está diseñado para integrarse de forma transparente al backend Vuexy Admin y utilizarse con frontends basados en Vite y plantillas como **Porto, Landwind, Notus** y otras. --- -## 📦 Instalación +### 📦 Características -Instalar vía **Composer**: +* Gestión de múltiples sitios web y dominios +* Configuración contextual del sitio (branding, idioma, indexación, manifest.json, etc.) +* Editor de contenido por bloques y plantillas Blade +* Administración de menús, páginas, SEO y JSON-LD +* Blog con artículos, etiquetas, categorías y comentarios +* Sistema de caché de contenido completo y por bloque +* Integraciones con APIs externas como Google Analytics, Pixel Meta, Tawk.to, etc. + +--- + +### 🚀 Instalación rápida ```bash composer require koneko/laravel-vuexy-website-admin -``` - -Publicar archivos de configuración y migraciones (si aplica): - -```bash -php artisan vendor:publish --tag=laravel-vuexy-website-admin-config -php artisan migrate -``` - ---- - -## 🚀 Uso básico - -```php -use Koneko\NombreLibreria\Models\Model; - -$model = Model::create([ - 'campo1' => 'Valor', - 'campo2' => 'Otro valor', -]); -``` - ---- - -## 📚 Configuración adicional - -Si necesitas personalizar la configuración del módulo, publica el archivo de configuración: - -```bash -php artisan vendor:publish --tag=laravel-vuexy-website-admin-config -``` - -Esto generará `config/nombre_libreria.php`, donde puedes modificar valores predeterminados. - ---- - -## 🛠 Dependencias - -Este paquete requiere las siguientes dependencias: -- Laravel 11 -- `koneko/laravel-vuexy-website-admin` -- Dependencias específicas de la librería - ---- - -## 📦 Publicación de Assets y Configuraciones - -Para publicar configuraciones y seeders: - -```bash -php artisan vendor:publish --tag=laravel-vuexy-website-admin-config -php artisan vendor:publish --tag=laravel-vuexy-website-admin-seeders php artisan migrate --seed +php artisan vendor:publish --tag=vuexy-website-admin-config ``` -Para publicar imágenes del tema: +> Debes tener instalado `laravel-vuexy-admin` antes de usar este complemento. + +--- + +### 📦 Comandos Incluidos ```bash -php artisan vendor:publish --tag=laravel-vuexy-website-admin-images +php artisan website:seo-helper +php artisan website:menu-helper +php artisan website:content-helper +php artisan website:cache-helper +php artisan website:sitemap-generate ``` --- -## 🛠 Pruebas +### ⚙️ Publicación de archivos -Ejecuta los tests con: +Este paquete publica: + +* Rutas, vistas y Livewire components +* Archivos de configuración y permisos (RBAC) +* Comandos Artisan y generadores de contenido +* Extensiones para menú y sistema modular ```bash -php artisan test +php artisan vendor:publish --tag=vuexy-website-admin-config ``` --- -## 🌍 Repositorio Principal y Sincronización +### 🔧 Uso y personalización -Este repositorio es una **copia sincronizada** del repositorio principal alojado en **[Tea - Koneko Git](https://git.koneko.mx/koneko/laravel-vuexy-website-admin)**. +Este módulo permite personalizar su estructura y comportamiento utilizando: -### 🔄 Sincronización con GitHub -- **Repositorio Principal:** [git.koneko.mx](https://git.koneko.mx/koneko/laravel-vuexy-website-admin) -- **Repositorio en GitHub:** [github.com/koneko/laravel-vuexy-website-admin](https://github.com/koneko/laravel-vuexy-website-admin) -- **Los cambios pueden reflejarse primero en Tea antes de GitHub.** +* Middlewares para contexto web y contenido +* Configuración multisitio con soporte para templates dinámicos +* Sistema de vistas desacoplado compatible con Blade, Vite y renderizado parcial -### 🤝 Contribuciones -Si deseas contribuir: -1. Puedes abrir un **Issue** en [GitHub Issues](https://github.com/koneko/laravel-vuexy-website-admin/issues). -2. Para Pull Requests, **preferimos contribuciones en Tea**. Contacta a `admin@koneko.mx` para solicitar acceso. - -⚠️ **Nota:** Algunos cambios pueden tardar en reflejarse en GitHub, ya que este repositorio se actualiza automáticamente desde Tea. +Puedes extender o sobreescribir cualquier vista, layout o componente Livewire publicado. --- -## 🏅 Licencia +## 🛠️ Requisitos -Este paquete es de código abierto bajo la licencia [MIT](LICENSE). +* PHP `^8.2` +* Laravel `^11.31` +* [koneko/laravel-vuexy-admin](https://github.com/koneko-mx/laravel-vuexy-admin) instalado y configurado + +--- + +## 📄 Licencia + +Este paquete se distribuye bajo la [Licencia Business Source 1.1 personalizada](LICENSE.es), con transición automática a MIT a los 3 años. Para uso comercial, redistribución o integraciones ampliadas, contacta a: + +📧 [opensource@koneko.mx](mailto:opensource@koneko.mx) + +--- + +## 📚 Más Información + +* [Core Vuexy Admin](https://github.com/koneko-mx/laravel-vuexy-admin) +* [Documentación en inglés](README.en.md) +* [Sitio Oficial Koneko ST](https://koneko.mx) +* [Correo de Contacto](mailto:opensource@koneko.mx) --- <p align="center"> - Hecho con ❤️ por <a href="https://koneko.mx">Koneko Soluciones Tecnológicas</a> + Hecho con ❤️ en México por <a href="https://koneko.mx">Koneko Soluciones Tecnológicas</a> </p> diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e733e2a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# 🛡️ Política de Seguridad + +Koneko Soluciones Tecnológicas se toma muy en serio la seguridad de este proyecto y de sus usuarios. + +## 🔒 Reporte responsable de vulnerabilidades + +Si encuentras alguna vulnerabilidad de seguridad, **no abras un issue público**. En lugar de ello, por favor repórtala de forma privada por cualquiera de los siguientes medios: + +- 📧 Correo oficial: [opensource@koneko.mx](mailto:opensource@koneko.mx) + +Por favor incluye los siguientes detalles: + +- Descripción del problema +- Versión afectada del paquete +- Instrucciones claras para reproducir el fallo (si es posible) + +Nos comprometemos a responder dentro de **5 días hábiles**. + +## 🛡️ Alcance + +Este procedimiento aplica únicamente al paquete: + +- `koneko/laravel-vuexy-admin` +Y a sus repositorios relacionados dentro del ecosistema oficial Koneko. + +## ✅ Reconocimientos + +Se podrá otorgar reconocimiento público (con tu permiso) en futuras notas de versión a quienes reporten vulnerabilidades de manera responsable. + +Gracias por ayudar a hacer más seguro el ecosistema de Koneko ERP. diff --git a/composer.json b/composer.json index 0253bf3..201ee1f 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,20 @@ { "name": "koneko/laravel-vuexy-website-admin", - "description": "Laravel Vuexy Website Admin, un modulo de administracion de sitios web.", - "keywords": ["laravel", "koneko", "framework", "vuexy", "website", "admin", "mexico"], + "description": "Web content management module for Laravel projects, integrated with Vuexy Admin and optimized for professional sites in Mexico.", + "keywords": [ + "laravel", "cms", "website-admin", "vuexy", "koneko", "mexico", "admin-panel", + "seo", "gestión de contenidos", "multi-site", "erp" + ], "type": "library", - "license": "MIT", + "license": "BUSL-1.1-custom", "require": { "php": "^8.2", - "koneko/laravel-vuexy-admin": "dev-main", + "koneko/laravel-vuexy-admin": "dev-develop", "laravel/framework": "^11.31" }, "autoload": { "psr-4": { - "Koneko\\VuexyWebsiteAdmin\\": "./" + "Koneko\\VuexyWebsiteAdmin\\": "src/" } }, "extra": { @@ -31,5 +34,6 @@ "source": "https://github.com/koneko-mx/laravel-vuexy-website-admin", "issues": "https://github.com/koneko-mx/laravel-vuexy-website-admin/issues" }, + "minimum-stability": "dev", "prefer-stable": true } diff --git a/config/koneko-website.php b/config/koneko-website.php new file mode 100644 index 0000000..e4046ff --- /dev/null +++ b/config/koneko-website.php @@ -0,0 +1,28 @@ +<?php +// Variables +return [ + // ================== 📦 CACHE DE COMPONENTE ================== + 'cache' => [ + 'enabled' => (bool) env('KONEKO_WEBSITE_CACHE_ENABLED', true), + 'ttl' => (int) env('KONEKO_WEBSITE_CACHE_TTL', 20 * 24 * 60), + ], + 'menu' => [ + 'cache' => [ + 'enabled' => (bool) env('VUEXY_WEBSITE_MENU_CACHE_ENABLED', true), + 'ttl' => (int) env('VUEXY_WEBSITE_MENU_CACHE_TTL', 3600), + ], + 'debug' => [ + 'show_broken_routes' => (bool) env('VUEXY_WEBSITE_MENU_DEBUG_SHOW_BROKEN_ROUTES', false), + 'show_disallowed_links' => (bool) env('VUEXY_WEBSITE_MENU_DEBUG_SHOW_DISALLOWED_LINKS', false), + ], + ], + 'html' => [ + 'cache' => [ + 'enabled' => (bool) env('VUEXY_WEBSITE_HTML_CACHE_ENABLED', true), + 'ttl' => (int) env('VUEXY_WEBSITE_HTML_CACHE_TTL', 900), + ], + 'debug' => [ + 'mode' => (bool) env('VUEXY_WEBSITE_HTML_DEBUG_MODE', true), + ] + ] +]; diff --git a/config/vuexy_apis_catalog.php.back b/config/vuexy_apis_catalog.php.back new file mode 100644 index 0000000..d43bf91 --- /dev/null +++ b/config/vuexy_apis_catalog.php.back @@ -0,0 +1,108 @@ +<?php + +// Este archivo **NO se registra como config**, es usado por + +return [ + + // ======================= GOOGLE ======================= + + 'google_analytics' => [ + 'name' => 'Google Analytics', + 'slug' => 'google_analytics', + 'provider' => ApiProvider::Google, + 'auth_type' => ApiAuthType::OAuth2, + 'environment' => ApiEnvironment::Production, + 'base_url' => 'https://analytics.googleapis.com', + 'scopes' => ['https://www.googleapis.com/auth/analytics.readonly'], + 'credentials' => [ + 'client_id' => env('GOOGLE_ANALYTICS_CLIENT_ID'), + 'client_secret' => env('GOOGLE_ANALYTICS_CLIENT_SECRET'), + 'redirect_uri' => env('GOOGLE_ANALYTICS_REDIRECT_URI', url('/oauth2/callback')), + ], + ], + + 'google_translate' => [ + 'name' => 'Google Translate', + 'slug' => 'google_translate', + 'provider' => ApiProvider::Google, + 'auth_type' => ApiAuthType::ApiKey, + 'environment' => ApiEnvironment::Production, + 'base_url' => 'https://translation.googleapis.com/language/translate/v2', + 'credentials' => [ + 'api_key' => env('GOOGLE_TRANSLATE_API_KEY'), + ], + ], + + 'google_search_console' => [ + 'name' => 'Google Search Console', + 'slug' => 'google_search_console', + 'provider' => ApiProvider::Google, + 'auth_type' => ApiAuthType::OAuth2, + 'environment' => ApiEnvironment::Production, + 'base_url' => 'https://searchconsole.googleapis.com', + 'scopes' => ['https://www.googleapis.com/auth/webmasters.readonly'], + 'credentials' => [ + 'client_id' => env('GOOGLE_SEARCH_CLIENT_ID'), + 'client_secret' => env('GOOGLE_SEARCH_CLIENT_SECRET'), + 'redirect_uri' => env('GOOGLE_SEARCH_REDIRECT_URI'), + ], + ], + + 'google_tag_manager' => [ + 'name' => 'Google Tag Manager', + 'slug' => 'google_tag_manager', + 'provider' => ApiProvider::Google, + 'auth_type' => ApiAuthType::OAuth2, + 'environment' => ApiEnvironment::Production, + 'base_url' => 'https://www.googleapis.com/tagmanager/v2', + 'scopes' => ['https://www.googleapis.com/auth/tagmanager.readonly'], + 'credentials' => [ + 'client_id' => env('GOOGLE_TAG_CLIENT_ID'), + 'client_secret' => env('GOOGLE_TAG_CLIENT_SECRET'), + 'redirect_uri' => env('GOOGLE_TAG_REDIRECT_URI'), + ], + ], + + // ======================= META / FACEBOOK ======================= + + 'facebook_messenger' => [ + 'name' => 'Facebook Messenger', + 'slug' => 'facebook_messenger', + 'provider' => ApiProvider::Facebook, + 'auth_type' => ApiAuthType::OAuth2, + 'environment' => ApiEnvironment::Production, + 'base_url' => 'https://graph.facebook.com/v17.0', + 'scopes' => ['pages_messaging', 'pages_show_list'], + 'credentials' => [ + 'app_id' => env('FACEBOOK_APP_ID'), + 'app_secret' => env('FACEBOOK_APP_SECRET'), + 'redirect_uri' => env('FACEBOOK_REDIRECT_URI'), + ], + ], + + // ======================= X / TWITTER ======================= + + 'twitter_api' => [ + 'name' => 'Twitter API v2', + 'slug' => 'twitter_api', + 'provider' => ApiProvider::Twitter, + 'auth_type' => ApiAuthType::BearerToken, + 'environment' => ApiEnvironment::Production, + 'base_url' => 'https://api.twitter.com/2', + 'credentials' => [ + 'bearer_token' => env('TWITTER_BEARER_TOKEN'), + ], + ], + + // ======================= TAWK.TO ======================= + + 'tawk_to' => [ + 'name' => 'Tawk.to Live Chat', + 'slug' => 'tawk_to', + 'provider' => ApiProvider::Custom, + 'auth_type' => ApiAuthType::None, + 'environment' => ApiEnvironment::Production, + 'base_url' => 'https://embed.tawk.to', + 'credentials' => [], + ], +]; diff --git a/config/vuexy_website_admin_menu copy.php b/config/vuexy_website_admin_menu copy.php new file mode 100644 index 0000000..5cfa12b --- /dev/null +++ b/config/vuexy_website_admin_menu copy.php @@ -0,0 +1,339 @@ +<?php + +declare(strict_types=1); + +// Este archivo **NO se registra como config**, es usado por VuexyMenuRegistry + +return [ + 'Web & SEO' => [ + '_meta' => [ + 'icon' => 'ti ti-settings', + 'description' => 'Administra la configuración, contenido, integraciones y visibilidad de tu sitio web empresarial.', + 'widget_label' => 'Sitio Web y SEO', + 'priority' => 200, + ], + 'submenu' => [ + 'Configuración general' => [ + '_meta' => [ + 'icon' => 'ti ti-settings', + 'description' => 'Ajustes generales del sitio, redes sociales y configuración de indexado.', + 'widget_label' => 'Configuración del Sitio Web', + 'home_at_root' => true, + 'priority' => 100, + ], + 'submenu' => [ + 'Ajustes generales' => [ + 'icon' => 'ti ti-settings', + 'route' => 'admin.website-admin.settings.general.index', + 'can' => 'admin.website-admin.settings.general.view', + 'description' => 'Personaliza el título, favicon, y otros aspectos básicos del sitio.', + ], + 'Enlaces sociales' => [ + 'icon' => 'ti ti-share', + 'route' => 'admin.website-admin.settings.social.index', + 'can' => 'admin.website-admin.settings.social.view', + 'description' => 'Administra los enlaces y metadatos de redes sociales.', + ], + 'Visibilidad en buscadores' => [ + 'icon' => 'ti ti-search', + 'route' => 'admin.website-admin.settings.indexing.index', + 'can' => 'admin.website-admin.settings.indexing.view', + 'description' => 'Controla el indexado del sitio web por los motores de búsqueda.', + ], + ] + ], + 'Contacto' => [ + '_meta' => [ + 'icon' => 'ti ti-device-mobile-question', + 'description' => 'Información y formularios de contacto del sitio.', + 'widget_label' => 'Información de contacto', + 'home_at_root' => true, + 'priority' => 200, + ], + 'submenu' => [ + 'Información de contacto' => [ + 'icon' => 'ti ti-device-mobile-message', + 'route' => 'admin.website-admin.contact.info.index', + 'can' => 'admin.website-admin.contact.info.view', + 'description' => 'Dirección, teléfonos, correos y ubicación.', + ], + 'Formulario de contacto' => [ + 'icon' => 'ti ti-mail-cog', + 'route' => 'admin.website-admin.contact.form.index', + 'can' => 'admin.website-admin.contact.form.view', + 'description' => 'Configuración y campos del formulario de contacto.', + ], + ] + ], + 'Chat & Comunicación' => [ + '_meta' => [ + 'icon' => 'ti ti-message-dots', + 'description' => 'Soporte al cliente y canales de comunicación directa.', + 'widget_label' => 'Chat & Comunicación de Sitio Web', + 'home_at_root' => true, + 'priority' => 500, + ], + 'submenu' => [ + 'Facebook Messenger' => [ + 'icon' => 'ti ti-brand-messenger', + 'route' => 'admin.website-admin.comunication.messenger.index', + 'can' => 'admin.website-admin.comunication.messenger.view', + 'description' => 'Activa el chat de Messenger en tu sitio.', + ], + 'Whatsapp Chat' => [ + 'icon' => 'ti ti-brand-whatsapp', + 'route' => 'admin.website-admin.comunication.whatsapp.index', + 'can' => 'admin.website-admin.comunication.whatsapp.view', + 'description' => 'Integra un botón de chat directo con WhatsApp.', + ], + 'Tawk.to' => [ + 'icon' => 'ti ti-message-dots', + 'route' => 'admin.website-admin.comunication.tawk-to.index', + 'can' => 'admin.website-admin.comunication.tawk-to.view', + 'description' => 'Agrega soporte en vivo con Tawk.to.', + ], + 'Twitter API' => [ + 'icon' => 'ti ti-brand-x', + 'route' => 'admin.website-admin.comunication.twitter.index', + 'can' => 'admin.website-admin.comunication.twitter.view', + 'description' => 'Configura la integración con Twitter/X.', + ], + ] + ], + 'CMS - Gestor de Contenidos' => [ + '_meta' => [ + 'icon' => 'ti ti-layout-grid-add', + 'description' => 'Crea, edita y publica contenido dinámico basado en Blade para tu sitio web.', + 'widget_label' => 'Administrador de Contenidos CMS', + 'priority' => 250, + ], + 'submenu' => [ + 'Menús del sitio' => [ + 'icon' => 'ti ti-hierarchy-3', + 'route' => 'admin.website-admin.cms.menus.index', + 'can' => 'admin.website-admin.cms.menus.view', + 'description' => 'Administra la estructura de navegación y menús dinámicos.', + ], + 'Perfil SEO' => [ + 'icon' => 'ti ti-settings', + 'route' => 'admin.website-admin.cms.seo.index', + 'can' => 'admin.website-admin.cms.seo.view', + 'description' => 'Configura las metas y parámetros SEO del sitio.', + ], + 'Contenidos dinámicos' => [ + 'icon' => 'ti ti-file-text', + 'route' => 'admin.website-admin.cms.contents.index', + 'can' => 'admin.website-admin.cms.contents.view', + 'description' => 'Administra páginas, bloques y parciales Blade de tu sitio.', + ], + 'Versiones de contenido' => [ + 'icon' => 'ti ti-clock-edit', + 'route' => 'admin.website-admin.cms.versions.index', + 'can' => 'admin.website-admin.cms.versions.view', + 'description' => 'Gestiona las versiones de contenido y restauraciones.', + ], + 'Plantillas disponibles' => [ + 'icon' => 'ti ti-template', + 'route' => 'admin.website-admin.cms.templates.index', + 'can' => 'admin.website-admin.cms.templates.view', + 'description' => 'Define y organiza los templates base disponibles.', + ], + ], + ], + 'Cache' => [ + '_meta' => [ + 'icon' => 'ti ti-cpu', + 'description' => 'Administración y limpieza de caché para mejorar el rendimiento.', + 'widget_label' => 'Cache de Sitio Web', + 'home_at_root' => true, + 'priority' => 900, + ], + 'submenu' => [ + 'Cache HTML renderizado' => [ + 'icon' => 'ti ti-file-type-html', + 'description' => 'Visualiza y limpia la caché de HTML completo del sitio web.', + 'route' => 'admin.website-admin.cache.fullpage.index', + 'can' => 'admin.website-admin.cache.fullpage.view', + ], + 'Previsualizaciones firmadas' => [ + 'icon' => 'ti ti-lock-access', + 'description' => 'URLs de vista previa y control de firma temporal.', + 'route' => 'admin.website-admin.cache.signed-previews.index', + 'can' => 'admin.website-admin.cache.signed-previews.view', + ], + ] + ], + 'Traducciones e internacional' => [ + '_meta' => [ + 'icon' => 'ti ti-language', + 'description' => 'Herramientas para traducción automática del sitio.', + 'widget_label' => 'Herramientas de traducción de Sitio Web', + 'home_at_root' => true, + 'priority' => 500, + ], + 'submenu' => [ + 'Google Translate' => [ + 'icon' => 'ti ti-language', + 'route' => 'admin.website-admin.translate.google.index', + 'can' => 'admin.website-admin.translate.google.view', + 'description' => 'Activa la traducción automática con Google Translate.', + ], + ] + ], + 'Contenido' => [ + '_meta' => [ + 'icon' => 'ti ti-hierarchy', + 'description' => 'Maneja contenido informativo y visual.', + 'widget_label' => 'Contenidos del Sitio Web', + 'home_at_root' => true, + 'priority' => 400, + ], + 'submenu' => [ + 'Preguntas frecuentes' => [ + 'icon' => 'ti ti-bubble-text', + 'route' => 'admin.website-admin.content.faq.index', + 'can' => 'admin.website-admin.content.faq.view', + 'description' => 'Administra las preguntas frecuentes del sitio.', + ], + 'Galería de imágenes' => [ + 'icon' => 'ti ti-photo', + 'route' => 'admin.website-admin.content.gallery.index', + 'can' => 'admin.website-admin.content.gallery.view', + 'description' => 'Agrega y organiza tus imágenes.', + ], + 'Avisos legales' => [ + 'icon' => 'ti ti-file-text-shield', + 'route' => 'admin.website-admin.content.legal.index', + 'can' => 'admin.website-admin.content.legal.view', + 'description' => 'Documentos como Términos, Aviso de Privacidad, etc.', + ], + ] + ], + 'Analítica y seguimiento' => [ + '_meta' => [ + 'icon' => 'ti ti-device-analytics', + 'description' => 'Conecta tu sitio con herramientas de análisis y tracking.', + 'widget_label' => 'Integraciones de analítica y seguimiento', + 'home_at_root' => true, + 'priority' => 300, + ], + 'submenu' => [ + 'Google Analytics' => [ + 'icon' => 'ti ti-chart-scatter-3d', + 'route' => 'admin.website-admin.analytics.google-analytics.index', + 'can' => 'admin.website-admin.analytics.google-analytics.view', + 'description' => 'Integra tu cuenta de Google Analytics.', + ], + 'Google Tags' => [ + 'icon' => 'ti ti-tags', + 'route' => 'admin.website-admin.analytics.google-tags.index', + 'can' => 'admin.website-admin.analytics.google-tags.view', + 'description' => 'Administra etiquetas de Google Tag Manager.', + ], + 'Google Search Console' => [ + 'icon' => 'ti ti-search', + 'route' => 'admin.website-admin.analytics.google-search-console.index', + 'can' => 'admin.website-admin.analytics.google-search-console.view', + 'description' => 'Verifica y gestiona tu sitio con Search Console.', + ], + 'Pixel Meta' => [ + 'icon' => 'ti ti-device-analytics', + 'route' => 'admin.website-admin.analytics.pixel-meta.index', + 'can' => 'admin.website-admin.analytics.pixel-meta.view', + 'description' => 'Integra el pixel de Meta (Facebook Ads).', + ], + ] + ], + 'Herramientas SEO' => [ + '_meta' => [ + 'icon' => 'ti ti-code-dots', + 'description' => 'Utilidades para mejorar la visibilidad en buscadores.', + 'widget_label' => 'Herramientas SEO y Metadatos', + 'home_at_root' => true, + 'priority' => 500, + ], + 'submenu' => [ + 'Mapa del sitio' => [ + 'icon' => 'ti ti-hierarchy', + 'route' => 'admin.website-admin.seo.sitemap.index', + 'can' => 'admin.website-admin.seo.sitemap.view', + 'description' => 'Genera y publica el sitemap.xml.', + ], + 'Google JSON-LD' => [ + 'icon' => 'ti ti-code-dots', + 'route' => 'admin.website-admin.seo.jsonld.index', + 'can' => 'admin.website-admin.seo.jsonld.view', + 'description' => 'Configura el esquema estructurado para Google.', + ], + 'Robots.txt' => [ + 'icon' => 'ti ti-code', + 'route' => 'admin.website-admin.seo.robots.index', + 'can' => 'admin.website-admin.seo.robots.view', + 'description' => 'Controla qué partes del sitio pueden ser indexadas.', + ], + 'manifest.json' => [ + 'icon' => 'ti ti-file-code', + 'route' => 'admin.website-admin.seo.manifest.index', + 'can' => 'admin.website-admin.seo.manifest.view', + 'description' => 'Configura la compatibilidad como PWA.', + ], + 'Canonical URLs' => [ + 'icon' => 'ti ti-link', + 'route' => 'admin.website-admin.seo.canonical.index', + 'can' => 'admin.website-admin.seo.canonical.view', + 'description' => 'Evita duplicidad de URLs con etiquetas canónicas.', + ], + 'Preview Social Cards' => [ + 'icon' => 'ti ti-brand-facebook', + 'route' => 'admin.website-admin.seo.social-cards.index', + 'can' => 'admin.website-admin.seo.social-cards.view', + 'description' => 'Define títulos, imágenes y previews sociales.', + ], + ] + ], + ] + ], + 'Blog' => [ + '_meta' => [ + 'icon' => 'ti ti-news', + 'description' => 'Publica, edita y organiza artículos, categorías y comentarios de tu blog.', + 'after_to' => 'Web & SEO', + ], + 'submenu' => [ + 'Categorias' => [ + 'icon' => 'ti ti-category', + 'route' => 'admin.website-admin.blog.categories.index', + 'can' => 'admin.website-admin.blog.categories.view', + ], + 'Etiquetas' => [ + 'icon' => 'ti ti-tags', + 'route' => 'admin.website-admin.blog.tags.index', + 'can' => 'admin.website-admin.blog.tags.view', + ], + 'Articulos' => [ + 'icon' => 'ti ti-news', + 'route' => 'admin.website-admin.blog.articles.index', + 'can' => 'admin.website-admin.blog.articles.view', + ], + 'Comentarios' => [ + 'icon' => 'ti ti-message', + 'route' => 'admin.website-admin.blog.comments.index', + 'can' => 'admin.website-admin.blog.comments.view', + ], + ] + ], + 'Plantillas' => [ + '_meta' => [ + 'icon' => 'ti ti-template', + 'description' => 'Gestiona las plantillas disponibles para tu sitio web.', + 'after_to' => 'Blog', + ], + 'submenu' => [ + 'Plantillas' => [ + 'icon' => 'ti ti-template', + 'route' => 'admin.website-admin.templates.index', + 'can' => 'admin.website-admin.templates.view', + ], + ] + ], +]; diff --git a/config/vuexy_website_admin_menu.php b/config/vuexy_website_admin_menu.php new file mode 100644 index 0000000..d00d9fe --- /dev/null +++ b/config/vuexy_website_admin_menu.php @@ -0,0 +1,218 @@ +<?php + +declare(strict_types=1); + +// Este archivo **NO se registra como config**, es usado por VuexyMenuRegistry + +return [ + 'Sitios Web' => [ + '_meta' => [ + 'icon' => 'ti ti-world', + 'description' => 'Administra múltiples sitios, dominios, plantillas y configuración global.', + 'widget_label' => 'Sitios Web y Dominios', + 'home_at_root' => true, + 'priority' => 100, + ], + 'submenu' => [ + 'Todos los sitios' => [ + 'icon' => 'ti ti-world', + 'route' => 'admin.website-admin.sites.index', + 'can' => 'admin.website-admin.sites.view', + 'description' => 'Listado general de sitios activos y configurables.', + ], + 'Crear nuevo sitio' => [ + 'icon' => 'ti ti-world-plus', + 'route' => 'admin.website-admin.sites.create', + 'can' => 'admin.website-admin.sites.create', + 'description' => 'Inicia un nuevo sitio web desde cero.', + ], + ], + ], + 'Configuración del Sitio' => [ + '_meta' => [ + 'icon' => 'ti ti-settings-cog', + 'description' => 'Ajustes, branding, visibilidad y plantilla del sitio actual.', + 'widget_label' => 'Configuración de Sitio Web', + 'home_at_root' => true, + 'priority' => 150, + ], + 'submenu' => [ + 'General y Branding' => [ + 'icon' => 'ti ti-tools', + 'route' => 'admin.website-admin.settings.general.index', + 'can' => 'admin.website-admin.settings.general.view', + 'description' => 'Nombre, idioma, favicon, y plantilla activa.', + ], + 'Indexación y Robots' => [ + 'icon' => 'ti ti-search', + 'route' => 'admin.website-admin.settings.indexing.index', + 'can' => 'admin.website-admin.settings.indexing.view', + 'description' => 'Controla el acceso de motores de búsqueda a tu sitio.', + ], + 'Canonical y manifest.json' => [ + 'icon' => 'ti ti-file-code', + 'route' => 'admin.website-admin.settings.canonical.index', + 'can' => 'admin.website-admin.settings.canonical.view', + 'description' => 'Evita duplicados y mejora la visibilidad PWA.', + ], + ], + ], + 'SEO y Metadatos' => [ + '_meta' => [ + 'icon' => 'ti ti-zoom-code', + 'description' => 'Herramientas de optimización SEO, metadatos y JSON-LD.', + 'widget_label' => 'SEO & Metadatos', + 'home_at_root' => true, + 'priority' => 200, + ], + 'submenu' => [ + 'Perfil SEO' => [ + 'icon' => 'ti ti-graph', + 'route' => 'admin.website-admin.seo.profile.index', + 'can' => 'admin.website-admin.seo.profile.view', + 'description' => 'Define metadatos, OG y Twitter Cards del sitio.', + ], + 'JSON-LD y Schema.org' => [ + 'icon' => 'ti ti-code-dots', + 'route' => 'admin.website-admin.seo.jsonld.index', + 'can' => 'admin.website-admin.seo.jsonld.view', + 'description' => 'Configura estructuras para Google y otros buscadores.', + ], + 'Mapa del sitio y Robots' => [ + 'icon' => 'ti ti-hierarchy', + 'route' => 'admin.website-admin.seo.sitemap.index', + 'can' => 'admin.website-admin.seo.sitemap.view', + 'description' => 'Controla el sitemap.xml y robots.txt globalmente.', + ], + ], + ], + 'CMS Koneko' => [ + '_meta' => [ + 'icon' => 'ti ti-layout-dashboard', + 'description' => 'Gestión de contenido visual, bloques, menús y plantillas.', + 'widget_label' => 'Editor de Contenido CMS', + 'home_at_root' => true, + 'priority' => 300, + ], + 'submenu' => [ + 'Páginas y bloques' => [ + 'icon' => 'ti ti-file-text', + 'route' => 'admin.website-admin.cms.contents.index', + 'can' => 'admin.website-admin.cms.contents.view', + 'description' => 'Administra contenido dinámico estructurado por bloques.', + ], + 'Menús del sitio' => [ + 'icon' => 'ti ti-hierarchy-3', + 'route' => 'admin.website-admin.cms.menus.index', + 'can' => 'admin.website-admin.cms.menus.view', + 'description' => 'Gestiona la navegación del sitio.', + ], + 'Versiones y previews' => [ + 'icon' => 'ti ti-clock-edit', + 'route' => 'admin.website-admin.cms.versions.index', + 'can' => 'admin.website-admin.cms.versions.view', + 'description' => 'Historial de versiones de contenido y vista previa.', + ], + 'Plantillas' => [ + 'icon' => 'ti ti-template', + 'route' => 'admin.website-admin.cms.templates.index', + 'can' => 'admin.website-admin.cms.templates.view', + 'description' => 'Gestiona y asigna plantillas de presentación.', + ], + ], + ], + 'Integraciones API' => [ + '_meta' => [ + 'icon' => 'ti ti-plug', + 'description' => 'Configura APIs y servicios externos como Analytics o Chat.', + 'widget_label' => 'Extensiones y APIs', + 'home_at_root' => true, + 'priority' => 400, + ], + 'submenu' => [ + 'Google & Meta' => [ + 'icon' => 'ti ti-brand-google', + 'route' => 'admin.website-admin.integrations.analytics.index', + 'can' => 'admin.website-admin.integrations.analytics.view', + 'description' => 'Google Analytics, Search Console, Pixel Meta, etc.', + ], + 'Chat y comunicación' => [ + 'icon' => 'ti ti-message-dots', + 'route' => 'admin.website-admin.integrations.chat.index', + 'can' => 'admin.website-admin.integrations.chat.view', + 'description' => 'Messenger, WhatsApp, Tawk.to y más.', + ], + 'Traducción e idioma' => [ + 'icon' => 'ti ti-language', + 'route' => 'admin.website-admin.integrations.translate.index', + 'can' => 'admin.website-admin.integrations.translate.view', + 'description' => 'Integraciones como Google Translate o DeepL.', + ], + ], + ], + 'Sistema de Cache' => [ + '_meta' => [ + 'icon' => 'ti ti-database-cog', + 'description' => 'Herramientas avanzadas de rendimiento y renderizado.', + 'widget_label' => 'Motor de Cache', + 'home_at_root' => true, + 'priority' => 500, + ], + 'submenu' => [ + 'HTML completo' => [ + 'icon' => 'ti ti-file-type-html', + 'route' => 'admin.website-admin.cache.full.index', + 'can' => 'admin.website-admin.cache.full.view', + 'description' => 'Gestiona caché render de páginas completas.', + ], + 'Bloques de contenido' => [ + 'icon' => 'ti ti-box-model', + 'route' => 'admin.website-admin.cache.blocks.index', + 'can' => 'admin.website-admin.cache.blocks.view', + 'description' => 'Visualiza y limpia caché por bloques individuales.', + ], + 'Previews firmadas' => [ + 'icon' => 'ti ti-key', + 'route' => 'admin.website-admin.cache.previews.index', + 'can' => 'admin.website-admin.cache.previews.view', + 'description' => 'Controla la vigencia y firma de URLs temporales.', + ], + ], + ], + 'Blog' => [ + '_meta' => [ + 'icon' => 'ti ti-news', + 'description' => 'Publica, edita y organiza artículos, categorías y comentarios de tu blog.', + 'after_to' => 'Web & SEO', + ], + 'submenu' => [ + 'Categorias' => [ + 'icon' => 'ti ti-category', + 'route' => 'admin.website-admin.blog.categories.index', + 'can' => 'admin.website-admin.blog.categories.view', + ], + 'Etiquetas' => [ + 'icon' => 'ti ti-tags', + 'route' => 'admin.website-admin.blog.tags.index', + 'can' => 'admin.website-admin.blog.tags.view', + ], + 'Articulos' => [ + 'icon' => 'ti ti-news', + 'route' => 'admin.website-admin.blog.articles.index', + 'can' => 'admin.website-admin.blog.articles.view', + ], + 'Comentarios' => [ + 'icon' => 'ti ti-message', + 'route' => 'admin.website-admin.blog.comments.index', + 'can' => 'admin.website-admin.blog.comments.view', + ], + ] + ], + 'Plantillas' => [ + '_meta' => [ + 'icon' => 'ti ti-template', + 'description' => 'Gestiona las plantillas disponibles para tu sitio web.', + 'after_to' => 'Blog', + ] + ], +]; diff --git a/database/data/rbac/permissions.json b/database/data/rbac/permissions.json new file mode 100644 index 0000000..adda4b5 --- /dev/null +++ b/database/data/rbac/permissions.json @@ -0,0 +1,1154 @@ +{ + "module": "admin.website-admin", + "name": { + "es": "Administrador de Sitio Web", + "en": "Website Administrator" + }, + "_meta": { + "description": { + "es": "Permisos para la gestión del sitio web", + "en": "Permissions for managing the website" + }, + "icon": "ti ti-settings" + }, + "priority": 200, + "groups": { + "website-settings": { + "name": { + "es": "Configuración del Sitio Web", + "en": "Website Settings" + }, + "_meta": { + "description": { + "es": "Permisos para ajustes generales y SEO del sitio web", + "en": "Permissions for general settings and website SEO" + }, + "icon": "ti ti-settings" + }, + "priority": 100, + "sub_groups": { + "general": { + "name": { + "es": "Ajustes generales", + "en": "General settings" + }, + "_meta": { + "description": { + "es": "Personalización general del sitio web", + "en": "General customization of the website" + }, + "icon": "ti ti-settings" + }, + "priority": 100, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración general del sitio", + "en": "View general site settings" + }, + "key": "settings.general.view" + } + ] + }, + "social": { + "name": { + "es": "Redes sociales", + "en": "Social media" + }, + "_meta": { + "description": { + "es": "Gestión de enlaces a redes sociales", + "en": "Social media link management" + }, + "icon": "ti ti-share" + }, + "priority": 200, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver enlaces sociales", + "en": "View social links" + }, + "key": "settings.social.view" + } + ] + }, + "indexing": { + "name": { + "es": "Indexación", + "en": "Indexing" + }, + "_meta": { + "description": { + "es": "Control de visibilidad en buscadores", + "en": "Control site visibility in search engines" + }, + "icon": "ti ti-search" + }, + "priority": 300, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de indexado", + "en": "View indexing settings" + }, + "key": "settings.indexing.view" + } + ] + }, + "info": { + "name": { + "es": "Información de contacto", + "en": "Contact information" + }, + "_meta": { + "description": { + "es": "Datos de contacto y localización", + "en": "Contact data and location" + }, + "icon": "ti ti-device-mobile-message" + }, + "priority": 400, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver información de contacto", + "en": "View contact information" + }, + "key": "contact.info.view" + } + ] + }, + "form": { + "name": { + "es": "Formulario de contacto", + "en": "Contact form" + }, + "_meta": { + "description": { + "es": "Gestión del formulario de contacto", + "en": "Contact form configuration" + }, + "icon": "ti ti-mail-cog" + }, + "priority": 500, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver formulario de contacto", + "en": "View contact form" + }, + "key": "contact.form.view" + } + ] + }, + "messenger": { + "name": { + "es": "Facebook Messenger", + "en": "Facebook Messenger" + }, + "_meta": { + "description": { + "es": "Activa el chat de Messenger en tu sitio.", + "en": "Activate Messenger chat on your site." + }, + "icon": "ti ti-brand-messenger" + }, + "priority": 600, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Messenger", + "en": "View Messenger configuration" + }, + "key": "comunication.messenger.view" + } + ] + }, + "whatsapp": { + "name": { + "es": "WhatsApp Chat", + "en": "WhatsApp Chat" + }, + "_meta": { + "description": { + "es": "Integra un botón de chat directo con WhatsApp.", + "en": "Integrate a WhatsApp direct chat button." + }, + "icon": "ti ti-brand-whatsapp" + }, + "priority": 700, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de WhatsApp", + "en": "View WhatsApp configuration" + }, + "key": "comunication.whatsapp.view" + } + ] + }, + "tawk-to": { + "name": { + "es": "Tawk.to", + "en": "Tawk.to" + }, + "_meta": { + "description": { + "es": "Agrega soporte en vivo con Tawk.to.", + "en": "Add live support with Tawk.to." + }, + "icon": "ti ti-message-dots" + }, + "priority": 800, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Tawk.to", + "en": "View Tawk.to configuration" + }, + "key": "comunication.tawk-to.view" + } + ] + }, + "twitter": { + "name": { + "es": "Twitter API", + "en": "Twitter API" + }, + "_meta": { + "description": { + "es": "Configura la integración con Twitter/X.", + "en": "Configure integration with Twitter/X." + }, + "icon": "ti ti-brand-x" + }, + "priority": 900, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Twitter", + "en": "View Twitter configuration" + }, + "key": "comunication.twitter.view" + } + ] + }, + "google-translate": { + "name": { + "es": "Google Translate", + "en": "Google Translate" + }, + "_meta": { + "description": { + "es": "Activa la traducción automática con Google Translate.", + "en": "Enable automatic translation using Google Translate." + }, + "icon": "ti ti-language" + }, + "priority": 1000, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Google Translate", + "en": "View Google Translate settings" + }, + "key": "translate.google-translate.view" + }, + { + "action": "update", + "label": { + "es": "Editar configuración de Google Translate", + "en": "Update Google Translate settings" + }, + "key": "translate.google-translate.update" + } + ] + } + } + }, + "website-cms": { + "name": { + "es": "Gestor de Contenidos", + "en": "Content Manager" + }, + "_meta": { + "description": { + "es": "Administrador de contenidos dinámicos del sitio web.", + "en": "Administrator of dynamic website content." + }, + "icon": "ti ti-layout-grid-add" + }, + "priority": 200, + "sub_groups": { + "menus": { + "name": { + "es": "Menús del sitio", + "en": "Site Menus" + }, + "_meta": { + "description": { + "es": "Estructura de navegación y menús.", + "en": "Navigation structure and menus." + }, + "icon": "ti ti-hierarchy-3" + }, + "priority": 100, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver menús del sitio", + "en": "View site menus" + }, + "key": "cms.menus.view" + }, + { + "action": "update", + "label": { + "es": "Editar menús del sitio", + "en": "Edit site menus" + }, + "key": "cms.menus.update" + } + ] + }, + "seo": { + "name": { + "es": "Perfil SEO", + "en": "SEO Profile" + }, + "_meta": { + "description": { + "es": "Configuración de parámetros SEO.", + "en": "SEO parameter configuration." + }, + "icon": "ti ti-settings" + }, + "priority": 200, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver perfil SEO", + "en": "View SEO profile" + }, + "key": "cms.seo.view" + }, + { + "action": "update", + "label": { + "es": "Editar perfil SEO", + "en": "Edit SEO profile" + }, + "key": "cms.seo.update" + } + ] + }, + "contents": { + "name": { + "es": "Contenidos dinámicos", + "en": "Dynamic Contents" + }, + "_meta": { + "description": { + "es": "Páginas, bloques y parciales Blade.", + "en": "Pages, blocks, and Blade partials." + }, + "icon": "ti ti-file-text" + }, + "priority": 300, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver contenidos", + "en": "View contents" + }, + "key": "cms.contents.view" + }, + { + "action": "create", + "label": { + "es": "Crear contenido", + "en": "Create content" + }, + "key": "cms.contents.create" + }, + { + "action": "update", + "label": { + "es": "Editar contenido", + "en": "Edit content" + }, + "key": "cms.contents.update" + }, + { + "action": "delete", + "label": { + "es": "Eliminar contenido", + "en": "Delete content" + }, + "key": "cms.contents.delete" + } + ] + }, + "versions": { + "name": { + "es": "Versiones de contenido", + "en": "Content Versions" + }, + "_meta": { + "description": { + "es": "Gestión de versiones y restauraciones.", + "en": "Version and restoration management." + }, + "icon": "ti ti-clock-edit" + }, + "priority": 400, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver versiones de contenido", + "en": "View content versions" + }, + "key": "cms.versions.view" + }, + { + "action": "reopen", + "label": { + "es": "Restaurar versión", + "en": "Restore version" + }, + "key": "cms.versions.reopen" + } + ] + }, + "templates": { + "name": { + "es": "Plantillas disponibles", + "en": "Available Templates" + }, + "_meta": { + "description": { + "es": "Plantillas base y organización.", + "en": "Base templates and organization." + }, + "icon": "ti ti-template" + }, + "priority": 500, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver plantillas", + "en": "View templates" + }, + "key": "cms.templates.view" + }, + { + "action": "update", + "label": { + "es": "Editar plantillas", + "en": "Edit templates" + }, + "key": "cms.templates.update" + } + ] + }, + "cache": { + "name": { + "es": "Caché HTML renderizado", + "en": "Rendered HTML Cache" + }, + "_meta": { + "description": { + "es": "Visualiza y limpia la caché de HTML completo del sitio web.", + "en": "View and clear full-page HTML cache for the website." + }, + "icon": "ti ti-file-type-html" + }, + "priority": 600, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver caché HTML", + "en": "View HTML Cache" + }, + "key": "website.cache.fullpage.view" + }, + { + "action": "clean", + "label": { + "es": "Limpiar caché HTML", + "en": "Clear HTML Cache" + }, + "key": "website.cache.fullpage.clean" + } + ] + }, + "signed-previews": { + "name": { + "es": "Previsualizaciones firmadas", + "en": "Signed Previews" + }, + "_meta": { + "description": { + "es": "URLs de vista previa y control de firma temporal.", + "en": "Preview URLs and temporary signature control." + }, + "icon": "ti ti-lock-access" + }, + "priority": 700, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver previsualizaciones firmadas", + "en": "View signed previews" + }, + "key": "website.cache.signed-previews.view" + }, + { + "action": "clean", + "label": { + "es": "Limpiar previsualizaciones firmadas", + "en": "Clear signed previews" + }, + "key": "website.cache.signed-previews.clean" + } + ] + } + } + }, + "website-content": { + "name": { + "es": "Contenido", + "en": "Content" + }, + "_meta": { + "description": { + "es": "Maneja contenido informativo y visual.", + "en": "Manage informational and visual content." + }, + "icon": "ti ti-hierarchy" + }, + "priority": 300, + "sub_groups": { + "faq": { + "name": { + "es": "Preguntas frecuentes", + "en": "Frequently Asked Questions" + }, + "_meta": { + "description": { + "es": "Administra las preguntas frecuentes del sitio.", + "en": "Manage the site's FAQ content." + }, + "icon": "ti ti-bubble-text" + }, + "priority": 100, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver preguntas frecuentes", + "en": "View FAQ" + }, + "key": "content.faq.view" + }, + { + "action": "create", + "label": { + "es": "Crear preguntas frecuentes", + "en": "Create FAQ" + }, + "key": "content.faq.create" + }, + { + "action": "update", + "label": { + "es": "Editar preguntas frecuentes", + "en": "Edit FAQ" + }, + "key": "content.faq.update" + }, + { + "action": "delete", + "label": { + "es": "Eliminar preguntas frecuentes", + "en": "Delete FAQ" + }, + "key": "content.faq.delete" + } + ] + }, + "gallery": { + "name": { + "es": "Galería de imágenes", + "en": "Image Gallery" + }, + "_meta": { + "description": { + "es": "Agrega y organiza tus imágenes.", + "en": "Add and organize your images." + }, + "icon": "ti ti-photo" + }, + "priority": 200, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver galería de imágenes", + "en": "View image gallery" + }, + "key": "content.gallery.view" + }, + { + "action": "create", + "label": { + "es": "Subir imagen", + "en": "Upload image" + }, + "key": "content.gallery.create" + }, + { + "action": "delete", + "label": { + "es": "Eliminar imagen", + "en": "Delete image" + }, + "key": "content.gallery.delete" + } + ] + }, + "legal": { + "name": { + "es": "Avisos legales", + "en": "Legal Notices" + }, + "_meta": { + "description": { + "es": "Documentos como Términos, Aviso de Privacidad, etc.", + "en": "Documents such as Terms of Service, Privacy Policy, etc." + }, + "icon": "ti ti-file-text-shield" + }, + "priority": 300, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver avisos legales", + "en": "View legal notices" + }, + "key": "content.legal.view" + }, + { + "action": "update", + "label": { + "es": "Editar avisos legales", + "en": "Edit legal notices" + }, + "key": "content.legal.update" + } + ] + } + } + }, + "website-analytics": { + "name": { + "es": "Analítica y seguimiento", + "en": "Analytics and Tracking" + }, + "_meta": { + "description": { + "es": "Permisos para gestionar integraciones de analítica y seguimiento.", + "en": "Permissions to manage analytics and tracking integrations." + }, + "icon": "ti ti-device-analytics" + }, + "priority": 400, + "sub_groups": { + "google-analytics": { + "name": { + "es": "Google Analytics", + "en": "Google Analytics" + }, + "_meta": { + "description": { + "es": "Integra tu cuenta de Google Analytics.", + "en": "Integrate your Google Analytics account." + }, + "icon": "ti ti-chart-scatter-3d" + }, + "priority": 100, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Google Analytics", + "en": "View Google Analytics configuration" + }, + "key": "analytics.google-analytics.view" + } + ] + }, + "google-tags": { + "name": { + "es": "Google Tags", + "en": "Google Tags" + }, + "_meta": { + "description": { + "es": "Administra etiquetas de Google Tag Manager.", + "en": "Manage Google Tag Manager tags." + }, + "icon": "ti ti-tags" + }, + "priority": 200, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Google Tags", + "en": "View Google Tags configuration" + }, + "key": "analytics.google-tags.view" + } + ] + }, + "google-search-console": { + "name": { + "es": "Google Search Console", + "en": "Google Search Console" + }, + "_meta": { + "description": { + "es": "Verifica y gestiona tu sitio con Search Console.", + "en": "Verify and manage your site with Search Console." + }, + "icon": "ti ti-search" + }, + "priority": 300, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Search Console", + "en": "View Search Console configuration" + }, + "key": "analytics.google-search-console.view" + } + ] + }, + "pixel-meta": { + "name": { + "es": "Pixel Meta", + "en": "Meta Pixel" + }, + "_meta": { + "description": { + "es": "Integra el pixel de Meta (Facebook Ads).", + "en": "Integrate Meta Pixel (Facebook Ads)." + }, + "icon": "ti ti-device-analytics" + }, + "priority": 400, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Meta Pixel", + "en": "View Meta Pixel configuration" + }, + "key": "analytics.pixel-meta.view" + } + ] + } + } + }, + "website-seo-tools": { + "name": { + "es": "Herramientas SEO", + "en": "SEO Tools" + }, + "_meta": { + "description": { + "es": "Permisos para gestionar herramientas y metadatos SEO.", + "en": "Permissions to manage SEO tools and metadata." + }, + "icon": "ti ti-code-dots" + }, + "priority": 500, + "sub_groups": { + "sitemap": { + "name": { + "es": "Mapa del sitio", + "en": "Sitemap" + }, + "_meta": { + "description": { + "es": "Genera y publica el sitemap.xml.", + "en": "Generate and publish sitemap.xml." + }, + "icon": "ti ti-hierarchy" + }, + "priority": 100, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Sitemap", + "en": "View Sitemap configuration" + }, + "key": "seo-tools.sitemap.view" + } + ] + }, + "jsonld": { + "name": { + "es": "Google JSON-LD", + "en": "Google JSON-LD" + }, + "_meta": { + "description": { + "es": "Configura el esquema estructurado para Google.", + "en": "Configure structured schema for Google." + }, + "icon": "ti ti-code-dots" + }, + "priority": 200, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de JSON-LD", + "en": "View JSON-LD configuration" + }, + "key": "seo-tools.jsonld.view" + } + ] + }, + "robots": { + "name": { + "es": "Robots.txt", + "en": "Robots.txt" + }, + "_meta": { + "description": { + "es": "Controla qué partes del sitio pueden ser indexadas.", + "en": "Control which parts of the site can be indexed." + }, + "icon": "ti ti-code" + }, + "priority": 300, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Robots.txt", + "en": "View Robots.txt configuration" + }, + "key": "seo-tools.robots.view" + } + ] + }, + "manifest": { + "name": { + "es": "manifest.json", + "en": "manifest.json" + }, + "_meta": { + "description": { + "es": "Configura la compatibilidad como PWA.", + "en": "Configure PWA compatibility." + }, + "icon": "ti ti-file-code" + }, + "priority": 400, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de manifest.json", + "en": "View manifest.json configuration" + }, + "key": "seo-tools.manifest.view" + } + ] + }, + "canonical": { + "name": { + "es": "Canonical URLs", + "en": "Canonical URLs" + }, + "_meta": { + "description": { + "es": "Evita duplicidad de URLs con etiquetas canónicas.", + "en": "Avoid URL duplication with canonical tags." + }, + "icon": "ti ti-link" + }, + "priority": 500, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Canonical", + "en": "View Canonical configuration" + }, + "key": "seo-tools.canonical.view" + } + ] + }, + "social-cards": { + "name": { + "es": "Preview Social Cards", + "en": "Preview Social Cards" + }, + "_meta": { + "description": { + "es": "Define títulos, imágenes y previews sociales.", + "en": "Define titles, images and social previews." + }, + "icon": "ti ti-brand-facebook" + }, + "priority": 600, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver configuración de Social Cards", + "en": "View Social Cards configuration" + }, + "key": "seo-tools.social-cards.view" + } + ] + } + } + }, + "website-blog": { + "name": { + "es": "Blog", + "en": "Blog" + }, + "_meta": { + "description": { + "es": "Publica, edita y organiza artículos, categorías y comentarios de tu blog.", + "en": "Publish, edit, and organize blog articles, categories, and comments." + }, + "icon": "ti ti-news", + "after_to": "Sitio web" + }, + "priority": 600, + "sub_groups": { + "categories": { + "name": { + "es": "Categorías", + "en": "Categories" + }, + "_meta": { + "icon": "ti ti-category" + }, + "priority": 100, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver categorías", + "en": "View categories" + }, + "key": "blog.categories.view" + }, + { + "action": "create", + "label": { + "es": "Crear categoría", + "en": "Create category" + }, + "key": "blog.categories.create" + }, + { + "action": "update", + "label": { + "es": "Editar categoría", + "en": "Edit category" + }, + "key": "blog.categories.update" + }, + { + "action": "delete", + "label": { + "es": "Eliminar categoría", + "en": "Delete category" + }, + "key": "blog.categories.delete" + } + ] + }, + "tags": { + "name": { + "es": "Etiquetas", + "en": "Tags" + }, + "_meta": { + "icon": "ti ti-tags" + }, + "priority": 200, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver etiquetas", + "en": "View tags" + }, + "key": "blog.tags.view" + }, + { + "action": "create", + "label": { + "es": "Crear etiqueta", + "en": "Create tag" + }, + "key": "blog.tags.create" + }, + { + "action": "update", + "label": { + "es": "Editar etiqueta", + "en": "Edit tag" + }, + "key": "blog.tags.update" + }, + { + "action": "delete", + "label": { + "es": "Eliminar etiqueta", + "en": "Delete tag" + }, + "key": "blog.tags.delete" + } + ] + }, + "articles": { + "name": { + "es": "Artículos", + "en": "Articles" + }, + "_meta": { + "icon": "ti ti-news" + }, + "priority": 300, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver artículos", + "en": "View articles" + }, + "key": "blog.articles.view" + }, + { + "action": "create", + "label": { + "es": "Crear artículo", + "en": "Create article" + }, + "key": "blog.articles.create" + }, + { + "action": "update", + "label": { + "es": "Editar artículo", + "en": "Edit article" + }, + "key": "blog.articles.update" + }, + { + "action": "delete", + "label": { + "es": "Eliminar artículo", + "en": "Delete article" + }, + "key": "blog.articles.delete" + }, + { + "action": "publish", + "label": { + "es": "Publicar artículo", + "en": "Publish article" + }, + "key": "blog.articles.publish" + }, + { + "action": "archive", + "label": { + "es": "Archivar artículo", + "en": "Archive article" + }, + "key": "blog.articles.archive" + } + ] + }, + "comments": { + "name": { + "es": "Comentarios", + "en": "Comments" + }, + "_meta": { + "icon": "ti ti-message" + }, + "priority": 400, + "permissions": [ + { + "action": "view", + "label": { + "es": "Ver comentarios", + "en": "View comments" + }, + "key": "blog.comments.view" + }, + { + "action": "delete", + "label": { + "es": "Eliminar comentario", + "en": "Delete comment" + }, + "key": "blog.comments.delete" + }, + { + "action": "approve", + "label": { + "es": "Aprobar comentario", + "en": "Approve comment" + }, + "key": "blog.comments.approve" + }, + { + "action": "cancel", + "label": { + "es": "Rechazar comentario", + "en": "Reject comment" + }, + "key": "blog.comments.cancel" + } + ] + } + } + } + } +} diff --git a/database/data/rbac/roles.json b/database/data/rbac/roles.json new file mode 100644 index 0000000..416014e --- /dev/null +++ b/database/data/rbac/roles.json @@ -0,0 +1,280 @@ +{ + "SuperAdmin" : { + "permissions" : [ + "admin.website-admin.settings.general.view", + "admin.website-admin.settings.social.view", + "admin.website-admin.settings.indexing.view", + "admin.website-admin.contact.info.view", + "admin.website-admin.contact.form.view", + "admin.website-admin.comunication.messenger.view", + "admin.website-admin.comunication.whatsapp.view", + "admin.website-admin.comunication.tawk-to.view", + "admin.website-admin.comunication.twitter.view", + "admin.website-admin.translate.google-translate.view", + "admin.website-admin.translate.google-translate.update", + "admin.website-admin.cms.menus.view", + "admin.website-admin.cms.menus.update", + "admin.website-admin.cms.seo.view", + "admin.website-admin.cms.seo.update", + "admin.website-admin.cms.contents.view", + "admin.website-admin.cms.contents.create", + "admin.website-admin.cms.contents.update", + "admin.website-admin.cms.contents.delete", + "admin.website-admin.cms.versions.view", + "admin.website-admin.cms.versions.reopen", + "admin.website-admin.cms.templates.view", + "admin.website-admin.cms.templates.update", + "admin.website-admin.website.cache.fullpage.view", + "admin.website-admin.website.cache.fullpage.clean", + "admin.website-admin.website.cache.signed-previews.view", + "admin.website-admin.website.cache.signed-previews.clean", + "admin.website-admin.content.faq.view", + "admin.website-admin.content.faq.create", + "admin.website-admin.content.faq.update", + "admin.website-admin.content.faq.delete", + "admin.website-admin.content.gallery.view", + "admin.website-admin.content.gallery.create", + "admin.website-admin.content.gallery.delete", + "admin.website-admin.content.legal.view", + "admin.website-admin.content.legal.update", + "admin.website-admin.analytics.google-analytics.view", + "admin.website-admin.analytics.google-tags.view", + "admin.website-admin.analytics.google-search-console.view", + "admin.website-admin.analytics.pixel-meta.view", + "admin.website-admin.seo-tools.sitemap.view", + "admin.website-admin.seo-tools.jsonld.view", + "admin.website-admin.seo-tools.robots.view", + "admin.website-admin.seo-tools.manifest.view", + "admin.website-admin.seo-tools.canonical.view", + "admin.website-admin.seo-tools.social-cards.view", + "admin.website-admin.blog.categories.view", + "admin.website-admin.blog.categories.create", + "admin.website-admin.blog.categories.update", + "admin.website-admin.blog.categories.delete", + "admin.website-admin.blog.tags.view", + "admin.website-admin.blog.tags.create", + "admin.website-admin.blog.tags.update", + "admin.website-admin.blog.tags.delete", + "admin.website-admin.blog.articles.view", + "admin.website-admin.blog.articles.create", + "admin.website-admin.blog.articles.update", + "admin.website-admin.blog.articles.delete", + "admin.website-admin.blog.articles.publish", + "admin.website-admin.blog.articles.archive", + "admin.website-admin.blog.comments.view", + "admin.website-admin.blog.comments.delete", + "admin.website-admin.blog.comments.approve", + "admin.website-admin.blog.comments.cancel" + ] + }, + "WebsiteAdmin": { + "_meta": { + "description": { + "es": "Gestiona contenidos, menús, SEO y configuración del sitio web.", + "en": "Manages contents, menus, SEO, and website settings." + }, + "icon": "ti ti-world", + "style": "warning" + }, + "permissions" : [ + "admin.website-admin.settings.general.view", + "admin.website-admin.settings.social.view", + "admin.website-admin.settings.indexing.view", + "admin.website-admin.contact.info.view", + "admin.website-admin.contact.form.view", + "admin.website-admin.comunication.messenger.view", + "admin.website-admin.comunication.whatsapp.view", + "admin.website-admin.comunication.tawk-to.view", + "admin.website-admin.comunication.twitter.view", + "admin.website-admin.translate.google-translate.view", + "admin.website-admin.translate.google-translate.update", + "admin.website-admin.cms.menus.view", + "admin.website-admin.cms.menus.update", + "admin.website-admin.cms.seo.view", + "admin.website-admin.cms.seo.update", + "admin.website-admin.cms.contents.view", + "admin.website-admin.cms.contents.create", + "admin.website-admin.cms.contents.update", + "admin.website-admin.cms.contents.delete", + "admin.website-admin.cms.versions.view", + "admin.website-admin.cms.versions.reopen", + "admin.website-admin.cms.templates.view", + "admin.website-admin.cms.templates.update", + "admin.website-admin.website.cache.fullpage.view", + "admin.website-admin.website.cache.fullpage.clean", + "admin.website-admin.website.cache.signed-previews.view", + "admin.website-admin.website.cache.signed-previews.clean", + "admin.website-admin.content.faq.view", + "admin.website-admin.content.faq.create", + "admin.website-admin.content.faq.update", + "admin.website-admin.content.faq.delete", + "admin.website-admin.content.gallery.view", + "admin.website-admin.content.gallery.create", + "admin.website-admin.content.gallery.delete", + "admin.website-admin.content.legal.view", + "admin.website-admin.content.legal.update", + "admin.website-admin.analytics.google-analytics.view", + "admin.website-admin.analytics.google-tags.view", + "admin.website-admin.analytics.google-search-console.view", + "admin.website-admin.analytics.pixel-meta.view", + "admin.website-admin.seo-tools.sitemap.view", + "admin.website-admin.seo-tools.jsonld.view", + "admin.website-admin.seo-tools.robots.view", + "admin.website-admin.seo-tools.manifest.view", + "admin.website-admin.seo-tools.canonical.view", + "admin.website-admin.seo-tools.social-cards.view", + "admin.website-admin.blog.categories.view", + "admin.website-admin.blog.categories.create", + "admin.website-admin.blog.categories.update", + "admin.website-admin.blog.categories.delete", + "admin.website-admin.blog.tags.view", + "admin.website-admin.blog.tags.create", + "admin.website-admin.blog.tags.update", + "admin.website-admin.blog.tags.delete", + "admin.website-admin.blog.articles.view", + "admin.website-admin.blog.articles.create", + "admin.website-admin.blog.articles.update", + "admin.website-admin.blog.articles.delete", + "admin.website-admin.blog.articles.publish", + "admin.website-admin.blog.articles.archive", + "admin.website-admin.blog.comments.view", + "admin.website-admin.blog.comments.delete", + "admin.website-admin.blog.comments.approve", + "admin.website-admin.blog.comments.cancel" + ] + }, + "WebsiteContentEditor": { + "_meta": { + "description": { + "es": "Puede crear, editar y publicar contenidos y artículos del blog.", + "en": "Can create, edit, and publish content and blog posts." + }, + "icon": "ti ti-article", + "style": "info" + }, + "permissions": [ + "admin.website-admin.cms.menus.view", + "admin.website-admin.cms.menus.update", + "admin.website-admin.cms.seo.view", + "admin.website-admin.cms.seo.update", + "admin.website-admin.cms.contents.view", + "admin.website-admin.cms.contents.create", + "admin.website-admin.cms.contents.update", + "admin.website-admin.cms.contents.delete", + "admin.website-admin.cms.versions.view", + "admin.website-admin.cms.templates.view", + "admin.website-admin.website.cache.fullpage.view", + "admin.website-admin.website.cache.fullpage.clean", + "admin.website-admin.website.cache.signed-previews.view", + "admin.website-admin.website.cache.signed-previews.clean", + "admin.website-admin.content.faq.view", + "admin.website-admin.content.faq.create", + "admin.website-admin.content.faq.update", + "admin.website-admin.content.faq.delete", + "admin.website-admin.content.gallery.view", + "admin.website-admin.content.gallery.create", + "admin.website-admin.content.gallery.delete", + "admin.website-admin.content.legal.view", + "admin.website-admin.content.legal.update", + "admin.website-admin.seo-tools.sitemap.view", + "admin.website-admin.seo-tools.jsonld.view", + "admin.website-admin.seo-tools.robots.view", + "admin.website-admin.seo-tools.manifest.view", + "admin.website-admin.seo-tools.canonical.view", + "admin.website-admin.seo-tools.social-cards.view" ] + }, + "BlogEditor": { + "_meta": { + "description": { + "es": "Puede crear, editar y publicar contenidos y artículos del blog.", + "en": "Can create, edit, and publish content and blog posts." + }, + "icon": "ti ti-article", + "style": "info" + }, + "permissions": [ + "admin.website-admin.blog.categories.view", + "admin.website-admin.blog.categories.create", + "admin.website-admin.blog.categories.update", + "admin.website-admin.blog.categories.delete", + "admin.website-admin.blog.tags.view", + "admin.website-admin.blog.tags.create", + "admin.website-admin.blog.tags.update", + "admin.website-admin.blog.tags.delete", + "admin.website-admin.blog.articles.view", + "admin.website-admin.blog.articles.create", + "admin.website-admin.blog.articles.update", + "admin.website-admin.blog.articles.delete", + "admin.website-admin.blog.articles.publish", + "admin.website-admin.blog.articles.archive", + "admin.website-admin.blog.comments.view", + "admin.website-admin.blog.comments.delete", + "admin.website-admin.blog.comments.approve", + "admin.website-admin.blog.comments.cancel" + ] + + }, + "WebsiteSEO": { + "_meta": { + "description": { + "es": "Gestiona herramientas SEO, Sitemap, Robots y JSON-LD.", + "en": "Manages SEO tools, Sitemap, Robots, and JSON-LD." + }, + "icon": "ti ti-chart-arcs", + "style": "warning" + }, + "permissions": [ + "admin.website-admin.settings.general.view", + "admin.website-admin.settings.indexing.view", + "admin.website-admin.analytics.google-analytics.view", + "admin.website-admin.analytics.google-tags.view", + "admin.website-admin.analytics.google-search-console.view", + "admin.website-admin.analytics.pixel-meta.view", + "admin.website-admin.seo-tools.sitemap.view", + "admin.website-admin.seo-tools.jsonld.view", + "admin.website-admin.seo-tools.robots.view", + "admin.website-admin.seo-tools.manifest.view", + "admin.website-admin.seo-tools.canonical.view", + "admin.website-admin.seo-tools.social-cards.view" + ] + + }, + "Auditor" : { + "permissions" : [ + "admin.website-admin.settings.general.view", + "admin.website-admin.settings.social.view", + "admin.website-admin.settings.indexing.view", + "admin.website-admin.contact.info.view", + "admin.website-admin.contact.form.view", + "admin.website-admin.comunication.messenger.view", + "admin.website-admin.comunication.whatsapp.view", + "admin.website-admin.comunication.tawk-to.view", + "admin.website-admin.comunication.twitter.view", + "admin.website-admin.translate.google-translate.view", + "admin.website-admin.cms.menus.view", + "admin.website-admin.cms.seo.view", + "admin.website-admin.cms.contents.view", + "admin.website-admin.cms.versions.view", + "admin.website-admin.cms.templates.view", + "admin.website-admin.website.cache.fullpage.view", + "admin.website-admin.website.cache.signed-previews.view", + "admin.website-admin.content.faq.view", + "admin.website-admin.content.gallery.view", + "admin.website-admin.content.legal.view", + "admin.website-admin.analytics.google-analytics.view", + "admin.website-admin.analytics.google-tags.view", + "admin.website-admin.analytics.google-search-console.view", + "admin.website-admin.analytics.pixel-meta.view", + "admin.website-admin.seo-tools.sitemap.view", + "admin.website-admin.seo-tools.jsonld.view", + "admin.website-admin.seo-tools.robots.view", + "admin.website-admin.seo-tools.manifest.view", + "admin.website-admin.seo-tools.canonical.view", + "admin.website-admin.seo-tools.social-cards.view", + "admin.website-admin.blog.categories.view", + "admin.website-admin.blog.tags.view", + "admin.website-admin.blog.articles.view", + "admin.website-admin.blog.comments.view" + ] + } +} diff --git a/database/data/website-admin/website_agroform_contents.json b/database/data/website-admin/website_agroform_contents.json new file mode 100644 index 0000000..b2ced92 --- /dev/null +++ b/database/data/website-admin/website_agroform_contents.json @@ -0,0 +1,192 @@ +[ + { + "site_id": 1, + "title": "Agroform", + "slug": "", + "description": "Bienvenido a agroform, soluciones tecnológicas para empresas.", + "keywords": ["agroform", "erp", "tecnología", "soluciones"], + "template": "home", + "type": "page", + "is_partial": false, + "seo_profile_id": "inicio-agroform", + "seo_overrides": null, + "canonical_url": "https://agroform.com.mx", + "content_blocks": [ + {"type": "hero", "title": "Soluciones Tecnológicas para tu Empresa", "subtitle": "Desde ERP hasta eCommerce", "image": "/images/hero.jpg"}, + {"type": "features", "items": [{"icon": "ti ti-server", "label": "Infraestructura"}, {"icon": "ti ti-code", "label": "Desarrollo a medida"}]}, + {"type": "cta", "text": "Solicita una demo sin costo", "button": "Agendar llamada"} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Nosotros", + "slug": "nosotros", + "description": "Conoce la historia, visión y equipo de Koneko.", + "keywords": ["nosotros", "equipo", "historia", "koneko"], + "template": "default", + "type": "page", + "is_partial": false, + "seo_profile_id": "inicio-agroform", + "seo_overrides": null, + "canonical_url": "https://agroform.com.mx/nosotros", + "content_blocks": [ + {"type": "text", "heading": "¿Quiénes somos?", "content": "Somos una empresa de tecnología con sede en México..."}, + {"type": "team", "members": [{"name": "Arturo", "role": "Director Técnico"}, {"name": "Laura", "role": "Marketing"}]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Servicios", + "slug": "servicios", + "description": "Explora nuestros servicios: ERP, eCommerce, hosting y más.", + "keywords": ["erp", "servicios", "agroform", "sistemas"], + "template": "services", + "type": "page", + "is_partial": false, + "seo_profile_id": "servicios-agroform", + "seo_overrides": null, + "canonical_url": "https://agroform.com.mx/servicios", + "content_blocks": [ + {"type": "service_list", "items": [ + {"title": "ERP Agroform", "description": "Sistema de gestión empresarial completo"}, + {"title": "Tienda Virtual", "description": "Ecommerce 100% administrable con SEO nativo"}, + {"title": "Infraestructura", "description": "Servidores, VPS, Proxmox y más"} + ]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Contacto", + "slug": "contacto", + "description": "Contáctanos para resolver tus dudas o agendar una demo.", + "keywords": ["contacto", "soporte", "demo", "ayuda"], + "template": "contact", + "type": "page", + "is_partial": false, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": "https://agroform.com.mx/contacto", + "content_blocks": [ + {"type": "form", "fields": ["nombre", "email", "mensaje"], "submit_label": "Enviar mensaje"}, + {"type": "map", "lat": 19.4326, "lng": -99.1332} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Aviso de Privacidad", + "slug": "aviso-privacidad", + "description": "Consulta nuestro aviso de privacidad actualizado.", + "keywords": ["legal", "privacidad", "datos personales"], + "template": "legal", + "type": "page", + "is_partial": false, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": "https://agroform.com.mx/aviso-privacidad", + "content_blocks": [ + {"type": "text", "heading": "Política de privacidad", "content": "Tus datos son importantes para nosotros..."} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Preguntas Frecuentes", + "slug": "faq", + "description": "Resuelve tus dudas rápidamente con nuestras respuestas frecuentes.", + "keywords": ["faq", "dudas", "soporte"], + "template": "faq", + "type": "page", + "is_partial": true, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "faq", "items": [ + {"q": "¿Tienen soporte técnico?", "a": "Sí, disponible 24/7 para clientes activos."}, + {"q": "¿Qué formas de pago aceptan?", "a": "Transferencia, tarjeta y criptomonedas."} + ]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Header Principal", + "slug": "header-principal", + "description": "Encabezado superior con navegación.", + "keywords": [], + "template": null, + "type": "partial", + "is_partial": true, + "seo_profile_id": 1, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "menu", "menu_slug": "principal"} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Footer General", + "slug": "footer-general", + "description": "Pie de página con enlaces y datos legales.", + "keywords": [], + "template": null, + "type": "partial", + "is_partial": true, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "footer", "links": ["inicio", "servicios", "contacto"]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Categoría: Blog", + "slug": "blog", + "description": "Explora nuestros artículos de tecnología y negocios.", + "keywords": ["blog", "noticias", "tips", "artículos"], + "template": "blog_index", + "type": "category", + "is_partial": false, + "seo_profile_id": "blog", + "seo_overrides": null, + "canonical_url": "https://agroform.com.mx/blog", + "content_blocks": [], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 1, + "title": "Artículo: Ventajas del ERP", + "slug": "blog/ventajas-del-erp", + "description": "Descubre cómo un ERP puede transformar tu empresa.", + "keywords": ["erp", "ventajas", "productividad", "sistemas"], + "template": "blog_article", + "type": "blog", + "is_partial": false, + "seo_profile_id": "promocion-computadoras-gamer", + "seo_overrides": null, + "canonical_url": "https://agroform.com.mx/blog/ventajas-del-erp", + "content_blocks": [ + {"type": "text", "heading": "Beneficios del ERP", "content": "Centralización de procesos, automatización..."}, + {"type": "image", "src": "/images/blog/erp-benefits.jpg", "alt": "ERP y eficiencia empresarial"} + ], + "is_draft": false, + "is_sensitive": false + } +] diff --git a/database/data/website-admin/website_agroform_menus.json b/database/data/website-admin/website_agroform_menus.json new file mode 100644 index 0000000..30162f4 --- /dev/null +++ b/database/data/website-admin/website_agroform_menus.json @@ -0,0 +1,66 @@ +[ + { + "menu_slug": "main-header", + "title": { + "es": "Inicio", + "en": "Home" + }, + "url": "/", + "type": "custom", + "target": "_self", + "order": 1, + "icon": "ti ti-home" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Artículos", + "en": "Articles" + }, + "url": "/blog", + "type": "blog_article", + "target": "_self", + "order": 2, + "icon": "ti ti-news" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Categorías", + "en": "Categories" + }, + "url": "/blog/categorias", + "type": "blog_category", + "target": "_self", + "order": 3, + "icon": "ti ti-list" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Iniciar Sesión", + "en": "Login" + }, + "url": "/login", + "type": "custom", + "target": "_self", + "order": 4, + "icon": "ti ti-login", + "badge": "Nuevo", + "badge_color": "bg-success" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Cerrar Sesión", + "en": "Logout" + }, + "url": "/logout", + "type": "action", + "target": "_self", + "order": 5, + "icon": "ti ti-logout", + "method": "POST", + "roles": ["authenticated"] + } +] diff --git a/database/data/website-admin/website_cleanfy_contents.json b/database/data/website-admin/website_cleanfy_contents.json new file mode 100644 index 0000000..3cf18e4 --- /dev/null +++ b/database/data/website-admin/website_cleanfy_contents.json @@ -0,0 +1,192 @@ +[ + { + "site_id": 2, + "title": "Cleanfy", + "slug": "", + "description": "Bienvenido a cleanfy, soluciones tecnológicas para empresas.", + "keywords": ["cleanfy", "erp", "tecnología", "soluciones"], + "template": "home", + "type": "page", + "is_partial": false, + "seo_profile_id": "inicio-cleanfy", + "seo_overrides": null, + "canonical_url": "https://cleanfy.mx", + "content_blocks": [ + {"type": "hero", "title": "Soluciones Tecnológicas para tu Empresa", "subtitle": "Desde ERP hasta eCommerce", "image": "/images/hero.jpg"}, + {"type": "features", "items": [{"icon": "ti ti-server", "label": "Infraestructura"}, {"icon": "ti ti-code", "label": "Desarrollo a medida"}]}, + {"type": "cta", "text": "Solicita una demo sin costo", "button": "Agendar llamada"} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Nosotros", + "slug": "nosotros", + "description": "Conoce la historia, visión y equipo de Koneko.", + "keywords": ["nosotros", "equipo", "historia", "koneko"], + "template": "default", + "type": "page", + "is_partial": false, + "seo_profile_id": "inicio-cleanfy", + "seo_overrides": null, + "canonical_url": "https://cleanfy.mx/nosotros", + "content_blocks": [ + {"type": "text", "heading": "¿Quiénes somos?", "content": "Somos una empresa de tecnología con sede en México..."}, + {"type": "team", "members": [{"name": "Arturo", "role": "Director Técnico"}, {"name": "Laura", "role": "Marketing"}]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Servicios", + "slug": "servicios", + "description": "Explora nuestros servicios: ERP, eCommerce, hosting y más.", + "keywords": ["erp", "servicios", "cleanfy", "sistemas"], + "template": "services", + "type": "page", + "is_partial": false, + "seo_profile_id": "servicios-cleanfy", + "seo_overrides": null, + "canonical_url": "https://cleanfy.mx/servicios", + "content_blocks": [ + {"type": "service_list", "items": [ + {"title": "ERP Cleanfy", "description": "Sistema de gestión empresarial completo"}, + {"title": "Tienda Virtual", "description": "Ecommerce 100% administrable con SEO nativo"}, + {"title": "Infraestructura", "description": "Servidores, VPS, Proxmox y más"} + ]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Contacto", + "slug": "contacto", + "description": "Contáctanos para resolver tus dudas o agendar una demo.", + "keywords": ["contacto", "soporte", "demo", "ayuda"], + "template": "contact", + "type": "page", + "is_partial": false, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": "https://cleanfy.mx/contacto", + "content_blocks": [ + {"type": "form", "fields": ["nombre", "email", "mensaje"], "submit_label": "Enviar mensaje"}, + {"type": "map", "lat": 19.4326, "lng": -99.1332} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Aviso de Privacidad", + "slug": "aviso-privacidad", + "description": "Consulta nuestro aviso de privacidad actualizado.", + "keywords": ["legal", "privacidad", "datos personales"], + "template": "legal", + "type": "page", + "is_partial": false, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": "https://cleanfy.mx/aviso-privacidad", + "content_blocks": [ + {"type": "text", "heading": "Política de privacidad", "content": "Tus datos son importantes para nosotros..."} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Preguntas Frecuentes", + "slug": "faq", + "description": "Resuelve tus dudas rápidamente con nuestras respuestas frecuentes.", + "keywords": ["faq", "dudas", "soporte"], + "template": "faq", + "type": "page", + "is_partial": true, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "faq", "items": [ + {"q": "¿Tienen soporte técnico?", "a": "Sí, disponible 24/7 para clientes activos."}, + {"q": "¿Qué formas de pago aceptan?", "a": "Transferencia, tarjeta y criptomonedas."} + ]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Header Principal", + "slug": "header-principal", + "description": "Encabezado superior con navegación.", + "keywords": [], + "template": null, + "type": "partial", + "is_partial": true, + "seo_profile_id": 1, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "menu", "menu_slug": "principal"} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Footer General", + "slug": "footer-general", + "description": "Pie de página con enlaces y datos legales.", + "keywords": [], + "template": null, + "type": "partial", + "is_partial": true, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "footer", "links": ["inicio", "servicios", "contacto"]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Categoría: Blog", + "slug": "blog", + "description": "Explora nuestros artículos de tecnología y negocios.", + "keywords": ["blog", "noticias", "tips", "artículos"], + "template": "blog_index", + "type": "category", + "is_partial": false, + "seo_profile_id": "blog", + "seo_overrides": null, + "canonical_url": "https://cleanfy.mx/blog", + "content_blocks": [], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 2, + "title": "Artículo: Ventajas del ERP", + "slug": "blog/ventajas-del-erp", + "description": "Descubre cómo un ERP puede transformar tu empresa.", + "keywords": ["erp", "ventajas", "productividad", "sistemas"], + "template": "blog_article", + "type": "blog", + "is_partial": false, + "seo_profile_id": "promocion-computadoras-gamer", + "seo_overrides": null, + "canonical_url": "https://cleanfy.mx/blog/ventajas-del-erp", + "content_blocks": [ + {"type": "text", "heading": "Beneficios del ERP", "content": "Centralización de procesos, automatización..."}, + {"type": "image", "src": "/images/blog/erp-benefits.jpg", "alt": "ERP y eficiencia empresarial"} + ], + "is_draft": false, + "is_sensitive": false + } +] diff --git a/database/data/website-admin/website_cleanfy_menus.json b/database/data/website-admin/website_cleanfy_menus.json new file mode 100644 index 0000000..ceb9052 --- /dev/null +++ b/database/data/website-admin/website_cleanfy_menus.json @@ -0,0 +1,74 @@ +[ + { + "menu_slug": "main-header", + "title": { + "es": "Inicio", + "en": "Home" + }, + "route_name": "website.home", + "type": "custom", + "target": "_self", + "order": 1, + "icon": "ti ti-home" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Nosotros", + "en": "About Us" + }, + "route_name": "website.about", + "type": "custom", + "target": "_self", + "order": 2, + "icon": "ti ti-users" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Servicios", + "en": "Services" + }, + "route_name": "website.services", + "type": "custom", + "target": "_self", + "order": 3, + "icon": "ti ti-briefcase" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Portafolio", + "en": "Portfolio" + }, + "route_name": "website.portfolio", + "type": "custom", + "target": "_self", + "order": 4, + "icon": "ti ti-photo" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Testimonios", + "en": "Testimonials" + }, + "route_name": "website.testimonials", + "type": "custom", + "target": "_self", + "order": 5, + "icon": "ti ti-messages" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Contacto", + "en": "Contact" + }, + "route_name": "website.contact", + "type": "custom", + "target": "_self", + "order": 6, + "icon": "ti ti-phone-call" + } +] diff --git a/database/data/website-admin/website_menus.json b/database/data/website-admin/website_menus.json new file mode 100644 index 0000000..b216ff7 --- /dev/null +++ b/database/data/website-admin/website_menus.json @@ -0,0 +1,44 @@ +[ + { + "site_id": 1, + "slug": "main-header", + "title": "Menú Principal", + "description": "Navegación principal del sitio web Cleanfy.", + "is_active": true + }, + { + "site_id": 1, + "slug": "footer", + "title": "Menú Footer", + "description": "Navegación del footer del sitio web Cleanfy.", + "is_active": true + }, + { + "site_id": 2, + "slug": "main-header", + "title": "Menú Principal", + "description": "Navegación principal del sitio web Agroform.", + "is_active": true + }, + { + "site_id": 2, + "slug": "footer", + "title": "Menú Footer", + "description": "Navegación del footer del sitio web Agroform.", + "is_active": true + }, + { + "site_id": 3, + "slug": "main-header", + "title": "Menú Principal", + "description": "Navegación principal del sitio web Realcity.", + "is_active": true + }, + { + "site_id": 3, + "slug": "footer", + "title": "Menú Footer", + "description": "Navegación del footer del sitio web Realcity.", + "is_active": true + } +] diff --git a/database/data/website-admin/website_realcity_contents.json b/database/data/website-admin/website_realcity_contents.json new file mode 100644 index 0000000..812e174 --- /dev/null +++ b/database/data/website-admin/website_realcity_contents.json @@ -0,0 +1,192 @@ +[ + { + "site_id": 3, + "title": "Realcity", + "slug": "", + "description": "Bienvenido a realcity, soluciones tecnológicas para empresas.", + "keywords": ["realcity", "erp", "tecnología", "soluciones"], + "template": "home", + "type": "page", + "is_partial": false, + "seo_profile_id": "inicio-realcity", + "seo_overrides": null, + "canonical_url": "https://realcity.com.mx", + "content_blocks": [ + {"type": "hero", "title": "Soluciones Tecnológicas para tu Empresa", "subtitle": "Desde ERP hasta eCommerce", "image": "/images/hero.jpg"}, + {"type": "features", "items": [{"icon": "ti ti-server", "label": "Infraestructura"}, {"icon": "ti ti-code", "label": "Desarrollo a medida"}]}, + {"type": "cta", "text": "Solicita una demo sin costo", "button": "Agendar llamada"} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Nosotros", + "slug": "nosotros", + "description": "Conoce la historia, visión y equipo de Koneko.", + "keywords": ["nosotros", "equipo", "historia", "koneko"], + "template": "default", + "type": "page", + "is_partial": false, + "seo_profile_id": "inicio-realcity", + "seo_overrides": null, + "canonical_url": "https://realcity.com.mx/nosotros", + "content_blocks": [ + {"type": "text", "heading": "¿Quiénes somos?", "content": "Somos una empresa de tecnología con sede en México..."}, + {"type": "team", "members": [{"name": "Arturo", "role": "Director Técnico"}, {"name": "Laura", "role": "Marketing"}]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Servicios", + "slug": "servicios", + "description": "Explora nuestros servicios: ERP, eCommerce, hosting y más.", + "keywords": ["erp", "servicios", "realcity", "sistemas"], + "template": "services", + "type": "page", + "is_partial": false, + "seo_profile_id": "servicios-realcity", + "seo_overrides": null, + "canonical_url": "https://realcity.com.mx/servicios", + "content_blocks": [ + {"type": "service_list", "items": [ + {"title": "ERP Realcity", "description": "Sistema de gestión empresarial completo"}, + {"title": "Tienda Virtual", "description": "Ecommerce 100% administrable con SEO nativo"}, + {"title": "Infraestructura", "description": "Servidores, VPS, Proxmox y más"} + ]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Contacto", + "slug": "contacto", + "description": "Contáctanos para resolver tus dudas o agendar una demo.", + "keywords": ["contacto", "soporte", "demo", "ayuda"], + "template": "contact", + "type": "page", + "is_partial": false, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": "https://realcity.com.mx/contacto", + "content_blocks": [ + {"type": "form", "fields": ["nombre", "email", "mensaje"], "submit_label": "Enviar mensaje"}, + {"type": "map", "lat": 19.4326, "lng": -99.1332} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Aviso de Privacidad", + "slug": "aviso-privacidad", + "description": "Consulta nuestro aviso de privacidad actualizado.", + "keywords": ["legal", "privacidad", "datos personales"], + "template": "legal", + "type": "page", + "is_partial": false, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": "https://realcity.com.mx/aviso-privacidad", + "content_blocks": [ + {"type": "text", "heading": "Política de privacidad", "content": "Tus datos son importantes para nosotros..."} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Preguntas Frecuentes", + "slug": "faq", + "description": "Resuelve tus dudas rápidamente con nuestras respuestas frecuentes.", + "keywords": ["faq", "dudas", "soporte"], + "template": "faq", + "type": "page", + "is_partial": true, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "faq", "items": [ + {"q": "¿Tienen soporte técnico?", "a": "Sí, disponible 24/7 para clientes activos."}, + {"q": "¿Qué formas de pago aceptan?", "a": "Transferencia, tarjeta y criptomonedas."} + ]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Header Principal", + "slug": "header-principal", + "description": "Encabezado superior con navegación.", + "keywords": [], + "template": null, + "type": "partial", + "is_partial": true, + "seo_profile_id": 1, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "menu", "menu_slug": "principal"} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Footer General", + "slug": "footer-general", + "description": "Pie de página con enlaces y datos legales.", + "keywords": [], + "template": null, + "type": "partial", + "is_partial": true, + "seo_profile_id": null, + "seo_overrides": null, + "canonical_url": null, + "content_blocks": [ + {"type": "footer", "links": ["inicio", "servicios", "contacto"]} + ], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Categoría: Blog", + "slug": "blog", + "description": "Explora nuestros artículos de tecnología y negocios.", + "keywords": ["blog", "noticias", "tips", "artículos"], + "template": "blog_index", + "type": "category", + "is_partial": false, + "seo_profile_id": "blog", + "seo_overrides": null, + "canonical_url": "https://realcity.com.mx/blog", + "content_blocks": [], + "is_draft": false, + "is_sensitive": false + }, + { + "site_id": 3, + "title": "Artículo: Ventajas del ERP", + "slug": "blog/ventajas-del-erp", + "description": "Descubre cómo un ERP puede transformar tu empresa.", + "keywords": ["erp", "ventajas", "productividad", "sistemas"], + "template": "blog_article", + "type": "blog", + "is_partial": false, + "seo_profile_id": "promocion-computadoras-gamer", + "seo_overrides": null, + "canonical_url": "https://realcity.com.mx/blog/ventajas-del-erp", + "content_blocks": [ + {"type": "text", "heading": "Beneficios del ERP", "content": "Centralización de procesos, automatización..."}, + {"type": "image", "src": "/images/blog/erp-benefits.jpg", "alt": "ERP y eficiencia empresarial"} + ], + "is_draft": false, + "is_sensitive": false + } +] diff --git a/database/data/website-admin/website_realcity_menus.json b/database/data/website-admin/website_realcity_menus.json new file mode 100644 index 0000000..a3065a3 --- /dev/null +++ b/database/data/website-admin/website_realcity_menus.json @@ -0,0 +1,92 @@ +[ + { + "menu_slug": "main-header", + "title": { + "es": "Inicio", + "en": "Home" + }, + "type": "cms_page", + "order": 1, + "icon": "ti ti-home" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Nuestro corporativo", + "en": "About Us" + }, + "type": "cms_page", + "order": 2, + "icon": "ti ti-users" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Soluciones y servicios", + "en": "Solutions and services" + }, + "type": "cms_page", + "order": 3, + "icon": "ti ti-briefcase" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Valor y Beneficios", + "en": "Value and Benefits" + }, + "type": "cms_page", + "order": 4, + "icon": "ti ti-briefcase" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Experiencia de éxito", + "en": "Success Experience" + }, + "type": "cms_page", + "order": 5, + "icon": "ti ti-briefcase" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Blog y contenido", + "en": "Blog and content" + }, + "type": "blog_article", + "order": 6, + "icon": "ti ti-news" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Residentes y usuarios", + "en": "Residents and users" + }, + "type": "cms_page", + "order": 7, + "icon": "ti ti-users" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Contacto", + "en": "Contact" + }, + "type": "action", + "order": 8, + "icon": "ti ti-phone-call" + }, + { + "menu_slug": "main-header", + "title": { + "es": "Legales", + "en": "Legal" + }, + "type": "cms_page", + "order": 9, + "icon": "ti ti-file" + } +] diff --git a/database/data/website-admin/website_seo_profiles.json b/database/data/website-admin/website_seo_profiles.json new file mode 100644 index 0000000..ea18d03 --- /dev/null +++ b/database/data/website-admin/website_seo_profiles.json @@ -0,0 +1,145 @@ +[ + { + "site_id": 1, + "type": "page", + "title": "AgroForm México | Jardinería Integral con Tecnología y Sostenibilidad", + "slug": "agroform-home", + "description": "AgroForm México transforma espacios verdes desde 2017 con servicios de jardinería, mantenimiento y diseño innovador. Combinamos sostenibilidad, tecnología y excelencia para embellecer y proteger tu inversión.", + "schema_org": { + "@context": "https://schema.org", + "@type": "Organization", + "name": "AgroForm México", + "url": "https://agroform.com.mx", + "logo": "https://agroform.com.mx/assets/logo.png", + "description": "Servicios de jardinería, mantenimiento de áreas verdes y paisajismo sostenible en México.", + "foundingDate": "2017", + "areaServed": "MX", + "sameAs": [ + "https://www.facebook.com/agroformmx", + "https://www.instagram.com/agroformmx" + ] + }, + "noindex": false, + "nofollow": false, + "locale": "es-MX", + "geo_location": { + "geo.region": "MX", + "geo.placename": "México" + }, + "og_type": "website", + "og_title": "AgroForm México | Jardinería Integral con Tecnología y Sostenibilidad", + "og_description": "Creamos y cuidamos jardines, parques y espacios verdes sostenibles con tecnología de punta y personal altamente capacitado.", + "og_image": "https://agroform.com.mx/assets/og-image.jpg", + "og_url": "https://agroform.com.mx", + "og_site_name": "AgroForm México", + "twitter_card": "summary_large_image", + "twitter_title": "AgroForm México", + "twitter_description": "Soluciones integrales en jardinería y paisajismo sostenible para parques y espacios verdes.", + "twitter_image": "https://agroform.com.mx/assets/twitter-card.jpg", + "twitter_site": "@agroformmx", + "twitter_creator": "@agroformmx", + "json_ld": { + "@context": "https://schema.org", + "@type": "WebPage", + "name": "AgroForm México", + "url": "https://agroform.com.mx", + "description": "Empresa mexicana líder en jardinería integral, mantenimiento de áreas verdes y paisajismo con enfoque tecnológico y sostenible." + }, + "created_by": 1, + "updated_by": 1, + "created_at": "2025-05-24T17:00:00", + "updated_at": "2025-05-24T17:00:00" + }, + { + "site_id": 2, + "type": "page", + "title": "CleanFy México | Soluciones Integrales de Limpieza y Sanitización", + "slug": "cleanfy-home", + "description": "CleanFy es tu socio estratégico en limpieza profesional y sanitización en México. Con personal capacitado, tecnología avanzada y enfoque sostenible, garantizamos espacios seguros, limpios y saludables.", + "schema_org": { + "@context": "https://schema.org", + "@type": "Organization", + "name": "CleanFy México", + "url": "https://cleanfy.mx", + "logo": "https://cleanfy.mx/assets/logo.png", + "description": "Empresa líder en soluciones integrales de limpieza, sanitización y mantenimiento para hogares, oficinas, industrias y más.", + "foundingDate": "2017", + "areaServed": "MX", + "sameAs": [ + "https://www.facebook.com/cleanfymx", + "https://www.instagram.com/cleanfymx" + ] + }, + "noindex": false, + "nofollow": false, + "locale": "es-MX", + "geo_location": { + "geo.region": "MX", + "geo.placename": "México" + }, + "og_type": "website", + "og_title": "CleanFy México | Soluciones Integrales de Limpieza y Sanitización", + "og_description": "Transformamos espacios en ambientes impecables y seguros. Limpieza profesional, protocolos tecnológicos, y compromiso con la calidad y la salud.", + "og_image": "https://cleanfy.mx/assets/og-image.jpg", + "og_url": "https://cleanfy.mx", + "og_site_name": "CleanFy México", + "twitter_card": "summary_large_image", + "twitter_title": "CleanFy México", + "twitter_description": "Empresa profesional de limpieza y sanitización con tecnología, compromiso y excelencia.", + "twitter_image": "https://cleanfy.mx/assets/twitter-card.jpg", + "twitter_site": "@cleanfymx", + "twitter_creator": "@cleanfymx", + "json_ld": { + "@context": "https://schema.org", + "@type": "WebPage", + "name": "CleanFy México", + "url": "https://cleanfy.mx", + "description": "Empresa mexicana especializada en limpieza y sanitización profesional para empresas, oficinas y hogares. Tecnología, personal capacitado y sostenibilidad al servicio de la salud y productividad." + } + } + , + { + "site_id": 3, + "type": "page", + "title": "RealCity México | Próximamente la Nueva Plataforma Inmobiliaria", + "slug": "realcity-coming-soon", + "description": "Prepárate para una experiencia inmobiliaria moderna y eficiente. RealCity está por llegar para transformar la forma de comprar, vender y rentar propiedades en México.", + "schema_org": { + "@context": "https://schema.org", + "@type": "Organization", + "name": "RealCity México", + "url": "https://realcity.com.mx", + "logo": "https://realcity.com.mx/assets/logo.png", + "description": "Plataforma inmobiliaria innovadora en México, especializada en conectar compradores, vendedores e inversionistas mediante tecnología moderna.", + "foundingDate": "2025", + "areaServed": "MX" + }, + "noindex": false, + "nofollow": false, + "locale": "es-MX", + "geo_location": { + "geo.region": "MX", + "geo.placename": "México" + }, + "og_type": "website", + "og_title": "RealCity México | Próximamente la Nueva Plataforma Inmobiliaria", + "og_description": "Muy pronto una nueva experiencia digital para el sector inmobiliario. Encuentra la propiedad ideal con RealCity México.", + "og_image": "https://realcity.com.mx/assets/og-image.jpg", + "og_url": "https://realcity.com.mx", + "og_site_name": "RealCity México", + "twitter_card": "summary_large_image", + "twitter_title": "RealCity México", + "twitter_description": "Innovación inmobiliaria en México. RealCity es la nueva forma de buscar, publicar y cerrar negocios en bienes raíces.", + "twitter_image": "https://realcity.com.mx/assets/twitter-card.jpg", + "twitter_site": "@realcitymx", + "twitter_creator": "@realcitymx", + "json_ld": { + "@context": "https://schema.org", + "@type": "WebPage", + "name": "RealCity México", + "url": "https://realcity.com.mx", + "description": "RealCity ofrece una plataforma tecnológica avanzada para transformar la experiencia inmobiliaria en México, con servicios digitales para compra, venta y renta de propiedades." + } + } + +] diff --git a/database/data/website-admin/website_sites.json b/database/data/website-admin/website_sites.json new file mode 100644 index 0000000..950d5ee --- /dev/null +++ b/database/data/website-admin/website_sites.json @@ -0,0 +1,220 @@ +[ + { + "name": "cleanfy.mx", + "slug": "cleanfy-mx", + "domain": "cleanfy.mx", + "template": "cleaning-services", + "status": "active", + "is_active": true, + "is_indexable": true, + "meta_title": "Limpieza Profesional - Cleanfy", + "meta_description": "Servicios de limpieza profesional y desinfección para empresas y hogares en México.", + "canonical_url": "https://cleanfy.mx", + "locale": "es-MX", + "robots_directives": "index,follow", + "config": { + "favicon": "/assets/cleanfy/favicon.ico", + "primary_color": "#27ae60", + "logo": "/assets/cleanfy/logo.svg" + }, + "social": { + "og:title": "Cleanfy.mx", + "og:image": "/assets/cleanfy/og-image.jpg", + "twitter:card": "summary_large_image", + "twitter:site": "@cleanfymx" + } + }, + { + "name": "agroform.com.mx", + "slug": "agroform-com-mx", + "domain": "agroform.com.mx", + "template": "renewable-energy", + "status": "coming_soon", + "is_active": true, + "is_indexable": false, + "meta_title": "AgroForm | Agricultura Inteligente", + "meta_description": "Soluciones tecnológicas para productores agrícolas: sensores, plataformas y más.", + "canonical_url": "https://agroform.com.mx", + "locale": "es-MX", + "robots_directives": "noindex,nofollow", + "config": { + "maintenance_page": "coming-soon", + "primary_color": "#388e3c", + "logo": "/assets/agroform/logo.svg" + }, + "social": { + "og:title": "AgroForm", + "og:description": "Innovación para el campo mexicano.", + "twitter:site": "@agroformmx" + } + }, + { + "name": "realcity.com.mx", + "slug": "realcity-com-mx", + "domain": "realcity.com.mx", + "template": "business-consulting-4", + "status": "active", + "is_active": true, + "is_indexable": true, + "meta_title": "RealCity | Bienes Raíces Premium", + "meta_description": "Compra y renta de propiedades en zonas exclusivas. Encuentra tu próximo hogar con RealCity.", + "canonical_url": "https://realcity.com.mx", + "locale": "es-MX", + "robots_directives": "index,follow", + "config": { + "template_variant": "real-estate", + "primary_color": "#34495e", + "logo": "/assets/realcity/logo.svg" + }, + "social": { + "og:title": "RealCity", + "og:image": "/assets/realcity/og-image.jpg", + "og:type": "website", + "twitter:site": "@realcitymx" + } + }, + { + "name": "anonymous.test", + "slug": "anonymous-test", + "domain": "anonymous.test", + "template": "anonymous", + "status": "active", + "is_active": true, + "is_indexable": true, + "meta_title": "Anonymous", + "meta_description": "Anonymous", + "canonical_url": "https://anonymous.test", + "locale": "es-MX", + "robots_directives": "index,follow", + "config": { + "primary_color": "#34495e", + "logo": "/assets/anonymous/logo.svg" + }, + "social": { + "og:title": "Anonymous", + "og:image": "/assets/anonymous/og-image.jpg", + "og:type": "website", + "twitter:site": "@anonymousmx" + } + }, + { + "name": "landwind.test", + "slug": "landwind-test", + "domain": "landwind.test", + "template": "landwind", + "status": "active", + "is_active": true, + "is_indexable": true, + "meta_title": "Landwind", + "meta_description": "Landwind", + "canonical_url": "https://landwind.test", + "locale": "es-MX", + "robots_directives": "index,follow", + "config": { + "primary_color": "#34495e", + "logo": "/assets/landwind/logo.svg" + }, + "social": { + "og:title": "Landwind", + "og:image": "/assets/landwind/og-image.jpg", + "og:type": "website", + "twitter:site": "@landwindmx" + } + }, + { + "name": "limaa-m.test", + "slug": "limaa-m-test", + "domain": "limaa-m.test", + "template": "limaa-m", + "status": "active", + "is_active": true, + "is_indexable": true, + "meta_title": "Limaa M", + "meta_description": "Limaa M", + "canonical_url": "https://limaa-m.test", + "locale": "es-MX", + "robots_directives": "index,follow", + "config": { + "primary_color": "#34495e", + "logo": "/assets/limaa-m/logo.svg" + }, + "social": { + "og:title": "Limaa M", + "og:image": "/assets/limaa-m/og-image.jpg", + "og:type": "website", + "twitter:site": "@limaa-mmx" + } + }, + { + "name": "maximus.test", + "slug": "maximus-test", + "domain": "maximus.test", + "template": "maximus", + "status": "active", + "is_active": true, + "is_indexable": true, + "meta_title": "Maximus", + "meta_description": "Maximus", + "canonical_url": "https://maximus.test", + "locale": "es-MX", + "robots_directives": "index,follow", + "config": { + "primary_color": "#34495e", + "logo": "/assets/maximus/logo.svg" + }, + "social": { + "og:title": "Maximus", + "og:image": "/assets/maximus/og-image.jpg", + "og:type": "website", + "twitter:site": "@maximusmx" + } + }, + { + "name": "notus.test", + "slug": "notus-test", + "domain": "notus.test", + "template": "notus", + "status": "active", + "is_active": true, + "is_indexable": true, + "meta_title": "Notus", + "meta_description": "Notus", + "canonical_url": "https://notus.test", + "locale": "es-MX", + "robots_directives": "index,follow", + "config": { + "primary_color": "#34495e", + "logo": "/assets/notus/logo.svg" + }, + "social": { + "og:title": "Notus", + "og:image": "/assets/notus/og-image.jpg", + "og:type": "website", + "twitter:site": "@notusmx" + } + }, + { + "name": "samuel-coming-soon.test", + "slug": "samuel-coming-soon-test", + "domain": "samuel-coming-soon.test", + "template": "samuel-coming-soon", + "status": "active", + "is_active": true, + "is_indexable": true, + "meta_title": "Samuel Coming Soon", + "meta_description": "Samuel Coming Soon", + "canonical_url": "https://samuel-coming-soon.test", + "locale": "es-MX", + "robots_directives": "index,follow", + "config": { + "primary_color": "#34495e", + "logo": "/assets/samuel-coming-soon/logo.svg" + }, + "social": { + "og:title": "Samuel Coming Soon", + "og:image": "/assets/samuel-coming-soon/og-image.jpg", + "og:type": "website", + "twitter:site": "@samuelcomingsoonmx" + } + } + ] diff --git a/database/migrations/2024_12_29_081809_create_website_seo_profiles_table.php b/database/migrations/2024_12_29_081809_create_website_seo_profiles_table.php new file mode 100644 index 0000000..68aada7 --- /dev/null +++ b/database/migrations/2024_12_29_081809_create_website_seo_profiles_table.php @@ -0,0 +1,77 @@ +<?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('website_seo_profiles', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('site_id')->nullable()->index(); + + // Metadata + $table->string('type', 16)->default('page')->index(); // Enum: Content, Landing, Product, Category Blog + $table->string('title')->nullable()->index(); // Título del perfil + $table->string('slug')->unique(); + $table->mediumText('description')->nullable(); // Descripción del perfil + + // Tipos de schema adicionales + $table->json('schema_org')->nullable(); + + // Robots Directives + $table->boolean('noindex')->default(false); + $table->boolean('nofollow')->default(false); + + // Idioma y Geolocalización + $table->string('locale', 8)->default('es-MX')->index(); // Para SEO internacional + $table->json('geo_location')->nullable(); // meta geo.region y geo.placename + + // Open Graph + $table->string('og_type')->nullable(); + $table->string('og_title')->nullable(); + $table->text('og_description')->nullable(); + $table->string('og_image')->nullable(); + $table->string('og_url')->nullable(); + $table->string('og_site_name')->nullable(); + + // Twitter Card + $table->string('twitter_card')->default('summary_large_image'); + $table->string('twitter_title')->nullable(); + $table->text('twitter_description')->nullable(); + $table->string('twitter_image')->nullable(); + $table->string('twitter_site')->nullable(); + $table->string('twitter_creator')->nullable(); + + // JSON-LD opcional (almacenado como bloque JSON) + $table->json('json_ld')->nullable(); + + // Auditoria + $table->unsignedMediumInteger('created_by')->nullable(); + $table->unsignedMediumInteger('updated_by')->nullable(); + $table->timestamps(); + + // Indices + $table->index(['site_id', 'type']); + $table->index(['site_id', 'type', 'slug']); + + // Relaciones + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('website_seo_profiles'); + } +}; diff --git a/database/migrations/2024_12_29_081811_create_website_sites_table.php b/database/migrations/2024_12_29_081811_create_website_sites_table.php new file mode 100644 index 0000000..4610b3a --- /dev/null +++ b/database/migrations/2024_12_29_081811_create_website_sites_table.php @@ -0,0 +1,64 @@ +<?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('website_sites', function (Blueprint $table) { + $table->smallIncrements('id'); + + // Identidad + $table->string('name')->index(); // Nombre visible en admin + $table->string('slug')->unique(); // Clave técnica + $table->string('domain')->unique(); // Dominio principal (sin protocolo) + $table->string('template')->nullable(); // Componente layout activo + + // Estado + $table->string('status', 16)->default('active')->index(); // Estados especiales del sitio + $table->boolean('is_indexable')->default(true)->index(); // SEO: permitir indexado o no + + // SEO + $table->unsignedSmallInteger('seo_profile_id')->nullable()->index(); + $table->string('canonical_url')->nullable(); // Canonical para root + + // Configuración + $table->json('config')->nullable(); // favicon, theme, brand, CDN, etc. + + // Auditoría + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + $table->timestamps(); + + // Indices + $table->index(['slug', 'status', 'is_indexable']); + + // Relaciones + $table->foreign('seo_profile_id')->references('id')->on('website_seo_profiles')->nullOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + + Schema::table('website_seo_profiles', function (Blueprint $table) { + $table->foreign('site_id')->references('id')->on('website_sites')->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('website_sites'); + + Schema::table('website_seo_profiles', function (Blueprint $table) { + $table->dropForeign(['site_id']); + }); + } +}; diff --git a/database/migrations/2024_12_29_081815_create_website_menus_table.php b/database/migrations/2024_12_29_081815_create_website_menus_table.php new file mode 100644 index 0000000..91bd373 --- /dev/null +++ b/database/migrations/2024_12_29_081815_create_website_menus_table.php @@ -0,0 +1,48 @@ +<?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('website_menus', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('site_id')->index(); + + $table->string('title')->index(); + $table->string('slug')->unique(); + + $table->string('description')->nullable(); + $table->boolean('is_active')->default(true)->index(); + + // Auditoría + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + + $table->timestamps(); + + // Indices + $table->index(['site_id', 'slug', 'is_active']); + + // Relaciones + $table->foreign('site_id')->references('id')->on('website_sites')->cascadeOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('website_menus'); + } +}; diff --git a/database/migrations/2024_12_29_081816_create_website_menu_items_table.php b/database/migrations/2024_12_29_081816_create_website_menu_items_table.php new file mode 100644 index 0000000..c867b4a --- /dev/null +++ b/database/migrations/2024_12_29_081816_create_website_menu_items_table.php @@ -0,0 +1,75 @@ +<?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('website_menu_items', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('menu_id')->index(); + $table->unsignedSmallInteger('parent_id')->nullable()->index(); + + $table->json('title'); // i18n multilanguage + $table->string('type', 16)->default('cms_page')->index(); // Enum; cms_page | url | laravel_route | blog_article | Evento Json + + // Construcción de enlace + $table->unsignedMediumInteger('linkable_id')->nullable()->index(); // Relación polimórfica con: + $table->string('linkable_type')->nullable()->index(); // páginas, entradas, productos, etc. + + $table->string('laravel_route')->nullable()->index(); + $table->string('url')->nullable()->index(); + $table->string('method')->nullable(); + $table->string('target', 16)->nullable(); // Enum _self, _blank, etc. + $table->string('js_event')->nullable(); + + // UI + $table->string('icon')->nullable(); + $table->string('badge')->nullable(); + $table->string('badge_color')->nullable(); + + // Visibilidad + $table->json('roles')->nullable(); + $table->json('permissions')->nullable(); + $table->boolean('hide_if_authenticated')->default(false)->index(); + $table->boolean('hide_if_guest')->default(false)->index(); + $table->timestamp('visible_from')->nullable(); + $table->timestamp('visible_until')->nullable(); + + $table->unsignedSmallInteger('order')->default(0); // Para ordenar en el menú + $table->boolean('is_active')->default(true)->index(); + + // Auditoría + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + + $table->timestamps(); + + // Indices + $table->index(['menu_id', 'is_active']); + $table->index(['menu_id', 'parent_id', 'is_active']); + $table->index(['linkable_id', 'linkable_type']); + + // Relaciones + $table->foreign('menu_id')->references('id')->on('website_menus')->cascadeOnDelete(); + $table->foreign('parent_id')->references('id')->on('website_menu_items')->nullOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('website_menu_items'); + } +}; diff --git a/database/migrations/2024_12_29_082518_create_website_contents_table.php b/database/migrations/2024_12_29_082518_create_website_contents_table.php new file mode 100644 index 0000000..f69d6cb --- /dev/null +++ b/database/migrations/2024_12_29_082518_create_website_contents_table.php @@ -0,0 +1,84 @@ +<?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('website_contents', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('site_id')->index(); + $table->unsignedSmallInteger('seo_profile_id')->nullable()->index(); + + // Metadata + $table->string('title')->index(); + $table->string('slug')->unique(); + $table->string('description'); + $table->json('keywords')->nullable(); + + // Template & Type content + $table->string('template')->nullable(); + $table->string('template_variant')->nullable(); + $table->string('type', 16)->default('page')->index(); // Enum: Content, Landing, Product, Category Blog, Partial + $table->string('render_mode', 16)->default('static'); + $table->string('block_mode', 16)->default('db'); + $table->string('source', 16)->default('db'); + $table->string('render_as')->nullable(); + + //canonical url + $table->string('canonical_url')->nullable(); + + // Content + $table->json('content_blocks')->nullable(); // Bloques estructurados + $table->json('seo_overrides')->nullable(); + + // Control + $table->boolean('is_draft')->default(true)->index(); + $table->boolean('is_sensitive')->default(false)->index(); // Allow dangerous Blade content + $table->boolean('is_partial')->default(false)->index(); + + // Visibilidad + $table->json('roles')->nullable(); + $table->json('permissions')->nullable(); + $table->boolean('hide_if_authenticated')->default(false)->index(); + $table->boolean('hide_if_guest')->default(false)->index(); + $table->timestamp('visible_from')->nullable(); + $table->timestamp('visible_until')->nullable(); + + // Cache + $table->boolean('enable_cache')->default(true)->index(); + $table->unsignedSmallInteger('cache_ttl')->default(60); // minutos + + // Auditoria + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + + $table->timestamps(); + + // Indices + $table->index(['site_id', 'type']); + $table->index(['site_id', 'type', 'slug']); + + // Relaciones + $table->foreign('site_id')->references('id')->on('website_sites')->cascadeOnDelete(); + $table->foreign('seo_profile_id')->references('id')->on('website_seo_profiles')->nullOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('website_contents'); + } +}; diff --git a/database/migrations/2024_12_29_082520_create_website_content_blocks_table.php b/database/migrations/2024_12_29_082520_create_website_content_blocks_table.php new file mode 100644 index 0000000..233a943 --- /dev/null +++ b/database/migrations/2024_12_29_082520_create_website_content_blocks_table.php @@ -0,0 +1,48 @@ +<?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('website_content_blocks', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('content_id')->index(); + $table->unsignedSmallInteger('parent_id')->nullable()->index(); + + $table->string('slug')->nullable(); + $table->string('type', 32); + $table->string('mode', 16)->default('view'); + $table->string('view_path')->nullable(); + $table->string('component_class')->nullable(); + + $table->boolean('is_enabled')->default(true); + $table->boolean('enable_cache')->default(true); + $table->unsignedSmallInteger('cache_ttl')->default(60); + + $table->json('settings')->nullable(); + $table->json('data')->nullable(); + $table->unsignedSmallInteger('order')->default(0); + + $table->timestamps(); + + $table->foreign('content_id')->references('id')->on('website_contents')->cascadeOnDelete(); + $table->foreign('parent_id')->references('id')->on('website_content_blocks')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('website_content_blocks'); + } +}; diff --git a/database/migrations/2024_12_29_082521_create_website_content_versions_table.php b/database/migrations/2024_12_29_082521_create_website_content_versions_table.php new file mode 100644 index 0000000..6c55a5d --- /dev/null +++ b/database/migrations/2024_12_29_082521_create_website_content_versions_table.php @@ -0,0 +1,42 @@ +<?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('website_content_versions', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('website_content_id')->index(); + $table->string('version_label')->nullable(); + $table->longText('content'); + $table->json('metadata')->nullable(); + + // Auditoria + $table->unsignedMediumInteger('created_by')->nullable(); + $table->unsignedMediumInteger('updated_by')->nullable(); + + $table->timestamps(); + + // Relaciones + $table->foreign('website_content_id')->references('id')->on('website_contents')->cascadeOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('website_content_versions'); + } +}; diff --git a/database/migrations/2024_12_29_090535_create_sitemap_profiles_table.php b/database/migrations/2024_12_29_090535_create_sitemap_profiles_table.php new file mode 100644 index 0000000..52b0b01 --- /dev/null +++ b/database/migrations/2024_12_29_090535_create_sitemap_profiles_table.php @@ -0,0 +1,44 @@ +<?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_profiles', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('site_id')->index(); // Soporte multisite + + $table->string('name'); // Nombre del perfil: 'Productos', 'Páginas CMS' + $table->string('slug')->unique(); // Clave técnica + + $table->string('entity_type')->nullable(); // Ej: App\Models\Product + $table->string('generator_class')->nullable(); // Clase que implementa SitemapUrlGeneratorInterface + + $table->boolean('is_active')->default(true)->index(); + // Auditoría + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + + $table->timestamps(); + + // Indices + $table->index(['site_id', 'slug']); + $table->index(['site_id', 'slug', 'is_active']); + + // Relaciones + $table->foreign('site_id')->references('id')->on('website_sites')->cascadeOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sitemap_profiles'); + } +}; diff --git a/database/migrations/2024_12_29_090537_create_sitemap_rules_table.php b/database/migrations/2024_12_29_090537_create_sitemap_rules_table.php new file mode 100644 index 0000000..88f426a --- /dev/null +++ b/database/migrations/2024_12_29_090537_create_sitemap_rules_table.php @@ -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 +{ + public function up(): void + { + Schema::create('sitemap_rules', function (Blueprint $table) { + $table->mediumIncrements('id'); + + $table->unsignedSmallInteger('sitemap_profile_id')->index(); + $table->string('rule_type'); // Ej: 'priority_override', 'exclude_flag', etc. + $table->json('rule_data'); // JSON con parámetros + + $table->timestamps(); + + // Indices + $table->index(['sitemap_profile_id', 'rule_type']); + + // Relaciones + $table->foreign('sitemap_profile_id')->references('id')->on('sitemap_profiles')->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sitemap_rules'); + } +}; diff --git a/database/migrations/create_sitemap_urls_table.php b/database/migrations/2024_12_29_090539_create_sitemap_urls_table.php similarity index 53% rename from database/migrations/create_sitemap_urls_table.php rename to database/migrations/2024_12_29_090539_create_sitemap_urls_table.php index 56a1541..6bb7b9b 100644 --- a/database/migrations/create_sitemap_urls_table.php +++ b/database/migrations/2024_12_29_090539_create_sitemap_urls_table.php @@ -12,13 +12,26 @@ return new class extends Migration public function up(): void { Schema::create('sitemap_urls', function (Blueprint $table) { - $table->id(); + $table->mediumIncrements('id'); + + $table->unsignedSmallInteger('sitemap_profile_id')->index(); $table->string('url')->unique(); - $table->enum('changefreq', ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'])->default('weekly'); + + $table->string('changefreq', 16)->default('weekly'); // Enum $table->decimal('priority', 2, 1)->default(0.5); $table->timestamp('lastmod')->nullable(); - $table->boolean('is_active')->default(true); + + $table->boolean('is_active')->default(true)->index(); + $table->json('alternate_locales')->nullable(); // SEO internacional + + // Auditoría $table->timestamps(); + + // Indices + $table->index(['sitemap_profile_id', 'is_active']); + + // Relaciones + $table->foreign('sitemap_profile_id')->references('id')->on('sitemap_profiles')->cascadeOnDelete(); }); } diff --git a/database/migrations/2024_12_29_090548_create_sitemap_index_files_table.php b/database/migrations/2024_12_29_090548_create_sitemap_index_files_table.php new file mode 100644 index 0000000..5390c20 --- /dev/null +++ b/database/migrations/2024_12_29_090548_create_sitemap_index_files_table.php @@ -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 +{ + public function up(): void + { + Schema::create('sitemap_index_files', function (Blueprint $table) { + $table->mediumIncrements('id'); + + $table->unsignedSmallInteger('sitemap_profile_id')->index(); + $table->string('file_name')->unique(); + $table->string('url'); + $table->timestamp('generated_at'); + + $table->integer('url_count')->default(0); + $table->boolean('is_current')->default(true); + + $table->timestamps(); + + $table->foreign('sitemap_profile_id')->references('id')->on('sitemap_profiles')->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sitemap_index_files'); + } +}; diff --git a/database/migrations/2024_12_29_081812_create_faq_categories_table.php b/database/migrations/2024_12_30_082012_create_faq_categories_table.php similarity index 50% rename from database/migrations/2024_12_29_081812_create_faq_categories_table.php rename to database/migrations/2024_12_30_082012_create_faq_categories_table.php index c2877d8..d370876 100644 --- a/database/migrations/2024_12_29_081812_create_faq_categories_table.php +++ b/database/migrations/2024_12_30_082012_create_faq_categories_table.php @@ -14,13 +14,27 @@ return new class extends Migration Schema::create('faq_categories', function (Blueprint $table) { $table->smallIncrements('id'); - $table->string('name')->unique(); + $table->unsignedSmallInteger('site_id')->index(); // Soporte multisite + + $table->string('name')->index(); $table->string('icon')->nullable(); $table->unsignedInteger('order')->default(0)->index(); $table->boolean('is_active')->default(true)->index(); // Auditoria + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + $table->timestamps(); + + // Indices + $table->unique(['name', 'site_id']); + $table->index(['name', 'is_active', 'site_id']); + + // Relaciones + $table->foreign('site_id')->references('id')->on('website_sites')->cascadeOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); }); } diff --git a/database/migrations/2024_12_29_081815_create_faqs_table.php b/database/migrations/2024_12_30_082017_create_faqs_table.php similarity index 63% rename from database/migrations/2024_12_29_081815_create_faqs_table.php rename to database/migrations/2024_12_30_082017_create_faqs_table.php index 22ebe1f..486a4fa 100644 --- a/database/migrations/2024_12_29_081815_create_faqs_table.php +++ b/database/migrations/2024_12_30_082017_create_faqs_table.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { Schema::create('faqs', function (Blueprint $table) { - $table->id(); + $table->smallIncrements('id'); $table->unsignedSmallInteger('category_id')->nullable()->index(); $table->string('question'); @@ -21,10 +21,18 @@ return new class extends Migration $table->boolean('is_active')->default(true)->index(); // Auditoria + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + $table->timestamps(); + // Indices + $table->index(['category_id', 'is_active']); + // Relaciones - $table->foreign('category_id')->references('id')->on('faq_categories')->nullOnDelete(); + $table->foreign('category_id')->references('id')->on('faq_categories')->restrictOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); }); } diff --git a/database/migrations/2024_12_30_082125_create_blog_categories_table.php b/database/migrations/2024_12_30_082125_create_blog_categories_table.php new file mode 100644 index 0000000..a99754b --- /dev/null +++ b/database/migrations/2024_12_30_082125_create_blog_categories_table.php @@ -0,0 +1,60 @@ +<?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('blog_categories', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('site_id')->index(); // Soporte multisite + + $table->string('name')->index(); + $table->string('slug')->index(); + $table->string('full_slug')->nullable()->index(); // NUEVO: Ruta cacheada + + $table->unsignedSmallInteger('parent_id')->nullable()->index(); + + $table->string('icon')->nullable(); // NUEVO: Soporte para icono + $table->string('group')->nullable()->index(); // NUEVO: Soporte para agrupación (ej: blog, noticias, promociones) + + + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true)->index(); + + // Auditoría + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + + $table->timestamps(); + + // Relaciones + $table->foreign('parent_id')->references('id')->on('blog_categories')->nullOnDelete(); + + // Indices + $table->index(['name', 'is_active', 'site_id']); + $table->unique(['slug', 'site_id']); + + // Relaciones + $table->foreign('site_id')->references('id')->on('website_sites')->cascadeOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_categories'); + } +}; diff --git a/database/migrations/2024_12_30_082126_create_blog_tags_table.php b/database/migrations/2024_12_30_082126_create_blog_tags_table.php new file mode 100644 index 0000000..899b36f --- /dev/null +++ b/database/migrations/2024_12_30_082126_create_blog_tags_table.php @@ -0,0 +1,48 @@ +<?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('blog_tags', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('site_id')->index(); // Soporte multisite + + $table->string('name')->index(); + $table->string('slug')->index(); + + $table->boolean('is_active')->default(true)->index(); + + // Auditoria + $table->unsignedMediumInteger('created_by')->nullable()->index(); + $table->unsignedMediumInteger('updated_by')->nullable()->index(); + + $table->timestamps(); + + // Indices + $table->unique(['slug', 'site_id']); + $table->index(['name', 'is_active']); + + // Relaciones + $table->foreign('site_id')->references('id')->on('website_sites')->cascadeOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_tags'); + } +}; diff --git a/database/migrations/2024_12_30_082127_create_blog_articles_table.php b/database/migrations/2024_12_30_082127_create_blog_articles_table.php new file mode 100644 index 0000000..decdc4e --- /dev/null +++ b/database/migrations/2024_12_30_082127_create_blog_articles_table.php @@ -0,0 +1,53 @@ +<?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('blog_articles', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('category_id')->index(); + + $table->string('title')->index(); + $table->string('slug')->index(); + + $table->text('excerpt')->nullable(); + $table->longText('content'); + $table->json('metadata')->nullable(); + + $table->boolean('is_published')->default(false)->index(); + $table->timestamp('published_at')->nullable()->index(); + + $table->unsignedMediumInteger('created_by')->index(); + $table->unsignedMediumInteger('updated_by')->index(); + + // Auditoria + $table->timestamps(); + + // Indices + $table->unique(['slug', 'category_id']); + $table->index(['title', 'is_published']); + + // Relaciones + $table->foreign('category_id')->references('id')->on('blog_categories')->restrictOnDelete(); + $table->foreign('created_by')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_articles'); + } +}; diff --git a/database/migrations/2024_12_30_082128_create_blog_article_tag_table.php b/database/migrations/2024_12_30_082128_create_blog_article_tag_table.php new file mode 100644 index 0000000..5c0e6d9 --- /dev/null +++ b/database/migrations/2024_12_30_082128_create_blog_article_tag_table.php @@ -0,0 +1,39 @@ +<?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('blog_article_tag', function (Blueprint $table) { + $table->smallIncrements('id'); + + $table->unsignedSmallInteger('blog_article_id')->index(); + $table->unsignedSmallInteger('blog_tag_id')->index(); + + // Auditoria + $table->timestamps(); + + // Indices + $table->index(['blog_article_id', 'blog_tag_id']); + + // Relaciones + $table->foreign('blog_article_id')->references('id')->on('blog_articles')->cascadeOnDelete(); + $table->foreign('blog_tag_id')->references('id')->on('blog_tags')->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_article_tag'); + } +}; diff --git a/database/migrations/2024_12_30_082129_create_blog_comments_table.php b/database/migrations/2024_12_30_082129_create_blog_comments_table.php new file mode 100644 index 0000000..85a6815 --- /dev/null +++ b/database/migrations/2024_12_30_082129_create_blog_comments_table.php @@ -0,0 +1,47 @@ +<?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('blog_comments', function (Blueprint $table) { + $table->mediumIncrements('id'); + + $table->unsignedSmallInteger('blog_article_id')->index(); + + $table->unsignedMediumInteger('author_id')->nullable()->index(); + $table->string('author_name')->index(); + $table->string('author_email')->index(); + $table->text('comment'); + + $table->boolean('is_approved')->default(false)->index(); + + // Auditoria + $table->unsignedMediumInteger('updated_by')->index(); + $table->timestamps(); + + // Indices + $table->index(['blog_article_id', 'is_approved']); + + // Relaciones + $table->foreign('blog_article_id')->references('id')->on('blog_articles')->restrictOnDelete(); + $table->foreign('author_id')->references('id')->on('users')->restrictOnDelete(); + $table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_comments'); + } +}; diff --git a/database/migrations/create_sitemap_configurations_table.php b/database/migrations/create_sitemap_configurations_table.php deleted file mode 100644 index 9c53366..0000000 --- a/database/migrations/create_sitemap_configurations_table.php +++ /dev/null @@ -1,23 +0,0 @@ -<?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'); - } -}; \ No newline at end of file diff --git a/database/rbac copy/permissions.json b/database/rbac copy/permissions.json new file mode 100644 index 0000000..260a63b --- /dev/null +++ b/database/rbac copy/permissions.json @@ -0,0 +1,70 @@ +[ + "admin.website-admin.settings.general.view", + "admin.website-admin.settings.general.update", + "admin.website-admin.settings.social.view", + "admin.website-admin.settings.social.update", + "admin.website-admin.settings.indexing.view", + "admin.website-admin.settings.indexing.update", + "admin.website-admin.contact.info.view", + "admin.website-admin.contact.info.update", + "admin.website-admin.contact.form.view", + "admin.website-admin.contact.form.update", + "admin.website-admin.analytics.google-analytics.view", + "admin.website-admin.analytics.google-analytics.update", + "admin.website-admin.analytics.google-tags.view", + "admin.website-admin.analytics.google-tags.update", + "admin.website-admin.analytics.google-search-console.view", + "admin.website-admin.analytics.google-search-console.update", + "admin.website-admin.analytics.pixel-meta.view", + "admin.website-admin.analytics.pixel-meta.update", + "admin.website-admin.comunication.messenger.view", + "admin.website-admin.comunication.messenger.update", + "admin.website-admin.comunication.whatsapp.view", + "admin.website-admin.comunication.whatsapp.update", + "admin.website-admin.comunication.tawk-to.view", + "admin.website-admin.comunication.tawk-to.update", + "admin.website-admin.comunication.twitter.view", + "admin.website-admin.comunication.twitter.update", + "admin.website-admin.translate.google.view", + "admin.website-admin.translate.google.update", + "admin.website-admin.content.faq.view", + "admin.website-admin.content.faq.update", + "admin.website-admin.content.faq.create", + "admin.website-admin.content.faq.delete", + "admin.website-admin.content.gallery.view", + "admin.website-admin.content.gallery.update", + "admin.website-admin.content.gallery.create", + "admin.website-admin.content.gallery.delete", + "admin.website-admin.content.legal.view", + "admin.website-admin.content.legal.update", + "admin.website-admin.content.legal.create", + "admin.website-admin.content.legal.delete", + "admin.website-admin.seo.sitemap.view", + "admin.website-admin.seo.sitemap.update", + "admin.website-admin.seo.jsonld.view", + "admin.website-admin.seo.jsonld.update", + "admin.website-admin.seo.robots.view", + "admin.website-admin.seo.robots.update", + "admin.website-admin.seo.manifest.view", + "admin.website-admin.seo.manifest.update", + "admin.website-admin.seo.canonical.view", + "admin.website-admin.seo.canonical.update", + "admin.website-admin.seo.social-cards.view", + "admin.website-admin.seo.social-cards.update", + "admin.website-admin.blog.categories.view", + "admin.website-admin.blog.categories.update", + "admin.website-admin.blog.categories.create", + "admin.website-admin.blog.categories.delete", + "admin.website-admin.blog.tags.view", + "admin.website-admin.blog.tags.update", + "admin.website-admin.blog.tags.create", + "admin.website-admin.blog.tags.delete", + "admin.website-admin.blog.articles.view", + "admin.website-admin.blog.articles.update", + "admin.website-admin.blog.articles.create", + "admin.website-admin.blog.articles.delete", + "admin.website-admin.blog.comments.view", + "admin.website-admin.blog.comments.update", + "admin.website-admin.blog.comments.create", + "admin.website-admin.blog.comments.delete" +] diff --git a/database/rbac copy/roles.json b/database/rbac copy/roles.json new file mode 100644 index 0000000..a7b0542 --- /dev/null +++ b/database/rbac copy/roles.json @@ -0,0 +1,177 @@ +{ + "SuperAdmin" : { + "permissions" : [ + "admin.website-admin.settings.general.view", + "admin.website-admin.settings.general.update", + "admin.website-admin.settings.social.view", + "admin.website-admin.settings.social.update", + "admin.website-admin.settings.indexing.view", + "admin.website-admin.settings.indexing.update", + "admin.website-admin.contact.info.view", + "admin.website-admin.contact.info.update", + "admin.website-admin.contact.form.view", + "admin.website-admin.contact.form.update", + "admin.website-admin.analytics.google-analytics.view", + "admin.website-admin.analytics.google-analytics.update", + "admin.website-admin.analytics.google-tags.view", + "admin.website-admin.analytics.google-tags.update", + "admin.website-admin.analytics.google-search-console.view", + "admin.website-admin.analytics.google-search-console.update", + "admin.website-admin.analytics.pixel-meta.view", + "admin.website-admin.analytics.pixel-meta.update", + "admin.website-admin.comunication.messenger.view", + "admin.website-admin.comunication.messenger.update", + "admin.website-admin.comunication.whatsapp.view", + "admin.website-admin.comunication.whatsapp.update", + "admin.website-admin.comunication.tawk-to.view", + "admin.website-admin.comunication.tawk-to.update", + "admin.website-admin.comunication.twitter.view", + "admin.website-admin.comunication.twitter.update", + "admin.website-admin.translate.google.view", + "admin.website-admin.translate.google.update", + "admin.website-admin.content.faq.view", + "admin.website-admin.content.faq.update", + "admin.website-admin.content.faq.create", + "admin.website-admin.content.faq.delete", + "admin.website-admin.content.gallery.view", + "admin.website-admin.content.gallery.update", + "admin.website-admin.content.gallery.create", + "admin.website-admin.content.gallery.delete", + "admin.website-admin.content.legal.view", + "admin.website-admin.content.legal.update", + "admin.website-admin.content.legal.create", + "admin.website-admin.content.legal.delete", + "admin.website-admin.seo.sitemap.view", + "admin.website-admin.seo.sitemap.update", + "admin.website-admin.seo.jsonld.view", + "admin.website-admin.seo.jsonld.update", + "admin.website-admin.seo.robots.view", + "admin.website-admin.seo.robots.update", + "admin.website-admin.seo.manifest.view", + "admin.website-admin.seo.manifest.update", + "admin.website-admin.seo.canonical.view", + "admin.website-admin.seo.canonical.update", + "admin.website-admin.seo.social-cards.view", + "admin.website-admin.seo.social-cards.update", + "admin.website-admin.blog.categories.view", + "admin.website-admin.blog.categories.update", + "admin.website-admin.blog.categories.create", + "admin.website-admin.blog.categories.delete", + "admin.website-admin.blog.tags.view", + "admin.website-admin.blog.tags.update", + "admin.website-admin.blog.tags.create", + "admin.website-admin.blog.tags.delete", + "admin.website-admin.blog.articles.view", + "admin.website-admin.blog.articles.update", + "admin.website-admin.blog.articles.create", + "admin.website-admin.blog.articles.delete", + "admin.website-admin.blog.comments.view", + "admin.website-admin.blog.comments.update", + "admin.website-admin.blog.comments.create", + "admin.website-admin.blog.comments.delete" + ] + }, + "Admin" : { + "permissions" : [ + "admin.website-admin.settings.general.view", + "admin.website-admin.settings.general.update", + "admin.website-admin.settings.social.view", + "admin.website-admin.settings.social.update", + "admin.website-admin.settings.indexing.view", + "admin.website-admin.settings.indexing.update", + "admin.website-admin.contact.info.view", + "admin.website-admin.contact.info.update", + "admin.website-admin.contact.form.view", + "admin.website-admin.contact.form.update", + "admin.website-admin.analytics.google-analytics.view", + "admin.website-admin.analytics.google-analytics.update", + "admin.website-admin.analytics.google-tags.view", + "admin.website-admin.analytics.google-tags.update", + "admin.website-admin.analytics.google-search-console.view", + "admin.website-admin.analytics.google-search-console.update", + "admin.website-admin.analytics.pixel-meta.view", + "admin.website-admin.analytics.pixel-meta.update", + "admin.website-admin.comunication.messenger.view", + "admin.website-admin.comunication.messenger.update", + "admin.website-admin.comunication.whatsapp.view", + "admin.website-admin.comunication.whatsapp.update", + "admin.website-admin.comunication.tawk-to.view", + "admin.website-admin.comunication.tawk-to.update", + "admin.website-admin.comunication.twitter.view", + "admin.website-admin.comunication.twitter.update", + "admin.website-admin.translate.google.view", + "admin.website-admin.translate.google.update", + "admin.website-admin.content.faq.view", + "admin.website-admin.content.faq.update", + "admin.website-admin.content.faq.create", + "admin.website-admin.content.faq.delete", + "admin.website-admin.content.gallery.view", + "admin.website-admin.content.gallery.update", + "admin.website-admin.content.gallery.create", + "admin.website-admin.content.gallery.delete", + "admin.website-admin.content.legal.view", + "admin.website-admin.content.legal.update", + "admin.website-admin.content.legal.create", + "admin.website-admin.content.legal.delete", + "admin.website-admin.seo.sitemap.view", + "admin.website-admin.seo.sitemap.update", + "admin.website-admin.seo.jsonld.view", + "admin.website-admin.seo.jsonld.update", + "admin.website-admin.seo.robots.view", + "admin.website-admin.seo.robots.update", + "admin.website-admin.seo.manifest.view", + "admin.website-admin.seo.manifest.update", + "admin.website-admin.seo.canonical.view", + "admin.website-admin.seo.canonical.update", + "admin.website-admin.seo.social-cards.view", + "admin.website-admin.seo.social-cards.update", + "admin.website-admin.blog.categories.view", + "admin.website-admin.blog.categories.update", + "admin.website-admin.blog.categories.create", + "admin.website-admin.blog.categories.delete", + "admin.website-admin.blog.tags.view", + "admin.website-admin.blog.tags.update", + "admin.website-admin.blog.tags.create", + "admin.website-admin.blog.tags.delete", + "admin.website-admin.blog.articles.view", + "admin.website-admin.blog.articles.update", + "admin.website-admin.blog.articles.create", + "admin.website-admin.blog.articles.delete", + "admin.website-admin.blog.comments.view", + "admin.website-admin.blog.comments.update", + "admin.website-admin.blog.comments.create", + "admin.website-admin.blog.comments.delete" + ] + }, + "Auditor" : { + "permissions" : [ + "admin.website-admin.settings.general.view", + "admin.website-admin.settings.social.view", + "admin.website-admin.settings.indexing.view", + "admin.website-admin.contact.info.view", + "admin.website-admin.contact.form.view", + "admin.website-admin.analytics.google-analytics.view", + "admin.website-admin.analytics.google-tags.view", + "admin.website-admin.analytics.google-search-console.view", + "admin.website-admin.analytics.pixel-meta.view", + "admin.website-admin.comunication.messenger.view", + "admin.website-admin.comunication.whatsapp.view", + "admin.website-admin.comunication.tawk-to.view", + "admin.website-admin.comunication.twitter.view", + "admin.website-admin.translate.google.view", + "admin.website-admin.content.faq.view", + "admin.website-admin.content.gallery.view", + "admin.website-admin.content.legal.view", + "admin.website-admin.seo.sitemap.view", + "admin.website-admin.seo.jsonld.view", + "admin.website-admin.seo.robots.view", + "admin.website-admin.seo.manifest.view", + "admin.website-admin.seo.canonical.view", + "admin.website-admin.seo.social-cards.view", + "admin.website-admin.blog.categories.view", + "admin.website-admin.blog.tags.view", + "admin.website-admin.blog.articles.view", + "admin.website-admin.blog.comments.view" + ] + } +} diff --git a/resources/js/chat-settings-card.js b/resources/js/chat-settings-card.js index 5d5bb40..4f7132d 100644 --- a/resources/js/chat-settings-card.js +++ b/resources/js/chat-settings-card.js @@ -1,6 +1,6 @@ -import '@vuexy-admin/notifications/LivewireNotification.js'; -import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; -import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; +//import '@vuexy-admin/assets/js/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/assets/js/forms/formCustomListener'; +//import registerLivewireHookOnce from '@vuexy-admin/assets/js/livewire/registerLivewireHookOnce'; // Inicializar formularios de ajustes de chat window.ChatSettingsForm = new FormCustomListener({ diff --git a/resources/js/contact-form-settings-card.js b/resources/js/contact-form-settings-card.js index f3b69f0..853f884 100644 --- a/resources/js/contact-form-settings-card.js +++ b/resources/js/contact-form-settings-card.js @@ -1,10 +1,10 @@ -import '@vuexy-admin/notifications/LivewireNotification.js'; -import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; -import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; +//import '@vuexy-admin/assets/js/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/assets/js/forms/formCustomListener'; +//import registerLivewireHookOnce from '@vuexy-admin/assets/js/livewire/registerLivewireHookOnce'; // Inicializar formularios de ajustes de Formularios de contacto window.ContactFormSettingsForm = new FormCustomListener({ - formSelector: '#website-contact-form-settings-card', + formSelector: '#website-contact-form-card-card', buttonSelectors: ['.btn-save', '.btn-cancel'], callbacks: [() => {}], dispatchOnSubmit: 'save', @@ -81,6 +81,6 @@ window.ContactFormSettingsForm = new FormCustomListener({ } }); -registerLivewireHookOnce('morphed', 'vuexy-website-admin::contact-form-settings', (component) => { +registerLivewireHookOnce('morphed', 'vuexy-website-admin::contact-form-card', (component) => { ContactFormSettingsForm.reloadValidation(); -}); \ No newline at end of file +}); diff --git a/resources/js/contact-info-settings-card.js b/resources/js/contact-info-settings-card.js index 9c8b8e2..b71dfa1 100644 --- a/resources/js/contact-info-settings-card.js +++ b/resources/js/contact-info-settings-card.js @@ -1,10 +1,10 @@ -import '@vuexy-admin/notifications/LivewireNotification.js'; -import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; -import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; +//import '@vuexy-admin/assets/js/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/assets/js/forms/formCustomListener'; +//import registerLivewireHookOnce from '@vuexy-admin/assets/js/livewire/registerLivewireHookOnce'; // Inicializar formularios de ajustes de información de contacto window.ContactInfoSettingsForm = new FormCustomListener({ - formSelector: '#website-contact-info-settings-card', + formSelector: '#website-contact-info-card-card', buttonSelectors: ['.btn-save', '.btn-cancel'], callbacks: [() => {}], dispatchOnSubmit: 'save', @@ -130,13 +130,13 @@ window.ContactInfoSettingsForm = new FormCustomListener({ } }); -registerLivewireHookOnce('morphed', 'vuexy-website-admin::contact-info-settings', (component) => { +registerLivewireHookOnce('morphed', 'vuexy-website-admin::contact-info-card', (component) => { ContactInfoSettingsForm.reloadValidation(); }); // Inicializar formularios de ajustes de ubicación window.LocationSettingsForm = new FormCustomListener({ - formSelector: '#website-location-settings-card', + formSelector: '#website-location-card-card', buttonSelectors: ['.btn-save', '.btn-cancel'], callbacks: [() => {}], dispatchOnSubmit: 'save', @@ -208,6 +208,6 @@ window.LocationSettingsForm = new FormCustomListener({ } }); -registerLivewireHookOnce('morphed', 'vuexy-website-admin::location-settings', (component) => { +registerLivewireHookOnce('morphed', 'vuexy-website-admin::location-card', (component) => { LocationSettingsForm.reloadValidation(); }); diff --git a/resources/js/google-analytics-settings-card.js b/resources/js/google-analytics-settings-card.js index 80ef540..2b76bf0 100644 --- a/resources/js/google-analytics-settings-card.js +++ b/resources/js/google-analytics-settings-card.js @@ -1,6 +1,6 @@ -import '@vuexy-admin/notifications/LivewireNotification.js'; -import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; -import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; +//import '@vuexy-admin/assets/js/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/assets/js/forms/formCustomListener'; +//import registerLivewireHookOnce from '@vuexy-admin/assets/js/livewire/registerLivewireHookOnce'; // Inicializar formularios de ajustes de análisis de datos window.AnalyticsSettingsForm = new FormCustomListener({ @@ -38,4 +38,4 @@ window.AnalyticsSettingsForm = new FormCustomListener({ registerLivewireHookOnce('morphed', 'vuexy-website-admin::analytics-index', (component) => { AnalyticsSettingsForm.reloadValidation(); -}); \ No newline at end of file +}); diff --git a/resources/js/website-settings-card.js b/resources/js/website-settings-card.js index 166336e..e3f28c8 100644 --- a/resources/js/website-settings-card.js +++ b/resources/js/website-settings-card.js @@ -1,6 +1,6 @@ -import '@vuexy-admin/notifications/LivewireNotification.js'; -import FormCustomListener from '@vuexy-admin/forms/formCustomListener'; -import registerLivewireHookOnce from '@vuexy-admin/livewire/registerLivewireHookOnce'; +//import '@vuexy-admin/assets/js/notifications/LivewireNotification.js'; +import FormCustomListener from '@vuexy-admin/assets/js/forms/formCustomListener'; +//import registerLivewireHookOnce from '@vuexy-admin/assets/js/livewire/registerLivewireHookOnce'; // Inicializar formularios de ajustes de social media window.SocialSettingsForm = new FormCustomListener({ @@ -128,6 +128,6 @@ window.SocialSettingsForm = new FormCustomListener({ } }); -registerLivewireHookOnce('morphed', 'vuexy-website-admin::social-media-settings', (component) => { +registerLivewireHookOnce('morphed', 'vuexy-website-admin::social-card', (component) => { SocialSettingsForm.reloadValidation(); }); diff --git a/resources/views/google-analytics/index.blade.php b/resources/views/analytics/google-analytics/index.blade.php similarity index 88% rename from resources/views/google-analytics/index.blade.php rename to resources/views/analytics/google-analytics/index.blade.php index 5010e3e..cad4379 100644 --- a/resources/views/google-analytics/index.blade.php +++ b/resources/views/analytics/google-analytics/index.blade.php @@ -17,13 +17,13 @@ @endsection @push('page-script') - @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/google-analytics-settings-card.js') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/google-analytics-card-card.js') @endpush @section('content') <div class="row"> <div class="col-md-6"> - @livewire('vuexy-website-admin::google-analytics-settings') + @livewire('vuexy-website-admin::google-analytics-card') </div> </div> @endsection diff --git a/resources/views/analytics/google-search-console/index.blade.php b/resources/views/analytics/google-search-console/index.blade.php new file mode 100644 index 0000000..494ea77 --- /dev/null +++ b/resources/views/analytics/google-search-console/index.blade.php @@ -0,0 +1,11 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Google Search Console') + +@section('content') + <div class="row"> + <div class="col-md-6"> + @livewire('vuexy-website-admin::google-search-console-card') + </div> + </div> +@endsection diff --git a/resources/views/analytics/google-tags/index.blade.php b/resources/views/analytics/google-tags/index.blade.php new file mode 100644 index 0000000..60e0d63 --- /dev/null +++ b/resources/views/analytics/google-tags/index.blade.php @@ -0,0 +1,11 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Google Tags') + +@section('content') + <div class="row"> + <div class="col-md-6"> + @livewire('vuexy-website-admin::google-tags-card') + </div> + </div> +@endsection diff --git a/resources/views/analytics/pixel-meta/index.blade.php b/resources/views/analytics/pixel-meta/index.blade.php new file mode 100644 index 0000000..104dab1 --- /dev/null +++ b/resources/views/analytics/pixel-meta/index.blade.php @@ -0,0 +1,11 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Pixel Meta') + +@section('content') + <div class="row"> + <div class="col-md-6"> + @livewire('vuexy-website-admin::pixel-meta-card') + </div> + </div> +@endsection diff --git a/resources/views/blog/article/index.blade.php b/resources/views/blog/article/index.blade.php new file mode 100644 index 0000000..ff077f3 --- /dev/null +++ b/resources/views/blog/article/index.blade.php @@ -0,0 +1,31 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Artículos del Blog') + +@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 + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + ]) +@endsection + +@push('page-script') + @vite([ + '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::blog-articles-table') + <?php /* + @livewire('vuexy-website-admin::blog-article-offcanvas-form') + */ ?> +@endsection diff --git a/resources/views/blog/category/index.blade.php b/resources/views/blog/category/index.blade.php new file mode 100644 index 0000000..c094933 --- /dev/null +++ b/resources/views/blog/category/index.blade.php @@ -0,0 +1,31 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Categorías del Blog') + +@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 + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + ]) +@endsection + +@push('page-script') + @vite([ + '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::blog-categories-table') + <?php /* + @livewire('vuexy-website-admin::blog-category-offcanvas-form') + */ ?> +@endsection diff --git a/resources/views/blog/comment/index.blade.php b/resources/views/blog/comment/index.blade.php new file mode 100644 index 0000000..3b6d4bc --- /dev/null +++ b/resources/views/blog/comment/index.blade.php @@ -0,0 +1,31 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Comentarios del Blog') + +@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 + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + ]) +@endsection + +@push('page-script') + @vite([ + '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::blog-comments-table') + <?php /* + @livewire('vuexy-website-admin::blog-comment-offcanvas-form') + */ ?> +@endsection diff --git a/resources/views/blog/tag/index.blade.php b/resources/views/blog/tag/index.blade.php new file mode 100644 index 0000000..ee4f89b --- /dev/null +++ b/resources/views/blog/tag/index.blade.php @@ -0,0 +1,31 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Etiquetas del Blog') + +@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 + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + ]) +@endsection + +@push('page-script') + @vite([ + '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::blog-tags-table') + <?php /* + @livewire('vuexy-website-admin::blog-tag-offcanvas-form') + */ ?> +@endsection diff --git a/resources/views/comunication/messenger/index.blade.php b/resources/views/comunication/messenger/index.blade.php new file mode 100644 index 0000000..4b2963f --- /dev/null +++ b/resources/views/comunication/messenger/index.blade.php @@ -0,0 +1,11 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Facebook Messenger') + +@section('content') + <div class="row"> + <div class="col-md-6"> + @livewire('vuexy-website-admin::messenger-card') + </div> + </div> +@endsection diff --git a/resources/views/comunication/tawk-to/index.blade.php b/resources/views/comunication/tawk-to/index.blade.php new file mode 100644 index 0000000..ed31ab8 --- /dev/null +++ b/resources/views/comunication/tawk-to/index.blade.php @@ -0,0 +1,11 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Tawk-to Chat') + +@section('content') + <div class="row"> + <div class="col-md-6"> + @livewire('vuexy-website-admin::tawk-to-card') + </div> + </div> +@endsection diff --git a/resources/views/social-media/index.blade.php b/resources/views/comunication/twitter/index.blade.php similarity index 77% rename from resources/views/social-media/index.blade.php rename to resources/views/comunication/twitter/index.blade.php index de1ffcf..22c580d 100644 --- a/resources/views/social-media/index.blade.php +++ b/resources/views/comunication/twitter/index.blade.php @@ -1,6 +1,6 @@ @extends('vuexy-admin::layouts.vuexy.layoutMaster') -@section('title', 'Redes Sociales') +@section('title', 'Twitter API') @section('vendor-style') @vite([ @@ -17,9 +17,13 @@ @endsection @push('page-script') - @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/website-settings-card.js') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/google-analytics-card-card.js') @endpush @section('content') - @livewire('vuexy-website-admin::social-media-settings') + <div class="row"> + <div class="col-md-6"> + @livewire('vuexy-website-admin::twitter-card') + </div> + </div> @endsection diff --git a/resources/views/chat/index.blade.php b/resources/views/comunication/whatsapp/index.blade.php similarity index 89% rename from resources/views/chat/index.blade.php rename to resources/views/comunication/whatsapp/index.blade.php index 4fd72a0..19e2f03 100644 --- a/resources/views/chat/index.blade.php +++ b/resources/views/comunication/whatsapp/index.blade.php @@ -1,6 +1,6 @@ @extends('vuexy-admin::layouts.vuexy.layoutMaster') -@section('title', 'Chat') +@section('title', 'Whatsapp Chat') @section('vendor-style') @vite([ @@ -23,7 +23,7 @@ @section('content') <div class="row"> <div class="col-md-6"> - @livewire('vuexy-website-admin::chat-settings') + @livewire('vuexy-website-admin::whatsapp-card') </div> </div> @endsection diff --git a/resources/views/contact-form/index.blade.php b/resources/views/contact/form/index.blade.php similarity index 89% rename from resources/views/contact-form/index.blade.php rename to resources/views/contact/form/index.blade.php index 20bb15f..dd71de6 100644 --- a/resources/views/contact-form/index.blade.php +++ b/resources/views/contact/form/index.blade.php @@ -17,13 +17,13 @@ @endsection @push('page-script') - @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/contact-form-settings-card.js') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/contact-form-card-card.js') @endpush @section('content') <div class="row"> <div class="col-md-6"> - @livewire('vuexy-website-admin::contact-form-settings') + @livewire('vuexy-website-admin::contact-form-card') </div> </div> @endsection diff --git a/resources/views/contact-info/index.blade.php b/resources/views/contact/info/index.blade.php similarity index 84% rename from resources/views/contact-info/index.blade.php rename to resources/views/contact/info/index.blade.php index c927cc2..2c5b1c0 100644 --- a/resources/views/contact-info/index.blade.php +++ b/resources/views/contact/info/index.blade.php @@ -17,16 +17,16 @@ @endsection @push('page-script') - @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/contact-info-settings-card.js') + @vite('vendor/koneko/laravel-vuexy-website-admin/resources/js/contact-info-card-card.js') @endpush @section('content') <div class="row"> <div class="col-md-6"> - @livewire('vuexy-website-admin::contact-info-settings') + @livewire('vuexy-website-admin::contact-info-card') </div> <div class="col-md-6"> - @livewire('vuexy-website-admin::location-settings') + @livewire('vuexy-website-admin::contact-location-card') </div> </div> @endsection diff --git a/resources/views/faq/index.blade.php b/resources/views/content/faq/index.blade.php similarity index 100% rename from resources/views/faq/index.blade.php rename to resources/views/content/faq/index.blade.php diff --git a/resources/views/images/index.blade.php b/resources/views/content/gallery/index.blade.php similarity index 82% rename from resources/views/images/index.blade.php rename to resources/views/content/gallery/index.blade.php index 4a81c63..b499ba6 100644 --- a/resources/views/images/index.blade.php +++ b/resources/views/content/gallery/index.blade.php @@ -7,5 +7,5 @@ @endpush @section('content') - @livewire('vuexy-website-admin::images-index') + @livewire('vuexy-website-admin::gallery-index') @endsection diff --git a/resources/views/legal-notices/index.blade.php b/resources/views/content/legal/index.blade.php similarity index 67% rename from resources/views/legal-notices/index.blade.php rename to resources/views/content/legal/index.blade.php index 7cadcbe..5e7fc29 100644 --- a/resources/views/legal-notices/index.blade.php +++ b/resources/views/content/legal/index.blade.php @@ -3,5 +3,5 @@ @section('title', 'Avisos Legales') @section('content') - @livewire('vuexy-website-admin::legal-notices-index') + @livewire('vuexy-website-admin::legal-index') @endsection diff --git a/resources/views/legal-notices/legal-index.blade.php b/resources/views/content/legal/legal-index.blade.php similarity index 83% rename from resources/views/legal-notices/legal-index.blade.php rename to resources/views/content/legal/legal-index.blade.php index ceeef63..338179c 100644 --- a/resources/views/legal-notices/legal-index.blade.php +++ b/resources/views/content/legal/legal-index.blade.php @@ -20,9 +20,11 @@ @endsection @section('page-script') - @vite('modules/Admin/Resources/js/website-settings/legal-settings-scripts.js') + @vite([ + //'modules/Admin/Resources/js/website-settings/legal-settings-scripts.js' + ]) @endsection @section('content') - @livewire('website-legal-settings') + @livewire('legal-index') @endsection diff --git a/resources/views/layouts/base/header.blade.php b/resources/views/layouts/base/header.blade.php new file mode 100644 index 0000000..7e504dc --- /dev/null +++ b/resources/views/layouts/base/header.blade.php @@ -0,0 +1,72 @@ +{{-- SEO HEAD - Generado automáticamente por SeoProfileService --}} +@php($_seo = $_seo ?? []) + +{{-- Canonical + Fonts --}} +<link rel="canonical" href="{{ $_seo['canonical'] ?? url()->current() }}" /> +<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin> +<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" /> + +{{-- Charset (el viewport va en layout, no en SEO) --}} +<meta charset="utf-8"> + +{{-- hreflang alternates (multi-language SEO) --}} +@foreach ($_seo['hreflangs'] ?? [] as $lang => $url) + <link rel="alternate" hreflang="{{ $lang }}" href="{{ $url }}" /> +@endforeach + +{{-- Título + SEO Meta --}} +<title>{{ $_seo['title'] ?? config('app.name') }}</title> +<meta name="description" content="{{ $_seo['description'] ?? '' }}"> +<meta name="robots" content="{{ $_seo['robots'] ?? 'index, follow' }}"> +<meta name="language" content="{{ $_seo['language'] ?? app()->getLocale() }}"> +<meta name="author" content="{{ $_seo['author'] ?? 'Koneko Team' }}"> +<meta name="keywords" content="{{ $_seo['keywords'] ?? '' }}"> +<meta name="distribution" content="global"> +<meta name="revisit-after" content="7 days"> +<meta name="copyright" content="{{ $_seo['author'] ?? 'Koneko' }}"> + +{{-- CSRF (Laravel) --}} +<meta name="csrf-token" content="{{ csrf_token() }}"> + +{{-- OpenGraph --}} +<meta property="og:title" content="{{ $_seo['og:title'] ?? $_seo['title'] ?? '' }}"> +<meta property="og:site_name" content="{{ $_seo['og:site_name'] ?? config('app.name') }}"> +<meta property="og:url" content="{{ $_seo['og:url'] ?? url()->current() }}"> +<meta property="og:description" content="{{ $_seo['og:description'] ?? $_seo['description'] ?? '' }}"> +<meta property="og:type" content="{{ $_seo['og:type'] ?? 'website' }}"> +<meta property="og:image" content="{{ $_seo['og:image'] ?? '' }}"> + +{{-- Twitter Card --}} +<meta name="twitter:card" content="{{ $_seo['twitter:card'] ?? 'summary_large_image' }}"> +<meta name="twitter:title" content="{{ $_seo['twitter:title'] ?? $_seo['title'] ?? '' }}"> +<meta name="twitter:description" content="{{ $_seo['twitter:description'] ?? $_seo['description'] ?? '' }}"> +<meta name="twitter:image" content="{{ $_seo['twitter:image'] ?? $_seo['og:image'] ?? '' }}"> +<meta name="twitter:site" content="{{ $_seo['twitter:site'] ?? '' }}"> +<meta name="twitter:creator" content="{{ $_seo['twitter:creator'] ?? '' }}"> + +{{-- Favicons dinámicos --}} +@foreach ($_seo['favicon'] ?? [] as $size => $path) + @php($fullPath = Str::startsWith($path, ['http://', 'https://']) ? $path : asset('storage/' . $path)) + @switch(true) + @case(Str::endsWith($path, '.svg')) + <link rel="icon" type="image/svg+xml" sizes="{{ $size }}" href="{{ $fullPath }}"> + @break + @case(Str::endsWith($path, '.apng')) + <link rel="icon" type="image/apng" sizes="{{ $size }}" href="{{ $fullPath }}"> + @break + @case(Str::startsWith($size, 'apple')) + <link rel="apple-touch-icon" sizes="{{ Str::after($size, 'apple-') }}" href="{{ $fullPath }}"> + @break + @default + <link rel="icon" type="image/png" sizes="{{ $size }}" href="{{ $fullPath }}"> + @endswitch +@endforeach + +{{-- Web App Manifest --}} +<link rel="manifest" href="{{ $_seo['manifest'] ?? asset('site.webmanifest') }}"> +<meta name="theme-color" content="{{ $_seo['theme-color'] ?? '#ffffff' }}"> + +{{-- JSON-LD Structured Data --}} +@if (!empty($_seo['ld+json'])) + <script type="application/ld+json">@json($_seo['ld+json'])</script> +@endif diff --git a/resources/views/livewire/vuexy/analytics-settings.blade.php b/resources/views/livewire/analytics/google-analytics/google-analytics-card.blade.php similarity index 100% rename from resources/views/livewire/vuexy/analytics-settings.blade.php rename to resources/views/livewire/analytics/google-analytics/google-analytics-card.blade.php diff --git a/resources/views/livewire/analytics/google-search-console/google-search-console-card.blade.php b/resources/views/livewire/analytics/google-search-console/google-search-console-card.blade.php new file mode 100644 index 0000000..f9e2067 --- /dev/null +++ b/resources/views/livewire/analytics/google-search-console/google-search-console-card.blade.php @@ -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> diff --git a/resources/views/livewire/analytics/google-tags/google-tags-card.blade.php b/resources/views/livewire/analytics/google-tags/google-tags-card.blade.php new file mode 100644 index 0000000..f9e2067 --- /dev/null +++ b/resources/views/livewire/analytics/google-tags/google-tags-card.blade.php @@ -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> diff --git a/resources/views/livewire/analytics/pixel-meta/pixel-meta-card.blade.php b/resources/views/livewire/analytics/pixel-meta/pixel-meta-card.blade.php new file mode 100644 index 0000000..f9e2067 --- /dev/null +++ b/resources/views/livewire/analytics/pixel-meta/pixel-meta-card.blade.php @@ -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> diff --git a/resources/views/livewire/faq/index.blade.php b/resources/views/livewire/blog/article/index.blade.php similarity index 100% rename from resources/views/livewire/faq/index.blade.php rename to resources/views/livewire/blog/article/index.blade.php diff --git a/resources/views/livewire/blog/category/index.blade.php b/resources/views/livewire/blog/category/index.blade.php new file mode 100644 index 0000000..12a6a21 --- /dev/null +++ b/resources/views/livewire/blog/category/index.blade.php @@ -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> diff --git a/resources/views/livewire/blog/comment/index.blade.php b/resources/views/livewire/blog/comment/index.blade.php new file mode 100644 index 0000000..12a6a21 --- /dev/null +++ b/resources/views/livewire/blog/comment/index.blade.php @@ -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> diff --git a/resources/views/livewire/blog/tag/index.blade.php b/resources/views/livewire/blog/tag/index.blade.php new file mode 100644 index 0000000..12a6a21 --- /dev/null +++ b/resources/views/livewire/blog/tag/index.blade.php @@ -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> diff --git a/resources/views/livewire/vuexy/chat-settings.blade.php b/resources/views/livewire/comunication/messenger/messenger-card.blade.php similarity index 100% rename from resources/views/livewire/vuexy/chat-settings.blade.php rename to resources/views/livewire/comunication/messenger/messenger-card.blade.php diff --git a/resources/views/livewire/comunication/tawk-to/tawk-to-card.blade.php b/resources/views/livewire/comunication/tawk-to/tawk-to-card.blade.php new file mode 100644 index 0000000..d202a7d --- /dev/null +++ b/resources/views/livewire/comunication/tawk-to/tawk-to-card.blade.php @@ -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> diff --git a/resources/views/livewire/comunication/twitter/twitter-card.blade.php b/resources/views/livewire/comunication/twitter/twitter-card.blade.php new file mode 100644 index 0000000..f9e2067 --- /dev/null +++ b/resources/views/livewire/comunication/twitter/twitter-card.blade.php @@ -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> diff --git a/resources/views/livewire/comunication/whatsapp/whatsapp-card.blade.php b/resources/views/livewire/comunication/whatsapp/whatsapp-card.blade.php new file mode 100644 index 0000000..d202a7d --- /dev/null +++ b/resources/views/livewire/comunication/whatsapp/whatsapp-card.blade.php @@ -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> diff --git a/resources/views/livewire/vuexy/contact-form-settings.blade.php b/resources/views/livewire/contact/form/contact-form-card.blade.php similarity index 93% rename from resources/views/livewire/vuexy/contact-form-settings.blade.php rename to resources/views/livewire/contact/form/contact-form-card.blade.php index 9a3edf4..bcbb6a7 100644 --- a/resources/views/livewire/vuexy/contact-form-settings.blade.php +++ b/resources/views/livewire/contact/form/contact-form-card.blade.php @@ -1,5 +1,5 @@ <div> - <x-vuexy-admin::form.form id="website-contact-form-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode> + <x-vuexy-admin::form.form id="website-contact-form-card-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" /> diff --git a/resources/views/livewire/vuexy/contact-info-settings.blade.php b/resources/views/livewire/contact/info/contact-info-card.blade.php similarity index 94% rename from resources/views/livewire/vuexy/contact-info-settings.blade.php rename to resources/views/livewire/contact/info/contact-info-card.blade.php index 25a6cc6..13a6ba9 100644 --- a/resources/views/livewire/vuexy/contact-info-settings.blade.php +++ b/resources/views/livewire/contact/info/contact-info-card.blade.php @@ -1,5 +1,5 @@ <div> - <x-vuexy-admin::form.form id="website-contact-info-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode> + <x-vuexy-admin::form.form id="website-contact-info-card-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" /> diff --git a/resources/views/livewire/vuexy/location-settings.blade.php b/resources/views/livewire/contact/info/location-card.blade.php similarity index 92% rename from resources/views/livewire/vuexy/location-settings.blade.php rename to resources/views/livewire/contact/info/location-card.blade.php index 6aeb34d..f0ce50a 100644 --- a/resources/views/livewire/vuexy/location-settings.blade.php +++ b/resources/views/livewire/contact/info/location-card.blade.php @@ -1,5 +1,5 @@ <div> - <x-vuexy-admin::form.form id="website-location-settings-card" class="form-custom-listener mb-4" whitOutId whitOutMode> + <x-vuexy-admin::form.form id="website-location-card-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"> diff --git a/resources/views/livewire/content/faq/index.blade.php b/resources/views/livewire/content/faq/index.blade.php new file mode 100644 index 0000000..12a6a21 --- /dev/null +++ b/resources/views/livewire/content/faq/index.blade.php @@ -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> diff --git a/resources/views/livewire/images/index.blade.php b/resources/views/livewire/content/gallery/index.blade.php similarity index 100% rename from resources/views/livewire/images/index.blade.php rename to resources/views/livewire/content/gallery/index.blade.php diff --git a/resources/views/livewire/legal-notices/index.blade.php b/resources/views/livewire/content/legal-notices/index.blade.php similarity index 100% rename from resources/views/livewire/legal-notices/index.blade.php rename to resources/views/livewire/content/legal-notices/index.blade.php diff --git a/resources/views/livewire/sitemap-manager/index.blade.php b/resources/views/livewire/seo/canonical/index.blade.php similarity index 100% rename from resources/views/livewire/sitemap-manager/index.blade.php rename to resources/views/livewire/seo/canonical/index.blade.php diff --git a/resources/views/livewire/seo/jsonld/index.blade.php b/resources/views/livewire/seo/jsonld/index.blade.php new file mode 100644 index 0000000..69e3e8c --- /dev/null +++ b/resources/views/livewire/seo/jsonld/index.blade.php @@ -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> diff --git a/resources/views/livewire/seo/manifest/manifest-card.blade.php b/resources/views/livewire/seo/manifest/manifest-card.blade.php new file mode 100644 index 0000000..69e3e8c --- /dev/null +++ b/resources/views/livewire/seo/manifest/manifest-card.blade.php @@ -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> diff --git a/resources/views/livewire/seo/robots/robot-card.blade.php b/resources/views/livewire/seo/robots/robot-card.blade.php new file mode 100644 index 0000000..69e3e8c --- /dev/null +++ b/resources/views/livewire/seo/robots/robot-card.blade.php @@ -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> diff --git a/resources/views/livewire/seo/sitemap/UrlOffcanvas-form.blade.php b/resources/views/livewire/seo/sitemap/UrlOffcanvas-form.blade.php new file mode 100644 index 0000000..31328b1 --- /dev/null +++ b/resources/views/livewire/seo/sitemap/UrlOffcanvas-form.blade.php @@ -0,0 +1,39 @@ +<div> + <x-vuexy-admin::offcanvas.basic :id="$offcanvasId" :tag-name="$tagName"> + <x-vuexy-admin::form :uid="$uniqueId" :id="$formId" :mode="$mode" wireSubmit="onSubmit"> + <x-slot name="actions"> + <x-vuexy-admin::button.offcanvas-buttons :mode="$mode" :tagName="$tagName" /> + </x-slot> + {{-- Usuario --}} + <x-vuexy-admin::form.input :uid="$uniqueId" model="name" label="Nombre(s)" /> + <x-vuexy-admin::form.input :uid="$uniqueId" model="last_name" label="Apellidos" /> + {{-- Correos electrónicos --}} + <x-vuexy-admin::form.input type="email" :uid="$uniqueId" model="email" label="Correo electrónico" icon="ti ti-mail" autocomplete="email" inputmode="email" /> + + {{-- Contraseña --}} + <x-vuexy-admin::form.input type="password" :uid="$uniqueId" model="password" label="Contraseña" icon="ti ti-lock" autocomplete="new-password" /> + + + <hr> + + </x-vuexy-admin::form> + </x-vuexy-admin::offcanvas.basic> +</div> + +@push('page-script') + <script> + // Evento para inicializar el formulario cuando se carga la página + document.addEventListener("DOMContentLoaded", function () { + const initializeUserForm = () => { + + }; + + var myOffcanvas = document.getElementById('{{ $offcanvasId }}'); + + myOffcanvas.addEventListener('show.bs.offcanvas', function () { + initializeUserForm(); + }); + }); + + </script> +@endpush diff --git a/resources/views/livewire/seo/sitemap/index.blade.php b/resources/views/livewire/seo/sitemap/index.blade.php new file mode 100644 index 0000000..69e3e8c --- /dev/null +++ b/resources/views/livewire/seo/sitemap/index.blade.php @@ -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> diff --git a/resources/views/livewire/seo/social-cards/index.blade.php b/resources/views/livewire/seo/social-cards/index.blade.php new file mode 100644 index 0000000..69e3e8c --- /dev/null +++ b/resources/views/livewire/seo/social-cards/index.blade.php @@ -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> diff --git a/resources/views/livewire/vuexy/template-settings.blade.php b/resources/views/livewire/settings/general/___template-card.blade.php similarity index 100% rename from resources/views/livewire/vuexy/template-settings.blade.php rename to resources/views/livewire/settings/general/___template-card.blade.php diff --git a/resources/views/livewire/vuexy/logo-on-dark-bg-settings.blade.php b/resources/views/livewire/settings/general/logo-on-dark-bg-card.blade.php similarity index 96% rename from resources/views/livewire/vuexy/logo-on-dark-bg-settings.blade.php rename to resources/views/livewire/settings/general/logo-on-dark-bg-card.blade.php index dd08120..1ff0762 100644 --- a/resources/views/livewire/vuexy/logo-on-dark-bg-settings.blade.php +++ b/resources/views/livewire/settings/general/logo-on-dark-bg-card.blade.php @@ -1,5 +1,5 @@ <div> - <div id="logo-on-dark-bg-settings-card" class="mb-4"> + <div id="logo-on-dark-bg-card-card" class="mb-4"> <x-vuexy-admin::card.basic title="Logotipo sobre fondo oscuro" class="mb-2"> <x-vuexy-admin::form.input type="file" diff --git a/resources/views/livewire/vuexy/logo-on-light-bg-settings.blade.php b/resources/views/livewire/settings/general/logo-on-light-bg-card.blade.php similarity index 96% rename from resources/views/livewire/vuexy/logo-on-light-bg-settings.blade.php rename to resources/views/livewire/settings/general/logo-on-light-bg-card.blade.php index 75cf53d..2e975ee 100644 --- a/resources/views/livewire/vuexy/logo-on-light-bg-settings.blade.php +++ b/resources/views/livewire/settings/general/logo-on-light-bg-card.blade.php @@ -1,5 +1,5 @@ <div> - <div id="logo-on-light-bg-settings-card" class="mb-4"> + <div id="logo-on-light-bg-card-card" class="mb-4"> <x-vuexy-admin::card.basic title="Logotipo sobre fondo claro" class="mb-2"> <x-vuexy-admin::form.input type="file" diff --git a/resources/views/livewire/vuexy/website-description-settings.blade.php b/resources/views/livewire/settings/general/website-description-card.blade.php similarity index 85% rename from resources/views/livewire/vuexy/website-description-settings.blade.php rename to resources/views/livewire/settings/general/website-description-card.blade.php index dde900c..6706fad 100644 --- a/resources/views/livewire/vuexy/website-description-settings.blade.php +++ b/resources/views/livewire/settings/general/website-description-card.blade.php @@ -1,8 +1,7 @@ <div> - <div id="website-description-settings-card" class="form-custom-listener mb-4"> + <div id="website-description-card-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"> diff --git a/resources/views/livewire/vuexy/website-favicon-settings.blade.php b/resources/views/livewire/settings/general/website-favicon-card.blade.php similarity index 98% rename from resources/views/livewire/vuexy/website-favicon-settings.blade.php rename to resources/views/livewire/settings/general/website-favicon-card.blade.php index 3705dd5..755b777 100644 --- a/resources/views/livewire/vuexy/website-favicon-settings.blade.php +++ b/resources/views/livewire/settings/general/website-favicon-card.blade.php @@ -1,5 +1,5 @@ <div> - <div id="website-favicon-settings-card" class="mb-4"> + <div id="website-favicon-card-card" class="mb-4"> <x-vuexy-admin::card.basic title="Favicon" class="mb-2"> <x-vuexy-admin::form.input type="file" diff --git a/resources/views/livewire/settings/indexing/indexing-card.blade.php b/resources/views/livewire/settings/indexing/indexing-card.blade.php new file mode 100644 index 0000000..8c1fa64 --- /dev/null +++ b/resources/views/livewire/settings/indexing/indexing-card.blade.php @@ -0,0 +1,4 @@ +<div> + <h2>Visibilidad en buscadores</h2> + +</div> diff --git a/resources/views/livewire/vuexy/social-media-settings.blade.php b/resources/views/livewire/settings/social/social-card.blade.php similarity index 100% rename from resources/views/livewire/vuexy/social-media-settings.blade.php rename to resources/views/livewire/settings/social/social-card.blade.php diff --git a/resources/views/livewire/translate/google/google-translate-card.blade.php b/resources/views/livewire/translate/google/google-translate-card.blade.php new file mode 100644 index 0000000..d202a7d --- /dev/null +++ b/resources/views/livewire/translate/google/google-translate-card.blade.php @@ -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> diff --git a/resources/views/seo/canonical/index.blade.php b/resources/views/seo/canonical/index.blade.php new file mode 100644 index 0000000..e68aaf4 --- /dev/null +++ b/resources/views/seo/canonical/index.blade.php @@ -0,0 +1,7 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Canonical URLs') + +@section('content') + @livewire('vuexy-website-admin::canonical-index') +@endsection diff --git a/resources/views/seo/jsonld/index.blade.php b/resources/views/seo/jsonld/index.blade.php new file mode 100644 index 0000000..6cc7653 --- /dev/null +++ b/resources/views/seo/jsonld/index.blade.php @@ -0,0 +1,7 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Google JSON-LD') + +@section('content') + @livewire('vuexy-website-admin::jsonld-index') +@endsection diff --git a/resources/views/seo/manifest/index.blade.php b/resources/views/seo/manifest/index.blade.php new file mode 100644 index 0000000..4bebb31 --- /dev/null +++ b/resources/views/seo/manifest/index.blade.php @@ -0,0 +1,7 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'manifest.js') + +@section('content') + @livewire('vuexy-website-admin::manifest-card') +@endsection diff --git a/resources/views/seo/robots/index.blade.php b/resources/views/seo/robots/index.blade.php new file mode 100644 index 0000000..4880701 --- /dev/null +++ b/resources/views/seo/robots/index.blade.php @@ -0,0 +1,7 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Robots.txt') + +@section('content') + @livewire('vuexy-website-admin::robots-card') +@endsection diff --git a/resources/views/sitemap-manager/index.blade.php b/resources/views/seo/sitemap/index.blade.php similarity index 67% rename from resources/views/sitemap-manager/index.blade.php rename to resources/views/seo/sitemap/index.blade.php index 0ed2b83..8ff6a4e 100644 --- a/resources/views/sitemap-manager/index.blade.php +++ b/resources/views/seo/sitemap/index.blade.php @@ -3,5 +3,5 @@ @section('title', 'Mapa del Sitio') @section('content') - @livewire('vuexy-website-admin::sitemap-manager-index') + @livewire('vuexy-website-admin::sitemap-index') @endsection diff --git a/resources/views/seo/social-cards/index.blade.php b/resources/views/seo/social-cards/index.blade.php new file mode 100644 index 0000000..f2d2c0e --- /dev/null +++ b/resources/views/seo/social-cards/index.blade.php @@ -0,0 +1,7 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Preview Social Cards') + +@section('content') + @livewire('vuexy-website-admin::social-cards-index') +@endsection diff --git a/resources/views/general-settings/index.blade.php b/resources/views/settings/general/index.blade.php similarity index 66% rename from resources/views/general-settings/index.blade.php rename to resources/views/settings/general/index.blade.php index f843973..23c0de2 100644 --- a/resources/views/general-settings/index.blade.php +++ b/resources/views/settings/general/index.blade.php @@ -9,12 +9,12 @@ @section('content') <div class="row"> <div class="col-lg-5"> - @livewire('vuexy-website-admin::website-description-settings') - @livewire('vuexy-website-admin::website-favicon-settings') + @livewire('vuexy-website-admin::website-description-card') + @livewire('vuexy-website-admin::website-favicon-card') </div> <div class="col-lg-4"> - @livewire('vuexy-website-admin::logo-on-light-bg-settings') - @livewire('vuexy-website-admin::logo-on-dark-bg-settings') + @livewire('vuexy-website-admin::logo-on-light-bg-card') + @livewire('vuexy-website-admin::logo-on-dark-bg-card') </div> </div> @endsection diff --git a/resources/views/settings/indexing/index.blade.php b/resources/views/settings/indexing/index.blade.php new file mode 100644 index 0000000..96e3d26 --- /dev/null +++ b/resources/views/settings/indexing/index.blade.php @@ -0,0 +1,11 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Indexación') + +@section('content') + <div class="row"> + <div class="col-lg-5"> + @livewire('vuexy-website-admin::indexing-card') + </div> + </div> +@endsection diff --git a/resources/views/settings/social/index.blade.php b/resources/views/settings/social/index.blade.php new file mode 100644 index 0000000..eb05070 --- /dev/null +++ b/resources/views/settings/social/index.blade.php @@ -0,0 +1,27 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Enlaces de 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-card') +@endsection diff --git a/resources/views/translate/google/index.blade.php b/resources/views/translate/google/index.blade.php new file mode 100644 index 0000000..11aded9 --- /dev/null +++ b/resources/views/translate/google/index.blade.php @@ -0,0 +1,11 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Google Translate') + +@section('content') + <div class="row"> + <div class="col-lg-5"> + @livewire('vuexy-website-admin::google-tanslate-card') + </div> + </div> +@endsection diff --git a/Models/SitemapConfiguration.php b/resources/views/website/blocks/call-to-action.blade.php similarity index 100% rename from Models/SitemapConfiguration.php rename to resources/views/website/blocks/call-to-action.blade.php diff --git a/resources/views/website/blocks/features.blade.php b/resources/views/website/blocks/features.blade.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/website/blocks/hero.blade.php b/resources/views/website/blocks/hero.blade.php new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/website/templates/default.blade.php b/resources/views/website/templates/default.blade.php new file mode 100644 index 0000000..265a75f --- /dev/null +++ b/resources/views/website/templates/default.blade.php @@ -0,0 +1,540 @@ +@extends('porto::layouts.master') + +@section('vendor-style') + <!-- Web Fonts --> + <link id="googleFonts" href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700,800%7CShadows+Into+Light&display=swap" rel="stylesheet" type="text/css"> + + @vite('vendor/koneko/laravel-vuexy-website-layout-porto/resources/assets/css/skins/default.css') +@endsection + +@section('content') + @if(request()->is('preview/*')) + <div class="alert alert-warning shadow mb-3"> + Estás visualizando una vista previa de contenido no publicado. + </div> + @endif + + <x-porto::header.menu menuSlug="main-header" /> + + <div class="owl-carousel owl-carousel-light owl-carousel-light-init-fadeIn owl-theme manual dots-inside dots-horizontal-center show-dots-hover nav-inside nav-inside-plus nav-dark nav-md nav-font-size-md show-nav-hover mb-0" data-plugin-options="{'autoplayTimeout': 7000}" data-dynamic-height="['670px','670px','670px','550px','500px']" style="height: 670px;"> + <div class="owl-stage-outer"> + <div class="owl-stage"> + + <!-- Carousel Slide 1 --> + <div class="owl-item position-relative" style="background-image: url(img/slides/slide-bg-performance.jpg); background-color: #2E3136; background-size: cover; background-position: center;"> + <div class="container position-relative z-index-1 h-100"> + <div class="d-flex flex-column align-items-center justify-content-center h-100"> + <h3 class="position-relative text-color-light text-5 line-height-5 font-weight-medium px-4 mb-2 appear-animation" data-appear-animation="fadeInDownShorter" data-plugin-options="{'minWindowWidth': 0}"> + <span class="position-absolute right-100pct top-50pct transform3dy-n50 opacity-3"> + <img src="img/slides/slide-title-border.png" class="w-auto appear-animation" data-appear-animation="fadeInLeftShorter" data-appear-animation-delay="250" data-plugin-options="{'minWindowWidth': 0}" alt="" /> + </span> + DO YOU NEED A <span class="position-relative">NEW <span class="position-absolute left-50pct transform3dx-n50 top-0 mt-4"><img src="img/slides/slide-blue-line.png" class="w-auto appear-animation" data-appear-animation="fadeInLeftShorterPlus" data-appear-animation-delay="1000" data-plugin-options="{'minWindowWidth': 0}" alt="" /></span></span> + <span class="position-absolute left-100pct top-50pct transform3dy-n50 opacity-3"> + <img src="img/slides/slide-title-border.png" class="w-auto appear-animation" data-appear-animation="fadeInRightShorter" data-appear-animation-delay="250" data-plugin-options="{'minWindowWidth': 0}" alt="" /> + </span> + </h3> + <h1 class="text-color-light font-weight-extra-bold text-12 mb-3 appear-animation" data-appear-animation="blurIn" data-appear-animation-delay="500" data-plugin-options="{'minWindowWidth': 0}">WEB DESIGN?</h1> + <p class="text-4 text-color-light font-weight-light opacity-7 mb-0" data-plugin-animated-letters data-plugin-options="{'startDelay': 1000, 'minWindowWidth': 0}">Check out our options and features</p> + </div> + </div> + </div> + + <!-- Carousel Slide 2 --> + <div class="owl-item position-relative overlay overlay-show overlay-op-8 lazyload" data-bg-src="img/slides/slide-bg-2.jpg" style="background-size: cover; background-position: center;"> + <div class="container position-relative z-index-3 h-100"> + <div class="row justify-content-center align-items-center h-100"> + <div class="col-lg-6"> + <div class="d-flex flex-column align-items-center"> + <h3 class="position-relative text-color-light text-5 line-height-5 font-weight-medium px-4 mb-2 appear-animation" data-appear-animation="fadeInDownShorter" data-plugin-options="{'minWindowWidth': 0}"> + <span class="position-absolute right-100pct top-50pct transform3dy-n50 opacity-3"> + <img loading="lazy" src="img/slides/slide-title-border.png" class="w-auto appear-animation" data-appear-animation="fadeInLeftShorter" data-appear-animation-delay="250" data-plugin-options="{'imgFluid': false, 'minWindowWidth': 0}" alt="" /> + </span> + WE WORK HARD AND PORTO HAS + <span class="position-absolute left-100pct top-50pct transform3dy-n50 opacity-3"> + <img loading="lazy" src="img/slides/slide-title-border.png" class="w-auto appear-animation" data-appear-animation="fadeInRightShorter" data-appear-animation-delay="250" data-plugin-options="{'imgFluid': false, 'minWindowWidth': 0}" alt="" /> + </span> + </h3> + <h2 class="text-color-light font-weight-extra-bold text-12 mb-3 appear-animation" data-appear-animation="blurIn" data-appear-animation-delay="500" data-plugin-options="{'minWindowWidth': 0}">THE BEST DESIGN</h2> + <p class="text-4 text-color-light font-weight-light opacity-7 text-center mb-0" data-plugin-animated-letters data-plugin-options="{'startDelay': 1000, 'minWindowWidth': 0, 'animationSpeed': 30}">Trusted by over <strong class="text-color-light">40,000</strong> satisfied users, Porto is a huge success in the one of largest world's MarketPlace</p> + </div> + </div> + </div> + </div> + </div> + + <!-- Carousel Slide 3 --> + <div class="owl-item position-relative overlay overlay-color-primary overlay-show overlay-op-8 lazyload" data-bg-src="img/slides/slide-bg-6.jpg" style="background-size: cover; background-position: center;"> + <div class="container position-relative z-index-3 h-100"> + <div class="row justify-content-center align-items-center h-100"> + <div class="col-lg-6"> + <div class="d-flex flex-column align-items-center"> + <h3 class="position-relative text-color-light text-4 line-height-5 font-weight-medium px-4 mb-2 appear-animation" data-appear-animation="fadeInDownShorter" data-plugin-options="{'minWindowWidth': 0}"> + <span class="position-absolute right-100pct top-50pct transform3dy-n50 opacity-3"> + <img loading="lazy" src="img/slides/slide-title-border.png" class="w-auto appear-animation" data-appear-animation="fadeInLeftShorter" data-appear-animation-delay="250" data-plugin-options="{'imgFluid': false, 'minWindowWidth': 0}" alt="" /> + </span> + WE CREATE DESIGNS, WE ARE + <span class="position-absolute left-100pct top-50pct transform3dy-n50 opacity-3"> + <img loading="lazy" src="img/slides/slide-title-border.png" class="w-auto appear-animation" data-appear-animation="fadeInRightShorter" data-appear-animation-delay="250" data-plugin-options="{'imgFluid': false, 'minWindowWidth': 0}" alt="" /> + </span> + </h3> + <h2 class="porto-big-title text-color-light font-weight-extra-bold mb-3" data-plugin-animated-letters data-plugin-options="{'startDelay': 1000, 'minWindowWidth': 0, 'animationSpeed': 300, 'animationName': 'fadeInRightShorterOpacity', 'letterClass': 'd-inline-block'}">PORTO</h2> + <p class="text-4 text-color-light font-weight-light text-center mb-0" data-plugin-animated-letters data-plugin-options="{'startDelay': 2000, 'minWindowWidth': 0}">The best choice for your new website</p> + </div> + </div> + </div> + </div> + </div> + + </div> + </div> + < + div class="owl-nav"> + <button type="button" role="presentation" class="owl-prev" aria-label="Previous"></button> + <button type="button" role="presentation" class="owl-next" aria-label="Next"></button> + </> + <div class="owl-dots mb-5"> + <button role="button" class="owl-dot active"><span></span></button> + <button role="button" class="owl-dot"><span></span></button> + <button role="button" class="owl-dot"><span></span></button> + </div> + </div> + + <div class="home-intro bg-primary" id="home-intro"> + <div class="container"> + + <div class="row align-items-center"> + <div class="col-lg-8"> + <p> + The fastest way to grow your business with the leader in <span class="highlighted-word">Technology</span> + <span>Check out our options and features included.</span> + </p> + </div> + <div class="col-lg-4"> + <div class="get-started text-start text-lg-end"> + <a href="#" class="btn btn-dark btn-lg text-3 font-weight-semibold px-4 py-3">Get Started Now</a> + <div class="learn-more">or <a href="index.html">learn more.</a></div> + </div> + </div> + </div> + + </div> + </div> + + <div class="container"> + + <div class="row text-center pt-3"> + <div class="col-md-10 mx-md-auto"> + <h1 class="word-rotator slide font-weight-bold text-8 mb-3 appear-animation" data-appear-animation="fadeInUpShorter"> + <span>Porto is </span> + <span class="word-rotator-words bg-dark"> + <b class="is-visible">incredibly</b> + <b>especially</b> + <b>extremely</b> + </span> + <span> beautiful and fully responsive.</span> + </h1> + <p class="lead appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="300"> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce elementum, nulla vel pellentesque consequat, ante nulla hendrerit arcu, ac tincidunt mauris lacus sed leo. + </p> + </div> + </div> + + </div> + + <div class="appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="200"> + <div class="home-concept mt-5"> + <div class="container"> + + <div class="row text-center"> + <span class="sun"></span> + <span class="cloud"></span> + <div class="col-lg-2 ms-lg-auto"> + <div class="process-image"> + <img src="img/home/home-concept-item-1.png" alt="" /> + <strong>Strategy</strong> + </div> + </div> + <div class="col-lg-2"> + <div class="process-image process-image-on-middle"> + <img src="img/home/home-concept-item-2.png" alt="" /> + <strong>Planning</strong> + </div> + </div> + <div class="col-lg-2"> + <div class="process-image"> + <img src="img/home/home-concept-item-3.png" alt="" /> + <strong>Build</strong> + </div> + </div> + <div class="col-lg-4 ms-lg-auto"> + <div class="project-image"> + <div id="fcSlideshow" class="fc-slideshow"> + <ul class="fc-slides"> + <li><a href="portfolio-single-wide-slider.html" aria-label=""><img class="img-fluid" src="img/projects/project-home-1.jpg" alt="" /></a></li> + <li><a href="portfolio-single-wide-slider.html" aria-label=""><img class="img-fluid" src="img/projects/project-home-2.jpg" alt="" /></a></li> + <li><a href="portfolio-single-wide-slider.html" aria-label=""><img class="img-fluid" src="img/projects/project-home-3.jpg" alt="" /></a></li> + </ul> + </div> + <strong class="our-work">Our Work</strong> + </div> + </div> + </div> + + </div> + </div> + </div> + + <div class="container mb-5 pb-4"> + + <div class="row"> + <div class="col mb-4"> + <hr class="my-5"> + </div> + </div> + + <div class="row pb-3"> + <div class="col-lg-8"> + <h2 class="font-weight-normal text-7">Our <strong class="font-weight-extra-bold">Features</strong></h2> + <div class="row"> + <div class="col-sm-6"> + <div class="feature-box feature-box-style-2"> + <div class="feature-box-icon"> + <i class="icons icon-support text-color-primary"></i> + </div> + <div class="feature-box-info"> + <h4 class="font-weight-bold text-4-5 mb-1">Customer Support</h4> + <p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit massa</p> + </div> + </div> + <div class="feature-box feature-box-style-2"> + <div class="feature-box-icon"> + <i class="icons icon-doc text-color-primary"></i> + </div> + <div class="feature-box-info"> + <h4 class="font-weight-bold text-4-5 mb-1">HTML5 / CSS3 / JS</h4> + <p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit massa</p> + </div> + </div> + <div class="feature-box feature-box-style-2"> + <div class="feature-box-icon"> + <i class="icons icon-social-google text-color-primary"></i> + </div> + <div class="feature-box-info"> + <h4 class="font-weight-bold text-4-5 mb-1">500+ Google Fonts</h4> + <p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit massa</p> + </div> + </div> + <div class="feature-box feature-box-style-2"> + <div class="feature-box-icon"> + <i class="icons icon-pencil text-color-primary"></i> + </div> + <div class="feature-box-info"> + <h4 class="font-weight-bold text-4-5 mb-1">Colors</h4> + <p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit massa</p> + </div> + </div> + </div> + <div class="col-sm-6"> + <div class="feature-box feature-box-style-2"> + <div class="feature-box-icon"> + <i class="icons icon-layers text-color-primary"></i> + </div> + <div class="feature-box-info"> + <h4 class="font-weight-bold text-4-5 mb-1">Sliders</h4> + <p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit massa</p> + </div> + </div> + <div class="feature-box feature-box-style-2"> + <div class="feature-box-icon"> + <i class="icons icon-user text-color-primary"></i> + </div> + <div class="feature-box-info"> + <h4 class="font-weight-bold text-4-5 mb-1">Icons</h4> + <p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit massa</p> + </div> + </div> + <div class="feature-box feature-box-style-2"> + <div class="feature-box-icon"> + <i class="icons icon-menu text-color-primary"></i> + </div> + <div class="feature-box-info"> + <h4 class="font-weight-bold text-4-5 mb-1">Buttons</h4> + <p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit massa</p> + </div> + </div> + <div class="feature-box feature-box-style-2"> + <div class="feature-box-icon"> + <i class="icons icon-screen-desktop text-color-primary"></i> + </div> + <div class="feature-box-info"> + <h4 class="font-weight-bold text-4-5 mb-1">Lightbox</h4> + <p class="mb-4">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit massa</p> + </div> + </div> + </div> + </div> + </div> + <div class="col-lg-4"> + <h2 class="font-weight-normal text-6">and more...</h2> + + <div class="accordion accordion-modern" id="accordion"> + <div class="card card-default"> + <div class="card-header"> + <h4 class="card-title m-0"> + <a class="accordion-toggle text-color-dark font-weight-bold" data-bs-toggle="collapse" data-bs-parent="#accordion" href="#collapseOne"> + <i class="icons icon-diamond text-color-primary"></i> + Creative Websites + </a> + </h4> + </div> + <div id="collapseOne" class="collapse show"> + <div class="card-body text-2"> + <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blanorem ipsum dolor sit amet, consecte.</p> + <p class="mb-0">Adipiscing elit phasellus blanit ma... <a href="#" class="d-block text-color-dark font-weight-semibold pt-4">read more <i class="fas fa-angle-right position-relative top-1 ms-1"></i></a></p> + </div> + </div> + </div> + <div class="card card-default"> + <div class="card-header"> + <h4 class="card-title m-0"> + <a class="accordion-toggle text-color-dark font-weight-bold" data-bs-toggle="collapse" data-bs-parent="#accordion" href="#collapseTwo"> + <i class="icons icon-bubble text-color-primary"></i> + Contact Forms + </a> + </h4> + </div> + <div id="collapseTwo" class="collapse"> + <div class="card-body text-2"> + <p class="mb-0">Donec tellus massa, tristique sit amet condimentum vel, facilisis quis sapien.</p> + </div> + </div> + </div> + <div class="card card-default"> + <div class="card-header"> + <h4 class="card-title m-0"> + <a class="accordion-toggle text-color-dark font-weight-bold" data-bs-toggle="collapse" data-bs-parent="#accordion" href="#collapseThree"> + <i class="icons icon-grid text-color-primary"></i> + Portfolio Pages + </a> + </h4> + </div> + <div id="collapseThree" class="collapse"> + <div class="card-body text-2"> + <p class="mb-0">Donec tellus massa, tristique sit amet condimentum vel, facilisis quis sapien.</p> + </div> + </div> + </div> + </div> + </div> + </div> + + <hr class="solid my-5"> + + <div class="row text-center pt-4"> + <div class="col"> + <h2 class="word-rotator slide font-weight-bold text-8 mb-2"> + <span>We're not the only ones </span> + <span class="word-rotator-words bg-primary"> + <b class="is-visible">excited</b> + <b>happy</b> + </span> + <span> about Porto Template...</span> + </h2> + <h4 class="text-primary lead tall text-4">50,000 CUSTOMERS IN 100 COUNTRIES USE PORTO TEMPLATE. MEET OUR CUSTOMERS.</h4> + </div> + </div> + + <div class="row text-center mt-5"> + <div class="owl-carousel owl-theme carousel-center-active-item" data-plugin-options="{'responsive': {'0': {'items': 1}, '476': {'items': 1}, '768': {'items': 5}, '992': {'items': 7}, '1200': {'items': 7}}, 'autoplay': true, 'autoplayTimeout': 3000, 'dots': false}"> + <div> + <img class="img-fluid" src="img/logos/logo-1.png" alt=""> + </div> + <div> + <img class="img-fluid" src="img/logos/logo-2.png" alt=""> + </div> + <div> + <img class="img-fluid" src="img/logos/logo-3.png" alt=""> + </div> + <div> + <img class="img-fluid" src="img/logos/logo-4.png" alt=""> + </div> + <div> + <img class="img-fluid" src="img/logos/logo-5.png" alt=""> + </div> + <div> + <img class="img-fluid" src="img/logos/logo-6.png" alt=""> + </div> + <div> + <img class="img-fluid" src="img/logos/logo-4.png" alt=""> + </div> + <div> + <img class="img-fluid" src="img/logos/logo-2.png" alt=""> + </div> + </div> + </div> + + </div> + + <section class="section section-custom-map appear-animation lazyload" data-appear-animation="fadeInUpShorter" data-bg-src="img/map.png" style="background-color: transparent; background-position: center 0; background-repeat: no-repeat;"> + <section class="section section-default section-footer"> + <div class="container"> + <div class="row mt-5 appear-animation" data-appear-animation="fadeInUpShorter"> + <div class="col-lg-6"> + <div class="recent-posts mb-5"> + <h2 class="font-weight-normal text-6 mb-4"><strong class="font-weight-extra-bold">Latest</strong> Posts</h2> + <div class="owl-carousel owl-theme dots-title mb-0" data-plugin-options="{'items': 1, 'autoHeight': true, 'autoplay': true, 'autoplayTimeout': 8000}"> + <div class="row"> + <div class="col-lg-6 mb-4 mb-lg-0"> + <article> + <div class="row"> + <div class="col-auto pe-0"> + <div class="date"> + <span class="day font-weight-extra-bold">15</span> + <span class="month text-1">JAN</span> + </div> + </div> + <div class="col ps-1"> + <h4 class="text-primary text-4"><a class="d-block" href="blog-post.html">Lorem ipsum dolor sit amet, consectetur</a></h4> + <p class="pe-4 mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> + <a href="/" class="read-more text-color-dark font-weight-semibold text-2">read more <i class="fas fa-angle-right position-relative top-1 ms-1"></i></a> + </div> + </div> + </article> + </div> + <div class="col-lg-6"> + <article> + <div class="row"> + <div class="col-auto pe-0"> + <div class="date"> + <span class="day font-weight-extra-bold">14</span> + <span class="month text-1">JAN</span> + </div> + </div> + <div class="col ps-1"> + <h4 class="text-primary text-4"><a class="d-block" href="blog-post.html">Lorem ipsum dolor sit amet, consectetur</a></h4> + <p class="pe-4 mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> + <a href="/" class="read-more text-color-dark font-weight-semibold text-2">read more <i class="fas fa-angle-right position-relative top-1 ms-1"></i></a> + </div> + </div> + </article> + </div> + </div> + <div class="row"> + <div class="col-lg-6 mb-4 mb-lg-0"> + <article> + <div class="row"> + <div class="col-auto pe-0"> + <div class="date"> + <span class="day font-weight-extra-bold">13</span> + <span class="month text-1">JAN</span> + </div> + </div> + <div class="col ps-1"> + <h4 class="text-primary text-4"><a class="d-block" href="blog-post.html">Lorem ipsum dolor sit amet, consectetur</a></h4> + <p class="pe-4 mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> + <a href="/" class="read-more text-color-dark font-weight-semibold text-2">read more <i class="fas fa-angle-right position-relative top-1 ms-1"></i></a> + </div> + </div> + </article> + </div> + <div class="col-lg-6"> + <article> + <div class="row"> + <div class="col-auto pe-0"> + <div class="date"> + <span class="day font-weight-extra-bold">12</span> + <span class="month text-1">JAN</span> + </div> + </div> + <div class="col ps-1"> + <h4 class="text-primary text-4"><a class="d-block" href="blog-post.html">Lorem ipsum dolor sit amet, consectetur</a></h4> + <p class="pe-4 mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> + <a href="/" class="read-more text-color-dark font-weight-semibold text-2">read more <i class="fas fa-angle-right position-relative top-1 ms-1"></i></a> + </div> + </div> + </article> + </div> + </div> + <div class="row"> + <div class="col-lg-6 mb-4 mb-lg-0"> + <article> + <div class="row"> + <div class="col-auto pe-0"> + <div class="date"> + <span class="day font-weight-extra-bold">11</span> + <span class="month text-1">JAN</span> + </div> + </div> + <div class="col ps-1"> + <h4 class="text-primary text-4"><a href="blog-post.html">Lorem ipsum dolor sit amet, consectetur</a></h4> + <p class="pe-4 mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> + <a href="/" class="read-more text-color-dark font-weight-semibold text-2">read more <i class="fas fa-angle-right position-relative top-1 ms-1"></i></a> + </div> + </div> + </article> + </div> + <div class="col-lg-6"> + <article> + <div class="row"> + <div class="col-auto pe-0"> + <div class="date"> + <span class="day font-weight-extra-bold">10</span> + <span class="month text-1">JAN</span> + </div> + </div> + <div class="col ps-1"> + <h4 class="text-primary text-4"><a href="blog-post.html">Lorem ipsum dolor sit amet, consectetur</a></h4> + <p class="pe-4 mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> + <a href="/" class="read-more text-color-dark font-weight-semibold text-2">read more <i class="fas fa-angle-right position-relative top-1 ms-1"></i></a> + </div> + </div> + </article> + </div> + </div> + </div> + </div> + </div> + <div class="col-lg-6"> + <h2 class="font-weight-normal text-6 mb-4"><strong class="font-weight-extra-bold">What</strong> Client’s Say</h2> + <div class="row"> + <div class="owl-carousel owl-theme dots-title dots-title-pos-2 mb-0" data-plugin-options="{'items': 1, 'autoHeight': true}"> + <div> + <div class="col"> + <div class="testimonial testimonial-primary"> + <blockquote> + <p class="mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec hendrerit vehicula est, in consequat.</p> + </blockquote> + <div class="testimonial-arrow-down"></div> + <div class="testimonial-author"> + <div class="testimonial-author-thumbnail"> + <img src="img/clients/client-1.jpg" class="rounded-circle" alt="" /> + </div> + <p><strong>John Doe</strong><span>Okler</span></p> + </div> + </div> + </div> + </div> + <div> + <div class="col"> + <div class="testimonial testimonial-primary"> + <blockquote> + <p class="mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec hendrerit vehicula est, in consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec hendrerit vehicula est, in consequat.</p> + </blockquote> + <div class="testimonial-arrow-down"></div> + <div class="testimonial-author"> + <div class="testimonial-author-thumbnail"> + <img src="img/clients/client-1.jpg" class="rounded-circle" alt="" /> + </div> + <p><strong>John Doe</strong><span>Okler</span></p> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </section> + </section> +@endsection diff --git a/routes/admin.php b/routes/admin.php deleted file mode 100644 index 739664a..0000000 --- a/routes/admin.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php - -use Illuminate\Support\Facades\Route; -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/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'); - }); -}); diff --git a/routes/koneko_website_admin.php b/routes/koneko_website_admin.php new file mode 100644 index 0000000..8116c87 --- /dev/null +++ b/routes/koneko_website_admin.php @@ -0,0 +1,64 @@ +<?php + +use Illuminate\Support\Facades\Route; +use Koneko\VuexyAdmin\Support\Routing\RouteScope; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\AnalyticsController; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\ComunicationController; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\ContactController; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\ContentController; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\SeoController; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\SettingsController; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\TranstaleController; + +RouteScope::auto(__FILE__, 'web-y-seo', function (RouteScope $r) { + // Web & SEO / Configuración general + $r->route('configuracion-general', 'settings.', SettingsController::class, function () { + Route::get('ajustes-generales', 'generalIndex')->name('general.index'); + Route::get('enlaces-sociales', 'socialIndex')->name('social.index'); + Route::get('visibilidad-en-buscadores', 'indexingIndex')->name('indexing.index'); + }); + + // Web & SEO / Contacto + $r->route('contacto', 'contact.', ContactController::class, function () { + Route::get('informacion-de-contacto', 'infoIndex')->name('info.index'); + Route::get('formulario-de-contacto', 'formIndex')->name('form.index'); + }); + + // Web & SEO / Analítica y seguimiento + $r->route('analitica-y-seguimiento', 'analytics.', AnalyticsController::class, function () { + Route::get('google-analytics', 'googleAnalyticsIndex')->name('google-analytics.index'); + Route::get('google-tags', 'googleTagsIndex')->name('google-tags.index'); + Route::get('google-search-console', 'googleSearchConsoleIndex')->name('google-search-console.index'); + Route::get('pixel-meta', 'pixelMetaIndex')->name('pixel-meta.index'); + }); + + // Web & SEO / Chat & Comunicación + $r->route('chat-y-comunicacion', 'comunication.', ComunicationController::class, function () { + Route::get('facebook-messenger', 'messengerIndex')->name('messenger.index'); + Route::get('whatsapp-chat', 'whatsappIndex')->name('whatsapp.index'); + Route::get('tawk-to', 'tawkToIndex')->name('tawk-to.index'); + Route::get('twitter-api', 'twitterIndex')->name('twitter.index'); + }); + + // Web & SEO / Traducciones e internacional + $r->route('traducciones-e-internacional', 'translate.', TranstaleController::class, function () { + Route::get('google-translate', 'googleIndex')->name('google.index'); + }); + + // Web & SEO / Contenido + $r->route('contenido', 'content.', ContentController::class, function () { + Route::get('preguntas-frecuentes', 'faqIndex')->name('faq.index'); + Route::get('galeria-de-imagenes', 'galleryIndex')->name('gallery.index'); + Route::get('avisos-legales', 'legalIndex')->name('legal.index'); + }); + + // Web & SEO / Herramientas SEO + $r->route('herramientas-seo', 'seo.', SeoController::class, function () { + Route::get('mapa-del-sitio', 'sitemapIndex')->name('sitemap.index'); + Route::get('google-json-ld', 'jsonldIndex')->name('jsonld.index'); + Route::get('robots-txt', 'robotsIndex')->name('robots.index'); + Route::get('manifest-json', 'manifestIndex')->name('manifest.index'); + Route::get('cannonical-urls', 'canonicalIndex')->name('canonical.index'); + Route::get('preview-social-cards', 'socialCardsIndex')->name('social-cards.index'); + }); +}); diff --git a/routes/koneko_website_blog.php b/routes/koneko_website_blog.php new file mode 100644 index 0000000..9f059b8 --- /dev/null +++ b/routes/koneko_website_blog.php @@ -0,0 +1,44 @@ +<?php + +use Illuminate\Support\Facades\Route; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\{ + BlogCategoryController, + BlogTagController, + BlogArticleController, + BlogCommentController +}; +use Koneko\VuexyAdmin\Support\Routing\RouteScope; + +RouteScope::auto(__FILE__, 'blog', 'blog.', function (RouteScope $r) { + // Categorías del Blog + $r->route('categorias', 'categories.', BlogCategoryController::class, function () { + Route::get('/', 'index')->name('index'); + Route::get('create', 'create')->name('create'); + Route::get('edit/{id}', 'edit')->name('edit'); + Route::get('delete/{id}', 'delete')->name('delete'); + }); + + // Etiquetas + $r->route('etiquetas', 'tags.', BlogTagController::class, function () { + Route::get('/', 'index')->name('index'); + Route::get('create', 'create')->name('create'); + Route::get('edit/{id}', 'edit')->name('edit'); + Route::get('delete/{id}', 'delete')->name('delete'); + }); + + // Artículos + $r->route('articulos', 'articles.', BlogArticleController::class, function () { + Route::get('/', 'index')->name('index'); + Route::get('create', 'create')->name('create'); + Route::get('edit/{id}', 'edit')->name('edit'); + Route::get('delete/{id}', 'delete')->name('delete'); + }); + + // Comentarios + $r->route('comentarios', 'comments.', BlogCommentController::class, function () { + Route::get('/', 'index')->name('index'); + Route::get('create', 'create')->name('create'); + Route::get('edit/{id}', 'edit')->name('edit'); + Route::get('delete/{id}', 'delete')->name('delete'); + }); +}); diff --git a/routes/koneko_website_cms.php b/routes/koneko_website_cms.php new file mode 100644 index 0000000..69180d1 --- /dev/null +++ b/routes/koneko_website_cms.php @@ -0,0 +1,8 @@ +<?php + +use Illuminate\Support\Facades\Route; +use Koneko\VuexyAdmin\Support\Routing\RouteScope; + +RouteScope::auto(__FILE__, function (RouteScope $r) { + +}); diff --git a/routes/koneko_website_sites.php b/routes/koneko_website_sites.php new file mode 100644 index 0000000..e3d60c0 --- /dev/null +++ b/routes/koneko_website_sites.php @@ -0,0 +1,14 @@ +<?php + +use Illuminate\Support\Facades\Route; +use Koneko\VuexyWebsiteAdmin\Application\Http\Controllers\WebsitePageController; + +// Contenido publico +Route::get('/{slug}', WebsitePageController::class) + ->where('slug', '^(?!admin|login|register|logout|email|user|storage|api|livewire|_debugbar|sanctum|preview)(.*)$') + ->name('website.content'); + +// Vista previa con firma +Route::get('/preview/{slug}', [WebsitePageController::class, 'preview']) + ->middleware(['signed']) // Protege con firma + ->name('website.preview'); diff --git a/src/Application/Bootstrap/Context/SiteContext.php b/src/Application/Bootstrap/Context/SiteContext.php new file mode 100644 index 0000000..0d92e24 --- /dev/null +++ b/src/Application/Bootstrap/Context/SiteContext.php @@ -0,0 +1,72 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Application\Bootstrap\Context; + +use Illuminate\Http\Request; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteSite; + +/** + * 🧠 Resolve contexto de sitio activo en modo multisite + */ +class SiteContext +{ + protected static ?WebsiteSite $resolved = null; + + /** + * Devuelve el sitio actual (usando cache interna si ya fue resuelto) + */ + public static function resolve(): ?WebsiteSite + { + return static::$resolved; + } + + /** + * Resuelve el sitio activo desde el request actual (dominio o path) + */ + public static function resolveFromRequest(Request $request): ?WebsiteSite + { + // Evita doble resolución + if (static::$resolved instanceof WebsiteSite) { + return static::$resolved; + } + + $host = $request->getHost(); + $path = trim($request->path(), '/'); + + // 🧪 Estrategia 1: dominio exacto + $site = WebsiteSite::where('domain', $host)->first(); + + // 🧪 Estrategia 2: subdominio match (ej. tienda1.koneko.mx) + if (!$site && str_contains($host, '.')) { + $subdomain = explode('.', $host)[0]; + $site = WebsiteSite::where('subdomain', $subdomain)->first(); + } + + // 🧪 Estrategia 3: segmento del path (ej. /site-x/*) + if (!$site && str_contains($path, '/')) { + $firstSegment = explode('/', $path)[0]; + $site = WebsiteSite::where('slug', $firstSegment)->first(); + } + + // Establece contexto (null si no hay match) + static::$resolved = $site; + + return $site; + } + + /** + * Fuerza un sitio específico (desde sesión o entorno controlado) + */ + public static function set(WebsiteSite $site): void + { + static::$resolved = $site; + } + + /** + * Limpia el contexto (en tests o entorno controlado) + */ + public static function forget(): void + { + static::$resolved = null; + } +} diff --git a/src/Application/Cache/Builders/BlogLayoutVarsBuilder.php b/src/Application/Cache/Builders/BlogLayoutVarsBuilder.php new file mode 100644 index 0000000..afdfe74 --- /dev/null +++ b/src/Application/Cache/Builders/BlogLayoutVarsBuilder.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Cache\Builders; + +use Koneko\VuexyWebsiteAdmin\Application\Cache\Service\WebsiteRenderCacheService; + +/** + * 🎛️ Builder de variables administrativas para el layout de administración. + * - Fuente primaria: settings globales (namespace 'koneko.core.layout.blog') + * - Permite override explícito por usuario autenticado. + */ +class BlogLayoutVarsBuilder +{ + +} diff --git a/src/Application/Cache/Builders/EcommerceLayoutVarsBuilder.php b/src/Application/Cache/Builders/EcommerceLayoutVarsBuilder.php new file mode 100644 index 0000000..8e1ad1d --- /dev/null +++ b/src/Application/Cache/Builders/EcommerceLayoutVarsBuilder.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Cache\Builders; + +use Koneko\VuexyWebsiteAdmin\Application\Cache\Service\WebsiteRenderCacheService; + +/** + * 🎛️ Builder de variables administrativas para el layout de administración. + * - Fuente primaria: settings globales (namespace 'koneko.core.layout.ecommerce') + * - Permite override explícito por usuario autenticado. + */ +class EcommerceLayoutVarsBuilder +{ + +} diff --git a/src/Application/Cache/Builders/LandingLayoutVarsBuilder.php b/src/Application/Cache/Builders/LandingLayoutVarsBuilder.php new file mode 100644 index 0000000..5564512 --- /dev/null +++ b/src/Application/Cache/Builders/LandingLayoutVarsBuilder.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Cache\Builders; + +use Koneko\VuexyWebsiteAdmin\Application\Cache\Service\WebsiteRenderCacheService; + +/** + * 🎛️ Builder de variables administrativas para el layout de administración. + * - Fuente primaria: settings globales (namespace 'koneko.core.layout.landing') + * - Permite override explícito por usuario autenticado. + */ +class LandingLayoutVarsBuilder +{ + +} diff --git a/src/Application/Cache/Builders/WebsiteLayoutVarsBuilder.php b/src/Application/Cache/Builders/WebsiteLayoutVarsBuilder.php new file mode 100644 index 0000000..dff65c2 --- /dev/null +++ b/src/Application/Cache/Builders/WebsiteLayoutVarsBuilder.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Cache\Builders; + +use Koneko\VuexyWebsiteAdmin\Application\Cache\Service\WebsiteRenderCacheService; + +/** + * 🎛️ Builder de variables administrativas para el layout de administración. + * - Fuente primaria: settings globales (namespace 'koneko.core.layout.website') + * - Permite override explícito por usuario autenticado. + */ +class WebsiteLayoutVarsBuilder +{ + +} diff --git a/src/Application/Cache/Builders/WebsiteSeoVarsBuilder.php b/src/Application/Cache/Builders/WebsiteSeoVarsBuilder.php new file mode 100644 index 0000000..a24fde0 --- /dev/null +++ b/src/Application/Cache/Builders/WebsiteSeoVarsBuilder.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Cache\Builders; + +use Koneko\VuexyWebsiteAdmin\Application\Cache\Service\WebsiteRenderCacheService; + +/** + * 🎛️ Builder de variables administrativas para el layout de administración. + * - Fuente primaria: settings globales (namespace 'koneko.core.seo.website') + * - Permite override explícito por usuario autenticado. + */ +class WebsiteSeoVarsBuilder +{ + +} diff --git a/src/Application/Cache/Builders/WebsiteSocialVarsBuilder.php b/src/Application/Cache/Builders/WebsiteSocialVarsBuilder.php new file mode 100644 index 0000000..3f3336b --- /dev/null +++ b/src/Application/Cache/Builders/WebsiteSocialVarsBuilder.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Cache\Builders; + +use Koneko\VuexyWebsiteAdmin\Application\Cache\Service\WebsiteRenderCacheService; + +/** + * 🎛️ Builder de variables administrativas para el layout de administración. + * - Fuente primaria: settings globales (namespace 'koneko.core.social.website') + * - Permite override explícito por usuario autenticado. + */ +class WebsiteSocialVarsBuilder +{ + +} diff --git a/src/Application/Enums/WebsiteContentType.php b/src/Application/Enums/WebsiteContentType.php new file mode 100644 index 0000000..035f475 --- /dev/null +++ b/src/Application/Enums/WebsiteContentType.php @@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Enums; + +enum WebsiteContentType: string +{ + case Page = 'page'; + case Partial = 'partial'; + case Menu = 'menu'; + case Component = 'component'; + + public static function values(): array + { + return array_column(self::cases(), 'value'); + } +} \ No newline at end of file diff --git a/src/Application/Enums/WebsiteMenuItem/WebsiteMenuItemTarget.php b/src/Application/Enums/WebsiteMenuItem/WebsiteMenuItemTarget.php new file mode 100644 index 0000000..cf94775 --- /dev/null +++ b/src/Application/Enums/WebsiteMenuItem/WebsiteMenuItemTarget.php @@ -0,0 +1,14 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Application\Enums\WebsiteMenuItem; + +enum WebsiteMenuItemTarget: string +{ + case _self = '_self'; + case _blank = '_blank'; + + public static function values(): array + { + return array_column(self::cases(), 'value'); + } +} \ No newline at end of file diff --git a/src/Application/Enums/WebsiteMenuItem/WebsiteMenuItemType.php b/src/Application/Enums/WebsiteMenuItem/WebsiteMenuItemType.php new file mode 100644 index 0000000..56a7aa1 --- /dev/null +++ b/src/Application/Enums/WebsiteMenuItem/WebsiteMenuItemType.php @@ -0,0 +1,17 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Application\Enums\WebsiteMenuItem; + +enum WebsiteMenuItemType: string +{ + case Custom = 'custom'; + case CmsPage = 'cms_page'; + case BlogArticle = 'blog_article'; + case BlogCategory = 'blog_category'; + case Action = 'action'; + + public static function values(): array + { + return array_column(self::cases(), 'value'); + } +} diff --git a/src/Application/Enums/WebsiteSeoProfile/WebsiteSeoProfileType.php b/src/Application/Enums/WebsiteSeoProfile/WebsiteSeoProfileType.php new file mode 100644 index 0000000..1267e67 --- /dev/null +++ b/src/Application/Enums/WebsiteSeoProfile/WebsiteSeoProfileType.php @@ -0,0 +1,14 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Enums\WebsiteSeoProfile; + +enum WebsiteSeoProfileType: string +{ + case Page = 'page'; + case Landing = 'landing'; + case Product = 'product'; + case Category = 'category'; + case Blog = 'blog'; +} diff --git a/src/Application/Http/Controllers/AnalyticsController.php b/src/Application/Http/Controllers/AnalyticsController.php new file mode 100644 index 0000000..87a90ad --- /dev/null +++ b/src/Application/Http/Controllers/AnalyticsController.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class AnalyticsController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function googleAnalyticsIndex() + { + return view('vuexy-website-admin::analytics.google-analytics.index'); + } + + public function googleTagsIndex() + { + return view('vuexy-website-admin::analytics.google-tags.index'); + } + + public function googleSearchConsoleIndex() + { + return view('vuexy-website-admin::analytics.google-search-console.index'); + } + + public function pixelMetaIndex() + { + return view('vuexy-website-admin::analytics.pixel-meta.index'); + } +} diff --git a/src/Application/Http/Controllers/BlogArticleController.php b/src/Application/Http/Controllers/BlogArticleController.php new file mode 100644 index 0000000..f9e863a --- /dev/null +++ b/src/Application/Http/Controllers/BlogArticleController.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class BlogArticleController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function index() + { + return view('vuexy-website-admin::blog.article.index'); + } + +} diff --git a/src/Application/Http/Controllers/BlogCategoryController.php b/src/Application/Http/Controllers/BlogCategoryController.php new file mode 100644 index 0000000..24f9a57 --- /dev/null +++ b/src/Application/Http/Controllers/BlogCategoryController.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class BlogCategoryController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function index() + { + return view('vuexy-website-admin::blog.category.index'); + } + +} diff --git a/src/Application/Http/Controllers/BlogCommentController.php b/src/Application/Http/Controllers/BlogCommentController.php new file mode 100644 index 0000000..3af9456 --- /dev/null +++ b/src/Application/Http/Controllers/BlogCommentController.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class BlogCommentController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function index() + { + return view('vuexy-website-admin::blog.comment.index'); + } + +} diff --git a/src/Application/Http/Controllers/BlogTagController.php b/src/Application/Http/Controllers/BlogTagController.php new file mode 100644 index 0000000..2c59edd --- /dev/null +++ b/src/Application/Http/Controllers/BlogTagController.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class BlogTagController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function index() + { + return view('vuexy-website-admin::blog.tag.index'); + } + +} diff --git a/src/Application/Http/Controllers/ComunicationController.php b/src/Application/Http/Controllers/ComunicationController.php new file mode 100644 index 0000000..4ae8b23 --- /dev/null +++ b/src/Application/Http/Controllers/ComunicationController.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class ComunicationController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function messengerIndex() + { + return view('vuexy-website-admin::comunication.messenger.index'); + } + + public function whatsappIndex() + { + return view('vuexy-website-admin::comunication.whatsapp.index'); + } + + public function tawkToIndex() + { + return view('vuexy-website-admin::comunication.tawk-to.index'); + } + + public function twitterIndex() + { + return view('vuexy-website-admin::comunication.twitter.index'); + } + +} diff --git a/src/Application/Http/Controllers/ContactController.php b/src/Application/Http/Controllers/ContactController.php new file mode 100644 index 0000000..8b26de0 --- /dev/null +++ b/src/Application/Http/Controllers/ContactController.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class ContactController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function infoIndex() + { + return view('vuexy-website-admin::contact.info.index'); + } + + public function formIndex() + { + return view('vuexy-website-admin::contact.form.index'); + } +} diff --git a/src/Application/Http/Controllers/ContentController.php b/src/Application/Http/Controllers/ContentController.php new file mode 100644 index 0000000..fd3b70f --- /dev/null +++ b/src/Application/Http/Controllers/ContentController.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; +use Illuminate\Http\Request; +use Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Faq\FaqTableConfigBuilder; + +class ContentController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function faqIndex(Request $request) + { + if ($request->ajax()) { + return app(FaqTableConfigBuilder::class) + ->getQueryBuilder($request) + ->getJson(); + } + return view('vuexy-website-admin::content.faq.index'); + } + + public function galleryIndex() + { + return view('vuexy-website-admin::content.gallery.index'); + } + + public function legalIndex() + { + return view('vuexy-website-admin::content.legal.index'); + } +} diff --git a/src/Application/Http/Controllers/SeoController.php b/src/Application/Http/Controllers/SeoController.php new file mode 100644 index 0000000..d1f8278 --- /dev/null +++ b/src/Application/Http/Controllers/SeoController.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class SeoController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function sitemapIndex() + { + return view('vuexy-website-admin::seo.sitemap.index'); + } + + public function jsonldIndex() + { + return view('vuexy-website-admin::seo.jsonld.index'); + } + + public function robotsIndex() + { + return view('vuexy-website-admin::seo.robots.index'); + } + + public function manifestIndex() + { + return view('vuexy-website-admin::seo.manifest.index'); + } + + public function canonicalIndex() + { + return view('vuexy-website-admin::seo.canonical.index'); + } + + public function socialCardsIndex() + { + return view('vuexy-website-admin::seo.social-cards.index'); + } +} diff --git a/src/Application/Http/Controllers/SettingsController.php b/src/Application/Http/Controllers/SettingsController.php new file mode 100644 index 0000000..513de95 --- /dev/null +++ b/src/Application/Http/Controllers/SettingsController.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class SettingsController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function generalIndex() + { + return view('vuexy-website-admin::settings.general.index'); + } + + public function socialIndex() + { + return view('vuexy-website-admin::settings.social.index'); + } + + public function indexingIndex() + { + return view('vuexy-website-admin::settings.indexing.index'); + } +} diff --git a/src/Application/Http/Controllers/TranstaleController.php b/src/Application/Http/Controllers/TranstaleController.php new file mode 100644 index 0000000..1524571 --- /dev/null +++ b/src/Application/Http/Controllers/TranstaleController.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; + +class TranstaleController extends Controller +{ + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function googleIndex() + { + return view('vuexy-website-admin::translate.google.index'); + } +} diff --git a/src/Application/Http/Controllers/WebsitePageController.php b/src/Application/Http/Controllers/WebsitePageController.php new file mode 100644 index 0000000..5f9c3ee --- /dev/null +++ b/src/Application/Http/Controllers/WebsitePageController.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Controllers; + +use Illuminate\Routing\Controller; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\View; + +class WebsitePageController extends Controller +{ + /** + * Renderiza la página pública principal. + */ + public function __invoke(Request $request, string $slug = null) + { + $template = View::shared('_layout.template') ?? 'Porto'; + $type = View::shared('_layout.type') ?? 'page'; + $view = "vuexy-website-layout-porto::{$type}"; // SIEMPRE apunta a Porto + + if (!View::exists($view)) { + abort(404, "Plantilla no encontrada: {$view}"); + } + + return view($view); + } + + + /** + * Vista previa segura firmada. + */ + public function preview(Request $request, string $slug) + { + if (! $request->hasValidSignature()) { + abort(403, 'Firma de vista previa no válida.'); + } + + $template = View::shared('_layout.template') ?? 'anonymous_template'; + $type = View::shared('_layout.type') ?? 'page'; + $view = "{$template}::{$type}"; + + if (!View::exists($view)) { + abort(404, "Plantilla de vista previa no encontrada: {$view}"); + } + + return view($view); + } +} diff --git a/src/Application/Http/Middleware/WebsiteContentMiddleware copy.php b/src/Application/Http/Middleware/WebsiteContentMiddleware copy.php new file mode 100644 index 0000000..d3d157e --- /dev/null +++ b/src/Application/Http/Middleware/WebsiteContentMiddleware copy.php @@ -0,0 +1,65 @@ +<?php + +namespace Koneko\VuexyAdmin\Application\Http\Middleware; + +use Closure; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\View; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteContent; + +class WebsiteContentMiddleware +{ + public function handle(Request $request, Closure $next) + { + $slug = $request->route('slug'); + + /** @var WebsiteSite $site */ + $site = app('currentSite'); + + /** @var WebsiteContent|null $content */ + $content = WebsiteContent::query() + ->with(['seoProfile']) + ->where('site_id', $site->id) + ->bySlug($slug) + ->firstOrFail(); + + // 🛑 Bloqueo por estado + if ($content->is_draft && !Auth::check()) { + abort(403, 'Contenido no publicado.'); + } + + // 🛑 Fechas de visibilidad + $now = now(); + if ( + $content->visible_from && $content->visible_from > $now || + $content->visible_until && $content->visible_until < $now + ) { + abort(403, 'Contenido no disponible.'); + } + + // 🔐 Roles o permisos + if (!empty($content->roles) && !Auth::user()?->hasAnyRole($content->roles)) { + abort(403, 'Acceso restringido.'); + } + + if (!empty($content->permissions) && !Auth::user()?->hasAnyPermission($content->permissions)) { + abort(403, 'Permiso insuficiente.'); + } + + // 🧠 SEO + Layout + Vista pública + $seo = $content->getEffectiveSeoMetadata(); + $layout = $content->template ?? $site->template ?? 'vuexy-website-layout-porto'; + $variant = $content->type ?? 'page'; + + // 📤 Compartir variables globales a la vista pública + View::share([ + '_content' => $content, + '_seo' => $seo, + '_template' => $layout, + '_variant' => $variant, + ]); + + return $next($request); + } +} diff --git a/src/Application/Http/Middleware/WebsiteContentMiddleware.php b/src/Application/Http/Middleware/WebsiteContentMiddleware.php new file mode 100644 index 0000000..f1babc4 --- /dev/null +++ b/src/Application/Http/Middleware/WebsiteContentMiddleware.php @@ -0,0 +1,67 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Middleware; + +use Closure; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\{Auth, View}; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteContent; +use Symfony\Component\HttpKernel\Exception\HttpException; + +class WebsiteContentMiddleware +{ + public function handle(Request $request, Closure $next) + { + $slug = $request->route('slug'); + + + /* + $site = app('currentSite'); + + $content = WebsiteContent::query() + ->with(['seoProfile']) + ->where('site_id', $site->id) + ->bySlug($slug) + ->firstOrFail(); + + if ($request->routeIs('website.preview') && $request->hasValidSignature()) { + View::share('_isPreview', true); + + } else { + if ($content->is_draft && !Auth::check()) { + throw new HttpException(403, 'Contenido no publicado.'); + } + + $now = now(); + if ( + ($content->visible_from && $content->visible_from > $now) || + ($content->visible_until && $content->visible_until < $now) + ) { + throw new HttpException(403, 'Contenido no disponible.'); + } + + $user = Auth::user(); + if (!empty($content->roles) && !$user?->hasAnyRole($content->roles)) { + throw new HttpException(403, 'Acceso restringido.'); + } + + if (!empty($content->permissions) && !$user?->hasAnyPermission($content->permissions)) { + throw new HttpException(403, 'Permiso insuficiente.'); + } + } + + $seo = $content->getEffectiveSeoMetadata(); + $layout = $content->template ?? $site->template ?? 'vuexy-website-layout-porto'; + $variant = $content->type ?? 'page'; + + View::share([ + '_content' => $content, + '_seo' => $seo, + '_template' => $layout, + '_variant' => $variant, + ]); + */ + + return $next($request); + } +} diff --git a/src/Application/Http/Middleware/WebsiteContextMiddleware.php b/src/Application/Http/Middleware/WebsiteContextMiddleware.php new file mode 100644 index 0000000..7ed05da --- /dev/null +++ b/src/Application/Http/Middleware/WebsiteContextMiddleware.php @@ -0,0 +1,94 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Middleware; + +use Closure; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\View; +use Koneko\VuexyWebsiteAdmin\Application\Bootstrap\Context\SiteContext; +use Koneko\VuexyWebsiteAdmin\Application\Cache\Builders\WebsiteLayoutVarsBuilder; +use Koneko\VuexyWebsiteAdmin\Application\Cache\Builders\WebsiteSeoVarsBuilder; +use Koneko\VuexyWebsiteAdmin\Application\Cache\Builders\WebsiteSocialVarsBuilder; + +/** + * 🌐 Middleware para detectar el sitio activo y compartir variables públicas (multisite). + */ +class WebsiteContextMiddleware +{ + public function handle(Request $request, Closure $next) + { + if (!str_contains($request->header('Accept'), 'text/html')) { + return $next($request); + } + + // Detecta el sitio activo desde dominio/subdominio/path + $site = SiteContext::resolveFromRequest($request); + + $layout = [ + 'template' => $site->template, + 'status' => $site->status, + 'is_indexable' => $site->is_indexable, + ]; + + $seo = [ + 'canonical' => $site->canonical ?? '', + 'viewport' => $site->viewport ?? '', + 'title' => $site->title ?? '', + 'description' => $site->description ?? '', + 'robots' => $site->robots ?? '', + 'language' => $site->language ?? '', + 'author' => $site->author ?? '', + 'og:title' => $site->og_title ?? '', + 'og:site_name' => $site->og_site_name ?? '', + 'og:url' => $site->og_url ?? '', + 'og:description' => $site->og_description ?? '', + 'og:type' => $site->og_type ?? '', + 'og:image' => $site->og_image ?? '', + 'twitter:card' => $site->twitter_card ?? '', + 'twitter:site' => $site->twitter_site ?? '', + 'twitter:creator' => $site->twitter_creator ?? '', + 'favicon' => [ + '180x180' => $site->favicon['180x180'] ?? '', + '152x152' => $site->favicon['152x152'] ?? '', + '120x120' => $site->favicon['120x120'] ?? '', + '76x76' => $site->favicon['76x76'] ?? '', + '192x192' => $site->favicon['192x192'] ?? '', + '16x16' => $site->favicon['16x16'] ?? '', + ], + 'theme-color' => $site->theme_color ?? '', + 'ld+json' => $site->ld_json ?? '', + ]; + + $social = [ + 'twitter:card' => $site->twitter_card ?? '', + 'twitter:site' => $site->twitter_site ?? '', + 'twitter:creator' => $site->twitter_creator ?? '', + ]; + + $contact = [ + 'email' => $site->contact_email ?? '', + 'phone' => $site->contact_phone ?? '', + 'address' => $site->contact_address ?? '', + ]; + + /* + // Variables visuales generales (favicon, logo, layout, etc.) + $layoutVars = app(WebsiteLayoutVarsBuilder::class)->forSite($site)->get(); + + // Variables SEO (title, meta, index, canonical, etc.) + $seoVars = app(WebsiteSeoVarsBuilder::class)->forSite($site)->get(); + + // Variables sociales (twitter, opengraph, etc.) + $socialVars = app(WebsiteSocialVarsBuilder::class)->forSite($site)->get(); + */ + // Compartir a todas las vistas públicas + View::share([ + '_layout' => $layout, + '_seo' => $seo, + '_social' => $social, + '_contact' => $contact, + ]); + + return $next($request); + } +} diff --git a/src/Application/Http/Middleware/WebsiteTemplateMiddleware.php b/src/Application/Http/Middleware/WebsiteTemplateMiddleware.php new file mode 100644 index 0000000..09bed30 --- /dev/null +++ b/src/Application/Http/Middleware/WebsiteTemplateMiddleware.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Http\Middleware; + +use Closure; +use Illuminate\Support\Facades\View; +use Koneko\VuexyWebsiteAdmin\Website\UX\Content\WebsiteBreadcrumbsBuilderService; +use Koneko\VuexyWebsiteAdmin\Website\UX\Menu\WebsiteMenuBuilderService; +use Koneko\VuexyWebsiteAdmin\Website\UX\Template\WebsiteVarsBuilderService; + +class WebsiteTemplateMiddleware +{ + public function __construct() + { + // + } + + public function handle($request, Closure $next) + { + // Aplicar configuración de layout antes de que la vista se cargue + if (str_contains($request->header('Accept'), 'text/html')) { + View::share([ + '_web' => []//app(WebsiteVarsBuilderService::class)->getWebsiteVars(), + //'_menu' => app(WebsiteMenuBuilderService::class)->getForUser(), + //'_breadcrumbs' => app(WebsiteBreadcrumbsBuilderService::class)->getBreadcrumbs(), + ]); + } + + return $next($request); + } +} diff --git a/src/Application/LocalModule.php b/src/Application/LocalModule.php new file mode 100644 index 0000000..ee9e178 --- /dev/null +++ b/src/Application/LocalModule.php @@ -0,0 +1,11 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Application; + +use Koneko\VuexyAdmin\Application\CoreModule; + +final class LocalModule +{ + public const NAMESPACE = CoreModule::NAMESPACE; + public const COMPONENT = 'website-admin'; +} diff --git a/src/Application/Template/VuexyAdminImageHandlerService.php b/src/Application/Template/VuexyAdminImageHandlerService.php new file mode 100644 index 0000000..0335b91 --- /dev/null +++ b/src/Application/Template/VuexyAdminImageHandlerService.php @@ -0,0 +1,198 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyAdmin\Application\UX\Template; + +use Illuminate\Support\Facades\Storage; +use Intervention\Image\ImageManager; +use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface; + +/** + * Servicio para gestionar favicon y logos administrativos. + */ +class VuexyAdminImageHandlerService +{ + private string $driver; + private string $imageDisk = 'public'; + private string $faviconBasePath = 'favicon/'; + private string $logoBasePath = 'images/logo/'; + + private SettingsRepositoryInterface $settings; + + /** + * Constructor. + */ + public function __construct(SettingsRepositoryInterface $settings) + { + $this->driver = config('image.driver', 'gd'); + $this->settings = $settings->self(); + } + + /** + * Procesa y guarda múltiples versiones del favicon. + * + * @param \Illuminate\Http\UploadedFile $image + * @return void + */ + public function processAndSaveFavicon(\Illuminate\Http\UploadedFile $image): void + { + Storage::makeDirectory("{$this->imageDisk}/{$this->faviconBasePath}"); + + $currentNamespace = $this->settings->get('favicon_ns'); + if ($currentNamespace) { + $this->deleteOldFiles($this->generateFaviconPaths($currentNamespace)); + } + + $imageManager = new ImageManager($this->driver); + $baseName = uniqid('favicon_', true); + + foreach ($this->getFaviconSizes() as $size => [$w, $h]) { + $resized = $imageManager->read($image->getRealPath())->cover($w, $h); + Storage::disk($this->imageDisk) + ->put("{$this->faviconBasePath}{$baseName}_{$size}.png", $resized->toPng(indexed: true)); + } + + $this->settings->set('favicon_ns', "{$this->faviconBasePath}{$baseName}"); + } + + /** + * Procesa y guarda versiones de imagen de logo. + * + * @param \Illuminate\Http\UploadedFile $image + * @param string $type + * @return void + */ + public function processAndSaveImageLogo(\Illuminate\Http\UploadedFile $image, string $type = ''): void + { + Storage::makeDirectory("{$this->imageDisk}/{$this->logoBasePath}"); + + $this->deleteOldLogoImages($type); + + $imageManager = new ImageManager($this->driver); + $original = $imageManager->read($image->getRealPath()); + + $this->saveResizedLogo($original, 22500, 'small', $type); + $this->saveResizedLogo($original, 75625, 'medium', $type); + $this->saveResizedLogo($original, 262144, '', $type); + $this->saveBase64Logo($original, 230400, $type); + } + + /** + * Redimensiona y guarda un logo. + */ + private function saveResizedLogo($image, int $maxPixels, string $suffix = '', string $type = ''): void + { + $resized = clone $image; + $this->resizeImageToMaxPixels($resized, $maxPixels); + + $fileName = uniqid("logo_{$suffix}{$type}", true) . '.png'; + $path = "{$this->logoBasePath}{$fileName}"; + + Storage::disk($this->imageDisk)->put($path, $resized->toPng(indexed: true)); + + $this->settings->set("image_logo" . ($suffix ? "_{$suffix}" : '') . ($type ? "_{$type}" : ''), $path); + } + + /** + * Guarda un logo en formato base64. + */ + private function saveBase64Logo($image, int $maxPixels, string $type = ''): void + { + $resized = clone $image; + $this->resizeImageToMaxPixels($resized, $maxPixels); + + $base64 = (string) $resized->toJpg(40)->toDataUri(); + + $this->settings->set("image_logo_base64" . ($type ? "_{$type}" : ''), $base64); + } + + /** + * Elimina archivos de imágenes antiguos. + */ + private function deleteOldFiles(array $paths): void + { + foreach ($paths as $path) { + if (Storage::disk($this->imageDisk)->exists($path)) { + Storage::disk($this->imageDisk)->delete($path); + } + } + } + + /** + * Elimina versiones anteriores de logos. + */ + private function deleteOldLogoImages(string $type = ''): void + { + $keys = [ + "image_logo" . ($type ? "_{$type}" : ''), + "image_logo_small" . ($type ? "_{$type}" : ''), + "image_logo_medium" . ($type ? "_{$type}" : ''), + ]; + + $paths = []; + + foreach ($keys as $key) { + $path = $this->settings->get($key); + if ($path) { + $paths[] = $path; + } + } + + $this->deleteOldFiles($paths); + } + + /** + * Redimensiona imagen conservando aspecto. + */ + private function resizeImageToMaxPixels($image, int $maxPixels) + { + $originalWidth = $image->width(); + $originalHeight = $image->height(); + $aspectRatio = $originalWidth / $originalHeight; + + if ($aspectRatio > 1) { + $newWidth = sqrt($maxPixels * $aspectRatio); + $newHeight = $newWidth / $aspectRatio; + + } else { + $newHeight = sqrt($maxPixels / $aspectRatio); + $newWidth = $newHeight * $aspectRatio; + } + + $image->resize( + (int) round($newWidth), + (int) round($newHeight), + function ($constraint) { + $constraint->aspectRatio(); + $constraint->upsize(); + } + ); + + return $image; + } + + + /** + * Obtiene los tamaños estándar para favicons. + */ + private function getFaviconSizes(): array + { + return [ + '16x16' => [16, 16], + '76x76' => [76, 76], + '120x120' => [120, 120], + '152x152' => [152, 152], + '180x180' => [180, 180], + '192x192' => [192, 192], + ]; + } + + /** + * Genera las rutas de favicons a eliminar. + */ + private function generateFaviconPaths(string $base): array + { + return array_map(fn($size) => "{$this->faviconBasePath}{$base}_{$size}.png", array_keys($this->getFaviconSizes())); + } +} diff --git a/Services/WebsiteSettingsService.php b/src/Application/Template/___WebsiteSettingsService.php similarity index 98% rename from Services/WebsiteSettingsService.php rename to src/Application/Template/___WebsiteSettingsService.php index bbe1c5e..4b9cc44 100644 --- a/Services/WebsiteSettingsService.php +++ b/src/Application/Template/___WebsiteSettingsService.php @@ -1,11 +1,13 @@ <?php +declare(strict_types=1); + namespace Koneko\VuexyWebsiteAdmin\Services; use Illuminate\Support\Facades\Storage; use Intervention\Image\ImageManager; use Koneko\VuexyAdmin\Models\Setting; -use Koneko\VuexyAdmin\Services\SettingsService; +use Koneko\VuexyAdmin\Application\Settings\SettingsService; /** * Servicio para gestionar la configuración del template del website. @@ -15,7 +17,7 @@ use Koneko\VuexyAdmin\Services\SettingsService; * * @package Koneko\VuexyWebsiteAdmin\Services */ -class WebsiteSettingsService +class ___WebsiteSettingsService { /** @var string Driver de procesamiento de imágenes */ private $driver; diff --git a/Services/WebsiteTemplateService.php b/src/Application/Template/___WebsiteTemplateService.php similarity index 97% rename from Services/WebsiteTemplateService.php rename to src/Application/Template/___WebsiteTemplateService.php index 107b876..210de87 100644 --- a/Services/WebsiteTemplateService.php +++ b/src/Application/Template/___WebsiteTemplateService.php @@ -1,6 +1,8 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Services; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\Services; use Illuminate\Support\Facades\{Cache,Schema}; use Koneko\VuexyAdmin\Models\Setting; @@ -12,11 +14,14 @@ use Koneko\VuexyAdmin\Models\Setting; * de personalización, logos, favicons y otras configuraciones de la interfaz. * Implementa un sistema de caché para optimizar el rendimiento. */ -class WebsiteTemplateService +class ___WebsiteTemplateService { /** @var int Tiempo de vida del caché en minutos (60 * 24 * 30 = 30 días) */ protected $cacheTTL = 60 * 24 * 30; + /** @var string Prefijo del caché */ + protected $cachePrefix = 'vuexy_website:'; + /** * Obtiene las variables del template del website. * @@ -149,9 +154,9 @@ class WebsiteTemplateService * @param string $default Valor predeterminado si no se encuentra la configuración * @return string Ruta de la imagen */ - private function getImagePath(array $settings, string $key, string $default): string + private function getImagePath(array $settings, string $key, ?string $default = ''): string { - return $settings[$key] ?? $default; + return (string) ($settings[$key] ?? $default ?? ''); } /* diff --git a/src/Application/UI/Livewire/Analytics/GoogleAnalytics/GoogleAnalyticsCard.php b/src/Application/UI/Livewire/Analytics/GoogleAnalytics/GoogleAnalyticsCard.php new file mode 100644 index 0000000..0019421 --- /dev/null +++ b/src/Application/UI/Livewire/Analytics/GoogleAnalytics/GoogleAnalyticsCard.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Analytics\GoogleAnalytics; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; +use Livewire\Component; + +class GoogleAnalyticsCard 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.analytics.google-analytics.google-analytics-card'); + } +} diff --git a/src/Application/UI/Livewire/Analytics/GoogleSearchConsole/GoogleSearchConsoleCard.php b/src/Application/UI/Livewire/Analytics/GoogleSearchConsole/GoogleSearchConsoleCard.php new file mode 100644 index 0000000..de9cd4f --- /dev/null +++ b/src/Application/UI/Livewire/Analytics/GoogleSearchConsole/GoogleSearchConsoleCard.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Analytics\GoogleSearchConsole; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; +use Livewire\Component; + +class GoogleSearchConsoleCard 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.analytics.google-search-console.google-search-console-card'); + } +} diff --git a/src/Application/UI/Livewire/Analytics/GoogleTags/GoogleTagsCard.php b/src/Application/UI/Livewire/Analytics/GoogleTags/GoogleTagsCard.php new file mode 100644 index 0000000..07cd066 --- /dev/null +++ b/src/Application/UI/Livewire/Analytics/GoogleTags/GoogleTagsCard.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Analytics\GoogleTags; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; +use Livewire\Component; + +class GoogleTagsCard 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.analytics.google-tags.google-tags-card'); + } +} diff --git a/src/Application/UI/Livewire/Analytics/PixelMeta/PixelMetaCard.php b/src/Application/UI/Livewire/Analytics/PixelMeta/PixelMetaCard.php new file mode 100644 index 0000000..f50b924 --- /dev/null +++ b/src/Application/UI/Livewire/Analytics/PixelMeta/PixelMetaCard.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Analytics\PixelMeta; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; +use Livewire\Component; + +class PixelMetaCard 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.analytics.pixel-meta.pixel-meta-card'); + } +} diff --git a/src/Application/UI/Livewire/Blog/Article/BlogArticleOffcanvasForm.php b/src/Application/UI/Livewire/Blog/Article/BlogArticleOffcanvasForm.php new file mode 100644 index 0000000..b97a0da --- /dev/null +++ b/src/Application/UI/Livewire/Blog/Article/BlogArticleOffcanvasForm.php @@ -0,0 +1,219 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Article; + +use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Rule; +use Koneko\VuexyAdmin\Support\Livewire\Components\Form\AbstractFormOffCanvasComponent; +use Koneko\VuexyContacts\Application\Services\ContactCatalogService; +use Koneko\VuexyStoreManager\Services\StoreCatalogService; +use Koneko\VuexyWarehouse\Models\Warehouse; + +/** + * Class ArticleOffcanvasForm + * + * 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 BlogArticleOffcanvasForm 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 focusColumnOnOpen(): 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->getCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->getCatalog('users', '', ['limit' => -1]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.faq.offcanvas-form'; + } +} diff --git a/src/Application/UI/Livewire/Blog/Article/BlogArticlesTable.php b/src/Application/UI/Livewire/Blog/Article/BlogArticlesTable.php new file mode 100644 index 0000000..9c02d6c --- /dev/null +++ b/src/Application/UI/Livewire/Blog/Article/BlogArticlesTable.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Article; + +use Koneko\VuexyAdmin\Support\Livewire\Components\Table\AbstractTableComponent; +use Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Blog\ArticlesTableConfigBuilder; + +class BlogArticlesTable extends AbstractTableComponent +{ + /** + * Define la clase del builder de configuración. + */ + protected function configBuilderClass(): ?string + { + return ArticlesTableConfigBuilder::class; + } + + /** + * Vista Blade que debe renderizar este componente. + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.content.faq.index'; + } +} diff --git a/src/Application/UI/Livewire/Blog/Category/BlogCategoriesTable.php b/src/Application/UI/Livewire/Blog/Category/BlogCategoriesTable.php new file mode 100644 index 0000000..3e0b58c --- /dev/null +++ b/src/Application/UI/Livewire/Blog/Category/BlogCategoriesTable.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Category; + +use Koneko\VuexyAdmin\Support\Livewire\Components\Table\AbstractTableComponent; +use Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Blog\CategoriesTableConfigBuilder; + +class BlogCategoriesTable extends AbstractTableComponent +{ + /** + * Define la clase del builder de configuración. + */ + protected function configBuilderClass(): ?string + { + return CategoriesTableConfigBuilder::class; + } + + /** + * Vista Blade que debe renderizar este componente. + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.blog.category.index'; + } +} diff --git a/src/Application/UI/Livewire/Blog/Category/BlogCategoryOffcanvasForm.php b/src/Application/UI/Livewire/Blog/Category/BlogCategoryOffcanvasForm.php new file mode 100644 index 0000000..e79054f --- /dev/null +++ b/src/Application/UI/Livewire/Blog/Category/BlogCategoryOffcanvasForm.php @@ -0,0 +1,219 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Category; + +use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Rule; +use Koneko\VuexyAdmin\Support\Livewire\Components\Form\AbstractFormOffCanvasComponent; +use Koneko\VuexyContacts\Application\Services\ContactCatalogService; +use Koneko\VuexyStoreManager\Services\StoreCatalogService; +use Koneko\VuexyWarehouse\Models\Warehouse; + +/** + * Class BlogCategoryOffcanvasForm + * + * 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 BlogCategoryOffcanvasForm 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 focusColumnOnOpen(): 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->getCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->getCatalog('users', '', ['limit' => -1]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.faq.offcanvas-form'; + } +} diff --git a/src/Application/UI/Livewire/Blog/Comment/BlogCommentOffcanvasForm.php b/src/Application/UI/Livewire/Blog/Comment/BlogCommentOffcanvasForm.php new file mode 100644 index 0000000..527d132 --- /dev/null +++ b/src/Application/UI/Livewire/Blog/Comment/BlogCommentOffcanvasForm.php @@ -0,0 +1,219 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Comment; + +use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Rule; +use Koneko\VuexyAdmin\Support\Livewire\Components\Form\AbstractFormOffCanvasComponent; +use Koneko\VuexyContacts\Application\Services\ContactCatalogService; +use Koneko\VuexyStoreManager\Services\StoreCatalogService; +use Koneko\VuexyWarehouse\Models\Warehouse; + +/** + * Class BlogCommentOffcanvasForm + * + * 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 BlogCommentOffcanvasForm 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 focusColumnOnOpen(): 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->getCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->getCatalog('users', '', ['limit' => -1]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.faq.offcanvas-form'; + } +} diff --git a/src/Application/UI/Livewire/Blog/Comment/BlogCommentsTable.php b/src/Application/UI/Livewire/Blog/Comment/BlogCommentsTable.php new file mode 100644 index 0000000..bd649c0 --- /dev/null +++ b/src/Application/UI/Livewire/Blog/Comment/BlogCommentsTable.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Comment; + +use Koneko\VuexyAdmin\Support\Livewire\Components\Table\AbstractTableComponent; +use Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Blog\CommentsTableConfigBuilder; + +class BlogCommentsTable extends AbstractTableComponent +{ + /** + * Define la clase del builder de configuración. + */ + protected function configBuilderClass(): ?string + { + return CommentsTableConfigBuilder::class; + } + + /** + * Vista Blade que debe renderizar este componente. + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.content.faq.index'; + } +} diff --git a/src/Application/UI/Livewire/Blog/Tag/BlogTagOffcanvasForm.php b/src/Application/UI/Livewire/Blog/Tag/BlogTagOffcanvasForm.php new file mode 100644 index 0000000..e961f63 --- /dev/null +++ b/src/Application/UI/Livewire/Blog/Tag/BlogTagOffcanvasForm.php @@ -0,0 +1,218 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Tag; + +use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Rule; +use Koneko\VuexyAdmin\Support\Livewire\Components\Form\AbstractFormOffCanvasComponent; +use Koneko\VuexyContacts\Application\Services\ContactCatalogService; +use Koneko\VuexyStoreManager\Services\StoreCatalogService; + +/** + * Class BlogTagOffcanvasForm + * + * 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\VuexyTag\Livewire\Tags + */ +class BlogTagOffcanvasForm 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 = [ + 'editTag' => 'loadFormModel', + 'confirmDeletionTag' => '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 Tag::class; + } + + /** + * Define los campos del formulario. + * + * @return array<string, mixed> + */ + protected function fields(): array + { + return (new Tag())->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 focusColumnOnOpen(): 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('Tags', '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->getCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->getCatalog('users', '', ['limit' => -1]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.faq.offcanvas-form'; + } +} diff --git a/src/Application/UI/Livewire/Blog/Tag/BlogTagsTable.php b/src/Application/UI/Livewire/Blog/Tag/BlogTagsTable.php new file mode 100644 index 0000000..1da23e2 --- /dev/null +++ b/src/Application/UI/Livewire/Blog/Tag/BlogTagsTable.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Tag; + +use Koneko\VuexyAdmin\Support\Livewire\Components\Table\AbstractTableComponent; +use Koneko\VuexyWebsiteAdmin\Application\UIX\ConfigBuilders\Blog\TagsTableConfigBuilder; + +class BlogTagsTable extends AbstractTableComponent +{ + /** + * Define la clase del builder de configuración. + */ + protected function configBuilderClass(): ?string + { + return TagsTableConfigBuilder::class; + } + + /** + * Vista Blade que debe renderizar este componente. + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.content.faq.index'; + } +} diff --git a/src/Application/UI/Livewire/Comunication/Messenger/MessengerCard.php b/src/Application/UI/Livewire/Comunication/Messenger/MessengerCard.php new file mode 100644 index 0000000..d7cc540 --- /dev/null +++ b/src/Application/UI/Livewire/Comunication/Messenger/MessengerCard.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Comunication\Messenger; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; +use Livewire\Component; + +class MessengerCard 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.comunication.messenger.messenger-card'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/ChatSettings.php b/src/Application/UI/Livewire/Comunication/TawkTo/TawkToCard.php similarity index 83% rename from Livewire/VuexyWebsiteAdmin/ChatSettings.php rename to src/Application/UI/Livewire/Comunication/TawkTo/TawkToCard.php index dc756ed..a152c5f 100644 --- a/Livewire/VuexyWebsiteAdmin/ChatSettings.php +++ b/src/Application/UI/Livewire/Comunication/TawkTo/TawkToCard.php @@ -1,12 +1,14 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); -use Koneko\VuexyAdmin\Services\SettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Comunication\TawkTo; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; use Livewire\Component; -class ChatSettings extends Component +class TawkToCard extends Component { private $targetNotify = "#website-chat-settings-card .notification-container"; @@ -62,6 +64,6 @@ class ChatSettings extends Component public function render() { - return view('vuexy-website-admin::livewire.vuexy.chat-settings'); + return view('vuexy-website-admin::livewire.comunication.tawk-to.tawk-to-card'); } } diff --git a/Livewire/VuexyWebsiteAdmin/GoogleAnalyticsSettings.php b/src/Application/UI/Livewire/Comunication/Twitter/TwitterCard.php similarity index 81% rename from Livewire/VuexyWebsiteAdmin/GoogleAnalyticsSettings.php rename to src/Application/UI/Livewire/Comunication/Twitter/TwitterCard.php index 78f9d8a..20f94db 100644 --- a/Livewire/VuexyWebsiteAdmin/GoogleAnalyticsSettings.php +++ b/src/Application/UI/Livewire/Comunication/Twitter/TwitterCard.php @@ -1,12 +1,14 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); -use Koneko\VuexyAdmin\Services\SettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Comunication\Twitter; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; use Livewire\Component; -class GoogleAnalyticsSettings extends Component +class TwitterCard extends Component { private $targetNotify = "#website-analytics-settings-card .notification-container"; @@ -58,6 +60,6 @@ class GoogleAnalyticsSettings extends Component public function render() { - return view('vuexy-website-admin::livewire.vuexy.analytics-settings'); + return view('vuexy-website-admin::livewire.comunication.twitter.twitter-card'); } } diff --git a/src/Application/UI/Livewire/Comunication/Whatsapp/WhatsappCard.php b/src/Application/UI/Livewire/Comunication/Whatsapp/WhatsappCard.php new file mode 100644 index 0000000..ca95de4 --- /dev/null +++ b/src/Application/UI/Livewire/Comunication/Whatsapp/WhatsappCard.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Comunication\Whatsapp; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; +use Livewire\Component; + +class WhatsappCard 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.comunication.whatsapp.whatsapp-card'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/ContactFormSettings.php b/src/Application/UI/Livewire/Contact/Form/ContactFormCard.php similarity index 81% rename from Livewire/VuexyWebsiteAdmin/ContactFormSettings.php rename to src/Application/UI/Livewire/Contact/Form/ContactFormCard.php index 4ac685b..bbd9ee0 100644 --- a/Livewire/VuexyWebsiteAdmin/ContactFormSettings.php +++ b/src/Application/UI/Livewire/Contact/Form/ContactFormCard.php @@ -1,14 +1,16 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); -use Koneko\VuexyAdmin\Services\SettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Contact\Form; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; use Livewire\Component; -class ContactFormSettings extends Component +class ContactFormCard extends Component { - private $targetNotify = "#website-contact-form-settings-card .notification-container"; + private $targetNotify = "#website-contact-form-card-card .notification-container"; public $to_email, $to_email_cc, @@ -65,6 +67,6 @@ class ContactFormSettings extends Component public function render() { - return view('vuexy-website-admin::livewire.vuexy.contact-form-settings'); + return view('vuexy-website-admin::livewire.contact.form.contact-form-card'); } } diff --git a/Livewire/VuexyWebsiteAdmin/ContactInfoSettings.php b/src/Application/UI/Livewire/Contact/Info/ContactInfoCard.php similarity index 84% rename from Livewire/VuexyWebsiteAdmin/ContactInfoSettings.php rename to src/Application/UI/Livewire/Contact/Info/ContactInfoCard.php index 671c62c..777b268 100644 --- a/Livewire/VuexyWebsiteAdmin/ContactInfoSettings.php +++ b/src/Application/UI/Livewire/Contact/Info/ContactInfoCard.php @@ -1,14 +1,16 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); -use Koneko\VuexyAdmin\Services\SettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Contact\Info; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; use Livewire\Component; -class ContactInfoSettings extends Component +class ContactInfoCard extends Component { - private $targetNotify = "#website-contact-info-settings-card .notification-container"; + private $targetNotify = "#website-contact-info-card-card .notification-container"; public $phone_number, $phone_number_ext, @@ -73,6 +75,6 @@ class ContactInfoSettings extends Component public function render() { - return view('vuexy-website-admin::livewire.vuexy.contact-info-settings'); + return view('vuexy-website-admin::livewire.contact.info.contact-info-card'); } } diff --git a/Livewire/VuexyWebsiteAdmin/LocationSettings.php b/src/Application/UI/Livewire/Contact/Info/ContactLocationCard.php similarity index 80% rename from Livewire/VuexyWebsiteAdmin/LocationSettings.php rename to src/Application/UI/Livewire/Contact/Info/ContactLocationCard.php index 1134b63..a59ec9d 100644 --- a/Livewire/VuexyWebsiteAdmin/LocationSettings.php +++ b/src/Application/UI/Livewire/Contact/Info/ContactLocationCard.php @@ -1,14 +1,16 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); -use Koneko\VuexyAdmin\Services\SettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Contact\Info; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; use Livewire\Component; -class LocationSettings extends Component +class ContactLocationCard extends Component { - private $targetNotify = "#website-location-settings-card .notification-container"; + private $targetNotify = "#website-location-card-card .notification-container"; public $direccion, $location_lat, @@ -64,6 +66,6 @@ class LocationSettings extends Component public function render() { - return view('vuexy-website-admin::livewire.vuexy.location-settings'); + return view('vuexy-website-admin::livewire.contact.info.location-card'); } } diff --git a/src/Application/UI/Livewire/Content/Faq/FaqIndex.php b/src/Application/UI/Livewire/Content/Faq/FaqIndex.php new file mode 100644 index 0000000..6d22ad5 --- /dev/null +++ b/src/Application/UI/Livewire/Content/Faq/FaqIndex.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Content\Faq; + +use Koneko\VuexyAdmin\Support\Livewire\Components\Table\AbstractTableComponent; +use Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Faq\FaqTableConfigBuilder; + +class FaqIndex extends AbstractTableComponent +{ + /** + * Define la clase del builder de configuración. + */ + protected function configBuilderClass(): ?string + { + return FaqTableConfigBuilder::class; + } + + /** + * Vista Blade que debe renderizar este componente. + */ + protected function viewPath(): string + { + return 'vuexy-website-admin::livewire.content.faq.index'; + } +} diff --git a/Livewire/Faq/FaqOffcanvasForm.php b/src/Application/UI/Livewire/Content/Faq/FaqOffcanvasForm.php similarity index 92% rename from Livewire/Faq/FaqOffcanvasForm.php rename to src/Application/UI/Livewire/Content/Faq/FaqOffcanvasForm.php index 9057a72..9efda7f 100644 --- a/Livewire/Faq/FaqOffcanvasForm.php +++ b/src/Application/UI/Livewire/Content/Faq/FaqOffcanvasForm.php @@ -1,11 +1,13 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\Faq; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Content\Faq; use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; -use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent; -use Koneko\VuexyContacts\Services\ContactCatalogService; +use Koneko\VuexyAdmin\Support\Livewire\Components\Form\AbstractFormOffCanvasComponent; +use Koneko\VuexyContacts\Application\Services\ContactCatalogService; use Koneko\VuexyStoreManager\Services\StoreCatalogService; use Koneko\VuexyWarehouse\Models\Warehouse; @@ -90,7 +92,7 @@ class FaqOffcanvasForm extends AbstractFormOffCanvasComponent * * @return string */ - protected function focusOnOpen(): string + protected function focusColumnOnOpen(): string { return 'code'; } @@ -200,8 +202,8 @@ class FaqOffcanvasForm extends AbstractFormOffCanvasComponent $contactCatalogService = app(ContactCatalogService::class); return [ - 'store_options' => $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]), - 'manager_options' => $contactCatalogService->searchCatalog('users', '', ['limit' => -1]), + 'store_options' => $storeCatalogService->getCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->getCatalog('users', '', ['limit' => -1]), ]; } diff --git a/src/Application/UI/Livewire/Content/Gallery/GalleryIndex.php b/src/Application/UI/Livewire/Content/Gallery/GalleryIndex.php new file mode 100644 index 0000000..9daa2e2 --- /dev/null +++ b/src/Application/UI/Livewire/Content/Gallery/GalleryIndex.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Content\Gallery; + +use Livewire\Component; + +class GalleryIndex extends Component +{ + public function render() + { + return view('vuexy-website-admin::livewire.content.gallery.index'); + } +} diff --git a/Livewire/LegalNotices/LegalNoticesIndex.php b/src/Application/UI/Livewire/Content/Legal/LegalIndex.php similarity index 91% rename from Livewire/LegalNotices/LegalNoticesIndex.php rename to src/Application/UI/Livewire/Content/Legal/LegalIndex.php index 3a72353..d8a0803 100644 --- a/Livewire/LegalNotices/LegalNoticesIndex.php +++ b/src/Application/UI/Livewire/Content/Legal/LegalIndex.php @@ -1,13 +1,14 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\LegalNotices; +declare(strict_types=1); +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Content\Legal; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; use Livewire\Component; -use Koneko\VuexyAdmin\Rules\NotEmptyHtml; -use Koneko\VuexyAdmin\Services\SettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; -class LegalNoticesIndex extends Component +class LegalIndex extends Component { private $targetNotify = "#website-legal-settings-card .notification-container"; @@ -104,6 +105,6 @@ class LegalNoticesIndex extends Component public function render() { - return view('vuexy-website-admin::livewire.legal-notices.index'); + return view('vuexy-website-admin::livewire.content.legal-notices.index'); } } diff --git a/Livewire/LegalNotices/LegalNoticeOffCanvasForm.php b/src/Application/UI/Livewire/Content/Legal/LegalOffCanvasForm.php similarity index 90% rename from Livewire/LegalNotices/LegalNoticeOffCanvasForm.php rename to src/Application/UI/Livewire/Content/Legal/LegalOffCanvasForm.php index c727eca..836557e 100644 --- a/Livewire/LegalNotices/LegalNoticeOffCanvasForm.php +++ b/src/Application/UI/Livewire/Content/Legal/LegalOffCanvasForm.php @@ -1,16 +1,18 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\LegalNotices; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Content\Legal; use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; -use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent; -use Koneko\VuexyContacts\Services\ContactCatalogService; +use Koneko\VuexyAdmin\Support\Livewire\Components\Form\AbstractFormOffCanvasComponent; +use Koneko\VuexyContacts\Application\Services\ContactCatalogService; use Koneko\VuexyStoreManager\Services\StoreCatalogService; use Koneko\VuexyWarehouse\Models\Warehouse; /** - * Class LegalNoticeOffCanvasForm + * Class LegalOffCanvasForm * * Componente Livewire para gestionar almacenes. * Extiende la clase AbstractFormOffCanvasComponent e implementa validaciones dinámicas, @@ -18,7 +20,7 @@ use Koneko\VuexyWarehouse\Models\Warehouse; * * @package Koneko\VuexyWarehouse\Livewire\Warehouses */ -class LegalNoticeOffCanvasForm extends AbstractFormOffCanvasComponent +class LegalOffCanvasForm extends AbstractFormOffCanvasComponent { /** * Propiedades del formulario relacionadas con el almacén. @@ -90,7 +92,7 @@ class LegalNoticeOffCanvasForm extends AbstractFormOffCanvasComponent * * @return string */ - protected function focusOnOpen(): string + protected function focusColumnOnOpen(): string { return 'code'; } @@ -200,8 +202,8 @@ class LegalNoticeOffCanvasForm extends AbstractFormOffCanvasComponent $contactCatalogService = app(ContactCatalogService::class); return [ - 'store_options' => $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]), - 'manager_options' => $contactCatalogService->searchCatalog('users', '', ['limit' => -1]), + 'store_options' => $storeCatalogService->getCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->getCatalog('users', '', ['limit' => -1]), ]; } diff --git a/src/Application/UI/Livewire/Seo/Canonical/CanonicalIndex.php b/src/Application/UI/Livewire/Seo/Canonical/CanonicalIndex.php new file mode 100644 index 0000000..1b2fe2b --- /dev/null +++ b/src/Application/UI/Livewire/Seo/Canonical/CanonicalIndex.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Canonical; + +use Livewire\Component; +use Koneko\VuexyWebsiteAdmin\Models\SitemapUrl; + +class CanonicalIndex 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.seo.canonical.index', ['urls' => $this->urls]); + }} diff --git a/src/Application/UI/Livewire/Seo/Jsonld/JsonldIndex.php b/src/Application/UI/Livewire/Seo/Jsonld/JsonldIndex.php new file mode 100644 index 0000000..9d3ac53 --- /dev/null +++ b/src/Application/UI/Livewire/Seo/Jsonld/JsonldIndex.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Jsonld; + +use Livewire\Component; +use Koneko\VuexyWebsiteAdmin\Models\SitemapUrl; + +class JsonldIndex 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.seo.jsonld.index', ['urls' => $this->urls]); + } +} diff --git a/src/Application/UI/Livewire/Seo/Manifest/ManifestCard.php b/src/Application/UI/Livewire/Seo/Manifest/ManifestCard.php new file mode 100644 index 0000000..d4b081e --- /dev/null +++ b/src/Application/UI/Livewire/Seo/Manifest/ManifestCard.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Manifest; + +use Livewire\Component; +use Koneko\VuexyWebsiteAdmin\Models\SitemapUrl; + +class ManifestCard 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.seo.manifest.manifest-card', ['urls' => $this->urls]); + }} diff --git a/src/Application/UI/Livewire/Seo/Robots/RobotsCard.php b/src/Application/UI/Livewire/Seo/Robots/RobotsCard.php new file mode 100644 index 0000000..fdf3a52 --- /dev/null +++ b/src/Application/UI/Livewire/Seo/Robots/RobotsCard.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Robots; + +use Livewire\Component; +use Koneko\VuexyWebsiteAdmin\Models\SitemapUrl; + +class RobotsCard 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.seo.robots.robot-card', ['urls' => $this->urls]); + } +} diff --git a/Livewire/SitemapManager/SitemapManagerIndex.php b/src/Application/UI/Livewire/Seo/Sitemap/SitemapIndex.php similarity index 75% rename from Livewire/SitemapManager/SitemapManagerIndex.php rename to src/Application/UI/Livewire/Seo/Sitemap/SitemapIndex.php index 836d8e8..089caa3 100644 --- a/Livewire/SitemapManager/SitemapManagerIndex.php +++ b/src/Application/UI/Livewire/Seo/Sitemap/SitemapIndex.php @@ -1,11 +1,13 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\SitemapManager; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Sitemap; use Livewire\Component; use Koneko\VuexyWebsiteAdmin\Models\SitemapUrl; -class SitemapManagerIndex extends Component +class SitemapIndex extends Component { public $urls, $newUrl, $changefreq = 'weekly', $priority = 0.5; @@ -34,5 +36,5 @@ class SitemapManagerIndex extends Component public function render() { - return view('vuexy-website-admin::livewire.sitemap-manager.index', ['urls' => $this->urls]); + return view('vuexy-website-admin::livewire.seo.sitemap.index', ['urls' => $this->urls]); }} diff --git a/Livewire/SitemapManager/SitemapUrlOffcanvasForm.php b/src/Application/UI/Livewire/Seo/Sitemap/SitemapUrlOffcanvasForm.php similarity index 90% rename from Livewire/SitemapManager/SitemapUrlOffcanvasForm.php rename to src/Application/UI/Livewire/Seo/Sitemap/SitemapUrlOffcanvasForm.php index 8959574..25ad134 100644 --- a/Livewire/SitemapManager/SitemapUrlOffcanvasForm.php +++ b/src/Application/UI/Livewire/Seo/Sitemap/SitemapUrlOffcanvasForm.php @@ -1,11 +1,13 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\SitemapManager; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Sitemap; use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; -use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent; -use Koneko\VuexyContacts\Services\ContactCatalogService; +use Koneko\VuexyAdmin\Support\Livewire\Components\Form\AbstractFormOffCanvasComponent; +use Koneko\VuexyContacts\Application\Services\ContactCatalogService; use Koneko\VuexyStoreManager\Services\StoreCatalogService; use Koneko\VuexyWarehouse\Models\Warehouse; @@ -90,7 +92,7 @@ class SitemapUrlOffcanvasForm extends AbstractFormOffCanvasComponent * * @return string */ - protected function focusOnOpen(): string + protected function focusColumnOnOpen(): string { return 'code'; } @@ -200,8 +202,8 @@ class SitemapUrlOffcanvasForm extends AbstractFormOffCanvasComponent $contactCatalogService = app(ContactCatalogService::class); return [ - 'store_options' => $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]), - 'manager_options' => $contactCatalogService->searchCatalog('users', '', ['limit' => -1]), + 'store_options' => $storeCatalogService->getCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->getCatalog('users', '', ['limit' => -1]), ]; } @@ -212,6 +214,6 @@ class SitemapUrlOffcanvasForm extends AbstractFormOffCanvasComponent */ protected function viewPath(): string { - return 'vuexy-website-admin::livewire.sitemap-manager.offcanvas-form'; + return 'vuexy-website-admin::livewire.seo.sitemap-manager.offcanvas-form'; } } diff --git a/src/Application/UI/Livewire/Seo/SocialCards/SocialCardsIndex.php b/src/Application/UI/Livewire/Seo/SocialCards/SocialCardsIndex.php new file mode 100644 index 0000000..0991724 --- /dev/null +++ b/src/Application/UI/Livewire/Seo/SocialCards/SocialCardsIndex.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\SocialCards; + +use Livewire\Component; +use Koneko\VuexyWebsiteAdmin\Models\SitemapUrl; + +class SocialCardsIndex 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.seo.social-cards.index', ['urls' => $this->urls]); + }} diff --git a/Livewire/VuexyWebsiteAdmin/LogoOnDarkBgSettings.php b/src/Application/UI/Livewire/Settings/General/LogoOnDarkBgCard.php similarity index 64% rename from Livewire/VuexyWebsiteAdmin/LogoOnDarkBgSettings.php rename to src/Application/UI/Livewire/Settings/General/LogoOnDarkBgCard.php index 3ef70ea..d1639d8 100644 --- a/Livewire/VuexyWebsiteAdmin/LogoOnDarkBgSettings.php +++ b/src/Application/UI/Livewire/Settings/General/LogoOnDarkBgCard.php @@ -1,24 +1,26 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\General; use Livewire\Component; use Livewire\WithFileUploads; use Koneko\VuexyWebsiteAdmin\Services\WebsiteSettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; -class LogoOnDarkBgSettings extends Component +class LogoOnDarkBgCard extends Component { use WithFileUploads; - private $targetNotify = "#logo-on-dark-bg-settings-card .notification-container"; + private $targetNotify = "#logo-on-dark-bg-card-card .notification-container"; public $website_image_logo_dark, $upload_image_logo_dark; public function mount() { - $this->resetForm(); + $this->loadForm(); } public function save() @@ -34,7 +36,7 @@ class LogoOnDarkBgSettings extends Component app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); // Recargamos el formulario - $this->resetForm(); + $this->loadForm(); // Notificación de éxito $this->dispatch( @@ -45,17 +47,17 @@ class LogoOnDarkBgSettings extends Component ); } - public function resetForm() + public function loadForm() { // Obtener los valores de las configuraciones de la base de datos - $settings = app(WebsiteTemplateService::class)->getWebsiteVars(); + //$settings = app(WebsiteTemplateService::class)->getWebsiteVars(); $this->upload_image_logo_dark = null; - $this->website_image_logo_dark = $settings['image_logo']['large_dark']; + //$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'); + return view('vuexy-website-admin::livewire.settings.general.logo-on-dark-bg-card'); } } diff --git a/Livewire/VuexyWebsiteAdmin/LogoOnLightBgSettings.php b/src/Application/UI/Livewire/Settings/General/LogoOnLightBgCard.php similarity index 64% rename from Livewire/VuexyWebsiteAdmin/LogoOnLightBgSettings.php rename to src/Application/UI/Livewire/Settings/General/LogoOnLightBgCard.php index 2b47e4b..8c3988a 100644 --- a/Livewire/VuexyWebsiteAdmin/LogoOnLightBgSettings.php +++ b/src/Application/UI/Livewire/Settings/General/LogoOnLightBgCard.php @@ -1,24 +1,26 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\General; use Livewire\Component; use Livewire\WithFileUploads; use Koneko\VuexyWebsiteAdmin\Services\WebsiteSettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; -class LogoOnLightBgSettings extends Component +class LogoOnLightBgCard extends Component { use WithFileUploads; - private $targetNotify = "#logo-on-light-bg-settings-card .notification-container"; + private $targetNotify = "#logo-on-light-bg-card-card .notification-container"; public $website_image_logo, $upload_image_logo; public function mount() { - $this->resetForm(); + $this->loadForm(); } public function save() @@ -34,7 +36,7 @@ class LogoOnLightBgSettings extends Component app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); // Recargamos el formulario - $this->resetForm(); + $this->loadForm(); // Notificación de éxito $this->dispatch( @@ -45,17 +47,17 @@ class LogoOnLightBgSettings extends Component ); } - public function resetForm() + public function loadForm() { // Obtener los valores de las configuraciones de la base de datos - $settings = app(WebsiteTemplateService::class)->getWebsiteVars(); + //$settings = app(WebsiteTemplateService::class)->getWebsiteVars(); $this->upload_image_logo = null; - $this->website_image_logo = $settings['image_logo']['large']; + //$this->website_image_logo = $settings['image_logo']['large']; } public function render() { - return view('vuexy-website-admin::livewire.vuexy.logo-on-light-bg-settings'); + return view('vuexy-website-admin::livewire.settings.general.logo-on-light-bg-card'); } } diff --git a/Livewire/VuexyWebsiteAdmin/WebsiteDescriptionSettings.php b/src/Application/UI/Livewire/Settings/General/WebsiteDescriptionCard.php similarity index 54% rename from Livewire/VuexyWebsiteAdmin/WebsiteDescriptionSettings.php rename to src/Application/UI/Livewire/Settings/General/WebsiteDescriptionCard.php index 2603bab..4794d60 100644 --- a/Livewire/VuexyWebsiteAdmin/WebsiteDescriptionSettings.php +++ b/src/Application/UI/Livewire/Settings/General/WebsiteDescriptionCard.php @@ -1,28 +1,28 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\General; use Livewire\Component; -use Koneko\VuexyAdmin\Services\SettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; -class WebsiteDescriptionSettings extends Component +class WebsiteDescriptionCard extends Component { - private $targetNotify = "#website-description-settings-card .notification-container"; + private $targetNotify = "#website-description-card-card .notification-container"; - public $title, - $description; + public $title; public function mount() { - $this->resetForm(); + $this->loadForm(); } public function save() { $this->validate([ - 'title' => 'required|string|max:255', - 'description' => 'nullable|string|max:255', + 'title' => 'required|string|max:255', ]); // Guardar título del sitio en configuraciones @@ -35,7 +35,7 @@ class WebsiteDescriptionSettings extends Component app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); // Recargamos el formulario - $this->resetForm(); + $this->loadForm(); // Notificación de éxito $this->dispatch( @@ -46,17 +46,17 @@ class WebsiteDescriptionSettings extends Component ); } - public function resetForm() + public function loadForm() { // Obtener los valores de las configuraciones de la base de datos - $settings = app(WebsiteTemplateService::class)->getWebsiteVars(); + //$settings = app(WebsiteTemplateService::class)->getWebsiteVars(); - $this->title = $settings['title']; - $this->description = $settings['description']; + //$this->title = $settings['title']; + //$this->description = $settings['description']; } public function render() { - return view('vuexy-website-admin::livewire.vuexy.website-description-settings'); + return view('vuexy-website-admin::livewire.settings.general.website-description-card'); } } diff --git a/Livewire/VuexyWebsiteAdmin/WebsiteFaviconSettings.php b/src/Application/UI/Livewire/Settings/General/WebsiteFaviconCard.php similarity index 78% rename from Livewire/VuexyWebsiteAdmin/WebsiteFaviconSettings.php rename to src/Application/UI/Livewire/Settings/General/WebsiteFaviconCard.php index 2666e24..2297dff 100644 --- a/Livewire/VuexyWebsiteAdmin/WebsiteFaviconSettings.php +++ b/src/Application/UI/Livewire/Settings/General/WebsiteFaviconCard.php @@ -1,17 +1,19 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\General; use Livewire\Component; use Livewire\WithFileUploads; use Koneko\VuexyWebsiteAdmin\Services\WebsiteSettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; -class WebsiteFaviconSettings extends Component +class WebsiteFaviconCard extends Component { use WithFileUploads; - private $targetNotify = "#website-favicon-settings-card .notification-container"; + private $targetNotify = "#website-favicon-card-card .notification-container"; public $website_favicon_16x16, $website_favicon_76x76, @@ -24,7 +26,7 @@ class WebsiteFaviconSettings extends Component public function mount() { - $this->resetForm(); + $this->loadForm(); } public function save() @@ -40,7 +42,7 @@ class WebsiteFaviconSettings extends Component app(WebsiteTemplateService::class)->clearWebsiteVarsCache(); // Recargamos el formulario - $this->resetForm(); + $this->loadForm(); // Notificación de éxito $this->dispatch( @@ -51,9 +53,10 @@ class WebsiteFaviconSettings extends Component ); } - public function resetForm() + public function loadForm() { // Obtener los valores de las configuraciones de la base de datos + /* $settings = app(WebsiteTemplateService::class)->getWebsiteVars(); $this->upload_image_favicon = null; @@ -63,10 +66,11 @@ class WebsiteFaviconSettings extends Component $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'); + return view('vuexy-website-admin::livewire.settings.general.website-favicon-card'); } } diff --git a/src/Application/UI/Livewire/Settings/Indexing/IndexingCard.php b/src/Application/UI/Livewire/Settings/Indexing/IndexingCard.php new file mode 100644 index 0000000..0a3b633 --- /dev/null +++ b/src/Application/UI/Livewire/Settings/Indexing/IndexingCard.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\Indexing; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyAdmin\Application\System\SettingsService as SystemSettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; +use Livewire\Component; + +class IndexingCard 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() + { + + } + + 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(SystemSettingsService::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.settings.indexing.indexing-card'); + } +} diff --git a/Livewire/VuexyWebsiteAdmin/SocialMediaSettings.php b/src/Application/UI/Livewire/Settings/Social/SocialCard.php similarity index 91% rename from Livewire/VuexyWebsiteAdmin/SocialMediaSettings.php rename to src/Application/UI/Livewire/Settings/Social/SocialCard.php index 12cbd29..9c16106 100644 --- a/Livewire/VuexyWebsiteAdmin/SocialMediaSettings.php +++ b/src/Application/UI/Livewire/Settings/Social/SocialCard.php @@ -1,12 +1,14 @@ <?php -namespace Koneko\VuexyWebsiteAdmin\Livewire\VuexyWebsiteAdmin; +declare(strict_types=1); -use Koneko\VuexyAdmin\Services\SettingsService; -use Koneko\VuexyWebsiteAdmin\Services\WebsiteTemplateService; +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\Social; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; use Livewire\Component; -class SocialMediaSettings extends Component +class SocialCard extends Component { private $targetNotify = "#website-social-settings-card .notification-container"; @@ -93,6 +95,6 @@ class SocialMediaSettings extends Component public function render() { - return view('vuexy-website-admin::livewire.vuexy.social-media-settings'); + return view('vuexy-website-admin::livewire.settings.social.social-card'); } } diff --git a/src/Application/UI/Livewire/Translate/Google/GoogleTanslateCard.php b/src/Application/UI/Livewire/Translate/Google/GoogleTanslateCard.php new file mode 100644 index 0000000..9df1437 --- /dev/null +++ b/src/Application/UI/Livewire/Translate/Google/GoogleTanslateCard.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Translate\Google; + +use Koneko\VuexyAdmin\Application\Settings\SettingsService; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteTemplateService; +use Livewire\Component; + +class GoogleTanslateCard 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.translate.google.google-translate-card'); + } +} diff --git a/src/Application/UIX/ConfigBuilders/Blog/ArticlesTableConfigBuilder.php b/src/Application/UIX/ConfigBuilders/Blog/ArticlesTableConfigBuilder.php new file mode 100644 index 0000000..f16c0a5 --- /dev/null +++ b/src/Application/UIX/ConfigBuilders/Blog/ArticlesTableConfigBuilder.php @@ -0,0 +1,175 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Blog; + +use Illuminate\Support\Facades\DB; +use Koneko\VuexyAdmin\Support\Builders\AbstractTableConfigBuilder; +use Koneko\VuexyWebsiteAdmin\Models\Faq; + +class ArticlesTableConfigBuilder extends AbstractTableConfigBuilder +{ + /** + * Devuelve el modelo de datos. + */ + public function getModelClass(): string + { + return Faq::class; + } + + /** + * Devuelve las columnas seleccionadas en la consulta SQL. + */ + public static function getIndexColumns(): array + { + return [ + 'faqs.id', + 'faqs.category_id', + 'faq_categories.name AS category_name', + 'faq_categories.icon AS category_icon', + 'faqs.question', + 'faqs.answer', + 'faqs.order', + 'faqs.is_active', + 'faqs.created_at', + 'faqs.updated_at', + ]; + } + + /** + * Devuelve las etiquetas (labels) para las columnas. + */ + public static function getIndexLabels(): array + { + return [ + 'action' => 'Acciones', + 'category_name' => 'Categoría', + 'question' => 'Pregunta', + 'answer' => 'Respuesta', + 'order' => 'Orden', + 'is_active' => 'Activo', + 'created_at' => 'Creado el', + 'updated_at' => 'Actualizado el', + 'deleted_at' => 'Eliminado el', + ]; + } + + /** + * Devuelve los JOINs requeridos por la consulta. + */ + public static function getIndexJoins(): array + { + return [ + ['faq_categories', 'faqs.category_id', '=', 'faq_categories.id'] + ]; + } + + /** + * Devuelve los filtros aplicables en la tabla (como búsqueda global). + */ + public static function getIndexFilters(): array + { + return [ + 'search' => [ + 'faqs.question', + 'faqs.answer', + ] + ]; + } + + /** + * Devuelve la configuración de formatos y visibilidad para cada columna. + */ + public static function getIndexFormatters(): array + { + return [ + /* + 'action' => [ + 'formatter' => 'VehicleAssignmentsAactionFormatter', + ], + */ + /* + 'carrier_id' => [ + 'formatter' => 'profilePhotoFormatter', + ], + */ + 'vehicle_type_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'brand_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'current_insurer_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'plate_number' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'model' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'year_manufacture' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'vin' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'fuel_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'axles' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'capacity_kg' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'start_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'end_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'max_km_allowed' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'assignment_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'status' => [ + 'formatter' => 'dynamicBadgeFormatter', + ], + 'created_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'updated_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'deleted_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + ]; + } + + /** + * Devuelve las rutas del CRUD para cada fila. + */ + public static function getIndexRoutes(): array + { + return [ + 'admin.user.show' => route('admin.core.users.users.show', ['user' => ':id']), + ]; + } + + /** + * Devuelve configuraciones adicionales del Bootstrap Table. + */ + public static function getIndexTableConfig(): array + { + return [ + 'search' => true, + 'pagination' => true, + 'showExport' => true, + ]; + } +} diff --git a/src/Application/UIX/ConfigBuilders/Blog/CategoriesTableConfigBuilder.php b/src/Application/UIX/ConfigBuilders/Blog/CategoriesTableConfigBuilder.php new file mode 100644 index 0000000..10bfe56 --- /dev/null +++ b/src/Application/UIX/ConfigBuilders/Blog/CategoriesTableConfigBuilder.php @@ -0,0 +1,175 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Blog; + +use Illuminate\Support\Facades\DB; +use Koneko\VuexyAdmin\Support\Builders\AbstractTableConfigBuilder; +use Koneko\VuexyWebsiteAdmin\Models\Faq; + +class CategoriesTableConfigBuilder extends AbstractTableConfigBuilder +{ + /** + * Devuelve el modelo de datos. + */ + public function getModelClass(): string + { + return Faq::class; + } + + /** + * Devuelve las columnas seleccionadas en la consulta SQL. + */ + public static function getIndexColumns(): array + { + return [ + 'faqs.id', + 'faqs.category_id', + 'faq_categories.name AS category_name', + 'faq_categories.icon AS category_icon', + 'faqs.question', + 'faqs.answer', + 'faqs.order', + 'faqs.is_active', + 'faqs.created_at', + 'faqs.updated_at', + ]; + } + + /** + * Devuelve las etiquetas (labels) para las columnas. + */ + public static function getIndexLabels(): array + { + return [ + 'action' => 'Acciones', + 'category_name' => 'Categoría', + 'question' => 'Pregunta', + 'answer' => 'Respuesta', + 'order' => 'Orden', + 'is_active' => 'Activo', + 'created_at' => 'Creado el', + 'updated_at' => 'Actualizado el', + 'deleted_at' => 'Eliminado el', + ]; + } + + /** + * Devuelve los JOINs requeridos por la consulta. + */ + public static function getIndexJoins(): array + { + return [ + ['faq_categories', 'faqs.category_id', '=', 'faq_categories.id'] + ]; + } + + /** + * Devuelve los filtros aplicables en la tabla (como búsqueda global). + */ + public static function getIndexFilters(): array + { + return [ + 'search' => [ + 'faqs.question', + 'faqs.answer', + ] + ]; + } + + /** + * Devuelve la configuración de formatos y visibilidad para cada columna. + */ + public static function getIndexFormatters(): array + { + return [ + /* + 'action' => [ + 'formatter' => 'VehicleAssignmentsAactionFormatter', + ], + */ + /* + 'carrier_id' => [ + 'formatter' => 'profilePhotoFormatter', + ], + */ + 'vehicle_type_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'brand_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'current_insurer_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'plate_number' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'model' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'year_manufacture' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'vin' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'fuel_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'axles' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'capacity_kg' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'start_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'end_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'max_km_allowed' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'assignment_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'status' => [ + 'formatter' => 'dynamicBadgeFormatter', + ], + 'created_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'updated_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'deleted_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + ]; + } + + /** + * Devuelve las rutas del CRUD para cada fila. + */ + public static function getIndexRoutes(): array + { + return [ + 'admin.user.show' => route('admin.core.users.users.show', ['user' => ':id']), + ]; + } + + /** + * Devuelve configuraciones adicionales del Bootstrap Table. + */ + public static function getIndexTableConfig(): array + { + return [ + 'search' => true, + 'pagination' => true, + 'showExport' => true, + ]; + } +} diff --git a/src/Application/UIX/ConfigBuilders/Blog/CommentsTableConfigBuilder.php b/src/Application/UIX/ConfigBuilders/Blog/CommentsTableConfigBuilder.php new file mode 100644 index 0000000..012ea39 --- /dev/null +++ b/src/Application/UIX/ConfigBuilders/Blog/CommentsTableConfigBuilder.php @@ -0,0 +1,175 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Blog; + +use Illuminate\Support\Facades\DB; +use Koneko\VuexyAdmin\Support\Builders\AbstractTableConfigBuilder; +use Koneko\VuexyWebsiteAdmin\Models\Faq; + +class CommentsTableConfigBuilder extends AbstractTableConfigBuilder +{ + /** + * Devuelve el modelo de datos. + */ + public function getModelClass(): string + { + return Faq::class; + } + + /** + * Devuelve las columnas seleccionadas en la consulta SQL. + */ + public static function getIndexColumns(): array + { + return [ + 'faqs.id', + 'faqs.category_id', + 'faq_categories.name AS category_name', + 'faq_categories.icon AS category_icon', + 'faqs.question', + 'faqs.answer', + 'faqs.order', + 'faqs.is_active', + 'faqs.created_at', + 'faqs.updated_at', + ]; + } + + /** + * Devuelve las etiquetas (labels) para las columnas. + */ + public static function getIndexLabels(): array + { + return [ + 'action' => 'Acciones', + 'category_name' => 'Categoría', + 'question' => 'Pregunta', + 'answer' => 'Respuesta', + 'order' => 'Orden', + 'is_active' => 'Activo', + 'created_at' => 'Creado el', + 'updated_at' => 'Actualizado el', + 'deleted_at' => 'Eliminado el', + ]; + } + + /** + * Devuelve los JOINs requeridos por la consulta. + */ + public static function getIndexJoins(): array + { + return [ + ['faq_categories', 'faqs.category_id', '=', 'faq_categories.id'] + ]; + } + + /** + * Devuelve los filtros aplicables en la tabla (como búsqueda global). + */ + public static function getIndexFilters(): array + { + return [ + 'search' => [ + 'faqs.question', + 'faqs.answer', + ] + ]; + } + + /** + * Devuelve la configuración de formatos y visibilidad para cada columna. + */ + public static function getIndexFormatters(): array + { + return [ + /* + 'action' => [ + 'formatter' => 'VehicleAssignmentsAactionFormatter', + ], + */ + /* + 'carrier_id' => [ + 'formatter' => 'profilePhotoFormatter', + ], + */ + 'vehicle_type_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'brand_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'current_insurer_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'plate_number' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'model' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'year_manufacture' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'vin' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'fuel_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'axles' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'capacity_kg' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'start_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'end_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'max_km_allowed' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'assignment_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'status' => [ + 'formatter' => 'dynamicBadgeFormatter', + ], + 'created_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'updated_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'deleted_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + ]; + } + + /** + * Devuelve las rutas del CRUD para cada fila. + */ + public static function getIndexRoutes(): array + { + return [ + 'admin.user.show' => route('admin.core.users.users.show', ['user' => ':id']), + ]; + } + + /** + * Devuelve configuraciones adicionales del Bootstrap Table. + */ + public static function getIndexTableConfig(): array + { + return [ + 'search' => true, + 'pagination' => true, + 'showExport' => true, + ]; + } +} diff --git a/src/Application/UIX/ConfigBuilders/Blog/TagsTableConfigBuilder.php b/src/Application/UIX/ConfigBuilders/Blog/TagsTableConfigBuilder.php new file mode 100644 index 0000000..e820f8b --- /dev/null +++ b/src/Application/UIX/ConfigBuilders/Blog/TagsTableConfigBuilder.php @@ -0,0 +1,175 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UIX\ConfigBuilders\Blog; + +use Illuminate\Support\Facades\DB; +use Koneko\VuexyAdmin\Support\Builders\AbstractTableConfigBuilder; +use Koneko\VuexyWebsiteAdmin\Models\Faq; + +class TagsTableConfigBuilder extends AbstractTableConfigBuilder +{ + /** + * Devuelve el modelo de datos. + */ + public function getModelClass(): string + { + return Faq::class; + } + + /** + * Devuelve las columnas seleccionadas en la consulta SQL. + */ + public static function getIndexColumns(): array + { + return [ + 'faqs.id', + 'faqs.category_id', + 'faq_categories.name AS category_name', + 'faq_categories.icon AS category_icon', + 'faqs.question', + 'faqs.answer', + 'faqs.order', + 'faqs.is_active', + 'faqs.created_at', + 'faqs.updated_at', + ]; + } + + /** + * Devuelve las etiquetas (labels) para las columnas. + */ + public static function getIndexLabels(): array + { + return [ + 'action' => 'Acciones', + 'category_name' => 'Categoría', + 'question' => 'Pregunta', + 'answer' => 'Respuesta', + 'order' => 'Orden', + 'is_active' => 'Activo', + 'created_at' => 'Creado el', + 'updated_at' => 'Actualizado el', + 'deleted_at' => 'Eliminado el', + ]; + } + + /** + * Devuelve los JOINs requeridos por la consulta. + */ + public static function getIndexJoins(): array + { + return [ + ['faq_categories', 'faqs.category_id', '=', 'faq_categories.id'] + ]; + } + + /** + * Devuelve los filtros aplicables en la tabla (como búsqueda global). + */ + public static function getIndexFilters(): array + { + return [ + 'search' => [ + 'faqs.question', + 'faqs.answer', + ] + ]; + } + + /** + * Devuelve la configuración de formatos y visibilidad para cada columna. + */ + public static function getIndexFormatters(): array + { + return [ + /* + 'action' => [ + 'formatter' => 'VehicleAssignmentsAactionFormatter', + ], + */ + /* + 'carrier_id' => [ + 'formatter' => 'profilePhotoFormatter', + ], + */ + 'vehicle_type_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'brand_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'current_insurer_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'plate_number' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'model' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'year_manufacture' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'vin' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'fuel_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'axles' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'capacity_kg' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'start_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'end_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'max_km_allowed' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'assignment_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'status' => [ + 'formatter' => 'dynamicBadgeFormatter', + ], + 'created_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'updated_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'deleted_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + ]; + } + + /** + * Devuelve las rutas del CRUD para cada fila. + */ + public static function getIndexRoutes(): array + { + return [ + 'admin.user.show' => route('admin.core.users.users.show', ['user' => ':id']), + ]; + } + + /** + * Devuelve configuraciones adicionales del Bootstrap Table. + */ + public static function getIndexTableConfig(): array + { + return [ + 'search' => true, + 'pagination' => true, + 'showExport' => true, + ]; + } +} diff --git a/src/Application/UIX/ConfigBuilders/Faq/FaqTableConfigBuilder.php b/src/Application/UIX/ConfigBuilders/Faq/FaqTableConfigBuilder.php new file mode 100644 index 0000000..cb04f13 --- /dev/null +++ b/src/Application/UIX/ConfigBuilders/Faq/FaqTableConfigBuilder.php @@ -0,0 +1,176 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\ConfigBuilders\Faq; + +use Illuminate\Support\Facades\DB; +use Koneko\VuexyAdmin\Support\Builders\AbstractTableConfigBuilder; +use Koneko\VuexyContacts\Models\VehicleAssignment; +use Koneko\VuexyWebsiteAdmin\Models\Faq; + +class FaqTableConfigBuilder extends AbstractTableConfigBuilder +{ + /** + * Devuelve el modelo de datos. + */ + public function getModelClass(): string + { + return Faq::class; + } + + /** + * Devuelve las columnas seleccionadas en la consulta SQL. + */ + public static function getIndexColumns(): array + { + return [ + 'faqs.id', + 'faqs.category_id', + 'faq_categories.name AS category_name', + 'faq_categories.icon AS category_icon', + 'faqs.question', + 'faqs.answer', + 'faqs.order', + 'faqs.is_active', + 'faqs.created_at', + 'faqs.updated_at', + ]; + } + + /** + * Devuelve las etiquetas (labels) para las columnas. + */ + public static function getIndexLabels(): array + { + return [ + 'action' => 'Acciones', + 'category_name' => 'Categoría', + 'question' => 'Pregunta', + 'answer' => 'Respuesta', + 'order' => 'Orden', + 'is_active' => 'Activo', + 'created_at' => 'Creado el', + 'updated_at' => 'Actualizado el', + 'deleted_at' => 'Eliminado el', + ]; + } + + /** + * Devuelve los JOINs requeridos por la consulta. + */ + public static function getIndexJoins(): array + { + return [ + ['faq_categories', 'faqs.category_id', '=', 'faq_categories.id'] + ]; + } + + /** + * Devuelve los filtros aplicables en la tabla (como búsqueda global). + */ + public static function getIndexFilters(): array + { + return [ + 'search' => [ + 'faqs.question', + 'faqs.answer', + ] + ]; + } + + /** + * Devuelve la configuración de formatos y visibilidad para cada columna. + */ + public static function getIndexFormatters(): array + { + return [ + /* + 'action' => [ + 'formatter' => 'VehicleAssignmentsAactionFormatter', + ], + */ + /* + 'carrier_id' => [ + 'formatter' => 'profilePhotoFormatter', + ], + */ + 'vehicle_type_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'brand_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'current_insurer_id' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'plate_number' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'model' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'year_manufacture' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'vin' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'fuel_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'axles' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'capacity_kg' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'start_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'end_date' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'max_km_allowed' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'assignment_type' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'status' => [ + 'formatter' => 'dynamicBadgeFormatter', + ], + 'created_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'updated_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'deleted_at' => [ + 'formatter' => 'textNowrapFormatter', + ], + ]; + } + + /** + * Devuelve las rutas del CRUD para cada fila. + */ + public static function getIndexRoutes(): array + { + return [ + 'admin.user.show' => route('admin.core.users.users.show', ['user' => ':id']), + ]; + } + + /** + * Devuelve configuraciones adicionales del Bootstrap Table. + */ + public static function getIndexTableConfig(): array + { + return [ + 'search' => true, + 'pagination' => true, + 'showExport' => true, + ]; + } +} diff --git a/Console/Commands/SitemapGenerate.php b/src/Console/Commands/SitemapGenerate.php similarity index 97% rename from Console/Commands/SitemapGenerate.php rename to src/Console/Commands/SitemapGenerate.php index 5a2fd67..4e71102 100644 --- a/Console/Commands/SitemapGenerate.php +++ b/src/Console/Commands/SitemapGenerate.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Koneko\VuexyWebsiteAdmin\Console\Commands; use Illuminate\Console\Command; @@ -34,4 +36,4 @@ class SitemapGenerate extends Command $this->info('✅ Sitemap generado en storage/app/public/sitemap.xml'); } -} \ No newline at end of file +} diff --git a/src/Console/Commands/WebsiteCacheHelperCommand.php b/src/Console/Commands/WebsiteCacheHelperCommand.php new file mode 100644 index 0000000..064fcf6 --- /dev/null +++ b/src/Console/Commands/WebsiteCacheHelperCommand.php @@ -0,0 +1,65 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Console\Commands; + +use Illuminate\Console\Command; +use Illuminate\Support\Facades\Cache; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteContent; +use Koneko\VuexyWebsiteAdmin\Application\Services\WebsiteRenderCacheService; + +class WebsiteCacheHelperCommand extends Command +{ + protected $signature = 'website:cache + {--slug= : Slug de la página (para operaciones individuales)} + {--clear : Limpia el caché de una página o de todo el sitio} + {--summary : Lista las claves de caché activas para contenido renderizado} + {--simulate : Simula el render y cachea la página especificada} + {--ttl=900 : TTL en segundos para el cacheo simulado} + '; + + protected $description = 'Herramientas para depurar y administrar el caché HTML renderizado del Website'; + + public function handle(): int + { + $slug = $this->option('slug'); + $ttl = (int) $this->option('ttl'); + + if ($this->option('summary')) { + $this->info('🔍 Claves de cacheo HTML renderizado (Redis/Tags):'); + $this->line('- (Nota: Laravel no permite listar claves directamente desde tags)'); + $this->line('💡 Usa observabilidad desde Redis CLI para inspección manual o eventos de log.'); + return self::SUCCESS; + } + + if ($this->option('clear')) { + if ($slug) { + WebsiteRenderCacheService::invalidate('website', $slug); + $this->info("🧹 Cache HTML limpiado para la página: {$slug}"); + } else { + Cache::tags(['rendered_html', 'website'])->flush(); + $this->info('🧼 Cache HTML global de website limpiado.'); + } + return self::SUCCESS; + } + + if ($this->option('simulate')) { + if (! $slug) { + $this->error('❌ Debes proporcionar un slug con --slug para simular cacheo.'); + return self::FAILURE; + } + + $content = WebsiteContent::published()->bySlug($slug)->first(); + if (! $content) { + $this->error("❌ Contenido no encontrado para slug: {$slug}"); + return self::FAILURE; + } + + $html = WebsiteRenderCacheService::getOrRender('website', $content->slug, fn() => $content->toHtml(), $ttl); + $this->info("✅ HTML cacheado para '{$slug}' con TTL de {$ttl} segundos"); + return self::SUCCESS; + } + + $this->warn('⚠️ No se especificó ninguna acción. Usa --help para ver las opciones disponibles.'); + return self::SUCCESS; + } +} diff --git a/src/Console/Commands/WebsiteContentHelperCommand.php b/src/Console/Commands/WebsiteContentHelperCommand.php new file mode 100644 index 0000000..eb0cf85 --- /dev/null +++ b/src/Console/Commands/WebsiteContentHelperCommand.php @@ -0,0 +1,114 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Console\Commands; + +use Illuminate\Console\Command; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteContent; +use Illuminate\Support\Facades\Cache; + +class WebsiteContentHelperCommand extends Command +{ + protected $signature = 'website:content + {--slug= : Slug del contenido a consultar} + {--id= : ID del contenido} + {--preview : Mostrar URL de previsualización firmada} + {--dump : Mostrar contenido completo con metadata} + {--summary : Listado resumen de contenidos} + {--clear-cache : Limpia la caché del contenido especificado} + {--clear-all-cache : Limpia toda la caché HTML renderizada} + {--versions : Listar versiones del contenido} + {--meta : Mostrar metadatos SEO efectivos} + {--html : Renderizar HTML del contenido en consola} + {--canonical : Mostrar URL canónica} + {--ttl= : TTL en minutos para previsualización (default 30)} + {--routes : Mostrar rutas públicas generadas a partir de los slugs} + '; + + protected $description = 'Utilidades para contenidos web: cacheo, previsualización, versiones, SEO, HTML, rutas, etc'; + + public function handle(): int + { + $slug = $this->option('slug'); + $id = $this->option('id'); + $ttl = (int) ($this->option('ttl') ?? 30); + + if ($this->option('clear-all-cache')) { + Cache::tags(['rendered_html'])->flush(); + $this->info('🧹 Caché HTML global limpiada.'); + return self::SUCCESS; + } + + if ($this->option('routes')) { + $this->info('🌐 Rutas públicas de contenido publicado:'); + WebsiteContent::published()->orderBy('id')->get()->each(function ($content, $i) { + $url = url($content->slug); + $this->line(sprintf("[%d] %s → %s", $i + 1, $url, $content->slug)); + }); + return self::SUCCESS; + } + + if ($slug || $id) { + $content = $slug + ? WebsiteContent::where('slug', $slug)->first() + : WebsiteContent::find($id); + + if (! $content) { + $this->error('❌ Contenido no encontrado.'); + return self::FAILURE; + } + + if ($this->option('clear-cache')) { + Cache::tags(["rendered_html", "website", "website_{$content->slug}"])->flush(); + $this->info("🧹 Caché HTML de '{$content->slug}' limpiada."); + } + + if ($this->option('preview')) { + $url = $content->previewUrl(auth()->id(), $ttl); + $this->line("🔍 Vista previa: <comment>{$url}</comment>"); + } + + if ($this->option('dump')) { + $this->info("📦 Contenido completo:"); + dump($content->toArray()); + } + + if ($this->option('versions')) { + $this->info("📜 Versiones:"); + foreach ($content->versions as $v) { + $this->line(" - {$v->version_label} [ID: {$v->id}] ({$v->created_at})"); + } + } + + if ($this->option('meta')) { + $this->info("📄 Metadatos SEO efectivos:"); + dump($content->getEffectiveSeoMetadata()); + } + + if ($this->option('canonical')) { + $this->line("🔗 Canonical URL: <info>{$content->getCanonicalUrl()}</info>"); + } + + if ($this->option('html')) { + $this->info("🖼 HTML renderizado:"); + $this->line($content->toHtml()); + } + } + + if ($this->option('summary')) { + $this->info("📚 Resumen de contenidos:"); + $all = WebsiteContent::select('id', 'slug', 'title', 'template', 'type', 'is_draft') + ->orderBy('id', 'asc')->get(); + + foreach ($all as $c) { + $flag = $c->is_draft ? '📝' : '✅'; + $this->line("[{$c->id}] {$flag} {$c->slug} — {$c->title} ({$c->template})"); + } + } + + if (! $slug && ! $id && ! $this->option('summary') && ! $this->option('clear-all-cache') && ! $this->option('routes')) { + $this->warn("⚠️ No se especificó ninguna acción. Usa --help para ver las opciones disponibles."); + } + + return self::SUCCESS; + } +} diff --git a/src/Console/Commands/WebsiteMenuHelperCommand.php b/src/Console/Commands/WebsiteMenuHelperCommand.php new file mode 100644 index 0000000..c7f40ee --- /dev/null +++ b/src/Console/Commands/WebsiteMenuHelperCommand.php @@ -0,0 +1,134 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Console\Commands; + +use Illuminate\Console\Command; +use Illuminate\Support\Facades\File; +use Koneko\VuexyAdmin\Models\User; +use Koneko\VuexyWebsiteAdmin\Website\Menu\WebsiteMenuRenderer; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteMenu; +use Symfony\Component\Console\Attribute\AsCommand; + +#[AsCommand(name: 'website:menu')] +class WebsiteMenuHelperCommand extends Command +{ + protected $signature = 'website:menu + {--list : Lista todos los menús disponibles} + {--tree= : Muestra el árbol completo del menú (por slug)} + {--slug= : Slug del menú a procesar} + {--as= : Simula visibilidad como visitante o user:ID} + {--json : Mostrar como JSON} + {--dump : Mostrar con dump()} + {--summary : Mostrar resumen del menú} + {--clear-cache : Limpia caché del menú indicado} + {--export= : Exporta el menú a un archivo JSON para seeder} + {--id-node= : Muestra un nodo por ID auto-generado}'; + + protected $description = 'Comando de ayuda para explorar, depurar y exportar los menús del Website.'; + + public function handle(): int + { + $slug = $this->option('slug') ?? $this->option('tree'); + $summary = $this->option('summary'); + $as = $this->option('as'); + $asUserId = null; + + if ($this->option('list')) { + $this->listMenus(); + return self::SUCCESS; + } + + if ($this->option('clear-cache')) { + if ($slug) { + WebsiteMenuRenderer::clearCache($slug); + $this->info("🔁 Caché limpiada para el menú '{$slug}'"); + } else { + WebsiteMenuRenderer::clearAllCache(); + $this->info("🔁 Caché global de menú limpiada"); + } + return self::SUCCESS; + } + + if (! $slug) { + $this->error('⚠️ Debes especificar el slug del menú con --slug o --tree.'); + return self::FAILURE; + } + + $user = null; + if ($as === 'visitor') { + $user = null; + } elseif (str_starts_with($as, 'user:')) { + $asUserId = (int) str_replace('user:', '', $as); + $user = User::find($asUserId); + } + + $tree = WebsiteMenuRenderer::tree($slug, $user); + + if ($this->option('id-node')) { + $nodeId = (int) $this->option('id-node'); + $node = $this->findNodeById($tree, $nodeId); + return $this->renderOutput($node); + } + + if ($this->option('export')) { + $file = $this->option('export'); + File::put($file, json_encode($tree, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + $this->info("📦 Menú exportado a: {$file}"); + return self::SUCCESS; + } + + if ($summary) { + $this->renderSummary($tree); + return self::SUCCESS; + } + + return $this->renderOutput($tree); + } + + protected function listMenus(): void + { + $this->info("🧭 Menús disponibles:"); + foreach (WebsiteMenu::all() as $menu) { + $this->line("- [{$menu->id}] {$menu->slug} — {$menu->title}"); + } + } + + protected function renderOutput(array $tree): int + { + if ($this->option('json')) { + $this->line(json_encode($tree, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } elseif ($this->option('dump')) { + dump($tree); + } else { + $this->warn('⚠️ Usa --json, --dump o --summary para mostrar la salida.'); + } + return self::SUCCESS; + } + + protected function renderSummary(array $items, string $prefix = ''): void + { + foreach ($items as $item) { + $id = $item['id'] ?? '??'; + $title = $item['title'] ?? '[sin título]'; + $this->line("[#{$id}] {$prefix}{$title}"); + + if (!empty($item['children'])) { + $this->renderSummary($item['children'], $prefix . ' └ '); + } + } + } + + protected function findNodeById(array $items, int $id): ?array + { + foreach ($items as $item) { + if (($item['id'] ?? null) === $id) { + return $item; + } + if (!empty($item['children'])) { + $found = $this->findNodeById($item['children'], $id); + if ($found) return $found; + } + } + return null; + } +} diff --git a/src/Console/Commands/WebsiteSeoHelperCommand.php b/src/Console/Commands/WebsiteSeoHelperCommand.php new file mode 100644 index 0000000..3f3d893 --- /dev/null +++ b/src/Console/Commands/WebsiteSeoHelperCommand.php @@ -0,0 +1,73 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Console\Commands; + +use Illuminate\Console\Command; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteSeoProfile; + +class WebsiteSeoHelperCommand extends Command +{ + protected $signature = 'website:seo + {--id= : ID del perfil SEO} + {--slug= : Slug del perfil SEO} + {--summary : Listar todos los perfiles SEO} + {--dump : Mostrar información detallada del perfil SEO} + {--meta : Mostrar metadatos SEO generados (meta tags)} + {--jsonld : Mostrar JSON-LD del perfil} + {--active : Filtrar solo perfiles activos (noindex = false)} + '; + + protected $description = 'Herramientas para gestionar WebsiteSeoProfile: inspección, metadata, JSON-LD, filtros'; + + public function handle(): int + { + $id = $this->option('id'); + $slug = $this->option('slug'); + $activeOnly = $this->option('active'); + + if ($this->option('summary')) { + $this->info('📄 Listado de perfiles SEO:'); + $query = WebsiteSeoProfile::select('id', 'slug', 'title', 'type'); + if ($activeOnly) { + $query->active(); + } + $all = $query->orderBy('id')->get(); + + foreach ($all as $seo) { + $this->line("[{$seo->id}] {$seo->slug} — {$seo->title} ({$seo->type->value})"); + } + return self::SUCCESS; + } + + if ($id || $slug) { + $seo = $id + ? WebsiteSeoProfile::find($id) + : WebsiteSeoProfile::where('slug', $slug)->first(); + + if (! $seo) { + $this->error('❌ Perfil SEO no encontrado.'); + return self::FAILURE; + } + + if ($this->option('dump')) { + $this->info("🧾 Información completa del perfil SEO:"); + dump($seo->toArray()); + } + + if ($this->option('meta')) { + $this->info("📑 Meta Tags generados:"); + dump($seo->getMetaTags()); + } + + if ($this->option('jsonld')) { + $this->info("🧬 JSON-LD:"); + dump($seo->toJsonLd()); + } + + return self::SUCCESS; + } + + $this->warn('⚠️ No se especificó ninguna acción. Usa --help para ver las opciones disponibles.'); + return self::SUCCESS; + } +} diff --git a/src/Database/Seeders/WebsiteContentSeeder.php b/src/Database/Seeders/WebsiteContentSeeder.php new file mode 100644 index 0000000..bcce5bc --- /dev/null +++ b/src/Database/Seeders/WebsiteContentSeeder.php @@ -0,0 +1,40 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Database\Seeders; + +use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder; +use Koneko\VuexyAdmin\Support\Traits\Seeders\HandlesFileSeeders; +use Koneko\VuexyWebsiteAdmin\Models\{WebsiteContent,WebsiteSeoProfile}; + +class WebsiteContentSeeder extends AbstractDataSeeder +{ + use HandlesFileSeeders; + + // Datos del Modelo + protected string $model = WebsiteContent::class; + protected string|array $uniqueBy = ['slug']; + + // Ruta del archivo de datos + protected string $targetFile = 'website_contents.json'; + + protected function sanitizeRow(array $row): array + { + return array_merge($row, [ + 'created_by' => $this->resolveSeederUserId(), + 'updated_by' => $this->resolveSeederUserId(), + 'is_draft' => $row['is_draft'] ?? false, + 'is_sensitive' => $row['is_sensitive'] ?? false, + 'seo_profile_id' => $this->findSeoProfileId($row['seo_profile_id']), + ]); + } + + protected function findSeoProfileId($slug): ?int + { + return WebsiteSeoProfile::where('slug', $slug)->first()?->id; + } + + protected function resolveSeederUserId(): ?int + { + return config('seeder.default_user_id') ?? 1; + } +} diff --git a/src/Database/Seeders/WebsiteMenuItemSeeder.php b/src/Database/Seeders/WebsiteMenuItemSeeder.php new file mode 100644 index 0000000..e014629 --- /dev/null +++ b/src/Database/Seeders/WebsiteMenuItemSeeder.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Database\Seeders; + +use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder; +use Koneko\VuexyAdmin\Support\Traits\Seeders\HandlesFileSeeders; +use Koneko\VuexyWebsiteAdmin\Models\{WebsiteMenuItem, WebsiteMenu}; + +class WebsiteMenuItemSeeder extends AbstractDataSeeder +{ + use HandlesFileSeeders; + + // Datos del Modelo + protected string $model = WebsiteMenuItem::class; + protected string|array $uniqueBy = ['menu_id', 'title']; + + // Ruta del archivo de datos + protected string $targetFile = 'website_menus_web_info.json'; + + protected function sanitizeRow(array $row): array + { + $menu = WebsiteMenu::where('slug', $row['menu_slug'])->firstOrFail(); + + $row['menu_id'] = $menu->id; + unset($row['menu_slug']); + + // Asegurar que title sea array + if (isset($row['title']) && is_string($row['title'])) { + $decoded = json_decode($row['title'], true); + + if (json_last_error() === JSON_ERROR_NONE) { + $row['title'] = $decoded; + } + } + + return $row; + } + +} diff --git a/src/Database/Seeders/WebsiteMenuSeeder.php b/src/Database/Seeders/WebsiteMenuSeeder.php new file mode 100644 index 0000000..ab927df --- /dev/null +++ b/src/Database/Seeders/WebsiteMenuSeeder.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Database\Seeders; + +use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder; +use Koneko\VuexyAdmin\Support\Traits\Seeders\HandlesFileSeeders; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteMenu; + +class WebsiteMenuSeeder extends AbstractDataSeeder +{ + use HandlesFileSeeders; + + // Datos del Modelo + protected string $model = WebsiteMenu::class; + protected string|array $uniqueBy = 'slug'; + + // Ruta del archivo de datos + protected string $targetFile = 'website_menus.json'; +} diff --git a/src/Database/Seeders/WebsiteSeoProfileSeeder.php b/src/Database/Seeders/WebsiteSeoProfileSeeder.php new file mode 100644 index 0000000..8d686b6 --- /dev/null +++ b/src/Database/Seeders/WebsiteSeoProfileSeeder.php @@ -0,0 +1,34 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Database\Seeders; + +use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder; +use Koneko\VuexyAdmin\Support\Traits\Seeders\HandlesFileSeeders; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteSeoProfile; + +class WebsiteSeoProfileSeeder extends AbstractDataSeeder +{ + use HandlesFileSeeders; + + // Datos del Modelo + protected string $model = WebsiteSeoProfile::class; + protected string|array $uniqueBy = ['title', 'locale']; + + // Ruta del archivo de datos + protected string $targetFile = 'website_seo_profiles.json'; + + protected function sanitizeRow(array $row): array + { + return array_merge($row, [ + 'created_by' => $this->resolveSeederUserId(), + ]); + } + + /** + * Opcional: usuario dummy o admin por defecto + */ + protected function resolveSeederUserId(): int|null + { + return config('seeder.default_user_id', null); // Usa config si está definido + } +} diff --git a/src/Database/Seeders/WebsiteSiteSeeder.php b/src/Database/Seeders/WebsiteSiteSeeder.php new file mode 100644 index 0000000..44c1621 --- /dev/null +++ b/src/Database/Seeders/WebsiteSiteSeeder.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Database\Seeders; + +use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder; +use Koneko\VuexyAdmin\Support\Traits\Seeders\HandlesFileSeeders; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteSite; + +class WebsiteSiteSeeder extends AbstractDataSeeder +{ + use HandlesFileSeeders; + + // Datos del Modelo + protected string $model = WebsiteSite::class; + protected string|array $uniqueBy = 'slug'; + + // Ruta del archivo de datos + protected string $targetFile = 'website_sites.json'; +} diff --git a/src/Models/BlogArticle.php b/src/Models/BlogArticle.php new file mode 100644 index 0000000..b4f8ad0 --- /dev/null +++ b/src/Models/BlogArticle.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Koneko\VuexyAdmin\Models\User; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class BlogArticle extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'title'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'artículo'; + public string $focusColumnOnOpen = 'title'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'category_id', + 'title', + 'slug', + 'excerpt', + 'content', + 'metadata', + 'is_published', + 'published_at', + 'created_by', + 'updated_by', + ]; + + protected $casts = [ + 'metadata' => 'array', + 'is_published' => 'boolean', + 'published_at' => 'datetime', + ]; + + protected $auditInclude = [ + 'category_id', + 'title', + 'slug', + 'excerpt', + 'content', + 'metadata', + 'is_published', + 'published_at', + 'updated_by', + ]; + + public function category() : BelongsTo + { + return $this->belongsTo(BlogCategory::class); + } + + public function tags() : BelongsToMany + { + return $this->belongsToMany(BlogTag::class, 'blog_article_tag'); + } + + public function comments() : HasMany + { + return $this->hasMany(BlogComment::class); + } + + public function creator() : BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function updater() : BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return $this->title; + } +} diff --git a/src/Models/BlogCategory.php b/src/Models/BlogCategory.php new file mode 100644 index 0000000..9edfa03 --- /dev/null +++ b/src/Models/BlogCategory.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class BlogCategory extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'name'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'categoría'; + public string $focusColumnOnOpen = 'name'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'name', + 'slug', + 'parent_id', + 'description', + 'is_active', + ]; + + protected $casts = [ + 'is_active' => 'boolean', + ]; + + protected $auditInclude = [ + 'name', + 'slug', + 'parent_id', + 'description', + 'is_active', + ]; + + public function parent() : BelongsTo + { + return $this->belongsTo(self::class, 'parent_id'); + } + + public function children() : HasMany + { + return $this->hasMany(self::class, 'parent_id'); + } + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return $this->name; + } +} diff --git a/src/Models/BlogComment.php b/src/Models/BlogComment.php new file mode 100644 index 0000000..ec51fa2 --- /dev/null +++ b/src/Models/BlogComment.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class BlogComment extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'created_at'; + public string $defaultSortOrder = 'desc'; + public string $singularName = 'comentario'; + public string $focusColumnOnOpen = 'created_at'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'blog_article_id', + 'author_name', + 'author_email', + 'comment', + 'is_approved', + ]; + + protected $casts = [ + 'is_approved' => 'boolean', + ]; + + protected $auditInclude = [ + 'comment', + 'is_approved', + ]; + + public function article() : BelongsTo + { + return $this->belongsTo(BlogArticle::class); + } + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return $this->comment; + } +} diff --git a/src/Models/BlogTag.php b/src/Models/BlogTag.php new file mode 100644 index 0000000..76bc360 --- /dev/null +++ b/src/Models/BlogTag.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class BlogTag extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'name'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'etiqueta'; + public string $focusColumnOnOpen = 'name'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'name', + 'slug', + 'is_active', + ]; + + protected $casts = [ + 'is_active' => 'boolean', + ]; + + protected $auditInclude = [ + 'name', + 'slug', + 'is_active', + ]; + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return $this->name; + } +} diff --git a/src/Models/Faq.php b/src/Models/Faq.php new file mode 100644 index 0000000..bae7c44 --- /dev/null +++ b/src/Models/Faq.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class Faq extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'name'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'faq'; + public string $focusColumnOnOpen = 'name'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'category_id', + 'question', + 'answer', + 'order', + 'is_active', + ]; + + protected $casts = [ + 'order' => 'integer', + 'is_active' => 'boolean', + ]; + + protected $auditInclude = [ + 'category_id', + 'question', + 'answer', + 'order', + 'is_active', + ]; + + // ===================== RELACIONES ===================== + + public function category(): BelongsTo + { + return $this->belongsTo(FaqCategory::class, 'category_id'); + } + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return (string) $this->question; + } +} diff --git a/Models/FaqCategory.php b/src/Models/FaqCategory.php similarity index 55% rename from Models/FaqCategory.php rename to src/Models/FaqCategory.php index c1feb8f..d849398 100644 --- a/Models/FaqCategory.php +++ b/src/Models/FaqCategory.php @@ -1,14 +1,19 @@ <?php +declare(strict_types=1); + namespace Koneko\VuexyWebsiteAdmin\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; -class FaqCategory extends Model +class FaqCategory extends Model implements AuditableContract { - use HasFactory; + use HasVuexyModelMetadata; + use Auditable; protected $fillable = [ 'name', @@ -22,6 +27,13 @@ class FaqCategory extends Model 'is_active' => 'boolean', ]; + protected $auditInclude = [ + 'name', + 'icon', + 'order', + 'is_active', + ]; + /** * FAQs asociadas a esta categoría. */ diff --git a/src/Models/SitemapIndexFile.php b/src/Models/SitemapIndexFile.php new file mode 100644 index 0000000..cb8c4d6 --- /dev/null +++ b/src/Models/SitemapIndexFile.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class SitemapIndexFile extends Model implements AuditableContract +{ + use Auditable; + + protected $fillable = [ + 'sitemap_profile_id', + 'file_name', + 'url', + 'generated_at', + 'url_count', + 'is_current', + ]; + + protected $casts = [ + 'generated_at' => 'datetime', + 'is_current' => 'boolean', + ]; + + protected $auditInclude = [ + 'file_name', + 'url', + 'generated_at', + 'url_count', + 'is_current', + ]; + + public function profile(): BelongsTo + { + return $this->belongsTo(SitemapProfile::class, 'sitemap_profile_id'); + } +} diff --git a/src/Models/SitemapProfile.php b/src/Models/SitemapProfile.php new file mode 100644 index 0000000..2d78c5f --- /dev/null +++ b/src/Models/SitemapProfile.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteSite; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class SitemapProfile extends Model implements AuditableContract +{ + use HasVuexyModelMetadata, + Auditable; + + protected $fillable = [ + 'site_id', + 'name', + 'slug', + 'entity_type', + 'generator_class', + 'is_active', + 'created_by', + 'updated_by', + ]; + + protected $casts = [ + 'is_active' => 'boolean', + ]; + + protected $auditInclude = [ + 'site_id', + 'name', + 'slug', + 'entity_type', + 'generator_class', + 'is_active', + ]; + + public function site(): BelongsTo + { + return $this->belongsTo(WebsiteSite::class); + } + + public function rules(): HasMany + { + return $this->hasMany(SitemapRule::class); + } + + public function urls(): HasMany + { + return $this->hasMany(SitemapUrl::class); + } + + public function indexFiles(): HasMany + { + return $this->hasMany(SitemapIndexFile::class); + } +} diff --git a/src/Models/SitemapRule.php b/src/Models/SitemapRule.php new file mode 100644 index 0000000..9e310dc --- /dev/null +++ b/src/Models/SitemapRule.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class SitemapRule extends Model implements AuditableContract +{ + use Auditable; + + protected $fillable = [ + 'sitemap_profile_id', + 'rule_type', + 'rule_data', + ]; + + protected $casts = [ + 'rule_data' => 'array', + ]; + + protected $auditInclude = [ + 'sitemap_profile_id', + 'rule_type', + 'rule_data', + ]; + + public function profile(): BelongsTo + { + return $this->belongsTo(SitemapProfile::class, 'sitemap_profile_id'); + } +} diff --git a/src/Models/SitemapUrl.php b/src/Models/SitemapUrl.php new file mode 100644 index 0000000..adb049b --- /dev/null +++ b/src/Models/SitemapUrl.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class SitemapUrl extends Model implements AuditableContract +{ + use Auditable; + + protected $fillable = [ + 'sitemap_profile_id', + 'url', + 'changefreq', + 'priority', + 'lastmod', + 'is_active', + 'alternate_locales', + ]; + + protected $casts = [ + 'is_active' => 'boolean', + 'lastmod' => 'datetime', + 'alternate_locales' => 'array', + 'priority' => 'float', + ]; + + protected $auditInclude = [ + 'url', + 'changefreq', + 'priority', + 'lastmod', + 'is_active', + 'alternate_locales', + ]; + + public function profile(): BelongsTo + { + return $this->belongsTo(SitemapProfile::class, 'sitemap_profile_id'); + } +} diff --git a/src/Models/WebsiteContent.php b/src/Models/WebsiteContent.php new file mode 100644 index 0000000..c040cf3 --- /dev/null +++ b/src/Models/WebsiteContent.php @@ -0,0 +1,194 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany}; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\URL; +use Koneko\VuexyAdmin\Support\Traits\Audit\{HasCreator, HasUpdater}; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class WebsiteContent extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + use HasCreator, + HasUpdater; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'title'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'contenido web'; + public string $focusColumnOnOpen = 'title'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'site_id', + 'seo_profile_id', + 'title', + 'slug', + 'description', + 'keywords', + 'template', + 'template_variant', + 'type', + 'render_mode', + 'block_mode', + 'resource', + 'render_as', + 'canonical_url', + 'content_blocks', + 'seo_overrides', + 'is_draft', + 'is_sensitive', + 'is_partial', + 'roles', + 'permissions', + 'hide_if_authenticated', + 'hide_if_guest', + 'visible_from', + 'visible_until', + 'enable_cache', + 'cache_ttl', + 'created_by', + 'updated_by' + ]; + + protected $casts = [ + 'keywords' => 'array', + 'content_blocks' => 'array', + 'seo_overrides' => 'array', + 'roles' => 'array', + 'permissions' => 'array', + 'is_draft' => 'boolean', + 'is_sensitive' => 'boolean', + 'is_partial' => 'boolean', + 'hide_if_authenticated' => 'boolean', + 'hide_if_guest' => 'boolean', + 'visible_from' => 'timestamp', + 'visible_until' => 'timestamp', + 'enable_cache' => 'boolean', + 'cache_ttl' => 'integer', + ]; + + protected $auditInclude = [ + 'site_id', + 'seo_profile_id', + 'title', + 'slug', + 'description', + 'keywords', + 'template', + 'template_variant', + 'type', + 'render_mode', + 'block_mode', + 'resource', + 'render_as', + 'canonical_url', + 'content_blocks', + 'seo_overrides', + 'is_draft', + 'is_sensitive', + 'is_partial', + 'roles', + 'permissions', + 'hide_if_authenticated', + 'hide_if_guest', + 'visible_from', + 'visible_until', + 'enable_cache', + 'cache_ttl', + ]; + + // ===================== RELACIONES ===================== + + public function site() : BelongsTo + { + return $this->belongsTo(WebsiteSite::class); + } + + public function seoProfile() : BelongsTo + { + return $this->belongsTo(WebsiteSeoProfile::class, 'seo_profile_id'); + } + + public function versions() : HasMany + { + return $this->hasMany(WebsiteContentVersion::class); + } + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return $this->title; + } + + public function getEffectiveSeoMetadata(): array + { + $base = $this->seoProfile?->getMetaTags() ?? []; + + return array_merge($base, $this->seo_overrides ?? []); + } + + public function getCanonicalUrl(): ?string + { + return $this->canonical_url ?: ($this->seoProfile->og_url ?? null); + } + + public function toHtml(): string + { + return view('website::templates.' . ($this->template ?? 'default'), [ + 'content' => $this, + ])->render(); + } + + // ===================== SCOPES ===================== + + public function scopePublished($query) : Builder + { + return $query + ->where('is_draft', false) + ->where(function ($q) { + $q->whereNull('visible_from')->orWhere('visible_from', '<=', now()); + }) + ->where(function ($q) { + $q->whereNull('visible_until')->orWhere('visible_until', '>=', now()); + }); + } + + public function scopeBySlug($query, string $slug) : Builder + { + return $query->where('slug', $slug); + } + + public function scopeDraft($query) : Builder + { + return $query->where('is_draft', true); + } + + + // ===================== PREVIEW ===================== + + public function previewUrl(?int $userId = null, int $ttl = 30): string + { + return URL::temporarySignedRoute( + 'website.preview', + now()->addMinutes($ttl), + [ + 'slug' => $this->slug, + 'user_id' => $userId ?? Auth::id() + ] + ); + } + +} diff --git a/src/Models/WebsiteContentBlock.php b/src/Models/WebsiteContentBlock.php new file mode 100644 index 0000000..bbb457d --- /dev/null +++ b/src/Models/WebsiteContentBlock.php @@ -0,0 +1,158 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany}; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\URL; +use Koneko\VuexyAdmin\Support\Traits\Audit\{HasCreator, HasUpdater}; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class WebsiteContentBlock extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + use HasCreator, + HasUpdater; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'title'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'contenido web'; + public string $focusColumnOnOpen = 'title'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'content_id', + 'parent_id', + 'slug', + 'type', + 'mode', + 'view', + 'view_path', + 'component_class', + 'is_enabled', + 'enable_cache', + 'cache_ttl', + 'settings', + 'data', + 'order' + ]; + + protected $casts = [ + 'settings' => 'array', + 'data' => 'array', + 'is_enabled' => 'boolean', + 'enable_cache' => 'boolean', + 'cache_ttl' => 'integer', + ]; + + protected $auditInclude = [ + 'content_id', + 'parent_id', + 'slug', + 'type', + 'mode', + 'view', + 'view_path', + 'component_class', + 'is_enabled', + 'enable_cache', + 'cache_ttl', + 'settings', + 'data', + 'order', + ]; + + + // ===================== RELACIONES ===================== + + public function content() : BelongsTo + { + return $this->belongsTo(WebsiteContent::class); + } + + public function parent() : BelongsTo + { + return $this->belongsTo(WebsiteContentBlock::class, 'parent_id'); + } + + public function versions() : HasMany + { + return $this->hasMany(WebsiteContentVersion::class); + } + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return $this->title; + } + + public function getEffectiveSeoMetadata(): array + { + $base = $this->seoProfile?->getMetaTags() ?? []; + + return array_merge($base, $this->seo_overrides ?? []); + } + + public function getCanonicalUrl(): ?string + { + return $this->canonical_url ?: ($this->seoProfile->og_url ?? null); + } + + public function toHtml(): string + { + return view('website::templates.' . ($this->template ?? 'default'), [ + 'content' => $this, + ])->render(); + } + + // ===================== SCOPES ===================== + + public function scopePublished($query) : Builder + { + return $query + ->where('is_draft', false) + ->where(function ($q) { + $q->whereNull('visible_from')->orWhere('visible_from', '<=', now()); + }) + ->where(function ($q) { + $q->whereNull('visible_until')->orWhere('visible_until', '>=', now()); + }); + } + + public function scopeBySlug($query, string $slug) : Builder + { + return $query->where('slug', $slug); + } + + public function scopeDraft($query) : Builder + { + return $query->where('is_draft', true); + } + + + // ===================== PREVIEW ===================== + + public function previewUrl(?int $userId = null, int $ttl = 30): string + { + return URL::temporarySignedRoute( + 'website.preview', + now()->addMinutes($ttl), + [ + 'slug' => $this->slug, + 'user_id' => $userId ?? Auth::id() + ] + ); + } + +} diff --git a/src/Models/WebsiteContentVersion.php b/src/Models/WebsiteContentVersion.php new file mode 100644 index 0000000..c4092c1 --- /dev/null +++ b/src/Models/WebsiteContentVersion.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Koneko\VuexyAdmin\Support\Traits\Audit\HasCreator; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class WebsiteContentVersion extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + use HasCreator; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'version_label'; + public string $defaultSortOrder = 'desc'; + public string $singularName = 'versión'; + public string $focusColumnOnOpen = 'version_label'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'website_content_id', + 'version_label', + 'content', + 'metadata', + 'created_by', + 'updated_by', + ]; + + protected $casts = [ + 'metadata' => 'array', + ]; + + protected $auditInclude = [ + 'website_content_id', + 'version_label', + 'content', + 'metadata', + 'created_by', + ]; + + // ===================== RELACIONES ===================== + + public function content(): BelongsTo + { + return $this->belongsTo(WebsiteContent::class, 'website_content_id'); + } + + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return $this->version_label; + } +} diff --git a/src/Models/WebsiteMenu.php b/src/Models/WebsiteMenu.php new file mode 100644 index 0000000..15cbdc8 --- /dev/null +++ b/src/Models/WebsiteMenu.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany}; +use Koneko\VuexyAdmin\Support\Traits\Audit\{HasCreator, HasUpdater}; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class WebsiteMenu extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + use HasCreator, + HasUpdater; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'title'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'menú'; + public string $focusColumnOnOpen = 'title'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'site_id', + 'title', + 'slug', + 'description', + 'is_active', + 'created_by', + 'updated_by', + ]; + + protected $casts = [ + 'is_active' => 'boolean', + ]; + + protected $auditInclude = [ + 'site_id', + 'title', + 'slug', + 'description', + 'is_active', + ]; + + // ===================== GETTERS ===================== + + public function items(): HasMany + { + return $this->hasMany(WebsiteMenuItem::class, 'menu_id') + ->whereNull('parent_id') + ->orderBy('order'); + } + + public function getDisplayName(): string + { + return $this->title; + } + + // ===================== RELACIONES ===================== + + public function site(): BelongsTo + { + return $this->belongsTo(WebsiteSite::class); + } +} diff --git a/src/Models/WebsiteMenuItem.php b/src/Models/WebsiteMenuItem.php new file mode 100644 index 0000000..105c25b --- /dev/null +++ b/src/Models/WebsiteMenuItem.php @@ -0,0 +1,121 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, MorphTo}; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use Koneko\VuexyWebsiteAdmin\Application\Enums\WebsiteMenuItem\{WebsiteMenuItemTarget, WebsiteMenuItemType}; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class WebsiteMenuItem extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'order'; + public string $defaultSortOrder = 'desc'; + public string $singularName = 'Elemento de menú'; + public string $focusColumnOnOpen = 'title'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'menu_id', + 'parent_id', + 'title', + 'type', + 'linkable_id', + 'linkable_type', + 'laravel_route', + 'url', + 'js_event', + 'target', + 'method', + 'icon', + 'badge', + 'badge_color', + 'roles', + 'permissions', + 'hide_if_authenticated', + 'hide_if_guest', + 'visible_from', + 'visible_until', + 'order', + 'is_active', + 'created_by', + 'updated_by' + ]; + + protected $casts = [ + 'title' => 'array', + 'type' => WebsiteMenuItemType::class, + 'target' => WebsiteMenuItemTarget::class, + 'roles' => 'array', + 'permissions' => 'array', + 'hide_if_authenticated' => 'boolean', + 'hide_if_guest' => 'boolean', + 'is_active' => 'boolean', + ]; + + protected $auditInclude = [ + 'title', + 'type', + 'linkable_id', + 'linkable_type', + 'laravel_route', + 'url', + 'js_event', + 'target', + 'method', + 'icon', + 'badge', + 'badge_color', + 'roles', + 'permissions', + 'hide_if_authenticated', + 'hide_if_guest', + 'visible_from', + 'visible_until', + 'order', + 'is_active', + ]; + + // ===================== RELACIONES ===================== + + public function menu(): BelongsTo + { + return $this->belongsTo(WebsiteMenu::class, 'menu_id'); + } + + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id'); + } + + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id')->orderBy('order'); + } + + public function linkable(): MorphTo + { + return $this->morphTo(); + } + + // ===================== GETTERS ===================== + + // Accesor para devolver título correcto según idioma actual + public function getLocalizedTitleAttribute(): string + { + $locale = app()->getLocale(); + $fallback = config('app.fallback_locale', 'es'); + + return $this->title[$locale] ?? $this->title[$fallback] ?? ''; + } +} diff --git a/src/Models/WebsiteSeoProfile.php b/src/Models/WebsiteSeoProfile.php new file mode 100644 index 0000000..5fa053a --- /dev/null +++ b/src/Models/WebsiteSeoProfile.php @@ -0,0 +1,141 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Koneko\VuexyAdmin\Support\Traits\Audit\{HasCreator,HasUpdater}; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use Koneko\VuexyWebsiteAdmin\Application\Enums\WebsiteSeoProfile\WebsiteSeoProfileType; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class WebsiteSeoProfile extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + use HasCreator, + HasUpdater; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'title'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'perfil SEO'; + public string $focusColumnOnOpen = 'title'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'site_id', + 'type', + 'title', + 'slug', + 'description', + 'schema_org', + 'noindex', + 'nofollow', + 'locale', + 'geo_location', + 'og_type', + 'og_title', + 'og_description', + 'og_image', + 'og_url', + 'og_site_name', + 'twitter_card', + 'twitter_title', + 'twitter_description', + 'twitter_image', + 'twitter_site', + 'twitter_creator', + 'json_ld', + 'created_by', + 'updated_by' + ]; + + protected $casts = [ + 'type' => WebsiteSeoProfileType::class, + 'schema_org' => 'array', + 'geo_location' => 'array', + 'json_ld' => 'array', + 'noindex' => 'boolean', + 'nofollow' => 'boolean', + ]; + + protected $auditInclude = [ + 'site_id', + 'type', + 'title', + 'slug', + 'description', + 'schema_org', + 'noindex', + 'nofollow', + 'locale', + 'geo_location', + 'og_type', + 'og_title', + 'og_description', + 'og_image', + 'og_url', + 'og_site_name', + 'twitter_card', + 'twitter_title', + 'twitter_description', + 'twitter_image', + 'twitter_site', + 'twitter_creator', + 'json_ld', + ]; + + // ===================== RELACIONES ===================== + + public function site(): BelongsTo + { + return $this->belongsTo(WebsiteSite::class); + } + + // ===================== SCOPES ===================== + + public function scopeActive($query) : Builder + { + return $query->where('noindex', false); + } + + // ===================== GETTERS ===================== + + public function getMetaTags(): array + { + return [ + 'title' => $this->title, + 'description' => $this->description, + 'robots' => ($this->noindex ? 'noindex' : 'index') . ', ' . ($this->nofollow ? 'nofollow' : 'follow'), + 'canonical' => $this->og_url ?? null, + 'og' => [ + 'type' => $this->og_type, + 'title' => $this->og_title, + 'description' => $this->og_description, + 'image' => $this->og_image, + 'url' => $this->og_url, + 'site_name' => $this->og_site_name, + ], + 'twitter' => [ + 'card' => $this->twitter_card, + 'title' => $this->twitter_title, + 'description' => $this->twitter_description, + 'image' => $this->twitter_image, + 'site' => $this->twitter_site, + 'creator' => $this->twitter_creator, + ] + ]; + } + + public function toJsonLd(): array + { + return $this->json_ld ?? []; + } +} diff --git a/src/Models/WebsiteSite.php b/src/Models/WebsiteSite.php new file mode 100644 index 0000000..15e4748 --- /dev/null +++ b/src/Models/WebsiteSite.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Koneko\VuexyAdmin\Support\Traits\Audit\{HasCreator,HasUpdater}; +use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata; +use Koneko\VuexyWebsiteAdmin\Website\Enums\WebsiteSiteStatus; +use OwenIt\Auditing\Contracts\Auditable as AuditableContract; +use OwenIt\Auditing\Auditable; + +class WebsiteSite extends Model implements AuditableContract +{ + use HasVuexyModelMetadata; + use Auditable; + use HasCreator, + HasUpdater; + + // ===================== METADATOS ===================== + + public string $sortColumn = 'name'; + public string $defaultSortOrder = 'asc'; + public string $singularName = 'sitio web'; + public string $focusColumnOnOpen = 'name'; + + // ===================== CONFIGURACIÓN ===================== + + protected $fillable = [ + 'name', + 'slug', + 'domain', + 'template', + 'status', + 'is_indexable', + 'seo_profile_id', + 'canonical_url', + 'config', + 'created_by', + 'updated_by' + ]; + + protected $casts = [ + 'status' => WebsiteSiteStatus::class, + 'is_indexable' => 'boolean', + 'config' => 'array', + ]; + + protected $auditInclude = [ + 'name', + 'slug', + 'domain', + 'template', + 'status', + 'is_indexable', + 'seo_profile_id', + 'canonical_url', + 'config', + ]; + + // ===================== RELACIONES ===================== + + public function menus(): HasMany + { + return $this->hasMany(WebsiteMenu::class); + } + + public function contents(): HasMany + { + return $this->hasMany(WebsiteContent::class); + } + + public function seoProfiles(): BelongsTo + { + return $this->belongsTo(WebsiteSeoProfile::class); + } + + // ===================== GETTERS ===================== + + public function getDisplayName(): string + { + return $this->name; + } +} diff --git a/src/Providers/VuexyWebsiteAdminServiceProvider.php b/src/Providers/VuexyWebsiteAdminServiceProvider.php new file mode 100644 index 0000000..1c0aa69 --- /dev/null +++ b/src/Providers/VuexyWebsiteAdminServiceProvider.php @@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Providers; + +use Illuminate\Support\ServiceProvider; +use Koneko\VuexyAdmin\Support\Traits\Modules\KonekoModuleBoots; + +class VuexyWebsiteAdminServiceProvider extends ServiceProvider +{ + use KonekoModuleBoots; + + public function register(): void + { + $this->registerKonekoModule(dirname(__DIR__)); + } +} diff --git a/src/Traits/Context/HasSiteContext.php b/src/Traits/Context/HasSiteContext.php new file mode 100644 index 0000000..25bb04a --- /dev/null +++ b/src/Traits/Context/HasSiteContext.php @@ -0,0 +1,60 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Support\Traits\Context; + +use Koneko\VuexyWebsiteAdmin\Application\Bootstrap\Context\SiteContext; +use Koneko\VuexyWebsiteAdmin\Models\WebsiteSite; + +/** + * 🧬 Trait para inyectar y resolver contexto de sitio activo + */ +trait HasSiteContext +{ + protected ?WebsiteSite $site = null; + + /** + * Asigna sitio manualmente + */ + public function setSite(WebsiteSite $site): static + { + $this->site = $site; + return $this; + } + + /** + * Devuelve el sitio activo desde el contexto global o el asignado + */ + public function getSite(): ?WebsiteSite + { + return $this->site ?? SiteContext::resolve(); + } + + /** + * Shortcut para obtener el ID del sitio (o null) + */ + public function getSiteId(): ?int + { + return $this->getSite()?->id; + } + + /** + * Shortcut para inyectar contexto en settings o servicios + */ + public function applySiteScopeToSettings(): static + { + if ($site = $this->getSite()) { + settings()->setScope('site', $site->id); + } + + return $this; + } + + public function applySiteScopeToCache(): static + { + if ($site = $this->getSite()) { + cache_m()->setScope('site', $site->id); + } + + return $this; + } +} diff --git a/src/Website/Cache/RenderCacheInvalidator.php b/src/Website/Cache/RenderCacheInvalidator.php new file mode 100644 index 0000000..b2b4ee8 --- /dev/null +++ b/src/Website/Cache/RenderCacheInvalidator.php @@ -0,0 +1,17 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Website\Cache; + +use Illuminate\Contracts\Cache\Repository; + +class RenderCacheInvalidator +{ + public function __construct(private Repository $cache) + { + } + + public function invalidate(): void + { + $this->cache->forget('website.render'); + } +} \ No newline at end of file diff --git a/src/Website/Cache/WebsiteRenderCacheService.php b/src/Website/Cache/WebsiteRenderCacheService.php new file mode 100644 index 0000000..0b23301 --- /dev/null +++ b/src/Website/Cache/WebsiteRenderCacheService.php @@ -0,0 +1,74 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Website\Cache; + +use Closure; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpFoundation\Response; + +class WebsiteRenderCacheService +{ + public const TAG_MAIN = 'rendered_html'; + + /** + * Genera la clave base del cache para un tipo y slug + */ + public static function cacheKeyFor(string $type, string $slug): string + { + return "rendered_page:{$type}:{$slug}"; + } + + /** + * Obtiene una vista renderizada desde cache, o la genera y almacena. + */ + public static function getOrRender(string $type, string $slug, Closure $callback, ?int $ttl = null): string + { + $ttl = $ttl ?? Config::get('koneko.website.cache.html.ttl.default', 900); + $key = self::cacheKeyFor($type, $slug); + + return Cache::tags([ + self::TAG_MAIN, + $type, + "{$type}_{$slug}" + ])->remember($key, now()->addMinutes($ttl), $callback); + } + + /** + * Invalida un contenido renderizado + */ + public static function invalidate(string $type, string $slug): void + { + Cache::tags([ + self::TAG_MAIN, + $type, + "{$type}_{$slug}" + ])->flush(); + } + + /** + * Devuelve una respuesta HTTP renderizada con headers de depuración. + */ + public static function responseWithHeaders(string $html, string $type, string $slug): Response + { + $debug = Config::get('koneko.website.cache.html.debug_mode', false); + + return response($html)->withHeaders([ + 'X-Koneko-Cache' => 'HIT', + 'X-Koneko-Type' => $type, + 'X-Koneko-Slug' => $slug, + 'X-Koneko-TTL' => Config::get('koneko.website.cache.html.ttl.default', 900), + 'X-Koneko-Debug' => $debug ? 'true' : 'false', + ]); + } + + /** + * Limpia toda la cache HTML + */ + public static function flushAll(): void + { + Cache::tags([self::TAG_MAIN])->flush(); + Log::info('[WebsiteRenderCacheService] Toda la cache HTML ha sido invalidada.'); + } +} diff --git a/src/Website/Enums/WebsiteSiteStatus.php b/src/Website/Enums/WebsiteSiteStatus.php new file mode 100644 index 0000000..af3e160 --- /dev/null +++ b/src/Website/Enums/WebsiteSiteStatus.php @@ -0,0 +1,10 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Website\Enums; + +enum WebsiteSiteStatus: string +{ + case Active = 'active'; + case ComingSoon = 'coming_soon'; + case Maintenance = 'maintenance'; +} diff --git a/src/Website/Menu/WebsiteMenuRenderer.php b/src/Website/Menu/WebsiteMenuRenderer.php new file mode 100644 index 0000000..6722c6d --- /dev/null +++ b/src/Website/Menu/WebsiteMenuRenderer.php @@ -0,0 +1,165 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Website\Menu; + +use Illuminate\Support\Facades\{Auth, Cache, Config, Route}; +use Koneko\VuexyWebsiteAdmin\Models\{WebsiteMenu, WebsiteMenuItem}; + +class WebsiteMenuRenderer +{ + public static function tree(string $slug): array + { + $cacheEnabled = Config::get('koneko.website.menu.cache.enabled', true); + $cacheTtl = Config::get('koneko.website.menu.cache.ttl', 3600); + + $cacheKey = self::cacheKey($slug); + + if ($cacheEnabled) { + return Cache::remember($cacheKey, $cacheTtl, fn() => self::build($slug)); + } + + return self::build($slug); + } + + public static function clearCache(string $slug): void + { + Cache::forget(self::cacheKey($slug)); + } + + public static function clearAllCache(): void + { + $prefix = 'website_menu:'; + foreach (Cache::getRedis()->keys("{$prefix}*") as $key) { + Cache::forget(str_replace(config('cache.prefix') . ':', '', $key)); + } + } + + private static function cacheKey(string $slug): string + { + return "website_menu:{$slug}"; + } + + private static function build(string $slug): array + { + $menu = WebsiteMenu::where('slug', $slug) + ->where('is_active', true) + ->with(['items' => fn($query) => $query->where('is_active', true)->orderBy('order')]) + ->first(); + + if (!$menu) { + return []; + } + + return self::buildTree($menu->items->whereNull('parent_id'), $menu->items); + } + + private static function buildTree($items, $allItems, $parentId = null): array + { + $tree = []; + + foreach ($items as $item) { + if ($item->parent_id !== $parentId) { + continue; + } + + if (!self::canAccess($item)) { + continue; + } + + if (!self::isVisible($item)) { + continue; + } + + $meta = self::generateUrl($item); + + $tree[] = [ + 'id' => $item->id, + 'title' => $item->localized_title, + 'slug' => $item->slug, + 'url' => $item->url ?? 'javascript:;', + 'target' => $item->target ?? '_self', + 'type' => $item->type->value ?? 'custom', + 'icon' => $item->icon, + 'badge' => $item->badge, + 'badge_color' => $item->badge_color, + 'method' => $item->method ?? 'GET', + 'js_event' => $item->js_event, + 'meta' => $meta, + 'children' => self::buildTree($allItems, $allItems, $item->id), + ]; + } + + return $tree; + } + + private static function canAccess(WebsiteMenuItem $item): bool + { + return self::canAccessRole($item) && self::canAccessPermission($item); + } + + private static function canAccessRole(WebsiteMenuItem $item): bool + { + if (empty($item->roles)) { + return true; + } + + if (!Auth::check()) { + return in_array('guest', $item->roles); + } + + return Auth::user()->hasAnyRole($item->roles); + } + + private static function canAccessPermission(WebsiteMenuItem $item): bool + { + if (empty($item->permissions)) { + return true; + } + + if (!Auth::check()) { + return false; + } + + return Auth::user()->hasAnyPermission($item->permissions); + } + + private static function isVisible(WebsiteMenuItem $item): bool + { + $now = now(); + + if ($item->visible_from && $now->lt($item->visible_from)) { + return false; + } + + if ($item->visible_until && $now->gt($item->visible_until)) { + return false; + } + + return true; + } + + private static function generateUrl(WebsiteMenuItem &$item): array + { + $debugBrokenRoutes = Config::get('koneko.website.menu.debug.show_broken_routes', false); + $meta = []; + + if (!empty($item->laravel_route)) { + if (Route::has($item->laravel_route)) { + $item->url = route($item->laravel_route); + $meta['route_found'] = true; + } else { + $item->url = $debugBrokenRoutes ? 'javascript:; /* broken route */' : 'javascript:;'; + $meta['route_found'] = false; + } + } elseif (!empty($item->url)) { + $meta['route_found'] = true; + } else { + $item->url = 'javascript:;'; + $meta['route_found'] = false; + } + + return $meta; + } +} diff --git a/src/Website/Traits/Cache/HasRenderCache.php b/src/Website/Traits/Cache/HasRenderCache.php new file mode 100644 index 0000000..87ee493 --- /dev/null +++ b/src/Website/Traits/Cache/HasRenderCache.php @@ -0,0 +1,28 @@ +<?php + +namespace Koneko\VuexyWebsiteAdmin\Website\Traits\Cache; + +use Illuminate\Support\Facades\Cache; + +trait HasRenderCache +{ + public function hasRenderCache(): bool + { + return Cache::has("rendered_html.{$this->slug}"); + } + + public function getRenderCache(): ?string + { + return Cache::get("rendered_html.{$this->slug}"); + } + + public function setRenderCache(string $html): void + { + Cache::tags(["rendered_html", "website", "website_{$this->slug}"])->put("rendered_html.{$this->slug}", $html); + } + + public function clearRenderCache(): void + { + Cache::tags(["rendered_html", "website", "website_{$this->slug}"])->forget("rendered_html.{$this->slug}"); + } +} diff --git a/src/Website/UX/Content/WebsiteBreadcrumbsBuilderService.php b/src/Website/UX/Content/WebsiteBreadcrumbsBuilderService.php new file mode 100644 index 0000000..1cc9e7b --- /dev/null +++ b/src/Website/UX/Content/WebsiteBreadcrumbsBuilderService.php @@ -0,0 +1,107 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Application\UX\Content; + +use Illuminate\Support\Facades\Route; +use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuBuilderService; + +class WebsiteBreadcrumbsBuilderService +{ + private array $menu; + + public function __construct(?array $menu = null) + { + // Permitir inyectar un menú preprocesado o usar el Builder + $this->menu = $menu ?? app(VuexyMenuBuilderService::class)->getForUser(); + } + + /** + * Devuelve el trail de breadcrumbs con "Inicio" siempre al inicio + */ + public function getBreadcrumbs(): array + { + // Si estamos en la página de inicio, no mostrar breadcrumbs + if (Route::currentRouteName() === 'admin.core.pages.home.index') { + return []; // Esto hará que $vuexyBreadcrumbs sea falsy y no se renderice el <nav> + } + + $breadcrumbs = $this->findBreadcrumbTrail($this->menu); + + // Asegura "Inicio" al principio + array_unshift($breadcrumbs, [ + 'name' => 'Inicio', + 'link' => route('admin.core.pages.home.index'), + 'active' => false, + ]); + + // Marca el último breadcrumb como activo + if (!empty($breadcrumbs)) { + $breadcrumbs[array_key_last($breadcrumbs)]['active'] = true; + } + + return $breadcrumbs; + } + + private function findBreadcrumbTrail(array $menu, array $trail = []): array + { + $currentRoute = Route::currentRouteName(); + $currentSlug = $this->getCurrentSlug(); + + foreach ($menu as $title => $item) { + $skip = $item['_meta']['breadcrumbs'] ?? false; + + // Verificación por route + $routeMatches = isset($item['route']) && $item['route'] === $currentRoute; + $partialMatch = isset($item['route']) && str_starts_with($currentRoute, dirname($item['route'])); + $slugMatches = $currentSlug && isset($item['_slug']) && $item['_slug'] === $currentSlug; + + $newTrail = $trail; + + // Si se va a agregar al trail visual (breadcrumb) + $isBreadcrumbNode = !$skip; + + // Verificamos si es el "Inicio" duplicado + $isDuplicateInicio = strtolower($title) === 'inicio' && + ( + ($item['route'] ?? null) === 'admin.core.pages.home.index' || + ($item['_slug'] ?? null) === 'inicio' + ); + + if ($isBreadcrumbNode && !$isDuplicateInicio) { + $newTrail[] = [ + 'name' => $item['_meta']['label'] ?? $title, + 'link' => isset($item['route']) ? route($item['route']) : + (isset($item['_slug']) ? route('admin.core.pages.folder.view', ['slug' => $item['_slug']]) : null), + 'active' => false, + ]; + } + + if ($routeMatches || $partialMatch || $slugMatches) { + return $newTrail; + } + + if (isset($item['submenu']) && is_array($item['submenu'])) { + $found = $this->findBreadcrumbTrail($item['submenu'], $newTrail); + if (!empty($found)) { + return $found; + } + } + } + + + return []; + } + + private function getCurrentSlug(): ?string + { + $route = Route::current(); + + if ($route && $route->getName() === 'admin.core.pages.folder.view') { + return $route->parameter('slug'); + } + + return null; + } +} diff --git a/src/Website/UX/Header/___VuexyQuicklinksBuilderService.php b/src/Website/UX/Header/___VuexyQuicklinksBuilderService.php new file mode 100644 index 0000000..09b69a8 --- /dev/null +++ b/src/Website/UX/Header/___VuexyQuicklinksBuilderService.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyAdmin\Application\UX\Navbar; + +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Support\Facades\{Route,Auth}; +use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuBuilderService; +use Koneko\VuexyAdmin\Models\Setting; +use Koneko\VuexyAdmin\Support\Traits\Cache\InteractsWithKonekoVarsCache; + +class ___VuexyQuicklinksBuilderService +{ + use InteractsWithKonekoVarsCache; + + private array $quicklinkRoutes = []; + + private ?Authenticatable $user = null; + + private const CACHE_PREFIX = 'vuexy_quick_links_user_id:'; + + public function __construct(?Authenticatable $user = null) + { + $this->user = $user ?? Auth::user(); + $this->initCacheConfig(true); + } + + public function getForUser(): array + { + $quickLinks = $this->cacheOrComputeForUser(fn () => $this->buildQuicklinks()); + + $currentRoute = Route::currentRouteName(); + $quickLinks['current_page_in_list'] = $this->isCurrentPageInList($quickLinks, $currentRoute); + + return $quickLinks; + } + + private function buildQuicklinks(): array + { + $menu = app(VuexyMenuBuilderService::class)->getForUser($this->user); + + $setting = Setting::where('user_id', $this->user->id) + ->where('key', 'quicklinks') + ->first(); + + $this->quicklinkRoutes = $setting ? json_decode($setting->value, true) : []; + + $links = []; + $this->collectFromMenu($menu, $links); + + return [ + 'totalLinks' => count($links), + 'rows' => array_chunk($links, 2), + ]; + } + + + private function collectFromMenu(array $menu, array &$links, ?string $parent = null): void + { + foreach ($menu as $title => $item) { + $route = $item['route'] ?? null; + + if ($route && in_array($route, $this->quicklinkRoutes)) { + $links[] = [ + 'title' => $title, + 'subtitle' => $parent ?? config('app.name'), + 'icon' => $item['icon'] ?? 'ti ti-point', + 'url' => Route::has($route) ? route($route) : ($item['url'] ?? 'javascript:;'), + 'route' => $route, + ]; + } + + if (isset($item['submenu']) && is_array($item['submenu'])) { + $this->collectFromMenu($item['submenu'], $links, $title); + } + } + } + + private function isCurrentPageInList(array $quickLinks, string $route): bool + { + foreach ($quickLinks['rows'] ?? [] as $row) { + foreach ($row as $item) { + if (($item['route'] ?? null) === $route) { + return true; + } + } + } + + return false; + } +} diff --git a/src/Website/UX/Header/___VuexySearchBuilderService.php b/src/Website/UX/Header/___VuexySearchBuilderService.php new file mode 100644 index 0000000..a9be372 --- /dev/null +++ b/src/Website/UX/Header/___VuexySearchBuilderService.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyAdmin\Application\UX\Navbar; + +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Support\Facades\Route; +use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuBuilderService; +use Koneko\VuexyAdmin\Support\Traits\Cache\InteractsWithKonekoVarsCache; + +class ___VuexySearchBuilderService +{ + use InteractsWithKonekoVarsCache; + + private const CACHE_PREFIX = 'vuexy_search_user_id:'; + + private Authenticatable $user; + + public function __construct(Authenticatable $user) + { + $this->user = $user; + $this->initCacheConfig(true); // relacionado al menú + } + + /** + * Obtiene el índice de búsqueda para el usuario autenticado. + */ + public function getForUser(): array + { + return $this->cacheOrComputeForUser(fn () => $this->buildIndex()); + } + + /** + * Construye el índice de búsqueda a partir del menú procesado. + */ + private function buildIndex(): array + { + $menu = app(VuexyMenuBuilderService::class)->getForUser($this->user); + return $this->buildFromMenu($menu); + } + + /** + * Recorre el menú para construir el índice plano de rutas accesibles. + */ + private function buildFromMenu(array $menu, string $parent = ''): array + { + $entries = []; + + foreach ($menu as $title => $item) { + $fullPath = $parent ? "$parent / $title" : $title; + + $url = $item['url'] ?? ( + isset($item['route']) && Route::has($item['route']) ? route($item['route']) : null + ); + + if ($url) { + $entries[] = [ + 'name' => $fullPath, + 'icon' => $item['icon'] ?? 'ti ti-point', + 'url' => $url, + ]; + } + + if (!empty($item['submenu'])) { + $entries = [...$entries, ...$this->buildFromMenu($item['submenu'], $fullPath)]; + } + } + + return $entries; + } +} diff --git a/src/Website/UX/Menu/___VuexyMenuRegistry.php b/src/Website/UX/Menu/___VuexyMenuRegistry.php new file mode 100644 index 0000000..b3ef36c --- /dev/null +++ b/src/Website/UX/Menu/___VuexyMenuRegistry.php @@ -0,0 +1,242 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyAdmin\Website\UX\Menu; + +use Illuminate\Support\Facades\File; +use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry; + +class ___VuexyMenuRegistry +{ + private array $baseMenu = []; + private array $moduleMenus = []; + private array $projectMenu = []; + + /** + * Construye y devuelve el menú completo del ERP. + */ + public function getMerged(): array + { + $this->baseMenu = $this->loadBaseMenu(); + $this->moduleMenus = $this->loadModuleMenus(); + $this->projectMenu = $this->loadProjectMenu(); + + + // Create a single array with all menus in the correct order + $allMenus = array_merge( + [$this->baseMenu], + $this->moduleMenus, + [$this->projectMenu] + ); + + return $this->sortByPriority( + $this->mergeMenus(...$allMenus) + ); + } + + /** + * Devuelve la estructura sin mezclar (útil para debugging). + */ + public function getRaw(): array + { + return [ + 'base' => $this->baseMenu, + 'modules' => $this->moduleMenus, + 'project' => $this->projectMenu, + ]; + } + + /** + * Carga el menú base del sistema (no registrado en config). + */ + private function loadBaseMenu(): array + { + $path = base_path('vendor/koneko/laravel-vuexy-admin/config/vuexy_menu.php'); + + return File::exists($path) ? require $path : []; + } + + /** + * Carga todos los menús definidos por módulos activos. + */ + private function loadModuleMenus(): array + { + $menus = []; + + foreach (KonekoModuleRegistry::enabled() as $module) { + $menuPath = $module->extensions['menu']['path'] ?? null; + + if ($menuPath) { + $fullPath = $module->basePath . DIRECTORY_SEPARATOR . $menuPath; + if (File::exists($fullPath)) { + $menus[] = require $fullPath; + } + } + } + + return $menus; + } + + /** + * Carga un menú adicional desde el proyecto (si existe). + */ + private function loadProjectMenu(): array + { + $menuConfig = config('vuexy-admin.extensions.menu'); + + // Si no está definido o es null, no carga menú del proyecto + if (!$menuConfig || !isset($menuConfig['path'])) { + return []; + } + + $path = $menuConfig['path']; + + // Permitir rutas relativas a storage/ + if (str_starts_with($path, 'storage/')) { + $fullPath = storage_path(substr($path, strlen('storage/'))); + } else { + // Por defecto: ruta relativa al config/ + $fullPath = base_path($path); + } + + return File::exists($fullPath) ? require $fullPath : []; + } + + /** + * Fusión recursiva de todos los menús (base + módulos + proyecto). + */ + private function mergeMenus(array ...$menus): array + { + $merged = []; + + foreach ($menus as $menu) { + $merged = $this->deepMerge($merged, $menu); + } + + return $merged; + } + + /** + * Fusión profunda para mantener jerarquía y submenús. + */ + private function deepMerge(array $base, array $extension): array + { + foreach ($extension as $key => $value) { + if (!isset($base[$key])) { + $base[$key] = $value; + continue; + } + + if (is_array($base[$key]) && is_array($value)) { + // Fusionar submenu recursivamente si existen + if (isset($base[$key]['submenu']) && isset($value['submenu'])) { + $base[$key]['submenu'] = $this->deepMerge( + $base[$key]['submenu'], + $value['submenu'] + ); + } + + // Fusionar _meta con prioridad del módulo o del proyecto + if (isset($base[$key]['_meta']) && isset($value['_meta'])) { + $base[$key]['_meta'] = array_merge( + $base[$key]['_meta'], + $value['_meta'] + ); + } + + // Merge general sin aplastar `submenu` ni `_meta` + $merged = array_merge($base[$key], $value); + $merged['submenu'] = $base[$key]['submenu'] ?? $value['submenu'] ?? null; + $merged['_meta'] = $base[$key]['_meta'] ?? $value['_meta'] ?? null; + + $base[$key] = $merged; + + } else { + // Sobrescribir directamente si no es array + $base[$key] = $value; + } + } + + return $base; + } + + /** + * Ordena un menú por _meta.priority y claves 'before' / 'after'. + */ + private function sortByPriority(array $menu): array + { + // Paso 1: extraer todos los ítems con claves + $items = []; + foreach ($menu as $key => $value) { + $meta = $value['_meta'] ?? []; + $items[$key] = [ + 'key' => $key, + 'value' => $value, + 'before' => $meta['before'] ?? null, + 'after' => $meta['after'] ?? null, + 'priority' => $meta['priority'] ?? 999, + ]; + } + + // Paso 2: ordenar respetando before/after y priority + $sorted = []; + $inserted = []; + + while (count($items) > 0) { + foreach ($items as $key => $item) { + $canInsert = true; + + if ($item['before'] && !isset($inserted[$item['before']])) { + $canInsert = false; + } + + if ($item['after'] && !isset($inserted[$item['after']])) { + $canInsert = false; + } + + if ($canInsert) { + $position = null; + + if ($item['before']) { + $position = array_search($item['before'], array_keys($sorted), true); + } elseif ($item['after']) { + $position = array_search($item['after'], array_keys($sorted), true); + $position = $position !== false ? $position + 1 : null; + } + + if ($position !== null) { + $sorted = array_slice($sorted, 0, $position, true) + + [$key => $item['value']] + + array_slice($sorted, $position, null, true); + } else { + $sorted[$key] = $item['value']; + } + + $inserted[$key] = true; + unset($items[$key]); + } + } + + // Si no pudimos insertar nada, rompemos el ciclo para evitar loop infinito + if (count($items) === count(array_diff_key($items, $inserted))) { + // Ordenar el resto por priority como fallback + uasort($items, fn ($a, $b) => $a['priority'] <=> $b['priority']); + foreach ($items as $key => $item) { + $sorted[$key] = $item['value']; + } + break; + } + } + + // Paso 3: ordenar submenús recursivamente + foreach ($sorted as &$item) { + if (isset($item['submenu']) && is_array($item['submenu'])) { + $item['submenu'] = $this->sortByPriority($item['submenu']); + } + } + + return $sorted; + } + +} diff --git a/src/Website/UX/Menu/___WebsiteMenuBuilderService.php b/src/Website/UX/Menu/___WebsiteMenuBuilderService.php new file mode 100644 index 0000000..5ea7ea0 --- /dev/null +++ b/src/Website/UX/Menu/___WebsiteMenuBuilderService.php @@ -0,0 +1,380 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Website\UX\Menu; + +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Support\Facades\{Auth,Route}; +use Illuminate\Support\Str; +use Koneko\VuexyAdmin\Models\User; +use Koneko\VuexyAdmin\Support\Traits\Cache\InteractsWithKonekoVarsCache; +use Spatie\Permission\Exceptions\PermissionDoesNotExist; + +/** + * Clase encargada de construir el menú dinámico Vuexy + * basado en permisos, configuración, y visibilidad. + */ +class ___WebsiteMenuBuilderService +{ + use InteractsWithKonekoVarsCache; + + /** @var array Arreglo de menú crudo combinado desde todos los módulos */ + private array $rawMenu = []; + + /** @var array Menú final procesado y autorizado */ + private array $finalMenu = []; + + /** @var bool Activa el modo de depuración completo */ + private bool $debugMode = false; + + private ?Authenticatable $user = null; + + private const CACHE_TAG = 'vuexy-menu'; + private const CACHE_PREFIX = 'vuexy_menu_user_id:'; + + /** + * Constructor del builder + * + * @param Authenticatable|null $user Usuario a simular o autenticar + */ + public function __construct(?Authenticatable $user = null) + { + $this->user = $user ?? Auth::user(); + $this->initCacheConfig(true); + } + + /** + * Construye el menú procesado, aplicando filtros y visibilidad + * + * @return array Menú final listo para renderizado + */ + public function build(): array + { + $this->rawMenu = (new VuexyMenuRegistry())->getMerged(); + $this->finalMenu = $this->processMenu($this->rawMenu); + + $this->assignAutoKeys($this->finalMenu); // Ya lo tienes + $this->assignSlugs($this->finalMenu); // <- NUEVO + + return $this->finalMenu; + } + + /** + * Aplica filtros de visibilidad y convierte rutas en URLs + * + * @param array $menu Arreglo jerárquico del menú + * @return array Menú procesado + */ + private function processMenu(array $menu): array + { + $result = []; + + foreach ($menu as $key => $item) { + // Validamos visibilidad + if (!$this->isVisible($item)) { + if (config(CoreModule::NAMESPACE.'.'.CoreModule::COMPONENT.'.menu.debug.show_hidden_items', false)) { + $item['_meta']['hidden_debug'] = true; + + } else { + continue; + } + } + + // Header + if (isset($item['_meta']['type']) && $item['_meta']['type'] === 'header') { + $result[$key] = ['_meta' => $item['_meta']]; + continue; + } + + // Convertir route a URL + $this->convertRouteToUrl($item); + + // Procesar submenu + if (isset($item['submenu'])) { + $item['submenu'] = $this->processMenu($item['submenu']); + + if (empty($item['submenu']) && !isset($item['route']) && !isset($item['url'])) { + continue; + } + } + + $result[$key] = $item; + } + + // Asignar claves automáticas + $this->assignAutoKeys($result); + + // Aplicar ordenamiento + return $this->applySorting($result); + } + + /** + * Determina si un ítem de menú es visible para el usuario actual + * + * @param array $item Elemento del menú + * @return bool Verdadero si es visible, falso si debe ocultarse + */ + private function isVisible(array $item): bool + { + $forceVisible = $this->debugMode; + + if ($forceVisible || config(CoreModule::NAMESPACE.'.'.CoreModule::COMPONENT.'.menu.debug.show_hidden_items', false)) { + return true; + } + + if (isset($item['_meta']['visible']) && !$item['_meta']['visible']) { + return false; + } + + if (isset($item['can']) && !$this->userCan($item['can'])) { + return (bool) config(CoreModule::NAMESPACE.'.'.CoreModule::COMPONENT.'.menu.debug.show_disallowed_links', false); + } + + if (isset($item['route']) && !Route::has($item['route'])) { + return (bool) config(CoreModule::NAMESPACE.'.'.CoreModule::COMPONENT.'.menu.debug.show_broken_routers', false); + } + + return true; + } + + /** + * Evalúa si el usuario tiene permisos para ver un ítem específico + * + * @param string|array $permissions Permiso o arreglo de permisos + * @return bool + */ + private function userCan(string|array $permissions): bool + { + if (!$this->user || !method_exists($this->user, 'hasPermissionTo')) { + return false; + } + + try { + if (is_array($permissions)) { + foreach ($permissions as $perm) { + if ($this->user->hasPermissionTo($perm)) return true; + } + return false; + } + + return $this->user->hasPermissionTo($permissions); + + } catch (PermissionDoesNotExist) { + return false; + } + } + + /** + * Convierte la clave 'route' en una URL real usando helper route() + * + * @param array $item Elemento individual del menú (modificado por referencia) + */ + private function convertRouteToUrl(array &$item): void + { + if (isset($item['route'])) { + if (Route::has($item['route'])) { + $item['url'] = route($item['route']); + + } elseif (config(CoreModule::NAMESPACE.'.'.CoreModule::COMPONENT.'.menu.debug.show_broken_routers', false)) { + $item['url'] = 'javascript:;'; + + } else { + $item['url'] = null; + } + } + } + + /** + * Asigna claves automáticas y mapeo jerárquico + */ + private function assignAutoKeys(array &$menu, array &$map = [], string $prefix = '', int &$index = 0): void + { + foreach ($menu as $key => &$item) { + $id = $index++; + $item['_meta']['auto_id'] = $id; + $map[$id] = $prefix . $key; + + if (isset($item['submenu']) && is_array($item['submenu'])) { + $this->assignAutoKeys($item['submenu'], $map, $prefix . $key . '/', $index); + } + } + } + + /** + * Ordena los elementos del menú usando prioridad, before_to y after_to + * + * @param array $items Arreglo de elementos del menú + * @return array Arreglo ordenado + */ + private function applySorting(array $items): array + { + // 1️⃣ Normalizamos los ítems con prioridad y dependencias + $nodes = []; + + foreach ($items as $key => $item) { + $meta = $item['_meta'] ?? []; + + $priority = $meta['priority'] ?? 500; + $priority = match (true) { + $priority === 'first' => -999999, + $priority === 'last' => 999999, + is_numeric($priority) => (int) $priority, + default => 500, + }; + + $nodes[$key] = [ + 'key' => $key, + 'item' => $item, + 'before' => $meta['before_to'] ?? null, + 'after' => $meta['after_to'] ?? null, + 'priority' => $priority, + 'inserted' => false, + ]; + } + + // 2️⃣ Orden inicial por prioridad + uasort($nodes, fn($a, $b) => $a['priority'] <=> $b['priority']); + + $sorted = []; + + // 3️⃣ Resolución iterativa de dependencias + while (!empty($nodes)) { + $progress = false; + + foreach ($nodes as $key => $node) { + $canInsert = true; + + // Si tiene dependencia y aún no se ha insertado el target, saltamos + if ($node['before'] && !isset($sorted[$node['before']])) { + $canInsert = false; + } + + if ($node['after'] && !isset($sorted[$node['after']])) { + $canInsert = false; + } + + if (!$canInsert) { + continue; + } + + // Insertamos en la posición correcta + if ($node['before'] && isset($sorted[$node['before']])) { + $position = array_search($node['before'], array_keys($sorted), true); + $sorted = array_slice($sorted, 0, $position, true) + + [$key => $node['item']] + + array_slice($sorted, $position, null, true); + } elseif ($node['after'] && isset($sorted[$node['after']])) { + $position = array_search($node['after'], array_keys($sorted), true) + 1; + $sorted = array_slice($sorted, 0, $position, true) + + [$key => $node['item']] + + array_slice($sorted, $position, null, true); + } else { + // Sin dependencias, o ya satisfechas + $sorted[$key] = $node['item']; + } + + unset($nodes[$key]); + $progress = true; + } + + // Prevención de loop infinito (por dependencias circulares o mal definidas) + if (!$progress) { + // Lo que queda se inserta en orden de prioridad + foreach ($nodes as $key => $node) { + $sorted[$key] = $node['item']; + } + break; + } + } + + return $sorted; + } + + protected function getUserId(): int|string + { + return $this->user->id ?? throw new \RuntimeException('No se puede obtener el ID de usuario.'); + } + + /** + * Obtiene el menú procesado para un usuario específico, visitante o el autenticado. + * Respeta la configuración de caché desde config/vuexy.php (VUEXY_CACHE_MENU). + * + * @param Authenticatable|null $user Usuario explícito o null para visitante. + * @return array Menú final procesado y autorizado para el usuario. + */ + public function getForUser(null|Authenticatable $user = null): array + { + $this->user = $user ?? Auth::user(); + + if (!$this->user || !$this->user->id) { + return $this->build(); + } + + $cacheKey = static::makeCacheKeyForUser($this->user->id); + + return $this->cacheOrComputeTagged($cacheKey, function () { + logger()->info("Regenerando menú para usuario {$this->user->id}"); + return $this->build(); + }); + } + + private function assignSlugs(array &$menu, string $trail = ''): void + { + foreach ($menu as $key => &$item) { + $currentTrail = trim($trail . ' ' . $key); + $slug = Str::slug($currentTrail); + + if (!empty($item['submenu']) && empty($item['route'])) { + $item['_slug'] = $slug; + $item['_path'] = $currentTrail; + } + + if (!empty($item['submenu'])) { + $this->assignSlugs($item['submenu'], $currentTrail . ' /'); + } + } + } + + public function getNodeTreeBySlug(string $slug, User $user): ?array + { + $menu = $this->getForUser($user); + return $this->searchTreeBySlug($menu, $slug); + } + + private function searchTreeBySlug(array $menu, string $slug, array $trail = []): ?array + { + foreach ($menu as $key => $item) { + $currentLabel = $item['_meta']['widget_label'] ?? $item['_meta']['original_key'] ?? $key; + + $currentTrail = array_merge($trail, [[ + 'label' => $currentLabel, + 'icon' => $item['icon'] ?? 'ti ti-folder', + 'description' => $item['description'] ?? '', + 'slug' => $item['_slug'] ?? null, + 'auto_id' => $item['_meta']['auto_id'] ?? null, + ]]); + + if (($item['_slug'] ?? null) === $slug) { + return [ + 'node' => $item, + 'tree' => $currentTrail, + ]; + } + + if (!empty($item['submenu'])) { + $found = $this->searchTreeBySlug($item['submenu'], $slug, $currentTrail); + if ($found) { + return $found; + } + } + } + + return null; + } + + public static function clearAllCache(): void + { + static::flushCacheTags(static::CACHE_TAG); + } +} diff --git a/src/Website/UX/Template/WebsiteVarsBuilderService.php b/src/Website/UX/Template/WebsiteVarsBuilderService.php new file mode 100644 index 0000000..b843e8e --- /dev/null +++ b/src/Website/UX/Template/WebsiteVarsBuilderService.php @@ -0,0 +1,115 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyWebsiteAdmin\Website\UX\Template; + +use Illuminate\Support\Facades\{Cache,Schema}; +use Koneko\VuexyAdmin\Support\Traits\Cache\InteractsWithKonekoVarsCache; + +class WebsiteVarsBuilderService +{ + use InteractsWithKonekoVarsCache; + + public const CACHE_WEBSITE_VARS_TAG = 'vuexy_website_settings_vars'; + + /** + * Constructor. + */ + public function __construct() + { + $this->initCacheConfig(); + } + + /** + * Obtiene las variables de website principales. + * + * @param string|null $key + * @return array|mixed + */ + public function getWebsiteVars(?string $key = null): mixed + { + /* + if (!Schema::hasTable('settings')) { + return $this->getDefaultWebsiteVars($key); + } + */ + + $websiteVars = $this->cacheOrCompute(self::CACHE_WEBSITE_VARS_TAG, function () { + $settings = settings()->self()->getGroup(); + + return $this->buildWebsiteVarsArray($settings); + }); + + return $key ? ($websiteVars[$key] ?? null) : $websiteVars; + } + + /** + * Construye las variables del website. + */ + private function buildWebsiteVarsArray(array $settings): array + { + return [ + 'title' => $settings['title'] ?? config('koneko.title', 'Default Title'), + 'author' => $settings['author'] ?? config('koneko.author', 'Default Author'), + 'description' => $settings['description'] ?? config('koneko.description', 'Default Description'), + 'favicon' => $this->buildFaviconPaths($settings), + 'app_name' => $settings['app_name'] ?? config('koneko.app_name', 'Default App Name'), + 'image_logo' => $this->buildImageLogoPaths($settings), + ]; + } + + /** + * Construye las rutas de favicon. + */ + private function buildFaviconPaths(array $settings): array + { + $namespace = $settings['favicon_ns'] ?? null; + $defaultFavicon = config('koneko.favicon', 'favicon.ico'); + + 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, + ]; + } + + /** + * Construye las rutas de logos. + */ + private function buildImageLogoPaths(array $settings): array + { + $defaultLogo = config('koneko.app_logo', 'logo-default.png'); + + return [ + 'small' => $settings['image_logo_small'] ?? $defaultLogo, + 'medium' => $settings['image_logo_medium'] ?? $defaultLogo, + 'large' => $settings['image_logo'] ?? $defaultLogo, + 'small_dark' => $settings['image_logo_small_dark'] ?? $defaultLogo, + 'medium_dark' => $settings['image_logo_medium_dark'] ?? $defaultLogo, + 'large_dark' => $settings['image_logo_dark'] ?? $defaultLogo, + ]; + } + + /** + * Valores de fallback si no hay base de datos. + */ + private function getDefaultWebsiteVars(?string $key = null): array + { + return $key + ? ($this->buildWebsiteVarsArray([])[$key] ?? null) + : $this->buildWebsiteVarsArray([]); + } + + /** + * Limpia las caches del website + */ + public static function clearCache(): void + { + Cache::forget(self::CACHE_WEBSITE_VARS_TAG); + } +} \ No newline at end of file diff --git a/src/Website/UX/Template/___VuexyInterfaceService.php b/src/Website/UX/Template/___VuexyInterfaceService.php new file mode 100644 index 0000000..02b6197 --- /dev/null +++ b/src/Website/UX/Template/___VuexyInterfaceService.php @@ -0,0 +1,237 @@ +<?php + +declare(strict_types=1); + +namespace Koneko\VuexyAdmin\Application\System; + +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\{Cache, Config, Crypt, Schema}; +use Koneko\VuexyAdmin\Models\Setting; + +class ___VuexyInterfaceService +{ + private $cacheTTL = 60 * 24 * 30; + + public function loadSystemConfig(): void + { + try { + if (!Schema::hasTable('migrations')) { + // Base de datos no inicializada: usar valores predeterminados + $config = $this->getDefaultSystemConfig(); + } else { + // Cargar configuración desde la caché o base de datos + $config = Cache::remember('global_system_config', $this->cacheTTL, function () { + $settings = Setting::withVirtualValue() + ->where('key', 'LIKE', 'config.%') + ->pluck('value', 'key') + ->toArray(); + + return [ + 'servicesFacebook' => $this->buildServiceConfig($settings, 'config.services.facebook.', 'services.facebook'), + 'servicesGoogle' => $this->buildServiceConfig($settings, 'config.services.google.', 'services.google'), + 'vuexy' => $this->buildVuexyConfig($settings), + ]; + }); + } + + // Aplicar configuración al sistema + Config::set('services.facebook', $config['servicesFacebook']); + Config::set('services.google', $config['servicesGoogle']); + Config::set('vuexy', $config['vuexy']); + + } catch (\Exception $e) { + // Manejo silencioso de errores para evitar interrupciones + Config::set('services.facebook', config('services.facebook', [])); + Config::set('services.google', config('services.google', [])); + Config::set('vuexy', config('vuexy', [])); + } + } + + /** + * Obtiene la configuración predeterminada del sistema. + * + * @return array Configuración predeterminada para servicios y Vuexy + */ + private function getDefaultSystemConfig(): array + { + return [ + 'servicesFacebook' => config('services.facebook', [ + 'client_id' => '', + 'client_secret' => '', + 'redirect' => '', + ]), + 'servicesGoogle' => config('services.google', [ + 'client_id' => '', + 'client_secret' => '', + 'redirect' => '', + ]), + 'vuexy' => config('vuexy', []), + ]; + } + + /** + * Verifica si existe configuración para un bloque específico. + * + * @param array $settings Array de configuraciones + * @param string $blockPrefix Prefijo del bloque a verificar + * @return bool True si existe configuración para el bloque + */ + protected function hasBlockConfig(array $settings, string $blockPrefix): bool + { + return array_key_exists($blockPrefix, array_filter($settings, fn($key) => str_starts_with($key, $blockPrefix), ARRAY_FILTER_USE_KEY)); + } + + /** + * Construye la configuración para un servicio específico. + * + * @param array $settings Array de configuraciones + * @param string $blockPrefix Prefijo del bloque de configuración + * @param string $defaultConfigKey Clave de configuración predeterminada + * @return array Configuración del servicio + */ + protected function buildServiceConfig(array $settings, string $blockPrefix, string $defaultConfigKey): array + { + if (!$this->hasBlockConfig($settings, $blockPrefix)) { + return config($defaultConfigKey)?? []; + } + + return [ + 'client_id' => $settings["{$blockPrefix}client_id"] ?? '', + 'client_secret' => $settings["{$blockPrefix}client_secret"] ?? '', + 'redirect' => $settings["{$blockPrefix}redirect"] ?? '', + ]; + } + + /** + * Construye la configuración de Vuexy. + * + * Combina la configuración predeterminada con los valores almacenados + * en la base de datos y normaliza los campos booleanos. + * + * @param array $settings Array de configuraciones + * @return array Configuración de Vuexy normalizada + */ + protected function buildVuexyConfig(array $settings): array + { + // Configuración predeterminada del sistema + $defaultVuexyConfig = config('vuexy', []); + + // Convertimos las claves planas a un array multidimensional + $settingsNested = Arr::undot($settings); + + // Navegamos hasta la parte relevante del array desanidado + $vuexySettings = $settingsNested['config']['vuexy'] ?? []; + + // Fusionamos la configuración predeterminada con los valores del sistema + $mergedConfig = array_replace_recursive($defaultVuexyConfig, $vuexySettings); + + // Normalizamos los valores booleanos + return $this->normalizeBooleanFields($mergedConfig); + } + + /** + * Normaliza los campos booleanos en la configuración. + * + * @param array $config Configuración a normalizar + * @return array Configuración con campos booleanos normalizados + */ + protected function normalizeBooleanFields(array $config): array + { + $booleanFields = [ + 'myRTLSupport', + 'myRTLMode', + 'hasCustomizer', + 'displayCustomizer', + 'footerFixed', + 'menuFixed', + 'menuCollapsed', + 'showDropdownOnHover', + ]; + + foreach ($booleanFields as $field) { + if (isset($config['vuexy'][$field])) { + $config['vuexy'][$field] = (bool) $config['vuexy'][$field]; + } + } + + return $config; + } + + /** + * Limpia la caché de configuración del sistema. + * + * @return void + */ + public static function clearSystemConfigCache(): void + { + Cache::forget('global_system_config'); + } + + /** + * Limpia la configuración de Vuexy de la base de datos y caché. + * + * @return void + */ + public static function clearVuexyConfig(): void + { + Setting::where('key', 'LIKE', 'config.vuexy.%')->delete(); + + Cache::forget('global_system_config'); + } + + /** + * Obtiene la configuración del sistema de correo. + * + * Recupera y estructura la configuración de correo incluyendo + * configuración SMTP, direcciones de envío y respuesta. + * + * @return array Configuración completa del sistema de correo + */ + public function getMailSystemConfig(): array + { + return Cache::remember('mail_system_config', $this->cacheTTL, function () { + $settings = Setting::withVirtualValue() + ->where('key', 'LIKE', 'mail.%') + ->pluck('value', 'key') + ->toArray(); + + $defaultMailersSmtpVars = config('mail.mailers.smtp'); + + return [ + 'mailers' => [ + 'smtp' => array_merge($defaultMailersSmtpVars, [ + 'url' => $settings['mail.mailers.smtp.url'] ?? $defaultMailersSmtpVars['url'], + 'host' => $settings['mail.mailers.smtp.host'] ?? $defaultMailersSmtpVars['host'], + 'port' => $settings['mail.mailers.smtp.port'] ?? $defaultMailersSmtpVars['port'], + 'encryption' => $settings['mail.mailers.smtp.encryption'] ?? 'TLS', + 'username' => $settings['mail.mailers.smtp.username'] ?? $defaultMailersSmtpVars['username'], + 'password' => isset($settings['mail.mailers.smtp.password']) && !empty($settings['mail.mailers.smtp.password']) + ? Crypt::decryptString($settings['mail.mailers.smtp.password']) + : $defaultMailersSmtpVars['password'], + 'timeout' => $settings['mail.mailers.smtp.timeout'] ?? $defaultMailersSmtpVars['timeout'], + ]), + ], + 'from' => [ + 'address' => $settings['mail.from.address'] ?? config('mail.from.address'), + 'name' => $settings['mail.from.name'] ?? config('mail.from.name'), + ], + 'reply_to' => [ + 'method' => $settings['mail.reply_to.method'] ?? config('mail.reply_to.method'), + 'email' => $settings['mail.reply_to.email'] ?? config('mail.reply_to.email'), + 'name' => $settings['mail.reply_to.name'] ?? config('mail.reply_to.name'), + ], + ]; + }); + } + + /** + * Limpia la caché de configuración del sistema de correo. + * + * @return void + */ + public static function clearMailSystemConfigCache(): void + { + Cache::forget('mail_system_config'); + } + +} diff --git a/src/koneko-vuexy.module.php b/src/koneko-vuexy.module.php new file mode 100644 index 0000000..e9ee192 --- /dev/null +++ b/src/koneko-vuexy.module.php @@ -0,0 +1,187 @@ +<?php + +declare(strict_types=1); + +use Koneko\VuexyWebsiteAdmin\Application\Http\Middleware\{WebsiteContentMiddleware, WebsiteContextMiddleware, WebsiteTemplateMiddleware}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Analytics\GoogleAnalytics\GoogleAnalyticsCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Analytics\GoogleSearchConsole\GoogleSearchConsoleCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Analytics\GoogleTags\GoogleTagsCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Analytics\PixelMeta\PixelMetaCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Article\{BlogArticlesTable, BlogArticleOffcanvasForm}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Category\{BlogCategoriesTable, BlogCategoryOffcanvasForm}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Comment\{BlogCommentsTable, BlogCommentOffcanvasForm}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Blog\Tag\{BlogTagsTable, BlogTagOffcanvasForm}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Content\Faq\{FaqIndex, FaqOffcanvasForm}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Contact\Form\ContactFormCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Contact\Info\{ContactInfoCard, ContactLocationCard}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Content\Gallery\GalleryIndex; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Content\Legal\{LegalIndex, LegalOffCanvasForm}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Comunication\Messenger\MessengerCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Comunication\TawkTo\TawkToCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Comunication\Twitter\TwitterCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Comunication\Whatsapp\WhatsappCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Canonical\CanonicalIndex; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Jsonld\JsonldIndex; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Manifest\ManifestCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Sitemap\{SitemapIndex, SitemapUrlOffcanvasForm}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\SocialCards\SocialCardsIndex; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Seo\Robots\RobotsCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\General\{WebsiteDescriptionCard, WebsiteFaviconCard, LogoOnDarkBgCard, LogoOnLightBgCard}; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\Indexing\IndexingCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Settings\Social\SocialCard; +use Koneko\VuexyWebsiteAdmin\Application\UI\Livewire\Translate\Google\GoogleTanslateCard; +use Koneko\VuexyWebsiteAdmin\Console\Commands\{SitemapGenerate, WebsiteCacheHelperCommand, WebsiteContentHelperCommand, WebsiteMenuHelperCommand, WebsiteSeoHelperCommand}; + +return [ + // 🌐 Identidad del Módulo + 'name' => 'Website admin', + 'description' => 'Gestión de contenido del sitio web.', + 'type' => 'plugin', + 'tags' => ['koneko-official', 'website', 'admin', 'website-admin', 'website-content', 'website-management'], + + // ⚙️ Namespace de configuraciones Koneko Vuexy Admin + 'componentNamespace' => 'website-admin', + + // 🧠 Metadatos visuales para UI del gestor + 'ui' => [ + 'image' => 'resources/img/module-cover.png', + 'readme' => 'README.md', + ], + + // ⚙️ Archivos de configuración del módulo + 'configs' => [ + 'koneko.website' => 'config/koneko-website.php', + ], + + // 🏭 Proveedores de servicio, Middleware y Aliases (runtime) + 'middleware' => [ + 'website' => WebsiteTemplateMiddleware::class, + 'website-context' => WebsiteContextMiddleware::class, + 'website-content' => WebsiteContentMiddleware::class, + ], + + // 📦 migraciones + 'migrations' => [ + 'database/migrations', + ], + + // 🗺️ Rutas + + 'routes' => [ + [ + 'middleware' => ['web', 'auth', 'admin'], + 'paths' => [ + 'routes/koneko_website_admin.php', + 'routes/koneko_website_cms.php', + 'routes/koneko_website_blog.php', + ], + ], + [ + 'middleware' => ['web', 'website-context', 'website-content'], + 'paths' => [ + 'routes/koneko_website_sites.php', + ], + ], + ], + + // 🗂️ Vistas, traducciones + 'views' => [ + 'vuexy-website-admin' => 'resources/views', + ], + + // 🧩 Componentes Blade y Livewire + 'livewire' => [ + 'vuexy-website-admin' => [ + // ajustes generales + 'website-description-card' => WebsiteDescriptionCard::class, + 'website-favicon-card' => WebsiteFaviconCard::class, + 'logo-on-light-bg-card' => LogoOnLightBgCard::class, + 'logo-on-dark-bg-card' => LogoOnDarkBgCard::class, + + // Enlaces Redes sociales + 'social-card' => SocialCard::class, + + // Visibilidad en buscadores + 'indexing-card' => IndexingCard::class, + + // Información de contacto + 'contact-info-card' => ContactInfoCard::class, + 'contact-location-card' => ContactLocationCard::class, + + // Formulario de contacto + 'contact-form-card' => ContactFormCard::class, + + // Analítica y seguimiento + 'google-analytics-card' => GoogleAnalyticsCard::class, + 'google-tags-card' => GoogleTagsCard::class, + 'google-search-console-card' => GoogleSearchConsoleCard::class, + 'pixel-meta-card' => PixelMetaCard::class, + + // Chat & Comunicación + 'messenger-card' => MessengerCard::class, + 'whatsapp-card' => WhatsappCard::class, + 'tawk-to-card' => TawkToCard::class, + 'twitter-card' => TwitterCard::class, + + // Traducciones e internacional + 'google-tanslate-card' => GoogleTanslateCard::class, + + // Preguntas frecuentes + 'faq-index' => FaqIndex::class, + 'faq-offcanvas-form' => FaqOffcanvasForm::class, + + // Galería de imágenes + 'gallery-index' => GalleryIndex::class, + + // Avisos legales + 'legal-index' => LegalIndex::class, + 'legal-offcanvas-form' => LegalOffCanvasForm::class, + + // Herramientas SEO + 'sitemap-index' => SitemapIndex::class, + 'sitemap-offcanvas-form' => SitemapUrlOffcanvasForm::class, + 'jsonld-index' => JsonldIndex::class, + 'robots-card' => RobotsCard::class, + 'manifest-card' => ManifestCard::class, + 'canonical-index' => CanonicalIndex::class, + 'social-cards-index' => SocialCardsIndex::class, + + // Blog + 'blog-articles-table' => BlogArticlesTable::class, + 'blog-article-offcanvas-form' => BlogArticleOffcanvasForm::class, + 'blog-categories-table' => BlogCategoriesTable::class, + 'blog-category-offcanvas-form' => BlogCategoryOffcanvasForm::class, + 'blog-tags-table' => BlogTagsTable::class, + 'blog-tag-offcanvas-form' => BlogTagOffcanvasForm::class, + 'blog-comments-table' => BlogCommentsTable::class, + 'blog-comment-offcanvas-form' => BlogCommentOffcanvasForm::class, + ] + ], + + // 🛠 Comandos Artisan + 'commands' => [ + SitemapGenerate::class, + WebsiteMenuHelperCommand::class, + WebsiteContentHelperCommand::class, + WebsiteCacheHelperCommand::class, + WebsiteSeoHelperCommand::class, + ], + + // 🛡️ Configuración de roles y permisos (RBAC) + 'rbac' => [ + 'permissions_path' => 'database/rbac/permissions.json', + 'roles_path' => 'database/rbac/roles.json', + ], + + // 🔗 Registro de APIs disponibles en el módulo + 'apis' => [ + 'catalog_path' => 'config/vuexy_apis_catalog.php', + ], + + // 🧠 Extensiones + 'extensions' => [ + 'menu' => [ + 'path' => 'config/vuexy_website_admin_menu.php', + ], + ], +];