--- id: KB-PLUGIN-043 title: "GLPI 11 — Padrões de UX de card kanban em plugin: anatomia do card nativo, own_ticket, is_private, TTR, cores de prioridade e gotchas de CSS" domain: plugin-dev tags: - glpi11 - plugin - kanban - card - ux - own-ticket - itilfollowup - time-to-resolve - priority - css - gotcha - mindscrum status: active severity: medium created_at: 2026-07-13 updated_at: 2026-07-13 applies_to: - GLPI 11.0.8 dev - plugin mindscrum 0.7.0–0.9.0 related_records: - KB-PLUGIN-040 - KB-PLUGIN-041 - KB-PLUGIN-042 --- # GLPI 11 — Padrões de UX de card kanban em plugin (aprendizados mindscrum 0.7–0.9) ## Anatomia do card do Kanban NATIVO (receita para replicar) Fonte: `Project::getKanbanColumns()` + Vue `Kanban-Card` (compilado em `public/build/vue/vue-sfc/`). O card nativo carrega: - `title`; **descrição** via `RichText::getTextFromHtml($content)` (em `_metadata`); - **link do pai** ("Subtask of X") → `getFormURLWithID` do pai; - contador "x / y tasks complete" a partir de `_steps` (`percent_done === 100`); - **barra de progresso** `Html::progress(100, $percent)` (Tabler `.progress`); - `_team` (badges de membros), `due_date` = `Html::convDateTime(plan_end_date)`; - `_form_link`; hooks de plugin `PRE/POST_KANBAN_CONTENT` e `KANBAN_ITEM_METADATA`. Replicar server-side com queries EM LOTE (1 p/ equipes, 1 p/ contagens `COUNT/SUM(GROUPBY)`, 1 p/ pais via join, 1 p/ nomes de usuários) — sem N+1. ## Técnicos elegíveis para atribuição (dropdown nativo) `User::getSqlSearchResult(false, 'own_ticket', $entities_id, 0, [], '', 0, $limit)` → retorna o **iterator** pronto. `'own_ticket'` é o right que o próprio core usa para o ator "atribuído" de Ticket (exclui contas só-requisitantes). Para equipe de projeto, right `'all'` na entidade do projeto. ## Followup privado (nota "só técnico") `ITILFollowup` nativo: campo `is_private`; right `followup` com `ITILFollowup::SEEPRIVATE`. Visibilidade: privada aparece só para o AUTOR ou quem tem SEEPRIVATE — replicar esse filtro ao listar followups por fora da API padrão. ## time_to_resolve (prazo/SLA manual) - `Ticket` NÃO tem data prevista de início nativa. Datas do chamado: `date` (abertura), `time_to_resolve`, `time_to_own` (+ `internal_*`). Início planejado vive nas tasks (`TicketTask.begin/end`, `ProjectTask.plan_*`). - `time_to_resolve` aceita valor manual no `add()`/`update()` — herdar `plan_end_date` da task de projeto ao criar chamado dá visão de estouro "de graça". - ⚠️ **Gotcha:** o core DESCARTA SILENCIOSAMENTE `time_to_resolve` anterior à data de abertura (`update()` retorna true e o campo fica null). Em testes, usar abertura retroativa (`date` no input do add) para simular estouro. - Barrinha de "SLA": `% = (now - date) / (time_to_resolve - date)`, capado em 100. ## Cores/ícones de prioridade Cores nativas configuráveis em `$CFG_GLPI['priority_1'..'priority_6']` (tons de vermelho claro — são cores de FUNDO). Para UI discreta, preferir ícone Tabler + tooltip (`getPriorityName`): chevrons (baixas), `ti-equal` (média), `ti-exclamation-circle`/`ti-alert-triangle`/`ti-alert-octagon` (altas). ## Gotchas de CSS/JS em modal custom - **Keyframe que anima `transform` SUBSTITUI o transform do elemento** durante a animação: se a centralização usa `translate(-50%,-50%)`, o elemento "nasce" deslocado e pula ao final. Solução: centralizar com flex no wrapper (`position:fixed; inset:0; display:flex`) e deixar o keyframe só com scale. - Flatpickr com `altInput`: estilizar via `altInputClass` (o input visível é NOVO; regras no input original não pegam) — senão o layout quebra. - `.catch()` de `fetch(...).then(render)` captura exceções do RENDER também — um erro de JS no template do modal aparece como se fosse falha de rede/endpoint. (Sintoma clássico: "modal funciona pra um itemtype e não pra outro".) - Popover/sub-modal empilhado sobre modal custom: z-index acima (1070+) e Esc tratado em pilha (fecha o de cima primeiro). ## Espelhamento de descrição (padrão mindscrum) Spawn task→chamado importa título + `content` (texto via RichText) + prazo. A seção "Descrição" do modal aponta para a FONTE: projeto → `Project.content`; chamado → `ProjectTask.content` da task de origem (fallback: do chamado). Editar grava na fonte via `update()` — allowlist de itemtype/campo no endpoint.