Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions apps/web/app/components/RiskIndicators.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { ContractDetail } from '@sigma/api-contract';
import { Link } from 'react-router';
import { pct } from '@sigma/shared';
import { evaluateRiskIndicators } from '../lib/riskLogic';

// The per-contract risk signals. Copy is deliberately non-accusatory (#219): each item states the
// structural fact and frames it as a signal that warrants a look, never as proven wrongdoing. The
// note below repeats the disclaimer next to the flags and links to the full methodology, so the
// caveat travels with the flags wherever they appear (issue #219 co-requisite of the risk numbers).
// Labels mirror methodology §10 and the homepage FLAG_LABELS for a consistent vocabulary.
export function RiskIndicators({ contract }: { contract: ContractDetail }) {
const flags = evaluateRiskIndicators(contract);

Expand All @@ -27,45 +33,53 @@ export function RiskIndicators({ contract }: { contract: ContractDetail }) {
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
Индикатори за риск
Сигнали за риск
</h2>
<ul className="risk-list">
{flags.map((flag, i) => {
Comment thread
todorkolev marked this conversation as resolved.
if (flag.type === 'eu_no_competition') {
return (
<li key={i}>
<strong>Риск при Еврофондове:</strong> Проектът е финансиран с европейски средства,
но е възложен без реална конкуренция (повишен риск според стандартите на ОЛАФ).
<strong>Липса на конкуренция (със средства от ЕС):</strong> проектът е финансиран с
европейски средства и е допусната само една оферта. Правилата за конкуренция при
еврофондове са по-строги, затова случаят заслужава преглед.
</li>
);
}
if (flag.type === 'no_competition') {
return (
<li key={i}>
<strong>Липса на конкуренция:</strong> Този договор е сключен след допускане на само
<strong>Липса на конкуренция:</strong> договорът е сключен след допускане на само
една оферта.
</li>
);
}
if (flag.type === 'high_markup') {
return (
<li key={i}>
<strong>Значително оскъпяване:</strong> Стойността на договора е нараснала с{' '}
{pct(flag.deltaPct!)} спрямо първоначално обявената — чрез допълнителни анекси.
<strong>Ръст на стойността чрез анекси:</strong> текущата стойност е нараснала с{' '}
{pct(flag.deltaPct!)} спрямо стойността при подписване.
</li>
);
}
if (flag.type === 'anomalies') {
return (
<li key={i}>
<strong>Аномалии в данните:</strong> Стойността на договора (или някои от датите) е
извън обичайния диапазон и подлежи на допълнителна проверка.
<strong>Стойностна или времева аномалия:</strong> стойността е с непотвърдена
достоверност или договорът е подписан след датата си на публикуване.
</li>
);
}
return null;
})}
</ul>
<p className="risk-note small muted">
Сигналите са ориентири за преглед, не присъда — отбелязват договор, който заслужава
внимание, а не доказано нарушение.{' '}
<Link to="/methodology#flagged">Как четем сигналите →</Link>
Comment thread
todorkolev marked this conversation as resolved.
{' · '}
<Link to="/methodology#contact">Смятате сигнал за грешен? →</Link>
</p>
</div>
);
}
22 changes: 20 additions & 2 deletions apps/web/app/routes/contract.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Link } from 'react-router';
import { count, longDate, money, moneyBare, plural, signedMoney, signedPct } from '@sigma/shared';
import {
count,
isNaturalPersonProfileName,
longDate,
money,
moneyBare,
plural,
signedMoney,
signedPct,
} from '@sigma/shared';
import { contractIdFromSlug, contractSlug, getContract, getDb } from '@sigma/db';
import type { CohortBand, ContractDetail } from '@sigma/api-contract';
import type { Route } from './+types/contract';
Expand Down Expand Up @@ -71,14 +80,23 @@ function AnnexDescription({ text }: { text: string | null }) {

export function meta({ data, params, matches }: Route.MetaArgs) {
const c = data?.contract;
return seoMeta({
const tags = seoMeta({
matches,
path: `/contracts/${contractSlug(contractIdFromSlug(params.id))}`,
title: `${c?.subject ?? 'Договор'} — СИГМА`,
description: c
? `Договор по УНП ${c.unp} между ${c.authority.name} и ${c.bidder.displayName}.`
: '',
});
// GDPR/ЗЗЛД (#219 review): when the bidder is a sole-trader (ЕТ) / natural person, keep this contract
// page — which carries the „Сигнали за риск" box — out of search indexes, mirroring the noindex on
// sole-trader company profiles (company.tsx) and their exclusion from the sitemap. The contract stays
// fully public on the site; only search-engine amplification of a named individual + risk label is
// avoided.
Comment thread
todorkolev marked this conversation as resolved.
if (c && isNaturalPersonProfileName(c.bidder.displayName)) {
Comment thread
todorkolev marked this conversation as resolved.
tags.push({ name: 'robots', content: 'noindex' });
}
return tags;
}

export function headers() {
Expand Down
63 changes: 60 additions & 3 deletions apps/web/app/routes/methodology.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const TOC = [
['identity', 'Имена, ЕИК, УНП'],
['gaps', 'Известни празнини в полетата'],
['export', 'Сваляне и достъп до данните'],
['flagged', 'Сигнали за риск'],
['contact', 'Поправки и обратна връзка'],
];

Expand Down Expand Up @@ -143,8 +144,10 @@ export default function Methodology({ loaderData }: Route.ComponentProps) {
</li>
</ul>
<p>
СИГМА е изцяло само за четене: не въвежда нови данни, не оценява процедурите и не
маркира фирми като рискови.
СИГМА е изцяло само за четене: не въвежда нови данни и не оценява процедурите. Тя
отбелязва <em>договори</em> със структурни сигнали за преглед (виж{' '}
<a href="#flagged">раздел 10</a>), но не обявява фирми или възложители за нарушители
Comment thread
todorkolev marked this conversation as resolved.
— сигналът е ориентир, не присъда.
</p>
<Callout title="За какво е подходяща тази версия">
<p className="m-0">
Expand Down Expand Up @@ -530,8 +533,62 @@ export default function Methodology({ loaderData }: Route.ComponentProps) {
<p>Засега не предлагаме публично API в реално време.</p>
</section>

<section className="section" aria-labelledby="flagged">
<h2 id="flagged">10. Сигнали за риск</h2>
Comment thread
todorkolev marked this conversation as resolved.
<p>
На страницата на всеки договор СИГМА отбелязва{' '}
<strong>структурни сигнали за риск</strong>, когато поне един признак заслужава
преглед. Сигналите са <em>ориентири</em>, не присъда: те не твърдят нарушение, а
насочват към договори, които си струва да се погледнат по-внимателно.
</p>
<p>Отбелязваме четири признака:</p>
Comment thread
todorkolev marked this conversation as resolved.
<dl className="glossary">
<dt>Липса на конкуренция</dt>
<dd>
<p>
Допусната е една-единствена оферта (приети оферти = 1), без финансиране от ЕС.
</p>
<span className="src">→ приети оферти = 1</span>
</dd>
<dt>Липса на конкуренция (със средства от ЕС)</dt>
<dd>
<p>
Същият признак, но при финансиране от ЕС, където правилата за конкуренция са
по-строги — затова го отделяме.
</p>
<span className="src">→ приети оферти = 1 · финансиране от ЕС</span>
</dd>
<dt>Ръст на стойността чрез анекси</dt>
<dd>
<p>
Текущата стойност е нараснала с над 20% спрямо стойността при подписване —
индикатор за оскъпяване след сключването.
</p>
<span className="src">→ текуща стойност &gt; 1.2 × стойност при подписване</span>
</dd>
<dt>Стойностна или времева аномалия</dt>
<dd>
<p>
Стойност с непотвърдена достоверност (виж речника) или договор, подписан повече
от два дни след датата си на публикуване. Двудневният допуск поема обичайното
забавяне при въвеждане; отвъд него редът на датите остава необяснен от
източника.
</p>
<span className="src">→ value_flag / date_flag</span>
</dd>
</dl>
<p>
<strong>Ограничения.</strong> Сигналите стъпват на публични данни и структурни
признаци, не на разследване. Възможни са грешки или празнини в източника (виж{' '}
<a href="#gaps">раздел 8</a>), затова един сигнал може да е фалшив — например
законна процедура с една оферта по обективни причини. Сигнал не е констатация за
нарушение и не оценява поведението на конкретно лице. Ако смятате конкретен сигнал
за неточен, пишете ни (<a href="#contact">раздел 11</a>).
</p>
</section>

<section className="section" aria-labelledby="contact">
<h2 id="contact">10. Поправки и обратна връзка</h2>
<h2 id="contact">11. Поправки и обратна връзка</h2>
<p>
Грешките поправяме ръчно при сигнал — двойни записи за институция/компания
(изпратете двата ЕИК/линка) или сума, която не отговаря на оригиналния документ
Expand Down
9 changes: 9 additions & 0 deletions apps/web/app/styles/pages.css
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@
.risk-list li:last-child {
margin-bottom: 0;
}
/* Non-accusatory disclaimer that travels with the per-contract flags (#219). */
.risk-note {
margin: var(--s-4) 0 0;
padding-top: var(--s-3);
border-top: 1px solid var(--warning);
}
.risk-note a {
color: var(--warning-strong);
}

/* Value-history strip + current lot row */
.value-history {
Expand Down