commit ac40d0f3997b6ee68e2b18ebe2917a1a5d39371b Author: Arturo Corro Date: Wed Mar 5 21:11:33 2025 -0600 first commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7333620 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,38 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore + +# Ignorar archivos de configuración y herramientas de desarrollo +.editorconfig export-ignore +.prettierrc.json export-ignore +.prettierignore export-ignore +.eslintrc.json export-ignore + +# Ignorar node_modules y dependencias locales +node_modules/ export-ignore +vendor/ export-ignore + +# Ignorar archivos de build +npm-debug.log export-ignore + +# Ignorar carpetas de logs y caché +storage/logs/ export-ignore +storage/framework/ export-ignore + +# Ignorar carpetas de compilación de frontend +public/build/ export-ignore +dist/ export-ignore + +# Ignorar archivos de CI/CD +.github/ export-ignore +.gitlab-ci.yml export-ignore +.vscode/ export-ignore +.idea/ export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d07bec2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/node_modules +/vendor +/.vscode +/.nova +/.fleet +/.phpactor.json +/.phpunit.cache +/.phpunit.result.cache +/.zed +/.idea diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5d3dfee --- /dev/null +++ b/.prettierignore @@ -0,0 +1,16 @@ +# Dependencias de Composer y Node.js +/vendor/ +/node_modules/ + +# Caché y logs +/storage/ +*.log +*.cache + +# Archivos del sistema +.DS_Store +Thumbs.db + +# Configuración de editores +.idea/ +.vscode/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..5f11c9c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,29 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "bracketSameLine": true, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": true, + "printWidth": 120, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "none", + "useTabs": false, + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto", + "overrides": [ + { + "files": [ + "resources/assets/**/*.js" + ], + "options": { + "semi": false + } + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1663013 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# 📜 CHANGELOG - Laravel Vuexy Contacts + +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-contacts)** y luego se refleja en GitHub. +Los cambios recientes pueden verse antes en **Tea** que en **GitHub** debido a la sincronización automática. + +--- + +📅 Última actualización: **2024-03-05**. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ac24fb5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +## 🔐 Acceso al Repositorio Privado + +Nuestro servidor Git en **Tea** tiene un registro cerrado. Para contribuir: + +1. Abre un **Issue** en [GitHub](https://github.com/koneko-mx/laravel-vuexy-contacts/issues) indicando tu interés en contribuir. +2. Alternativamente, envía un correo a **contacto@koneko.mx** solicitando acceso. +3. Una vez aprobado, recibirás una invitación para registrarte y clonar el repositorio. + +Si solo necesitas acceso de lectura, puedes clonar la versión pública en **GitHub**. diff --git a/Http/Controllers/ContactController.php b/Http/Controllers/ContactController.php new file mode 100644 index 0000000..5f2a46a --- /dev/null +++ b/Http/Controllers/ContactController.php @@ -0,0 +1,181 @@ +ajax()) { + $bootstrapTableIndexConfig = [ + 'table' => 'users', + 'columns' => [ + 'users.id', + 'users.code', + DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS full_name"), + 'users.email', + DB::raw("CONCAT_WS(' ', parent.name, parent.last_name) AS parent_name"), + 'parent.email AS parent_email', + DB::raw("CONCAT_WS(' ', agent.name, agent.last_name) AS agent_name"), + 'agent.email AS agent_email', + 'users.company', + 'users.birth_date', + 'users.hire_date', + 'users.curp', + 'users.nss', + 'users.job_title', + 'users.rfc', + 'users.nombre_fiscal', + 'users.tipo_persona', + 'users.c_regimen_fiscal', + 'sat_regimen_fiscal.descripcion AS regimen_fiscal', + 'users.domicilio_fiscal', + 'users.c_uso_cfdi', + 'sat_uso_cfdi.descripcion AS uso_cfdi', + 'sat_estado.nombre_del_estado AS estado', + 'sat_municipio.descripcion AS municipio', + 'sat_localidad.descripcion AS localidad', + 'users.profile_photo_path', + 'users.is_partner', + 'users.is_employee', + 'users.is_prospect', + 'users.is_customer', + 'users.is_provider', + 'users.is_user', + 'users.status', + DB::raw("CONCAT_WS(' ', created.name, created.last_name) AS creator"), + 'created.email AS creator_email', + 'users.created_at', + 'users.updated_at', + ], + 'joins' => [ + [ + 'table' => 'users as parent', + 'first' => 'users.parent_id', + 'second' => 'parent.id', + 'type' => 'leftJoin', + ], + [ + 'table' => 'users as agent', + 'first' => 'users.agent_id', + 'second' => 'agent.id', + 'type' => 'leftJoin', + ], + [ + 'table' => 'users as created', + 'first' => 'users.created_by', + 'second' => 'created.id', + 'type' => 'leftJoin', + ], + [ + 'table' => 'sat_codigo_postal', + 'first' => 'users.domicilio_fiscal', + 'second' => 'sat_codigo_postal.c_codigo_postal', + 'type' => 'leftJoin', + ], + [ + 'table' => 'sat_estado', + 'first' => 'sat_codigo_postal.c_estado', + 'second' => 'sat_estado.c_estado', + 'type' => 'leftJoin', + 'and' => [ + 'sat_estado.c_pais = "MEX"', + ], + ], + [ + 'table' => 'sat_localidad', + 'first' => 'sat_codigo_postal.c_localidad', + 'second' => 'sat_localidad.c_localidad', + 'type' => 'leftJoin', + 'and' => [ + 'sat_codigo_postal.c_estado = sat_localidad.c_estado', + ], + ], + [ + 'table' => 'sat_municipio', + 'first' => 'sat_codigo_postal.c_municipio', + 'second' => 'sat_municipio.c_municipio', + 'type' => 'leftJoin', + 'and' => [ + 'sat_codigo_postal.c_estado = sat_municipio.c_estado', + ], + ], + [ + 'table' => 'sat_regimen_fiscal', + 'first' => 'users.c_regimen_fiscal', + 'second' => 'sat_regimen_fiscal.c_regimen_fiscal', + 'type' => 'leftJoin', + ], + [ + 'table' => 'sat_uso_cfdi', + 'first' => 'users.c_uso_cfdi', + 'second' => 'sat_uso_cfdi.c_uso_cfdi', + 'type' => 'leftJoin', + ], + ], + 'filters' => [ + 'search' => ['users.name', 'users.email', 'users.code', 'parent.name', 'created.name'], + ], + 'sort_column' => 'users.name', + 'default_sort_order' => 'asc', + ]; + + return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson(); + } + + $pageConfigs = ['contentLayout' => 'wide']; + + $breadcrumbs = [ + ['route' => 'admin.home', 'name' => "Inicio"], + ['name' => "Contactos", 'active' => true] + ]; + + return view('vuexy-contacts::contacts.index', compact('breadcrumbs', 'pageConfigs')); + } + + /** + * Show the crud for creating a new resource. + */ + public function create() + { + return view('vuexy-contacts::contacts.crud') + ->with('mode', 'create') + ->with('contact', null); + } + + /** + * Display the specified resource. + */ + public function show(User $contact) + { + return view('vuexy-contacts::contacts.show', compact('contact')); + } + + /** + * Show the crud for editing the specified resource. + */ + public function edit(User $contact) + { + //$contact = User::findOrFail($id); + + + + return view('vuexy-contacts::contacts.crud', compact('contact'))->with('mode', 'edit'); + } + + /** + * Show the crud for editing the specified resource. + */ + public function delete(User $contact) + { + return view('vuexy-contacts::contact.crud', compact('contact'))->with('mode', 'delete'); + + } + +} diff --git a/Http/Controllers/CustomerController.php b/Http/Controllers/CustomerController.php new file mode 100644 index 0000000..c6c40b4 --- /dev/null +++ b/Http/Controllers/CustomerController.php @@ -0,0 +1,65 @@ +id ?? null); + } + + /** + * Cargar opciones de formularios según el modo actual. + * + * @param string $mode + */ + private function loadOptions(string $mode): void + { + $this->manager_id_options = User::getUsersListWithInactive($this->manager_id, ['type' => 'user', 'status' => 1]); + $this->c_regimen_fiscal_options = RegimenFiscal::selectList(); + $this->c_pais_options = Pais::selectList(); + $this->c_estado_options = Estado::selectList($this->c_pais)->toArray(); + + if ($mode !== 'create') { + $this->c_localidad_options = Localidad::selectList($this->c_estado)->toArray(); + $this->c_municipio_options = Municipio::selectList($this->c_estado, $this->c_municipio)->toArray(); + $this->c_colonia_options = Colonia::selectList($this->c_codigo_postal, $this->c_colonia)->toArray(); + } + } + + // ===================== MÉTODOS OBLIGATORIOS ===================== + + /** + * Devuelve el modelo Eloquent asociado. + * + * @return string + */ + protected function model(): string + { + return Store::class; + } + + /** + * Reglas de validación dinámicas según el modo actual. + * + * @param string $mode + * @return array + */ + protected function dynamicRules(string $mode): array + { + switch ($mode) { + case 'create': + case 'edit': + return [ + 'code' => [ + 'required', 'string', 'alpha_num', 'max:16', + Rule::unique('stores', 'code')->ignore($this->id) + ], + 'name' => 'required|string|max:96', + 'description' => 'nullable|string|max:1024', + 'manager_id' => 'nullable|exists:users,id', + + // Información fiscal + 'rfc' => ['nullable', 'string', 'regex:/^([A-ZÑ&]{3,4})(\d{6})([A-Z\d]{3})$/i', 'max:13'], + 'nombre_fiscal' => 'nullable|string|max:255', + 'c_regimen_fiscal' => 'nullable|exists:sat_regimen_fiscal,c_regimen_fiscal', + 'domicilio_fiscal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal', + + // Ubicación + 'c_pais' => 'nullable|exists:sat_pais,c_pais|string|size:3', + 'c_estado' => 'nullable|exists:sat_estado,c_estado|string|min:2|max:3', + 'c_municipio' => 'nullable|exists:sat_municipio,c_municipio|integer', + 'c_localidad' => 'nullable|integer', + 'c_codigo_postal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal|integer', + 'c_colonia' => 'nullable|exists:sat_colonia,c_colonia|integer', + 'direccion' => 'nullable|string|max:255', + 'num_ext' => 'nullable|string|max:50', + 'num_int' => 'nullable|string|max:50', + 'lat' => 'nullable|numeric|between:-90,90', + 'lng' => 'nullable|numeric|between:-180,180', + + // Contacto + 'email' => ['nullable', 'email', 'required_if:enable_ecommerce,true'], + 'tel' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'], + 'tel2' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'], + + // Configuración web y estado + 'show_on_website' => 'nullable|boolean', + 'enable_ecommerce' => 'nullable|boolean', + 'status' => 'nullable|boolean', + ]; + + case 'delete': + return [ + 'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación + ]; + + default: + return []; + } + } + + /** + * Inicializa los datos del formulario en función del modo. + * + * @param Store|null $store + * @param string $mode + */ + protected function initializeFormData(mixed $store, string $mode): void + { + if ($store) { + $this->code = $store->code; + $this->name = $store->name; + $this->description = $store->description; + $this->manager_id = $store->manager_id; + $this->rfc = $store->rfc; + $this->nombre_fiscal = $store->nombre_fiscal; + $this->c_regimen_fiscal = $store->c_regimen_fiscal; + $this->domicilio_fiscal = $store->domicilio_fiscal; + $this->c_pais = $store->c_pais; + $this->c_estado = $store->c_estado; + $this->c_municipio = $store->c_municipio; + $this->c_localidad = $store->c_localidad; + $this->c_codigo_postal = $store->c_codigo_postal; + $this->c_colonia = $store->c_colonia; + $this->direccion = $store->direccion; + $this->num_ext = $store->num_ext; + $this->num_int = $store->num_int; + $this->lat = $store->lat; + $this->lng = $store->lng; + $this->email = $store->email; + $this->tel = $store->tel; + $this->tel2 = $store->tel2; + $this->show_on_website = (bool) $store->show_on_website; + $this->enable_ecommerce = (bool) $store->enable_ecommerce; + $this->status = (bool) $store->status; + + } else { + $this->c_pais = 'MEX'; + $this->status = true; + $this->show_on_website = false; + $this->enable_ecommerce = false; + } + + $this->loadOptions($mode); + } + + /** + * Prepara los datos validados para su almacenamiento. + * + * @param array $validatedData + * @return array + */ + protected function prepareData(array $validatedData): array + { + return [ + 'code' => $validatedData['code'], + 'name' => $validatedData['name'], + 'description' => strip_tags($validatedData['description']), + 'manager_id' => $validatedData['manager_id'], + 'rfc' => $validatedData['rfc'], + 'nombre_fiscal' => $validatedData['nombre_fiscal'], + 'c_regimen_fiscal' => $validatedData['c_regimen_fiscal'], + 'domicilio_fiscal' => $validatedData['domicilio_fiscal'], + 'c_codigo_postal' => $validatedData['c_codigo_postal'], + 'c_pais' => $validatedData['c_pais'], + 'c_estado' => $validatedData['c_estado'], + 'c_localidad' => $validatedData['c_localidad'], + 'c_municipio' => $validatedData['c_municipio'], + 'c_colonia' => $validatedData['c_colonia'], + 'direccion' => $validatedData['direccion'], + 'num_ext' => $validatedData['num_ext'], + 'num_int' => $validatedData['num_int'], + 'email' => $validatedData['email'], + 'tel' => $validatedData['tel'], + 'tel2' => $validatedData['tel2'], + 'lat' => $validatedData['lat'], + 'lng' => $validatedData['lng'], + 'status' => $validatedData['status'], + 'show_on_website' => $validatedData['show_on_website'], + 'enable_ecommerce' => $validatedData['enable_ecommerce'], + ]; + } + + /** + * Definición de los contenedores de notificación. + * + * @return array + */ + protected function targetNotifies(): array + { + return [ + "index" => "#bt-stores .notification-container", + "form" => "#store-form .notification-container", + ]; + } + + /** + * Ruta de vista asociada al formulario. + * + * @return \Illuminate\Contracts\View\View + */ + protected function viewPath(): string + { + return 'vuexy-store-manager::livewire.stores.form'; + } + + // ===================== VALIDACIONES ===================== + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'code' => 'código de sucursal', + 'name' => 'nombre de la sucursal', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'code.required' => 'El código de la sucursal es obligatorio.', + 'code.unique' => 'Este código ya está en uso por otra sucursal.', + 'name.required' => 'El nombre de la sucursal es obligatorio.', + ]; + } + + // ===================== PREPARACIÓN DE DATOS ===================== + + // ===================== NOTIFICACIONES Y EVENTOS ===================== + + /** + * Definición de los eventos del componente. + * + * @return array + */ + protected function dispatches(): array + { + return [ + 'on-failed-validation' => 'on-failed-validation-store', + 'on-hydrate' => 'on-hydrate-store-modal', + ]; + } + + // ===================== REDIRECCIÓN ===================== + + /** + * Define la ruta de redirección tras guardar o eliminar. + * + * @return string + */ + protected function getRedirectRoute(): string + { + return 'admin.store-manager.stores.index'; + } + +} diff --git a/Livewire/Contacts/ContactIndex.php b/Livewire/Contacts/ContactIndex.php new file mode 100644 index 0000000..c41c718 --- /dev/null +++ b/Livewire/Contacts/ContactIndex.php @@ -0,0 +1,236 @@ + 'Acciones', + 'code' => 'Código personal', + 'full_name' => 'Nombre Completo', + 'email' => 'Correo Electrónico', + 'parent_name' => 'Responsable', + 'parent_email' => 'Correo Responsable', + 'company' => 'Empresa', + 'birth_date' => 'Fecha de Nacimiento', + 'hire_date' => 'Fecha de Contratación', + 'curp' => 'CURP', + 'nss' => 'NSS', + 'job_title' => 'Puesto', + 'rfc' => 'RFC', + 'nombre_fiscal' => 'Nombre Fiscal', + 'tipo_persona' => 'Tipo de Persona', + 'c_regimen_fiscal'=> 'Régimen Fiscal', + 'domicilio_fiscal'=> 'Domicilio Fiscal', + 'c_uso_cfdi' => 'Clave Uso CFDI', + 'uso_cfdi' => 'Uso CFDI', + 'profile_photo_path' => 'Foto de Perfil', + 'is_partner' => 'Socio', + 'is_employee' => 'Empleado', + 'is_prospect' => 'Prospecto', + 'is_customer' => 'Cliente', + 'is_provider' => 'Proveedor', + 'is_user' => 'Usuario', + 'status' => 'Estatus', + 'creator' => 'Creado Por', + 'creator_email' => 'Correo Creador', + 'created_at' => 'Fecha de Creación', + 'updated_at' => 'Última Modificación', + ]; + } + + /** + * Retorna el formato (formatter) para cada columna. + */ + protected function format(): array + { + return [ + 'action' => [ + 'formatter' => 'contactActionFormatter', + 'onlyFormatter' => true, + ], + 'code' => [ + 'formatter' => [ + 'name' => 'dynamicBadgeFormatter', + 'params' => ['color' => 'secondary'], + ], + 'align' => 'center', + 'switchable' => false, + ], + 'full_name' => [ + 'formatter' => 'contactProfileFormatter', + ], + 'email' => [ + 'formatter' => 'emailFormatter', + 'visible' => false, + ], + 'parent_name' => [ + 'formatter' => 'contactParentFormatter', + 'visible' => false, + ], + 'agent_name' => [ + 'formatter' => 'agentFormatter', + 'visible' => false, + ], + 'company' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'curp' => [ + 'visible' => false, + ], + 'nss' => [ + 'visible' => false, + ], + 'job_title' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'rfc' => [ + 'visible' => false, + ], + 'nombre_fiscal' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'domicilio_fiscal' => [ + 'visible' => false, + ], + 'c_uso_cfdi' => [ + 'formatter' => 'usoCfdiFormatter', + 'visible' => false, + ], + 'tipo_persona' => [ + 'formatter' => 'dynamicBadgeFormatter', + 'align' => 'center', + 'visible' => false, + ], + 'c_regimen_fiscal' => [ + 'formatter' => 'regimenFiscalFormatter', + 'visible' => false, + ], + 'birth_date' => [ + 'align' => 'center', + 'visible' => false, + ], + 'hire_date' => [ + 'align' => 'center', + 'visible' => false, + ], + 'estado' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'municipio' => [ + 'formatter' => 'textNowrapFormatter', + ], + 'localidad' => [ + 'formatter' => 'textNowrapFormatter', + 'visible' => false, + ], + 'is_partner' => [ + 'formatter' => [ + 'name' => 'dynamicBooleanFormatter', + 'params' => ['tag' => 'checkSI'], + ], + 'align' => 'center', + ], + 'is_employee' => [ + 'formatter' => [ + 'name' => 'dynamicBooleanFormatter', + 'params' => ['tag' => 'checkSI'], + ], + 'align' => 'center', + ], + 'is_prospect' => [ + 'formatter' => [ + 'name' => 'dynamicBooleanFormatter', + 'params' => ['tag' => 'checkSI'], + ], + 'align' => 'center', + ], + 'is_customer' => [ + 'formatter' => [ + 'name' => 'dynamicBooleanFormatter', + 'params' => ['tag' => 'checkSI'], + ], + 'align' => 'center', + ], + 'is_provider' => [ + 'formatter' => [ + 'name' => 'dynamicBooleanFormatter', + 'params' => ['tag' => 'checkSI'], + ], + 'align' => 'center', + ], + 'is_user' => [ + 'formatter' => [ + 'name' => 'dynamicBooleanFormatter', + 'params' => ['tag' => 'checkSI'], + ], + 'align' => 'center', + ], + 'status' => [ + 'formatter' => 'statusIntBadgeBgFormatter', + 'align' => 'center', + ], + 'creator' => [ + 'formatter' => 'creatorFormatter', + 'visible' => false, + ], + 'created_at' => [ + 'formatter' => 'textNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + 'updated_at' => [ + 'formatter' => 'textNowrapFormatter', + 'align' => 'center', + 'visible' => false, + ], + ]; + } + + /** + * Montamos el componente y llamamos al parent::mount() para configurar la tabla. + */ + public function mount(): void + { + parent::mount(); + + // Definimos las rutas específicas de este componente + $this->routes = [ + 'admin.user.show' => route('admin.core.users.show', ['user' => ':id']), + 'admin.contact.show' => route('admin.contacts.contacts.show', ['contact' => ':id']), + 'admin.contact.edit' => route('admin.contacts.contacts.edit', ['contact' => ':id']), + 'admin.contact.delete' => route('admin.contacts.contacts.delete', ['contact' => ':id']), + ]; + } + + /** + * Retorna la vista a renderizar por este componente. + */ + protected function viewPath(): string + { + return 'vuexy-contacts::livewire.contacts.index'; + } +} diff --git a/Livewire/Contacts/ContactOffCanvasForm.php b/Livewire/Contacts/ContactOffCanvasForm.php new file mode 100644 index 0000000..fb3e8ef --- /dev/null +++ b/Livewire/Contacts/ContactOffCanvasForm.php @@ -0,0 +1,233 @@ + 'loadFormModel', + 'confirmDeletionContacts' => 'loadFormModelForDeletion', + ]; + + /** + * Definición de tipos de datos que se deben castear. + * + * @var array + */ + protected $casts = [ + 'status' => 'boolean', + ]; + + /** + * Define el modelo Eloquent asociado con el formulario. + * + * @return string + */ + protected function model(): string + { + return User::class; + } + + /** + * Define los campos del formulario. + * + * @return array + */ + protected function fields(): array + { + return (new User())->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 'name'; + } + + /** + * Define reglas de validación dinámicas basadas en el modo actual. + * + * @param string $mode El modo actual del formulario ('create', 'edit', 'delete'). + * @return array + */ + protected function dynamicRules(string $mode): array + { + switch ($mode) { + case 'create': + case 'edit': + return [ + 'store_id' => ['required', 'integer', 'exists:stores,id'], + 'work_center_id' => ['nullable', 'integer', 'exists:store_work_centers,id'], + 'code' => ['required', 'string', 'max:16', Rule::unique('contact', 'code')->ignore($this->id)], + 'name' => ['required', 'string', 'max:96'], + 'description' => ['nullable', 'string', 'max:1024'], + 'manager_id' => ['nullable', 'integer', 'exists:users,id'], + 'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], + 'tel2' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'], + 'priority' => ['nullable', 'numeric', 'between:0,99'], + 'status' => ['nullable', 'boolean'], + ]; + + case 'delete': + return [ + 'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación + ]; + + default: + return []; + } + } + + // ===================== VALIDACIONES ===================== + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + protected function attributes(): array + { + return [ + 'code' => 'código de almacén', + 'name' => 'nombre del almacén', + ]; + } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + protected function messages(): array + { + return [ + 'store_id.required' => 'El almacén debe estar asociado a un negocio.', + 'code.required' => 'El código del almacén es obligatorio.', + 'code.unique' => 'Este código ya está en uso por otro almacén.', + 'name.required' => 'El nombre del almacén es obligatorio.', + ]; + } + + /** + * Carga el formulario con datos del almacén y actualiza las opciones dinámicas. + * + * @param int $id + */ + public function loadFormModel($id): void + { + parent::loadFormModel($id); + + $this->work_center_options = $this->store_id + ? DB::table('store_work_centers') + ->where('store_id', $this->store_id) + ->pluck('name', 'id') + ->toArray() + : []; + } + + /** + * Carga el formulario para eliminar un almacén, actualizando las opciones necesarias. + * + * @param int $id + */ + public function loadFormModelForDeletion($id): void + { + parent::loadFormModelForDeletion($id); + + $this->work_center_options = DB::table('store_work_centers') + ->where('store_id', $this->store_id) + ->pluck('name', 'id') + ->toArray(); + } + + /** + * Define las opciones de los selectores desplegables. + * + * @return array + */ + protected function options(): array + { + $storeCatalogService = app(StoreCatalogService::class); + $contactCatalogService = app(ContactCatalogService::class); + + return [ + 'store_options' => $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]), + 'manager_options' => $contactCatalogService->searchCatalog('users', '', ['limit' => -1]), + ]; + } + + /** + * Ruta de la vista asociada con este formulario. + * + * @return string + */ + protected function viewPath(): string + { + return 'vuexy-contacts::livewire.contacts.offcanvas-form'; + } +} diff --git a/Livewire/Contacts/ContactShow.php b/Livewire/Contacts/ContactShow.php new file mode 100644 index 0000000..18451e5 --- /dev/null +++ b/Livewire/Contacts/ContactShow.php @@ -0,0 +1,283 @@ + 'nullable|integer', + 'name' => 'required|string|min:3|max:255', + 'cargo' => 'nullable|string|min:3|max:255', + 'is_prospect' => 'nullable|boolean', + 'is_customer' => 'nullable|boolean', + 'is_provider' => 'nullable|boolean', + 'is_user' => 'nullable|boolean', + 'pricelist_id' => 'nullable|integer', + 'enable_credit' => 'nullable|boolean', + 'credit_days' => 'nullable|integer', + 'credit_limit' => 'nullable|numeric|min:0|max:9999999.99|regex:/^\d{1,7}(\.\d{1,2})?$/', + 'image' => 'nullable|mimes:jpg,png|image|max:20480', // 20MB Max + ]; + + // Reglas de validación para los campos fiscales + protected $rulesFacturacion = [ + 'rfc' => 'nullable|string|max:13', + 'domicilio_fiscal' => [ + 'nullable', + 'regex:/^[0-9]{5}$/', + 'exists:sat_codigo_postal,c_codigo_postal' + ], + 'nombre_fiscal' => 'nullable|string|max:255', + 'c_regimen_fiscal' => 'nullable|integer', + 'c_uso_cfdi' => 'nullable|string', + ]; + + public function mount($userId) + { + $this->user = User::findOrFail($userId); + + $this->reloadUserData(); + + $this->pricelists_options = DropdownList::selectList(DropdownList::POS_PRICELIST); + + $this->status_options = [ + User::STATUS_ENABLED => User::$statusList[User::STATUS_ENABLED], + User::STATUS_DISABLED => User::$statusList[User::STATUS_DISABLED], + ]; + + $this->regimen_fiscal_options = RegimenFiscal::selectList(); + $this->uso_cfdi_options = UsoCfdi::selectList(); + } + + + public function reloadUserData() + { + $this->tipo_persona = $this->user->tipo_persona; + $this->name = $this->user->name; + $this->cargo = $this->user->cargo; + $this->is_prospect = $this->user->is_prospect? true : false; + $this->is_customer = $this->user->is_customer? true : false; + $this->is_provider = $this->user->is_provider? true : false; + $this->is_user = $this->user->is_user? true : false; + $this->pricelist_id = $this->user->pricelist_id; + $this->enable_credit = $this->user->enable_credit? true : false; + $this->credit_days = $this->user->credit_days; + $this->credit_limit = $this->user->credit_limit; + $this->profile_photo = $this->user->profile_photo_url; + $this->profile_photo_path = $this->user->profile_photo_path; + $this->image = null; + $this->deleteUserImage = false; + + $this->status = $this->user->status; + $this->email = $this->user->email; + $this->password = null; + $this->password_confirmation = null; + + $this->rfc = $this->user->rfc; + $this->domicilio_fiscal = $this->user->domicilio_fiscal; + $this->nombre_fiscal = $this->user->nombre_fiscal; + $this->c_regimen_fiscal = $this->user->c_regimen_fiscal; + $this->c_uso_cfdi = $this->user->c_uso_cfdi; + + $this->cuentaUsuarioAlert = null; + $this->accesosAlert = null; + $this->facturacionElectronicaAlert = null; + } + + + public function saveCuentaUsuario() + { + try { + // Validar Información de usuario + $validatedData = $this->validate($this->rulesUser); + + $validatedData['name'] = trim($validatedData['name']); + $validatedData['cargo'] = $validatedData['cargo']? trim($validatedData['cargo']): null; + $validatedData['is_prospect'] = $validatedData['is_prospect'] ? 1 : 0; + $validatedData['is_customer'] = $validatedData['is_customer'] ? 1 : 0; + $validatedData['is_provider'] = $validatedData['is_provider'] ? 1 : 0; + $validatedData['is_user'] = $validatedData['is_user'] ? 1 : 0; + $validatedData['pricelist_id'] = $validatedData['pricelist_id'] ?: null; + $validatedData['enable_credit'] = $validatedData['enable_credit'] ? 1 : 0; + $validatedData['credit_days'] = $validatedData['credit_days'] ?: null; + $validatedData['credit_limit'] = $validatedData['credit_limit'] ?: null; + + if($this->tipo_persona == User::TIPO_RFC_PUBLICO){ + $validatedData['cargo'] = null; + $validatedData['is_prospect'] = null; + $validatedData['is_provider'] = null; + $validatedData['is_user'] = null; + $validatedData['enable_credit'] = null; + $validatedData['credit_days'] = null; + $validatedData['credit_limit'] = null; + } + + if(!$this->user->is_prospect && !$this->user->is_customer){ + $validatedData['pricelist_id'] = null; + } + + if(!$this->user->is_customer){ + $validatedData['enable_credit'] = null; + $validatedData['credit_days'] = null; + $validatedData['credit_limit'] = null; + } + + $this->user->update($validatedData); + + + if($this->deleteUserImage && $this->user->profile_photo_path){ + $this->user->deleteProfilePhoto(); + + // Reiniciar variables después de la eliminación + $this->deleteUserImage = false; + $this->profile_photo_path = null; + $this->profile_photo = $this->user->profile_photo_url; + + }else if ($this->image) { + $image = ImageManager::imagick()->read($this->image->getRealPath()); + $image = $image->scale(520, 520); + + $imageName = $this->image->hashName(); // Genera un nombre único + + $image->save(storage_path('app/public/profile-photos/' . $imageName)); + + $this->user->deleteProfilePhoto(); + + $this->profile_photo_path = $this->user->profile_photo_path = 'profile-photos/' . $imageName; + $this->profile_photo = $this->user->profile_photo_url; + $this->user->save(); + + unlink($this->image->getRealPath()); + + $this->reset('image'); + } + + // Puedes también devolver un mensaje de éxito si lo deseas + $this->setAlert('Se guardó los cambios exitosamente.', 'cuentaUsuarioAlert'); + + } catch (\Illuminate\Validation\ValidationException $e) { + // Si hay errores de validación, los puedes capturar y manejar aquí + $this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'cuentaUsuarioAlert', 'danger'); + } + } + + public function saveAccesos() + { + try { + $validatedData = $this->validate([ + 'status' => 'integer', + 'email' => ['required', 'email', 'unique:users,email,' . $this->user->id], + 'password' => ['nullable', 'string', 'min:6', 'max:32', 'confirmed'], // La regla 'confirmed' valida que ambas contraseñas coincidan + ], [ + 'email.required' => 'El correo electrónico es obligatorio.', + 'email.email' => 'Debes ingresar un correo electrónico válido.', + 'email.unique' => 'Este correo ya está en uso.', + 'password.min' => 'La contraseña debe tener al menos 5 caracteres.', + 'password.max' => 'La contraseña no puede tener más de 32 caracteres.', + 'password.confirmed' => 'Las contraseñas no coinciden.', + ]); + + // Si la validación es exitosa, continuar con el procesamiento + $validatedData['email'] = trim($this->email); + + if ($this->password) + $validatedData['password'] = bcrypt($this->password); + + else + unset($validatedData['password']); + + $this->user->update($validatedData); + + $this->password = null; + $this->password_confirmation = null; + + // Puedes también devolver un mensaje de éxito si lo deseas + $this->setAlert('Se guardó los cambios exitosamente.', 'accesosAlert'); + + } catch (\Illuminate\Validation\ValidationException $e) { + // Si hay errores de validación, los puedes capturar y manejar aquí + $this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'accesosAlert', 'danger'); + } + } + + public function saveFacturacionElectronica() + { + try { + // Validar Información fiscal + $validatedData = $this->validate($this->rulesFacturacion); + + $validatedData['rfc'] = strtoupper(trim($validatedData['rfc'])) ?: null; + $validatedData['domicilio_fiscal'] = $validatedData['domicilio_fiscal'] ?: null; + $validatedData['nombre_fiscal'] = strtoupper(trim($validatedData['nombre_fiscal'])) ?: null; + $validatedData['c_regimen_fiscal'] = $validatedData['c_regimen_fiscal'] ?: null; + $validatedData['c_uso_cfdi'] = $validatedData['c_uso_cfdi'] ?: null; + + $this->user->update($validatedData); + + // Puedes también devolver un mensaje de éxito si lo deseas + $this->setAlert('Se guardó los cambios exitosamente.', 'facturacionElectronicaAlert'); + + } catch (\Illuminate\Validation\ValidationException $e) { + // Si hay errores de validación, los puedes capturar y manejar aquí + $this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'facturacionElectronicaAlert', 'danger'); + } + } + + + private function setAlert($message, $alertName, $type = 'success') + { + $this->$alertName = [ + 'message' => $message, + 'type' => $type + ]; + } + + public function render() + { + return view('livewire.admin.crm.contact-view'); + } +} diff --git a/Models/ContactUser.php b/Models/ContactUser.php new file mode 100644 index 0000000..47c3529 --- /dev/null +++ b/Models/ContactUser.php @@ -0,0 +1,16 @@ + 'decimal:6', + 'lng' => 'decimal:6', + 'preference_level' => 'integer', + ]; + + /** + * Relación polimórfica con cualquier modelo (User, Empresa, etc.) + */ + public function contactable() + { + return $this->morphTo(); + } + + /** + * Obtiene el país de la dirección basado en el catálogo SAT + */ + public function country() + { + return $this->belongsTo(SatPais::class, 'c_pais', 'c_pais'); + } + + /** + * Obtiene el estado basado en el catálogo SAT + */ + public function state() + { + return $this->belongsTo(SatEstado::class, ['c_estado', 'c_pais'], ['c_estado', 'c_pais']); + } + + /** + * Obtiene el municipio basado en el catálogo SAT + */ + public function municipality() + { + return $this->belongsTo(SatMunicipio::class, ['c_municipio', 'c_estado'], ['c_municipio', 'c_estado']); + } + + /** + * Obtiene la localidad basada en el catálogo SAT + */ + public function locality() + { + return $this->belongsTo(SatLocalidad::class, ['c_localidad', 'c_estado'], ['c_localidad', 'c_estado']); + } + + /** + * Obtiene la colonia basada en el catálogo SAT + */ + public function neighborhood() + { + return $this->belongsTo(SatColonia::class, ['c_colonia', 'c_codigo_postal'], ['c_colonia', 'c_codigo_postal']); + } + + /** + * Define si la dirección es preferida + */ + public function isPreferred() + { + return $this->preference_level === 1; + } + + /** + * Devuelve la dirección en formato legible + */ + public function getFullAddressAttribute() + { + return "{$this->direccion}, {$this->num_ext}" . + ($this->num_int ? " Int. {$this->num_int}" : '') . + ", {$this->neighborhood->nombre ?? ''}, {$this->municipality->nombre ?? ''}, {$this->state->nombre ?? ''}, {$this->country->nombre ?? ''}"; + } +} diff --git a/Models/ContactableItem.PHP b/Models/ContactableItem.PHP new file mode 100644 index 0000000..e18d284 --- /dev/null +++ b/Models/ContactableItem.PHP @@ -0,0 +1,56 @@ + 'integer', + ]; + + /** + * Relación polimórfica con cualquier modelo (User, Empresa, etc.) + */ + public function contactable() + { + return $this->morphTo(); + } + + /** + * Devuelve la información del tipo de contacto desde `settings` + */ + public function getTypeInfoAttribute() + { + return app(ContactableItemService::class)->getTypeById($this->type); + } + + /** + * Devuelve el nombre del tipo de contacto + */ + public function getTypeTextAttribute() + { + return $this->type_info['label'] ?? 'Desconocido'; + } + + /** + * Devuelve el icono del tipo de contacto + */ + public function getTypeIconAttribute() + { + return $this->type_info['icon'] ?? 'ti-help-circle'; + } +} diff --git a/Providers/VuexyContactsServiceProvider.php b/Providers/VuexyContactsServiceProvider.php new file mode 100644 index 0000000..8e6e3ad --- /dev/null +++ b/Providers/VuexyContactsServiceProvider.php @@ -0,0 +1,53 @@ +loadRoutesFrom(__DIR__.'/../routes/admin.php'); + + + // Cargar vistas del paquete + $this->loadViewsFrom(__DIR__.'/../resources/views', 'vuexy-contacts'); + + + // Register the migrations + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + + + // Registrar Livewire Components + $components = [ + 'contact-index' => ContactIndex::class, + 'contact-show' => ContactShow::class, + 'contact-form' => ContactForm::class, + 'contact-offcanvas-form' => ContactOffCanvasForm::class, + ]; + + foreach ($components as $alias => $component) { + Livewire::component($alias, $component); + } + + + // Registrar auditoría en usuarios + //User::observe(AuditableObserver::class); + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4076b4 --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# 🎨 Laravel Vuexy Contacts - Vuexy Admin + +

