Implemented Planner System to its basic functions

This commit is contained in:
Jadowyne Ulve 2025-08-12 12:29:51 -05:00
parent 94f7bad2e6
commit 40b0a5f527
40 changed files with 1988 additions and 8 deletions

View File

@ -1,5 +1,6 @@
CREATE TABLE IF NOT EXISTS %%site_name%%_food_info ( CREATE TABLE IF NOT EXISTS %%site_name%%_food_info (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
food_info_uuid UUID gen_random_uuid(),
food_groups TEXT [], food_groups TEXT [],
ingrediants TEXT [], ingrediants TEXT [],
nutrients JSONB, nutrients JSONB,

View File

@ -1,19 +1,22 @@
CREATE TABLE IF NOT EXISTS %%site_name%%_items( CREATE TABLE IF NOT EXISTS %%site_name%%_items(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
item_uuid UUID DEFAULT gen_random_uuid(), item_uuid UUID DEFAULT gen_random_uuid(),
barcode VARCHAR(255) NOT NULL, barcode VARCHAR(255),
item_name VARCHAR(255) NOT NULL, item_name VARCHAR(255) NOT NULL,
brand INTEGER, brand INTEGER,
description TEXT, description TEXT,
tags TEXT [], tags TEXT [],
links JSONB, links JSONB,
item_info_id INTEGER NOT NULL, item_info_id INTEGER NOT NULL,
item_info_uuid UUID NOT NULL,
logistics_info_id INTEGER NOT NULL, logistics_info_id INTEGER NOT NULL,
logistics_info_uuid UUID NOT NULL,
food_info_id INTEGER, food_info_id INTEGER,
food_info_uuid UUID NOT NULL,
row_type VARCHAR(255) NOT NULL, row_type VARCHAR(255) NOT NULL,
item_type VARCHAR(255) NOT NULL, item_type VARCHAR(255) NOT NULL,
search_string TEXT NOT NULL, search_string TEXT NOT NULL,
UNIQUE(item_uuid, barcode, item_info_id), UNIQUE(item_uuid),
CONSTRAINT fk_item_info CONSTRAINT fk_item_info
FOREIGN KEY(item_info_id) FOREIGN KEY(item_info_id)
REFERENCES %%site_name%%_item_info(id) REFERENCES %%site_name%%_item_info(id)

View File

@ -1,6 +1,7 @@
CREATE TABLE IF NOt EXISTS %%site_name%%_item_info ( CREATE TABLE IF NOt EXISTS %%site_name%%_item_info (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
barcode VARCHAR(255) NOT NULL, item_info_uuid UUID gen_random_uuid(),
barcode VARCHAR(255),
packaging VARCHAR(255), packaging VARCHAR(255),
uom_quantity FLOAT8, uom_quantity FLOAT8,
uom INTEGER, uom INTEGER,

View File

@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS %%site_name%%_logistics_info( CREATE TABLE IF NOT EXISTS %%site_name%%_logistics_info(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
barcode VARCHAR(255) NOT NULL, logistics_info_uuid UUID gen_random_uuid(),
barcode VARCHAR(255),
primary_location INTEGER NOT NULL, primary_location INTEGER NOT NULL,
primary_zone INTEGER NOT NULL, primary_zone INTEGER NOT NULL,
auto_issue_location INTEGER NOT NULL, auto_issue_location INTEGER NOT NULL,

View File

@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS %%site_name%%_plan_events(
id SERIAL PRIMARY KEY,
event_uuid UUID DEFAULT gen_random_uuid(),
plan_uuid UUID,
recipe_uuid UUID,
event_shortname VARCHAR(32) NOT NULL,
event_description TEXT,
event_date_start TIMESTAMP NOT NULL,
event_date_end TIMESTAMP NOT NULL,
created_by INTEGER NOT NULL,
event_type VARCHAR(32) NOT NULL,
UNIQUE(event_uuid)
)

View File

@ -1,9 +1,11 @@
CREATE TABLE IF NOT EXISTS %%site_name%%_recipes ( CREATE TABLE IF NOT EXISTS %%site_name%%_recipes (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
recipe_uuid UUID DEFAULT gen_random_uuid() NOT NULL,
name VARCHAR, name VARCHAR,
author INTEGER, author INTEGER,
description TEXT, description TEXT,
creation_date TIMESTAMP, creation_date TIMESTAMP,
instructions TEXT [], instructions TEXT [],
picture_path TEXT picture_path TEXT,
UNIQUE(recipe_uuid)
); );

View File

@ -32,6 +32,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -564,6 +564,30 @@ class BrandsPayload:
self.name, self.name,
) )
@dataclass
class PlanEventPayload:
plan_uuid: str
event_shortname: str
event_description: str
event_date_start: datetime.datetime
event_date_end: datetime.datetime
created_by: int
recipe_uuid: str
event_type: str
def payload(self):
return (
self.plan_uuid,
self.event_shortname,
self.event_description,
self.event_date_start,
self.event_date_end,
self.created_by,
self.recipe_uuid,
self.event_type
)
@dataclass @dataclass
class SiteManager: class SiteManager:
site_name: str site_name: str

View File

@ -43,6 +43,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -55,6 +55,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -37,6 +37,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -41,6 +41,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -39,6 +39,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

View File

@ -0,0 +1,112 @@
# 3RD PARTY IMPORTS
from flask import (
Blueprint, request, render_template, redirect, session, url_for, send_file, jsonify, Response
)
import psycopg2
import math
import datetime
# APPLICATION IMPORTS
from config import config
from application.access_module import access_api
from application import postsqldb, database_payloads
from application.meal_planner import meal_planner_database
meal_planner_api = Blueprint('meal_planner_api', __name__, template_folder="templates", static_folder="static")
@meal_planner_api.route('/', methods=["GET"])
@access_api.login_required
def plannerIndex():
sites = [site[1] for site in postsqldb.get_sites(session['user']['sites'])]
return render_template('meal_planner.html', current_site=session['selected_site'], sites=sites)
@meal_planner_api.route('/api/getEventsByMonth', methods=["GET"])
@access_api.login_required
def getEventsByMonth():
if request.method == "GET":
site_name = session['selected_site']
year = int(request.args.get('year', 2025))
month = int(request.args.get('month', 1))
events = ()
events = meal_planner_database.selectPlanEventsByMonth(site_name, (year, month))
return jsonify(status=201, message="Events fetched Successfully!", events=events)
return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!", events=events)
@meal_planner_api.route('/api/getEventByUUID', methods=["GET"])
@access_api.login_required
def getEventByUUID():
if request.method == "GET":
site_name = session['selected_site']
event_uuid = request.args.get('event_uuid', "")
event = ()
event = meal_planner_database.selectPlanEventByUUID(site_name, (event_uuid,))
return jsonify(status=201, message="Event fetched Successfully!", event=event)
return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!", event=event)
@meal_planner_api.route('/api/getRecipes', methods=["GET"])
@access_api.login_required
def getRecipes():
if request.method == "GET":
site_name = session['selected_site']
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 50))
search_string = request.args.get('search_string', "")
offset = (page - 1) * limit
recipes, count = [], 0
recipes, count = meal_planner_database.paginateRecipesTuples(site_name, (limit, offset))
return jsonify(status=201, message="Recipes fetched Successfully!", recipes=recipes, end=math.ceil(count/limit))
return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!", recipes=recipes, end=math.ceil(count/limit))
@meal_planner_api.route('/api/addEvent', methods=["POST"])
@access_api.login_required
def addEvent():
if request.method == "POST":
site_name = session['selected_site']
event_date_start = datetime.datetime.strptime(request.get_json()['event_date_start'], "%Y-%m-%d")
event_date_end = datetime.datetime.strptime(request.get_json()['event_date_end'], "%Y-%m-%d")
event_payload = database_payloads.PlanEventPayload(
plan_uuid=None,
event_shortname=request.get_json()['event_shortname'],
event_description=request.get_json()['event_description'],
event_date_start=event_date_start,
event_date_end=event_date_end,
created_by=session['user_id'],
recipe_uuid=request.get_json()['recipe_uuid'],
event_type=request.get_json()['event_type']
)
meal_planner_database.insertPlanEventTuple(site_name, event_payload.payload())
return jsonify(status=201, message="Event added Successfully!")
return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!")
@meal_planner_api.route('/api/saveEvent', methods=["POST"])
@access_api.login_required
def saveEvent():
if request.method == "POST":
site_name = session['selected_site']
event_uuid = request.get_json()['event_uuid']
update = request.get_json()['update']
meal_planner_database.updatePlanEventTuple(site_name, {'uuid': event_uuid, "update": update})
return jsonify(status=201, message="Event Saved Successfully!")
return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!")
@meal_planner_api.route('/api/removeEvent', methods=["POST"])
@access_api.login_required
def removeEvent():
if request.method == "POST":
site_name = session['selected_site']
event_uuid = request.get_json()['event_uuid']
meal_planner_database.deletePlanEventTuple(site_name, (event_uuid, ))
return jsonify(status=201, message="Event removed Successfully!")
return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!")

View File

@ -0,0 +1,187 @@
import psycopg2
from application import postsqldb
import config
def paginateRecipesTuples(site: str, payload: tuple, convert=True, conn=None):
self_conn = False
recipes = ()
count = 0
sql = f"SELECT * FROM {site}_recipes ORDER BY name ASC LIMIT %s OFFSET %s;"
sql_count = f"SELECT COUNT(*) FROM {site}_recipes;"
try:
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = True
self_conn = True
with conn.cursor() as cur:
cur.execute(sql, payload)
rows = cur.fetchall()
if rows and convert:
recipes = [postsqldb.tupleDictionaryFactory(cur.description, row) for row in rows]
if rows and not convert:
recipes = rows
cur.execute(sql_count)
count = cur.fetchone()[0]
if self_conn:
conn.close()
return recipes, count
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def selectPlanEventsByMonth(site: str, payload: tuple, convert=True, conn=None):
"""payload=(year, month)"""
self_conn = False
event_tuples = ()
with open('application/meal_planner/sql/selectPlanEventsByMonth.sql', 'r+') as file:
sql = file.read().replace("%%site_name%%", site)
try:
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = True
self_conn = True
with conn.cursor() as cur:
cur.execute(sql, payload)
rows = cur.fetchall()
if rows and convert:
event_tuples = [postsqldb.tupleDictionaryFactory(cur.description, row) for row in rows]
if rows and not convert:
event_tuples = rows
if self_conn:
conn.close()
return event_tuples
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def selectPlanEventByUUID(site: str, payload: tuple, convert=True, conn=None):
"""payload=(event_uuid,)"""
self_conn = False
event_tuple = ()
sql = f"SELECT * FROM {site}_plan_events WHERE event_uuid = %s;"
try:
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = True
self_conn = True
with conn.cursor() as cur:
cur.execute(sql, payload)
rows = cur.fetchone()
if rows and convert:
event_tuple = postsqldb.tupleDictionaryFactory(cur.description, rows)
if rows and not convert:
event_tuple = rows
if self_conn:
conn.close()
return event_tuple
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def insertPlanEventTuple(site: str, payload: tuple, convert=True, conn=None):
self_conn = False
event_tuple = ()
with open('application/meal_planner/sql/insertPlanEvent.sql', 'r+') as file:
sql = file.read().replace("%%site_name%%", site)
try:
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = True
self_conn = True
with conn.cursor() as cur:
cur.execute(sql, payload)
rows = cur.fetchone()
if rows and convert:
event_tuple = postsqldb.tupleDictionaryFactory(cur.description, rows)
if rows and not convert:
event_tuple = rows
if self_conn:
conn.commit()
conn.close()
return event_tuple
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def updatePlanEventTuple(site:str, payload: dict, convert=True, conn=None):
""" payload (dict): {'barcode': row_id, 'update': {... column_to_update: value_to_update_to...}} """
updated = ()
self_conn = False
set_clause, values = postsqldb.updateStringFactory(payload['update'])
values.append(payload['uuid'])
sql = f"UPDATE {site}_plan_events SET {set_clause} WHERE event_uuid=%s RETURNING *;"
try:
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = False
self_conn = True
with conn.cursor() as cur:
cur.execute(sql, values)
rows = cur.fetchone()
if rows and convert:
updated = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
updated = rows
if self_conn:
conn.commit()
conn.close()
return updated
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def deletePlanEventTuple(site, payload, convert=True, conn=None):
""" payload = (ids...)"""
deleted = ()
self_conn = False
sql = f"WITH deleted_rows AS (DELETE FROM {site}_plan_events WHERE event_uuid IN ({','.join(['%s'] * len(payload))}) RETURNING *) SELECT * FROM deleted_rows;"
try:
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = True
self_conn = True
with conn.cursor() as cur:
cur.execute(sql, payload)
rows = cur.fetchall()
if rows and convert:
deleted = [postsqldb.tupleDictionaryFactory(cur.description, r) for r in rows]
elif rows and not convert:
deleted = rows
if self_conn:
conn.commit()
conn.close()
return deleted
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)

View File

@ -0,0 +1,4 @@
INSERT INTO %%site_name%%_plan_events
(plan_uuid, event_shortname, event_description, event_date_start, event_date_end, created_by, recipe_uuid, event_type)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *;

View File

@ -0,0 +1,30 @@
WITH arguments AS (
SELECT %s AS year, %s AS month
),
sum_cte AS (
SELECT mi.item_uuid, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum
FROM %%site_name%%_item_locations mil
JOIN %%site_name%%_items mi ON mil.part_id = mi.id
GROUP BY mi.id
),
cte_recipe_items AS (
SELECT rp_item.rp_id, rp_item.qty, COALESCE(sum_cte.total_sum, 0) as quantity_on_hand FROM %%site_name%%_recipe_items rp_item
LEFT JOIN sum_cte ON sum_cte.item_uuid = rp_item.item_uuid
),
recipe_missing_items AS (
SELECT
rp_id, bool_or(qty > quantity_on_hand) AS has_missing_ingredients
FROM cte_recipe_items
GROUP BY rp_id
)
SELECT events.*,
COALESCE(row_to_json(recipes.*), '{}') as recipe,
COALESCE(recipe_missing_items.has_missing_ingredients, FALSE) AS has_missing_ingredients
FROM %%site_name%%_plan_events events
LEFT JOIN %%site_name%%_recipes recipes ON recipes.recipe_uuid = events.recipe_uuid
LEFT JOIN recipe_missing_items ON recipe_missing_items.rp_id = recipes.id
WHERE
event_date_end >= make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1)
AND
event_date_start < (make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) + INTERVAL '1 month');

View File

@ -0,0 +1,138 @@
#calendar_container {
background-color: whitesmoke;
box-shadow: wheat;
border-radius: 10px;
}
#calender_table {
table-layout: fixed;
width: 100%;
}
#calender_table th {
width: 14.28%;
min-width: 100px;
max-width: 1fr;
height: 40px;
vertical-align: top;
box-sizing: border-box;
font-weight: bold;
background-color: whitesmoke;
}
#calender_table td {
width: 14.28%;
min-width: 100px;
max-width: 1fr;
height: 120px;
vertical-align: top;
box-sizing: border-box;
}
.calendar-cell-empty {
background-color: whitesmoke;
border: 1px solid rgba(155, 155, 155, 30%);
}
.calendar-cell {
position: relative;
width: 150px;
height: 120px;
vertical-align: top;
padding: 5px;
margin: 5px;
background-color: white;
border: 1px solid rgba(155, 155, 155, 30%);
}
.calendar-cell:hover{
background-color: whitesmoke;
border: 2px solid rgba(155, 155, 155, 30%);
}
.calendar-cell:hover, .calendar-cell-selected{
background-color: whitesmoke;
border: 2px solid rgba(155, 155, 155, 30%);
}
.date-box {
width: 25px;
height: 25px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 5px;
top: 5px;
z-index: 2;
}
.recipes-box {
position: absolute;
top: 5px;
left: 35px;
right: 5px;
bottom: 5px;
padding: 3px;
overflow: auto;
z-index: 1;
}
.recipe-label.recipe-success {
background:rgb(158, 221, 145);
margin-bottom: 3px;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.recipe-label.recipe-error {
background-color: rgb(218, 143, 143);
margin-bottom: 3px;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.recipe-label.recipe-success:hover{
background-color: rgb(178, 241, 165);
cursor: pointer;
}
.recipe-label.recipe-success:hover, .recipe-label-selected {
background-color: rgb(178, 241, 165);
}
.recipe-label.recipe-error:hover{
background-color:rgb(238, 163, 163);
cursor: pointer;
}
.recipe-label.recipe-error:hover, .recipe-label-selected {
background-color: rgb(238, 163, 163);
}
.custom-label {
background:rgb(211, 211, 211);
margin-bottom: 3px;
padding: 2px 5px;
border-radius: 1px;
font-size: 12px;
font-weight: bold;
}
.custom-label:hover{
background-color: rgb(225, 255, 255);
cursor: pointer;
}
.custom-label:hover, .custom-label-selected {
background-color: rgb(255, 255, 255);
}
.my-list-item:hover {
background-color: whitesmoke;
}

View File

@ -0,0 +1,596 @@
var year = 2025
var month = 8
const monthNames = ["", "January", "February", "March", "April", "May", "June","July", "August", "September", "October", "November", "December"];
var eventsByDay = {
3: ["Chicken Stir Fry", "Salad"],
8: ["Spaghetti Bolognese"],
12: ["Fish Tacos", "Rice", "Beans"],
31: ['Brats']
};
async function changeSite(site){
console.log(site)
const response = await fetch(`/changeSite`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
site: site,
}),
});
data = await response.json();
transaction_status = "success"
if (data.error){
transaction_status = "danger"
}
UIkit.notification({
message: data.message,
status: transaction_status,
pos: 'top-right',
timeout: 5000
});
location.reload(true)
}
async function getEventsByMonth() {
const url = new URL('/planner/api/getEventsByMonth', window.location.origin);
url.searchParams.append('year', year);
url.searchParams.append('month', month);
const response = await fetch(url);
data = await response.json();
return data.events;
}
async function getEventByUUID(event_uuid) {
const url = new URL('/planner/api/getEventByUUID', window.location.origin);
url.searchParams.append('event_uuid', event_uuid);
const response = await fetch(url);
data = await response.json();
return data.event;
}
async function parseEvents(events) {
eventsByDay = {}
for (let i = 0; i < events.length; i++){
console.log(`new event -- ${events[i].event_shortname}`)
let event_date_start = new Date(events[i].event_date_start)
let event_date_end = new Date(events[i].event_date_end)
let this_month = month
let start_day = event_date_start.getUTCDate()
let start_month = event_date_start.getUTCMonth() + 1
let end_day = event_date_end.getUTCDate()
let end_month = event_date_end.getUTCMonth() + 1
if(start_month !== this_month){
start_day = 1
}
if(end_month !== this_month){
end_day = new Date(year, month, 0).getUTCDate();
}
for (let y = start_day; y <= end_day; y++){
if (!eventsByDay[y]) {
eventsByDay[y] = [];
}
let dayarray = eventsByDay[y]
dayarray.push(events[i])
eventsByDay[y] = dayarray
}
}
console.log(eventsByDay)
}
document.addEventListener('DOMContentLoaded', async function() {
let today = new Date();
year = today.getFullYear();
month = today.getMonth() + 1;
await setupCalendarAndEvents()
})
async function setupCalendarAndEvents(){
console.log(year, month)
events = await getEventsByMonth()
await parseEvents(events)
await createCalender()
document.getElementById('calender_table').addEventListener('contextmenu', function(e) {
e.preventDefault();
let recipeLabel = e.target.closest('.recipe-label');
let calendarCell = e.target.closest('.calendar-cell');
let customLabel = e.target.closest('.custom-label');
if (recipeLabel) {
recipeLabel.classList.add('recipe-label-selected')
let rect = recipeLabel.getBoundingClientRect();
let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
let menuX = rect.left + scrollLeft;
let menuY = rect.bottom + scrollTop;
showContextMenuForEvent(recipeLabel, menuX, menuY);
} else if (customLabel) {
customLabel.classList.add('custom-label-selected')
let rect = customLabel.getBoundingClientRect();
let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
let menuX = rect.left + scrollLeft;
let menuY = rect.bottom + scrollTop;
showContextMenuForEvent(customLabel, menuX, menuY);
} else if (calendarCell) {
calendarCell.classList.add('calendar-cell-selected')
let rect = calendarCell.getBoundingClientRect();
let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
let menuX = rect.left + scrollLeft;
let menuY = rect.bottom + scrollTop;
showContextMenuForCell(calendarCell, menuX, menuY);
} else {
hideContextMenu();
}
});
}
async function createCalender() {
let calender_container = document.getElementById('calendar_container')
calender_container.innerHTML = ""
let firstDay = new Date(year, month - 1, 1);
let numDays = new Date(year, month, 0).getDate();
let startDay = firstDay.getDay();
let calender_table = document.createElement('table')
calender_table.setAttribute('id', 'calender_table')
calender_table.setAttribute('class', 'uk-table uk-table-middle uk-table-large uk-table-responsive')
let table_headers = document.createElement('thead')
table_headers.innerHTML = `<tr><th class="uk-text-center">Sunday</th><th class="uk-text-center">Monday</th><th class="uk-text-center">Tuesday</th><th class="uk-text-center">Wednesday</th><th class="uk-text-center">Thursday</th><th class="uk-text-center">Friday</th><th class="uk-text-center">Saturday</th></tr>`
calender_table.append(table_headers)
let tableRow = document.createElement('tr')
for (let i = 0; i < startDay; i++){
let table_cell = document.createElement('td')
table_cell.setAttribute('class', 'uk-table-expand uk-visible@m calendar-cell-empty')
tableRow.append(table_cell)
}
console.log(eventsByDay)
for (let day = 1; day <= numDays; day++) {
let table_cell = document.createElement('td')
let eventsHTML = "";
if (eventsByDay[day]) {
eventsByDay[day].forEach(event => {
if(event.event_type==="recipe" && event.has_missing_ingredients){
eventsHTML += `<div class="recipe-label recipe-error" data-event_uuid="${event.event_uuid}" data-day="${day}">${event.event_shortname}</div>`;
} else if (event.event_type==="recipe" && !event.has_missing_ingredients){
eventsHTML += `<div class="recipe-label recipe-success" data-event_uuid="${event.event_uuid}" data-day="${day}">${event.event_shortname}</div>`;
} else {
eventsHTML += `<div class="custom-label" data-event_uuid="${event.event_uuid}" data-day="${day}">${event.event_shortname}</div>`;
}
});
}
table_cell.innerHTML = `<div class="date-box" data-day="${day}">${day}</div><div class="recipes-box">${eventsHTML}</div>`;
table_cell.classList.add("calendar-cell");
table_cell.dataset.day = day;
tableRow.append(table_cell)
if ((startDay + day) % 7 === 0 && day !== numDays){
calender_table.append(tableRow)
tableRow = document.createElement('tr')
};
}
let lastDayOfWeek = (startDay + numDays - 1) % 7;
for (let i = lastDayOfWeek + 1; i <= 6; i++) {
let table_cell = document.createElement('td')
table_cell.setAttribute('class', 'uk-visible@m calendar-cell-empty')
tableRow.append(table_cell)
}
calender_table.append(tableRow)
let table_footer = document.createElement('tr')
table_footer.innerHTML = `<th></th><th></th><th></th><th></th><th></th><th></th><th></th>`
calender_table.append(table_footer)
calender_container.append(calender_table)
document.getElementById("month-year-title").innerHTML = `${monthNames[month]} ${year}`;
}
function showContextMenuForEvent(eventLabel, x, y) {
const menu = document.getElementById('calendarContextMenu');
// Set only "Edit" and "Remove" (and optionally "Add Another")
menu.className = "uk-dropdown uk-open";
menu.innerHTML = `
<ul class="uk-nav uk-dropdown-nav">
<li><a href="#" onclick="editEvent('${eventLabel.dataset.event_uuid}')">Edit Event</a></li>
<li><a href="#" onclick="postRemoveEvent('${eventLabel.dataset.event_uuid}')">Remove Event</a></li>
</ul>
`;
menu.style.display = 'block';
menu.style.left = x + 'px';
menu.style.top = y + 'px';
}
function showContextMenuForCell(calendarCell, x, y) {
const menu = document.getElementById('calendarContextMenu');
// Only "Add Event"
menu.className = "uk-dropdown uk-open";
menu.innerHTML = `
<ul class="uk-nav uk-dropdown-nav">
<li><a href="#" onclick="addEvent('${calendarCell.dataset.day}')">Add Event</a></li>
</ul>
`;
menu.style.display = 'block';
menu.style.left = x + 'px';
menu.style.top = y + 'px';
}
window.addEventListener('click', function() {
document.getElementById('calendarContextMenu').style.display = 'none';
document.querySelectorAll('.calendar-cell-selected').forEach(el => el.classList.remove('calendar-cell-selected'));
document.querySelectorAll('.custom-label-selected').forEach(el => el.classList.remove('custom-label-selected'));
document.querySelectorAll('.recipe-label-selected').forEach(el => el.classList.remove('recipe-label-selected'));
});
async function addEvent(day) {
let menu = document.getElementById('calendarContextMenu');
//let day = menu.getAttribute('data-day')
console.log(year, month, day)
let customDate = new Date(year, month-1, day);
document.getElementById('event_date_start').value = customDate.toISOString().split('T')[0];
document.getElementById('event_date_end').value = customDate.toISOString().split('T')[0];
UIkit.modal(document.getElementById('eventModal')).show();
}
async function editEvent(event_uuid) {
console.log(event_uuid)
let event = await getEventByUUID(event_uuid)
console.log(event)
document.getElementById('event_uuid_edit').value = event_uuid
let event_date_start = new Date(event.event_date_start)
let y = event_date_start.getFullYear();
let m = event_date_start.getUTCMonth();
let d = event_date_start.getUTCDate();
event_date_start = new Date(y, m, d);
let event_date_end = new Date(event.event_date_end)
let end_y = event_date_end.getFullYear();
let end_m = event_date_end.getUTCMonth();
let end_d = event_date_end.getUTCDate();
event_date_end = new Date(end_y, end_m, end_d);
document.getElementById('event_date_edit_start').value = event_date_start.toISOString().split('T')[0]
document.getElementById('event_date_edit_end').value = event_date_end.toISOString().split('T')[0]
document.getElementById('event_type_edit').value = event.event_type
document.getElementById('recipe_label_modal_edit').value = event.recipe_uuid
document.getElementById('event_description_edit').value = event.event_description
document.getElementById('event_name_edit').value = event.event_shortname
if(event.event_type==="recipe"){
document.getElementById('event_name_edit').classList.add('uk-disabled')
document.getElementById('event_name_edit').classList.add('uk-form-blank')
document.getElementById('recipe_label_edit_parent').hidden = false
} else {
document.getElementById('event_name_edit').classList.remove('uk-disabled')
document.getElementById('event_name_edit').classList.remove('uk-form-blank')
document.getElementById('recipe_label_edit_parent').hidden = true
}
UIkit.modal(document.getElementById('eventEditModal')).show();
}
async function postNewEvent(){
let event_shortname = document.getElementById('event_name').value
let event_description = document.getElementById('event_description').value
let event_date_start = document.getElementById('event_date_start').value
let event_date_end = document.getElementById('event_date_end').value
let event_type = document.getElementById('event_type').value
let recipe_uuid = null
if (event_type === "recipe"){
recipe_uuid = document.getElementById('selected-recipe').value
}
const response = await fetch('/planner/api/addEvent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
event_shortname: event_shortname,
event_description: event_description,
event_date_start: event_date_start,
event_date_end: event_date_end,
recipe_uuid: recipe_uuid,
event_type: event_type
})
});
data = await response.json();
response_status = 'primary'
if (!data.status === 201){
response_status = 'danger'
}
UIkit.notification({
message: data.message,
status: response_status,
pos: 'top-right',
timeout: 5000
});
await setupCalendarAndEvents()
UIkit.modal(document.getElementById('eventModal')).hide();
}
async function postEditEvent(){
let event_uuid = document.getElementById('event_uuid_edit').value
let event_shortname = document.getElementById('event_name_edit').value
let event_description = document.getElementById('event_description_edit').value
let event_date_start = document.getElementById('event_date_edit_start').value
let event_date_end = document.getElementById('event_date_edit_end').value
let event_type = document.getElementById('event_type_edit').value
const response = await fetch('/planner/api/saveEvent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
event_uuid: event_uuid,
update: {
event_shortname: event_shortname,
event_description: event_description,
event_date_start: event_date_start,
event_date_end: event_date_end,
event_type: event_type
}
})
});
data = await response.json();
response_status = 'primary'
if (!data.status === 201){
response_status = 'danger'
}
UIkit.notification({
message: data.message,
status: response_status,
pos: 'top-right',
timeout: 5000
});
await setupCalendarAndEvents()
UIkit.modal(document.getElementById('eventEditModal')).hide();
}
async function postRemoveEvent(event_uuid){
const response = await fetch('/planner/api/removeEvent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
event_uuid: event_uuid
})
});
data = await response.json();
response_status = 'primary'
if (!data.status === 201){
response_status = 'danger'
}
UIkit.notification({
message: data.message,
status: response_status,
pos: 'top-right',
timeout: 5000
});
await setupCalendarAndEvents()
}
// main window functions
async function backOneMonth() {
if(month === 1){
year = year - 1
month = 12
} else {
month = month - 1
}
await setupCalendarAndEvents()
}
async function forwardOneMonth() {
if(month === 12){
year = year + 1
month = 1
} else {
month = month + 1
}
await setupCalendarAndEvents()
}
// Main Modal Functions
var eventModal_type = "recipe"
async function setEventTypeForm(){
let event_type = document.getElementById('event_type').value
document.getElementById('event_name').value = ""
document.getElementById('event_description').value = ""
document.getElementById('selected-recipe').value = ""
if(event_type === "custom"){
eventModal_type = "custom"
document.getElementById('recipe_button_modal').hidden = true
document.getElementById('recipe_label_modal').hidden = true
document.getElementById('event_name').classList.remove('uk-disabled')
} else if (event_type === "recipe"){
eventModal_type = "recipe"
document.getElementById('recipe_button_modal').hidden = false
document.getElementById('recipe_label_modal').hidden = false
document.getElementById('event_name').classList.add('uk-disabled')
}
}
// Select Row Modal Handlers
var eventModal_page = 1
var eventModal_end = 1
var eventModal_search = ""
var eventModal_limit = 50
async function selectRecipeEvent() {
document.getElementById('mainEventBody').hidden = true
document.getElementById('paginationModalBody').hidden = false
document.getElementById('eventsModalFooter').hidden = true
let recipes = await fetchRecipes()
await updateEventsPaginationElement()
await updateEventsTableWithRecipes(recipes)
}
async function fetchRecipes() {
const url = new URL('/planner/api/getRecipes', window.location.origin);
url.searchParams.append('page', eventModal_page);
url.searchParams.append('limit', eventModal_limit);
url.searchParams.append('search_string', eventModal_search);
const response = await fetch(url);
data = await response.json();
eventModal_end = data.end
return data.recipes;
}
async function updateEventsTableWithRecipes(recipes) {
let eventsTableBody = document.getElementById('eventsTableBody')
eventsTableBody.innerHTML = ""
for (let i = 0; i < recipes.length; i++){
let tableRow = document.createElement('tr')
let nameCell = document.createElement('td')
nameCell.innerHTML = `${recipes[i].name}`
let opCell = document.createElement('td')
let selectButton = document.createElement('button')
selectButton.setAttribute('class', 'uk-button uk-button-primary uk-button-small')
selectButton.innerHTML = "Select"
selectButton.onclick = async function() {
document.getElementById('selected-recipe').value = recipes[i].recipe_uuid
document.getElementById('event_name').value = recipes[i].name
document.getElementById('mainEventBody').hidden = false
document.getElementById('paginationModalBody').hidden = true
document.getElementById('eventsModalFooter').hidden = false
}
opCell.append(selectButton)
tableRow.append(nameCell, opCell)
eventsTableBody.append(tableRow)
}
}
async function setEventModalPage(pageNumber){
eventModal_page = pageNumber;
if (eventModal_type == "recipe"){
let records = await fetchRecipes()
}
await updateItemsModalTable(records)
await updateItemsPaginationElement()
}
async function updateEventsPaginationElement() {
let paginationElement = document.getElementById('eventPage');
paginationElement.innerHTML = "";
// previous
let previousElement = document.createElement('li')
if(eventModal_page<=1){
previousElement.innerHTML = `<a><span uk-pagination-previous></span></a>`;
previousElement.classList.add('uk-disabled');
}else {
previousElement.innerHTML = `<a onclick="setEventModalPage(${eventModal_page-1})"><span uk-pagination-previous></span></a>`;
}
paginationElement.append(previousElement)
//first
let firstElement = document.createElement('li')
if(eventModal_page<=1){
firstElement.innerHTML = `<a><strong>1</strong></a>`;
firstElement.classList.add('uk-disabled');
}else {
firstElement.innerHTML = `<a onclick="setEventModalPage(1)">1</a>`;
}
paginationElement.append(firstElement)
// ...
if(eventModal_page-2>1){
let firstDotElement = document.createElement('li')
firstDotElement.classList.add('uk-disabled')
firstDotElement.innerHTML = `<span>…</span>`;
paginationElement.append(firstDotElement)
}
// last
if(eventModal_page-2>0){
let lastElement = document.createElement('li')
lastElement.innerHTML = `<a onclick="setEventModalPage(${eventModal_page-1})">${eventModal_page-1}</a>`
paginationElement.append(lastElement)
}
// current
if(eventModal_page!=1 && eventModal_page != eventModal_end){
let currentElement = document.createElement('li')
currentElement.innerHTML = `<li class="uk-active"><span aria-current="page"><strong>${eventModal_page}</strong></span></li>`
paginationElement.append(currentElement)
}
// next
if(eventModal_page+2<eventModal_end+1){
let nextElement = document.createElement('li')
nextElement.innerHTML = `<a onclick="setEventModalPage(${eventModal_page+1})">${eventModal_page+1}</a>`
paginationElement.append(nextElement)
}
// ...
if(eventModal_page+2<=eventModal_end){
let secondDotElement = document.createElement('li')
secondDotElement.classList.add('uk-disabled')
secondDotElement.innerHTML = `<span>…</span>`;
paginationElement.append(secondDotElement)
}
//end
let endElement = document.createElement('li')
if(eventModal_page>=eventModal_end){
endElement.innerHTML = `<a><strong>${eventModal_end}</strong></a>`;
endElement.classList.add('uk-disabled');
}else {
endElement.innerHTML = `<a onclick="setEventModalPage(${eventModal_end})">${eventModal_end}</a>`;
}
paginationElement.append(endElement)
//next button
let nextElement = document.createElement('li')
if(eventModal_page>=eventModal_end){
nextElement.innerHTML = `<a><span uk-pagination-next></span></a>`;
nextElement.classList.add('uk-disabled');
}else {
nextElement.innerHTML = `<a onclick="setEventModalPage(${eventModal_page+1})"><span uk-pagination-next></span></a>`;
console.log(nextElement.innerHTML)
}
paginationElement.append(nextElement)
}

