possibly auth
This commit is contained in:
+230
-101
@@ -1,114 +1,243 @@
|
||||
import { state, saveState, generateUUID } from './state.js';
|
||||
import { fetchWorkoutTypes, fetchMetricTypes, fetchClients, syncData } from './api.js';
|
||||
import { renderMainScreen } from './render/main-screen.js';
|
||||
import { state, saveState, generateUUID } from "./state.js";
|
||||
import { syncData } from "./api.js";
|
||||
import { renderMainScreen } from "./render/main-screen.js";
|
||||
|
||||
export async function initAppData() {
|
||||
try {
|
||||
// Загружаем справочники с бэкенда
|
||||
state.workoutTypes = await fetchWorkoutTypes();
|
||||
state.metricTypes = await fetchMetricTypes();
|
||||
state.clients = await fetchClients();
|
||||
saveState();
|
||||
console.log('Справочники загружены с бэкенда');
|
||||
} catch (e) {
|
||||
console.warn('Не удалось загрузить справочники (возможно, бэк недоступен):', e);
|
||||
// Не ломаем приложение, работаем офлайн с тем, что есть
|
||||
}
|
||||
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);
|
||||
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 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 btn = document.querySelector('.header-actions .btn-secondary');
|
||||
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 = true;
|
||||
btn.textContent = '⏳ Отправка...';
|
||||
}
|
||||
|
||||
try {
|
||||
// Маппинг данных из формата фронтенда в формат бэкенда
|
||||
const payload = {
|
||||
workouts: dirty.map(w => {
|
||||
// Находим ID типа тренировки по его имени
|
||||
const wt = state.workoutTypes.find(t => t.name === w.type);
|
||||
|
||||
return {
|
||||
sync_id: w.sync_id || generateUUID(),
|
||||
client_id: state.currentClientId || null,
|
||||
workout_type_id: wt ? wt.id : null,
|
||||
start_time: w.startTime,
|
||||
end_time: w.endTime,
|
||||
notes: w.notes || "",
|
||||
workout_exercises: (w.exercises || []).map((ex, exIdx) => ({
|
||||
sync_id: ex.sync_id || generateUUID(),
|
||||
exercise_id: ex.exercise_id || null,
|
||||
exercise_name: ex.name,
|
||||
sort_order: exIdx,
|
||||
sets: (ex.sets || []).map((s, sIdx) => ({
|
||||
sync_id: s.sync_id || generateUUID(),
|
||||
is_dropset: s.isDropset ? 1 : 0,
|
||||
weight: s.isDropset ? null : (parseFloat(s.weight) || null),
|
||||
reps: s.isDropset ? null : (parseInt(s.reps) || null),
|
||||
rating: s.rating || 3,
|
||||
notes: s.notes || "",
|
||||
sort_order: sIdx,
|
||||
timestamp: s.timestamp || new Date().toISOString(),
|
||||
dropset_items: s.isDropset ? (s.subSets || []).map((item, idx) => ({
|
||||
weight: parseFloat(item.weight) || 0,
|
||||
reps: parseInt(item.reps) || 0,
|
||||
sort_order: idx
|
||||
})) : []
|
||||
}))
|
||||
})),
|
||||
cardio_sessions: (w.cardio || []).map((c, cIdx) => ({
|
||||
sync_id: c.sync_id || generateUUID(),
|
||||
cardio_type: c.type,
|
||||
purpose: c.purpose,
|
||||
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: cIdx
|
||||
}))
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
console.log('📤 Отправляем на синхронизацию:', payload);
|
||||
const response = await syncData(payload);
|
||||
console.log('✅ Ответ синхронизации:', response);
|
||||
|
||||
// Помечаем локально как синхронизированные
|
||||
dirty.forEach(w => w.synced = true);
|
||||
saveState();
|
||||
|
||||
// Перерисовываем интерфейс (чтобы обновить бейджи и read-only режим)
|
||||
if (typeof window.renderMainScreen === 'function') {
|
||||
window.renderMainScreen();
|
||||
} else {
|
||||
// Fallback если функция не экспортирована в window
|
||||
location.reload();
|
||||
}
|
||||
|
||||
alert(`Успешно синхронизировано: ${response.created.workouts || 0} тренировок`);
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ Sync error:', e);
|
||||
alert('Ошибка синхронизации: ' + e.message);
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔄';
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = "🔄";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user