mirror of
https://github.com/sstent/foodplanner.git
synced 2026-01-25 11:11:36 +00:00
working import
This commit is contained in:
Binary file not shown.
161
main.py
161
main.py
@@ -1,18 +1,20 @@
|
||||
# Meal Planner FastAPI Application
|
||||
# Run with: uvicorn main:app --reload
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException, Request, Form
|
||||
from fastapi import FastAPI, Depends, HTTPException, Request, Form, Body
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey, Text, Date
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Session, relationship
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from datetime import date, datetime
|
||||
import os
|
||||
|
||||
import csv
|
||||
from fastapi import File, UploadFile
|
||||
# Database setup - Use SQLite for easier setup
|
||||
DATABASE_URL = "sqlite:///./meal_planner.db"
|
||||
# For production, use PostgreSQL: DATABASE_URL = "postgresql://username:password@localhost/meal_planner"
|
||||
@@ -185,6 +187,148 @@ async def foods_page(request: Request, db: Session = Depends(get_db)):
|
||||
foods = db.query(Food).all()
|
||||
return templates.TemplateResponse("foods.html", {"request": request, "foods": foods})
|
||||
|
||||
@app.post("/foods/upload")
|
||||
async def bulk_upload_foods(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
||||
"""Handle bulk food upload from CSV"""
|
||||
try:
|
||||
contents = await file.read()
|
||||
decoded = contents.decode('utf-8').splitlines()
|
||||
reader = csv.DictReader(decoded)
|
||||
|
||||
stats = {'created': 0, 'updated': 0, 'errors': []}
|
||||
|
||||
for row_num, row in enumerate(reader, 2): # Row numbers start at 2 (1-based + header)
|
||||
try:
|
||||
# Map CSV columns to model fields
|
||||
food_data = {
|
||||
'name': f"{row['ID']} ({row['Brand']})",
|
||||
'serving_size': str(round(float(row['Serving (g)']), 3)),
|
||||
'serving_unit': 'g',
|
||||
'calories': round(float(row['Calories']), 2),
|
||||
'protein': round(float(row['Protein (g)']), 2),
|
||||
'carbs': round(float(row['Carbohydrate (g)']), 2),
|
||||
'fat': round(float(row['Fat (g)']), 2),
|
||||
'fiber': round(float(row.get('Fiber (g)', 0)), 2),
|
||||
'sugar': round(float(row.get('Sugar (g)', 0)), 2),
|
||||
'sodium': round(float(row.get('Sodium (mg)', 0)), 2),
|
||||
'calcium': round(float(row.get('Calcium (mg)', 0)), 2)
|
||||
}
|
||||
|
||||
# Check for existing food
|
||||
existing = db.query(Food).filter(Food.name == food_data['name']).first()
|
||||
|
||||
if existing:
|
||||
# Update existing food
|
||||
for key, value in food_data.items():
|
||||
setattr(existing, key, value)
|
||||
stats['updated'] += 1
|
||||
else:
|
||||
# Create new food
|
||||
food = Food(**food_data)
|
||||
db.add(food)
|
||||
stats['created'] += 1
|
||||
|
||||
except (KeyError, ValueError) as e:
|
||||
stats['errors'].append(f"Row {row_num}: {str(e)}")
|
||||
|
||||
db.commit()
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@app.post("/meals/upload")
|
||||
async def bulk_upload_meals(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
||||
"""Handle bulk meal upload from CSV"""
|
||||
try:
|
||||
contents = await file.read()
|
||||
decoded = contents.decode('utf-8').splitlines()
|
||||
reader = csv.reader(decoded)
|
||||
|
||||
stats = {'created': 0, 'updated': 0, 'errors': []}
|
||||
|
||||
# Skip header rows
|
||||
next(reader) # First header
|
||||
next(reader) # Second header
|
||||
|
||||
for row_num, row in enumerate(reader, 3): # Start at row 3
|
||||
if not row:
|
||||
continue
|
||||
|
||||
try:
|
||||
meal_name = row[0].strip()
|
||||
ingredients = []
|
||||
|
||||
# Process ingredient pairs (item, grams)
|
||||
for i in range(1, len(row), 2):
|
||||
if i+1 >= len(row) or not row[i].strip():
|
||||
continue
|
||||
|
||||
food_name = row[i].strip()
|
||||
quantity = round(float(row[i+1].strip()) / 100, 3) # Convert grams to 100g units and round to 3 decimal places
|
||||
|
||||
# Try multiple matching strategies for food names
|
||||
food = None
|
||||
|
||||
# Strategy 1: Exact match
|
||||
food = db.query(Food).filter(Food.name.ilike(food_name)).first()
|
||||
|
||||
# Strategy 2: Match food name within stored name (handles "ID (Brand) Name" format)
|
||||
if not food:
|
||||
food = db.query(Food).filter(Food.name.ilike(f"%{food_name}%")).first()
|
||||
|
||||
# Strategy 3: Try to match food name after closing parenthesis in "ID (Brand) Name" format
|
||||
if not food:
|
||||
# Look for pattern like ") mushrooms" at end of name
|
||||
search_pattern = f") {food_name}"
|
||||
food = db.query(Food).filter(Food.name.ilike(f"%{search_pattern}%")).first()
|
||||
|
||||
if not food:
|
||||
# Get all food names for debugging
|
||||
all_foods = db.query(Food.name).limit(10).all()
|
||||
food_names = [f[0] for f in all_foods]
|
||||
raise ValueError(f"Food '{food_name}' not found. Available foods include: {', '.join(food_names[:5])}...")
|
||||
ingredients.append((food.id, quantity))
|
||||
|
||||
# Create/update meal
|
||||
existing = db.query(Meal).filter(Meal.name == meal_name).first()
|
||||
if existing:
|
||||
# Remove existing ingredients
|
||||
db.query(MealFood).filter(MealFood.meal_id == existing.id).delete()
|
||||
existing.meal_type = "custom" # Default type
|
||||
stats['updated'] += 1
|
||||
else:
|
||||
existing = Meal(name=meal_name, meal_type="custom")
|
||||
db.add(existing)
|
||||
stats['created'] += 1
|
||||
|
||||
db.flush() # Get meal ID
|
||||
|
||||
# Add new ingredients
|
||||
for food_id, quantity in ingredients:
|
||||
meal_food = MealFood(
|
||||
meal_id=existing.id,
|
||||
food_id=food_id,
|
||||
quantity=quantity
|
||||
)
|
||||
db.add(meal_food)
|
||||
|
||||
db.commit()
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
db.rollback()
|
||||
stats['errors'].append(f"Row {row_num}: {str(e)}")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
stats['errors'].append(f"Row {row_num}: Unexpected error - {str(e)}")
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@app.post("/foods/add")
|
||||
async def add_food(request: Request, db: Session = Depends(get_db),
|
||||
name: str = Form(...), serving_size: str = Form(...),
|
||||
@@ -230,6 +374,19 @@ async def add_food_to_meal(meal_id: int, food_id: int = Form(...),
|
||||
db.commit()
|
||||
return {"status": "success"}
|
||||
|
||||
@app.post("/meals/delete")
|
||||
async def delete_meals(meal_ids: dict = Body(...), db: Session = Depends(get_db)):
|
||||
try:
|
||||
# Delete meal foods first
|
||||
db.query(MealFood).filter(MealFood.meal_id.in_(meal_ids["meal_ids"])).delete(synchronize_session=False)
|
||||
# Delete meals
|
||||
db.query(Meal).filter(Meal.id.in_(meal_ids["meal_ids"])).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
# Plan tab
|
||||
@app.get("/plan", response_class=HTMLResponse)
|
||||
async def plan_page(request: Request, person: str = "Person A", db: Session = Depends(get_db)):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
sqlalchemy>=2.0.24
|
||||
#psycopg2-binary==2.9.9
|
||||
python-multipart==0.0.6
|
||||
jinja2==3.1.2
|
||||
@@ -2,6 +2,15 @@
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h3>Bulk Import</h3>
|
||||
<form action="/foods/upload" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">CSV File</label>
|
||||
<input type="file" class="form-control" name="file" accept=".csv" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary mb-4">Upload CSV</button>
|
||||
</form>
|
||||
|
||||
<h3>Add New Food</h3>
|
||||
<form action="/foods/add" method="post">
|
||||
<div class="mb-3">
|
||||
@@ -62,6 +71,15 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8" id="upload-results" style="display: none;">
|
||||
<div class="alert alert-success">
|
||||
<strong>Upload Results:</strong>
|
||||
<span id="created-count"></span> created,
|
||||
<span id="updated-count"></span> updated
|
||||
<div id="error-list" class="mt-2 text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<h3>Foods Database</h3>
|
||||
<div class="table-responsive">
|
||||
@@ -82,15 +100,16 @@
|
||||
<tbody>
|
||||
{% for food in foods %}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="selected_foods" value="{{ food.id }}"></td>
|
||||
<td>{{ food.name }}</td>
|
||||
<td>{{ food.serving_size }} {{ food.serving_unit }}</td>
|
||||
<td>{{ "%.1f"|format(food.calories) }}</td>
|
||||
<td>{{ "%.1f"|format(food.protein) }}g</td>
|
||||
<td>{{ "%.1f"|format(food.carbs) }}g</td>
|
||||
<td>{{ "%.1f"|format(food.fat) }}g</td>
|
||||
<td>{{ "%.1f"|format(food.fiber) }}g</td>
|
||||
<td>{{ "%.1f"|format(food.sodium) }}mg</td>
|
||||
<td>{{ "%.1f"|format(food.calcium) }}mg</td>
|
||||
<td>{{ "%.2f"|format(food.calories) }}</td>
|
||||
<td>{{ "%.2f"|format(food.protein) }}g</td>
|
||||
<td>{{ "%.2f"|format(food.carbs) }}g</td>
|
||||
<td>{{ "%.2f"|format(food.fat) }}g</td>
|
||||
<td>{{ "%.2f"|format(food.fiber) }}g</td>
|
||||
<td>{{ "%.2f"|format(food.sodium) }}mg</td>
|
||||
<td>{{ "%.2f"|format(food.calcium) }}mg</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -98,4 +117,52 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector('form[action="/foods/upload"]').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const resultsDiv = document.getElementById('upload-results');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span> Uploading...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch('/foods/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
const results = await response.json();
|
||||
|
||||
// Always show results div even if no changes
|
||||
resultsDiv.style.display = 'block';
|
||||
document.getElementById('created-count').textContent = results.created || 0;
|
||||
document.getElementById('updated-count').textContent = results.updated || 0;
|
||||
|
||||
if (results.errors?.length > 0) {
|
||||
document.getElementById('error-list').innerHTML =
|
||||
`<strong>Errors (${results.errors.length}):</strong><br>` + results.errors.join('<br>');
|
||||
} else {
|
||||
document.getElementById('error-list').innerHTML = '';
|
||||
}
|
||||
|
||||
if (results.created || results.updated) {
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
resultsDiv.style.display = 'block';
|
||||
resultsDiv.querySelector('.alert').className = 'alert alert-danger';
|
||||
document.getElementById('error-list').innerHTML =
|
||||
`<strong>Upload Failed:</strong> ${error.message}`;
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Upload CSV';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,101 +1,186 @@
|
||||
<!-- templates/meals.html -->
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h3>Add New Food</h3>
|
||||
<form action="/foods/add" method="post">
|
||||
<h3>Bulk Import Meals</h3>
|
||||
<form action="/meals/upload" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<label class="form-label">CSV File</label>
|
||||
<input type="file" class="form-control" name="file" accept=".csv" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary mb-4">Upload CSV</button>
|
||||
</form>
|
||||
|
||||
<h3>Create New Meal</h3>
|
||||
<form action="/meals/add" method="post">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meal Name</label>
|
||||
<input type="text" class="form-control" name="name" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Serving Size</label>
|
||||
<input type="text" class="form-control" name="serving_size" required>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Unit</label>
|
||||
<input type="text" class="form-control" name="serving_unit" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meal Type</label>
|
||||
<select class="form-control" name="meal_type" required>
|
||||
<option value="breakfast">Breakfast</option>
|
||||
<option value="lunch">Lunch</option>
|
||||
<option value="dinner">Dinner</option>
|
||||
<option value="snack">Snack</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Calories</label>
|
||||
<input type="number" step="0.1" class="form-control" name="calories" required>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Protein (g)</label>
|
||||
<input type="number" step="0.1" class="form-control" name="protein" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Carbs (g)</label>
|
||||
<input type="number" step="0.1" class="form-control" name="carbs" required>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Fat (g)</label>
|
||||
<input type="number" step="0.1" class="form-control" name="fat" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Fiber (g)</label>
|
||||
<input type="number" step="0.1" class="form-control" name="fiber" value="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Sugar (g)</label>
|
||||
<input type="number" step="0.1" class="form-control" name="sugar" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Sodium (mg)</label>
|
||||
<input type="number" step="0.1" class="form-control" name="sodium" value="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Calcium (mg)</label>
|
||||
<input type="number" step="0.1" class="form-control" name="calcium" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-3">Add Food</button>
|
||||
<button type="submit" class="btn btn-primary">Create Meal</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4>Add Food to Meal</h4>
|
||||
<form id="addFoodForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Select Meal</label>
|
||||
<select class="form-control" id="mealSelect">
|
||||
<option value="">Choose meal...</option>
|
||||
{% for meal in meals %}
|
||||
<option value="{{ meal.id }}">{{ meal.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Select Food</label>
|
||||
<select class="form-control" id="foodSelect">
|
||||
<option value="">Choose food...</option>
|
||||
{% for food in foods %}
|
||||
<option value="{{ food.id }}">{{ food.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Quantity</label>
|
||||
<input type="number" step="0.1" class="form-control" id="quantity" value="1">
|
||||
</div>
|
||||
<button type="button" onclick="addFoodToMeal()" class="btn btn-success">Add Food</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<h3>Foods Database</h3>
|
||||
<h3>Meals</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="select-all-meals" onclick="toggleAllMeals(this)"></th>
|
||||
<th>Name</th>
|
||||
<th>Serving</th>
|
||||
<th>Cal</th>
|
||||
<th>Protein</th>
|
||||
<th>Carbs</th>
|
||||
<th>Fat</th>
|
||||
<th>Fiber</th>
|
||||
<th>Sodium</th>
|
||||
<th>Calcium</th>
|
||||
<th>Type</th>
|
||||
<th>Food Items</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for food in foods %}
|
||||
{% for meal in meals %}
|
||||
<tr>
|
||||
<td>{{ food.name }}</td>
|
||||
<td>{{ food.serving_size }} {{ food.serving_unit }}</td>
|
||||
<td>{{ "%.1f"|format(food.calories) }}</td>
|
||||
<td>{{ "%.1f"|format(food.protein) }}g</td>
|
||||
<td>{{ "%.1f"|format(food.carbs) }}g</td>
|
||||
<td>{{ "%.1f"|format(food.fat) }}g</td>
|
||||
<td>{{ "%.1f"|format(food.fiber) }}g</td>
|
||||
<td>{{ "%.1f"|format(food.sodium) }}mg</td>
|
||||
<td>{{ "%.1f"|format(food.calcium) }}mg</td>
|
||||
<td><input type="checkbox" name="selected_meals" value="{{ meal.id }}"></td>
|
||||
<td>{{ meal.name }}</td>
|
||||
<td>{{ meal.meal_type.title() }}</td>
|
||||
<td>
|
||||
{% if meal.meal_foods %}
|
||||
<ul class="list-unstyled">
|
||||
{% for meal_food in meal.meal_foods %}
|
||||
<li>{{ meal_food.quantity }} × {{ meal_food.food.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<em>No foods added</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-danger mt-3" onclick="deleteSelectedMeals()" style="margin-top: 20px !important;">
|
||||
<i class="bi bi-trash"></i> Delete Selected Meals
|
||||
</button>
|
||||
100| </div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<script>
|
||||
function toggleAllMeals(source) {
|
||||
console.log('Toggling all meals');
|
||||
const checkboxes = document.querySelectorAll('input[name="selected_meals"]');
|
||||
checkboxes.forEach(checkbox => checkbox.checked = source.checked);
|
||||
}
|
||||
|
||||
async function deleteSelectedMeals() {
|
||||
console.log('Delete selected meals called');
|
||||
const selected = Array.from(document.querySelectorAll('input[name="selected_meals"]:checked'))
|
||||
.map(checkbox => checkbox.value);
|
||||
|
||||
if (selected.length === 0) {
|
||||
alert('Please select meals to delete');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`Delete ${selected.length} selected meals?`)) {
|
||||
try {
|
||||
console.log('Deleting meals:', selected);
|
||||
const response = await fetch('/meals/delete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({meal_ids: selected})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Delete response:', result);
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(`Delete failed: ${result.message || response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete failed:', error);
|
||||
alert('Delete failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Meal CSV upload handling
|
||||
document.querySelector('form[action="/meals/upload"]').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const resultsDiv = document.createElement('div');
|
||||
resultsDiv.className = 'alert alert-info mt-3';
|
||||
form.parentNode.insertBefore(resultsDiv, form.nextSibling);
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Uploading...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch('/meals/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const results = await response.json();
|
||||
|
||||
if (results.errors?.length > 0) {
|
||||
resultsDiv.className = 'alert alert-danger';
|
||||
resultsDiv.innerHTML = `<strong>Errors (${results.errors.length}):</strong><br>` +
|
||||
results.errors.join('<br>');
|
||||
} else {
|
||||
resultsDiv.className = 'alert alert-success';
|
||||
resultsDiv.innerHTML = `Successfully created ${results.created} meals, updated ${results.updated}`;
|
||||
}
|
||||
|
||||
if (results.created || results.updated) {
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
resultsDiv.className = 'alert alert-danger';
|
||||
resultsDiv.textContent = `Upload failed: ${error.message}`;
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Upload CSV';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user