View File

@ -0,0 +1,315 @@
<!DOCTYPE html>
<html lang="en" dir="ltr" id="main_html">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8" />
<title id="title"></title>
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<!-- Material Symbols - Outlined Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<!-- Material Symbols - Rounded Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded" rel="stylesheet" />
<!-- Material Symbols - Sharp Set -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/uikit.min.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/pantry.css') }}"/>
<link rel="stylesheet" href="{{ url_for('meal_planner_api.static', filename='css/planner.css') }}"/>
{% if session['user']['flags']['darkmode'] %}
<link id="dark-mode" rel="stylesheet" href="{{ url_for('static', filename='css/dark-mode.css') }}"/>
{% endif %}
</head>
{% if session['user']['flags']['darkmode'] %}
<body class="uk-light">
{% else %}
<body>
{% endif %}
<nav class="uk-navbar-container">
<div class="uk-container uk-container-expand">
<div class="uk-navbar uk-navbar-primary">
<!-- Application Navigation-->
<div class="uk-navbar-left">
<ul class="uk-navbar-nav">
<li>
<a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav">
<li class="uk-active"><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li>
<li><a href="/items">Items</a></li>
<li><a href="/items/transaction">Transaction</a></li>
<li><a href="/receipts">Receipts</a></li>
<li class="uk-nav-header">Points of Ease</li>
<li><a href="/poe/scanner">Transaction Scanner</a></li>
<li class="uk-active"><a href="/poe/receipts">Receipts Scanner</a></li>
</ul>
</div>
</li>
</ul>
</div>
<!-- Breadcrumbs Navigation -->
<div class="uk-navbar-center uk-visible@m">
<ul class="uk-breadcrumb uk-margin-remove">
<li style="cursor: pointer;"><span><strong>{{current_site}}</strong></span>
<div uk-dropdown="mode: hover">
<ul class="uk-nav uk-dropdown-nav">
<li class="uk-nav-header">Select Site</li>
<li class="uk-nav-divider"></li>
{% for site in sites %}
{% if site == current_site %}
<li><a class="uk-disabled" href="#">{{site}}</a></li>
{% else %}
<li><a onclick="changeSite('{{site}}')">{{site}}</a></li>
{% endif %}
{% endfor %}
</ul>
</div>
</li>
<li style="cursor: default; user-select: none;" class="uk-disabled"><span>Meal Planner</span></li>
</ul>
</div>
<!-- Profile/Management Navigation-->
<div class="uk-navbar-right">
<ul class="uk-navbar-nav">
<li>
<a href="#">
<img src="{{session['user']['profile_pic_url']}}" alt="Profile Picture" class="profile-pic uk-visible@m" style="width: 40px; height: 40px; border-radius: 50%; margin-right: 5px;">
{{username}}
</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/profile">Profile</a></li>
<li><a onclick="toggleDarkMode()">Dark Mode</a></li>
<li><a href="/site-management">Site Management</a></li>
<li><a href="/administration">System Management</a></li>
<li><a href="/access/logout">Logout</a></li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
</nav>
<div class="uk-section">
<div class="uk-container uk-text-center">
<div uk-grid>
<div class="uk-width-1-5">
<a onclick="backOneMonth()" uk-icon="icon: chevron-left"></a>
</div>
<div class="uk-width-3-5">
<h3 id="month-year-title" class=""></h3>
</div>
<div class="uk-width-1-5">
<a onclick="forwardOneMonth()" uk-icon="icon: chevron-right"></a>
</div>
</div>
</div>
<div class="uk-container-xlarge uk-align-center">
<div id='calendar_container' class="uk-align-center uk-overflow-auto">
</div>
</div>
<div id="calendarContextMenu"
class="uk-dropdown uk-open"
style="display:none; position:absolute; z-index:9999;">
<ul class="uk-nav uk-dropdown-nav">
<li onclick="addEventContextAction()"><a href="#">Add Event</a></li>
<li onclick=""><a href="#">Edit</a></li>
<li onclick=""><a href="#">Remove</a></li>
</ul>
</div>
</div>
<!-- Event Edit Modal-->
<div id="eventEditModal" class="uk-flex-top" uk-modal>
<div class="uk-modal-dialog">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<h2 class="uk-modal-title">Event Form</h2>
</div>
<div class="uk-modal-body">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_uuid_edit">Event UUID</label>
<div class="uk-form-controls">
<input id="event_uuid_edit" class="uk-input uk-disabled uk-form-blank" type="text">
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_date_edit_start">Event Date</label>
<div class="uk-form-controls">
<input id="event_date_edit_start" class="uk-input" type="date">
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_date_edit_end">Event Date</label>
<div class="uk-form-controls">
<input id="event_date_edit_end" class="uk-input" type="date">
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_type_edit">Event Type</label>
<div class="uk-form-controls">
<input id="event_type_edit" class="uk-input uk-disabled uk-form-blank" type="text">
</div>
</div>
</div>
<div class="uk-width-1-1" id="recipe_label_edit_parent">
<div class="uk-margin">
<label class="uk-form-label" for="recipe_label_modal_edit">Event Recipe UUID</label>
<div class="uk-form-controls">
<input id="recipe_label_modal_edit" class="uk-input uk-disabled uk-form-blank" type="text">
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_name_edit">Event Name</label>
<div class="uk-form-controls">
<input class="uk-input" id="event_name_edit" type="text" placeholder="Enter Event Name...">
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_description_edit">Event Description</label>
<div class="uk-form-controls">
<textarea id="event_description_edit" class="uk-textarea" rows="5" placeholder="Enter Event Description..." aria-label="Textarea"></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="uk-modal-footer uk-text-right" id="eventsModalFooter">
<button class="uk-button uk-button-default uk-modal-close" type="button">Cancel</button>
<button onclick="postEditEvent()" class="uk-button uk-button-primary" type="button">Save</button>
</div>
</div>
</div>
<!-- Event Modal-->
<div id="eventModal" class="uk-flex-top" uk-modal>
<div class="uk-modal-dialog">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<h2 class="uk-modal-title">Event Form</h2>
</div>
<div id="paginationModalBody" class="uk-modal-body" hidden>
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-1">
<div id="searchItemsForm" onkeydown="" class="uk-search uk-search-default uk-align-center">
<input id="searchItemsInput" class="uk-border-pill uk-search-input" type="search" placeholder="" aria-label="">
<span class="uk-search-icon-flip" uk-search-icon></span>
</div>
</div>
<div class="uk-width-1-1">
<nav aria-label="Pagination">
<ul id="eventPage" class="uk-pagination uk-flex-center" uk-margin>
<li><a href="#"><span uk-pagination-previous></span></a></li>
<li><a href="#">1</a></li>
<li class="uk-disabled"><span></span></li>
<li><a href="#">5</a></li>
<li><a href="#">6</a></li>
<li class="uk-active"><span aria-current="page">7</span></li>
<li><a href="#">8</a></li>
<li><a href="#"><span uk-pagination-next></span></a></li>
</ul>
</nav>
</div>
<div class="uk-width-1-1">
<caption>Select a Recipe from the system...</caption>
<table class="uk-table uk-table-striped">
<thead>
<tr>
<th>Name</th>
<th>Operations</th>
</tr>
</thead>
<tbody id="eventsTableBody">
</tbody>
</table>
</div>
</div>
</div>
<div id="mainEventBody" class="uk-modal-body">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-1">
<p>Adding an event requires a type of event and the following information.</p>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_date_start">Event Date Start</label>
<div class="uk-form-controls">
<input id="event_date_start" class="uk-input uk-disabled" type="date">
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_date_end">Event Date End</label>
<div class="uk-form-controls">
<input id="event_date_end" class="uk-input uk-disabled" type="date">
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_type">Select Event Type</label>
<div class="uk-form-controls">
<select onchange="setEventTypeForm()" class="uk-select" id="event_type">
<option value="custom" selected>Custom</option>
<option value="recipe">Recipe</option>
</select>
</div>
</div>
</div>
<div class="uk-width-1-1" id="recipe_button_modal" hidden>
<div class="uk-margin">
<button class="uk-button uk-button-default" type="button" onclick="selectRecipeEvent()">Choose Recipe...</button>
</div>
</div>
<div class="uk-width-1-1" id="recipe_label_modal" hidden>
<div class="uk-margin">
<input id="selected-recipe" class="uk-input uk-disabled" type="text" placeholder="No recipe selected">
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_name">Event Name</label>
<div class="uk-form-controls">
<input class="uk-input" id="event_name" type="text" placeholder="Enter Event Name...">
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-margin">
<label class="uk-form-label" for="event_description">Event Description</label>
<div class="uk-form-controls">
<textarea id="event_description" class="uk-textarea" rows="5" placeholder="Enter Event Description..." aria-label="Textarea"></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="uk-modal-footer uk-text-right" id="eventsModalFooter">
<button class="uk-button uk-button-default uk-modal-close" type="button">Cancel</button>
<button onclick="postNewEvent()" class="uk-button uk-button-primary" type="button">Save</button>
</div>
</div>
</div>
</body>
{% assets "js_all" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
<script src="{{ url_for('meal_planner_api.static', filename='js/mealPlannerHandler.js') }}"></script>
</html>

View File

@ -57,6 +57,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -40,6 +40,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -40,6 +40,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -38,6 +38,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -38,6 +38,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -45,6 +45,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown uk-light" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown uk-light" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -38,6 +38,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li class="uk-active"><a href="/recipes">Recipes</a></li> <li class="uk-active"><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -39,6 +39,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -39,6 +39,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li class="uk-active"><a href="/shopping-lists">Shopping Lists</a></li> <li class="uk-active"><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -39,6 +39,7 @@
<a href>Apps</a> <a href>Apps</a>
<div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false"> <div class="uk-navbar-dropdown" uk-drop="mode: click; multi:false">
<ul class="uk-nav uk-navbar-dropdown-nav"> <ul class="uk-nav uk-navbar-dropdown-nav">
<li><a href="/planner">Planner</a></li>
<li><a href="/recipes">Recipes</a></li> <li><a href="/recipes">Recipes</a></li>
<li><a href="/shopping-lists">Shopping Lists</a></li> <li><a href="/shopping-lists">Shopping Lists</a></li>
<li class="uk-nav-header">Logistics</li> <li class="uk-nav-header">Logistics</li>

View File

@ -145,4 +145,95 @@
sql='SELECT conversions.conv_factor FROM test_conversions conversions WHERE part_id = %s, uom_id = %s;') sql='SELECT conversions.conv_factor FROM test_conversions conversions WHERE part_id = %s, uom_id = %s;')
2025-08-10 10:18:03.658146 --- ERROR --- DatabaseError(message='column "part_id" does not existLINE 1: ...nv_factor FROM test_conversions conversions WHERE part_id = ... ^', 2025-08-10 10:18:03.658146 --- ERROR --- DatabaseError(message='column "part_id" does not existLINE 1: ...nv_factor FROM test_conversions conversions WHERE part_id = ... ^',
payload=(2016, 6), payload=(2016, 6),
sql='SELECT conversions.conv_factor FROM test_conversions conversions WHERE part_id = %s AND uom_id = %s;') sql='SELECT conversions.conv_factor FROM test_conversions conversions WHERE part_id = %s AND uom_id = %s;')
2025-08-10 11:11:24.348070 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "test_logistics_info_barcode_key"DETAIL: Key (barcode)=() already exists.',
payload=('', 1, 1, 1, 1),
sql='INSERT INTO test_logistics_info(barcode, primary_location, primary_zone, auto_issue_location, auto_issue_zone) VALUES (%s, %s, %s, %s, %s) RETURNING *;')
2025-08-10 19:30:16.195296 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "test_logistics_info_barcode_key"DETAIL: Key (barcode)=(%PLU0%) already exists.',
payload=('%PLU0%', 1, 1, 1, 1),
sql='INSERT INTO test_logistics_info(barcode, primary_location, primary_zone, auto_issue_location, auto_issue_zone) VALUES (%s, %s, %s, %s, %s) RETURNING *;')
2025-08-10 19:30:27.706841 --- ERROR --- DatabaseError(message='syntax error at or near "gen_random_uuid"LINE 3: event_uuid UUID gen_random_uuid(), ^',
payload=CREATE TABLE IF NOT EXISTS test_plan_events(
id SERIAL PRIMARY KEY,
event_uuid UUID gen_random_uuid(),
plan_uuid UUID,
event_shortname VARCHAR(32) NOT NULL,
event_description TEXT,
event_date TIMESTAMP NOT NULL,
created_by INTEGER NOT NULL,
UNIQUE(event_uuid)
)
,
sql='plan_events')
2025-08-11 17:29:32.899829 --- ERROR --- DatabaseError(message='invalid input syntax for type uuid: ""LINE 3: VALUES ('', 'Mongolian Beef Noodles', 'test', '2025-08-04T00... ^',
payload=('', 'Mongolian Beef Noodles', 'test', datetime.datetime(2025, 8, 4, 0, 0), 1, '1cc556f6-48b6-459f-8a3e-072c6c69ce3a', 'recipe'),
sql='INSERT INTO test_plan_events(plan_uuid, event_shortname, event_description, event_date, created_by, recipe_uuid, event_type) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING *;')
2025-08-11 18:10:54.890311 --- ERROR --- DatabaseError(message='invalid input syntax for type uuid: ""LINE 3: ... WORK YAY!', '2025-08-07T00:00:00'::timestamp, 1, '', 'custo... ^',
payload=(None, 'No Work', 'WE DONT HAVE WORK YAY!', datetime.datetime(2025, 8, 7, 0, 0), 1, '', 'custom'),
sql='INSERT INTO test_plan_events(plan_uuid, event_shortname, event_description, event_date, created_by, recipe_uuid, event_type) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING *;')
2025-08-11 18:47:53.328502 --- ERROR --- DatabaseError(message='relation "main_plan_events" does not existLINE 1: SELECT * FROM main_plan_events ^',
payload=('2025', '8'),
sql='SELECT * FROM main_plan_eventsWHERE EXTRACT(YEAR FROM event_date) = %s AND EXTRACT(MONTH FROM event_date) = %s;')
2025-08-11 18:47:56.885307 --- ERROR --- DatabaseError(message='relation "main_plan_events" does not existLINE 1: SELECT * FROM main_plan_events ^',
payload=('2025', '8'),
sql='SELECT * FROM main_plan_eventsWHERE EXTRACT(YEAR FROM event_date) = %s AND EXTRACT(MONTH FROM event_date) = %s;')
2025-08-11 18:48:24.644410 --- ERROR --- DatabaseError(message='relation "main_plan_events" does not existLINE 1: SELECT * FROM main_plan_events ^',
payload=('2025', '8'),
sql='SELECT * FROM main_plan_eventsWHERE EXTRACT(YEAR FROM event_date) = %s AND EXTRACT(MONTH FROM event_date) = %s;')
2025-08-11 18:52:46.905608 --- ERROR --- DatabaseError(message='relation "main_plan_events" does not existLINE 1: SELECT * FROM main_plan_events ^',
payload=('2025', '8'),
sql='SELECT * FROM main_plan_eventsWHERE EXTRACT(YEAR FROM event_date) = %s AND EXTRACT(MONTH FROM event_date) = %s;')
2025-08-11 18:53:54.266726 --- ERROR --- DatabaseError(message='relation "main_plan_events" does not existLINE 1: SELECT * FROM main_plan_events ^',
payload=('2025', '8'),
sql='SELECT * FROM main_plan_eventsWHERE EXTRACT(YEAR FROM event_date) = %s AND EXTRACT(MONTH FROM event_date) = %s;')
2025-08-12 07:40:16.852940 --- ERROR --- DatabaseError(message='relation "main_plan_events" does not existLINE 1: SELECT * FROM main_plan_events ^',
payload=('2025', '8'),
sql='SELECT * FROM main_plan_eventsWHERE EXTRACT(YEAR FROM event_date) = %s AND EXTRACT(MONTH FROM event_date) = %s;')
2025-08-12 08:43:31.049364 --- ERROR --- DatabaseError(message='invalid input syntax for type uuid: ""LINE 1: ...test 2', event_date = '2025-09-05', recipe_uuid = '', event_... ^',
payload={'uuid': 'c703059d-d03d-4046-866a-64ebfad7f12c', 'update': {'event_shortname': 'Spicy Bacon Chilli', 'event_description': 'test 2', 'event_date': '2025-09-05', 'recipe_uuid': '', 'event_type': 'recipe'}},
sql='UPDATE test_plan_events SET event_shortname = %s, event_description = %s, event_date = %s, recipe_uuid = %s, event_type = %s WHERE event_uuid=%s RETURNING *;')
2025-08-12 09:02:56.840495 --- ERROR --- DatabaseError(message='column "event_date" does not existLINE 2: WHERE EXTRACT(YEAR FROM event_date) = '2025' ^HINT: Perhaps you meant to reference the column "test_plan_events.event_type".',
payload=('2025', '8'),
sql='SELECT * FROM test_plan_eventsWHERE EXTRACT(YEAR FROM event_date) = %s AND EXTRACT(MONTH FROM event_date) = %s;')
2025-08-12 09:44:45.289711 --- ERROR --- DatabaseError(message='relation "main_plan_events" does not existLINE 1: SELECT * FROM main_plan_events ^',
payload=('2025', '8'),
sql='SELECT * FROM main_plan_eventsWHERE EXTRACT(YEAR FROM event_date_start) = %s AND EXTRACT(MONTH FROM event_date_start) = %s;')
2025-08-12 09:44:49.727766 --- ERROR --- DatabaseError(message='relation "main_plan_events" does not existLINE 1: SELECT * FROM main_plan_events ^',
payload=('2025', '8'),
sql='SELECT * FROM main_plan_eventsWHERE EXTRACT(YEAR FROM event_date_start) = %s AND EXTRACT(MONTH FROM event_date_start) = %s;')
2025-08-12 09:58:22.528782 --- ERROR --- DatabaseError(message='argument formats can't be mixed',
payload=('2025', '8'),
sql='/*SELECT * FROM test_plan_eventsWHERE EXTRACT(YEAR FROM event_date_start) = %s AND EXTRACT(MONTH FROM event_date_start) = %s; */SELECT *FROM test_plan_eventsWHERE event_date_start <= (MAKE_DATE(%(year)s, %(month)s, 1) + INTERVAL '1 month' - INTERVAL '1 day')::date AND event_date_end >= MAKE_DATE(%(year)s, %(month)s, 1)')
2025-08-12 10:02:49.084186 --- ERROR --- DatabaseError(message='tuple index out of range',
payload=('2025', '8'),
sql='/*SELECT * FROM test_plan_eventsWHERE EXTRACT(YEAR FROM event_date_start) = %s AND EXTRACT(MONTH FROM event_date_start) = %s; */SELECT *FROM test_plan_eventsWHERE event_date_start <= %s AND event_date_end >= %s')
2025-08-12 10:03:17.111210 --- ERROR --- DatabaseError(message='invalid input syntax for type timestamp: "2025"LINE 5: WHERE event_date_start <= '2025' ^',
payload=('2025', '8'),
sql='SELECT *FROM test_plan_eventsWHERE event_date_start <= %s AND event_date_end >= %s')
2025-08-12 10:04:55.071530 --- ERROR --- DatabaseError(message='operator does not exist: numeric = dateLINE 2: WHERE EXTRACT(YEAR FROM event_date_start) = '2025-08-01'::da... ^HINT: No operator matches the given name and argument types. You might need to add explicit type casts.',
payload=('2025', '8'),
sql='SELECT * FROM test_plan_eventsWHERE EXTRACT(YEAR FROM event_date_start) = %s AND EXTRACT(MONTH FROM event_date_start) = %s;')
2025-08-12 10:09:58.906650 --- ERROR --- DatabaseError(message='missing FROM-clause entry for table "arguments"LINE 4: WHERE EXTRACT(YEAR FROM event_date_start) = arguments.year ^',
payload=('2025', '8'),
sql='WITH arguments AS (SELECT %s AS year, %s AS month)SELECT * FROM test_plan_eventsWHERE EXTRACT(YEAR FROM event_date_start) = arguments.year AND EXTRACT(MONTH FROM event_date_start) = arguments.month;')
2025-08-12 10:10:33.179512 --- ERROR --- DatabaseError(message='operator does not exist: numeric = textLINE 4: WHERE EXTRACT(YEAR FROM event_date_start) = (SELECT year FRO... ^HINT: No operator matches the given name and argument types. You might need to add explicit type casts.',
payload=('2025', '8'),
sql='WITH arguments AS (SELECT %s AS year, %s AS month)SELECT * FROM test_plan_eventsWHERE EXTRACT(YEAR FROM event_date_start) = (SELECT year FROM arguments) AND EXTRACT(MONTH FROM event_date_start) = (SELECT month FROM arguments);')
2025-08-12 10:11:48.528557 --- ERROR --- DatabaseError(message='tuple index out of range',
payload=('2025', '8'),
sql='WITH arguments AS (SELECT %s AS year, %s AS month)WITH arguments AS ( SELECT %s AS year, %s AS month)SELECT *FROM test_plan_eventsWHERE event_date_end >= make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) AND event_date_start < (make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) + INTERVAL '1 month');')
2025-08-12 10:12:03.257652 --- ERROR --- DatabaseError(message='function make_date(text, text, integer) does not existLINE 7: event_date_end >= make_date((SELECT year FROM arguments), ... ^HINT: No function matches the given name and argument types. You might need to add explicit type casts.',
payload=('2025', '8'),
sql='WITH arguments AS ( SELECT %s AS year, %s AS month)SELECT *FROM test_plan_eventsWHERE event_date_end >= make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) AND event_date_start < (make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) + INTERVAL '1 month');')
2025-08-12 11:05:42.431102 --- ERROR --- DatabaseError(message='missing FROM-clause entry for table "main_brands"LINE 5: COALESCE(row_to_json(main_brands.*), "{}") as recipe ^',
payload=(2025, 8),
sql='WITH arguments AS ( SELECT %s AS year, %s AS month)SELECT events.*, COALESCE(row_to_json(main_brands.*), "{}") as recipeFROM main_plan_events eventsLEFT JOIN main_recipes recipes ON recipes.recipe_uuid = events.recipe_uuidWHERE event_date_end >= make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) AND event_date_start < (make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) + INTERVAL '1 month');')
2025-08-12 11:05:56.643860 --- ERROR --- DatabaseError(message='column "{}" does not existLINE 5: COALESCE(row_to_json(recipes.*), "{}") as recipe ^',
payload=(2025, 8),
sql='WITH arguments AS ( SELECT %s AS year, %s AS month)SELECT events.*, COALESCE(row_to_json(recipes.*), "{}") as recipeFROM main_plan_events eventsLEFT JOIN main_recipes recipes ON recipes.recipe_uuid = events.recipe_uuidWHERE event_date_end >= make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) AND event_date_start < (make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) + INTERVAL '1 month');')
2025-08-12 11:09:37.978621 --- ERROR --- DatabaseError(message='column g.rp_id does not existLINE 17: ..._to_json(g)), '{}') FROM cte_recipe_items g WHERE g.rp_id = ... ^',
payload=(2025, 8),
sql='WITH arguments AS ( SELECT %s AS year, %s AS month),sum_cte AS ( SELECT mi.item_uuid, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM main_item_locations mil JOIN main_items mi ON mil.part_id = mi.id GROUP BY mi.id ),cte_recipe_items AS ( SELECT rp_item.qty, COALESCE(sum_cte.total_sum, 0) as quantity_on_hand FROM main_recipe_items rp_item LEFT JOIN sum_cte ON sum_cte.item_uuid = rp_item.item_uuid )SELECT events.*, COALESCE(row_to_json(recipes.*), '{}') as recipe, (SELECT COALESCE(array_agg(row_to_json(g)), '{}') FROM cte_recipe_items g WHERE g.rp_id = recipes.id) AS rp_itemsFROM main_plan_events eventsLEFT JOIN main_recipes recipes ON recipes.recipe_uuid = events.recipe_uuidWHERE event_date_end >= make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) AND event_date_start < (make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) + INTERVAL '1 month');')
2025-08-12 11:13:30.870202 --- ERROR --- DatabaseError(message='missing FROM-clause entry for table "recipe_missing_items"LINE 25: COALESCE(recipe_missing_items.has_missing_ingredients,... ^',
payload=(2025, 8),
sql='WITH arguments AS ( SELECT %s AS year, %s AS month),sum_cte AS ( SELECT mi.item_uuid, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM main_item_locations mil JOIN main_items mi ON mil.part_id = mi.id GROUP BY mi.id ),cte_recipe_items AS ( SELECT rp_item.rp_id, rp_item.qty, COALESCE(sum_cte.total_sum, 0) as quantity_on_hand FROM main_recipe_items rp_item LEFT JOIN sum_cte ON sum_cte.item_uuid = rp_item.item_uuid ), recipe_missing_items AS ( SELECT rp_id, bool_or(qty > quantity_on_hand) AS has_missing_ingredients FROM cte_recipe_items GROUP BY rp_id)SELECT events.*, COALESCE(row_to_json(recipes.*), '{}') as recipe, (SELECT COALESCE(array_agg(row_to_json(g)), '{}') FROM cte_recipe_items g WHERE g.rp_id = recipes.id) AS rp_items, COALESCE(recipe_missing_items.has_missing_ingredients, FALSE) AS has_missing_ingredientsFROM main_plan_events eventsLEFT JOIN main_recipes recipes ON recipes.recipe_uuid = events.recipe_uuidWHERE event_date_end >= make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) AND event_date_start < (make_date((SELECT year FROM arguments), (SELECT month FROM arguments), 1) + INTERVAL '1 month');')

