Gate STO workplace by role
Some checks failed
ci / test (push) Has been cancelled

This commit is contained in:
VPN SaaS Dev
2026-05-16 10:33:33 +09:00
parent 83ad880b9d
commit ac5845d5a0
10 changed files with 1612 additions and 593 deletions

View File

@@ -423,11 +423,12 @@ function formData(form) {
}
async function api(path, options = {}) {
const headers = { "Content-Type": "application/json", ...authHeaders(options.headers || {}) };
const { headers: optionHeaders = {}, ...fetchOptions } = options;
const headers = { "Content-Type": "application/json", ...authHeaders(optionHeaders) };
if (options.body instanceof FormData) delete headers["Content-Type"];
const response = await fetch(`/api${path}`, {
...fetchOptions,
headers,
...options,
});
if (!response.ok) {
const text = await response.text();
@@ -541,9 +542,47 @@ function hideAuthOverlay() {
document.body.classList.remove("auth-required");
}
const APPROVED_SERVICE_STATUSES = new Set(["approved", "verified"]);
const STO_WORKPLACE_ROLES = new Set(["owner", "mechanic"]);
const STO_CALENDAR_ROLES = new Set(["owner", "manager", "receptionist"]);
function isPlatformAdmin() {
return ["admin", "verifier", "moderator"].includes(state.user?.platform_role);
}
function approvedServiceCenters() {
return state.serviceCenters.filter((center) => APPROVED_SERVICE_STATUSES.has(center.verification_status));
}
function stoWorkplaceCenters() {
return approvedServiceCenters().filter((center) => STO_WORKPLACE_ROLES.has(center.employee_role || "owner"));
}
function stoCalendarCenters() {
return approvedServiceCenters().filter((center) => STO_CALENDAR_ROLES.has(center.employee_role || "owner"));
}
function canUseServiceProfile() {
return state.serviceCenters.length > 0 || state.user?.platform_role === "service_owner" || isPlatformAdmin();
}
function canOpenDrawerSection(sectionId) {
if (sectionId === "adminSection") return isPlatformAdmin();
if (sectionId === "mechanicWorkplaceSection") return stoWorkplaceCenters().length > 0;
if (sectionId === "stoCalendarSection") return stoCalendarCenters().length > 0;
if (sectionId === "servicePanelSection") return canUseServiceProfile();
return true;
}
function updateRoleVisibility() {
const isAdmin = ["admin", "verifier", "moderator"].includes(state.user?.platform_role);
const isAdmin = isPlatformAdmin();
const hasWorkplace = stoWorkplaceCenters().length > 0;
const hasCalendar = stoCalendarCenters().length > 0;
const hasServiceProfile = canUseServiceProfile();
document.querySelectorAll(".admin-only").forEach((node) => node.classList.toggle("hidden", !isAdmin));
document.querySelectorAll(".sto-workplace-only").forEach((node) => node.classList.toggle("hidden", !hasWorkplace));
document.querySelectorAll(".sto-calendar-only").forEach((node) => node.classList.toggle("hidden", !hasCalendar));
document.querySelectorAll(".service-owner-only").forEach((node) => node.classList.toggle("hidden", !hasServiceProfile));
}
function showTelegramOpenHint() {
@@ -1051,17 +1090,20 @@ async function loadMyServiceCenters({ withTrust = false } = {}) {
state.activeServiceCenterId = state.serviceCenters[0]?.id || null;
}
renderServiceProfileCard();
updateRoleVisibility();
return state.serviceCenters;
}
function renderServiceProfileCard() {
const card = document.querySelector("#serviceProfileCard");
if (!card) return;
const hasCenters = state.serviceCenters.length > 0;
card.classList.toggle("hidden", !hasCenters);
document.querySelectorAll(".sto-only").forEach((node) => node.classList.toggle("hidden", !hasCenters));
if (!hasCenters) return;
const center = state.serviceCenters.find((item) => item.id === state.activeServiceCenterId) || state.serviceCenters[0];
updateRoleVisibility();
const centers = stoWorkplaceCenters();
const hasWorkplace = centers.length > 0;
card.classList.toggle("hidden", !hasWorkplace);
if (!hasWorkplace) return;
const center = centers.find((item) => item.id === state.activeServiceCenterId) || centers[0];
state.activeServiceCenterId = center.id;
const role = serviceRoleLabel(center.employee_role || "owner");
document.querySelector("#serviceProfileTitle").textContent = center.display_name || center.name || "Рабочее место";
document.querySelector("#serviceProfileMeta").textContent = `${role} · ${serviceStatusLabel(center.verification_status)}`;
@@ -1394,13 +1436,12 @@ async function loadStoCalendar() {
if (!summary || !list) return;
try {
if (!state.serviceCenters.length) {
const centers = await api("/service-centers/my");
state.serviceCenters = centers;
await loadMyServiceCenters();
}
const center = state.serviceCenters[0];
const center = stoCalendarCenters()[0];
if (!center) {
summary.innerHTML = "";
list.innerHTML = `<div class="empty">СТО пока не создано</div>`;
list.innerHTML = `<div class="empty">Календарь доступен только сотрудникам подтвержденного СТО.</div>`;
return;
}
const [dashboard, appointments] = await Promise.all([
@@ -1459,18 +1500,21 @@ async function loadMechanicWorkplace() {
if (!centerSelect || !summary || !list) return;
try {
if (!state.serviceCenters.length) await loadMyServiceCenters();
if (!state.serviceCenters.length) {
const centers = stoWorkplaceCenters();
if (!centers.length) {
centerSelect.innerHTML = "";
summary.innerHTML = "";
list.innerHTML = `<div class="empty">Сначала зарегистрируйте СТО или примите приглашение сотрудника.</div>`;
list.innerHTML = `<div class="empty">Рабочее место доступно владельцу подтвержденного СТО и активным механикам.</div>`;
return;
}
centerSelect.innerHTML = state.serviceCenters
.filter((center) => centers.some((item) => item.id === center.id))
.map((center) => `<option value="${center.id}">${escapeHtml(center.display_name || center.name)}</option>`)
.join("");
centerSelect.value = String(state.activeServiceCenterId || state.serviceCenters[0].id);
const selectedCenter = centers.find((item) => item.id === state.activeServiceCenterId) || centers[0];
centerSelect.value = String(selectedCenter.id);
const serviceCenterId = Number(centerSelect.value);
const center = state.serviceCenters.find((item) => item.id === serviceCenterId) || state.serviceCenters[0];
const center = centers.find((item) => item.id === serviceCenterId) || centers[0];
state.activeServiceCenterId = serviceCenterId;
renderServiceProfileCard();
@@ -2552,7 +2596,13 @@ function mountEntryForms() {
}
async function openDrawerSection(sectionId, options = {}) {
if (!canOpenDrawerSection(sectionId)) {
toast("Этот раздел недоступен для вашей роли", "error");
haptic("error");
sectionId = "carsSection";
}
document.querySelector("#userDrawer").classList.remove("hidden");
const drawerContent = document.querySelector(".drawer-content");
document.querySelectorAll(".drawer-section").forEach((section) => {
section.classList.toggle("hidden", section.id !== sectionId);
});
@@ -2583,11 +2633,11 @@ async function openDrawerSection(sectionId, options = {}) {
if (sectionId === "reviewsSection") renderServiceReviews();
if (sectionId === "adminSection") await loadAdminPendingServices();
if (options.expenseCategory) {
openDrawerSection("expensesSection");
await openDrawerSection("expensesSection");
presetExpense(options.expenseCategory);
return;
}
document.querySelector(`#${sectionId}`)?.scrollIntoView({ behavior: "smooth", block: "start" });
if (drawerContent) drawerContent.scrollTo({ top: 0, behavior: "smooth" });
}
function presetExpense(category) {
@@ -2649,6 +2699,17 @@ document.querySelectorAll("[data-menu-section]").forEach((button) => {
});
});
document.querySelectorAll("[data-open-sto-page]").forEach((button) => {
button.addEventListener("click", () => {
if (!stoWorkplaceCenters().length) {
toast("Панель СТО доступна владельцу подтвержденного СТО и активным механикам", "error");
haptic("error");
return;
}
window.location.href = "/sto.html";
});
});
document.querySelector("#mechanicCenterSelect")?.addEventListener("change", async (event) => {
state.activeServiceCenterId = Number(event.currentTarget.value);
await runAction(event.currentTarget, "Обновляю рабочее место...", loadMechanicWorkplace);