const tg = window.Telegram?.WebApp; tg?.ready(); tg?.expand(); const state = { user: null, authConfig: null, detail: null, }; function orderId() { return Number(new URLSearchParams(window.location.search).get("id") || 0); } function authHeaders(extra = {}) { const headers = { ...extra }; if (tg?.initData) headers["X-Telegram-Init-Data"] = tg.initData; if (!tg?.initData && state.authConfig?.allow_dev_auth) { headers["X-Dev-Telegram-Id"] = localStorage.getItem("driversDevTelegramId") || "1"; } return headers; } async function api(path, options = {}) { const { headers: optionHeaders = {}, ...fetchOptions } = options; const headers = { "Content-Type": "application/json", ...authHeaders(optionHeaders) }; const response = await fetch(`/api${path}`, { ...fetchOptions, headers }); if (!response.ok) throw new Error(await response.text() || response.statusText); if (response.status === 204) return null; return response.json(); } async function loadAuthConfig() { state.authConfig = await api("/users/auth/config"); } async function ensureUser() { if (tg?.initData) { state.user = await api("/users/webapp-auth", { method: "POST", body: JSON.stringify({ init_data: tg.initData }), }); } else if (state.authConfig?.allow_dev_auth) { state.user = await api("/users/me"); } else { showAuthOverlay(); throw new Error("Требуется вход через Telegram"); } document.body.classList.remove("auth-required"); document.querySelector("#authOverlay")?.classList.add("hidden"); } function showAuthOverlay() { document.body.classList.add("auth-required"); const botUsername = state.authConfig?.bot_username; const link = document.querySelector("#telegramLoginLink"); if (botUsername && link) { link.href = `https://t.me/${botUsername}`; link.classList.remove("hidden"); } } function toast(message, tone = "success") { const node = document.querySelector("#toast"); if (!node) return; node.textContent = message; node.className = `toast ${tone}`; window.clearTimeout(toast.timer); toast.timer = window.setTimeout(() => node.classList.add("hidden"), 2600); } function money(value, currency = "RUB") { return Number(value || 0).toLocaleString("ru-RU", { style: "currency", currency, maximumFractionDigits: currency === "KRW" ? 0 : 2, }); } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function statusLabel(status) { return { draft: "Черновик", diagnosis: "Диагностика", waiting_owner_approval: "Ждет согласования", approved_by_owner: "Согласован", rejected_by_owner: "Отклонен", in_progress: "В работе", completed: "Завершен", cancelled: "Отменен", }[status] || status || "Без статуса"; } function itemTotal(item) { return item.total ?? item.price ?? Number(item.quantity || 0) * Number(item.unit_price || 0); } function renderItems(rootSelector, items, emptyText) { const root = document.querySelector(rootSelector); root.innerHTML = items.length ? items.map((item) => `
${escapeHtml(item.title)} ${item.description ? `${escapeHtml(item.description)}` : ""} ${Number(item.quantity || 1)} ${escapeHtml(item.unit || "шт")} × ${money(item.unit_price || item.price || 0, state.detail.visit.currency)}
Сумма: ${money(itemTotal(item), state.detail.visit.currency)}
`).join("") : `
${emptyText}
`; } function renderDecision(detail) { const status = detail.visit.status; const isWaiting = status === "waiting_owner_approval"; document.querySelector("#approveBtn").classList.toggle("hidden", !isWaiting); document.querySelector("#rejectBtn").classList.toggle("hidden", !isWaiting); document.querySelector("#ownerComment").disabled = !isWaiting; if (isWaiting) { document.querySelector("#decisionTitle").textContent = "Нужно ваше решение"; document.querySelector("#decisionText").textContent = "Проверьте работы, материалы и итоговую сумму. Решение попадет в историю заказ-наряда."; return; } if (status === "approved_by_owner") { document.querySelector("#decisionTitle").textContent = "Заказ-наряд согласован"; document.querySelector("#decisionText").textContent = "СТО может выполнять и закрывать работы по согласованной смете."; return; } if (status === "completed") { document.querySelector("#decisionTitle").textContent = "Работы завершены"; document.querySelector("#decisionText").textContent = "Заказ-наряд сохранен в истории автомобиля."; return; } document.querySelector("#decisionTitle").textContent = "Решение сейчас не требуется"; document.querySelector("#decisionText").textContent = "Когда СТО отправит смету на согласование, здесь появятся кнопки решения."; } function renderProfileLink(detail) { const link = document.querySelector("#fillProfileLink"); const missing = detail.catalog?.missing_vehicle_fields || []; if (!missing.length || detail.visit.status === "completed") { link.classList.add("hidden"); return; } link.href = `/car_profile.html?car_id=${detail.vehicle.id}`; link.classList.remove("hidden"); } async function loadDetail() { const id = orderId(); if (!id) throw new Error("Не указан заказ-наряд"); state.detail = await api(`/work-orders/${id}/detail`); const detail = state.detail; document.querySelector("#centerName").textContent = detail.service_center.display_name || detail.service_center.name; document.querySelector("#orderTitle").textContent = detail.visit.work_order_number || `Заказ-наряд #${detail.visit.id}`; document.querySelector("#vehicleMeta").textContent = [ detail.vehicle.name, detail.vehicle.license_plate_display, detail.visit.odometer ? `${detail.visit.odometer} км` : "", ].filter(Boolean).join(" · "); document.querySelector("#statusBadge").textContent = statusLabel(detail.visit.status); document.querySelector("#orderTotal").textContent = money(detail.visit.final_total || detail.visit.total_cost || 0, detail.visit.currency); document.querySelector("#ownerComment").value = detail.visit.owner_comment || ""; renderItems("#laborList", detail.work_items || [], "Работы пока не добавлены"); renderItems("#productList", detail.product_items || [], "Материалы пока не добавлены"); renderDecision(detail); renderProfileLink(detail); } async function decide(action) { const id = orderId(); const comment = document.querySelector("#ownerComment").value.trim() || null; const button = action === "approve" ? document.querySelector("#approveBtn") : document.querySelector("#rejectBtn"); button.disabled = true; try { await api(`/work-orders/${id}/${action}`, { method: "POST", body: JSON.stringify({ comment }), }); toast(action === "approve" ? "Заказ-наряд согласован" : "Заказ-наряд отклонен"); await loadDetail(); } catch (error) { console.error(error); toast(error.message || "Ошибка", "error"); } finally { button.disabled = false; } } document.querySelector("#refreshBtn").addEventListener("click", () => loadDetail()); document.querySelector("#approveBtn").addEventListener("click", () => decide("approve")); document.querySelector("#rejectBtn").addEventListener("click", () => decide("reject")); document.querySelector("#telegramRetryBtn").addEventListener("click", () => window.location.reload()); Promise.all([loadAuthConfig()]) .then(() => ensureUser()) .then(() => loadDetail()) .catch((error) => { if (error.message === "Требуется вход через Telegram") return; console.error(error); toast(error.message || "Ошибка", "error"); });