+ Koneko Soluciones Tecnológicas Logo +

+

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

+ +--- + +## 📌 Descripción + +**Laravel Vuexy Contacts** 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-contacts +``` + +Publicar archivos de configuración y migraciones (si aplica): + +```bash +php artisan vendor:publish --tag=laravel-vuexy-contacts-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-contacts-config +``` + +Esto generará `config/laravel-vuexy-contacts.php`, donde puedes modificar valores predeterminados. + +--- + +## 🛠 Dependencias + +Este paquete requiere las siguientes dependencias: +- Laravel 11 +- `koneko/laravel-vuexy-contacts` +- 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-contacts-config +php artisan vendor:publish --tag=laravel-vuexy-contacts-seeders +php artisan migrate --seed +``` + +Para publicar imágenes del tema: + +```bash +php artisan vendor:publish --tag=laravel-vuexy-contacts-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-contacts)**. + +### 🔄 Sincronización con GitHub +- **Repositorio Principal:** [git.koneko.mx](https://git.koneko.mx/koneko/laravel-vuexy-contacts) +- **Repositorio en GitHub:** [github.com/koneko/laravel-vuexy-contacts](https://github.com/koneko/laravel-vuexy-contacts) +- **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-contacts/issues). +2. Para Pull Requests, **preferimos contribuciones en Tea**. Contacta a `admin@koneko.mx` para solicitar acceso. + +⚠️ **Nota:** Algunos cambios pueden tardar en reflejarse en GitHub, ya que este repositorio se actualiza automáticamente desde Tea. + +--- + +## 🏅 Licencia + +Este paquete es de código abierto bajo la licencia [MIT](LICENSE). + +--- + +

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