319
plu_items.csv Normal file
View File

@ -0,0 +1,319 @@
item_name,description
Bananas,A bunch of ripe yellow bananas
Bananas Organic,Organic ripe yellow bananas
Apples Red Delicious,Classic sweet red apples
Apples Granny Smith,Tart green apples for baking and eating
Apples Fuji,Extra sweet crisp apples
Apples Gala,Medium small mildly sweet apple
Apples Honeycrisp,Juicy crisp and sweet apples
Apples Golden Delicious,Yellow-skinned sweet apples
Apples Pink Lady,Blush pink sweet-tart apple
Oranges Navel,Easy-to-peel seedless orange
Oranges Valencia,Juicy great for orange juice
Oranges Cara Cara,Pink-fleshed navel orange
Oranges Blood,Deep red-fleshed orange
Tangerines,Small easy-to-peel orange citrus
Clementines,Seedless mandarin oranges
Mandarins,Sweet citrus similar to clementines
Grapefruit Pink,Pink fleshed juicy grapefruit
Grapefruit White,Traditional tart grapefruit
Pomelo,Large fragrant ancestor of the grapefruit
Lemons,Traditional bright yellow lemons
Limes,Standard tart green limes
Key Limes,Small round extra tangy limes
Kumquats,Tiny citrus eaten whole rind and all
Pineapple,Juicy tropical fruit with spiky skin
Mango Tommy Atkins,Firm mild mango
Mango Ataulfo,Small yellow creamy mango
Mango Kent, Juicy green-red mango
Papaya,Honey-sweet tropical fruit with seeds
Dragonfruit,Exotic cactus fruit white or red flesh
Passionfruit,Round aromatic fruit with edible seeds
Guava,Sweet fragrant tropical fruit
Starfruit,Yellow crisp star-shaped when sliced
Avocados Hass,Classic creamy pebbly-skinned avocado
Avocados Fuerte,Smooth-skinned green avocado
Peaches,Juicy fuzzy-skinned summer fruit
Nectarines,Peach-like fruit with smooth skin
Plums,Juicy tart stone fruit
Pluots,Plum-apricot hybrid
Apricots,Small orange stone fruit
Cherries Sweet,Bing and similar red sweet cherry
Cherries Tart,Montmorency and pie cherries
Grapes Red Seedless,Classic sweet eating grape
Grapes Green Seedless,Larger green sweet grape
Grapes Black Seedless,Dark colored sweet grape
Raisins Dried Grapes,Classic dried sweet grapes
Strawberries,Red fragrant berries
Blueberries,Small round blue berries
Raspberries,Delicate tart red berries
Blackberries,Darker juicy tart berries
Cranberries,Small tart red berry
Gooseberries,Tart small usually green berries
Currants,Red black or white tart berries
Melon Watermelon,Large red pink melon with seeds
Melon Watermelon Seedless,Red sweet almost no seeds
Melon Cantaloupe,Orange-fleshed webbed melon
Melon Honeydew,Green-fleshed smooth-skinned melon
Melon Galia,Fragrant light green-flesh melon
Melon Canary,Bright yellow sweet melon
Melon Crenshaw,Sweet juicy hybrid melon
Pomegranate,Ruby red seeds inside a hard fruit
Figs Brown Turkey,Soft brownish edible fig
Figs Black Mission,Intensely sweet black fig
Dates Medjool,Large soft sweet dried date
Dates Deglet Noor,Smaller sweet dried date
Kiwi,Zesty green-flesh fuzzy brown
Kiwi Gold,Smooth-skinned yellow-flesh kiwi
Persimmons Fuyu,Orange eaten firm like apple
Persimmons Hachiya,Longer extremely astringent if unripe
Lychee,Small white fragrant fruit red skin
Rambutan,Hairy red Asian lychee relative
Longan,Sweet mild translucent Asian fruit
Jackfruit,Very large tropical fruit yellow flesh
Sapote Black,Chocolate pudding fruit
Soursop,Green spiky soft white tropical fruit
Tamarind,Brown pod tangy sweet-sour pulp
Breadfruit,Starchy Polynesian staple fruit
Squash Zucchini,Standard green summer squash
Squash Yellow,Yellow-skinned variety of squash
Squash Acorn,Small ribbed sweet-fleshed winter squash
Squash Butternut,Tan bell-shaped orange-fleshed winter squash
Squash Spaghetti,Pale yellow stringy flesh
Squash Delicata,Oblong sweet edible-skin
Pumpkin, Orange winter squash often large
Sweet Corn,Golden corn on the cob
Snap Peas,Edible sweet crunchy pods
Snow Peas,Flat tender often used in stir fries
Green Beans,String beans long and crispy
Fava Beans,Large flat green beans
Lima Beans,Flattened creamy bean
Peas English/fresh,Green round peas in a pod
Okra,Fuzzy green pods used in gumbo
Eggplant Globe,Large dark purple eggplant
Eggplant Italian,Small oval eggplant
Eggplant Japanese,Slender tender purple eggplant
Bell Pepper Red, Sweet juicy pepper
Bell Pepper Green, Classic crisp pepper
Bell Pepper Yellow,Sweet mild yellow pepper
Bell Pepper Orange,Sweet mild orange
Bell Pepper Purple,Rarer mild purple variety
Hot Pepper Jalapeño,Classic green spicy pepper
Hot Pepper Serrano,Thinner spicier pepper
Hot Pepper Habanero,Very hot orange pepper
Hot Pepper Anaheim,Mild light green pepper
Hot Pepper Poblano,Dark green mild pepper
Hot Pepper Thai Chili,Small very hot red/green
Hot Pepper Fresno,Red mild-medium looks like jalapeño
Hot Pepper Scotch Bonnet,Similar heat to habanero
Tomatoes Beefsteak,Jumbo sized great for slicing
Tomatoes Roma,Plum-shaped meaty for sauces
Tomatoes Cherry,Small round sweet tomatoes
Tomatoes Grape,Oblong bite-sized tomatoes
Tomatoes Heirloom,Old multi-colored flavorful
Tomatillos,Green paper-husked tart
Potatoes Russet,Large brown-skinned baking potato
Potatoes Yukon Gold,Yellow-fleshed buttery potato
Potatoes Red,Waxier bright red skin
Potatoes Fingerling,Mini elongated gourmet potato
Potatoes Purple,Blue-purple skinned and fleshed
Sweet Potatoes Orange,Classic moist sweet potato
Sweet Potatoes White,Palest orange or yellow
Yams,Often confused with sweet potatoes
Onion Yellow,Classic brown skin all-purpose
Onion White,Sharply flavored white onion
Onion Red,Strong purple skin often raw
Onion Vidalia,Sweet tender yellow onion
Onion Shallot,Fancy small mild flavor
Green Onions,Scallions mild green-topped
Leeks,Large tender white and green stalks
Chives,Thin mild green herb
Garlic,Pungent flavored bulbs
Garlic Elephant,Giant garlic with mild flavor
Radish,Crunchy red and white root
Daikon,Large white Asian radish
Beets Red,Classic earthy root sweet when cooked
Beets Golden,Milder yellow-skinned beet
Turnip,Purple-topped white root
Rutabaga,Large sweet yellow root
Parsnip,White sweet carrot-like root
Carrots Orange,Classic orange root
Carrots Rainbow,Bunches of various colored carrots
Celery,Stalks with crisp texture
Celery Root,Celeriac knobby root with celery flavor
Bok Choy,Asian green with white stalks
Napa Cabbage,Soft-leaved Chinese cabbage
Cabbage Green,Tight-headed green cabbage
Cabbage Red,Red/purple fine-leaved cabbage
Brussels Sprouts,Little round green buds
Kale Curly,Frilly green leaves popular in salads
Kale Lacinato,Firm blue-green dinosaur kale
Mustard Greens,Spicy frilly salad green
Collard Greens,Large dark green leathery leaves
Spinach,Small mild tender leaves
Swiss Chard,Colorful leafy vegetable
Arugula,Peppery leafy fun salad green
Lettuce Iceberg,Classic crisp pale green lettuce
Lettuce Romaine,Upright green perfect for Caesar
Lettuce Green Leaf,Soft fresh green leaves
Lettuce Red Leaf,Tender burgundy-green leaves
Escarole,Bitter broad more robust salad green
Endive,Curly bitter light green
Radicchio,Small burgundy bitter lettuce-like
Basil,Fragrant green Italian herb
Cilantro (Coriander),Fresh bright citrusy herb
Dill,Feathery aromatic often with pickles
Flat-leaf Parsley,Bright clean flavored herb
Curly Parsley,Classic garnish fluffy
Oregano,Earthy bold Mediterranean seasoning
Rosemary,Piney robust herb
Sage,Gray-green leaves strong flavor
Mint,Cool bright refreshing herb
Thyme,Tiny-leaved savory fragrant herb
Sorrel,Lemony spinach-like herb green
Tarragon,Anise-flavored cooking herb
Watercress,Peppery green leafy herb
Mushrooms White Button,Classic round mushrooms
Mushrooms Cremini,Deeper brown firmer
Mushrooms Portobello,Large brown mature mushrooms
Mushrooms Shiitake,Wide-capped Japanese mushroom
Mushrooms Oyster,Soft-fleshed cluster mushrooms
Mushrooms Enoki,Tiny white-capped
Seaweed,Nori sheets or kelp
Sprouts Alfalfa,Tender crunchy tiny sprout greens
Sprouts Bean,White crisp bean sprouts
Sprouts Broccoli,Nutritious peppery sprout
Artichokes,Tender green thistly Mediterranean buds
Asparagus,Green or white tender spears
Fennel,White bulb with fronds licorice flavor
Jicama,Large round sweet crunchy root
Kohlrabi,Baseball-shaped mild crunchy veggie
Okra,Gumbo vegetable with fuzzy pods
Lotus Root,Edible crisp root with holes
Turmeric root,Small orange potent-anti-inflammatory
Ginger root,Spicy knobby aromatic root
Horseradish root,Strong white spicy root
Sunchokes,Crunchy nutty Jerusalem artichoke
Burdock root,Long brown earthy-Japanese vegetable
Edamame,Immature soybeans in the pod
Plantains,Starchy cooking banana
Yuca (Cassava),Tropical brown starchy tuber
Taro root,Purple-tinged starchy root
Beef Ribeye Steak, Well-marbled boneless steak from rib section
Beef Rib Steak,Bone-in steak from rib section
Beef Tenderloin/Filet Mignon,Lean tender boneless steak from loin
Beef Sirloin Steak,Flavorful steak from rear back portion
Beef Top Sirloin,Lean sirloin cut for grilling or roasting
Beef Flank Steak,Long flat cut good for fajitas or stir fry
Beef Skirt Steak,Long thin cut best for grilling and marinating
Beef Strip Steak (NY Strip),Boneless steak from short loin well-marbled
Beef T-Bone Steak,Iconic bone-in steak with strip and tenderloin
Beef Porterhouse Steak,Larger T-bone with bigger tenderloin section
Beef Round Steak,Lean boneless steak from back leg
Beef Chuck Roast,Well-marbled cut ideal for slow cooking
Beef Brisket,Flat fatty cut ideal for barbecue or braising
Beef Short Ribs,Rich bone-in pieces ideal for slow roasting or braising
Beef Cross Rib Roast,Flavorsome roast cut
Beef Stew Meat,Cubed beef for stews
Beef Ground Beef 80/20,Standard ground beef for burgers and tacos
Beef Ground Beef 90/10,Lean ground beef
Beef Shank,Meaty cross-cut beef leg slice for soups and osso buco
Beef Oxtail,Bony flavorful ox tail cuts for stews
Beef Tri-Tip,Triangular roast popular for grilling or roasting
Beef Rump Roast,Leaner roast from the round
Beef Eye of Round Roast,"Lean, dense, boneless roast from the round"
Beef Top Round Steak,Lean steak for marinating or broiling
Beef Back Ribs,Beef ribs from rib section
Pork Loin Roast,Bone-in or boneless loin roast
Pork Loin Chops,Lean chops from the loin
Pork Rib Chops,Rib bone-in chops from the loin
Pork Center Cut Chops,Boneless or bone-in thick cut
Pork Tenderloin,Small tender boneless roast
Pork Shoulder Roast (Boston Butt),Marbled roast for pulled pork
Pork Picnic Roast,Oval-shaped cut from lower shoulder
Pork Spare Ribs,Large flat ribs from the belly side
Pork Baby Back Ribs,Short curved ribs from the back
Pork Country Style Ribs,Meaty ribs with more meat than bone
Pork St. Louis Ribs,Trimmed spare ribs
Pork Belly,Uncured slab used for bacon
Pork Ham,Fully cooked or fresh leg portion
Pork Fresh Ham,Uncured leg portion
Pork Smoked Ham,Smoked and cured pork leg
Pork Ground Pork,Ground meat from pork
Pork Sausage,Ground pork seasoned and formed into links or patties
Pork Hocks (Ham Hocks),Meaty lower leg cut used for soups
Pork Spareribs,Tender ribs from undersurface of ribs
Pork Shoulder Steak,Flavorful marbled steak for grilling
Pork Salt Pork,Heavily salted fatty pork cut
Pork Fatback,Layer of pork fat from the back
Lamb Loin Chop,Small bone-in tender chop
Lamb Rib Chop,Rib-section bone-in cut
Lamb Leg Roast,Bone-in or boneless roast from leg
Lamb Shoulder Chop,Well-flavored less tender cut
Lamb Rack,Full rib section for roasting and chops
Lamb Shank,Meaty lower leg cut excellent for braising
Lamb Ground Lamb,Ground lamb for burgers or kebabs
Lamb Stew Meat,Cubed lamb for stews
Lamb Sirloin Chop,Boneless chop from leg/top sirloin
Lamb Neck Slices,Collagen-rich cut for stews and braises
Lamb Breast,Flatter flavorful section of rib and breast
Chicken Whole Chicken,Raw whole ready-to-cook chicken
Chicken Breast Boneless Skinless,Most popular white meat cut
Chicken Breast Bone-In,Moist white meat with rib bone
Chicken Tenders,Thin strips from under breast
Chicken Thighs Boneless Skinless,Juicy flavorful dark meat
Chicken Thighs Bone-In Juicy,thigh portion of chicken leg
Chicken Drumsticks,Lower leg portion great for frying and baking
Chicken Wings,Popular for appetizers and barbecues
Chicken Legs,Whole leg (drumstick and thigh attached)
Chicken Split Breast,Bone-in white meat cut in half
Chicken Back,Used for stock and soup
Chicken Liver,Edible poultry organ for pâté or sautéing
Chicken Gizzards,Muscular stomach popular fried or sautéed
Chicken Hearts,Small organ popular grilled or stewed
Turkey Whole Turkey,Whole ready-to-cook turkey
Turkey Breast,Large boneless or bone-in white meat cut
Turkey Thighs,Juicy dark meat portion
Turkey Drumsticks,Large flavorful portion for roasting
Turkey Wings,Meaty for roasting or stock
Turkey Ground,Ground white and dark turkey meat
Duck Whole Duck,Whole ready-to-cook duck
Duck Breast,Breast portion with rich flavor
Duck Legs,Leg quarter with dark rich flavor
Duck Wings,For roasting or confit
Duck Confit,Legs cooked slowly in duck fat
Venison Steaks,Boneless wild game steak
Venison Roast,Large cut for roasting
Venison Ground,Ground wild game meat
Venison Sausage,Wild game sausage links
Bison Steak,Lean boneless steak
Bison Ground,Ground lean bison
Rabbit Whole Rabbit,Ready-to-cook whole rabbit
Rabbit Legs,Dark meat for braising or stewing
Pheasant Whole Pheasant,Game bird for roasting
Quail Whole Quail,Small game bird for roasting/braising
Goose Whole Goose,Rich dark-meat poultry for roasting
Salmon Fillet,Boned side of salmon skin on or off
Salmon Steak,Cut end-to-end with backbone
Trout Whole,Whole raw trout
Trout Fillets,Filleted sides of trout
Cod Fillet,Boneless flaky white fish fillet
Halibut Steak,Thick cross-section cut from halibut
Tilapia Fillet,Mild boneless white fish
Snapper Fillet,Boneless fillet from snapper
Catfish Fillet,Mild boneless fillet
Swordfish Steak,Thick firm boneless steak
Mahi Mahi Fillet,Lean flavorful white fish
Tuna Steak,Firm meaty typically grilled
Scallops,Meaty adductor muscle of shellfish
Shrimp Raw,Headless shell-on or peeled raw shrimp
Shrimp Cooked,Precooked shrimp tail-on or off
Crab Whole,Live or cooked whole crab
Crab Legs,Steamed or frozen crab legs
Lobster Whole,Live lobster
Lobster Tail,Meaty tail section of lobster
Oysters,Fresh or shucked
Clams,Fresh or shucked
Mussels,Whole fresh blue or green mussels
Octopus,Fresh or cleaned octopus
Squid,Fresh or cleaned squid (calamari)
1 item_name description
2 Bananas A bunch of ripe yellow bananas
3 Bananas Organic Organic ripe yellow bananas
4 Apples Red Delicious Classic sweet red apples
5 Apples Granny Smith Tart green apples for baking and eating
6 Apples Fuji Extra sweet crisp apples
7 Apples Gala Medium small mildly sweet apple
8 Apples Honeycrisp Juicy crisp and sweet apples
9 Apples Golden Delicious Yellow-skinned sweet apples
10 Apples Pink Lady Blush pink sweet-tart apple
11 Oranges Navel Easy-to-peel seedless orange
12 Oranges Valencia Juicy great for orange juice
13 Oranges Cara Cara Pink-fleshed navel orange
14 Oranges Blood Deep red-fleshed orange
15 Tangerines Small easy-to-peel orange citrus
16 Clementines Seedless mandarin oranges
17 Mandarins Sweet citrus similar to clementines
18 Grapefruit Pink Pink fleshed juicy grapefruit
19 Grapefruit White Traditional tart grapefruit
20 Pomelo Large fragrant ancestor of the grapefruit
21 Lemons Traditional bright yellow lemons
22 Limes Standard tart green limes
23 Key Limes Small round extra tangy limes
24 Kumquats Tiny citrus eaten whole rind and all
25 Pineapple Juicy tropical fruit with spiky skin
26 Mango Tommy Atkins Firm mild mango
27 Mango Ataulfo Small yellow creamy mango
28 Mango Kent Juicy green-red mango
29 Papaya Honey-sweet tropical fruit with seeds
30 Dragonfruit Exotic cactus fruit white or red flesh
31 Passionfruit Round aromatic fruit with edible seeds
32 Guava Sweet fragrant tropical fruit
33 Starfruit Yellow crisp star-shaped when sliced
34 Avocados Hass Classic creamy pebbly-skinned avocado
35 Avocados Fuerte Smooth-skinned green avocado
36 Peaches Juicy fuzzy-skinned summer fruit
37 Nectarines Peach-like fruit with smooth skin
38 Plums Juicy tart stone fruit
39 Pluots Plum-apricot hybrid
40 Apricots Small orange stone fruit
41 Cherries Sweet Bing and similar red sweet cherry
42 Cherries Tart Montmorency and pie cherries
43 Grapes Red Seedless Classic sweet eating grape
44 Grapes Green Seedless Larger green sweet grape
45 Grapes Black Seedless Dark colored sweet grape
46 Raisins Dried Grapes Classic dried sweet grapes
47 Strawberries Red fragrant berries
48 Blueberries Small round blue berries
49 Raspberries Delicate tart red berries
50 Blackberries Darker juicy tart berries
51 Cranberries Small tart red berry
52 Gooseberries Tart small usually green berries
53 Currants Red black or white tart berries
54 Melon Watermelon Large red pink melon with seeds
55 Melon Watermelon Seedless Red sweet almost no seeds
56 Melon Cantaloupe Orange-fleshed webbed melon
57 Melon Honeydew Green-fleshed smooth-skinned melon
58 Melon Galia Fragrant light green-flesh melon
59 Melon Canary Bright yellow sweet melon
60 Melon Crenshaw Sweet juicy hybrid melon
61 Pomegranate Ruby red seeds inside a hard fruit
62 Figs Brown Turkey Soft brownish edible fig
63 Figs Black Mission Intensely sweet black fig
64 Dates Medjool Large soft sweet dried date
65 Dates Deglet Noor Smaller sweet dried date
66 Kiwi Zesty green-flesh fuzzy brown
67 Kiwi Gold Smooth-skinned yellow-flesh kiwi
68 Persimmons Fuyu Orange eaten firm like apple
69 Persimmons Hachiya Longer extremely astringent if unripe
70 Lychee Small white fragrant fruit red skin
71 Rambutan Hairy red Asian lychee relative
72 Longan Sweet mild translucent Asian fruit
73 Jackfruit Very large tropical fruit yellow flesh
74 Sapote Black Chocolate pudding fruit
75 Soursop Green spiky soft white tropical fruit
76 Tamarind Brown pod tangy sweet-sour pulp
77 Breadfruit Starchy Polynesian staple fruit
78 Squash Zucchini Standard green summer squash
79 Squash Yellow Yellow-skinned variety of squash
80 Squash Acorn Small ribbed sweet-fleshed winter squash
81 Squash Butternut Tan bell-shaped orange-fleshed winter squash
82 Squash Spaghetti Pale yellow stringy flesh
83 Squash Delicata Oblong sweet edible-skin
84 Pumpkin Orange winter squash often large
85 Sweet Corn Golden corn on the cob
86 Snap Peas Edible sweet crunchy pods
87 Snow Peas Flat tender often used in stir fries
88 Green Beans String beans long and crispy
89 Fava Beans Large flat green beans
90 Lima Beans Flattened creamy bean
91 Peas English/fresh Green round peas in a pod
92 Okra Fuzzy green pods used in gumbo
93 Eggplant Globe Large dark purple eggplant
94 Eggplant Italian Small oval eggplant
95 Eggplant Japanese Slender tender purple eggplant
96 Bell Pepper Red Sweet juicy pepper
97 Bell Pepper Green Classic crisp pepper
98 Bell Pepper Yellow Sweet mild yellow pepper
99 Bell Pepper Orange Sweet mild orange
100 Bell Pepper Purple Rarer mild purple variety
101 Hot Pepper Jalapeño Classic green spicy pepper
102 Hot Pepper Serrano Thinner spicier pepper
103 Hot Pepper Habanero Very hot orange pepper
104 Hot Pepper Anaheim Mild light green pepper
105 Hot Pepper Poblano Dark green mild pepper
106 Hot Pepper Thai Chili Small very hot red/green
107 Hot Pepper Fresno Red mild-medium looks like jalapeño
108 Hot Pepper Scotch Bonnet Similar heat to habanero
109 Tomatoes Beefsteak Jumbo sized great for slicing
110 Tomatoes Roma Plum-shaped meaty for sauces
111 Tomatoes Cherry Small round sweet tomatoes
112 Tomatoes Grape Oblong bite-sized tomatoes
113 Tomatoes Heirloom Old multi-colored flavorful
114 Tomatillos Green paper-husked tart
115 Potatoes Russet Large brown-skinned baking potato
116 Potatoes Yukon Gold Yellow-fleshed buttery potato
117 Potatoes Red Waxier bright red skin
118 Potatoes Fingerling Mini elongated gourmet potato
119 Potatoes Purple Blue-purple skinned and fleshed
120 Sweet Potatoes Orange Classic moist sweet potato
121 Sweet Potatoes White Palest orange or yellow
122 Yams Often confused with sweet potatoes
123 Onion Yellow Classic brown skin all-purpose
124 Onion White Sharply flavored white onion
125 Onion Red Strong purple skin often raw
126 Onion Vidalia Sweet tender yellow onion
127 Onion Shallot Fancy small mild flavor
128 Green Onions Scallions mild green-topped
129 Leeks Large tender white and green stalks
130 Chives Thin mild green herb
131 Garlic Pungent flavored bulbs
132 Garlic Elephant Giant garlic with mild flavor
133 Radish Crunchy red and white root
134 Daikon Large white Asian radish
135 Beets Red Classic earthy root sweet when cooked
136 Beets Golden Milder yellow-skinned beet
137 Turnip Purple-topped white root
138 Rutabaga Large sweet yellow root
139 Parsnip White sweet carrot-like root
140 Carrots Orange Classic orange root
141 Carrots Rainbow Bunches of various colored carrots
142 Celery Stalks with crisp texture
143 Celery Root Celeriac knobby root with celery flavor
144 Bok Choy Asian green with white stalks
145 Napa Cabbage Soft-leaved Chinese cabbage
146 Cabbage Green Tight-headed green cabbage
147 Cabbage Red Red/purple fine-leaved cabbage
148 Brussels Sprouts Little round green buds
149 Kale Curly Frilly green leaves popular in salads
150 Kale Lacinato Firm blue-green dinosaur kale
151 Mustard Greens Spicy frilly salad green
152 Collard Greens Large dark green leathery leaves
153 Spinach Small mild tender leaves
154 Swiss Chard Colorful leafy vegetable
155 Arugula Peppery leafy fun salad green
156 Lettuce Iceberg Classic crisp pale green lettuce
157 Lettuce Romaine Upright green perfect for Caesar
158 Lettuce Green Leaf Soft fresh green leaves
159 Lettuce Red Leaf Tender burgundy-green leaves
160 Escarole Bitter broad more robust salad green
161 Endive Curly bitter light green
162 Radicchio Small burgundy bitter lettuce-like
163 Basil Fragrant green Italian herb
164 Cilantro (Coriander) Fresh bright citrusy herb
165 Dill Feathery aromatic often with pickles
166 Flat-leaf Parsley Bright clean flavored herb
167 Curly Parsley Classic garnish fluffy
168 Oregano Earthy bold Mediterranean seasoning
169 Rosemary Piney robust herb
170 Sage Gray-green leaves strong flavor
171 Mint Cool bright refreshing herb
172 Thyme Tiny-leaved savory fragrant herb
173 Sorrel Lemony spinach-like herb green
174 Tarragon Anise-flavored cooking herb
175 Watercress Peppery green leafy herb
176 Mushrooms White Button Classic round mushrooms
177 Mushrooms Cremini Deeper brown firmer
178 Mushrooms Portobello Large brown mature mushrooms
179 Mushrooms Shiitake Wide-capped Japanese mushroom
180 Mushrooms Oyster Soft-fleshed cluster mushrooms
181 Mushrooms Enoki Tiny white-capped
182 Seaweed Nori sheets or kelp
183 Sprouts Alfalfa Tender crunchy tiny sprout greens
184 Sprouts Bean White crisp bean sprouts
185 Sprouts Broccoli Nutritious peppery sprout
186 Artichokes Tender green thistly Mediterranean buds
187 Asparagus Green or white tender spears
188 Fennel White bulb with fronds licorice flavor
189 Jicama Large round sweet crunchy root
190 Kohlrabi Baseball-shaped mild crunchy veggie
191 Okra Gumbo vegetable with fuzzy pods
192 Lotus Root Edible crisp root with holes
193 Turmeric root Small orange potent-anti-inflammatory
194 Ginger root Spicy knobby aromatic root
195 Horseradish root Strong white spicy root
196 Sunchokes Crunchy nutty Jerusalem artichoke
197 Burdock root Long brown earthy-Japanese vegetable
198 Edamame Immature soybeans in the pod
199 Plantains Starchy cooking banana
200 Yuca (Cassava) Tropical brown starchy tuber
201 Taro root Purple-tinged starchy root
202 Beef Ribeye Steak Well-marbled boneless steak from rib section
203 Beef Rib Steak Bone-in steak from rib section
204 Beef Tenderloin/Filet Mignon Lean tender boneless steak from loin
205 Beef Sirloin Steak Flavorful steak from rear back portion
206 Beef Top Sirloin Lean sirloin cut for grilling or roasting
207 Beef Flank Steak Long flat cut good for fajitas or stir fry
208 Beef Skirt Steak Long thin cut best for grilling and marinating
209 Beef Strip Steak (NY Strip) Boneless steak from short loin well-marbled
210 Beef T-Bone Steak Iconic bone-in steak with strip and tenderloin
211 Beef Porterhouse Steak Larger T-bone with bigger tenderloin section
212 Beef Round Steak Lean boneless steak from back leg
213 Beef Chuck Roast Well-marbled cut ideal for slow cooking
214 Beef Brisket Flat fatty cut ideal for barbecue or braising
215 Beef Short Ribs Rich bone-in pieces ideal for slow roasting or braising
216 Beef Cross Rib Roast Flavorsome roast cut
217 Beef Stew Meat Cubed beef for stews
218 Beef Ground Beef 80/20 Standard ground beef for burgers and tacos
219 Beef Ground Beef 90/10 Lean ground beef
220 Beef Shank Meaty cross-cut beef leg slice for soups and osso buco
221 Beef Oxtail Bony flavorful ox tail cuts for stews
222 Beef Tri-Tip Triangular roast popular for grilling or roasting
223 Beef Rump Roast Leaner roast from the round
224 Beef Eye of Round Roast Lean, dense, boneless roast from the round
225 Beef Top Round Steak Lean steak for marinating or broiling
226 Beef Back Ribs Beef ribs from rib section
227 Pork Loin Roast Bone-in or boneless loin roast
228 Pork Loin Chops Lean chops from the loin
229 Pork Rib Chops Rib bone-in chops from the loin
230 Pork Center Cut Chops Boneless or bone-in thick cut
231 Pork Tenderloin Small tender boneless roast
232 Pork Shoulder Roast (Boston Butt) Marbled roast for pulled pork
233 Pork Picnic Roast Oval-shaped cut from lower shoulder
234 Pork Spare Ribs Large flat ribs from the belly side
235 Pork Baby Back Ribs Short curved ribs from the back
236 Pork Country Style Ribs Meaty ribs with more meat than bone
237 Pork St. Louis Ribs Trimmed spare ribs
238 Pork Belly Uncured slab used for bacon
239 Pork Ham Fully cooked or fresh leg portion
240 Pork Fresh Ham Uncured leg portion
241 Pork Smoked Ham Smoked and cured pork leg
242 Pork Ground Pork Ground meat from pork
243 Pork Sausage Ground pork seasoned and formed into links or patties
244 Pork Hocks (Ham Hocks) Meaty lower leg cut used for soups
245 Pork Spareribs Tender ribs from undersurface of ribs
246 Pork Shoulder Steak Flavorful marbled steak for grilling
247 Pork Salt Pork Heavily salted fatty pork cut
248 Pork Fatback Layer of pork fat from the back
249 Lamb Loin Chop Small bone-in tender chop
250 Lamb Rib Chop Rib-section bone-in cut
251 Lamb Leg Roast Bone-in or boneless roast from leg
252 Lamb Shoulder Chop Well-flavored less tender cut
253 Lamb Rack Full rib section for roasting and chops
254 Lamb Shank Meaty lower leg cut excellent for braising
255 Lamb Ground Lamb Ground lamb for burgers or kebabs
256 Lamb Stew Meat Cubed lamb for stews
257 Lamb Sirloin Chop Boneless chop from leg/top sirloin
258 Lamb Neck Slices Collagen-rich cut for stews and braises
259 Lamb Breast Flatter flavorful section of rib and breast
260 Chicken Whole Chicken Raw whole ready-to-cook chicken
261 Chicken Breast Boneless Skinless Most popular white meat cut
262 Chicken Breast Bone-In Moist white meat with rib bone
263 Chicken Tenders Thin strips from under breast
264 Chicken Thighs Boneless Skinless Juicy flavorful dark meat
265 Chicken Thighs Bone-In Juicy thigh portion of chicken leg
266 Chicken Drumsticks Lower leg portion great for frying and baking
267 Chicken Wings Popular for appetizers and barbecues
268 Chicken Legs Whole leg (drumstick and thigh attached)
269 Chicken Split Breast Bone-in white meat cut in half
270 Chicken Back Used for stock and soup
271 Chicken Liver Edible poultry organ for pâté or sautéing
272 Chicken Gizzards Muscular stomach popular fried or sautéed
273 Chicken Hearts Small organ popular grilled or stewed
274 Turkey Whole Turkey Whole ready-to-cook turkey
275 Turkey Breast Large boneless or bone-in white meat cut
276 Turkey Thighs Juicy dark meat portion
277 Turkey Drumsticks Large flavorful portion for roasting
278 Turkey Wings Meaty for roasting or stock
279 Turkey Ground Ground white and dark turkey meat
280 Duck Whole Duck Whole ready-to-cook duck
281 Duck Breast Breast portion with rich flavor
282 Duck Legs Leg quarter with dark rich flavor
283 Duck Wings For roasting or confit
284 Duck Confit Legs cooked slowly in duck fat
285 Venison Steaks Boneless wild game steak
286 Venison Roast Large cut for roasting
287 Venison Ground Ground wild game meat
288 Venison Sausage Wild game sausage links
289 Bison Steak Lean boneless steak
290 Bison Ground Ground lean bison
291 Rabbit Whole Rabbit Ready-to-cook whole rabbit
292 Rabbit Legs Dark meat for braising or stewing
293 Pheasant Whole Pheasant Game bird for roasting
294 Quail Whole Quail Small game bird for roasting/braising
295 Goose Whole Goose Rich dark-meat poultry for roasting
296 Salmon Fillet Boned side of salmon skin on or off
297 Salmon Steak Cut end-to-end with backbone
298 Trout Whole Whole raw trout
299 Trout Fillets Filleted sides of trout
300 Cod Fillet Boneless flaky white fish fillet
301 Halibut Steak Thick cross-section cut from halibut
302 Tilapia Fillet Mild boneless white fish
303 Snapper Fillet Boneless fillet from snapper
304 Catfish Fillet Mild boneless fillet
305 Swordfish Steak Thick firm boneless steak
306 Mahi Mahi Fillet Lean flavorful white fish
307 Tuna Steak Firm meaty typically grilled
308 Scallops Meaty adductor muscle of shellfish
309 Shrimp Raw Headless shell-on or peeled raw shrimp
310 Shrimp Cooked Precooked shrimp tail-on or off
311 Crab Whole Live or cooked whole crab
312 Crab Legs Steamed or frozen crab legs
313 Lobster Whole Live lobster
314 Lobster Tail Meaty tail section of lobster
315 Oysters Fresh or shucked
316 Clams Fresh or shucked
317 Mussels Whole fresh blue or green mussels
318 Octopus Fresh or cleaned octopus
319 Squid Fresh or cleaned squid (calamari)

