knowledge-base/records/plugin-dev/KB-PLUGIN-041-glpi11-project-ticket-lifecycle-sync-and-componentized-card.md
Gemini f10ee9dfcf KB-PLUGIN-041: ciclo projeto->task->chamado (sync do ponto cego), card componentizado, adaptador de tarefas e flatpickr em modal
Aprendizado do mindscrum 0.5.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 17:59:20 -03:00

5.1 KiB

id title domain tags status severity created_at updated_at applies_to related_records
KB-PLUGIN-041 GLPI 11 — Ciclo projeto→task→chamado com sincronização (o ponto cego), modal de card componentizado e adaptador de tarefas plugin-dev
glpi11
plugin
project
projecttask
ticket
tickettask
lifecycle
hooks
sync
flatpickr
mindscrum
active high 2026-07-10 2026-07-10
GLPI 11.0.8 dev
plugin mindscrum 0.5.0
KB-PLUGIN-039
KB-PLUGIN-040
KB-PLUGIN-010
KB-PLUGIN-036

GLPI 11 — Ciclo projeto→task→chamado, sincronização e card componentizado

Modelo de domínio validado no mindscrum 0.5.0. O card do kanban é o CHAMADO; a task de projeto é a origem (não é card). Fluxo:

Projeto (datas planejadas) → tasks do projeto (subtarefas)
  → [iniciar task] cria um chamado a partir dela (nativo) → grava real_start_date
  → chamado é o card; técnico executa (ticket_tasks) e soluciona
  → [ao solucionar] fecha a task 1:1 e grava real_end_date  ← ponto cego do GLPI resolvido

Criar chamado a partir de uma task (spawn nativo)

Ticket::add(['name'=>…, 'content'=>…, 'entities_id'=>…, '_projecttasks_id'=>$taskId]). O core cria o vínculo ProjectTask_Ticket sozinho em Ticket::post_addItem (campo mágico _projecttasks_id). Atribuir o técnico responsável depois com Ticket_User::add(['tickets_id'=>…, 'users_id'=>…, 'type'=>CommonITILActor::ASSIGN]).

As duas automações (hooks) — fecham o ponto cego

Registro em setup.php:

$PLUGIN_HOOKS['item_add']['mindscrum']    = ['Ticket' => 'plugin_mindscrum_ticket_add'];
$PLUGIN_HOOKS['item_update']['mindscrum'] = ['Ticket' => 'plugin_mindscrum_ticket_update'];
  • item_add: se o chamado nasce ligado a uma task ($ticket->input['_projecttasks_id'] ou vínculo em glpi_projecttasks_tickets), grava real_start_date da task (se vazio).
  • item_update: se in_array('status', $ticket->updates) e status ∈ {5 Solucionado, 6 Fechado}, fecha a(s) task(s) vinculada(s): projectstates_id = estado is_finished, percent_done = 100, real_end_date = agora. Tudo via ProjectTask::update() (nunca SQL direto — preserva Log/regras). O core NÃO faz nada disso (verificado: zero referência a ProjectTask no fluxo de solução do CommonITILObject).

Hooks disparam em CLI também (Kernel bootstrap) — dá pra testar E2E o ciclo inteiro. Gotcha de teste CLI: $_SESSION['glpigroups'] precisa existir ([]), senão TicketTask::add() estoura em Reminder::getVisibilityCriteria (o navegador sempre popula).

Modal de card componentizado — um render, esquema normalizado

Em vez de um modal por itemtype, o backend (CardDetail::get) devolve um esquema único e o JS tem um renderCard() só:

{ itemtype, id, type_label, title, native_url,
  properties: [ {key, label, kind:'text'|'priority'|'dates', value/…, editable} ],
  attachments: [...],
  tasklist: { label, action:'spawn'|'toggle', can_add, items:[…] },
  notes: { items:[…], can_post },
  progress: { done, total } }

Cada itemtype só preenche esse contrato. tasklist.action decide o comportamento da bolinha (projeto=spawn de chamado; chamado=marca feito). Notas: chamado→ITILFollowup, projeto→tabela própria cardnotes.

Adaptador de tarefas — ProjectTask ↔ TicketTask

Uma classe Tasks com a mesma interface (listFor/create/toggle) para os dois. Mapa dos campos comuns (fonte: schemas reais):

Comum ProjectTask (glpi_projecttasks) TicketTask (glpi_tickettasks)
Descrição name/content content (sem título)
Início previsto plan_start_date begin
Fim previsto plan_end_date end
Estado a fazer/feito projectstates_id/percent_done state (Planning::TODO=1/DONE=2)
Responsável users_id users_id_tech
Tempo real effective_duration actiontime

Só do projeto (o plugin preenche via automação, não digitado): real_start_date, real_end_date, percent_done, planned_duration, is_milestone.

Datepicker nativo (flatpickr) dentro de modal injetado por JS

Html::header já carrega flatpickr (JS+CSS) e o plugin de botões (js/flatpickr_buttons_plugin.js, que expõe CustomFlatpickrButtons()) em toda página central — não precisa requireJs. Para usar num modal montado via innerHTML, inicializar depois de inserir no DOM:

$(el).flatpickr({
  altInput: true, altFormat: 'd/m/Y H:i', dateFormat: 'Y-m-d H:i:S',
  enableTime: true, time_24hr: true, allowInput: true,
  plugins: [ CustomFlatpickrButtons() ]   // botão "agora"/"hoje"
});

$ (jQuery) é global; guardar com fallback para <input type=text> se indisponível. O input original mantém o valor Y-m-d H:i:S para enviar ao backend (o backend deve aceitar datetime completo, não só Y-m-d).

Regra para agentes

O card é o chamado; a task de projeto vira chamado por spawn. Nunca sincronizar projeto↔chamado por SQL — sempre update() nos models, dentro de hooks item_add/ item_update. Para UI de data/hora em modal de plugin, reusar o flatpickr que o core já carregou; não embutir picker de terceiro.