fix: validate simulator dates and reconcile QA calculations reports and language
This commit is contained in:
+1
-1
@@ -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.
|
@@ -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."
|
||||
}
|
||||
@@ -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.',
|
||||
];
|
||||
@@ -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'],
|
||||
];
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>`);
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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' &&
|
||||
[
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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.',
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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.',
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user