# Handoff: Dashboard com Tickets (issues)

**Objetivo:** Substituir o hábito do Anderson de pedir mudanças por chat por um
registro estruturado de **Tickets** (internamente "issues"). Primeira versão bem
simples: ao logar, ele vê o grid de tickets do projeto DAROS-PDV (vazio no
início), cria um ticket novo e edita tickets existentes — tudo em páginas
separadas, pensado para uso no celular.

> ## Como usar este arquivo (protocolo de execução)
> - Executar **uma etapa por vez**, na ordem. Cada etapa é **estanque** e tem como
>   verificar no fim.
> - Ao concluir uma etapa: marcar `[x]` (etapa e sub-itens), **preencher "Notas de
>   execução"**, salvar este arquivo e **PARAR**.
> - **Esperar o aval** do usuário **ou** a correção de erros. Erros → corrigir na
>   mesma etapa e parar de novo.
> - **No aval:** fazer **commit + push** dos arquivos da etapa (incluindo este
>   handoff e os untracked relevantes). Em seguida rodar `python deploy.py`. Depois
>   **esperar a liberação** para iniciar a **próxima** etapa.
> - **Entrega é do Claude; deploy/teste costuma ser do usuário.** Onde disser
>   "Verificar", o Claude entrega e descreve o teste; se não der para rodar aqui, o
>   usuário roda e retorna o comportamento (ou os erros/logs).

---

## Decisões (já acordadas)

1. **Páginas separadas** (não modal) para criar/editar — Anderson usará no
   **celular**, e a página deixa claro no título se é **"Novo ticket"** ou
   **"Editar ticket #XXX"**. Layout mobile-first.
2. **Modelo de dados de uma issue:** `id`, `title` (`String(50)`, not null),
   `description` (`db.Text`, ~2 parágrafos), `created_at` (`DateTime`, default UTC).
3. **Tabela de junção `project_issues`** liga issue↔projeto (mesmo padrão de
   `user_projects`/`user_roles`, PK composta). Ao **criar** a issue, grava também
   o vínculo `(project_id, issue_id)`.
4. **Escopo fixo no DAROS-PDV (project id=1)** por enquanto — a dashboard sempre
   lista as issues do projeto 1. Trocar por "projeto do usuário" no futuro.
5. **Na edição:** `id` e `created_at` são **somente leitura**; `title` e
   `description` editáveis. Cada formulário tem **OK** (salva) e **Cancelar**
   (volta para a dashboard sem salvar).
6. **Nomenclatura:** no código/DB = "issue"; nos HTML/labels visíveis = "Ticket".
7. **Convenções do projeto:** modelos em `app/models/`, blueprint por concern em
   `app/routes/issues/` (prefixo de URL `/tickets`), serviço fino em
   `app/services/issue_service.py`, templates em `app/templates/tickets/`.

---

## Estado atual (pontos de toque)

- @app/app.py — `create_app()`, registra blueprints, importa modelos, comando `create-db` (seed roles + DAROS-PDV).
- @app/routes/main/main.py — `GET /dashboard` (`@login_required`), hoje só `render_template("dashboard.html")`.
- @app/templates/dashboard.html — vazio (só `.dashboard-main` + footer com usuário/Sair).
- @app/models/project.py — modelo `Project` (id, name).
- @app/models/user_project.py — join table de exemplo (PK composta, `from database import db`).
- @app/services/auth_service.py — padrão de serviço fino a espelhar.
- @app/static/css/main.css — tema/estilos; receberá grid e form responsivos.

---

## Etapas

### [x] Etapa 1 — Modelos `Issue` + `ProjectIssue` e criação no banco

**Objetivo:** Criar os modelos e garantir que `create-db` gere as tabelas
`issues` e `project_issues`.

Arquivos:
@app/models/issue.py
@app/models/project_issue.py
@app/app.py

- [x] `app/models/issue.py` — `Issue`: `id`, `title` (`String(50)`, not null), `description` (`db.Text`), `created_at` (`DateTime`, default `lambda: datetime.now(timezone.utc)`, igual ao `User`).
- [x] `app/models/project_issue.py` — `ProjectIssue`: PK composta `project_id` (FK→projects.id) + `issue_id` (FK→issues.id), igual a `UserProject`.
- [x] `app/app.py` — importar os dois modelos em `create_app()` (`# noqa: F401`) para registrar no metadata; `create-db` já chama `db.create_all()`, então passam a ser criados.

