feat: vínculo Entidade -> Projeto Redmine (clientes de suporte recorrente)
- nova aba Redmine em Administração > Entidades (PluginRedmineEntity) - tabela glpi_plugin_redmine_entities (1:1 entidade -> projeto Redmine, + categoria) - hook resolve o projeto Redmine por: projeto vinculado OU, na ausência, a entidade do chamado (match exato, sem herança de árvore) - twigs do tab generalizados (owner_field/owner_id) p/ reuso projeto+entidade Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c6ac9f7f04
commit
92d1ac044a
7 changed files with 236 additions and 26 deletions
79
front/entity.form.php
Normal file
79
front/entity.form.php
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
include ("../../../inc/includes.php");
|
||||||
|
|
||||||
|
Session::checkLoginUser();
|
||||||
|
|
||||||
|
if (isset($_POST["action"])) {
|
||||||
|
$action = $_POST["action"];
|
||||||
|
$entities_id = (int) $_POST["entities_id"];
|
||||||
|
|
||||||
|
// Permission check
|
||||||
|
$entity = new Entity();
|
||||||
|
if (!$entity->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");
|
||||||
69
hook.php
69
hook.php
|
|
@ -91,6 +91,20 @@ function plugin_redmine_install() {
|
||||||
$migration->addPostQuery($query);
|
$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").
|
// Cache of Redmine users (populated by "Sincronizar Metadados").
|
||||||
if (!$DB->tableExists("glpi_plugin_redmine_users")) {
|
if (!$DB->tableExists("glpi_plugin_redmine_users")) {
|
||||||
$query = "CREATE TABLE `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_tickets",
|
||||||
"glpi_plugin_redmine_tasks",
|
"glpi_plugin_redmine_tasks",
|
||||||
"glpi_plugin_redmine_users",
|
"glpi_plugin_redmine_users",
|
||||||
"glpi_plugin_redmine_usermap"
|
"glpi_plugin_redmine_usermap",
|
||||||
|
"glpi_plugin_redmine_entities"
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($tables as $table) {
|
foreach ($tables as $table) {
|
||||||
|
|
@ -307,28 +322,40 @@ function _plugin_redmine_process_tickettask(TicketTask $task) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$projectId) {
|
// Resolve the Redmine project. Priority: the linked GLPI project; if none,
|
||||||
return; // Not linked to a project
|
// 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
|
// Entity-based clients (recurring support): ticket born in a linked entity, no project.
|
||||||
// Project tab (it may use a custom identifier or an existing project).
|
if (!$redmineProjectId) {
|
||||||
$redmineProjectId = null;
|
$entMap = $DB->request([
|
||||||
$projRow = null;
|
'FROM' => 'glpi_plugin_redmine_entities',
|
||||||
$mapping = $DB->request([
|
'WHERE' => ['entities_id' => (int) $ticket->fields['entities_id']]
|
||||||
'FROM' => 'glpi_plugin_redmine_projects',
|
]);
|
||||||
'WHERE' => ['projects_id' => $projectId]
|
if (count($entMap) > 0) {
|
||||||
]);
|
$linkRow = $entMap->current();
|
||||||
if (count($mapping) > 0) {
|
$redmineProjectId = (int) $linkRow['redmine_project_id'];
|
||||||
$projRow = $mapping->current();
|
}
|
||||||
$redmineProjectId = (int) $projRow['redmine_project_id'];
|
|
||||||
} else {
|
|
||||||
// Fallback: resolve by the conventional identifier.
|
|
||||||
$redmineProjectId = PluginRedmineRedmineapi::getProjectByIdentifier("glpi-proj-" . $projectId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$redmineProjectId) {
|
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;
|
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_tracker_id'])) { $extra['tracker_id'] = (int) $cfg['default_tracker_id']; }
|
||||||
if (!empty($cfg['default_priority_id'])) { $extra['priority_id'] = (int) $cfg['default_priority_id']; }
|
if (!empty($cfg['default_priority_id'])) { $extra['priority_id'] = (int) $cfg['default_priority_id']; }
|
||||||
|
|
||||||
// Category is per project link.
|
// Category is per link (project or entity).
|
||||||
if ($projRow && !empty($projRow['default_category_id'])) {
|
if ($linkRow && !empty($linkRow['default_category_id'])) {
|
||||||
$extra['category_id'] = (int) $projRow['default_category_id'];
|
$extra['category_id'] = (int) $linkRow['default_category_id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue status mirrors the GLPI ticket status (mapped).
|
// Issue status mirrors the GLPI ticket status (mapped).
|
||||||
|
|
|
||||||
99
inc/entity.class.php
Normal file
99
inc/entity.class.php
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if (!defined('GLPI_ROOT')) {
|
||||||
|
die("Sorry. You can't access this file directly");
|
||||||
|
}
|
||||||
|
|
||||||
|
class PluginRedmineEntity extends CommonDBTM {
|
||||||
|
|
||||||
|
static function getTypeName($nb = 0) {
|
||||||
|
return 'Redmine';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) {
|
||||||
|
if ($item->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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,8 @@ class PluginRedmineProject extends CommonDBTM {
|
||||||
|
|
||||||
\Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_linked.html.twig', [
|
\Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_linked.html.twig', [
|
||||||
'action_url' => $CFG_GLPI['root_doc'] . '/plugins/redmine/front/project.form.php',
|
'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,
|
'redmine_project_id' => $redmineProjectId,
|
||||||
'cat_options' => $cat_options,
|
'cat_options' => $cat_options,
|
||||||
'default_category_id' => (int) ($link['default_category_id'] ?? 0),
|
'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', [
|
\Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/project_form.html.twig', [
|
||||||
'action_url' => $CFG_GLPI['root_doc'] . '/plugins/redmine/front/project.form.php',
|
'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,
|
'redmine_projects' => $redmine_projects,
|
||||||
'project_name' => $project->fields['name'],
|
'project_name' => $project->fields['name'],
|
||||||
'project_description' => $description,
|
'project_description' => $description,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('PLUGIN_REDMINE_VERSION', '1.3.0');
|
define('PLUGIN_REDMINE_VERSION', '1.4.0');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init hooks of the plugin.
|
* Init hooks of the plugin.
|
||||||
|
|
@ -17,6 +17,9 @@ function plugin_init_redmine() {
|
||||||
Plugin::registerClass('PluginRedmineProject', [
|
Plugin::registerClass('PluginRedmineProject', [
|
||||||
'addtabon' => ['Project']
|
'addtabon' => ['Project']
|
||||||
]);
|
]);
|
||||||
|
Plugin::registerClass('PluginRedmineEntity', [
|
||||||
|
'addtabon' => ['Entity']
|
||||||
|
]);
|
||||||
|
|
||||||
// Set configuration page
|
// Set configuration page
|
||||||
$PLUGIN_HOOKS['config_page']['redmine'] = 'front/config.form.php';
|
$PLUGIN_HOOKS['config_page']['redmine'] = 'front/config.form.php';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% import 'components/form/fields_macros.html.twig' as fields %}
|
{% import 'components/form/fields_macros.html.twig' as fields %}
|
||||||
|
|
||||||
<form action="{{ action_url }}" method="post" class="mt-4" data-ajax="false">
|
<form action="{{ action_url }}" method="post" class="mt-4" data-ajax="false">
|
||||||
<input type="hidden" name="projects_id" value="{{ projects_id }}">
|
<input type="hidden" name="{{ owner_field }}" value="{{ owner_id }}">
|
||||||
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token_value }}">
|
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token_value }}">
|
||||||
<input type="hidden" name="_glpi_simple_form" value="1">
|
<input type="hidden" name="_glpi_simple_form" value="1">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
<form method="post" action="{{ action_url }}" class="mt-2">
|
<form method="post" action="{{ action_url }}" class="mt-2">
|
||||||
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token_value }}">
|
<input type="hidden" name="_glpi_csrf_token" value="{{ csrf_token_value }}">
|
||||||
<input type="hidden" name="projects_id" value="{{ projects_id }}">
|
<input type="hidden" name="{{ owner_field }}" value="{{ owner_id }}">
|
||||||
<input type="hidden" name="action" value="set_category">
|
<input type="hidden" name="action" value="set_category">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue