commit10
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# Página inicial e menus agrupados
|
||||
|
||||
**Limpeza expressamente pedida, 30/10/2026:** removidos da página inicial «Consultas e atalhos permitidos», os cartões expansíveis, os atalhos de preparação/validação e o cartão de contexto que repetia o cabeçalho. Ficam o título Início e o aviso de unidade quando aplicável, sem novo conteúdo para ocupar o espaço. Destinos continuam exclusivamente nos dropdowns existentes, por assunto, com Compras, Stock e Contabilidade separados; nenhum direito ou destino foi removido. Removidos os estilos e propriedades específicos dos cartões. Esta decisão substitui a organização em cartões descrita abaixo. Check de navegação/renderização e compilação aprovados; alteração local, sem publicação ou revisão visual declarada. O conteúdo futuro da página inicial será decidido posteriormente.
|
||||
|
||||
**Organização da página inicial, 30/10/2026:** após a indicação de conteúdo espalhado, a lista plana de consultas foi substituída por cartões expansíveis por assunto, reutilizando navigationTopics e os grupos já filtrados. Cartões começam fechados, conservam destinos, estados de preparação e secções internas, incluindo Administração. Preparação/validação e contexto mantidos. CSS responsivo com atalhos em coluna dentro de cada cartão; details/summary nativos para teclado e expansão. Check de renderização cobre os assuntos principais, cartões fechados e ausência da lista plana. Alteração exclusivamente local, sem publicação, dados ou direitos novos; verificação técnica não é revisão visual no navegador.
|
||||
|
||||
**Reposição de visibilidade da Contabilidade, 29/10/2026:** a entrada não tinha sido eliminada: o agrupamento horizontal colocara o grupo accounting dentro de Financeiro. A pedido da Tempo, Contabilidade volta a assunto principal, com dropdown próprio, preservando os oito destinos e os mesmos direitos. A ordem passa a Início, Comercial, Compras, Stock, Oficinas, Financeiro, Contabilidade, Configuração. Sem alteração de dados ou concessões. O check existente passa a manter inventário independente dos onze grupos, dos oito destinos contabilísticos e dos assuntos principais; cobre company.view e os dois direitos contabilísticos isolados, além da ausência de direitos. O comando build executa esse check antes do Vite, interrompendo a compilação perante regressão; não depende de hooks npm opcionais. Novas alterações devem actualizar explicitamente o inventário apenas quando o âmbito aprovado mudar. Esta guarda detecta remoção/agrupamento indevido conhecido, não substitui revisão visual ou prova no destino publicado. Prova HTTP da instância 8002 verifica também Contabilidade separada para as cinco funções sintéticas existentes. Sem publicação ou checkpoint.
|
||||
|
||||
**Conferência da instância em execução, 29/10/2026:** após «continua a executar», scripts/navigation-live-check.mjs verificou as cinco funções sintéticas já existentes (proprietário, preparador, validador, técnico e verificador) por entrada/sessão/saída em 8002. Compras e Stock surgem separados nos cinco conjuntos efectivos de concessões; todos os destinos visíveis têm componente registado e não há IDs duplicados. A aplicação serve o bundle actual com purchasing_home, não apenas código-fonte local não compilado. Prova sanitizada em docs/proofs/navigation-live.json. Sem novas contas, alteração de concessões ou comandos de negócio. Conferência HTTP e renderização estática; não prova interactiva em navegador.
|
||||
|
||||
@@ -26,14 +26,7 @@
|
||||
.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){
|
||||
@@ -44,8 +37,8 @@
|
||||
.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}
|
||||
.workspace main{padding:20px 16px}.workspace header .local-badge{white-space:normal;text-align:center}
|
||||
.context-card{grid-template-columns:1fr}.workspace header strong{overflow-wrap:anywhere}.workspace header .local-badge{display:block}
|
||||
}
|
||||
|
||||
/* Facturação: document lists and filters, kept separate from financial engines. */
|
||||
|
||||
@@ -1,31 +1,8 @@
|
||||
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([
|
||||
['purchasing_budget','purchasing_budget.create',unit],['purchasing_budget','purchasing_budget.record',unit],
|
||||
['workshop_corrections','workshop_corrections.request',unit],
|
||||
['workshop_billing','workshop_billing.prepare',unit],
|
||||
['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([
|
||||
['purchasing_budget','purchasing_budget.approve',unit],
|
||||
['workshop_corrections','workshop_corrections.review',unit],
|
||||
['workshop_billing','workshop_billing.review',unit],
|
||||
['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 <div className="home-shortcuts">{[...new Map(items.map(i=>[i.id,i])).values()].map(i=><button key={i.id} onClick={()=>navigate(i.id)}>{i.label}{i.informational&&<small className="shortcut-status">Em preparação</small>}<span aria-hidden="true"> →</span></button>)}</div>;}
|
||||
return <><div className="page-title"><p className="eyebrow">O SEU ESPAÇO DE TRABALHO</p><h1>Início</h1><p className="muted">Escolha o percurso no contexto seleccionado. Cada acção mantém a sua autorização própria.</p></div>
|
||||
export default function WorkspaceHome({context}) {
|
||||
return <>
|
||||
<div className="page-title"><h1>Início</h1></div>
|
||||
{!context.unit&&<p className="alert home-notice" role="status">Seleccione uma unidade para consultar os percursos de trabalho dessa unidade.</p>}
|
||||
<div className="home-work-grid">{prepare.length>0&&<section className="panel"><h2>A preparar e acompanhar</h2><p>Abra o percurso para preparar propostas e acompanhar o estado das versões.</p>{actions(prepare)}</section>}{validate.length>0&&<section className="panel"><h2>A validar</h2><p>Consulte as versões no respectivo percurso antes de tomar uma decisão. Não é permitida a auto-validação.</p>{actions(validate)}</section>}</div>
|
||||
<section className="panel"><h2>Consultas e atalhos permitidos</h2>{actions(available.filter(i=>!['home','company','units','access'].includes(i.id)))}</section>
|
||||
<p className="help">Esta página não agrega nem conta tarefas pendentes. Abra cada percurso para consultar os estados actuais.</p>
|
||||
<section className="panel home-context"><h2>Contexto actual</h2><dl><div><dt>Empresa</dt><dd>{context.company.name}</dd></div><div><dt>Unidade</dt><dd>{context.unit?.name??'Sem unidade seleccionada'}</dd></div></dl><div className="actions"><button onClick={()=>navigate('company')}>Consultar empresa</button><button onClick={()=>navigate('units')}>Ver unidades autorizadas</button></div></section>
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ function App() {
|
||||
{error && <div className="alert error" role="alert">{error}</div>}{notice && <div className="alert success" role="status">{notice}</div>}
|
||||
<section className="context-card"><label>Empresa<select aria-label="Empresa activa" disabled={busy} value={context?.company.id || ''} onChange={e => select(e.target.value)}><option value="">Escolher empresa autorizada</option>{state.companies.map(c => <option value={c.id} key={c.id}>{c.name}</option>)}</select></label><label>Unidade<select aria-label="Unidade activa" disabled={busy || !context} value={context?.unit?.id || ''} onChange={e => select(context.company.id, e.target.value ? Number(e.target.value) : null)}><option value="">Sem unidade seleccionada</option>{context?.units.map(u => <option value={u.id} key={u.id}>{u.name}</option>)}</select></label><p>Apenas os contextos com acesso explícito são apresentados.</p></section>
|
||||
{!state.companies.length ? <section className="panel"><h1>Sem empresas autorizadas</h1><p>A conta existe, mas ainda não tem acesso a uma empresa. Peça ao responsável uma concessão explícita.</p></section> : !context ? <section className="panel"><h1>Escolha onde vai trabalhar</h1><p>Seleccione uma empresa. A pertença a um cliente contratante não dá acesso automático às suas empresas ou unidades.</p></section> : <div key={context.token}>
|
||||
{page === 'home' && <WorkspaceHome context={context} can={can} groups={groups} navigate={navigate} />}
|
||||
{page === 'home' && <WorkspaceHome context={context} />}
|
||||
{page === 'workshop_billing' && <WorkshopBillingWorkspace context={context} can={can} api={api} busy={busy} />}
|
||||
{page === 'workshop_corrections' && <WorkshopCorrectionsWorkspace context={context} can={can} api={api} busy={busy} />}
|
||||
{page === 'company' && <section className="panel"><p className="eyebrow">DADOS BÁSICOS</p><h1>Empresa</h1><NameForm key={context.company.id} label="Nome da empresa" initial={context.company.name} editable={can('company.edit')} busy={busy} onSave={name => perform(() => api(`/api/companies/${context.company.id}`, 'PATCH', { name }, context.token), 'Nome da empresa actualizado.')} />{!can('company.edit') && <p className="help">O seu acesso permite consulta. A edição exige uma concessão própria.</p>}</section>}
|
||||
|
||||
@@ -15,16 +15,16 @@ try {
|
||||
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('<h2>A validar</h2>'));
|
||||
assert(preparer.html.includes('<h1>Início</h1>'));
|
||||
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('<h2>A validar</h2>'));assert(!reviewer.html.includes('<h2>A preparar e acompanhar</h2>'));
|
||||
assert(!reviewer.html.includes('<button'));
|
||||
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('<h2>A validar</h2>'));assert(!reader.html.includes('<h2>A preparar e acompanhar</h2>'));
|
||||
assert(reader.html.includes('não agrega nem conta tarefas pendentes'));
|
||||
assert(!reader.html.includes('home-context'));
|
||||
const allGroups=navigationGroups(()=>true,7),topics=navigationTopics(allGroups);
|
||||
assert.deepEqual(topics.map(t=>t.label),['Início','Comercial','Compras','Stock','Oficinas','Financeiro','Contabilidade','Configuração']);
|
||||
// Independent inventory: comparing the generated menu with itself cannot detect a removed module.
|
||||
@@ -39,6 +39,10 @@ try {
|
||||
assert(!navigationTopics(noAccess.groups).some(t=>t.id==='accounting'));
|
||||
const before=allGroups.flatMap(g=>g.items.map(i=>i.id)).sort(),after=topics.flatMap(t=>t.sections.flatMap(g=>g.items.map(i=>i.id))).sort();
|
||||
assert.deepEqual(after,before);assert.equal(new Set(after).size,after.length);
|
||||
const groupedHome=renderToStaticMarkup(React.createElement(Home,{context,can:()=>true,groups:allGroups,topics,navigate:()=>{}}));
|
||||
assert(groupedHome.includes('<h1>Início</h1>'));
|
||||
for(const marker of ['<button','<details','<summary','class="panel','Consultas e atalhos permitidos','A preparar e acompanhar','A validar','Contexto actual'])assert(!groupedHome.includes(marker));
|
||||
for(const id of ['accounting_panel','purchasing_home'])assert(allGroups.flatMap(g=>g.items).some(i=>i.id===id));
|
||||
const navHtml=renderToStaticMarkup(React.createElement(Navigation,{groups:allGroups,page:'billing_drafts',navigate:()=>{},user:{name:'Avaliador sintético',email:'consulta@example.test'},busy:false,logout:()=>{}}));
|
||||
assert(navHtml.includes('workspace-topbar'));assert(!navHtml.includes('grouped-sidebar'));assert(!navHtml.includes('navigation-drawer'));
|
||||
for(const topic of topics.filter(t=>t.id!=='start'))assert(navHtml.includes(`aria-controls="topic-${topic.id}"`));
|
||||
|
||||
Reference in New Issue
Block a user