mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 15:04:50 +00:00
fix: добавить 15-секундный таймаут fetch во все модули (measurements, assembly, proposals, request)
Кнопки больше не зависают бесконечно при медленном или недоступном бэкенде. AbortController + дружелюбное сообщение «Сервер не отвечает — попробуйте ещё раз». Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
40e2275949
commit
0fb0597b8d
@ -17,6 +17,20 @@ const Assembly = (function () {
|
|||||||
manager_note: "",
|
manager_note: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function _fetchWithTimeout(url, body, timeoutMs = 15000) {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { method: "POST", signal: ctrl.signal, body: JSON.stringify(body) });
|
||||||
|
return await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mount(container) {
|
function mount(container) {
|
||||||
root = container;
|
root = container;
|
||||||
document.body.classList.remove("has-bottom-nav");
|
document.body.classList.remove("has-bottom-nav");
|
||||||
@ -198,11 +212,7 @@ const Assembly = (function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/assembly_create`, {
|
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/assembly_create`, body);
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
result.innerHTML = `<div class="error">Ошибка: ${escHtml(data.error)}</div>`;
|
result.innerHTML = `<div class="error">Ошибка: ${escHtml(data.error)}</div>`;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@ -247,14 +257,10 @@ const Assembly = (function () {
|
|||||||
const loading = el(`<div class="loader-inline"><div class="spinner"></div></div>`);
|
const loading = el(`<div class="loader-inline"><div class="spinner"></div></div>`);
|
||||||
root.appendChild(loading);
|
root.appendChild(loading);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/assembly_list`, {
|
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/assembly_list`, {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
initData: tg?.initData || "",
|
initData: tg?.initData || "",
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: tg?.initDataUnsafe || null,
|
||||||
}),
|
});
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
loading.remove();
|
loading.remove();
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
root.appendChild(el(`<div class="error">${escHtml(data.error)}</div>`));
|
root.appendChild(el(`<div class="error">${escHtml(data.error)}</div>`));
|
||||||
@ -309,15 +315,11 @@ const Assembly = (function () {
|
|||||||
root.appendChild(loading);
|
root.appendChild(loading);
|
||||||
let a;
|
let a;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/assembly_detail`, {
|
a = await _fetchWithTimeout(`${BACKEND_URL}/api/assembly_detail`, {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
initData: tg?.initData || "",
|
initData: tg?.initData || "",
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: tg?.initDataUnsafe || null,
|
||||||
assembly_id: id,
|
assembly_id: id,
|
||||||
}),
|
});
|
||||||
});
|
|
||||||
a = await res.json();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loading.remove();
|
loading.remove();
|
||||||
root.appendChild(el(`<div class="error">Сеть: ${escHtml(e.message)}</div>`));
|
root.appendChild(el(`<div class="error">Сеть: ${escHtml(e.message)}</div>`));
|
||||||
|
|||||||
@ -94,20 +94,30 @@ const Measurements = (function () {
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _fetchWithTimeout(url, body, timeoutMs = 15000) {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { method: "POST", signal: ctrl.signal, body: JSON.stringify(body) });
|
||||||
|
return await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadRequestAndStart() {
|
async function loadRequestAndStart() {
|
||||||
root.innerHTML = "";
|
root.innerHTML = "";
|
||||||
root.appendChild(renderHeader("Закрыть заявку"));
|
root.appendChild(renderHeader("Закрыть заявку"));
|
||||||
root.appendChild(el(`<div class="loader-inline"><div class="spinner"></div></div>`));
|
root.appendChild(el(`<div class="loader-inline"><div class="spinner"></div></div>`));
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/measurement_detail`, {
|
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/measurement_detail`, {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
initData: tg?.initData || "",
|
initData: tg?.initData || "",
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: tg?.initDataUnsafe || null,
|
||||||
measurement_id: measurementId,
|
measurement_id: measurementId,
|
||||||
}),
|
});
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
root.innerHTML = "";
|
root.innerHTML = "";
|
||||||
root.appendChild(renderHeader("Ошибка"));
|
root.appendChild(renderHeader("Ошибка"));
|
||||||
@ -332,14 +342,10 @@ const Measurements = (function () {
|
|||||||
|
|
||||||
async function fetchNextZamerNo(node) {
|
async function fetchNextZamerNo(node) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/measurement_next_no`, {
|
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/measurement_next_no`, {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
initData: tg?.initData || "",
|
initData: tg?.initData || "",
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: tg?.initDataUnsafe || null,
|
||||||
}),
|
});
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
const hint = node.querySelector("#zamerNoHint");
|
const hint = node.querySelector("#zamerNoHint");
|
||||||
const input = node.querySelector("#zamerNoInput");
|
const input = node.querySelector("#zamerNoInput");
|
||||||
if (data.ok && data.next_no && input && !state.zamer_no) {
|
if (data.ok && data.next_no && input && !state.zamer_no) {
|
||||||
@ -529,14 +535,10 @@ const Measurements = (function () {
|
|||||||
|
|
||||||
let clients = [];
|
let clients = [];
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/clients`, {
|
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/clients`, {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
initData: tg?.initData || "",
|
initData: tg?.initData || "",
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: tg?.initDataUnsafe || null,
|
||||||
}),
|
});
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
clients = (data.clients || []).sort((a, b) =>
|
clients = (data.clients || []).sort((a, b) =>
|
||||||
(a.client_name || "").localeCompare(b.client_name || "", "ru")
|
(a.client_name || "").localeCompare(b.client_name || "", "ru")
|
||||||
);
|
);
|
||||||
@ -937,15 +939,11 @@ const Measurements = (function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/measurement`, {
|
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/measurement`, {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
initData: tg?.initData || "",
|
initData: tg?.initData || "",
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: tg?.initDataUnsafe || null,
|
||||||
measurement,
|
measurement,
|
||||||
}),
|
});
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
result.innerHTML = `<div class="error">Ошибка: ${data.error}</div>`;
|
result.innerHTML = `<div class="error">Ошибка: ${data.error}</div>`;
|
||||||
btn.disabled = false; btn.textContent = isUpdate ? "Закрыть заявку" : "Сохранить замер";
|
btn.disabled = false; btn.textContent = isUpdate ? "Закрыть заявку" : "Сохранить замер";
|
||||||
|
|||||||
@ -19,13 +19,23 @@ const Proposals = (function () {
|
|||||||
return { initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null };
|
return { initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiFetch(path, extra = {}) {
|
async function apiFetch(path, extra = {}, timeoutMs = 15000) {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/${path}`, {
|
const ctrl = new AbortController();
|
||||||
method: "POST",
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
body: JSON.stringify({ ...authBody(), ...extra }),
|
try {
|
||||||
});
|
const res = await fetch(`${BACKEND_URL}/api/${path}`, {
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
method: "POST",
|
||||||
return res.json();
|
signal: ctrl.signal,
|
||||||
|
body: JSON.stringify({ ...authBody(), ...extra }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
|
return res.json();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Constants ─────────────────────────────────────────────
|
// ── Constants ─────────────────────────────────────────────
|
||||||
|
|||||||
@ -15,6 +15,20 @@ const MeasurementRequest = (function () {
|
|||||||
};
|
};
|
||||||
let measurers = [];
|
let measurers = [];
|
||||||
|
|
||||||
|
async function _fetchWithTimeout(url, body, timeoutMs = 15000) {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { method: "POST", signal: ctrl.signal, body: JSON.stringify(body) });
|
||||||
|
return await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === "AbortError") throw new Error("Сервер не отвечает — попробуйте ещё раз");
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mount(container) {
|
function mount(container) {
|
||||||
root = container;
|
root = container;
|
||||||
document.body.classList.remove("has-bottom-nav");
|
document.body.classList.remove("has-bottom-nav");
|
||||||
@ -116,11 +130,9 @@ const MeasurementRequest = (function () {
|
|||||||
|
|
||||||
async function loadMeasurers() {
|
async function loadMeasurers() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/staff_list`, {
|
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/staff_list`, {
|
||||||
method: "POST",
|
initData: tg?.initData || "", role: "measurer",
|
||||||
body: JSON.stringify({ initData: tg?.initData || "", role: "measurer" }),
|
});
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
measurers = data.staff || [];
|
measurers = data.staff || [];
|
||||||
const sel = document.getElementById("measurerSelect");
|
const sel = document.getElementById("measurerSelect");
|
||||||
const hint = document.getElementById("measurerHint");
|
const hint = document.getElementById("measurerHint");
|
||||||
@ -163,21 +175,16 @@ const MeasurementRequest = (function () {
|
|||||||
result.innerHTML = "";
|
result.innerHTML = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BACKEND_URL}/api/measurement_request`, {
|
const data = await _fetchWithTimeout(`${BACKEND_URL}/api/measurement_request`, {
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
initData: tg?.initData || "",
|
initData: tg?.initData || "",
|
||||||
initDataUnsafe: tg?.initDataUnsafe || null,
|
initDataUnsafe: tg?.initDataUnsafe || null,
|
||||||
client_name: name,
|
client_name: name,
|
||||||
client_phone: phone,
|
client_phone: phone,
|
||||||
address: state.address || "",
|
address: state.address || "",
|
||||||
assigned_to_tg_id: state.assigned_to_tg_id || "",
|
assigned_to_tg_id: state.assigned_to_tg_id || "",
|
||||||
// Примечание (рекомендации по дате + особенности) — единое поле
|
|
||||||
preferred_note: state.preferred_note || "",
|
preferred_note: state.preferred_note || "",
|
||||||
preferred_type: "tbd",
|
preferred_type: "tbd",
|
||||||
}),
|
});
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
result.innerHTML = `<div class="error">Ошибка: ${data.error}</div>`;
|
result.innerHTML = `<div class="error">Ошибка: ${data.error}</div>`;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
|
|||||||
@ -41,10 +41,10 @@
|
|||||||
<script src="assets/podbor.js?v=20260517d"></script>
|
<script src="assets/podbor.js?v=20260517d"></script>
|
||||||
<script src="assets/clients.js?v=20260518e"></script>
|
<script src="assets/clients.js?v=20260518e"></script>
|
||||||
<script src="assets/zamer-picts.js?v=20260516h"></script>
|
<script src="assets/zamer-picts.js?v=20260516h"></script>
|
||||||
<script src="assets/measurements.js?v=20260517d"></script>
|
<script src="assets/measurements.js?v=20260518f"></script>
|
||||||
<script src="assets/request.js?v=20260517d"></script>
|
<script src="assets/request.js?v=20260518f"></script>
|
||||||
<script src="assets/assembly.js?v=20260517d"></script>
|
<script src="assets/assembly.js?v=20260518f"></script>
|
||||||
<script src="assets/proposals.js?v=20260516h"></script>
|
<script src="assets/proposals.js?v=20260518f"></script>
|
||||||
<script src="assets/app.js?v=20260517d"></script>
|
<script src="assets/app.js?v=20260517d"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user