diff --git a/AGENTS.md b/AGENTS.md index b24f85f..5811186 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,9 @@ # SGF Tempo — instruções locais +- Preservação autorizada em 28/10/2026: registar avaliação técnica da página inicial/menu e criar checkpoint local sem credenciais/dados privados; não presumir revisão visual ou aceitação visual. Encerramento do orçamento permanece em `04870f0` com limitações explícitas. Próximo passo autorizado apenas de especificação: oficina → rascunho de facturação; definir facturabilidade, revisão, preços e duplicações antes de implementar. `110000`, sete concessões e utilização real permanecem por autorizar. + +- Autorização posterior de 28/10/2026: implementar página inicial e navegação agrupada após a proposta `docs/HOME_NAVIGATION_PROPOSAL.md`. Ver `docs/HOME_NAVIGATION_DELIVERY.md`. Preservar `04870f0` e anteriores, direitos por contexto e bloqueios. Sem novos acessos, módulos financeiros, contagens/indicadores fictícios ou uso real; fontes agregadas de pendências não implementadas. Prova visual permanece separada, sem presumir aceitação ou checkpoint. `110000` e sete concessões continuam por autorizar. + - Aceitação específica de 28/10/2026: utilizador aceitou o ensaio de orçamento versus realizado com limitação das capturas documentada e autorizou checkpoint exclusivamente local, sem credenciais/dados privados. Ver `docs/INDUSTRIAL_BUDGET_ACCEPTANCE.md`. Não transformar provas pendentes em realizadas; automação Edge bloqueada e capturas não recebidas permanecem limitações. Substitui apenas a pendência de aceitação/encerramento. `110000`, sete concessões e utilização real continuam por autorizar; sem aplicação ou publicação. Próxima prioridade: página inicial e menus agrupados, preservando entregas e direitos existentes. - Autorização de 28/10/2026: implementar o ensaio de orçamento versus realizado da secção 16 de `docs/INDUSTRIAL_COST_ACCOUNTING_SCOPE.md`, só sintético em `sgf_tempo_test`, preservando `cbdf2b4`, `4a94e8a` e alterações posteriores. Orçamento aprovado como hipótese: 900 material + 600 trabalho + 300 indirectos imputados; comparar versão validada de 1000/500/300, 10 UN, MZN, mesmo produto/lote/período, com desvios +100/−100/0 e total 0; residual 300 separado. Osmane prepara; Samira valida orçamento exacto por identidade distinta e consulta comparação. Direitos próprios, sem herança ou concessões reais. Sem taxa material: 800 conhecidos, comparação incompleta, nunca poupança. Preservar versões/origens e bloqueios SQL/aplicação. Migração `110000` e sete concessões apenas propostas, NÃO aplicar em desenvolvimento. Sem dependências novas, stock, lançamentos, facturação, publicação ou aceitação presumida; checkpoint de encerramento só após aceitação. Ver `docs/INDUSTRIAL_BUDGET_DELIVERY.md` para estado e provas. diff --git a/README.md b/README.md index 70b2e2b..6720c90 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,8 @@ O primeiro núcleo de cálculo de serviço e a política operacional de truncage O menu **Classificação contabilística**, disponível após actualizar a sessão com os acessos especificamente concedidos, permite preparar e submeter versões PGC-NIRF, validar/recusar sem editar e consultar classificação por data. Categoria e excepções são empresariais; operações e respectivos históricos continuam limitados às unidades autorizadas. As tabelas reais começam vazias, sem contas de movimento ou regras aprovadas por omissão. Consultar [utilização, provas e limites da classificação](docs/CATALOGUE_ACCOUNTING_DELIVERY.md). ## Ensaio isolado de custo directo +Página inicial e menus agrupados: [alterações, verificações e limites visuais](docs/HOME_NAVIGATION_DELIVERY.md). Os atalhos dependem dos acessos existentes; não apresentam contagens de pendências ou indicadores financeiros simulados. + O ensaio de orçamento versus realizado está descrito em [INDUSTRIAL_BUDGET_DELIVERY.md](docs/INDUSTRIAL_BUDGET_DELIVERY.md), com percurso, estado das provas e propostas separadas. Continua limitado a `sgf_tempo_test`; a instalação normal e os acessos novos não são aplicados por arrasto. O percurso `WORKSHOP-DIRECT-COST-SIM-v1` e as instruções de prova estão em [WORKSHOP_DIRECT_COST_DELIVERY.md](docs/WORKSHOP_DIRECT_COST_DELIVERY.md). Reutiliza apontamentos verificados sem duplicar consumos; não activa custeio real. Migração `090000` e quatro concessões continuam propostas, não aplicadas. diff --git a/docs/HOME_NAVIGATION_DELIVERY.md b/docs/HOME_NAVIGATION_DELIVERY.md new file mode 100644 index 0000000..bbe13d8 --- /dev/null +++ b/docs/HOME_NAVIGATION_DELIVERY.md @@ -0,0 +1,35 @@ +# Página inicial e menus agrupados + +## Avaliação técnica e preservação local — 28/10/2026 + +A pedido do utilizador, preservar esta implementação num checkpoint exclusivamente local. Avaliação do agente: organização coerente com os destinos existentes, grupos filtrados por contexto, distinção entre consulta/preparação/validação, ausência de indicadores fictícios e identidade visual alinhada com as folhas públicas do InvesteMoz. Build, renderização por perfil e entrega HTTP aprovados. Esta é avaliação técnica do código e dos resultados automatizados, não aprovação visual pessoal do utilizador. Capturas e verificação visual/interactiva no Edge continuam indisponíveis; o checkpoint não as transforma em provas realizadas. Nenhum segredo ou dado privado incluído, sem push/publicação. Encerramento do ensaio de orçamento conferido em `04870f0` e `docs/INDUSTRIAL_BUDGET_ACCEPTANCE.md`, mantendo expressamente as limitações originais. + +## Compatibilização visual com InvesteMoz — 28/10/2026 + +Por pedido posterior do utilizador, acrescentada `resources/css/tempo-theme.css`, importada depois dos estilos base do SGF. Referência conferida exclusivamente em leitura nas folhas de apresentação públicas `resources/css/app.css` e `resources/css/tempo-header.css` do portal-investidor: cabeçalho #090909, destaque #ed1c24, cinzentos neutros, superfície branca, bordas #e2e8f0 e raio de 10px. Não se acedeu a configurações privadas nem se alteraram ficheiros, dados ou serviços do InvesteMoz. + +Aplicados ao SGF: navegação em carvão, marcador activo vermelho, botões principais vermelhos, cabeçalho/conteúdo claros, cartões neutros, hierarquia de títulos, campos e tabelas alinhados com a mesma linguagem visual. Grupos e destinos permanecem os autorizados, sem copiar menu público, marcas ou conteúdos do portal. Verde/âmbar/erro mantêm significado de estados, distinguindo-os da cor de marca. Instrument Sans é só preferência local, com fallback de sistema; nenhum tipo de letra ou recurso externo foi descarregado. Compilação aprovada com Vite 8.3.5 e diff sem erros. Sem nova prova visual no Edge: compatibilização fundamentada em CSS, com revisão visual manual ainda pendente. + +Verificação HTTP posterior: na instância `8002`, diagnóstico confirmou `browser-testing`/`sgf_tempo_test`; página inicial, JavaScript e CSS responderam 200. O HTML referencia o ficheiro JavaScript do manifesto actual e o CSS servido contém o destaque #ed1c24. Esta prova confirma a entrega dos ficheiros compilados, sem comprovar o aspecto visual ou a interacção no navegador. + +28/10/2026. Implementação autorizada após [mapa e esboço](HOME_NAVIGATION_PROPOSAL.md). Preservado o ensaio aceite em `04870f0`, checkpoints anteriores e limites financeiros. Sem migrações, concessões, novas dependências ou activação real. Sem aceitação presumida ou checkpoint desta alteração. + +## Alterações + +Barra lateral com oito grupos: Início, Clientes e serviços, Facturação, Recebimentos, Oficinas, Custos e orçamento, Contabilidade e fiscalidade e Administração. Só aparecem grupos com destinos permitidos. Grupo da página actual sempre aberto, opção activa destacada com marcador e `aria-current`; restantes grupos expansíveis com `aria-expanded`. Destinos reaproveitam os ecrãs existentes, sem criar módulos de equipamentos, afectações ou séries independentes. Apresentação do rascunho permanece em Facturação. Empresa/unidade continuam em contexto explícito; mudança de acessos que retire o destino devolve à página inicial quando a sessão actualizada recebe essas concessões. + +Página inicial com atalhos de preparação/acompanhamento e validação conforme os direitos, nunca conforme nomes/emails. Consulta mantém atalhos de leitura. As acções abrem os percursos existentes: não criam decisões ou alteram dados no painel inicial. Não há contagens, montantes, gráficos ou listas de tarefas inventados. Consulta agregada de pendências ainda não implementada: mensagem explícita explica que os estados são consultados no respectivo percurso. Consulta não confere preparação/validação. + +Cabeçalho fixo com empresa/unidade, ambiente local e aviso de operações reais bloqueadas. Os avisos específicos dos módulos permanecem. Em ecrã pequeno, botão Menu abre um diálogo nativo com fecho por botão/Escape e gestão de foco do navegador. Menu mantém conta actual e saída. Tabelas directamente contidas em painéis têm scroll próprio; contexto e atalhos adaptam-se a uma coluna. Cores base preservadas. + +## Verificações e limites + +- `scripts/sgf.ps1 npm run build`: aprovado, Vite 8.3.5, sem alteração de dependências. +- `scripts/sgf.ps1 npm exec --offline -c 'node scripts/home-navigation-check.mjs'`: verificações de renderização aprovadas para preparador, validador, leitor, outra unidade e ausência de permissões. Não usam base de dados ou navegador. +- `git diff --check`: aprovado. +- Não se repetiu a bateria financeira: alteração limitada à apresentação, sem mudanças no servidor, cálculos ou base. A prova manual industrial anterior permanece preservada. +- **Sem validação visual nova no Edge ou capturas:** automação continua bloqueada; não foi repetida. Renderização e build não provam comportamento visual ou interacção real do diálogo móvel. Revisão manual a efectuar no navegador. + +Ficheiros: `resources/js/WorkspaceNavigation.jsx`, `resources/js/WorkspaceHome.jsx`, integração em `resources/js/app.jsx`, estilos em `resources/css/app.css`, verificação `scripts/home-navigation-check.mjs` e documentação. Não foram aplicadas `110000` ou sete concessões por consequência desta alteração. + +Para rever: actualizar `http://127.0.0.1:8002/` enquanto a instância de prova SGF estiver activa, entrar pelas contas sintéticas já preparadas e abrir Início. Conferir grupos de acordo com concessões, preparação versus validação, permanência do grupo actual aberto e menu em janela estreita, Escape e retorno de foco. Não enviar credenciais em capturas. Se a instância estiver parada, seguir o procedimento de retoma de prova isolada documentado, sem executar testes destrutivos sobre a fixture manual. diff --git a/docs/HOME_NAVIGATION_PROPOSAL.md b/docs/HOME_NAVIGATION_PROPOSAL.md new file mode 100644 index 0000000..64ce988 --- /dev/null +++ b/docs/HOME_NAVIGATION_PROPOSAL.md @@ -0,0 +1,60 @@ +# Página inicial e navegação agrupada — proposta + +28/10/2026. Primeiro passo de desenho solicitado pelo utilizador; **sem alteração da aplicação**. Preservar o ensaio aceite no checkpoint `04870f0` e anteriores. A migração `110000`, sete concessões e utilização real continuam por autorizar. Este mapa não cria funcionalidades, direitos ou acesso operacional. + +## Base actual conferida no código + +`resources/js/app.jsx` apresenta uma lista lateral plana, filtrada pelas concessões do contexto, e destaca a página seleccionada. A página inicial tem saudação e dois cartões: empresa e unidade. O cabeçalho mostra o contexto, com selectores de empresa/unidade antes do conteúdo. Os módulos de ensaio apresentam avisos próprios; não há hoje uma lista agregada de tarefas pendentes na página inicial. Esta descrição é conferência de fonte, não captura visual da aplicação em execução. + +Captura actual da página inicial/menu ainda não disponível. A automação do Edge permaneceu bloqueada e não deve ser repetida ou contornada. Para ajustar o desenho visual ao ecrã real, a captura manual deve mostrar a página inicial e o menu, sem credenciais; dados pessoais podem ser ocultados. O protótipo abaixo é um esboço proposto, nunca uma captura. + +## Mapa proposto e correspondência com o existente + +| Grupo | Destinos existentes a reaproveitar | Limites e subdivisões futuras | +| --- | --- | --- | +| Início | Área inicial | Contexto, atalhos permitidos e pendências apenas onde existam dados/consulta autorizada | +| Clientes e serviços | Clientes (`clients`), Catálogo de serviços (`services`), Prestações em rascunho (`service_drafts`) | Manter nomes que distingam rascunho de operação confirmada | +| Facturação | Preparação documental (`documents`), Rascunhos de facturação (`billing_drafts`), Facturas de teste (`service_invoices`), Apresentação do rascunho (`preview_model`) | Documentos/séries pertencem ao percurso existente de facturas de teste; não criar emissão real ou entrada independente sem suporte | +| Recebimentos | Recebimentos simulados (`service_receivables`) | Afectações/reconciliação mantêm-se dentro do percurso de teste existente; sem novas páginas vazias ou ligação bancária | +| Oficinas | Oficina de teste (`workshop`) | Ordens, tarefas e apontamentos continuam no módulo existente; equipamentos só como destino futuro se houver funcionalidade autorizada, nunca botão fictício | +| Custos e orçamento | Custos directos de teste (`direct_costs`), Custos industriais de teste (`industrial_costs`), Orçamento versus realizado (`industrial_budgets`) | Lotes pertencem ao módulo industrial; orçamento/comparação partilham o ecrã actual. Não dividir páginas sem necessidade confirmada | +| Contabilidade e fiscalidade | Classificação contabilística (`catalogue_accounting`), Perfil fiscal do serviço (`service_fiscal`) | Preservar validações e bloqueios fiscais; sem lançamentos | +| Administração | Empresa (`company`), Unidades (`units`), Acessos (`access`) | Apresentação do rascunho pode ficar aqui em vez de Facturação, mas nunca duplicada; configurações adicionais só se existentes e autorizadas | + +O grupo só aparece se houver pelo menos um destino permitido. Destino e acção mantêm as verificações existentes; nome/email não atribuem perfil. As designações Osmane/Samira exemplificam distribuições já concedidas, sem associação nominativa no código. Consulta não implica preparação, validação ou administração. + +## Esboço da página inicial — estrutura, sem dados inventados + +```text +┌────────────────────────┬─────────────────────────────────────────────┐ +│ SGF TEMPO │ Empresa seleccionada · Unidade seleccionada │ +│ │ Alterar contexto │ +│ ● Início ├─────────────────────────────────────────────┤ +│ ▸ Clientes e serviços │ Ambiente / avisos de ensaio e bloqueios │ +│ ▸ Facturação ├─────────────────────────────────────────────┤ +│ ▸ Recebimentos │ Início │ +│ ▸ Oficinas │ Trabalho no contexto seleccionado │ +│ ▸ Custos e orçamento │ │ +│ ▸ Contab. e fiscalidade│ A preparar e acompanhar | A validar │ +│ ▸ Administração │ Apenas acções permitidas; estados reais │ +│ │ │ +│ Conta actual │ Atalhos permitidos │ +│ Sair │ Clientes · Rascunhos · Consultas autorizadas │ +└────────────────────────┴─────────────────────────────────────────────┘ +``` + +Em destino interno, o grupo dessa página permanece aberto, com opção activa destacada por texto e marcador além da cor. Cabeçalhos expansíveis são botões de teclado com `aria-expanded`; destino activo usa `aria-current`. Abrir um grupo não altera o contexto nem concede acesso. + +Para o preparador, dar prioridade a «A preparar e acompanhar», com atalhos para as operações que já pode iniciar e consulta do estado das suas propostas. Para a validadora, dar prioridade a «A validar», com ligações a versões exactas pendentes e acesso de consulta. Quem tiver ambos os tipos de poderes vê ambas as secções, sem poder validar a própria proposta. Não criar uma decisão directamente no cartão inicial: encaminhar para o percurso de revisão existente. + +**Pendências:** ainda é necessário definir quais os módulos que fornecem consulta segura e actualizada de pendências. Não inferir contagens no cliente, consultar módulos sem autorização nem tratar erro/indisponibilidade como zero. Onde não houver fonte, mostrar apenas atalhos; não apresentar listas ou números simulados. Distinguir «Sem pendências» (consulta bem-sucedida), «Consulta indisponível» e «Escolha uma unidade». Rótulos do esboço não afirmam que haja tarefas concretas. + +## Ecrãs pequenos, estados e contexto + +Em ecrã pequeno, menu em painel recolhível por botão «Menu», fechado por Escape, foco devolvido ao botão e destino activo identificável. Empresa/unidade e aviso relevante permanecem visíveis no cabeçalho; selectores podem abrir por «Alterar contexto». Conteúdo em coluna única, alvos de toque confortáveis e tabelas com scroll horizontal próprio, sem esconder montantes/estados ou provocar scroll de toda a página. + +Estados com rótulo textual consistente: «Rascunho», «Aguarda validação», «Validado», «Recusado», «Histórico/desactualizado», «Incompleto» e «Operação bloqueada». Verde discreto para validação concluída, âmbar para pendência/incompletude, neutro para histórico e vermelho para erro; cor nunca é a única informação. Preservar cores base do SGF; melhorar espaçamento, alinhamento de montantes e leitura de tabelas. Não introduzir indicadores financeiros, saldos, receitas, margens ou gráficos fictícios. + +## Próxima revisão do desenho + +Comparar este mapa/esboço com uma captura manual da página inicial e menu actuais. Confirmar onde fica «Apresentação do rascunho» e a primeira selecção de fontes reais de pendências. Só depois alterar a aplicação, preservando permissões, separação empresa/unidade e avisos. Nenhuma nova implementação ou validação visual é declarada por este documento. diff --git a/resources/css/app.css b/resources/css/app.css index a18b752..54d7664 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -14,3 +14,36 @@ .invoice-test-document{margin:24px 0}.invoice-test-document>p{white-space:pre-wrap;margin:8px 0;line-height:1.6}.invoice-test-document>h3{border-top:1px solid #dce6e1;padding-top:18px;margin-top:26px;color:#176b55}.invoice-test-document form{margin-top:28px;border-top:1px solid #dce6e1;padding-top:24px}.invoice-pdf-link{display:inline-block;padding:12px 18px;background:#176b55;color:#fff;border-radius:7px;text-decoration:none;font-weight:650} .invoice-test-workspace>.fiscal-notice{position:sticky;top:0;z-index:20} + +/* Grouped workspace navigation: permissions remain owned by the existing modules. */ +.grouped-sidebar{width:270px;position:sticky;top:0;height:100dvh;overflow-y:auto} +.grouped-sidebar .sidebar-label{margin-top:30px} +.navigation-group{margin:0 0 5px} +.sidebar nav .navigation-heading{display:flex;justify-content:space-between;gap:12px;width:100%;color:#f5fbf7;font-size:.86rem;padding:12px 10px} +.sidebar nav .navigation-item{display:block;width:100%;padding:11px 12px 11px 24px;font-size:.83rem;line-height:1.5;border-left:3px solid transparent;border-radius:0 7px 7px 0} +.sidebar nav .navigation-item.active{border-left-color:#bfe4cf;background:#275b4b;color:white;font-weight:750} +.sidebar button:focus-visible{outline:2px solid #cbe8dc;outline-offset:2px} +.workspace>header{position:sticky;top:0;z-index:25;flex-wrap:wrap;box-shadow:0 2px 8px #123f3508} +.workspace>header>.workspace-safety{flex-basis:100%;margin:4px 0 0} +.workspace-safety{padding:12px 16px;background:#fff6df;border:1px solid #e6d4a2;color:#6e551c;border-radius:8px;margin-bottom:20px;font-size:.85rem;line-height:1.6} +.home-work-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));gap:20px} +.home-work-grid .panel{margin-bottom:20px} +.home-work-grid h2,.home-context h2{margin-top:0} +.home-shortcuts{display:flex;flex-wrap:wrap;gap:10px} +.home-shortcuts button{line-height:1.5;text-align:left} +.home-notice{background:#fff6df;color:#6e551c} +.home-context dl{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin:24px 0} +.home-context dt{font-size:.8rem;color:#5d716d;margin-bottom:6px}.home-context dd{margin:0;font-weight:600} +.panel:has(>table){overflow-x:auto}.panel>table{min-width:540px}td,dd{overflow-wrap:anywhere} +.mobile-menu-toggle,.navigation-drawer{display:none} +@media(max-width:800px){ + .grouped-sidebar{display:none}.shell{display:block}.workspace>header{padding:20px 18px 20px 92px;min-height:94px} + .mobile-menu-toggle{display:block;position:fixed;top:23px;left:14px;z-index:30;padding:10px} + .navigation-drawer[open]{display:block;width:min(90vw,340px);height:100dvh;max-height:100dvh;max-width:100vw;margin:0;padding:0;border:0;background:#123f35;color:#f5fbf7;overflow-y:auto} + .navigation-drawer::backdrop{background:#102c26aa}.navigation-drawer .sidebar{display:flex;width:100%;min-height:calc(100dvh - 60px);padding:20px} + .navigation-drawer .sidebar nav{display:flex;flex-direction:column;overflow:visible;white-space:normal}.navigation-drawer .sidebar-label{display:block;margin-top:24px} + .navigation-drawer .sidebar-bottom{display:block;padding-top:28px}.drawer-close{margin:12px 16px 0;background:#275b4b;color:white;border-color:#568475} + .navigation-drawer .sidebar-bottom strong,.navigation-drawer .sidebar-bottom small,.navigation-drawer .brand span{display:block} + .workspace main{padding:20px 16px}.home-context dl{grid-template-columns:1fr}.workspace header .local-badge{white-space:normal;text-align:center} + .home-shortcuts{display:grid;grid-template-columns:1fr}.context-card{grid-template-columns:1fr}.workspace header strong{overflow-wrap:anywhere}.workspace header .local-badge{display:block} +} diff --git a/resources/css/tempo-theme.css b/resources/css/tempo-theme.css new file mode 100644 index 0000000..22ff8fd --- /dev/null +++ b/resources/css/tempo-theme.css @@ -0,0 +1,50 @@ +/* SGF presentation aligned with the public InvesteMoz/Tempo stylesheet. + No external fonts, assets, services or application dependencies. */ +:root{ + --tempo-ink:#171717;--tempo-muted:#64748b;--tempo-line:#e2e8f0; + --tempo-canvas:#f8fafc;--tempo-paper:#fff;--tempo-dark:#090909; + --tempo-accent:#ed1c24;--tempo-accent-strong:#b91c1c;--tempo-accent-soft:#fef2f2; + font-family:'Instrument Sans','Segoe UI',system-ui,sans-serif; + color:var(--tempo-ink);background:var(--tempo-canvas); +} +button{color:var(--tempo-ink);border-color:var(--tempo-line);min-height:42px;border-radius:8px;transition:background-color .15s,border-color .15s} +button:hover{background:#f1f5f9;border-color:#cbd5e1} +button.primary,.invoice-pdf-link{background:var(--tempo-accent);border-color:var(--tempo-accent);color:white} +button.primary:hover,.invoice-pdf-link:hover{background:var(--tempo-accent-strong);border-color:var(--tempo-accent-strong)} +input,select,textarea{color:var(--tempo-ink);border-color:#cbd5e1;border-radius:8px} +input[readonly]{background:#f8fafc} +input:focus,select:focus,textarea:focus,button:focus-visible,a:focus-visible{outline:3px solid #ed1c2440;outline-offset:2px} +.muted,.help,footer{color:var(--tempo-muted)} +.eyebrow{color:#64748b;letter-spacing:.12em} +.workspace :is(h1,h2,h3,h4){color:var(--tempo-ink)} +.sidebar,.navigation-drawer[open]{background:var(--tempo-dark);color:white} +.sidebar{border-right:1px solid #ffffff14} +.sidebar .brand{letter-spacing:.09em;border-left:3px solid var(--tempo-accent);padding-left:12px} +.sidebar-label{color:#94a3b8} +.sidebar nav .navigation-heading{color:#e2e8f0;font-weight:600} +.sidebar nav .navigation-heading:hover{background:#ffffff0d} +.sidebar nav .navigation-item{color:#cbd5e1} +.sidebar nav .navigation-item:hover{background:#ffffff0d;color:white} +.sidebar nav .navigation-item.active{background:#ffffff10;color:white;border-left-color:var(--tempo-accent)} +.sidebar-bottom{border-top:1px solid #ffffff1a;margin-top:24px;padding-top:22px} +.sidebar-bottom small{color:#94a3b8}.sidebar-bottom button{border-color:#ffffff30;color:#e2e8f0} +.sidebar-bottom button:hover{background:#ffffff0d}.sidebar button:focus-visible{outline-color:#fb7185} +.workspace>header{background:white;border-bottom:1px solid var(--tempo-line);box-shadow:0 2px 8px #0f172a05;gap:12px} +.workspace header .muted{color:var(--tempo-muted)} +.local-badge{color:#475569;border-color:var(--tempo-line);background:#f8fafc} +.workspace-safety,.home-notice{background:#fffbeb;border-color:#fde68a;color:#854d0e} +.context-card{background:white;border-color:var(--tempo-line);border-radius:10px;margin-bottom:28px;box-shadow:0 1px 2px #0f172a03} +.context-card p{color:var(--tempo-muted)} +.panel{background:var(--tempo-paper);border-color:var(--tempo-line);border-radius:10px;box-shadow:0 1px 3px #0f172a04} +.page-title{margin:24px 0}.page-title h1{letter-spacing:-.035em} +.home-work-grid .panel{border-top:3px solid #171717} +.home-shortcuts{gap:10px}.home-shortcuts button{background:#fff;border-color:var(--tempo-line);font-weight:500} +.home-shortcuts button:hover{background:var(--tempo-accent-soft);border-color:#fecaca;color:var(--tempo-accent-strong)} +.home-context dt{color:var(--tempo-muted)} +th{background:#f8fafc;color:#475569}th,td{border-color:var(--tempo-line);line-height:1.6}td small{color:var(--tempo-muted)} +.unit-list li{border-color:var(--tempo-line)} +.drawer-close{background:#262626;color:white;border-color:#525252} +.login-layout>aside{background:var(--tempo-dark);border-top:5px solid var(--tempo-accent)} +.login-layout>aside p{color:#cbd5e1}.login-layout>aside small{color:#94a3b8} +.login-layout>aside .brand{color:white}.login-card .eyebrow{color:var(--tempo-accent-strong)} +@media(prefers-reduced-motion:reduce){button{transition:none}} diff --git a/resources/js/WorkspaceHome.jsx b/resources/js/WorkspaceHome.jsx new file mode 100644 index 0000000..1496b72 --- /dev/null +++ b/resources/js/WorkspaceHome.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +export default function WorkspaceHome({context,can,groups,navigate}) { + const unit=context.unit?.id??null; + const available=groups.flatMap(g=>g.items); + const links=(rules)=>rules.filter(([,permission,scope])=>can(permission,scope)).map(([id,,,label])=>({id,label:label??available.find(i=>i.id===id)?.label})).filter(i=>available.some(a=>a.id===i.id)); + const prepare=links([ + ['clients','clients.create',null],['services','services.create',null],['service_drafts','service_drafts.create',unit],['billing_drafts','billing_drafts.create',unit], + ['documents','issuer_documents.edit',null],['documents','client_documents.edit',null],['documents','draft_documents.edit',unit], + ['catalogue_accounting','catalogue_accounting.propose',null],['service_fiscal','service_fiscal.propose',null], + ['workshop','workshop.open',unit],['direct_costs','direct_costs.prepare',unit],['industrial_costs','industrial_costs.prepare',unit],['industrial_budgets','industrial_budgets.prepare',unit], + ]); + const validate=links([ + ['catalogue_accounting','catalogue_accounting.approve',null],['service_fiscal','service_fiscal.approve',null],['service_invoices','invoice_series.approve',null], + ['service_receivables','service_receivables.confirm',unit],['service_receivables','service_receivables.correction_approve',unit],['workshop','workshop.verify',unit],['direct_costs','direct_costs.validate',unit],['industrial_costs','industrial_costs.validate',unit],['industrial_budgets','industrial_budgets.validate',unit], + ]); + function actions(items){return
{[...new Map(items.map(i=>[i.id,i])).values()].map(i=>)}
;} + return <>

