fix(opportunities): distinguish missing verification and improve metadata
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
export function opportunityDescription(record: {
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
}): string {
|
||||
const summary = record.summary
|
||||
?.replace(/<[^>]*>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
const text =
|
||||
summary || `Consulte a ficha de ${record.title} no InvesteMoz.`;
|
||||
return Array.from(
|
||||
text
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim(),
|
||||
)
|
||||
.slice(0, 160)
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function recordedVerificationDate(value?: string | null): string {
|
||||
if (!value?.trim()) return 'Não registada';
|
||||
const parts = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
||||
if (!parts) return 'Data não interpretável';
|
||||
const [, year, month, day] = parts.map(Number);
|
||||
const calendar = new Date(Date.UTC(year, month - 1, day));
|
||||
if (
|
||||
calendar.getUTCFullYear() !== year ||
|
||||
calendar.getUTCMonth() !== month - 1 ||
|
||||
calendar.getUTCDate() !== day
|
||||
)
|
||||
return 'Data não interpretável';
|
||||
if (value.length > 10 && !/(Z|[+-]\d{2}:\d{2})$/.test(value))
|
||||
return 'Fuso horário não registado';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return 'Data não interpretável';
|
||||
return new Intl.DateTimeFormat('pt-MZ', {
|
||||
dateStyle: 'medium',
|
||||
timeZone: 'Africa/Maputo',
|
||||
}).format(date);
|
||||
}
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
} from '@/lib/search-territory';
|
||||
import type { EducationalArticle } from '@/components/educational-article-card';
|
||||
import { ClassificationSummary } from '@/components/opportunity-classification';
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
import { Head, Link, router, usePage } from '@inertiajs/react';
|
||||
import {
|
||||
opportunityDescription,
|
||||
recordedVerificationDate,
|
||||
} from '@/lib/opportunity-information';
|
||||
import { useState } from 'react';
|
||||
import { downloadPublicOpportunityOffline } from '@/lib/public-opportunity-offline';
|
||||
import InvestmentMapPanel from '@/components/investment-map-panel';
|
||||
@@ -46,6 +50,23 @@ export default function Detail({
|
||||
const [offlineMessage, setOfflineMessage] = useState('');
|
||||
return (
|
||||
<Frame title={r.title}>
|
||||
<Head>
|
||||
<meta
|
||||
head-key="description"
|
||||
name="description"
|
||||
content={opportunityDescription(r)}
|
||||
/>
|
||||
<meta
|
||||
head-key="og:title"
|
||||
property="og:title"
|
||||
content={r.title}
|
||||
/>
|
||||
<meta
|
||||
head-key="og:description"
|
||||
property="og:description"
|
||||
content={opportunityDescription(r)}
|
||||
/>
|
||||
</Head>
|
||||
{r.maturity_label === 'Em preparação' && (
|
||||
<p className="rounded border border-amber-700 bg-amber-50 p-4">
|
||||
<strong>Em preparação.</strong> Consulte a oportunidade e
|
||||
@@ -273,6 +294,10 @@ export default function Detail({
|
||||
<dl className={card + ' space-y-4'}>
|
||||
{[
|
||||
['Promotor', r.promoter_name],
|
||||
[
|
||||
'Última verificação registada',
|
||||
recordedVerificationDate(r.last_validated_at),
|
||||
],
|
||||
['Investimento', money(r)],
|
||||
['Maturidade', r.maturity_label],
|
||||
[
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
opportunityDescription,
|
||||
recordedVerificationDate,
|
||||
} from '../../resources/js/lib/opportunity-information.ts';
|
||||
|
||||
test('metadata uses the actual summary, normalises markup and never invents investment terms', () => {
|
||||
assert.equal(opportunityDescription({ title: 'Projecto', summary: '<b>Resumo</b>\n declarado' }), 'Resumo declarado');
|
||||
const fallback = opportunityDescription({ title: 'Avicultura', summary: null });
|
||||
assert.match(fallback, /Avicultura/);
|
||||
assert.match(opportunityDescription({ title: 'Projecto', summary: '<b> </b>' }), /Projecto/);
|
||||
assert.doesNotMatch(fallback, /A negociar|USD|aprovad|benefício/i);
|
||||
assert.equal(Array.from(opportunityDescription({ title: 'Projecto', summary: 'a'.repeat(300) })).length, 160);
|
||||
});
|
||||
|
||||
test('unknown and invalid verification dates remain distinct from a recorded verification', () => {
|
||||
assert.equal(recordedVerificationDate(null), 'Não registada');
|
||||
assert.equal(recordedVerificationDate('invalid'), 'Data não interpretável');
|
||||
assert.equal(recordedVerificationDate('2026-02-30T12:00:00Z'), 'Data não interpretável');
|
||||
assert.equal(recordedVerificationDate('2026-11-10 12:00:00'), 'Fuso horário não registado');
|
||||
assert.match(recordedVerificationDate('2026-11-10T12:00:00Z'), /2026/);
|
||||
});
|
||||
Reference in New Issue
Block a user