feat: preserve grounded sector ideas through private investment dossiers in QA
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import type { SectorAnalysis } from './sector-content-analysis';
|
||||
import { Link } from '@inertiajs/react';
|
||||
|
||||
export type EducationalArticle = {
|
||||
sector_analysis?: SectorAnalysis;
|
||||
title: string;
|
||||
href: string;
|
||||
summary: string;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export type SectorIdea = {
|
||||
title: string;
|
||||
start_href: string;
|
||||
location_label: string | null;
|
||||
evidence: { key: string; quote: string; section: string };
|
||||
};
|
||||
export type SectorAnalysis = {
|
||||
ideas: SectorIdea[];
|
||||
region?: string | null;
|
||||
notice: string;
|
||||
source_date: string;
|
||||
};
|
||||
|
||||
/** The same evidence-to-preparation controls on sector and regional articles. */
|
||||
export default function SectorContentAnalysis({
|
||||
data,
|
||||
}: {
|
||||
data: SectorAnalysis;
|
||||
}) {
|
||||
const [selected, setSelected] = useState(0);
|
||||
const idea = data.ideas[selected];
|
||||
if (!idea) return null;
|
||||
return (
|
||||
<section
|
||||
className="space-y-4 rounded-xl border bg-white p-4"
|
||||
aria-label="Ideias fundamentadas neste contexto"
|
||||
>
|
||||
<h2 className="text-xl font-semibold">Do contexto à sua ideia</h2>
|
||||
<p>{data.notice}</p>
|
||||
<label className="grid gap-1">
|
||||
Possibilidade a explorar
|
||||
<select
|
||||
className="min-w-0 rounded border p-3"
|
||||
value={selected}
|
||||
onChange={(e) => setSelected(Number(e.target.value))}
|
||||
>
|
||||
{data.ideas.map((item, index) => (
|
||||
<option key={item.title} value={index}>
|
||||
{item.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<p>
|
||||
<strong>Hipótese de negócio:</strong> {idea.title}
|
||||
</p>
|
||||
<p>
|
||||
{idea.location_label
|
||||
? `Contexto provincial: ${idea.location_label}. O distrito e a implantação serão escolhidos por si.`
|
||||
: 'A localização do investimento fica por escolher. Não é necessário pertencer a uma zona demarcada.'}
|
||||
</p>
|
||||
<details className="rounded border p-3">
|
||||
<summary>Fundamento e origem</summary>
|
||||
{idea.evidence.section && <p>{idea.evidence.section}</p>}
|
||||
<blockquote className="my-3 border-l-2 pl-3">
|
||||
{idea.evidence.quote}
|
||||
</blockquote>
|
||||
<p>
|
||||
Edição consultada: {data.source_date}. Afirmação atribuída à
|
||||
fonte; não constitui previsão do seu negócio.
|
||||
</p>
|
||||
</details>
|
||||
<ol className="list-decimal space-y-2 pl-5">
|
||||
<li>Escolha ou confirme a localização conhecida.</li>
|
||||
<li>
|
||||
Compare actividades similares e complementares; confira a
|
||||
cobertura dos registos.
|
||||
</li>
|
||||
<li>Prepare custos, receitas e cenários no mesmo dossier.</li>
|
||||
</ol>
|
||||
<nav
|
||||
className="flex flex-wrap gap-3"
|
||||
aria-label="Continuar esta ideia"
|
||||
>
|
||||
<Link
|
||||
className="rounded bg-emerald-800 p-3 text-white"
|
||||
href={idea.start_href}
|
||||
>
|
||||
Preparar investimento a partir deste contexto
|
||||
</Link>
|
||||
<Link
|
||||
className="rounded border p-3"
|
||||
href={`${idea.start_href}&analysis_action=compare`}
|
||||
>
|
||||
Comparar com a oferta existente
|
||||
</Link>
|
||||
</nav>
|
||||
<p className="text-sm">
|
||||
A comparação é feita no dossier privado, com os acessos
|
||||
autorizados. Zero anúncios ou registos não prova ausência de
|
||||
empresas, concorrência ou potencial.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import SectorResourceReference, { type SectorResourceData } from './sector-resource-reference';
|
||||
import SectorContentAnalysis from './sector-content-analysis';
|
||||
import SectorResourceReference, {
|
||||
type SectorResourceData,
|
||||
} from './sector-resource-reference';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import EducationalArticleCard, {
|
||||
type EducationalArticle,
|
||||
@@ -45,7 +48,18 @@ export default function SectorEntry({
|
||||
modalidade adequada. Cada oportunidade tem promotor, objetivo e
|
||||
dados próprios.
|
||||
</p>
|
||||
{data.resource_reference && <SectorResourceReference data={data.resource_reference} />}
|
||||
{data.resource_reference && (
|
||||
<SectorResourceReference data={data.resource_reference} />
|
||||
)}
|
||||
{data.articles.map(
|
||||
(article) =>
|
||||
article.sector_analysis && (
|
||||
<SectorContentAnalysis
|
||||
key={article.href}
|
||||
data={article.sector_analysis}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<nav aria-label="Ações no setor" className="flex flex-wrap gap-3">
|
||||
{data.start_href ? (
|
||||
<Link
|
||||
|
||||
@@ -35,7 +35,24 @@ export type PreparationReport = {
|
||||
swot: SwotDraft;
|
||||
swot_review_required: boolean;
|
||||
};
|
||||
export type SectorAnalysisSnapshot = {
|
||||
key: string;
|
||||
title: string;
|
||||
quote: string;
|
||||
section: string;
|
||||
source_sha256: string;
|
||||
source_version: number;
|
||||
source_date: string;
|
||||
location_label: string | null;
|
||||
location_precision: string;
|
||||
href: string;
|
||||
};
|
||||
export type InvestorDossierReportInput = {
|
||||
content_sources?: {
|
||||
title: string;
|
||||
source_url: string;
|
||||
analyses?: SectorAnalysisSnapshot[];
|
||||
}[];
|
||||
supply_aggregates?: SupplyAggregate[];
|
||||
case: { id: string; title: string; version: number };
|
||||
context_opportunity: string | null;
|
||||
@@ -332,6 +349,28 @@ export function createInvestorDossierReport(
|
||||
origin = 'Registo guardado neste dossier',
|
||||
classification = 'Preparação do investimento',
|
||||
): DossierReportRow => ({ field, value, origin, classification });
|
||||
const sectorEvidence = (input.content_sources ?? []).flatMap((source) =>
|
||||
(source.analyses ?? []).flatMap((a) => [
|
||||
row(
|
||||
'Hipótese: ' + a.title,
|
||||
a.quote,
|
||||
`${source.title} — ${a.section}; ${source.source_url}; edição ${a.source_date}`,
|
||||
'Hipótese; afirmação atribuída à fonte',
|
||||
),
|
||||
row(
|
||||
'Âmbito territorial',
|
||||
a.location_label ?? 'Âmbito da fonte; implantação por definir',
|
||||
a.href,
|
||||
a.location_precision,
|
||||
),
|
||||
row(
|
||||
'Versão preservada',
|
||||
`${a.source_version}; SHA256 ${a.source_sha256}`,
|
||||
a.href,
|
||||
'Rastreabilidade da evidência',
|
||||
),
|
||||
]),
|
||||
);
|
||||
const finance: DossierReportRow[] = [];
|
||||
for (const result of input.results.filter(
|
||||
(item) =>
|
||||
@@ -656,21 +695,26 @@ export function createInvestorDossierReport(
|
||||
},
|
||||
{
|
||||
id: 'market',
|
||||
title: 'Análise SWOT',
|
||||
title: sectorEvidence.length
|
||||
? 'Mercado, evidências sectoriais e SWOT'
|
||||
: 'Análise SWOT',
|
||||
private: true,
|
||||
paragraphs: swot?.rows.length
|
||||
? [
|
||||
`Objecto: ${swot.object_key}; âmbito: ${swot.scope ?? 'não indicado'}. ${preparation?.swot_review_required ? 'Revisão necessária.' : 'Revisão guardada.'}`,
|
||||
]
|
||||
: ['Análise SWOT ainda não produzida neste âmbito.'],
|
||||
rows: (swot?.rows ?? []).map((item) =>
|
||||
row(
|
||||
`${dimensions[item.dimension]} — ${item.text}`,
|
||||
`${classifications[item.classification]}; data ${item.date ?? 'por confirmar'}`,
|
||||
item.reference || 'Fonte por confirmar',
|
||||
classifications[item.classification],
|
||||
rows: [
|
||||
...(swot?.rows ?? []).map((item) =>
|
||||
row(
|
||||
`${dimensions[item.dimension]} — ${item.text}`,
|
||||
`${classifications[item.classification]}; data ${item.date ?? 'por confirmar'}`,
|
||||
item.reference || 'Fonte por confirmar',
|
||||
classifications[item.classification],
|
||||
),
|
||||
),
|
||||
),
|
||||
...sectorEvidence,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sources',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import SectorContentAnalysis, {
|
||||
type SectorAnalysis,
|
||||
} from '@/components/sector-content-analysis';
|
||||
import { router } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import CatalogueChoiceSelect from '@/components/catalogue-choice-select';
|
||||
@@ -20,6 +23,7 @@ function DemographySelection({
|
||||
filters,
|
||||
territories,
|
||||
sectors,
|
||||
sectorReadings = [],
|
||||
}: {
|
||||
evidence: DemographicAnalysis;
|
||||
filters: {
|
||||
@@ -35,6 +39,11 @@ function DemographySelection({
|
||||
parent_id: string | null;
|
||||
}[];
|
||||
sectors: Record<string, string>;
|
||||
sectorReadings?: {
|
||||
title: string;
|
||||
href: string;
|
||||
analysis: SectorAnalysis;
|
||||
}[];
|
||||
}) {
|
||||
const [selected, setSelected] = useState(filters.territories ?? []);
|
||||
const [province, setProvince] = useState('');
|
||||
@@ -226,6 +235,15 @@ function DemographySelection({
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
{sectorReadings.map((reading) => (
|
||||
<section key={reading.href} className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">{reading.title}</h2>
|
||||
<a href={reading.href} className="underline">
|
||||
Consultar contexto provincial completo
|
||||
</a>
|
||||
<SectorContentAnalysis data={reading.analysis} />
|
||||
</section>
|
||||
))}
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export type InvestmentContext = {
|
||||
export default function InvestmentContexts({
|
||||
contexts,
|
||||
contentSource,
|
||||
sectorIdea,
|
||||
analysisAction,
|
||||
sectorSource,
|
||||
resourceSource,
|
||||
zoneSource,
|
||||
@@ -30,6 +32,15 @@ export default function InvestmentContexts({
|
||||
companyComparisonAnswers?: CompanyAnswers | null;
|
||||
guideSelection?: GuideSelection | null;
|
||||
contentSource?: LegacyArticle;
|
||||
sectorIdea?: {
|
||||
title: string;
|
||||
quote: string;
|
||||
location_label: string | null;
|
||||
evidence_key: string;
|
||||
source_sha256: string;
|
||||
source_version: number;
|
||||
} | null;
|
||||
analysisAction?: 'prepare' | 'compare';
|
||||
zoneSource?: {
|
||||
zone_id: string;
|
||||
consulted_public_version: number;
|
||||
@@ -44,10 +55,14 @@ export default function InvestmentContexts({
|
||||
zone_id: zoneSource?.zone_id,
|
||||
consulted_public_version: zoneSource?.consulted_public_version,
|
||||
confirm: !!zoneSource,
|
||||
title: '',
|
||||
title: sectorIdea?.title ?? '',
|
||||
client_key: key,
|
||||
reuse_consent: false,
|
||||
content_resource_id: contentSource?.id,
|
||||
evidence: sectorIdea?.evidence_key,
|
||||
source_sha256: sectorIdea?.source_sha256,
|
||||
source_version: sectorIdea?.source_version,
|
||||
idea: sectorIdea?.title,
|
||||
sector_slug: sectorSource?.slug,
|
||||
sector_reference: resourceSource
|
||||
? {
|
||||
@@ -101,9 +116,15 @@ export default function InvestmentContexts({
|
||||
{resourceSource && (
|
||||
<section className="rounded border p-4">
|
||||
<h2>{resourceSource.title}</h2>
|
||||
<p>{resourceSource.location_label ?? resourceSource.locations_text}</p>
|
||||
<p>
|
||||
{resourceSource.location_label ??
|
||||
resourceSource.locations_text}
|
||||
</p>
|
||||
<p>{resourceSource.value_chain}</p>
|
||||
<p>Esta referência será guardada apenas no dossier que escolher.</p>
|
||||
<p>
|
||||
Esta referência será guardada apenas no dossier que
|
||||
escolher.
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
{contentSource && (
|
||||
@@ -115,6 +136,25 @@ export default function InvestmentContexts({
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
{sectorIdea && (
|
||||
<section className="space-y-2 rounded border p-4">
|
||||
<h2>Hipótese a preparar: {sectorIdea.title}</h2>
|
||||
<p>
|
||||
{sectorIdea.location_label ??
|
||||
'Localização por escolher'}
|
||||
. Confirme a implantação no passo seguinte.
|
||||
</p>
|
||||
<details>
|
||||
<summary>Fundamento seleccionado</summary>
|
||||
<blockquote>{sectorIdea.quote}</blockquote>
|
||||
</details>
|
||||
<p>
|
||||
{analysisAction === 'compare'
|
||||
? 'Continue no seu dossier, guarde a localização e consulte a análise de mercado e oferta existente.'
|
||||
: 'A ideia e a fonte ficam no mesmo dossier. Complete apenas os dados em falta.'}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
<p>
|
||||
Prepare cada investimento no seu dossier. Pode trabalhar em
|
||||
várias etapas em paralelo.
|
||||
@@ -174,7 +214,13 @@ export default function InvestmentContexts({
|
||||
{
|
||||
version: item.version,
|
||||
sector_slug: sectorSource.slug,
|
||||
...(resourceSource ? { sector_reference: form.data.sector_reference } : {}),
|
||||
...(resourceSource
|
||||
? {
|
||||
sector_reference:
|
||||
form.data
|
||||
.sector_reference,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -192,6 +238,12 @@ export default function InvestmentContexts({
|
||||
{
|
||||
version: item.version,
|
||||
resource_id: contentSource.id,
|
||||
evidence: sectorIdea?.evidence_key,
|
||||
source_sha256:
|
||||
sectorIdea?.source_sha256,
|
||||
source_version:
|
||||
sectorIdea?.source_version,
|
||||
idea: sectorIdea?.title,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SectorAnalysisSnapshot } from '@/lib/investor-dossier-report';
|
||||
import type { SectorResourceSelection } from '@/components/sector-resource-reference';
|
||||
import { InvestorDossierReports } from '@/components/investor-dossier-reports';
|
||||
import type { InvestorDossierReportInput } from '@/lib/investor-dossier-report';
|
||||
@@ -120,6 +121,8 @@ type Path = {
|
||||
tools: Record<string, string | null>;
|
||||
results: PathResult[];
|
||||
content_sources: {
|
||||
analyses?: SectorAnalysisSnapshot[];
|
||||
analysis_review_required?: boolean;
|
||||
resource_id: number;
|
||||
title: string;
|
||||
return_href: string;
|
||||
@@ -375,7 +378,9 @@ function Stage({ step, path }: { step: Step; path: Path }) {
|
||||
}));
|
||||
form.post(
|
||||
`/investimentos/${path.case.id}/etapas/${step.code}`,
|
||||
{ preserveScroll: true },
|
||||
{
|
||||
preserveScroll: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -851,6 +856,44 @@ export default function InvestmentPath(path: Path) {
|
||||
>
|
||||
Exemplar e origem
|
||||
</Link>
|
||||
{source.analysis_review_required && (
|
||||
<p role="status">
|
||||
A fonte foi alterada ou deixou de estar
|
||||
disponível. Reveja a hipótese; o trecho
|
||||
guardado foi preservado.
|
||||
</p>
|
||||
)}
|
||||
{(source.analyses ?? []).map((analysis) => (
|
||||
<details
|
||||
className="my-2 rounded border p-3"
|
||||
key={analysis.key}
|
||||
>
|
||||
<summary>
|
||||
Hipótese guardada: {analysis.title}
|
||||
</summary>
|
||||
<p>
|
||||
{analysis.location_label ??
|
||||
'Implantação por definir'}
|
||||
. Contexto da fonte, sem atribuição
|
||||
distrital automática.
|
||||
</p>
|
||||
<blockquote>
|
||||
{analysis.quote}
|
||||
</blockquote>
|
||||
<p>
|
||||
Edição: {analysis.source_date}.
|
||||
Preserve este fundamento e reveja-o
|
||||
se a fonte mudar.
|
||||
</p>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${path.case.id}/contexto/editar`}
|
||||
>
|
||||
Continuar a preparação no mesmo
|
||||
dossier
|
||||
</Link>
|
||||
</details>
|
||||
))}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -910,7 +953,9 @@ export default function InvestmentPath(path: Path) {
|
||||
}));
|
||||
facts.post(
|
||||
`/investimentos/${path.case.id}/aplicabilidade`,
|
||||
{ preserveScroll: true },
|
||||
{
|
||||
preserveScroll: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -1252,7 +1297,9 @@ export default function InvestmentPath(path: Path) {
|
||||
}));
|
||||
task.post(
|
||||
`/investimentos/${path.case.id}/tarefas`,
|
||||
{ preserveScroll: true },
|
||||
{
|
||||
preserveScroll: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import SectorContentAnalysis, {
|
||||
type SectorAnalysis,
|
||||
} from '@/components/sector-content-analysis';
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import EducationalArticleCard, {
|
||||
type EducationalArticle,
|
||||
} from '@/components/educational-article-card';
|
||||
|
||||
export type LegacyArticle = {
|
||||
sector_analysis?: SectorAnalysis;
|
||||
summary: string;
|
||||
opportunities_href?: string | null;
|
||||
opportunities_label?: string | null;
|
||||
@@ -72,6 +76,9 @@ export default function LegacySector({
|
||||
.legacy-article ol { list-style: decimal; padding-inline-start: 1.6em; }
|
||||
.legacy-article a { text-decoration: underline; }
|
||||
`}</style>
|
||||
{article.sector_analysis && (
|
||||
<SectorContentAnalysis data={article.sector_analysis} />
|
||||
)}
|
||||
<nav
|
||||
className="flex flex-wrap gap-4"
|
||||
aria-label={article.group.label}
|
||||
|
||||
Reference in New Issue
Block a user