**Verificar:** (local, na venv)
```bash
cd app
flask --app app create-db
```
Confirmar no SQLite que existem as tabelas `issues` e `project_issues` (ex.: `flask --app app shell` → inspecionar `db.metadata.tables`, ou abrir o `.db`). Sem erro no comando.

_Notas de execução:_ Criados `app/models/issue.py` (`Issue` com `id`, `title` `String(50)` not null, `description` `db.Text`, `created_at` `DateTime` default UTC + `__repr__`) e `app/models/project_issue.py` (`ProjectIssue`, PK composta `project_id`/`issue_id`, espelhando `UserProject`). Em `app/app.py`, adicionados os imports `from models.issue import Issue` e `from models.project_issue import ProjectIssue` (`# noqa: F401`) em `create_app()`. Rodado `flask --app app create-db` na venv (`app/.venv`) → "Database created and roles/projects seeded." sem erro. Inspeção via `db.inspect`: `issues -> ['id', 'title', 'description', 'created_at']` e `project_issues -> ['project_id', 'issue_id']`. Tabelas criadas com as colunas corretas.

---

### [x] Etapa 2 — Backend: serviço, rotas de tickets e dashboard listando

**Objetivo:** CRUD funcional via servidor — serviço fino, blueprint `/tickets`
(novo/editar) e a dashboard consultando as issues do projeto 1.

Arquivos:
@app/services/issue_service.py
@app/routes/issues/__init__.py
@app/routes/issues/issues.py
@app/routes/main/main.py
@app/app.py

- [x] `services/issue_service.py` — `list_project_issues(project_id)` (join via `ProjectIssue`, ordenado por `created_at` desc), `create_issue(project_id, title, description)` (cria `Issue`, `flush` p/ id, cria `ProjectIssue`, commit, retorna), `get_issue(issue_id)` (ou 404), `update_issue(issue_id, title, description)` (commit).
- [x] `routes/issues/` — blueprint (prefixo `/tickets`): `GET/POST /tickets/novo`, `GET/POST /tickets/<int:id>/editar`. `__init__.py` exporta `blueprint`. POST cria/atualiza e redireciona para `/dashboard`. `@login_required`.
- [x] `app/app.py` — registrar `routes.issues.blueprint`.
- [x] `routes/main/main.py` — `dashboard()` passa `issues=list_project_issues(1)` ao template.

**Verificar:** (local)
- `POST /tickets/novo` (form `title`/`description`) → 302 p/ `/dashboard`; a issue e o vínculo em `project_issues` aparecem no banco.
- `POST /tickets/<id>/editar` → atualiza e redireciona.
- `GET /dashboard` retorna 200 e (com o template ainda básico ou após Etapa 3) contém a issue criada.

_Notas de execução:_ Criados `app/services/issue_service.py` (`list_project_issues`
com join `Issue`↔`ProjectIssue` ordenado por `created_at desc`; `create_issue` com
`flush` p/ obter o id antes do vínculo + commit; `get_issue` via `db.get_or_404`;
`update_issue` com commit) e o blueprint `app/routes/issues/` (`__init__.py` exporta
`blueprint`; `issues.py` com prefixo `/tickets`, rotas `GET/POST /novo` e
`GET/POST /<int:issue_id>/editar`, ambas `@login_required`, POST cria/atualiza e
redireciona p/ `main.dashboard`; `PROJECT_ID = 1` fixo). Registrado o blueprint em
`app/app.py`. Em `app/routes/main/main.py`, `dashboard()` agora passa
`issues=list_project_issues(1)` ao template. As rotas GET renderizam
`tickets/form.html` (template só na Etapa 3). **Verificação** (via app context na
venv, pois as rotas são `@login_required`): rotas registradas
(`/tickets/novo`, `/tickets/<int:issue_id>/editar`); `create_issue(1, ...)` →
issue id=1 + vínculo `project_issues (1, 1)`; `list_project_issues(1)` retorna a
issue; `update_issue` altera o título; `get_issue` confirma. Linhas de teste
removidas ao final (issues=0, links=0) para a dashboard começar vazia na Etapa 3.

---

