import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import '../css/app.css'; import ServicesWorkspace, { serviceAreas } from './ServicesWorkspace.jsx'; import DocumentPreparationWorkspace from './DocumentPreparationWorkspace.jsx'; import { DocumentPreviewConfiguration } from './DocumentPreview.jsx'; import CatalogueAccountingWorkspace from './CatalogueAccountingWorkspace.jsx'; import ServiceFiscalWorkspace from './ServiceFiscalWorkspace.jsx'; import ServiceInvoiceWorkspace from './ServiceInvoiceWorkspace.jsx'; import ServiceReceivablesWorkspace from './ServiceReceivablesWorkspace.jsx'; import WorkshopWorkspace from './WorkshopWorkspace.jsx'; import DirectCostsWorkspace from './DirectCostsWorkspace.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 context = state?.context; const can = (permission, unit = null) => !!context?.grants.some(g => g.permission === permission && g.unit_id === unit); 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(() => ({})); if (!response.ok) { if (response.status === 401) { 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.'; throw new Error(message); } return result; } 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; await perform(() => api('/api/context', 'POST', { tenant_id: company.tenant_id, company_id: company.id, unit_id: unitId }), 'Contexto alterado. Os formulários foram recarregados.'); } 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
{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 === '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); }} />} {page === 'preview_model' && } {page === 'documents' && } {page === 'workshop' && } {page === 'direct_costs' && } {page === 'service_receivables' && } {page === 'service_invoices' && } {page === 'service_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); }}>