first commit

This commit is contained in:
Arturo Corro 2025-03-05 20:43:35 -06:00
commit aa938a3cab
47 changed files with 4388 additions and 0 deletions

18
.editorconfig Normal file
View File

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

38
.gitattributes vendored Normal file
View File

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

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
/node_modules
/vendor
/.vscode
/.nova
/.fleet
/.phpactor.json
/.phpunit.cache
/.phpunit.result.cache
/.zed
/.idea

16
.prettierignore Normal file
View File

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

29
.prettierrc.json Normal file
View File

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

39
CHANGELOG.md Normal file
View File

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

9
CONTRIBUTING.md Normal file
View File

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

View File

@ -0,0 +1,24 @@
<?php
namespace Koneko\VuexyStoreManager\Http\Controllers;
use App\Http\Controllers\Controller;
class CompanyController extends Controller
{
/**
* Display the specified resource.
*/
public function index()
{
return view('vuexy-store-manager::company.index');
}
/**
* Show the form for editing the specified resource.
*/
public function edit()
{
return view('vuexy-store-manager::company.edit');
}
}

View File

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

View File

@ -0,0 +1,142 @@
<?php
namespace Koneko\VuexyStoreManager\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Koneko\VuexyAdmin\Helpers\CatalogHelper;
use Koneko\VuexyAdmin\Queries\GenericQueryBuilder;
use Koneko\SatCatalogs\Http\Controllers\SatCatalogController;
use Koneko\VuexyStoreManager\Models\StoreWorkCenter;
class WorkCenterController extends SatCatalogController
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
if ($request->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);
}
}

9
LICENSE Normal file
View File

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

View File

@ -0,0 +1,269 @@
<?php
namespace Koneko\VuexyStoreManager\Livewire\Company;
use Illuminate\Support\Facades\DB;
use Koneko\SatCatalogs\Models\Colonia;
use Koneko\SatCatalogs\Models\Estado;
use Koneko\SatCatalogs\Models\Localidad;
use Koneko\SatCatalogs\Models\Municipio;
use Koneko\SatCatalogs\Models\Pais;
use Koneko\SatCatalogs\Models\RegimenFiscal;
use Koneko\VuexyStoreManager\Models\Store;
use Livewire\Component;
class CompanyIndex extends Component
{
public $btnSubmitText;
public $mode;
public $storeId;
public $code,
$name,
$description,
$manager_id,
$rfc,
$nombre_fiscal,
$c_regimen_fiscal,
$domicilio_fiscal,
$serie_ingresos,
$serie_egresos,
$serie_pagos,
$c_codigo_postal,
$c_pais,
$c_estado,
$c_localidad,
$c_municipio,
$c_colonia,
$direccion,
$num_ext,
$num_int,
$email,
$tel,
$tel2,
$lat,
$lng,
$status,
$show_on_website,
$enable_ecommerce;
public $manager_id_options = [],
$c_regimen_fiscal_options = [],
$c_pais_options = [],
$c_estado_options = [],
$c_localidad_options = [],
$c_municipio_options = [],
$c_colonia_options = [];
protected $listeners = [
'editStore' => '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');
}
}

190
Livewire/Store/PostForm.php Normal file
View File

@ -0,0 +1,190 @@
<?php
namespace Koneko\VuexyStoreManager\Livewire\Stores;
use Illuminate\Support\Facades\DB;
use Koneko\VuexyWarehouse\Models\Warehouse;
use Livewire\Component;
class StoreForm extends Component
{
public $form_title;
public $mode = 'create';
public $warehouseId,
$store_id,
$workcenter_id,
$code,
$name,
$description,
$is_active = true,
$is_default,
$confirm_delete;
public $store_options = [],
$workcenter_options = [];
protected $listeners = [
'editWarehouse' => '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');
}
}

View File

