Improve CarPass product UX and service flows
This commit is contained in:
@@ -313,6 +313,7 @@ const state = {
|
||||
selectedCarId: null,
|
||||
latestFuel: [],
|
||||
latestService: [],
|
||||
latestExpenses: [],
|
||||
latestStats: null,
|
||||
allStats: null,
|
||||
analytics: null,
|
||||
@@ -412,11 +413,8 @@ function formData(form) {
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const headers = { "Content-Type": "application/json", ...(options.headers || {}) };
|
||||
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";
|
||||
}
|
||||
const headers = { "Content-Type": "application/json", ...authHeaders(options.headers || {}) };
|
||||
if (options.body instanceof FormData) delete headers["Content-Type"];
|
||||
const response = await fetch(`/api${path}`, {
|
||||
headers,
|
||||
...options,
|
||||
@@ -429,6 +427,15 @@ async function api(path, options = {}) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
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 loadAuthConfig() {
|
||||
state.authConfig = await api("/users/auth/config");
|
||||
window.APP_VAPID_PUBLIC_KEY = state.authConfig.vapid_public_key || "";
|
||||
@@ -539,7 +546,7 @@ function showTelegramOpenHint() {
|
||||
if (slot && !slot.dataset.ready) slot.textContent = "";
|
||||
if (note) {
|
||||
note.textContent = isMobileBrowser()
|
||||
? "После перехода в Telegram нажмите в боте кнопку «Открыть гараж»."
|
||||
? "После перехода нажмите Start, затем кнопку «Открыть CarPass» под сообщением бота."
|
||||
: "На компьютере можно войти кнопкой Telegram ниже или открыть бота.";
|
||||
}
|
||||
if (!botUsername) {
|
||||
@@ -638,9 +645,12 @@ function applyPeriodPreset(preset = "month") {
|
||||
const to = dateValue(now);
|
||||
let fromDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
if (preset === "all") fromDate = new Date(2000, 0, 1);
|
||||
if (preset === "7d") fromDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
|
||||
if (preset === "30d" || preset === "month") fromDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 29);
|
||||
if (preset === "3m" || preset === "quarter") fromDate = shiftMonths(now, -3);
|
||||
if (preset === "6m") fromDate = shiftMonths(now, -6);
|
||||
if (preset === "12m" || preset === "year") fromDate = shiftMonths(now, -12);
|
||||
if (preset === "day") fromDate = now;
|
||||
if (preset === "quarter") fromDate = shiftMonths(now, -3);
|
||||
if (preset === "year") fromDate = shiftMonths(now, -12);
|
||||
if (preset !== "custom") {
|
||||
document.querySelector("#periodFrom").value = dateValue(fromDate);
|
||||
document.querySelector("#periodTo").value = to;
|
||||
@@ -745,14 +755,12 @@ function resetCarCatalog() {
|
||||
|
||||
function updateHero(stats) {
|
||||
const car = selectedCar();
|
||||
document.querySelector("#selectedCarTitle").textContent = car?.name || t("Не выбран");
|
||||
document.querySelector("#selectedCarTitle").textContent = stats ? money(stats.cost_per_month || stats.total_cost || 0) : t("Не выбран");
|
||||
document.querySelector("#selectedCarMeta").textContent = car
|
||||
? [car.make, car.model, car.trim, car.year, car.fuel_type].filter(Boolean).join(" ") || t("Без деталей")
|
||||
: t("Добавь авто или выбери из списка");
|
||||
document.querySelector("#summaryTotal").textContent = money(stats?.total_cost);
|
||||
document.querySelector("#summaryConsumption").textContent = stats?.avg_consumption_l_per_100km
|
||||
? `${stats.avg_consumption_l_per_100km.toFixed(2)} л`
|
||||
: "-";
|
||||
document.querySelector("#summaryTotal").textContent = stats?.cost_per_km ? money(stats.cost_per_km) : "-";
|
||||
document.querySelector("#summaryConsumption").textContent = stats ? money(stats.forecast_next_month || 0) : "-";
|
||||
}
|
||||
|
||||
function formatFuelPrice(value) {
|
||||
@@ -762,12 +770,14 @@ function formatFuelPrice(value) {
|
||||
|
||||
function renderCars() {
|
||||
const root = document.querySelector("#cars");
|
||||
const drawerRoot = document.querySelector("#drawerCars");
|
||||
if (!state.cars.length) {
|
||||
root.innerHTML = `<div class="empty">${t("Добавь первый автомобиль")}</div>`;
|
||||
if (drawerRoot) drawerRoot.innerHTML = root.innerHTML;
|
||||
updateHero(null);
|
||||
return;
|
||||
}
|
||||
root.innerHTML = state.cars
|
||||
const markup = state.cars
|
||||
.map(
|
||||
(car) => `
|
||||
<button class="car-item ${car.id === state.selectedCarId ? "active" : ""}" data-car="${car.id}">
|
||||
@@ -780,7 +790,9 @@ function renderCars() {
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
root.querySelectorAll("[data-car]").forEach((button) => {
|
||||
root.innerHTML = markup;
|
||||
if (drawerRoot) drawerRoot.innerHTML = markup;
|
||||
document.querySelectorAll("[data-car]").forEach((button) => {
|
||||
button.addEventListener("click", () => selectCar(Number(button.dataset.car)));
|
||||
});
|
||||
}
|
||||
@@ -818,10 +830,7 @@ function fillCarProfileForm() {
|
||||
}
|
||||
|
||||
function openCarProfile() {
|
||||
document.querySelector("#userDrawer").classList.remove("hidden");
|
||||
document.querySelector("#carProfileSection").classList.remove("hidden");
|
||||
fillCarProfileForm();
|
||||
document.querySelector("#carProfileSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
openDrawerSection("carProfileSection");
|
||||
}
|
||||
|
||||
async function loadServiceCenters() {
|
||||
@@ -859,6 +868,36 @@ function renderServiceCenters() {
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function loadPublicServiceCenters() {
|
||||
const root = document.querySelector("#publicServiceCenters");
|
||||
if (!root) return;
|
||||
try {
|
||||
const centers = await api("/service-centers/public");
|
||||
root.innerHTML = centers.length
|
||||
? centers
|
||||
.map(
|
||||
(center) => `
|
||||
<div class="stack-item">
|
||||
<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>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
: `<div class="empty">Проверенных СТО пока нет</div>`;
|
||||
} catch (error) {
|
||||
root.innerHTML = `<div class="empty">Не удалось загрузить СТО</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderServiceReviews() {
|
||||
const root = document.querySelector("#serviceReviews");
|
||||
if (!root) return;
|
||||
root.innerHTML = `<div class="empty">Отзывы доступны в карточке проверенного СТО. Выберите сервис в разделе «СТО».</div>`;
|
||||
}
|
||||
|
||||
function trustLabel(level) {
|
||||
const labels = {
|
||||
new_service: "Новый сервис",
|
||||
@@ -894,6 +933,11 @@ function renderStats(stats) {
|
||||
const costPer100 = stats.cost_per_km ? stats.cost_per_km * 100 : null;
|
||||
const periodTitles = {
|
||||
all: t("За весь срок"),
|
||||
"7d": "7 дней",
|
||||
"30d": "30 дней",
|
||||
"3m": "3 месяца",
|
||||
"6m": "6 месяцев",
|
||||
"12m": "12 месяцев",
|
||||
month: t("За месяц"),
|
||||
day: t("За день"),
|
||||
quarter: t("За квартал"),
|
||||
@@ -904,6 +948,8 @@ function renderStats(stats) {
|
||||
root.innerHTML = `
|
||||
<button class="stat pop" data-report="summary"><span>${t("За весь срок")}</span><strong>${money(all.total_cost)}</strong><em>${all.fuel_entries_count + all.service_entries_count} ${t("записей")}</em></button>
|
||||
<button class="stat pop" data-report="summary"><span>${periodTitle}</span><strong>${money(stats.total_cost)}</strong><em>${stats.date_from} - ${stats.date_to}</em></button>
|
||||
<button class="stat pop" data-report="summary"><span>В месяц</span><strong>${money(stats.cost_per_month || 0)}</strong><em>${t("среднее в периоде")}</em></button>
|
||||
<button class="stat pop" data-report="summary"><span>Прогноз</span><strong>${money(stats.forecast_next_month || 0)}</strong><em>ближайший месяц</em></button>
|
||||
<button class="stat pop" data-report="efficiency"><span>${t("За день")}</span><strong>${money(costPerDay)}</strong><em>${t("среднее в периоде")}</em></button>
|
||||
<button class="stat pop" data-report="efficiency"><span>${t("На 100 км")}</span><strong>${costPer100 ? money(costPer100) : "-"}</strong><em>${stats.distance_km} км</em></button>
|
||||
<button class="stat pop" data-report="efficiency"><span>${t("На 1 км")}</span><strong>${stats.cost_per_km ? money(stats.cost_per_km) : "-"}</strong><em>${stats.avg_consumption_l_per_100km ? `${stats.avg_consumption_l_per_100km.toFixed(2)} л/100` : t("нет данных")}</em></button>
|
||||
@@ -929,6 +975,13 @@ function recordsForPeriod() {
|
||||
meta: item.vendor || serviceLabel(item.service_type),
|
||||
cost: item.total_cost,
|
||||
})),
|
||||
...state.latestExpenses.map((item) => ({
|
||||
date: item.entry_date,
|
||||
type: "expense",
|
||||
title: item.title,
|
||||
meta: expenseLabel(item.category),
|
||||
cost: item.total_cost,
|
||||
})),
|
||||
].sort((a, b) => b.date.localeCompare(a.date));
|
||||
}
|
||||
|
||||
@@ -1156,11 +1209,40 @@ function serviceLabel(value) {
|
||||
}[value] || value;
|
||||
}
|
||||
|
||||
function monthlySeries(fuel, service) {
|
||||
function expenseLabel(value) {
|
||||
return {
|
||||
insurance: "Страховка",
|
||||
tax: "Налог",
|
||||
fine: "Штраф",
|
||||
parking: "Парковка",
|
||||
car_wash: "Мойка",
|
||||
toll: "Платная дорога",
|
||||
tires: "Шины",
|
||||
wheels: "Диски",
|
||||
battery: "Аккумулятор",
|
||||
parts: "Запчасти",
|
||||
repair: "Ремонт",
|
||||
maintenance: "Плановое ТО",
|
||||
diagnostics: "Диагностика",
|
||||
towing: "Эвакуатор",
|
||||
loan_payment: "Кредит / лизинг",
|
||||
loan_interest: "Проценты",
|
||||
state_fee: "Госпошлина",
|
||||
registration: "Регистрация",
|
||||
inspection: "Техосмотр",
|
||||
other: "Прочее",
|
||||
}[value] || value;
|
||||
}
|
||||
|
||||
function monthlySeries(fuel, service, expenses = []) {
|
||||
const map = new Map();
|
||||
[...fuel.map((item) => ({ ...item, type: "fuel" })), ...service.map((item) => ({ ...item, type: "service" }))].forEach((item) => {
|
||||
[
|
||||
...fuel.map((item) => ({ ...item, type: "fuel" })),
|
||||
...service.map((item) => ({ ...item, type: "service" })),
|
||||
...expenses.map((item) => ({ ...item, type: "other" })),
|
||||
].forEach((item) => {
|
||||
const key = item.entry_date.slice(0, 7);
|
||||
const current = map.get(key) || { label: key, fuel: 0, service: 0 };
|
||||
const current = map.get(key) || { label: key, fuel: 0, service: 0, other: 0 };
|
||||
current[item.type] += Number(item.total_cost || 0);
|
||||
map.set(key, current);
|
||||
});
|
||||
@@ -1168,8 +1250,8 @@ function monthlySeries(fuel, service) {
|
||||
}
|
||||
|
||||
function drawCharts(fuel, service, stats) {
|
||||
drawExpensesChart(monthlySeries(fuel, service));
|
||||
drawSplitChart(Number(stats?.fuel_cost || 0), Number(stats?.service_cost || 0));
|
||||
drawExpensesChart(monthlySeries(fuel, service, state.latestExpenses));
|
||||
drawSplitChart(stats?.cost_by_category || { fuel: Number(stats?.fuel_cost || 0), service: Number(stats?.service_cost || 0) });
|
||||
}
|
||||
|
||||
function setupCanvas(canvas) {
|
||||
@@ -1200,7 +1282,7 @@ function drawExpensesChart(series) {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
const pad = 28;
|
||||
const chartH = height - pad * 2;
|
||||
const max = Math.max(...series.map((item) => item.fuel + item.service), 1);
|
||||
const max = Math.max(...series.map((item) => item.fuel + item.service + item.other), 1);
|
||||
const barGap = 12;
|
||||
const barW = Math.max(18, (width - pad * 2 - barGap * (series.length - 1)) / series.length);
|
||||
|
||||
@@ -1216,17 +1298,21 @@ function drawExpensesChart(series) {
|
||||
|
||||
series.forEach((item, index) => {
|
||||
const x = pad + index * (barW + barGap);
|
||||
const total = item.fuel + item.service;
|
||||
const total = item.fuel + item.service + item.other;
|
||||
const totalH = (total / max) * chartH;
|
||||
const fuelH = total ? (item.fuel / total) * totalH : 0;
|
||||
const serviceH = totalH - fuelH;
|
||||
const serviceH = total ? (item.service / total) * totalH : 0;
|
||||
const otherH = Math.max(totalH - fuelH - serviceH, 0);
|
||||
const y = height - pad - totalH;
|
||||
|
||||
ctx.fillStyle = "#36a388";
|
||||
roundRect(ctx, x, y + serviceH, barW, fuelH, 6);
|
||||
roundRect(ctx, x, y + serviceH + otherH, barW, fuelH, 6);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#3f7fba";
|
||||
roundRect(ctx, x, y, barW, serviceH, 6);
|
||||
roundRect(ctx, x, y + otherH, barW, serviceH, 6);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#d6a64f";
|
||||
roundRect(ctx, x, y, barW, otherH, 6);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = "#7c8783";
|
||||
@@ -1236,10 +1322,15 @@ function drawExpensesChart(series) {
|
||||
});
|
||||
}
|
||||
|
||||
function drawSplitChart(fuelCost, serviceCost) {
|
||||
function drawSplitChart(categories) {
|
||||
const canvas = document.querySelector("#splitChart");
|
||||
const { ctx, width, height } = setupCanvas(canvas);
|
||||
const total = fuelCost + serviceCost;
|
||||
const entries = Object.entries(categories || {})
|
||||
.map(([key, value]) => [key, Number(value || 0)])
|
||||
.filter(([, value]) => value > 0)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5);
|
||||
const total = entries.reduce((sum, [, value]) => sum + value, 0);
|
||||
if (!total) {
|
||||
drawEmpty(ctx, width, height, "Нет расходов");
|
||||
return;
|
||||
@@ -1248,26 +1339,26 @@ function drawSplitChart(fuelCost, serviceCost) {
|
||||
const cx = width / 2;
|
||||
const cy = height / 2 - 8;
|
||||
const radius = Math.min(width, height) * 0.31;
|
||||
const fuelAngle = (fuelCost / total) * Math.PI * 2;
|
||||
|
||||
ctx.lineWidth = 22;
|
||||
ctx.lineCap = "round";
|
||||
ctx.strokeStyle = "#36a388";
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, -Math.PI / 2, -Math.PI / 2 + fuelAngle);
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "#3f7fba";
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, -Math.PI / 2 + fuelAngle + 0.05, Math.PI * 1.5 - 0.05);
|
||||
ctx.stroke();
|
||||
let start = -Math.PI / 2;
|
||||
const colors = ["#36a388", "#3f7fba", "#d6a64f", "#c7645d", "#768a82"];
|
||||
entries.forEach(([, value], index) => {
|
||||
const angle = (value / total) * Math.PI * 2;
|
||||
ctx.strokeStyle = colors[index % colors.length];
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, start, start + Math.max(angle - 0.05, 0.02));
|
||||
ctx.stroke();
|
||||
start += angle;
|
||||
});
|
||||
|
||||
ctx.fillStyle = "#1d2522";
|
||||
ctx.font = "700 22px system-ui";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(`${Math.round((fuelCost / total) * 100)}%`, cx, cy + 5);
|
||||
ctx.fillText(`${Math.round((entries[0][1] / total) * 100)}%`, cx, cy + 5);
|
||||
ctx.fillStyle = "#7c8783";
|
||||
ctx.font = "12px system-ui";
|
||||
ctx.fillText(t("топливо"), cx, cy + 25);
|
||||
ctx.fillText(expenseLabel(entries[0][0]), cx, cy + 25);
|
||||
}
|
||||
|
||||
function roundRect(ctx, x, y, width, height, radius) {
|
||||
@@ -1310,6 +1401,7 @@ async function loadSelectedCar() {
|
||||
if (!state.selectedCarId) {
|
||||
state.latestFuel = [];
|
||||
state.latestService = [];
|
||||
state.latestExpenses = [];
|
||||
state.latestStats = null;
|
||||
state.allStats = null;
|
||||
state.analytics = null;
|
||||
@@ -1319,11 +1411,12 @@ async function loadSelectedCar() {
|
||||
renderStats(null);
|
||||
return;
|
||||
}
|
||||
const [stats, allStats, fuel, service, analytics, vehicleScore] = await Promise.all([
|
||||
const [stats, allStats, fuel, service, expenses, analytics, vehicleScore] = await Promise.all([
|
||||
api(`/cars/${state.selectedCarId}/stats${periodQuery()}`),
|
||||
api(`/cars/${state.selectedCarId}/stats${allPeriodQuery()}`),
|
||||
api(`/cars/${state.selectedCarId}/fuel${periodQuery()}`),
|
||||
api(`/cars/${state.selectedCarId}/service${periodQuery()}`),
|
||||
api(`/cars/${state.selectedCarId}/expenses${periodQuery()}`),
|
||||
api(`/cars/${state.selectedCarId}/analytics`),
|
||||
api(`/my/vehicles/${state.selectedCarId}/score`),
|
||||
]);
|
||||
@@ -1335,6 +1428,7 @@ async function loadSelectedCar() {
|
||||
state.allStats = allStats;
|
||||
state.latestFuel = fuel;
|
||||
state.latestService = service;
|
||||
state.latestExpenses = expenses;
|
||||
state.analytics = analytics;
|
||||
state.vehicleScore = vehicleScore;
|
||||
state.vehicleTimeline = timeline;
|
||||
@@ -1343,11 +1437,11 @@ async function loadSelectedCar() {
|
||||
drawCharts(fuel, service, stats);
|
||||
}
|
||||
|
||||
document.querySelectorAll('input[type="date"]').forEach((input) => {
|
||||
if (input.name !== "next_due_date") input.value = today();
|
||||
document.querySelectorAll('input[name="entry_date"]').forEach((input) => {
|
||||
input.value = today();
|
||||
});
|
||||
|
||||
applyPeriodPreset("month");
|
||||
applyPeriodPreset("30d");
|
||||
|
||||
document.querySelector("#refreshBtn").addEventListener("click", (event) => {
|
||||
runAction(event.currentTarget, "Обновляю данные...", loadCars).then(() => {
|
||||
@@ -1514,6 +1608,40 @@ document.querySelector("#serviceForm").addEventListener("submit", async (event)
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector("#expenseForm").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!state.selectedCarId) {
|
||||
toast("Выбери автомобиль", "error");
|
||||
return;
|
||||
}
|
||||
const form = event.currentTarget;
|
||||
await runAction(form.querySelector('button[type="submit"]'), "Сохраняю...", async () => {
|
||||
const data = formData(form);
|
||||
await api("/expenses", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
car_id: state.selectedCarId,
|
||||
entry_date: data.entry_date,
|
||||
category: data.category,
|
||||
title: data.title,
|
||||
total_cost: Number(data.total_cost),
|
||||
currency: data.currency || state.user?.currency || "RUB",
|
||||
vendor: data.vendor || null,
|
||||
odometer: numberOrNull(data.odometer),
|
||||
period_start: data.period_start || null,
|
||||
period_end: data.period_end || null,
|
||||
is_recurring: Boolean(data.is_recurring),
|
||||
}),
|
||||
});
|
||||
form.reset();
|
||||
form.entry_date.value = today();
|
||||
form.currency.value = state.user?.currency || "RUB";
|
||||
await loadSelectedCar();
|
||||
toast("Сохранено");
|
||||
haptic("success");
|
||||
});
|
||||
});
|
||||
|
||||
function setAction(action) {
|
||||
document.querySelectorAll(".action-card[data-action]").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.action === action);
|
||||
@@ -1528,6 +1656,62 @@ function openScanModal() {
|
||||
document.querySelector("#scanModal").classList.remove("hidden");
|
||||
}
|
||||
|
||||
function mountEntryForms() {
|
||||
const fuelMount = document.querySelector("#fuelFormMount");
|
||||
const serviceMount = document.querySelector("#serviceFormMount");
|
||||
const fuelForm = document.querySelector("#fuelForm");
|
||||
const serviceForm = document.querySelector("#serviceForm");
|
||||
if (fuelMount && fuelForm && !fuelMount.contains(fuelForm)) {
|
||||
fuelForm.classList.remove("hidden");
|
||||
fuelMount.appendChild(fuelForm);
|
||||
}
|
||||
if (serviceMount && serviceForm && !serviceMount.contains(serviceForm)) {
|
||||
serviceForm.classList.remove("hidden");
|
||||
serviceMount.appendChild(serviceForm);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDrawerSection(sectionId, options = {}) {
|
||||
document.querySelector("#userDrawer").classList.remove("hidden");
|
||||
document.querySelectorAll(".drawer-section").forEach((section) => {
|
||||
section.classList.toggle("hidden", section.id !== sectionId);
|
||||
});
|
||||
document.querySelectorAll(".menu-row").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.menuSection === sectionId);
|
||||
});
|
||||
mountEntryForms();
|
||||
if (sectionId === "carProfileSection") fillCarProfileForm();
|
||||
if (sectionId === "settingsSection") {
|
||||
document.querySelector("#localeSelect").value = state.user?.locale || "ru";
|
||||
document.querySelector("#currencySelect").value = state.user?.currency || "RUB";
|
||||
}
|
||||
if (sectionId === "notificationsSection") {
|
||||
updateNotificationStatus(
|
||||
"Notification" in window && Notification.permission === "granted"
|
||||
? "Уведомления включены"
|
||||
: "Напомним о ТО, страховке и регулярном внесении пробега.",
|
||||
);
|
||||
}
|
||||
if (sectionId === "confirmationsSection") renderPlaceholderList("#confirmationRequests", "Новых запросов нет");
|
||||
if (sectionId === "connectedServicesSection") renderPlaceholderList("#connectedServices", "Подключенных автосервисов пока нет");
|
||||
if (sectionId === "servicePanelSection") await loadServiceCenters();
|
||||
if (sectionId === "publicServicesSection") await loadPublicServiceCenters();
|
||||
if (sectionId === "reviewsSection") renderServiceReviews();
|
||||
if (options.expenseCategory) {
|
||||
openDrawerSection("expensesSection");
|
||||
presetExpense(options.expenseCategory);
|
||||
return;
|
||||
}
|
||||
document.querySelector(`#${sectionId}`)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
|
||||
function presetExpense(category) {
|
||||
const form = document.querySelector("#expenseForm");
|
||||
form.category.value = category;
|
||||
form.title.value = expenseLabel(category);
|
||||
if (category === "insurance") form.is_recurring.checked = true;
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-action]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
haptic();
|
||||
@@ -1557,54 +1741,29 @@ document.querySelectorAll("[data-service-title]").forEach((button) => {
|
||||
|
||||
document.querySelector("#menuBtn").addEventListener("click", () => {
|
||||
document.querySelector("#userDrawer").classList.remove("hidden");
|
||||
openDrawerSection("carsSection");
|
||||
});
|
||||
|
||||
document.querySelector("#addCarQuickBtn").addEventListener("click", () => {
|
||||
document.querySelector("#userDrawer").classList.remove("hidden");
|
||||
document.querySelector("#carFormSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
openDrawerSection("carFormSection");
|
||||
});
|
||||
|
||||
document.querySelector("#openCarFormBtn").addEventListener("click", () => {
|
||||
document.querySelector("#carFormSection").classList.remove("hidden");
|
||||
document.querySelector("#carFormSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
document.querySelector("#addRecordPrimaryBtn").addEventListener("click", () => {
|
||||
openDrawerSection("expensesSection");
|
||||
});
|
||||
|
||||
document.querySelector("#openCarProfileBtn").addEventListener("click", openCarProfile);
|
||||
|
||||
document.querySelector("#openSettingsBtn").addEventListener("click", () => {
|
||||
document.querySelector("#settingsSection").classList.remove("hidden");
|
||||
document.querySelector("#localeSelect").value = state.user?.locale || "ru";
|
||||
document.querySelector("#currencySelect").value = state.user?.currency || "RUB";
|
||||
document.querySelector("#settingsSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
document.querySelectorAll("[data-menu-section]").forEach((button) => {
|
||||
button.addEventListener("click", async (event) => {
|
||||
await runAction(event.currentTarget, "Обновляю данные...", async () => {
|
||||
await openDrawerSection(event.currentTarget.dataset.menuSection);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector("#openNotificationsBtn").addEventListener("click", () => {
|
||||
document.querySelector("#notificationsSection").classList.remove("hidden");
|
||||
updateNotificationStatus(
|
||||
"Notification" in window && Notification.permission === "granted"
|
||||
? "Уведомления включены"
|
||||
: "Напомним о ТО, страховке и регулярном внесении пробега.",
|
||||
);
|
||||
document.querySelector("#notificationsSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
|
||||
document.querySelector("#openConfirmationsBtn").addEventListener("click", () => {
|
||||
document.querySelector("#confirmationsSection").classList.remove("hidden");
|
||||
renderPlaceholderList("#confirmationRequests", "Новых запросов нет");
|
||||
document.querySelector("#confirmationsSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
|
||||
document.querySelector("#openConnectedServicesBtn").addEventListener("click", () => {
|
||||
document.querySelector("#connectedServicesSection").classList.remove("hidden");
|
||||
renderPlaceholderList("#connectedServices", "Подключенных автосервисов пока нет");
|
||||
document.querySelector("#connectedServicesSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
|
||||
document.querySelector("#openServicePanelBtn").addEventListener("click", async (event) => {
|
||||
await runAction(event.currentTarget, "Загружаю СТО...", async () => {
|
||||
document.querySelector("#servicePanelSection").classList.remove("hidden");
|
||||
await loadServiceCenters();
|
||||
document.querySelector("#servicePanelSection").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
document.querySelectorAll("[data-expense-preset]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
openDrawerSection("expensesSection");
|
||||
presetExpense(button.dataset.expensePreset);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1622,6 +1781,12 @@ document.querySelector("#serviceCenterForm").addEventListener("submit", async (e
|
||||
city: data.city || null,
|
||||
address: data.address || null,
|
||||
phone: data.phone || null,
|
||||
contact_person: data.contact_person || null,
|
||||
description: data.description || null,
|
||||
specializations: data.specializations
|
||||
? data.specializations.split(",").map((item) => item.trim()).filter(Boolean)
|
||||
: null,
|
||||
working_hours: data.working_hours || null,
|
||||
business_registration_number: data.business_registration_number || null,
|
||||
}),
|
||||
});
|
||||
@@ -1633,7 +1798,7 @@ document.querySelector("#serviceCenterForm").addEventListener("submit", async (e
|
||||
|
||||
document.querySelector("#enableNotificationsBtn").addEventListener("click", enableNotifications);
|
||||
|
||||
document.querySelector("#openScanBtn").addEventListener("click", () => {
|
||||
document.querySelector("#fuelScanBtn").addEventListener("click", () => {
|
||||
openScanModal();
|
||||
});
|
||||
|
||||
@@ -1676,7 +1841,7 @@ document.querySelector("#ocrForm").addEventListener("submit", async (event) => {
|
||||
payload.append("file", file);
|
||||
const response = await fetch("/api/ocr/parse-text-receipt", {
|
||||
method: "POST",
|
||||
headers: tg?.initData ? { "X-Telegram-Init-Data": tg.initData } : {},
|
||||
headers: authHeaders(),
|
||||
body: payload,
|
||||
});
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
@@ -1686,8 +1851,8 @@ document.querySelector("#ocrForm").addEventListener("submit", async (event) => {
|
||||
if (result.liters) fuelForm.liters.value = result.liters;
|
||||
if (result.price_per_liter) fuelForm.price_per_liter.value = result.price_per_liter;
|
||||
if (result.station) fuelForm.station.value = result.station;
|
||||
setAction("fuel");
|
||||
document.querySelector("#scanModal").classList.add("hidden");
|
||||
await openDrawerSection("fuelSection");
|
||||
toast("Проверь распознанные значения");
|
||||
haptic("success");
|
||||
});
|
||||
@@ -1712,6 +1877,8 @@ Promise.all([loadAuthConfig()])
|
||||
.then(() => {
|
||||
document.querySelector("#localeSelect").value = state.user?.locale || "ru";
|
||||
document.querySelector("#currencySelect").value = state.user?.currency || "RUB";
|
||||
document.querySelector("#expenseForm").currency.value = state.user?.currency || "RUB";
|
||||
mountEntryForms();
|
||||
applyTranslations();
|
||||
initCarCatalog();
|
||||
return loadCars();
|
||||
|
||||
Reference in New Issue
Block a user