This commit is contained in:
210
web/static/work_order.js
Normal file
210
web/static/work_order.js
Normal file
@@ -0,0 +1,210 @@
|
||||
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, ">")
|
||||
.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) => `
|
||||
<div class="stack-item work-order-card">
|
||||
<strong>${escapeHtml(item.title)}</strong>
|
||||
${item.description ? `<small>${escapeHtml(item.description)}</small>` : ""}
|
||||
<small>${Number(item.quantity || 1)} ${escapeHtml(item.unit || "шт")} × ${money(item.unit_price || item.price || 0, state.detail.visit.currency)}</small>
|
||||
<div class="work-order-totals single-total">
|
||||
<span>Сумма: <strong>${money(itemTotal(item), state.detail.visit.currency)}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div class="empty">${emptyText}</div>`;
|
||||
}
|
||||
|
||||
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 = `/?section=carProfile&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");
|
||||
});
|
||||
Reference in New Issue
Block a user