From aa938a3cab24394cebc9f24d8926f501bcb5f779 Mon Sep 17 00:00:00 2001 From: Arturo Corro Date: Wed, 5 Mar 2025 20:43:35 -0600 Subject: [PATCH] first commit --- .editorconfig | 18 + .gitattributes | 38 + .gitignore | 10 + .prettierignore | 16 + .prettierrc.json | 29 + CHANGELOG.md | 39 + CONTRIBUTING.md | 9 + Http/Controllers/CompanyController.php | 24 + Http/Controllers/StoreController.php | 158 ++++ Http/Controllers/WorkCenterController.php | 142 ++++ LICENSE | 9 + Livewire/Company/CompanyIndex.php | 269 ++++++ Livewire/Store/PostForm.php | 190 +++++ Livewire/Stores/StoreForm.php | 306 +++++++ Livewire/Stores/StoreIndex.php | 230 ++++++ Livewire/WorkCenters/WorkCenterIndex.php | 211 +++++ .../WorkCenters/WorkCenterOffcanvasForm.php | 202 +++++ Models/Currency.php | 40 + Models/CurrencyExchangeRate.php | 39 + Models/EmailTransaction.php | 61 ++ Models/Store.php | 160 ++++ Models/StoreUser.php | 16 + Models/StoreWorkCenter.php | 73 ++ .../VuexyStoreManagerServiceProvider.php | 56 ++ README.md | 133 +++ Services/StoreCatalogService.php | 117 +++ Traits/HasUsersRelations.php | 63 ++ composer.json | 36 + .../2024_12_15_110508_create_store_table.php | 76 ++ ...110511_create_store_work_centers_table.php | 54 ++ ...5_112793_create_store_user_roles_table.php | 38 + ...4_12_15_118085_create_currencie_tables.php | 63 ++ ...124251_create_email_transactions_table.php | 53 ++ database/seeders/CurrencySeeder.php | 85 ++ .../bootstrap-table/satCatalogsFormatters.js | 104 +++ resources/views/company/index.blade.php | 20 + .../views/livewire/company/index.blade.php | 775 ++++++++++++++++++ .../views/livewire/stores/form.blade.php | 170 ++++ .../views/livewire/stores/index.blade.php | 7 + .../views/livewire/work-center/form.blade.php | 118 +++ .../livewire/work-center/index.blade.php | 12 + resources/views/store/crud.blade.php | 24 + resources/views/store/index.blade.php | 25 + resources/views/store/show.blade.php | 9 + resources/views/work-center/index.blade.php | 26 + resources/views/work-center/show.blade.php | 9 + routes/admin.php | 26 + 47 files changed, 4388 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Http/Controllers/CompanyController.php create mode 100644 Http/Controllers/StoreController.php create mode 100644 Http/Controllers/WorkCenterController.php create mode 100644 LICENSE create mode 100644 Livewire/Company/CompanyIndex.php create mode 100644 Livewire/Store/PostForm.php create mode 100644 Livewire/Stores/StoreForm.php create mode 100644 Livewire/Stores/StoreIndex.php create mode 100644 Livewire/WorkCenters/WorkCenterIndex.php create mode 100644 Livewire/WorkCenters/WorkCenterOffcanvasForm.php create mode 100644 Models/Currency.php create mode 100644 Models/CurrencyExchangeRate.php create mode 100644 Models/EmailTransaction.php create mode 100644 Models/Store.php create mode 100644 Models/StoreUser.php create mode 100644 Models/StoreWorkCenter.php create mode 100644 Providers/VuexyStoreManagerServiceProvider.php create mode 100644 README.md create mode 100644 Services/StoreCatalogService.php create mode 100644 Traits/HasUsersRelations.php create mode 100644 composer.json create mode 100644 database/migrations/2024_12_15_110508_create_store_table.php create mode 100644 database/migrations/2024_12_15_110511_create_store_work_centers_table.php create mode 100644 database/migrations/2024_12_15_112793_create_store_user_roles_table.php create mode 100644 database/migrations/2024_12_15_118085_create_currencie_tables.php create mode 100644 database/migrations/2024_12_15_124251_create_email_transactions_table.php create mode 100644 database/seeders/CurrencySeeder.php create mode 100644 resources/assets/js/bootstrap-table/satCatalogsFormatters.js create mode 100644 resources/views/company/index.blade.php create mode 100644 resources/views/livewire/company/index.blade.php create mode 100644 resources/views/livewire/stores/form.blade.php create mode 100644 resources/views/livewire/stores/index.blade.php create mode 100644 resources/views/livewire/work-center/form.blade.php create mode 100644 resources/views/livewire/work-center/index.blade.php create mode 100644 resources/views/store/crud.blade.php create mode 100644 resources/views/store/index.blade.php create mode 100644 resources/views/store/show.blade.php create mode 100644 resources/views/work-center/index.blade.php create mode 100644 resources/views/work-center/show.blade.php create mode 100644 routes/admin.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7333620 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,38 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore + +# Ignorar archivos de configuración y herramientas de desarrollo +.editorconfig export-ignore +.prettierrc.json export-ignore +.prettierignore export-ignore +.eslintrc.json export-ignore + +# Ignorar node_modules y dependencias locales +node_modules/ export-ignore +vendor/ export-ignore + +# Ignorar archivos de build +npm-debug.log export-ignore + +# Ignorar carpetas de logs y caché +storage/logs/ export-ignore +storage/framework/ export-ignore + +# Ignorar carpetas de compilación de frontend +public/build/ export-ignore +dist/ export-ignore + +# Ignorar archivos de CI/CD +.github/ export-ignore +.gitlab-ci.yml export-ignore +.vscode/ export-ignore +.idea/ export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d07bec2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/node_modules +/vendor +/.vscode +/.nova +/.fleet +/.phpactor.json +/.phpunit.cache +/.phpunit.result.cache +/.zed +/.idea diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5d3dfee --- /dev/null +++ b/.prettierignore @@ -0,0 +1,16 @@ +# Dependencias de Composer y Node.js +/vendor/ +/node_modules/ + +# Caché y logs +/storage/ +*.log +*.cache + +# Archivos del sistema +.DS_Store +Thumbs.db + +# Configuración de editores +.idea/ +.vscode/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..5f11c9c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,29 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "bracketSameLine": true, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": true, + "printWidth": 120, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "none", + "useTabs": false, + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto", + "overrides": [ + { + "files": [ + "resources/assets/**/*.js" + ], + "options": { + "semi": false + } + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dc533a5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# 📜 CHANGELOG - Laravel Vuexy Store Manager + +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.]. + +--- + +## 📅 Próximos Cambios Planeados +- 📊 **Mejoras en [feature futuro]**. +- 🏪 **Compatibilidad con [Laravel 11, Vuexy, etc.]**. +- 📍 **Integración con [API o funcionalidad esperada]**. + +--- + +**📌 Nota:** Esta es una versión **ALPHA**, aún en desarrollo. + +--- + +## 🔄 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-store-manager)** 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**. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8c09717 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +## 🔐 Acceso al Repositorio Privado + +Nuestro servidor Git en **Tea** tiene un registro cerrado. Para contribuir: + +1. Abre un **Issue** en [GitHub](https://github.com/koneko-mx/laravel-vuexy-store-manager/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**. diff --git a/Http/Controllers/CompanyController.php b/Http/Controllers/CompanyController.php new file mode 100644 index 0000000..391ac50 --- /dev/null +++ b/Http/Controllers/CompanyController.php @@ -0,0 +1,24 @@ +ajax()) { + $bootstrapTableIndexConfig = [ + 'table' => 'stores', + 'columns' => [ + 'stores.id', + 'stores.code', + 'stores.name', + 'stores.description', + 'stores.c_codigo_postal AS codigo_postal', + 'sat_pais.descripcion AS pais', + DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS manager_name"), + 'users.email AS manager_email', + 'sat_estado.nombre_del_estado AS estado', + 'sat_localidad.descripcion AS localidad', + 'sat_municipio.descripcion AS municipio', + 'sat_colonia.nombre_del_asentamiento AS colonia', + DB::raw("CONCAT_WS(' ', COALESCE(stores.direccion, ''), COALESCE(stores.num_ext, ''), IF(stores.num_int IS NOT NULL, CONCAT('Int ', stores.num_int), '')) AS direccion"), + 'stores.lat', + 'stores.lng', + 'stores.email', + 'stores.tel', + 'stores.tel2', + 'stores.rfc', + 'stores.nombre_fiscal', + 'sat_regimen_fiscal.descripcion AS regimen_fiscal', + 'stores.domicilio_fiscal', + 'stores.show_on_website', + 'stores.enable_ecommerce', + 'stores.status', + 'stores.created_at', + 'stores.updated_at', + ], + 'joins' => [ + [ + 'table' => 'sat_pais', + 'first' => 'stores.c_pais', + 'second' => 'sat_pais.c_pais', + 'type' => 'leftJoin', + ], + [ + 'table' => 'sat_estado', + 'first' => 'stores.c_estado', + 'second' => 'sat_estado.c_estado', + 'type' => 'leftJoin', + 'and' => [ + 'stores.c_pais = sat_estado.c_pais', + ], + ], + [ + 'table' => 'sat_localidad', + 'first' => 'stores.c_localidad', + 'second' => 'sat_localidad.c_localidad', + 'type' => 'leftJoin', + 'and' => [ + 'stores.c_estado = sat_localidad.c_estado', + ], + ], + [ + 'table' => 'sat_municipio', + 'first' => 'stores.c_municipio', + 'second' => 'sat_municipio.c_municipio', + 'type' => 'leftJoin', + 'and' => [ + 'stores.c_estado = sat_municipio.c_estado', + ], + ], + [ + 'table' => 'sat_colonia', + 'first' => 'stores.c_colonia', + 'second' => 'sat_colonia.c_colonia', + 'type' => 'leftJoin', + 'and' => [ + 'stores.c_codigo_postal = sat_colonia.c_codigo_postal', + ], + ], + [ + 'table' => 'sat_regimen_fiscal', + 'first' => 'stores.c_regimen_fiscal', + 'second' => 'sat_regimen_fiscal.c_regimen_fiscal', + 'type' => 'leftJoin', + ], + [ + 'table' => 'users', + 'first' => 'stores.manager_id', + 'second' => 'users.id', + 'type' => 'leftJoin', + ], + ], + 'filters' => [ + 'search' => ['stores.name', 'stores.code', 'users.name', 'users.email'], // Búsqueda por nombre, código o manager + ], + 'sort_column' => 'stores.name', // Ordenamiento por defecto + 'default_sort_order' => 'asc', // Orden ascendente por defecto + ]; + + return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson(); + } + + return view('vuexy-store-manager::store.index'); + } + + /** + * Show the crud for creating a new resource. + */ + public function create() + { + return view('vuexy-store-manager::store.crud') + ->with('mode', 'create') + ->with('store', null); + } + + /** + * Display the specified resource. + */ + public function show(Store $store) + { + return view('vuexy-store-manager::store.crud', compact('store')); + } + + /** + * Show the crud for editing the specified resource. + */ + public function edit(Store $store) + { + //$store = Store::findOrFail($id); + + + + return view('vuexy-store-manager::store.crud', compact('store'))->with('mode', 'edit'); + } + + /** + * Show the crud for editing the specified resource. + */ + public function delete(Store $store) + { + return view('vuexy-store-manager::store.crud', compact('store'))->with('mode', 'delete'); + + } + +} diff --git a/Http/Controllers/WorkCenterController.php b/Http/Controllers/WorkCenterController.php new file mode 100644 index 0000000..86e4eee --- /dev/null +++ b/Http/Controllers/WorkCenterController.php @@ -0,0 +1,142 @@ +ajax()) { + $bootstrapTableIndexConfig = [ + 'table' => 'store_work_centers', + 'columns' => [ + 'store_work_centers.id', + 'stores.code AS stores_code', + 'stores.name AS stores_name', + 'store_work_centers.code', + 'store_work_centers.name', + 'store_work_centers.description', + 'store_work_centers.manager_id', + DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS manager_name"), + 'users.email AS manager_email', + 'store_work_centers.tel', + 'store_work_centers.tel2', + 'stores.c_codigo_postal AS codigo_postal', + 'sat_pais.descripcion AS pais', + 'sat_estado.nombre_del_estado AS estado', + 'sat_localidad.descripcion AS localidad', + 'sat_municipio.descripcion AS municipio', + 'sat_colonia.nombre_del_asentamiento AS colonia', + DB::raw("CONCAT_WS(' ', COALESCE(stores.direccion, ''), COALESCE(stores.num_ext, ''), IF(stores.num_int IS NOT NULL, CONCAT('Int ', stores.num_int), '')) AS direccion"), + 'store_work_centers.lat', + 'store_work_centers.lng', + 'store_work_centers.status', + 'store_work_centers.created_at', + 'store_work_centers.updated_at', + ], + 'joins' => [ + [ + 'table' => 'stores', + 'first' => 'store_work_centers.store_id', + 'second' => 'stores.id', + 'type' => 'join', // INNER JOIN + ], + [ + 'table' => 'sat_pais', + 'first' => 'stores.c_pais', + 'second' => 'sat_pais.c_pais', + 'type' => 'leftJoin', // LEFT OUTER JOIN + ], + [ + 'table' => 'sat_estado', + 'first' => 'stores.c_estado', + 'second' => 'sat_estado.c_estado', + 'and' => [ + 'stores.c_pais = sat_estado.c_pais', + ], + 'type' => 'leftJoin', + ], + [ + 'table' => 'sat_localidad', + 'first' => 'stores.c_localidad', + 'second' => 'sat_localidad.c_localidad', + 'and' => [ + 'stores.c_estado = sat_localidad.c_estado', + ], + 'type' => 'leftJoin', + ], + [ + 'table' => 'sat_municipio', + 'first' => 'stores.c_municipio', + 'second' => 'sat_municipio.c_municipio', + 'and' => [ + 'stores.c_estado = sat_municipio.c_estado', + ], + 'type' => 'leftJoin', + ], + [ + 'table' => 'sat_colonia', + 'first' => 'stores.c_colonia', + 'second' => 'sat_colonia.c_colonia', + 'and' => [ + 'stores.c_codigo_postal = sat_colonia.c_codigo_postal', + ], + 'type' => 'leftJoin', + ], + [ + 'table' => 'users', + 'first' => 'store_work_centers.manager_id', + 'second' => 'users.id', + 'type' => 'leftJoin', + ], + ], + 'filters' => [ + 'search' => [ + 'store_work_centers.code', + 'store_work_centers.name', + 'stores.code', + 'stores.name', + ], + ], + 'sort_column' => 'store_work_centers.name', // Columna por defecto para ordenamiento + 'default_sort_order' => 'asc', // Orden por defecto + ]; + + return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson(); + } + + return view('vuexy-store-manager::work-center.index'); + } + + public function ajax(\Illuminate\Http\Request $request) + { + $options = [ + 'id' => $request->input('id', null), + 'searchTerm' => $request->input('searchTerm', null), + 'limit' => $request->input('limit', 20), + 'keyField' => 'id', + 'valueField' => 'custom_name', // Usamos un alias que agregaremos con DB::raw() + 'responseType' => $request->input('responseType', 'select2'), + 'filters' => [ + 'store_id' => $request->input('store_id', null), + ] + ]; + + // Aquí añadimos la expresión de concatenación con un alias + $query = StoreWorkCenter::query() + ->select('store_work_centers.*', DB::raw("CONCAT_WS(' - ', code, name) AS custom_name")); + + return CatalogHelper::ajaxFlexibleResponse($query, $options); + } + +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..486d340 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +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: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. diff --git a/Livewire/Company/CompanyIndex.php b/Livewire/Company/CompanyIndex.php new file mode 100644 index 0000000..62c0c7a --- /dev/null +++ b/Livewire/Company/CompanyIndex.php @@ -0,0 +1,269 @@ + 'loadStore', + 'confirmDeletionStore' => 'loadStoreForDeletion', + ]; + + + public function mount(String $mode = 'create', Store $store = null) + { + $this->mode = $mode; + + $this->loadData($store); + $this->loadOptions(); + } + + private function loadData(Store $store) + { + switch($this->mode){ + case 'create': + $this->btnSubmitText = 'Crear sucursal'; + + $this->c_pais = 'MEX'; + $this->status = true; + break; + + case 'edit': + + $this->btnSubmitText = 'Guardar cambios'; + break; + + case 'delete': + $this->btnSubmitText = 'Eliminar sucursal'; + break; + } + + if($store){ + $this->storeId = $store->id; + + $this->code = $store->code; + $this->name = $store->name; + $this->description = $store->description; + $this->manager_id = $store->manager_id; + $this->rfc = $store->rfc; + $this->nombre_fiscal = $store->nombre_fiscal; + $this->c_regimen_fiscal = $store->c_regimen_fiscal; + $this->domicilio_fiscal = $store->domicilio_fiscal; + $this->serie_ingresos = $store->serie_ingresos; + $this->serie_egresos = $store->serie_egresos; + $this->serie_pagos = $store->serie_pagos; + $this->c_codigo_postal = $store->c_codigo_postal; + $this->c_pais = $store->c_pais; + $this->c_estado = $store->c_estado; + $this->c_localidad = $store->c_localidad; + $this->c_municipio = $store->c_municipio; + $this->c_colonia = $store->c_colonia; + $this->direccion = $store->direccion; + $this->num_ext = $store->num_ext; + $this->num_int = $store->num_int; + $this->email = $store->email; + $this->tel = $store->tel; + $this->tel2 = $store->tel2; + $this->lat = $store->lat; + $this->lng = $store->lng; + $this->status = $store->status; + $this->show_on_website = $store->show_on_website; + $this->enable_ecommerce = $store->enable_ecommerce; + } + } + + private function loadOptions() + { + $this->manager_id_options = DB::table('users') + ->select('id', DB::raw("CONCAT(CONCAT_WS(' ', name, last_name), ' - ', email) as full_name")) + //->where('is_user', 1) + ->orderBy('full_name') + ->pluck('full_name', 'id'); + + $this->c_regimen_fiscal_options = RegimenFiscal::selectList(); + $this->c_pais_options = Pais::selectList(); + + if($this->mode !== 'create'){ + $this->c_estado_options = ['' => 'Seleccione el estado'] + Estado::selectList($this->c_pais)->toArray(); + $this->c_localidad_options = ['' => 'Seleccione la localidad'] + Localidad::selectList($this->c_estado)->toArray(); + $this->c_municipio_options = ['' => 'Seleccione el municipio'] + Municipio::selectList($this->c_estado, $this->c_municipio)->toArray(); + $this->c_colonia_options = ['' => 'Seleccione la colonia'] + Colonia::selectList($this->c_codigo_postal, $this->c_colonia)->toArray(); + } + } + + public function onSubmit() + { + if ($this->mode === 'delete') { + return $this->delete(); + } + + return $this->save(); + } + + private function save() + { + $validatedData = $this->validate([ + 'code' => 'required|string|max:16', + 'name' => 'required|string|max:96', + 'description' => 'nullable|string|max:1024', + + 'manager_id' => 'nullable|exists:users,id', + 'rfc' => 'nullable|string|max:13', + 'nombre_fiscal' => 'nullable|string|max:255', + 'c_regimen_fiscal' => 'nullable|integer', + 'domicilio_fiscal' => 'nullable|integer', + + 'c_pais' => 'nullable|string|max:3', + 'c_estado' => 'nullable|string|max:3', + 'c_municipio' => 'nullable|integer', + 'c_localidad' => 'nullable|integer', + 'c_codigo_postal' => 'nullable|integer', + 'c_colonia' => 'nullable|integer', + 'direccion' => 'nullable|string|max:255', + 'num_ext' => 'nullable|string|max:50', + 'num_int' => 'nullable|string|max:50', + 'lat' => 'nullable|numeric', + 'lng' => 'nullable|numeric', + + 'email' => 'nullable|email|max:96', + 'tel' => 'nullable|string|max:15', + 'tel2' => 'nullable|string|max:15', + + 'status' => 'nullable|boolean', + 'show_on_website' => 'nullable|boolean', + 'enable_ecommerce' => 'nullable|boolean', + ]); + + try { + $store = Store::updateOrCreate( + [ 'id' => $this->storeId ], // Si $this->storeId es null, creará un nuevo registro + [ + 'code' => $validatedData['code'], + 'name' => $validatedData['name'], + 'description' => $validatedData['description'] ?? null, + + 'manager_id' => $validatedData['manager_id'] ?? null, + 'rfc' => $validatedData['rfc'] ?? null, + 'nombre_fiscal' => $validatedData['nombre_fiscal'] ?? null, + 'c_regimen_fiscal' => $validatedData['c_regimen_fiscal'] ?? null, + 'domicilio_fiscal' => $validatedData['domicilio_fiscal'] ?? null, + + 'c_pais' => $validatedData['c_pais'] ?? null, + 'c_estado' => $validatedData['c_estado'] ?? null, + 'c_municipio' => $validatedData['c_municipio'] ?? null, + 'c_localidad' => $validatedData['c_localidad'] ?? null, + 'c_codigo_postal' => $validatedData['c_codigo_postal'] ?? null, + 'c_colonia' => $validatedData['c_colonia'] ?? null, + 'direccion' => $validatedData['direccion'] ?? null, + 'num_ext' => $validatedData['num_ext'] ?? null, + 'num_int' => $validatedData['num_int'] ?? null, + 'lat' => $validatedData['lat'] ?? null, + 'lng' => $validatedData['lng'] ?? null, + + 'email' => $validatedData['email'] ?? null, + 'tel' => $validatedData['tel'] ?? null, + 'tel2' => $validatedData['tel2'] ?? null, + + 'show_on_website' => (bool) $validatedData['show_on_website'], + 'enable_ecommerce' => (bool) $validatedData['enable_ecommerce'], + 'status' => (bool) $validatedData['status'], + ] + ); + + session()->flash('success', 'Sucursal guardada correctamente.'); + + return redirect()->route('admin.store-manager.stores.index'); + + } catch (QueryException $e) { + // Manejar un error específico de SQL/DB, por ejemplo duplicados, FK violation, etc. + // O podrías capturar \Exception para cualquier error genérico + session()->flash('error', 'Ocurrió un error al guardar la sucursal.'); + // Opcionalmente: loguear el error para depuración: + \Log::error($e->getMessage()); + + // Si no haces return, el método continuará, podrías redirigir o quedarte en la misma página + return; + } + + } + + public function delete() + { + if ($this->storeId) { + try { + Store::find($this->storeId)->delete(); + + session()->flash('warning', 'Sucursal eliminada correctamente.'); + + return redirect()->route('admin.store-manager.stores.index'); + + } catch (QueryException $e) { + // Manejar un error específico de SQL/DB, por ejemplo duplicados, FK violation, etc. + // O podrías capturar \Exception para cualquier error genérico + session()->flash('error', 'Ocurrió un error al eliminar la sucursal.'); + // Opcionalmente: loguear el error para depuración: + \Log::error($e->getMessage()); + + // Si no haces return, el método continuará, podrías redirigir o quedarte en la misma página + return; + } + } + } + + public function render() + { + return view('vuexy-store-manager::livewire.company.index'); + } + +} diff --git a/Livewire/Store/PostForm.php b/Livewire/Store/PostForm.php new file mode 100644 index 0000000..7fec845 --- /dev/null +++ b/Livewire/Store/PostForm.php @@ -0,0 +1,190 @@ + 'loadWarehouse', + 'confirmDeleteWarehouse' => 'loadWarehouseForDeletion', + ]; + + + public function mount() + { + $this->loadOptions(); + $this->resetForm(); + } + + private function loadOptions() + { + $this->store_options = DB::table('stores') + ->select('id', 'name') + ->orderBy('name') + ->pluck('name', 'id'); + + $this->workcenter_options = DB::table('store_work_centers') + ->select('id', 'name') + ->orderBy('name') + ->pluck('name', 'id'); + } + + public function loadWarehouse($id) + { + $warehouse = Warehouse::find($id); + + if ($warehouse) { + $this->fill($warehouse->only(['id', 'store_id', 'workcenter_id', 'code', 'name', 'description', 'is_active', 'is_default'])); + $this->form_title = "Editar: $warehouse->name"; + $this->mode = 'edit'; + + $this->dispatch('on-edit-warehouse-modal'); + } + } + + public function loadWarehouseForDeletion($id) + { + $warehouse = Warehouse::find($id); + + if ($warehouse) { + $this->fill($warehouse->only(['id', 'store_id', 'workcenter_id', 'code', 'name', 'description', 'is_active', 'is_default'])); + $this->form_title = "Eliminar: $warehouse->name"; + $this->mode = 'delete'; + + $this->dispatch('on-delete-warehouse-modal'); + } + } + + public function editWarehouse($id) + { + $warehouse = Warehouse::find($id); + + if ($warehouse) { + $this->form_title = 'Editar: ' . $warehouse->name; + $this->mode = 'edit'; + + $this->warehouseId = $warehouse->id; + $this->store_id = $warehouse->store_id; + $this->workcenter_id = $warehouse->workcenter_id; + $this->code = $warehouse->code; + $this->name = $warehouse->name; + $this->description = $warehouse->description; + $this->is_active = $warehouse->is_active; + $this->is_default = $warehouse->is_default; + + $this->dispatch('on-edit-warehouse-modal'); + } + } + + public function confirmDeleteWarehouse($id) + { + $warehouse = Warehouse::find($id); + + if ($warehouse) { + $this->form_title = 'Eliminar: ' . $warehouse->name; + $this->mode = 'delete'; + + $this->warehouseId = $warehouse->id; + $this->store_id = $warehouse->store_id; + $this->workcenter_id = $warehouse->workcenter_id; + $this->code = $warehouse->code; + $this->name = $warehouse->name; + $this->description = $warehouse->description; + $this->is_active = $warehouse->is_active; + $this->is_default = $warehouse->is_default; + + $this->dispatch('on-delete-warehouse-modal'); + } + } + + public function onSubmit() + { + if ($this->mode === 'delete') { + return $this->delete(); + } + + return $this->save(); + } + + private function save() + { + try { + $validatedData = $this->validate([ + 'store_id' => 'required', + 'code' => 'required|string|max:16', + 'name' => 'required|string|max:96', + 'description' => 'nullable|string|max:1024', + ]); + } catch (\Illuminate\Validation\ValidationException $e) { + $this->dispatch('on-failed-validation-warehouse-modal'); + $this->dispatch('warehouse-message', ['type' => 'danger', 'message' => 'Error en la validación']); + throw $e; + } + + Warehouse::updateOrCreate( + ['id' => $this->warehouseId], + [ + 'store_id' => $validatedData['store_id'], + 'workcenter_id' => $this->workcenter_id, + 'code' => $validatedData['code'], + 'name' => $validatedData['name'], + 'description' => $validatedData['description'] ?? null, + 'is_active' => (bool) $this->is_active, + 'is_default' => (bool) $this->is_default, + ] + ); + + $this->dispatch('warehouse-message', ['type' => 'success', 'message' => 'Almacén guardado correctamente']); + $this->dispatch('reload-warehouse-table'); + $this->dispatch('close-warehouse-modal'); + $this->resetForm(); + } + + public function delete() + { + if ($this->warehouseId) { + Warehouse::find($this->warehouseId)->delete(); + + $this->dispatch('warehouse-message', ['type' => 'warning', 'message' => 'Almacén eliminado']); + $this->dispatch('reload-warehouse-table'); + $this->dispatch('close-warehouse-modal'); + + $this->resetForm(); + } + } + + public function resetForm() + { + $this->reset(['warehouseId', 'store_id', 'workcenter_id', 'code', 'name', 'description', 'is_default', 'confirm_delete']); + + $this->form_title = 'Agregar almacén'; + $this->mode = 'create'; + $this->is_active = true; + } + + public function render() + { + return view('vuexy-warehouse::livewire.stores.form'); + } + +} diff --git a/Livewire/Stores/StoreForm.php b/Livewire/Stores/StoreForm.php new file mode 100644 index 0000000..9a35501 --- /dev/null +++ b/Livewire/Stores/StoreForm.php @@ -0,0 +1,306 @@ +id ?? null); + } + + /** + * Cargar opciones de formularios según el modo actual. + * + * @param string $mode + */ + private function loadOptions(string $mode): void + { + $this->manager_id_options = User::getUsersListWithInactive($this->manager_id, ['type' => 'user', 'status' => 1]); + $this->c_regimen_fiscal_options = RegimenFiscal::selectList(); + $this->c_pais_options = Pais::selectList(); + $this->c_estado_options = Estado::selectList($this->c_pais)->toArray(); + + if ($mode !== 'create') { + $this->c_localidad_options = Localidad::selectList($this->c_estado)->toArray(); + $this->c_municipio_options = Municipio::selectList($this->c_estado, $this->c_municipio)->toArray(); + $this->c_colonia_options = Colonia::selectList($this->c_codigo_postal, $this->c_colonia)->toArray(); + } + } + + // ===================== MÉTODOS OBLIGATORIOS ===================== + + /** + * Devuelve el modelo Eloquent asociado. + * + * @return string + */ + protected function model(): string + { + return Store::class; + } + + /** + * Reglas de validación dinámicas según el modo actual. + * + * @param string $mode + * @return array + */ + protected function dynamicRules(string $mode): array + { + switch ($mode) { + case 'create': + case 'edit': + return [ + 'code' => [ + 'required', 'string', 'alpha_num', 'max:16', + Rule::unique('stores', 'code')->ignore($this->id) + ], + 'name' => 'required|string|max:96', + 'description' => 'nullable|string|max:1024', + 'manager_id' => 'nullable|exists:users,id', + + // Información fiscal + 'rfc' => ['nullable', 'string', 'regex:/^([A-ZÑ&]{3,4})(\d{6})([A-Z\d]{3})$/i', 'max:13'], + 'nombre_fiscal' => 'nullable|string|max:255', + 'c_regimen_fiscal' => 'nullable|exists:sat_regimen_fiscal,c_regimen_fiscal', + 'domicilio_fiscal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal', + + // Ubicación + 'c_pais' => 'nullable|exists:sat_pais,c_pais|string|size:3', + 'c_estado' => 'nullable|exists:sat_estado,c_estado|string|min:2|max:3', + 'c_municipio' => 'nullable|exists:sat_municipio,c_municipio|integer', + 'c_localidad' => 'nullable|integer', + 'c_codigo_postal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal|integer', + 'c_colonia' => 'nullable|exists:sat_colonia,c_colonia|integer', + 'direccion' => 'nullable|string|max:255', + 'num_ext' => 'nullable|string|max:50', + 'num_int' => 'nullable|string|max:50', + 'lat' => 'nullable|numeric|between:-90,90', + 'lng' => 'nullable|numeric|between:-180,180', + + // Contacto + 'email' => ['nullable', 'email', 'required_if:enable_ecommerce,true'], + 'tel' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'], + 'tel2' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'], + + // Configuración web y estado + 'show_on_website' => 'nullable|boolean', + 'enable_ecommerce' => 'nullable|boolean', + 'status' => 'nullable|boolean', + ]; + + case 'delete': + return [ + 'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación + ]; + + default: + return []; + } + } + + /** + * Inicializa los datos del formulario en función del modo. + * + * @param Store|null $store + * @param string $mode + */ + protected function initializeFormData(mixed $store, string $mode): void + { + if ($store) { + $this->code = $store->code; + $this->name = $store->name; + $this->description = $store->description; + $this->manager_id = $store->manager_id; + $this->rfc = $store->rfc; + $this->nombre_fiscal = $store->nombre_fiscal; + $this->c_regimen_fiscal = $store->c_regimen_fiscal; + $this->domicilio_fiscal = $store->domicilio_fiscal; + $this->c_pais = $store->c_pais; + $this->c_estado = $store->c_estado; + $this->c_municipio = $store->c_municipio; + $this->c_localidad = $store->c_localidad; + $this->c_codigo_postal = $store->c_codigo_postal; + $this->c_colonia = $store->c_colonia; + $this->direccion = $store->direccion; + $this->num_ext = $store->num_ext; + $this->num_int = $store->num_int; + $this->lat = $store->lat; + $this->lng = $store->lng; + $this->email = $store->email; + $this->tel = $store->tel; + $this->tel2 = $store->tel2; + $this->show_on_website = (bool) $store->show_on_website; + $this->enable_ecommerce = (bool) $store->enable_ecommerce; + $this->status = (bool) $store->status; + + } else { + $this->c_pais = 'MEX'; + $this->status = true; + $this->show_on_website = false; + $this->enable_ecommerce = false; + } + + $this->loadOptions($mode); + } + + /** + * Prepara los datos validados para su almacenamiento. + * + * @param array $validatedData + * @return array + */ + protected function prepareData(array $validatedData): array + { + return [ + 'code' => $validatedData['code'], + 'name' => $validatedData['name'], + 'description' => strip_tags($validatedData['description']), + 'manager_id' => $validatedData['manager_id'], + 'rfc' => $validatedData['rfc'], + 'nombre_fiscal' => $validatedData['nombre_fiscal'], + 'c_regimen_fiscal' => $validatedData['c_regimen_fiscal'], + 'domicilio_fiscal' => $validatedData['domicilio_fiscal'], + 'c_codigo_postal' => $validatedData['c_codigo_postal'], + 'c_pais' => $validatedData['c_pais'], + 'c_estado' => $validatedData['c_estado'], + 'c_localidad' => $validatedData['c_localidad'], + 'c_municipio' => $validatedData['c_municipio'], + 'c_colonia' => $validatedData['c_colonia'], + 'direccion' => $validatedData['direccion'], + 'num_ext' => $validatedData['num_ext'], + 'num_int' => $validatedData['num_int'], + 'email' => $validatedData['email'], + 'tel' => $validatedData['tel'], + 'tel2' => $validatedData['tel2'], + 'lat' => $validatedData['lat'], + 'lng' => $validatedData['lng'], + 'status' => $validatedData['status'], + 'show_on_website' => $validatedData['show_on_website'], + 'enable_ecommerce' => $validatedData['enable_ecommerce'], + ]; + } + + /** + * Definición de los contenedores de notificación. + * + * @return array + */ + protected function targetNotifies(): array + { + return [ + "index" => "#bt-stores .notification-container", + "form" => "#store-form .notification-container", + ]; + } + + /** + * Ruta de vista asociada al formulario. + * + * @return \Illuminate\Contracts\View\View + */ + protected function viewPath(): string + { + return 'vuexy-store-manager::livewire.stores.form'; + } + + // ===================== VALIDACIONES ===================== + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'code' => 'código de sucursal', + 'name' => 'nombre de la sucursal', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'code.required' => 'El código de la sucursal es obligatorio.', + 'code.unique' => 'Este código ya está en uso por otra sucursal.', + 'name.required' => 'El nombre de la sucursal es obligatorio.', + ]; + } + + // ===================== PREPARACIÓN DE DATOS ===================== + + // ===================== NOTIFICACIONES Y EVENTOS ===================== + + /** + * Definición de los eventos del componente. + * + * @return array + */ + protected function dispatches(): array + { + return [ + 'on-failed-validation' => 'on-failed-validation-store', + 'on-hydrate' => 'on-hydrate-store-modal', + ]; + } + + // ===================== REDIRECCIÓN ===================== + + /** + * Define la ruta de redirección tras guardar o eliminar. + * + * @return string + */ + protected function getRedirectRoute(): string + { + return 'admin.store-manager.stores.index'; + } + +} diff --git a/Livewire/Stores/StoreIndex.php b/Livewire/Stores/StoreIndex.php new file mode 100644 index 0000000..3ffc2ea --- /dev/null +++ b/Livewire/Stores/StoreIndex.php @@ -0,0 +1,230 @@ + 'Acciones', + 'code' => 'Código', + 'name' => 'Nombre de la tienda', + 'description' => 'Descripción', + 'manager_name' => 'Gerente', + 'pais' => 'País', + 'estado' => 'Estado', + 'localidad' => 'Localidad', + 'municipio' => 'Municipio', + 'codigo_postal' => 'Código Postal', + 'colonia' => 'Colonia', + 'direccion' => 'Dirección', + 'lat' => 'Latitud', + 'lng' => 'Longitud', + 'email' => 'Correo de la tienda', + 'tel' => 'Teléfono', + 'tel2' => 'Teléfono Alternativo', + 'rfc' => 'RFC', + 'nombre_fiscal' => 'Nombre Fiscal', + 'regimen_fiscal' => 'Régimen Fiscal', + 'domicilio_fiscal' => 'Domicilio Fiscal', + 'show_on_website' => 'Visible en Sitio Web', + 'enable_ecommerce' => 'eCommerce', + 'status' => 'Estatus', + 'created_at' => 'Creada', + 'updated_at' => 'Modificada', + ]; + } + + /** + * Retorna el formato (formatter) para cada columna (similar a 'bt_datatable.format'). + * + * @return array + */ + protected function format(): array + { + return [ + 'action' => [ + 'formatter' => 'storeActionFormatter', + 'onlyFormatter' => true, + ], + 'code' => [ + 'formatter' => [ + 'name' => 'dynamicBadgeFormatter', + 'params' => ['color' => 'secondary'], + ], + 'align' => 'center', + 'switchable' => false, + ], + 'name' => [ + 'switchable' => false, + ], + 'description' => [ + 'visible' => false, + ], + 'codigo_postal' => [ + 'align' => 'center', + 'visible' => false, + ], + 'manager_name' => [ + 'formatter' => 'managerFormatter', + 'visible' => true, + ], + 'pais' => [ + 'align' => 'center', + ], + 'estado' => [ + 'formatter' => 'textNowrapFormatter', + 'align' => 'center', + ], + 'localidad' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'municipio' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + // la segunda definición de 'codigo_postal' la omites, pues ya está arriba + 'colonia' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'direccion' => [ + 'formatter' => 'direccionFormatter', + 'visible' => false, + ], + 'lat' => [ + 'align' => 'center', + 'visible' => false, + ], + 'lng' => [ + 'align' => 'center', + 'visible' => false, + ], + 'email' => [ + 'visible' => true, + 'formatter' => 'emailFormatter', + ], + 'tel' => [ + 'formatter' => 'telFormatter', + ], + 'tel2' => [ + 'formatter' => 'telFormatter', + 'visible' => false, + ], + 'rfc' => [ + 'align' => 'center', + 'visible' => false, + ], + 'nombre_fiscal' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'regimen_fiscal' => [ + 'visible' => false, + ], + 'domicilio_fiscal' => [ + 'align' => 'center', + 'visible' => false, + ], + 'show_on_website' => [ + 'formatter' => 'dynamicBooleanFormatter', + 'align' => 'center', + ], + 'enable_ecommerce' => [ + 'formatter' => 'dynamicBooleanFormatter', + 'align' => 'center', + ], + '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, + ], + ]; + } + + /** + * Sobrescribe la config base de la tabla para inyectar + * tus valores (similar a 'bt_datatable'). + * + * @return array + */ + protected function bootstraptableConfig(): array + { + // Llamamos al padre y reemplazamos/ajustamos lo que necesitemos. + return array_merge(parent::bootstraptableConfig(), [ + 'sortName' => 'code', + 'exportFileName' => 'Tiendas', + 'showFullscreen' => false, + 'showPaginationSwitch'=> false, + 'showRefresh' => false, + 'pagination' => false, + ]); + } + + /** + * Montamos el componente (ajustando rutas o algo adicional), + * y llamamos al parent::mount() para que se configure la tabla. + */ + public function mount(): void + { + parent::mount(); + + // Definimos las rutas específicas de este componente + $this->routes = [ + 'admin.store-manager.stores.edit' => route('admin.store-manager.stores.edit', ['store' => ':id']), + 'admin.store-manager.stores.delete' => route('admin.store-manager.stores.delete', ['store' => ':id']), + ]; + } + + /** + * Retorna la vista a renderizar por este componente. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-store-manager::livewire.stores.index'; + } +} diff --git a/Livewire/WorkCenters/WorkCenterIndex.php b/Livewire/WorkCenters/WorkCenterIndex.php new file mode 100644 index 0000000..70ae132 --- /dev/null +++ b/Livewire/WorkCenters/WorkCenterIndex.php @@ -0,0 +1,211 @@ +storeOptions = $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]); + } + + /** + * Indica la clase (o instancia) de tu modelo. + * + * @return string + */ + protected function model(): string + { + // Retornamos la clase del modelo + return StoreWorkCenter::class; + } + + /** + * Retorna las columnas (header) de tu tabla. + * + * @return array + */ + protected function columns(): array + { + return [ + 'action' => 'Acciones', + 'stores_code' => 'Código de Tienda', + 'stores_name' => 'Nombre de la Tienda', + 'code' => 'Código del Centro', + 'name' => 'Nombre del Centro', + 'description' => 'Descripción', + 'manager_name' => 'Gerente', + 'tel' => 'Teléfono', + 'tel2' => 'Teléfono Alternativo', + 'codigo_postal' => 'Código Postal', + 'pais' => 'País', + 'estado' => 'Estado', + 'localidad' => 'Localidad', + 'municipio' => 'Municipio', + 'colonia' => 'Colonia', + 'direccion' => 'Dirección', + 'lat' => 'Latitud', + 'lng' => 'Longitud', + 'status' => 'Estatus', + 'created_at' => 'Creado', + 'updated_at' => 'Actualizado', + ]; + } + + /** + * Retorna el formato (formatter) de las columnas. + * + * @return array + */ + protected function format(): array + { + return [ + 'action' => [ + 'formatter' => 'workCenterActionFormatter', + 'onlyFormatter' => true, + ], + 'stores_code' => [ + 'formatter' => [ + 'name' => 'dynamicBadgeFormatter', + 'params' => ['color' => 'secondary'], + ], + 'align' => 'center', + ], + 'stores_name' => [ + 'visible' => false, + ], + 'code' => [ + 'formatter' => [ + 'name' => 'dynamicBadgeFormatter', + 'params' => ['color' => 'secondary'], + ], + 'align' => 'center', + 'switchable' => false, + ], + 'name' => [ + 'switchable' => false, + ], + 'description' => [ + 'visible' => false, + ], + 'manager_name' => [ + 'formatter' => 'managerFormatter', + ], + 'tel' => [ + 'formatter' => 'telFormatter', + 'align' => 'center', + ], + 'tel2' => [ + 'formatter' => 'telFormatter', + 'align' => 'center', + 'visible' => false, + ], + 'pais' => [ + 'align' => 'center', + 'visible' => false, + ], + 'estado' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'localidad' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'municipio' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'codigo_postal' => [ + 'align' => 'center', + 'visible' => false, + ], + 'colonia' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'direccion' => [ + 'formatter' => 'direccionFormatter', + 'visible' => false, + ], + 'lat' => [ + 'align' => 'center', + 'visible' => false, + ], + 'lng' => [ + 'align' => 'center', + 'visible' => false, + ], + '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, + ], + ]; + } + + /** + * Sobrescribe la config base para adaptarla al caso de los Centros de Trabajo. + * + * @return array + */ + protected function bootstraptableConfig(): array + { + // Llamamos al padre y luego ajustamos lo que necesitemos. + return array_merge(parent::bootstraptableConfig(), [ + 'sortName' => 'code', + 'exportFileName' => 'Centros de Trabajo', + 'showFullscreen' => false, + 'showPaginationSwitch'=> false, + 'showRefresh' => false, + 'pagination' => false, + ]); + } + + /** + * Retorna la vista que se usará para renderizar este componente. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-store-manager::livewire.work-center.index'; + } +} diff --git a/Livewire/WorkCenters/WorkCenterOffcanvasForm.php b/Livewire/WorkCenters/WorkCenterOffcanvasForm.php new file mode 100644 index 0000000..bb888b7 --- /dev/null +++ b/Livewire/WorkCenters/WorkCenterOffcanvasForm.php @@ -0,0 +1,202 @@ + 'loadFormModel', + 'confirmDeletionWorkCenter' => 'loadFormModelForDeletion', + ]; + + /** + * Definición de tipos de datos que se deben castear. + * + * @var array + */ + protected $casts = [ + // + 'status' => 'boolean', + ]; + + /** + * Devuelve el modelo relacionado con el formulario. + * + * @return string + */ + protected function model(): string + { + return StoreWorkCenter::class; + } + + /** + * Define los campos del formulario. + * + * @return array + */ + protected function fields(): array + { + return (new StoreWorkCenter())->getFillable(); + } + + /** + * Valores por defecto para el formulario. + * + * @return array + */ + protected function defaults(): array + { + return [ + 'status' => true, + ]; + } + + /** + * Campo que se debe enfocar cuando se abra el formulario. + * + * @return string + */ + protected function focusOnOpen(): string + { + return 'code'; + } + + /** + * Define reglas de validación dinámicas basadas en el modo actual. + * + * @param string $mode El modo actual del formulario ('create', 'edit', 'delete'). + * @return array + */ + protected function dynamicRules(string $mode): array + { + switch ($mode) { + case 'create': + case 'edit': + return [ + 'store_id' => ['required', 'integer', 'exists:stores,id'], + 'code' => [ + 'required', 'string', 'max:16', + 'regex:/^[a-zA-Z0-9_-]+$/', // Solo alfanuméricos, guiones y guiones bajos + Rule::unique('store_work_centers', 'code')->ignore($this->id), + ], + 'name' => [ + 'required', 'string', 'max:96', + 'regex:/^[a-zA-ZáéíóúÁÉÍÓÚñÑ0-9\s-]+$/', // Solo letras, números y espacios + Rule::unique('store_work_centers') + ->where(fn ($query) => $query->where('store_id', $this->store_id)) + ->ignore($this->id), + ], + 'description' => ['nullable', 'string', 'max:1024'], + 'manager_id' => ['nullable', 'integer', 'exists:users,id'], + 'tel' => ['nullable', 'regex:/^\+?[0-9()\s-]+$/', 'max:20'], // Permitir formatos internacionales + 'tel2' => ['nullable', 'regex:/^\+?[0-9()\s-]+$/', 'max:20'], + 'lat' => ['nullable', 'numeric', 'between:-90,90'], + 'lng' => ['nullable', 'numeric', 'between:-180,180'], + 'status' => ['nullable', 'boolean'] + ]; + + case 'delete': + return [ + 'confirmDeletion' => 'accepted', // Confirma que el usuario acepta eliminar + ]; + + default: + return []; + } + } + + // ===================== VALIDACIONES ===================== + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + protected function attributes(): array + { + return [ + 'store_id' => 'negocio', + 'code' => 'código del centro de trabajo', + 'name' => 'nombre del centro de trabajo', + 'tel' => 'teléfono', + 'tel2' => 'teléfono alternativo', + 'lat' => 'latitud', + 'lng' => 'longitud', + 'status' => 'estado', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + protected function messages(): array + { + return [ + 'store_id.required' => 'El centro de trabajo debe estar asociado a un negocio.', + 'code.required' => 'El código del centro de trabajo es obligatorio.', + 'code.unique' => 'Este código ya está en uso por otro centro de trabajo.', + 'name.required' => 'El nombre del centro de trabajo es obligatorio.', + 'name.unique' => 'Ya existe un centro de trabajo con este nombre en este negocio.', + 'name.regex' => 'El nombre solo puede contener letras, números, espacios y guiones.', + ]; + } + + + /** + * Define las opciones de los selectores desplegables. + * + * @return array + */ + protected function options(): array + { + $storeCatalogService = app(StoreCatalogService::class); + $contactCatalogService = app(ContactCatalogService::class); + + return [ + 'store_options' => $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->searchCatalog('users', '', ['limit' => -1]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-store-manager::livewire.work-center.form-offcanvas'; + } +} diff --git a/Models/Currency.php b/Models/Currency.php new file mode 100644 index 0000000..dab92a0 --- /dev/null +++ b/Models/Currency.php @@ -0,0 +1,40 @@ + 'boolean', + 'used_in_sales' => 'boolean', + 'used_in_ecommerce' => 'boolean', + 'main_currency' => 'boolean', + 'auto_update_exchange_rates' => 'boolean', + 'update_interval' => 'integer', + 'status' => 'integer', + ]; + + // Relación con el historial de tipos de cambio + public function exchangeRates() + { + return $this->hasMany(CurrencyExchangeRate::class, 'c_currency', 'c_currency'); + } +} diff --git a/Models/CurrencyExchangeRate.php b/Models/CurrencyExchangeRate.php new file mode 100644 index 0000000..3874a62 --- /dev/null +++ b/Models/CurrencyExchangeRate.php @@ -0,0 +1,39 @@ + 'decimal:6', + 'exchange_date' => 'date', + 'source' => 'string', + 'updated_by' => 'integer', + ]; + + // Relación con la moneda + public function currency() + { + return $this->belongsTo(Currency::class, 'c_currency', 'c_currency'); + } + + // Relación con el usuario que actualizó el tipo de cambio + public function updatedByUser() + { + return $this->belongsTo(User::class, 'updated_by'); + } +} diff --git a/Models/EmailTransaction.php b/Models/EmailTransaction.php new file mode 100644 index 0000000..bd7f901 --- /dev/null +++ b/Models/EmailTransaction.php @@ -0,0 +1,61 @@ + 'array', + 'bcc' => 'array', + 'status' => 'integer', + ]; + + /** + * Relación polimórfica con modelos como pedidos, facturas, etc. + */ + public function emailable(): MorphTo + { + return $this->morphTo(); + } + + /** + * Relación con el usuario que creó el registro. + */ + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } +} diff --git a/Models/Store.php b/Models/Store.php new file mode 100644 index 0000000..875c990 --- /dev/null +++ b/Models/Store.php @@ -0,0 +1,160 @@ + 'boolean', + 'enable_ecommerce' => 'boolean', + 'status' => 'boolean', + ]; + + /** + * Nombre de la etiqueta para generar Componentes + * + * @var string + */ + public $tagName = 'Store'; + + /** + * Nombre de la columna que contiee el nombre del registro + * + * @var string + */ + public $columnNameLabel = 'name'; + + /** + * Nombre singular del registro. + * + * @var string + */ + public $singularName = 'sucursal'; + + /** + * Nombre plural del registro. + * + * @var string + */ + public $pluralName = 'sucursales'; + + /** + * Relación con el catálogo de códigos postales SAT. + */ + public function codigoPostal(): BelongsTo + { + return $this->belongsTo(CodigoPostal::class, 'c_codigo_postal', 'c_codigo_postal'); + } + + /** + * Relación con el catálogo de estados SAT. + */ + public function estado(): BelongsTo + { + return $this->belongsTo(Estado::class, 'c_estado', 'c_estado'); + } + + /** + * Relación con el catálogo de municipios SAT. + */ + public function municipio(): BelongsTo + { + return $this->belongsTo(Municipio::class, 'c_municipio', 'c_municipio'); + } + + /** + * Relación con el catálogo de colonias SAT. + */ + public function colonia(): BelongsTo + { + return $this->belongsTo(Colonia::class, 'c_colonia', 'c_colonia'); + } + + /** + * Relación con el régimen fiscal SAT. + */ + public function regimenFiscal(): BelongsTo + { + return $this->belongsTo(RegimenFiscal::class, 'c_regimen_fiscal', 'c_regimen_fiscal'); + } + + /** + * Relación con el domicilio fiscal (Código Postal SAT). + */ + public function domicilioFiscal(): BelongsTo + { + return $this->belongsTo(CodigoPostal::class, 'domicilio_fiscal', 'c_codigo_postal'); + } + + public function workCenters(): HasMany + { + return $this->hasMany(StoreWorkCenter::class); + } + + public function warehouses(): HasMany + { + return $this->hasMany(Warehouses::class); + } + + public function manager(): BelongsTo + { + return $this->belongsTo(User::class, 'manager_id'); + } + + public function scopeActive($query) + { + return $query->where('status', true); + } + + public function getFullAddressAttribute() + { + return "{$this->direccion}, {$this->num_ext}, {$this->num_int}, {$this->c_colonia}, {$this->c_municipio}, {$this->c_estado}, {$this->c_pais}"; + } + + +} diff --git a/Models/StoreUser.php b/Models/StoreUser.php new file mode 100644 index 0000000..256e3a1 --- /dev/null +++ b/Models/StoreUser.php @@ -0,0 +1,16 @@ + 'decimal:6', + 'lng' => 'decimal:6', + 'status' => 'integer', + ]; + + /** + * Nombre de la etiqueta para generar Componentes + * + * @var string + */ + public $tagName = 'WorkCenter'; + + /** + * Nombre de la columna que contiee el nombre del registro + * + * @var string + */ + public $columnNameLabel = 'name'; + + /** + * Nombre singular del registro. + * + * @var string + */ + public $singularName = 'centro de trabajo'; + + /** + * Nombre plural del registro. + * + * @var string + */ + public $pluralName = 'centros de trabajo'; + + /** + * Relación con la sucursal a la que pertenece el centro de trabajo. + */ + public function store(): BelongsTo + { + return $this->belongsTo(Store::class, 'store_id'); + } + + /** + * Relación con el usuario que gestiona el centro de trabajo. + */ + public function manager(): BelongsTo + { + return $this->belongsTo(User::class, 'manager_id'); + } +} diff --git a/Providers/VuexyStoreManagerServiceProvider.php b/Providers/VuexyStoreManagerServiceProvider.php new file mode 100644 index 0000000..91260f1 --- /dev/null +++ b/Providers/VuexyStoreManagerServiceProvider.php @@ -0,0 +1,56 @@ +loadRoutesFrom(__DIR__.'/../routes/admin.php'); + + + // Cargar vistas del paquete + $this->loadViewsFrom(__DIR__.'/../resources/views', 'vuexy-store-manager'); + + + // Register the migrations + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + + + // Registrar Livewire Components + $components = [ + 'company-index' => CompanyIndex::class, + 'store-index' => StoreIndex::class, + 'store-form' => StoreForm::class, + 'work-center-index' => WorkCenterIndex::class, + 'work-center-offcanvas-form' => WorkCenterOffcanvasForm::class, + ]; + + foreach ($components as $alias => $component) { + Livewire::component($alias, $component); + } + + + // Registrar auditoría en usuarios + //User::observe(AuditableObserver::class); + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf22ab4 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# 🎨 Laravel Vuexy Store Manager - Vuexy Admin + +