View File

@ -2,10 +2,11 @@
--background: #121212; --background: #121212;
--background-text: #ffffff; --background-text: #ffffff;
--surface: #121212; --surface: #181818;
--surface-text: #ffffff; --surface-text: #ffffff;
--surface-two: #252525; --surface-two: #252525;
--surface-three: #383838;
--error: #CF6679; --error: #CF6679;
--error-text: #000000; --error-text: #000000;
@ -251,4 +252,128 @@ select, option {
.uk-button.uk-button-danger { .uk-button.uk-button-danger {
background-color: var(--error); background-color: var(--error);
color: var(--error-text); color: var(--error-text);
}
/* Darkmode settings for planner plage */
#calendar_container {
background-color: var(--surface);
box-shadow: var(--elevation-high);
border-radius: var(--radius);
}
#calender_table {
background-color: var(--surface);
}
#calender_table th{
background-color: var(--surface);
font-weight: bold;
}
.calendar-cell-empty {
background-color: var(--surface);
border: 1px solid var(--surface-three);
}
.calendar-cell {
background-color: var(--surface-two);
border: 1px solid var(--surface-three);
position: relative;
width: 150px;
height: 120px;
vertical-align: top;
padding: 5px;
margin: 5px;
}
.calendar-cell:hover{
background-color: var(--surface-two);
border: 2px solid var(--primary-color);
}
.calendar-cell:hover, .calendar-cell-selected{
background-color: var(--surface-two);
border: 2px solid var(--primary-color);
}
.date-box {
width: 25px;
height: 25px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 5px;
top: 5px;
z-index: 2;
}
.recipes-box {
position: absolute;
top: 5px;
left: 35px;
right: 5px;
bottom: 5px;
padding: 3px;
overflow: auto;
z-index: 1;
}
.recipe-label.recipe-success {
background:rgba(158, 221, 145, 0.479);
margin-bottom: 3px;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.recipe-label.recipe-error {
background-color: rgba(218, 143, 143, 0.425);
margin-bottom: 3px;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.recipe-label.recipe-success:hover{
background-color: rgba(158, 221, 145, 0.185);
cursor: pointer;
}
.recipe-label.recipe-success:hover, .recipe-label-selected {
background-color: rgba(158, 221, 145, 0.185);
}
.recipe-label.recipe-error:hover{
background-color:rgba(218, 143, 143, 0.185);
cursor: pointer;
}
.recipe-label.recipe-error:hover, .recipe-label-selected {
background-color: rgba(218, 143, 143, 0.185);
}
.custom-label {
background:rgba(211, 211, 211, 0.459);
margin-bottom: 3px;
padding: 2px 5px;
border-radius: 1px;
font-size: 12px;
font-weight: bold;
}
.custom-label:hover{
background-color: rgba(211, 211, 211, 0.185);
cursor: pointer;
}
.custom-label:hover, .custom-label-selected {
background-color: rgba(211, 211, 211, 0.185);
} }

View File

@ -12,6 +12,7 @@ from application.items import items_API
from application.poe import poe_api from application.poe import poe_api
from application.shoppinglists import shoplist_api from application.shoppinglists import shoplist_api
from application.receipts import receipts_api from application.receipts import receipts_api
from application.meal_planner import meal_planner_api
from flasgger import Swagger from flasgger import Swagger
from outh import oauth from outh import oauth
@ -43,6 +44,7 @@ app.register_blueprint(site_management_api.site_management_api, url_prefix="/sit
app.register_blueprint(receipts_api.receipt_api, url_prefix='/receipts') app.register_blueprint(receipts_api.receipt_api, url_prefix='/receipts')
app.register_blueprint(shoplist_api.shopping_list_api, url_prefix="/shopping-lists") app.register_blueprint(shoplist_api.shopping_list_api, url_prefix="/shopping-lists")
app.register_blueprint(recipes_api.recipes_api, url_prefix='/recipes') app.register_blueprint(recipes_api.recipes_api, url_prefix='/recipes')
app.register_blueprint(meal_planner_api.meal_planner_api, url_prefix='/planner')
js = Bundle('js/uikit.min.js', 'js/uikit-icons.min.js', output='gen/main.js') js = Bundle('js/uikit.min.js', 'js/uikit-icons.min.js', output='gen/main.js')
assets.register('js_all', js) assets.register('js_all', js)