diff --git a/Services/ConstanciaFiscalService.php b/Services/ConstanciaFiscalService.php new file mode 100644 index 0000000..03860a3 --- /dev/null +++ b/Services/ConstanciaFiscalService.php @@ -0,0 +1,212 @@ +getClientOriginalExtension() !== 'pdf') { + throw new Exception('El archivo debe ser un PDF.'); + } + + $text = Pdf::getText($file->getRealPath()); + $text_array = array_values(array_filter(explode("\n", $text), 'trim')); // Limpia líneas vacías + + // **Estructura base con NULL en todos los campos** + $this->data = array_fill_keys([ + "rfc", "curp", "name", "last_name", "second_last_name", "nombre_comercial", "telefono", + "fecha_inicio_operaciones", "estatus_padron", "fecha_ultimo_cambio_estado", + "c_codigo_postal", "c_estado", "estado", "c_localidad", "localidad", + "c_municipio", "municipio", "c_colonia", "colonia", + "vialidad", "entre_calle", "num_ext", "num_int", "regimenes" + ], null); + + $this->data['regimenes'] = []; // Siempre inicializar como array + + $this->procesarDatosInline($text_array); + + $this->procesarRegimenes($text_array); + + if (empty($this->data['rfc'])) { + throw new Exception('No se encontró RFC en el documento.'); + } + + return $this->data; + } + + /** + * Procesa los datos generales hasta encontrar "Regímenes:". + */ + protected function procesarDatosInline(array $text_array) + { + foreach ($text_array as $index => $linea) { + $linea = trim($linea); + + if ($linea === "Regímenes:") { + return; // Detener la iteración + } + + // RFC, CURP, Nombre y Apellidos + if (str_contains($linea, "RFC:")) { + $this->data['rfc'] = trim($text_array[$index + 1]); + + } elseif (str_contains($linea, "CURP:")) { + $this->data['curp'] = trim($text_array[$index + 1]); + + } elseif (str_contains($linea, "Nombre (s):")) { + $this->data['name'] = trim($text_array[$index + 1]); + + } elseif (str_contains($linea, "Primer Apellido:")) { + $this->data['last_name'] = trim($text_array[$index + 1]); + + } elseif (str_contains($linea, "Segundo Apellido:")) { + $this->data['second_last_name'] = trim($text_array[$index + 1]); + } + + // Fechas y estatus + elseif (str_contains($linea, "Fecha inicio de operaciones:")) { + $this->data['fecha_inicio_operaciones'] = trim($text_array[$index + 1]); + + } elseif (str_contains($linea, "Estatus en el padrón:")) { + $this->data['estatus_padron'] = trim($text_array[$index + 1]); + + } elseif (str_contains($linea, "Fecha de último cambio de estado:")) { + $this->data['fecha_ultimo_cambio_estado'] = trim($text_array[$index + 1]); + } + + // Nombre Comercial + elseif (str_contains($linea, "Nombre Comercial:")) { + $this->data['nombre_comercial'] = trim($text_array[$index + 1]); + } + + // **Teléfono (Dos Líneas)** + elseif (str_contains($linea, "Tel. Fijo Lada:")) { + $lada = trim(str_replace("Tel. Fijo Lada:", "", $linea)); + + if (isset($text_array[$index + 1]) && str_contains($text_array[$index + 1], "Número:")) { + $numero = trim(str_replace("Número:", "", $text_array[$index + 1])); + $this->data['telefono'] = $lada . " " . $numero; + } + } + + // **Código Postal** + elseif (str_contains($linea, "Código Postal:")) { + $this->data['c_codigo_postal'] = trim(str_replace("Código Postal:", "", $linea)); + } + + // **Estado** + elseif (str_contains($linea, "Nombre de la Entidad Federativa:")) { + $estado_nombre = trim(str_replace("Nombre de la Entidad Federativa:", "", $linea)); + $estado = Estado::where('nombre_del_estado', 'like', "%{$estado_nombre}%")->first(); + + $this->data['c_estado'] = $estado->c_estado ?? null; + $this->data['estado'] = $estado_nombre; + } + + // **Municipio** + elseif (str_contains($linea, "Nombre del Municipio o Demarcación Territorial:")) { + $municipio_nombre = trim(str_replace("Nombre del Municipio o Demarcación Territorial:", "", $linea)); + $municipio = Municipio::where('descripcion', 'like', "%{$municipio_nombre}%")->first(); + + $this->data['c_municipio'] = $municipio->c_municipio ?? null; + $this->data['municipio'] = $municipio_nombre; + } + + // **Colonia** + elseif (str_contains($linea, "Nombre de la Colonia:")) { + $colonia_nombre = trim(str_replace("Nombre de la Colonia:", "", $linea)); + $colonia = Colonia::where('nombre_del_asentamiento', 'like', "%{$colonia_nombre}%")->first(); + + $this->data['c_colonia'] = $colonia->c_colonia ?? null; + $this->data['colonia'] = $colonia_nombre; + } + + // **Localidad** (Nueva implementación) + elseif (str_contains($linea, "Nombre de la Localidad:")) { + $localidad_nombre = trim(str_replace("Nombre de la Localidad:", "", $linea)); + $localidad = Localidad::where('descripcion', 'like', "%{$localidad_nombre}%")->first(); + + $this->data['c_localidad'] = $localidad->c_localidad ?? null; + $this->data['localidad'] = $localidad_nombre; + } + + // **Entre Calle** + elseif (str_contains($linea, "Entre Calle:")) { + $this->data['entre_calle'] = trim(str_replace("Entre Calle:", "", $linea)); + } + + // **Dirección** + elseif (str_contains($linea, "Nombre de Vialidad:")) { + $this->data['vialidad'] = trim(str_replace("Nombre de Vialidad:", "", $linea)); + + } elseif (str_contains($linea, "Número Exterior:")) { + $this->data['num_ext'] = trim(str_replace("Número Exterior:", "", $linea)); + + } elseif (str_contains($linea, "Número Interior:") && !str_contains($text_array[$index + 1], "Nombre de la Colonia:")) { + $this->data['num_int'] = trim(str_replace("Número Interior:", "", $linea)); + } + } + } + + /** + * Procesa los regímenes fiscales hasta encontrar "Obligaciones:". + */ + protected function procesarRegimenes(array $text_array) + { + $procesando = false; + + foreach ($text_array as $index => $linea) { + if (trim($linea) === "Regímenes:") { + $procesando = true; + continue; + } + + if (trim($linea) === "Obligaciones:") { + break; + } + + if ($procesando) { + // **Filtrar líneas no relevantes** + if (in_array($linea, ["Régimen", "Fecha Inicio", "Fecha Fin", "Obligaciones:"])) { + continue; + } + + // **Si la línea es una fecha, asociarla al régimen anterior** + if (preg_match('/\d{2}\/\d{2}\/\d{4}/', $linea)) { + $ultimo_regimen = &$this->data['regimenes'][count($this->data['regimenes']) - 1]; + if (isset($ultimo_regimen)) { + $ultimo_regimen['fecha_inicio'] = trim($linea); + } + continue; + } + + if (!empty($linea)) { + $regimenFiscal = RegimenFiscal::where('descripcion', 'like', "%{$linea}%")->first(); + $this->data['regimenes'][] = [ + 'regimen_fiscal' => trim($linea), + 'c_regimen_fiscal' => $regimenFiscal->c_regimen_fiscal ?? null, + 'fecha_inicio' => null, + ]; + } + } + } + } + +} diff --git a/Services/ContactCatalogService.php b/Services/ContactCatalogService.php new file mode 100644 index 0000000..33185f1 --- /dev/null +++ b/Services/ContactCatalogService.php @@ -0,0 +1,101 @@ + [ + 'table' => 'users', + 'key' => 'id', + 'value' => "CONCAT(name, ' ', COALESCE(last_name, ''), ' - ', email) as item", + 'search_columns' => ['name', 'last_name', 'email', 'rfc'], + 'order_by' => 'name', + 'limit' => 20, + ], + 'contacts' => [ + 'table' => 'users', + 'key' => 'id', + 'value' => "CONCAT(name, ' ', COALESCE(last_name, '')) as item", + 'search_columns' => ['name', 'last_name', 'rfc', 'company'], + 'extra_conditions' => ['is_customer' => true], + 'order_by' => 'name', + 'limit' => 20, + ], + 'providers' => [ + 'table' => 'users', + 'key' => 'id', + 'value' => "CONCAT(name, ' ', COALESCE(last_name, '')) as item", + 'search_columns' => ['name', 'last_name', 'rfc', 'company'], + 'extra_conditions' => ['is_provider' => true], + 'order_by' => 'name', + 'limit' => 20, + ], + 'addresses' => [ + 'table' => 'contactable_addresses', + 'key' => 'id', + 'value' => "CONCAT(direccion, ' #', num_ext, ' ', COALESCE(num_int, ''), ', ', c_codigo_postal) as item", + 'search_columns' => ['direccion', 'num_ext', 'c_codigo_postal'], + 'extra_conditions' => ['contactable_id', 'contactable_type'], + 'order_by' => 'direccion', + 'limit' => 10, + ], + 'emails' => [ + 'table' => 'contactable_items', + 'key' => 'id', + 'value' => 'data_contact as item', + 'search_columns' => ['data_contact'], + 'extra_conditions' => ['contactable_id', 'contactable_type', 'type' => 1], // 1 = email + 'order_by' => 'data_contact', + 'limit' => 10, + ], + ]; + + /** + * Método para obtener datos de catálogos con filtrado flexible. + * + * @param string $catalog + * @param string|null $searchTerm + * @param array $options + * @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']}"); + + // Aplicar condiciones extra + if (!empty($config['extra_conditions'])) { + foreach ($config['extra_conditions'] as $column => $value) { + if (isset($options[$column])) { + $query->where($column, $options[$column]); + } + } + } + + // Búsqueda + if ($searchTerm) { + $query->where(function ($subQ) use ($config, $searchTerm) { + foreach ($config['search_columns'] as $column) { + $subQ->orWhere($column, 'LIKE', "%{$searchTerm}%"); + } + }); + } + + // Orden y límite + $query->orderBy($config['order_by'] ?? $config['key'], 'asc'); + $query->limit($options['limit'] ?? $config['limit'] ?? 20); + + return $query->pluck('item', $config['key'])->toArray(); + } +} diff --git a/Services/ContactableItemService.php b/Services/ContactableItemService.php new file mode 100644 index 0000000..529015a --- /dev/null +++ b/Services/ContactableItemService.php @@ -0,0 +1,48 @@ +settingsKey)->first(); + + return $setting ? json_decode($setting->value, true) : []; + } + + /** + * Guarda una lista de tipos de medios de contacto. + * + * @param array $types + * @return bool + */ + public function saveContactableTypes(array $types): bool + { + return Setting::updateOrCreate( + ['key' => $this->settingsKey], + ['value' => json_encode($types)] + ) ? true : false; + } + + /** + * Obtiene un tipo de contacto específico por ID. + * + * @param int $id + * @return array|null + */ + public function getTypeById(int $id): ?array + { + $types = $this->getContactableTypes(); + return collect($types)->firstWhere('id', $id); + } +} diff --git a/Services/FacturaXmlService.php b/Services/FacturaXmlService.php new file mode 100644 index 0000000..98a2908 --- /dev/null +++ b/Services/FacturaXmlService.php @@ -0,0 +1,159 @@ +isValid()) { + throw new Exception("El archivo no es válido o está corrupto."); + } + + // Validamos que sea XML + if ($file->getClientOriginalExtension() !== 'xml') { + throw new Exception("Solo se permiten archivos XML."); + } + + // Cargar XML desde el contenido del archivo + $xmlContent = file_get_contents($file->getRealPath()); + $this->loadXml($xmlContent); + + // Extraer datos del XML + return $this->extractData(); + } + + /** + * Carga el XML y lo convierte en un objeto SimpleXMLElement + * + * @param string $xmlContent + * @throws Exception + */ + public function loadXml(string $xmlContent): void + { + try { + $this->xml = new SimpleXMLElement($xmlContent); + + // Registrar espacios de nombres + $this->xml->registerXPathNamespace('cfdi', 'http://www.sat.gob.mx/cfd/4'); + $this->xml->registerXPathNamespace('tfd', 'http://www.sat.gob.mx/TimbreFiscalDigital'); + + } catch (Exception $e) { + throw new Exception('Error al cargar el XML: ' . $e->getMessage()); + } + } + + + /** + * Extrae los datos clave de la factura XML + * + * @return array + */ + public function extractData(): array + { + if (!$this->xml) { + throw new Exception('No se ha cargado un XML válido.'); + } + + $this->data['comprobante'] = $this->extractComprobante(); + $this->data['emisor'] = $this->extractEmisor(); + $this->data['receptor'] = $this->extractReceptor(); + $this->data['conceptos'] = $this->extractConceptos(); + $this->data['impuestos'] = $this->extractImpuestos(); + $this->data['timbreFiscal'] = $this->extractTimbreFiscal(); + + return $this->data; + } + + protected function extractComprobante(): array + { + return [ + 'fecha' => (string) $this->xml['Fecha'] ?? null, + 'total' => (float) $this->xml['Total'] ?? 0.0, + 'moneda' => (string) $this->xml['Moneda'] ?? 'MXN', + 'tipoDeComprobante' => (string) $this->xml['TipoDeComprobante'] ?? null, + 'metodoPago' => (string) $this->xml['MetodoPago'] ?? null, + 'lugarExpedicion' => (string) $this->xml['LugarExpedicion'] ?? null, + ]; + } + + protected function extractEmisor(): array + { + $emisor = $this->xml->xpath('//cfdi:Emisor')[0] ?? null; + if (!$emisor) return []; + + return [ + 'rfc' => (string) $emisor['Rfc'] ?? null, + 'nombre' => (string) $emisor['Nombre'] ?? null, + 'regimenFiscal' => (string) $emisor['RegimenFiscal'] ?? null, + ]; + } + + protected function extractReceptor(): array + { + $receptor = $this->xml->xpath('//cfdi:Receptor')[0] ?? null; + if (!$receptor) return []; + + return [ + 'rfc' => (string) $receptor['Rfc'] ?? null, + 'nombre' => (string) $receptor['Nombre'] ?? null, + 'usoCFDI' => (string) $receptor['UsoCFDI'] ?? null, + 'regimenFiscal' => (string) $receptor['RegimenFiscalReceptor'] ?? null, + 'domicilioFiscal' => (string) $receptor['DomicilioFiscalReceptor'] ?? null, + ]; + } + + protected function extractConceptos(): array + { + $conceptos = []; + foreach ($this->xml->xpath('//cfdi:Concepto') as $concepto) { + $conceptos[] = [ + 'descripcion' => (string) $concepto['Descripcion'] ?? null, + 'cantidad' => (float) $concepto['Cantidad'] ?? 1.0, + 'valorUnitario' => (float) $concepto['ValorUnitario'] ?? 0.0, + 'importe' => (float) $concepto['Importe'] ?? 0.0, + 'claveProdServ' => (string) $concepto['ClaveProdServ'] ?? null, + 'claveUnidad' => (string) $concepto['ClaveUnidad'] ?? null, + ]; + } + return $conceptos; + } + + protected function extractImpuestos(): array + { + $impuestos = $this->xml->xpath('//cfdi:Impuestos')[0] ?? null; + if (!$impuestos) return []; + + return [ + 'totalImpuestosTrasladados' => (float) $impuestos['TotalImpuestosTrasladados'] ?? 0.0, + 'totalImpuestosRetenidos' => (float) $impuestos['TotalImpuestosRetenidos'] ?? 0.0, + ]; + } + + protected function extractTimbreFiscal(): array + { + $timbre = $this->xml->xpath('//tfd:TimbreFiscalDigital')[0] ?? null; + if (!$timbre) return []; + + return [ + 'uuid' => (string) $timbre['UUID'] ?? null, + 'fechaTimbrado' => (string) $timbre['FechaTimbrado'] ?? null, + 'selloCFD' => (string) $timbre['SelloCFD'] ?? null, + 'selloSAT' => (string) $timbre['SelloSAT'] ?? null, + ]; + } +} diff --git a/Traits/HasContactsAttributes.php b/Traits/HasContactsAttributes.php new file mode 100644 index 0000000..e431a8a --- /dev/null +++ b/Traits/HasContactsAttributes.php @@ -0,0 +1,77 @@ + 'RFC inválido', + self::TIPO_RFC_FISICA => 'Persona física', + self::TIPO_RFC_MORAL => 'Persona moral', + self::TIPO_RFC_PUBLICO => 'Público en general', + ]; + + /** + * Agrega atributos específicos al `$fillable` del modelo sin modificarlo directamente. + * + * @return array + */ + public function getFillable() + { + return array_merge(parent::getFillable(), [ + 'code', + 'parent_id', + 'agent_id', + 'company', + 'birth_date', + 'hire_date', + 'curp', + 'nss', + 'job_title', + 'rfc', + 'nombre_fiscal', + 'tipo_persona', + 'c_regimen_fiscal', + 'domicilio_fiscal', + 'c_uso_cfdi', + 'note', + 'is_partner', + 'is_employee', + 'is_prospect', + 'is_customer', + 'is_provider', + 'is_user', + ]); + } + + /** + * Detecta el tipo de RFC con base en el formato de caracteres. + */ + public function getTipoRfcAttribute(): int + { + $rfc = strtoupper(trim($this->attributes['rfc'] ?? '')); + + if ($rfc === 'XAXX010101000') { + return self::TIPO_RFC_PUBLICO; + } + + $longitud = strlen($rfc); + + if (!in_array($longitud, [12, 13])) { + return self::TIPO_RFC_INVALID; + } + + return ($longitud === 13) ? self::TIPO_RFC_FISICA : self::TIPO_RFC_MORAL; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b8f03f8 --- /dev/null +++ b/composer.json @@ -0,0 +1,38 @@ +{ + "name": "koneko/laravel-vuexy-contacts", + "description": "Laravel Vuexy Contacts, un modulo de administracion de contactos optimizado para México.", + "keywords": ["laravel", "koneko", "framework", "vuexy", "contacts", "admin", "mexico"], + "type": "library", + "license": "MIT", + "require": { + "php": "^8.2", + "koneko/laravel-sat-catalogs": "@dev", + "koneko/laravel-vuexy-admin": "@dev", + "laravel/framework": "^11.31", + "spatie/pdf-to-text": "^1.54" + }, + "autoload": { + "psr-4": { + "Koneko\\VuexyContacts\\": "" + } + }, + "extra": { + "laravel": { + "providers": [ + "Koneko\\VuexyContacts\\Providers\\VuexyContactsServiceProvider" + ] + } + }, + "authors": [ + { + "name": "Arturo Corro Pacheco", + "email": "arturo@koneko.mx" + } + ], + "support": { + "source": "https://github.com/koneko-mx/laravel-vuexy-contacts", + "issues": "https://github.com/koneko-mx/laravel-vuexy-contacts/issues" + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/database/migrations/2024_12_15_093544_add_contacts_columns_to_users_table.php b/database/migrations/2024_12_15_093544_add_contacts_columns_to_users_table.php new file mode 100644 index 0000000..57e5a9c --- /dev/null +++ b/database/migrations/2024_12_15_093544_add_contacts_columns_to_users_table.php @@ -0,0 +1,85 @@ +string('code', 50)->unique()->nullable()->comment('Código único del contacto')->after('id'); + $table->unsignedMediumInteger('parent_id')->nullable()->index()->after('code'); + $table->unsignedMediumInteger('agent_id')->nullable()->index()->after('parent_id'); + $table->string('company', 100)->nullable()->comment('Nombre de la empresa')->index()->after('email_verified_at'); + $table->date('birth_date')->nullable()->comment('Fecha de nacimiento')->after('company'); + $table->date('hire_date')->nullable()->comment('Fecha de contratación')->after('birth_date'); + $table->string('curp', 50)->nullable()->comment('Clave Única de Registro de Población (CURP)')->index()->after('hire_date'); + $table->string('nss', 11)->nullable()->comment('Número de seguridad social')->index()->after('curp'); + $table->string('job_title', 100)->nullable()->comment('Cargo del contacto en la empresa')->after('nss'); + $table->string('rfc', 13)->unique()->nullable()->index()->after('job_title'); + $table->string('nombre_fiscal')->nullable()->index()->after('rfc'); + $table->unsignedTinyInteger('tipo_persona')->nullable()->index()->after('nombre_fiscal'); + $table->unsignedSmallInteger('c_regimen_fiscal')->nullable()->index()->after('tipo_persona'); + $table->unsignedMediumInteger('domicilio_fiscal')->nullable()->index()->after('c_regimen_fiscal'); + $table->char('c_uso_cfdi', 4)->charset('ascii')->collation('ascii_general_ci')->nullable()->index()->after('domicilio_fiscal'); + $table->mediumText('note')->nullable()->after('c_uso_cfdi'); + + $table->unsignedTinyInteger('is_partner')->nullable()->index()->after('profile_photo_path'); + $table->unsignedTinyInteger('is_employee')->nullable()->index()->after('is_partner'); + $table->unsignedTinyInteger('is_prospect')->nullable()->index()->after('is_employee'); + $table->unsignedTinyInteger('is_customer')->nullable()->index()->after('is_prospect'); + $table->unsignedTinyInteger('is_provider')->nullable()->index()->after('is_customer'); + $table->unsignedTinyInteger('is_user')->nullable()->index()->after('is_provider'); + + $table->text('notes')->nullable()->after('is_user'); + + $table->foreign('parent_id') + ->references('id') + ->on('users') + ->onUpdate('restrict') + ->onDelete('restrict'); + + $table->foreign('agent_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_uso_cfdi') + ->references('c_uso_cfdi') + ->on('sat_uso_cfdi') + ->onUpdate('restrict') + ->onDelete('restrict'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['created_by', 'c_regimen_fiscal', 'domicilio_fiscal', 'c_uso_cfdi']); + $table->dropColumn([ + 'c_regimen_fiscal', 'domicilio_fiscal', 'c_uso_cfdi', + 'code', 'company', 'birth_date', 'hire_date', + 'curp', 'nss', 'job_title', 'rfc', 'nombre_fiscal', 'tipo_persona', + 'is_partner', 'is_employee', 'is_prospect', 'is_customer', 'is_provider', 'is_user' + ]); + }); + } +}; diff --git a/database/migrations/2024_12_15_100406_create_addresses_contacts_items_tables.php b/database/migrations/2024_12_15_100406_create_addresses_contacts_items_tables.php new file mode 100644 index 0000000..97a8c97 --- /dev/null +++ b/database/migrations/2024_12_15_100406_create_addresses_contacts_items_tables.php @@ -0,0 +1,99 @@ +mediumIncrements('id'); + + // Relación optimizada (sin morphs por rendimiento) + $table->unsignedMediumInteger('contactable_id')->index(); + $table->string('contactable_type')->index(); + + // Tipo de dirección + $table->unsignedTinyInteger('type')->index(); + + // Ubicación basada en el SAT + $table->char('c_pais', 3)->charset('ascii')->collation('ascii_general_ci')->nullable()->index(); + $table->unsignedMediumInteger('c_codigo_postal')->nullable()->index(); + $table->char('c_estado', 3)->charset('ascii')->collation('ascii_general_ci')->nullable()->index(); + $table->unsignedTinyInteger('c_localidad')->nullable()->index(); + $table->unsignedSmallInteger('c_municipio')->nullable()->index(); + $table->unsignedMediumInteger('c_colonia')->nullable()->index(); + + // Datos de la dirección + $table->string('direccion')->nullable(); + $table->string('num_ext')->nullable(); + $table->string('num_int')->nullable(); + $table->string('referencia')->nullable(); + $table->decimal('lat', 9, 6)->nullable(); + $table->decimal('lng', 9, 6)->nullable(); + + // Preferencia + $table->unsignedTinyInteger('preference_level')->nullable(); + + // Notas o comentarios + $table->text('notes')->nullable(); // Nuevo campo para comentarios + + // Autoría + $table->timestamps(); + + // Índices + $table->index(['contactable_type', 'contactable_id']); + $table->index(['contactable_type', 'contactable_id', 'type']); + $table->index(['c_municipio', 'c_estado']); + + // Relaciones con catálogos SAT + $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'); + }); + + Schema::create('contactable_items', function (Blueprint $table) { + $table->mediumIncrements('id'); + + // Relación optimizada + $table->unsignedMediumInteger('contactable_id')->index(); + $table->string('contactable_type')->index(); + + // Tipo de medio de contacto (ej. 1=Email, 2=Teléfono, 3=WhatsApp) + $table->unsignedTinyInteger('type')->index(); + + // Dato de contacto (ej. email o teléfono) + $table->string('data_contact')->index(); + + // Preferencia + $table->unsignedTinyInteger('preference_level')->nullable(); + + // Notas o comentarios + $table->text('notes')->nullable(); // Nuevo campo para comentarios + + // Autoría + $table->timestamps(); + + // Índices + $table->index(['contactable_type', 'contactable_id']); + $table->index(['contactable_type', 'contactable_id', 'type']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contactable_addresses'); + Schema::dropIfExists('contactable_items'); + } +}; diff --git a/resources/assets/js/addresses/AddressFormHandler.js b/resources/assets/js/addresses/AddressFormHandler.js new file mode 100644 index 0000000..49df803 --- /dev/null +++ b/resources/assets/js/addresses/AddressFormHandler.js @@ -0,0 +1,519 @@ +export default class AddressFormHandler { + constructor(formSelectors, ajaxRoutes, livewireInstance, csrfToken) { + if (!ajaxRoutes && !ajaxRoutes.estado) { + throw new Error("ajax AddressFormHandler routes not found"); + } + + this.formSelectors = formSelectors; + this.ajaxRoutes = ajaxRoutes; + this.livewireInstance = livewireInstance; // Livewire Instance + this.csrfToken = csrfToken; + this.placeholders = { + pais: 'Selecciona el país', + estado: 'Selecciona el estado', + localidad: 'Selecciona la localidad', + municipio: 'Selecciona el municipio', + colonia: 'Selecciona la colonia', + codigo_postal: 'Ingresa el código postal', + } + + this.synchronizedZipCode = false; + + this.initializeSelects(); + this.initializeZipCodeInput(); + this.loadDefaultValuesFromLivewire(); + } + + + initializeSelects() { + const { c_codigo_postal, c_pais, c_estado, c_localidad, c_municipio, c_colonia } = this.formSelectors; + + // País + $(c_pais) + .select2({ + language: "es", + placeholder: this.placeholders.pais, + allowClear: true, + width: "100%" + }) + .on('select2:select', (e) => this.onCountrySelect(e)) + .on('select2:clear', () => { + this.resetFieldWithDependencies('c_pais'); + }); + + // Estado + $(c_estado) + .select2({ + language: "es", + placeholder: this.placeholders.estado, + allowClear: true, + width: "100%" + }) + .on('select2:select', (e) => this.onStateSelect(e)) + .on('select2:clear', () => { + this.resetFieldWithDependencies('c_estado'); + }); + + // Localidad + $(c_localidad) + .select2({ + language: "es", + placeholder: this.placeholders.localidad, + allowClear: true, + width: "100%" + }) + .on('select2:select', (e) => this.onLocalitySelect(e)) + .on('select2:clear', () => { + this.resetFieldWithDependencies('c_localidad'); + }); + + // Municipio + $(c_municipio) + .select2({ + ajax: { + url: this.ajaxRoutes['municipio'], + type: "post", + delay: 250, + dataType: 'json', + data: (params) => ({ + _token: this.csrfToken, + select2Mode: true, + searchTerm: params.term, + c_estado: $(c_estado).val(), + }), + processResults: (response) => ({ + results: response + }), + cache: true + }, + language: "es", + placeholder: this.placeholders.municipio, + allowClear: true, + width: "100%" + }) + .on('select2:select', (e) => this.onMunicipalitySelect(e)) + .on('select2:clear', () => { + this.resetFieldWithDependencies('c_municipio'); + }); + + // Colonia + $(c_colonia) + .select2({ + ajax: { + url: this.ajaxRoutes['colonia'], + type: "post", + delay: 250, + dataType: 'json', + data: (params) => ({ + _token: this.csrfToken, + select2Mode: true, + searchTerm: params.term, + c_codigo_postal: $(c_codigo_postal).val(), + c_estado: $(c_estado).val(), + c_municipio: $(c_municipio).val(), + }), + processResults: (response) => ({ + results: response + }), + cache: true + }, + language: "es", + placeholder: this.placeholders.colonia, + allowClear: true, + width: "100%" + }) + .on('select2:select', (e) => this.onColoniaSelect(e)) + .on('select2:clear', () => { + this.livewireInstance.set('c_colonia', null, false); + }); + } + + initializeZipCodeInput() { + const { c_codigo_postal } = this.formSelectors; + + let lastPostalCode = ''; // Para evitar solicitudes duplicadas + let debounceTimeout; + + $(c_codigo_postal).on('input', () => { + const postalCode = $(c_codigo_postal).val(); + + clearTimeout(debounceTimeout); // Cancelar el temporizador anterior + + this.synchronizedZipCode = false; + + debounceTimeout = setTimeout(() => { + if (postalCode.length === 5 && postalCode !== lastPostalCode) { + lastPostalCode = postalCode; + + this.fetchZipCode(postalCode); + } + }, 300); // Ejecutar después de 300 ms de inactividad + }); + } + + async fetchZipCode(postalCode) { + try { + const response = await this.makeAjaxPost(this.ajaxRoutes.codigo_postal, { + _token: this.csrfToken, + searchTerm: postalCode, + firstRow: true, + rawMode: true + }); + + // Realizar solicitud AJAX para obtener datos del código postal + if (response.c_codigo_postal) { + // Limpiamos Interface + this.resetFieldWithDependencies('c_codigo_postal', response.c_codigo_postal); + + // Cargamos Estados + this.toggleSelector('c_estado', response.c_estado); + + // Cargar localidades + this.fillSelectOptions('c_localidad', {[response.c_localidad]: response.localidad }, response.c_localidad); + + // Cargar municipios + this.fillSelectOptions('c_municipio', {[response.c_municipio]: response.municipio}, response.c_municipio); + + // Abrir select de colonia + this.openSelect2('c_colonia'); + + // Marcar como sincronizado el código postal + this.synchronizedZipCode = true; + + } else { + const { notification } = this.formSelectors; + + // Emitir una notificación simple desde JavaScript + window.livewireNotification.emitNotification({ + message: `Código postal ${postalCode} no encontrado.`, + type: 'warning', + target: notification, + }); + } + + } catch (error) { + console.error("Error al obtener datos del código postal:", error); + } + } + + + onCountrySelect(event) { + const countryCode = event.params.data.id; + + // Limpiamos Interface + this.resetFieldWithDependencies('c_pais', countryCode); + + // Cargamos Interface de estados + this.fetchStates(countryCode); + } + + async fetchStates(countryCode) { + try { + const response = await this.makeAjaxPost(this.ajaxRoutes.estado, { + _token: this.csrfToken, + c_pais: countryCode + }); + + // Realizamos solicitud AJAX para obtener datos de los estados + if (response) { + const { c_codigo_postal } = this.formSelectors; + + // Cargar los estados + this.fillSelectOptions('c_estado', response); + + // Ocultar y resetear los campos si no es México + if (countryCode === 'MEX') { + $('.if_local_address_show').show(); + + // Colocar el foco en el código postal + $(c_codigo_postal).focus(); + + } else { + $('.if_local_address_show').hide(); + + // Abrir select de estado + this.openSelect2('c_estado'); + } + } + + } catch (error) { + console.error("Error al cargar los estados:", error); + } + } + + + onStateSelect(event) { + const { c_pais, direccion } = this.formSelectors; + const stateId = event.params.data.id; + + // Limpiar Interface + this.resetFieldWithDependencies('c_estado', stateId); + + // Si es México + if($(c_pais).val() == 'MEX'){ + // Cargar localidades + this.fetchLocalities(stateId); + + }else{ + // Colocar el foco en la dirección + $(direccion).focus(); + } + } + + async fetchLocalities(stateId) { + try { + const response = await this.makeAjaxPost(this.ajaxRoutes.localidad, { + _token: this.csrfToken, + c_estado: stateId, + limit: null, + }); + + if (response) { + const { c_localidad } = this.formSelectors; + + // Cargar localidades + this.fillSelectOptions('c_localidad', response, c_localidad); + + // Abrir select de localidad + this.openSelect2('c_localidad'); + } + + } catch (error) { + console.error("Error al cargar las localidades:", error); + } + } + + + onLocalitySelect(event) { + const locationId = event.params.data.id; + + // Limpiar Interface + this.resetFieldWithDependencies('c_localidad', locationId); + + // Abrir select de municipio + this.openSelect2('c_municipio'); + } + + + onMunicipalitySelect(event) { + const municipalityId = event.params.data.id; + + // Limpiar Interface + this.resetFieldWithDependencies('c_municipio', municipalityId); + + // Habilitamos colonias + this.toggleSelector('c_colonia'); + + // Abrir select colonia + this.openSelect2('c_colonia'); + } + + + onColoniaSelect(event) { + const coloniaId = event.params.data.id; + + // Cargar colonia + this.fillSelectOptions('c_colonia', {[coloniaId]: event.params.data.text}, coloniaId); + + // Actualizar código postal si no está sincronizado + if(!this.synchronizedZipCode){ + this.fetchZipCodeByData(this.livewireInstance.c_estado, this.livewireInstance.c_municipio, coloniaId); + } + } + + async fetchZipCodeByData(stateId, municipalityId, coloniaId) { + try { + const response = await this.makeAjaxPost(this.ajaxRoutes.colonia, { + _token: this.csrfToken, + c_estado: stateId, + c_municipio: municipalityId, + c_colonia: coloniaId, + firstRow: true, + rawMode: true, + }); + + if (response) { + const { c_codigo_postal, direccion } = this.formSelectors; + + // Actualizar código postal si no está sincronizado + if($(c_codigo_postal).val() !== response.c_codigo_postal){ + $(c_codigo_postal).val(response.c_codigo_postal); + + // Actualizar en Livewire + this.livewireInstance.set('c_codigo_postal', response.c_codigo_postal, false); + } + + // Marcar como sincronizado el código postal + this.synchronizedZipCode = true; + + // Abrir select colonia + $(direccion).focus(); + } + + } catch (error) { + console.error("Error al cargar las localidades:", error); + } + } + + + + fillSelectOptions(selector, data, selected) { + const placeholder = this.placeholders[selector] || 'Selecciona una opción'; + const $selector = $(this.formSelectors[selector]); + + // Actualizar las opciones directamente en Livewire + this.livewireInstance.set(`${selector}_options`, data, false); + + // Limpiar y agregar las nuevas opciones al selector + $selector.empty().append(new Option(placeholder, '', true, true)); // Agregar el placeholder + + // Agregar opciones obtenidas por AJAX + if (typeof data === 'object' && Object.keys(data).length > 0) { + $.each(data, (id, value) => { + $selector.append(new Option(value, id, false, id == selected)); + }); + } + + // Actualizar el valor seleccionado si corresponde + if (selected) { + this.livewireInstance.set(selector, selected, false); + + $selector.val(selected).trigger('change.select2'); + } + + // Habilitar el select + $selector.prop('disabled', false).trigger('change.select2'); + } + + toggleSelector(selector, selected = null) { + const $selector = $(this.formSelectors[selector]); + + if(typeof selected === 'number' || typeof selected === 'string'){ + this.livewireInstance.set(selector, selected, false); + + $selector + .val(selected) + .trigger('change.select2'); + } + + $selector.prop('disabled', !(selected || $selector.val())).trigger('change.select2'); + } + + resetFieldWithDependencies(field, value = null) { + const dependencies = { + c_codigo_postal: ['c_localidad', 'c_municipio', 'c_colonia'], + c_pais: ['c_codigo_postal', 'c_estado', 'c_localidad', 'c_municipio', 'c_colonia'], + c_estado: ['c_codigo_postal', 'c_localidad', 'c_municipio', 'c_colonia'], + c_localidad: ['c_codigo_postal', 'c_municipio', 'c_colonia'], + c_municipio: ['c_codigo_postal', 'c_colonia'] + }; + + const resetFields = (fields) => { + fields.forEach((key) => { + const field = this.formSelectors[key]; // Obtener el selector por clave + const placeholder = this.placeholders[key] || 'Selecciona una opción'; + const $field = $(field); + + // Limpiar valor en Livewire + if (this.livewireInstance[key] !== null) { + this.livewireInstance.set(key, null, false); + } + + if ($field.is('select')) { + // Resetear select + $field.empty() + .append(new Option(placeholder, '', true, true)) + .prop('disabled', true) + .trigger('change.select2'); // Actualizar select2 + + } else if ($field.is('input[type="text"]') || $field.is('input[type="number"]')) { + // Resetear input de texto o número + $field.val(''); + + } else { + console.warn(`El campo ${field} no es un input ni un select válido.`); + } + }); + }; + + // Limpieza de campos dependientes + if (dependencies[field]) { + resetFields(dependencies[field]); + } + + // Actualizar el valor del campo principal en Livewire + this.livewireInstance.set(field, value, false); + } + + openSelect2(selector) { + const $selector = $(this.formSelectors[selector]); + + $selector.prop('disabled', false).trigger('change.select2'); + + setTimeout(() => $selector.select2('open'), 100); + } + + async makeAjaxPost(url, data) { + try { + const response = await $.post(url, { + ...data, + _token: this.csrfToken + }); + + return response; + + } catch (error) { + console.error(`Error al realizar la solicitud AJAX a ${url}:`, error); + + window.livewireNotification.emitNotification({ + message: `Error al intentar realizar la solicitud.`, + target: this.formSelectors.notification, + type: 'danger', + }); + + return null; // Devuelve null en caso de error para evitar llamadas infinitas + } + } + + loadDefaultValuesFromLivewire() { + const { c_pais, c_estado, c_localidad, c_municipio, c_colonia, c_codigo_postal } = this.livewireInstance; + const { c_pais: paisSelector, c_estado: estadoSelector, c_localidad: localidadSelector, c_municipio: municipioSelector, c_colonia: coloniaSelector, c_codigo_postal: codigoPostalSelector } = this.formSelectors; + + // Cargar país por defecto + if (c_pais && $(paisSelector).val() !== c_pais) { + $(paisSelector).val(c_pais).trigger('change.select2'); + } + + // Cargar estados por defecto si están disponibles + if (c_estado && $(estadoSelector).val() !== c_estado) { + $(estadoSelector).val(c_estado).trigger('change.select2'); + } + + // Si el país es México, mostrar campos de dirección + if (c_pais === 'MEX') { + $('.if_local_address_show').show(); + + if (c_localidad && $(localidadSelector).val() !== c_localidad) { + $(localidadSelector).val(c_localidad).trigger('change.select2'); + } + + if (c_municipio && $(municipioSelector).val() !== c_municipio) { + $(municipioSelector).val(c_municipio).trigger('change.select2'); + } + + if (c_colonia && $(coloniaSelector).val() !== c_colonia) { + $(coloniaSelector).val(c_colonia).trigger('change.select2'); + } + + if (c_codigo_postal && $(codigoPostalSelector).val() !== c_codigo_postal) { + $(codigoPostalSelector).val(c_codigo_postal); + } + + } else { + $('.if_local_address_show').hide(); + } + } + +} + +window.AddressFormHandler = AddressFormHandler; diff --git a/resources/assets/js/bootstrap-table/contactsFormatters.js b/resources/assets/js/bootstrap-table/contactsFormatters.js new file mode 100644 index 0000000..68560a2 --- /dev/null +++ b/resources/assets/js/bootstrap-table/contactsFormatters.js @@ -0,0 +1,93 @@ +import {routes} from '../../../../../laravel-vuexy-admin/resources/assets/js/bootstrap-table/globalConfig.js'; + + + +export const contactActionFormatter = (value, row, index) => { + if (!row.id) return ''; + + const showUrl = routes['admin.contact.show'].replace(':id', row.id); + const editUrl = routes['admin.contact.edit'].replace(':id', row.id); + const deleteUrl = routes['admin.contact.delete'].replace(':id', row.id); + + return ` + + `.trim(); +}; + +export const agentFormatter = (value, row, index) => { + if (!row.agent_name) return ''; + + const email = row.agent_email || 'Sin correo'; + const userUrl = routes['admin.user.show'].replace(':id', row.id); + + return ` +
+ ${row.agent_name} + ${email} +
+ `; +}; + +export const contactParentFormatter = (value, row, index) => { + if (!row.parent_name) return ''; + + const email = row.parent_email || 'Sin correo'; + const showUrl = routes['admin.contact.show'].replace(':id', row.id); + + return ` +
+ ${row.parent_name} + ${email} +
+ `; +}; + + + + + +export const emailFormatter = (value, row, index) => { + if (!value) return ''; + return ` + + + ${value} + + `; +}; + +export const telFormatter = (value, row, index) => { + if (!value) return ''; + return ` + + + ${value} + + `; +}; + +export const direccionFormatter = (value, row, index) => { + let direccion = row.direccion ? row.direccion.trim() : ''; + let numExt = row.num_ext ? ` #${row.num_ext}` : ''; + let numInt = row.num_int ? `, Int. ${row.num_int}` : ''; + + let fullAddress = `${direccion}${numExt}${numInt}`.trim(); + + return fullAddress ? `${fullAddress}` : 'Sin dirección'; +}; + + + + + + diff --git a/resources/views/components/card/address.blade.php b/resources/views/components/card/address.blade.php new file mode 100644 index 0000000..80e0dd6 --- /dev/null +++ b/resources/views/components/card/address.blade.php @@ -0,0 +1,29 @@ +@props([ + 'uid' => uniqid(), + 'paisOptions' => [], + 'estadoOptions' => [], + 'localidadOptions' => [], + 'municipioOptions' => [], + 'coloniaOptions' => [], +]) + + +
+ +
+ + +
+ + + + + + + + +
+ + +
+
diff --git a/resources/views/components/card/location.blade.php b/resources/views/components/card/location.blade.php new file mode 100644 index 0000000..e1c6cdf --- /dev/null +++ b/resources/views/components/card/location.blade.php @@ -0,0 +1,15 @@ +@props([ + 'uid' => uniqid(), + 'mapId' => 'geo_map', + 'mapHeight' => '400px', + 'searchPlaceholder' => 'Buscar ubicación', +]) + + + +
+ + +
+
+
diff --git a/resources/views/contacts/crud.blade.php b/resources/views/contacts/crud.blade.php new file mode 100644 index 0000000..d602c9c --- /dev/null +++ b/resources/views/contacts/crud.blade.php @@ -0,0 +1,28 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', $contacto->name) + +@section('vendor-style') + @vite([ + 'resources/assets/admin/vendor/libs/datatables-bs5/datatables.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/select2/select2.scss', + 'resources/assets/admin/vendor/libs/dropzone/dropzone.scss', + ]) +@endsection + +@section('content') + @livewire('contact-form', ['userId' => $contacto->id]) +@endsection + +@section('vendor-script') + @vite([ + 'resources/assets/admin/vendor/libs/moment/moment.js', + 'resources/assets/admin/vendor/libs/datatables-bs5/datatables-bootstrap5.js', + 'resources/assets/admin/vendor/libs/datatables-bs5/datatable-lang-es.js', + 'resources/assets/admin/vendor/libs/dropzone/dropzone.js', + 'resources/assets/admin/vendor/libs/select2/select2.js', + 'resources/assets/admin/vendor/libs/jquery-validation/jquery.validate.js', + ]) +@endsection diff --git a/resources/views/contacts/index.blade.php b/resources/views/contacts/index.blade.php new file mode 100644 index 0000000..d94e0b9 --- /dev/null +++ b/resources/views/contacts/index.blade.php @@ -0,0 +1,31 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Contactos') + +@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/dropzone/dropzone.scss', + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + ]) +@endsection + +@push('page-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/bootstrap-table/bootstrapTableManager.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/forms/formConvasHelper.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/dropzone/dropzone.js', + ]) +@endpush + +@section('content') + @livewire('contact-index') + @livewire('contact-offcanvas-form') +@endsection diff --git a/resources/views/contacts/show.blade.php b/resources/views/contacts/show.blade.php new file mode 100644 index 0000000..1aa7398 --- /dev/null +++ b/resources/views/contacts/show.blade.php @@ -0,0 +1,28 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', $contact->name) + +@section('vendor-style') + @vite([ + 'resources/assets/admin/vendor/libs/datatables-bs5/datatables.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/select2/select2.scss', + 'resources/assets/admin/vendor/libs/dropzone/dropzone.scss', + ]) +@endsection + +@section('content') + +@endsection + +@section('vendor-script') + @vite([ + 'resources/assets/admin/vendor/libs/moment/moment.js', + 'resources/assets/admin/vendor/libs/datatables-bs5/datatables-bootstrap5.js', + 'resources/assets/admin/vendor/libs/datatables-bs5/datatable-lang-es.js', + 'resources/assets/admin/vendor/libs/dropzone/dropzone.js', + 'resources/assets/admin/vendor/libs/select2/select2.js', + 'resources/assets/admin/vendor/libs/jquery-validation/jquery.validate.js', + ]) +@endsection diff --git a/resources/views/employees/index.blade.php b/resources/views/employees/index.blade.php new file mode 100644 index 0000000..7dad7cc --- /dev/null +++ b/resources/views/employees/index.blade.php @@ -0,0 +1,28 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Empleados') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.scss', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/bootstrap-table/bootstrap-table.scss', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/fonts/bootstrap-icons.scss', + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + ]) +@endsection + +@push('page-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/bootstrap-table/bootstrapTableManager.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/forms/formConvasHelper.js', + ]) +@endpush + +@section('content') + @livewire('employee-index') +@endsection diff --git a/resources/views/employees/show.blade.php b/resources/views/employees/show.blade.php new file mode 100644 index 0000000..698b1dc --- /dev/null +++ b/resources/views/employees/show.blade.php @@ -0,0 +1,30 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', $contacto->name) + +@section('vendor-style') + @vite([ + 'resources/assets/admin/vendor/libs/datatables-bs5/datatables.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss', + 'resources/assets/admin/vendor/libs/select2/select2.scss', + 'resources/assets/admin/vendor/libs/dropzone/dropzone.scss', + ]) +@endsection + +@section('content') +
+ @livewire('admin.crm.contact-view', ['userId' => $contacto->id]) +
+@endsection + +@section('vendor-script') + @vite([ + 'resources/assets/admin/vendor/libs/moment/moment.js', + 'resources/assets/admin/vendor/libs/datatables-bs5/datatables-bootstrap5.js', + 'resources/assets/admin/vendor/libs/datatables-bs5/datatable-lang-es.js', + 'resources/assets/admin/vendor/libs/dropzone/dropzone.js', + 'resources/assets/admin/vendor/libs/select2/select2.js', + 'resources/assets/admin/vendor/libs/jquery-validation/jquery.validate.js', + ]) +@endsection diff --git a/resources/views/livewire/contacts/contacts-index.blade copy.php b/resources/views/livewire/contacts/contacts-index.blade copy.php new file mode 100644 index 0000000..6db8bb3 --- /dev/null +++ b/resources/views/livewire/contacts/contacts-index.blade copy.php @@ -0,0 +1,586 @@ +
+
+ +
+
+
+
+ +
+
+
+

