fix(portal): identify operator and institutional support contact

This commit is contained in:
2026-11-10 10:41:47 +02:00
parent 377c40a21a
commit 4e61480272
4 changed files with 76 additions and 6 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ return [
// Experimental proposal only; functional approval and operational SLA remain pending.
'opportunity_modular_profiles_qa_enabled' => env('PORTAL_OPPORTUNITY_MODULAR_PROFILES_QA_ENABLED', false),
'opportunity_other_qa_subtypes' => [],
'operator_name' => env('PORTAL_OPERATOR_NAME'),
'operator_name' => env('PORTAL_OPERATOR_NAME') ?: 'Tempo, Lda.',
// Tempo, 19/10/2026: institutional contact; commercial department remains separate.
'institutional_contact_email' => env('PORTAL_INSTITUTIONAL_CONTACT_EMAIL', 'comercial@tempo.co.mz'),
'support' => [
+5 -1
View File
@@ -10,8 +10,9 @@ const navigation = [
export function TempoFooter() {
const { t, language } = useTempoUi();
const { portalInstitutionalContactEmail } = usePage<{
const { portalInstitutionalContactEmail, portalOperator } = usePage<{
portalInstitutionalContactEmail?: string | null;
portalOperator?: string | null;
}>().props;
return (
<footer
@@ -27,6 +28,9 @@ export function TempoFooter() {
>
<PortalProductIdentity />
</Link>
<p className="text-sm text-slate-300">
{t('Operado por')} {portalOperator || 'Tempo, Lda.'}
</p>
<div className="flex items-start gap-4">
<MapPin
className="mt-1 size-[22px] shrink-0 text-[#ed1c24]"
+17 -4
View File
@@ -432,9 +432,10 @@ function Simulator() {
}
export default function Services(p: Props) {
const { flash, auth } = usePage<{
const { flash, auth, portalInstitutionalContactEmail } = usePage<{
flash?: { success?: string };
auth: { user: unknown };
portalInstitutionalContactEmail?: string | null;
}>().props;
const currentUrl = usePage<{ url: string }>().url;
const filter = useForm({
@@ -1053,6 +1054,19 @@ export default function Services(p: Props) {
<>
<section className="space-y-3">
<h2 className="text-xl font-bold">Contactos</h2>
{portalInstitutionalContactEmail &&
portalInstitutionalContactEmail !==
p.contacts?.email && (
<p>
Contacto institucional:{' '}
<a
className="underline"
href={`mailto:${portalInstitutionalContactEmail}`}
>
{portalInstitutionalContactEmail}
</a>
</p>
)}
{p.contacts?.email && (
<p>
Email:{' '}
@@ -1073,9 +1087,8 @@ export default function Services(p: Props) {
)}
{!p.contacts?.email && !p.contacts?.phone && (
<p>
Utilize o formulário para contactar a
equipa. Os contactos directos ainda não
estão publicados.
Para pedidos de apoio ao investimento,
utilize o formulário desta página.
</p>
)}
</section>
@@ -0,0 +1,53 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { compileFunction } from 'node:vm';
import { createRequire } from 'node:module';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import ts from 'typescript';
const require = createRequire(import.meta.url);
const Empty = () => null;
const passthrough = ({ children }) => React.createElement('div', null, children);
function load(file, page) {
const code = ts.transpileModule(fs.readFileSync(new URL(file, import.meta.url), 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, target: ts.ScriptTarget.ES2022 },
}).outputText;
const module = { exports: {} };
const aliases = new Proxy({ default: Empty }, { get: (target, name) => name in target ? target[name] : Empty });
compileFunction(code, ['require', 'module', 'exports'])((name) => {
if (name === 'lucide-react') return aliases;
if (name === '@inertiajs/react') return {
Link: 'a', Head: Empty, usePage: () => page,
useForm: (data) => ({ data, errors: {}, processing: false, setData() {}, post() {}, reset() {} }),
};
if (name === '@/components/tempo-brand') return { useTempoUi: () => ({ t: (text) => text, language: 'pt' }), TempoSocialLinks: Empty };
if (name === '@/pages/investment/shared') return { Frame: passthrough };
if (name.startsWith('@/')) return aliases;
return require(name);
}, module, module.exports);
return module.exports;
}
test('footer identifies the decided operator without implying a public partnership', () => {
const { TempoFooter } = load('../../resources/js/components/tempo-footer.tsx', {
props: { portalOperator: 'Tempo, Lda.', portalInstitutionalContactEmail: 'institution@example.invalid' },
});
const html = renderToStaticMarkup(React.createElement(TempoFooter));
assert.match(html, /Operado por.*Tempo, Lda\./s);
assert.match(html, /mailto:institution@example.invalid/);
assert.doesNotMatch(html, /parceria com a APIEX/);
});
test('support distinguishes the existing institutional contact from dedicated support', () => {
const { default: Services } = load('../../resources/js/pages/portal/services.tsx', {
url: '/apoio', props: { auth: { user: null }, portalInstitutionalContactEmail: 'institution@example.invalid' },
});
const html = renderToStaticMarkup(React.createElement(Services, { section: 'support', contacts: {}, countries: [] }));
assert.match(html, /Contacto institucional/);
assert.match(html, /mailto:institution@example.invalid/);
assert.doesNotMatch(html, /Os contactos directos ainda não/);
assert.match(html, /utilize o formulário desta página/);
});