O SEU ESPAÇO DE TRABALHO

Início

Escolha o percurso no contexto seleccionado. Cada acção mantém a sua autorização própria.

+ {!context.unit&&

Seleccione uma unidade para consultar os percursos de trabalho dessa unidade.

} +
{prepare.length>0&&

A preparar e acompanhar

Abra o percurso para preparar propostas e acompanhar o estado das versões.

{actions(prepare)}
}{validate.length>0&&

A validar

Consulte as versões no respectivo percurso antes de tomar uma decisão. Não é permitida a auto-validação.

{actions(validate)}
}
+

Consultas e atalhos permitidos

{actions(available.filter(i=>!['home','company','units','access'].includes(i.id)))}
+

Esta página não agrega nem conta tarefas pendentes. Abra cada percurso para consultar os estados actuais.

+

Contexto actual

Empresa
{context.company.name}
Unidade
{context.unit?.name??'Sem unidade seleccionada'}
+ ; +} diff --git a/resources/js/WorkspaceNavigation.jsx b/resources/js/WorkspaceNavigation.jsx new file mode 100644 index 0000000..3b819d5 --- /dev/null +++ b/resources/js/WorkspaceNavigation.jsx @@ -0,0 +1,25 @@ +import React, {useRef, useState} from 'react'; + +export function navigationGroups(can, unit) { + const item=(id,label,permission,scope=null)=>({id,label,visible:can(permission,scope)}); + return [ + {id:'start',label:'Início',items:[{id:'home',label:'Página inicial',visible:true}]}, + {id:'services',label:'Clientes e serviços',items:[item('clients','Clientes','clients.view'),item('services','Catálogo de serviços','services.view'),item('service_drafts','Prestações em rascunho','service_drafts.view',unit)]}, + {id:'billing',label:'Facturação',items:[{id:'documents',label:'Preparação documental',visible:can('issuer_documents.view')||can('client_documents.view')||can('draft_documents.view',unit)},item('billing_drafts','Rascunhos de facturação','billing_drafts.view',unit),item('service_invoices','Documentos e séries de teste','service_invoices.view',unit),item('preview_model','Apresentação do rascunho','issuer_documents.view')]}, + {id:'receivables',label:'Recebimentos',items:[item('service_receivables','Recebimentos, afectações e reconciliação de teste','service_receivables.view',unit)]}, + {id:'workshop',label:'Oficinas',items:[item('workshop','Ordens, tarefas e apontamentos de teste','workshop.view',unit)]}, + {id:'costs',label:'Custos e orçamento',items:[item('direct_costs','Custo directo de teste','direct_costs.view',unit),item('industrial_costs','Lotes e custos industriais de teste','industrial_costs.view',unit),item('industrial_budgets','Orçamentos e comparação','industrial_budgets.view',unit)]}, + {id:'accounting',label:'Contabilidade e fiscalidade',items:[item('catalogue_accounting','Classificação contabilística','catalogue_accounting.view'),item('service_fiscal','Perfis fiscais do serviço','service_fiscal.view')]}, + {id:'admin',label:'Administração',items:[item('company','Empresa','company.view'),{id:'units',label:'Unidades autorizadas',visible:can('company.view')},item('access','Acessos','access.manage')]}, + ].map(g=>({...g,items:g.items.filter(i=>i.visible)})).filter(g=>g.items.length); +} + +export default function WorkspaceNavigation({groups,page,navigate,user,busy,logout}) { + const [expanded,setExpanded]=useState({});const drawer=useRef(null); + function choose(id){navigate(id);drawer.current?.close();} + function contents(prefix){return <>TEMPOSGF

ESPAÇO DE TRABALHO

{user.name}{user.email}
;} + return <>
{contents('mobile-nav')}
; +} diff --git a/resources/js/app.jsx b/resources/js/app.jsx index cb2506f..5d49dbb 100644 --- a/resources/js/app.jsx +++ b/resources/js/app.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import '../css/app.css'; +import '../css/tempo-theme.css'; import ServicesWorkspace, { serviceAreas } from './ServicesWorkspace.jsx'; import DocumentPreparationWorkspace from './DocumentPreparationWorkspace.jsx'; import { DocumentPreviewConfiguration } from './DocumentPreview.jsx'; @@ -12,6 +13,8 @@ import WorkshopWorkspace from './WorkshopWorkspace.jsx'; import DirectCostsWorkspace from './DirectCostsWorkspace.jsx'; import IndustrialBudgetsWorkspace from './IndustrialBudgetsWorkspace.jsx'; import IndustrialCostsWorkspace from './IndustrialCostsWorkspace.jsx'; +import WorkspaceNavigation, {navigationGroups} from './WorkspaceNavigation.jsx'; +import WorkspaceHome from './WorkspaceHome.jsx'; const labels = { 'catalogue_accounting.view':'Consultar classificação contabilística', 'catalogue_accounting.propose':'Preparar classificação contabilística', 'catalogue_accounting.approve':'Validar ou recusar classificação contabilística', 'company.view': 'Consultar empresa', 'company.edit': 'Editar nome da empresa', 'unit.create': 'Criar unidades', 'access.manage': 'Gerir acessos', 'unit.view': 'Consultar unidade', 'unit.edit': 'Editar nome da unidade' }; @@ -25,6 +28,10 @@ function App() { const context = state?.context; const can = (permission, unit = null) => !!context?.grants.some(g => g.permission === permission && g.unit_id === unit); + const groups = navigationGroups(can, context?.unit?.id ?? null); + const navigate = id => { setPage(id); setError(''); }; + useEffect(() => { if (!groups.some(g => g.items.some(i => i.id === page))) setPage('home'); }, [context?.token, JSON.stringify(context?.grants), page]); + async function api(path, method = 'GET', body, token = context?.token, commandKey = null) { const response = await fetch(path, { method, credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': state?.csrf || '', 'X-SGF-Context': token || '', ...(commandKey ? { 'Idempotency-Key': commandKey } : {}) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }); const result = await response.json().catch(() => ({})); @@ -56,11 +63,11 @@ function App() { if (!state) return

SGF TEMPO

{error || 'A carregar…'}

; if (state.setup_pending) return

SGF TEMPO · PREPARAÇÃO LOCAL

Falta concluir a primeira conta

A aplicação está pronta, mas ainda não existe um utilizador. O login ficará disponível depois de definires a palavra-passe no procedimento local já autorizado.

Concluir no terminal do VS Code

Executa este comando, confere o âmbito apresentado e define a palavra-passe nos dois pedidos de entrada oculta:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\laragon\data\sgf-tempo\run\complete-first-admin.ps1

Não envies a palavra-passe pelo chat. Nenhuma conta é criada por este ecrã.

{error &&

{error}

}
; if (!state.user) return

BEM-VINDO AO SGF TEMPO

Entrar na aplicação

Use a conta preparada pelo procedimento local autorizado.

{error &&
{error}
} perform(() => api('/api/login', 'POST', { email, password }), 'Sessão iniciada. Seleccione o contexto de trabalho.')} />

Precisa de recuperar o acesso? Solicite recuperação local assistida. Não existe recuperação por email nesta etapa.

; - return
CONTEXTO DE TRABALHO{context?.company.name || 'Seleccione uma empresa'}{context?.unit?.name || 'Sem unidade seleccionada'}
Ambiente local
+ return
perform(() => api("/api/logout", "POST"), "Sessão terminada.")} />
CONTEXTO DE TRABALHO{context?.company.name || 'Seleccione uma empresa'}{context?.unit?.name || 'Sem unidade seleccionada'}
Ambiente local
Ensaios e rascunhos nos percursos identificados · Operações reais de custeio, oficinas, recebimentos e emissão bloqueadas.
{error &&
{error}
}{notice &&
{notice}
}

Apenas os contextos com acesso explícito são apresentados.

{!state.companies.length ?

Sem empresas autorizadas

A conta existe, mas ainda não tem acesso a uma empresa. Peça ao responsável uma concessão explícita.

: !context ?

Escolha onde vai trabalhar

Seleccione uma empresa. A pertença a um cliente contratante não dá acesso automático às suas empresas ou unidades.

:
- {page === 'home' && <>

ÁREA INICIAL

Bem-vindo, {state.user.name.split(' ')[0]}.

O contexto permanece visível e cada operação é validada pelo servidor.

EMPRESA ACTIVA

{context.company.name}

Cliente contratante #{context.company.tenant_id} · Empresa #{context.company.id}

UNIDADE ACTIVA

{context.unit?.name || 'Ainda não seleccionada'}

{context.units.length} unidade(s) com consulta autorizada

} + {page === 'home' && } {page === 'company' &&

DADOS BÁSICOS

Empresa

perform(() => api(`/api/companies/${context.company.id}`, 'PATCH', { name }, context.token), 'Nome da empresa actualizado.')} />{!can('company.edit') &&

O seu acesso permite consulta. A edição exige uma concessão própria.

}
} {page === 'units' && <>

DADOS BÁSICOS

Unidades autorizadas

{!context.units.length &&

Não tem consulta concedida a nenhuma unidade desta empresa.

}
    {context.units.map(u =>
  • {u.name}
  • )}
{context.unit && perform(() => api(`/api/units/${context.unit.id}`, 'PATCH', { name }, context.token), 'Nome da unidade actualizado.')} />}
{can('unit.create') &&

Criar unidade

A unidade será criada nesta empresa. Receberá consulta e edição explícitas apenas para a nova unidade.

perform(() => api('/api/units', 'POST', { name }, context.token), 'Unidade criada com os seus acessos explícitos.')} />
}} {serviceAreas[page] && { setASelection({area,id}); setPage(area); }} />} diff --git a/scripts/home-navigation-check.mjs b/scripts/home-navigation-check.mjs new file mode 100644 index 0000000..2b27c8e --- /dev/null +++ b/scripts/home-navigation-check.mjs @@ -0,0 +1,28 @@ +// Render-only checks: no browser, database, credentials or additional dependencies. +import assert from 'node:assert/strict'; +import React from 'react'; +import {renderToStaticMarkup} from 'react-dom/server'; +import {createServer} from 'vite'; +const server=await createServer({server:{middlewareMode:true},appType:'custom'}); +try { + const {navigationGroups}=await server.ssrLoadModule('/resources/js/WorkspaceNavigation.jsx'); + const {default:Home}=await server.ssrLoadModule('/resources/js/WorkspaceHome.jsx'); + const context={company:{name:'Empresa sintética'},unit:{id:7,name:'Unidade sintética'}}; + const check=(grants)=>{ + const can=(permission,unit=null)=>grants.some(g=>g[0]===permission&&g[1]===unit); + const groups=navigationGroups(can,7); + return {groups,html:renderToStaticMarkup(React.createElement(Home,{context,can,groups,navigate:()=>{}}))}; + }; + const preparer=check([['company.view',null],['industrial_budgets.view',7],['industrial_budgets.prepare',7]]); + assert(preparer.html.includes('A preparar e acompanhar'));assert(!preparer.html.includes('

A validar

')); + assert(!preparer.groups.some(g=>g.id==='billing'));assert(!preparer.groups.flatMap(g=>g.items).some(i=>i.id==='access')); + const reviewer=check([['company.view',null],['industrial_budgets.view',7],['industrial_budgets.validate',7]]); + assert(reviewer.html.includes('

A validar

'));assert(!reviewer.html.includes('

A preparar e acompanhar

')); + const otherUnit=check([['company.view',null],['industrial_budgets.view',99],['industrial_budgets.prepare',99]]); + assert(!otherUnit.groups.some(g=>g.id==='costs')); + const noAccess=check([]);assert.deepEqual(noAccess.groups.map(g=>g.id),['start']); + const reader=check([['company.view',null],['industrial_budgets.view',7]]); + assert(!reader.html.includes('

A validar

'));assert(!reader.html.includes('

A preparar e acompanhar

')); + assert(reader.html.includes('não agrega nem conta tarefas pendentes')); + console.log('Home/navigation render checks passed: preparer, reviewer, reader, other unit and absent permissions. No database or browser used.'); +} finally {await server.close();}