Improve CarPass product UX and service flows

This commit is contained in:
VPN SaaS Dev
2026-05-14 19:33:25 +09:00
parent b85db333d8
commit caa5f6d3db
36 changed files with 1836 additions and 366 deletions

View File

@@ -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();