Files
gym-bro/frontend/js/sync.js
T
2026-10-03 09:44:11 +03:00

244 lines
6.5 KiB
JavaScript

import { state, saveState, generateUUID } from "./state.js";
import { syncData } from "./api.js";
import { renderMainScreen } from "./render/main-screen.js";
export async function initAppData() {
try {
const { fetchWorkoutTypes, fetchMetricTypes } = await import("./api.js");
state.workoutTypes = await fetchWorkoutTypes();
state.metricTypes = await fetchMetricTypes();
saveState();
console.log("Справочники загружены с бэкенда");
} catch (e) {
console.warn("Не удалось загрузить справочники (возможно, бэк недоступен):", e);
}
}
export function getDirtyWorkouts() {
return state.workouts.filter((w) => !w.synced);
}
function ensureSyncId(obj) {
if (!obj) return null;
if (!obj.sync_id) {
obj.sync_id = generateUUID();
}
return obj.sync_id;
}
function findWorkoutTypeByName(name) {
if (!name) return null;
return state.workoutTypes.find((t) => t.name === name) || null;
}
function findExerciseRefByName(name) {
const normalized = String(name || "").trim().toLowerCase();
if (!normalized) return null;
return (
state.exerciseRef.find(
(e) => String(e.name || "").trim().toLowerCase() === normalized
) || null
);
}
function findClientByLocalId(clientId) {
if (clientId === undefined || clientId === null || clientId === "") return null;
return (
state.clients.find((c) => String(c.id) === String(clientId)) || null
);
}
function buildClientsPayload() {
return state.clients.map((c) => {
ensureSyncId(c);
return {
sync_id: c.sync_id,
full_name: c.full_name || "Без имени",
birth_date: c.birth_date || null,
goal: c.goal || "",
notes: c.notes || "",
color: c.color || null,
is_active: c.is_active === undefined ? 1 : c.is_active,
};
});
}
function buildExercisesPayload() {
return state.exerciseRef.map((e) => {
ensureSyncId(e);
const wt = findWorkoutTypeByName(e.workoutType);
return {
sync_id: e.sync_id,
name: e.name,
workout_type_id: wt ? wt.id : null,
};
});
}
function buildSetsPayload(sets) {
return (sets || []).map((s, setIndex) => {
ensureSyncId(s);
const isDropset = !!s.isDropset;
return {
sync_id: s.sync_id,
is_dropset: isDropset ? 1 : 0,
weight: isDropset ? null : (parseFloat(s.weight) || null),
reps: isDropset ? null : (parseInt(s.reps, 10) || null),
rating: s.rating || 3,
notes: s.notes || "",
sort_order: s.sort_order ?? setIndex,
timestamp: s.timestamp || new Date().toISOString(),
dropset_items: isDropset
? (s.subSets || []).map((item, itemIndex) => ({
weight: parseFloat(item.weight) || 0,
reps: parseInt(item.reps, 10) || 0,
sort_order: item.sort_order ?? itemIndex,
}))
: [],
};
});
}
function buildWorkoutExercisesPayload(exercises) {
return (exercises || []).map((ex, exIndex) => {
ensureSyncId(ex);
const exerciseRef = findExerciseRefByName(ex.name);
return {
sync_id: ex.sync_id,
exercise_sync_id: exerciseRef ? exerciseRef.sync_id : null,
exercise_id: ex.exercise_id || null,
exercise_name: ex.name,
sort_order: ex.sort_order ?? exIndex,
sets: buildSetsPayload(ex.sets),
};
});
}
function buildCardioPayload(cardioList) {
return (cardioList || []).map((c, cIndex) => {
ensureSyncId(c);
return {
sync_id: c.sync_id,
cardio_type: c.type || "другое",
purpose: c.purpose || "separate",
duration: c.duration || null,
distance: c.distance || null,
speed: c.speed || null,
incline: c.incline || null,
resistance: c.resistance || null,
notes: c.notes || "",
sort_order: c.sort_order ?? cIndex,
};
});
}
function buildWorkoutsPayload(dirty) {
return dirty.map((w) => {
ensureSyncId(w);
const client = findClientByLocalId(w.client_id);
const wt = findWorkoutTypeByName(w.type);
return {
sync_id: w.sync_id,
client_id: null,
client_sync_id: client ? client.sync_id : null,
workout_type_id: wt ? wt.id : null,
start_time: w.startTime,
end_time: w.endTime || null,
notes: w.notes || "",
workout_exercises: buildWorkoutExercisesPayload(w.exercises),
cardio_sessions: buildCardioPayload(w.cardio),
};
});
}
export async function sync() {
const dirty = getDirtyWorkouts();
if (dirty.length === 0) {
alert("Нет данных для синхронизации");
return;
}
const btn = document.querySelector(".header-actions .btn-secondary");
if (btn) {
btn.disabled = true;
btn.textContent = "⏳";
}
try {
// Генерируем sync_id локально и сохраняем, чтобы при повторе не плодить дубли
state.clients.forEach(ensureSyncId);
state.exerciseRef.forEach(ensureSyncId);
dirty.forEach((w) => {
ensureSyncId(w);
(w.exercises || []).forEach((ex) => {
ensureSyncId(ex);
(ex.sets || []).forEach(ensureSyncId);
});
(w.cardio || []).forEach(ensureSyncId);
});
saveState();
const payload = {
clients: buildClientsPayload(),
exercises: buildExercisesPayload(),
workouts: buildWorkoutsPayload(dirty),
body_metrics: [], // динамические замеры подключим следующим этапом
};
console.log("📤 Отправляем на синхронизацию:", payload);
const response = await syncData(payload);
console.log("✅ Ответ синхронизации:", response);
dirty.forEach((w) => {
w.synced = true;
});
saveState();
if (typeof window.renderMainScreen === "function") {
window.renderMainScreen();
} else {
renderMainScreen();
}
const createdWorkouts = response.created?.workouts || 0;
const updatedWorkouts = response.updated?.workouts || 0;
const warnings = response.warnings || [];
let message = `Синхронизировано: создано ${createdWorkouts}, обновлено ${updatedWorkouts}`;
if (warnings.length) {
message += `\n\nПредупреждения:\n${warnings.join("\n")}`;
}
alert(message);
} catch (e) {
console.error("❌ Sync error:", e);
alert("Ошибка синхронизации: " + e.message);
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = "🔄";
}
}
}