diff --git a/index.json b/index.json index d8ee757..5210f5b 100755 --- a/index.json +++ b/index.json @@ -785,6 +785,28 @@ "severity": "high", "path": "records/plugin-dev/KB-PLUGIN-040-glpi11-unified-kanban-multi-itemtype.md", "summary": "id: KB-PLUGIN-040" + }, + { + "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", + "path": "records/plugin-dev/KB-PLUGIN-041-glpi11-project-ticket-lifecycle-sync-and-componentized-card.md", + "summary": "id: KB-PLUGIN-041" } ] } diff --git a/records/plugin-dev/KB-PLUGIN-041-glpi11-project-ticket-lifecycle-sync-and-componentized-card.md b/records/plugin-dev/KB-PLUGIN-041-glpi11-project-ticket-lifecycle-sync-and-componentized-card.md new file mode 100644 index 0000000..76ea811 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-041-glpi11-project-ticket-lifecycle-sync-and-componentized-card.md @@ -0,0 +1,124 @@ +--- +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.