SGF TEMPO
{error || 'A carregar…'}
import GeographyLink from './GeographyLink.jsx';
import React, { useEffect, useState, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import '../css/app.css';
import '../css/tempo-theme.css';
import '../css/multiline-invoice.css';
import ServicesWorkspace, { serviceAreas } from './ServicesWorkspace.jsx';
import DocumentPreparationWorkspace from './DocumentPreparationWorkspace.jsx';
import { DocumentPreviewConfiguration } from './DocumentPreview.jsx';
import CatalogueAccountingWorkspace from './CatalogueAccountingWorkspace.jsx';
const ServiceFiscalWorkspace=React.lazy(()=>import('./ServiceFiscalWorkspace.jsx'));
import ServiceInvoiceWorkspace from './ServiceInvoiceWorkspace.jsx';
import ServiceReceivablesWorkspace from './ServiceReceivablesWorkspace.jsx';
import WorkshopWorkspace from './WorkshopWorkspace.jsx';
import DirectCostsWorkspace from './DirectCostsWorkspace.jsx';
import IndustrialBudgetsWorkspace from './IndustrialBudgetsWorkspace.jsx';
import PurchasingBudgetWorkspace from './PurchasingBudgetWorkspace.jsx';
import IndustrialCostsWorkspace from './IndustrialCostsWorkspace.jsx';
import {navigationGroups} from './WorkspaceNavigation.jsx';
import ModuleNavigation, {moduleGroups,moduleFunctionAllowed} from './ModuleNavigation.jsx';
import {ContextTransition} from './context-transition.js';
import ModuleRecordsWorkspace from './ModuleRecordsWorkspace.jsx';
import ContextTransitionDialog from './ContextTransitionDialog.jsx';
const MultiLineInvoiceWorkspace=React.lazy(()=>import('./MultiLineInvoiceWorkspace.jsx'));
const FixedAssetsWorkspace=React.lazy(()=>import('./FixedAssetsWorkspace.jsx'));
const PersonnelWorkspace=React.lazy(()=>import('./PersonnelWorkspace.jsx'));
const RemunerationsWorkspace=React.lazy(()=>import('./RemunerationsWorkspace.jsx'));
const InternalStatementsWorkspace=React.lazy(()=>import('./InternalStatementsWorkspace.jsx'));
const FiscalObligationsWorkspace=React.lazy(()=>import('./FiscalObligationsWorkspace.jsx'));
const PayablePositionsWorkspace=React.lazy(()=>import('./PayablePositionsWorkspace.jsx'));
const ReceivablePositionsWorkspace=React.lazy(()=>import('./ReceivablePositionsWorkspace.jsx'));
const WorkspaceHome=React.lazy(()=>import('./WorkspaceHome.jsx'));
import WorkshopBillingWorkspace from './WorkshopBillingWorkspace.jsx';
import WorkshopCorrectionsWorkspace from './WorkshopCorrectionsWorkspace.jsx';
import BillingMenuWorkspace from './BillingMenuWorkspace.jsx';
import {billingSections} from './billing-menu.js';
import WorkshopMenuWorkspace from './WorkshopMenuWorkspace.jsx';
import {workshopSections} from './workshop-menu.js';
import StockMenuWorkspace from './StockMenuWorkspace.jsx';
import {openStockReceipt} from './stock-receipt-trace.js';
import {stockSections} from './stock-menu.js';
import AccountingMenuWorkspace from './AccountingMenuWorkspace.jsx';
import {accountingSections} from './accounting-menu.js';
import PurchasingHome from './PurchasingHome.jsx';
import {presentationLanding} from './presentation-landing.js';
const CommercialQuotesWorkspace=React.lazy(()=>import('./CommercialQuotesWorkspace.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' };
function App() {
const [state, setState] = useState(null);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const [busy, setBusy] = useState(false);
const [page, setPage] = useState('home');
const [aSelection, setASelection] = useState(null);
const transitions = useRef(new ContextTransition());
const [pendingTransition, setPendingTransition] = useState(null);
const [transitionBusy, setTransitionBusy] = useState(false);
const context = state?.context;
const currentToken = useRef(context?.token); currentToken.current = context?.token;
const mountedToken = useRef(context?.token);
useEffect(() => { if(mountedToken.current !== context?.token) { transitions.current.discard(); setASelection(null); setPage('home'); mountedToken.current=context?.token; } }, [context?.token]);
useEffect(() => { const leave=e=>{transitions.current.prune(); if(transitions.current.dirty.size||transitions.current.pending||document.querySelector('[data-sgf-operation-uncertain="true"]')){e.preventDefault();e.returnValue='';}}; window.addEventListener('beforeunload',leave); return ()=>window.removeEventListener('beforeunload',leave); }, []);
function transition(action) { transitions.current.prune(); if(transitions.current.pending||busy||transitionBusy||document.querySelector('[data-sgf-operation-uncertain="true"]')) {setError('Aguarde a conclusão da operação antes de mudar de contexto.');return;} if(transitions.current.dirty.size) setPendingTransition(()=>action); else action(); }
async function resolveTransition(save) { setTransitionBusy(true);setError(''); try { if(save) await transitions.current.save(); else transitions.current.discard(); const action=pendingTransition;setPendingTransition(null); await action(); } catch(e){setError(e.message);} finally{setTransitionBusy(false);} }
const can = (permission, unit = null) => !!context?.grants.some(g => g.permission === permission && g.unit_id === unit);
const allGroups = navigationGroups(can, context?.unit?.id ?? null,{multilineInvoices:context?.multiline_invoices_enabled===true,fixedAssets:context?.fixed_assets_enabled===true,personnel:context?.hr_personnel_enabled===true,remunerations:context?.remunerations_enabled===true,stockCatalogue:context?.stock?.catalogue===true,payablePositions:context?.single_installation===true,fiscalObligations:context?.fiscal_obligations_enabled===true,internalStatements:context?.internal_statements_enabled===true});
const groups = [...allGroups.filter(g=>['start','admin'].includes(g.id)), ...moduleGroups(allGroups,context?.module_id)];
const navigate = (id,selection=null) => transition(async () => {
setError('');
if(!groups.some(g=>g.items.some(i=>i.id===id))) {
const target=context?.modules?.find(m=>moduleGroups(allGroups,m.id).some(g=>g.items.some(i=>i.id===id)));
if(!target){setError('Destino sem módulo ou permissão autorizados.');return;}
try {
const next=await api('/api/context','POST',{tenant_id:context.company.tenant_id,company_id:context.company.id,unit_id:context.unit?.id??null,module_id:target.id});
mountedToken.current=next.context.token;currentToken.current=next.context.token;setState(next);
} catch(e){setError(e.message);return;}
}
transitions.current.discard();setASelection(selection);setPage(id);
});
const openModulePage=(module_id,id)=>transition(async()=>{
if(!moduleFunctionAllowed(context,allGroups,module_id,id)){setError('Função sem módulo ou permissão autorizados.');return;}
setError('');try{if(context.module_id!==module_id){const next=await api('/api/context','POST',{tenant_id:context.company.tenant_id,company_id:context.company.id,unit_id:context.unit?.id??null,module_id});mountedToken.current=next.context.token;currentToken.current=next.context.token;setState(next);}transitions.current.discard();setASelection(null);setPage(id);}catch(e){setError(e.message);}
});
useEffect(() => { if (!groups.some(g => g.items.some(i => i.id === page))) setPage('home'); }, [context?.token, JSON.stringify(context?.grants), page]);
const initialLanding=useRef(false);
useEffect(()=>{if(initialLanding.current||!context?.token)return;initialLanding.current=true;if(!context.single_installation)return;const url=new URL(window.location.href),start=url.searchParams.get('start');if(!start)return;const target=presentationLanding(context,groups,start);if(target)setPage(target);else setError('Entrada sem página ou permissão autorizadas. Escolha um percurso no módulo actual.');url.searchParams.delete('start');window.history.replaceState(null,'',url.pathname+url.search+url.hash);},[context?.token]);
async function api(path, method = 'GET', body, token = context?.token, commandKey = null) {
const execute = async () => {
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(() => ({}));
if (!response.ok) {
if ([401,403,409].includes(response.status)) { await refresh(); }
const message = response.status === 419 ? 'A sessão expirou. Actualize a página e entre novamente.' : Object.values(result.errors || {}).flat().join(' ') || result.message || 'Não foi possível concluir a operação.';
const error=new Error(message);error.status=response.status;throw error;
}
if(token && token !== currentToken.current) throw new Error('Resposta de um contexto anterior ignorada.');
return result;
};
return method==='GET' ? execute() : transitions.current.command(execute);
}
const openStockReceiptOrigin = movement => transition(async()=>{
setError('');
try { await openStockReceipt({movement,context,active:()=>currentToken.current===context.token,
select:body=>api('/api/context','POST',body),
commit:next=>{mountedToken.current=next.context.token;currentToken.current=next.context.token;setState(next);},
go:href=>location.assign(href)});
}catch(e){setError(e.message);}
});
async function refresh() {
const response = await fetch('/api/session', { credentials: 'same-origin', headers: { Accept: 'application/json' } });
if (!response.ok) throw new Error('Não foi possível carregar a sessão.');
const next = await response.json(); setState(next);
if (next.notice) setNotice(next.notice);
}
useEffect(() => { refresh().catch(e => setError(e.message)); }, []);
async function perform(operation, message) {
setBusy(true); setError(''); setNotice('');
try { await operation(); await refresh(); if (message) setNotice(message); }
catch (e) { setError(e.message); }
finally { setBusy(false); }
}
async function select(companyId, unitId = null) {
const company = state.companies.find(c => c.id === Number(companyId));
if (!company) return;
transition(() => perform(() => api('/api/context', 'POST', { tenant_id: company.tenant_id, company_id: company.id, unit_id: unitId }), 'Contexto alterado. Seleccione o módulo.'));
}
if (!state) return {error || 'A carregar…'} SGF TEMPO · PREPARAÇÃO LOCAL 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. Executa este comando, confere o âmbito apresentado e define a palavra-passe nos dois pedidos de entrada oculta: Não envies a palavra-passe pelo chat. Nenhuma conta é criada por este ecrã. {error}SGF TEMPO
Falta concluir a primeira conta
Concluir no terminal do VS Code
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\laragon\data\sgf-tempo\run\complete-first-admin.ps1
BEM-VINDO AO SGF TEMPO
Use a conta preparada pelo procedimento local autorizado.
{error &&Precisa de recuperar o acesso? Solicite recuperação local assistida. Não existe recuperação por email nesta etapa.
Apenas os contextos com acesso explícito são apresentados.
A conta existe, mas ainda não tem acesso a uma empresa. Peça ao responsável uma concessão explícita.
Seleccione uma empresa. A pertença a um cliente contratante não dá acesso automático às suas empresas ou unidades.
Cadastros e rascunhos: apenas associações explícitas ao módulo activo. Dados anteriores sem associação estão preservados e pendentes de conferência pelo responsável. {can('access.manage')&&}
} {page === 'home' &&Pesquisar o catálogo, preparar um novo artigo e submeter a revisão distinta. Artigos anteriores permanecem só para consulta; aprovar não cria saldo inicial ou movimento.
Abrir cadastro, ficha e histórico de artigosDADOS BÁSICOS
O seu acesso permite consulta. A edição exige uma concessão própria.
}DADOS BÁSICOS
Não tem consulta concedida a nenhuma unidade desta empresa.
}A unidade será criada nesta empresa. Receberá consulta e edição explícitas apenas para a nova unidade.
Consultar a obrigação original de Compras, preparar factos de pagamento, conferir afectações e reversões por identidades distintas. Sem outra dívida ou execução de dinheiro real.
Abrir o percurso financeiro integradoAbertura e contagem explícitas, classificação individual de um recebimento efectivo de teste, despesa fictícia e revisão distinta. Sem dinheiro real, ajuste automático ou reabertura.
Abrir Caixa QA, extracto e histórico>:O percurso de Caixa ainda não está disponibilizado neste contexto.
}Percurso sintético ligado ao núcleo comum. Preparação, validação distinta e consulta, sem pagamentos ou vencimento presumido.
{context.single_installation?:Abrir preparação e consulta de obrigações}Ensaio sintético: preparação e confirmação distintas, afectação explícita e saldo canónico. Não executa transferências bancárias.
{context.single_installation?:Abrir pagamentos e afectações sintéticas}Fontes conciliadas, critérios revistos e retratos explicáveis, sem rácios gerais ou recomendação de pagamento.
Abrir critérios, indicadores e histórico>:Este percurso ainda não está disponibilizado neste contexto.
}Extracto artificial, correspondências individuais com o diário existente e revisão distinta. Sem pagamentos ou banco real.
Abrir extracto, conferência e histórico>:O percurso ainda não está disponibilizado neste contexto.
}Fontes, parâmetros, revisão distinta e cálculo de ensaio por vigência. Sem activação legal ou atribuição automática a documentos.
Abrir catálogo, histórico e cálculo QA>:Este percurso ainda não está disponibilizado neste contexto.
}Escolha explícita da origem, revisão distinta e mapa interno. Sem apuramento completo da empresa ou modelo oficial.
Abrir livro, mapa e histórico>:Este percurso ainda não está disponibilizado no contexto.
}A gestão de acessos exige uma concessão própria.
AUTORIZAÇÕES EXPLÍCITAS
Actua apenas nesta empresa e pode delegar apenas direitos que possui no mesmo âmbito. Os utilizadores são preparados pelo procedimento local.
{loadError &&{loadError}
}| Utilizador | Permissão | Âmbito |
|---|---|---|
| {member?.name}{member?.email} | {labels[g.permission]} | {g.unit_id ? context.units.find(u => u.id === g.unit_id)?.name || 'Unidade autorizada' : 'Empresa'} |
A carregar acessos…
}