feat: group duplicate field signals
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s

This commit is contained in:
ik
2026-09-07 18:45:16 +07:00
parent 02b67741f8
commit 0f9c255596
4 changed files with 38 additions and 3 deletions
+34
View File
@@ -0,0 +1,34 @@
---
import SourceBadge from "./SourceBadge.astro";
import { ago, kg, type PublicObservation } from "../lib/api";
const { signals } = Astro.props as { signals: PublicObservation[] };
const groups = [...signals.reduce((map, signal) => {
const key = [signal.fish_name, signal.waterbody_name, signal.x, signal.y, signal.weight_g].join("|").toLocaleLowerCase("ru");
const current = map.get(key);
if (!current) map.set(key, { ...signal, observations: [signal] });
else {
current.observations.push(signal);
current.last_seen_at = new Date(signal.last_seen_at) > new Date(current.last_seen_at) ? signal.last_seen_at : current.last_seen_at;
current.missing_fields = [...new Set([...current.missing_fields, ...signal.missing_fields])];
if (signal.quality === "incomplete") current.quality = "incomplete";
}
return map;
}, new Map<string, PublicObservation & { observations: PublicObservation[] }>()).values()];
---
<section class="signal-section content-grid" aria-labelledby="signals-title">
<div class="signal-heading"><div><span class="overline">Сырые данные источников</span><h2 id="signals-title">Полевые сигналы</h2><p>Показываем сразу, даже если часть полей отсутствует. Эти карточки не участвуют в расчёте активности до полного подтверждения.</p></div><span class="signal-count">{signals.length} сигналов · {groups.length} сюжетов</span></div>
<div class="signal-grid">{groups.map(signal => {
const provenance = [...new Map(signal.observations.map(item => [`${item.source_system}|${item.source_url}`, item])).values()];
return <article class="signal-card" data-quality={signal.quality}>
<div class="signal-card__top"><div class="source-strip">{provenance.map(item => <SourceBadge source={item.source_system} href={item.source_url}/>)}</div><span class="quality-chip"><i>{signal.quality === "incomplete" ? "!" : "?"}</i> {signal.quality === "incomplete" ? "Неполные данные" : "Ждёт проверки"}</span></div>
<h3>{signal.fish_name}</h3><p>{signal.waterbody_name}</p>
<dl><div><dt>Точка</dt><dd>{signal.x == null || signal.y == null ? "не указана" : `${signal.x}:${signal.y}`}</dd></div><div><dt>Вес</dt><dd>{signal.weight_g == null ? "не указан" : kg(signal.weight_g)}</dd></div><div><dt>Получено</dt><dd>{ago(signal.last_seen_at)}</dd></div></dl>
{signal.observations.length > 1 && <p class="repeat-note">Совпали {signal.observations.length} наблюдения · все источники сохранены</p>}
<p class="missing-note"><span>{signal.missing_fields.length ? "Не хватает:" : "Статус:"}</span> {signal.missing_fields.join(", ") || "проверяется соответствие справочнику"}</p>
</article>;
})}</div>
</section>
<style>
.repeat-note{position:relative;z-index:1;margin:-8px 0 10px!important;padding:6px 9px;border-left:2px solid #6b8d74;color:#496357!important;font-size:10px!important}
.signal-card__top .source-strip{margin:0}
</style>