diff --git a/ci4/app/Config/Email.php b/ci4/app/Config/Email.php index 43b3edb0..9447a491 100755 --- a/ci4/app/Config/Email.php +++ b/ci4/app/Config/Email.php @@ -6,7 +6,7 @@ use CodeIgniter\Config\BaseConfig; class Email extends BaseConfig { - public string $fromEmail = 'safekat@imnavajas.es'; + public string $fromEmail = 'soporte_erp@safekat.es'; public string $fromName = 'Safekat ERP'; public string $recipients = ''; @@ -28,27 +28,27 @@ class Email extends BaseConfig /** * SMTP Server Hostname */ - public string $SMTPHost = 'imnavajas.es'; + public string $SMTPHost = 'smtp.ionos.es'; /** * SMTP Username */ - public string $SMTPUser = 'safekat@imnavajas.es'; + public string $SMTPUser = 'soporte_erp@safekat.es'; /** * SMTP Password */ - public string $SMTPPass = 'Etkd9~448'; + public string $SMTPPass = 'H%5&qDkDkWnfLTGN'; /** * SMTP Port */ - public int $SMTPPort = 25; + public int $SMTPPort = 465; /** * SMTP Timeout (in seconds) */ - public int $SMTPTimeout = 5; + public int $SMTPTimeout = 15; /** * Enable persistent SMTP connections diff --git a/ci4/app/Config/RBAC/permissionMatrix.php b/ci4/app/Config/RBAC/permissionMatrix.php index ce466841..c9f0a2ee 100644 --- a/ci4/app/Config/RBAC/permissionMatrix.php +++ b/ci4/app/Config/RBAC/permissionMatrix.php @@ -89,6 +89,9 @@ const SK_PERMISSION_MATRIX = [ "roles-permisos.edit", "roles-permisos.delete", "roles-permisos.menu", + "tickets.create", + "tickets.edit", + "tickets.menu", ], "cliente-admin" => [ "presupuesto-cliente.create", @@ -218,6 +221,10 @@ const SK_PERMISSION_MATRIX = [ "tarifa-encuadernacion.edit", "tarifa-encuadernacion.delete", "tarifa-encuadernacion.menu", + "tarifa-extra.create", + "tarifa-extra.edit", + "tarifa-extra.delete", + "tarifa-extra.menu", "tarifa-envio.create", "tarifa-envio.edit", "tarifa-envio.delete", @@ -262,5 +269,8 @@ const SK_PERMISSION_MATRIX = [ "roles-permisos.edit", "roles-permisos.delete", "roles-permisos.menu", + "tickets.create", + "tickets.edit", + "tickets.menu", ], ]; diff --git a/ci4/app/Config/RBAC/permissions.php b/ci4/app/Config/RBAC/permissions.php index 4c640b77..35e8235f 100644 --- a/ci4/app/Config/RBAC/permissions.php +++ b/ci4/app/Config/RBAC/permissions.php @@ -93,4 +93,7 @@ const SK_PERMISSIONS = [ 'roles-permisos.edit' => 'Can edit', 'roles-permisos.delete' => 'Can delete', 'roles-permisos.menu' => 'Menu shall be visualize', + 'tickets.create' => 'Can create', + 'tickets.edit' => 'Can edit', + 'tickets.menu' => 'Menu shall be visualize', ]; diff --git a/ci4/app/Config/Routes.php b/ci4/app/Config/Routes.php index 8ae7e295..2d01a49f 100644 --- a/ci4/app/Config/Routes.php +++ b/ci4/app/Config/Routes.php @@ -909,6 +909,27 @@ $routes->group('chat', ['namespace' => 'App\Controllers\Chat'], function ($route }); +$routes->group('messages', ['namespace' => 'App\Controllers\Chat'], function ($routes) { + $routes->get('datatable', 'ChatController::datatable_messages', ['as' => 'getDatatableMessages']); + $routes->get('datatable/presupuesto', 'ChatController::datatable_presupuesto_messages', ['as' => 'getDatatablePresupuestoMessages']); + $routes->get('datatable/pedido', 'ChatController::datatable_pedido_messages', ['as' => 'getDatatablePedidoMessages']); + $routes->get('datatable/factura', 'ChatController::datatable_factura_messages', ['as' => 'getDatatableFacturaMessages']); + + $routes->post('direct', 'ChatController::store_new_direct_message', ['as' => 'storeNewDirectMessage']); + $routes->post('direct/client', 'ChatController::store_new_direct_message_client', ['as' => 'storeNewDirectMessageClient']); +}); + + +$routes->group('soporte', ['namespace' => 'App\Controllers\Soporte'], function ($routes) { + $routes->get('', 'Ticketcontroller::index', ['as' => 'TicketIndex']); + $routes->get('add', 'Ticketcontroller::add', ['as' => 'NewTicket']); + $routes->post('add', 'Ticketcontroller::add', ['as' => 'createTicket']); + $routes->get('edit/(:num)', 'Ticketcontroller::edit/$1', ['as' => 'editTicket']); + $routes->post('edit/(:num)', 'Ticketcontroller::edit/$1', ['as' => 'updateTicket']); + $routes->post('ticketlist', 'Ticketcontroller::datatable'); + $routes->get('image/(:segment)', 'Ticketcontroller::image/$1'); +}); + $routes->group('produccion', ['namespace' => 'App\Controllers\Produccion'], function ($routes) { $routes->group('ordentrabajo', ['namespace' => 'App\Controllers\Produccion'], function ($routes) { diff --git a/ci4/app/Controllers/Sistema/Intranet.php b/ci4/app/Controllers/Sistema/Intranet.php index 99d3b206..73aa48f2 100644 --- a/ci4/app/Controllers/Sistema/Intranet.php +++ b/ci4/app/Controllers/Sistema/Intranet.php @@ -32,4 +32,29 @@ class Intranet extends Controller } + function tickets($resource_name) + { + helper('file'); + + $resource_path = WRITEPATH . 'uploads/tickets/' . $resource_name; + + if (file_exists($resource_path)) { + // Get the mime type of the file + $mime_type = mime_content_type($resource_path); + + // Get an instance of the Response class + $response = service('response'); + + // Set the content type + $response->setContentType($mime_type); + + // Set the output + $response->setBody(file_get_contents($resource_path)); + + // Send the response to the browser + $response->send(); + } + + } + } \ No newline at end of file diff --git a/ci4/app/Controllers/Soporte/Ticketcontroller.php b/ci4/app/Controllers/Soporte/Ticketcontroller.php new file mode 100644 index 00000000..a50c7889 --- /dev/null +++ b/ci4/app/Controllers/Soporte/Ticketcontroller.php @@ -0,0 +1,429 @@ +viewData['pageTitle'] = lang('Tickets.moduleTitle'); + + // Breadcrumbs + $this->viewData['breadcrumb'] = [ + ['title' => lang("App.menu_soporte"), 'route' => "javascript:void(0);", 'active' => false], + ['title' => lang("App.menu_soporte_ticket_list"), 'route' => route_to('TicketIndex'), 'active' => true] + ]; + + parent::initController($request, $response, $logger); + } + + public function index() + { + //checkPermission('tickets.menu'); + + $viewData = [ + 'currentModule' => static::$controllerSlug, + 'pageSubTitle' => lang('Basic.global.ManageAllRecords', [lang('Tickets.tickets')]), + 'usingServerSideDataTable' => true, + 'userType' => auth()->user()->can('tickets.edit') ? 1 : 0, + ]; + + $viewData = array_merge($this->viewData, $viewData); // merge any possible values from the parent controller class + + return view(static::$viewPath . 'viewTicketList', $viewData); + } + + private function sendMail($subject, $body, $recipient) + { + $settings_model = new SettingsModel(); + $config = $settings_model->first()->toArray(); + $gateway = $config['email_gateway']; + $body = html_entity_decode($body); + + if ($gateway == 'smtp') { + try { + //https://codeigniter.com/user_guide/libraries/email.html + $email = \Config\Services::email(); + $config['protocol'] = $config['email_gateway']; + $config['SMTPHost'] = $config['email_smtp']; + $config['SMTPUser'] = $config['email_address']; + $config['SMTPPass'] = $config['email_pass']; + $config['SMTPPort'] = intval($config['email_port']); + $config['SMTPCrypto'] = $config['email_cert'] == 'none' ? '' : $config['email_cert']; + $config['SMTPTimeout'] = 15; + $config['mailType'] = 'html'; + $config['wordWrap'] = true; + + $email->initialize($config); + + $email->setFrom($config['email_address'], $config['email_name']); + $email->setTo($recipient); + + $email->setSubject($subject); + $email->setMessage($body); + + if (!$email->send()) { + return false; + } else { + return true; + } + } catch (\Exception $ex) { + return false; + } + } + return false; + } + + public function add() + { + + //checkPermission('tickets.create', $this->indexRoute); + + if ($this->request->getPost()): + + $nullIfEmpty = false; // !(phpversion() >= '8.1'); + + $postData = $this->request->getPost(); + + // get user id + $postData['usuario_id'] = auth()->user()->id; + $postData['user_soporte_id'] = model('App\Models\Configuracion\ConfigVariableModel')->getVariable('default_soporte_user_id')->value; + + $sanitizedData = $this->sanitized($postData, $nullIfEmpty); + + $noException = true; + if ($successfulResult = $this->canValidate()): // if ($successfulResult = $this->validate($this->formValidationRules) ) : + + + if ($this->canValidate()): + try { + $successfulResult = $this->model->skipValidation(true)->save($sanitizedData); + } catch (\Exception $e) { + $noException = false; + $this->dealWithException($e); + } + else: + $this->viewData['errorMessage'] = lang('Basic.global.formErr1', [lang('Basic.global.record')]); + $this->session->setFlashdata('formErrors', $this->model->errors()); + endif; + + $thenRedirect = true; // Change this to false if you want your user to stay on the form after submission + endif; + if ($noException && $successfulResult): + + $id = $this->model->db->insertID(); + + $this->saveImages($id, $this->request->getFiles()); + + $message = lang('Basic.global.saveSuccess', [lang('Basic.global.record')]) . '.'; + + $userModel = new \App\Models\UserModel(); + + $this->sendMail(lang('Tickets.newTicket'), lang('Tickets.newTicketBody') . base_url(route_to('editTicket', $id)), $userModel->find($sanitizedData['user_soporte_id'])->email); + + if ($thenRedirect): + if (!empty($this->indexRoute)): + return redirect()->to(route_to($this->indexRoute))->with('successMessage', $message); + else: + return $this->redirect2listView('successMessage', $message); + endif; + else: + $this->session->setFlashData('sweet-success', $message); + endif; + + endif; // $noException && $successfulResult + + endif; // ($requestMethod === 'post') + + $this->viewData['ticket'] = isset($sanitizedData) ? new TicketEntity($sanitizedData) : new TicketEntity(); + + $this->viewData['formAction'] = route_to('NewTicket'); + + $this->viewData['categorias'] = $this->model->getCategorias(); + $this->viewData['estados'] = $this->model->getEstados(); + $this->viewData['secciones'] = $this->model->getSecciones(); + + $this->viewData['supportUsers'] = $this->getSupportUsers(); + + $this->viewData['boxTitle'] = lang('Basic.global.addNew') . ' ' . lang('Tickets.ticket') . ' ' . lang('Basic.global.addNewSuffix'); + + + return $this->displayForm(__METHOD__); + } // end function add() + + public function edit($requestedId = null) + { + $modelRespuesta = new \App\Models\Soporte\TicketRespuestaModel(); + + if ($requestedId == null): + return $this->redirect2listView(); + endif; + $id = filter_var($requestedId, FILTER_SANITIZE_URL); + $ticket = $this->model->find($id); + + if ($ticket == false): + $message = lang('Basic.global.notFoundWithIdErr', [mb_strtolower(lang('Tickets.ticket')), $id]); + return $this->redirect2listView('errorMessage', $message); + endif; + + if (!auth()->user()->can('Tickets.edit') && auth()->user()->id != $ticket->usuario_id) { + return redirect()->to(route_to('TicketIndex'))->with('errorMessage', lang('Basic.global.noPermission')); + } + + if ($this->request->getPost()): + + $oldUserSupport = $ticket->user_soporte_id; + $oldState = $ticket->estado_id; + + $postData = $this->request->getPost(); + $sanitizedData = $this->sanitized($postData, false); + + $noException = true; + if ($successfulResult = $this->canValidate()): // if ($successfulResult = $this->validate($this->formValidationRules) ) : + if ($this->canValidate()): + try { + $successfulResult = $this->model->skipValidation(true)->update($id, $sanitizedData); + + $this->saveImages($id, $this->request->getFiles()); + + if (auth()->user()->can('Tickets.edit')) { + + $respuesta = $modelRespuesta->where('ticket_id', $id)->first(); + if ($respuesta == null) { + $modelRespuesta->insert([ + 'ticket_id' => $id, + 'usuario_id' => auth()->user()->id, + 'mensaje' => $sanitizedData['respuesta_mensaje'] + ]); + } else { + $modelRespuesta->update($respuesta->id, [ + 'mensaje' => $sanitizedData['respuesta_mensaje'], + 'usuario_id' => auth()->user()->id, + ]); + } + + // envio de correos + $userModel = new \App\Models\UserModel(); + if ($oldUserSupport != $sanitizedData['user_soporte_id']) { + $this->sendMail(lang('Tickets.asgignToChanged'), lang('Tickets.asgignToChangedBody') . base_url(route_to('editTicket', $id)), $userModel->find($sanitizedData['user_soporte_id'])->email); + } + + if ($oldState != $sanitizedData['estado_id']) { + $this->sendMail(lang('Tickets.stateChange'), lang('Tickets.stateChangeBody') . base_url(route_to('editTicket', $id)), $userModel->find($ticket->usuario_id)->email); + } + + } + + } catch (\Exception $e) { + $noException = false; + $this->dealWithException($e); + } + else: + $this->viewData['warningMessage'] = lang('Basic.global.formErr1', [mb_strtolower(lang('Tickets.ticket'))]); + $this->session->setFlashdata('formErrors', $this->model->errors()); + + endif; + + $ticket->fill($sanitizedData); + + $thenRedirect = false; + endif; + if ($noException && $successfulResult): + $id = $ticket->id ?? $id; + $message = lang('Basic.global.updateSuccess', [lang('Basic.global.record')]) . '.'; + + if ($thenRedirect): + if (!empty($this->indexRoute)): + return redirect()->to(route_to($this->indexRoute))->with('successMessage', $message); + else: + return $this->redirect2listView('successMessage', $message); + endif; + else: + return redirect()->to(route_to("editTicket", $id))->with('successMessage', $message); + //$this->session->setFlashData('sweet-success', $message); + endif; + + endif; // $noException && $successfulResult + endif; // ($requestMethod === 'post') + + $this->viewData['ticket'] = $ticket; + $this->viewData['respuesta'] = $modelRespuesta->where('ticket_id', $id)->first(); + + $this->viewData['formAction'] = route_to('updateTicket', $id); + + $this->viewData['categorias'] = $this->model->getCategorias(); + $this->viewData['estados'] = $this->model->getEstados(); + $this->viewData['secciones'] = $this->model->getSecciones(); + $this->viewData['supportUsers'] = $this->getSupportUsers(); + + $this->viewData['imagesTicket'] = $this->getImages('ticket', $id); + $this->viewData['imagesRespuesta'] = $this->getImages('respuesta', $id); + + $this->viewData['boxTitle'] = lang('Basic.global.edit2') . ' ' . lang('Tickets.ticket') . ' ' . lang('Basic.global.edit3'); + + + return $this->displayForm(__METHOD__, $id); + } // end function edit(...) + + public function datatable() + { + if ($this->request->isAJAX()) { + $reqData = $this->request->getPost(); + if (!isset($reqData['draw']) || !isset($reqData['columns'])) { + $errstr = 'No data available in response to this specific request.'; + $response = $this->respond(Collection::datatable([], 0, 0, $errstr), 400, $errstr); + return $response; + } + $start = $reqData['start'] ?? 0; + $length = $reqData['length'] ?? 5; + + $requestedOrder = $reqData['order'] ?? []; + + $searchValues = get_filter_datatables_columns($reqData); + + if (auth()->user()->can('tickets.edit')) { + $user_id = null; + } else { + $user_id = auth()->user()->id; + } + + $resourceData = $this->model->getResource($searchValues, $user_id); + foreach ($requestedOrder as $order) { + $column = $order['column'] ?? 0; + $dir = $order['dir'] ?? 'asc'; + $orderColumn = $this->model::SORTABLE[$column] ?? null; + if ($orderColumn) { + $resourceData->orderBy($orderColumn, $dir); + } + } + $resourceData = $resourceData->limit($length, $start)->get()->getResultObject(); + + return $this->respond(Collection::datatable( + $resourceData, + $this->model->getResource($searchValues)->countAllResults(), + $this->model->getResource($searchValues)->countAllResults() + )); + } else { + return $this->failUnauthorized('Invalid request', 403); + } + } + + public function image($imageName) + { + $filePath = WRITEPATH . "uploads/tickets/" . $imageName; + + if (!file_exists($filePath)) { + return $this->response->setStatusCode(404, 'Imagen no encontrada'); + } + + $mimeType = mime_content_type($filePath); + + return $this->response + ->setHeader('Content-Type', $mimeType) + ->setBody(file_get_contents($filePath)); + } + + private function saveImages($ticket_id, $files = []) + { + $uploadPath = WRITEPATH . 'uploads/tickets/'; + + $fileModel = new ticketFileModel(); + + if ($files && isset($files['files'])) { + foreach ($files['files'] as $file) { + if ($file->isValid() && !$file->hasMoved()) { + $originalName = $file->getClientName(); + $fileExt = $file->getExtension(); + + // Generar hash SHA-256 basado en el contenido del archivo + $fileHash = hash_file("sha256", $file->getTempName()); + $newFileName = $fileHash . '.' . $fileExt; + + // Mover el archivo con el nombre basado en el hash + $file->move($uploadPath, $newFileName); + + // Guardar en la base de datos + $fileModel->insert([ + 'nombre' => $originalName, + 'ticket_id' => $ticket_id, + 'hash' => $fileHash, + 'path' => 'uploads/tickets/' . $newFileName + ]); + } + } + } + } + + private function getImages($tipo = 'ticket', $id = null) + { + $images = []; + + $model = new ticketFileModel(); + if ($tipo == 'ticket') { + $files = $model->where('ticket_id', $id)->findAll(); + foreach ($files as $file) { + $ext = pathinfo($file['nombre'], PATHINFO_EXTENSION); + array_push($images, array( + "path" => '/soporte/image/' . $file['hash'] . "." . $ext, + "name" => $file['nombre'] + )); + } + } else { + $files = $model->where('respuesta_id', $id)->findAll(); + foreach ($files as $file) { + $ext = pathinfo($file['nombre'], PATHINFO_EXTENSION); + array_push($images, array( + "path" => '/soporte/image/' . $file['hash'] . "." . $ext, + "name" => $file['nombre'] + )); + } + } + return $images; + } + + private function getSupportUsers() + { + $defatulSoporteUserId = model('App\Models\Configuracion\ConfigVariableModel')->getVariable('default_soporte_user_id')->value; + $supportUsers = array( + array( + 'id' => $defatulSoporteUserId, + 'name' => model('App\Models\UserModel')->getFullName($defatulSoporteUserId) + ), + array( + 'id' => 2, + 'name' => model('App\Models\UserModel')->getFullName(2) + ), + array( + 'id' => 1, + 'name' => model('App\Models\UserModel')->getFullName(1) + ), + ); + + return $supportUsers; + } +} diff --git a/ci4/app/Database/Migrations/2025-02-14-114500_create_tickets_system.php b/ci4/app/Database/Migrations/2025-02-14-114500_create_tickets_system.php new file mode 100644 index 00000000..c0bbab1f --- /dev/null +++ b/ci4/app/Database/Migrations/2025-02-14-114500_create_tickets_system.php @@ -0,0 +1,99 @@ +forge->addField([ + 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], + 'keyword' => ['type' => 'VARCHAR', 'constraint' => 100, 'unique' => true], + ]); + $this->forge->addPrimaryKey('id'); + $this->forge->createTable('tickets_categorias'); + + // Tabla de secciones + $this->forge->addField([ + 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], + 'keyword' => ['type' => 'VARCHAR', 'constraint' => 100, 'unique' => true], + ]); + $this->forge->addPrimaryKey('id'); + $this->forge->createTable('tickets_secciones'); + + // Tabla de Estados + $this->forge->addField([ + 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], + 'keyword' => ['type' => 'VARCHAR', 'constraint' => 100, 'unique' => true], + ]); + $this->forge->addPrimaryKey('id'); + $this->forge->createTable('tickets_estados'); + + // Tabla de Tickets + $this->forge->addField([ + 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], + 'usuario_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], + 'user_soporte_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], + 'categoria_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], + 'seccion_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], + 'estado_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'default' => 1], + 'prioridad' => ['type' => 'ENUM', 'constraint' => ['alta', 'media', 'baja'], 'default' => 'media'], + 'titulo' => ['type' => 'VARCHAR', 'constraint' => 255], + 'descripcion' => ['type' => 'TEXT'], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addPrimaryKey('id'); + $this->forge->addForeignKey('usuario_id', 'users', 'id', 'NO ACTION', 'NO ACTION'); + $this->forge->addForeignKey('user_soporte_id', 'users', 'id', 'NO ACTION', 'NO ACTION'); + $this->forge->addForeignKey('categoria_id', 'tickets_categorias', 'id', 'NO ACTION', 'NO ACTION'); + $this->forge->addForeignKey('seccion_id', 'tickets_secciones', 'id', 'NO ACTION', 'NO ACTION'); + $this->forge->addForeignKey('estado_id', 'tickets_estados', 'id', 'NO ACTION', 'NO ACTION'); + $this->forge->createTable('tickets'); + + // Tabla de Respuestas + $this->forge->addField([ + 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], + 'ticket_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], + 'usuario_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true], + 'mensaje' => ['type' => 'TEXT'], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addPrimaryKey('id'); + $this->forge->addForeignKey('ticket_id', 'tickets', 'id', '', 'NO ACTION'); + $this->forge->addForeignKey('usuario_id', 'users', 'id', 'NO ACTION', 'NO ACTION'); + $this->forge->createTable('tickets_respuestas'); + + // Tabla de Adjuntos + $this->forge->addField([ + 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], + 'ticket_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], + 'respuesta_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true], + 'nombre' => ['type' => 'VARCHAR', 'constraint' => 255], + 'hash' => ['type' => 'VARCHAR', 'constraint' => 255], + 'path' => ['type' => 'VARCHAR', 'constraint' => 255], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addPrimaryKey('id'); + $this->forge->addForeignKey('ticket_id', 'tickets', 'id', 'NO ACTION', 'NO ACTION'); + $this->forge->addForeignKey('respuesta_id', 'tickets_respuestas', 'id', 'NO ACTION', 'NO ACTION'); + $this->forge->createTable('tickets_adjuntos'); + } + + public function down() + { + $this->forge->dropTable('tickets_adjuntos'); + $this->forge->dropTable('tickets_respuestas'); + $this->forge->dropTable('tickets'); + $this->forge->dropTable('tickets_categorias'); + $this->forge->dropTable('tickets_estados'); + $this->forge->dropTable('tickets_secciones'); + + $this->db->table('config_variables_app')->where('name', 'default_soporte_user_id')->delete(); + } +} diff --git a/ci4/app/Database/Seeds/TicketsSeeder.php b/ci4/app/Database/Seeds/TicketsSeeder.php new file mode 100644 index 00000000..f7d0ea93 --- /dev/null +++ b/ci4/app/Database/Seeds/TicketsSeeder.php @@ -0,0 +1,76 @@ + 'errores', + ], + [ + 'keyword' => 'consultas', + ], + [ + 'keyword' => 'sugerencias', + ], + ]; + $this->db->table('tickets_categorias')->insertBatch($data); + + // secciones + $data = [ + [ + 'keyword' => 'presupuestos', + ], + [ + 'keyword' => 'pedidos', + ], + [ + 'keyword' => 'facturacion', + ], + [ + 'keyword' => 'logistica', + ], + [ + 'keyword' => 'configuracion', + ], + [ + 'keyword' => 'general', + ], + ]; + $this->db->table('tickets_secciones')->insertBatch($data); + + // estados + $data = [ + [ + 'keyword' => 'abierto', + ], + [ + 'keyword' => 'en_proceso', + ], + [ + 'keyword' => 'resuelto', + ], + [ + 'keyword' => 'archivado', + ], + ]; + $this->db->table('tickets_estados')->insertBatch($data); + + // config variables + $data = [ + [ + 'name' => 'default_soporte_user_id', + 'value' => '10', + 'description' => 'ID del usuario por defecto para asignar tickets', + ], + ]; + $this->db->table('config_variables_app')->insertBatch($data); + } +} \ No newline at end of file diff --git a/ci4/app/Entities/Soporte/TicketEntity.php b/ci4/app/Entities/Soporte/TicketEntity.php new file mode 100644 index 00000000..192e8d51 --- /dev/null +++ b/ci4/app/Entities/Soporte/TicketEntity.php @@ -0,0 +1,52 @@ + null, + 'usuario_id' => null, + 'tecnico_id' => null, + 'categoria_id'=> null, + 'estado_id' => null, + 'prioridad' => 'media', + 'titulo' => '', + 'descripcion' => '', + 'created_at' => null, + 'updated_at' => null, + ]; + + protected $dates = ['created_at', 'updated_at']; + + public function setTitulo(string $titulo) + { + $this->attributes['titulo'] = ucfirst($titulo); // Capitaliza el título + return $this; + } + + public function setDescripcion(string $descripcion) + { + $this->attributes['descripcion'] = ucfirst($descripcion); + return $this; + } + + public function getPrioridad(): string + { + return ucfirst($this->attributes['prioridad']); + } + + public function asignarTecnico(int $tecnicoId) + { + $this->attributes['tecnico_id'] = $tecnicoId; + return $this; + } + + public function cambiarEstado(int $estadoId) + { + $this->attributes['estado_id'] = $estadoId; + return $this; + } +} diff --git a/ci4/app/Entities/Soporte/TicketRespuestaEntity.php b/ci4/app/Entities/Soporte/TicketRespuestaEntity.php new file mode 100644 index 00000000..d7a082a3 --- /dev/null +++ b/ci4/app/Entities/Soporte/TicketRespuestaEntity.php @@ -0,0 +1,20 @@ + null, + 'ticket_id' => null, + 'usuario_id' => null, + 'mensaje' => null, + 'created_at' => null, + 'updated_at' => null, + ]; + + protected $dates = ['created_at', 'updated_at']; + +} diff --git a/ci4/app/Language/es/App.php b/ci4/app/Language/es/App.php index 44552d73..a24597ea 100755 --- a/ci4/app/Language/es/App.php +++ b/ci4/app/Language/es/App.php @@ -818,5 +818,9 @@ return [ "menu_oauth" => "Autenticaciones", "menu_template" => "Plantillas", + "menu_soporte" => "Soporte", + "menu_soporte_new_ticket" => "Crear ticket", + "menu_soporte_ticket_list" => "Mis tickets", + ]; \ No newline at end of file diff --git a/ci4/app/Language/es/Basic.php b/ci4/app/Language/es/Basic.php index be74f2d1..3f1c5bf5 100755 --- a/ci4/app/Language/es/Basic.php +++ b/ci4/app/Language/es/Basic.php @@ -88,6 +88,7 @@ return [ 'line' => 'la línea', 'error_tittle' => 'Error', ], + 'noPermission' => 'No tiene permiso para acceder a esta página.', 'ok' => 'Ok', 'wait' => 'Espere', 'yes' => 'Si', diff --git a/ci4/app/Language/es/RolesPermisos.php b/ci4/app/Language/es/RolesPermisos.php index 84e56b7f..ccc4176a 100644 --- a/ci4/app/Language/es/RolesPermisos.php +++ b/ci4/app/Language/es/RolesPermisos.php @@ -55,7 +55,7 @@ return [ 'ajustesSection' => 'Ajustes', 'actividadSection' => 'Accesos', - + "ticketsSection" => "Tickets", 'validation' => [ 'id' => [ diff --git a/ci4/app/Language/es/Tickets.php b/ci4/app/Language/es/Tickets.php new file mode 100644 index 00000000..020d304f --- /dev/null +++ b/ci4/app/Language/es/Tickets.php @@ -0,0 +1,70 @@ + 'Ticket', + 'tickets' => 'Tickets', + 'moduleTitle' => 'Tickets', + + "asunto" => "Asunto", + "tipo" => "Tipo", + "seccion" => "Sección", + "estado" => "Estado", + "prioridad" => "Prioridad", + "descripcion" => "Descripción", + "asignarTo" => "Asignado a", + "usuario" => "Creado por", + "createTicket" => "Crear Ticket", + "fechaCreacion" => "Fecha de creación", + "respuesta" => "Respuesta", + + // categorías + 'errores' => 'Errores', + 'consultas' => 'Consultas', + 'sugerencias' => 'Sugerencias', + + // secciones + 'presupuestos' => 'Presupuestos', + 'pedidos' => 'Pedidos', + 'facturacion' => 'Facturación', + 'logistica' => 'Logística', + 'configuracion' => 'Configuración', + 'general' => 'General', + + // estados + 'abierto' => 'Abierto', + 'en_proceso' => 'En proceso', + 'resuelto' => 'Resuelto', + 'archivado' => 'Archivado', + + //Prioridades + 'alta' => 'Alta', + 'media' => 'Media', + 'baja' => 'Baja', + + // FIcheros + 'adjuntos' => 'Adjuntar imágenes', + 'adjuntos_ticket' => 'Imágenes adjuntas', + + // Mail + 'newTicket' => 'Nuevo Ticket en ERP Safekat', + 'stateChange' => 'Cambio de estado en ticket en ERP Safekat', + 'asgignToChanged' => 'Asignado ticket en ERP Safekat', + 'newTicketBody' => '

Se ha creado un nuevo ticket en el sistema de soporte de Safekat ERP.

Puede verlo en el siguiente enlace:

', + 'stateChangeBody' => '

El estado de un ticket en el sistema de soporte de Safekat ERP ha cambiado.

Puede verlo en el siguiente enlace:

', + 'asgignToChangedBody' => '

Se le ha asignado un ticket en el sistema de soporte de Safekat ERP.

Puede verlo en el siguiente enlace:

', + + 'validation' => [ + 'titulo' => [ + 'max_length' => 'El campo {field} no puede exceder {param} caracteres en longitud.', + 'required' => 'El campo {field} es obligatorio.', + + ], + + 'descripcion' => [ + 'required' => 'El campo {field} es obligatorio.', + + ], + ], +]; diff --git a/ci4/app/Models/Soporte/TicketModel.php b/ci4/app/Models/Soporte/TicketModel.php new file mode 100644 index 00000000..0ab09a66 --- /dev/null +++ b/ci4/app/Models/Soporte/TicketModel.php @@ -0,0 +1,165 @@ + "t1.id", + 1 => "t1.categoria_id", + 2 => "t1.seccion_id", + 3 => "t1.estado_id", + 4 => "t1.prioridad", + 5 => "t1.titulo", + 6 => "t1.usuario_id", + 7 => "t1.usuario_soporte_id", + 8 => "t1.created_at", + ]; + + protected $validationRules = [ + "titulo" => [ + "label" => "Tickets.asunto", + "rules" => "trim|required|max_length[255]", + ], + "descripcion" => [ + "label" => "Tickets.descripcion", + "rules" => "trim|required", + ], + ]; + + protected $validationMessages = [ + "titulo" => [ + "max_length" => "Tickets.validation.titulo.max_length", + "required" => "Tickets.validation.titulo.required", + ], + "descripcion" => [ + "decimal" => "Tickets.validation.descripcion.decimal", + "required" => "Tickets.validation.descripcion.required", + ], + ]; + + public function getEstados() + { + $values = $this->db->table('tickets_estados')->get()->getResultArray(); + + for ($i = 0; $i < count($values); $i++) { + $values[$i]['text'] = lang("Tickets." . $values[$i]['keyword']); + } + + return $values; + } + + public function getCategorias() + { + $values = $this->db->table('tickets_categorias')->get()->getResultArray(); + + for ($i = 0; $i < count($values); $i++) { + $values[$i]['text'] = lang("Tickets." . $values[$i]['keyword']); + } + + return $values; + } + + public function getSecciones() + { + $values = $this->db->table('tickets_secciones')->get()->getResultArray(); + + for ($i = 0; $i < count($values); $i++) { + $values[$i]['text'] = lang("Tickets." . $values[$i]['keyword']); + } + + return $values; + } + + public function getTickets($id = null) + { + if ($id === null) { + return $this->select('tickets.*, users.nombre as usuario, categorias.nombre as categoria, estados.nombre as estado') + ->join('users', 'users.id = tickets.usuario_id') + ->join('categorias', 'categorias.id = tickets.categoria_id') + ->join('estados', 'estados.id = tickets.estado_id') + ->findAll(); + } + + return $this->find($id); + } + + public function getResource($search = [], $user_id = null) + { + $builder = $this->db + ->table($this->table . " t1") + ->select( + "t1.id as id, t1.usuario_id AS usuario_id, CONCAT(t2.first_name, ' ', t2.last_name) AS usuario, + t1.user_soporte_id AS user_soporte_id, CONCAT(t6.first_name, ' ', t6.last_name) AS user_soporte, + t1.categoria_id AS categoria_id, t3.keyword AS categoria, + t1.seccion_id AS seccion_id, t5.keyword AS seccion, + t1.estado_id AS estado_id, t4.keyword AS estado, + t1.prioridad AS prioridad, t1.titulo AS titulo, t1.created_at AS created_at + " + ); + + $builder->join("users t2", "t1.usuario_id = t2.id", "left"); + $builder->join("users t6", "t1.user_soporte_id = t6.id", "left"); + $builder->join("tickets_categorias t3", "t1.categoria_id = t3.id", "left"); + $builder->join("tickets_estados t4", "t1.estado_id = t4.id", "left"); + $builder->join("tickets_secciones t5", "t1.seccion_id = t5.id", "left"); + + if ($user_id !== null) + $builder->where("t1.usuario_id", $user_id); + + if (empty($search)) + return $builder; + else { + $builder->groupStart(); + foreach ($search as $col_search) { + if ($col_search[1] == "seccion_id") { + $id = $this->getIdFromKeyword("tickets_secciones", $col_search[2]); + $builder->where("t1." . $col_search[1], $id); + } else if ($col_search[1] == "categoria_id") { + $id = $this->getIdFromKeyword("tickets_categorias", $col_search[2]); + $builder->where("t1." . $col_search[1], $id); + } else if ($col_search[1] == "estado_id") { + $id = $this->getIdFromKeyword("tickets_estados", $col_search[2]); + $builder->where("t1." . $col_search[1], $id); + } else if ($col_search[1] == "prioridad") { + $builder->where("t1." . $col_search[1], $col_search[2]); + } else if ($col_search[1] == "created_at") { + $dates = explode(" ", $col_search[2]); + $builder->where("t1." . $col_search[1] . ">=", $dates[0]); + $builder->where("t1." . $col_search[1] . "<=", $dates[1]); + } else if ($col_search[1] == "usuario_id") { + $builder->like("t2.first_name", $col_search[2]); + $builder->orLike("t2.last_name", $col_search[2]); + } else if ($col_search[1] == "user_soporte_id") { + $builder->like("t6.first_name", $col_search[2]); + $builder->orLike("t6.last_name", $col_search[2]); + } + else + $builder->like("t1." . $col_search[1], $col_search[2]); + } + $builder->groupEnd(); + return $builder; + + } + } + + private function getIdFromKeyword($table, $keyword) + { + $subquery = $this->db->table($table) + ->select('id') + ->where('keyword', $keyword) + ->get() + ->getRow(); + return $subquery->id; + } +} diff --git a/ci4/app/Models/Soporte/TicketRespuestaModel.php b/ci4/app/Models/Soporte/TicketRespuestaModel.php new file mode 100644 index 00000000..5d616c85 --- /dev/null +++ b/ci4/app/Models/Soporte/TicketRespuestaModel.php @@ -0,0 +1,17 @@ +db + ->table("users" . " t1") + ->select( + "CONCAT(t1.first_name, ' ', t1.last_name) AS name" + ); + + $builder->where('t1.deleted_at', null); + $builder->where('t1.id', $id); + + return $builder->get()->getRow()->name; + } + // Método para comprobar si el email ya está registrado public function isEmailUnique($email) { diff --git a/ci4/app/Views/themes/vuexy/form/soporte/viewTicketForm.php b/ci4/app/Views/themes/vuexy/form/soporte/viewTicketForm.php new file mode 100644 index 00000000..4a12a566 --- /dev/null +++ b/ci4/app/Views/themes/vuexy/form/soporte/viewTicketForm.php @@ -0,0 +1,192 @@ +include("themes/_commonPartialsBs/sweetalert") ?> +include('themes/_commonPartialsBs/datatables') ?> +extend('themes/vuexy/main/defaultlayout') ?> +section('content'); ?> + + + + +
+
+
+
+

+
+
+
+ + + getErrors()) ? $validation->listErrors("bootstrap_style") : "" ?> +
+
+ + id)) ? "readonly" : "" ?> + value="titulo) ?>"> +
+
+ +
+ +
+ + +
+ + +
+ + +
+ +
+ + user()->can('tickets.edit')): ?> + + + + + + +
+ +
+ + user()->can('tickets.edit')): ?> + +
+
+ + +
+ +
+ + +
+ +
+ + + +
+ + +
+ + user()->can('tickets.edit'))) : ?> + +

+ +
+ + +
+ + + + user()->can('tickets.edit'))): ?> + +
+
+ + +
+
+ + + +

+
+ +
+
+ +
+
+ +
+ + + +
+ user()->can('tickets.edit')) + ): ?> + " /> + + "btn btn-secondary"]) ?> + +
+
+
+
+
+
+ +endSection(); ?> + +section('css') ?> + +endSection() ?> + + +section('additionalExternalJs') ?> + + + +endSection() ?> \ No newline at end of file diff --git a/ci4/app/Views/themes/vuexy/form/soporte/viewTicketList.php b/ci4/app/Views/themes/vuexy/form/soporte/viewTicketList.php new file mode 100644 index 00000000..b022bbee --- /dev/null +++ b/ci4/app/Views/themes/vuexy/form/soporte/viewTicketList.php @@ -0,0 +1,76 @@ +include('themes/_commonPartialsBs/datatables') ?> +extend('themes/vuexy/main/defaultlayout') ?> +section('content')?> + +
+
+ +
+
+

+ 'btn btn-primary float-end']); ?> +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
ID
+
+ +
+
+
+ +endSection() ?> + +section('css') ?> +"> + +endSection() ?> + + +section('additionalExternalJs') ?> + + + + + + + + + + + + + + +endSection() ?> diff --git a/ci4/app/Views/themes/vuexy/main/menu_impresion.php b/ci4/app/Views/themes/vuexy/main/menu_impresion.php index 2ba24ff5..fc43702e 100644 --- a/ci4/app/Views/themes/vuexy/main/menu_impresion.php +++ b/ci4/app/Views/themes/vuexy/main/menu_impresion.php @@ -48,6 +48,8 @@ require "menus/mensajes_menu.php"; + require "menus/soporte_menu.php"; + require "menus/sistema_menu.php"; ?> diff --git a/ci4/app/Views/themes/vuexy/main/menus/soporte_menu.php b/ci4/app/Views/themes/vuexy/main/menus/soporte_menu.php new file mode 100644 index 00000000..be2b5866 --- /dev/null +++ b/ci4/app/Views/themes/vuexy/main/menus/soporte_menu.php @@ -0,0 +1,22 @@ + + diff --git a/ci4/writable/logs/index.html b/ci4/writable/logs/index.html deleted file mode 100755 index b702fbc3..00000000 --- a/ci4/writable/logs/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - 403 Forbidden - - - -

Directory access is forbidden.

- - - diff --git a/httpdocs/assets/js/safekat/pages/soporte/tickets.js b/httpdocs/assets/js/safekat/pages/soporte/tickets.js new file mode 100644 index 00000000..c442772f --- /dev/null +++ b/httpdocs/assets/js/safekat/pages/soporte/tickets.js @@ -0,0 +1,309 @@ +import Table from '../../components/table.js'; +import Ajax from '../../components/ajax.js'; + + + +class Ticket { + + constructor() { + + // check if url includes "add" + this.action = 'list'; + if (window.location.href.includes("add")) + this.action = "add"; + else if (window.location.href.includes("edit")) + this.action = "edit"; + + this.table = null; + + this.init(); + } + + + init() { + + if (this.action == "edit") { + + $('.gallery-img').on('click', function () { + let imageUrl = $(this).data('src'); // Obtiene la URL de la imagen + $('#modalImage').attr('src', imageUrl); // Cambia la imagen en el modal + $('#imageModal').modal('show'); // Muestra el modal + }); + + } + else if (this.action == "list") { + this.#initDatatable(); + } + } + + #initDatatable() { + + const self = this; + + self.#headerSearcher(); + + const actions = ['view']; + + let columns = []; + if (window.userType == 1) { + columns = [ + { 'data': 'id' }, + { + 'data': 'categoria_id', + render: function (data, type, row) { + return window.language.Tickets[row.categoria]; + } + }, + { + 'data': 'seccion_id', + render: function (data, type, row) { + return window.language.Tickets[row.seccion]; + } + }, + { + 'data': 'estado_id', + render: function (data, type, row) { + return window.language.Tickets[row.estado]; + } + }, + { + 'data': 'prioridad', + render: function (data, type, row) { + return window.language.Tickets[data]; + }, + }, + { 'data': 'titulo' }, + { + 'data': 'usuario_id', + render: function (data, type, row) { + return row.usuario; + }, + }, + { + 'data': 'user_soporte_id', + render: function (data, type, row) { + return row.user_soporte; + }, + }, + { 'data': 'created_at' }, + ]; + } + else { + columns = [ + { 'data': 'id' }, + { + 'data': 'categoria_id', + render: function (data, type, row) { + return window.language.Tickets[row.categoria]; + } + }, + { + 'data': 'seccion_id', + render: function (data, type, row) { + return window.language.Tickets[row.seccion]; + } + }, + { + 'data': 'estado_id', + render: function (data, type, row) { + return window.language.Tickets[row.estado]; + } + }, + { 'data': 'titulo' }, + { 'data': 'created_at' }, + ]; + } + + + this.table = new Table( + $('#tableOfTickets'), + 'tickets', + '/soporte/ticketlist', + columns, + ); + + + this.table.init({ + actions: actions, + buttonsExport: true, + }); + + this.table.table.on('init.dt', function () { + + self.table.table.page.len(100).draw(); + self.table.table.draw(); + + }); + + this.table.setEditCallback(function (id) { + window.location.href = '/soporte/edit/' + id; + }) + } + + #headerSearcher() { + const self = this; + + $('#tableOfTickets thead tr.search-header').remove(); + let searchRow = $('#tableOfTickets thead tr').first().clone(true); + searchRow.addClass('search-header').appendTo('#tableOfTickets thead'); + + $('#tableOfTickets thead tr:eq(1) th').each(function (i) { + if (!$(this).hasClass("noFilter")) { + var title = $(this).text(); + if (title == window.language.Tickets.fechaCreacion) { + + $(this).html(''); + var bsRangePickerRange = $('#bs-rangepicker-range') + bsRangePickerRange.daterangepicker({ + ranges: { + [window.language.datePicker.hoy]: [moment(), moment()], + [window.language.datePicker.ayer]: [moment().subtract(1, 'days'), moment().subtract(1, 'days')], + [window.language.datePicker.ultimos7]: [moment().subtract(6, 'days'), moment()], + [window.language.datePicker.ultimos30]: [moment().subtract(29, 'days'), moment()], + [window.language.datePicker.esteMes]: [moment().startOf('month'), moment().endOf('month')], + [window.language.datePicker.ultimoMes]: [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] + }, + opens: 'right', + language: $('html').attr('lang'), + "locale": { + "customRangeLabel": window.language.datePicker.personalizar, + "format": "YYYY-MM-DD", + "separator": " ", + "applyLabel": window.language.datePicker.aplicar, + "cancelLabel": window.language.datePicker.limpiar, + + }, + "alwaysShowCalendars": true, + autoUpdateInput: false, + + }); + + bsRangePickerRange.on('apply.daterangepicker', function (ev, picker) { + $(this).val(picker.startDate.format('YYYY-MM-DD') + ' ' + picker.endDate.format('YYYY-MM-DD')); + self.table.table + .column(i) + .search(this.value) + .draw(); + }); + + bsRangePickerRange.on('cancel.daterangepicker', function (ev, picker) { + $(this).val(''); + self.table.table + .column(i) + .search(this.value) + .draw(); + }); + + } + else if (title == window.language.Tickets.tipo) { + // Agregar un selector en la tercera columna + $(this).html(''); + + // Agregar opciones al selector + var selectorTipo = $('select', this); + selectorTipo.append(''); // Opción vacía + selectorTipo.append(''); + selectorTipo.append(''); + selectorTipo.append(''); + + selectorTipo.on('change', function () { + var val = $.fn.dataTable.util.escapeRegex( + $(this).val() + ); + self.table.table.column(i).search(val).draw(); + }); + } + + else if (title == window.language.Tickets.seccion) { + // Agregar un selector en la tercera columna + $(this).html(''); + + // Agregar opciones al selector + var selectorSeccion = $('select', this); + selectorSeccion.append(''); // Opción vacía + selectorSeccion.append(''); + selectorSeccion.append(''); + selectorSeccion.append(''); + selectorSeccion.append(''); + selectorSeccion.append(''); + selectorSeccion.append(''); + + selectorSeccion.on('change', function () { + var val = $.fn.dataTable.util.escapeRegex( + $(this).val() + ); + self.table.table.column(i).search(val).draw(); + }); + } + else if (title == window.language.Tickets.estado) { + // Agregar un selector en la tercera columna + $(this).html(''); + + // Agregar opciones al selector + var selectorEstado = $('select', this); + selectorEstado.append(''); // Opción vacía + selectorEstado.append(''); + selectorEstado.append(''); + selectorEstado.append(''); + + selectorEstado.on('change', function () { + var val = $.fn.dataTable.util.escapeRegex( + $(this).val() + ); + self.table.table.column(i).search(val).draw(); + }); + } + else if (title == window.language.Tickets.prioridad) { + // Agregar un selector en la tercera columna + $(this).html(''); + + // Agregar opciones al selector + var selectorPrioridad = $('select', this); + selectorPrioridad.append(''); // Opción vacía + selectorPrioridad.append(''); + selectorPrioridad.append(''); + selectorPrioridad.append(''); + + selectorPrioridad.on('change', function () { + var val = $.fn.dataTable.util.escapeRegex( + $(this).val() + ); + self.table.table.column(i).search(val).draw(); + }); + } + else { + $(this).html(''); + + $('input', this).on('change clear', function () { + if (self.table.table.column(i).search() !== this.value) { + self.table.table + .column(i) + .search(this.value) + .draw(); + } + }); + } + } + else { + $(this).html(''); + } + }); + } +} + +document.addEventListener('DOMContentLoaded', function () { + + const locale = document.querySelector('meta[name="locale"]').getAttribute('content'); + + new Ajax('/translate/getTranslation', { locale: locale, translationFile: ['Tickets', 'datePicker'] }, {}, + function (translations) { + window.language = JSON.parse(translations); + new Ticket(); + }, + function (error) { + console.log("Error getting translations:", error); + } + ).post(); +}); + +export default Ticket; \ No newline at end of file