chore: сохранение текущих изменений (deploy local-bundle/local-jar, request-service, slots UI markedObjectIds)

Безопасный коммит незакоммиченной работы перед реализацией бронирования слотов.
Вне коммита оставлены: package-lock.json (пустой, мусор), requests.json (scratch-данные).
This commit is contained in:
Дмитрий Соловьев
2026-06-11 14:00:12 +03:00
parent 6689351ef0
commit 345529cde0
36 changed files with 1674 additions and 37 deletions
@@ -67,7 +67,10 @@ data class CreateRequestDto(
*/
@field:DecimalMin(value = "0", inclusive = false)
@field:DecimalMax("100")
val coverageRequiredPercent: Double = 100.0,
// Nullable с дефолтом: под Jackson 3 (MVC) FAIL_ON_NULL_FOR_PRIMITIVES включён по умолчанию,
// и явный null от клиента падал бы на примитиве Double. Опциональное поле с дефолтом трактуем
// null как «взять дефолт» — при маппинге в сущность коалесим к 100.0.
val coverageRequiredPercent: Double? = 100.0,
/** Параметры оптической съемки. */
@field:Valid
@@ -81,7 +81,7 @@ class RequestService(
remainingArea = geometryArea,
// Порог завершения по покрытию приходит из API (по умолчанию 100%); это прогресс съёмки,
// в отличие от importance — важности заявки для карты приоритетов ячеек.
coverageRequiredPercent = request.coverageRequiredPercent,
coverageRequiredPercent = request.coverageRequiredPercent ?: 100.0,
importance = importance,
beginDateTime = request.beginDateTime,
endDateTime = request.endDateTime,
@@ -112,6 +112,23 @@ class RequestServiceTest {
assertEquals(80.0, persistedRequests.single().coverageRequiredPercent)
}
@Test
fun `create request coalesces null coverage required percent to default`() {
val persistedRequests = mutableListOf<RequestEntity>()
doReturn(false)
.`when`(requestRepository)
.existsById(REQUEST_ID)
doAnswer { invocation ->
persistedRequests += invocation.getArgument<RequestEntity>(0)
null
}.`when`(entityManager).persist(any(RequestEntity::class.java))
// Явный null (клиент прислал coverageRequiredPercent: null) трактуем как «взять дефолт».
service.createRequest(createRequestDto(coverageRequiredPercent = null))
assertEquals(100.0, persistedRequests.single().coverageRequiredPercent)
}
@Test
fun `get request by id returns name and importance`() {
val request = existingRequest()
@@ -296,7 +313,7 @@ class RequestServiceTest {
verify(requestCellRepository).deleteByRequestId(REQUEST_ID)
}
private fun createRequestDto(coverageRequiredPercent: Double = 100.0): CreateRequestDto {
private fun createRequestDto(coverageRequiredPercent: Double? = 100.0): CreateRequestDto {
return CreateRequestDto(
id = REQUEST_ID,
name = REQUEST_NAME,
@@ -0,0 +1,9 @@
ARG RUNTIME_IMAGE=repo.nstart.cloud/bellsoft/liberica-openjre-alpine:21.0.5
FROM ${RUNTIME_IMAGE}
ENV JAVA_OPTS=""
ADD ./build/libs/*.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
@@ -24,6 +24,8 @@ type Tgu2DMapViewProps = {
selectedRequestPolygonId?: string;
/** Внешняя подсветка объекта (контур съёмки или линия сброса) по его id — «как будто кликнули на карте». */
selectedObjectId?: string;
/** Отмеченные объекты (постоянная подсветка независимо от выбора) — напр. слоты, отобранные под бронь. */
markedObjectIds?: ReadonlySet<string>;
/** Точка, на которую центрировать карту; перецентрирование срабатывает при смене focusToken. */
focusPoint?: GeoPoint;
focusToken?: number;
@@ -84,6 +86,7 @@ export function Tgu2DMapView({
hoveredRequestPolygonId,
selectedRequestPolygonId,
selectedObjectId,
markedObjectIds,
focusPoint,
focusToken,
enableObjectChooser = false,
@@ -529,6 +532,14 @@ export function Tgu2DMapView({
[hoveredRequestPolygonId, activePolygons]
);
const overlayPolygons = uniquePolygons([externalSelectedPolygon, selectedPolygon, externalHoveredPolygon, hoverRef.current]);
// Отмеченные объекты (напр. отобранные под бронь слоты) — постоянная подсветка под обычным выбором.
const markedPolygons = useMemo(
() =>
markedObjectIds && markedObjectIds.size > 0
? activePolygons.filter((p) => markedObjectIds.has(p.id))
: [],
[markedObjectIds, activePolygons]
);
const overlayMarkers = uniqueMarkers([selectedMarker, hoverMarkerRef.current]);
// Накопленное расстояние (км) от первой вершины линейки до каждой — по дуге большого круга.
@@ -563,6 +574,24 @@ export function Tgu2DMapView({
>
<canvas className="tgu-2d-map__canvas" ref={canvasRef} />
<svg className="tgu-2d-map__overlay" width={size.width} height={size.height} aria-hidden="true">
{markedPolygons.flatMap((polygon) => {
const raw = polygon.points.map((point) => projection.project(point));
const pts = unwrapX(raw, projection.worldSize);
return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
const visible = pts.some((p) => {
const x = p.x + dx;
return x >= -64 && x <= size.width + 64 && p.y >= -64 && p.y <= size.height + 64;
});
if (!visible) return [];
return (
<polygon
key={`marked:${polygon.id}:${dx}`}
points={pts.map((p) => `${p.x + dx},${p.y}`).join(" ")}
className="is-marked"
/>
);
});
})}
{overlayPolygons.flatMap((polygon) => {
const raw = polygon.points.map((point) => projection.project(point));
const pts = unwrapX(raw, projection.worldSize);
@@ -113,6 +113,12 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
// Ключи забронированных слотов — для бейджа «бронь» в таблице и отдельного слоя на карте.
const bookedKeys = useMemo(() => new Set(bookedSlots.map(slotKey)), [bookedSlots]);
// id полигонов слотов, отобранных галочками под бронь — постоянная подсветка на карте.
const markedSlotIds = useMemo(
() => new Set(Array.from(selectedKeys, (key) => `slot:${key}`)),
[selectedKeys]
);
// Слот по ключу — чтобы из выбора на карте (id "slot:key"/"booked:key") восстановить полные данные.
const slotByKey = useMemo(() => {
const map = new Map<string, SlotDTO>();
@@ -758,6 +764,7 @@ export function TguSlotsTab({ range }: TguSlotsTabProps) {
onObjectsAtPoint={handleObjectsAtPoint}
onObjectSelect={handleObjectSelect}
selectedObjectId={hasRequest ? activeSlotId : activeHitId}
markedObjectIds={hasRequest ? markedSlotIds : undefined}
focusPoint={focusPoint}
focusToken={focusToken}
/>
@@ -1175,6 +1175,14 @@
stroke-width: 2;
}
/* Отобранные под бронь слоты — постоянная зелёная подсветка (отличается от оранжевого выбора). */
.tgu-2d-map__overlay polygon.is-marked {
fill: rgba(58, 209, 122, 0.22);
stroke: #3ad17a;
stroke-width: 2;
stroke-dasharray: 6 4;
}
.tgu-2d-map__overlay polyline.is-selected {
stroke: #f0ad2e;
stroke-width: 7;