Переработаны роутеры приложения

Переписано APi WebSocket для работы с новыми роутерами
This commit is contained in:
2025-10-03 17:11:31 +03:00
parent d75fb7ec3d
commit 6ec6523d71
54 changed files with 2593 additions and 3749 deletions

View File

@@ -0,0 +1,624 @@
let map_card;
const Territory_card = {
// Глобальні змінні стану
id: null,
socket: null,
reconnectTimeout: null,
reconnectAttempts: 0,
listEntrances: [],
listApartment: [],
listBuilding: [],
// Кольори статусів квартир
color_status: [
["var(--ColorThemes2)", "var(--ColorThemes3)"],
["#fbf1e0", "#ff8300"],
["#fce3e2", "#ff0000"],
["#d7ddec", "#2919bd"],
["#d5e9dd", "#11a568"],
["#d7ebfa", "#3fb4fc"],
["#e8dbf5", "#b381eb"]
],
// Ініціалізація сторінки
async init(type, Id) {
// Завантажуємо HTML
const html = await fetch('/lib/pages/territory/card/index.html').then(r => r.text());
app.innerHTML = html;
Territory_card.id = Id;
// Закриваємо старий WebSocket
if (this.socket) this.socket.close(1000, "Перезапуск з'єднання");
// this.cloud.start(makeid(6));
this.cloud.start()
// Якщо це сторінка будинку, отримуємо під’їзди та стартуємо WebSocket
if (type === "house") {
const controls = document.getElementById('page-card-controls');
controls.style.display = "flex";
// Застосовуємо режим сортування
this.sort(localStorage.getItem('sort_mode'), false);
this.getEntrances({ update: false });
} else if (type === "homestead") {
this.getHomestead.map({});
}
// Додаємо обробник закриття попапу
const popup = document.getElementById('card-new-date');
if (!popup.dataset.listenerAdded) {
popup.addEventListener('click', (e) => {
if (!popup.querySelector('.mess').contains(e.target)) {
this.dateEditor.close();
}
});
popup.dataset.listenerAdded = 'true';
}
},
// Робота з WebSocket
cloud: {
start() {
const uuid = localStorage.getItem("uuid");
const ws = new WebSocket(CONFIG.wss, uuid);
Territory_card.socket = ws;
ws.onopen = () => {
console.log("[WebSocket] З'єднання встановлено");
Territory_card.cloud.setStatus('ok');
ws.send(JSON.stringify({
event: 'connection',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
uuid,
user: {
name: USER.name,
id: USER.id
},
data: {}
}));
Territory_card.reconnectAttempts = 0;
clearTimeout(Territory_card.reconnectTimeout);
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.event === 'connection' && data.user.id !== USER.id) {
console.log(`Новий користувач: ${data.user}`);
}
if (data.event === 'message') {
Territory_card.cloud.update(data);
}
};
ws.onclose = () => {
console.warn("[WebSocket] З'єднання розірвано");
Territory_card.cloud.setStatus('err');
Territory_card.reconnectAttempts++;
if (Territory_card.reconnectAttempts <= 5) {
Territory_card.reconnectTimeout = setTimeout(() => {
Territory_card.getEntrances({ update: true });
Territory_card.cloud.start();
}, 1000);
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Territory_card.reconnectAttempts = 0;
Territory_card.getEntrances({ update: true });
Territory_card.cloud.start();
}
}
};
ws.onerror = (err) => {
console.error("[WebSocket] Помилка", err);
Territory_card.cloud.setStatus('err');
};
},
setStatus(mode) {
const ids = ['cloud_1', 'cloud_2', 'cloud_3'];
ids.forEach((id, idx) => {
const el = document.getElementById(id);
el.setAttribute('data-state', ['sync', 'ok', 'err'].indexOf(mode) === idx ? 'active' : '');
});
},
update(msg) {
if (msg.type !== "apartment" && msg.type !== "building") return;
const [bg, color] = Territory_card.color_status[msg.data.status];
const id = msg.data.id;
const el = document.getElementById(`status_${id}`);
const redDot = document.getElementById(`redDot_${id}`);
if (msg.type === "building") {
redDot.style = `background:${bg};border:2px solid ${color}`;
const apt = Territory_card.listBuilding.find(e => e.id === id);
if (!apt) return;
apt.status = msg.data.status;
apt.description = msg.data.description;
apt.updated_at = msg.data.updated_at;
if (!el) return;
let date = new Date(msg.data.updated_at);
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
document.getElementById(`date_${id}`).value = date.toISOString().slice(0, 16);
} else if (msg.type === "apartment") {
if (!el) return;
document.getElementById(`date_text_${id}`).innerText = formattedDateTime(msg.data.updated_at);
}
if (!el) return;
document.getElementById(`card_${id}`).style = `background:${bg};color:${color};border:1px solid ${color}`;
el.value = msg.data.status;
el.style = `background:${bg};color:${color};border:1px solid ${color}`;
document.getElementById(`description_${id}`).value = msg.data.description;
},
messApartment({ number, id, update, time }) {
const apt = Territory_card.listApartment[number]?.find(e => e.id === id);
if (!apt) return;
const statusEl = document.getElementById(`status_${id}`);
const descEl = document.getElementById(`description_${id}`);
let date = () => {
if (!update && !time) {
return apt.updated_at;
} else if (update && !time) {
return getTimeInSeconds();
} else if (update && time) {
return getTimeInSeconds(time);
}
}
apt.status = Number(statusEl.value);
apt.description = descEl.value;
apt.updated_at = date();
const [bg, color] = Territory_card.color_status[apt.status];
statusEl.style = `background:${bg};color:${color};border:1px solid ${color}`;
const message = {
event: 'message',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
user: {
name: USER.name,
id: USER.id
},
type: "apartment",
data: {
...apt,
sheep_id: USER.id
}
};
if (Territory_card.socket?.readyState === WebSocket.OPEN) {
Territory_card.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Territory_card.getEntrances({ update: true });
Territory_card.cloud.start();
}
}
},
messBuildings({ id, update, time }) {
const apt = Territory_card.listBuilding.find(e => e.id === id);
if (!apt) return;
const statusEl = document.getElementById(`status_${id}`);
const descEl = document.getElementById(`description_${id}`);
const dateEl = document.getElementById(`date_text_${id}`);
let date = () => {
if (!update && !time) {
return apt.updated_at;
} else if (update && !time) {
return getTimeInSeconds();
} else if (update && time) {
const ts = new Date(time).getTime();
return getTimeInSeconds(ts);
}
}
apt.status = Number(statusEl.value);
apt.description = descEl.value;
apt.updated_at = date();
const [bg, color] = Territory_card.color_status[apt.status];
statusEl.style = `background:${bg};color:${color};border:1px solid ${color}`;
const message = {
event: 'message',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
user: {
name: USER.name,
id: USER.id
},
type: "building",
data: {
...apt,
sheep_id: USER.id
}
};
if (Territory_card.socket?.readyState === WebSocket.OPEN) {
Territory_card.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Territory_card.getEntrances({ update: true });
Territory_card.cloud.start();
}
}
}
},
// Отримання під’їздів
async getEntrances({ house_id = Territory_card.id, update = false }) {
const uuid = localStorage.getItem("uuid");
const res = await fetch(`${CONFIG.api}/house/${house_id}/entrances`, {
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
const data = await res.json();
this.listEntrances = data;
const container = document.getElementById('list');
if (!update) container.innerHTML = "";
if (update) {
for (const { id, entrance_number } of data) {
this.getApartment({ id, number: entrance_number, update: true });
}
return;
}
const fragment = document.createDocumentFragment();
const canManage = USER.mode === 2 || (USER.mode === 1 && USER.possibilities.can_manager_territory);
for (const element of data) {
const { id, entrance_number, title, history, working } = element;
const isMy = ((history.name === "Групова" && history.group_id == USER.group_id) || history.name === USER.name);
const show = (isMy && working) ? "open" : canManage ? "close" : null;
if (!show) continue;
const icon = isMy && working
? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M 12 1 C 9.1277778 1 6.7189086 3.0461453 6.1230469 5.7871094 L 8.078125 6.2128906 C 8.4822632 4.3538547 10.072222 3 12 3 C 14.27619 3 16 4.7238095 16 7 L 16 8 L 6 8 C 4.9069372 8 4 8.9069372 4 10 L 4 20 C 4 21.093063 4.9069372 22 6 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 10 C 20 8.9069372 19.093063 8 18 8 L 18 7 C 18 3.6761905 15.32381 1 12 1 z M 6 10 L 18 10 L 18 20 L 6 20 L 6 10 z M 12 13 C 10.9 13 10 13.9 10 15 C 10 16.1 10.9 17 12 17 C 13.1 17 14 16.1 14 15 C 14 13.9 13.1 13 12 13 z"/></svg>`
: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M 12 1 C 8.6761905 1 6 3.6761905 6 7 L 6 8 C 4.9069372 8 4 8.9069372 4 10 L 4 20 C 4 21.093063 4.9069372 22 6 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 10 C 20 8.9069372 19.093063 8 18 8 L 18 7 C 18 3.6761905 15.32381 1 12 1 z M 12 3 C 14.27619 3 16 4.7238095 16 7 L 16 8 L 8 8 L 8 7 C 8 4.7238095 9.7238095 3 12 3 z M 6 10 L 18 10 L 18 20 L 6 20 L 6 10 z M 12 13 C 10.9 13 10 13.9 10 15 C 10 16.1 10.9 17 12 17 C 13.1 17 14 16.1 14 15 C 14 13.9 13.1 13 12 13 z"/></svg>`;
const details = document.createElement('details');
if (show === "open") details.setAttribute('open', '');
details.innerHTML = `
<summary><p>${title}</p>${icon}</summary>
<div id="apartments_${id}" class="apartments_list"></div>
`;
fragment.appendChild(details);
this.getApartment({ id, number: entrance_number, update: false });
}
container.appendChild(fragment);
},
async getApartment({ id, number, update }) {
const uuid = localStorage.getItem('uuid');
const res = await fetch(`${CONFIG.api}/apartment/${id}`, {
headers: { "Authorization": uuid }
});
const data = await res.json();
this.listApartment[number] = data;
const sort_mode = localStorage.getItem('sort_mode') ?? "1";
const sorters = {
"1": (a, b) => a.apartment_number - b.apartment_number,
"2": (a, b) => b.apartment_number - a.apartment_number,
"3": (a, b) => a.updated_at - b.updated_at,
"4": (a, b) => b.updated_at - a.updated_at,
};
data.sort(sorters[sort_mode] || sorters["1"]);
const container = document.getElementById(`apartments_${id}`);
if (!update) container.innerHTML = "";
const statusOptions = (selected) => {
const labels = ["", "Відмова (Не цікавить)", "Не заходити (Груба відмова)", "Нема домофона", "Повторна відвідина", "Немає вдома", "Свідки Єгови"];
return labels.map((txt, i) => `<option value="${i}" ${i === selected ? "selected" : ""}>${txt}</option>`).join("");
};
if (update) {
for (const apt of data) {
const [bg, color] = this.color_status[apt.status];
const style = `background:${bg};color:${color};border:1px solid ${color}`;
const dateEl = document.getElementById(`date_${apt.id}`);
const cardEl = document.getElementById(`card_${apt.id}`);
const statusEl = document.getElementById(`status_${apt.id}`);
const dateTextEl = document.getElementById(`date_text_${apt.id}`);
const descEl = document.getElementById(`description_${apt.id}`);
if (cardEl) cardEl.style = style;
if (statusEl) {
statusEl.value = apt.status;
statusEl.style = style;
}
if (dateEl) dateEl.setAttribute('onclick', `Territory_card.dateEditor.open({id: ${apt.id}, number: ${number}, updated_at: ${apt.updated_at}})`);
if (dateTextEl) dateTextEl.innerText = formattedDateTime(apt.updated_at);
if (descEl) descEl.innerText = apt.description ?? "";
}
} else {
const fragment = document.createDocumentFragment();
if (data.length == 0) {
const p = document.createElement('p');
p.innerHTML = `
Інформація про цей під'їзд відсутня. Надайте інформацію відповідальному за території.
`;
return container.appendChild(p);
}
for (const apt of data) {
const [bg, color] = this.color_status[apt.status];
const style = `background:${bg};color:${color};border:1px solid ${color}`;
const div = document.createElement('div');
div.className = `card_info`;
div.id = `card_${apt.id}`;
div.style = style;
div.innerHTML = `
<div class="info">
<span>кв.${apt.title}</span>
<select id="status_${apt.id}" onchange="Territory_card.cloud.messApartment({number:${number},id:${apt.id},update:true})" style="${style}">
${statusOptions(apt.status)}
</select>
<button id="date_${apt.id}" onclick="Territory_card.dateEditor.open({id:${apt.id},number:${number},updated_at:${apt.updated_at}})">
<p id="date_text_${apt.id}">${formattedDateTime(apt.updated_at)}</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 22.828125 3 C 22.316375 3 21.804562 3.1954375 21.414062 3.5859375 L 19 6 L 24 11 L 26.414062 8.5859375 C 27.195062 7.8049375 27.195062 6.5388125 26.414062 5.7578125 L 24.242188 3.5859375 C 23.851688 3.1954375 23.339875 3 22.828125 3 z M 17 8 L 5.2597656 19.740234 C 5.2597656 19.740234 6.1775313 19.658 6.5195312 20 C 6.8615312 20.342 6.58 22.58 7 23 C 7.42 23.42 9.6438906 23.124359 9.9628906 23.443359 C 10.281891 23.762359 10.259766 24.740234 10.259766 24.740234 L 22 13 L 17 8 z M 4 23 L 3.0566406 25.671875 A 1 1 0 0 0 3 26 A 1 1 0 0 0 4 27 A 1 1 0 0 0 4.328125 26.943359 A 1 1 0 0 0 4.3378906 26.939453 L 4.3632812 26.931641 A 1 1 0 0 0 4.3691406 26.927734 L 7 26 L 5.5 24.5 L 4 23 z"></path></svg>
</button>
</div>
<textarea id="description_${apt.id}" onchange="Territory_card.cloud.messApartment({number:${number},id:${apt.id}})" placeholder="Коротка нотатка.">${apt.description || ""}</textarea>
`;
fragment.appendChild(div);
}
container.appendChild(fragment);
}
},
getHomestead: {
markers: {},
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 map({ homestead_id = Territory_card.id }) {
let data = await this.loadAPI({ url: `${CONFIG.api}homestead/${homestead_id}` });
console.log(data);
let lat = data.geo?.lat ?? data.points?.[0]?.[0]?.[0]?.lat ?? 49.5629016;
let lng = data.geo?.lng ?? data.points?.[0]?.[0]?.[0]?.lng ?? 25.6145625;
let zoom = 15;
if (map_card && map_Territory_card.remove) {
map_Territory_card.stopLocate();
map_Territory_card.remove();
}
const mapElement = document.getElementById('map_card');
mapElement.style.display = "flex";
if (!mapElement) return;
let googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
maxZoom: 20,
minZoom: 15,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
});
let osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
});
let mytile = L.tileLayer('https://sheep-service.com/map/{z}/{x}/{y}.webp', {
maxZoom: 20,
minZoom: 15,
tms: true
});
map_card = L.map(mapElement, {
renderer: L.canvas(),
center: [lat, lng],
zoom,
zoomControl: false,
layers: [
googleHybrid,
osm,
mytile
]
});
// слежение в реальном времени
map_Territory_card.locate({ setView: false, watch: true, enableHighAccuracy: true });
map_Territory_card.on('locationfound', (e) => {
if (!map_Territory_card._userMarker) {
map_Territory_card._userMarker = L.marker(e.latlng).addTo(map_card)
.bindPopup("Ви тут!");
} else {
map_Territory_card._userMarker.setLatLng(e.latlng);
}
});
let baseMaps = {
"Google Hybrid": googleHybrid,
"OpenStreetMap": osm,
"Sheep Service Map": mytile,
};
let layerControl = L.control.layers(baseMaps, [], { position: 'bottomright' }).addTo(map_card);
map_Territory_card.pm.setLang("ua");
const polygonOptions = {
color: "#f2bd53",
radius: 500,
fillOpacity: 0.3,
dashArray: '20,15',
dashOffset: '20',
};
L.polygon(data.points, polygonOptions).addTo(map_card);
map_Territory_card.setZoom(data.zoom);
// map_Territory_card.getZoom()
// console.log(data.zoom);
Territory_card.listBuilding = await this.loadAPI({ url: `${CONFIG.api}building/${homestead_id}` });
const statusOptions = (selected) => {
const labels = ["", "Відмова (Не цікавить)", "Не заходити (Груба відмова)", "Нема домофона", "Повторна відвідина", "Немає вдома", "Свідки Єгови"];
return labels.map((txt, i) => `<option value="${i}" ${i === selected ? "selected" : ""}>${txt}</option>`).join("");
};
for (let i = 0; i < Territory_card.listBuilding.length; i++) {
const element = Territory_card.listBuilding[i];
const [bg, color] = Territory_card.color_status[element.status];
const redDot = L.divIcon({
className: "leaflet_drop",
html: `<div id="redDot_${element.id}" style='background:${bg};border:2px solid ${color}'></div>`,
iconSize: [16, 16],
iconAnchor: [8, 8]
});
// создаём маркер
const marker = L.marker(element.geo, { icon: redDot }).addTo(map_card);
marker.bindPopup("");
// при открытии popup генерим div заново
marker.on("popupopen", () => {
const el = Territory_card.listBuilding.find(e => e.id === element.id);
const [bg, color] = Territory_card.color_status[el.status];
const style = `background:${bg};color:${color};border:1px solid ${color}`;
const div = document.createElement("div");
div.className = "card_info card_info_homestead";
div.id = `card_${el.id}`;
div.style = style;
let date = new Date(el.updated_at);
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
div.innerHTML = `
<div class="info">
<select id="status_${element.id}" onchange="Territory_card.cloud.messBuildings({id:${element.id},update:true})" style="${style}">
${statusOptions(element.status)}
</select>
<input type="datetime-local" id="date_${element.id}" placeholder="Дата" onchange="Territory_card.cloud.messBuildings({id:${element.id},update:true, time: this.value })" value="${date.toISOString().slice(0, 16)}">
</div>
<textarea id="description_${element.id}" onchange="Territory_card.cloud.messBuildings({id:${element.id}})" placeholder="Коротка нотатка.">${element.description || ""}</textarea>
`;
marker.setPopupContent(div);
});
Territory_card.getHomestead.markers[element.id] = marker; // сохраним ссылку на маркер
}
},
},
// Сортування
sort(mode, load) {
const idx = Math.max(1, Math.min(4, Number(mode) || 1));
['sort_1', 'sort_2', 'sort_3', 'sort_4'].forEach((id, i) => {
document.getElementById(id)?.setAttribute('data-state', i + 1 === idx ? 'active' : '');
});
localStorage.setItem('sort_mode', idx);
if (!load) this.getEntrances({ update: false });
},
// Редактор дати
dateEditor: {
open({ id, number, updated_at }) {
const block = document.getElementById('card-new-date');
const input = document.getElementById('card-new-date-input');
const button = document.getElementById('card-new-date-button');
// Приводимо дату до ISO без зсуву часового поясу
let date = new Date(updated_at == 0 ? Date.now() : updated_at);
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
input.value = date.toISOString().slice(0, 16)
// Призначаємо обробники
input.setAttribute("onchange", `Territory_card.dateEditor.edit({ id: ${id}, number: ${number} })`)
button.setAttribute("onclick", `Territory_card.dateEditor.edit({ id: ${id}, number: ${number}, type: 'now'})`)
// Показуємо блок
block.style.display = "";
requestAnimationFrame(() => block.style.opacity = "1");
},
close() {
// Робимо плавне зникнення
const block = document.getElementById('card-new-date');
block.style.opacity = "0";
const onTransitionEnd = () => {
block.style.display = "none";
block.removeEventListener("transitionend", onTransitionEnd);
};
block.addEventListener("transitionend", onTransitionEnd);
},
edit({ id, number, type }) {
let input = document.getElementById('card-new-date-input').value;
if (type == "now") {
Territory_card.cloud.messApartment({ number: number, id: id, update: true });
} else {
if (input) {
const ts = new Date(input).getTime();
Territory_card.cloud.messApartment({ number, id, update: true, time: ts });
} else {
Territory_card.cloud.messApartment({ number: number, id: id });
}
}
this.close();
}
}
}