Telezab/app/static/js/regions.js
UdoChudo 52e31864b3 feat: Develop web interface
- Implemented the initial version of the web interface.
refactor: Begin Telegram bot refactoring
- Started restructuring the bot’s code for better maintainability.
chore: Migrate to Flask project structure
- Reorganized the application to follow Flask's project structure.
cleanup: Extensive code cleanup
- Removed redundant code and improved readability.

Signed-off-by: UdoChudo <stream@udochudo.ru>
2025-06-10 14:39:11 +05:00

649 lines
24 KiB
JavaScript
Raw Permalink 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.

const perPage=10
let currentPage=1
let totalPages=1
let regionsCurrentPage=1
let sortField="region_id"
let sortOrder="asc"
let currentFetchController=null
let currentSystemIdToDelete=null
let systemsCurrentPage=1
let systemsTotalPages=1
let systemsSortField="system_id"
let systemsSortOrder="asc"
// Предварительно создаём объекты модалей (чтобы не создавать каждый раз заново)
const editRegionNameModal = new bootstrap.Modal(document.getElementById('editRegionNameModal'));
const deleteRegionModal = new bootstrap.Modal(document.getElementById('deleteRegionModal'));
const regionSubscribersModal = new bootstrap.Modal(document.getElementById('regionSubscribersModal'));
const editSystemNameModal = new bootstrap.Modal(document.getElementById('editSystemNameModal'));
const deleteSystemModal = new bootstrap.Modal(document.getElementById('deleteSystemModal'));
function loadRegions(e){
if(e<1||e>totalPages)return;
currentPage=e;
let t=`/telezab/rest/api/regions?page=${currentPage}&per_page=${perPage}&sort_field=${sortField}&sort_order=${sortOrder}`;
currentFetchController&&currentFetchController.abort(),
safeFetch(t,{signal:(currentFetchController=new AbortController).signal})
.then(e=>e.json())
.then(e=>{
currentFetchController=null,
totalPages=e.total_pages,
updateRegionsTable(e.regions),
updatePagination(e.current_page,e.total_pages,"pagination-regions")
})
.catch(e=>{
"AbortError"===e.name||console.error("Error fetching regions:",e),
currentFetchController=null
})
}
function updateRegionsTable(e){
let t=document.getElementById("regions-table");
if(!t) {
console.error("regions-table element not found!");
return;
}
t.innerHTML="";
e.forEach(e=>{
let n=document.createElement("tr");
n.innerHTML=`
<td>${e.region_id}</td>
<td>${e.name}</td>
<td>
<span id="region-status-label-${e.region_id}">${e.active ? "Включен" : "Выключен"}</span>
</td>
<td>
<div class="d-flex align-items-center">
<div class="form-check form-switch me-2">
<input class="form-check-input region-status-switch" type="checkbox" role="switch" id="region-status-${e.region_id}" data-id="${e.region_id}" ${e.active ? "checked" : ""}>
<label class="form-check-label" for="region-status-${e.region_id}"></label>
</div>
<button class="btn btn-sm btn-primary edit-name-btn me-2" data-id="${e.region_id}" data-name="${e.name}" title="Редактировать название региона">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-info subscribers-btn me-2" data-id="${e.region_id}" title="Посмотреть подписчиков региона">
<i class="bi bi-people"></i>
</button>
<button class="btn btn-sm btn-danger delete-btn" data-id="${e.region_id}" title="Удалить регион">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
`;
t.appendChild(n)
});
setupRegionActions();
}
function setupRegionActions(){
// Используем делегирование событий для избежания множественных обработчиков
const regionsTable = document.getElementById("regions-table");
if (!regionsTable) return;
// Удаляем старые обработчики событий с таблицы
const newTable = regionsTable.cloneNode(true);
regionsTable.parentNode.replaceChild(newTable, regionsTable);
// Используем делегирование событий
newTable.addEventListener('click', function(e) {
const target = e.target.closest('button');
if (!target) return;
if (target.classList.contains('delete-btn')) {
e.preventDefault();
deleteRegion(target.dataset.id);
} else if (target.classList.contains('edit-name-btn')) {
e.preventDefault();
handleEditRegionName(target);
} else if (target.classList.contains('subscribers-btn')) {
e.preventDefault();
showRegionSubscribers(target.dataset.id);
}
});
// Обработчик для переключателей статуса
newTable.addEventListener('change', function(e) {
if (e.target.classList.contains('region-status-switch')) {
let id = e.target.dataset.id;
let active = e.target.checked;
toggleRegionStatus(id, active);
document.getElementById(`region-status-label-${id}`).textContent = active ? "Включен" : "Выключен";
}
});
// Обновляем заголовки для сортировки
document.querySelectorAll("th[data-sort]").forEach(e=>{
const newHeader = e.cloneNode(true);
e.parentNode.replaceChild(newHeader, e);
newHeader.addEventListener("click", ()=>{
let field = newHeader.dataset.sort;
if(field === sortField) {
sortOrder = sortOrder === "asc" ? "desc" : "asc";
} else {
sortField = field;
sortOrder = "asc";
}
loadRegions(currentPage);
});
});
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[title]'));
tooltipTriggerList.map(el => new bootstrap.Tooltip(el));
}
function handleEditRegionName(e){
let t = e.dataset.id,
n = e.dataset.name;
document.getElementById("old-region-name").value = n;
document.getElementById("new-region-name").value = n;
editRegionNameModal.show();
let s = 5;
document.getElementById("edit-region-name-timer").textContent = s;
let a = setInterval(() => {
s--;
document.getElementById("edit-region-name-timer").textContent = s;
if (s === 0) {
clearInterval(a);
document.getElementById("save-region-name-btn").removeAttribute("disabled");
}
}, 1000);
let r = document.getElementById("save-region-name-btn"),
o = r.cloneNode(true);
r.parentNode.replaceChild(o, r);
o.addEventListener("click", () => {
updateRegionName(t, document.getElementById("new-region-name").value);
editRegionNameModal.hide();
});
}
function showRegionSubscribers(e){
safeFetch(`/telezab/rest/api/regions/${e}/subscribers`)
.then(res => res.json())
.then(data => {
let t = document.getElementById("regionSubscribersTableBody");
t.innerHTML = "";
if(data.subscribers && data.subscribers.length > 0){
data.subscribers.forEach(sub => {
let n = document.createElement("tr");
n.innerHTML = `
<td>${sub.telegram_id}</td>
<td>${sub.email}</td>
`;
t.appendChild(n);
});
} else {
let n = document.createElement("tr");
n.innerHTML = `<td colspan="2">Нет подписчиков для этого региона.</td>`;
t.appendChild(n);
}
regionSubscribersModal.show();
})
.catch(err => {
console.error("Ошибка при получении подписчиков региона:", err);
toastr.error("Ошибка при получении подписчиков региона. Пожалуйста, попробуйте позже.");
});
}
function updateRegionName(regionId, newName) {
safeFetch("/telezab/rest/api/regions/name", {
method:"PUT",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({region_id:regionId, name:newName})
})
.then(()=>{
loadRegions(currentPage);
toastr.success("Название региона изменено.");
})
.catch(e=>{
console.error("Ошибка при изменении названия региона:",e);
toastr.error("Ошибка при изменении названия региона. Пожалуйста, попробуйте позже.");
});
}
function toggleRegionStatus(e,t){
safeFetch("/telezab/rest/api/regions/status",{
method:"PUT",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({region_id:e,active:t})
})
.then(()=>{
loadRegions(currentPage);
t?toastr.success("Регион активирован."):toastr.success("Регион деактивирован.");
})
.catch(e=>{
console.error("Ошибка при изменении статуса региона:",e);
toastr.error("Ошибка при изменении статуса региона. Пожалуйста, попробуйте позже.");
});
}
function deleteRegion(e){
deleteRegionModal.show();
let t = document.getElementById("deleteConfirmationInput"),
n = document.getElementById("confirmDeleteButton"),
s = document.getElementById("deleteRegionModal"),
a = t.cloneNode(true),
r = n.cloneNode(true);
t.parentNode.replaceChild(a, t);
n.parentNode.replaceChild(r, n);
a.addEventListener("input", function(){
let val = this.value;
r.disabled = val === "УДАЛИТЬ" ? false : true;
});
r.addEventListener("click", function(){
safeFetch(`/telezab/rest/api/regions/${e}`, { method: "DELETE" })
.then(() => {
loadRegions(currentPage);
toastr.success("Регион успешно удален.");
deleteRegionModal.hide();
})
.catch(err => {
console.error("Ошибка при удалении региона:", err);
toastr.error("Ошибка при удалении региона. Пожалуйста, попробуйте позже.");
});
});
s.addEventListener("hidden.bs.modal", function(){
a.value = "";
r.disabled = true;
}, { once: true });
}
function loadSystems(e){
if(e<1||e>systemsTotalPages)return;
systemsCurrentPage=e;
let t=`/telezab/rest/api/systems?page=${systemsCurrentPage}&per_page=10&sort_field=${systemsSortField}&sort_order=${systemsSortOrder}`;
currentFetchController&&currentFetchController.abort(),
safeFetch(t,{signal:(currentFetchController=new AbortController).signal})
.then(e=>e.json())
.then(e=>{
currentFetchController=null,
systemsTotalPages=e.total_pages,
updateSystemsTable(e.systems),
updatePagination(e.current_page,e.total_pages,"pagination-systems")
})
.catch(e=>{
"AbortError"===e.name||console.error("Error fetching systems:",e),
currentFetchController=null
});
}
function updateSystemsTable(e) {
let t = document.getElementById("systems-table");
if (!t) {
console.error("systems-table element not found!");
return;
}
t.innerHTML = "";
e.forEach(e => {
let n = document.createElement("tr");
n.innerHTML = `
<td>${e.system_id}</td>
<td>${e.system_name}</td>
<td>${e.name}</td>
<td>
<div class="d-flex align-items-center">
<button class="btn btn-sm btn-primary edit-name-btn me-2"
data-id="${e.system_id}"
data-latin-name="${e.system_name}"
data-name="${e.name}"
title="Редактировать название системы">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger delete-btn"
data-id="${e.system_id}"
title="Удалить систему">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
`;
t.appendChild(n);
});
setupSystemActions();
// Инициализация Bootstrap tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[title]'));
tooltipTriggerList.map(el => new bootstrap.Tooltip(el));
}
function setupSystemActions(){
// Используем делегирование событий для таблицы систем
const systemsTable = document.getElementById("systems-table");
if (!systemsTable) return;
// Удаляем старые обработчики событий с таблицы
const newTable = systemsTable.cloneNode(true);
systemsTable.parentNode.replaceChild(newTable, systemsTable);
// Используем делегирование событий
newTable.addEventListener('click', function(e) {
const target = e.target.closest('button');
if (!target) return;
if (target.classList.contains('delete-btn')) {
e.preventDefault();
deleteSystem(target.dataset.id);
} else if (target.classList.contains('edit-name-btn')) {
e.preventDefault();
handleEditSystemName(target);
}
});
// Обновляем заголовки для сортировки
document.querySelectorAll("th[data-sort]").forEach(e=>{
const newHeader = e.cloneNode(true);
e.parentNode.replaceChild(newHeader, e);
newHeader.addEventListener("click", ()=>{
let field = newHeader.dataset.sort;
if(field === systemsSortField) {
systemsSortOrder = systemsSortOrder === "asc" ? "desc" : "asc";
} else {
systemsSortField = field;
systemsSortOrder = "asc";
}
loadSystems(systemsCurrentPage);
});
});
}
function handleEditSystemName(e){
let t = e.dataset.id,
n = e.dataset.latinName,
s = e.dataset.name;
let a = document.getElementById("edit-system-id"),
r = document.getElementById("edit-system-name-lat"),
o = document.getElementById("old-system-name"),
l = document.getElementById("new-system-name");
if(!a || !r || !o || !l){
console.error("❌ Один или несколько элементов не найдены:");
!a && console.error(" - Не найден элемент #edit-system-id");
!r && console.error(" - Не найден элемент #edit-system-name-lat");
!o && console.error(" - Не найден элемент #old-system-name");
!l && console.error(" - Не найден элемент #new-system-name");
return;
}
a.value = t;
r.value = n;
o.value = s;
l.value = s;
editSystemNameModal.show();
let d = 5,
i = document.getElementById("edit-system-name-timer");
i.textContent = d;
let m = setInterval(() => {
d--;
i.textContent = d;
if(d === 0){
clearInterval(m);
document.getElementById("saveSystemNameBtn").removeAttribute("disabled");
}
}, 1000);
let c = document.getElementById("saveSystemNameBtn"),
g = c.cloneNode(true);
c.parentNode.replaceChild(g, c);
g.addEventListener("click", () => {
let id = a.value,
latinName = r.value,
newName = l.value.trim();
if(!newName){
toastr.error("Введите новое название системы.");
return;
}
updateSystemName(id, latinName, newName);
editSystemNameModal.hide();
});
}
function updateSystemName(systemId, systemNameLat, nameCyr) {
if (!systemId || !systemNameLat || !nameCyr) {
toastr.error("Не все параметры заполнены.");
return;
}
safeFetch("/telezab/rest/api/systems", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
system_id: systemId,
system_name: systemNameLat,
name: nameCyr
})
})
.then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.message || "Ошибка сервера") }))
.then(() => {
loadSystems(systemsCurrentPage);
toastr.success("Название системы успешно обновлено.");
})
.catch(e => {
console.error("Ошибка при изменении названия системы:", e);
toastr.error(e.message || "Ошибка при изменении названия системы. Пожалуйста, попробуйте позже.");
});
}
function deleteSystem(e){
deleteSystemModal.show();
let t = document.getElementById("deleteSystemConfirmationInput"),
n = document.getElementById("confirmDeleteSystemButton"),
s = document.getElementById("deleteSystemModal"),
a = t.cloneNode(true),
r = n.cloneNode(true);
t.parentNode.replaceChild(a, t);
n.parentNode.replaceChild(r, n);
a.addEventListener("input", function(){
let val = this.value;
r.disabled = val === "УДАЛИТЬ" ? false : true;
});
r.addEventListener("click", function(){
safeFetch(`/telezab/rest/api/systems/${e}`, { method: "DELETE" })
.then(() => {
loadSystems(systemsCurrentPage);
toastr.success("Система успешно удалена.");
deleteSystemModal.hide();
})
.catch(err => {
console.error("Ошибка при удалении системы:", err);
toastr.error("Ошибка при удалении системы. Пожалуйста, попробуйте позже.");
});
});
s.addEventListener("hidden.bs.modal", function(){
a.value = "";
r.disabled = true;
}, { once: true });
}
function updatePagination(e,t,n){
let s=document.getElementById(n);
if(!s){
console.error(`Container with ID ${n} not found!`);
return;
}
s.innerHTML="";
let a=document.createElement("li");
a.classList.add("page-item");
a.classList.toggle("disabled",1===e);
a.innerHTML=`<a class="page-link" href="#" aria-label="Previous" onclick="loadPage(${e-1}, '${n}')">&laquo;</a>`;
s.appendChild(a);
for(let r=1;r<=t;r++){
let o=document.createElement("li");
o.classList.add("page-item");
o.classList.toggle("active",r===e);
let l=document.createElement("a");
l.classList.add("page-link");
l.href="#";
l.textContent=r;
l.onclick=()=>loadPage(r,n);
o.appendChild(l);
s.appendChild(o);
}
let i=document.createElement("li");
i.classList.add("page-item");
i.classList.toggle("disabled",e===t);
i.innerHTML=`<a class="page-link" href="#" aria-label="Next" onclick="loadPage(${e+1}, '${n}')">&raquo;</a>`;
s.appendChild(i);
}
function loadPage(e,t){
"pagination-regions"===t?loadRegions(e):"pagination-systems"===t&&loadSystems(e);
}
// Настройка toastr
toastr.options={
closeButton:!0,
debug:!1,
newestOnTop:!1,
progressBar:!1,
positionClass:"toast-bottom-right",
preventDuplicates:!1,
onclick:null,
showDuration:"300",
hideDuration:"1000",
timeOut:"5000",
extendedTimeOut:"1000",
showEasing:"swing",
hideEasing:"linear",
showMethod:"fadeIn",
hideMethod:"fadeOut"
};
// Обработчики форм
document.getElementById("add-region-form").addEventListener("submit",e=>{
e.preventDefault();
let regionId=document.getElementById("region-id").value;
let regionName=document.getElementById("region-name").value;
let regionActive=document.getElementById("region-active").checked;
if(!/^\d+$/.test(regionId)){
toastr.error("ID региона должен содержать только числа.");
console.log("Ошибка: ID региона не является числом.");
return;
}
safeFetch("/telezab/rest/api/regions",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({region_id:regionId,name:regionName,active:regionActive})
})
.then(e=>e.ok?e.json():e.json().then(e=>{
throw console.error("Ошибка от сервера:",e),new Error(e.message||"Ошибка добавления региона");
}))
.then(e=>{
if("success"===e.status){
console.log("Регион успешно добавлен.");
document.getElementById("region-id").value="";
document.getElementById("region-name").value="";
document.getElementById("region-active").checked=!1;
loadRegions(regionsCurrentPage);
toastr.success(e.message);
$("#addRegionModal").modal("hide");
}else{
throw console.log("Ошибка: Статус ответа не 'success'."),new Error(e.message||"Неизвестная ошибка");
}
})
.catch(e=>{
console.error("Ошибка при добавлении региона:",e);
toastr.error(e.message||"Ошибка при добавлении региона. Пожалуйста, попробуйте позже.");
});
});
document.getElementById("add-system-form").addEventListener("submit",e=>{
e.preventDefault();
let systemId=document.getElementById("system-id").value;
let systemNameLat=document.getElementById("system-name-lat").value;
let systemNameCyr=document.getElementById("system-name-cyr").value;
if(!/^\d+$/.test(systemId)){
toastr.error("ID системы должен содержать только числа.");
return;
}
safeFetch("/telezab/rest/api/systems",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({system_id:systemId,system_name:systemNameLat,name:systemNameCyr})
})
.then(e=>e.ok?e.json():e.json().then(e=>{
throw new Error(e.message||"Ошибка добавления системы");
}))
.then(e=>{
document.getElementById("system-id").value="";
document.getElementById("system-name-lat").value="";
document.getElementById("system-name-cyr").value="";
loadSystems(systemsCurrentPage);
toastr.success("Система успешно добавлена.");
$("#addSystemModal").modal("hide");
})
.catch(e=>{
console.error("Ошибка при добавлении системы:",e);
toastr.error(e.message||"Ошибка при добавлении системы. Пожалуйста, попробуйте позже.");
});
});
// Инициализация при загрузке страницы
document.addEventListener("DOMContentLoaded",()=>{
let regionsTab=document.getElementById("regions-tab");
let systemsTab=document.getElementById("systems-tab");
regionsTab.addEventListener("shown.bs.tab",()=>{
loadRegions(currentPage);
});
systemsTab.addEventListener("shown.bs.tab",()=>{
loadSystems(systemsCurrentPage);
});
regionsTab.classList.contains("active")&&loadRegions(currentPage);
// Очистка форм при закрытии модальных окон
let modalsConfig=[
{modalId:"addRegionModal",formId:"add-region-form"},
{modalId:"addSystemModal",formId:"add-system-form"},
{modalId:"editRegionNameModal",formId:"edit-region-name-form"},
{modalId:"editSystemNameModal",formId:"edit-system-name-form"}
];
modalsConfig.forEach(({modalId,formId})=>{
let modal=document.getElementById(modalId);
let form=document.getElementById(formId);
if(modal&&form){
modal.addEventListener("hidden.bs.modal",()=>{
form.reset();
});
}
});
});