Skip to content

Commit 20bd22b

Browse files
authored
Merge pull request #3 from ParkTrack-Project/fix/corrections-from-nawinds-1
Fix/design, Zone and stage logic
2 parents d36a365 + ef395fb commit 20bd22b

8 files changed

Lines changed: 186 additions & 374 deletions

File tree

public/sample.png

928 KB
Loading

src/api/client.ts

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ParkingZone, GeoPoint, PxPoint, ParkingLot, Id } from '@/types';
1+
import { ParkingZone, GeoPoint, PxPoint, Id } from '@/types';
22
import { useRequestLog } from './requestLog';
33

44
type Config = { baseUrl: string; token?: string };
@@ -34,29 +34,13 @@ async function request<T>(method: 'GET'|'POST'|'PUT'|'DELETE', path: string, bod
3434
}
3535

3636
// --- helpers & mappers ---
37-
const gp = (x:number, y:number, long:number|null=null, lat:number|null=null): GeoPoint => ({ x,y,long,lat });
37+
const gp = (x:number, y:number, longitude:number|null=null, latitude:number|null=null): GeoPoint => ({ x,y,longitude,latitude });
3838
const px = (p: GeoPoint): PxPoint => ({ x: p.x, y: p.y });
3939

40-
function mapLotFromAPI(l: any): ParkingLot {
41-
const pts = (l.points || []).map((p: any) => gp(+p.x, +p.y, p.long ?? null, p.lat ?? null)) as GeoPoint[];
42-
return {
43-
lot_id: l.lot_id as Id,
44-
long: l.long ?? null,
45-
lat: l.lat ?? null,
46-
points: pts,
47-
image_polygon: pts.map(px)
48-
};
49-
}
50-
5140
function mapZoneFromAPI(z: any): ParkingZone {
52-
const pts = (z.points || []).map((p: any) => gp(+p.x, +p.y, p.long ?? null, p.lat ?? null)) as GeoPoint[];
41+
const pts = (z.points || []).map((p: any) => gp(+p.x, +p.y, p.longitude ?? null, p.latitude ?? null)) as GeoPoint[];
5342
const quad = pts.slice(0,4).map(px) as [PxPoint, PxPoint, PxPoint, PxPoint];
5443

55-
// если сервер вернёт lots внутри зоны — распарсим
56-
const lotsArr: ParkingLot[] = Array.isArray(z.lots)
57-
? z.lots.map(mapLotFromAPI)
58-
: [];
59-
6044
return {
6145
id: z.zone_id as Id,
6246
camera_id: +z.camera_id,
@@ -65,34 +49,20 @@ function mapZoneFromAPI(z: any): ParkingZone {
6549
pay: +z.pay,
6650
image_quad: quad,
6751
points: pts.slice(0,4) as any,
68-
lots: lotsArr,
69-
lots_count: z.lots_count,
7052
created_at: z.created_at,
7153
updated_at: z.updated_at
7254
};
7355
}
7456

75-
// ——— ТЕПЕРЬ ЗОНЫ ОТПРАВЛЯЕМ С L O T S В ТЕЛЕ ———
76-
function buildLotsForBody(lots: ParkingLot[]) {
77-
return lots.map(l => ({
78-
lot_id: l.lot_id,
79-
long: l.long ?? null,
80-
lat: l.lat ?? null,
81-
// формируем points из image_polygon; geo пока null
82-
points: (l.image_polygon?.length ? l.image_polygon : l.points)?.map(p => ({
83-
x: p.x, y: p.y, long: null, lat: null
84-
}))
85-
}));
86-
}
87-
8857
function buildCreateZoneBody(z: ParkingZone) {
8958
return {
9059
camera_id: z.camera_id,
9160
zone_type: z.zone_type,
9261
capacity: z.capacity,
9362
pay: z.pay,
94-
points: z.points.map(p => ({ x: p.x, y: p.y, long: p.long, lat: p.lat })),
95-
lots: buildLotsForBody(z.lots || [])
63+
points: z.points.map(p => ({
64+
x: p.x, y: p.y, longitude: p.longitude, latitude: p.latitude
65+
}))
9666
};
9767
}
9868

@@ -101,23 +71,22 @@ function buildUpdateZoneBody(z: ParkingZone) {
10171
zone_type: z.zone_type,
10272
capacity: z.capacity,
10373
pay: z.pay,
104-
points: z.points.map(p => ({ x: p.x, y: p.y, long: p.long, lat: p.lat })),
105-
lots: buildLotsForBody(z.lots || [])
74+
points: z.points.map(p => ({
75+
x: p.x, y: p.y, longitude: p.longitude, latitude: p.latitude
76+
}))
10677
};
10778
}
10879

10980
// --- public API ---
11081
export const api = {
111-
// ZONES
11282
async listZones(cameraId?: number) {
11383
const q = cameraId ? `?camera_id=${encodeURIComponent(cameraId)}` : '';
11484
const arr = await request<any[]>('GET', `/zones${q}`);
11585
return arr.map(mapZoneFromAPI);
11686
},
11787
async createZone(z: ParkingZone) {
118-
// Сервер может вернуть { zone_id } или целую зону — поддержим оба
11988
const resp = await request<any>('POST', `/zones/new`, buildCreateZoneBody(z));
120-
return resp;
89+
return resp; // { zone_id } или полная зона — поддерживаем оба
12190
},
12291
async updateZone(zoneId: Id, z: ParkingZone) {
12392
const updated = await request<any>('PUT', `/zones/${encodeURIComponent(String(zoneId))}`, buildUpdateZoneBody(z));
@@ -127,14 +96,6 @@ export const api = {
12796
await request<void>('DELETE', `/zones/${encodeURIComponent(String(zoneId))}`);
12897
},
12998

130-
// При чтении lots по активной зоне можно оставить вспомогательные маршруты,
131-
// если на сервере они есть; фронт больше их НЕ вызывает при сохранении.
132-
async getLots(zoneId: Id) {
133-
const arr = await request<any[]>('GET', `/zones/${encodeURIComponent(String(zoneId))}/lots`);
134-
return arr.map(mapLotFromAPI);
135-
},
136-
137-
// Snapshot JSON
13899
async getSnapshot(cameraId: number) {
139100
return request<{ image_url: string; captured_at?: string; width?: number; height?: number }>(
140101
'GET',

src/components/ImageViewport.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ import useImage from 'use-image';
33
import { useEffect, useRef, useState } from 'react';
44
import { useStore } from '@/store/useStore';
55
import ZoneLayer from './ZoneLayer';
6-
import LotLayer from './LotLayer';
76
import type { KonvaEventObject } from 'konva/lib/Node';
87

98
export default function ImageViewport() {
10-
const { image, scale, offsetX, offsetY, setView } = useStore();
9+
const { image, scale, offsetX, offsetY, setView, tool } = useStore();
1110
const [img] = useImage(image?.url ?? '');
1211
const stageRef = useRef<any>(null);
1312
const containerRef = useRef<HTMLDivElement>(null);
@@ -48,8 +47,10 @@ export default function ImageViewport() {
4847
setView(newScale, newPos.x, newPos.y);
4948
};
5049

51-
const onDragEnd = (e: KonvaEventObject<DragEvent>) => {
52-
const st = e.target; // Stage
50+
const onDragEnd = (_e: KonvaEventObject<DragEvent>) => {
51+
// Stage draggable только в select — значит, просто фиксируем позицию
52+
const st = stageRef.current;
53+
if (!st) return;
5354
setView(scale, st.x(), st.y());
5455
};
5556

@@ -62,7 +63,7 @@ export default function ImageViewport() {
6263
) : (
6364
<>
6465
<div className="toolbar">
65-
<div className="badge">scale: {scale.toFixed(2)}</div>
66+
<div className="badge">scale: {scale.toFixed(2)} • tool: {tool}</div>
6667
</div>
6768
<Stage
6869
ref={stageRef}
@@ -73,7 +74,7 @@ export default function ImageViewport() {
7374
x={offsetX}
7475
y={offsetY}
7576
onWheel={onWheel}
76-
draggable
77+
draggable={tool === 'select'}
7778
onDragEnd={onDragEnd}
7879
>
7980
<Layer>
@@ -85,7 +86,6 @@ export default function ImageViewport() {
8586
/>
8687
)}
8788
<ZoneLayer />
88-
<LotLayer />
8989
</Layer>
9090
</Stage>
9191
</>

src/components/LotLayer.tsx

Lines changed: 0 additions & 85 deletions
This file was deleted.

src/components/Sidebar.tsx

Lines changed: 16 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,12 @@ import { Button, Field, Input, Select } from './UiKit';
44
export default function Sidebar() {
55
const s = useStore();
66
const zone = s.zones.find(z => String(z.id) === String(s.activeZoneId));
7-
const lot = zone?.lots.find(l => String(l.lot_id) === String(s.activeLotId));
87

9-
function startNewZone() {
10-
const z = s.addZone();
11-
s.selectZone(z.id);
12-
}
13-
function startDrawLot() {
14-
if (!zone) return;
15-
s.lotDraftClear();
16-
s.setTool('drawLot');
8+
function startDrawZone() {
9+
s.addZone(); // теперь это включает drawZone и очищает черновик
1710
}
1811
function finishEditing() {
1912
s.setTool('select');
20-
s.lotDraftClear();
2113
}
2214

2315
return (
@@ -30,7 +22,10 @@ export default function Sidebar() {
3022
<hr/>
3123

3224
<div className="col">
33-
<Button onClick={startNewZone}>+ Добавить зону</Button>
25+
<Button onClick={startDrawZone}>+ Добавить зону</Button>
26+
{s.tool === 'drawZone' && s.zoneDraft && s.zoneDraft.length > 0 && (
27+
<Button className="danger" onClick={()=>s.zoneDraftClear()}>Отменить рисование</Button>
28+
)}
3429
<Button className="ghost" onClick={()=>s.loadZones()}>Загрузить зоны (GET)</Button>
3530
</div>
3631

@@ -43,9 +38,7 @@ export default function Sidebar() {
4338
<span className="badge">{z.zone_type}</span>
4439
</div>
4540
<div className="small">
46-
capacity: {z.capacity}
47-
{' • '}lots_count: {z.lots_count ?? 0}
48-
{' • '}pay: {z.pay}
41+
capacity: {z.capacity} • pay: {z.pay}
4942
</div>
5043
</div>
5144
))}
@@ -62,7 +55,7 @@ export default function Sidebar() {
6255
<option value="disabled">disabled</option>
6356
</Select>
6457
</Field>
65-
<Field label="Capacity (lots)">
58+
<Field label="Capacity">
6659
<Input type="number" min={0} value={zone.capacity}
6760
onChange={e=>s.updateZone(zone.id,{capacity: parseInt(e.target.value||'0',10)})}/>
6861
</Field>
@@ -76,48 +69,18 @@ export default function Sidebar() {
7669
<Button className="ghost" onClick={finishEditing}>Готово</Button>
7770
</div>
7871
<div className="row" style={{gap:8}}>
79-
<Button onClick={()=>s.saveZone(zone.id)}>Сохранить зону (PUT/POST с lots)</Button>
72+
<Button onClick={()=>s.saveZone(zone.id)}>Сохранить зону (PUT/POST)</Button>
8073
<Button className="danger" onClick={()=>s.removeZone(zone.id)}>Удалить зону (DELETE)</Button>
8174
</div>
82-
<hr/>
83-
<div className="row" style={{justifyContent:'space-between', alignItems:'baseline'}}>
84-
<h4>Места (Lots)</h4>
85-
<div className="row" style={{gap:6}}>
86-
<Button className="ghost" onClick={startDrawLot}>+ Добавить лот</Button>
87-
{s.tool==='drawLot' && s.lotDraft && s.lotDraft.length>=3 && (
88-
<Button onClick={()=>s.lotDraftComplete()}>Завершить лот</Button>
89-
)}
90-
{s.tool==='drawLot' && s.lotDraft && s.lotDraft.length>0 && (
91-
<Button className="danger" onClick={()=>s.lotDraftClear()}>Отменить</Button>
92-
)}
93-
</div>
94-
</div>
75+
</>
76+
)}
9577

96-
<div className="list">
97-
{zone.lots.map(l => (
98-
<div key={String(l.lot_id)}
99-
className={`item ${String(s.activeLotId)===String(l.lot_id) ? 'active':''}`}
100-
onClick={()=>s.selectLot(String(l.lot_id))}>
101-
<div style={{display:'flex', justifyContent:'space-between'}}>
102-
<div>{String(l.lot_id)}</div>
103-
<span className="badge">{l.image_polygon.length} pts</span>
104-
</div>
105-
</div>
106-
))}
78+
{s.tool === 'drawZone' && (
79+
<>
80+
<hr/>
81+
<div className="small" style={{opacity:0.8}}>
82+
Режим рисования зоны: кликните 4 точки на изображении, чтобы замкнуть четырёхугольник.
10783
</div>
108-
109-
{lot && (
110-
<>
111-
<hr/>
112-
<h4>Свойства лота</h4>
113-
{/* label удалён — оставляем только редактирование вершин */}
114-
<div className="row" style={{gap:8}}>
115-
<Button onClick={()=>s.setTool('editLot')}>Редактировать вершины</Button>
116-
<Button className="ghost" onClick={finishEditing}>Готово</Button>
117-
<Button className="danger" onClick={()=>s.removeLot(zone.id, String(lot.lot_id))}>Удалить лот</Button>
118-
</div>
119-
</>
120-
)}
12184
</>
12285
)}
12386
</div>

0 commit comments

Comments
 (0)