diff --git a/application/__pycache__/database_payloads.cpython-313.pyc b/application/__pycache__/database_payloads.cpython-313.pyc
index 134fe43..0d26499 100644
Binary files a/application/__pycache__/database_payloads.cpython-313.pyc and b/application/__pycache__/database_payloads.cpython-313.pyc differ
diff --git a/application/administration/sql/CREATE/food_info.sql b/application/administration/sql/CREATE/food_info.sql
index 18a0e92..0ec79a9 100644
--- a/application/administration/sql/CREATE/food_info.sql
+++ b/application/administration/sql/CREATE/food_info.sql
@@ -1,5 +1,6 @@
CREATE TABLE IF NOT EXISTS %%site_name%%_food_info (
id SERIAL PRIMARY KEY,
+ food_info_uuid UUID gen_random_uuid(),
food_groups TEXT [],
ingrediants TEXT [],
nutrients JSONB,
diff --git a/application/administration/sql/CREATE/item.sql b/application/administration/sql/CREATE/item.sql
index 5824864..85db086 100644
--- a/application/administration/sql/CREATE/item.sql
+++ b/application/administration/sql/CREATE/item.sql
@@ -1,19 +1,22 @@
CREATE TABLE IF NOT EXISTS %%site_name%%_items(
id SERIAL PRIMARY KEY,
item_uuid UUID DEFAULT gen_random_uuid(),
- barcode VARCHAR(255) NOT NULL,
+ barcode VARCHAR(255),
item_name VARCHAR(255) NOT NULL,
brand INTEGER,
description TEXT,
tags TEXT [],
links JSONB,
item_info_id INTEGER NOT NULL,
+ item_info_uuid UUID NOT NULL,
logistics_info_id INTEGER NOT NULL,
+ logistics_info_uuid UUID NOT NULL,
food_info_id INTEGER,
+ food_info_uuid UUID NOT NULL,
row_type VARCHAR(255) NOT NULL,
item_type VARCHAR(255) NOT NULL,
search_string TEXT NOT NULL,
- UNIQUE(item_uuid, barcode, item_info_id),
+ UNIQUE(item_uuid),
CONSTRAINT fk_item_info
FOREIGN KEY(item_info_id)
REFERENCES %%site_name%%_item_info(id)
diff --git a/application/administration/sql/CREATE/item_info.sql b/application/administration/sql/CREATE/item_info.sql
index 0dd1d93..54a985b 100644
--- a/application/administration/sql/CREATE/item_info.sql
+++ b/application/administration/sql/CREATE/item_info.sql
@@ -1,6 +1,7 @@
CREATE TABLE IF NOt EXISTS %%site_name%%_item_info (
id SERIAL PRIMARY KEY,
- barcode VARCHAR(255) NOT NULL,
+ item_info_uuid UUID gen_random_uuid(),
+ barcode VARCHAR(255),
packaging VARCHAR(255),
uom_quantity FLOAT8,
uom INTEGER,
diff --git a/application/administration/sql/CREATE/logistics_info.sql b/application/administration/sql/CREATE/logistics_info.sql
index dbf9f6e..22a52fa 100644
--- a/application/administration/sql/CREATE/logistics_info.sql
+++ b/application/administration/sql/CREATE/logistics_info.sql
@@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS %%site_name%%_logistics_info(
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_zone INTEGER NOT NULL,
auto_issue_location INTEGER NOT NULL,
diff --git a/application/administration/sql/CREATE/plan_events.sql b/application/administration/sql/CREATE/plan_events.sql
new file mode 100644
index 0000000..53fbe22
--- /dev/null
+++ b/application/administration/sql/CREATE/plan_events.sql
@@ -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)
+)
diff --git a/application/administration/sql/CREATE/recipes.sql b/application/administration/sql/CREATE/recipes.sql
index 5611abb..c0320b0 100644
--- a/application/administration/sql/CREATE/recipes.sql
+++ b/application/administration/sql/CREATE/recipes.sql
@@ -1,9 +1,11 @@
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,
author INTEGER,
description TEXT,
creation_date TIMESTAMP,
instructions TEXT [],
- picture_path TEXT
+ picture_path TEXT,
+ UNIQUE(recipe_uuid)
);
\ No newline at end of file
diff --git a/application/administration/templates/admin_index.html b/application/administration/templates/admin_index.html
index 3c20105..503aea5 100644
--- a/application/administration/templates/admin_index.html
+++ b/application/administration/templates/admin_index.html
@@ -32,6 +32,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/database_payloads.py b/application/database_payloads.py
index 84cd7d8..bea0256 100644
--- a/application/database_payloads.py
+++ b/application/database_payloads.py
@@ -564,6 +564,30 @@ class BrandsPayload:
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
class SiteManager:
site_name: str
diff --git a/application/items/templates/index.html b/application/items/templates/index.html
index 1931405..b422f77 100644
--- a/application/items/templates/index.html
+++ b/application/items/templates/index.html
@@ -43,6 +43,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/items/templates/item_new.html b/application/items/templates/item_new.html
index 0afdc15..500efcd 100644
--- a/application/items/templates/item_new.html
+++ b/application/items/templates/item_new.html
@@ -55,6 +55,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/items/templates/itemlink.html b/application/items/templates/itemlink.html
index f8c5516..c37a42b 100644
--- a/application/items/templates/itemlink.html
+++ b/application/items/templates/itemlink.html
@@ -37,6 +37,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/items/templates/transaction.html b/application/items/templates/transaction.html
index 2075cf1..c8ea573 100644
--- a/application/items/templates/transaction.html
+++ b/application/items/templates/transaction.html
@@ -41,6 +41,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/items/templates/transactions.html b/application/items/templates/transactions.html
index 6bacda0..139839b 100644
--- a/application/items/templates/transactions.html
+++ b/application/items/templates/transactions.html
@@ -39,6 +39,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/meal_planner/__init__.py b/application/meal_planner/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/application/meal_planner/__pycache__/__init__.cpython-313.pyc b/application/meal_planner/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..36f3107
Binary files /dev/null and b/application/meal_planner/__pycache__/__init__.cpython-313.pyc differ
diff --git a/application/meal_planner/__pycache__/meal_planner_api.cpython-313.pyc b/application/meal_planner/__pycache__/meal_planner_api.cpython-313.pyc
new file mode 100644
index 0000000..0edf40e
Binary files /dev/null and b/application/meal_planner/__pycache__/meal_planner_api.cpython-313.pyc differ
diff --git a/application/meal_planner/__pycache__/meal_planner_database.cpython-313.pyc b/application/meal_planner/__pycache__/meal_planner_database.cpython-313.pyc
new file mode 100644
index 0000000..e1e4104
Binary files /dev/null and b/application/meal_planner/__pycache__/meal_planner_database.cpython-313.pyc differ
diff --git a/application/meal_planner/meal_planner_api.py b/application/meal_planner/meal_planner_api.py
new file mode 100644
index 0000000..e311b31
--- /dev/null
+++ b/application/meal_planner/meal_planner_api.py
@@ -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!")
\ No newline at end of file
diff --git a/application/meal_planner/meal_planner_database.py b/application/meal_planner/meal_planner_database.py
new file mode 100644
index 0000000..a7037a0
--- /dev/null
+++ b/application/meal_planner/meal_planner_database.py
@@ -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)
\ No newline at end of file
diff --git a/application/meal_planner/meal_planner_processes.py b/application/meal_planner/meal_planner_processes.py
new file mode 100644
index 0000000..e69de29
diff --git a/application/meal_planner/sql/insertPlanEvent.sql b/application/meal_planner/sql/insertPlanEvent.sql
new file mode 100644
index 0000000..6902fa8
--- /dev/null
+++ b/application/meal_planner/sql/insertPlanEvent.sql
@@ -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 *;
\ No newline at end of file
diff --git a/application/meal_planner/sql/selectPlanEventsByMonth.sql b/application/meal_planner/sql/selectPlanEventsByMonth.sql
new file mode 100644
index 0000000..751f8d1
--- /dev/null
+++ b/application/meal_planner/sql/selectPlanEventsByMonth.sql
@@ -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');
diff --git a/application/meal_planner/static/css/planner.css b/application/meal_planner/static/css/planner.css
new file mode 100644
index 0000000..576ac2b
--- /dev/null
+++ b/application/meal_planner/static/css/planner.css
@@ -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;
+}
diff --git a/application/meal_planner/static/js/mealPlannerHandler.js b/application/meal_planner/static/js/mealPlannerHandler.js
new file mode 100644
index 0000000..dccf52f
--- /dev/null
+++ b/application/meal_planner/static/js/mealPlannerHandler.js
@@ -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 = `| Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday |
`
+
+ 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 += `${event.event_shortname}
`;
+ } else if (event.event_type==="recipe" && !event.has_missing_ingredients){
+ eventsHTML += `${event.event_shortname}
`;
+ } else {
+ eventsHTML += `${event.event_shortname}
`;
+ }
+
+ });
+ }
+
+ table_cell.innerHTML = `${day}
${eventsHTML}
`;
+ 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 = ` | | | | | | | `
+
+ 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 = `
+
+ `;
+ 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 = `
+
+ `;
+ 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 = ``;
+ previousElement.classList.add('uk-disabled');
+ }else {
+ previousElement.innerHTML = ``;
+ }
+ paginationElement.append(previousElement)
+
+ //first
+ let firstElement = document.createElement('li')
+ if(eventModal_page<=1){
+ firstElement.innerHTML = `1`;
+ firstElement.classList.add('uk-disabled');
+ }else {
+ firstElement.innerHTML = `1`;
+ }
+ paginationElement.append(firstElement)
+
+ // ...
+ if(eventModal_page-2>1){
+ let firstDotElement = document.createElement('li')
+ firstDotElement.classList.add('uk-disabled')
+ firstDotElement.innerHTML = `…`;
+ paginationElement.append(firstDotElement)
+ }
+ // last
+ if(eventModal_page-2>0){
+ let lastElement = document.createElement('li')
+ lastElement.innerHTML = `${eventModal_page-1}`
+ paginationElement.append(lastElement)
+ }
+ // current
+ if(eventModal_page!=1 && eventModal_page != eventModal_end){
+ let currentElement = document.createElement('li')
+ currentElement.innerHTML = `- ${eventModal_page}
`
+ paginationElement.append(currentElement)
+ }
+ // next
+ if(eventModal_page+2${eventModal_page+1}`
+ paginationElement.append(nextElement)
+ }
+ // ...
+ if(eventModal_page+2<=eventModal_end){
+ let secondDotElement = document.createElement('li')
+ secondDotElement.classList.add('uk-disabled')
+ secondDotElement.innerHTML = `…`;
+ paginationElement.append(secondDotElement)
+ }
+ //end
+ let endElement = document.createElement('li')
+ if(eventModal_page>=eventModal_end){
+ endElement.innerHTML = `${eventModal_end}`;
+ endElement.classList.add('uk-disabled');
+ }else {
+ endElement.innerHTML = `${eventModal_end}`;
+ }
+ paginationElement.append(endElement)
+ //next button
+ let nextElement = document.createElement('li')
+ if(eventModal_page>=eventModal_end){
+ nextElement.innerHTML = ``;
+ nextElement.classList.add('uk-disabled');
+ }else {
+ nextElement.innerHTML = ``;
+ console.log(nextElement.innerHTML)
+ }
+ paginationElement.append(nextElement)
+}
\ No newline at end of file
diff --git a/application/meal_planner/templates/meal_planner.html b/application/meal_planner/templates/meal_planner.html
new file mode 100644
index 0000000..a63d943
--- /dev/null
+++ b/application/meal_planner/templates/meal_planner.html
@@ -0,0 +1,315 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if session['user']['flags']['darkmode'] %}
+
+ {% endif %}
+
+
+
+
+ {% if session['user']['flags']['darkmode'] %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select a Recipe from the system...
+
+
+
+ | Name |
+ Operations |
+
+
+
+
+
+
+
+
+
+
+
+
Adding an event requires a type of event and the following information.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% assets "js_all" %}
+
+ {% endassets %}
+
+
\ No newline at end of file
diff --git a/application/poe/templates/receipts.html b/application/poe/templates/receipts.html
index 411777c..27307c6 100644
--- a/application/poe/templates/receipts.html
+++ b/application/poe/templates/receipts.html
@@ -57,6 +57,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/poe/templates/scanner.html b/application/poe/templates/scanner.html
index 89234fc..d40e67e 100644
--- a/application/poe/templates/scanner.html
+++ b/application/poe/templates/scanner.html
@@ -40,6 +40,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/receipts/templates/receipt.html b/application/receipts/templates/receipt.html
index 9ab588c..2607854 100644
--- a/application/receipts/templates/receipt.html
+++ b/application/receipts/templates/receipt.html
@@ -40,6 +40,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/receipts/templates/receipts_index.html b/application/receipts/templates/receipts_index.html
index 635fd74..6f4cc69 100644
--- a/application/receipts/templates/receipts_index.html
+++ b/application/receipts/templates/receipts_index.html
@@ -38,6 +38,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/recipes/templates/recipe_edit.html b/application/recipes/templates/recipe_edit.html
index 27d19a7..ae277d2 100644
--- a/application/recipes/templates/recipe_edit.html
+++ b/application/recipes/templates/recipe_edit.html
@@ -38,6 +38,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/recipes/templates/recipe_view.html b/application/recipes/templates/recipe_view.html
index 50048f4..30587aa 100644
--- a/application/recipes/templates/recipe_view.html
+++ b/application/recipes/templates/recipe_view.html
@@ -45,6 +45,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/recipes/templates/recipes_index.html b/application/recipes/templates/recipes_index.html
index 841695c..fa86500 100644
--- a/application/recipes/templates/recipes_index.html
+++ b/application/recipes/templates/recipes_index.html
@@ -38,6 +38,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/shoppinglists/templates/edit.html b/application/shoppinglists/templates/edit.html
index ce0cede..06ae517 100644
--- a/application/shoppinglists/templates/edit.html
+++ b/application/shoppinglists/templates/edit.html
@@ -39,6 +39,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/shoppinglists/templates/lists.html b/application/shoppinglists/templates/lists.html
index 0f66219..bfa2b0f 100644
--- a/application/shoppinglists/templates/lists.html
+++ b/application/shoppinglists/templates/lists.html
@@ -39,6 +39,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/application/shoppinglists/templates/view.html b/application/shoppinglists/templates/view.html
index b610437..00422ea 100644
--- a/application/shoppinglists/templates/view.html
+++ b/application/shoppinglists/templates/view.html
@@ -39,6 +39,7 @@
Apps
+ - Planner
- Recipes
- Shopping Lists
diff --git a/logs/database.log b/logs/database.log
index 5f2d138..456f65e 100644
--- a/logs/database.log
+++ b/logs/database.log
@@ -145,4 +145,95 @@
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 = ... ^',
payload=(2016, 6),
- sql='SELECT conversions.conv_factor FROM test_conversions conversions WHERE part_id = %s AND uom_id = %s;')
\ No newline at end of file
+ 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');')
\ No newline at end of file
diff --git a/plu_items.csv b/plu_items.csv
new file mode 100644
index 0000000..cdf31dc
--- /dev/null
+++ b/plu_items.csv
@@ -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)
\ No newline at end of file
diff --git a/static/css/dark-mode.css b/static/css/dark-mode.css
index 380a7a8..4ba64e5 100644
--- a/static/css/dark-mode.css
+++ b/static/css/dark-mode.css
@@ -2,10 +2,11 @@
--background: #121212;
--background-text: #ffffff;
- --surface: #121212;
+ --surface: #181818;
--surface-text: #ffffff;
--surface-two: #252525;
+ --surface-three: #383838;
--error: #CF6679;
--error-text: #000000;
@@ -251,4 +252,128 @@ select, option {
.uk-button.uk-button-danger {
background-color: var(--error);
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);
}
\ No newline at end of file
diff --git a/webserver.py b/webserver.py
index 4663bbc..dd7d6b1 100644
--- a/webserver.py
+++ b/webserver.py
@@ -12,6 +12,7 @@ from application.items import items_API
from application.poe import poe_api
from application.shoppinglists import shoplist_api
from application.receipts import receipts_api
+from application.meal_planner import meal_planner_api
from flasgger import Swagger
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(shoplist_api.shopping_list_api, url_prefix="/shopping-lists")
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')
assets.register('js_all', js)