Export/import recipes and supplements via JSON files
Recipes:
- Export all: downloads baldav-recipes.json with ingredients + product KBJU
- Export one: button in recipe detail downloads {name}.json
- Import: reads JSON file, creates recipes + missing products
Supplements:
- Export all: downloads baldav-supplements.json
- Import: reads JSON, creates supplements (skips id, gets new one)
Buttons: arrow-down (import) and arrow-up (export) next to + on both screens.
Uses downloadFile() helper for browser download, pickFile() for file selection.
Import matches products by name, creates missing ones from exported KBJU.
This commit is contained in:
parent
7b41382ea4
commit
5ecce90744
1 changed files with 137 additions and 3 deletions
|
|
@ -1424,7 +1424,11 @@ async function renderRecipes(container) {
|
|||
<div class="products-screen">
|
||||
<div class="section-header">
|
||||
<h2>Рецепты</h2>
|
||||
<button class="add-btn" id="add-recipe-btn">+</button>
|
||||
<span style="display:flex;gap:6px">
|
||||
<button class="add-btn" id="import-recipes-btn" title="Импорт" style="font-size:14px">⬇</button>
|
||||
<button class="add-btn" id="export-recipes-btn" title="Экспорт всех" style="font-size:14px">⬆</button>
|
||||
<button class="add-btn" id="add-recipe-btn">+</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="recipe-list" class="prod-list">
|
||||
${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) {
|
|||
|
||||
<button id="rd-log" class="btn-primary full-width" style="margin-top:16px">Записать в дневник</button>
|
||||
<button id="rd-edit" class="btn-secondary full-width" style="margin-top:8px">Редактировать</button>
|
||||
<button id="rd-export" class="btn-secondary full-width" style="margin-top:8px">Экспорт рецепта</button>
|
||||
<button id="rd-delete" class="btn-secondary full-width" style="margin-top:8px;color:#cf6679">Удалить рецепт</button>
|
||||
`, () => {});
|
||||
|
||||
|
|
@ -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) {
|
|||
<div class="products-screen">
|
||||
<div class="section-header">
|
||||
<h2>Добавки</h2>
|
||||
<button class="add-btn" id="add-new-supp">+</button>
|
||||
<span style="display:flex;gap:6px">
|
||||
<button class="add-btn" id="import-supps-btn" title="Импорт" style="font-size:14px">⬇</button>
|
||||
<button class="add-btn" id="export-supps-btn" title="Экспорт всех" style="font-size:14px">⬆</button>
|
||||
<button class="add-btn" id="add-new-supp">+</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="supp-list" class="prod-list">
|
||||
${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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue