diff --git a/frontend/js/main.js b/frontend/js/main.js index eb272b0..15cb8e8 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1424,7 +1424,11 @@ async function renderRecipes(container) {

Рецепты

- + + + + +
${recipes.length ? recipes.map(r => { @@ -1449,6 +1453,20 @@ async function renderRecipes(container) { container.querySelector("#add-recipe-btn").onclick = () => showRecipeEditorModal(null); + container.querySelector("#export-recipes-btn").onclick = async () => { + const allRecipes = await exportAllRecipes(); + const json = JSON.stringify(allRecipes, null, 2); + downloadFile("baldav-recipes.json", json, "application/json"); + }; + + container.querySelector("#import-recipes-btn").onclick = () => { + pickFile(".json", async (text) => { + const data = JSON.parse(text); + await importRecipes(data); + renderRecipes(container); + }); + }; + container.querySelectorAll(".recipe-card").forEach(card => { card.onclick = () => { const rid = parseInt(card.dataset.rid); @@ -1519,6 +1537,7 @@ async function showRecipeDetailModal(recipeId) { + `, () => {}); @@ -1526,6 +1545,7 @@ async function showRecipeDetailModal(recipeId) { const overlay = [...document.querySelectorAll(".modal-overlay")].pop(); const logBtn = overlay.querySelector("#rd-log"); const editBtn = overlay.querySelector("#rd-edit"); + const exportBtn = overlay.querySelector("#rd-export"); const delBtn = overlay.querySelector("#rd-delete"); logBtn.onclick = async () => { @@ -1536,8 +1556,22 @@ async function showRecipeDetailModal(recipeId) { renderScreen(state.screen); }; + exportBtn.onclick = async () => { + const ings = await DB.getRecipeIngredients(recipeId); + const exported = { + name: recipe.name, cooking_method_id: recipe.cooking_method_id, + servings: recipe.servings, notes: recipe.notes, + ingredients: ings.map(i => ({ + product_name: i.product_name, custom_name: i.custom_name, + amount_g: i.amount_g, cooking_method_id: i.cooking_method_id, + kcal_per_100g: i.kcal_per_100g, protein_per_100g: i.protein_per_100g, + fat_per_100g: i.fat_per_100g, carbs_per_100g: i.carbs_per_100g, + })), + }; + downloadFile(`${recipe.name}.json`, JSON.stringify(exported, null, 2), "application/json"); + }; + editBtn.onclick = () => { - // Close this modal via winStack, then open editor if (winStack.length > 0) winStack.pop().close(); _skipPops = 1; history.back(); @@ -3397,7 +3431,11 @@ async function renderSupplements(container) {

Добавки

- + + + + +
${supps.length ? supps.map(s => ` @@ -3415,6 +3453,19 @@ async function renderSupplements(container) { container.querySelector("#add-new-supp").onclick = () => showEditSupplementModal(null); + container.querySelector("#export-supps-btn").onclick = async () => { + const supps = await DB.getSupplements(); + downloadFile("baldav-supplements.json", JSON.stringify(supps, null, 2), "application/json"); + }; + + container.querySelector("#import-supps-btn").onclick = () => { + pickFile(".json", async (text) => { + const data = JSON.parse(text); + await importSupplements(data); + renderSupplements(container); + }); + }; + container.querySelectorAll(".prod-card[data-sid]").forEach(card => { card.querySelector(".prod-card-header").onclick = async () => { const sid = parseInt(card.dataset.sid); @@ -4225,6 +4276,89 @@ function addZoomDisplay(map) { ctrl.addTo(map); } +// --------------------------------------------------------------------------- +// Export/Import: recipes, supplements +// --------------------------------------------------------------------------- + +function downloadFile(filename, content, mimeType) { + const blob = new Blob([content], { type: mimeType || "application/json" }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); +} + +async function exportAllRecipes() { + const recipes = await DB.getRecipes(); + const result = []; + for (const r of recipes) { + const ings = await DB.getRecipeIngredients(r.id); + result.push({ + name: r.name, + cooking_method_id: r.cooking_method_id, + cooking_method_name: r.cooking_method_name, + servings: r.servings, + notes: r.notes, + ingredients: ings.map(i => ({ + product_name: i.product_name, + custom_name: i.custom_name, + amount_g: i.amount_g, + cooking_method_id: i.cooking_method_id, + cooking_method_name: i.cooking_method_name, + // Store product nutrients so recipient can create product if missing + kcal_per_100g: i.kcal_per_100g, + protein_per_100g: i.protein_per_100g, + fat_per_100g: i.fat_per_100g, + carbs_per_100g: i.carbs_per_100g, + })), + }); + } + return result; +} + +async function importRecipes(data) { + const arr = Array.isArray(data) ? data : [data]; + for (const r of arr) { + if (!r.name) continue; + const rid = await DB.saveRecipe(r.name, r.cooking_method_id ?? null, r.servings ?? 1, r.notes ?? null); + if (!rid) continue; + for (const ing of (r.ingredients || [])) { + // Try to find product by name + let pid = 0; + if (ing.product_name) { + const found = await DB.searchProducts(ing.product_name, 1); + if (found.length) { + pid = found[0].id; + } else if (ing.kcal_per_100g != null) { + // Create product from exported data + pid = await DB.addProduct({ + name: ing.product_name, + kcal_per_100g: ing.kcal_per_100g, + protein_per_100g: ing.protein_per_100g, + fat_per_100g: ing.fat_per_100g, + carbs_per_100g: ing.carbs_per_100g, + source: "recipe_import", + }); + } + } + await DB.addRecipeIngredient(rid, pid, ing.custom_name ?? null, ing.amount_g ?? 100, ing.cooking_method_id ?? null); + } + } +} + +async function importSupplements(data) { + const arr = Array.isArray(data) ? data : [data]; + for (const s of arr) { + if (!s.name) continue; + // Remove id — will get new one + const clean = { ...s }; + delete clean.id; + await DB.addSupplement(clean); + } +} + function pickFile(accept, callback, asBinary) { console.log("[pickFile] accept:", accept, "asBinary:", asBinary); const input = document.createElement("input");