diff --git a/api/controllers/history.apartment.controller.js b/api/controllers/history.apartment.controller.js new file mode 100644 index 0000000..e6bf467 --- /dev/null +++ b/api/controllers/history.apartment.controller.js @@ -0,0 +1,26 @@ +const historyApartmentService = require('../services/history.apartment.service'); + +class historyApartmentController { + async getList(req, res) { + const { limit } = req.query; + + if (req.mode == 2) { + let result = await historyApartmentService.getList(limit); + if (result) { + return res + .status(200) + .send(result); + } else { + return res + .status(500) + .send({ message: 'Internal server error.' }); + } + } else { + return res + .status(403) + .send({ message: 'The user does not have enough rights.' }); + } + } +} + +module.exports = new historyApartmentController(); \ No newline at end of file diff --git a/api/routes/history.apartment.routes.js b/api/routes/history.apartment.routes.js new file mode 100644 index 0000000..1ec5b1d --- /dev/null +++ b/api/routes/history.apartment.routes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router({ mergeParams: true }); +const historyApartmentController = require('../controllers/history.apartment.controller'); +const authenticate = require("../middleware/auth"); + +router + .route('/list') + .get(authenticate, historyApartmentController.getList); + +module.exports = router; \ No newline at end of file diff --git a/api/routes/index.js b/api/routes/index.js index dbb3a5e..69e08c9 100644 --- a/api/routes/index.js +++ b/api/routes/index.js @@ -11,6 +11,7 @@ const entrancesRoutes = require('./entrances.routes'); const apartmentsRoutes = require('./apartments.routes'); const historyEntranceRoutes = require('./history.entrance.routes'); const historyHomesteadRoutes = require('./history.homestead.routes'); +const historyApartmentRoutes = require('./history.apartment.routes'); const standRoutes = require('./stand.routes'); const pushRoutes = require('./push.routes'); @@ -27,6 +28,7 @@ router.use('/house/:house_id/entrances', entrancesRoutes); router.use('/apartments?/:entrance_id', apartmentsRoutes); router.use('/history/entrance/:entrance_id', historyEntranceRoutes); router.use('/history/homestead/:homestead_id', historyHomesteadRoutes); +router.use('/history/apartments?', historyApartmentRoutes); router.use('/stand', standRoutes); router.use('/push', pushRoutes); diff --git a/api/services/entrances.service.js b/api/services/entrances.service.js index 7c7628b..f2a9b43 100644 --- a/api/services/entrances.service.js +++ b/api/services/entrances.service.js @@ -69,7 +69,7 @@ class EntrancesService { `; db.run(sql, [ - house_id, + Number(house_id), Number(data.entrance_number), data.title, data.description, @@ -101,10 +101,6 @@ class EntrancesService { `; db.run(sql, [ data.title, - JSON.stringify(data.points), - JSON.stringify(data.points_number), - data.floors_quantity, - data.apartments_quantity, data.description, Math.floor(new Date(Date.now()).getTime()), data.id diff --git a/api/services/history.apartment.service.js b/api/services/history.apartment.service.js new file mode 100644 index 0000000..f26af79 --- /dev/null +++ b/api/services/history.apartment.service.js @@ -0,0 +1,69 @@ +const db = require("../config/db"); + +class historyApartmentService { + getList(limit) { + return new Promise((res, rej) => { + let sql = ` + SELECT + ah.*, + s.name AS sheep_name, + s.group_id AS sheep_group_id, + s.icon AS sheep_icon, + h.title AS house_title, + h.number AS house_number, + h.id AS house_id, + e.title AS entrance_title, + a.title AS apartment_title + FROM + apartments_history ah + LEFT JOIN + sheeps s ON s.id = ah.sheep_id + LEFT JOIN + apartments a ON a.id = ah.apartments_id + LEFT JOIN + entrance e ON e.id = a.entrance_id + LEFT JOIN + house h ON h.id = e.house_id + ORDER BY + ah.id DESC + LIMIT ${limit ?? 100}; + `; + + db.all(sql, (err, rows) => { + if (err) { + console.error(err.message); + return res(false); + } else { + let data = rows.map((row) => { + return { + "id": Number(row.id), + "apartments_id": Number(row.apartments_id), + "house_id": Number(row.house_id), + "address": { + "house": { + "title": row.house_title, + "number": row.house_number + }, + "entrance": row.entrance_title, + "apartment": row.apartment_title + }, + "status": Number(row.status), + "description": row.description, + "sheep": { + "id": Number(row.sheep_id), + "name": row.sheep_name, + "group_id": Number(row.sheep_group_id), + "icon": row.sheep_icon + }, + "created_at": Number(row.created_at) + } + }) + + return res(data); + } + }); + }); + } +} + +module.exports = new historyApartmentService(); \ No newline at end of file diff --git a/api/services/houses.service.js b/api/services/houses.service.js index 535dfda..c1d00eb 100644 --- a/api/services/houses.service.js +++ b/api/services/houses.service.js @@ -36,8 +36,6 @@ class HousesService { }, "entrance_number": Number(row.entrance_number), "title": row.title, - "points": JSON.parse(row.points), - "points_number": JSON.parse(row.points_number), "floors_quantity": row.floors_quantity, "apartments_quantity": row.apartments_quantity, "description": row.description, diff --git a/web/css/main.css b/web/css/main.css index 138218b..ac84f6b 100644 --- a/web/css/main.css +++ b/web/css/main.css @@ -1,11 +1,11 @@ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap'); :root { - --FontSize1: 12px; - --FontSize2: 13px; - --FontSize3: 14px; - --FontSize4: 15px; - --FontSize5: 16px; + --FontSize1: 12px; + --FontSize2: 13px; + --FontSize3: 14px; + --FontSize4: 15px; + --FontSize5: 16px; } @media (prefers-color-scheme: light) { @@ -100,7 +100,12 @@ } } -a { +a, +a:visited, +a:hover, +a:active { + color: inherit; + text-decoration: none; text-decoration: none; font-size: var(--FontSize3); font-weight: 700; @@ -708,7 +713,7 @@ body.modal-open { padding: 0 !important; } -.leaflet-popup-content > .map_dell { +.leaflet-popup-content>.map_dell { border-radius: 10px; padding: 5px 10px; width: 100%; @@ -737,9 +742,11 @@ body.modal-open { border: 2px solid #fff; margin: -3px 0 0 -3px; } + .leaflet-pm-tooltip { display: none !important; } + .tooltip-hidden { - display: none; + display: none; } \ No newline at end of file diff --git a/web/index.html b/web/index.html index b34c071..8171c5a 100644 --- a/web/index.html +++ b/web/index.html @@ -79,6 +79,9 @@ + + + diff --git a/web/lib/pages/constructor/script.js b/web/lib/pages/constructor/script.js index 12b85af..bea7bcb 100644 --- a/web/lib/pages/constructor/script.js +++ b/web/lib/pages/constructor/script.js @@ -799,22 +799,37 @@ const Constructor = { delete(house) { // убрать слой с карты - if (Constructor.info.type === 'house') { + if (Editor.info.type === 'house') { houseGroup.removeLayer(house); - } else if (Constructor.info.type === 'homestead') { + } else if (Editor.info.type === 'homestead') { homesteadGroup.removeLayer(house); } // найти индекс полигона в points - const index = Constructor.info.points.findIndex(p => p === house.getLatLngs()); + const target = house.getLatLngs()[0]; // вершины полигона - if (index !== -1) { - // удалить из points и points_number по индексу - Constructor.info.points.splice(index, 1); - Constructor.info.points_number.splice(index, 1); + const index = Editor.info.points.findIndex((p) => { + const copy = p.slice(); // делаем копию + copy[0].pop(); // убираем последний элемент + + if (isSamePolygon(p, target)) return true; // проверка как есть + if (isSamePolygon(copy, target)) return true; // проверка без последнего + return false; + }); + + function isSamePolygon(a, b) { + if (a.length !== b.length) return false; + return a.every((pt, i) => pt.lat === b[i].lat && pt.lng === b[i].lng); } - Constructor.osm.autoZoom(Constructor.info.points); + + if (index) { + // удалить из points и points_number по индексу + Editor.info.points.splice(index, 1); + Editor.info.points_number.splice(index, 1); + } + + Editor.osm.autoZoom(Editor.info.points); }, async getOSM(wayId) { diff --git a/web/lib/pages/constructor/style.css b/web/lib/pages/constructor/style.css index 679f954..8a187c6 100644 --- a/web/lib/pages/constructor/style.css +++ b/web/lib/pages/constructor/style.css @@ -6,9 +6,9 @@ margin: 20px 20px 0 20px; } -#part-1, -#part-2, -#part-3 { +.page-constructor>#part-1, +.page-constructor>#part-2, +.page-constructor>#part-3 { border-radius: 10px; width: calc(100% - 40px); display: flex; @@ -23,9 +23,9 @@ position: relative; } -#part-1>h1, -#part-2>h1, -#part-3>h1 { +.page-constructor>#part-1>h1, +.page-constructor>#part-2>h1, +.page-constructor>#part-3>h1 { width: calc(100% - 40px); color: var(--ColorThemes3); border-radius: var(--border-radius); @@ -35,19 +35,19 @@ position: relative; } -#part-1>h1>span, -#part-2>h1>span, -#part-3>h1>span { +.page-constructor>#part-1>h1>span, +.page-constructor>#part-2>h1>span, +.page-constructor>#part-3>h1>span { font-weight: 500; } -#part-1>#info-type { +.page-constructor>#part-1>#info-type { display: flex; align-items: center; justify-content: center; } -#part-1>#info-type>.tabs { +.page-constructor>#part-1>#info-type>.tabs { display: flex; position: relative; background-color: var(--ColorThemes0); @@ -58,11 +58,11 @@ } -#part-1>#info-type>.tabs>input[type="radio"] { +.page-constructor>#part-1>#info-type>.tabs>input[type="radio"] { display: none; } -#part-1>#info-type>.tabs>.tab { +.page-constructor>#part-1>#info-type>.tabs>.tab { display: flex; align-items: center; justify-content: center; @@ -77,23 +77,23 @@ z-index: 2; } -#part-1>#info-type>.tabs>.tab>svg { +.page-constructor>#part-1>#info-type>.tabs>.tab>svg { width: 20px; height: 20px; } -#part-1>#info-type>.tabs>.tab>span { +.page-constructor>#part-1>#info-type>.tabs>.tab>span { margin-left: 6px; font-size: var(--FontSize1); font-weight: 400; } -#part-1>#info-type>.tabs>input[type="radio"]:checked+label { +.page-constructor>#part-1>#info-type>.tabs>input[type="radio"]:checked+label { color: var(--PrimaryColorText); fill: var(--PrimaryColorText); } -#part-1>#info-type>.tabs>.glider { +.page-constructor>#part-1>#info-type>.tabs>.glider { position: absolute; display: flex; height: 40px; @@ -105,47 +105,47 @@ } @media (min-width: 601px) { - #part-1>#info-type>.tabs>input[id="info-type-house"]:checked~.glider { + .page-constructor>#part-1>#info-type>.tabs>input[id="info-type-house"]:checked~.glider { transform: translateX(0); } - #part-1>#info-type>.tabs>input[id="info-type-homestead"]:checked~.glider { + .page-constructor>#part-1>#info-type>.tabs>input[id="info-type-homestead"]:checked~.glider { transform: translateX(100%); } - #part-1>#info-type>.tabs>input[id="info-type-points"]:checked~.glider { + .page-constructor>#part-1>#info-type>.tabs>input[id="info-type-points"]:checked~.glider { transform: translateX(200%); } } @media (max-width: 600px) { - #part-1>#info-type>.tabs { + .page-constructor>#part-1>#info-type>.tabs { flex-direction: column; } - #part-1>#info-type>.tabs>.tab { + .page-constructor>#part-1>#info-type>.tabs>.tab { width: calc(100% - 8px); padding: 0 4px; } - #part-1>#info-type>.tabs>.glider { + .page-constructor>#part-1>#info-type>.tabs>.glider { width: calc(100% - 8px); } - #part-1>#info-type>.tabs>input[id="info-type-house"]:checked~.glider { + .page-constructor>#part-1>#info-type>.tabs>input[id="info-type-house"]:checked~.glider { transform: translateY(0); } - #part-1>#info-type>.tabs>input[id="info-type-homestead"]:checked~.glider { + .page-constructor>#part-1>#info-type>.tabs>input[id="info-type-homestead"]:checked~.glider { transform: translateY(100%); } - #part-1>#info-type>.tabs>input[id="info-type-points"]:checked~.glider { + .page-constructor>#part-1>#info-type>.tabs>input[id="info-type-points"]:checked~.glider { transform: translateY(200%); } } -#part-1>form>div { +.page-constructor>#part-1>form>div { width: 100%; display: flex; margin: 20px 0; @@ -153,7 +153,7 @@ flex-direction: column; } -#part-1>form>div>label { +.page-constructor>#part-1>form>div>label { display: flex; justify-content: center; flex-direction: column; @@ -162,7 +162,7 @@ margin-bottom: 5px; } -#part-1>form>div>input { +.page-constructor>#part-1>form>div>input { width: calc(100% - 10px); min-width: 140px; padding: 0 5px; @@ -173,9 +173,9 @@ font-size: var(--FontSize2); } -#part-1>form>button, -#part-2>button, -#part-3>button { +.page-constructor>#part-1>form>button, +.page-constructor>#part-2>button, +.page-constructor>#part-3>button { border-radius: 6px; background: var(--PrimaryColor); color: var(--PrimaryColorText); @@ -187,27 +187,27 @@ text-transform: uppercase; } -#part-2>.osm-info { +.page-constructor>#part-2>.osm-info { padding-bottom: 20px; display: flex; flex-direction: column; align-items: center; } -#part-2>.osm-info>div { +.page-constructor>#part-2>.osm-info>div { display: flex; align-items: center; width: 100%; } -#part-2>.osm-info>div { +.page-constructor>#part-2>.osm-info>div { width: 100%; display: flex; align-items: flex-start; flex-direction: column; } -#part-2>.osm-info>div>label { +.page-constructor>#part-2>.osm-info>div>label { display: flex; justify-content: center; flex-direction: column; @@ -216,13 +216,13 @@ margin-bottom: 5px; } -#part-2>.osm-info>div>div { +.page-constructor>#part-2>.osm-info>div>div { display: flex; align-items: center; width: 100%; } -#part-2>.osm-info>div>div>input { +.page-constructor>#part-2>.osm-info>div>div>input { width: calc(100% - 10px); min-width: 140px; padding: 0 5px; @@ -233,25 +233,25 @@ font-size: var(--FontSize2); } -#part-2>.osm-info>div>div>a { +.page-constructor>#part-2>.osm-info>div>div>a { height: 26px; width: 26px; margin-left: 10px; } -#part-2>.osm-info>div>div>a>svg { +.page-constructor>#part-2>.osm-info>div>div>a>svg { height: 26px; width: 26px; fill: var(--ColorThemes3); } -#part-2>.osm-info>span { +.page-constructor>#part-2>.osm-info>span { font-size: var(--FontSize5); margin: 10px; color: var(--ColorThemes3); } -#part-2>.osm-info>button { +.page-constructor>#part-2>.osm-info>button { display: flex; align-items: center; justify-content: center; @@ -267,7 +267,7 @@ font-size: var(--FontSize3); } -#part-2>.block-map { +.page-constructor>#part-2>.block-map { width: 100%; height: 500px; border-radius: 6px; @@ -279,12 +279,12 @@ box-shadow: var(--shadow-l1); } -#part-2>#map { +.page-constructor>#part-2>.block-map>#map { width: 100%; height: 100%; } -#part-3>input { +.page-constructor>#part-3>input { font-weight: 500; position: absolute; right: 0; @@ -298,12 +298,12 @@ width: 40px; } -#part-3>#house { +.page-constructor>#part-3>#house { display: flex; overflow: auto; } -#part-3>#house>button { +.page-constructor>#part-3>#house>button { display: flex; position: relative; width: 34px; @@ -319,14 +319,14 @@ cursor: pointer; } -#part-3>#house>button>svg { +.page-constructor>#part-3>#house>button>svg { width: 20px; height: 20px; fill: var(--PrimaryColorText); transform: rotate(45deg); } -#part-3>#house>.entrance { +.page-constructor>#part-3>#house>.entrance { min-height: 200px; border: 1px solid var(--ColorThemes3); border-style: dashed; @@ -334,7 +334,7 @@ margin: 0 10px 10px 0; } -#part-3>#house>.entrance>.entrance-info>input { +.page-constructor>#part-3>#house>.entrance>.entrance-info>input { text-align: center; font-size: var(--FontSize5); font-weight: 400; @@ -346,8 +346,8 @@ width: calc(100% - 14px - 20px); } -#part-3>#house>.entrance>.entrance-info>button, -#part-3>#house>.entrance>.floor>.floor-info>button { +.page-constructor>#part-3>#house>.entrance>.entrance-info>button, +.page-constructor>#part-3>#house>.entrance>.floor>.floor-info>button { display: flex; position: relative; width: 34px; @@ -362,15 +362,15 @@ cursor: pointer; } -#part-3>#house>.entrance>.entrance-info>button>svg, -#part-3>#house>.entrance>.floor>.floor-info>button>svg { +.page-constructor>#part-3>#house>.entrance>.entrance-info>button>svg, +.page-constructor>#part-3>#house>.entrance>.floor>.floor-info>button>svg { width: 20px; height: 20px; fill: var(--PrimaryColorText); transform: rotate(45deg); } -#part-3>#house>.entrance>.floor { +.page-constructor>#part-3>#house>.entrance>.floor { position: relative; display: flex; width: calc(100% - 22px); @@ -379,7 +379,7 @@ border-radius: 4px; } -#part-3>#house>.entrance>.floor>.floor-info>h2 { +.page-constructor>#part-3>#house>.entrance>.floor>.floor-info>h2 { position: absolute; width: 65px; right: -1px; @@ -393,7 +393,7 @@ text-align: center; } -#part-3>#house>.entrance>.floor>.apartment { +.page-constructor>#part-3>#house>.entrance>.floor>.apartment { display: flex; position: relative; width: 60px; @@ -406,7 +406,7 @@ justify-content: center; } -#part-3>#house>.entrance>.floor>.apartment>input { +.page-constructor>#part-3>#house>.entrance>.floor>.apartment>input { width: 50px; height: 50px; font-size: var(--FontSize5); @@ -416,7 +416,7 @@ background: 0; } -#part-3>#house>.entrance>.floor>.apartment>button { +.page-constructor>#part-3>#house>.entrance>.floor>.apartment>button { position: absolute; top: 0; right: 0; @@ -433,22 +433,22 @@ justify-content: center; } -#part-3>#house>.entrance>.floor>.apartment>button>svg { +.page-constructor>#part-3>#house>.entrance>.floor>.apartment>button>svg { width: 16px; height: 16px; fill: var(--PrimaryColorText); } -#part-3>.info {} +.page-constructor>#part-3>.info {} -#part-3>.info>p { +.page-constructor>#part-3>.info>p { font-size: var(--FontSize2); color: var(--ColorThemes3); opacity: 0.8; font-style: oblique; } -#part-3>.info>button { +.page-constructor>#part-3>.info>button { border-radius: 6px; background: var(--ColorThemes3); width: fit-content; @@ -464,14 +464,14 @@ cursor: pointer; } -#part-3>.info>button>svg { +.page-constructor>#part-3>.info>button>svg { width: 20px; height: 20px; fill: var(--ColorThemes0); margin-right: 10px; } -#part-3>.info>button>span { +.page-constructor>#part-3>.info>button>span { font-size: var(--FontSize3); font-weight: 400; color: var(--ColorThemes0); diff --git a/web/lib/pages/editor/index.html b/web/lib/pages/editor/index.html index 8672af1..befd507 100644 --- a/web/lib/pages/editor/index.html +++ b/web/lib/pages/editor/index.html @@ -1,89 +1,84 @@
-
- +
+

Крок 1. Інформація про будинок, територію, точки на карті -

+ +
-
- +
+
-
- - +
+ +
-
- +
+
-
+
-
- Крок 2. Перегляд -
-
+
+
+
- + diff --git a/web/lib/pages/editor/script.js b/web/lib/pages/editor/script.js index b6d627a..815a3a5 100644 --- a/web/lib/pages/editor/script.js +++ b/web/lib/pages/editor/script.js @@ -1,19 +1,26 @@ const Editor = { + info: {}, + + // Ініціалізація редактора за типом об'єкта та його ID async init(type, id) { let html = await fetch('/lib/pages/editor/index.html').then((response) => response.text()); app.innerHTML = html; + // Очищення груп та карти map = ""; houseGroup = ""; - entransePolygonsGroup = ""; - entranseNumPolygonsGroup = ""; - splitPolygonsGroup = ""; - RectangleGroup = ""; + homesteadGroup = ""; + buildingGroup = ""; + pointsGroup = ""; numApartments = 1; - Editor.info.setHTML(type, id); + this.info.id = Number(id); + this.info.type = type; + + this.setHTML(type, id); }, + // Отримання даних з API з авторизацією через UUID async loadAPI(URL) { let uuid = localStorage.getItem("uuid"); return await fetch(URL, { @@ -25,75 +32,747 @@ const Editor = { }).then((response) => response.json()); }, - info: { - list: { - type: null, title: null, number: null, - points: [], points_number: [], point_icons: [], - geo: [], osm_id: [], settlement: [], description: null, - entrance: [], apartments: {} + // Встановлення HTML-контенту для редактора залежно від типу об'єкта + async setHTML(type, id) { + let list = await Editor.loadAPI(`${CONFIG.api}${type}/${id}`); + + console.log(list); + + // Заповнення даних об'єкта + let info_title = document.getElementById("info-title"); + let info_number = document.getElementById("info-number"); + let info_settlement = document.getElementById("info-settlement"); + let info_osm = document.getElementById("info-osm"); + + this.info.points = list.points; + this.info.points_number = list.points_number ?? []; + this.info.geo = list.geo; + this.info.osm_id = list.osm_id; + this.info.zoom = list.zoom; + this.info.title = list.title; + this.info.number = list.number; + this.info.settlement = list.settlement; + this.info.description = list.description ?? null; + + info_title.value = this.info.title; + info_number.value = this.info.number; + info_settlement.value = this.info.settlement; + info_osm.value = this.info.osm_id; + + this.osm.init(); + + switch (type) { + case 'points': + this.points.init(); + break; + + case 'homestead': + this.homestead.init(); + break; + + case 'house': + this.house.init(); + break; + } + }, + + points: { + init() { } + }, + + homestead: { + init() { + const part_3 = document.getElementById('part-3'); + const title = part_3.querySelector('h1'); + part_3.innerHTML = ''; + + title.innerHTML = `Крок 3. Створення будинків`; + + part_3.appendChild(title); + part_3.innerHTML += ` +
+

*Натисніть кнопку нижче, а потім клацайте на карті, щоб додати будинки. Після цього натисніть "Зберегти".

+

*Щоб видалити будинок, клацніть на ньому та у спливаючому вікні оберіть "Видалити".

+
+ +
+ `; + part_3.style.display = ""; + + this.building.init(); }, - async setHTML(type, id) { - const els = { - title: document.getElementById('info-address-title'), - number: document.getElementById('info-number-title'), - settlement: document.getElementById('info-settlement-title') - }; + building: { + list: [], editing: false, + async init() { + this.editing = false; + setLeafletCursor('pointer'); - this.list = await Editor.loadAPI(`${CONFIG.api}${type}/${id}`); - Editor.info.list.type = type; - Editor.info.list.entrance = []; - Editor.info.list.apartments = {}; + this.list = await Editor.loadAPI(`${CONFIG.api}building/${Editor.info.id}`); - console.log(Editor.info.list); + // Обробник кліку на карту + homesteadGroup.on('click', e => { + console.log(this.editing); - els.title.value = this.list.title; - els.number.value = this.list.number; - els.settlement.value = this.list.settlement; + if (e.layer instanceof L.Marker || !this.editing) return; - Editor.osm.init(); - this.setMap(); + const { lat, lng } = e.latlng; + console.log(`Координати: ${lat.toFixed(5)}, ${lng.toFixed(5)}`); - if (type == "house") { - Editor.entrances.setHTML(id); - document.getElementById('details-area').style.display = ""; - } else if (type == "homestead") { - Editor.homestead.init(id); - } - }, + setLeafletCursor('progress'); + this.editing = false; - setMap() { - houseGroup.clearLayers(); + this.addBuilding({ geo: e.latlng, title: this.list.length + 1 }); + }); - for (let i = 0; i < Editor.info.list.points.length; i++) { - const element = Editor.info.list.points[i]; + for (const element of this.list) { + // Додаємо маркер на карту + const redDot = L.divIcon({ + className: "leaflet_drop", + html: `
`, + iconSize: [16, 16], + iconAnchor: [8, 8] + }); - if (Editor.info.list.type == "homestead") { - map.setView([this.list.geo.lat, this.list.geo.lng], this.list.zoom); - - L.polygon(element, { - color: "#f2bd53", - radius: 500, - fillOpacity: 0.3, - dashArray: '20,15', - dashOffset: '20', - }).addTo(houseGroup); - } else if (Editor.info.list.type == "house") { - map.setView([this.list.geo.lat, this.list.geo.lng], this.list.zoom); - - L.polygon(element, { - color: "#585858", - fillColor: "#f2bd53", - fillOpacity: 0.8, - tm_id: `house_${i}` - }).addTo(houseGroup); + L.marker(element.geo, { icon: redDot }) + .addTo(buildingGroup) + .bindPopup(` + Точка: ${element.id}
+ Координати: ${element.geo.lat.toFixed(5)}, ${element.geo.lng.toFixed(5)}
+ + `); } + }, + + async addBuilding({ geo, title }) { + this.list.push({ + title: title, + geo: geo + }); + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}building/${Editor.info.id}`; + + try { + const response = await fetch(URL, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ title, geo }) + }); + const data = await response.json(); + console.log(data); + + // Додаємо маркер на карту + const redDot = L.divIcon({ + className: "leaflet_drop", + html: `
`, + iconSize: [16, 16], + iconAnchor: [8, 8] + }); + + const marker = L.marker(geo, { icon: redDot }).addTo(buildingGroup); + marker.bindPopup(` + Точка: ${data.id}
+ Координати: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)}
+ + `); + + setLeafletCursor('crosshair'); + this.editing = true; + + } catch (err) { + console.error("Помилка при додаванні будівлі:", err); + } + }, + + async delleteBuilding({ id }) { + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}building/${id}`; + + try { + const response = await fetch(URL, { + method: 'DELETE', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + } + }); + const data = await response.json(); + + // Видаляємо елемент списку та маркер + const el = document.getElementById(`redDot_${id}`); + if (el) el.remove(); + + const block = document.getElementById(`Building_${id}`); + if (block) block.remove(); + + buildingGroup.eachLayer(layer => { + if (layer instanceof L.Marker && layer.getPopup()?.getContent().includes(`Точка: ${id}`)) { + buildingGroup.removeLayer(layer); + } + }); + + } catch (err) { + console.error("Помилка при видаленні будівлі:", err); + } + }, + + newHouse(element) { + const btn = element; + this.editing = !this.editing; + setLeafletCursor(this.editing ? 'crosshair' : 'pointer'); + btn.innerHTML = this.editing + ? ` Завершити додавання` + : ` Додати будинок`; + if (this.editing) alert("Натискаючи на карту будуть створюватись нові точки (будинки)"); } } }, + house: { + init() { + const part_3 = document.getElementById('part-3'); + const title = part_3.querySelector('h1'); + part_3.innerHTML = ''; + + title.innerHTML = `Крок 3. Конструктор квартир`; + + part_3.appendChild(title); + part_3.innerHTML += `
`; + part_3.style.display = ""; + + this.entrances.setHTML(Editor.info.id); + }, + + entrances: { + list: [], + + async setHTML(id) { + this.list = await Editor.loadAPI(`${CONFIG.api}house/${id}/entrances`); + + const houseDiv = document.getElementById('house'); + if (!houseDiv) return; + + houseDiv.innerHTML = ""; + + const newBtn = document.createElement('button'); + newBtn.className = "entrance-button"; + newBtn.type = "button"; + newBtn.title = "Додати під'їзд"; + newBtn.innerHTML = ``; + newBtn.onclick = () => Editor.house.entrances.addEntrance(); + + houseDiv.appendChild(newBtn); + + for (const element of this.list) { + // Блок entrance + const entranceDiv = document.createElement('div'); + entranceDiv.className = "entrance"; + entranceDiv.id = `entrance-${element.id}`; + + const input = document.createElement('input'); + input.value = element.title; + input.type = "text" + input.onchange = () => Editor.house.entrances.editEntrance(element.id, input.value); + + const delBtn = document.createElement('button'); + delBtn.type = "button"; + delBtn.title = "Видалити під'їзд"; + delBtn.innerHTML = ``; + delBtn.onclick = () => Editor.house.entrances.deleteEntrance(element.id); + + const headerDiv = document.createElement('div'); + headerDiv.className = "entrance-header"; + headerDiv.append(input, delBtn); + + const addBtn = document.createElement('button'); + addBtn.className = "addFloors"; + addBtn.type = "button"; + addBtn.title = "Додати поверх"; + addBtn.innerHTML = ``; + addBtn.onclick = () => Editor.house.apartments.addFloors(element.id); + + + const infoDiv = document.createElement('div'); + infoDiv.className = "entrance-info"; + infoDiv.append(headerDiv, addBtn); + + entranceDiv.append(infoDiv); + houseDiv.insertBefore(entranceDiv, houseDiv.querySelector(".entrance-button")); + + // Завантажуємо квартири для ентрансів + Editor.house.apartments.setHTML(element.id); + } + }, + + async editEntrance(floor, value) { + const pos = this.list.findIndex(e => e.id === Number(floor)); + if (pos === -1) return; + + this.list[pos].title = value; + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}house/${Editor.info.id}/entrances`; + + try { + const response = await fetch(URL, { + method: 'PUT', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + id: this.list[pos].id, + title: this.list[pos].title, + description: this.list[pos].description + }) + }); + + const data = await response.json(); + console.log(data); + + } catch (err) { + console.error("Помилка при редагуванні під'їзду:", err); + } + }, + + async addEntrance() { + console.log('addEntrance'); + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}house/${Editor.info.id}/entrances`; + + try { + const response = await fetch(URL, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + house_id: Editor.info.id, + entrance_number: this.list.length, + title: `Під'їзд ${this.list.length + 1}`, + description: null + }) + }); + + const data = await response.json(); + console.log(data); + + let element = { + id: data.id, + house_id: Editor.info.id, + entrance_number: this.list.length, + title: `Під'їзд ${this.list.length + 1}`, + description: null + } + + this.list.push(element); + Editor.house.apartments.list[element.id] = [] + console.log(this.list); + + + const houseDiv = document.getElementById('house'); + const entranceDiv = document.createElement('div'); + entranceDiv.className = "entrance"; + entranceDiv.id = `entrance-${element.id}`; + + const input = document.createElement('input'); + input.value = element.title; + input.type = "text" + input.onchange = () => Editor.house.entrances.editEntrance(element.id, input.value); + + const delBtn = document.createElement('button'); + delBtn.type = "button"; + delBtn.title = "Видалити під'їзд"; + delBtn.innerHTML = ``; + delBtn.onclick = () => Editor.house.entrances.deleteEntrance(element.id); + + const headerDiv = document.createElement('div'); + headerDiv.className = "entrance-header"; + headerDiv.append(input, delBtn); + + const addBtn = document.createElement('button'); + addBtn.className = "addFloors"; + addBtn.type = "button"; + addBtn.title = "Додати поверх"; + addBtn.innerHTML = ``; + addBtn.onclick = () => Editor.house.apartments.addFloors(element.id); + + + const infoDiv = document.createElement('div'); + infoDiv.className = "entrance-info"; + infoDiv.append(headerDiv, addBtn); + + entranceDiv.append(infoDiv); + houseDiv.insertBefore(entranceDiv, houseDiv.querySelector(".entrance-button")); + + + } catch (err) { + console.error("Помилка при створенні під'їзду:", err); + } + }, + + async deleteEntrance(entrance) { + console.log(entrance); + + if (Editor.house.apartments.list[entrance].length == 0) { + console.log("OK"); + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}house/${Editor.info.id}/entrances`; + + try { + const response = await fetch(URL, { + method: 'DELETE', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + id: entrance + }) + }); + + const data = await response.json(); + console.log(data); + + const index = this.list.findIndex(item => item.id === entrance); + if (index !== -1) this.list.splice(index, 1); + delete Editor.house.apartments.list[entrance]; + + document.getElementById(`entrance-${entrance}`).remove(); + + } catch (err) { + console.error("Помилка при видаленні під'їзду:", err); + } + + } else { + alert("Для видалення під'їзду спочатку видаліть всі квартири з нього"); + } + } + }, + + apartments: { + list: {}, + + async setHTML(id) { + this.list[id] = await Editor.loadAPI(`${CONFIG.api}apartments/${id}`); + const entranceDiv = document.getElementById(`entrance-${id}`); + if (!entranceDiv) return; + + // Унікальні поверхи + const uniqueFloors = [...new Set(this.list[id].map(a => a.floors_number))].sort((a, b) => a - b); + + // Створюємо блоки поверхів + for (const num of uniqueFloors) { + const floorDiv = document.createElement('div'); + floorDiv.className = "floor"; + floorDiv.id = `floor-${id}-${num}`; + + const h2 = document.createElement('h2'); + h2.textContent = `Поверх ${num}`; + floorDiv.appendChild(h2); + + const addBtn = document.createElement('button'); + addBtn.id = `buttonApartment-${id}-${num}`; + addBtn.type = "button"; + addBtn.title = "Додати квартиру"; + addBtn.innerHTML = ``; + addBtn.onclick = () => Editor.house.apartments.addApartment(id, num); + + + const infoDiv = document.createElement('div'); + infoDiv.className = "floor-info"; + infoDiv.append(h2, addBtn); + + floorDiv.appendChild(infoDiv); + entranceDiv.insertBefore(floorDiv, entranceDiv.querySelector(".floor")); + } + + // Сортуємо квартири за назвою + this.list[id].sort((a, b) => b.title - a.title); + + // Створюємо блоки квартир + for (const apartment of this.list[id]) { + const floorDiv = document.getElementById(`floor-${id}-${apartment.floors_number}`); + if (!floorDiv) continue; + + const apartmentDiv = document.createElement('div'); + apartmentDiv.className = "apartment"; + apartmentDiv.id = `apartment-${id}-${apartment.id}`; + + const input = document.createElement('input'); + input.type = "text"; + input.value = apartment.title; + input.id = `apartment-input-${id}-${apartment.id}`; + input.onchange = () => Editor.house.apartments.editApartment(id, apartment.id); + + const delBtn = document.createElement('button'); + delBtn.type = "button"; + delBtn.title = "Видалити квартиру"; + delBtn.innerHTML = ``; + delBtn.onclick = () => Editor.house.apartments.deleteApartment(id, apartment.id); + + apartmentDiv.append(input, delBtn); + floorDiv.prepend(apartmentDiv); + + numApartments++; + } + + const nextApartmentTitle = document.getElementById('next-apartment-title'); + if (nextApartmentTitle) nextApartmentTitle.value = numApartments; + }, + + async addFloors(entrance) { + console.log(entrance); + + const entranceBlock = document.getElementById(`entrance-${entrance}`); + const uniqueFloors = [...new Set(this.list[entrance].map(obj => obj.floors_number))]; + console.log(uniqueFloors); + + const newFloors = uniqueFloors.length + 1; + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}/apartments/${entrance}`; + + try { + const response = await fetch(URL, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + apartment_number: this.list[entrance].length, + title: numApartments.toString(), + floors_number: newFloors + }) + }); + const data = await response.json(); + console.log(data); + + // Створюємо блок поверху + const floorDiv = document.createElement('div'); + floorDiv.className = "floor"; + floorDiv.id = `floor-${entrance}-${newFloors}`; + + // Заголовок поверху + const h2 = document.createElement('h2'); + h2.textContent = `Поверх ${newFloors}`; + + // Кнопка додати квартиру + const addBtn = document.createElement('button'); + addBtn.className = "addApartment"; + addBtn.id = `buttonApartment-${entrance}-${newFloors}`; + addBtn.title = "Додати квартиру"; + addBtn.type = "button"; + addBtn.onclick = () => Editor.house.apartments.addApartment(entrance, newFloors); + addBtn.innerHTML = ``; + + const infoDiv = document.createElement('div'); + infoDiv.className = "floor-info"; + infoDiv.append(h2, addBtn); + + + + // Блок квартири + const apartmentBlock = document.createElement('div'); + apartmentBlock.className = "apartment"; + apartmentBlock.id = `apartment-${entrance}-${data.id}`; + + const input = document.createElement('input'); + input.type = "text"; + input.value = numApartments; + input.id = `apartment-input-${entrance}-${data.id}`; + input.onchange = () => Editor.house.apartments.editApartment(entrance, data.id); + + const delBtn = document.createElement('button'); + delBtn.type = "button"; + delBtn.title = "Видалити квартиру"; + delBtn.innerHTML = ``; + delBtn.onclick = () => Editor.house.apartments.deleteApartment(entrance, data.id); + apartmentBlock.append(input, delBtn); + + + floorDiv.appendChild(apartmentBlock); + floorDiv.appendChild(infoDiv); + + + + entranceBlock.insertBefore(floorDiv, entranceBlock.querySelector(".floor")); + + // Оновлюємо список квартир + this.list[entrance].push({ + id: data.id, + entrance_id: Number(entrance), + apartment_number: this.list[entrance].length, + title: numApartments.toString(), + floors_number: newFloors + }); + + numApartments++; + const nextApartmentTitle = document.getElementById('next-apartment-title'); + if (nextApartmentTitle) nextApartmentTitle.value = numApartments; + + } catch (err) { + console.error("Помилка при додаванні поверху:", err); + } + }, + + async addApartment(entrance, floor) { + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}/apartments/${entrance}`; + + try { + const response = await fetch(URL, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + apartment_number: this.list[entrance].length, + title: numApartments.toString(), + floors_number: Number(floor) + }) + }); + + const data = await response.json(); + console.log(data); + + // Оновлюємо список квартир + this.list[entrance].push({ + id: data.id, + entrance_id: Number(entrance), + apartment_number: this.list[entrance].length, + title: numApartments.toString(), + floors_number: Number(floor) + }); + + const floorDiv = document.getElementById(`floor-${entrance}-${floor}`); + + // Створюємо блок нової квартири + const apartmentDiv = document.createElement('div'); + apartmentDiv.className = "apartment"; + apartmentDiv.id = `apartment-${entrance}-${data.id}`; + + const input = document.createElement('input'); + input.type = "text"; + input.value = numApartments; + input.id = `apartment-input-${entrance}-${data.id}`; + input.onchange = () => Editor.house.apartments.editApartment(entrance, data.id); + + const delBtn = document.createElement('button'); + delBtn.type = "button"; + delBtn.title = "Видалити квартиру"; + delBtn.innerHTML = ``; + delBtn.onclick = () => Editor.house.apartments.deleteApartment(entrance, data.id); + + apartmentDiv.append(input, delBtn); + floorDiv.insertBefore(apartmentDiv, floorDiv.querySelector(".floor-info")); + + numApartments++; + const nextApartmentTitle = document.getElementById('next-apartment-title'); + if (nextApartmentTitle) nextApartmentTitle.value = numApartments; + + } catch (err) { + console.error("Помилка при додаванні квартири:", err); + } + }, + + async editApartment(entrance, apartment) { + console.log(entrance, apartment); + + const input = document.getElementById(`apartment-input-${entrance}-${apartment}`); + if (!input) return; + + const newTitle = input.value; + + // Оновлюємо локальний список квартир + const pos = this.list[entrance].findIndex(e => e.id === Number(apartment)); + if (pos === -1) return; + + this.list[entrance][pos].title = newTitle; + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}/apartments/${entrance}`; + + try { + const response = await fetch(URL, { + method: 'PUT', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + id: this.list[entrance][pos].id, + title: this.list[entrance][pos].title, + status: this.list[entrance][pos].status, + description: this.list[entrance][pos].description + }) + }); + + const data = await response.json(); + console.log(data); + + } catch (err) { + console.error("Помилка при редагуванні квартири:", err); + } + }, + + async deleteApartment(entrance, apartment) { + const pos = this.list[entrance].findIndex(e => e.id === Number(apartment)); + if (pos === -1) return; + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}/apartments/${entrance}`; + + try { + const response = await fetch(URL, { + method: 'DELETE', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ id: this.list[entrance][pos].id }) + }); + + const data = await response.json(); + console.log(data); + + // Видаляємо елемент з DOM + const apartmentBlock = document.getElementById(`apartment-${entrance}-${apartment}`); + if (apartmentBlock) apartmentBlock.remove(); + + // Оновлюємо локальний список + this.list[entrance].splice(pos, 1); + + // Оновлюємо номер наступної квартири + numApartments = Math.max(0, numApartments - 1); + const nextApartmentTitle = document.getElementById('next-apartment-title'); + if (nextApartmentTitle) nextApartmentTitle.value = numApartments; + + } catch (err) { + console.error("Помилка при видаленні квартири:", err); + } + }, + + editNum(el) { numApartments = Number(el.value) }, + }, + }, + osm: { init() { + const center = Editor.info.geo; + const zoom = Editor.info.zoom; + const googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', { subdomains: ['mt0', 'mt1', 'mt2', 'mt3'] }); @@ -104,32 +783,28 @@ const Editor = { if (!map) { houseGroup = new L.FeatureGroup(); - splitPolygonsGroup = new L.FeatureGroup(); - RectangleGroup = new L.FeatureGroup(); - entransePolygonsGroup = new L.FeatureGroup(); - entranseNumPolygonsGroup = new L.FeatureGroup(); + homesteadGroup = new L.FeatureGroup(); + buildingGroup = new L.FeatureGroup(); + pointsGroup = new L.FeatureGroup(); map = L.map('map', { - renderer: L.canvas(), zoom: 17, - layers: [googleHybrid, osm, mytile, houseGroup, - entransePolygonsGroup, entranseNumPolygonsGroup, - splitPolygonsGroup, RectangleGroup], + renderer: L.canvas(), center, zoom, + layers: [googleHybrid, osm, mytile, houseGroup, homesteadGroup, buildingGroup, pointsGroup], zoomControl: false }); + L.control.layers( { "Google Hybrid": googleHybrid, "OpenStreetMap": osm, "Territory Map": mytile }, { - "Будинки": houseGroup, - "Під'їзди": entransePolygonsGroup, - "Номера під'їздів": entranseNumPolygonsGroup, - "Слой редагування": splitPolygonsGroup, - "Слой лінійки": RectangleGroup + "Багатоповерхові будинки": houseGroup, + "Житлові райони": homesteadGroup, + "Приватні будинки": buildingGroup, + "Точки на карті": pointsGroup }, { position: 'bottomright' } ).addTo(map); - map.pm.setLang("ua"); map.pm.addControls({ position: 'bottomright', drawCircleMarker: false, @@ -140,497 +815,386 @@ const Editor = { drawText: false, drawMarker: false, cutPolygon: false, - tooltips: false + tooltips: false, + editMode: true, + dragMode: true, }); - map.pm.toggleControls(); - map.pm.setGlobalOptions({ layerGroup: splitPolygonsGroup }); - } - } - }, - - entrances: { - list: [], - - async setHTML(id) { - this.list = await Editor.loadAPI(`${CONFIG.api}house/${id}/entrances`); - - entransePolygonsGroup.clearLayers(); - entranseNumPolygonsGroup.clearLayers(); - - const listArea = document.getElementById('list-area'); - if (!listArea) return; - - listArea.innerHTML = ""; - - for (const element of this.list) { - // Блок area - const divArea = document.createElement('div'); - divArea.className = "block-area"; - divArea.id = `block-area-${element.id}`; - - const h3 = document.createElement('h3'); - h3.textContent = element.title; - - const addBtn = document.createElement('button'); - addBtn.className = "addFloors"; - addBtn.type = "button"; - addBtn.title = "Додати поверх"; - addBtn.dataset.entranceId = element.id; - addBtn.innerHTML = ``; - addBtn.setAttribute(`onclick`, `Editor.apartments.addFloors("${element.id}")`); - - const innerArea = document.createElement('div'); - innerArea.id = `area-${element.id}`; - - divArea.append(h3, addBtn, innerArea); - listArea.appendChild(divArea); - - // Завантажуємо квартири для ентрансів - Editor.apartments.setHTML(element.id); - } - } - }, - - apartments: { - list: {}, - - async setHTML(id) { - this.list[id] = await Editor.loadAPI(`${CONFIG.api}apartments/${id}`); - const area = document.getElementById(`area-${id}`); - if (!area) return; - - // Унікальні поверхи - const uniqueFloors = [...new Set(this.list[id].map(a => a.floors_number))].sort((a, b) => a - b); - - // Створюємо блоки поверхів - for (const num of uniqueFloors) { - const div = document.createElement('div'); - div.className = "block-apartments-floors"; - div.id = `floors-${id}-${num}`; - - const h2 = document.createElement('h2'); - h2.textContent = `Поверх ${num}`; - div.appendChild(h2); - - const addBtn = document.createElement('button'); - addBtn.className = "addApartment"; - addBtn.id = `buttonApartment-${id}-${num}`; - addBtn.type = "button"; - addBtn.title = "Додати квартиру"; - addBtn.dataset.area = id; - addBtn.dataset.floors = num; - addBtn.innerHTML = ``; - addBtn.setAttribute(`onclick`, `Editor.apartments.addApartment("${id}", "${num}")`); + map.pm.toggleControls() - div.appendChild(addBtn); - area.prepend(div); + // Событие после завершения рисования + map.on('pm:create', e => { + const layer = e.layer; + + let LatLngs = layer.getLatLngs(); + LatLngs[0].push(LatLngs[0][0]); + + Editor.info.points.push(LatLngs); + let geo = this.center(layer.getLatLngs()); + + const house = layer; // сохраняем именно слой + + if (Editor.info.type === 'house') { + houseGroup.addLayer(house); + Editor.info.points_number.push(this.center(layer.getLatLngs())); + } else if (Editor.info.type === 'homestead') { + homesteadGroup.addLayer(house); + } + + house.bindPopup(` + Координати: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)}
+ + `); + + // при открытии popup вешаем обработчик удаления + house.on('popupopen', (e) => { + if (Editor.homestead.building.editing) { + house.closePopup(); + return; + } + + const btn = e.popup.getElement().querySelector('.map_dell'); + if (btn) { + btn.addEventListener('click', () => { + Editor.osm.delete(house); + }); + } + }); + + Editor.osm.autoZoom(Editor.info.points); + }); + + map.pm.setLang("ua"); } - // Сортуємо квартири за назвою - this.list[id].sort((a, b) => b.title - a.title); - - // Створюємо блоки квартир - for (const apartment of this.list[id]) { - const floorsBlock = document.getElementById(`floors-${id}-${apartment.floors_number}`); - if (!floorsBlock) continue; - - const div = document.createElement('div'); - div.className = "block-apartments-number"; - div.id = `block-apartments-${id}-${apartment.id}`; - - const input = document.createElement('input'); - input.type = "text"; - input.value = apartment.title; - input.id = `apartament-${id}-${apartment.id}`; - input.dataset.area = id; - input.dataset.apartment = apartment.id; - input.setAttribute(`onclick`, `Editor.apartments.editApartment("${id}", "${apartment.id}")`); - - const delBtn = document.createElement('button'); - delBtn.type = "button"; - delBtn.dataset.area = id; - delBtn.dataset.apartment = apartment.id; - delBtn.innerHTML = ``; - delBtn.setAttribute(`onclick`, `Editor.apartments.deleteApartment("${id}", "${apartment.id}")`); - - div.append(input, delBtn); - floorsBlock.prepend(div); - - numApartments++; - } - - const nextApartmentTitle = document.getElementById('next-apartment-title'); - if (nextApartmentTitle) nextApartmentTitle.value = numApartments; - }, - - async addFloors(area) { - const areaBlock = document.getElementById(`area-${area}`); - const uniqueFloors = [...new Set(this.list[area].map(obj => obj.floors_number))]; - const newFloors = uniqueFloors.length + 1; - - const uuid = localStorage.getItem('uuid'); - const URL = `${CONFIG.api}/apartments/${area}`; - - try { - const response = await fetch(URL, { - method: 'POST', - headers: { - "Content-Type": "application/json", - "Authorization": uuid - }, - body: JSON.stringify({ - apartment_number: this.list[area].length, - title: numApartments.toString(), - floors_number: newFloors - }) - }); - const data = await response.json(); - console.log(data); - - // Створюємо блок поверху - const div = document.createElement('div'); - div.className = "block-apartments-floors"; - div.id = `floors-${area}-${newFloors}`; - - // Заголовок поверху - const h2 = document.createElement('h2'); - h2.textContent = `Поверх ${newFloors}`; - div.appendChild(h2); - - // Блок квартири - const apartmentBlock = document.createElement('div'); - apartmentBlock.className = "block-apartments-number"; - apartmentBlock.id = `block-apartments-${area}-${data.id}`; - - const input = document.createElement('input'); - input.type = "text"; - input.value = numApartments; - input.id = `apartament-${area}-${data.id}`; - input.onchange = () => Editor.apartments.editApartment(area, data.id); - - const delBtn = document.createElement('button'); - delBtn.type = "button"; - delBtn.innerHTML = ``; - delBtn.onclick = () => Editor.apartments.deleteApartment(area, data.id); - - apartmentBlock.append(input, delBtn); - div.appendChild(apartmentBlock); - - // Кнопка додати квартиру - const addBtn = document.createElement('button'); - addBtn.className = "addApartment"; - addBtn.id = `buttonApartment-${area}-${newFloors}`; - addBtn.title = "Додати квартиру"; - addBtn.type = "button"; - addBtn.onclick = () => Editor.apartments.addApartment(area, newFloors); - addBtn.innerHTML = ``; - div.appendChild(addBtn); - - areaBlock.prepend(div); - - // Оновлюємо список квартир - this.list[area].push({ - id: data.id, - entrance_id: Number(area), - apartment_number: this.list[area].length, - title: numApartments.toString(), - floors_number: newFloors - }); - - numApartments++; - const nextApartmentTitle = document.getElementById('next-apartment-title'); - if (nextApartmentTitle) nextApartmentTitle.value = numApartments; - - } catch (err) { - console.error("Помилка при додаванні поверху:", err); - } - }, - - async addApartment(area, floors) { - const uuid = localStorage.getItem('uuid'); - const URL = `${CONFIG.api}/apartments/${area}`; - - try { - const response = await fetch(URL, { - method: 'POST', - headers: { - "Content-Type": "application/json", - "Authorization": uuid - }, - body: JSON.stringify({ - apartment_number: this.list[area].length, - title: numApartments.toString(), - floors_number: Number(floors) - }) - }); - - const data = await response.json(); - console.log(data); - - // Оновлюємо список квартир - this.list[area].push({ - id: data.id, - entrance_id: Number(area), - apartment_number: this.list[area].length, - title: numApartments.toString(), - floors_number: Number(floors) - }); - - const floorsBlock = document.getElementById(`floors-${area}-${floors}`); - - // Видаляємо стару кнопку додати квартиру - const oldButton = document.getElementById(`buttonApartment-${area}-${floors}`); - if (oldButton) oldButton.remove(); - - // Створюємо блок нової квартири - const apartmentDiv = document.createElement('div'); - apartmentDiv.className = "block-apartments-number"; - apartmentDiv.id = `block-apartments-${area}-${data.id}`; - - const input = document.createElement('input'); - input.type = "text"; - input.value = numApartments; - input.id = `apartament-${area}-${data.id}`; - input.onchange = () => Editor.apartments.editApartment(area, data.id); - - const delBtn = document.createElement('button'); - delBtn.type = "button"; - delBtn.innerHTML = ``; - delBtn.onclick = () => Editor.apartments.deleteApartment(area, data.id); - - apartmentDiv.append(input, delBtn); - floorsBlock.appendChild(apartmentDiv); - - // Додаємо кнопку "додати квартиру" знову - const addBtn = document.createElement('button'); - addBtn.className = "addApartment"; - addBtn.id = `buttonApartment-${area}-${floors}`; - addBtn.title = "Додати квартиру"; - addBtn.type = "button"; - addBtn.onclick = () => Editor.apartments.addApartment(area, floors); - addBtn.innerHTML = ``; - floorsBlock.appendChild(addBtn); - - numApartments++; - const nextApartmentTitle = document.getElementById('next-apartment-title'); - if (nextApartmentTitle) nextApartmentTitle.value = numApartments; - - } catch (err) { - console.error("Помилка при додаванні квартири:", err); - } - }, - - async editApartment(area, apartment) { - const input = document.getElementById(`apartament-${area}-${apartment}`); - if (!input) return; - - const newTitle = input.value; - - // Оновлюємо локальний список квартир - const pos = this.list[area].findIndex(e => e.id === Number(apartment)); - if (pos === -1) return; - - this.list[area][pos].title = newTitle; - - const uuid = localStorage.getItem('uuid'); - const URL = `${CONFIG.api}/apartments/${area}`; - - try { - const response = await fetch(URL, { - method: 'PUT', - headers: { - "Content-Type": "application/json", - "Authorization": uuid - }, - body: JSON.stringify({ - id: this.list[area][pos].id, - title: this.list[area][pos].title, - status: this.list[area][pos].status, - description: this.list[area][pos].description - }) - }); - - const data = await response.json(); - console.log(data); - - } catch (err) { - console.error("Помилка при редагуванні квартири:", err); - } - }, - - async deleteApartment(area, apartment) { - const pos = this.list[area].findIndex(e => e.id === Number(apartment)); - if (pos === -1) return; - - const uuid = localStorage.getItem('uuid'); - const URL = `${CONFIG.api}/apartments/${area}`; - - try { - const response = await fetch(URL, { - method: 'DELETE', - headers: { - "Content-Type": "application/json", - "Authorization": uuid - }, - body: JSON.stringify({ id: this.list[area][pos].id }) - }); - - const data = await response.json(); - console.log(data); - - // Видаляємо елемент з DOM - const apartmentBlock = document.getElementById(`block-apartments-${area}-${apartment}`); - if (apartmentBlock) apartmentBlock.remove(); - - // Оновлюємо локальний список - this.list[area].splice(pos, 1); - - // Оновлюємо номер наступної квартири - numApartments = Math.max(0, numApartments - 1); - const nextApartmentTitle = document.getElementById('next-apartment-title'); - if (nextApartmentTitle) nextApartmentTitle.value = numApartments; - - } catch (err) { - console.error("Помилка при видаленні квартири:", err); - } - }, - - editNum(el) { numApartments = Number(el.value) }, - }, - - homestead: { - id: null, list: [], editing: false, - - async init(id) { - this.editing = false; - this.id = id; - setLeafletCursor('pointer'); - - document.getElementById('homestead-editing').style.display = ""; - - // Завантаження даних будівлі - this.list = await Editor.loadAPI(`${CONFIG.api}building/${id}`); - - // Обробник кліку на карту - houseGroup.on('click', e => { - if (e.layer instanceof L.Marker || !this.editing) return; - - const { lat, lng } = e.latlng; - console.log(`Координати: ${lat.toFixed(5)}, ${lng.toFixed(5)}`); - - setLeafletCursor('progress'); - this.editing.editing = false; - - this.addBuilding({ geo: e.latlng, title: this.list.length + 1 }); - this.list.push({}); - }); - - // Встановлюємо вид карти - const viewLatLng = Editor.info.list.geo?.lat - ? [Editor.info.list.geo.lat, Editor.info.list.geo.lng] - : [Editor.info.list.points[0][0][0].lat, Editor.info.list.points[0][0][0].lng]; - map.setView(viewLatLng, Editor.info.list.zoom); - - - for (const element of this.list) { - // Додаємо маркер на карту - const redDot = L.divIcon({ - className: "leaflet_drop", - html: `
`, - iconSize: [16, 16], - iconAnchor: [8, 8] - }); - - L.marker(element.geo, { icon: redDot }) - .addTo(houseGroup) - .bindPopup(` - Точка: ${element.id}
- Координати: ${element.geo.lat.toFixed(5)}, ${element.geo.lng.toFixed(5)}
- - `); - } - }, - - async addBuilding({ geo, title }) { - const uuid = localStorage.getItem('uuid'); - const URL = `${CONFIG.api}building/${this.id}`; - - try { - const response = await fetch(URL, { - method: 'POST', - headers: { - "Content-Type": "application/json", - "Authorization": uuid - }, - body: JSON.stringify({ title, geo }) - }); - const data = await response.json(); - console.log(data); - - // Додаємо маркер на карту - const redDot = L.divIcon({ - className: "leaflet_drop", - html: `
`, - iconSize: [16, 16], - iconAnchor: [8, 8] - }); - - const marker = L.marker(geo, { icon: redDot }).addTo(houseGroup); - marker.bindPopup(` - Точка: ${data.id}
- Координати: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)}
- + houseGroup.clearLayers(); + homesteadGroup.clearLayers(); + buildingGroup.clearLayers(); + pointsGroup.clearLayers(); + + for (let i = 0; i < Editor.info.points.length; i++) { + const LatLngs = Editor.info.points[i]; + + // Создаем L.polygon + const polyOptions = Editor.info.type === 'homestead' + ? { color: "#f2bd53", fillColor: "#f2bd53", fillOpacity: 0.4, dashArray: '5,10' } + : { color: "#585858", fillColor: "#f2bd53", fillOpacity: 0.8 }; + + const house = L.polygon(LatLngs, polyOptions); + + // Добавляем в нужную группу + if (Editor.info.type === 'house') { + houseGroup.addLayer(house); + } else if (Editor.info.type === 'homestead') { + homesteadGroup.addLayer(house); + } + + house.bindPopup(` + Координати: ${Editor.info.geo.lat.toFixed(5)}, ${Editor.info.geo.lng.toFixed(5)}
+ `); - setLeafletCursor('crosshair'); - this.editing = true; - - } catch (err) { - console.error("Помилка при додаванні будівлі:", err); - } - }, - - async delleteBuilding({ id }) { - const uuid = localStorage.getItem('uuid'); - const URL = `${CONFIG.api}building/${id}`; - - try { - const response = await fetch(URL, { - method: 'DELETE', - headers: { - "Content-Type": "application/json", - "Authorization": uuid + // при открытии popup вешаем обработчик удаления + house.on('popupopen', (e) => { + if (Editor.homestead.building.editing) { + house.closePopup(); + return; } - }); - const data = await response.json(); - // Видаляємо елемент списку та маркер - const el = document.getElementById(`redDot_${id}`); - if (el) el.remove(); - - const block = document.getElementById(`Building_${id}`); - if (block) block.remove(); - - houseGroup.eachLayer(layer => { - if (layer instanceof L.Marker && layer.getPopup()?.getContent().includes(`Точка: ${id}`)) { - houseGroup.removeLayer(layer); + const btn = e.popup.getElement().querySelector('.map_dell'); + if (btn) { + btn.addEventListener('click', () => { + Editor.osm.delete(house); + }); } }); - } catch (err) { - console.error("Помилка при видаленні будівлі:", err); + Editor.osm.autoZoom(Editor.info.points); } }, - editing_mode() { - const btn = document.getElementById('homestead-editing'); - this.editing = !this.editing; - setLeafletCursor(this.editing ? 'crosshair' : 'pointer'); - btn.innerHTML = this.editing - ? ` ` - : ` `; - if (this.editing) alert("Натискаючи на карту будуть створюватись нові точки (будинки)"); + newPoligon() { + if (Editor.info.type === 'house') { + map.pm.enableDraw('Polygon', { + snappable: true, + snapDistance: 20, + layerGroup: houseGroup, + templineStyle: { + color: '#585858', + radius: 500, + fillOpacity: 0.4, + dashArray: '5, 10', + dashOffset: '20', + }, + hintlineStyle: { + color: '#C14D4D', + dashArray: '5, 10' + }, + pathOptions: { + color: "#585858", + fillColor: "#f2bd53", + fillOpacity: 0.8 + } + }); + } else if (Editor.info.type === 'homestead') { + map.pm.enableDraw('Polygon', { + snappable: true, + snapDistance: 20, + layerGroup: houseGroup, + templineStyle: { + color: '#585858', + radius: 500, + fillOpacity: 0.3, + dashArray: '5, 10', + dashOffset: '20', + }, + hintlineStyle: { + color: '#C14D4D', + dashArray: '5, 10' + }, + pathOptions: { + color: "#f2bd53", + fillColor: "#f2bd53", + radius: 500, + fillOpacity: 0.3, + dashArray: '5, 10' + } + }); + } }, + + async autoPoligon(IDs) { + if (!IDs) return; + + const ids_list = IDs.replace(/\s+/g, "").split(','); + Editor.info.osm_id = ids_list; + + houseGroup.clearLayers(); + homesteadGroup.clearLayers(); + + Editor.info.points = []; + Editor.info.points_number = []; + Editor.info.geo = {} + + + // 1006306041, 1006306065 + + + for (let i = 0; i < ids_list.length; i++) { + const element = await Editor.osm.getOSM(Editor.info.osm_id[i]); + + // Преобразуем координаты в LatLng + const LatLngs = [[]]; + element[0].forEach(feature => LatLngs[0].push({ lat: feature.lat, lng: feature.lng })); + + // Замыкаем полигон + // if (LatLngs[0][0] && LatLngs[0][0] !== LatLngs[0][LatLngs[0].length - 1]) { + // LatLngs[0].push(LatLngs[0][0]); + // } + + // Считаем центр + const center = this.center(LatLngs); + + // Сохраняем в points / points_number + Editor.info.points.push(LatLngs); + Editor.info.points_number.push(center); + + // Создаем L.polygon + const polyOptions = Editor.info.type === 'homestead' + ? { color: "#f2bd53", fillColor: "#f2bd53", fillOpacity: 0.4, dashArray: '5,10' } + : { color: "#585858", fillColor: "#f2bd53", fillOpacity: 0.8 }; + + const house = L.polygon(LatLngs, polyOptions); + + // Добавляем в нужную группу + if (Editor.info.type === 'house') { + houseGroup.addLayer(house); + } else if (Editor.info.type === 'homestead') { + homesteadGroup.addLayer(house); + } + + // Bind popup с кнопкой удаления + house.bindPopup(` + Координати: ${center.lat.toFixed(5)}, ${center.lng.toFixed(5)}
+ + `); + + house.on('popupopen', (e) => { + if (Editor.homestead.building.editing) { + house.closePopup(); + return; + } + + const btn = e.popup.getElement().querySelector('.map_dell'); + if (btn) { + btn.addEventListener('click', () => { + Editor.osm.delete(house); + }); + } + }); + } + + Editor.osm.autoZoom(Editor.info.points); + }, + + center(geo) { + // Получаем координаты полигона Leaflet + let latlngs = geo[0]; + + // Преобразуем в формат GeoJSON для Turf + const coordinates = latlngs.map(ll => [ll.lng, ll.lat]); + const polygonGeoJSON = { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [coordinates] + } + }; + + // Находим центроид + const centroid = turf.centroid(polygonGeoJSON); + + latlngs = { lat: centroid.geometry.coordinates[1], lng: centroid.geometry.coordinates[0] } + + return latlngs; + }, + + autoZoom(polygons) { + if (!polygons || !polygons.length) return; + + const allBounds = []; + + polygons.forEach(polygon => { + const ring = polygon[0]; + if (!ring || ring.length < 3) return; + + const coords = ring.map(p => [p.lng, p.lat]); + if (coords[0][0] !== coords[coords.length - 1][0] || coords[0][1] !== coords[coords.length - 1][1]) { + coords.push(coords[0]); + } + + const polygonGeoJSON = turf.polygon([coords]); + const bbox = turf.bbox(polygonGeoJSON); + const bounds = L.latLngBounds( + [bbox[1], bbox[0]], + [bbox[3], bbox[2]] + ); + + allBounds.push(bounds); + }); + + if (!allBounds.length) return; + + // Если один полигон, просто fitBounds на него + if (allBounds.length === 1) { + map.fitBounds(allBounds[0]); + } else { + // Несколько полигонов → объединяем bounds + let finalBounds = allBounds[0]; + for (let i = 1; i < allBounds.length; i++) { + finalBounds = finalBounds.extend(allBounds[i]); + } + map.fitBounds(finalBounds); + } + + + if (map.getZoom() > 18) map.setZoom(18); + + setTimeout(() => { + Editor.info.zoom = map.getZoom(); + Editor.info.geo = map.getCenter(); + }, 200) + }, + + delete(house) { + // убрать слой с карты + if (Editor.info.type === 'house') { + houseGroup.removeLayer(house); + } else if (Editor.info.type === 'homestead') { + homesteadGroup.removeLayer(house); + } + + // найти индекс полигона в points + const target_1 = house.getLatLngs(); // вершины полигона + const target_2 = house.getLatLngs(); + target_1[0].push(target_1[0][0]) + + + const index = Editor.info.points.findIndex( + poly => { + if(JSON.stringify(poly[0]) === JSON.stringify(target_1[0])) return true + else if(JSON.stringify(poly[0]) === JSON.stringify(target_2[0])) return true + return false + } + ); + + + console.log("index ", index); + console.log(Editor.info.points); + + + + if (index !== -1) { + // удалить из points и points_number по индексу + Editor.info.points.splice(index, 1); + Editor.info.points_number.splice(index, 1); + } + + Editor.osm.autoZoom(Editor.info.points); + }, + + async getOSM(wayId) { + const overpassUrl = `https://overpass-api.de/api/interpreter?data=[out:json];way(${wayId});(._;>;);out;`; + + return await fetch(overpassUrl) + .then(response => response.json()) + .then(data => { + const nodes = new Map(); + + data.elements.forEach(el => { + if (el.type === "node") { + nodes.set(el.id, { lat: el.lat, lng: el.lon }); + } + }); + + const way = data.elements.find(el => el.type === "way"); + if (way) { + const coordinates = way.nodes.map(nodeId => nodes.get(nodeId)); + + return [coordinates]; + } else { + console.error("Way не найден!"); + } + }) + .catch(error => console.error("Ошибка запроса:", error)); + }, + }, + + async save() { + console.log(Editor.info); + + setLeafletCursor('pointer'); + Editor.homestead.building.editing = false; + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}${Editor.info.type}/${Editor.info.id}`; + + try { + const response = await fetch(URL, { + method: 'PUT', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify(Editor.info) + }); + + const data = await response.json(); + console.log(data); + + } catch (err) { + console.error("Помилка при редагуванні під'їзду:", err); + } + } } \ No newline at end of file diff --git a/web/lib/pages/editor/style.css b/web/lib/pages/editor/style.css index 14f2393..59095c6 100644 --- a/web/lib/pages/editor/style.css +++ b/web/lib/pages/editor/style.css @@ -6,21 +6,11 @@ margin: 20px 20px 0 20px; } -.page-editor form>button { - border-radius: 6px; - background: var(--PrimaryColor); - color: var(--PrimaryColorText); - width: 100%; - height: 40px; - font-size: var(--FontSize3); - font-weight: 400; - margin: 20px 0; - text-transform: uppercase; -} - -.page-editor details { +.page-editor>#part-1, +.page-editor>#part-2, +.page-editor>#part-3 { border-radius: 10px; - width: 100%; + width: calc(100% - 40px); display: flex; flex-direction: column; align-items: stretch; @@ -29,36 +19,169 @@ color: var(--ColorThemes3); border: 1px solid var(--ColorThemes2); box-shadow: var(--shadow-l1); + padding: 0 20px; + position: relative; } -.page-editor>details[disabled] summary, -.page-editor>details.disabled summary { - pointer-events: none; - user-select: none; -} - -.page-editor>details summary::-webkit-details-marker, -.page-editor>details summary::marker { - display: none; - content: ""; -} - -.page-editor summary { +.page-editor>#part-1>h1, +.page-editor>#part-2>h1, +.page-editor>#part-3>h1 { width: calc(100% - 40px); - cursor: pointer; color: var(--ColorThemes3); border-radius: var(--border-radius); font-size: var(--FontSize5); font-weight: 300; - padding: 20px; + padding: 20px 0; position: relative; } -.page-editor summary span { +.page-editor>#part-1>h1>span, +.page-editor>#part-2>h1>span, +.page-editor>#part-3>h1>span { font-weight: 500; } -.page-editor summary input { +.page-editor>#part-1>form>div { + width: 100%; + display: flex; + margin: 0 0 20px 0; + align-items: flex-start; + flex-direction: column; +} + +.page-editor>#part-1>form>div>label { + display: flex; + justify-content: center; + flex-direction: column; + font-size: var(--FontSize1); + font-weight: 500; + margin-bottom: 5px; +} + +.page-editor>#part-1>form>div>input { + width: calc(100% - 10px); + min-width: 140px; + padding: 0 5px; + border-radius: 6px; + height: 30px; + background: var(--ColorThemes0); + color: var(--ColorThemes3); + font-size: var(--FontSize2); +} + +.page-editor>#part-1>form>button, +.page-editor>#part-2>button, +.page-editor>#part-3>button { + border-radius: 6px; + background: var(--PrimaryColor); + color: var(--PrimaryColorText); + width: 100%; + height: 40px; + font-size: var(--FontSize3); + font-weight: 400; + margin: 0 0 20px 0; + text-transform: uppercase; +} + +.page-editor>#part-2>.osm-info { + padding-bottom: 20px; + display: flex; + flex-direction: column; + align-items: center; +} + +.page-editor>#part-2>.osm-info>div { + display: flex; + align-items: center; + width: 100%; +} + +.page-editor>#part-2>.osm-info>div { + width: 100%; + display: flex; + align-items: flex-start; + flex-direction: column; +} + +.page-editor>#part-2>.osm-info>div>label { + display: flex; + justify-content: center; + flex-direction: column; + font-size: var(--FontSize1); + font-weight: 500; + margin-bottom: 5px; +} + +.page-editor>#part-2>.osm-info>div>div { + display: flex; + align-items: center; + width: 100%; +} + +.page-editor>#part-2>.osm-info>div>div>input { + width: calc(100% - 10px); + min-width: 140px; + padding: 0 5px; + border-radius: 6px; + height: 30px; + background: var(--ColorThemes0); + color: var(--ColorThemes3); + font-size: var(--FontSize2); +} + +.page-editor>#part-2>.osm-info>div>div>a { + height: 26px; + width: 26px; + margin-left: 10px; +} + +.page-editor>#part-2>.osm-info>div>div>a>svg { + height: 26px; + width: 26px; + fill: var(--ColorThemes3); +} + +.page-editor>#part-2>.osm-info>span { + font-size: var(--FontSize5); + margin: 10px; + color: var(--ColorThemes3); +} + +.page-editor>#part-2>.osm-info>button { + display: flex; + align-items: center; + justify-content: center; + height: 30px; + width: 100%; + cursor: pointer; + transition: 0.15s ease-in; + color: var(--PrimaryColorText); + background: var(--PrimaryColor); + border-radius: 6px; + flex-direction: row; + z-index: 2; + font-size: var(--FontSize3); +} + +.page-editor>#part-2>.block-map { + width: 100%; + height: 500px; + border-radius: 6px; + overflow: hidden; + position: relative; + background: var(--ColorThemes1); + color: var(--ColorThemes3); + border: 1px solid var(--ColorThemes2); + box-shadow: var(--shadow-l1); + margin-bottom: 20px; +} + +.page-editor>#part-2>.block-map>#map { + width: 100%; + height: 100%; +} + +.page-editor>#part-3>input { font-weight: 500; position: absolute; right: 0; @@ -72,177 +195,49 @@ width: 40px; } -.page-editor #info-form, -.page-editor #map-form, -.page-editor #area-form { - padding: 0 20px; -} - -#details-info-type { - display: flex; - align-items: center; - justify-content: center; -} - -#details-info-type>.tabs { - display: flex; - position: relative; - background-color: var(--ColorThemes0); - padding: 4px; - border-radius: 6px; - width: calc(100% - 8px); - z-index: 2; -} - -#details-info-type>.tabs>input[type="radio"] { - display: none; -} - -#details-info-type>.tabs>.tab { - display: flex; - align-items: center; - justify-content: center; - height: 40px; - width: calc(100% / 3); - cursor: pointer; - padding: 0 15px; - transition: 0.15s ease-in; - color: var(--ColorThemes3); - fill: var(--ColorThemes3); - flex-direction: row; - z-index: 2; -} - -#details-info-type>.tabs>.tab>svg { - width: 20px; - height: 20px; -} - -#details-info-type>.tabs>.tab>span { - margin-left: 6px; - font-size: var(--FontSize1); - font-weight: 400; -} - -#details-info-type>.tabs>input[type="radio"]:checked+label { - color: var(--PrimaryColorText); - fill: var(--PrimaryColorText); -} - -#details-info-type>.tabs>.glider { - position: absolute; - display: flex; - height: 40px; - width: calc((100% - 8px) / 3); - background-color: var(--PrimaryColor); - z-index: 1; - border-radius: 4px; - transition: 0.25s ease-out; -} - -@media (min-width: 601px) { - #details-info-type>.tabs>input[id="info-type-house"]:checked~.glider { - transform: translateX(0); - } - - #details-info-type>.tabs>input[id="info-type-homestead"]:checked~.glider { - transform: translateX(100%); - } - - #details-info-type>.tabs>input[id="info-type-points"]:checked~.glider { - transform: translateX(200%); - } -} - -@media (max-width: 600px) { - #details-info-type>.tabs { - flex-direction: column; - } - - #details-info-type>.tabs>.tab { - width: calc(100% - 8px); - padding: 0 4px; - } - - #details-info-type>.tabs>.glider { - width: calc(100% - 8px); - } - - #details-info-type>.tabs>input[id="info-type-house"]:checked~.glider { - transform: translateY(0); - } - - #details-info-type>.tabs>input[id="info-type-homestead"]:checked~.glider { - transform: translateY(100%); - } - - #details-info-type>.tabs>input[id="info-type-points"]:checked~.glider { - transform: translateY(200%); - } -} - -.page-editor .details-info-input { - width: 100%; - display: flex; - margin: 20px 0; - align-items: flex-start; - flex-direction: column; -} - -.page-editor .details-info-input label { - display: flex; - justify-content: center; - flex-direction: column; - font-size: var(--FontSize1); - font-weight: 500; - margin-bottom: 5px; - -} - -.page-editor .details-info-input input { - width: calc(100% - 10px); - min-width: 140px; - padding: 0 5px; - border-radius: 6px; - height: 30px; - background: var(--ColorThemes0); - color: var(--ColorThemes3); - font-size: var(--FontSize2); -} - -.page-editor #details-info-osm div { - display: flex; - align-items: center; - width: 100%; -} - -.page-editor #details-info-osm input { - width: calc(100% - 10px); - min-width: 140px; - padding: 0 5px; - border-radius: 6px; - height: 30px; -} - -.page-editor .details-info-input a { - height: 26px; - width: 26px; - margin-left: 10px; -} - -.page-editor .details-info-input a>svg { - height: 26px; - width: 26px; - fill: var(--ColorThemes3) -} - -.page-editor #list-area { +.page-editor>#part-3>#house { display: flex; overflow: auto; - margin: 15px 0 20px 0; } -.page-editor #list-area h3 { +.page-editor>#part-3>#house>button { + display: flex; + position: relative; + width: 34px; + min-width: 34px; + height: calc(100% - 10px); + background: var(--PrimaryColor); + color: var(--PrimaryColorText); + margin: 0 10px; + border-radius: 6px; + align-items: center; + justify-content: center; + font-size: 30px; + cursor: pointer; +} + +.page-editor>#part-3>#house>button>svg { + width: 20px; + height: 20px; + fill: var(--PrimaryColorText); + transform: rotate(45deg); +} + +.page-editor>#part-3>#house>.entrance { + min-height: 200px; + border: 1px solid var(--ColorThemes3); + border-style: dashed; + border-radius: 6px; + margin: 0 10px 10px 0; +} + +.page-editor>#part-3>#house>.entrance>.entrance-info>.entrance-header { + display: flex; + align-items: center; + min-width: 180px; +} + +.page-editor>#part-3>#house>.entrance>.entrance-info>.entrance-header>input { text-align: center; font-size: var(--FontSize5); font-weight: 400; @@ -251,18 +246,29 @@ color: var(--ColorThemes0); background: var(--ColorThemes3); border-radius: 4px; + width: calc(100% - 14px - 20px); } -.block-area { - min-height: 200px; - border: 1px solid var(--ColorThemes3); - border-style: dashed; - border-radius: 6px; - margin: 0 10px 10px 0; +.page-editor>#part-3>#house>.entrance>.entrance-info>.entrance-header>button { + display: flex; + margin: 10px 10px 10px 0px; + border-radius: 4px; + min-width: 33px; + min-height: 33px; + align-items: center; + justify-content: center; + cursor: pointer; } -.addFloors, -.addApartment { +.page-editor>#part-3>#house>.entrance>.entrance-info>.entrance-header>button>svg { + width: 16px; + height: 16px; + fill: #C14D4D; +} + + +.page-editor>#part-3>#house>.entrance>.entrance-info>button, +.page-editor>#part-3>#house>.entrance>.floor>.floor-info>button { display: flex; position: relative; width: 34px; @@ -277,15 +283,15 @@ cursor: pointer; } -.addFloors>svg, -.addApartment>svg { +.page-editor>#part-3>#house>.entrance>.entrance-info>button>svg, +.page-editor>#part-3>#house>.entrance>.floor>.floor-info>button>svg { width: 20px; height: 20px; fill: var(--PrimaryColorText); transform: rotate(45deg); } -.block-apartments-floors { +.page-editor>#part-3>#house>.entrance>.floor { position: relative; display: flex; width: calc(100% - 22px); @@ -294,7 +300,7 @@ border-radius: 4px; } -.block-apartments-floors h2 { +.page-editor>#part-3>#house>.entrance>.floor>.floor-info>h2 { position: absolute; width: 65px; right: -1px; @@ -308,7 +314,7 @@ text-align: center; } -.block-apartments-number { +.page-editor>#part-3>#house>.entrance>.floor>.apartment { display: flex; position: relative; width: 60px; @@ -321,7 +327,7 @@ justify-content: center; } -.block-apartments-number input { +.page-editor>#part-3>#house>.entrance>.floor>.apartment>input { width: 50px; height: 50px; font-size: var(--FontSize5); @@ -331,7 +337,7 @@ background: 0; } -.block-apartments-number button { +.page-editor>#part-3>#house>.entrance>.floor>.apartment>button { position: absolute; top: 0; right: 0; @@ -348,189 +354,45 @@ justify-content: center; } -.block-apartments-number button>svg { +.page-editor>#part-3>#house>.entrance>.floor>.apartment>button>svg { width: 16px; height: 16px; fill: var(--PrimaryColorText); } +.page-editor>#part-3>.info {} -.block-map { - width: 100%; - height: 500px; - border-radius: 6px; - overflow: hidden; - margin-bottom: 20px; - position: relative; - - background: var(--ColorThemes1); +.page-editor>#part-3>.info>p { + font-size: var(--FontSize2); color: var(--ColorThemes3); - border: 1px solid var(--ColorThemes2); - box-shadow: var(--shadow-l1); + opacity: 0.8; + font-style: oblique; } -#homestead-editing { - position: absolute; - height: 40px; - background: var(--PrimaryColor); - font-size: var(--FontSize5); - margin: 10px; - border: 0; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-weight: 500; - padding: 10px; +.page-editor>#part-3>.info>button { border-radius: 6px; - width: 40px; - z-index: 999; - right: 0; - box-shadow: var(--shadow-l1); -} - -#homestead-editing>svg { - width: 20px; - height: 20px; - fill: var(--PrimaryColorText); -} - -#map { - width: 100%; - height: 100%; -} - -.entranse_number { - left: -10px !important; - top: -10px !important; -} - -.markerEntranse { - background: hsl(0deg 0% 52.12% / 90%); - font-size: 24px; - border: 2px solid #676767; - border-radius: 2px; - display: flex !important; - justify-content: center; - align-items: center; - color: #fff; - min-width: 30px; - min-height: 30px; - height: 30px; - width: 30px; -} - -.editor-buttons { - margin-top: 15px; -} - -.editor-buttons>button { - width: 100%; - min-height: 30px; - margin: 5px 0; - border: 0; - color: var(--PrimaryColorText); - display: flex; - align-items: center; - justify-content: center; - background: var(--PrimaryColor); - text-transform: uppercase; - cursor: pointer; - border-radius: 6px; - font-weight: 400; - font-size: var(--FontSize1); -} - -.editor-buttons>div { - display: flex; - width: 100%; - margin: 10px 0; - height: 30px; - justify-content: space-between; -} - -.editor-buttons>div>button { background: var(--ColorThemes3); - color: var(--ColorThemes1); - width: 100%; - margin: 0; - border-radius: 6px; + width: fit-content; + height: 40px; + padding: 0 10px; + margin-bottom: 20px; text-transform: uppercase; - font-weight: 400; - font-size: var(--FontSize1); -} - -.page-editor #list-entranse, -.page-editor #list-homestead { - width: 100%; - min-height: 86px; display: flex; flex-direction: row; - flex-wrap: nowrap; - overflow-x: initial; - overflow-y: auto; - border: 1px solid var(--ColorThemes3); - background: 0; - border-radius: 6px; - border-style: dashed; - margin: 10px 0; - position: relative; -} - -.page-editor #list-entranse>.house, -.page-editor #list-homestead>.house { - display: flex; - position: relative; - width: 60px; - height: 60px; - min-width: 60px; - min-height: 60px; - background: var(--ColorThemes1); - border: 2px solid var(--PrimaryColor); - margin: 10px; - border-radius: 4px; align-items: center; justify-content: center; + cursor: pointer; } - -.page-editor #list-entranse>.house>input, -.page-editor #list-homestead>.house>input, -.page-editor #list-entranse>.house>p, -.page-editor #list-homestead>.house>p { - width: 50px; - height: 50px; - font-size: var(--FontSize5); - font-weight: 400; - text-align: center; - color: var(--ColorThemes3); - background: 0; - display: flex; - align-items: center; - justify-content: center; -} - -.page-editor #list-entranse>.house>button, -.page-editor #list-homestead>.house>button { - position: absolute; - top: 0; - right: 0; +.page-editor>#part-3>.info>button>svg { width: 20px; height: 20px; - border-radius: 0 4px 0 4px; - background: var(--PrimaryColor); - font-size: var(--FontSize5); - margin: -2px; - border: 0; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; + fill: var(--ColorThemes0); + margin-right: 10px; } -.page-editor #list-entranse>.house>button>svg, -.page-editor #list-homestead>.house>button>svg { - width: 16px; - height: 16px; - fill: var(--PrimaryColorText); +.page-editor>#part-3>.info>button>span { + font-size: var(--FontSize3); + font-weight: 400; + color: var(--ColorThemes0); } \ No newline at end of file diff --git a/web/lib/pages/editor_old/index.html b/web/lib/pages/editor_old/index.html new file mode 100644 index 0000000..8672af1 --- /dev/null +++ b/web/lib/pages/editor_old/index.html @@ -0,0 +1,89 @@ +
+
+ + Крок 1. Інформація про будинок, територію, точки на карті + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ Крок 2. Перегляд +
+ + +
+ +
+
+ +
+
+ + +
diff --git a/web/lib/pages/editor_old/script.js b/web/lib/pages/editor_old/script.js new file mode 100644 index 0000000..b6d627a --- /dev/null +++ b/web/lib/pages/editor_old/script.js @@ -0,0 +1,636 @@ +const Editor = { + async init(type, id) { + let html = await fetch('/lib/pages/editor/index.html').then((response) => response.text()); + app.innerHTML = html; + + map = ""; + houseGroup = ""; + entransePolygonsGroup = ""; + entranseNumPolygonsGroup = ""; + splitPolygonsGroup = ""; + RectangleGroup = ""; + numApartments = 1; + + Editor.info.setHTML(type, id); + }, + + async loadAPI(URL) { + let uuid = localStorage.getItem("uuid"); + return await fetch(URL, { + method: 'GET', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + } + }).then((response) => response.json()); + }, + + info: { + list: { + type: null, title: null, number: null, + points: [], points_number: [], point_icons: [], + geo: [], osm_id: [], settlement: [], description: null, + entrance: [], apartments: {} + }, + + async setHTML(type, id) { + const els = { + title: document.getElementById('info-address-title'), + number: document.getElementById('info-number-title'), + settlement: document.getElementById('info-settlement-title') + }; + + this.list = await Editor.loadAPI(`${CONFIG.api}${type}/${id}`); + Editor.info.list.type = type; + Editor.info.list.entrance = []; + Editor.info.list.apartments = {}; + + console.log(Editor.info.list); + + els.title.value = this.list.title; + els.number.value = this.list.number; + els.settlement.value = this.list.settlement; + + Editor.osm.init(); + this.setMap(); + + if (type == "house") { + Editor.entrances.setHTML(id); + document.getElementById('details-area').style.display = ""; + } else if (type == "homestead") { + Editor.homestead.init(id); + } + }, + + setMap() { + houseGroup.clearLayers(); + + for (let i = 0; i < Editor.info.list.points.length; i++) { + const element = Editor.info.list.points[i]; + + if (Editor.info.list.type == "homestead") { + map.setView([this.list.geo.lat, this.list.geo.lng], this.list.zoom); + + L.polygon(element, { + color: "#f2bd53", + radius: 500, + fillOpacity: 0.3, + dashArray: '20,15', + dashOffset: '20', + }).addTo(houseGroup); + } else if (Editor.info.list.type == "house") { + map.setView([this.list.geo.lat, this.list.geo.lng], this.list.zoom); + + L.polygon(element, { + color: "#585858", + fillColor: "#f2bd53", + fillOpacity: 0.8, + tm_id: `house_${i}` + }).addTo(houseGroup); + } + } + } + }, + + osm: { + init() { + const googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', { + subdomains: ['mt0', 'mt1', 'mt2', 'mt3'] + }); + const osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'); + const mytile = L.tileLayer('https://sheep-service.com/map/{z}/{x}/{y}.webp', { + maxZoom: 20, minZoom: 15, tms: true + }); + + if (!map) { + houseGroup = new L.FeatureGroup(); + splitPolygonsGroup = new L.FeatureGroup(); + RectangleGroup = new L.FeatureGroup(); + entransePolygonsGroup = new L.FeatureGroup(); + entranseNumPolygonsGroup = new L.FeatureGroup(); + + map = L.map('map', { + renderer: L.canvas(), zoom: 17, + layers: [googleHybrid, osm, mytile, houseGroup, + entransePolygonsGroup, entranseNumPolygonsGroup, + splitPolygonsGroup, RectangleGroup], + zoomControl: false + }); + + L.control.layers( + { "Google Hybrid": googleHybrid, "OpenStreetMap": osm, "Territory Map": mytile }, + { + "Будинки": houseGroup, + "Під'їзди": entransePolygonsGroup, + "Номера під'їздів": entranseNumPolygonsGroup, + "Слой редагування": splitPolygonsGroup, + "Слой лінійки": RectangleGroup + }, + { position: 'bottomright' } + ).addTo(map); + + map.pm.setLang("ua"); + map.pm.addControls({ + position: 'bottomright', + drawCircleMarker: false, + drawPolyline: false, + drawPolygon: false, + drawRectangle: false, + drawCircle: false, + drawText: false, + drawMarker: false, + cutPolygon: false, + tooltips: false + }); + map.pm.toggleControls(); + map.pm.setGlobalOptions({ layerGroup: splitPolygonsGroup }); + } + } + }, + + entrances: { + list: [], + + async setHTML(id) { + this.list = await Editor.loadAPI(`${CONFIG.api}house/${id}/entrances`); + + entransePolygonsGroup.clearLayers(); + entranseNumPolygonsGroup.clearLayers(); + + const listArea = document.getElementById('list-area'); + if (!listArea) return; + + listArea.innerHTML = ""; + + for (const element of this.list) { + // Блок area + const divArea = document.createElement('div'); + divArea.className = "block-area"; + divArea.id = `block-area-${element.id}`; + + const h3 = document.createElement('h3'); + h3.textContent = element.title; + + const addBtn = document.createElement('button'); + addBtn.className = "addFloors"; + addBtn.type = "button"; + addBtn.title = "Додати поверх"; + addBtn.dataset.entranceId = element.id; + addBtn.innerHTML = ``; + addBtn.setAttribute(`onclick`, `Editor.apartments.addFloors("${element.id}")`); + + const innerArea = document.createElement('div'); + innerArea.id = `area-${element.id}`; + + divArea.append(h3, addBtn, innerArea); + listArea.appendChild(divArea); + + // Завантажуємо квартири для ентрансів + Editor.apartments.setHTML(element.id); + } + } + }, + + apartments: { + list: {}, + + async setHTML(id) { + this.list[id] = await Editor.loadAPI(`${CONFIG.api}apartments/${id}`); + const area = document.getElementById(`area-${id}`); + if (!area) return; + + // Унікальні поверхи + const uniqueFloors = [...new Set(this.list[id].map(a => a.floors_number))].sort((a, b) => a - b); + + // Створюємо блоки поверхів + for (const num of uniqueFloors) { + const div = document.createElement('div'); + div.className = "block-apartments-floors"; + div.id = `floors-${id}-${num}`; + + const h2 = document.createElement('h2'); + h2.textContent = `Поверх ${num}`; + div.appendChild(h2); + + const addBtn = document.createElement('button'); + addBtn.className = "addApartment"; + addBtn.id = `buttonApartment-${id}-${num}`; + addBtn.type = "button"; + addBtn.title = "Додати квартиру"; + addBtn.dataset.area = id; + addBtn.dataset.floors = num; + addBtn.innerHTML = ``; + addBtn.setAttribute(`onclick`, `Editor.apartments.addApartment("${id}", "${num}")`); + + + div.appendChild(addBtn); + area.prepend(div); + } + + // Сортуємо квартири за назвою + this.list[id].sort((a, b) => b.title - a.title); + + // Створюємо блоки квартир + for (const apartment of this.list[id]) { + const floorsBlock = document.getElementById(`floors-${id}-${apartment.floors_number}`); + if (!floorsBlock) continue; + + const div = document.createElement('div'); + div.className = "block-apartments-number"; + div.id = `block-apartments-${id}-${apartment.id}`; + + const input = document.createElement('input'); + input.type = "text"; + input.value = apartment.title; + input.id = `apartament-${id}-${apartment.id}`; + input.dataset.area = id; + input.dataset.apartment = apartment.id; + input.setAttribute(`onclick`, `Editor.apartments.editApartment("${id}", "${apartment.id}")`); + + const delBtn = document.createElement('button'); + delBtn.type = "button"; + delBtn.dataset.area = id; + delBtn.dataset.apartment = apartment.id; + delBtn.innerHTML = ``; + delBtn.setAttribute(`onclick`, `Editor.apartments.deleteApartment("${id}", "${apartment.id}")`); + + div.append(input, delBtn); + floorsBlock.prepend(div); + + numApartments++; + } + + const nextApartmentTitle = document.getElementById('next-apartment-title'); + if (nextApartmentTitle) nextApartmentTitle.value = numApartments; + }, + + async addFloors(area) { + const areaBlock = document.getElementById(`area-${area}`); + const uniqueFloors = [...new Set(this.list[area].map(obj => obj.floors_number))]; + const newFloors = uniqueFloors.length + 1; + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}/apartments/${area}`; + + try { + const response = await fetch(URL, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + apartment_number: this.list[area].length, + title: numApartments.toString(), + floors_number: newFloors + }) + }); + const data = await response.json(); + console.log(data); + + // Створюємо блок поверху + const div = document.createElement('div'); + div.className = "block-apartments-floors"; + div.id = `floors-${area}-${newFloors}`; + + // Заголовок поверху + const h2 = document.createElement('h2'); + h2.textContent = `Поверх ${newFloors}`; + div.appendChild(h2); + + // Блок квартири + const apartmentBlock = document.createElement('div'); + apartmentBlock.className = "block-apartments-number"; + apartmentBlock.id = `block-apartments-${area}-${data.id}`; + + const input = document.createElement('input'); + input.type = "text"; + input.value = numApartments; + input.id = `apartament-${area}-${data.id}`; + input.onchange = () => Editor.apartments.editApartment(area, data.id); + + const delBtn = document.createElement('button'); + delBtn.type = "button"; + delBtn.innerHTML = ``; + delBtn.onclick = () => Editor.apartments.deleteApartment(area, data.id); + + apartmentBlock.append(input, delBtn); + div.appendChild(apartmentBlock); + + // Кнопка додати квартиру + const addBtn = document.createElement('button'); + addBtn.className = "addApartment"; + addBtn.id = `buttonApartment-${area}-${newFloors}`; + addBtn.title = "Додати квартиру"; + addBtn.type = "button"; + addBtn.onclick = () => Editor.apartments.addApartment(area, newFloors); + addBtn.innerHTML = ``; + div.appendChild(addBtn); + + areaBlock.prepend(div); + + // Оновлюємо список квартир + this.list[area].push({ + id: data.id, + entrance_id: Number(area), + apartment_number: this.list[area].length, + title: numApartments.toString(), + floors_number: newFloors + }); + + numApartments++; + const nextApartmentTitle = document.getElementById('next-apartment-title'); + if (nextApartmentTitle) nextApartmentTitle.value = numApartments; + + } catch (err) { + console.error("Помилка при додаванні поверху:", err); + } + }, + + async addApartment(area, floors) { + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}/apartments/${area}`; + + try { + const response = await fetch(URL, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + apartment_number: this.list[area].length, + title: numApartments.toString(), + floors_number: Number(floors) + }) + }); + + const data = await response.json(); + console.log(data); + + // Оновлюємо список квартир + this.list[area].push({ + id: data.id, + entrance_id: Number(area), + apartment_number: this.list[area].length, + title: numApartments.toString(), + floors_number: Number(floors) + }); + + const floorsBlock = document.getElementById(`floors-${area}-${floors}`); + + // Видаляємо стару кнопку додати квартиру + const oldButton = document.getElementById(`buttonApartment-${area}-${floors}`); + if (oldButton) oldButton.remove(); + + // Створюємо блок нової квартири + const apartmentDiv = document.createElement('div'); + apartmentDiv.className = "block-apartments-number"; + apartmentDiv.id = `block-apartments-${area}-${data.id}`; + + const input = document.createElement('input'); + input.type = "text"; + input.value = numApartments; + input.id = `apartament-${area}-${data.id}`; + input.onchange = () => Editor.apartments.editApartment(area, data.id); + + const delBtn = document.createElement('button'); + delBtn.type = "button"; + delBtn.innerHTML = ``; + delBtn.onclick = () => Editor.apartments.deleteApartment(area, data.id); + + apartmentDiv.append(input, delBtn); + floorsBlock.appendChild(apartmentDiv); + + // Додаємо кнопку "додати квартиру" знову + const addBtn = document.createElement('button'); + addBtn.className = "addApartment"; + addBtn.id = `buttonApartment-${area}-${floors}`; + addBtn.title = "Додати квартиру"; + addBtn.type = "button"; + addBtn.onclick = () => Editor.apartments.addApartment(area, floors); + addBtn.innerHTML = ``; + floorsBlock.appendChild(addBtn); + + numApartments++; + const nextApartmentTitle = document.getElementById('next-apartment-title'); + if (nextApartmentTitle) nextApartmentTitle.value = numApartments; + + } catch (err) { + console.error("Помилка при додаванні квартири:", err); + } + }, + + async editApartment(area, apartment) { + const input = document.getElementById(`apartament-${area}-${apartment}`); + if (!input) return; + + const newTitle = input.value; + + // Оновлюємо локальний список квартир + const pos = this.list[area].findIndex(e => e.id === Number(apartment)); + if (pos === -1) return; + + this.list[area][pos].title = newTitle; + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}/apartments/${area}`; + + try { + const response = await fetch(URL, { + method: 'PUT', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ + id: this.list[area][pos].id, + title: this.list[area][pos].title, + status: this.list[area][pos].status, + description: this.list[area][pos].description + }) + }); + + const data = await response.json(); + console.log(data); + + } catch (err) { + console.error("Помилка при редагуванні квартири:", err); + } + }, + + async deleteApartment(area, apartment) { + const pos = this.list[area].findIndex(e => e.id === Number(apartment)); + if (pos === -1) return; + + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}/apartments/${area}`; + + try { + const response = await fetch(URL, { + method: 'DELETE', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ id: this.list[area][pos].id }) + }); + + const data = await response.json(); + console.log(data); + + // Видаляємо елемент з DOM + const apartmentBlock = document.getElementById(`block-apartments-${area}-${apartment}`); + if (apartmentBlock) apartmentBlock.remove(); + + // Оновлюємо локальний список + this.list[area].splice(pos, 1); + + // Оновлюємо номер наступної квартири + numApartments = Math.max(0, numApartments - 1); + const nextApartmentTitle = document.getElementById('next-apartment-title'); + if (nextApartmentTitle) nextApartmentTitle.value = numApartments; + + } catch (err) { + console.error("Помилка при видаленні квартири:", err); + } + }, + + editNum(el) { numApartments = Number(el.value) }, + }, + + homestead: { + id: null, list: [], editing: false, + + async init(id) { + this.editing = false; + this.id = id; + setLeafletCursor('pointer'); + + document.getElementById('homestead-editing').style.display = ""; + + // Завантаження даних будівлі + this.list = await Editor.loadAPI(`${CONFIG.api}building/${id}`); + + // Обробник кліку на карту + houseGroup.on('click', e => { + if (e.layer instanceof L.Marker || !this.editing) return; + + const { lat, lng } = e.latlng; + console.log(`Координати: ${lat.toFixed(5)}, ${lng.toFixed(5)}`); + + setLeafletCursor('progress'); + this.editing.editing = false; + + this.addBuilding({ geo: e.latlng, title: this.list.length + 1 }); + this.list.push({}); + }); + + // Встановлюємо вид карти + const viewLatLng = Editor.info.list.geo?.lat + ? [Editor.info.list.geo.lat, Editor.info.list.geo.lng] + : [Editor.info.list.points[0][0][0].lat, Editor.info.list.points[0][0][0].lng]; + map.setView(viewLatLng, Editor.info.list.zoom); + + + for (const element of this.list) { + // Додаємо маркер на карту + const redDot = L.divIcon({ + className: "leaflet_drop", + html: `
`, + iconSize: [16, 16], + iconAnchor: [8, 8] + }); + + L.marker(element.geo, { icon: redDot }) + .addTo(houseGroup) + .bindPopup(` + Точка: ${element.id}
+ Координати: ${element.geo.lat.toFixed(5)}, ${element.geo.lng.toFixed(5)}
+ + `); + } + }, + + async addBuilding({ geo, title }) { + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}building/${this.id}`; + + try { + const response = await fetch(URL, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + }, + body: JSON.stringify({ title, geo }) + }); + const data = await response.json(); + console.log(data); + + // Додаємо маркер на карту + const redDot = L.divIcon({ + className: "leaflet_drop", + html: `
`, + iconSize: [16, 16], + iconAnchor: [8, 8] + }); + + const marker = L.marker(geo, { icon: redDot }).addTo(houseGroup); + marker.bindPopup(` + Точка: ${data.id}
+ Координати: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)}
+ + `); + + setLeafletCursor('crosshair'); + this.editing = true; + + } catch (err) { + console.error("Помилка при додаванні будівлі:", err); + } + }, + + async delleteBuilding({ id }) { + const uuid = localStorage.getItem('uuid'); + const URL = `${CONFIG.api}building/${id}`; + + try { + const response = await fetch(URL, { + method: 'DELETE', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + } + }); + const data = await response.json(); + + // Видаляємо елемент списку та маркер + const el = document.getElementById(`redDot_${id}`); + if (el) el.remove(); + + const block = document.getElementById(`Building_${id}`); + if (block) block.remove(); + + houseGroup.eachLayer(layer => { + if (layer instanceof L.Marker && layer.getPopup()?.getContent().includes(`Точка: ${id}`)) { + houseGroup.removeLayer(layer); + } + }); + + } catch (err) { + console.error("Помилка при видаленні будівлі:", err); + } + }, + + editing_mode() { + const btn = document.getElementById('homestead-editing'); + this.editing = !this.editing; + setLeafletCursor(this.editing ? 'crosshair' : 'pointer'); + btn.innerHTML = this.editing + ? ` ` + : ` `; + if (this.editing) alert("Натискаючи на карту будуть створюватись нові точки (будинки)"); + }, + } +} \ No newline at end of file diff --git a/web/lib/pages/editor_old/style.css b/web/lib/pages/editor_old/style.css new file mode 100644 index 0000000..14f2393 --- /dev/null +++ b/web/lib/pages/editor_old/style.css @@ -0,0 +1,536 @@ +.page-editor { + width: calc(100% - 40px); + display: flex; + flex-direction: column; + align-items: center; + margin: 20px 20px 0 20px; +} + +.page-editor form>button { + border-radius: 6px; + background: var(--PrimaryColor); + color: var(--PrimaryColorText); + width: 100%; + height: 40px; + font-size: var(--FontSize3); + font-weight: 400; + margin: 20px 0; + text-transform: uppercase; +} + +.page-editor details { + border-radius: 10px; + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + margin-bottom: 20px; + background: var(--ColorThemes1); + color: var(--ColorThemes3); + border: 1px solid var(--ColorThemes2); + box-shadow: var(--shadow-l1); +} + +.page-editor>details[disabled] summary, +.page-editor>details.disabled summary { + pointer-events: none; + user-select: none; +} + +.page-editor>details summary::-webkit-details-marker, +.page-editor>details summary::marker { + display: none; + content: ""; +} + +.page-editor summary { + width: calc(100% - 40px); + cursor: pointer; + color: var(--ColorThemes3); + border-radius: var(--border-radius); + font-size: var(--FontSize5); + font-weight: 300; + padding: 20px; + position: relative; +} + +.page-editor summary span { + font-weight: 500; +} + +.page-editor summary input { + font-weight: 500; + position: absolute; + right: 0; + top: 0; + padding: 10px; + margin: 13px; + font-size: var(--FontSize1); + background: var(--ColorThemes3); + color: var(--ColorThemes0); + border-radius: 6px; + width: 40px; +} + +.page-editor #info-form, +.page-editor #map-form, +.page-editor #area-form { + padding: 0 20px; +} + +#details-info-type { + display: flex; + align-items: center; + justify-content: center; +} + +#details-info-type>.tabs { + display: flex; + position: relative; + background-color: var(--ColorThemes0); + padding: 4px; + border-radius: 6px; + width: calc(100% - 8px); + z-index: 2; +} + +#details-info-type>.tabs>input[type="radio"] { + display: none; +} + +#details-info-type>.tabs>.tab { + display: flex; + align-items: center; + justify-content: center; + height: 40px; + width: calc(100% / 3); + cursor: pointer; + padding: 0 15px; + transition: 0.15s ease-in; + color: var(--ColorThemes3); + fill: var(--ColorThemes3); + flex-direction: row; + z-index: 2; +} + +#details-info-type>.tabs>.tab>svg { + width: 20px; + height: 20px; +} + +#details-info-type>.tabs>.tab>span { + margin-left: 6px; + font-size: var(--FontSize1); + font-weight: 400; +} + +#details-info-type>.tabs>input[type="radio"]:checked+label { + color: var(--PrimaryColorText); + fill: var(--PrimaryColorText); +} + +#details-info-type>.tabs>.glider { + position: absolute; + display: flex; + height: 40px; + width: calc((100% - 8px) / 3); + background-color: var(--PrimaryColor); + z-index: 1; + border-radius: 4px; + transition: 0.25s ease-out; +} + +@media (min-width: 601px) { + #details-info-type>.tabs>input[id="info-type-house"]:checked~.glider { + transform: translateX(0); + } + + #details-info-type>.tabs>input[id="info-type-homestead"]:checked~.glider { + transform: translateX(100%); + } + + #details-info-type>.tabs>input[id="info-type-points"]:checked~.glider { + transform: translateX(200%); + } +} + +@media (max-width: 600px) { + #details-info-type>.tabs { + flex-direction: column; + } + + #details-info-type>.tabs>.tab { + width: calc(100% - 8px); + padding: 0 4px; + } + + #details-info-type>.tabs>.glider { + width: calc(100% - 8px); + } + + #details-info-type>.tabs>input[id="info-type-house"]:checked~.glider { + transform: translateY(0); + } + + #details-info-type>.tabs>input[id="info-type-homestead"]:checked~.glider { + transform: translateY(100%); + } + + #details-info-type>.tabs>input[id="info-type-points"]:checked~.glider { + transform: translateY(200%); + } +} + +.page-editor .details-info-input { + width: 100%; + display: flex; + margin: 20px 0; + align-items: flex-start; + flex-direction: column; +} + +.page-editor .details-info-input label { + display: flex; + justify-content: center; + flex-direction: column; + font-size: var(--FontSize1); + font-weight: 500; + margin-bottom: 5px; + +} + +.page-editor .details-info-input input { + width: calc(100% - 10px); + min-width: 140px; + padding: 0 5px; + border-radius: 6px; + height: 30px; + background: var(--ColorThemes0); + color: var(--ColorThemes3); + font-size: var(--FontSize2); +} + +.page-editor #details-info-osm div { + display: flex; + align-items: center; + width: 100%; +} + +.page-editor #details-info-osm input { + width: calc(100% - 10px); + min-width: 140px; + padding: 0 5px; + border-radius: 6px; + height: 30px; +} + +.page-editor .details-info-input a { + height: 26px; + width: 26px; + margin-left: 10px; +} + +.page-editor .details-info-input a>svg { + height: 26px; + width: 26px; + fill: var(--ColorThemes3) +} + +.page-editor #list-area { + display: flex; + overflow: auto; + margin: 15px 0 20px 0; +} + +.page-editor #list-area h3 { + text-align: center; + font-size: var(--FontSize5); + font-weight: 400; + margin: 10px; + padding: 7px; + color: var(--ColorThemes0); + background: var(--ColorThemes3); + border-radius: 4px; +} + +.block-area { + min-height: 200px; + border: 1px solid var(--ColorThemes3); + border-style: dashed; + border-radius: 6px; + margin: 0 10px 10px 0; +} + +.addFloors, +.addApartment { + display: flex; + position: relative; + width: 34px; + height: 34px; + background: var(--PrimaryColor); + color: var(--PrimaryColorText); + margin: 25px; + border-radius: 50%; + align-items: center; + justify-content: center; + font-size: 30px; + cursor: pointer; +} + +.addFloors>svg, +.addApartment>svg { + width: 20px; + height: 20px; + fill: var(--PrimaryColorText); + transform: rotate(45deg); +} + +.block-apartments-floors { + position: relative; + display: flex; + width: calc(100% - 22px); + border: 1px solid var(--ColorThemes3); + margin: 10px; + border-radius: 4px; +} + +.block-apartments-floors h2 { + position: absolute; + width: 65px; + right: -1px; + top: -1px; + margin: 0; + background: var(--ColorThemes3); + color: var(--ColorThemes2); + border-radius: 0 4px 0 4px; + font-size: var(--FontSize1); + padding: 2px 4px; + text-align: center; +} + +.block-apartments-number { + display: flex; + position: relative; + width: 60px; + height: 60px; + background: var(--ColorThemes1); + border: 2px solid var(--PrimaryColor); + margin: 10px; + border-radius: 4px; + align-items: center; + justify-content: center; +} + +.block-apartments-number input { + width: 50px; + height: 50px; + font-size: var(--FontSize5); + font-weight: 400; + text-align: center; + color: var(--ColorThemes3); + background: 0; +} + +.block-apartments-number button { + position: absolute; + top: 0; + right: 0; + width: 20px; + height: 20px; + border-radius: 0 4px 0 4px; + background: var(--PrimaryColor); + font-size: var(--FontSize5); + margin: -2px; + border: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.block-apartments-number button>svg { + width: 16px; + height: 16px; + fill: var(--PrimaryColorText); +} + + +.block-map { + width: 100%; + height: 500px; + border-radius: 6px; + overflow: hidden; + margin-bottom: 20px; + position: relative; + + background: var(--ColorThemes1); + color: var(--ColorThemes3); + border: 1px solid var(--ColorThemes2); + box-shadow: var(--shadow-l1); +} + +#homestead-editing { + position: absolute; + height: 40px; + background: var(--PrimaryColor); + font-size: var(--FontSize5); + margin: 10px; + border: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + padding: 10px; + border-radius: 6px; + width: 40px; + z-index: 999; + right: 0; + box-shadow: var(--shadow-l1); +} + +#homestead-editing>svg { + width: 20px; + height: 20px; + fill: var(--PrimaryColorText); +} + +#map { + width: 100%; + height: 100%; +} + +.entranse_number { + left: -10px !important; + top: -10px !important; +} + +.markerEntranse { + background: hsl(0deg 0% 52.12% / 90%); + font-size: 24px; + border: 2px solid #676767; + border-radius: 2px; + display: flex !important; + justify-content: center; + align-items: center; + color: #fff; + min-width: 30px; + min-height: 30px; + height: 30px; + width: 30px; +} + +.editor-buttons { + margin-top: 15px; +} + +.editor-buttons>button { + width: 100%; + min-height: 30px; + margin: 5px 0; + border: 0; + color: var(--PrimaryColorText); + display: flex; + align-items: center; + justify-content: center; + background: var(--PrimaryColor); + text-transform: uppercase; + cursor: pointer; + border-radius: 6px; + font-weight: 400; + font-size: var(--FontSize1); +} + +.editor-buttons>div { + display: flex; + width: 100%; + margin: 10px 0; + height: 30px; + justify-content: space-between; +} + +.editor-buttons>div>button { + background: var(--ColorThemes3); + color: var(--ColorThemes1); + width: 100%; + margin: 0; + border-radius: 6px; + text-transform: uppercase; + font-weight: 400; + font-size: var(--FontSize1); +} + +.page-editor #list-entranse, +.page-editor #list-homestead { + width: 100%; + min-height: 86px; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + overflow-x: initial; + overflow-y: auto; + border: 1px solid var(--ColorThemes3); + background: 0; + border-radius: 6px; + border-style: dashed; + margin: 10px 0; + position: relative; +} + +.page-editor #list-entranse>.house, +.page-editor #list-homestead>.house { + display: flex; + position: relative; + width: 60px; + height: 60px; + min-width: 60px; + min-height: 60px; + background: var(--ColorThemes1); + border: 2px solid var(--PrimaryColor); + margin: 10px; + border-radius: 4px; + align-items: center; + justify-content: center; +} + + +.page-editor #list-entranse>.house>input, +.page-editor #list-homestead>.house>input, +.page-editor #list-entranse>.house>p, +.page-editor #list-homestead>.house>p { + width: 50px; + height: 50px; + font-size: var(--FontSize5); + font-weight: 400; + text-align: center; + color: var(--ColorThemes3); + background: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.page-editor #list-entranse>.house>button, +.page-editor #list-homestead>.house>button { + position: absolute; + top: 0; + right: 0; + width: 20px; + height: 20px; + border-radius: 0 4px 0 4px; + background: var(--PrimaryColor); + font-size: var(--FontSize5); + margin: -2px; + border: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.page-editor #list-entranse>.house>button>svg, +.page-editor #list-homestead>.house>button>svg { + width: 16px; + height: 16px; + fill: var(--PrimaryColorText); +} \ No newline at end of file diff --git a/web/lib/pages/territory/index.html b/web/lib/pages/territory/index.html index 722bac9..9d44e65 100644 --- a/web/lib/pages/territory/index.html +++ b/web/lib/pages/territory/index.html @@ -15,9 +15,27 @@ + +
diff --git a/web/lib/pages/territory/script.js b/web/lib/pages/territory/script.js index b2a746c..7ee1fa3 100644 --- a/web/lib/pages/territory/script.js +++ b/web/lib/pages/territory/script.js @@ -9,6 +9,7 @@ const Territory = { if (USER.mode == 2) { document.getElementById("buttons-list").style.display = "flex"; + document.getElementById("historyButton").style.display = ""; } if (USER.possibilities.can_add_territory) { document.getElementById("buttons-list").style.display = "flex"; diff --git a/web/lib/pages/territory_history/index.html b/web/lib/pages/territory_history/index.html new file mode 100644 index 0000000..d164c28 --- /dev/null +++ b/web/lib/pages/territory_history/index.html @@ -0,0 +1,3 @@ +
+
+
\ No newline at end of file diff --git a/web/lib/pages/territory_history/script.js b/web/lib/pages/territory_history/script.js new file mode 100644 index 0000000..135c765 --- /dev/null +++ b/web/lib/pages/territory_history/script.js @@ -0,0 +1,82 @@ +const Territory_History = { + list: [], + + async init() { + let html = await fetch('/lib/pages/territory_history/index.html').then((response) => response.text()); + app.innerHTML = html; + + this.setHTML() + }, + + async loadAPI(url) { + const uuid = localStorage.getItem("uuid"); + + const response = await fetch(url, { + method: 'GET', + headers: { + "Content-Type": "application/json", + "Authorization": uuid + } + }); + return await response.json(); + }, + + async setHTML() { + const block_list = document.getElementById('list'); + const url = `${CONFIG.api}history/apartments/list?limit=5000`; + let list = this.list.length > 0 ? this.list : await this.loadAPI(url); + + list.sort((a, b) => b.id - a.id); + + let html = ""; + for (const element of list) { + html += this.renderCard({ element }); + } + + block_list.innerHTML = html; + }, + + renderCard: ({ element }) => { + console.log(element); + + const labels = ["", "Відмова (Не цікавить)", "Не заходити (Груба відмова)", "Нема домофона", "Повторна відвідина", "Немає вдома", "Свідки Єгови"]; + const color_status = [ + ["var(--ColorThemes2)", "var(--ColorThemes3)"], + ["#fbf1e0", "#ff8300"], + ["#fce3e2", "#ff0000"], + ["#d7ddec", "#2919bd"], + ["#d5e9dd", "#11a568"], + ["#d7ebfa", "#3fb4fc"], + ["#e8dbf5", "#b381eb"] + ]; + + const [bg, color] = color_status[element.status]; + + let description = element.description + ? `

${element.description}

` + : ``; + + return ` +
+
+ +

${element.address.house.title} ${element.address.house.number}

+
+
+

кв. ${element.address.apartment}

+
+
+

${labels[element.status]}

+
+ +

${element.sheep.name}

+
+
+

${formattedDateTime(element.created_at)}

+
+
+ ${description} +
+ `; + }, +} \ No newline at end of file diff --git a/web/lib/pages/territory_history/style.css b/web/lib/pages/territory_history/style.css new file mode 100644 index 0000000..4f080bb --- /dev/null +++ b/web/lib/pages/territory_history/style.css @@ -0,0 +1,71 @@ +.page-territory_history { + width: calc(100% - 18px); + display: flex; + flex-direction: row; + margin: 20px 9px 0 9px; + justify-content: space-between; + position: relative; +} + +.page-territory_history>#list { + width: 100%; +} + +.page-territory_history>#list>.card { + display: flex; + width: calc(100% - 2px); + background: var(--ColorThemes1); + border: 1px solid var(--ColorThemes2); + border-radius: var(--border-radius); + margin-bottom: 10px; + flex-direction: column; + position: relative; +} + +.page-territory_history>#list>.card>.info { + display: flex; + justify-content: space-between; + align-items: center; + margin: 10px; +} + +.page-territory_history>#list>.card>.info>div { + text-align: center; +} + +.page-territory_history>#list>.card>.info>div>a, +.page-territory_history>#list>.card>.info>div>p { + font-size: var(--FontSize3); +} + +.page-territory_history>#list>.card>.info>.address { + width: 20%; + text-align: left; +} + +.page-territory_history>#list>.card>.info>.apartment { + width: 60px; +} + +.page-territory_history>#list>.card>.info>.status { + width: 180px; +} + +.page-territory_history>#list>.card>.info>.name { + width: 150px; + text-align: center; +} + +.page-territory_history>#list>.card>.info>.date { + width: 125px; +} + +.page-territory_history>#list>.card>.description { + display: flex; + justify-content: space-between; + align-items: center; + margin: 0 10px 10px 10px; + background: var(--ColorThemes0); + padding: 10px; + border-radius: 8px; +} \ No newline at end of file diff --git a/web/lib/router/routes.js b/web/lib/router/routes.js index 29bf041..bf00af8 100644 --- a/web/lib/router/routes.js +++ b/web/lib/router/routes.js @@ -15,6 +15,10 @@ Router pageActive(); Card.init(type, id); }) + .add('territory/history', function () { + pageActive(); + Territory_History.init(); + }) .add('territory', function () { pageActive('territory'); Territory.init(); diff --git a/ws/ws.js b/ws/ws.js index 20b6226..9eb973c 100644 --- a/ws/ws.js +++ b/ws/ws.js @@ -108,7 +108,7 @@ function updateDatabase(message) { Number(message.data.status), message.data.description, message.data.sheep_id, - message.data.updated_at + Math.floor(Date.now()) ], function (err) { if (err) { console.error(err.message); @@ -144,7 +144,7 @@ function updateDatabase(message) { Number(message.data.status), message.data.description, message.data.sheep_id, - message.data.updated_at + Math.floor(Date.now()) ], function (err) { if (err) { console.error(err.message);