feat(tgu-ui): #50 слой поддержки решений по слотам + выбор полосами
Поверх ручного бронирования — информационный слой (достаточность судит оператор). M1 покрытие: coverage.ts (растровое Monte-Carlo % покрытия заявки, чистые функции + coverage.test.ts), CoveragePanel (живой % + бар), слой coverage на карте (drawCoverage, union контуров, клип по заявке). M2 эргономика: сортировка списка по вкладу в покрытие (жадный маргинальный прирост), по крену; колонка «Вклад» (+N%). M3 конфликты: бейдж держателя BUSY-слота (заявка + важность) из holders в SlotDTO. Выбор полосами (TASK_slot-strip-selection it.1): slotStrips.ts (groupStrips — same-roll цепочки по-КА, ε=0.01 с якорным креном, тотальный порядок) + slotStrips.test.ts; toggleStripSelection в slotSelection.ts; чип «полоса ×N» в строке, рельс по StripId в сортировке «время», подсветка полосы при наведении. Физику выполнимости не дублируем на фронте — это backend validate (it.2). Смежный баг: anchorIndexRef (якорь Shift-выбора) сбрасывается при смене видимого набора (фильтры/окно суток), не только сортировки.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import type { CoverageResult } from "./coverage";
|
||||
|
||||
type CoveragePanelProps = {
|
||||
/** Покрытие только забронированным набором. null — покрытие недоступно (нет геометрии/0 сэмплов). */
|
||||
booked: CoverageResult | null;
|
||||
/** Покрытие union(забронированное ∪ выбранное галочками). */
|
||||
withSelection: CoverageResult | null;
|
||||
/** Есть ли выбор сверх брони (показывать второй сегмент «с выбором»). */
|
||||
hasSelection: boolean;
|
||||
/** Геометрия заявки пригодна для расчёта (false → «недоступно»). */
|
||||
available: boolean;
|
||||
/** Тумблер следа покрытия на карте (D4 — локальный, вкл. по умолчанию). */
|
||||
coverageOn: boolean;
|
||||
onToggleCoverage: (on: boolean) => void;
|
||||
};
|
||||
|
||||
// Панель покрытия над списком слотов (D-D1): «забронировано NN% · с выбором MM%» + двухсегментный
|
||||
// бар + явные состояния. Цвет = цвет слоя coverage на карте (violet) — единый язык «покрыто».
|
||||
export function CoveragePanel({
|
||||
booked,
|
||||
withSelection,
|
||||
hasSelection,
|
||||
available,
|
||||
coverageOn,
|
||||
onToggleCoverage,
|
||||
}: CoveragePanelProps) {
|
||||
const bookedPct = booked?.percent ?? 0;
|
||||
const withPct = withSelection?.percent ?? bookedPct;
|
||||
// Прирост от выбора (второй сегмент) — поверх забронированного.
|
||||
const gainPct = Math.max(0, withPct - bookedPct);
|
||||
const lowPrecision = !!(booked?.lowPrecision || withSelection?.lowPrecision);
|
||||
|
||||
return (
|
||||
<div className="tgu-coverage">
|
||||
<div className="tgu-coverage__head">
|
||||
<span className="tgu-coverage__title">Покрытие заявки</span>
|
||||
<label className="tgu-coverage__toggle" title="Показывать след покрытия на карте">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={coverageOn}
|
||||
onChange={(event) => onToggleCoverage(event.target.checked)}
|
||||
/>
|
||||
След на карте
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!available ? (
|
||||
<div className="tgu-coverage__unavailable">Покрытие недоступно — у заявки нет геометрии.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="tgu-coverage__bar" role="presentation">
|
||||
<div className="tgu-coverage__seg tgu-coverage__seg--booked" style={{ width: `${bookedPct}%` }} />
|
||||
{hasSelection && gainPct > 0 && (
|
||||
<div className="tgu-coverage__seg tgu-coverage__seg--sel" style={{ width: `${gainPct}%` }} />
|
||||
)}
|
||||
</div>
|
||||
<div className="tgu-coverage__legend">
|
||||
<span className="tgu-coverage__booked">забронировано {bookedPct}%</span>
|
||||
{hasSelection && (
|
||||
<span className="tgu-coverage__sel"> · с выбором {withPct}%</span>
|
||||
)}
|
||||
{lowPrecision && (
|
||||
<span className="tgu-coverage__low" title="Заявка слишком мала для надёжной оценки">
|
||||
{" "}
|
||||
· низкая точность
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user