545 lines
24 KiB
JavaScript
545 lines
24 KiB
JavaScript
const tg = window.Telegram?.WebApp;
|
||
tg?.ready();
|
||
tg?.expand();
|
||
|
||
const APPROVED_SERVICE_STATUSES = new Set(["approved", "verified"]);
|
||
const STO_WORKPLACE_ROLES = new Set(["owner", "manager", "receptionist", "mechanic"]);
|
||
|
||
const state = {
|
||
user: null,
|
||
authConfig: null,
|
||
centers: [],
|
||
activeCenterId: null,
|
||
appointments: [],
|
||
workOrders: [],
|
||
employees: [],
|
||
catalogsByVehicleId: {},
|
||
catalogLookup: {},
|
||
};
|
||
|
||
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) {
|
||
const currency = state.user?.currency || "RUB";
|
||
return Number(value || 0).toLocaleString("ru-RU", {
|
||
style: "currency",
|
||
currency,
|
||
maximumFractionDigits: currency === "KRW" ? 0 : 2,
|
||
});
|
||
}
|
||
|
||
function formatDateTime(value) {
|
||
if (!value) return "-";
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return String(value).slice(0, 16).replace("T", " ");
|
||
return date.toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function formData(form) {
|
||
return Object.fromEntries(new FormData(form).entries());
|
||
}
|
||
|
||
function numberOrNull(value) {
|
||
return value === "" || value == null ? null : Number(value);
|
||
}
|
||
|
||
function activeCenter() {
|
||
return state.centers.find((center) => center.id === state.activeCenterId) || state.centers[0] || null;
|
||
}
|
||
|
||
function roleLabel(role) {
|
||
return {
|
||
owner: "Владелец",
|
||
manager: "Менеджер",
|
||
receptionist: "Администратор",
|
||
mechanic: "Механик",
|
||
}[role] || role || "Сотрудник";
|
||
}
|
||
|
||
function statusLabel(status) {
|
||
return {
|
||
draft: "Черновик",
|
||
diagnosis: "Диагностика",
|
||
waiting_owner_approval: "Ждет согласования",
|
||
approved_by_owner: "Согласован",
|
||
rejected_by_owner: "Отклонен клиентом",
|
||
in_progress: "В работе",
|
||
completed: "Завершен",
|
||
requested: "Новая заявка",
|
||
confirmed: "Подтверждена",
|
||
confirmed_by_sto: "Подтверждена СТО",
|
||
proposed_new_time: "Предложено другое время",
|
||
invited: "Приглашен",
|
||
active: "Активен",
|
||
inactive: "Отключен",
|
||
revoked: "Отозван",
|
||
expired: "Истекло",
|
||
}[status] || status || "Без статуса";
|
||
}
|
||
|
||
async function loadCenters() {
|
||
const centers = await api("/service-centers/my");
|
||
state.centers = centers.filter((center) =>
|
||
APPROVED_SERVICE_STATUSES.has(center.verification_status) && STO_WORKPLACE_ROLES.has(center.employee_role || "owner"),
|
||
);
|
||
if (!state.activeCenterId && state.centers.length) state.activeCenterId = state.centers[0].id;
|
||
if (state.activeCenterId && !state.centers.some((center) => center.id === state.activeCenterId)) {
|
||
state.activeCenterId = state.centers[0]?.id || null;
|
||
}
|
||
}
|
||
|
||
function renderAccessDenied() {
|
||
document.querySelector("#centerTitle").textContent = "Доступ закрыт";
|
||
document.querySelector("#centerMeta").textContent = "Страница доступна владельцу подтвержденного СТО и активным механикам.";
|
||
document.querySelector("#roleBadge").textContent = "Нет доступа";
|
||
document.querySelector("#centerSelect").innerHTML = "";
|
||
document.querySelector("#dashboardStats").innerHTML = "";
|
||
document.querySelector("#appointmentsList").innerHTML = `<div class="empty">Нет доступного подтвержденного СТО.</div>`;
|
||
document.querySelector("#workOrdersList").innerHTML = `<div class="empty">Рабочее место недоступно.</div>`;
|
||
document.querySelector("#staffPanel").classList.add("hidden");
|
||
}
|
||
|
||
async function loadWorkplace() {
|
||
await loadCenters();
|
||
if (!state.centers.length) {
|
||
renderAccessDenied();
|
||
return;
|
||
}
|
||
const center = activeCenter();
|
||
document.querySelector("#centerSelect").innerHTML = state.centers
|
||
.map((item) => `<option value="${item.id}">${escapeHtml(item.display_name || item.name)}</option>`)
|
||
.join("");
|
||
document.querySelector("#centerSelect").value = String(center.id);
|
||
document.querySelector("#centerTitle").textContent = center.display_name || center.name;
|
||
document.querySelector("#centerMeta").textContent = [center.city, center.address].filter(Boolean).join(", ") || "Адрес не указан";
|
||
document.querySelector("#roleBadge").textContent = roleLabel(center.employee_role || "owner");
|
||
|
||
const [dashboard, appointments, visits, employees] = await Promise.all([
|
||
api(`/sto/dashboard?service_center_id=${center.id}`).catch(() => null),
|
||
api(`/sto/appointments?service_center_id=${center.id}`).catch(() => []),
|
||
api(`/service-centers/${center.id}/visits`).catch(() => []),
|
||
center.employee_role === "owner" ? api(`/service-centers/${center.id}/employees`).catch(() => []) : Promise.resolve([]),
|
||
]);
|
||
state.appointments = appointments.filter((item) => ["requested", "confirmed", "confirmed_by_sto", "proposed_new_time"].includes(item.status));
|
||
state.workOrders = visits.filter((item) => !["completed", "cancelled", "archived", "confirmed", "disputed"].includes(item.status));
|
||
state.employees = employees;
|
||
await loadWorkOrderCatalogs(center.id);
|
||
renderDashboard(dashboard);
|
||
renderAppointments();
|
||
renderWorkOrders();
|
||
renderStaff();
|
||
}
|
||
|
||
async function loadWorkOrderCatalogs(serviceCenterId) {
|
||
state.catalogsByVehicleId = {};
|
||
state.catalogLookup = {};
|
||
const vehicleIds = [...new Set(state.workOrders.map((item) => item.vehicle_id).filter(Boolean))];
|
||
await Promise.all(vehicleIds.map(async (vehicleId) => {
|
||
try {
|
||
state.catalogsByVehicleId[vehicleId] = await api(`/work-orders/catalog?service_center_id=${serviceCenterId}&vehicle_id=${vehicleId}`);
|
||
} catch (_) {
|
||
state.catalogsByVehicleId[vehicleId] = { items: [], vehicle_suggestions: [], missing_vehicle_fields: [] };
|
||
}
|
||
}));
|
||
}
|
||
|
||
function catalogForWorkOrder(workOrder) {
|
||
return state.catalogsByVehicleId[workOrder.vehicle_id] || { items: [], vehicle_suggestions: [], missing_vehicle_fields: [] };
|
||
}
|
||
|
||
function registerCatalogOption(item) {
|
||
const key = `${item.source || "catalog"}:${item.id || Object.keys(state.catalogLookup).length}:${item.item_type}:${item.title}`;
|
||
state.catalogLookup[key] = item;
|
||
return key;
|
||
}
|
||
|
||
function catalogOptions(workOrder, itemType) {
|
||
const catalog = catalogForWorkOrder(workOrder);
|
||
const catalogItems = (catalog.items || []).filter((item) => item.item_type === itemType);
|
||
const suggestions = itemType === "product" ? (catalog.vehicle_suggestions || []) : [];
|
||
return [...catalogItems, ...suggestions].map((item) => {
|
||
const key = registerCatalogOption(item);
|
||
const meta = [item.brand, item.category, item.specification || item.sku].filter(Boolean).join(" · ");
|
||
return `<option value="${escapeHtml(key)}">${escapeHtml(item.title)}${meta ? ` · ${escapeHtml(meta)}` : ""}</option>`;
|
||
}).join("");
|
||
}
|
||
|
||
function missingVehicleFieldsText(fields) {
|
||
const labels = {
|
||
engine_oil: "моторное масло",
|
||
transmission_fluid: "трансмиссионная жидкость",
|
||
coolant: "антифриз",
|
||
brake_fluid: "тормозная жидкость",
|
||
};
|
||
return fields.map((field) => labels[field] || field).join(", ");
|
||
}
|
||
|
||
function renderDashboard(dashboard) {
|
||
document.querySelector("#dashboardStats").innerHTML = dashboard
|
||
? `
|
||
<div class="stat-card"><span>Заявки</span><strong>${dashboard.pending_appointments}</strong></div>
|
||
<div class="stat-card"><span>Подтверждено</span><strong>${dashboard.confirmed_appointments}</strong></div>
|
||
<div class="stat-card"><span>Заказ-наряды</span><strong>${dashboard.active_work_orders}</strong></div>
|
||
<div class="stat-card"><span>Авто</span><strong>${dashboard.connected_vehicles}</strong></div>
|
||
<div class="stat-card"><span>Месяц</span><strong>${money(dashboard.revenue_month || 0)}</strong></div>
|
||
`
|
||
: `<div class="empty">Сводка недоступна</div>`;
|
||
}
|
||
|
||
function renderAppointments() {
|
||
const role = activeCenter()?.employee_role || "owner";
|
||
const canManage = ["owner", "manager", "receptionist"].includes(role);
|
||
document.querySelector("#appointmentsList").innerHTML = state.appointments.length
|
||
? state.appointments.map((item) => `
|
||
<div class="stack-item work-order-card">
|
||
<strong>${escapeHtml(item.service_name)}</strong>
|
||
<small>${formatDateTime(item.confirmed_start_at || item.requested_start_at)} · авто #${item.vehicle_id}</small>
|
||
<span class="trust-badge">${statusLabel(item.status)}</span>
|
||
${canManage ? `<div class="row-actions">
|
||
${item.status === "requested" ? `<button type="button" data-confirm-appointment="${item.id}">Подтвердить</button>` : ""}
|
||
${["confirmed", "confirmed_by_sto"].includes(item.status) ? `<button type="button" data-create-work-order="${item.id}">Открыть заказ-наряд</button>` : ""}
|
||
<button type="button" class="ghost-btn" data-reject-appointment="${item.id}">Отклонить</button>
|
||
${!["converted_to_work_order", "completed"].includes(item.status) ? `<button type="button" class="danger-btn" data-delete-appointment="${item.id}">Удалить</button>` : ""}
|
||
</div>` : ""}
|
||
</div>
|
||
`).join("")
|
||
: `<div class="empty">Новых записей нет</div>`;
|
||
}
|
||
|
||
function renderWorkOrders() {
|
||
const role = activeCenter()?.employee_role || "owner";
|
||
const canComplete = role === "owner";
|
||
state.catalogLookup = {};
|
||
document.querySelector("#workOrdersList").innerHTML = state.workOrders.length
|
||
? state.workOrders.map((item) => {
|
||
const catalog = catalogForWorkOrder(item);
|
||
const missingFields = catalog.missing_vehicle_fields || [];
|
||
return `
|
||
<div class="stack-item work-order-card">
|
||
<div class="work-order-head">
|
||
<div>
|
||
<strong>${escapeHtml(item.work_order_number || `Заказ-наряд #${item.id}`)}</strong>
|
||
<small>${item.visit_date} · авто #${item.vehicle_id} · ${item.odometer || "-"} км</small>
|
||
</div>
|
||
<span class="trust-badge">${statusLabel(item.status)}</span>
|
||
</div>
|
||
${item.customer_complaint ? `<small>Жалоба: ${escapeHtml(item.customer_complaint)}</small>` : ""}
|
||
${item.diagnosis ? `<small>Диагностика: ${escapeHtml(item.diagnosis)}</small>` : ""}
|
||
<div class="work-order-totals">
|
||
<span>Работы: <strong>${money(item.labor_total || 0)}</strong></span>
|
||
<span>Запчасти: <strong>${money(item.product_total || 0)}</strong></span>
|
||
<span>Итого: <strong>${money(item.final_total || item.total_cost || 0)}</strong></span>
|
||
</div>
|
||
${missingFields.length ? `<div class="tip-card compact-tip">
|
||
Для точного подбора не хватает: ${escapeHtml(missingVehicleFieldsText(missingFields))}.
|
||
<button type="button" class="ghost-btn" data-request-vehicle-profile="${item.id}" data-missing-fields="${escapeHtml(missingFields.join(","))}">Попросить заполнить</button>
|
||
</div>` : ""}
|
||
<form class="inline-work-form catalog-work-form" data-labor-form="${item.id}">
|
||
<select name="catalog_item" data-catalog-select>
|
||
<option value="">Работа из каталога</option>
|
||
${catalogOptions(item, "work")}
|
||
</select>
|
||
<input name="title" placeholder="Работа" required />
|
||
<input name="quantity" type="number" min="0.001" step="0.001" value="1" aria-label="Количество" />
|
||
<input name="unit_price" type="number" min="0" step="0.01" placeholder="Цена" required />
|
||
<input name="unit" type="hidden" value="job" />
|
||
<input name="work_type" type="hidden" value="repair" />
|
||
<input name="category" type="hidden" />
|
||
<button type="submit">+ Работа</button>
|
||
</form>
|
||
<form class="inline-work-form catalog-work-form" data-product-form="${item.id}">
|
||
<select name="catalog_item" data-catalog-select>
|
||
<option value="">Материал из каталога</option>
|
||
${catalogOptions(item, "product")}
|
||
</select>
|
||
<input name="title" placeholder="Запчасть / материал" required />
|
||
<input name="quantity" type="number" min="0.001" step="0.001" value="1" aria-label="Количество" />
|
||
<input name="unit_price" type="number" min="0" step="0.01" placeholder="Цена" required />
|
||
<input name="unit" type="hidden" value="pcs" />
|
||
<input name="product_type" type="hidden" value="part" />
|
||
<input name="category" type="hidden" />
|
||
<input name="brand" type="hidden" />
|
||
<input name="sku" type="hidden" />
|
||
<input name="viscosity" type="hidden" />
|
||
<input name="specification" type="hidden" />
|
||
<input name="volume" type="hidden" />
|
||
<button type="submit">+ Материал</button>
|
||
</form>
|
||
<div class="row-actions">
|
||
${["draft", "diagnosis", "approved_by_owner"].includes(item.status) ? `<button type="button" data-start-work-order="${item.id}">В работу</button>` : ""}
|
||
${role === "owner" ? `<button type="button" data-submit-work-order="${item.id}">На согласование</button>` : ""}
|
||
${canComplete ? `<button type="button" class="ghost-btn" data-complete-work-order="${item.id}">Завершить</button>` : ""}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join("")
|
||
: `<div class="empty">Активных заказ-нарядов нет</div>`;
|
||
}
|
||
|
||
function renderStaff() {
|
||
const center = activeCenter();
|
||
const panel = document.querySelector("#staffPanel");
|
||
if (!center || center.employee_role !== "owner") {
|
||
panel.classList.add("hidden");
|
||
return;
|
||
}
|
||
panel.classList.remove("hidden");
|
||
document.querySelector("#employeesList").innerHTML = state.employees.length
|
||
? state.employees.map((employee) => `
|
||
<div class="stack-item">
|
||
<strong>${escapeHtml(employee.first_name || employee.username || `Telegram ${employee.telegram_id || employee.user_id}`)}</strong>
|
||
<small>${employee.telegram_id ? `Telegram ID: ${employee.telegram_id}` : `User #${employee.user_id}`}</small>
|
||
<span class="trust-badge">${roleLabel(employee.role)} · ${statusLabel(employee.status)}</span>
|
||
${employee.invite_token ? `<small>Команда для сотрудника: /accept_sto_invite ${employee.invite_token}</small>` : ""}
|
||
${employee.role !== "owner" ? `<div class="row-actions">
|
||
<button type="button" data-role-employee="${employee.id}" data-role="mechanic">Механик</button>
|
||
<button type="button" data-role-employee="${employee.id}" data-role="receptionist">Администратор</button>
|
||
<button type="button" class="ghost-btn" data-disable-employee="${employee.id}">Отключить</button>
|
||
</div>` : ""}
|
||
</div>
|
||
`).join("")
|
||
: `<div class="empty">Сотрудников пока нет</div>`;
|
||
}
|
||
|
||
async function runAction(button, callback) {
|
||
if (button) button.disabled = true;
|
||
try {
|
||
await callback();
|
||
toast("Готово");
|
||
await loadWorkplace();
|
||
} catch (error) {
|
||
console.error(error);
|
||
toast(error.message || "Ошибка", "error");
|
||
} finally {
|
||
if (button) button.disabled = false;
|
||
}
|
||
}
|
||
|
||
document.querySelector("#centerSelect").addEventListener("change", async (event) => {
|
||
state.activeCenterId = Number(event.currentTarget.value);
|
||
await loadWorkplace();
|
||
});
|
||
|
||
document.querySelector("#refreshBtn").addEventListener("click", () => loadWorkplace());
|
||
|
||
document.querySelector("#inviteForm").addEventListener("submit", async (event) => {
|
||
event.preventDefault();
|
||
const form = event.currentTarget;
|
||
const center = activeCenter();
|
||
const data = formData(form);
|
||
await runAction(form.querySelector('button[type="submit"]'), async () => {
|
||
const employee = await api(`/service-centers/${center.id}/employees/invite`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
telegram_id: Number(data.telegram_id),
|
||
role: data.role,
|
||
}),
|
||
});
|
||
const result = document.querySelector("#inviteResult");
|
||
result.classList.remove("hidden");
|
||
result.textContent = `Передайте сотруднику команду: /accept_sto_invite ${employee.invite_token}`;
|
||
form.reset();
|
||
});
|
||
});
|
||
|
||
document.body.addEventListener("click", async (event) => {
|
||
const button = event.target.closest("button");
|
||
if (!button) return;
|
||
if (button.dataset.confirmAppointment) {
|
||
await runAction(button, () => api(`/sto/appointments/${button.dataset.confirmAppointment}/confirm`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ comment: "Подтверждено в панели СТО" }),
|
||
}));
|
||
}
|
||
if (button.dataset.rejectAppointment) {
|
||
await runAction(button, () => api(`/sto/appointments/${button.dataset.rejectAppointment}/reject`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ comment: "Отклонено в панели СТО" }),
|
||
}));
|
||
}
|
||
if (button.dataset.deleteAppointment) {
|
||
if (!window.confirm("Удалить бронь из панели СТО?")) return;
|
||
await runAction(button, () => api(`/sto/appointments/${button.dataset.deleteAppointment}`, {
|
||
method: "DELETE",
|
||
}));
|
||
}
|
||
if (button.dataset.createWorkOrder) {
|
||
const odometer = window.prompt("Пробег на приемке, км") || "";
|
||
await runAction(button, () => api(`/sto/appointments/${button.dataset.createWorkOrder}/create-work-order`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ odometer: numberOrNull(odometer), notes: "Создано в панели СТО" }),
|
||
}));
|
||
}
|
||
if (button.dataset.startWorkOrder) {
|
||
await runAction(button, () => api(`/work-orders/${button.dataset.startWorkOrder}/start`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ comment: "Взято в работу" }),
|
||
}));
|
||
}
|
||
if (button.dataset.submitWorkOrder) {
|
||
await runAction(button, () => api(`/work-orders/${button.dataset.submitWorkOrder}/submit-approval`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ comment: "Смета готова к согласованию" }),
|
||
}));
|
||
}
|
||
if (button.dataset.completeWorkOrder) {
|
||
const odometer = window.prompt("Пробег на закрытии, км. Можно оставить пустым, если пробег уже указан.") || "";
|
||
await runAction(button, () => api(`/work-orders/${button.dataset.completeWorkOrder}/complete`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ comment: "Работы завершены", odometer: numberOrNull(odometer) }),
|
||
}));
|
||
}
|
||
if (button.dataset.requestVehicleProfile) {
|
||
const missingFields = (button.dataset.missingFields || "").split(",").filter(Boolean);
|
||
await runAction(button, () => api(`/work-orders/${button.dataset.requestVehicleProfile}/request-vehicle-profile`, {
|
||
method: "POST",
|
||
body: JSON.stringify({ missing_fields: missingFields }),
|
||
}));
|
||
}
|
||
if (button.dataset.roleEmployee) {
|
||
await runAction(button, () => api(`/service-centers/employees/${button.dataset.roleEmployee}`, {
|
||
method: "PATCH",
|
||
body: JSON.stringify({ role: button.dataset.role, status: "active" }),
|
||
}));
|
||
}
|
||
if (button.dataset.disableEmployee) {
|
||
await runAction(button, () => api(`/service-centers/employees/${button.dataset.disableEmployee}`, {
|
||
method: "PATCH",
|
||
body: JSON.stringify({ status: "inactive" }),
|
||
}));
|
||
}
|
||
});
|
||
|
||
document.body.addEventListener("change", (event) => {
|
||
const select = event.target.closest("[data-catalog-select]");
|
||
if (!select || !select.value) return;
|
||
const item = state.catalogLookup[select.value];
|
||
if (!item) return;
|
||
const form = select.closest("form");
|
||
form.title.value = item.title || "";
|
||
form.quantity.value = item.default_quantity || 1;
|
||
form.unit_price.value = item.default_unit_price || 0;
|
||
if (form.unit) form.unit.value = item.unit || form.unit.value;
|
||
if (form.category) form.category.value = item.category || "";
|
||
if (form.work_type) form.work_type.value = item.work_type || "repair";
|
||
if (form.product_type) form.product_type.value = item.product_type || "part";
|
||
if (form.brand) form.brand.value = item.brand || "";
|
||
if (form.sku) form.sku.value = item.sku || "";
|
||
if (form.viscosity) form.viscosity.value = item.viscosity || "";
|
||
if (form.specification) form.specification.value = item.specification || "";
|
||
if (form.volume) form.volume.value = item.volume || "";
|
||
});
|
||
|
||
document.body.addEventListener("submit", async (event) => {
|
||
const form = event.target;
|
||
if (form.matches("[data-labor-form]")) {
|
||
event.preventDefault();
|
||
const data = formData(form);
|
||
await runAction(form.querySelector('button[type="submit"]'), () => api(`/work-orders/${form.dataset.laborForm}/labor-items`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
title: data.title,
|
||
category: data.category || null,
|
||
quantity: Number(data.quantity || 1),
|
||
unit: data.unit || "job",
|
||
unit_price: Number(data.unit_price || 0),
|
||
work_type: data.work_type || "repair",
|
||
}),
|
||
}));
|
||
}
|
||
if (form.matches("[data-product-form]")) {
|
||
event.preventDefault();
|
||
const data = formData(form);
|
||
await runAction(form.querySelector('button[type="submit"]'), () => api(`/work-orders/${form.dataset.productForm}/product-items`, {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
title: data.title,
|
||
category: data.category || null,
|
||
quantity: Number(data.quantity || 1),
|
||
unit: data.unit || "pcs",
|
||
unit_price: Number(data.unit_price || 0),
|
||
product_type: data.product_type || "part",
|
||
brand: data.brand || null,
|
||
sku: data.sku || null,
|
||
viscosity: data.viscosity || null,
|
||
specification: data.specification || null,
|
||
volume: numberOrNull(data.volume),
|
||
}),
|
||
}));
|
||
}
|
||
});
|
||
|
||
document.querySelector("#telegramRetryBtn").addEventListener("click", () => window.location.reload());
|
||
|
||
Promise.all([loadAuthConfig()])
|
||
.then(() => ensureUser())
|
||
.then(() => loadWorkplace())
|
||
.catch((error) => {
|
||
if (error.message === "Требуется вход через Telegram") return;
|
||
console.error(error);
|
||
toast(error.message || "Ошибка", "error");
|
||
});
|