+ Koneko Soluciones Tecnológicas Logo +

+

+ Sitio Web + Latest Stable Version + License + Servidor Git + Build Status + Issues +

+ +--- + +## 📌 Descripción + +**Laravel Vuexy Store Manager** es un módulo diseñado para **Laravel Vuexy Admin**, proporcionando [breve descripción de la funcionalidad]. + +### ✨ Características: +- 🔹 Integración completa con Vuexy Admin. +- 🔹 Funcionalidad clave 1. +- 🔹 Funcionalidad clave 2. + +--- + +## 📦 Instalación + +Instalar vía **Composer**: + +```bash +composer require koneko/laravel-vuexy-store-manager +``` + +Publicar archivos de configuración y migraciones (si aplica): + +```bash +php artisan vendor:publish --tag=laravel-vuexy-store-manager-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-store-manager-config +``` + +Esto generará `config/laravel-vuexy-store-manager.php`, donde puedes modificar valores predeterminados. + +--- + +## 🛠 Dependencias + +Este paquete requiere las siguientes dependencias: +- Laravel 11 +- `koneko/laravel-vuexy-store-manager` +- 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-store-manager-config +php artisan vendor:publish --tag=laravel-vuexy-store-manager-seeders +php artisan migrate --seed +``` + +Para publicar imágenes del tema: + +```bash +php artisan vendor:publish --tag=laravel-vuexy-store-manager-images +``` + +--- + +## 🛠 Pruebas + +Ejecuta los tests con: + +```bash +php artisan test +``` + +--- + +## 🌍 Repositorio Principal y Sincronización + +Este repositorio es una **copia sincronizada** del repositorio principal alojado en **[Tea - Koneko Git](https://git.koneko.mx/koneko/laravel-vuexy-store-manager)**. + +### 🔄 Sincronización con GitHub +- **Repositorio Principal:** [git.koneko.mx](https://git.koneko.mx/koneko/laravel-vuexy-store-manager) +- **Repositorio en GitHub:** [github.com/koneko/laravel-vuexy-store-manager](https://github.com/koneko/laravel-vuexy-store-manager) +- **Los cambios pueden reflejarse primero en Tea antes de GitHub.** + +### 🤝 Contribuciones +Si deseas contribuir: +1. Puedes abrir un **Issue** en [GitHub Issues](https://github.com/koneko/laravel-vuexy-store-manager/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. + +--- + +## 🏅 Licencia + +Este paquete es de código abierto bajo la licencia [MIT](LICENSE). + +--- + +

