diff --git a/package.json b/package.json index d884a40..e62254d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "elo", - "version": "1.43.1", + "version": "1.46.0", "private": true, "type": "module", "scripts": { diff --git a/public/handoffs/design_handoff_solicitacoes_2/HANDOFF-subpaginas.md b/public/handoffs/design_handoff_solicitacoes_2/HANDOFF-subpaginas.md new file mode 100644 index 0000000..be91126 --- /dev/null +++ b/public/handoffs/design_handoff_solicitacoes_2/HANDOFF-subpaginas.md @@ -0,0 +1,89 @@ +# Handoff: Sub-páginas do Módulo de Solicitações (elo) + +> Complemento ao `README.md` deste pacote. Cobre as **três telas internas** que sucedem o catálogo da Visão do Cliente, todas no mesmo tema (teal da marca, gradiente ambiente vermelho/verde, claro+escuro, Geist): +> +> 1. **Raiz do formulário** — `/forms/{slug}` +> 2. **Responder** — `/forms/{slug}/respond` +> 3. **Minhas solicitações** — `/forms/my-responses` +> +> Estes designs **já têm scaffolding no codebase** (`src/components/forms/v2/`). O objetivo do handoff é fechar o visual/comportamento dessas páginas usando os componentes que já existem. + +## Sobre os arquivos de design +Referências em **HTML/React (Babel inline)** — protótipo, **não** código de produção. Recriar no codebase real (Next.js App Router + shadcn/ui + Tailwind + tRPC + Prisma) reaproveitando componentes existentes. Para rodar: abrir `Solicitacoes.html` no navegador, ligar **"Novo layout"**, **Visão do cliente**, e navegar pelo catálogo. + +**Novos arquivos do protótipo nesta entrega:** +- `pages.jsx` — as três sub-páginas + `SubPageShell` (breadcrumb), `FieldInput` (todos os tipos de campo), drawer de detalhe do cliente. +- `data.js` — agora inclui `FORM_FIELDS` (definição de campos por formulário, espelhando `form-types.ts`), `CURRENT_USER` e `MY_REQUESTS` enriquecido (status, comentário, dados enviados, conversa). +- `client.jsx` / `shell.jsx` — navegação cliente (catálogo → raiz / responder / minhas solicitações). + +--- + +## Mapeamento para o codebase + +| Protótipo (`pages.jsx`) | Rota real | Componente real | +|---|---|---| +| `FormRootPage` | `src/app/(authenticated)/forms/[id]/page.tsx` | `FormsSubPageShell` + `FormsPanel` + `FormPreview` (readOnly) | +| `RespondPage` / `RespondForm` | `src/app/(authenticated)/forms/[id]/respond/page.tsx` | `FormsSubPageShell` + `FormsPanel` + `FormResponseComponent` | +| `MyResponsesPage` | `src/app/(authenticated)/forms/my-responses/page.tsx` | `FormsSubPageShell` + `UserResponsesList` | +| `SubPageShell` + `Breadcrumb` | — | `src/components/forms/v2/forms-sub-page-shell.tsx` (`FormsSubPageShell`, `FormsPanel`) + `useSetBreadcrumbs` (`breadcrumb-context`) | +| `FieldInput` / `FieldRow` | — | já existe em `form-response.tsx` / `form-preview.tsx` (render por `field.type`) | +| `RequestDrawer` (detalhe do cliente) | — | reaproveitar `response-details.tsx` + `response-chat.tsx` dentro de um `Sheet` | +| `MultiCombo` | — | `src/components/forms/multi-select.tsx` | +| `FORM_FIELDS` (mock) | — | `form.fields` (vem de `api.form.getById`), tipado por `src/lib/form-types.ts` | + +> A casca (`FormsSubPageShell`) já publica a trilha de navegação no header global via `useSetBreadcrumbs`. As três páginas só precisam passar o array `breadcrumbs`, `title`, `description` e (quando houver) `actions`. + +--- + +## Tokens / vocabulário (reaproveitar de `globals.css`) +Mesmos do README principal. Específicos destas telas: + +- **Casca:** breadcrumb 12.5px (último item `font-weight:600`, cor `--foreground`); título 28px/700/-0.025em; `description` muted, `max-width: 70ch`; `actions` no topo direito. +- **Painel (`FormsPanel`):** `rounded-[var(--v2-radius-card)]`, `border hsl(var(--v2-border-soft))`, `bg hsl(var(--card)/.75)`, `shadow-[var(--v2-shadow)]`, `backdrop-blur`, padding 26–28px. +- **Campos do formulário:** label 13.5px/600 + `*` obrigatório em vermelho (`hsl(0 72% 58%)`); inputs altura 42px, radius 9px, `bg hsl(var(--v2-card-2)/.7)`, foco com borda accent + `box-shadow 0 0 0 3px accent/.12`; erro com borda `hsl(0 72% 55%/.7)` + mensagem 12.5px. +- **Faixa de info (raiz):** grid de 4 colunas, células `hsl(var(--card)/.9)` separadas por `border-soft`, label uppercase 11px + valor 14px com ícone Lucide. +- **Pílulas de status (minimalista):** Não iniciado → neutro (cinza); Em andamento → âmbar `hsl(38 92% 55%)`; Concluído → verde `hsl(158 64% 45%)`. (Os mesmos `dot`/tons do README.) + +--- + +## Telas + +### 1. Raiz do formulário — `/forms/{slug}` +**Propósito:** página inicial de um tipo de solicitação — entender do que se trata e abrir uma nova. + +- **Casca:** breadcrumb `Home / Solicitações / {título}`; título = `form.title`; descrição = `Criado há {tempo} · {setor}` + `form.description` (via `FormDescription`); ações à direita. +- **Ações (perfil colaborador):** `Minhas solicitações` (ghost → `/forms/my-responses`) + **`Abrir nova solicitação`** (accent teal → `/forms/{id}/respond`). + - *No `page.tsx` real* há também `Respostas` e `Editar` (admin, gated por `canEditForm`) e `CreateManualResponseButtonWrapper`. Mantê-los condicionados à permissão; para o colaborador, exibir só as duas ações acima. +- **Faixa de info:** Setor responsável · Prazo de resposta · Campos (`form.fields.length`) · Em aberto agora. +- **Painel:** `FormPreview` (`readOnly`) — título do formulário + **todos os campos desabilitados**; campos `dynamic` já mostram nome/setor do usuário com cadeado "Preenchido automaticamente". Rodapé com `Abrir nova solicitação`. + +### 2. Responder — `/forms/{slug}/respond` +**Propósito:** preencher e enviar a solicitação. Equivale a `FormResponseComponent`. + +- **Casca:** breadcrumb `Home / Solicitações / {título} / Responder`; título = `form.title`; descrição = `form.description`. +- **Painel:** formulário real, um `FieldRow` por campo. Tipos suportados (de `form-types.ts`): `text`, `number`, `textarea`, `combobox` (single = `Select`; multiple = `MultiSelect`/`MultiCombo` com chips), `formatted` (`InputMask` — cpf/cnpj/phone/email), `file` (dropzone com tipos aceitos), `checkbox`, `dynamic` (read-only, preenchido por `api.user.me`). +- **Validação:** schema Zod dinâmico (já implementado em `form-response.tsx`); obrigatórios com `*` e mensagem "Este campo é obrigatório". Cabeçalho do painel informa nº de campos obrigatórios. +- **Rodapé:** `Cancelar` (→ raiz do formulário) + **`Enviar solicitação`** (accent, `Send`). +- **Sucesso (pós-`mutate`):** alerta verde "Solicitação enviada com sucesso!" + `Ver minhas solicitações` (ghost) e `Abrir nova solicitação` (accent → `reset` do form). Espelha o estado `isSubmitted` do componente real. + +### 3. Minhas solicitações — `/forms/my-responses` +**Propósito:** acompanhar todas as próprias solicitações. Evolução visual de `UserResponsesList` para o vocabulário v2. + +- **Casca:** breadcrumb `Home / Solicitações / Minhas solicitações`; título "Minhas solicitações"; descrição "Visualize e acompanhe o status…"; ação `Nova solicitação` (→ catálogo). +- **KPIs discretos:** Total · Em aberto · Aguardando você (âmbar) · Concluídas (accent). +- **Toolbar:** busca (nº / formulário) + segmented **`Lista | Quadro`** (substitui o `Tabs` "Lista com Filtros / Kanban" do componente atual). +- **Lista:** chips de filtro por status (`Todas / Não iniciadas / Em andamento / Concluídas`) + linhas ricas: `#id` (mono), pílula de status, badge "Aguardando você", título do formulário, **comentário de status** (`statusComment`), meta (`setor · Enviada {idade} · Responsável`). Linha inteira clicável. +- **Quadro:** 3 colunas (Não iniciado / Em progresso / Concluído) com contagem; cards compactos. (No real, manter o `DragDropContext`/`KanbanColumn`, **bloqueando** o arraste da própria solicitação — regra já existente.) +- **Drawer de detalhe (clique numa linha/card):** `#id` + status, título, nota de status, **Dados enviados** (grid de campos), **Conversa** (bolhas: você à direita em accent/.12, setor/técnico à esquerda) e caixa de resposta. Recriar com `Sheet` + `response-details.tsx` + `response-chat.tsx`. + +--- + +## Navegação / estado (cliente) +- **Catálogo → telas:** "Ver" (linha) → raiz; "Abrir solicitação" (linha) e "Iniciar solicitação" (drawer rápido `FormQuickOpenDrawer`) → responder; "Minhas solicitações" / "Ver todas" / "Abrir o Kanban" → my-responses. +- **Estado (my-responses):** `view` (lista/quadro), `query`, `filter` (status), `selected` (drawer). Dados de `api.formResponse.listUserResponses`; resposta na conversa via `response-chat`; mudança de status no quadro via `formResponse.updateStatus`. +- **Campos dinâmicos:** preenchidos por `api.user.me` (`user_name`, `user_sector`) — read-only com cadeado. +- **Transições:** `fade-in` 0.3s nas trocas de página; drawer desliza da direita 0.26s `cubic-bezier(.2,.8,.2,1)`. *Garantir estado de repouso visível (opacity 1)* para não depender da animação completar. +- **Responsivo:** faixa de info colapsa para 2 colunas <860px; quadro vira 1 coluna <1000px; toolbar/ações quebram para coluna no mobile. + +## Definição de campos (referência) +`data.js → FORM_FIELDS` traz um conjunto realista por formulário (categoria, urgência, local, descrição, anexo, etc.), apenas para o protótipo. **Em produção os campos vêm de `form.fields`** (montados no Form Builder e persistidos no Prisma); o renderer deve ser agnóstico ao conteúdo e dirigido por `field.type`. diff --git a/public/handoffs/design_handoff_solicitacoes_2/README.md b/public/handoffs/design_handoff_solicitacoes_2/README.md new file mode 100644 index 0000000..632dae0 --- /dev/null +++ b/public/handoffs/design_handoff_solicitacoes_2/README.md @@ -0,0 +1,113 @@ +# Handoff: Redesign do Módulo de Solicitações (elo) + +## Visão geral +Redesign do módulo **Solicitações** da intranet *elo* (Grupo R Henz), com **duas visões distintas** e um **toggle "Novo layout"** que troca o visual da página por usuário (preferência individual, sem afetar os demais): + +- **Visão do Cliente** — catálogo limpo para o colaborador escolher um tipo de solicitação e abrir um chamado, além de acompanhar as próprias solicitações. Inspiração: Zendesk (limpo, corporativo). +- **Visão do Técnico** — workspace resolutivo para atender chamados: **Fila + Detalhe** lado a lado e um **Quadro** (kanban) refinado como visão alternativa. + +Mantém: marca **elo**, fonte **Geist**, gradiente ambiente vermelho/verde como identidade, tema **claro e escuro**, iconografia minimalista (Lucide). + +## Sobre os arquivos de design +Os arquivos deste pacote são **referências de design feitas em HTML/React (Babel inline)** — protótipos que mostram aparência e comportamento pretendidos, **não código de produção para copiar**. A tarefa é **recriar estes designs no codebase real** (Next.js App Router + React + shadcn/ui + Tailwind + tRPC + Prisma), reutilizando os componentes e padrões já existentes. + +## Fidelidade +**Alta fidelidade (hi-fi).** Cores, tipografia, espaçamento e interações são finais. Recriar pixel a pixel usando a biblioteca de UI existente (shadcn/ui) e os tokens do `globals.css`. + +--- + +## Mapeamento para o codebase atual + +| Protótipo | Componente real a alterar/criar | +|---|---| +| Shell (sidebar + topbar + toggle de tema) | já existe — apenas adicionar o botão **"Novo layout"** na topbar e o contexto de preferência | +| **Visão do Cliente** (catálogo) | `src/components/forms/forms-list.tsx` | +| Drawer "abrir solicitação" | novo: `src/components/forms/form-quick-open-drawer.tsx` (usar `Sheet` do shadcn) | +| **Visão do Técnico — Fila+Detalhe** | nova página/visão sobre `src/components/forms/responses-list.tsx` + `response-details.tsx` + `response-chat.tsx` + `status-update-button.tsx` | +| **Visão do Técnico — Quadro** | evolução do kanban atual de `/forms` (Kanban de Solicitações) | +| Filtros | reaproveitar `responses-filters.tsx`, `setor-search.tsx`, `user-search.tsx` | + +### Toggle "Novo layout" (preferência por usuário) +- Persistir uma flag por usuário: campo `uiLayoutVersion` (`"classic" | "v2"`) no model `User` (Prisma) **ou** preferência leve em `localStorage` para um rollout gradual. +- Expor um `LayoutPreferenceContext` (client) que lê/escreve a flag; a topbar recebe o botão de toggle (componente ``). +- Enquanto `v2` estiver ativo, renderizar os novos componentes; caso contrário, os atuais. Recomendado começar via `localStorage` + feature flag por role para testar com poucos usuários. + +--- + +## Design Tokens + +Reaproveitar os tokens HSL já existentes em `src/styles/globals.css` (shadcn). Adições/uso específico do redesign: + +```css +/* Accent da marca (cristal) — usar com parcimônia */ +--accent: 184 100% 33%; /* #00a5a7 teal */ +--accent-foreground: 0 0% 100%; + +/* Gradiente ambiente de identidade (camada fixa, pointer-events:none, z-index 0) */ +/* dark: glow-a 0 72% 45% (vermelho), glow-b 158 64% 40% (verde), opacidade .22 */ +/* light: glow-a 0 72% 60%, glow-b 158 54% 50%, opacidade .10 */ +background: + radial-gradient(60% 55% at 92% 8%, hsl(var(--glow-a) / var(--glow-op)), transparent 60%), + radial-gradient(55% 60% at 6% 96%, hsl(var(--glow-b) / var(--glow-op)), transparent 62%); + +/* radius padrão de card no redesign */ --radius-card: 12px; +``` + +**Status (tom minimalista):** +- Não iniciado → neutro (`dot` cinza `hsl(0 0% 55%)`) +- Em progresso → âmbar `hsl(38 92% 55%)` (texto), fundo `/.1`, borda `/.25` +- Concluído → verde `hsl(158 64% 45%)` (texto), fundo `/.1`, borda `/.25` + +**Prioridade (dots):** alta `hsl(0 72% 55%)` · média `hsl(38 92% 55%)` · baixa `hsl(0 0% 50%)`. + +**Tipografia:** Geist (já configurada via `--font-geist-sans`). Geist Mono para números/IDs (`#1159`), SLA e métricas. Título de página 28px/700/-0.025em; títulos de card 15px/600. + +**Espaçamento:** container `max-width: 1380px`, padding de conteúdo `30px 34px`. Cards `padding 18px`, `gap 10–18px`, `border 1px hsl(var(--border-soft))`. + +--- + +## Telas / Visões + +### 1. Visão do Cliente — Catálogo de Solicitações +- **Propósito:** colaborador encontra o tipo certo de solicitação, abre um chamado e acompanha os seus. +- **Layout:** header (título + subtítulo à esquerda; ações `Minhas solicitações` ghost + `Criar formulário` primary à direita). Abaixo, **faixa de KPIs discretos** (números grandes em mono, label pequena): *Em aberto, Aguardando você, Concluídas no mês, Tempo médio de resposta*. Depois, grid `1fr / 320px`: + - **Coluna principal:** busca (input com ícone) + **chips** de filtro por setor (Todos, TI, Compras, Marketing, Infraestrutura) + **lista de linhas de catálogo**. + - **Trilho direito (sticky):** card "Minhas solicitações" com itens compactos (#id, status pill, título, idade, responsável / "Aguardando você") + botão "Abrir o Kanban". +- **Linha de catálogo (`.cat-row`):** ícone do setor em quadrado teal `42px` (fundo `accent/.1`, borda `accent/.2`) · título + badge opcional "Mais usado" · descrição (muted, máx ~62ch) · meta (`setor · N campos · �clock Resposta em {SLA}`) · ações à direita (`Ver` ghost + `Abrir solicitação` accent). Hover: borda accent + leve `translateY(-1px)` + sombra. +- **Drawer (Sheet à direita, 460px):** ao clicar numa linha — ícone+título, descrição, grid de info (Setor responsável / Prazo / Em aberto agora), placeholders dos campos do formulário, footer com `Cancelar` + `Iniciar solicitação`. + +### 2. Visão do Técnico — Fila + Detalhe (`view = "fila"`, padrão) +- **Propósito:** atender e resolver chamados rápido, sem trocar de tela. +- **Layout:** header (título "Central de chamados" + `Gerenciar Tags` ghost + segmented `Fila | Quadro`). Faixa de KPIs: *Não iniciados, Em progresso, Sem responsável, SLA em risco, Resolvidos hoje*. Abaixo, grid `372px / 1fr`, altura `calc(100vh - 290px)`. + - **Fila (esquerda):** busca + tabs (`Todos / Novos / Andamento / Concluídos`) + lista de `.q-row` (dot de prioridade, `#id`, badge "Novo", status à direita, resumo, avatar+solicitante, SLA — vermelho se em risco). Item ativo: borda+fundo accent. + - **Detalhe (direita):** header (`#id`, prioridade, formulário, título do chamado; ações: **status changer** com menu, **Assumir chamado** / pílula de responsável, menu `...`). Corpo: 2 cards (Solicitante / SLA), **Dados da solicitação** (grid de campos enviados), **Conversa** (bolhas: solicitante à esquerda neutro, técnico à direita em accent/.12). Rodapé fixo: anexar + input "Responder ao solicitante…" + `Enviar`. +- **Comportamentos resolutivos:** "Assumir" define responsável = usuário atual e move `NOT_STARTED → IN_PROGRESS`; responder também promove para `IN_PROGRESS`; trocar status via menu (Não iniciado / Em progresso / Concluído). + +### 3. Visão do Técnico — Quadro (`view = "quadro"`) +- Kanban refinado de 3 colunas (Não iniciado / Em progresso / Concluído) com contadores reais (18 / 21 / 746). Cards compactos: dot de prioridade, `#id`, badge "Novo", resumo, solicitante, rodapé com SLA + responsável (ou "Sem responsável" em vermelho). Clicar num card abre o **Detalhe** (volta para a Fila com o item selecionado). Arrastar entre colunas para mudar status é o próximo passo natural. + +--- + +## Interações & Estado +- **Estado (técnico):** lista de tickets em estado client-side derivada da query tRPC `formResponse.listByForm` (ou uma nova `formResponse.listQueue`); `selectedId`, `view` (fila/quadro), `tab` (status), `query` (busca). Mutations: `status` (já existe `status-update-button`), `assign`, `reply` (usar `response-chat`). +- **Estado (cliente):** `query`, `categoria` (setor), `drawerForm`. +- **Tema:** classe `data-theme` / `dark` no `` (já há toggle de tema). Todos os tokens respeitam claro/escuro. +- **Transições:** fade-in 0.3s nas trocas de visão; drawer desliza da direita 0.26s `cubic-bezier(.2,.8,.2,1)`. +- **Responsivo:** abaixo de 1100px a fila/detalhe e o grid do cliente colapsam para 1 coluna; sidebar recolhe. + +## Assets +- Logo **elo**: glyph de corrente (ver `public/favicon.svg`) + wordmark "elo". +- Accent teal `#00a5a7` derivado de `public/icon_cristal.svg`. +- Ícones: Lucide (já usado no projeto). Sem SVGs desenhados à mão. + +## Arquivos neste pacote (referência de design) +- `Solicitacoes.html` — entrypoint (tokens CSS de claro/escuro, gradiente, fontes, mount React) +- `shell.jsx` — sidebar + topbar + toggle "Novo layout" + segmented de visão + navegação das sub-páginas +- `client.jsx` — Visão do Cliente (catálogo + drawer + minhas solicitações) +- `pages.jsx` — sub-páginas do cliente (raiz do formulário, responder, minhas solicitações) — ver `HANDOFF-subpaginas.md` +- `tech.jsx` — Visão do Técnico (fila + detalhe + quadro) +- `classic.jsx` — recriação do visual atual (o "antes" do toggle) +- `icons.jsx` — ícones Lucide e o glyph da marca +- `data.js` — mock derivado das telas reais (formulários e chamados) + +> Para rodar o protótipo: abrir `Solicitacoes.html` num navegador (sem build). É só referência visual/comportamental. diff --git a/public/handoffs/design_handoff_solicitacoes_2/Solicitacoes.html b/public/handoffs/design_handoff_solicitacoes_2/Solicitacoes.html new file mode 100644 index 0000000..9717184 --- /dev/null +++ b/public/handoffs/design_handoff_solicitacoes_2/Solicitacoes.html @@ -0,0 +1,300 @@ + + + + + +Módulo de Solicitações — elo + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/public/handoffs/design_handoff_solicitacoes_2/classic.jsx b/public/handoffs/design_handoff_solicitacoes_2/classic.jsx new file mode 100644 index 0000000..93443cc --- /dev/null +++ b/public/handoffs/design_handoff_solicitacoes_2/classic.jsx @@ -0,0 +1,140 @@ +/* ============================================================ + VISÃO CLÁSSICA (o "antes") — recriação aproximada das telas atuais + ============================================================ */ + +function ClassicCard({ form }) { + return ( +
+
{form.title.toUpperCase()}
+
Criado há cerca de 1 ano
+
+ +
+

{form.desc}

+
{form.fields} campos
+
+ + +
+
+ ); +} + +function ClassicClient() { + return ( +
+
+
+

Solicitações

+

Crie, gerencie e responda solicitações personalizadas.

+
+
+ + +
+
+
+ + +
+
+ {window.FORMS.map(f => )} +
+
+ ); +} + +function ClassicKanbanCard({ t }) { + return ( +
+
+ #{t.id} + {t.form.toUpperCase()} + {t.isNew && Novo} +
+ {t.tag && {t.tag}} +
+ {t.requester} +
+
+ {t.age} +
+ +
+ ); +} + +function ClassicTech() { + const cols = [ + { key: "NOT_STARTED", label: "Não Iniciado", n: 18, accent: "0 72% 50%" }, + { key: "IN_PROGRESS", label: "Em Progresso", n: 21, accent: "45 90% 50%" }, + { key: "COMPLETED", label: "Concluído", n: 746, accent: "158 64% 42%" }, + ]; + return ( +
+
+
+

Kanban de Solicitações

+

Visualize e organize as respostas recebidas nos seus formulários.

+
+ +
+ +
+
+ Filtros Avançados +
+
+ {["Número", "Solicitante", "Setor", "Formulário", "Status", "Prioridade", "Tags", "Período"].map(f => ( +
+
{f}
+
+
+ ))} +
+
+ +
+ {cols.map(c => ( +
+
{c.label} ({c.n})
+
+ {window.TICKETS.filter(t => t.status === c.key).map(t => )} +
+
+ ))} +
+
+ ); +} + +function ClassicView({ role, setRole }) { + return ( +
+ +
+ +
+ {role === "client" ? : } +
+ ); +} + +const classicCss = ` +.cl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; } +@media (max-width: 1000px) { .cl-grid { grid-template-columns: 1fr 1fr; } } +@media (max-width: 700px) { .cl-grid { grid-template-columns: 1fr; } } +.cl-card { display: flex; flex-direction: column; padding: 20px; border-radius: var(--radius); background: hsl(var(--card) / .85); border: 1px solid hsl(var(--border-soft)); min-height: 290px; } +.cl-card-title { font-size: 15px; font-weight: 700; line-height: 1.3; letter-spacing: -.01em; } +.cl-filters { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } +@media (max-width: 900px) { .cl-filters { grid-template-columns: 1fr 1fr; } } +.cl-input { height: 38px; border-radius: 9px; background: hsl(var(--card-2)); border: 1px solid hsl(var(--border-soft)); } +.clk-board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: start; } +@media (max-width: 1000px) { .clk-board { grid-template-columns: 1fr; } } +.clk-col { background: hsl(var(--card) / .5); border: 1px solid hsl(var(--border-soft)); border-radius: var(--radius); padding: 14px; } +.clk-col-head { font-size: 16px; font-weight: 700; margin-bottom: 14px; } +.clk-card { padding: 13px; border-radius: 10px; background: hsl(var(--card)); border: 1px solid hsl(var(--border-soft)); } +.clk-tag { display: inline-block; margin-top: 8px; font-size: 11px; font-weight: 600; color: #fff; background: linear-gradient(90deg, hsl(330 80% 55%), hsl(280 70% 55%)); padding: 2px 10px; border-radius: 99px; } +`; + +window.ClassicView = ClassicView; diff --git a/public/handoffs/design_handoff_solicitacoes_2/client.jsx b/public/handoffs/design_handoff_solicitacoes_2/client.jsx new file mode 100644 index 0000000..b54144d --- /dev/null +++ b/public/handoffs/design_handoff_solicitacoes_2/client.jsx @@ -0,0 +1,245 @@ +/* ============================================================ + VISÃO DO CLIENTE — catálogo de solicitações (estilo limpo) + ============================================================ */ + +const SECTOR_ICON = { + "TI": I.admin, "Compras": I.cart, "Marketing": I.mega, + "Infraestrutura": I.settings, "Marketing ": I.mega, +}; + +function statusPill(status, small) { + const m = window.STATUS_META[status]; + return ( + + {m.short} + + ); +} + +function CatalogRow({ form, onOpen, nav }) { + const Icon = SECTOR_ICON[form.sector] || I.form; + return ( +
onOpen(form)}> +
+
+
+ {form.title} + {form.popular && Mais usado} +
+

{form.desc}

+
+ {form.sector} + + {form.fields} campos + + Resposta em {form.sla} +
+
+
+ + +
+
+ ); +} + +function MyRequestCard({ r }) { + return ( +
+
+ #{r.id} + {statusPill(r.status, true)} +
+
{r.form}
+
+ {r.age} + {r.waiting + ? Aguardando você + : {r.assignee ?? "Sem responsável"}} +
+
+ ); +} + +function Drawer({ form, onClose, nav }) { + if (!form) return null; + const Icon = SECTOR_ICON[form.sector] || I.form; + return ( +
+
e.stopPropagation()}> +
+
+
+
{form.title}
+
+ {form.sector}{form.fields} campos +
+
+ +
+

{form.desc}

+ +
+
Setor responsável{form.sector}
+
Prazo de resposta{form.sla}
+
Em aberto agora{form.open} solicitações
+
+ +
+
Campos do formulário
+
+ {Array.from({ length: Math.min(form.fields, 5) }).map((_, i) => ( +
+
+
+
+ ))} + {form.fields > 5 && + {form.fields - 5} outros campos} +
+ +
+ + +
+
+
+ ); +} + +function ClientView({ nav }) { + const [q, setQ] = React.useState(""); + const [cat, setCat] = React.useState("Todos"); + const [drawer, setDrawer] = React.useState(null); + const cats = ["Todos", "TI", "Compras", "Marketing", "Infraestrutura"]; + + const forms = window.FORMS.filter(f => + (cat === "Todos" || f.sector === cat) && + (q === "" || f.title.toLowerCase().includes(q.toLowerCase()) || f.desc.toLowerCase().includes(q.toLowerCase())) + ); + + return ( +
+ + + {/* header */} +
+
+

Solicitações

+

Escolha um tipo de solicitação e abra um chamado para o setor responsável.

+
+
+ + +
+
+ + {/* KPIs discretos */} +
+
1Em aberto
+
1Aguardando você
+
2Concluídas no mês
+
~6hTempo médio de resposta
+
+ +
+ {/* main: catalog */} +
+
+
+ + setQ(e.target.value)} /> +
+
+
+ {cats.map(c => ( + + ))} +
+ +
+ {forms.map(f => )} + {forms.length === 0 && ( +
+ +
Nenhum tipo encontrado para “{q}”.
+
+ )} +
+
+ + {/* rail: my requests */} + +
+ + setDrawer(null)} nav={nav} /> +
+ ); +} + +const clientCss = ` +.cv-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; flex-wrap: wrap; } +.cv-grid { display: grid; grid-template-columns: 1fr 320px; gap: 24px; align-items: start; } +@media (max-width: 1100px) { .cv-grid { grid-template-columns: 1fr; } } +.chips { display: flex; gap: 8px; flex-wrap: wrap; } +.cat-list { display: flex; flex-direction: column; gap: 10px; } +.cat-row { + display: flex; align-items: center; gap: 16px; padding: 18px; + background: hsl(var(--card) / .9); border: 1px solid hsl(var(--border-soft)); + border-radius: var(--radius); cursor: pointer; transition: all .16s; backdrop-filter: blur(6px); +} +.cat-row:hover { border-color: hsl(var(--accent) / .4); transform: translateY(-1px); box-shadow: var(--shadow); } +.cat-ic { + width: 42px; height: 42px; border-radius: 11px; flex-shrink: 0; + display: grid; place-items: center; color: hsl(var(--accent)); + background: hsl(var(--accent) / .1); border: 1px solid hsl(var(--accent) / .2); +} +.cat-title { font-size: 15px; font-weight: 600; letter-spacing: -.01em; } +.cat-desc { color: hsl(var(--muted)); font-size: 13px; margin: 4px 0 8px; line-height: 1.5; max-width: 62ch; } +.cat-meta { display: flex; align-items: center; gap: 9px; font-size: 12px; color: hsl(var(--faint)); } +.dotsep { width: 3px; height: 3px; border-radius: 50%; background: hsl(var(--faint)); } +.cat-actions { display: flex; gap: 8px; flex-shrink: 0; } +@media (max-width: 720px) { .cat-actions { display: none; } } + +.myreq { + padding: 12px 13px; border-radius: 10px; border: 1px solid hsl(var(--border-soft)); + background: hsl(var(--card-2) / .6); display: flex; flex-direction: column; gap: 7px; cursor: pointer; transition: border-color .15s; +} +.myreq:hover { border-color: hsl(var(--border)); } +.myreq-title { font-size: 13px; font-weight: 500; line-height: 1.35; } +.link-btn { font-size: 12.5px; color: hsl(var(--accent)); font-weight: 600; } + +/* drawer */ +.drawer-wrap { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,.45); display: flex; justify-content: flex-end; } +.drawer { width: 460px; max-width: 92vw; height: 100%; overflow-y: auto; background: hsl(var(--panel)); border-left: 1px solid hsl(var(--border)); padding: 24px; } +.fade-in-r { animation: slideR .26s cubic-bezier(.2,.8,.2,1); } +@keyframes slideR { from { transform: translateX(40px); opacity: .4; } to { transform: none; opacity: 1; } } +.drawer-info { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; padding: 16px; border-radius: 11px; background: hsl(var(--card-2) / .7); border: 1px solid hsl(var(--border-soft)); } +.drawer-info > div { display: flex; flex-direction: column; gap: 3px; } +.drawer-info span.faint { font-size: 11px; } +.drawer-info strong { font-size: 13.5px; } +.field-skel { display: flex; flex-direction: column; gap: 6px; } +.fs-label { height: 9px; border-radius: 4px; background: hsl(var(--border)); } +.fs-input { height: 36px; border-radius: 8px; background: hsl(var(--card-2)); border: 1px solid hsl(var(--border-soft)); } +.drawer-foot { display: flex; gap: 10px; margin-top: 24px; padding-top: 18px; border-top: 1px solid hsl(var(--border-soft)); } +`; + +window.ClientView = ClientView; +window.statusPill = statusPill; diff --git a/public/handoffs/design_handoff_solicitacoes_2/data.js b/public/handoffs/design_handoff_solicitacoes_2/data.js new file mode 100644 index 0000000..977b411 --- /dev/null +++ b/public/handoffs/design_handoff_solicitacoes_2/data.js @@ -0,0 +1,366 @@ +/* ============================================================ + Mock data — Módulo de Solicitações (Grupo R Henz / elo) + Derivado das telas reais. Não é dado de produção. + ============================================================ */ + +// Catálogo de tipos de solicitação (visão cliente) +window.FORMS = [ + { + id: "mkt", + title: "Solicitação ao Marketing", + sector: "Marketing", + fields: 7, + sla: "2 dias úteis", + open: 4, + desc: "Solicitações diversas ao MKT: adesivo, placa, organização de eventos, etc.", + }, + { + id: "material", + title: "Material de uso interno", + sector: "Compras", + fields: 9, + sla: "3 dias úteis", + open: 9, + desc: "Compra de materiais para uso e consumo interno: escritório, limpeza, EPIs, eletrônicos, embalagens.", + }, + { + id: "tec", + title: "Implementação de tecnologia", + sector: "TI", + fields: 7, + sla: "5 dias úteis", + open: 2, + desc: "Solicitações para implementação de aplicativos de desenvolvimento interno ou de fornecedores externos.", + }, + { + id: "infra", + title: "Manutenção e/ou infraestrutura", + sector: "Infraestrutura", + fields: 9, + sla: "2 dias úteis", + open: 6, + desc: "Manutenção predial, elétrica, hidráulica e conserto de equipamentos.", + }, + { + id: "chamado-ti", + title: "Chamado de atendimento de TI", + sector: "TI", + fields: 7, + sla: "4 horas", + open: 18, + desc: "Atendimento de TI: internet, cabeamento, depósito, recebimento e setor de garantia.", + popular: true, + }, + { + id: "usuarios", + title: "Usuários SIGIN / Windows / E-mails", + sector: "TI", + fields: 7, + sla: "1 dia útil", + open: 3, + desc: "Criação e ajuste de contas de usuário: SIGIN, Windows e e-mail corporativo.", + }, +]; + +// Usuário logado (visão cliente) +window.CURRENT_USER = { name: "Rafael Müller", sector: "Comercial", initials: "RM" }; + +// Solicitações do próprio usuário (visão cliente) +window.MY_REQUESTS = [ + { + id: 1162, formId: "chamado-ti", form: "Chamado de atendimento de TI", sector: "TI", + status: "IN_PROGRESS", assignee: "Equipe Dev", age: "há 3 horas", updated: "há 40 min", waiting: false, + statusComment: "Técnico a caminho do galpão para verificar o ponto de rede.", + fieldsData: [ + ["Categoria do atendimento", "Internet / Rede"], + ["Local / estação", "Galpão 2 — baia 4"], + ["Urgência", "Alta — operação parada"], + ["Descrição", "O computador do recebimento parou de conectar na rede via cabo hoje cedo."], + ], + thread: [ + { who: "Rafael Müller", role: "Você", time: "08:12", msg: "Bom dia, o computador do recebimento parou de conectar na rede hoje cedo." }, + { who: "Equipe Dev", role: "Técnico", time: "08:40", msg: "Recebido, Rafael. Já estamos indo até o galpão verificar o cabeamento." }, + ], + }, + { + id: 1149, formId: "material", form: "Material de uso interno", sector: "Compras", + status: "NOT_STARTED", assignee: null, age: "há 1 dia", updated: "há 1 dia", waiting: true, + statusComment: "Aguardando aprovação do centro de custo informado.", + fieldsData: [ + ["Categoria", "Escritório"], + ["Item", "Papel sulfite A4 75g"], + ["Quantidade", "5 resmas"], + ["Centro de custo", "4.01 — Administrativo"], + ], + thread: [ + { who: "Rafael Müller", role: "Você", time: "Ontem 09:20", msg: "Acabou o papel A4 no financeiro, preciso de reposição." }, + { who: "Compras", role: "Setor", time: "Ontem 10:05", msg: "Para liberar precisamos confirmar o centro de custo. Pode validar com a gestão?" }, + ], + }, + { + id: 1131, formId: "mkt", form: "Solicitação ao Marketing", sector: "Marketing", + status: "COMPLETED", assignee: "Caroline Doege", age: "há 4 dias", updated: "há 2 dias", waiting: false, + statusComment: "Arte final entregue na pasta compartilhada do setor.", + fieldsData: [ + ["Tipo de material", "Placa / sinalização"], + ["Título da peça", "Sinalização de doca — Expedição"], + ["Prazo desejado", "30/05/2026"], + ], + thread: [ + { who: "Rafael Müller", role: "Você", time: "12/05 14:02", msg: "Preciso de uma placa de sinalização nova para a doca de expedição." }, + { who: "Caroline Doege", role: "Marketing", time: "13/05 16:30", msg: "Arte aprovada e enviada para impressão. Arquivo final na pasta do setor." }, + ], + }, + { + id: 1175, formId: "infra", form: "Manutenção e/ou infraestrutura", sector: "Infraestrutura", + status: "IN_PROGRESS", assignee: "Equipe Manutenção", age: "há 5 horas", updated: "há 2 horas", waiting: false, + statusComment: "Peça solicitada ao fornecedor — prazo de 2 dias úteis.", + fieldsData: [ + ["Tipo de serviço", "Elétrica"], + ["Local", "Escritório central — sala 3"], + ["Urgência", "Média"], + ["Descrição", "Tomada da sala 3 sem energia, disjuntor desarmando."], + ], + thread: [ + { who: "Rafael Müller", role: "Você", time: "Hoje 09:10", msg: "A tomada da sala 3 está sem energia e o disjuntor desarma sozinho." }, + { who: "Equipe Manutenção", role: "Técnico", time: "Hoje 11:30", msg: "Identificamos o curto. Peça de reposição já solicitada ao fornecedor." }, + ], + }, + { + id: 1168, formId: "tec", form: "Implementação de tecnologia", sector: "TI", + status: "NOT_STARTED", assignee: null, age: "há 2 dias", updated: "há 2 dias", waiting: false, + statusComment: "Em análise de viabilidade pela equipe de tecnologia.", + fieldsData: [ + ["Nome do sistema", "Painel de indicadores comerciais"], + ["Origem", "Desenvolvimento interno"], + ["Setores impactados", "Comercial, Financeiro"], + ], + thread: [ + { who: "Rafael Müller", role: "Você", time: "2 dias atrás", msg: "Gostaria de um painel para acompanhar indicadores do comercial em tempo real." }, + ], + }, + { + id: 1098, formId: "usuarios", form: "Usuários SIGIN / Windows / E-mails", sector: "TI", + status: "COMPLETED", assignee: "Gilnei Wolf", age: "há 12 dias", updated: "há 11 dias", waiting: false, + statusComment: "Usuário criado e credenciais enviadas por e-mail.", + fieldsData: [ + ["Tipo de solicitação", "Criar usuário"], + ["Colaborador", "Maria Eduarda Lopes"], + ["Acessos", "Windows, E-mail, SIGIN"], + ["Data de início", "26/05/2026"], + ], + thread: [ + { who: "Rafael Müller", role: "Você", time: "04/06 10:00", msg: "Nova colaboradora começa segunda, precisa dos acessos básicos." }, + { who: "Gilnei Wolf", role: "Técnico", time: "05/06 16:30", msg: "Usuário criado e credenciais enviadas. Qualquer coisa estou à disposição." }, + ], + }, +]; + +// Fila de chamados (visão técnico) +window.TICKETS = [ + { + id: 1159, form: "Chamado de atendimento de TI", sector: "TI", status: "NOT_STARTED", + priority: "high", requester: "Jozimar Daniel Haag", initials: "JH", assignee: null, + age: "há 17 horas", sla: "Vence em 2h", slaRisk: true, isNew: true, + summary: "Computador do recebimento não conecta na rede via cabo.", + fieldsData: [ + ["Setor", "Recebimento — Depósito"], + ["Equipamento", "Desktop Dell OptiPlex 3070"], + ["Tipo de problema", "Sem internet via cabo"], + ["Urgência", "Alta — operação parada"], + ["Local", "Galpão 2, baia 4"], + ], + thread: [ + { who: "Jozimar Daniel Haag", role: "Solicitante", time: "08:12", msg: "Bom dia, o computador do recebimento parou de conectar na rede hoje cedo." }, + ], + }, + { + id: 1158, form: "Chamado de atendimento de TI", sector: "TI", status: "IN_PROGRESS", + priority: "medium", requester: "Luana De Mello H", initials: "LM", assignee: "Equipe Dev", + age: "há 19 horas", sla: "Dentro do prazo", slaRisk: false, isNew: true, tag: "Equipe Dev", + summary: "Sistema de etiquetas travando ao gerar lote acima de 200 itens.", + fieldsData: [ + ["Setor", "Expedição"], + ["Sistema", "Gerador de etiquetas"], + ["Comportamento", "Trava ao gerar lote > 200"], + ["Frequência", "Diária"], + ], + thread: [ + { who: "Luana De Mello H", role: "Solicitante", time: "Ontem 14:02", msg: "O gerador trava quando o lote passa de 200 etiquetas." }, + { who: "Gilnei Wolf", role: "Equipe Dev", time: "Ontem 15:20", msg: "Conseguimos reproduzir aqui. Investigando o limite de memória do processo." }, + ], + }, + { + id: 1155, form: "Chamado de atendimento de TI", sector: "TI", status: "IN_PROGRESS", + priority: "low", requester: "Gilnei Wolf", initials: "GW", assignee: "Gilnei Wolf", + age: "há 1 dia", sla: "Dentro do prazo", slaRisk: false, tag: "Equipe Dev", + summary: "Solicitação de acesso ao painel de relatórios de vendas.", + fieldsData: [ + ["Setor", "Comercial"], + ["Acesso", "Painel de relatórios de vendas"], + ["Justificativa", "Acompanhamento de metas mensais"], + ], + thread: [ + { who: "Gilnei Wolf", role: "Solicitante", time: "Ontem 09:40", msg: "Preciso de acesso ao painel de relatórios para acompanhar metas." }, + ], + }, + { + id: 1156, form: "Usuários SIGIN / Windows / E-mails", sector: "TI", status: "COMPLETED", + priority: "medium", requester: "Caroline Doege", initials: "CD", assignee: "Caroline Doege", + age: "há 22 horas", sla: "Concluído", slaRisk: false, isNew: true, + summary: "Criação de usuário Windows e e-mail para nova colaboradora.", + fieldsData: [ + ["Colaborador", "Maria Eduarda Lopes"], + ["Setor", "Financeiro"], + ["Acessos", "Windows, E-mail, SIGIN"], + ["Data de início", "26/05/2026"], + ], + thread: [ + { who: "Caroline Doege", role: "Solicitante", time: "Ontem 10:00", msg: "Nova colaboradora começa segunda, precisa dos acessos." }, + { who: "Equipe Dev", role: "Técnico", time: "Ontem 16:30", msg: "Usuário criado e credenciais enviadas. Chamado concluído." }, + ], + }, + { + id: 1154, form: "Chamado de atendimento de TI", sector: "TI", status: "COMPLETED", + priority: "low", requester: "Caroline Doege", initials: "CD", assignee: "Caroline Doege", + age: "há 1 dia", sla: "Concluído", slaRisk: false, + summary: "Impressora do RH sem comunicação com o servidor de impressão.", + fieldsData: [ + ["Setor", "RH"], + ["Equipamento", "Impressora HP LaserJet"], + ["Problema", "Sem comunicação com servidor"], + ], + thread: [ + { who: "Caroline Doege", role: "Solicitante", time: "2 dias atrás", msg: "A impressora do RH parou de imprimir." }, + { who: "Gilnei Wolf", role: "Técnico", time: "Ontem 11:15", msg: "Reinstalado driver e fila de impressão. Funcionando." }, + ], + }, + { + id: 1152, form: "Chamado de atendimento de TI", sector: "TI", status: "NOT_STARTED", + priority: "medium", requester: "Lenilson Fonseca", initials: "LF", assignee: null, + age: "há 1 dia", sla: "Vence em 6h", slaRisk: false, + summary: "Telefone IP do setor de garantia sem linha.", + fieldsData: [ + ["Setor", "Garantia"], + ["Equipamento", "Telefone IP Grandstream"], + ["Problema", "Sem linha / sem tom"], + ], + thread: [ + { who: "Lenilson Fonseca", role: "Solicitante", time: "Ontem 13:50", msg: "O telefone da garantia está sem linha desde ontem." }, + ], + }, + { + id: 1145, form: "Material de uso interno", sector: "Compras", status: "NOT_STARTED", + priority: "low", requester: "Ana Beatriz Kunz", initials: "AK", assignee: null, + age: "há 2 dias", sla: "Vence em 1 dia", slaRisk: false, + summary: "Reposição de material de escritório para o setor financeiro.", + fieldsData: [ + ["Setor", "Financeiro"], + ["Itens", "Papel A4, canetas, grampeador"], + ["Quantidade", "5 resmas, 2 cx canetas"], + ], + thread: [ + { who: "Ana Beatriz Kunz", role: "Solicitante", time: "2 dias atrás", msg: "Acabou o papel A4 no financeiro." }, + ], + }, +]; + +// Contadores de coluna (do kanban real) +window.BOARD_COUNTS = { NOT_STARTED: 18, IN_PROGRESS: 21, COMPLETED: 746 }; + +window.STATUS_META = { + NOT_STARTED: { label: "Não iniciado", short: "Não iniciado", tone: "neutral" }, + IN_PROGRESS: { label: "Em progresso", short: "Em progresso", tone: "active" }, + COMPLETED: { label: "Concluído", short: "Concluído", tone: "done" }, +}; + +window.PRIORITY_META = { + high: { label: "Alta", tone: "high" }, + medium: { label: "Média", tone: "medium" }, + low: { label: "Baixa", tone: "low" }, +}; + +/* ============================================================ + Definição dos campos de cada formulário (visão responder) + Espelha src/lib/form-types.ts: + text | number | checkbox | formatted | combobox | file | textarea | dynamic + ============================================================ */ +const opt = (arr) => arr.map((v) => (typeof v === "string" ? { label: v, value: v } : v)); + +window.FORM_FIELDS = { + "chamado-ti": [ + { name: "solicitante", type: "dynamic", dynamicType: "user_name", label: "Solicitante" }, + { name: "setor", type: "dynamic", dynamicType: "user_sector", label: "Setor" }, + { name: "categoria", type: "combobox", label: "Categoria do atendimento", required: true, + placeholder: "Selecione a categoria", + options: opt(["Internet / Rede", "Cabeamento", "Depósito", "Recebimento", "Setor de garantia", "Outro"]) }, + { name: "local", type: "text", label: "Local / estação de trabalho", required: true, placeholder: "Ex.: Galpão 2 — baia 4" }, + { name: "urgencia", type: "combobox", label: "Urgência", required: true, placeholder: "Selecione a urgência", + options: opt(["Baixa", "Média", "Alta — operação parada"]) }, + { name: "descricao", type: "textarea", label: "Descreva o problema", required: true, rows: 4, + placeholder: "Conte o que está acontecendo, desde quando, e o que já tentou.", + helpText: "Quanto mais detalhes, mais rápido conseguimos resolver." }, + { name: "anexo", type: "file", label: "Anexar foto ou print", acceptedFileTypes: ".pdf,.jpg,.png", multipleFiles: true }, + ], + mkt: [ + { name: "solicitante", type: "dynamic", dynamicType: "user_name", label: "Solicitante" }, + { name: "setor", type: "dynamic", dynamicType: "user_sector", label: "Setor" }, + { name: "tipo", type: "combobox", label: "Tipo de material", required: true, placeholder: "Selecione o tipo", + options: opt(["Adesivo", "Placa / sinalização", "Organização de evento", "Brinde", "Arte para redes", "Outro"]) }, + { name: "titulo", type: "text", label: "Título da peça / evento", required: true, placeholder: "Ex.: Sinalização da nova doca" }, + { name: "briefing", type: "textarea", label: "Briefing", required: true, rows: 4, + placeholder: "Objetivo, público, mensagem principal, formato e medidas." }, + { name: "prazo", type: "text", label: "Prazo desejado", required: true, placeholder: "dd/mm/aaaa" }, + { name: "referencias", type: "file", label: "Referências (opcional)", acceptedFileTypes: ".pdf,.jpg,.png", multipleFiles: true }, + ], + material: [ + { name: "solicitante", type: "dynamic", dynamicType: "user_name", label: "Solicitante" }, + { name: "setor", type: "dynamic", dynamicType: "user_sector", label: "Setor" }, + { name: "categoria", type: "combobox", label: "Categoria", required: true, placeholder: "Selecione a categoria", + options: opt(["Escritório", "Limpeza", "EPIs", "Eletrônicos", "Embalagens"]) }, + { name: "item", type: "text", label: "Item", required: true, placeholder: "Ex.: Papel sulfite A4 75g" }, + { name: "quantidade", type: "number", label: "Quantidade", required: true, min: 1, placeholder: "0" }, + { name: "unidade", type: "combobox", label: "Unidade", required: true, placeholder: "Selecione a unidade", + options: opt(["Unidade", "Caixa", "Pacote", "Resma", "Litro", "Par"]) }, + { name: "centro", type: "text", label: "Centro de custo", placeholder: "Ex.: 4.01 — Administrativo" }, + { name: "justificativa", type: "textarea", label: "Justificativa", rows: 3, placeholder: "Para que será usado este material?" }, + { name: "urgente", type: "checkbox", label: "Compra urgente", placeholder: "Marcar como urgente (ruptura de estoque)" }, + ], + tec: [ + { name: "solicitante", type: "dynamic", dynamicType: "user_name", label: "Solicitante" }, + { name: "setor", type: "dynamic", dynamicType: "user_sector", label: "Setor" }, + { name: "nome", type: "text", label: "Nome do app / sistema", required: true, placeholder: "Como o sistema deve se chamar?" }, + { name: "origem", type: "combobox", label: "Origem", required: true, placeholder: "Selecione a origem", + options: opt(["Desenvolvimento interno", "Fornecedor externo", "Ainda não definido"]) }, + { name: "objetivo", type: "textarea", label: "Objetivo da implementação", required: true, rows: 4, + placeholder: "Qual problema esse sistema resolve? Quem vai usar?" }, + { name: "setores", type: "combobox", label: "Setores impactados", multiple: true, placeholder: "Selecione os setores", + options: opt(["TI", "Comercial", "Financeiro", "Expedição", "RH", "Compras", "Marketing"]) }, + { name: "meta", type: "text", label: "Prazo / meta", placeholder: "dd/mm/aaaa" }, + ], + infra: [ + { name: "solicitante", type: "dynamic", dynamicType: "user_name", label: "Solicitante" }, + { name: "setor", type: "dynamic", dynamicType: "user_sector", label: "Setor" }, + { name: "tipo", type: "combobox", label: "Tipo de serviço", required: true, placeholder: "Selecione o tipo", + options: opt(["Predial", "Elétrica", "Hidráulica", "Conserto de equipamento", "Outro"]) }, + { name: "local", type: "text", label: "Local", required: true, placeholder: "Unidade / setor / sala" }, + { name: "urgencia", type: "combobox", label: "Urgência", required: true, placeholder: "Selecione a urgência", + options: opt(["Baixa", "Média", "Alta"]) }, + { name: "descricao", type: "textarea", label: "Descrição do problema", required: true, rows: 4, + placeholder: "Descreva o que precisa de manutenção." }, + { name: "ramal", type: "formatted", formattedType: "phone", label: "Ramal / telefone para contato", placeholder: "(00) 00000-0000" }, + { name: "risco", type: "checkbox", label: "Risco à segurança", placeholder: "Há risco à segurança das pessoas" }, + { name: "foto", type: "file", label: "Foto do local (opcional)", acceptedFileTypes: ".jpg,.png", multipleFiles: true }, + ], + usuarios: [ + { name: "solicitante", type: "dynamic", dynamicType: "user_name", label: "Solicitante" }, + { name: "tipo", type: "combobox", label: "Tipo de solicitação", required: true, placeholder: "Selecione o tipo", + options: opt(["Criar usuário", "Ajustar acessos", "Desativar usuário", "Redefinir senha"]) }, + { name: "colaborador", type: "text", label: "Nome do colaborador", required: true, placeholder: "Nome completo" }, + { name: "cargo", type: "text", label: "Cargo / função", required: true, placeholder: "Ex.: Assistente financeiro" }, + { name: "acessos", type: "combobox", label: "Acessos necessários", multiple: true, placeholder: "Selecione os acessos", + options: opt(["SIGIN", "Windows", "E-mail corporativo", "VPN"]) }, + { name: "setorColab", type: "text", label: "Setor do colaborador", required: true, placeholder: "Ex.: Financeiro" }, + { name: "inicio", type: "text", label: "Data de início", placeholder: "dd/mm/aaaa" }, + ], +}; diff --git a/public/handoffs/design_handoff_solicitacoes_2/icons.jsx b/public/handoffs/design_handoff_solicitacoes_2/icons.jsx new file mode 100644 index 0000000..8f55235 --- /dev/null +++ b/public/handoffs/design_handoff_solicitacoes_2/icons.jsx @@ -0,0 +1,67 @@ +/* Ícones minimalistas (estilo Lucide), stroke currentColor */ +const Ic = ({ d, size = 18, sw = 2, fill = "none", children, style }) => ( + + {d ? : children} + +); + +// elo brand glyph (chain link, rotated like favicon) +const EloGlyph = ({ size = 28 }) => ( + + + + +); + +const I = { + dash: (p) => } />, + reserve: (p) => } />, + mega: (p) => , + file: (p) => } />, + bulb: (p) => } />, + form: (p) => } />, + cart: (p) => } />, + phone: (p) => , + admin: (p) => } />, + spark: (p) => , + sun: (p) => } />, + moon: (p) => , + menu: (p) => } />, + chev: (p) => , + chevDown: (p) => , + eye: (p) => } />, + edit: (p) => } />, + plus: (p) => } />, + search: (p) => } />, + msg: (p) => , + tag: (p) => } />, + filter: (p) => , + clock: (p) => } />, + user: (p) => } />, + users: (p) => } />, + check: (p) => , + checkCircle: (p) => } />, + arrowRight: (p) => , + layout: (p) => } />, + board: (p) => } />, + list: (p) => } />, + send: (p) => , + alert: (p) => } />, + inbox: (p) => } />, + bolt: (p) => , + paperclip: (p) => , + more: (p) => } />, + settings: (p) => } />, + hash: (p) => } />, + calendar: (p) => } />, + trend: (p) => } />, + arrowLeft: (p) => , + lock: (p) => } />, + building: (p) => } />, + home: (p) => } />, + refresh: (p) => } />, +}; + +Object.assign(window, { Ic, I, EloGlyph }); diff --git a/public/handoffs/design_handoff_solicitacoes_2/pages.jsx b/public/handoffs/design_handoff_solicitacoes_2/pages.jsx new file mode 100644 index 0000000..fbd0ffd --- /dev/null +++ b/public/handoffs/design_handoff_solicitacoes_2/pages.jsx @@ -0,0 +1,662 @@ +/* ============================================================ + SUB-PÁGINAS DO MÓDULO (visão cliente) + - FormRootPage → /forms/{slug} + - RespondPage → /forms/{slug}/responder + - MyResponsesPage→ /forms/minhas-solicitacoes + Espelha FormsSubPageShell + FormResponseComponent + UserResponsesList + ============================================================ */ + +/* ---------- trilha de navegação (breadcrumb) ---------- */ +function Breadcrumb({ items }) { + return ( + + ); +} + +/* ---------- casca padrão das sub-páginas ---------- */ +function SubPageShell({ breadcrumbs, backFn, title, titlePrefix, description, actions, children }) { + return ( +
+ + {breadcrumbs && } +
+
+ {backFn && ( + + )} +
+ {titlePrefix} +

{title}

+
+ {description &&
{description}
} +
+ {actions &&
{actions}
} +
+ {children} +
+ ); +} + +function Panel({ children, style }) { + return
{children}
; +} + +/* ============================================================ + CONTROLES DE CAMPO (renderiza por tipo, como no codebase real) + ============================================================ */ +function MultiCombo({ field, value, onChange, disabled }) { + const [open, setOpen] = React.useState(false); + const ref = React.useRef(null); + React.useEffect(() => { + const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; + document.addEventListener("mousedown", h); + return () => document.removeEventListener("mousedown", h); + }, []); + const toggle = (v) => onChange(value.includes(v) ? value.filter(x => x !== v) : [...value, v]); + return ( +
+ + {open && !disabled && ( +
+ {field.options.map(o => ( + + ))} +
+ )} +
+ ); +} + +function FileInput({ field, value, onChange, disabled }) { + const id = "file-" + field.name; + return ( + + ); +} + +function FieldInput({ field, value, onChange, disabled, error }) { + const f = field; + const errC = error ? " fr-err" : ""; + if (f.type === "text" || f.type === "formatted") + return onChange(e.target.value)} />; + if (f.type === "number") + return onChange(e.target.value)} />; + if (f.type === "textarea") + return