Refactor menu flows into dedicated pages
Some checks failed
ci / test (push) Has been cancelled

This commit is contained in:
VPN SaaS Dev
2026-05-16 11:59:09 +09:00
parent 01a69fc42d
commit ecfb5aa949
20 changed files with 2415 additions and 97 deletions

179
web/static/sto_settings.js Normal file
View File

@@ -0,0 +1,179 @@
const page = CarPassPage;
const APPROVED_STATUSES = new Set(["approved", "verified"]);
const MANAGER_ROLES = new Set(["owner", "manager"]);
const state = {
centers: [],
activeCenterId: null,
catalog: null,
};
function activeCenter() {
return state.centers.find((item) => item.id === state.activeCenterId) || null;
}
function roleLabel(role) {
return { owner: "Владелец", manager: "Менеджер", receptionist: "Администратор", mechanic: "Механик" }[role] || role || "СТО";
}
function timeValue(value) {
return String(value || "").slice(0, 5);
}
function setScheduleForm(settings) {
const form = document.querySelector("#bookingSettingsForm");
form.open_time.value = timeValue(settings.open_time || "09:00");
form.close_time.value = timeValue(settings.close_time || "18:00");
form.lunch_break_start.value = timeValue(settings.lunch_break_start || "");
form.lunch_break_end.value = timeValue(settings.lunch_break_end || "");
form.slot_duration_minutes.value = settings.slot_duration_minutes ?? 30;
form.booking_buffer_minutes.value = settings.booking_buffer_minutes ?? 0;
form.max_parallel_bookings.value = settings.max_parallel_bookings ?? 1;
form.timezone.value = settings.timezone || "Asia/Seoul";
form.accepts_online_booking.checked = settings.accepts_online_booking !== false;
const days = new Set(settings.working_days || [0, 1, 2, 3, 4]);
form.querySelectorAll('[name="working_days"]').forEach((input) => {
input.checked = days.has(Number(input.value));
});
}
function schedulePayload(form, centerId) {
const data = page.formData(form);
const workingDays = [...form.querySelectorAll('[name="working_days"]:checked')].map((input) => Number(input.value));
return {
service_center_id: centerId,
working_days: workingDays,
open_time: data.open_time || "09:00",
close_time: data.close_time || "18:00",
lunch_break_start: data.lunch_break_start || null,
lunch_break_end: data.lunch_break_end || null,
timezone: data.timezone || "Asia/Seoul",
slot_duration_minutes: Number(data.slot_duration_minutes || 30),
booking_buffer_minutes: Number(data.booking_buffer_minutes || 0),
max_parallel_bookings: Number(data.max_parallel_bookings || 1),
accepts_online_booking: Boolean(data.accepts_online_booking),
};
}
function catalogPayload(form, centerId) {
const data = page.formData(form);
const isWork = data.item_type === "work";
return {
service_center_id: centerId,
item_type: data.item_type,
title: data.title,
category: data.category || null,
description: data.description || null,
work_type: isWork ? (data.category || "other") : null,
product_type: isWork ? null : (data.category || "other"),
brand: data.brand || null,
sku: data.sku || null,
unit: data.unit || (isWork ? "pcs" : "pcs"),
default_quantity: page.numberOrNull(data.default_quantity) || 1,
default_unit_price: page.numberOrNull(data.default_unit_price) || 0,
viscosity: data.viscosity || null,
specification: data.specification || null,
};
}
function renderHeader() {
const center = activeCenter();
document.querySelector("#centerSelect").innerHTML = state.centers
.map((item) => `<option value="${item.id}">${page.escapeHtml(item.display_name || item.name)}</option>`)
.join("");
if (center) document.querySelector("#centerSelect").value = String(center.id);
document.querySelector("#settingsTitle").textContent = center ? (center.display_name || center.name) : "Нет доступной СТО";
document.querySelector("#settingsHint").textContent = center
? [center.city, center.address].filter(Boolean).join(", ") || "Заполните график и каталог для команды."
: "Настройки доступны владельцу или менеджеру подтвержденной СТО.";
document.querySelector("#roleBadge").textContent = center ? roleLabel(center.employee_role) : "Нет доступа";
}
function renderCatalog() {
const root = document.querySelector("#catalogList");
const centerId = activeCenter()?.id;
const items = (state.catalog?.items || []).filter((item) => item.service_center_id === centerId);
root.innerHTML = items.length
? items.map((item) => `
<div class="stack-item">
<strong>${page.escapeHtml(item.title)}</strong>
<small>${page.escapeHtml([item.item_type === "work" ? "Работа" : "Материал", item.category, item.brand, item.sku].filter(Boolean).join(" · "))}</small>
<small>${page.escapeHtml(item.default_quantity)} ${page.escapeHtml(item.unit)} · ${page.escapeHtml(item.default_unit_price)}</small>
</div>
`).join("")
: `<div class="empty">Пока нет собственных позиций. Системный каталог уже доступен в заказ-нарядах.</div>`;
}
async function loadCenters() {
const centers = await page.api("/service-centers/my");
state.centers = centers.filter((center) =>
APPROVED_STATUSES.has(center.verification_status) && MANAGER_ROLES.has(center.employee_role || "owner"),
);
if (!state.activeCenterId && state.centers.length) state.activeCenterId = state.centers[0].id;
if (state.activeCenterId && !state.centers.some((item) => item.id === state.activeCenterId)) {
state.activeCenterId = state.centers[0]?.id || null;
}
renderHeader();
}
async function loadSettings() {
const center = activeCenter();
if (!center) {
document.querySelector("#bookingSettingsForm").classList.add("hidden");
document.querySelector("#catalogForm").classList.add("hidden");
document.querySelector("#catalogList").innerHTML = `<div class="empty">Нет подтвержденной СТО с ролью владельца или менеджера.</div>`;
return;
}
document.querySelector("#bookingSettingsForm").classList.remove("hidden");
document.querySelector("#catalogForm").classList.remove("hidden");
const [settings, catalog] = await Promise.all([
page.api(`/sto/settings/booking?service_center_id=${center.id}`),
page.api(`/work-orders/catalog?service_center_id=${center.id}`),
]);
state.catalog = catalog;
setScheduleForm(settings);
renderCatalog();
}
document.querySelector("#centerSelect").addEventListener("change", async (event) => {
state.activeCenterId = Number(event.currentTarget.value);
renderHeader();
await loadSettings();
});
document.querySelector("#bookingSettingsForm").addEventListener("submit", async (event) => {
event.preventDefault();
const center = activeCenter();
if (!center) return;
await page.runAction(document.querySelector("#saveScheduleBtn"), "Сохраняю график...", async () => {
const settings = await page.api("/sto/settings/booking", {
method: "POST",
body: JSON.stringify(schedulePayload(event.currentTarget, center.id)),
});
setScheduleForm(settings);
page.toast("График сохранен");
});
});
document.querySelector("#catalogForm").addEventListener("submit", async (event) => {
event.preventDefault();
const center = activeCenter();
if (!center) return;
await page.runAction(document.querySelector("#saveCatalogBtn"), "Добавляю позицию...", async () => {
await page.api("/work-orders/catalog", {
method: "POST",
body: JSON.stringify(catalogPayload(event.currentTarget, center.id)),
});
event.currentTarget.reset();
event.currentTarget.default_quantity.value = 1;
event.currentTarget.default_unit_price.value = 0;
state.catalog = await page.api(`/work-orders/catalog?service_center_id=${center.id}`);
renderCatalog();
page.toast("Позиция добавлена");
});
});
page.boot(async () => {
await loadCenters();
await loadSettings();
});