Add CarPass passport UI widgets

This commit is contained in:
VPN SaaS Dev
2026-05-12 20:11:12 +09:00
parent 8ef59a6446
commit 26875e396c
3 changed files with 302 additions and 61 deletions

View File

@@ -317,6 +317,9 @@ const state = {
allStats: null,
analytics: null,
serviceCenters: [],
vehicleScore: null,
vehicleTimeline: [],
achievements: [],
receiptFile: null,
serviceWorkerRegistration: null,
period: {
@@ -889,21 +892,115 @@ function recordsForPeriod() {
function updateScore() {
const car = selectedCar();
const fuelCount = state.latestFuel.length;
const serviceCount = state.latestService.length;
let score = 0;
if (car) score += 25;
if (fuelCount > 0) score += 25;
if (serviceCount > 0) score += 20;
if (state.latestStats?.distance_km > 0) score += 20;
if (fuelCount + serviceCount >= 6) score += 10;
const title = score >= 90 ? t("Профиль точный") : score >= 60 ? t("Хороший учет") : score >= 30 ? t("Набираем данные") : t("Старт");
document.querySelector("#scoreTitle").textContent = title;
document.querySelector("#scoreBar").style.width = `${score}%`;
document.querySelector("#scoreHint").textContent =
score >= 90
? t("Отчеты уже достаточно надежны для решений по расходам")
: t("Чем регулярнее записи, тем точнее расход, цена километра и напоминания");
const score = state.vehicleScore?.completeness_score || 0;
const title = scoreLabel(state.vehicleScore?.profile_quality, score);
const ring = document.querySelector("#scoreRing");
document.querySelector("#scoreValue").textContent = score;
document.querySelector("#scoreTitle").textContent = car ? title : t("Старт");
document.querySelector("#verifiedHistoryStatus").textContent = historyLabel(state.vehicleScore?.verified_history_status);
document.querySelector("#maintenanceStatus").textContent = healthLabel(state.vehicleScore?.maintenance_status);
if (ring) {
ring.style.background = `conic-gradient(#5ee0bd ${score * 3.6}deg, rgba(255,255,255,0.12) 0deg)`;
}
document.querySelector("#scoreHint").textContent = car
? "Качество паспорта растет от подтвержденных данных, сервисной истории и точного пробега."
: t("Добавь авто и первую запись, чтобы видеть точные отчеты");
renderScoreActions(state.vehicleScore?.missing_items || []);
renderAchievements();
renderVehicleTimeline();
}
function scoreLabel(quality, score) {
if (quality === "high_confidence" || score >= 86) return "High-confidence passport";
if (quality === "strong" || score >= 61) return "Strong profile";
if (quality === "useful" || score >= 31) return "Useful profile";
return "Basic profile";
}
function historyLabel(status) {
const labels = {
verified: "Verified maintenance history",
partially_verified: "Partially verified",
self_reported: "Self-reported",
};
return labels[status] || "Self-reported";
}
function healthLabel(status) {
const labels = {
green: "Green",
yellow: "Attention soon",
red: "Overdue",
unknown: "Needs baseline",
};
return labels[status] || "Needs baseline";
}
function renderScoreActions(items) {
const root = document.querySelector("#scoreActions");
if (!root) return;
const visible = items.slice(0, 3);
if (!visible.length) {
root.innerHTML = `<div class="passport-note">Паспорт выглядит надежно. Следующий рост даст подтвержденная сервисная история.</div>`;
return;
}
root.innerHTML = visible
.map(
(item) => `
<div class="passport-action">
<strong>${item.title}</strong>
<span>${item.description}</span>
</div>
`,
)
.join("");
}
function renderAchievements() {
const root = document.querySelector("#achievementList");
if (!root) return;
const car = selectedCar();
const achievements = state.achievements.filter((item) => !item.vehicle_id || item.vehicle_id === car?.id).slice(0, 4);
if (!achievements.length) {
root.innerHTML = `<div class="achievement-card muted"><strong>Evidence badges</strong><span>Появятся после первых качественных записей.</span></div>`;
return;
}
root.innerHTML = achievements
.map(
(item) => `
<div class="achievement-card">
<strong>${item.title}</strong>
<span>${item.description}</span>
</div>
`,
)
.join("");
}
function renderVehicleTimeline() {
const root = document.querySelector("#vehicleTimeline");
if (!root) return;
const items = state.vehicleTimeline.slice(0, 5);
if (!items.length) {
root.innerHTML = `<div class="timeline-empty">Timeline появится после заправок, сервиса и подтверждений.</div>`;
return;
}
root.innerHTML = `
<div class="timeline-title">Vehicle timeline</div>
${items
.map(
(item) => `
<div class="timeline-item ${item.type}">
<span></span>
<div>
<strong>${item.title}</strong>
<small>${String(item.date).slice(0, 10)}${item.status ? ` · ${item.status}` : ""}</small>
</div>
</div>
`,
)
.join("")}
`;
}
function openReport(type = "summary") {
@@ -1174,21 +1271,32 @@ async function loadSelectedCar() {
state.latestStats = null;
state.allStats = null;
state.analytics = null;
state.vehicleScore = null;
state.vehicleTimeline = [];
state.achievements = [];
renderStats(null);
return;
}
const [stats, allStats, fuel, service, analytics] = await Promise.all([
const [stats, allStats, fuel, service, 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}/analytics`),
api(`/my/vehicles/${state.selectedCarId}/score`),
]);
const [timeline, achievements] = await Promise.all([
api(`/my/vehicles/${state.selectedCarId}/timeline?limit=30`),
api("/me/achievements"),
]);
state.latestStats = stats;
state.allStats = allStats;
state.latestFuel = fuel;
state.latestService = service;
state.analytics = analytics;
state.vehicleScore = vehicleScore;
state.vehicleTimeline = timeline;
state.achievements = achievements;
renderStats(stats);
drawCharts(fuel, service, stats);
}