--- id: KB-PLUGIN-041 title: "GLPI 11 — Ciclo projeto→task→chamado com sincronização (o ponto cego), modal de card componentizado e adaptador de tarefas" domain: plugin-dev tags: - glpi11 - plugin - project - projecttask - ticket - tickettask - lifecycle - hooks - sync - flatpickr - mindscrum status: active severity: high created_at: 2026-07-10 updated_at: 2026-07-10 applies_to: - GLPI 11.0.8 dev - plugin mindscrum 0.5.0 related_records: - 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`: ```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: ```js $(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 `` 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.