let map_card; const Card = { // Глобальні змінні стану 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; // Закриваємо старий WebSocket if (this.socket) this.socket.close(1000, "Перезапуск з'єднання"); this.cloud.start(makeid(6)); // Якщо це сторінка будинку, отримуємо під’їзди та стартуємо 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(name) { if (!name) return; Card.username = name; 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); } } } }, // Отримання під’їздів 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; } 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 ? `` : ``; const details = document.createElement('details'); if (show === "open") details.setAttribute('open', ''); details.innerHTML = `

${title}

${icon}
`; 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) => ``).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 { 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 = `
кв.${apt.title}
`; 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 = 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) => ``).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: `
`, // iconSize: [16, 16], // iconAnchor: [8, 8] // }); // div.className = `card_info card_info_homestead`; // div.id = `card_${element.id}`; // div.style = style; // div.innerHTML = ` //
// // //
// // `; // 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: `
`, 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 = `
`; 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(); } } }