+ +

+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+ +
+ +@push('page-script') + +@endpush diff --git a/resources/views/livewire/contacts/form.blade.php b/resources/views/livewire/contacts/form.blade.php new file mode 100644 index 0000000..e7ae0ae --- /dev/null +++ b/resources/views/livewire/contacts/form.blade.php @@ -0,0 +1,165 @@ +
+ + + + +
+
+ {{-- Identificación --}} + + + + + + + {{-- Series de facturación --}} + + + + + + + {{-- Configuraciones --}} + + + + + +
+
+ {{-- Información de contacto --}} + + + + + + + + {{-- Información fiscal --}} + + + + + + +
+
+ {{-- Dirección --}} + + + {{-- Ubicación --}} + +
+
+
+
+ +@push('page-script') + +@endpush diff --git a/resources/views/livewire/contacts/index.blade.php b/resources/views/livewire/contacts/index.blade.php new file mode 100644 index 0000000..78413f9 --- /dev/null +++ b/resources/views/livewire/contacts/index.blade.php @@ -0,0 +1,57 @@ + + +
+ +
+
+ +
+ +
+
+
+ + @push('page-script') + + @endpush diff --git a/resources/views/livewire/contacts/offcanvas-form.blade.php b/resources/views/livewire/contacts/offcanvas-form.blade.php new file mode 100644 index 0000000..6faba7a --- /dev/null +++ b/resources/views/livewire/contacts/offcanvas-form.blade.php @@ -0,0 +1,94 @@ +
+ + {{-- Dropzone Constancia de Situación Fiscal --}} + + + + + + + + {{-- Selección de Sucursal --}} + + + +
+ +
+ +
+ + {{-- Teléfonos y Correos --}} + + + +
+ + + +
+ + {{-- Estado del Centro de Trabajo --}} + + + + + + +
+ +
+
+
+ +@push('page-script') + +@endpush diff --git a/resources/views/livewire/contacts/show.blade.php b/resources/views/livewire/contacts/show.blade.php new file mode 100644 index 0000000..8a49bd8 --- /dev/null +++ b/resources/views/livewire/contacts/show.blade.php @@ -0,0 +1,1685 @@ +
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + +
idNameEmailDateSalaryStatusAction
+
+
+
+
+
+
+ + + + + + + + + + + + + +
idNameEmailDateSalaryStatusAction
+
+
+
+
+
+
+ + + + + + + + + + + + + +
idNameEmailDateSalaryStatusAction
+
+
+
+
+
+
+ + + + + + + + + + + + + +
idNameEmailDateSalaryStatusAction
+
+
+
+
+
+
+ + + + + + + + + + + + + +
idNameEmailDateSalaryStatusAction
+
+
+
+
+
+ +
+ +@push('page-script') + +@endpush diff --git a/resources/views/suppliers/index.blade.php b/resources/views/suppliers/index.blade.php new file mode 100644 index 0000000..f7c1c82 --- /dev/null +++ b/resources/views/suppliers/index.blade.php @@ -0,0 +1,28 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', 'Proveedores') + +@section('vendor-style') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.scss', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/bootstrap-table/bootstrap-table.scss', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/fonts/bootstrap-icons.scss', + ]) +@endsection + +@section('vendor-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/vendor/libs/select2/select2.js', + ]) +@endsection + +@push('page-script') + @vite([ + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/bootstrap-table/bootstrapTableManager.js', + 'vendor/koneko/laravel-vuexy-admin/resources/assets/js/forms/formConvasHelper.js', + ]) +@endpush + +@section('content') + @livewire('supplier-index') +@endsection diff --git a/resources/views/suppliers/show.blade.php b/resources/views/suppliers/show.blade.php new file mode 100644 index 0000000..41088fb --- /dev/null +++ b/resources/views/suppliers/show.blade.php @@ -0,0 +1,27 @@ +@extends('vuexy-admin::layouts.vuexy.layoutMaster') + +@section('title', $contacto->name) + +@section('vendor-style') + @vite([ + 'resources/assets/admin/vendor/libs/select2/select2.scss', + 'resources/assets/admin/vendor/libs/dropzone/dropzone.scss', + ]) +@endsection + +@section('content') +
+ @livewire('admin.crm.contact-view', ['userId' => $contacto->id]) +
+@endsection + +@section('vendor-script') + @vite([ + 'resources/assets/admin/vendor/libs/moment/moment.js', + 'resources/assets/admin/vendor/libs/datatables-bs5/datatables-bootstrap5.js', + 'resources/assets/admin/vendor/libs/datatables-bs5/datatable-lang-es.js', + 'resources/assets/admin/vendor/libs/dropzone/dropzone.js', + 'resources/assets/admin/vendor/libs/select2/select2.js', + 'resources/assets/admin/vendor/libs/jquery-validation/jquery.validate.js', + ]) +@endsection diff --git a/routes/admin.php b/routes/admin.php new file mode 100644 index 0000000..9decfdc --- /dev/null +++ b/routes/admin.php @@ -0,0 +1,59 @@ +name('admin.contacts.')->middleware(['web', 'auth', 'admin'])->group(function () { + // Contactos + Route::controller(ContactController::class)->prefix('contactos/contactos')->name('contacts.')->group(function () { + Route::get('/', 'index')->name('index'); + Route::get('create', 'create')->name('create'); + Route::get('{contact}', 'show')->name('show'); + Route::get('{contact}/delete', 'delete')->name('delete'); + Route::get('{contact}/edit', 'edit')->name('edit'); + }); + + // Contactos + Route::controller(ContactController::class)->prefix('contactos')->group(function () { + Route::post('extraer-datos-pdf-constancia', 'extraerDataConstancia')->name('extraer-datos-pdf-constancia'); + }); + + // Proveedores + Route::controller(SupplierController::class)->prefix('inventario-y-logistica/proveedores')->name('suppliers.')->group(function () { + Route::get('/', 'index')->name('index'); // Listar + Route::get('create', 'create')->name('create'); // Formulario de creación + Route::post('proveedores', 'store')->name('store'); // Guardar + Route::get('{supplier}', 'show')->name('show'); // Ver + Route::get('{supplier}/edit', 'edit')->name('edit'); // Formulario de edición + Route::put('{supplier}', 'update')->name('update'); // Actualizar + Route::delete('{supplier}', 'destroy')->name('destroy'); // Eliminar + }); + + // Clientes + Route::controller(CustomerController::class)->prefix('ventas/clientes')->name('customers.')->group(function () { + Route::get('/', 'index')->name('index'); // Listar + Route::get('create', 'create')->name('create'); // Formulario de creación + Route::post('clientes', 'store')->name('store'); // Guardar + Route::get('{customer}', 'show')->name('show'); // Ver + Route::get('{customer}/edit', 'edit')->name('edit'); // Formulario de edición + Route::put('{customer}', 'update')->name('update'); // Actualizar + Route::delete('{customer}', 'destroy')->name('destroy'); // Eliminar + }); + + // Empleados + Route::controller(EmployeeController::class)->prefix('rrhh/empleados')->name('employees.')->group(function () { + Route::get('/', 'index')->name('index'); // Listar + Route::get('create', 'create')->name('create'); // Formulario de creación + Route::post('empleados', 'store')->name('store'); // Guardar + Route::get('{employee}', 'show')->name('show'); // Ver + Route::get('{employee}/edit', 'edit')->name('edit'); // Formulario de edición + Route::put('{employee}', 'update')->name('update'); // Actualizar + Route::delete('{employee}', 'destroy')->name('destroy'); // Eliminar + }); + +});