Add CarPass passport UI widgets
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user