@ -0,0 +1,306 @@
<?php
namespace Koneko\VuexyStoreManager\Livewire\Stores;
use Illuminate\Validation\Rule;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormComponent;
use Koneko\SatCatalogs\Models\{Colonia, Estado, Localidad, Municipio, Pais, RegimenFiscal};
use Koneko\VuexyStoreManager\Models\Store;
/**
* Class StoreForm
*
* Componente Livewire para manejar el formulario CRUD de sucursales en el sistema ERP.
* Implementa la creación, edición y eliminación de sucursales con validaciones dinámicas.
*/
class StoreForm extends AbstractFormComponent
{
/**
* Campos específicos del formulario.
*/
public $code, $name, $description, $manager_id, $rfc, $nombre_fiscal, $c_regimen_fiscal,
$domicilio_fiscal, $serie_ingresos, $serie_egresos, $serie_pagos, $c_codigo_postal,
$c_pais, $c_estado, $c_localidad, $c_municipio, $c_colonia, $direccion, $num_ext,
$num_int, $email, $tel, $tel2, $lat, $lng, $show_on_website, $enable_ecommerce, $status;
public $confirmDeletion = false;
/**
* Listas de opciones para selects en el formulario.
*/
public $manager_id_options = [],
$c_regimen_fiscal_options = [],
$c_pais_options = [],
$c_estado_options = [],
$c_localidad_options = [],
$c_municipio_options = [],
$c_colonia_options = [];
/**
* Montar el formulario e inicializar datos específicos.
*
* @param string $mode Modo del formulario: create, edit, delete.
* @param Store|null $store El modelo Store si está en modo edición o eliminación.
*/
public function mount(string $mode = 'create', mixed $store = null): void
{
parent::mount($mode, $store->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<string, string>
*/
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<string, string>
*/
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';
}
}

View File

@ -0,0 +1,230 @@
<?php
namespace Koneko\VuexyStoreManager\Livewire\Stores;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
use Koneko\VuexyStoreManager\Models\Store;
/**
* Listado de Tiendas, extiende de la clase base AbstractIndexComponent
* para reutilizar la lógica de configuración y renderizado de tablas.
*/
class StoreIndex extends AbstractIndexComponent
{
/**
* Almacena rutas útiles para la funcionalidad de edición o eliminación.
* (En tu caso, lo llenas en mount())
*/
public $routes = [];
/**
* Método que define la clase o instancia del modelo a usar en este Index.
*
* @return string
*/
protected function model(): string
{
return Store::class;
}
/**
* Retorna las columnas (header) de la tabla.
*
* @return array
*/
protected function columns(): array
{
return [
'action' => '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';
}
}

View File

@ -0,0 +1,211 @@
<?php
namespace Koneko\VuexyStoreManager\Livewire\WorkCenters;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
use Koneko\VuexyStoreManager\Models\StoreWorkCenter;
use Koneko\VuexyStoreManager\Services\StoreCatalogService;
/**
* Index para Centros de Trabajo, extendiendo la clase base AbstractIndexComponent.
*/
class WorkCenterIndex extends AbstractIndexComponent
{
/**
* Usado para filtrar (opcional) o cargar catálogos en la vista.
*/
public $store_id;
/**
* Opciones de tiendas (por ejemplo, para un select de filtrado).
*/
public $storeOptions = [];
/**
* Montamos (inicializamos) el componente y llamamos al mount() del padre
* para configurar la tabla y setear $tagName, $singularName, $formId, etc.
*/
public function mount(): void
{
parent::mount();
// Ahora cargamos las opciones de tienda, usando el servicio necesario.
$storeCatalogService = app(StoreCatalogService::class);
$this->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';
}
}

View File

@ -0,0 +1,202 @@
<?php
namespace Koneko\VuexyStoreManager\Livewire\Workcenters;
use Illuminate\Validation\Rule;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
use Koneko\VuexyContacts\Services\ContactCatalogService;
use Koneko\VuexyStoreManager\Models\StoreWorkCenter;
use Koneko\VuexyStoreManager\Services\StoreCatalogService;
/**
* Class WorkCenterOffcanvasForm
*
* Componente Livewire para gestionar centros de trabajo.
* Extiende las funcionalidades generales de AbstractFormOffCanvasComponent.
*
* @package Koneko\VuexyStoreManager\Livewire\Workcenters
*/
class WorkCenterOffcanvasForm extends AbstractFormOffCanvasComponent
{
/**
* Campos específicos del centro de trabajo.
*/
public $store_id, $code, $name, $description, $manager_id, $tel, $tel2, $lat, $lng, $status;
/**
* Listas de opciones para selects en el formulario.
*/
public $store_options = [],
$manager_options = [];
/**
* Eventos de escucha de Livewire.
*
* @var array
*/
protected $listeners = [
'editWorkCenter' => '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<string, mixed>
*/
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<string, string>
*/
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<string, string>
*/
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';
}
}

40
Models/Currency.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace Koneko\VuexyStoreManager\Models;
use Illuminate\Database\Eloquent\Model;
class Currency extends Model
{
const STATUS_ENABLED = 10;
const STATUS_DISABLED = 1;
const STATUS_REMOVED = 0;
protected $fillable = [
'c_currency',
'symbol',
'used_in_purchases',
'used_in_sales',
'used_in_ecommerce',
'main_currency',
'auto_update_exchange_rates',
'update_interval',
'status'
];
protected $casts = [
'used_in_purchases' => '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');
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Koneko\VuexyStoreManager\Models;
use Illuminate\Database\Eloquent\Model;
use Koneko\VuexyAdmin\Models\User;
class CurrencyExchangeRate extends Model
{
protected $table = 'currency_exchange_rates';
protected $fillable = [
'c_currency',
'exchange_rate',
'exchange_date',
'source',
'updated_by',
'comments',
];
protected $casts = [
'exchange_rate' => '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');
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Koneko\VuexyStoreManager\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Koneko\VuexyAdmin\Models\User;
class EmailTransaction extends Model
{
use HasFactory;
protected $table = 'email_transactions';
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'mediumint';
protected $fillable = [
'emailable_id',
'emailable_type',
'email_provider',
'smtp_server',
'smtp_port',
'smtp_username',
'subject',
'body',
'recipient',
'cc',
'bcc',
'reply_to',
'sender_name',
'sender_email',
'status',
'error_message',
'created_by',
];
protected $casts = [
'cc' => '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');
}
}

160
Models/Store.php Normal file
View File

@ -0,0 +1,160 @@
<?php
namespace Koneko\VuexyStoreManager\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Koneko\VuexyAdmin\Models\User;
use Koneko\SatCatalogs\Models\CodigoPostal;
use Koneko\SatCatalogs\Models\Colonia;
use Koneko\SatCatalogs\Models\Estado;
use Koneko\SatCatalogs\Models\Municipio;
use Koneko\SatCatalogs\Models\RegimenFiscal;
class Store extends Model
{
use HasFactory;
// Valores GPS por defecto
const LATITUDE_DEFAULT = 19.0436;
const LONGITUDE_DEFAULT = -98.1980;
protected $fillable = [
'code',
'name',
'description',
'manager_id',
'c_pais',
'c_codigo_postal',
'c_estado',
'c_municipio',
'c_localidad',
'c_colonia',
'direccion',
'num_ext',
'num_int',
'lat',
'lng',
'email',
'tel',
'tel2',
'rfc',
'nombre_fiscal',
'c_regimen_fiscal',
'domicilio_fiscal',
'show_on_website',
'enable_ecommerce',
'status',
];
protected $casts = [
'show_on_website' => '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}";
}
}

16
Models/StoreUser.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace Koneko\VuexyStoreManager\Models;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyStoreManager\Traits\HasUsersRelations;
class StoreUser extends User
{
use HasUsersRelations;
protected $fillable = [
...parent::FILLABLE,
'store_id',
];
}

View File

@ -0,0 +1,73 @@
<?php
namespace Koneko\VuexyStoreManager\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Koneko\VuexyAdmin\Models\User;
class StoreWorkCenter extends Model
{
protected $fillable = [
'store_id',
'code',
'name',
'description',
'manager_id',
'tel',
'tel2',
'lat',
'lng',
'status',
];
protected $casts = [
'lat' => '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');
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Koneko\VuexyStoreManager\Providers;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
use Koneko\VuexyStoreManager\Livewire\Company\CompanyIndex;
use Koneko\VuexyStoreManager\Livewire\Stores\{StoreIndex,StoreForm};
use Koneko\VuexyStoreManager\Livewire\WorkCenters\{WorkCenterOffcanvasForm,WorkCenterIndex};
use OwenIt\Auditing\AuditableObserver;
class VuexyStoreManagerServiceProvider 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-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);
}
}

133
README.md Normal file
View File

@ -0,0 +1,133 @@
# 🎨 Laravel Vuexy Store Manager - Vuexy 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="Sitio Web"></a>
<a href="https://packagist.org/packages/koneko/laravel-vuexy-store-manager"><img src="https://img.shields.io/packagist/v/koneko/laravel-vuexy-store-manager" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/koneko/laravel-vuexy-store-manager"><img src="https://img.shields.io/packagist/l/koneko/laravel-vuexy-store-manager" 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-store-manager/actions/workflows/tests.yml"><img src="https://github.com/koneko-mx/laravel-vuexy-store-manager/actions/workflows/tests.yml/badge.svg" alt="Build Status"></a>
<a href="https://github.com/koneko-mx/laravel-vuexy-store-manager/issues"><img src="https://img.shields.io/github/issues/koneko/laravel-vuexy-store-manager" alt="Issues"></a>
</p>
---
## 📌 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).
---
<p align="center">
Hecho con ❤️ por <a href="https://koneko.mx">Koneko Soluciones Tecnológicas</a>
</p>

View File

@ -0,0 +1,117 @@
<?php
namespace Koneko\VuexyStoreManager\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class StoreCatalogService
{
/**
* Definición de catálogos disponibles.
*/
protected array $catalogs = [
'stores' => [
'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();
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Koneko\VuexyStoreManager\Traits;
use Spatie\Permission\Models\Role;
trait HasUsersRelations
{
/**
* Relación muchos a muchos entre usuarios y roles en sucursales
*/
public function storeRoles()
{
return $this->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
}
}

36
composer.json Normal file
View File

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

View File

@ -0,0 +1,76 @@
<?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('stores', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,54 @@
<?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
{
// centros de trabajo o estaciones de producción
Schema::create('store_work_centers', function (Blueprint $table) {
$table->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');
}
};

View File

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

View File

@ -0,0 +1,63 @@
<?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('currencies', function (Blueprint $table) {
$table->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']);
}
};

View File

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

View File

@ -0,0 +1,85 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Koneko\VuexyStoreManager\Models\Currency;
class CurrencySeeder extends Seeder
{
/**
* Lista de divisas a insertar.
*/
protected static array $divisas = [
[
'c_currency' => '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.');
}
}

View File

@ -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 `<span class="badge bg-label-${colores[value] || 'secondary'}">${motivos[value] || 'Desconocido'}</span>`;
};
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 "<div style='min-width: 150px'>" + value + " - " + row.moneda + "</div>";
}
export const uuidFormatter = (value, row, index) => {
if (value)
return `<span style="min-width:330px; display:block;">${value.toUpperCase()}</span>`;
}
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 ? `<span class="text-nowrap">${row.c_uso_cfdi} - ${row.uso_cfdi}</span>` : '';
}
export const regimenFiscalFormatter = (value, row, index) => {
if (row.c_regimen_fiscal)
return value ? `<span class="text-nowrap">${row.c_regimen_fiscal} - ${row.regimen_fiscal}</span>` : '';
}

View File

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

View File

@ -0,0 +1,775 @@
<div>
<form action="" wire:submit.prevent="onSubmit">
<div class="row">
<div class="col-12 mb-6">
<button type="submit" class="btn btn-submit btn-{{ $mode == 'delete' ? 'danger' : 'primary' }} waves-effect waves mr-3 mb-3">{{ $btnSubmitText }}</button>
<a href="{{ route('admin.store-manager.stores.index') }}" class="btn btn-label-secondary waves-effect waves mr-3 mb-3">Cancelar</a>
</div>
</div>
<div class="row">
<div class="col-lg-4">
<div class="card mb-6">
<div class="card-header">
<h5 class="card-title mb-0">Identificación</h5>
</div>
<div class="card-body">
<div class="row">
<div class="mb-4 col-6 fv-row">
<label class="form-label" for="code">Identificador único</label>
<input type="text" name="code" id="code" wire:model='code' class="form-control" placeholder="UID code" />
@error('code')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
</div>
<div class="mb-4 fv-row">
<label class="form-label" for="name">Nombre</label>
<input type="text" name="name" id="name" wire:model="name" class="form-control" placeholder="Nombre de la sucursal" />
@error('name')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="mb-4 fv-row">
<label class="form-label" for="description">Descripción</label>
<textarea id="description" name="description" wire:model='description' class="form-control" placeholder="Descripción de la sucursal" aria-label="Descripción de almacén" /></textarea>
@error('description')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
</div>
</div>
@if(false)
<div class="card mb-6">
<div class="card-header">
<h5 class="card-title mb-0">Series de facturación</h5>
</div>
<div class="card-body">
<div class="row">
<div class="mb-4 fv-row">
<label class="form-label" for="serie_ingresos">Serie para Ingresos</label>
<input type="text" name="serie_ingresos" id="serie_ingresos" wire:model="serie_ingresos" class="form-control" placeholder="Serie para Ingresos" />
@error('serie_ingresos')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="mb-4 fv-row">
<label class="form-label" for="serie_egresos">Serie para Egresos</label>
<input type="text" name="serie_egresos" id="serie_egresos" wire:model="serie_egresos" class="form-control" placeholder="Serie para Egresos" />
@error('serie_egresos')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="mb-4 fv-row">
<label class="form-label" for="serie_pagos">Serie para Pagos</label>
<input type="text" name="serie_pagos" id="serie_pagos" wire:model="serie_pagos" class="form-control" placeholder="Serie para Pagos" />
@error('serie_pagos')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
</div>
</div>
</div>
@endIf
<div class="card mb-6">
<div class="card-header">
<h5 class="card-title mb-0">Configuraciones</h5>
</div>
<div class="card-body">
<div class="mb-4 fv-row">
<x-vuexy-admin::form.checkbox
wire:model='status'
parent_class='form-switch'>
Habilitar sucursal
</x-vuexy-admin::form.checkbox>
</div>
<div class="mb-4 fv-row">
<x-vuexy-admin::form.checkbox
wire:model='show_on_website'
parent_class='form-switch'>
Mostrar en sitio web
</x-vuexy-admin::form.checkbox>
</div>
<div class="mb-4 fv-row">
<x-vuexy-admin::form.checkbox
wire:model='enable_ecommerce'
parent_class='form-switch'>
Habilitar eCommerce
</x-vuexy-admin::form.checkbox>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-6">
<div class="card-header">
<h5 class="card-title mb-0">Información de contacto</h5>
</div>
<div class="card-body">
<div class="mb-4 fv-row">
<label class="form-label" for="tel">Teléfono</label>
<input type="tel" name="tel" id="tel" wire:model="tel" class="form-control" placeholder="Teléfono" />
@error('tel')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="mb-4 fv-row">
<label class="form-label" for="tel2">Teléfono secundario</label>
<input type="tel" name="tel2" id="tel2" wire:model="tel2" class="form-control" placeholder="Teléfono secundario" />
@error('tel2')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="mb-4 fv-row">
<label class="form-label" for="email">Correo electrónico</label>
<input type="email" name="email" id="email" wire:model="email" class="form-control" placeholder="Correo electrónico" />
@error('email')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<hr>
<div class="mb-4 fv-row">
<label for="manager_id" class="form-label">Gerente</label>
<x-vuexy-admin::form.select
wire:model='manager_id'
:options="$manager_id_options"
:selected="$manager_id"
placeholder="Selecciona el gerente"
class="select2 form-select" />
</div>
</div>
</div>
<div class="card mb-6">
<div class="card-header">
<h5 class="card-title mb-0">Información fiscal</h5>
</div>
<div class="card-body">
<div class="mb-4 fv-row">
<label class="form-label" for="rfc">RFC</label>
<input type="text" name="rfc" id="rfc" wire:model="rfc" class="form-control" placeholder="Registro Federal de Contribuyentes" />
@error('rfc')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="mb-4 fv-row">
<label class="form-label" for="nombre_fiscal">Nombre fiscal</label>
<input type="text" name="nombre_fiscal" id="nombre_fiscal" wire:model="nombre_fiscal" class="form-control" placeholder="Nombre fiscal" />
@error('nombre_fiscal')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="mb-4 fv-row">
<label for="c_regimen_fiscal" class="form-label">Régimen fiscal</label>
<x-vuexy-admin::form.select
wire:model='c_regimen_fiscal'
:options="$c_regimen_fiscal_options"
:selected="$c_regimen_fiscal"
placeholder="Selecciona el regimen fiscal"
class="select2 form-select" />
</div>
<div class="mb-4 fv-row">
<label class="form-label" for="domicilio_fiscal">Domicilio fiscal</label>
<input type="text" name="domicilio_fiscal" id="domicilio_fiscal" wire:model="domicilio_fiscal" class="form-control" maxlength=5, placeholder="Domicilio fiscal (C.P)" />
@error('domicilio_fiscal')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-6">
<div class="card-header">
<h5 class="card-title mb-0">Dirección</h5>
</div>
<div class="card-body">
@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
<div class="row">
<div class="col-8 mb-4 fv-row">
<label for="c_pais" class="form-label">Pais</label>
<x-vuexy-admin::form.select
wire:model='c_pais'
:options="$c_pais_options"
:selected="$c_pais"
placeholder="Selecciona el pais"
class="select2 form-select" />
</div>
<div class="col-4 mb-4 fv-row if_local_address_show">
<label for="c_codigo_postal" class="form-label">Código postal</label>
<input type="text" name="c_codigo_postal" id="c_codigo_postal" wire:model="c_codigo_postal" class="form-control text-center" maxlength=5, placeholder="Código Postal" />
@error('c_codigo_postal')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
</div>
<div class="mb-4 fv-row">
<label for="c_estado" class="form-label">Estado</label>
<x-vuexy-admin::form.select
wire:model='c_estado'
:options="$c_estado_options"
:selected="$c_estado"
placeholder="Selecciona el estado"
class="select2 form-select" />
</div>
<div class="mb-4 fv-row if_local_address_show">
<label for="c_localidad" class="form-label">Localidad</label>
<x-vuexy-admin::form.select
wire:model='c_localidad'
:options="$c_localidad_options"
:selected="$c_localidad"
class="select2 form-select" />
</div>
<div class="mb-4 fv-row if_local_address_show">
<label for="c_municipio" class="form-label">Municipio</label>
<x-vuexy-admin::form.select
wire:model='c_municipio'
:options="$c_municipio_options"
:selected="$c_municipio"
class="select2 form-select" />
</div>
<div class="mb-4 fv-row if_local_address_show">
<label for="c_colonia" class="form-label">Colonia</label>
<x-vuexy-admin::form.select
wire:model='c_colonia'
:options="$c_colonia_options"
:selected="$c_colonia"
class="select2 form-select" />
</div>
<div class="mb-4 fv-row">
<label for="direccion" class="form-label">Dirección</label>
<input type="text" name="direccion" id="direccion" wire:model="direccion" class="form-control" placeholder="Dirección" />
@error('direccion')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="row">
<div class="col-6 mb-4 fv-row">
<label for="num_ext" class="form-label">Número exterior</label>
<input type="text" name="num_ext" id="num_ext" wire:model="num_ext" class="form-control" placeholder="Núm. exterior" />
@error('num_ext')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="col-6 mb-4 fv-row">
<label for="num_int" class="form-label">Número interior</label>
<input type="text" name="num_int" id="num_int" wire:model="num_int" class="form-control" placeholder="Núm. interior" />
@error('num_int')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
</div>
</div>
</div>
<div class="card mb-6">
<div class="card-header">
<h5 class="card-title mb-0">Ubicación</h5>
</div>
<div class="card-body">
<div class="input-group input-group-merge form-send-message">
<textarea class="form-control message-input" placeholder="Dirección de búsqueda" rows="2"></textarea>
<button type="button" class="message-actions input-group-text">
<i class="ti ti-map-pin-search"></i>
</button>
</div>
<div class="row">
<div class="col-6 mb-4 fv-row">
<label for="lat" class="form-label">Latitud</label>
<input type="number" step="0.000001" name="lat" id="lat" wire:model="lat" class="form-control text-center" placeholder="Latitud" />
@error('lat')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
<div class="col-6 mb-4 fv-row">
<label for="lng" class="form-label">Longitud</label>
<input type="number" step="0.000001" name="lng" id="lng" wire:model="lng" class="form-control text-center" placeholder="Longitud" />
@error('lng')
<span class="text-danger">{{ $message }}</span>
@enderror
</div>
</div>
<div style="height: 400px; z-index: 1;" id="geo_map"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 mb-6">
<button type="submit" class="btn btn-submit btn-{{ $mode == 'delete' ? 'danger' : 'primary' }} waves-effect waves mr-3 mb-3">{{ $btnSubmitText }}</button>
<a href="{{ route('admin.store-manager.stores.index') }}" class="btn btn-label-secondary waves-effect waves mr-3 mb-3">Cancelar</a>
</div>
</div>
</form>
</section>
@push('page-script')
<script>
const csrf_token = "{{ csrf_token() }}",
geo_coordinates = [{{ isset($sucursal) && $sucursal->lat && $sucursal->lng? ($sucursal->lat . ', ' . $sucursal->lng): (Koneko\VuexyStoreManager\Models\Store::LATITUDE_DEFAULT . ', ' . Koneko\VuexyStoreManager\Models\Store::LONGITUDE_DEFAULT) }}],
marker_coordinates = [{{ isset($sucursal) && $sucursal->lat && $sucursal->lng? ($sucursal->lat . ', ' . $sucursal->lng): 'false' }}];
let update_codigo_postal = true;
document.addEventListener("DOMContentLoaded", function () {
let $manager_id = $("#manager_id"),
$c_regimen_fiscal = $("#c_regimen_fiscal"),
$c_pais = $("#c_pais"),
$c_localidad = $("#c_localidad"),
$c_codigo_postal = $("#c_codigo_postal"),
$c_estado = $("#c_estado"),
$c_municipio = $("#c_municipio"),
$c_colonia = $("#c_colonia"),
$direccion = $("#direccion");
$manager_id
.select2({
language: "es",
placeholder: "Selecciona el gerente",
allowClear: true,
width: "100%"
})
.on('select2:select', function (e) {
@this.manager_id = e.params.data.id;
})
.on("select2:select select2:clear", function (e) {
@this.manager_id = null;
});
$c_regimen_fiscal
.select2({
language: "es",
placeholder: "Selecciona el gerente",
allowClear: true,
width: "100%"
})
.on('select2:select', function (e) {
@this.c_regimen_fiscal = e.params.data.id;
})
.on("select2:select select2:clear", function (e) {
@this.c_regimen_fiscal = null;
});
$c_codigo_postal
.on('keyup change', function(){
$c_estado.val('').trigger('change.select2');
$c_localidad.empty().prop('disabled', true).trigger('change.select2');
$c_municipio.empty().prop('disabled', true).trigger('change.select2');
$c_colonia.empty().prop('disabled', true).trigger('change.select2');
@this.c_estado = null;
@this.c_localidad = null;
@this.c_municipio = null;
@this.c_colonia = null;
let codigo_postal = $(this).val();
if(codigo_postal.length == 5){
$.ajax({
url: "{{ route('admin.core.sat.get.ajax', 'codigo_postal') }}",
type: 'post',
data: {
_token: csrf_token,
searchTerm: codigo_postal,
firstRow: true,
rawMode: true,
},
success: function(result){
$c_localidad.append(new Option('Selecciona la localidad', '', true, false));
$c_municipio.append(new Option('Selecciona el municipio', '', true, false));
$c_colonia.append(new Option('Selecciona la colonia', '', true, false));
if(result.c_codigo_postal){
$c_estado
.val(result.c_estado)
.prop('disabled', false)
.trigger('change.select2');
@this.c_estado = result.c_estado;
$.ajax({
url: "{{ route('admin.core.sat.get.ajax', 'localidad') }}",
type: "post",
data: {
_token: "{{ csrf_token() }}",
c_estado: result.c_estado,
limit: null
},
success: function (result) {
$.each(result, function (c_localidad, localidad) {
$c_localidad.append(new Option(localidad, c_localidad, false, false));
});
$c_localidad.prop('disabled', false).trigger('change.select2');
},
error: function() {
console.error('Error al cargar los localidades.');
}
});
$c_municipio
.append(new Option(result.municipio, result.c_municipio, false, true))
.prop('disabled', false)
.trigger('change.select2');
@this.c_municipio = result.c_municipio;
$c_colonia.prop('disabled', false).trigger('change.select2');
setTimeout(() => {
$c_colonia.select2('open');
}, 0)
}
}
});
}
});
$c_pais
.select2({
language: "es",
placeholder: "Selecciona el país",
allowClear: true,
width: "100%"
})
.on('select2:open', () => {
document.querySelector('.select2-search__field').focus();
})
.on("select2:select select2:clear", function (e) {
$('.if_local_address_show').hide();
$c_codigo_postal.val('');
$c_estado.empty().prop('disabled', true).trigger('change.select2');
$c_localidad.empty().prop('disabled', true).trigger('change.select2');
$c_municipio.empty().prop('disabled', true).trigger('change.select2');
$c_colonia.empty().prop('disabled', true).trigger('change.select2');
@this.c_pais = null;
@this.c_estado = null;
@this.c_localidad = null;
@this.c_municipio = null;
@this.c_colonia = null;
})
.on('select2:select', function (e) {
// Si NO es México, mostramos las .if_local_address_show y limpiamos c_estado
$c_pais.val() !== 'MEX'
? $('.if_local_address_show').hide()
: $('.if_local_address_show').show();
$c_codigo_postal.val('');
$c_estado.empty().prop('disabled', true).trigger('change.select2');
$c_localidad.empty().prop('disabled', true).trigger('change.select2');
$c_municipio.empty().prop('disabled', true).trigger('change.select2');
$c_colonia.empty().prop('disabled', true).trigger('change.select2');
@this.c_pais = e.params.data.id;
@this.c_estado = null;
@this.c_localidad = null;
@this.c_municipio = null;
@this.c_colonia = null;
$.ajax({
url: "{{ route('admin.core.sat.get.ajax', 'estado') }}",
type: "post",
data: {
_token: "{{ csrf_token() }}",
c_pais: $c_pais.val(),
limit: null
},
success: function (result) {
// Cargar estados en el select c_estado
$c_estado.append(new Option('Selecciona el estado', '', true, true));
$.each(result, function (clave, nombre) {
$c_estado.append(new Option(nombre, clave, false, false));
});
$c_estado.prop('disabled', false).trigger('change.select2');
if($c_estado.find('option').length > 1){
setTimeout(() => {
$c_estado.select2('open');
}, 0)
}
},
error: function() {
console.error('Error al cargar los estados.');
}
});
});
$c_estado
.select2({
language: "es",
placeholder: "Selecciona el estado",
allowClear: true,
width: "100%"
})
.on('select2:open', () => {
document.querySelector('.select2-search__field').focus();
})
.on('select2:select select2:clear', function (e) {
$c_codigo_postal.val('');
$c_localidad.empty().prop('disabled', true).trigger('change.select2');
$c_municipio.empty().prop('disabled', true).trigger('change.select2');
$c_colonia.empty().prop('disabled', true).trigger('change.select2');
@this.c_estado = null;
@this.c_localidad = null;
@this.c_municipio = null;
@this.c_colonia = null;
})
.on('select2:select', function (e) {
$c_codigo_postal.val('');
$c_localidad.empty().prop('disabled', true).trigger('change.select2');
$c_municipio.empty().prop('disabled', true).trigger('change.select2');
$c_colonia.empty().prop('disabled', true).trigger('change.select2');
@this.c_estado = e.params.data.id;
if($c_pais.val() === 'MEX'){
$.ajax({
url: "{{ route('admin.core.sat.get.ajax', 'localidad') }}",
type: "post",
data: {
_token: "{{ csrf_token() }}",
c_estado: $c_estado.val(),
limit: null
},
success: function (result) {
// Cargar localidades en el select c_localidad
$c_localidad.append(new Option('Selecciona la localidad', '', true, true));
$.each(result, function (clave, nombre) {
$c_localidad.append(new Option(nombre, clave, false, false));
});
$c_municipio.prop('disabled', false).trigger('change.select2');
$c_localidad.prop('disabled', false).trigger('change.select2');
setTimeout(() => {
$c_localidad.select2('open');
}, 0)
},
error: function() {
console.error('Error al cargar los localidades.');
}
});
}else{
$direccion.focus();
}
});
$c_localidad
.select2({
language: "es",
placeholder: "Selecciona el estado",
allowClear: true,
width: "100%"
})
.on('select2:open', () => {
document.querySelector('.select2-search__field').focus();
})
.on('select2:select', function (e) {
@this.c_localidad = e.params.data.id;
if(!($c_municipio.val())){
$c_municipio.select2('open');
}
});
$c_municipio
.select2({
ajax: {
url: "{{ route('admin.core.sat.get.ajax', 'municipio') }}",
type: "post",
delay: 250,
dataType: 'json',
data: function(params) {
return {
_token: csrf_token,
select2Mode: true,
searchTerm: params.term,
c_estado: $c_estado.val(),
};
},
processResults: function(response) {
return {
results: response
};
},
cache: true
},
language: "es",
placeholder: "Buscar municipio",
minimumInputLength: 3,
allowClear: true,
width: "100%",
})
.on('select2:open', () => {
document.querySelector('.select2-search__field').focus();
})
.on('select2:select select2:clear', function (e) {
$c_codigo_postal.val('');
$c_colonia.empty().prop('disabled', true).trigger('change.select2');
@this.c_municipio = null;
@this.c_colonia = null;
})
.on('select2:select', function (e) {
$c_codigo_postal.val('');
$c_colonia.empty().prop('disabled', true).trigger('change.select2');
@this.c_municipio = e.params.data.id;
@this.c_colonia = null;
$.ajax({
url: "{{ route('admin.core.sat.get.ajax', 'colonia') }}",
type: 'post',
data: {
_token: csrf_token,
c_estado: $c_estado.val(),
c_municipio: $c_municipio.val(),
limit: null
},
success: function(result){
for (const [c_colonia, colonia] of Object.entries(result)) {
$c_colonia.append(new Option(colonia, c_colonia, false, false))
}
$c_colonia.prop('disabled', false).trigger('change.select2');
setTimeout(() => {
$c_colonia.select2('open');
}, 0)
}
});
});
$c_colonia
.select2({
ajax: {
url: "{{ route('admin.core.sat.get.ajax', 'colonia') }}",
type: "post",
delay: 250,
dataType: 'json',
data: function(params) {
return {
_token: csrf_token,
select2Mode: true,
searchTerm: params.term,
c_codigo_postal: $c_codigo_postal.val(),
c_estado: $c_estado.val(),
c_municipio: $c_municipio.val(),
};
},
processResults: function(response) {
return {
results: response
};
},
cache: true
},
language: "es",
placeholder: "Buscar colonia",
allowClear: true,
width: "100%",
})
.on('select2:open', () => {
update_codigo_postal = true;
document.querySelector('.select2-search__field').focus();
})
.on('select2:clear', function (e) {
update_codigo_postal = false;
$c_codigo_postal.val('');
@this.c_colonia = null;
})
.on('select2:select', function (e) {
if(update_codigo_postal || !($c_codigo_postal.val())){
@this.c_colonia = e.params.data.id;
$.ajax({
url: "{{ route('admin.core.sat.get.ajax', 'colonia') }}",
type: 'post',
data: {
_token: csrf_token,
c_estado: $c_estado.val(),
c_municipio: $c_municipio.val(),
c_colonia: $c_colonia.val(),
firstRow: true,
rawMode: true,
},
success: function(result){
if($c_codigo_postal.val() !== result.c_codigo_postal){
$c_codigo_postal.val(result.c_codigo_postal);
@this.c_codigo_postal = result.c_codigo_postal;
}
$direccion.focus();
}
});
}
update_codigo_postal = true;
});
// Opcionalmente, ocultamos la dirección extranjera al cargar la página,
// si el valor inicial de #c_pais es "MEX"
$(function(){
if ($c_pais.val() === 'MEX') {
$('.if_local_address_show').show();
} else {
$('.if_local_address_show').hide();
}
});
$(document).ready(function(){
var draggableMap = L.map('geo_map').setView(geo_coordinates, 12);
if(marker_coordinates[0]){
var markerLocation = L.marker(marker_coordinates, {
draggable: 'true'
}).addTo(draggableMap);
markerLocation.bindPopup("<b>Aquí es la ubicación</b>").openPopup();
}
L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
maxZoom: 18
}).addTo(draggableMap);
})
});
</script>
@endpush

View File

@ -0,0 +1,170 @@
<div>
<x-vuexy-admin::form id="{{ $formId }}" :mode="$mode" wireSubmit="onSubmit" actionPosition="both">
<x-slot name="actions">
<button type="submit" class="btn btn-submit btn-{{ $mode == 'delete' ? 'danger' : 'primary' }} waves-effect waves mr-3 mb-3">{{ $btnSubmitText }}</button>
<a href="{{ route('admin.store-manager.stores.index') }}" class="btn {{ $mode == 'delete' ? 'btn-text-secondary waves-effect waves-light' : 'btn btn-label-secondary waves-effect' }} mr-3 mb-3">Cancelar</a>
@if($mode == 'delete')
<x-vuexy-admin::form.checkbox model="confirmDeletion" label="Confirmar eliminación" class="data-always-enabled" switch=true switchType="square" color="danger" size="lg" withIcon=true />
@endif
</x-slot>
<div class="row">
<div class="col-lg-4">
{{-- Identificación --}}
<x-vuexy-admin::card.basic title="Identificación">
<x-vuexy-admin::form.input :uid="$uniqueId" model="code" label="Identificador único" icon="ti ti-tag" placeholder="UID code" />
<x-vuexy-admin::form.input :uid="$uniqueId" model="name" label="Nombre de la sucursal" />
<x-vuexy-admin::form.textarea :uid="$uniqueId" model="description" label="Descripción" placeholder="Descripción de la sucursal" :autosize=true />
</x-vuexy-admin::card.basic>
{{-- Series de facturación --}}
<x-vuexy-admin::card.basic title="Series de facturación">
<x-vuexy-admin::form.input :uid="$uniqueId" model="serie_ingresos" label="Serie para Ingresos" inline=true :labelCol=6 :inputCol=6 />
<x-vuexy-admin::form.input :uid="$uniqueId" model="serie_egresos" label="Serie para Egresos" inline=true :labelCol=6 :inputCol=6 />
<x-vuexy-admin::form.input :uid="$uniqueId" model="serie_pagos" label="Serie para Pagos" inline=true :labelCol=6 :inputCol=6 />
</x-vuexy-admin::card.basic>
{{-- Configuraciones --}}
<x-vuexy-admin::card.basic title="Configuraciones">
<x-vuexy-admin::form.checkbox uid="random" model="status" label="Habilitar sucursal" switch="true" />
<x-vuexy-admin::form.checkbox uid="random" model="show_on_website" label="Mostrar en sitio web" switch="true" />
<x-vuexy-admin::form.checkbox uid="random" model="enable_ecommerce" label="eCommerce habilitado en sitio Web" switch="true" />
</x-vuexy-admin::card.basic>
</div>
<div class="col-lg-4">
{{-- Información de contacto --}}
<x-vuexy-admin::card.basic title="Información de contacto">
<x-vuexy-admin::form.input :uid="$uniqueId" model="tel" label="Teléfono" icon="ti ti-phone" />
<x-vuexy-admin::form.input :uid="$uniqueId" model="tel2" label="Teléfono secundario" icon="ti ti-phone" />
<x-vuexy-admin::form.input :uid="$uniqueId" model="email" label="Correo electrónico" icon="ti ti-mail" />
<x-vuexy-admin::form.select :uid="$uniqueId" model="manager_id" label="Gerente" :options="$manager_id_options" placeholder="Selecciona el gerente" />
</x-vuexy-admin::card.basic>
{{-- Información fiscal --}}
<x-vuexy-admin::card.basic title="Información fiscal">
<x-vuexy-admin::form.input :uid="$uniqueId" model="rfc" label="RFC" />
<x-vuexy-admin::form.input :uid="$uniqueId" model="nombre_fiscal" label="Nombre fiscal" />
<x-vuexy-admin::form.select :uid="$uniqueId" model="c_regimen_fiscal" label="Régimen fiscal" :options="$c_regimen_fiscal_options" placeholder="Selecciona el régimen fiscal" />
<x-vuexy-admin::form.input :uid="$uniqueId" model="domicilio_fiscal" label="Domicilio fiscal" max="5" />
</x-vuexy-admin::card.basic>
</div>
<div class="col-lg-4">
{{-- Dirección --}}
<x-vuexy-contacts::card.address :uid="$uniqueId" :paisOptions="$c_pais_options" :estadoOptions="$c_estado_options" :localidadOptions="$c_localidad_options" :municipioOptions="$c_municipio_options" :coloniaOptions="$c_colonia_options"/>
{{-- Ubicación --}}
<x-vuexy-contacts::card.location :uid="$uniqueId" />
</div>
</div>
</x-vuexy-admin::form>
</div>
@push('page-script')
<script>
const initializeStoreForm = (mode) => {
const initializeContactInformation = () => {
let $manager_id = $("#manager_id_{{ $uniqueId }}");
$manager_id
.select2({
language: "es",
placeholder: "Selecciona el gerente",
allowClear: true,
width: "100%"
})
.on('select2:select select2:clear', function (e) {
@this.manager_id = e.params?.data?.id || null;
});
}
const initializeFiscalInformation = () => {
let $c_regimen_fiscal = $("#c_regimen_fiscal_{{ $uniqueId }}");
$c_regimen_fiscal
.select2({
language: "es",
placeholder: "Selecciona el regimen fiscal",
allowClear: true,
width: "100%"
})
.on('select2:select select2:clear', function (e) {
@this.c_regimen_fiscal = e.params?.data?.id || null;
});
}
const initializeLocationIQ = () => {
//
}
const initializeAddressFormHandler = () => {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
// Definición de selectores AddressFormHandler
formSelectors = {
c_pais: '#c_pais_{{ $uniqueId }}',
c_estado: '#c_estado_{{ $uniqueId }}',
c_localidad: '#c_localidad_{{ $uniqueId }}',
c_municipio: '#c_municipio_{{ $uniqueId }}',
c_colonia: '#c_colonia_{{ $uniqueId }}',
c_codigo_postal: '#c_codigo_postal_{{ $uniqueId }}',
direccion: '#direccion_{{ $uniqueId }}',
notification: '#{{ $formId }} .address-notification'
};
// Definición de rutas AJAX Componente AddressFormHandler
const ajaxRoutes = {
codigo_postal: "{{ route('admin.core.sat.get.ajax', 'codigo_postal') }}",
localidad: "{{ route('admin.core.sat.get.ajax', 'localidad') }}",
estado: "{{ route('admin.core.sat.get.ajax', 'estado') }}",
municipio: "{{ route('admin.core.sat.get.ajax', 'municipio') }}",
colonia: "{{ route('admin.core.sat.get.ajax', 'colonia') }}"
};
// Inicializamos el handler de la información de la dirección
new AddressFormHandler(formSelectors, ajaxRoutes, @this, csrfToken);
}
const initializeLocationCard = (mode) => {
const locationInputs = {
search: '#location_search_{{ $uniqueId }}',
btnSearch: '#btn_search_{{ $uniqueId }}',
lat: '#lat_{{ $uniqueId }}',
lng: '#lng_{{ $uniqueId }}',
btnClear: '#{{ $formId }} .btn-clear-coords',
mapId: 'locationMap_{{ $uniqueId }}',
}
leafletMap = LeafletMapHelper.initializeMap(locationInputs, mode, @this);
}
// Inicializamos Tarjeta de Información de contacto
initializeContactInformation();
// Inicializamos Tarjeta de Información fiscal
initializeFiscalInformation();
// Inicializamos Tarjeta de Dirección
initializeAddressFormHandler();
// Inicializamos Tarjeta de Ubicación
initializeLocationCard(mode);
// Deshabilitamos el formulario si estamos eliminando
if (mode === 'delete') {
window.disableStoreForm('#{{ $formId }}');
}
}
// Evento para inicializar el formulario
window.addEventListener("DOMContentLoaded", () => {
window.addEventListener('on-failed-validation-store', (event) => {
setTimeout(() => {
initializeStoreForm('{{ $mode }}');
}, 10);
});
initializeStoreForm('{{ $mode }}');
});
</script>
@endpush

View File

@ -0,0 +1,7 @@
<x-vuexy-admin::table.bootstrap.manager :tagName="$tagName" :datatableConfig="$bt_datatable" :routes="$routes" noFilterButtons>
<x-slot name="tools">
<div class="mb-4 pr-2">
<x-vuexy-admin::button.basic icon="ti ti-pencil-plus" :label="$singularName" route='admin.store-manager.stores.create' />
</div>
</x-slot>
</x-vuexy-admin::table.bootstrap.manager>

View File

@ -0,0 +1,118 @@
<div>
<x-vuexy-admin::offcanvas.basic :id="$offcanvasId" :tag-name="$tagName">
<x-vuexy-admin::form :id="$formId" :mode="$mode" wireSubmit="onSubmit" actionPosition="both">
<x-slot name="actions">
<x-vuexy-admin::button.offcanvasButtons :mode="$mode" :tagName="$tagName" />
</x-slot>
<x-vuexy-admin::form.input :uid="$uniqueId" type="hidden" model="id" />
<x-vuexy-admin::form.input :uid="$uniqueId" type="hidden" model="mode" />
<x-vuexy-admin::form.select :uid="$uniqueId" model="store_id" label="Sucursal" placeholder="Selecciona una sucursal" :options="$store_options" />
<div class="row">
<x-vuexy-admin::form.input :uid="$uniqueId" model="code" label="Código de centro de trabajo" icon="ti ti-tag" parentClass="col-md-8" />
</div>
<x-vuexy-admin::form.input :uid="$uniqueId" model="name" label="Nombre del centro de trabajo" />
<x-vuexy-admin::form.textarea :uid="$uniqueId" model="description" label="Descripción" />
<hr>
<x-vuexy-admin::form.select :uid="$uniqueId" model="manager_id" label="Gerente" placeholder="Selecciona un gerente" :options="$manager_options" class="select2 form-select" />
<x-vuexy-admin::form.input :uid="$uniqueId" model="tel" label="Teléfono" icon="ti ti-phone" />
<x-vuexy-admin::form.input :uid="$uniqueId" model="tel2" label="Teléfono alternativo" icon="ti ti-phone" />
<hr>
<x-vuexy-admin::form.textarea :uid="$uniqueId" name="location_search" label="Dirección de búsqueda" placeholder="Buscar dirección" rows="2" button-icon="ti ti-map-pin-search" onClickButton="clearCoordinates()" />
<div class="row">
<x-vuexy-admin::form.input :uid="$uniqueId" model="lat" label="Latitud" type="number" step="0.000001" max="90" min="-90" parentClass="col-5" align="center" size="small" />
<x-vuexy-admin::form.input :uid="$uniqueId" model="lng" label="Longitud" type="number" step="0.000001" max="180" min="-180" parentClass="col-5" align="center" size="small" />
<div class="col-2 mt-5 !pl-0">
<x-vuexy-admin::button.basic variant="secondary" outline size="sm" icon="ti ti-map-pin-off ti-md" onClick="clearCoordinates()" />
</div>
</div>
<div style="height:400px; z-index: 1;" id="locationMap_{{ $uniqueId }}"></div>
<hr>
{{ $status }}
<x-vuexy-admin::form.checkbox :uid="$uniqueId" model="status" label="Activo" switch=true />
</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
window.addEventListener("DOMContentLoaded", () => {
const clearCoordinates = () => {
console.log('clearCoordinates');
// Aqui coloca la logica para limpiar la ubicación
//LeafletMapHelper.removeMarker();
}
/**
* Inicializa el formulario de centro de trabajo
*/
const initializeWorkCenterForm = () => {
const initializeSelect2 = () => {
const select2Selectors = {
[`#manager_id_{{ $uniqueId }}`]: {
placeholder: 'Selecciona un gerente',
onSelect: (id) => {
@this.set('updateManagerId', id, false);
},
onClear: () => {
@this.set('updateManagerId', null, false);
}
}
};
const parent = $('#storeWorkCenterForm');
$.each(select2Selectors, (selector, config) => {
const $element = parent.find(selector);
if ($element.length) {
$element
.select2({
placeholder: config.placeholder,
allowClear: true,
closeOnSelect: false
})
.on('select2:select', (e) => {
let selectedId = e.params.data.id;
config.onSelect(selectedId);
})
.on('select2:clear', () => {
config.onClear();
});
}
});
};
const initializeLocationCard = () => {
const locationInputs = {
search: '#location_search_{{ $uniqueId }}',
btnSearch: '#btn_search_{{ $uniqueId }}',
lat: '#lat_{{ $uniqueId }}',
lng: '#lng_{{ $uniqueId }}',
btnClear: '#storeWorkCenterForm .btn-clear-coords',
mapId: 'locationMap_{{ $uniqueId }}',
}
LeafletMapHelper.initializeMap(locationInputs, mode, @this);
}
const mode = @this.get('mode')?? 'create';
// Inicializa los select2
initializeSelect2();
// Inicializa el card de ubicación
initializeLocationCard(mode);
}
var myOffcanvas = document.getElementById('{{ $offcanvasId }}')
myOffcanvas.addEventListener('show.bs.offcanvas', function () {
initializeWorkCenterForm();
});
});
</script>
@endpush

View File

@ -0,0 +1,12 @@
<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.IndexOffcanvas :label="$singularName" :tagName="$tagName" />
</div>
@if(count($storeOptions) > 1)
<div class="mb-4 pr-2" style="max-width: 320px; min-width: 220px;">
<x-vuexy-admin::form.select model="store_id" :options="$storeOptions" placeholder="[Sucursal]" size="sm" />
</div>
@endif
</x-slot>
</x-vuexy-admin::table.bootstrap.manager>

View File

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

View File

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

View File

@ -0,0 +1,9 @@
@extends('vuexy-admin::layouts.vuexy.layoutMaster')
@section('title', $store->name)
@section('content')
<pre>
{{ print_r($store) }}
</pre>
@endsection

View File

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

View File

@ -0,0 +1,9 @@
@extends('vuexy-admin::layouts.vuexy.layoutMaster')
@section('title', $workcenter->name)
@section('content')
<pre>
{{ print_r($workcenter) }}
</pre>
@endsection

26
routes/admin.php Normal file
View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Support\Facades\Route;
use Koneko\VuexyStoreManager\Http\Controllers\CompanyController;
use Koneko\VuexyStoreManager\Http\Controllers\StoreController;
use Koneko\VuexyStoreManager\Http\Controllers\WorkCenterController;
// Grupo raíz para admin con middleware y prefijos comunes
Route::prefix('admin/ajustes/empresa')->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');
});
});