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 ? `` : ``; 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', `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 = `
кв.${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 = 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) => ``).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: `
`, 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 = `
`; 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(); } } }