diff --git a/inc/config.class.php b/inc/config.class.php index 4dd04af..dc25c9b 100644 --- a/inc/config.class.php +++ b/inc/config.class.php @@ -213,33 +213,16 @@ class PluginRedmineConfig extends CommonDBTM { ]; } - // Referência do ambiente Redmine (ids -> nomes) para consulta na paleta - $palette_ref = []; - $ref_src = [ - __('Trackers', 'redmine') => $metadata['trackers'] ?? [], - __('Status', 'redmine') => $metadata['statuses'] ?? [], - __('Prioridades', 'redmine') => $metadata['priorities'] ?? [], - __('Atividades', 'redmine') => $metadata['activities'] ?? [], - ]; - foreach ($ref_src as $glabel => $list) { - foreach ($list as $el) { - if (isset($el['id'], $el['name'])) { - $palette_ref[$glabel][] = $el['id'] . ' = ' . $el['name']; - } - } - } - foreach (($metadata['project_custom_fields'] ?? []) as $f) { - $vals = array_column($f['possible_values'] ?? [], 'value'); - $palette_ref[__('Custom fields (projeto)', 'redmine')][] = - 'cf ' . $f['id'] . ' = ' . $f['name'] - . (!empty($f['is_required']) ? ' *obrigatório*' : '') - . ($vals ? ' [' . implode(' | ', $vals) . ']' : ''); + // Dicionário "PARA" por operação: campos que o Redmine espera/aceita + $field_dicts = []; + foreach (PluginRedmineTemplateengine::OPERATIONS as $op) { + $field_dicts[$op] = PluginRedmineTemplateengine::fieldDictionary($op); } \Glpi\Application\View\TemplateRenderer::getInstance()->display('@redmine/config_form.html.twig', [ 'templates' => $templates, 'palette_vars' => PluginRedmineTemplateengine::variableCatalog(), - 'palette_ref' => $palette_ref, + 'field_dicts' => $field_dicts, 'action_url' => Toolbox::getItemTypeFormURL(__CLASS__), 'csrf_token_value' => Session::getNewCSRFToken(), 'redmine_url' => $config['redmine_url'] ?? '', diff --git a/inc/redmineapi.class.php b/inc/redmineapi.class.php index fde1118..b0e45c6 100644 --- a/inc/redmineapi.class.php +++ b/inc/redmineapi.class.php @@ -344,6 +344,15 @@ class PluginRedmineRedmineapi { return $out; } + /** + * All custom field definitions (project, issue, time_entry...). Admin only. + * @return array + */ + public static function getAllCustomFields() { + $result = self::get("/custom_fields.json"); + return $result['custom_fields'] ?? []; + } + /** * Ensure a Redmine user is a member of a project (idempotent). * Impersonation requires the user to be a project member with a role. @@ -418,6 +427,7 @@ class PluginRedmineRedmineapi { 'activities' => self::getActivities(), 'roles' => self::getRoles(), 'project_custom_fields' => self::getProjectCustomFields(), + 'custom_fields' => self::getAllCustomFields(), ]; } diff --git a/inc/templateengine.class.php b/inc/templateengine.class.php index d5c5b8f..1c04a67 100644 --- a/inc/templateengine.class.php +++ b/inc/templateengine.class.php @@ -172,6 +172,8 @@ class PluginRedmineTemplateengine { $add('config', 'config.atividade_padrao', 'Atividade padrão (formconfig)'); $add('config', 'config.observadores', 'Observadores padrão (array de ids)'); $add('map', 'map.status[chamado.status]', 'Status Redmine mapeado do status do chamado'); + $add('chamado', 'chamado.tecnico', 'Nome do técnico atribuído (texto; a AUTORIA real é automática via impersonation)'); + $add('tarefa', 'tarefa.autor', 'Nome de quem lançou a tarefa (texto; a AUTORIA real é automática via impersonation)'); $add('form (criar projeto)', 'form.nome', 'Nome digitado no form'); $add('form (criar projeto)', 'form.identificador | identificador', 'Identificador normalizado'); $add('form (criar projeto)', 'form.descricao', 'Descrição do form'); @@ -183,6 +185,100 @@ class PluginRedmineTemplateengine { return $cat; } + /** + * Dicionário "PARA" da operação: os campos que o Redmine espera/aceita, + * com obrigatoriedade e valores possíveis do ambiente (via metadata cache). + * Alimenta a tabela "Campos que o Redmine espera" de cada card do editor. + * @return array de ['path','desc','required','values'=>[]] + */ + public static function fieldDictionary($operation) { + global $DB; + $it = $DB->request(['FROM' => 'glpi_plugin_redmine_configs', 'WHERE' => ['id' => 1]]); + $config = count($it) > 0 ? $it->current() : []; + $meta = !empty($config['metadata_cache']) ? json_decode($config['metadata_cache'], true) : []; + + $vals = function ($list) { + $out = []; + foreach (($list ?? []) as $e) { + if (isset($e['id'], $e['name'])) { + $out[] = $e['id'] . ' = ' . $e['name']; + } + } + return $out; + }; + $trackers = $vals($meta['trackers'] ?? []); + $statuses = $vals($meta['statuses'] ?? []); + $priorities = $vals($meta['priorities'] ?? []); + $activities = $vals($meta['activities'] ?? []); + + $D = []; + $add = function ($path, $desc, $req = false, $values = []) use (&$D) { + $D[] = ['path' => $path, 'desc' => $desc, 'required' => $req, 'values' => $values]; + }; + + switch ($operation) { + case 'create_issue': + $add('issue.subject', 'Título da tarefa (máx. 255)', true); + $add('issue.description', 'Descrição'); + $add('issue.tracker_id', 'Tipo de tarefa', false, $trackers); + $add('issue.status_id', 'Situação', false, $statuses); + $add('issue.priority_id', 'Prioridade', false, $priorities); + $add('issue.category_id', 'Categoria (os IDs variam por projeto Redmine)'); + $add('issue.assigned_to_id', 'Atribuído para (ID de usuário Redmine)'); + $add('issue.fixed_version_id', 'Versão/marco (por projeto)'); + $add('issue.parent_issue_id', 'Tarefa pai (subtarefa)'); + $add('issue.estimated_hours', 'Horas estimadas (decimal)'); + $add('issue.start_date', 'Data de início (AAAA-MM-DD)'); + $add('issue.due_date', 'Data limite (AAAA-MM-DD)'); + $add('issue.is_private', 'Privada (true/false)'); + $add('issue.watcher_user_ids', 'Observadores (array de IDs)'); + break; + case 'log_time': + $add('time_entry.hours', 'Horas (decimal — use {{ tarefa.horas }})', true); + $add('time_entry.comments', 'Comentário (obrigatório em alguns ambientes)'); + $add('time_entry.activity_id', 'Atividade (habilitada no projeto de destino!)', false, $activities); + $add('time_entry.spent_on', 'Data do lançamento (AAAA-MM-DD)'); + break; + case 'update_status': + $add('issue.status_id', 'Situação', false, $statuses); + $add('issue.notes', 'Comentário adicionado à issue na atualização'); + $add('issue.assigned_to_id', 'Atribuído para (ID de usuário Redmine)'); + $add('issue.done_ratio', '% concluído (0-100)'); + break; + case 'create_project': + $add('project.name', 'Nome do projeto', true); + $add('project.identifier', 'Identificador (minúsculas — use o filtro | identificador)', true); + $add('project.description', 'Descrição'); + $add('project.is_public', 'Público (true/false)'); + $add('project.parent_id', 'ID do projeto pai (subprojeto de)'); + $add('project.inherit_members', 'Herdar membros do pai (true/false)'); + $add('project.tracker_ids', 'Tipos de tarefa habilitados (array)', false, $trackers); + $add('project.enabled_module_names', 'Módulos (array: issue_tracking, time_tracking, wiki...)'); + break; + } + + // Custom fields do ambiente, por tipo de objeto da operação + $type_by_op = ['create_project' => 'project', 'create_issue' => 'issue', 'log_time' => 'time_entry']; + $root_by_op = ['create_project' => 'project', 'create_issue' => 'issue', 'log_time' => 'time_entry']; + if (isset($type_by_op[$operation])) { + $cfs = $meta['custom_fields'] ?? $meta['project_custom_fields'] ?? []; + foreach ($cfs as $f) { + if (($f['customized_type'] ?? '') !== $type_by_op[$operation]) { + continue; + } + $pv = array_column($f['possible_values'] ?? [], 'value'); + $add( + $root_by_op[$operation] . '.custom_field_values.' . (int) $f['id'], + $f['name'] . ' (campo personalizado do seu Redmine)', + !empty($f['is_required']), + $pv + ); + } + } + + return $D; + } + // ------------------------------------------------------------------ // Contexto // ------------------------------------------------------------------ @@ -241,12 +337,24 @@ class PluginRedmineTemplateengine { ]; if ($ticket !== null) { + global $DB; + $tecnico = null; + $itT = $DB->request([ + 'SELECT' => 'users_id', + 'FROM' => 'glpi_tickets_users', + 'WHERE' => ['tickets_id' => (int) $ticket->getID(), 'type' => \CommonITILActor::ASSIGN, ['users_id' => ['>', 0]]], + 'LIMIT' => 1, + ]); + if (count($itT) > 0) { + $tecnico = getUserName($itT->current()['users_id']); + } $ctx['chamado'] = [ 'id' => (int) $ticket->getID(), 'titulo' => (string) $ticket->fields['name'], 'descricao' => (string) $ticket->fields['content'], 'status' => (int) $ticket->fields['status'], 'entidade' => (int) $ticket->fields['entities_id'], + 'tecnico' => $tecnico, ]; } @@ -261,6 +369,7 @@ class PluginRedmineTemplateengine { 'comentario' => ($desc !== '') ? $desc : 'Tempo lançado via GLPI (chamado #' . (int) $task->fields['tickets_id'] . ')', 'data' => substr((string) ($task->fields['date'] ?? ''), 0, 10) ?: null, + 'autor' => !empty($task->fields['users_id']) ? getUserName($task->fields['users_id']) : null, ]; } diff --git a/templates/config_form.html.twig b/templates/config_form.html.twig index 589cb78..3d17761 100644 --- a/templates/config_form.html.twig +++ b/templates/config_form.html.twig @@ -157,8 +157,11 @@ {# ---- Paleta ---- #}
-
{{ __('Variáveis (clique para copiar)', 'redmine') }}
-
+
+ {{ __('Variáveis do GLPI — o "DE"', 'redmine') }} +
{{ __('Clique para copiar a tag. Os campos que o Redmine espera (o "PARA") estão em cada operação, à direita.', 'redmine') }}
+
+
{% set lastgroup = '' %} {% for v in palette_vars %} {% if v.group != lastgroup %} @@ -170,16 +173,6 @@
{% endfor %} - {% if palette_ref is not empty %} -
-
{{ __('Referência do seu Redmine', 'redmine') }}
- {% for glabel, items in palette_ref %} -
{{ glabel }}
- {% for item in items %} -
{{ item }}
- {% endfor %} - {% endfor %} - {% endif %}
@@ -209,6 +202,33 @@ + {% if field_dicts[op] is defined and field_dicts[op] is not empty %} +
+ + {{ __('Campos que o Redmine espera — o "PARA" (clique num campo para adicioná-lo)', 'redmine') }} + +
+ + + {% for f in field_dicts[op] %} + + + + + {% endfor %} + +
+ {{ f.path }} + {% if f.required %}{{ __('obrigatório', 'redmine') }}{% endif %} + + {{ f.desc }} + {% if f.values is not empty %} +
{{ f.values|join(' · ') }}
+ {% endif %} +
+
+
+ {% endif %}