Add STO booking and maintenance automation
This commit is contained in:
@@ -319,6 +319,9 @@ const state = {
|
||||
analytics: null,
|
||||
serviceCenters: [],
|
||||
publicServiceCenters: [],
|
||||
appointments: [],
|
||||
maintenanceRecommendations: [],
|
||||
stoCalendar: [],
|
||||
confirmations: null,
|
||||
connectedServices: [],
|
||||
adminPendingServices: [],
|
||||
@@ -1060,7 +1063,7 @@ async function loadPublicServiceCenters() {
|
||||
const root = document.querySelector("#publicServiceCenters");
|
||||
if (!root) return;
|
||||
try {
|
||||
const centers = await api("/service-centers/public");
|
||||
const centers = await api("/sto/catalog");
|
||||
state.publicServiceCenters = centers;
|
||||
root.innerHTML = centers.length
|
||||
? centers
|
||||
@@ -1070,7 +1073,7 @@ async function loadPublicServiceCenters() {
|
||||
<strong>${center.display_name || center.name}</strong>
|
||||
<small>${[center.city, center.address].filter(Boolean).join(", ") || "Адрес не указан"}</small>
|
||||
<small>${center.specializations?.join(", ") || "Специализация не указана"}</small>
|
||||
<span class="trust-badge">${center.rating_avg ? `★ ${center.rating_avg}` : "Проверка пройдена"}</span>
|
||||
<span class="trust-badge">${center.nearest_slot_at ? `Окно ${formatDateTime(center.nearest_slot_at)}` : center.rating_avg ? `★ ${center.rating_avg}` : "Проверка пройдена"}</span>
|
||||
</button>
|
||||
`,
|
||||
)
|
||||
@@ -1124,6 +1127,33 @@ async function openServiceCard(serviceCenterId) {
|
||||
<div class="service-actions">
|
||||
<button type="button" class="ghost-btn" id="attachServiceBtn">Привязать выбранное авто</button>
|
||||
</div>
|
||||
<form class="grid-form drawer-form" id="serviceBookingForm">
|
||||
<label>
|
||||
Услуга
|
||||
<select name="service_type">
|
||||
<option value="oil_change">Замена масла</option>
|
||||
<option value="diagnostics">Диагностика</option>
|
||||
<option value="maintenance">ТО</option>
|
||||
<option value="tire_service">Шиномонтаж</option>
|
||||
<option value="brakes">Тормоза</option>
|
||||
<option value="repair">Ремонт</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Дата
|
||||
<input name="date" type="date" value="${today()}" />
|
||||
</label>
|
||||
<label>
|
||||
Свободное окно
|
||||
<select name="slot" id="bookingSlotSelect"></select>
|
||||
</label>
|
||||
<label>
|
||||
Комментарий
|
||||
<input name="customer_comment" placeholder="Что нужно сделать" />
|
||||
</label>
|
||||
<button type="submit">Записаться</button>
|
||||
</form>
|
||||
<form class="grid-form drawer-form" id="serviceReviewForm">
|
||||
<label>
|
||||
Оценка
|
||||
@@ -1182,9 +1212,212 @@ async function openServiceCard(serviceCenterId) {
|
||||
haptic("success");
|
||||
});
|
||||
});
|
||||
const bookingForm = card.querySelector("#serviceBookingForm");
|
||||
const reloadSlots = () => loadServiceBookingSlots(serviceCenterId, bookingForm);
|
||||
bookingForm.querySelector('[name="service_type"]').addEventListener("change", reloadSlots);
|
||||
bookingForm.querySelector('[name="date"]').addEventListener("change", reloadSlots);
|
||||
bookingForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!state.selectedCarId) {
|
||||
toast("Выбери автомобиль", "error");
|
||||
return;
|
||||
}
|
||||
const data = formData(bookingForm);
|
||||
if (!data.slot) {
|
||||
toast("Выбери свободное окно", "error");
|
||||
return;
|
||||
}
|
||||
await runAction(bookingForm.querySelector('button[type="submit"]'), "Создаю запись...", async () => {
|
||||
await api("/appointments", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
service_center_id: serviceCenterId,
|
||||
vehicle_id: state.selectedCarId,
|
||||
service_type: data.service_type,
|
||||
service_name: bookingServiceName(data.service_type),
|
||||
requested_start_at: data.slot,
|
||||
customer_comment: data.customer_comment || null,
|
||||
}),
|
||||
});
|
||||
await loadAppointments();
|
||||
toast("Заявка отправлена в СТО");
|
||||
haptic("success");
|
||||
});
|
||||
});
|
||||
await reloadSlots();
|
||||
card.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
|
||||
function bookingServiceName(type) {
|
||||
const names = {
|
||||
oil_change: "Замена масла",
|
||||
diagnostics: "Диагностика",
|
||||
maintenance: "ТО",
|
||||
tire_service: "Шиномонтаж",
|
||||
brakes: "Тормоза",
|
||||
repair: "Ремонт",
|
||||
other: "Другое",
|
||||
};
|
||||
return names[type] || "Обслуживание";
|
||||
}
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
async function loadServiceBookingSlots(serviceCenterId, form) {
|
||||
const select = form.querySelector("#bookingSlotSelect");
|
||||
const serviceType = form.querySelector('[name="service_type"]').value;
|
||||
const date = form.querySelector('[name="date"]').value || today();
|
||||
select.innerHTML = `<option value="">Загружаю...</option>`;
|
||||
try {
|
||||
const slots = await api(`/sto/${serviceCenterId}/available-slots?service_type=${encodeURIComponent(serviceType)}&date_from=${date}&date_to=${date}`);
|
||||
select.innerHTML = slots.length
|
||||
? slots.map((slot) => `<option value="${slot.start_at}">${formatDateTime(slot.start_at)}</option>`).join("")
|
||||
: `<option value="">Нет свободных окон</option>`;
|
||||
} catch (error) {
|
||||
select.innerHTML = `<option value="">Слоты не загрузились</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAppointments() {
|
||||
const root = document.querySelector("#appointmentsList");
|
||||
if (!root) return;
|
||||
try {
|
||||
state.appointments = await api("/appointments/my");
|
||||
root.innerHTML = state.appointments.length
|
||||
? state.appointments.map((item) => `
|
||||
<div class="stack-item">
|
||||
<strong>${item.service_name}</strong>
|
||||
<small>${formatDateTime(item.confirmed_start_at || item.proposed_start_at || item.requested_start_at)}</small>
|
||||
<span class="trust-badge">${item.status}</span>
|
||||
${item.status === "proposed_new_time" ? `
|
||||
<div class="service-actions">
|
||||
<button type="button" data-accept-appointment="${item.id}">Принять время</button>
|
||||
<button type="button" class="ghost-btn" data-reject-appointment="${item.id}">Отклонить</button>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div class="empty">Записей пока нет</div>`;
|
||||
root.querySelectorAll("[data-accept-appointment]").forEach((button) => {
|
||||
button.addEventListener("click", () => runAction(button, "Сохраняю...", async () => {
|
||||
await api(`/appointments/${button.dataset.acceptAppointment}/accept-proposed-time`, { method: "POST" });
|
||||
await loadAppointments();
|
||||
}));
|
||||
});
|
||||
root.querySelectorAll("[data-reject-appointment]").forEach((button) => {
|
||||
button.addEventListener("click", () => runAction(button, "Сохраняю...", async () => {
|
||||
await api(`/appointments/${button.dataset.rejectAppointment}/reject-proposed-time`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ comment: "Отклонено в Mini App" }),
|
||||
});
|
||||
await loadAppointments();
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
root.innerHTML = `<div class="empty">Записи не загрузились</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMaintenanceRecommendations() {
|
||||
const root = document.querySelector("#maintenanceRecommendations");
|
||||
if (!root) return;
|
||||
if (!state.selectedCarId) {
|
||||
root.innerHTML = `<div class="empty">Выбери автомобиль</div>`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
state.maintenanceRecommendations = await api(`/vehicles/${state.selectedCarId}/maintenance-recommendations`);
|
||||
root.innerHTML = state.maintenanceRecommendations.length
|
||||
? state.maintenanceRecommendations.map((item) => `
|
||||
<div class="stack-item">
|
||||
<strong>${item.title}</strong>
|
||||
<small>${item.description || "Плановое обслуживание"}</small>
|
||||
<small>${[item.due_odometer_km ? `${item.due_odometer_km} км` : "", item.due_date || ""].filter(Boolean).join(" · ")}</small>
|
||||
<span class="trust-badge">${item.priority} · ${item.status}</span>
|
||||
${item.status === "active" ? `<button type="button" class="ghost-btn" data-dismiss-recommendation="${item.id}">Скрыть</button>` : ""}
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div class="empty">Рекомендаций пока нет</div>`;
|
||||
root.querySelectorAll("[data-dismiss-recommendation]").forEach((button) => {
|
||||
button.addEventListener("click", () => runAction(button, "Скрываю...", async () => {
|
||||
await api(`/maintenance-recommendations/${button.dataset.dismissRecommendation}/dismiss`, { method: "POST" });
|
||||
await loadMaintenanceRecommendations();
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
root.innerHTML = `<div class="empty">Рекомендации не загрузились</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStoCalendar() {
|
||||
const summary = document.querySelector("#stoDashboardSummary");
|
||||
const list = document.querySelector("#stoCalendarList");
|
||||
if (!summary || !list) return;
|
||||
try {
|
||||
if (!state.serviceCenters.length) {
|
||||
const centers = await api("/service-centers/my");
|
||||
state.serviceCenters = centers;
|
||||
}
|
||||
const center = state.serviceCenters[0];
|
||||
if (!center) {
|
||||
summary.innerHTML = "";
|
||||
list.innerHTML = `<div class="empty">СТО пока не создано</div>`;
|
||||
return;
|
||||
}
|
||||
const [dashboard, appointments] = await Promise.all([
|
||||
api(`/sto/dashboard?service_center_id=${center.id}`),
|
||||
api(`/sto/calendar?service_center_id=${center.id}`),
|
||||
]);
|
||||
summary.innerHTML = `
|
||||
<div class="stat-card"><span>Авто</span><strong>${dashboard.connected_vehicles}</strong></div>
|
||||
<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>${money(dashboard.revenue_month || 0)}</strong></div>
|
||||
`;
|
||||
list.innerHTML = appointments.length
|
||||
? appointments.map((item) => `
|
||||
<div class="stack-item">
|
||||
<strong>${item.service_name}</strong>
|
||||
<small>${formatDateTime(item.confirmed_start_at || item.requested_start_at)} · авто #${item.vehicle_id}</small>
|
||||
<span class="trust-badge">${item.status}</span>
|
||||
${item.status === "requested" ? `
|
||||
<div class="service-actions">
|
||||
<button type="button" data-confirm-sto-appointment="${item.id}">Подтвердить</button>
|
||||
<button type="button" class="ghost-btn" data-reject-sto-appointment="${item.id}">Отклонить</button>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div class="empty">Записей на ближайший период нет</div>`;
|
||||
list.querySelectorAll("[data-confirm-sto-appointment]").forEach((button) => {
|
||||
button.addEventListener("click", () => runAction(button, "Подтверждаю...", async () => {
|
||||
await api(`/sto/appointments/${button.dataset.confirmStoAppointment}/confirm`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ comment: "Подтверждено в Mini App" }),
|
||||
});
|
||||
await loadStoCalendar();
|
||||
}));
|
||||
});
|
||||
list.querySelectorAll("[data-reject-sto-appointment]").forEach((button) => {
|
||||
button.addEventListener("click", () => runAction(button, "Отклоняю...", async () => {
|
||||
await api(`/sto/appointments/${button.dataset.rejectStoAppointment}/reject`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ comment: "Отклонено в Mini App" }),
|
||||
});
|
||||
await loadStoCalendar();
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
summary.innerHTML = "";
|
||||
list.innerHTML = `<div class="empty">Календарь СТО не загрузился</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function trustLabel(level) {
|
||||
const labels = {
|
||||
new_service: "Новый сервис",
|
||||
@@ -2024,6 +2257,9 @@ async function openDrawerSection(sectionId, options = {}) {
|
||||
if (sectionId === "connectedServicesSection") await loadConnectedServices();
|
||||
if (sectionId === "servicePanelSection") await loadServiceCenters();
|
||||
if (sectionId === "publicServicesSection") await loadPublicServiceCenters();
|
||||
if (sectionId === "appointmentsSection") await loadAppointments();
|
||||
if (sectionId === "maintenanceRecommendationsSection") await loadMaintenanceRecommendations();
|
||||
if (sectionId === "stoCalendarSection") await loadStoCalendar();
|
||||
if (sectionId === "reviewsSection") renderServiceReviews();
|
||||
if (sectionId === "adminSection") await loadAdminPendingServices();
|
||||
if (options.expenseCategory) {
|
||||
|
||||
Reference in New Issue
Block a user