124 lines
5.1 KiB
Markdown
124 lines
5.1 KiB
Markdown
---
|
|
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 `<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.
|