fix: validate simulator dates and reconcile QA calculations reports and language

This commit is contained in:
2026-10-20 21:00:52 +02:00
parent 59ab4fe956
commit a690c665fb
39 changed files with 757 additions and 184 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ return [
|
*/
'locale' => env('APP_LOCALE', 'en'),
'locale' => env('APP_LOCALE', 'pt'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
@@ -0,0 +1,281 @@
{
"base": "59ab4fe95635",
"branch": "codex/simulator-integrated-review",
"scope": "QA only; no main merge or remote change",
"source_files": [
{
"path": "config/app.php",
"sha256": "a01f552093570f8744f7b874392824c15b5a6b49d61759273a658e8660a1d240"
},
{
"path": "docs/investmoz/unification/traceability-and-manual.md",
"sha256": "347c340bcc4f5adc7965bd66cef1f357d98b6c271fdcd35206a3a086e59ad34d"
},
{
"path": "docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv",
"sha256": "a3473a391634c32fa8c53ecb2528b4de08f145ec0284b50f3bbf89aa97e5cf24"
},
{
"path": "docs/investmoz/v2/auditoria-portal/backlog.md",
"sha256": "1294586bfd452e6430aa1fcb71a512cbf64177e529a416ac28dfab954839664b"
},
{
"path": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
"sha256": "f57861a36e310a892d55a16be6d06fecd48763defe11011036df8a3a33a57d6d"
},
{
"path": "lang/pt.json",
"sha256": "c21fa0b63a3729e001f7d4ba891a23890bcfcfd718137d99a7c6c18d3be1a235"
},
{
"path": "lang/pt/auth.php",
"sha256": "d32cc31b75d4056ef561baf501d9cbf71ec136c7f63bef635c0c3e31b9a2357d"
},
{
"path": "lang/pt/validation.php",
"sha256": "bb95fe0f683cd6d8a6d0475dc322c71bce58197b952220e70ac74f5e97cdcf60"
},
{
"path": "resources/js/components/appearance-tabs.tsx",
"sha256": "d53f48b0883713bf786fdf4b7877e03638bcf2e707b64ea6b8559501bfc8c4c7"
},
{
"path": "resources/js/components/delete-user.tsx",
"sha256": "2f94b4dc6edf20bf9e00940d4d7e7b5da31bdf560f26b84efd65672bcee585b8"
},
{
"path": "resources/js/components/manage-passkeys.tsx",
"sha256": "8f3e8970d3b82deba721ac27e57718e2e7dcecc67b1e78cb6404c475c9f9c62e"
},
{
"path": "resources/js/components/manage-two-factor.tsx",
"sha256": "35fea615cf3a0454448057b0ef3137e24c21df4de30a58fce04cb847eb89eba1"
},
{
"path": "resources/js/components/passkey-item.tsx",
"sha256": "a307c5ef209e24f3f913f77e355a4d04a1433f59f520ad45ad40112111ab4677"
},
{
"path": "resources/js/components/passkey-register.tsx",
"sha256": "e7cb9d853eb943fd0899bc8530378eddbf941f9334a77bcbb7693d2e29d99470"
},
{
"path": "resources/js/components/passkey-verify.tsx",
"sha256": "6db387920437ab39cd734a2af9bec7e64ae6469c0f1b368d536b275e57cbea37"
},
{
"path": "resources/js/components/simulation-fiscal-status.tsx",
"sha256": "540dc6addbec0be79114526f7d27f53942291b0c7be05a2608a515498970d3e0"
},
{
"path": "resources/js/components/simulation-form.tsx",
"sha256": "1b2d3a917e354eef7ccdb47201fb7e515ea646d45099beb9b15798c80e1dd2db"
},
{
"path": "resources/js/components/two-factor-recovery-codes.tsx",
"sha256": "9384ae3987a58ae89018f2a997dec7faebec4f51f516ebbdc2b337c8e757f00f"
},
{
"path": "resources/js/components/two-factor-setup-modal.tsx",
"sha256": "8586833e10310dbca8a097df80b86f40638f874170b7668fa476f0d7017a311b"
},
{
"path": "resources/js/components/ui/dialog.tsx",
"sha256": "abeb6df2254f0bf5f73de1c9b5aeb4e667afe8ac0319e8b9aa099a612cc70b8d"
},
{
"path": "resources/js/components/ui/sheet.tsx",
"sha256": "a525cc98beaf974e0dfe6cda1c5da1101798b6843ea355a758b3c5df5235f506"
},
{
"path": "resources/js/components/user-menu-content.tsx",
"sha256": "9cb79c2a999bb363bd9d4fc1869b672c474b088eff23fca6e9f588ecae00d3ce"
},
{
"path": "resources/js/hooks/use-two-factor-auth.ts",
"sha256": "16d9434379531f1994096e4d1fcfd778b2c829602cce5731d602a25ea9c0073f"
},
{
"path": "resources/js/layouts/settings/layout.tsx",
"sha256": "78ea66c1eb6cad907e23de9c81a4d309a127f251355f04771f72eb5689609a4e"
},
{
"path": "resources/js/lib/project-dossier-document.ts",
"sha256": "d0a66ca759e8710c1a3900bcd27cc7f0506587c782ab798c49703396c1fb31d6"
},
{
"path": "resources/js/lib/project-dossier-report.ts",
"sha256": "6c5f52362a6218048f321b41211bfd328fa1e19ea4ab45fd1424932130482c8d"
},
{
"path": "resources/js/lib/simulation-form.ts",
"sha256": "40df1a9b6939b1910d12045835e981d6e47d4c1d534775631ac2f6b655a5e2bd"
},
{
"path": "resources/js/pages/auth/forgot-password.tsx",
"sha256": "6cdedca491d5a05dad0083fb637cd618d0b290bd6b48b98e7c6163dc0d740166"
},
{
"path": "resources/js/pages/auth/login.tsx",
"sha256": "8468308dacd26bd94ee41683684dcb7719386a1a53196ed90496ca406b1b94da"
},
{
"path": "resources/js/pages/auth/reset-password.tsx",
"sha256": "f3561726ae5d7ee682d968a295bbf60ec9f8cbabba1721c33f48bafd80d1b20a"
},
{
"path": "resources/js/pages/auth/two-factor-challenge.tsx",
"sha256": "0fd14dff2f0a572f4b99905d0fb152baa476eaa55a221998b67da85fa4787610"
},
{
"path": "resources/js/pages/auth/verify-email.tsx",
"sha256": "ffc3220d0ee6cc243e8e08ea46f8b1637794626fa45d7aeaa77f54203692864d"
},
{
"path": "resources/js/pages/portal/services.tsx",
"sha256": "9369add7c012c1e8a9e217b46be55e2927382bb8f18b90973d3b1f4199442d9b"
},
{
"path": "resources/js/pages/settings/appearance.tsx",
"sha256": "638367104837b028aeeb49b4cad1d2c9a44a92d1b9aebe71c31995605e0479b4"
},
{
"path": "resources/js/pages/settings/profile.tsx",
"sha256": "c599b1ca523ece69c3f5d3fe480a9f2ce4c323e97afb42095f61f2ddb7d4ba7b"
},
{
"path": "resources/js/pages/settings/security.tsx",
"sha256": "39264620329657af975f990a50d657c42186ead57458cd83040aa07f9cd78444"
},
{
"path": "tests/frontend/project-dossier-report.test.mjs",
"sha256": "fcdcabfc659f2f4ca0d5a51e592719d53d51420271c7cbe350176b48cef3fc97"
},
{
"path": "tests/frontend/simulation-form.test.mjs",
"sha256": "294a1b996d8131771ebaca85d2ff0e44bb6afd62e460073b7535486f13ae3a6b"
}
],
"evidence": [
{
"path": "outputs/simulator-integrated-review/effective-parameter-inventory.json",
"sha256": "aeec1251ef5dbbf7ae547af9f1ebcd2575aaa6c251ef65b37c6c31b8cf371acc"
},
{
"path": "outputs/simulator-integrated-review/independent-controls-proof.json",
"sha256": "47ad49b2136fd74c801b7a5d057c891732d820fb6a5002ccf08c71a2ffabf575"
},
{
"path": "outputs/simulator-integrated-review/visible-language-inventory.json",
"sha256": "5f1f50236ae492c8428a9528af350900e4f2c44c387fe8f23ebce0e4f1b14b3a"
},
{
"path": "outputs/simulator-integrated-review/dynamic-language-inventory.json",
"sha256": "fe3246f570c333c2c59e9fe2acb0ec01078a158a46e53d37087ec59d419578a5"
},
{
"path": "outputs/simulator-integrated-review/portuguese-validation-proof.json",
"sha256": "b7a454cdc1e934faada40b683ff2b7a6edce73aebfd22ba34e5da17a1854b0a1"
},
{
"path": "outputs/simulator-integrated-review/http-reports-proof.json",
"sha256": "9d37fbee89f7dc556ff9496f6b0980e1c9626790eb50a41a499cf23e2f27fe5f"
},
{
"path": "outputs/simulator-integrated-review/date-field-error.jpg",
"sha256": "6a792c09c61857a0a064a56d3d61b47fc2adc1d6c8bcf627161f6fa77c7b099d"
},
{
"path": "outputs/simulator-integrated-review/stale-result.jpg",
"sha256": "84b37c41ba47d44f52979e83dbc3426184f5cbfee6d38e7c7d4caa3be225320b"
},
{
"path": "outputs/simulator-integrated-review/tax-lookup-error.jpg",
"sha256": "d3f366690fa3acbda45bc021ade90dc003cea7dc0517c4d2932935548dbb40d0"
},
{
"path": "outputs/simulator-integrated-review/positivo.docx",
"sha256": "61b1ea80476922aa1be8c29f9a757b389c3ecc8f0e75f414a29eddd5b37ecefd"
},
{
"path": "outputs/simulator-integrated-review/positivo-model.json",
"sha256": "fc8df79e7ce81e4a475b99f5ea07763c1cee140bc9dcb24550ed26ce412e4e0d"
},
{
"path": "outputs/simulator-integrated-review/checked-positivo/positivo.pdf",
"sha256": "60d655b33fcc27dbbaae40a0739c1ebeb9b366df9b00621a6536feecd60da452"
},
{
"path": "outputs/simulator-integrated-review/negativo.docx",
"sha256": "f266cff1a1f9f63449703cc5ef51a7f4d2d7bf94230be70ae382bc89af8139bd"
},
{
"path": "outputs/simulator-integrated-review/negativo-model.json",
"sha256": "6aff37f66695e1d2a7441bf7b361f7980bc10f6951f915713f55a01e5b00da48"
},
{
"path": "outputs/simulator-integrated-review/checked-negativo/negativo.pdf",
"sha256": "3a2577194bc6c9e7234b650e69f3795f6cef6aef41d62658839c0790989a8f5c"
},
{
"path": "outputs/simulator-integrated-review/nulo.docx",
"sha256": "efb83aa9044e35515880e40641d6bea52c6ea6f81289c3abc9d3bdbc76a9ba26"
},
{
"path": "outputs/simulator-integrated-review/nulo-model.json",
"sha256": "94bbc6cf72cc613ebcd5ab9e9c074220b2f50dfb5b03959e92a7c84032a86750"
},
{
"path": "outputs/simulator-integrated-review/checked-nulo/nulo.pdf",
"sha256": "c93e989a7af230e56f5d01766a4e386161e6fbbfa99341d5bfa0569bafe9e18e"
},
{
"path": "outputs/simulator-integrated-review/incompleto.docx",
"sha256": "026f0ff1877ed5f23359d220e3223a319c213e742ff9704d81812fc2ed3ae20e"
},
{
"path": "outputs/simulator-integrated-review/incompleto-model.json",
"sha256": "748b9cecde2acc99d7e2bbfd16188bf9f9ac0ab411ad850ba687c6e1f2e36cf5"
},
{
"path": "outputs/simulator-integrated-review/checked-incompleto/incompleto.pdf",
"sha256": "0daac41c80ae131df6d66b7b542353a61a804d02d2e77769b9e5cc2bc9cfbc88"
},
{
"path": "outputs/simulator-integrated-review/completed-calculation.jpg",
"sha256": "7180ff0f536e2e377c2a28b85574ec7c7fefcce5eed28ed08f07628b16870293"
}
],
"checks": {
"targeted_frontend": 34,
"typescript": "passed",
"php_lint": "passed",
"build": "passed",
"reports_pages": {
"positivo": 6,
"negativo": 6,
"nulo": 6,
"incompleto": 5
},
"tax_lookup_retry": "503 then successful lookup; IVA; 2026-01-01 through 2026-12-31; indeterminate parameters",
"arithmetic_ui": "100*16/100*(1-25/100)=12; base change removes result",
"build_manifest_sha256": "af96a5f5682dad7a741d0ee8b302d8dc7f4d08a1806bc000265bd2602e5bfc61",
"terminal_status_ui": "200*16/100*(1-25/100)=24; completed notice visible; awaiting-calculation notice absent"
},
"remaining_hours": [
6,
10
],
"limitations": [
"No published fiscal rule versions in QA; no legal tax validation",
"Authenticated cross-dossier and new-session frontend delta still pending",
"Parameter-version change and additional business-plan/cash-flow controls pending",
"PDF examples are DOCX rendering; native Edge print and final UI download comparison pending",
"Dynamic backend/catalogue messages and directed save/export error checks remain open"
],
"deployment": {
"authorised_now": false,
"migrations": [],
"locale_on_future_deploy": "APP_LOCALE=pt; preserve existing secrets"
}
}
@@ -1569,3 +1569,28 @@ Provas reutilizáveis identificadas: `SimulationExecutionTest`, `SimulationDraft
Esforço inicial para este recorte: 18–30 h: dados/cálculos 6–10; isolamento/continuidade 5–8; relatórios/erros 4–7; idioma 3–5. Estimativa de engenharia, não tempo já executado nem desenvolvimento fiscal novo. Reestimar após a reconciliação das provas e parâmetros efectivos; fontes/catálogos realmente ausentes ficam separados e não impedem cálculos independentes que disponham dos dados necessários. Nenhuma nova função organizacional exigida.
Correcções iniciais verificadas: 38 testes frontend dirigidos passaram, TypeScript passou. Sem ensaio remoto, SQL de alteração, novas migrations ou mudança do motor fiscal. A revisão integral permanece aberta. Roteiro Tempo, após completar as verificações: escolher contexto → preencher/rever dados → calcular → guardar/recuperar → alterar e recalcular → comparar e exportar. Os relatórios finais e a matriz problema/causa/correcção/prova/pendência serão complementados nesta mesma secção, sem outro backlog.
### Simulador integrado — saldo da verificação QA, 20/10/2026
Continuação de `59ab4fe95635`, exclusivamente na branch `codex/simulator-integrated-review`, baseada no editorial `e69ab149`. Não houve integração em main, instalação remota, migrations, importação ou activação de parâmetros. A base utilizada foi `opportunity_search_20261020_test`, porta 55441, com guarda de isolamento. Os exemplos são sintéticos e explicitamente identificados.
| Item | Resultado e causa/correcção | Prova e saldo concreto |
| --- | --- | --- |
| AUD-SIM-INT01 | Data pontual inválida passa a produzir erro em `fiscal_reference_date`, sem período alternativo. | Testes de vazio, data impossível e ano incompatível; navegador mostrou mensagem junto ao campo, `aria-invalid` e descrição associada. Encerrado este delta. |
| AUD-SIM-INT02 | Inventário efectivo: zero versões de regras fiscais na base QA. As seis fontes territoriais existentes não fornecem taxas ao motor. | Consulta IVA/2026 devolveu indeterminado, nunca zero. Fórmula executada é apenas base × taxa/100 × (1−redução/100), `portal-arithmetic-v1`, valores declarados, sem moeda, regra fiscal ou interpretação legal. A data de execução não é período de vigência fiscal. Fontes oficiais mantêm a aceitação; não se pediu nova autenticação da proveniência. Parâmetros fiscais ausentes ficam indisponíveis, não estimados pela aplicação. |
| AUD-SIM-INT03 | Remontagem do formulário por conta/contexto/rascunho para não reutilizar estado de outro âmbito. Recuperação do rascunho QA observada após reload. | Provas backend anteriores de isolamento mantidas; falta a conferência positiva do delta frontend com dois dossiers e nova sessão autenticada. Reload não foi contado como nova sessão. |
| AUD-SIM-INT04 | Alterar uma entrada retira o resultado actual e mostra aviso de recálculo; não escreve sobre snapshots históricos. | Navegador calculou 12 e, ao alterar base 100→200, retirou o resultado e mostrou o aviso. Versões/hashes backend preservados. Falta concluir a conferência positiva do histórico/contexto no consumidor autenticado e da mudança de versão de parâmetros. |
| AUD-SIM-INT05 | Controlos independentes por livro de caixa, Decimal e recorrência mensal/bissecção para o crédito. | Saldo mensal +100/−100/0; excedente de fundos −200; equilíbrio 11 unidades; crédito sem juros 100 por mês; nominal 12%/ano, 1000/12 meses: 88,85 na primeira prestação; campo vazio recusado. Cenário personalizado 100×16%×75%=12 confirmado no servidor. São controlos matemáticos, não validação de taxas legais. Restam controlos delimitados de cash-flow/plano de negócio. |
| AUD-SIM-INT06 | Resumo do relatório passou a apresentar os resultados financeiros do mesmo modelo de detalhe. Secção sem anexos ficou compacta, eliminando uma página quase vazia. | Quatro exemplos completos pelo exportador real: positivo, negativo, nulo (6 páginas cada), incompleto (5). Todas as páginas DOCX renderizadas conferidas; testes comparam resumo/detalhe. PDF disponibilizado é a renderização do DOCX, não prova da impressão nativa do Edge. Falta essa impressão e a comparação final ecrã/download pela interface. |
| AUD-SIM-INT07 | Removidos requisitos de residência/documentação que não entram no cenário percentual personalizado; mantida validação específica de base/taxa/redução. Consulta fiscal tem nova tentativa e ignora respostas abortadas. Estado de espera é substituído pelo estado terminal do cálculo. | Cenário personalizado calculou sem esses campos; erro 503 sintético na consulta mostrou mensagem e Tentar novamente; repetição preservou IVA/2026 e devolveu indeterminado por falta de parâmetro. Falhas de gravação/exportação ainda carecem de conferência dirigida dos consumidores afectados. |
| AUD-SIM-INT08 | Traduzidos textos de autenticação, perfil, preferências, MFA/chaves, diálogos, notificações e validações comuns, preservando códigos e mecanismos. | Inventário de 4519 textos/atributos JSX literais em todos os TSX; três candidatos restantes são português com a palavra continue. Isto não cobre todas as mensagens dinâmicas do backend ou do catálogo. Inglês explicitamente seleccionável no formulário bilingue e títulos de fontes preservados. Falta classificar o inventário dinâmico e conferir mensagens residuais em execução. |
Operação de idioma para uma eventual disponibilização: usar `APP_LOCALE=pt` na configuração nominal. O fallback de `config/app.php` passou a pt, mas não substitui um valor explícito do ambiente; não copiar `.env` QA. Foram adicionados `lang/pt/auth.php`, `lang/pt/validation.php` e `lang/pt.json`; as regras comuns cobertas não equivalem ao catálogo completo de validações Laravel.
Complemento do inventário: os 77 candidatos de literais TS/TSX restantes foram classificados como classes CSS internas ou traduções explicitamente seleccionáveis (incluindo menus, formulário bilingue e assistência, cuja activação não foi alterada). Foram ainda corrigidos o breadcrumb de segurança e as três mensagens de falha no carregamento de QR/chave/códigos. O fecho desse levantamento estático não certifica todas as mensagens produzidas por backend, conteúdo e serviços em execução; esse é o saldo de idioma ainda aberto.
Provas novas: 34 testes dirigidos de formulário/relatório passaram; TypeScript, build e lint PHP dos ficheiros de idioma/configuração passaram. Mantidas as provas anteriores não afectadas de contratos, APIs, isolamento e versões; nenhum RefreshDatabase executado sobre o acervo QA. Artefactos locais: `outputs/simulator-integrated-review/` (inventário efectivo, controlos independentes, modelos dos quatro relatórios, DOCX/HTML e `checked-*` com PDF/páginas conferidas). As pastas anteriores `render-*`/`final-*` são intermédias e não são as versões entregues.
Exemplos para revisão por HTTP na aplicação QA local, sem `file:`: `http://127.0.0.1:18570/qa/relatorios/simulador/positivo.pdf`, `negativo.pdf`, `nulo.pdf`, `incompleto.pdf`, e os mesmos nomes com extensão `.docx`. Estes endereços são uma whitelist do router privado de QA, exclusivamente loopback; não são rotas públicas de produção. Ficheiros não sintéticos e configuração privada não são servidos por esta whitelist.
**Estimativa restante: 6–10 h**, já descontado o trabalho acima e as provas aceites; não são as 18–30 h iniciais. Distribuição: continuidade autenticada/histórico e mudança de parâmetros 2–3 h; controlos adicionais delimitados 1–2 h; impressão Edge e comparação ecrã/download 1–2 h; mensagens dinâmicas/falhas de gravação/exportação e fecho 2–3 h. Não inclui cadastrar regras fiscais ausentes nem decidir taxas/incentivos sem fundamento. Falta de dados fiscais não bloqueia os controlos aritméticos disponíveis. A revisão integral continua aberta nos pontos precisos desta tabela.
@@ -7382,11 +7382,11 @@ P1,AUD-SEARCH-T09,QA — revisão Tempo pendente,Gestão de procura/capacidade p
P1,AUD-SEARCH-T10,QA — revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,"Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
P1,AUD-SEARCH-T11,QA — revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,"Situação jurídica, consulta e cálculo continuam independentes.",,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
P1,AUD-GUIDE-CARD-20261020,QA review pending,New delta; not approved for main,Common educational card and existing source/context adapter,,Desktop/mobile/keyboard; public-state and source checks,,,,,,educational presentation,Tempo 20/10/2026; same manual,,,Tempo visual review; library integration verified on existing QA; previous dossier proof retained,,Integration approval for new delta,,,,1 test / 8 assertions; TypeScript/build; real public-source visual preview,,,,No import; no remote changes; no claim of new dossier proof,
P1,AUD-SIM-INT01,Fixed in QA; invalid-date checks; 38 directed frontend tests passed,Invalid one-off fiscal date can throw instead of returning validation state,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Fixed in QA; invalid-date checks; 38 directed frontend tests passed,,,,No claim of full review completion,
P1,AUD-SIM-INT02,Verification pending; no parameter activation,Review actual fiscal parameters sources periods and eligibility,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Verification pending; no parameter activation,,,,No claim of full review completion,
P1,AUD-SIM-INT03,Existing controls and accepted proofs to reconcile; no leak established,Review isolation recovery across projects and sessions,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Existing controls and accepted proofs to reconcile; no leak established,,,,No claim of full review completion,
P1,AUD-SIM-INT04,Existing versions/fingerprints; targeted verification pending,Review stale and historical results and correct dossier,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Existing versions/fingerprints; targeted verification pending,,,,No claim of full review completion,
P1,AUD-SIM-INT05,Reconciliation pending; no invented tax assumptions,Independent financial controls unknown zero units and currencies,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Reconciliation pending; no invented tax assumptions,,,,No claim of full review completion,
P1,AUD-SIM-INT06,Existing exporters; examples for this review still pending,Generated reports screen download and pagination,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Existing exporters; examples for this review still pending,,,,No claim of full review completion,
P1,AUD-SIM-INT07,Existing idempotency; consumer-level verification pending,Explicit validate calculate save export errors and retry,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Existing idempotency; consumer-level verification pending,,,,No claim of full review completion,
P1,AUD-SIM-INT08,"Partial QA fix: rate labels, password confirmation/security and spinner; wider inventory pending",Portuguese across portal and simulator outputs,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,"Partial QA fix: rate labels, password confirmation/security and spinner; wider inventory pending",,,,No claim of full review completion,
P1,AUD-SIM-INT01,QA verified: inline date error and no substitute period,Invalid one-off fiscal date can throw instead of returning validation state,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,QA verified: inline date error and no substitute period,,,,No claim of full review completion,
P1,AUD-SIM-INT02,QA inventory complete: no fiscal rules; custom arithmetic only; unavailable parameters remain indeterminate,Review actual fiscal parameters sources periods and eligibility,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,QA inventory complete: no fiscal rules; custom arithmetic only; unavailable parameters remain indeterminate,,,,No claim of full review completion,
P1,AUD-SIM-INT03,Scope remount fixed; reload recovery verified; authenticated two-dossier and new-session frontend check pending,Review isolation recovery across projects and sessions,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Scope remount fixed; reload recovery verified; authenticated two-dossier and new-session frontend check pending,,,,No claim of full review completion,
P1,AUD-SIM-INT04,Stale warning verified after input edit; historical backend preserved; authenticated history and parameter-version delta pending,Review stale and historical results and correct dossier,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Stale warning verified after input edit; historical backend preserved; authenticated history and parameter-version delta pending,,,,No claim of full review completion,
P1,AUD-SIM-INT05,Independent cash rounding break-even and loan controls passed; remaining cash-flow and business-plan controls delimited,Independent financial controls unknown zero units and currencies,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Independent cash rounding break-even and loan controls passed; remaining cash-flow and business-plan controls delimited,,,,No claim of full review completion,
P1,AUD-SIM-INT06,Four real-exporter synthetic DOCX reports checked at 6/6/6/5 pages; native Edge print and UI comparison pending,Generated reports screen download and pagination,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Four real-exporter synthetic DOCX reports checked at 6/6/6/5 pages; native Edge print and UI comparison pending,,,,No claim of full review completion,
P1,AUD-SIM-INT07,Custom arithmetic context requirement removed; fiscal error/retry verified; save/export fault checks pending,Explicit validate calculate save export errors and retry,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Custom arithmetic context requirement removed; fiscal error/retry verified; save/export fault checks pending,,,,No claim of full review completion,
P1,AUD-SIM-INT08,4519 TSX literal inventory and auth/settings/common translations; dynamic backend/catalogue inventory remains open,Portuguese across portal and simulator outputs,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,4519 TSX literal inventory and auth/settings/common translations; dynamic backend/catalogue inventory remains open,,,,No claim of full review completion,
Can't render this file because it is too large.
@@ -3002,3 +3002,5 @@ AUD-GUIDE-CARD-20261020: componente comum com imagem autorizada, título, resumo
## Integrated simulator review - QA 20/10/2026
AUD-SIM-INT01 through AUD-SIM-INT08 are recorded in the same matrix and manual. Initial diagnosis and targeted date/Portuguese corrections only; full review remains open. Initial effort 18-30 h, subject to discounting accepted evidence and completed work. No main integration, real-data modification or tax parameter activation.
Actualização QA de 20/10/2026: os mesmos oito itens foram reconciliados no manual e nas duas tabelas existentes. Data com mensagem junto ao campo; inventário dos parâmetros efectivos (zero regras fiscais QA); controlos matemáticos independentes; aviso de recálculo; quatro relatórios reais do exportador com 6/6/6/5 páginas; consulta fiscal com erro/nova tentativa e traduções comuns verificados. O formulário personalizado já não exige contexto fiscal que não envia ao cálculo. Restam continuidade autenticada entre dois dossiers/nova sessão e histórico, mudança de parâmetros, controlos delimitados de cash-flow/plano, impressão nativa Edge/comparação pela interface e mensagens dinâmicas/falhas dirigidas de gravação/exportação. Estimativa restante **6–10 h**, distinta das 18–30 h iniciais. Sem main, servidor remoto, importação ou activação fiscal. Exemplos PDF são renderizações do DOCX; não certificam impressão do Edge. Ver o saldo detalhado AUD-SIM-INT01–08 no mesmo manual.
@@ -10651,11 +10651,11 @@ AUD-SEARCH-T09,Consistência transversal da pesquisa e navegação — Educaçã
AUD-SEARCH-T10,Consistência transversal da pesquisa e navegação — Biblioteca,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/documentos,"portal/services; portal/document — Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,QA — delta delimitado; revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Dois resumos de contagem, filtros activos sem remoção individual, controlos sem opções e rodapé local repetido.",Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
AUD-SEARCH-T11,Consistência transversal da pesquisa e navegação — Legislação,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/legislacao,portal/services; portal/document — Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,QA — delta delimitado; revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mesmo padrão da biblioteca; não deve expor um universo concorrente.,"Situação jurídica, consulta e cálculo continuam independentes.",portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,"Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Situação jurídica, consulta e cálculo continuam independentes.",,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
AUD-GUIDE-CARD-20261020,"Related educational cards: image, source summary, clear actions",Tempo 20/10/2026; independent delta after 0e7d51f18fb4,educational presentation,/sectores/{slug}; /explorar/{group}; articles; /oportunidades and detail,EducationalArticleCard; EducationalGuides; LegacyContent; SectorInvestment,QA only,1 test / 8 assertions; PHP syntax; TypeScript/build; desktop2048/mobile390; keyboard,QA review pending,Normal QA sector and article verified against existing library resource 10 and local image 25; previous dossier proof retained,,Source-derived summary; public images; conditional opportunity action; context preserved,P1,,,,,,,,,,,,,,,,,,presentation,,,,20/10/2026,,,,,,,,,,,,content; navigation; visual presentation,,Ready for QA review
AUD-SIM-INT01,Invalid one-off fiscal date can throw instead of returning validation state,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Fixed in QA; invalid-date checks; 38 directed frontend tests passed,Initial diagnosis; integral review open,,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT02,Review actual fiscal parameters sources periods and eligibility,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Verification pending; no parameter activation,Initial diagnosis; integral review open,,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT03,Review isolation recovery across projects and sessions,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing controls and accepted proofs to reconcile; no leak established,Initial diagnosis; integral review open,,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT04,Review stale and historical results and correct dossier,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing versions/fingerprints; targeted verification pending,Initial diagnosis; integral review open,,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT05,Independent financial controls unknown zero units and currencies,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Reconciliation pending; no invented tax assumptions,Initial diagnosis; integral review open,,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT06,Generated reports screen download and pagination,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing exporters; examples for this review still pending,Initial diagnosis; integral review open,,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT07,Explicit validate calculate save export errors and retry,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing idempotency; consumer-level verification pending,Initial diagnosis; integral review open,,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT08,Portuguese across portal and simulator outputs,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,"Partial QA fix: rate labels, password confirmation/security and spinner; wider inventory pending",Initial diagnosis; integral review open,,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT01,Invalid one-off fiscal date can throw instead of returning validation state,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Fixed in QA; invalid-date checks; 38 directed frontend tests passed,QA verified: inline date error and no substitute period,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT02,Review actual fiscal parameters sources periods and eligibility,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Verification pending; no parameter activation,QA inventory complete: no fiscal rules; custom arithmetic only; unavailable parameters remain indeterminate,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT03,Review isolation recovery across projects and sessions,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing controls and accepted proofs to reconcile; no leak established,Scope remount fixed; reload recovery verified; authenticated two-dossier and new-session frontend check pending,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT04,Review stale and historical results and correct dossier,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing versions/fingerprints; targeted verification pending,Stale warning verified after input edit; historical backend preserved; authenticated history and parameter-version delta pending,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT05,Independent financial controls unknown zero units and currencies,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Reconciliation pending; no invented tax assumptions,Independent cash rounding break-even and loan controls passed; remaining cash-flow and business-plan controls delimited,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT06,Generated reports screen download and pagination,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing exporters; examples for this review still pending,Four real-exporter synthetic DOCX reports checked at 6/6/6/5 pages; native Edge print and UI comparison pending,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT07,Explicit validate calculate save export errors and retry,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing idempotency; consumer-level verification pending,Custom arithmetic context requirement removed; fiscal error/retry verified; save/export fault checks pending,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
AUD-SIM-INT08,Portuguese across portal and simulator outputs,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,"Partial QA fix: rate labels, password confirmation/security and spinner; wider inventory pending",4519 TSX literal inventory and auth/settings/common translations; dynamic backend/catalogue inventory remains open,Delta verified; precise remaining work in same manual; 6-10 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,,,20/10/2026,,,,,,,,,,,,,,
Can't render this file because it is too large.
+6
View File
@@ -0,0 +1,6 @@
{
"The provided two factor authentication code was invalid.": "O código de autenticação está incorrecto ou expirou. Introduza um novo código da sua aplicação autenticadora.",
"The provided two factor recovery code was invalid.": "O código de recuperação está incorrecto ou já foi utilizado.",
"The provided password is incorrect.": "A palavra-passe está incorrecta.",
"The provided password does not match your current password.": "A palavra-passe indicada não corresponde à palavra-passe actual."
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'failed' => 'O email ou a palavra-passe não correspondem aos nossos registos.',
'password' => 'A palavra-passe está incorrecta.',
'throttle' => 'Foram feitas demasiadas tentativas. Tente novamente dentro de :seconds segundos.',
];
+29
View File
@@ -0,0 +1,29 @@
<?php
return [
'required' => 'O campo :attribute é obrigatório.',
'required_if' => 'O campo :attribute é obrigatório quando :other tem o valor :value.',
'present' => 'O campo :attribute deve estar presente.',
'filled' => 'Preencha o campo :attribute.',
'numeric' => 'O campo :attribute deve conter um número.',
'integer' => 'O campo :attribute deve conter um número inteiro.',
'decimal' => 'O campo :attribute deve ter :decimal casas decimais.',
'string' => 'O campo :attribute deve conter texto.',
'array' => 'O campo :attribute deve conter uma lista.',
'boolean' => 'O campo :attribute deve indicar sim ou não.',
'email' => 'Introduza um endereço de correio electrónico válido em :attribute.',
'uuid' => 'A referência indicada em :attribute não é válida.',
'in' => 'Seleccione uma opção válida em :attribute.',
'exists' => 'A referência seleccionada em :attribute não foi encontrada.',
'unique' => 'O valor indicado em :attribute já está registado.',
'confirmed' => 'A confirmação de :attribute não corresponde ao valor introduzido.',
'current_password' => 'A palavra-passe actual está incorrecta.',
'date' => 'Introduza uma data válida em :attribute.',
'date_format' => 'Introduza uma data válida para :attribute.',
'before_or_equal' => 'A data em :attribute deve ser anterior ou igual a :date.',
'after_or_equal' => 'A data em :attribute deve ser posterior ou igual a :date.',
'accepted' => 'Confirme o campo :attribute para continuar.',
'between' => ['numeric' => 'O valor de :attribute deve estar entre :min e :max.', 'string' => 'O campo :attribute deve ter entre :min e :max caracteres.', 'array' => 'O campo :attribute deve ter entre :min e :max elementos.', 'file' => 'O ficheiro :attribute deve ter entre :min e :max KB.'],
'min' => ['numeric' => 'O valor de :attribute deve ser igual ou superior a :min.', 'string' => 'O campo :attribute deve ter pelo menos :min caracteres.', 'array' => 'O campo :attribute deve ter pelo menos :min elementos.', 'file' => 'O ficheiro :attribute deve ter pelo menos :min KB.'],
'max' => ['numeric' => 'O valor de :attribute não pode exceder :max.', 'string' => 'O campo :attribute não pode exceder :max caracteres.', 'array' => 'O campo :attribute não pode exceder :max elementos.', 'file' => 'O ficheiro :attribute não pode exceder :max KB.'],
'attributes' => ['base' => 'base de cálculo', 'rate' => 'taxa', 'reduction' => 'redução', 'email' => 'email', 'password' => 'palavra-passe', 'current_password' => 'palavra-passe actual', 'economic_snapshot.fiscal_reference_date' => 'data da operação fiscal'],
];
+3 -3
View File
@@ -12,9 +12,9 @@ export default function AppearanceToggleTab({
const { appearance, updateAppearance } = useAppearance();
const tabs: { value: Appearance; icon: LucideIcon; label: string }[] = [
{ value: 'light', icon: Sun, label: 'Light' },
{ value: 'dark', icon: Moon, label: 'Dark' },
{ value: 'system', icon: Monitor, label: 'System' },
{ value: 'light', icon: Sun, label: 'Claro' },
{ value: 'dark', icon: Moon, label: 'Escuro' },
{ value: 'system', icon: Monitor, label: 'Sistema' },
];
return (
+20 -13
View File
@@ -23,14 +23,16 @@ export default function DeleteUser() {
<div className="space-y-6">
<Heading
variant="small"
title="Delete account"
description="Delete your account and all of its resources"
title="Eliminar conta"
description="Eliminar a sua conta e os recursos associados"
/>
<div className="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
<div className="relative space-y-0.5 text-red-600 dark:text-red-100">
<p className="font-medium">Warning</p>
<p className="text-sm">
Please proceed with caution, this cannot be undone.
{' '}
Esta operação é irreversível. Confirme os dados antes de
continuar.{' '}
</p>
</div>
@@ -40,18 +42,20 @@ export default function DeleteUser() {
variant="destructive"
data-test="delete-user-button"
>
Delete account
{' '}
Eliminar conta{' '}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
Are you sure you want to delete your account?
{' '}
Pretende eliminar a sua conta?{' '}
</DialogTitle>
<DialogDescription>
Once your account is deleted, all of its resources
and data will also be permanently deleted. Please
enter your password to confirm you would like to
permanently delete your account.
{' '}
Ao eliminar a conta, os recursos e dados associados
também serão eliminados permanentemente. Introduza a
sua palavra-passe para confirmar.{' '}
</DialogDescription>
<Form
@@ -70,14 +74,15 @@ export default function DeleteUser() {
htmlFor="password"
className="sr-only"
>
Password
{' '}
Palavra-passe{' '}
</Label>
<PasswordInput
id="password"
name="password"
ref={passwordInput}
placeholder="Password"
placeholder="Palavra-passe"
autoComplete="current-password"
/>
@@ -92,7 +97,8 @@ export default function DeleteUser() {
resetAndClearErrors()
}
>
Cancel
{' '}
Cancelar{' '}
</Button>
</DialogClose>
@@ -105,7 +111,8 @@ export default function DeleteUser() {
type="submit"
data-test="confirm-delete-user-button"
>
Delete account
{' '}
Eliminar conta{' '}
</button>
</Button>
</DialogFooter>
+5 -4
View File
@@ -17,9 +17,10 @@ const EmptyState = () => {
<div className="bg-muted mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl">
<KeyRound className="text-muted-foreground h-7 w-7" />
</div>
<p className="font-medium">No passkeys yet</p>
<p className="font-medium"> Ainda não existem chaves de acesso </p>
<p className="text-muted-foreground mt-1 text-sm">
Add a passkey to sign in without a password
{' '}
Adicione uma chave de acesso para entrar sem palavra-passe{' '}
</p>
</div>
);
@@ -47,8 +48,8 @@ export default function ManagePasskeys(props: Props) {
<div className="space-y-6">
<Heading
variant="small"
title="Passkeys"
description="Manage your passkeys for passwordless sign-in"
title="Chaves de acesso"
description="Gerir as chaves de acesso para entrar sem palavra-passe"
/>
<div className="border-border overflow-hidden rounded-lg border">
+16 -13
View File
@@ -48,15 +48,15 @@ export default function ManageTwoFactor(props: Props) {
<div className="space-y-6">
<Heading
variant="small"
title="Two-factor authentication"
description="Manage your two-factor authentication settings"
title="Autenticação de dois factores"
description="Gerir a autenticação de dois factores"
/>
{twoFactorEnabled ? (
<div className="flex flex-col items-start justify-start space-y-4">
<p className="text-muted-foreground text-sm">
You will be prompted for a secure, random pin during
login, which you can retrieve from the TOTP-supported
application on your phone.
{' '}
Ao entrar, será solicitado o código temporário da
aplicação autenticadora do seu telefone.{' '}
</p>
<div className="relative inline">
@@ -67,7 +67,9 @@ export default function ManageTwoFactor(props: Props) {
type="submit"
disabled={processing}
>
Disable 2FA
{' '}
Desactivar autenticação de dois
factores{' '}
</Button>
)}
</Form>
@@ -82,17 +84,16 @@ export default function ManageTwoFactor(props: Props) {
) : (
<div className="flex flex-col items-start justify-start space-y-4">
<p className="text-muted-foreground text-sm">
When you enable two-factor authentication, you will be
prompted for a secure pin during login. This pin can be
retrieved from a TOTP-supported application on your
phone.
{' '}
Depois de activar a autenticação de dois factores, use o
código temporário da aplicação autenticadora para
entrar.{' '}
</p>
<div>
{hasSetupData ? (
<Button onClick={() => setShowSetupModal(true)}>
<ShieldCheck />
Continue setup
<ShieldCheck /> Continuar configuração{' '}
</Button>
) : (
<Form
@@ -101,7 +102,9 @@ export default function ManageTwoFactor(props: Props) {
>
{({ processing }) => (
<Button type="submit" disabled={processing}>
Enable 2FA
{' '}
Activar autenticação de dois
factores{' '}
</Button>
)}
</Form>
+9 -9
View File
@@ -43,13 +43,14 @@ export default function PasskeyItem({ passkey, onDelete }: Props) {
)}
</div>
<p className="text-muted-foreground text-sm">
Added {passkey.created_at_diff}
{' '}
Adicionada em {passkey.created_at_diff}
{passkey.last_used_at_diff && (
<>
<span className="text-muted-foreground/50 mx-1">
/
</span>
Last used {passkey.last_used_at_diff}
</span>{' '}
Última utilização {passkey.last_used_at_diff}
</>
)}
</p>
@@ -64,19 +65,18 @@ export default function PasskeyItem({ passkey, onDelete }: Props) {
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Remove</span>
<span className="sr-only"> Remover </span>
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Remove passkey</DialogTitle>
<DialogTitle>Remover chave de acesso</DialogTitle>
<DialogDescription>
Are you sure you want to remove the "{passkey.name}"
passkey? You will no longer be able to use it to sign
in.
Pretende remover a chave de acesso «{passkey.name}»?
Deixará de poder usá-la para entrar.
</DialogDescription>
<DialogFooter className="gap-2">
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
<Button variant="secondary"> Cancelar </Button>
</DialogClose>
<Button
variant="destructive"
+11 -7
View File
@@ -59,7 +59,8 @@ export default function PasskeyRegistration({ onSuccess }: Props) {
if (!isSupported) {
return (
<div className="text-muted-foreground text-sm">
Passkeys are not supported in this browser.
{' '}
Este navegador não suporta chaves de acesso.{' '}
</div>
);
}
@@ -67,7 +68,8 @@ export default function PasskeyRegistration({ onSuccess }: Props) {
if (!showForm) {
return (
<Button variant="outline" onClick={() => setShowForm(true)}>
Add passkey
{' '}
Adicionar chave de acesso{' '}
</Button>
);
}
@@ -78,18 +80,19 @@ export default function PasskeyRegistration({ onSuccess }: Props) {
className="border-border bg-muted/50 space-y-4 rounded-lg border p-4"
>
<div className="grid gap-2">
<Label htmlFor="passkey-name">Passkey name</Label>
<Label htmlFor="passkey-name"> Nome da chave de acesso </Label>
<Input
id="passkey-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., MacBook Pro, iPhone"
placeholder="Por exemplo, MacBook Pro ou iPhone"
className="border-foreground/20 mt-1 block w-full"
autoFocus
/>
<p className="text-muted-foreground text-xs">
A name helps you identify this passkey later.
{' '}
O nome permite identificar esta chave mais tarde.{' '}
</p>
</div>
@@ -97,10 +100,11 @@ export default function PasskeyRegistration({ onSuccess }: Props) {
<div className="flex gap-2">
<Button type="submit" disabled={isLoading || !name.trim()}>
{isLoading ? 'Registering...' : 'Register passkey'}
{isLoading ? 'Registering...' : 'Registar chave de acesso'}
</Button>
<Button type="button" variant="ghost" onClick={handleCancel}>
Cancel
{' '}
Cancelar{' '}
</Button>
</div>
</form>
+3 -3
View File
@@ -51,8 +51,8 @@ export default function PasskeyVerify({
>
{isLoading ? <Spinner /> : <KeyRound className="h-4 w-4" />}
{isLoading
? (loadingLabel ?? 'Authenticating...')
: (label ?? 'Sign in with a passkey')}
? (loadingLabel ?? 'A autenticar…')
: (label ?? 'Entrar com uma chave de acesso')}
</Button>
{error && (
<InputError message={error} className="text-center" />
@@ -65,7 +65,7 @@ export default function PasskeyVerify({
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background text-muted-foreground px-2">
{separator ?? 'Or continue with email'}
{separator ?? 'Ou continuar com email'}
</span>
</div>
</div>
@@ -66,6 +66,7 @@ export function SimulationFiscalStatus({
}) {
const [parameters, setParameters] = useState<Parameter[]>([]);
const [requestState, setRequestState] = useState('idle');
const [retry, setRetry] = useState(0);
useEffect(() => {
const apiComponent = component
@@ -104,12 +105,14 @@ export function SimulationFiscalStatus({
const payload = (await response.json()) as {
parameters?: Parameter[];
};
if (abort.signal.aborted) return;
setParameters(
Array.isArray(payload.parameters) ? payload.parameters : [],
);
setRequestState('done');
})
.catch((error: unknown) => {
if (abort.signal.aborted) return;
if (
error instanceof DOMException &&
error.name === 'AbortError'
@@ -125,6 +128,7 @@ export function SimulationFiscalStatus({
periodStart,
periodEnd,
JSON.stringify(context ?? {}),
retry,
]);
const salaryViewMessages: Record<string, string> = {
@@ -160,6 +164,16 @@ export function SimulationFiscalStatus({
: `Período consultado: ${periodStart} a ${periodEnd}.`}
</p>
)}
{requestState === 'error' &&
scenarioMode === 'approved_parameters' && (
<button
type="button"
onClick={() => setRetry((value) => value + 1)}
className="rounded border border-amber-900 px-4 py-2 text-amber-950 underline focus-visible:outline-2 focus-visible:outline-offset-2"
>
Tentar novamente
</button>
)}
{scenarioMode === 'custom_planning' && (
<p role="note" className="text-sm">
Modo de planeamento separado: pressupostos do utilizador,
+35 -1
View File
@@ -864,6 +864,10 @@ export function SimulationForm() {
const [isCalculating, setIsCalculating] = useState(false);
const [hasSaveConflict, setHasSaveConflict] = useState(false);
const [result, setResult] = useState<ApiSimulationExecutionV1 | null>(null);
const [calculationStale, setCalculationStale] = useState(false);
useEffect(() => {
if (result) setCalculationStale(false);
}, [result]);
const arithmeticResult = useMemo(
() => readArithmeticSnapshot(result?.result_snapshot),
[result],
@@ -1039,6 +1043,13 @@ export function SimulationForm() {
: (simulationFieldGroups[current.id] ?? []);
const fields = fieldsForCurrent.filter(
(field) =>
!(
current.id === 'tax' &&
draft.fiscal_scenario_mode === 'custom_planning' &&
!['fiscal_scenario_mode', 'base', 'rate', 'reduction'].includes(
field.key,
)
) &&
(!field.showWhen || field.showWhen(draft)) &&
!(
draft.purpose === 'simulate_only' &&
@@ -1943,7 +1954,8 @@ export function SimulationForm() {
return next;
});
if (key === 'purpose') setStep(0);
if (key !== 'purpose') setResult(null);
if (result) setCalculationStale(true);
setResult(null);
}
async function clearDraft() {
@@ -2274,6 +2286,18 @@ export function SimulationForm() {
)
setResult(simulation ?? null);
if (canSavePersonalData) void loadSimulationHistory();
if (simulation?.status === 'completed') {
setSaveStatus(
'Cálculo concluído. Reveja o resultado e os pressupostos.',
);
} else if (
simulation?.status === 'failed' ||
simulation?.status === 'cancelled'
) {
setSaveStatus(
'O cálculo não foi concluído. Os dados introduzidos foram preservados.',
);
}
sessionStorage.removeItem(idempotencyStorageKey);
} catch (cause) {
setServerError(
@@ -2396,6 +2420,16 @@ export function SimulationForm() {
aria-labelledby="simulator-step-title"
aria-busy={!ready}
>
{calculationStale && (
<p
role="status"
className="rounded border border-amber-700 bg-amber-50 p-3 text-amber-950"
>
As entradas foram alteradas. Calcule novamente antes de
guardar ou exportar um novo resultado. Os resultados
anteriores mantêm-se no histórico.
</p>
)}
{!ready && (
<p role="status">
A recuperar o rascunho. Aguarde para editar.
@@ -56,12 +56,14 @@ export default function TwoFactorRecoveryCodes({
<Card>
<CardHeader>
<CardTitle className="flex gap-3">
<LockKeyhole className="size-4" aria-hidden="true" />
2FA recovery codes
<LockKeyhole className="size-4" aria-hidden="true" />{' '}
Códigos de recuperação{' '}
</CardTitle>
<CardDescription>
Recovery codes let you regain access if you lose your 2FA
device. Store them in a secure password manager.
{' '}
Os códigos de recuperação permitem recuperar o acesso se
perder o dispositivo autenticador. Guarde-os num gestor de
palavras-passe seguro.{' '}
</CardDescription>
</CardHeader>
<CardContent>
@@ -76,7 +78,8 @@ export default function TwoFactorRecoveryCodes({
className="size-4"
aria-hidden="true"
/>
{codesAreVisible ? 'Hide' : 'View'} recovery codes
{codesAreVisible ? 'Ocultar' : 'Ver'} códigos de
recuperação{' '}
</Button>
{canRegenerateCodes && (
@@ -92,7 +95,7 @@ export default function TwoFactorRecoveryCodes({
disabled={processing}
aria-describedby="regenerate-warning"
>
<RefreshCw /> Regenerate codes
<RefreshCw /> Gerar novos códigos{' '}
</Button>
)}
</Form>
@@ -112,7 +115,7 @@ export default function TwoFactorRecoveryCodes({
ref={codesSectionRef}
className="bg-muted grid gap-1 rounded-lg p-4 font-mono text-sm"
role="list"
aria-label="Recovery codes"
aria-label="Códigos de recuperação"
>
{recoveryCodesList.length ? (
recoveryCodesList.map((code, index) => (
@@ -127,7 +130,7 @@ export default function TwoFactorRecoveryCodes({
) : (
<div
className="space-y-2"
aria-label="Loading recovery codes"
aria-label="A carregar os códigos de recuperação"
>
{Array.from(
{ length: 8 },
@@ -145,13 +148,15 @@ export default function TwoFactorRecoveryCodes({
<div className="text-muted-foreground text-xs select-none">
<p id="regenerate-warning">
Each recovery code can be used once to
access your account and will be removed
after use. If you need more, click{' '}
{' '}
Cada código permite uma única entrada e
deixa de ser válido após a utilização.
Para obter novos códigos, seleccione{' '}
<span className="font-bold">
Regenerate codes
{' '}
Gerar novos códigos{' '}
</span>{' '}
above.
acima.{' '}
</p>
</div>
</>
@@ -104,7 +104,8 @@ function TwoFactorSetupStep({
<div className="relative flex w-full items-center justify-center">
<div className="bg-border absolute inset-0 top-1/2 h-px w-full" />
<span className="bg-card relative px-2 py-1">
or, enter the code manually
{' '}
ou introduza a chave manualmente{' '}
</span>
</div>
@@ -210,7 +211,8 @@ function TwoFactorVerificationStep({
onClick={onBack}
disabled={processing}
>
Back
{' '}
Voltar{' '}
</Button>
<Button
type="submit"
@@ -219,7 +221,8 @@ function TwoFactorVerificationStep({
processing || code.length < OTP_MAX_LENGTH
}
>
Confirm
{' '}
Confirmar{' '}
</Button>
</div>
</div>
@@ -262,27 +265,27 @@ export default function TwoFactorSetupModal({
}>(() => {
if (twoFactorEnabled) {
return {
title: 'Two-factor authentication enabled',
title: 'Autenticação de dois factores activada',
description:
'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.',
buttonText: 'Close',
'A autenticação de dois factores está activada. Leia o código QR ou introduza a chave na aplicação autenticadora.',
buttonText: 'Fechar',
};
}
if (showVerificationStep) {
return {
title: 'Verify authentication code',
title: 'Confirmar código de autenticação',
description:
'Enter the 6-digit code from your authenticator app',
buttonText: 'Continue',
'Introduza o código de seis dígitos da aplicação autenticadora',
buttonText: 'Continuar',
};
}
return {
title: 'Enable two-factor authentication',
title: 'Activar autenticação de dois factores',
description:
'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app',
buttonText: 'Continue',
'Para concluir a configuração, leia o código QR ou introduza a chave na aplicação autenticadora.',
buttonText: 'Continuar',
};
}, [twoFactorEnabled, showVerificationStep]);
+1 -1
View File
@@ -63,7 +63,7 @@ function DialogContent({
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
<span className="sr-only"> Fechar </span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
+1 -1
View File
@@ -72,7 +72,7 @@ function SheetContent({
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
<span className="sr-only"> Fechar </span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
@@ -40,8 +40,7 @@ export function UserMenuContent({ user }: Props) {
prefetch
onClick={cleanup}
>
<Settings className="mr-2" />
Settings
<Settings className="mr-2" /> Definições{' '}
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
+12 -3
View File
@@ -55,7 +55,10 @@ export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
setQrCodeSvg(svg);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch QR code']);
setErrors((prev) => [
...prev,
'Não foi possível carregar o código QR.',
]);
setQrCodeSvg(null);
}
}, [submit]);
@@ -68,7 +71,10 @@ export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
setManualSetupKey(key);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch a setup key']);
setErrors((prev) => [
...prev,
'Não foi possível carregar a chave de configuração.',
]);
setManualSetupKey(null);
}
}, [submit]);
@@ -79,7 +85,10 @@ export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
const codes = (await submit(recoveryCodes())) as string[];
setRecoveryCodesList(codes);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch recovery codes']);
setErrors((prev) => [
...prev,
'Não foi possível carregar os códigos de recuperação.',
]);
setRecoveryCodesList([]);
}
}, [submit]);
+6 -6
View File
@@ -12,17 +12,17 @@ import type { NavItem } from '@/types';
const sidebarNavItems: NavItem[] = [
{
title: 'Profile',
title: 'Perfil',
href: edit(),
icon: null,
},
{
title: 'Security',
title: 'Segurança',
href: editSecurity(),
icon: null,
},
{
title: 'Appearance',
title: 'Aparência',
href: editAppearance(),
icon: null,
},
@@ -34,15 +34,15 @@ export default function SettingsLayout({ children }: PropsWithChildren) {
return (
<div className="px-4 py-6">
<Heading
title="Settings"
description="Manage your profile and account settings"
title="Definições"
description="Gerir o perfil e as definições da conta"
/>
<div className="flex flex-col lg:flex-row lg:space-x-12">
<aside className="w-full max-w-xl lg:w-48">
<nav
className="flex flex-col space-y-1 space-x-0"
aria-label="Settings"
aria-label="Definições"
>
{sidebarNavItems.map((item, index) => (
<Button
+20 -1
View File
@@ -398,6 +398,15 @@ export async function generateProjectDossierDocx(
];
for (const section of model.sections) {
if (section.id === 'annexes') {
if (!attachments.some((item) => item.include)) {
children.push(
new Paragraph({
text: 'Anexos: não foram seleccionados ficheiros.',
spacing: { before: 80, after: 80 },
}),
);
continue;
}
children.push(
new Paragraph({
text: section.title,
@@ -447,7 +456,10 @@ export async function generateProjectDossierDocx(
alignment: AlignmentType.RIGHT,
children: [
new TextRun({
text: 'InvesteMoz · '+model.title.slice(0, 60)+' · privado · página ',
text:
'InvesteMoz · ' +
model.title.slice(0, 60) +
' · privado · página ',
size: 18,
}),
new SimpleField('PAGE'),
@@ -580,6 +592,13 @@ export async function createProjectDossierPrintHtml(
): Promise<string> {
const content: string[] = [];
for (const section of model.sections) {
if (
section.id === 'annexes' &&
!attachments.some((item) => item.include)
) {
content.push('<p>Anexos: não foram seleccionados ficheiros.</p>');
continue;
}
content.push(`<section><h2>${escapeHtml(section.title)}</h2>`);
for (const paragraph of section.paragraphs)
content.push(`<p>${escapeHtml(paragraph)}</p>`);
+71 -12
View File
@@ -111,29 +111,57 @@ function classification(origin: string): string {
return 'Referência/estado indicado';
}
export function formatDossierReportValue(field: string, value: string, currency = ''): string {
export function formatDossierReportValue(
field: string,
value: string,
currency = '',
): string {
if (field === 'Estado do plano detalhado') {
return value.replace('Snapshot versionado disponível; usar também a exportação do plano financeiro', 'Plano guardado disponível; consulte também a exportação financeira');
return value.replace(
'Snapshot versionado disponível; usar também a exportação do plano financeiro',
'Plano guardado disponível; consulte também a exportação financeira',
);
}
if (field === 'Reconciliação do financiamento') {
const states: Record<string, string> = { shortfall: 'Financiamento insuficiente', surplus: 'Fontes superiores aos usos previstos', balanced: 'Fontes e usos equilibrados', unknown: 'Informação insuficiente', incomplete: 'Informação insuficiente' };
const states: Record<string, string> = {
shortfall: 'Financiamento insuficiente',
surplus: 'Fontes superiores aos usos previstos',
balanced: 'Fontes e usos equilibrados',
unknown: 'Informação insuficiente',
incomplete: 'Informação insuficiente',
};
return states[value] ?? value;
}
if (/^Necessidade \d+$/.test(field)) {
value = value.replace('; gap;', '; Competência em falta;').replace('; unknown;', '; Por confirmar;').replace('; available;', '; Disponível (declarado);');
value = value
.replace('; gap;', '; Competência em falta;')
.replace('; unknown;', '; Por confirmar;')
.replace('; available;', '; Disponível (declarado);');
}
if (/^Marco \d+$/.test(field)) {
value = value.replace('; planned;', '; Planeado;').replace('; in_progress;', '; Em curso;').replace('; completed;', '; Concluído (declarado);');
value = value
.replace('; planned;', '; Planeado;')
.replace('; in_progress;', '; Em curso;')
.replace('; completed;', '; Concluído (declarado);');
}
if (/^(Necessidade|Marco) \d+$/.test(field)) {
value = value.replace(/(custo|planeado|realizado): ([^;]*)(?=;|$)/g, (_, label: string, amount: string) => {
const raw = amount.trim();
return `${label}: ${raw ? (/^\d+(?:\.\d{1,2})?$/.test(raw) ? `${raw} ${currency || '(moeda não informada)'}` : raw) : 'Não informado'}`;
}).replace(/prazo: (?=;|$)/g, 'prazo: Não informado');
value = value
.replace(
/(custo|planeado|realizado): ([^;]*)(?=;|$)/g,
(_, label: string, amount: string) => {
const raw = amount.trim();
return `${label}: ${raw ? (/^\d+(?:\.\d{1,2})?$/.test(raw) ? `${raw} ${currency || '(moeda não informada)'}` : raw) : 'Não informado'}`;
},
)
.replace(/prazo: (?=;|$)/g, 'prazo: Não informado');
}
// Presentation only: group the decimal string without converting the amount
// to a JavaScript float or changing the saved financial snapshot.
return value.replace(/^(-?)(\d+)\.(\d{2}) ([A-Z]{3})$/, (_, sign: string, whole: string, cents: string, currency: string) => `${sign}${whole.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')},${cents} ${currency}`);
return value.replace(
/^(-?)(\d+)\.(\d{2}) ([A-Z]{3})$/,
(_, sign: string, whole: string, cents: string, currency: string) =>
`${sign}${whole.replace(/\B(?=(\d{3})+(?!\d))/g, ' ')},${cents} ${currency}`,
);
}
const rowSection: Record<string, DossierReportSectionId> = {
Projecto: 'project',
@@ -214,6 +242,32 @@ function makeSections(
classification: 'Referência da revisão',
},
);
const highlights = new Set([
'Saldo operacional estimado',
'Diferença por financiar',
'Prestação do primeiro período',
]);
const budgetPeriod = rows.find(
(row) => row[1] === 'Periodicidade do orçamento',
)?.[2];
for (const [, field, value, origin] of rows.slice(1)) {
if (!highlights.has(field)) continue;
summary.rows.push({
field:
field === 'Saldo operacional estimado' && budgetPeriod
? `${field} — ${budgetPeriod}`
: field,
value: absent(
formatDossierReportValue(
field,
value,
text(draft, 'currency'),
),
),
origin,
classification: classification(origin),
});
}
}
for (const [sectionName, field, value, origin] of rows.slice(1)) {
@@ -226,9 +280,14 @@ function makeSections(
if (!section) continue;
section.rows.push({
field,
value: absent(formatDossierReportValue(field, value, text(draft, 'currency'))),
value: absent(
formatDossierReportValue(field, value, text(draft, 'currency')),
),
origin,
classification: value.trim() === '' ? 'Informação em falta' : classification(origin),
classification:
value.trim() === ''
? 'Informação em falta'
: classification(origin),
});
}
+13
View File
@@ -1544,6 +1544,10 @@ export function validateSimulationDraft(
draft: SimulationDraft,
sectionId: string,
): Record<string, string> {
// The separate arithmetic validator checks its three inputs. Fiscal
// eligibility facts are not inputs of this planning-only calculation.
if (sectionId === 'tax' && draft.fiscal_scenario_mode === 'custom_planning')
return {};
const errors: Record<string, string> = {};
for (const field of simulationFieldGroups[sectionId] ?? []) {
if (field.showWhen && !field.showWhen(draft)) continue;
@@ -1594,6 +1598,15 @@ export function validateSimulationDraft(
errors[field.key] =
'Introduza um endereço de correio electrónico válido.';
}
if (
sectionId === 'tax' &&
draft.fiscal_scenario_mode === 'approved_parameters' &&
draft.fiscal_periodicity === 'one_off' &&
!getFiscalPeriodRange(draft)
) {
errors.fiscal_reference_date =
'Introduza uma data válida da operação no ano fiscal seleccionado. Não foi usada uma data alternativa.';
}
if (
sectionId === 'financial' &&
[
+10 -6
View File
@@ -12,7 +12,7 @@ import { email } from '@/routes/password';
export default function ForgotPassword({ status }: { status?: string }) {
return (
<>
<Head title="Forgot password" />
<Head title="Recuperar palavra-passe" />
{status && (
<div className="mb-4 text-center text-sm font-medium text-green-600">
@@ -25,7 +25,10 @@ export default function ForgotPassword({ status }: { status?: string }) {
{({ processing, errors }) => (
<>
<div className="grid gap-2">
<Label htmlFor="email">Email address</Label>
<Label htmlFor="email">
{' '}
Endereço de correio electrónico{' '}
</Label>
<Input
id="email"
type="email"
@@ -46,8 +49,8 @@ export default function ForgotPassword({ status }: { status?: string }) {
>
{processing && (
<LoaderCircle className="h-4 w-4 animate-spin" />
)}
Email password reset link
)}{' '}
Enviar ligação de recuperação{' '}
</Button>
</div>
</>
@@ -64,6 +67,7 @@ export default function ForgotPassword({ status }: { status?: string }) {
}
ForgotPassword.layout = {
title: 'Forgot password',
description: 'Enter your email to receive a password reset link',
title: 'Recuperar palavra-passe',
description:
'Indique o seu email para receber a ligação de recuperação da palavra-passe',
};
+20 -8
View File
@@ -39,15 +39,19 @@ export default function Login({ status, canResetPassword, qaAccount }: Props) {
Ambiente QA — conta {qaAccount.id}
</p>
<p>
Para os cenários A/B: {qaAccount.email}. Use a password
e o autenticador desta conta. Outros perfis devem usar
as respetivas contas sintéticas QA.
Para os cenários A/B: {qaAccount.email}. Use a
palavra-passe e o autenticador desta conta. Outros
perfis devem usar as respetivas contas sintéticas QA.
</p>
</aside>
)}
{/* @chisel-passkeys */}
<PasskeyVerify label="Entrar com chave de acesso" loadingLabel="A autenticar…" separator="Ou continuar com email" />
<PasskeyVerify
label="Entrar com chave de acesso"
loadingLabel="A autenticar…"
separator="Ou continuar com email"
/>
{/* @end-chisel-passkeys */}
<Form
@@ -68,7 +72,10 @@ export default function Login({ status, canResetPassword, qaAccount }: Props) {
tabIndex={1}
autoComplete="email"
placeholder="email@example.com"
defaultValue={qaAccount?.email ?? participationIdentity().email}
defaultValue={
qaAccount?.email ??
participationIdentity().email
}
/>
<InputError
message={errors.email ?? pageErrors.email}
@@ -77,7 +84,9 @@ export default function Login({ status, canResetPassword, qaAccount }: Props) {
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Palavra-passe</Label>
<Label htmlFor="password">
Palavra-passe
</Label>
{canResetPassword && (
<TextLink
href={request()}
@@ -105,7 +114,9 @@ export default function Login({ status, canResetPassword, qaAccount }: Props) {
name="remember"
tabIndex={3}
/>
<Label htmlFor="remember">Manter sessão iniciada</Label>
<Label htmlFor="remember">
Manter sessão iniciada
</Label>
</div>
<Button
@@ -143,5 +154,6 @@ export default function Login({ status, canResetPassword, qaAccount }: Props) {
Login.layout = {
title: 'Entrar na sua conta',
description: 'Entre para continuar a operação que escolheu. Os dados preenchidos permanecem guardados neste navegador.',
description:
'Entre para continuar a operação que escolheu. Os dados preenchidos permanecem guardados neste navegador.',
};
+10 -9
View File
@@ -16,7 +16,7 @@ type Props = {
export default function ResetPassword({ token, email, passwordRules }: Props) {
return (
<>
<Head title="Reset password" />
<Head title="Redefinir palavra-passe" />
<Form
{...update.form()}
@@ -43,14 +43,14 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Label htmlFor="password"> Palavra-passe </Label>
<PasswordInput
id="password"
name="password"
autoComplete="new-password"
className="mt-1 block w-full"
autoFocus
placeholder="Password"
placeholder="Palavra-passe"
passwordrules={passwordRules}
/>
<InputError message={errors.password} />
@@ -58,14 +58,15 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
<div className="grid gap-2">
<Label htmlFor="password_confirmation">
Confirm password
{' '}
Confirmar palavra-passe{' '}
</Label>
<PasswordInput
id="password_confirmation"
name="password_confirmation"
autoComplete="new-password"
className="mt-1 block w-full"
placeholder="Confirm password"
placeholder="Confirmar palavra-passe"
passwordrules={passwordRules}
/>
<InputError
@@ -80,8 +81,8 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
disabled={processing}
data-test="reset-password-button"
>
{processing && <Spinner />}
Reset password
{processing && <Spinner />} Redefinir
palavra-passe{' '}
</Button>
</div>
)}
@@ -91,6 +92,6 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
}
ResetPassword.layout = {
title: 'Reset password',
description: 'Please enter your new password below',
title: 'Redefinir palavra-passe',
description: 'Introduza a nova palavra-passe',
};
@@ -27,18 +27,18 @@ export default function TwoFactorChallenge({
}>(() => {
if (showRecoveryInput) {
return {
title: 'Recovery code',
title: 'Código de recuperação',
description:
'Please confirm access to your account by entering one of your emergency recovery codes.',
toggleText: 'login using an authentication code',
'Confirme o acesso à conta com um dos seus códigos de recuperação.',
toggleText: 'entrar com um código de autenticação',
};
}
return {
title: 'Authentication code',
title: 'Código de autenticação',
description:
'Enter the authentication code provided by your authenticator application.',
toggleText: 'login using a recovery code',
'Introduza o código apresentado pela sua aplicação autenticadora.',
toggleText: 'entrar com um código de recuperação',
};
}, [showRecoveryInput]);
@@ -55,7 +55,7 @@ export default function TwoFactorChallenge({
return (
<>
<Head title="Two-factor authentication" />
<Head title="Autenticação de dois factores" />
{qaAccount && (
<p
@@ -81,7 +81,7 @@ export default function TwoFactorChallenge({
<Input
name="recovery_code"
type="text"
placeholder="Enter recovery code"
placeholder="Introduza um código de recuperação"
autoFocus={showRecoveryInput}
required
/>
@@ -123,7 +123,8 @@ export default function TwoFactorChallenge({
className="w-full"
disabled={processing}
>
Continue
{' '}
Continuar{' '}
</Button>
<div className="text-muted-foreground text-center text-sm">
+10 -9
View File
@@ -9,12 +9,13 @@ import { send } from '@/routes/verification';
export default function VerifyEmail({ status }: { status?: string }) {
return (
<>
<Head title="Email verification" />
<Head title="Confirmação de email" />
{status === 'verification-link-sent' && (
<div className="mb-4 text-center text-sm font-medium text-green-600">
A new verification link has been sent to the email address
you provided during registration.
{' '}
Foi enviada uma nova ligação de confirmação para o email
indicado no registo.{' '}
</div>
)}
@@ -22,15 +23,16 @@ export default function VerifyEmail({ status }: { status?: string }) {
{({ processing }) => (
<>
<Button disabled={processing} variant="secondary">
{processing && <Spinner />}
Resend verification email
{processing && <Spinner />} Reenviar email de
confirmação{' '}
</Button>
<TextLink
href={logout()}
className="mx-auto block text-sm"
>
Log out
{' '}
Terminar sessão{' '}
</TextLink>
</>
)}
@@ -40,7 +42,6 @@ export default function VerifyEmail({ status }: { status?: string }) {
}
VerifyEmail.layout = {
title: 'Email verification',
description:
'Please verify your email address by clicking on the link we just emailed to you.',
title: 'Confirmação de email',
description: 'Confirme o seu email através da ligação que enviámos.',
};
+4 -1
View File
@@ -419,6 +419,9 @@ function PartnershipForm() {
}
function Simulator() {
const page = usePage<{ auth: { user: { id: number } | null } }>();
const query = new URL(page.url, 'https://investmoz.invalid').searchParams;
const scope = `${page.props.auth.user?.id ?? 'anonymous'}:${query.get('context') ?? ''}:${query.get('draft') ?? ''}`;
return (
<div className="space-y-5">
<p>
@@ -428,7 +431,7 @@ function Simulator() {
para rever mercado, localização, recursos, formalização e
execução.
</p>
<SimulationForm />
<SimulationForm key={scope} />
</div>
);
}
+5 -5
View File
@@ -6,15 +6,15 @@ import { edit as editAppearance } from '@/routes/appearance';
export default function Appearance() {
return (
<>
<Head title="Appearance settings" />
<Head title="Definições de aparência" />
<h1 className="sr-only">Appearance settings</h1>
<h1 className="sr-only"> Definições de aparência </h1>
<div className="space-y-6">
<Heading
variant="small"
title="Appearance settings"
description="Update the appearance settings for your account"
title="Definições de aparência"
description="Escolha a aparência da sua conta"
/>
<AppearanceTabs />
</div>
@@ -25,7 +25,7 @@ export default function Appearance() {
Appearance.layout = {
breadcrumbs: [
{
title: 'Appearance settings',
title: 'Definições de aparência',
href: editAppearance(),
},
],
+25 -15
View File
@@ -34,15 +34,15 @@ export default function Profile(
return (
<>
<Head title="Profile settings" />
<Head title="Definições do perfil" />
<h1 className="sr-only">Profile settings</h1>
<h1 className="sr-only"> Definições do perfil </h1>
<div className="space-y-6">
<Heading
variant="small"
title="Profile"
description="Update your name and email address"
title="Perfil"
description="Actualize o seu nome e endereço de correio electrónico"
/>
<Form
@@ -55,7 +55,7 @@ export default function Profile(
{({ processing, errors }) => (
<>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Label htmlFor="name"> Nome </Label>
<Input
id="name"
@@ -64,7 +64,7 @@ export default function Profile(
name="name"
required
autoComplete="name"
placeholder="Full name"
placeholder="Nome completo"
/>
<InputError
@@ -74,7 +74,10 @@ export default function Profile(
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email address</Label>
<Label htmlFor="email">
{' '}
Endereço de correio electrónico{' '}
</Label>
<Input
id="email"
@@ -84,7 +87,7 @@ export default function Profile(
name="email"
required
autoComplete="username"
placeholder="Email address"
placeholder="Endereço de correio electrónico"
/>
<InputError
@@ -98,22 +101,28 @@ export default function Profile(
auth.user.email_verified_at === null && (
<div>
<p className="text-muted-foreground -mt-4 text-sm">
Your email address is unverified.{' '}
{' '}
O seu endereço de correio
electrónico ainda não foi
confirmado.{' '}
<Link
href={send()}
as="button"
className="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
>
Click here to re-send the
verification email.
{' '}
Reenviar o email de
confirmação.{' '}
</Link>
</p>
{status ===
'verification-link-sent' && (
<div className="mt-2 text-sm font-medium text-green-600">
A new verification link has been
sent to your email address.
{' '}
Foi enviada uma nova ligação de
confirmação para o seu
email.{' '}
</div>
)}
</div>
@@ -125,7 +134,8 @@ export default function Profile(
disabled={processing}
data-test="update-profile-button"
>
Save
{' '}
Guardar{' '}
</Button>
</div>
</>
@@ -141,7 +151,7 @@ export default function Profile(
Profile.layout = {
breadcrumbs: [
{
title: 'Profile settings',
title: 'Definições do perfil',
href: edit(),
},
],
+1 -1
View File
@@ -151,7 +151,7 @@ export default function Security(props: Props) {
Security.layout = {
breadcrumbs: [
{
title: 'Security settings',
title: 'Definições de segurança',
href: edit(),
},
],
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import JSZip from 'jszip';
import { serializeFinancialToolsSnapshot } from '../../resources/js/lib/financial-calculators.ts';
import {
createProjectDossierPrintHtml,
createProjectDossierReport,
@@ -10,6 +11,17 @@ import {
classifyDossierAttachment,
validateDossierAttachments,
} from '../../resources/js/lib/project-dossier-report.ts';
test('executive summary exposes the same financial result and period without replacing missing data by zero', () => {
const draft = { currency:'MZN', financial_tools_snapshot:serializeFinancialToolsSnapshot({budgetPeriod:'Mensal',loan:{principal:'1200',rate:'0',months:'12',rateType:'nominal',periodMonths:'1',gracePeriods:'0',upfrontFee:'0',recurringFee:'0'},breakEven:{fixed:'100.01',price:'30',variable:'20'},funding:{total:'1000',secured:'800'},incomes:[{label:'Receita QA',amount:'1000'}],expenses:[{label:'Despesa QA',amount:'1100'}]}) };
const report = createProjectDossierReport(draft,{selectedSections:new Set(['summary','finance'])});
const summary=report.sections.find(s=>s.id==='summary');
const detail=report.sections.find(s=>s.id==='finance');
assert.equal(summary.rows.find(r=>r.field.startsWith('Saldo operacional')).value,detail.rows.find(r=>r.field==='Saldo operacional estimado').value);
assert.match(summary.rows.find(r=>r.field.startsWith('Saldo operacional')).field,/Mensal/);
const missing=createProjectDossierReport({}, {selectedSections:new Set(['summary'])});
assert.equal(missing.sections[0].rows.some(r=>r.field.startsWith('Saldo operacional')),false);
});
import {
serializeBusinessPlanSnapshot,
forecastBusinessPlan,
+10
View File
@@ -19,6 +19,15 @@ import {
getFiscalPeriodRange,
} from '../../resources/js/lib/simulation-form.ts';
void test('an invalid fiscal operation date has a field error and no substitute period', () => {
for (const date of ['', '2026-99-99', '2026-02-30', '2025-06-15']) {
const draft = { fiscal_period: '2026', fiscal_periodicity: 'one_off', fiscal_scenario_mode: 'approved_parameters', fiscal_reference_date: date };
assert.equal(getFiscalPeriodRange(draft), null);
assert.match(validateSimulationDraft(draft, 'tax').fiscal_reference_date, /data válida/);
}
assert.equal(validateSimulationDraft({ fiscal_period: '2026', fiscal_periodicity: 'one_off', fiscal_scenario_mode: 'approved_parameters', fiscal_reference_date: '2026-06-15' }, 'tax').fiscal_reference_date, undefined);
});
void test('fiscal period range requires and respects exact annual, monthly, quarterly, and one-off dates', () => {
for (const invalid of ['2026-99-99', '2026-02-30', '2026-00-01', '2026-01-00', '']) {
assert.equal(getFiscalPeriodRange({ fiscal_period: '2026', fiscal_periodicity: 'one_off', fiscal_reference_date: invalid }), null);
@@ -336,6 +345,7 @@ void test('approved-parameter fiscal mode does not ask users to enter a rate', (
});
void test('custom planning mode requires user assumptions and validates percentage limits', () => {
assert.deepEqual(validateSimulationDraft({ fiscal_scenario_mode: 'custom_planning' }, 'tax'), {});
const base = {
purpose: 'simulate_only',
fiscal_period: '2026',