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

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
{transitions.current.submitting=e.target;}} onInputCapture={e=>e.target.closest('main')&&!e.target.closest('.context-card')&&!e.target.closest('[data-sgf-read-filters="true"]')&&transitions.current.changed(e.target.closest('form')||e.target.closest('section')||e.target)} onChangeCapture={e=>e.target.closest('main')&&!e.target.closest('.context-card')&&!e.target.closest('[data-sgf-read-filters="true"]')&&transitions.current.changed(e.target.closest('form')||e.target.closest('section')||e.target)} onClickCapture={e=>{transitions.current.submitting=e.target.closest('form')||e.target.closest('section');const a=e.target.closest('a[href]');if(a&&a.origin===location.origin){e.preventDefault();transition(()=>{location.href=a.href;});}}}>transition(()=>perform(()=>api('/api/logout','POST'),'Sessão terminada.'))} />{pendingTransition&&resolveTransition(true)} onDiscard={()=>resolveTransition(false)} onCancel={()=>setPendingTransition(null)}/>}
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' && page !== 'module_records' && context.module_id &&

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' && A abrir início do módulo…

}>
} {page==='stock_catalogue'&&context.stock?.catalogue===true&&

Artigos de Stock

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 artigos
} {page === 'module_records' && } {page === 'workshop_billing' && } {page === 'workshop_corrections' && } {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.')} />}
{context.unit&&}{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] && navigate(area,{area,id})} />} {billingSections.some(s=>s.id===page)&&page!=='billing_drafts'&&!(page==='billing_quotes'&&context.commercial_quotes_enabled===true)&&navigate(area,{area,id,create})}/>} {stockSections.some(s=>s.id===page)&&} {accountingSections.some(s=>s.id===page)&&} {page==='native_payables'&&

Financeiro · fornecedores

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 integrado
} {page==='cash'&&

Financeiro · Caixa

{context.cash_enabled?<>

Abertura 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.

}
} {page === 'preview_model' && } {page === 'documents' && } {page === 'workshop' && navigate(id)} />} {workshopSections.some(s=>s.id===page)&&page!=='workshop'&&navigate(area,{area,id})}/>} {page === 'direct_costs' && } {page === 'industrial_budgets' && } {page === 'purchasing_budget' && } {page === 'purchasing_home' && } {page === 'supplier_payables' &&

Obrigações a fornecedores

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}
} {page === 'supplier_payments' &&

Pagamentos e afectações a fornecedores

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}
} {page === 'industrial_costs' && } {page==='multiline_invoices'&&A abrir facturação…

}>{if(element)for(const changed of transitions.current.dirty)if(changed===element||element.contains?.(changed))transitions.current.dirty.delete(changed);}}/>
} {page==='fixed_assets'&&A abrir activos…

}>
} {page==='internal_statements'&&A abrir demonstrações internas…

}>
} {page==='hr_personnel'&&A abrir contratos, assiduidade e férias…

}>
} {page==='remunerations'&&A abrir Recursos Humanos…

}>
} {page==='fiscal_obligations'&&A abrir obrigações…

}>
} {['payable_positions','payable_aging'].includes(page)&&A abrir posições de fornecedores…

}>
} {page === 'receivable_positions' && A abrir posições…

}>navigate('service_receivables',{area:'service_receivables',id})}/>
} {page === 'service_receivables' && } {page === 'service_invoice_setup' && } {page==='billing_quotes'&&context.commercial_quotes_enabled===true&&A abrir orçamentos…

}>
} {page==='financial_indicators'&&

Critérios e indicadores financeiros QA

{context.analysis_qa_enabled?<>

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.

}
} {page==='bank_reconciliation'&&

Conferência de extracto QA

{context.bank_qa_enabled?<>

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.

}
} {page==='fiscal_rules'&&

Regras fiscais versionadas QA

{context.fiscal_rules_qa_enabled?<>

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.

}
} {page==='fiscal_iva'&&

Livro e mapa interno de IVA QA

{context.fiscal_qa_enabled?<>

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.

}
} {page === 'service_fiscal' && A abrir enquadramento fiscal…

}>
} {page === 'catalogue_accounting' && } {page === 'access' && (can('access.manage') ? :

Acesso não autorizado

A gestão de acessos exige uma concessão própria.

)}
}
; } function Login({ busy, onSubmit }) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); return
{ e.preventDefault(); await onSubmit(email, password); setPassword(''); }}>
; } function NameForm({ label, initial, editable, busy, onSave, button = 'Guardar nome' }) { const [name, setName] = useState(initial); return
{ e.preventDefault(); onSave(name); }}>{editable && }
; } function AccessPanel({ context, can, api, perform, busy }) { const [list, setList] = useState(null); const [loadError, setLoadError] = useState(''); const [email, setEmail] = useState(''); const [scope, setScope] = useState('company'); const [permission, setPermission] = useState('company.view'); const [reason, setReason] = useState(''); const unit = scope === 'company' ? null : Number(scope); const options = (unit === null ? ['company.view', 'company.edit', 'unit.create', 'access.manage'] : ['unit.view', 'unit.edit']).filter(p => can(p, unit)); useEffect(() => { api('/api/accesses').then(setList).catch(e => setLoadError(e.message)); }, [context.token]); async function change(grant) { await perform(async () => { await api(grant ? '/api/accesses' : '/api/accesses/revoke', 'POST', { email, permission, unit_id: unit, reason }, context.token); setList(await api('/api/accesses')); }, grant ? 'Concessão registada.' : 'Revogação registada.'); } return <>

AUTORIZAÇÕES EXPLÍCITAS

Gestão de acessos

Actua apenas nesta empresa e pode delegar apenas direitos que possui no mesmo âmbito. Os utilizadores são preparados pelo procedimento local.

{loadError &&

{loadError}

}
{ e.preventDefault(); change(true); }}>