diff --git a/front/entity.form.php b/front/entity.form.php new file mode 100644 index 0000000..a656cd9 --- /dev/null +++ b/front/entity.form.php @@ -0,0 +1,79 @@ +getFromDB($entities_id) || !$entity->can($entities_id, UPDATE)) { + Html::displayRightError(); + } + + global $DB; + + if ($action === 'sync') { + $metadata = PluginRedmineRedmineapi::syncMetadata(); + if ($metadata !== false) { + $DB->update('glpi_plugin_redmine_configs', [ + 'metadata_cache' => json_encode($metadata) + ], ['id' => 1]); + PluginRedmineRedmineapi::syncUsers(); + Session::addMessageAfterRedirect(__('Metadados sincronizados com sucesso!', 'redmine')); + } else { + Session::addMessageAfterRedirect(__('Erro ao sincronizar com o Redmine.', 'redmine'), false, ERROR); + } + } + elseif ($action === 'set_category') { + $DB->update('glpi_plugin_redmine_entities', [ + 'default_category_id' => (int) ($_POST['default_category_id'] ?? 0) + ], [ + 'entities_id' => $entities_id + ]); + Session::addMessageAfterRedirect(__('Categoria padrão salva com sucesso!', 'redmine')); + } + elseif ($action === 'process') { + $redmine_project_id = $_POST['redmine_project_id'] ?? ''; + + if (!empty($redmine_project_id)) { + // LINK OPERATION + $DB->insert('glpi_plugin_redmine_entities', [ + 'entities_id' => $entities_id, + 'redmine_project_id' => $redmine_project_id + ]); + Session::addMessageAfterRedirect(__('Entidade vinculada com sucesso!', 'redmine')); + } else { + // CREATE OPERATION + $payload = [ + 'name' => $_POST['name'], + 'identifier' => $_POST['identifier'], + 'description' => $_POST['description'], + 'is_public' => !empty($_POST['is_public']) + ]; + if (!empty($_POST['parent_id'])) { + $payload['parent_id'] = $_POST['parent_id']; + } + if (!empty($_POST['enabled_module_names'])) { + $payload['enabled_module_names'] = $_POST['enabled_module_names']; + } + + $new_redmine_id = PluginRedmineRedmineapi::createProjectFull($payload); + if ($new_redmine_id) { + $DB->insert('glpi_plugin_redmine_entities', [ + 'entities_id' => $entities_id, + 'redmine_project_id' => $new_redmine_id + ]); + Session::addMessageAfterRedirect(__('Projeto criado e vinculado à entidade com sucesso!', 'redmine')); + } else { + Session::addMessageAfterRedirect(__('Erro ao criar projeto no Redmine.', 'redmine'), false, ERROR); + } + } + } + + Html::redirect($CFG_GLPI["root_doc"] . "/front/entity.form.php?id=" . $entities_id); +} + +Html::displayErrorAndDie("Lost"); diff --git a/hook.php b/hook.php index f24f0e0..b3c0dca 100644 --- a/hook.php +++ b/hook.php @@ -91,6 +91,20 @@ function plugin_redmine_install() { $migration->addPostQuery($query); } + // Mapping table for GLPI Entities <-> Redmine Projects (1:1, exact match). + // For recurring-support clients whose tickets are born in an entity, with no project. + if (!$DB->tableExists("glpi_plugin_redmine_entities")) { + $query = "CREATE TABLE `glpi_plugin_redmine_entities` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `entities_id` int(11) NOT NULL, + `redmine_project_id` int(11) NOT NULL, + `default_category_id` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `entities_id` (`entities_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; + $migration->addPostQuery($query); + } + // Cache of Redmine users (populated by "Sincronizar Metadados"). if (!$DB->tableExists("glpi_plugin_redmine_users")) { $query = "CREATE TABLE `glpi_plugin_redmine_users` ( @@ -158,7 +172,8 @@ function plugin_redmine_uninstall() { "glpi_plugin_redmine_tickets", "glpi_plugin_redmine_tasks", "glpi_plugin_redmine_users", - "glpi_plugin_redmine_usermap" + "glpi_plugin_redmine_usermap", + "glpi_plugin_redmine_entities" ]; foreach ($tables as $table) { @@ -307,28 +322,40 @@ function _plugin_redmine_process_tickettask(TicketTask $task) { } } - if (!$projectId) { - return; // Not linked to a project + // Resolve the Redmine project. Priority: the linked GLPI project; if none, + // fall back to the ticket's ENTITY (1:1, exact match, no tree inheritance). + // $linkRow holds the mapping row (carries default_category_id) in either case. + $redmineProjectId = null; + $linkRow = null; + + if ($projectId) { + $mapping = $DB->request([ + 'FROM' => 'glpi_plugin_redmine_projects', + 'WHERE' => ['projects_id' => $projectId] + ]); + if (count($mapping) > 0) { + $linkRow = $mapping->current(); + $redmineProjectId = (int) $linkRow['redmine_project_id']; + } else { + // Fallback: resolve by the conventional identifier. + $redmineProjectId = PluginRedmineRedmineapi::getProjectByIdentifier("glpi-proj-" . $projectId); + } } - // Source of truth for the Redmine project is the link saved by the - // Project tab (it may use a custom identifier or an existing project). - $redmineProjectId = null; - $projRow = null; - $mapping = $DB->request([ - 'FROM' => 'glpi_plugin_redmine_projects', - 'WHERE' => ['projects_id' => $projectId] - ]); - if (count($mapping) > 0) { - $projRow = $mapping->current(); - $redmineProjectId = (int) $projRow['redmine_project_id']; - } else { - // Fallback: resolve by the conventional identifier. - $redmineProjectId = PluginRedmineRedmineapi::getProjectByIdentifier("glpi-proj-" . $projectId); + // Entity-based clients (recurring support): ticket born in a linked entity, no project. + if (!$redmineProjectId) { + $entMap = $DB->request([ + 'FROM' => 'glpi_plugin_redmine_entities', + 'WHERE' => ['entities_id' => (int) $ticket->fields['entities_id']] + ]); + if (count($entMap) > 0) { + $linkRow = $entMap->current(); + $redmineProjectId = (int) $linkRow['redmine_project_id']; + } } if (!$redmineProjectId) { - Toolbox::logInFile('redmine', "Redmine project not found for GLPI project ID $projectId\n"); + Toolbox::logInFile('redmine', "No Redmine link for ticket #$ticketId (no project nor entity link).\n"); return; } @@ -347,9 +374,9 @@ function _plugin_redmine_process_tickettask(TicketTask $task) { if (!empty($cfg['default_tracker_id'])) { $extra['tracker_id'] = (int) $cfg['default_tracker_id']; } if (!empty($cfg['default_priority_id'])) { $extra['priority_id'] = (int) $cfg['default_priority_id']; } - // Category is per project link. - if ($projRow && !empty($projRow['default_category_id'])) { - $extra['category_id'] = (int) $projRow['default_category_id']; + // Category is per link (project or entity). + if ($linkRow && !empty($linkRow['default_category_id'])) { + $extra['category_id'] = (int) $linkRow['default_category_id']; } // Issue status mirrors the GLPI ticket status (mapped). diff --git a/inc/entity.class.php b/inc/entity.class.php new file mode 100644 index 0000000..2889b4d --- /dev/null +++ b/inc/entity.class.php @@ -0,0 +1,99 @@ +getType() == 'Entity') { + return self::createTabEntry(__('Redmine', 'redmine'), 0, $item::getType(), 'fas fa-sitemap'); + } + return ''; + } + + static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) { + if ($item->getType() == 'Entity') { + self::showForEntity($item); + } + return true; + } + + static function getIcon() { + return 'fas fa-sitemap'; + } + + static function showForEntity(Entity $entity) { + global $DB, $CFG_GLPI; + + $entities_id = $entity->getID(); + $form_url = $CFG_GLPI['root_doc'] . '/plugins/redmine/front/entity.form.php'; + + // Already linked? + $iterator = $DB->request([ + 'FROM' => 'glpi_plugin_redmine_entities', + 'WHERE' => ['entities_id' => $entities_id] + ]); + + if (count($iterator) > 0) { + $link = $iterator->current(); + $redmineProjectId = (int) $link['redmine_project_id']; + + $cat_options = []; + foreach (PluginRedmineRedmineapi::getProjectCategories($redmineProjectId) as $c) { + if (isset($c['id'], $c['name'])) { + $cat_options[$c['id']] = $c['name']; + } + } + + \Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_linked.html.twig', [ + 'action_url' => $form_url, + 'owner_field' => 'entities_id', + 'owner_id' => $entities_id, + 'redmine_project_id' => $redmineProjectId, + 'cat_options' => $cat_options, + 'default_category_id' => (int) ($link['default_category_id'] ?? 0), + 'csrf_token_value' => Session::getNewCSRFToken(), + ]); + return; + } + + // Not linked: show the link/create form (reuses the project tab template). + $config_it = $DB->request(['FROM' => 'glpi_plugin_redmine_configs', 'WHERE' => ['id' => 1]]); + $config = count($config_it) > 0 ? $config_it->current() : []; + $metadata = !empty($config['metadata_cache']) ? json_decode($config['metadata_cache'], true) : []; + $redmine_projects = $metadata['projects'] ?? []; + + $modules = [ + 'issue_tracking' => 'Gerenciamento de Tarefas', + 'time_tracking' => 'Gerenciamento de tempo', + 'news' => 'Notícias', + 'documents' => 'Documentos', + 'files' => 'Arquivos', + 'wiki' => 'Wiki', + 'repository' => 'Repositório', + 'boards' => 'Fóruns', + 'calendar' => 'Calendário', + 'gantt' => 'Gantt' + ]; + + $description = \Glpi\RichText\RichText::getTextFromHtml($entity->fields['comment'] ?? '', false, true, true); + + \Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_form.html.twig', [ + 'action_url' => $form_url, + 'owner_field' => 'entities_id', + 'owner_id' => $entities_id, + 'redmine_projects' => $redmine_projects, + 'project_name' => $entity->fields['name'], + 'project_description' => $description, + 'project_identifier' => 'glpi-ent-' . $entities_id, + 'modules' => $modules, + 'csrf_token_value' => Session::getNewCSRFToken() + ]); + } +} diff --git a/inc/project.class.php b/inc/project.class.php index 5e21920..d52b3f2 100644 --- a/inc/project.class.php +++ b/inc/project.class.php @@ -53,7 +53,8 @@ class PluginRedmineProject extends CommonDBTM { \Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_linked.html.twig', [ 'action_url' => $CFG_GLPI['root_doc'] . '/plugins/redmine/front/project.form.php', - 'projects_id' => $projects_id, + 'owner_field' => 'projects_id', + 'owner_id' => $projects_id, 'redmine_project_id' => $redmineProjectId, 'cat_options' => $cat_options, 'default_category_id' => (int) ($link['default_category_id'] ?? 0), @@ -92,7 +93,8 @@ class PluginRedmineProject extends CommonDBTM { \Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_form.html.twig', [ 'action_url' => $CFG_GLPI['root_doc'] . '/plugins/redmine/front/project.form.php', - 'projects_id' => $projects_id, + 'owner_field' => 'projects_id', + 'owner_id' => $projects_id, 'redmine_projects' => $redmine_projects, 'project_name' => $project->fields['name'], 'project_description' => $description, diff --git a/setup.php b/setup.php index 21a1028..aad4e35 100644 --- a/setup.php +++ b/setup.php @@ -1,6 +1,6 @@ ['Project'] ]); + Plugin::registerClass('PluginRedmineEntity', [ + 'addtabon' => ['Entity'] + ]); // Set configuration page $PLUGIN_HOOKS['config_page']['redmine'] = 'front/config.form.php'; diff --git a/templates/project_form.html.twig b/templates/project_form.html.twig index 31e483f..d7dd8db 100644 --- a/templates/project_form.html.twig +++ b/templates/project_form.html.twig @@ -1,7 +1,7 @@ {% import 'components/form/fields_macros.html.twig' as fields %}