Files
Sheep-Service/web/lib/pages/territory/card/script.js
Rozenrod 3f08f3f6c9 Додана сторінка "Стенд"
Додане повідомлення про оновлення застосунку
Оновлен Service Worker
Перероблен WebSocket APІ
2025-10-19 00:55:30 +03:00

548 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
let map_card;
const Territory_card = {
// Глобальні змінні стану
id: null,
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 (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 ids = ['cloud_1', 'cloud_2', 'cloud_3'];
ids.forEach((id, idx) => {
const el = document.getElementById(id);
if(!el) return;
el.setAttribute('data-state', ['sync', 'ok', 'err'].indexOf(Cloud.status) === idx ? 'active' : '');
});
// Додаємо обробник закриття попапу
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: {
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',
user: {
name: USER.name,
id: USER.id
},
type: "apartment",
data: apt
};
if (Cloud.socket?.readyState === WebSocket.OPEN) {
Cloud.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Territory_card.getEntrances({ update: true });
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',
user: {
name: USER.name,
id: USER.id
},
type: "building",
data: apt
};
if (Cloud.socket?.readyState === WebSocket.OPEN) {
Cloud.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_card.remove) {
map_card.stopLocate();
map_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_card.locate({ setView: false, watch: true, enableHighAccuracy: true });
map_card.on('locationfound', (e) => {
if (!map_card._userMarker) {
map_card._userMarker = L.marker(e.latlng).addTo(map_card)
.bindPopup("Ви тут!");
} else {
map_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_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_card.setZoom(data.zoom);
// map_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();
}
}
}