This commit is contained in:
2025-09-09 00:10:53 +03:00
parent 38f2a05107
commit 204fc092d7
239 changed files with 22447 additions and 9536 deletions

View File

@@ -1,358 +1,641 @@
let socket, username;
let listEntrances = []
let listApartment = []
let holdTimer;
let startTime;
let map_card;
const Card = {
init: async (type, id) => {
let html = await fetch('/lib/pages/card/index.html').then((response) => response.text());
// Глобальні змінні стану
id: null,
socket: null,
reconnectTimeout: null,
reconnectAttempts: 0,
username: 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/card/index.html').then(r => r.text());
app.innerHTML = html;
Card.id = Id;
house = id;
// Закриваємо старий WebSocket
if (this.socket) this.socket.close(1000, "Перезапуск з'єднання");
this.cloud.start(makeid(6));
if (socket) socket.close(1000, "Перезапуск соединения");
// Якщо це сторінка будинку, отримуємо під’їзди та стартуємо WebSocket
if (type === "house") {
const controls = document.getElementById('page-card-controls');
controls.style.display = "flex";
if (type == "house") {
getEntrances();
start(makeid(6));
// Застосовуємо режим сортування
this.sort(localStorage.getItem('sort_mode'), false);
this.getEntrances({ update: false });
} else if (type === "homestead") {
this.getHomestead.map({});
}
document.addEventListener("mousedown", handleStart);
document.addEventListener("touchstart", handleStart);
document.addEventListener("mouseup", handleCancel);
document.addEventListener("mouseleave", handleCancel);
document.addEventListener("touchend", handleCancel);
document.addEventListener("touchcancel", handleCancel);
function handleStart(event) {
const button = event.target.closest(".hold-button");
if (!button) return;
// event.preventDefault();
startTime = Date.now();
holdTimer = setTimeout(() => {
console.log("Долгое нажатие на", button.name);
let number_id = button.name.split("-");
mess(Number(number_id[0]), Number(number_id[1]));
}, 1000);
// Додаємо обробник закриття попапу
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';
}
},
function handleCancel() {
const holdDuration = Date.now() - startTime; // Считаем, сколько длилось нажатие
// Робота з WebSocket
cloud: {
start(name) {
if (!name) return;
Card.username = name;
if (holdDuration < 1000) {
clearTimeout(holdTimer); // Если нажали менее 1 секунды, сбрасываем таймер
const uuid = localStorage.getItem("uuid");
const ws = new WebSocket(`${CONFIG.wss}?uuid=${uuid}`);
Card.socket = ws;
ws.onopen = () => {
console.log("[WebSocket] З'єднання встановлено");
Card.cloud.setStatus('ok');
ws.send(JSON.stringify({
event: 'connection',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
uuid,
username: name,
data: {}
}));
Card.reconnectAttempts = 0;
clearTimeout(Card.reconnectTimeout);
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.event === 'connection' && data.username !== Card.username) {
console.log(`Новий користувач: ${data.username}`);
}
if (data.event === 'message') {
Card.cloud.update(data);
}
};
ws.onclose = () => {
console.warn("[WebSocket] З'єднання розірвано");
Card.cloud.setStatus('err');
Card.reconnectAttempts++;
if (Card.reconnectAttempts <= 5) {
Card.reconnectTimeout = setTimeout(() => {
Card.getEntrances({ update: true });
Card.cloud.start(Card.username);
}, 1000);
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Card.reconnectAttempts = 0;
Card.getEntrances({ update: true });
Card.cloud.start(Card.username);
}
}
};
ws.onerror = (err) => {
console.error("[WebSocket] Помилка", err);
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] = 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 = 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;
} else if (msg.type === "apartment") {
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;
document.getElementById(`date_text_${id}`).innerText = formattedDateTime(msg.data.updated_at);
}
},
messApartment({ number, id, update, time }) {
const apt = 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] = Card.color_status[apt.status];
statusEl.style = `background:${bg};color:${color};border:1px solid ${color}`;
const message = {
event: 'message',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
username: Card.username,
type: "apartment",
data: {
...apt,
sheep_id: USER.id
}
};
if (Card.socket?.readyState === WebSocket.OPEN) {
Card.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Card.getEntrances({ update: true });
Card.cloud.start(Card.username);
}
}
},
messBuildings({ id, update, time }) {
const apt = Card.listBuilding.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] = Card.color_status[apt.status];
statusEl.style = `background:${bg};color:${color};border:1px solid ${color}`;
const message = {
event: 'message',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
username: Card.username,
type: "building",
data: {
...apt,
sheep_id: USER.id
}
};
if (Card.socket?.readyState === WebSocket.OPEN) {
Card.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Card.getEntrances({ update: true });
Card.cloud.start(Card.username);
}
}
}
}
}
},
// let color_status = [
// "#000000",
// "#C16917",
// "#b10202",
// "#3d3d3d",
// "#11734b",
// "#6cc5fc",
// "#5a3286"
// ];
// let color_status = [
// ["#ffffff", "#000000"],
// ["#e7af32", "#ffffff"],
// ["#fc2a2a", "#ffffff"],
// ["#3d3d3d", "#ffffff"],
// ["#11a568", "#ffffff"],
// ["#6cc5fc", "#ffffff"],
// ["#b381eb", "#ffffff"]
// ];
let color_status = [
["var(--ColorThemes2)", "var(--ColorThemes3)"],
["#fbf1e0", "#ff8300"],
["#fce3e2", "#ff0000"],
["#d7ddec", "#2919bd"],
["#d5e9dd", "#11a568"],
["#d7ebfa", "#3fb4fc"],
["#e8dbf5", "#b381eb"]
];
function start(name) {
if (!name) return;
document.getElementById("hash").innerText = `HASH: ${name}`
username = name;
let uuid = localStorage.getItem("uuid");
socket = new WebSocket(`${CONFIG.wss}?uuid=${uuid}`);
socket.onopen = function (e) {
console.log("[WebSocket | open] Соединение установлено");
document.getElementById("status").innerText = "WebSocket | open";
const message = {
event: 'connection',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
uuid: uuid,
username: name,
data: {
id: 1,
entrance_id: 1,
apartment_number: 1,
title: "1",
group_number: 1,
status: 1,
description: "",
created_at: 1727541827,
updated_at: 1727541827
// Отримання під’їздів
async getEntrances({ house_id = 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;
}
socket.send(JSON.stringify(message))
};
const fragment = document.createDocumentFragment();
socket.onmessage = function (event) {
let data = JSON.parse(event.data)
const canManage = USER.mode === 2 || (USER.mode === 1 && USER.possibilities.can_manager_territory);
if (data.event == 'connection') {
if (data.username == username) return
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);
console.log(`Добавлен новый пользователь по имени ${data.username}`);
} else if (data.event == 'message') {
update(data);
const show = (isMy && working) ? "open" : canManage ? "close" : null;
if (!show) continue;
if (data.username == username) return
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>`;
console.log(`${data.username} пишет: `, data.data);
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 });
}
};
socket.onclose = function (event) {
if (event.wasClean) {
document.getElementById("status").innerText = "WebSocket | close"
console.log(`[WebSocket | close] Соединение закрыто чисто, код=${event.code} причина=${event.reason}`);
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', `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 {
document.getElementById("status").innerText = "WebSocket | close"
console.log('[WebSocket | close] Соединение прервано');
const fragment = document.createDocumentFragment();
// setTimeout(function() {
// start(username);
// }, 1000);
if (data.length == 0) {
const p = document.createElement('p');
const result = confirm(`З'єднання розірвано! Перепідключитись?`);
if (result) {
getEntrances();
start(username);
}
}
};
socket.onerror = function (error) {
console.log(`[WebSocket | error]`);
document.getElementById("status").innerText = "WebSocket | error"
};
}
function mess(entrance_number, id, date_type) {
let sort_mode = localStorage.getItem('sort_mode') ?? true;
console.log(id, listApartment[entrance_number]);
const pos = listApartment[entrance_number].map(e => e.id).indexOf(id);
let apartment = listApartment[entrance_number][pos];
console.log(pos, apartment);
let status = document.getElementById(`status_${id}`);
let description = document.getElementById(`description_${id}`);
let date = new Date(document.getElementById(`date_${id}`).value);
const timestamp = date.getTime();
apartment.description = description.value;
apartment.status = Number(status.value);
apartment.updated_at = date_type ? getTimeInSeconds(timestamp) : getTimeInSeconds(),
status.style.backgroundColor = color_status[status.value][0];
status.style.color = color_status[status.value][1];
status.style.border = `1px solid ${color_status[status.value][1]}`;
let user_hash = localStorage.getItem('hash');
let message = {
event: 'message',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
hash: user_hash,
username: username,
data: {
id: apartment.id,
entrance_id: apartment.entrance_id,
apartment_number: apartment.apartment_number,
title: apartment.title,
group_number: apartment.group_number,
status: apartment.status,
description: apartment.description,
updated_at: apartment.updated_at,
}
}
socket.send(JSON.stringify(message));
if (!date_type && sort_mode != 'false') sort(apartment.id, apartment.entrance_id);
}
function update(message) {
if (!document.getElementById(`status_${message.data.id}`)) return;
let now = new Date(message.data.updated_at);
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
document.getElementById(`card_${message.data.id}`).style.backgroundColor = color_status[message.data.status][0];
document.getElementById(`card_${message.data.id}`).style.color = color_status[message.data.status][1];
document.getElementById(`card_${message.data.id}`).style.border = `1px solid ${color_status[message.data.status][1]}`;
document.getElementById(`status_${message.data.id}`).style.backgroundColor = color_status[message.data.status][0];
document.getElementById(`status_${message.data.id}`).style.color = color_status[message.data.status][1];
document.getElementById(`status_${message.data.id}`).style.border = `1px solid ${color_status[message.data.status][1]}`;
document.getElementById(`status_${message.data.id}`).value = message.data.status;
document.getElementById(`description_${message.data.id}`).value = message.data.description;
document.getElementById(`date_${message.data.id}`).value = now.toISOString().slice(0, 16);
}
function getEntrances(house_id = house) {
const uuid = localStorage.getItem('uuid');
const URL = `${CONFIG.api}/house/${house_id}/entrances`;
fetch(URL, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
})
.then(function (response) {
return response.json();
})
.then(function (data) {
listEntrances = data;
document.getElementById('list').innerHTML = "";
for (let i = 0; i < listEntrances.length; i++) {
const element = listEntrances[i];
let status = () => {
if ((element.history.name == "Групова" || element.history.name == USER.name) && element.working) return "open";
else if (USER.administrator.uuid || (USER.moderator.uuid && USER.moderator.can_manager_territory)) return "close";
return "style='display: none;'"
}
let statusIcon = () => {
if ((element.history.name == "Групова" || element.history.name == USER.name) && element.working) return '<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>'
else return '<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>'
}
document.getElementById('list').innerHTML += `
<details ${status()}>
<summary>
<p>${element.title}</p>
${statusIcon()}
</summary>
<div id="apartments_${element.id}" class="apartments_list">
</div>
</details>
p.innerHTML = `
Інформація про цей під'їзд відсутня. Надайте інформацію відповідальному за території.
`;
getApartment(element.id, element.entrance_number);
console.log(element);
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}`;
function getApartment(entrance_id, entrance_number) {
const uuid = localStorage.getItem('uuid');
const URL = `${CONFIG.api}/apartment/${entrance_id}`;
fetch(URL, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
})
.then(function (response) {
return response.json();
})
.then(function (data) {
listApartment[entrance_number] = data;
const div = document.createElement('div');
data.sort((a, b) => a.apartment_number - b.apartment_number);
div.className = `card_info`;
div.id = `card_${apt.id}`;
div.style = style;
data.sort((a, b) => a.updated_at - b.updated_at);
for (let i = 0; i < data.length; i++) {
const element = data[i];
let now = new Date(element.updated_at);
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
now = now.toISOString().slice(0, 16)
let disabled = () => {
if (USER.administrator.uuid || (USER.moderator.uuid && USER.moderator.can_manager_territory)) return '';
else if (element.status == 2) return "disabled";
}
document.getElementById(`apartments_${entrance_id}`).innerHTML += `
<div id="card_${element.id}" style="border: 1px solid ${color_status[element.status][1]};background: ${color_status[element.status][0]};color: ${color_status[element.status][1]};">
<div class="info">
<span>кв.${element.title}</span>
<select id="status_${element.id}" onchange="mess(${entrance_number}, ${element.id})" style="background-color: ${color_status[element.status][0]}; color: ${color_status[element.status][1]}; border: 1px solid ${color_status[element.status][1]};" ${disabled()}>
<option value="0" ${element.status == 0 ? "selected" : ""}></option>
<option value="1" ${element.status == 1 ? "selected" : ""}>Відмова</option>
<option value="2" ${element.status == 2 ? "selected" : ""}>Не заходити (Груба відмова)</option>
<option value="3" ${element.status == 3 ? "selected" : ""}>Нема домофона</option>
<option value="4" ${element.status == 4 ? "selected" : ""}>Повторна відвідина</option>
<option value="5" ${element.status == 5 ? "selected" : ""}>Немає вдома</option>
<option value="6" ${element.status == 6 ? "selected" : ""}>Свідки Єгови</option>
</select>
<input onchange="mess(${entrance_number}, ${element.id}, true)" name="${entrance_number}-${element.id}" class="hold-button" type="datetime-local" id="date_${element.id}" placeholder="Дата" value="${element.updated_at ? now : ""}" ${disabled()} style="max-width: 170px;">
</div>
<textarea onchange="mess(${entrance_number}, ${element.id}, true)" id="description_${element.id}" placeholder="Нотатки..." ${disabled()}}>${element.description ?? ""}</textarea>
div.innerHTML = `
<div class="info">
<span>кв.${apt.title}</span>
<select id="status_${apt.id}" onchange="Card.cloud.messApartment({number:${number},id:${apt.id},update:true})" style="${style}">
${statusOptions(apt.status)}
</select>
<button id="date_${apt.id}" onclick="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="Card.cloud.messApartment({number:${number},id:${apt.id}})" placeholder="Коротка нотатка.">${apt.description || ""}</textarea>
`;
fragment.appendChild(div);
}
})
}
container.appendChild(fragment);
}
},
function sort(id, entrance_id) {
let child = document.getElementById(`card_${id}`);
getHomestead: {
markers: {},
document.getElementById(`apartments_${entrance_id}`).removeChild(child);
async loadAPI({ url }) {
const uuid = localStorage.getItem("uuid");
document.getElementById(`apartments_${entrance_id}`).append(child);
const response = await fetch(url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
child.style.border = "1px solid var(--PrimaryColor)";
}
return await response.json();
},
function getTimeInSeconds(time = Date.now()) {
// Если время больше 10 знаков (это значит, что время в миллисекундах)
if (time.toString().length < 10) {
// Округляем до секунд, убирая последние 3 цифры (миллисекунды)
time = Math.floor(time * 1000);
async map({ homestead_id = 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);
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 < Card.listBuilding.length; i++) {
// const element = Card.listBuilding[i];
// const [bg, color] = Card.color_status[element.status];
// const style = `background:${bg};color:${color};border:1px solid ${color}`;
// const div = document.createElement('div');
// 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]
// });
// div.className = `card_info card_info_homestead`;
// div.id = `card_${element.id}`;
// div.style = style;
// div.innerHTML = `
// <div class="info">
// <select id="status_${element.id}" onchange="Card.cloud.messBuildings({id:${element.id},update:true})" style="${style}">
// ${statusOptions(element.status)}
// </select>
// <button id="date_${element.id}" onclick="Card.dateEditor.open({id:${element.id},updated_at:${element.updated_at}})">
// <p id="date_text_${element.id}">${formattedDateTime(element.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_${element.id}" onchange="Card.cloud.messBuildings({id:${element.id}})" placeholder="Коротка нотатка.">${element.description || ""}</textarea>
// `;
// L.marker(element.geo, { icon: redDot }).addTo(map_card)
// .bindPopup(div);
// }
for (let i = 0; i < Card.listBuilding.length; i++) {
const element = Card.listBuilding[i];
const [bg, color] = 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 = Card.listBuilding.find(e => e.id === element.id);
const [bg, color] = 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="Card.cloud.messBuildings({id:${element.id},update:true})" style="${style}">
${statusOptions(element.status)}
</select>
<input type="datetime-local" id="date_${element.id}" placeholder="Дата" onchange="Card.cloud.messBuildings({id:${element.id}})" value="${date.toISOString().slice(0, 16)}">
</div>
<textarea id="description_${element.id}" onchange="Card.cloud.messBuildings({id:${element.id}})" placeholder="Коротка нотатка.">${element.description || ""}</textarea>
`;
marker.setPopupContent(div);
});
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", `Card.dateEditor.edit({ id: ${id}, number: ${number} })`)
button.setAttribute("onclick", `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") {
Card.cloud.messApartment({ number: number, id: id, update: true });
} else {
if (input) {
const ts = new Date(input).getTime();
Card.cloud.messApartment({ number, id, update: true, time: ts });
} else {
Card.cloud.messApartment({ number: number, id: id });
}
}
this.close();
}
}
return time;
}