+ Hecho con ❤️ por Koneko Soluciones Tecnológicas +

diff --git a/Services/StoreCatalogService.php b/Services/StoreCatalogService.php new file mode 100644 index 0000000..0947d05 --- /dev/null +++ b/Services/StoreCatalogService.php @@ -0,0 +1,117 @@ + [ + 'table' => 'stores', + 'key' => 'id', + 'value' => 'name AS item', + 'order_by' => 'name', + 'search_columns' => ['code', 'name'], + 'use_status' => true, + 'limit' => 50 + ], + 'work_centers' => [ + 'table' => 'store_work_centers', + 'key' => 'id', + 'value' => "CONCAT_WS(' - ', code , name) as item", + 'search_columns' => ['code', 'name'], + 'extra_conditions' => ['store_id'], + 'limit' => 50 + ], + 'currencies' => [ + 'table' => 'currencies', + 'key' => 'code', + 'value' => "CONCAT_WS(' - ', code , name) as item", + 'order_by' => 'name', + 'search_columns' => ['code', 'name'], + 'limit' => 20 + ], + 'email_transactions' => [ + 'table' => 'email_transactions', + 'key' => 'id', + 'value' => "CONCAT('Email ', id) as item", + 'order_by' => 'created_at', + 'limit' => 10 + ] + ]; + + /** + * Busca en un catálogo definido. + * + * @param string $catalog Nombre del catálogo. + * @param string $searchTerm Término de búsqueda opcional. + * @param array $options Opciones adicionales de filtrado. + * @return array + */ + public function searchCatalog(string $catalog, string $searchTerm = '', array $options = []): array + { + if (!isset($this->catalogs[$catalog])) { + return []; + } + + $config = $this->catalogs[$catalog]; + + $query = DB::table($config['table']); + + // Selección de columnas + $query->selectRaw("{$config['key']}, {$config['value']}"); + + // Filtrar por estado si es necesario + if (($config['use_status'] ?? false) === true) { + $query->where('status', $options['status'] ?? true); + } + + // Aplicar filtros adicionales + if (!empty($config['extra_conditions'])) { + foreach ($config['extra_conditions'] as $field) { + if (isset($options[$field])) { + $query->where($field, $options[$field]); + } + } + } + + // Aplicar búsqueda si se proporciona un término + if (!empty($searchTerm) && !empty($config['search_columns'])) { + $query->where(function ($subQuery) use ($config, $searchTerm) { + foreach ($config['search_columns'] as $column) { + $subQuery->orWhere($column, 'LIKE', "%{$searchTerm}%"); + } + }); + } + + // Ordenación + $query->orderBy($config['order_by'] ?? $config['key'], 'asc'); + + // Límite de resultados + if (isset($config['limit'])) { + $query->limit($config['limit']); + } + + // Modos de salida: `raw`, `select2`, `pluck` + $rawMode = $options['rawMode'] ?? false; + $select2Mode = $options['select2Mode'] ?? false; + + if ($rawMode) { + return $query->get()->toArray(); + } + + if ($select2Mode) { + return $query->get()->map(fn ($row) => [ + 'id' => $row->{$config['key']}, + 'text' => $row->item + ])->toArray(); + } + + return $query->pluck('item', $config['key'])->toArray(); + } +} diff --git a/Traits/HasUsersRelations.php b/Traits/HasUsersRelations.php new file mode 100644 index 0000000..eb031aa --- /dev/null +++ b/Traits/HasUsersRelations.php @@ -0,0 +1,63 @@ +belongsToMany(Role::class, 'store_user_roles') + ->withPivot('store_id') + ->withTimestamps(); + } + + /** + * Obtener el rol de un usuario en una sucursal específica + */ + public function getRoleForStore($storeId) + { + return $this->storeRoles()->wherePivot('store_id', $storeId)->first(); + } + + /** + * Verificar si el usuario tiene un rol en una sucursal específica + */ + public function hasRoleInStore($roleName, $storeId) + { + return $this->storeRoles() + ->wherePivot('store_id', $storeId) + ->where('name', $roleName) + ->exists(); + } + + /** + * Asignar un rol a un usuario en una sucursal específica + */ + public function assignRoleToStore($roleId, $storeId) + { + // Verificar si ya tiene el rol en la sucursal + if (!$this->hasRoleInStore($roleId, $storeId)) { + return $this->storeRoles()->attach($roleId, ['store_id' => $storeId]); + } + + return false; // No hacer nada si ya tiene el rol + } + + /** + * Remover un rol de un usuario en una sucursal específica + */ + public function removeRoleFromStore($roleId, $storeId) + { + // Verificar si realmente tiene el rol antes de eliminarlo + if ($this->hasRoleInStore($roleId, $storeId)) { + return $this->storeRoles()->detach($roleId, ['store_id' => $storeId]); + } + + return false; // No hacer nada si no tiene el rol + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6c5cf44 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "koneko/laravel-vuexy-store-manager", + "description": "Laravel Vuexy Store Manager, un modulo de administracion de tiendas optimizado para México.", + "keywords": ["laravel", "koneko", "framework", "vuexy", "store", "manager", "mexico"], + "type": "library", + "license": "MIT", + "require": { + "php": "^8.2", + "koneko/laravel-vuexy-contacts": "@dev", + "laravel/framework": "^11.31" + }, + "autoload": { + "psr-4": { + "Koneko\\VuexyStoreManager\\": "" + } + }, + "extra": { + "laravel": { + "providers": [ + "Koneko\\VuexyStoreManager\\Providers\\VuexyStoreManagerServiceProvider" + ] + } + }, + "authors": [ + { + "name": "Arturo Corro Pacheco", + "email": "arturo@koneko.mx" + } + ], + "support": { + "source": "https://github.com/koneko-mx/laravel-vuexy-store-manager", + "issues": "https://github.com/koneko-mx/laravel-vuexy-store-manager/issues" + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/database/migrations/2024_12_15_110508_create_store_table.php b/database/migrations/2024_12_15_110508_create_store_table.php new file mode 100644 index 0000000..6ae5040 --- /dev/null +++ b/database/migrations/2024_12_15_110508_create_store_table.php @@ -0,0 +1,76 @@ +smallIncrements('id'); + + // Información general + $table->string('code', 16)->unique(); + $table->string('name', 96)->index(); + $table->mediumText('description')->nullable(); + $table->unsignedMediumInteger('manager_id')->nullable()->index(); // sat_codigo_postal. + + // Ubicación + $table->char('c_pais', 3)->charset('ascii')->collation('ascii_general_ci')->nullable()->index(); // sat_estado. + $table->unsignedMediumInteger('c_codigo_postal')->nullable()->index(); // sat_codigo_postal. + $table->string('c_estado', 3)->charset('ascii')->collation('ascii_general_ci')->nullable()->index(); // sat_estado. + $table->unsignedTinyInteger('c_localidad')->nullable()->index(); + $table->unsignedSmallInteger('c_municipio')->nullable()->index(); // sat_municipio. + $table->unsignedMediumInteger('c_colonia')->nullable()->index(); // sat_colonia. + $table->string('direccion')->nullable(); + $table->string('num_ext')->nullable(); + $table->string('num_int')->nullable(); + $table->decimal('lat', 9, 6)->nullable(); + $table->decimal('lng', 9, 6)->nullable(); + + // Contacto + $table->string('email')->nullable(); + $table->string('tel')->nullable(); + $table->string('tel2')->nullable(); + + // Información fiscal + $table->string('rfc', 13)->nullable(); + $table->string('nombre_fiscal')->nullable(); + $table->unsignedSmallInteger('c_regimen_fiscal')->nullable()->index(); // sat_regimen_fiscal. + $table->unsignedMediumInteger('domicilio_fiscal')->nullable(); // sat_codigo_postal. + + $table->boolean('show_on_website')->default(false)->index(); + $table->boolean('enable_ecommerce')->default(false)->index(); + + $table->boolean('status')->default(true)->index(); + + // Auditoria + $table->timestamps(); // Campos created_at y updated_at + + // Relaciones + $table->foreign('manager_id')->references('id')->on('users')->onUpdate('restrict')->onDelete('restrict'); + $table->foreign('c_regimen_fiscal')->references('c_regimen_fiscal')->on('sat_regimen_fiscal')->onUpdate('restrict')->onDelete('restrict'); + $table->foreign('domicilio_fiscal')->references('c_codigo_postal')->on('sat_codigo_postal')->onUpdate('restrict')->onDelete('restrict'); + $table->foreign('c_pais')->references('c_pais')->on('sat_pais')->onUpdate('restrict')->onDelete('restrict'); + $table->foreign('c_codigo_postal')->references('c_codigo_postal')->on('sat_codigo_postal')->onUpdate('restrict')->onDelete('restrict'); + $table->foreign(['c_estado', 'c_pais'])->references(['c_estado', 'c_pais'])->on('sat_estado')->onUpdate('restrict')->onDelete('restrict'); + $table->foreign(['c_municipio', 'c_estado'])->references(['c_municipio', 'c_estado'])->on('sat_municipio')->onUpdate('restrict')->onDelete('restrict'); + $table->foreign(['c_localidad', 'c_estado'])->references(['c_localidad', 'c_estado'])->on('sat_localidad')->onUpdate('restrict')->onDelete('restrict'); + $table->foreign(['c_colonia', 'c_codigo_postal'])->references(['c_colonia', 'c_codigo_postal'])->on('sat_colonia')->onUpdate('restrict')->onDelete('restrict'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('stores'); + + } +}; diff --git a/database/migrations/2024_12_15_110511_create_store_work_centers_table.php b/database/migrations/2024_12_15_110511_create_store_work_centers_table.php new file mode 100644 index 0000000..b2364b6 --- /dev/null +++ b/database/migrations/2024_12_15_110511_create_store_work_centers_table.php @@ -0,0 +1,54 @@ +smallIncrements('id'); + + $table->unsignedSmallInteger('store_id')->index(); + + $table->string('code', 16)->nullable()->unique()->comment('Código único del centro de trabajo'); + $table->string('name', 96)->index(); // Nombre del centro de trabajo + $table->mediumText('description')->nullable(); // Descripción del centro + + $table->unsignedMediumInteger('manager_id')->nullable()->index(); // sat_codigo_postal. + + $table->string('tel')->nullable(); + $table->string('tel2')->nullable(); + + $table->decimal('lat', 9, 6)->nullable()->comment('Latitud de la ubicación del centros de trabajo'); + $table->decimal('lng', 9, 6)->nullable()->comment('Longitud de la ubicación del centros de trabajo'); + + $table->unsignedTinyInteger('status')->index(); // 'active', 'inactive' + + $table->timestamps(); + + // Indices + $table->unique(['store_id', 'name']); + + // Relaciones + $table->foreign('store_id')->references('id')->on('stores')->onDelete('restrict'); + $table->foreign('manager_id')->references('id')->on('users')->onUpdate('restrict')->onDelete('restrict'); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('store_work_centers'); + + } +}; diff --git a/database/migrations/2024_12_15_112793_create_store_user_roles_table.php b/database/migrations/2024_12_15_112793_create_store_user_roles_table.php new file mode 100644 index 0000000..6b9dac8 --- /dev/null +++ b/database/migrations/2024_12_15_112793_create_store_user_roles_table.php @@ -0,0 +1,38 @@ +id(); + + $table->unsignedSmallInteger('store_id')->index(); + $table->unsignedMediumInteger('user_id')->index(); + $table->unsignedBigInteger('role_id')->index(); + + //Auditoria + $table->timestamps(); + + // Relaciones + $table->foreign('store_id')->references('id')->on('stores')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('store_user_roles'); + } +}; diff --git a/database/migrations/2024_12_15_118085_create_currencie_tables.php b/database/migrations/2024_12_15_118085_create_currencie_tables.php new file mode 100644 index 0000000..3713c38 --- /dev/null +++ b/database/migrations/2024_12_15_118085_create_currencie_tables.php @@ -0,0 +1,63 @@ +smallIncrements('id'); + + $table->char('c_currency', 3)->charset('ascii')->collation('ascii_general_ci')->unique(); + $table->string('symbol', 10)->nullable(); + + $table->boolean('auto_update_exchange_rates')->default(true); + $table->unsignedInteger('refresh_interval')->default(24); // Tiempo de actualización en horas + $table->decimal('adjustment_percent', 5, 2)->default(0); // Ajuste porcentual opcional + + $table->boolean('status'); + + // Auditoria + $table->timestamps(); + + // Relaciones + $table->foreign('c_currency')->references('c_moneda')->on('sat_moneda')->onUpdate('restrict')->onDelete('restrict'); + }); + + Schema::create('currency_exchange_rates', function (Blueprint $table) { + $table->id(); + $table->char('c_currency', 3)->charset('ascii')->collation('ascii_general_ci'); + $table->decimal('exchange_rate', 10, 4); + $table->date('exchange_date'); // Se almacena la fecha de la tasa de cambio + $table->string('source'); // Fuente (banxico, fixer, etc.) + $table->unsignedMediumInteger('updated_by')->nullable(); // Usuario que hizo el cambio + $table->text('comments')->nullable(); // Comentarios sobre la modificación + + // Auditori + $table->timestamps(); + + // Indicies + $table->unique(['c_currency', 'exchange_date', 'source']); // Evita duplicados + $table->index(['c_currency', 'exchange_date']); + + // Llaves foráneas + $table->foreign('c_currency')->references('c_currency')->on('currencies')->onDelete('cascade'); + $table->foreign('updated_by')->references('id')->on('users')->onDelete('set null'); // Relación con usuarios + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists(['currencies', 'currency_exchange_rates']); + + } +}; diff --git a/database/migrations/2024_12_15_124251_create_email_transactions_table.php b/database/migrations/2024_12_15_124251_create_email_transactions_table.php new file mode 100644 index 0000000..0465eca --- /dev/null +++ b/database/migrations/2024_12_15_124251_create_email_transactions_table.php @@ -0,0 +1,53 @@ +mediumIncrements('id'); + + // Relación polimórfica: puede ser un pedido, una factura, etc. + $table->unsignedMediumInteger('emailable_id')->index(); + $table->string('emailable_type')->index(); + + $table->unsignedTinyInteger('email_provider')->index(); // Proveedor en CONST en modelo + $table->string('smtp_server')->nullable(); // Servidor SMTP personalizado + $table->string('smtp_port')->nullable(); // Puerto SMTP personalizado + $table->string('smtp_username')->nullable(); // Nombre de usuario para autenticación SMTP + + $table->string('subject'); // Asunto del correo + $table->mediumtext('body'); // Cuerpo del correo + $table->string('recipient'); // Destinatario principal + $table->json('cc')->nullable(); // Destinatarios en copia (CC), separados por coma + $table->json('bcc')->nullable(); // Destinatarios en copia oculta (BCC), separados por coma + $table->string('reply_to')->nullable(); // Dirección de correo para respuestas + $table->string('sender_name')->nullable(); // Nombre del remitente + $table->string('sender_email')->nullable(); // Correo electrónico del remitente + + $table->unsignedTinyInteger('status')->index()->comment('0: Pendiente, 1: En proceso, 2: Enviado, 3: Fallido, 4: En cola'); + + $table->mediumtext('error_message')->nullable(); // Mensaje de error si el envío falla + + // Authoría + $table->unsignedMediumInteger('created_by')->index(); // Usuario que creó el registro + $table->timestamps(); + + // Índices + $table->index(['emailable_type', 'emailable_id']); + + // Auditoría + $table->foreign('created_by')->references('id')->on('users')->onDelete('restrict'); + }); + } + + public function down(): void + { + Schema::dropIfExists('email_transactions'); + } +}; diff --git a/database/seeders/CurrencySeeder.php b/database/seeders/CurrencySeeder.php new file mode 100644 index 0000000..82351b6 --- /dev/null +++ b/database/seeders/CurrencySeeder.php @@ -0,0 +1,85 @@ + 'MXN', + 'symbol' => '$', + 'used_in_purchases' => true, + 'used_in_sales' => true, + 'used_in_ecommerce' => false, + 'main_currency' => true, + 'auto_update_exchange_rates' => true, + 'update_interval' => 24, + 'status' => Currency::STATUS_ENABLED, + ], + [ + 'c_currency' => 'USD', + 'symbol' => '$', + 'used_in_purchases' => true, + 'used_in_sales' => true, + 'used_in_ecommerce' => false, + 'main_currency' => false, + 'auto_update_exchange_rates' => true, + 'update_interval' => 24, + 'status' => Currency::STATUS_ENABLED, + ], + [ + 'c_currency' => 'EUR', + 'symbol' => '€', + 'used_in_purchases' => true, + 'used_in_sales' => true, + 'used_in_ecommerce' => false, + 'main_currency' => false, + 'auto_update_exchange_rates' => true, + 'update_interval' => 24, + 'status' => Currency::STATUS_ENABLED, + ], + [ + 'c_currency' => 'GBP', + 'symbol' => '£', + 'used_in_purchases' => true, + 'used_in_sales' => false, + 'used_in_ecommerce' => false, + 'main_currency' => false, + 'auto_update_exchange_rates' => true, + 'update_interval' => 24, + 'status' => Currency::STATUS_ENABLED, + ], + [ + 'c_currency' => 'JPY', + 'symbol' => '¥', + 'used_in_purchases' => true, + 'used_in_sales' => false, + 'used_in_ecommerce' => false, + 'main_currency' => false, + 'auto_update_exchange_rates' => true, + 'update_interval' => 24, + 'status' => Currency::STATUS_ENABLED, + ], + ]; + + /** + * Run the database seeds. + */ + public function run() + { + foreach (self::$divisas as $divisa) { + Currency::updateOrCreate( + ['c_currency' => $divisa['c_currency']], // Clave única + $divisa // Valores a insertar/actualizar + ); + } + + $this->command->info('Divisas insertadas/actualizadas correctamente.'); + } +} diff --git a/resources/assets/js/bootstrap-table/satCatalogsFormatters.js b/resources/assets/js/bootstrap-table/satCatalogsFormatters.js new file mode 100644 index 0000000..0969f02 --- /dev/null +++ b/resources/assets/js/bootstrap-table/satCatalogsFormatters.js @@ -0,0 +1,104 @@ +import {routes} from '../../../../../laravel-vuexy-admin/resources/assets/js/bootstrap-table/globalConfig.js'; + +export const motivoCancelacionFormatter = (value, row, index) => { + let motivos = { + '01': 'Comprobantes emitidos con errores con relación', + '02': 'Comprobantes emitidos con errores sin relación', + '03': 'No se llevó a cabo la operación', + '04': 'Operación nominativa en factura global' + }; + + let colores = { + '01': 'warning', + '02': 'danger', + '03': 'info', + '04': 'primary' + }; + + return `${motivos[value] || 'Desconocido'}`; +}; + +export const objetoImpFormatter = (value, row, index) => { + switch (parseInt(value)) { + case 1: + return '01 - No objeto de impuesto'; + case 2: + return '02 - Sí objeto de impuesto'; + case 3: + return '03 - Sí objeto del impuesto y no obligado al desglose'; + case 4: + return '04 - Sí objeto del impuesto y no causa impuesto'; + } +} + +export const claveProdServFormatter = (value, row, index) => { + if (row.c_clave_prod_serv) + return row.c_clave_prod_serv + (row.clave_prod_serv == undefined ? '' : ' - ' + row.clave_prod_serv); +} + +export const claveUnidadFormatter = (value, row, index) => { + if (row.c_clave_unidad) + return row.c_clave_unidad + ' - ' + row.clave_unidad; +} + +export const monedaFormatter = (value, row, index) => { + if (value) + return "
" + value + " - " + row.moneda + "
"; +} + +export const uuidFormatter = (value, row, index) => { + if (value) + return `${value.toUpperCase()}`; +} + +export const emisorRegimenFiscalFormatter = (value, row, index) => { + if (row.emisor_c_regimen_fiscal) + return row.emisor_c_regimen_fiscal + ' - ' + row.emisor_regimen_fiscal; +} + +export const receptorRegimenFiscalFormatter = (value, row, index) => { + if (row.receptor_c_regimen_fiscal) + return row.receptor_c_regimen_fiscal + ' - ' + row.receptor_regimen_fiscal; +} + +export const emisorUsoCfdiFormatter = (value, row, index) => { + if (row.receptor_c_uso_cfdi) + return row.receptor_c_uso_cfdi + ' - ' + row.receptor_uso_cfdi; +} + +export const tipoPersonaFormatter = (value, row, index) => { + switch (parseInt(value)) { + case 0: + return 'RFC inválido'; + case 2: + return 'Persona moral'; + case 1: + return 'Persona física'; + case 9: + return 'Público en general'; + } +} + +export const metodoPagoFormatter = (value, row, index) => { + switch (value) { + case 'PUE': + return 'PUE - Pago en una sola exhibición'; + case 'PPD': + return 'PPD - Pago en parcialidades o diferido'; + } +} + +export const formaPagoFormatter = (value, row, index) => { + if (row.c_forma_pago) + return String(row.c_forma_pago).padStart(2, '0') + ' - ' + row.forma_pago; +} + +export const usoCfdiFormatter = (value, row, index) => { + if (row.c_uso_cfdi) + return value ? `${row.c_uso_cfdi} - ${row.uso_cfdi}` : ''; +} + +export const regimenFiscalFormatter = (value, row, index) => { + if (row.c_regimen_fiscal) + return value ? `${row.c_regimen_fiscal} - ${row.regimen_fiscal}` : ''; +} diff --git a/resources/views/company/index.blade.php b/resources/views/company/index.blade.php new file mode 100644 index 0000000..dc57ea1 --- /dev/null +++ b/resources/views/company/index.blade.php @@ -0,0 +1,20 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Compañia') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.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 + +@section('content') + @livewire('company-index') +@endsection diff --git a/resources/views/livewire/company/index.blade.php b/resources/views/livewire/company/index.blade.php new file mode 100644 index 0000000..698e05d --- /dev/null +++ b/resources/views/livewire/company/index.blade.php @@ -0,0 +1,775 @@ +
+
+
+
+ + Cancelar +
+
+
+
+
+
+
Identificación
+
+
+
+
+ + + @error('code') + {{ $message }} + @enderror +
+
+
+ + + @error('name') + {{ $message }} + @enderror +
+
+ + + @error('description') + {{ $message }} + @enderror +
+
+
+ + @if(false) +
+
+
Series de facturación
+
+
+
+
+ + + @error('serie_ingresos') + {{ $message }} + @enderror +
+
+ + + @error('serie_egresos') + {{ $message }} + @enderror +
+
+ + + @error('serie_pagos') + {{ $message }} + @enderror +
+
+
+
+ @endIf + +
+
+
Configuraciones
+
+
+
+ + Habilitar sucursal + +
+
+ + Mostrar en sitio web + +
+
+ + Habilitar eCommerce + +
+
+
+
+
+
+
+
Información de contacto
+
+
+
+ + + @error('tel') + {{ $message }} + @enderror +
+
+ + + @error('tel2') + {{ $message }} + @enderror +
+
+ + + @error('email') + {{ $message }} + @enderror +
+
+
+ + +
+
+
+
+
+
Información fiscal
+
+
+
+ + + @error('rfc') + {{ $message }} + @enderror +
+
+ + + @error('nombre_fiscal') + {{ $message }} + @enderror +
+
+ + +
+
+ + + @error('domicilio_fiscal') + {{ $message }} + @enderror +
+
+
+
+
+
+
+
Dirección
+
+
+ @php + /* + dump($c_codigo_postal); + dump($c_estado); + dump($c_localidad); + dump($c_municipio); + dump($c_colonia); + + dump($c_localidad_options); + dump($c_municipio_options); + dump($c_colonia_options); + */ + @endphp + + +
+
+ + +
+
+ + + @error('c_codigo_postal') + {{ $message }} + @enderror +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + @error('direccion') + {{ $message }} + @enderror +
+
+
+ + + @error('num_ext') + {{ $message }} + @enderror +
+
+ + + @error('num_int') + {{ $message }} + @enderror +
+
+
+
+
+
+
Ubicación
+
+
+
+ + +
+
+
+ + + @error('lat') + {{ $message }} + @enderror +
+
+ + + @error('lng') + {{ $message }} + @enderror +
+
+
+
+
+
+
+
+
+ + Cancelar +
+
+
+ + +@push('page-script') + +@endpush diff --git a/resources/views/livewire/stores/form.blade.php b/resources/views/livewire/stores/form.blade.php new file mode 100644 index 0000000..b8e981c --- /dev/null +++ b/resources/views/livewire/stores/form.blade.php @@ -0,0 +1,170 @@ + +
+ + + + Cancelar + @if($mode == 'delete') + + @endif + +
+
+ {{-- Identificación --}} + + + + + + + {{-- Series de facturación --}} + + + + + + + {{-- Configuraciones --}} + + + + + +
+
+ {{-- Información de contacto --}} + + + + + + + + {{-- Información fiscal --}} + + + + + + +
+
+ {{-- Dirección --}} + + + {{-- Ubicación --}} + +
+
+
+
+ +@push('page-script') + +@endpush diff --git a/resources/views/livewire/stores/index.blade.php b/resources/views/livewire/stores/index.blade.php new file mode 100644 index 0000000..a155cbc --- /dev/null +++ b/resources/views/livewire/stores/index.blade.php @@ -0,0 +1,7 @@ + + +
+ +
+
+
diff --git a/resources/views/livewire/work-center/form.blade.php b/resources/views/livewire/work-center/form.blade.php new file mode 100644 index 0000000..10f3972 --- /dev/null +++ b/resources/views/livewire/work-center/form.blade.php @@ -0,0 +1,118 @@ +
+ + + + + + + + +
+ +
+ + +
+ + + +
+ +
+ + +
+ +
+
+
+
+ {{ $status }} + +
+
+
+ +@push('page-script') + +@endpush diff --git a/resources/views/livewire/work-center/index.blade.php b/resources/views/livewire/work-center/index.blade.php new file mode 100644 index 0000000..7b71778 --- /dev/null +++ b/resources/views/livewire/work-center/index.blade.php @@ -0,0 +1,12 @@ + + +
+ +
+ @if(count($storeOptions) > 1) +
+ +
+ @endif +
+
diff --git a/resources/views/store/crud.blade.php b/resources/views/store/crud.blade.php new file mode 100644 index 0000000..133fecb --- /dev/null +++ b/resources/views/store/crud.blade.php @@ -0,0 +1,24 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', $store? (ucfirst($mode) . ': ' . $store?->name) : 'Nueva Sucursal') + +@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/leaflet/leaflet.scss', + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/es.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/notifications/LivewireNotification.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/maps/LeafletMapHelper.js', + 'vendor/koneko/laravel-vuexy-contacts/resources/assets/js/addresses/AddressFormHandler.js', + ]) +@endsection + +@section('content') + @livewire('store-form', compact('mode', 'store')) +@endsection diff --git a/resources/views/store/index.blade.php b/resources/views/store/index.blade.php new file mode 100644 index 0000000..e225f18 --- /dev/null +++ b/resources/views/store/index.blade.php @@ -0,0 +1,25 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Sucursales') + +@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') +@endpush + +@section('content') + @livewire('store-index') +@endsection diff --git a/resources/views/store/show.blade.php b/resources/views/store/show.blade.php new file mode 100644 index 0000000..1d304ec --- /dev/null +++ b/resources/views/store/show.blade.php @@ -0,0 +1,9 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', $store->name) + +@section('content') +
+        {{ print_r($store) }}
+    
+@endsection diff --git a/resources/views/work-center/index.blade.php b/resources/views/work-center/index.blade.php new file mode 100644 index 0000000..8dde563 --- /dev/null +++ b/resources/views/work-center/index.blade.php @@ -0,0 +1,26 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Centro de trabajo') + +@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', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/leaflet/leaflet.scss', + ]) +@endsection + +@push('page-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/bootstrap-table/bootstrapTableManager.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/forms/formConvasHelper.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/maps/LeafletMapHelper.js', + ]) +@endpush + +@section('content') + @livewire('work-center-index') + @livewire('work-center-form') +@endsection diff --git a/resources/views/work-center/show.blade.php b/resources/views/work-center/show.blade.php new file mode 100644 index 0000000..a1866c2 --- /dev/null +++ b/resources/views/work-center/show.blade.php @@ -0,0 +1,9 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', $workcenter->name) + +@section('content') +
+        {{ print_r($workcenter) }}
+    
+@endsection diff --git a/routes/admin.php b/routes/admin.php new file mode 100644 index 0000000..7562fbe --- /dev/null +++ b/routes/admin.php @@ -0,0 +1,26 @@ +name('admin.store-manager.')->middleware(['web', 'auth', 'admin'])->group(function () { + Route::controller(CompanyController::class)->prefix('informacion-general')->name('company.')->group(function () { + Route::get('/', 'index')->name('index'); + }); + + Route::controller(StoreController::class)->prefix('sucursales')->name('stores.')->group(function () { + Route::get('/', 'index')->name('index'); + Route::get('create', 'create')->name('create'); + Route::get('{store}', 'show')->name('show'); + Route::get('{store}/delete', 'delete')->name('delete'); + Route::get('{store}/edit', 'edit')->name('edit'); + }); + + Route::controller(WorkCenterController::class)->prefix('centros-de-trabajo')->name('work-centers.')->group(function () { + Route::get('/', 'index')->name('index'); + Route::post('ajax', 'ajax')->name('ajax'); + }); +});