### [x] Etapa 3 — Templates e CSS: grid + páginas de form (mobile-first)

**Objetivo:** Telas finais — grid de tickets na dashboard e páginas de
criar/editar com título dinâmico, campos read-only na edição e OK/Cancelar.

Arquivos:
@app/templates/dashboard.html
@app/templates/tickets/form.html
@app/static/css/main.css

- [x] `dashboard.html` — cabeçalho "Tickets" + botão **"Novo ticket"** (link p/ `/tickets/novo`); grid com **#id · título · data** e link **"Alterar"** por linha (→ `/tickets/<id>/editar`); estado vazio "Nenhum ticket ainda." Manter o footer existente (usuário/Sair).
- [x] `tickets/form.html` — compartilhado novo/editar: título dinâmico ("Novo ticket" vs "Editar ticket #<id>"); na edição mostra `#id` e `created_at` como texto somente leitura; campos `title` (input `maxlength=50`) e `description` (textarea ~6 linhas); botões **OK** (submit) e **Cancelar** (link p/ `/dashboard`).
- [x] `main.css` — estilos do grid e do form, **mobile-first** (coluna única, inputs full-width), coerentes com o tema atual.

**Verificar:** (local, navegador)
1. Logar → `/dashboard`: grid vazio ("Nenhum ticket ainda").
2. "Novo ticket" → título "Novo ticket"; preencher + OK → volta ao grid com o ticket.
3. "Alterar" → título "Editar ticket #N"; id e data read-only; mudar + OK → grid reflete.
4. "Cancelar" → volta ao grid sem salvar.
5. Viewport estreito (DevTools mobile): páginas usáveis em coluna única.

_Notas de execução:_ `dashboard.html` reescrito: header "Tickets" + botão "Novo
ticket" (`issues.novo`), grid `<ul.tickets-grid>` com `#id · título · data`
(`created_at.strftime('%d/%m/%Y')`) e link "Alterar" (`issues.editar`) por linha,
estado vazio "Nenhum ticket ainda."; footer usuário/Sair mantido. Criado
`templates/tickets/form.html` compartilhado novo/editar: `{% if issue %}` controla
título dinâmico, bloco `<dl.ticket-meta>` (id + `created_at` read-only só na edição)
e o `action` do form (`issues.editar` vs `issues.novo`); input `title`
`maxlength=50` required com `value` pré-preenchido na edição, `textarea`
`description` rows=6, botões OK (submit) / Cancelar (link p/ dashboard). CSS
acrescentado em `main.css` (mobile-first, coluna única, coerente com o tema):
`.tickets-header`, `.btn-inline`, `.tickets-grid`/`.ticket-row`/`.ticket-info`
(id/title/date), `.tickets-empty`, `.ticket-form-wrap`, `.ticket-meta`,
`.form textarea`, `.form-actions`; `.dashboard-main` ganhou `max-width:720px`,
centralização e `padding-bottom` p/ não ficar sob o footer fixo.
**Verificação** (Flask test client com usuário logado via sessão, na venv):
`GET /dashboard` 200 com estado vazio + header; `GET /tickets/novo` 200 título
"Novo ticket"; `POST /tickets/novo` 302→`/dashboard`; `GET /dashboard` passa a
mostrar o título e o `#id` do ticket; `GET /tickets/<id>/editar` 200 com título
"Editar ticket #<id>", campos pré-preenchidos e "Criado em" read-only. Linhas de
teste removidas ao final (issues=0). Teste visual em viewport estreito fica para o
usuário no navegador.

---

## Riscos / observações

- **Project id=1 fixo é temporário.** Quando houver múltiplos projetos por
  usuário, trocar por seleção via `user_projects` (Decisão 4).
- **Prod é MySQL, dev é SQLite.** Usar tipos portáveis (`db.Text`, `db.String`).
  As tabelas novas precisarão ser criadas em produção (rodar `create-db` no
  servidor após o deploy da etapa de modelos).
- **Sem suíte de testes.** Verificação é manual (curl/navegador), como no resto
  do projeto.
- **Deploy:** após o aval de cada etapa, `git push` + `python deploy.py`
  (ver CLAUDE.md).
- **Nomenclatura cruzada:** cuidado para não vazar "issue" nos textos visíveis —
  o usuário vê sempre "Ticket".
